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.
Files changed (91) hide show
  1. package/dist/index.js +21 -0
  2. package/dist/install.js +18 -0
  3. package/dist/prompts.js +62 -0
  4. package/dist/scaffold.js +159 -0
  5. package/dist/utils/__tests__/merge-json.test.js +144 -0
  6. package/dist/utils/__tests__/replace-tokens.test.js +212 -0
  7. package/dist/utils/copy-dir.js +22 -0
  8. package/dist/utils/merge-json.js +19 -0
  9. package/dist/utils/replace-tokens.js +8 -0
  10. package/package.json +48 -0
  11. package/templates/nextjs/base/.env.example +8 -0
  12. package/templates/nextjs/base/.github/workflows/ci.yml +40 -0
  13. package/templates/nextjs/base/.husky/commit-msg +7 -0
  14. package/templates/nextjs/base/.husky/pre-commit +3 -0
  15. package/templates/nextjs/base/.husky/pre-push +46 -0
  16. package/templates/nextjs/base/.lighthouserc.json +28 -0
  17. package/templates/nextjs/base/.prettierignore +11 -0
  18. package/templates/nextjs/base/.prettierrc.json +10 -0
  19. package/templates/nextjs/base/Dockerfile +42 -0
  20. package/templates/nextjs/base/app/globals.css +82 -0
  21. package/templates/nextjs/base/app/layout.tsx +32 -0
  22. package/templates/nextjs/base/app/not-found.tsx +13 -0
  23. package/templates/nextjs/base/app/page.tsx +15 -0
  24. package/templates/nextjs/base/app/robots.ts +9 -0
  25. package/templates/nextjs/base/commitlint.config.mjs +32 -0
  26. package/templates/nextjs/base/components/navs/navbar-mobile.tsx +39 -0
  27. package/templates/nextjs/base/components/navs/navbar.tsx +39 -0
  28. package/templates/nextjs/base/components/providers.tsx +12 -0
  29. package/templates/nextjs/base/components/sections/features-section.tsx +78 -0
  30. package/templates/nextjs/base/components/sections/footer-section.tsx +98 -0
  31. package/templates/nextjs/base/components/sections/hero-section.tsx +74 -0
  32. package/templates/nextjs/base/components/ui/accordion.tsx +47 -0
  33. package/templates/nextjs/base/components/ui/button.tsx +44 -0
  34. package/templates/nextjs/base/components/ui/dialog.tsx +61 -0
  35. package/templates/nextjs/base/components/ui/dropdown-menu.tsx +55 -0
  36. package/templates/nextjs/base/components/ui/sonner.tsx +6 -0
  37. package/templates/nextjs/base/components.json +19 -0
  38. package/templates/nextjs/base/constants/common.ts +15 -0
  39. package/templates/nextjs/base/eslint.config.mjs +25 -0
  40. package/templates/nextjs/base/lib/metadata.ts +36 -0
  41. package/templates/nextjs/base/lib/utils.ts +7 -0
  42. package/templates/nextjs/base/next.config.ts +33 -0
  43. package/templates/nextjs/base/package.json +61 -0
  44. package/templates/nextjs/base/postcss.config.mjs +7 -0
  45. package/templates/nextjs/base/scripts/build-and-scan.sh +127 -0
  46. package/templates/nextjs/base/scripts/lighthouse-check.sh +86 -0
  47. package/templates/nextjs/base/styles/theme.css +63 -0
  48. package/templates/nextjs/base/tsconfig.json +21 -0
  49. package/templates/nextjs/base/types/index.ts +16 -0
  50. package/templates/nextjs/optional/docker/files/.dockerignore +6 -0
  51. package/templates/nextjs/optional/docker/files/Dockerfile +36 -0
  52. package/templates/nextjs/optional/docker/files/docker-compose.yml +9 -0
  53. package/templates/nextjs/optional/i18n-dict/files/app/[lang]/layout.tsx +19 -0
  54. package/templates/nextjs/optional/i18n-dict/files/app/[lang]/page.tsx +15 -0
  55. package/templates/nextjs/optional/i18n-dict/files/components/navs/language-switcher.tsx +39 -0
  56. package/templates/nextjs/optional/i18n-dict/files/components/navs/navbar-mobile.tsx +41 -0
  57. package/templates/nextjs/optional/i18n-dict/files/components/navs/navbar.tsx +41 -0
  58. package/templates/nextjs/optional/i18n-dict/files/components/providers.tsx +16 -0
  59. package/templates/nextjs/optional/i18n-dict/files/components/sections/features-section.tsx +80 -0
  60. package/templates/nextjs/optional/i18n-dict/files/components/sections/footer-section.tsx +98 -0
  61. package/templates/nextjs/optional/i18n-dict/files/dictionaries/en.json +21 -0
  62. package/templates/nextjs/optional/i18n-dict/files/dictionaries/vi.json +21 -0
  63. package/templates/nextjs/optional/i18n-dict/files/get-dictionary.ts +10 -0
  64. package/templates/nextjs/optional/i18n-dict/files/i18n-config.ts +6 -0
  65. package/templates/nextjs/optional/i18n-dict/files/lib/dict-context.tsx +23 -0
  66. package/templates/nextjs/optional/i18n-dict/files/middleware.ts +31 -0
  67. package/templates/nextjs/optional/i18n-dict/pkg.json +9 -0
  68. package/templates/nextjs/optional/sections/about/files/components/sections/about-section.tsx +36 -0
  69. package/templates/nextjs/optional/sections/about/inject/app__[lang]__page.tsx +5 -0
  70. package/templates/nextjs/optional/sections/about/inject/app__page.tsx +5 -0
  71. package/templates/nextjs/optional/sections/about/inject/constants__common.ts +2 -0
  72. package/templates/nextjs/optional/sections/blog/files/components/sections/blog-section.tsx +191 -0
  73. package/templates/nextjs/optional/sections/blog/inject/app__[lang]__page.tsx +5 -0
  74. package/templates/nextjs/optional/sections/blog/inject/app__page.tsx +5 -0
  75. package/templates/nextjs/optional/sections/blog/inject/constants__common.ts +2 -0
  76. package/templates/nextjs/optional/sections/contact/files/components/sections/contact-section.tsx +79 -0
  77. package/templates/nextjs/optional/sections/contact/inject/app__[lang]__page.tsx +5 -0
  78. package/templates/nextjs/optional/sections/contact/inject/app__page.tsx +5 -0
  79. package/templates/nextjs/optional/sections/contact/inject/constants__common.ts +2 -0
  80. package/templates/nextjs/optional/tanstack-query/files/lib/custom-fetch.ts +9 -0
  81. package/templates/nextjs/optional/tanstack-query/files/lib/query-client.ts +21 -0
  82. package/templates/nextjs/optional/tanstack-query/inject/components__providers.tsx +9 -0
  83. package/templates/nextjs/optional/tanstack-query/pkg.json +5 -0
  84. package/templates/nextjs/optional/zustand/files/store/ui-store.ts +16 -0
  85. package/templates/nextjs/optional/zustand/inject/components__providers.tsx +3 -0
  86. package/templates/nextjs/optional/zustand/pkg.json +5 -0
  87. package/templates/nextjs/themes/dark.css +36 -0
  88. package/templates/nextjs/themes/forest.css +58 -0
  89. package/templates/nextjs/themes/ocean.css +58 -0
  90. package/templates/nextjs/themes/pila.css +75 -0
  91. 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,6 @@
1
+ "use client";
2
+ import { Toaster as SonnerToaster } from "sonner";
3
+
4
+ export function Toaster() {
5
+ return <SonnerToaster richColors position="bottom-right" />;
6
+ }
@@ -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,7 @@
1
+ import { clsx, type ClassValue } from "clsx";
2
+ import { twMerge } from "tailwind-merge";
3
+
4
+ // Merge Tailwind classes safely — use this everywhere instead of string interpolation
5
+ export function cn(...inputs: ClassValue[]) {
6
+ return twMerge(clsx(inputs));
7
+ }
@@ -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
+ }
@@ -0,0 +1,7 @@
1
+ const config = {
2
+ plugins: {
3
+ "@tailwindcss/postcss": {},
4
+ },
5
+ };
6
+
7
+ export default config;