create-landing-app 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +21 -0
- package/dist/install.js +18 -0
- package/dist/prompts.js +62 -0
- package/dist/scaffold.js +159 -0
- package/dist/utils/__tests__/merge-json.test.js +144 -0
- package/dist/utils/__tests__/replace-tokens.test.js +212 -0
- package/dist/utils/copy-dir.js +22 -0
- package/dist/utils/merge-json.js +19 -0
- package/dist/utils/replace-tokens.js +8 -0
- package/package.json +48 -0
- package/templates/nextjs/base/.env.example +8 -0
- package/templates/nextjs/base/.github/workflows/ci.yml +40 -0
- package/templates/nextjs/base/.husky/commit-msg +7 -0
- package/templates/nextjs/base/.husky/pre-commit +3 -0
- package/templates/nextjs/base/.husky/pre-push +46 -0
- package/templates/nextjs/base/.lighthouserc.json +28 -0
- package/templates/nextjs/base/.prettierignore +11 -0
- package/templates/nextjs/base/.prettierrc.json +10 -0
- package/templates/nextjs/base/Dockerfile +42 -0
- package/templates/nextjs/base/app/globals.css +82 -0
- package/templates/nextjs/base/app/layout.tsx +32 -0
- package/templates/nextjs/base/app/not-found.tsx +13 -0
- package/templates/nextjs/base/app/page.tsx +15 -0
- package/templates/nextjs/base/app/robots.ts +9 -0
- package/templates/nextjs/base/commitlint.config.mjs +32 -0
- package/templates/nextjs/base/components/navs/navbar-mobile.tsx +39 -0
- package/templates/nextjs/base/components/navs/navbar.tsx +39 -0
- package/templates/nextjs/base/components/providers.tsx +12 -0
- package/templates/nextjs/base/components/sections/features-section.tsx +78 -0
- package/templates/nextjs/base/components/sections/footer-section.tsx +98 -0
- package/templates/nextjs/base/components/sections/hero-section.tsx +74 -0
- package/templates/nextjs/base/components/ui/accordion.tsx +47 -0
- package/templates/nextjs/base/components/ui/button.tsx +44 -0
- package/templates/nextjs/base/components/ui/dialog.tsx +61 -0
- package/templates/nextjs/base/components/ui/dropdown-menu.tsx +55 -0
- package/templates/nextjs/base/components/ui/sonner.tsx +6 -0
- package/templates/nextjs/base/components.json +19 -0
- package/templates/nextjs/base/constants/common.ts +15 -0
- package/templates/nextjs/base/eslint.config.mjs +25 -0
- package/templates/nextjs/base/lib/metadata.ts +36 -0
- package/templates/nextjs/base/lib/utils.ts +7 -0
- package/templates/nextjs/base/next.config.ts +33 -0
- package/templates/nextjs/base/package.json +61 -0
- package/templates/nextjs/base/postcss.config.mjs +7 -0
- package/templates/nextjs/base/scripts/build-and-scan.sh +127 -0
- package/templates/nextjs/base/scripts/lighthouse-check.sh +86 -0
- package/templates/nextjs/base/styles/theme.css +63 -0
- package/templates/nextjs/base/tsconfig.json +21 -0
- package/templates/nextjs/base/types/index.ts +16 -0
- package/templates/nextjs/optional/docker/files/.dockerignore +6 -0
- package/templates/nextjs/optional/docker/files/Dockerfile +36 -0
- package/templates/nextjs/optional/docker/files/docker-compose.yml +9 -0
- package/templates/nextjs/optional/i18n-dict/files/app/[lang]/layout.tsx +19 -0
- package/templates/nextjs/optional/i18n-dict/files/app/[lang]/page.tsx +15 -0
- package/templates/nextjs/optional/i18n-dict/files/components/navs/language-switcher.tsx +39 -0
- package/templates/nextjs/optional/i18n-dict/files/components/navs/navbar-mobile.tsx +41 -0
- package/templates/nextjs/optional/i18n-dict/files/components/navs/navbar.tsx +41 -0
- package/templates/nextjs/optional/i18n-dict/files/components/providers.tsx +16 -0
- package/templates/nextjs/optional/i18n-dict/files/components/sections/features-section.tsx +80 -0
- package/templates/nextjs/optional/i18n-dict/files/components/sections/footer-section.tsx +98 -0
- package/templates/nextjs/optional/i18n-dict/files/dictionaries/en.json +21 -0
- package/templates/nextjs/optional/i18n-dict/files/dictionaries/vi.json +21 -0
- package/templates/nextjs/optional/i18n-dict/files/get-dictionary.ts +10 -0
- package/templates/nextjs/optional/i18n-dict/files/i18n-config.ts +6 -0
- package/templates/nextjs/optional/i18n-dict/files/lib/dict-context.tsx +23 -0
- package/templates/nextjs/optional/i18n-dict/files/middleware.ts +31 -0
- package/templates/nextjs/optional/i18n-dict/pkg.json +9 -0
- package/templates/nextjs/optional/sections/about/files/components/sections/about-section.tsx +36 -0
- package/templates/nextjs/optional/sections/about/inject/app__[lang]__page.tsx +5 -0
- package/templates/nextjs/optional/sections/about/inject/app__page.tsx +5 -0
- package/templates/nextjs/optional/sections/about/inject/constants__common.ts +2 -0
- package/templates/nextjs/optional/sections/blog/files/components/sections/blog-section.tsx +191 -0
- package/templates/nextjs/optional/sections/blog/inject/app__[lang]__page.tsx +5 -0
- package/templates/nextjs/optional/sections/blog/inject/app__page.tsx +5 -0
- package/templates/nextjs/optional/sections/blog/inject/constants__common.ts +2 -0
- package/templates/nextjs/optional/sections/contact/files/components/sections/contact-section.tsx +79 -0
- package/templates/nextjs/optional/sections/contact/inject/app__[lang]__page.tsx +5 -0
- package/templates/nextjs/optional/sections/contact/inject/app__page.tsx +5 -0
- package/templates/nextjs/optional/sections/contact/inject/constants__common.ts +2 -0
- package/templates/nextjs/optional/tanstack-query/files/lib/custom-fetch.ts +9 -0
- package/templates/nextjs/optional/tanstack-query/files/lib/query-client.ts +21 -0
- package/templates/nextjs/optional/tanstack-query/inject/components__providers.tsx +9 -0
- package/templates/nextjs/optional/tanstack-query/pkg.json +5 -0
- package/templates/nextjs/optional/zustand/files/store/ui-store.ts +16 -0
- package/templates/nextjs/optional/zustand/inject/components__providers.tsx +3 -0
- package/templates/nextjs/optional/zustand/pkg.json +5 -0
- package/templates/nextjs/themes/dark.css +36 -0
- package/templates/nextjs/themes/forest.css +58 -0
- package/templates/nextjs/themes/ocean.css +58 -0
- package/templates/nextjs/themes/pila.css +75 -0
- package/templates/nextjs/themes/purple.css +58 -0
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import Link from "next/link";
|
|
2
|
+
import { SITE_NAME, NAV_LINKS, SOCIAL_LINKS } from "@/constants/common";
|
|
3
|
+
import { Github, Twitter, Linkedin } from "lucide-react";
|
|
4
|
+
|
|
5
|
+
// Server component — static links, no interactivity needed
|
|
6
|
+
export default function FooterSection() {
|
|
7
|
+
return (
|
|
8
|
+
<footer className="bg-footer text-white">
|
|
9
|
+
<div className="content-container py-16">
|
|
10
|
+
<div className="mb-12 grid gap-12 md:grid-cols-4">
|
|
11
|
+
{/* Brand */}
|
|
12
|
+
<div className="space-y-4 md:col-span-2">
|
|
13
|
+
<p className="text-2xl font-bold">{SITE_NAME}</p>
|
|
14
|
+
<p className="max-w-xs text-sm leading-relaxed text-white/70">
|
|
15
|
+
A modern landing page starter kit. Built for developers who ship fast.
|
|
16
|
+
</p>
|
|
17
|
+
{/* Social links */}
|
|
18
|
+
<div className="flex items-center gap-4">
|
|
19
|
+
{SOCIAL_LINKS.github && (
|
|
20
|
+
<Link
|
|
21
|
+
href={SOCIAL_LINKS.github}
|
|
22
|
+
className="text-white/70 transition-colors hover:text-white"
|
|
23
|
+
>
|
|
24
|
+
<Github className="h-5 w-5" />
|
|
25
|
+
</Link>
|
|
26
|
+
)}
|
|
27
|
+
{SOCIAL_LINKS.twitter && (
|
|
28
|
+
<Link
|
|
29
|
+
href={SOCIAL_LINKS.twitter}
|
|
30
|
+
className="text-white/70 transition-colors hover:text-white"
|
|
31
|
+
>
|
|
32
|
+
<Twitter className="h-5 w-5" />
|
|
33
|
+
</Link>
|
|
34
|
+
)}
|
|
35
|
+
{SOCIAL_LINKS.linkedin && (
|
|
36
|
+
<Link
|
|
37
|
+
href={SOCIAL_LINKS.linkedin}
|
|
38
|
+
className="text-white/70 transition-colors hover:text-white"
|
|
39
|
+
>
|
|
40
|
+
<Linkedin className="h-5 w-5" />
|
|
41
|
+
</Link>
|
|
42
|
+
)}
|
|
43
|
+
</div>
|
|
44
|
+
</div>
|
|
45
|
+
|
|
46
|
+
{/* Nav links */}
|
|
47
|
+
<div className="space-y-4">
|
|
48
|
+
<p className="text-sm font-semibold uppercase tracking-wider text-white/50">
|
|
49
|
+
Navigation
|
|
50
|
+
</p>
|
|
51
|
+
<ul className="space-y-3">
|
|
52
|
+
{NAV_LINKS.map((link) => (
|
|
53
|
+
<li key={link.href}>
|
|
54
|
+
<Link
|
|
55
|
+
href={link.href}
|
|
56
|
+
className="text-sm text-white/70 transition-colors hover:text-white"
|
|
57
|
+
>
|
|
58
|
+
{link.label}
|
|
59
|
+
</Link>
|
|
60
|
+
</li>
|
|
61
|
+
))}
|
|
62
|
+
</ul>
|
|
63
|
+
</div>
|
|
64
|
+
|
|
65
|
+
{/* Legal */}
|
|
66
|
+
<div className="space-y-4">
|
|
67
|
+
<p className="text-sm font-semibold uppercase tracking-wider text-white/50">Legal</p>
|
|
68
|
+
<ul className="space-y-3">
|
|
69
|
+
<li>
|
|
70
|
+
<Link
|
|
71
|
+
href="/privacy"
|
|
72
|
+
className="text-sm text-white/70 transition-colors hover:text-white"
|
|
73
|
+
>
|
|
74
|
+
Privacy Policy
|
|
75
|
+
</Link>
|
|
76
|
+
</li>
|
|
77
|
+
<li>
|
|
78
|
+
<Link
|
|
79
|
+
href="/terms"
|
|
80
|
+
className="text-sm text-white/70 transition-colors hover:text-white"
|
|
81
|
+
>
|
|
82
|
+
Terms of Service
|
|
83
|
+
</Link>
|
|
84
|
+
</li>
|
|
85
|
+
</ul>
|
|
86
|
+
</div>
|
|
87
|
+
</div>
|
|
88
|
+
|
|
89
|
+
{/* Bottom bar */}
|
|
90
|
+
<div className="border-t border-white/10 pt-8">
|
|
91
|
+
<p className="text-center text-sm text-white/50">
|
|
92
|
+
© {new Date().getFullYear()} {SITE_NAME}. All rights reserved.
|
|
93
|
+
</p>
|
|
94
|
+
</div>
|
|
95
|
+
</div>
|
|
96
|
+
</footer>
|
|
97
|
+
);
|
|
98
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { motion } from "motion/react";
|
|
3
|
+
import { Button } from "@/components/ui/button";
|
|
4
|
+
import Navbar from "@/components/navs/navbar";
|
|
5
|
+
|
|
6
|
+
// Hero section — the first thing users see
|
|
7
|
+
// Replace headline, subtext, and CTA content with your actual copy
|
|
8
|
+
export default function HeroSection() {
|
|
9
|
+
return (
|
|
10
|
+
<>
|
|
11
|
+
<Navbar />
|
|
12
|
+
<section
|
|
13
|
+
id="hero"
|
|
14
|
+
className="relative flex min-h-[calc(100vh-4rem)] items-center overflow-hidden py-24"
|
|
15
|
+
>
|
|
16
|
+
{/* Background decoration */}
|
|
17
|
+
<div
|
|
18
|
+
className="absolute inset-0 -z-10"
|
|
19
|
+
style={{
|
|
20
|
+
background:
|
|
21
|
+
"radial-gradient(ellipse 80% 60% at 50% 0%, var(--brand-surface) 0%, transparent 70%)",
|
|
22
|
+
}}
|
|
23
|
+
/>
|
|
24
|
+
|
|
25
|
+
<div className="content-container">
|
|
26
|
+
<motion.div
|
|
27
|
+
initial={{ opacity: 0, y: 40 }}
|
|
28
|
+
animate={{ opacity: 1, y: 0 }}
|
|
29
|
+
transition={{ duration: 0.7, ease: "easeOut" }}
|
|
30
|
+
className="mx-auto max-w-3xl space-y-8 text-center"
|
|
31
|
+
>
|
|
32
|
+
{/* Badge */}
|
|
33
|
+
<motion.div
|
|
34
|
+
initial={{ opacity: 0, scale: 0.9 }}
|
|
35
|
+
animate={{ opacity: 1, scale: 1 }}
|
|
36
|
+
transition={{ delay: 0.1 }}
|
|
37
|
+
className="inline-flex items-center gap-2 rounded-full border border-border bg-background px-4 py-1.5 text-sm font-medium"
|
|
38
|
+
>
|
|
39
|
+
<span className="h-2 w-2 rounded-full bg-primary" />
|
|
40
|
+
New — v1.0 just shipped
|
|
41
|
+
</motion.div>
|
|
42
|
+
|
|
43
|
+
{/* Headline */}
|
|
44
|
+
<h1 className="text-5xl font-bold leading-tight tracking-tight md:text-7xl">
|
|
45
|
+
Build faster,{" "}
|
|
46
|
+
<span className="text-gradient-brand">ship better</span>
|
|
47
|
+
</h1>
|
|
48
|
+
|
|
49
|
+
{/* Subtext */}
|
|
50
|
+
<p className="mx-auto max-w-2xl text-xl leading-relaxed text-muted-foreground">
|
|
51
|
+
The modern landing page starter for your next project. Production-ready, beautifully
|
|
52
|
+
designed, fully customizable.
|
|
53
|
+
</p>
|
|
54
|
+
|
|
55
|
+
{/* CTAs */}
|
|
56
|
+
<div className="flex flex-col justify-center gap-4 sm:flex-row">
|
|
57
|
+
<Button size="lg" className="btn-primary px-8 text-white">
|
|
58
|
+
Get started free
|
|
59
|
+
</Button>
|
|
60
|
+
<Button size="lg" variant="outline" className="btn-outline px-8">
|
|
61
|
+
View documentation
|
|
62
|
+
</Button>
|
|
63
|
+
</div>
|
|
64
|
+
|
|
65
|
+
{/* Social proof */}
|
|
66
|
+
<p className="text-sm text-muted-foreground">
|
|
67
|
+
Trusted by <span className="font-semibold text-foreground">500+</span> developers
|
|
68
|
+
</p>
|
|
69
|
+
</motion.div>
|
|
70
|
+
</div>
|
|
71
|
+
</section>
|
|
72
|
+
</>
|
|
73
|
+
);
|
|
74
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import * as AccordionPrimitive from "@radix-ui/react-accordion";
|
|
3
|
+
import { ChevronDown } from "lucide-react";
|
|
4
|
+
import { cn } from "@/lib/utils";
|
|
5
|
+
|
|
6
|
+
const Accordion = AccordionPrimitive.Root;
|
|
7
|
+
|
|
8
|
+
const AccordionItem = ({
|
|
9
|
+
className,
|
|
10
|
+
...props
|
|
11
|
+
}: React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Item>) => (
|
|
12
|
+
<AccordionPrimitive.Item className={cn("border-b", className)} {...props} />
|
|
13
|
+
);
|
|
14
|
+
|
|
15
|
+
const AccordionTrigger = ({
|
|
16
|
+
className,
|
|
17
|
+
children,
|
|
18
|
+
...props
|
|
19
|
+
}: React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Trigger>) => (
|
|
20
|
+
<AccordionPrimitive.Header className="flex">
|
|
21
|
+
<AccordionPrimitive.Trigger
|
|
22
|
+
className={cn(
|
|
23
|
+
"flex flex-1 items-center justify-between py-4 font-medium hover:underline [&[data-state=open]>svg]:rotate-180",
|
|
24
|
+
className
|
|
25
|
+
)}
|
|
26
|
+
{...props}
|
|
27
|
+
>
|
|
28
|
+
{children}
|
|
29
|
+
<ChevronDown className="h-4 w-4 shrink-0 transition-transform duration-200" />
|
|
30
|
+
</AccordionPrimitive.Trigger>
|
|
31
|
+
</AccordionPrimitive.Header>
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
const AccordionContent = ({
|
|
35
|
+
className,
|
|
36
|
+
children,
|
|
37
|
+
...props
|
|
38
|
+
}: React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Content>) => (
|
|
39
|
+
<AccordionPrimitive.Content
|
|
40
|
+
className="overflow-hidden text-sm transition-all data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down"
|
|
41
|
+
{...props}
|
|
42
|
+
>
|
|
43
|
+
<div className={cn("pb-4 pt-0", className)}>{children}</div>
|
|
44
|
+
</AccordionPrimitive.Content>
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
export { Accordion, AccordionItem, AccordionTrigger, AccordionContent };
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { Slot } from "@radix-ui/react-slot";
|
|
3
|
+
import { cva, type VariantProps } from "class-variance-authority";
|
|
4
|
+
import { cn } from "@/lib/utils";
|
|
5
|
+
|
|
6
|
+
const buttonVariants = cva(
|
|
7
|
+
"inline-flex items-center justify-center gap-2 rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50",
|
|
8
|
+
{
|
|
9
|
+
variants: {
|
|
10
|
+
variant: {
|
|
11
|
+
default: "bg-primary text-primary-foreground hover:bg-primary/90",
|
|
12
|
+
outline:
|
|
13
|
+
"border border-input bg-background hover:bg-accent hover:text-accent-foreground",
|
|
14
|
+
ghost: "hover:bg-accent hover:text-accent-foreground",
|
|
15
|
+
link: "text-primary underline-offset-4 hover:underline",
|
|
16
|
+
},
|
|
17
|
+
size: {
|
|
18
|
+
default: "h-10 px-4 py-2",
|
|
19
|
+
sm: "h-8 px-3 text-xs",
|
|
20
|
+
lg: "h-12 px-8",
|
|
21
|
+
icon: "h-10 w-10",
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
defaultVariants: { variant: "default", size: "default" },
|
|
25
|
+
}
|
|
26
|
+
);
|
|
27
|
+
|
|
28
|
+
export interface ButtonProps
|
|
29
|
+
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
|
|
30
|
+
VariantProps<typeof buttonVariants> {
|
|
31
|
+
asChild?: boolean;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
|
35
|
+
({ className, variant, size, asChild = false, ...props }, ref) => {
|
|
36
|
+
const Comp = asChild ? Slot : "button";
|
|
37
|
+
return (
|
|
38
|
+
<Comp className={cn(buttonVariants({ variant, size, className }))} ref={ref} {...props} />
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
);
|
|
42
|
+
Button.displayName = "Button";
|
|
43
|
+
|
|
44
|
+
export { buttonVariants };
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import * as DialogPrimitive from "@radix-ui/react-dialog";
|
|
3
|
+
import { X } from "lucide-react";
|
|
4
|
+
import { cn } from "@/lib/utils";
|
|
5
|
+
|
|
6
|
+
const Dialog = DialogPrimitive.Root;
|
|
7
|
+
const DialogTrigger = DialogPrimitive.Trigger;
|
|
8
|
+
const DialogPortal = DialogPrimitive.Portal;
|
|
9
|
+
const DialogClose = DialogPrimitive.Close;
|
|
10
|
+
|
|
11
|
+
const DialogOverlay = ({
|
|
12
|
+
className,
|
|
13
|
+
...props
|
|
14
|
+
}: React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>) => (
|
|
15
|
+
<DialogPrimitive.Overlay
|
|
16
|
+
className={cn(
|
|
17
|
+
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
|
|
18
|
+
className
|
|
19
|
+
)}
|
|
20
|
+
{...props}
|
|
21
|
+
/>
|
|
22
|
+
);
|
|
23
|
+
|
|
24
|
+
const DialogContent = ({
|
|
25
|
+
className,
|
|
26
|
+
children,
|
|
27
|
+
...props
|
|
28
|
+
}: React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>) => (
|
|
29
|
+
<DialogPortal>
|
|
30
|
+
<DialogOverlay />
|
|
31
|
+
<DialogPrimitive.Content
|
|
32
|
+
className={cn(
|
|
33
|
+
"fixed left-1/2 top-1/2 z-50 w-full max-w-lg -translate-x-1/2 -translate-y-1/2 bg-background p-6 shadow-lg rounded-lg border",
|
|
34
|
+
className
|
|
35
|
+
)}
|
|
36
|
+
{...props}
|
|
37
|
+
>
|
|
38
|
+
{children}
|
|
39
|
+
<DialogClose className="absolute right-4 top-4 opacity-70 hover:opacity-100">
|
|
40
|
+
<X className="h-4 w-4" />
|
|
41
|
+
<span className="sr-only">Close</span>
|
|
42
|
+
</DialogClose>
|
|
43
|
+
</DialogPrimitive.Content>
|
|
44
|
+
</DialogPortal>
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
const DialogHeader = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
|
|
48
|
+
<div className={cn("flex flex-col space-y-1.5", className)} {...props} />
|
|
49
|
+
);
|
|
50
|
+
const DialogTitle = DialogPrimitive.Title;
|
|
51
|
+
const DialogDescription = DialogPrimitive.Description;
|
|
52
|
+
|
|
53
|
+
export {
|
|
54
|
+
Dialog,
|
|
55
|
+
DialogTrigger,
|
|
56
|
+
DialogContent,
|
|
57
|
+
DialogHeader,
|
|
58
|
+
DialogTitle,
|
|
59
|
+
DialogDescription,
|
|
60
|
+
DialogClose,
|
|
61
|
+
};
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu";
|
|
3
|
+
import { cn } from "@/lib/utils";
|
|
4
|
+
|
|
5
|
+
const DropdownMenu = DropdownMenuPrimitive.Root;
|
|
6
|
+
const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger;
|
|
7
|
+
const DropdownMenuPortal = DropdownMenuPrimitive.Portal;
|
|
8
|
+
|
|
9
|
+
const DropdownMenuContent = ({
|
|
10
|
+
className,
|
|
11
|
+
sideOffset = 4,
|
|
12
|
+
...props
|
|
13
|
+
}: React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content>) => (
|
|
14
|
+
<DropdownMenuPortal>
|
|
15
|
+
<DropdownMenuPrimitive.Content
|
|
16
|
+
sideOffset={sideOffset}
|
|
17
|
+
className={cn(
|
|
18
|
+
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
|
|
19
|
+
className
|
|
20
|
+
)}
|
|
21
|
+
{...props}
|
|
22
|
+
/>
|
|
23
|
+
</DropdownMenuPortal>
|
|
24
|
+
);
|
|
25
|
+
|
|
26
|
+
const DropdownMenuItem = ({
|
|
27
|
+
className,
|
|
28
|
+
...props
|
|
29
|
+
}: React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item>) => (
|
|
30
|
+
<DropdownMenuPrimitive.Item
|
|
31
|
+
className={cn(
|
|
32
|
+
"relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
|
33
|
+
className
|
|
34
|
+
)}
|
|
35
|
+
{...props}
|
|
36
|
+
/>
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
const DropdownMenuSeparator = ({
|
|
40
|
+
className,
|
|
41
|
+
...props
|
|
42
|
+
}: React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Separator>) => (
|
|
43
|
+
<DropdownMenuPrimitive.Separator
|
|
44
|
+
className={cn("-mx-1 my-1 h-px bg-muted", className)}
|
|
45
|
+
{...props}
|
|
46
|
+
/>
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
export {
|
|
50
|
+
DropdownMenu,
|
|
51
|
+
DropdownMenuTrigger,
|
|
52
|
+
DropdownMenuContent,
|
|
53
|
+
DropdownMenuItem,
|
|
54
|
+
DropdownMenuSeparator,
|
|
55
|
+
};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://ui.shadcn.com/schema.json",
|
|
3
|
+
"style": "new-york",
|
|
4
|
+
"rsc": true,
|
|
5
|
+
"tsx": true,
|
|
6
|
+
"tailwind": {
|
|
7
|
+
"config": "",
|
|
8
|
+
"css": "app/globals.css",
|
|
9
|
+
"baseColor": "neutral",
|
|
10
|
+
"cssVariables": true
|
|
11
|
+
},
|
|
12
|
+
"aliases": {
|
|
13
|
+
"components": "@/components",
|
|
14
|
+
"utils": "@/lib/utils",
|
|
15
|
+
"ui": "@/components/ui",
|
|
16
|
+
"lib": "@/lib",
|
|
17
|
+
"hooks": "@/hooks"
|
|
18
|
+
}
|
|
19
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export const SITE_NAME = "__PROJECT_NAME__";
|
|
2
|
+
export const BASE_URL = process.env.NEXT_PUBLIC_SITE_URL ?? "https://yourdomain.com";
|
|
3
|
+
|
|
4
|
+
export const NAV_LINKS = [
|
|
5
|
+
{ label: "Features", href: "#features" },
|
|
6
|
+
// __NAV_LINK_ABOUT__
|
|
7
|
+
// __NAV_LINK_BLOG__
|
|
8
|
+
// __NAV_LINK_CONTACT__
|
|
9
|
+
] as const;
|
|
10
|
+
|
|
11
|
+
export const SOCIAL_LINKS = {
|
|
12
|
+
twitter: "#",
|
|
13
|
+
github: "#",
|
|
14
|
+
linkedin: "#",
|
|
15
|
+
} as const;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { dirname } from "path";
|
|
2
|
+
import { fileURLToPath } from "url";
|
|
3
|
+
import { FlatCompat } from "@eslint/eslintrc";
|
|
4
|
+
|
|
5
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
6
|
+
const __dirname = dirname(__filename);
|
|
7
|
+
|
|
8
|
+
const compat = new FlatCompat({ baseDirectory: __dirname });
|
|
9
|
+
|
|
10
|
+
const eslintConfig = [
|
|
11
|
+
{ ignores: [".next/**", "node_modules/**"] },
|
|
12
|
+
...compat.extends("next/core-web-vitals", "next/typescript"),
|
|
13
|
+
{
|
|
14
|
+
rules: {
|
|
15
|
+
// Enforce no unused variables — common source of tech debt
|
|
16
|
+
"@typescript-eslint/no-unused-vars": ["error", { argsIgnorePattern: "^_" }],
|
|
17
|
+
// No explicit any — forces proper typing
|
|
18
|
+
"@typescript-eslint/no-explicit-any": "warn",
|
|
19
|
+
// React hooks rules
|
|
20
|
+
"react-hooks/exhaustive-deps": "warn",
|
|
21
|
+
},
|
|
22
|
+
},
|
|
23
|
+
];
|
|
24
|
+
|
|
25
|
+
export default eslintConfig;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { Metadata } from "next";
|
|
2
|
+
|
|
3
|
+
const BASE_URL = process.env.NEXT_PUBLIC_SITE_URL ?? "https://yourdomain.com";
|
|
4
|
+
|
|
5
|
+
interface MetadataOptions {
|
|
6
|
+
title: string;
|
|
7
|
+
description: string;
|
|
8
|
+
image?: string;
|
|
9
|
+
path?: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
// Factory for consistent metadata across all pages
|
|
13
|
+
export function createMetadata({ title, description, image, path = "" }: MetadataOptions): Metadata {
|
|
14
|
+
const url = `${BASE_URL}${path}`;
|
|
15
|
+
const ogImage = image ?? `${BASE_URL}/og-image.png`;
|
|
16
|
+
|
|
17
|
+
return {
|
|
18
|
+
title,
|
|
19
|
+
description,
|
|
20
|
+
metadataBase: new URL(BASE_URL),
|
|
21
|
+
openGraph: {
|
|
22
|
+
title,
|
|
23
|
+
description,
|
|
24
|
+
url,
|
|
25
|
+
images: [{ url: ogImage, width: 1200, height: 630 }],
|
|
26
|
+
type: "website",
|
|
27
|
+
},
|
|
28
|
+
twitter: {
|
|
29
|
+
card: "summary_large_image",
|
|
30
|
+
title,
|
|
31
|
+
description,
|
|
32
|
+
images: [ogImage],
|
|
33
|
+
},
|
|
34
|
+
alternates: { canonical: url },
|
|
35
|
+
};
|
|
36
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { NextConfig } from "next";
|
|
2
|
+
|
|
3
|
+
const nextConfig: NextConfig = {
|
|
4
|
+
experimental: {
|
|
5
|
+
optimizePackageImports: ["lucide-react"],
|
|
6
|
+
},
|
|
7
|
+
compiler: {
|
|
8
|
+
// Remove console.log in production
|
|
9
|
+
removeConsole: process.env.NODE_ENV === "production",
|
|
10
|
+
},
|
|
11
|
+
output: "standalone", // Required for Docker VPS deploy
|
|
12
|
+
images: {
|
|
13
|
+
minimumCacheTTL: 2592000, // 30-day image cache
|
|
14
|
+
remotePatterns: [
|
|
15
|
+
// Add your image domains here
|
|
16
|
+
// { protocol: "https", hostname: "your-s3-domain.com", pathname: "/**" }
|
|
17
|
+
],
|
|
18
|
+
},
|
|
19
|
+
headers: async () => [
|
|
20
|
+
{
|
|
21
|
+
// Immutable cache for hashed Next.js static bundles
|
|
22
|
+
source: "/_next/static/:path*",
|
|
23
|
+
headers: [{ key: "Cache-Control", value: "public, max-age=2592000, immutable" }],
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
// Long-lived cache for public assets
|
|
27
|
+
source: "/:path*.(webp|png|jpg|jpeg|svg|ico|woff2|woff|ttf)",
|
|
28
|
+
headers: [{ key: "Cache-Control", value: "public, max-age=2592000" }],
|
|
29
|
+
},
|
|
30
|
+
],
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export default nextConfig;
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "__PROJECT_NAME__",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"scripts": {
|
|
6
|
+
"dev": "next dev --turbopack",
|
|
7
|
+
"build": "next build",
|
|
8
|
+
"start": "next start",
|
|
9
|
+
"lint": "next lint",
|
|
10
|
+
"lint:fix": "next lint --fix",
|
|
11
|
+
"format": "prettier --write .",
|
|
12
|
+
"format:check": "prettier --check .",
|
|
13
|
+
"prepare": "husky",
|
|
14
|
+
"build-and-scan": "bash ./scripts/build-and-scan.sh",
|
|
15
|
+
"lighthouse": "bash ./scripts/lighthouse-check.sh"
|
|
16
|
+
},
|
|
17
|
+
"lint-staged": {
|
|
18
|
+
"*.{ts,tsx,js,jsx}": [
|
|
19
|
+
"prettier --write",
|
|
20
|
+
"eslint --fix --no-warn-ignored --max-warnings 0"
|
|
21
|
+
],
|
|
22
|
+
"*.{css,json,md,yml,yaml}": [
|
|
23
|
+
"prettier --write"
|
|
24
|
+
]
|
|
25
|
+
},
|
|
26
|
+
"dependencies": {
|
|
27
|
+
"@radix-ui/react-accordion": "^1.2.12",
|
|
28
|
+
"@radix-ui/react-dialog": "^1.1.14",
|
|
29
|
+
"@radix-ui/react-dropdown-menu": "^2.1.16",
|
|
30
|
+
"@radix-ui/react-slot": "^1.2.3",
|
|
31
|
+
"class-variance-authority": "^0.7.1",
|
|
32
|
+
"clsx": "^2.1.1",
|
|
33
|
+
"lucide-react": "^0.537.0",
|
|
34
|
+
"motion": "^12.0.0",
|
|
35
|
+
"next": "15.3.0",
|
|
36
|
+
"next-themes": "^0.4.6",
|
|
37
|
+
"react": "^19.0.0",
|
|
38
|
+
"react-dom": "^19.0.0",
|
|
39
|
+
"sonner": "^2.0.0",
|
|
40
|
+
"tailwind-merge": "^3.0.0"
|
|
41
|
+
},
|
|
42
|
+
"devDependencies": {
|
|
43
|
+
"@commitlint/cli": "^19.0.0",
|
|
44
|
+
"@commitlint/config-conventional": "^19.0.0",
|
|
45
|
+
"@eslint/eslintrc": "^3",
|
|
46
|
+
"@lhci/cli": "^0.14.0",
|
|
47
|
+
"@tailwindcss/postcss": "^4",
|
|
48
|
+
"@types/node": "^20",
|
|
49
|
+
"@types/react": "^19",
|
|
50
|
+
"@types/react-dom": "^19",
|
|
51
|
+
"eslint": "^9",
|
|
52
|
+
"eslint-config-next": "15.3.0",
|
|
53
|
+
"husky": "^9.0.0",
|
|
54
|
+
"lint-staged": "^15.0.0",
|
|
55
|
+
"prettier": "^3.0.0",
|
|
56
|
+
"prettier-plugin-tailwindcss": "^0.6.0",
|
|
57
|
+
"tailwindcss": "^4",
|
|
58
|
+
"tw-animate-css": "^1.3.0",
|
|
59
|
+
"typescript": "^5"
|
|
60
|
+
}
|
|
61
|
+
}
|