create-landing-app 0.2.8 → 0.3.1

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 (57) hide show
  1. package/dist/prompts.js +7 -3
  2. package/dist/scaffold.js +5 -2
  3. package/package.json +1 -1
  4. package/templates/nextjs/base/.dockerignore +6 -0
  5. package/templates/nextjs/base/.editorconfig +15 -0
  6. package/templates/nextjs/base/.env.example +8 -2
  7. package/templates/nextjs/base/.husky/pre-push +8 -10
  8. package/templates/nextjs/base/CLAUDE.md +169 -0
  9. package/templates/nextjs/base/Dockerfile +3 -9
  10. package/templates/nextjs/base/Makefile +25 -0
  11. package/templates/nextjs/base/app/layout.tsx +6 -9
  12. package/templates/nextjs/base/app/sitemap.ts +15 -0
  13. package/templates/nextjs/base/commitlint.config.mjs +6 -22
  14. package/templates/nextjs/base/components/navs/navbar-mobile.tsx +60 -27
  15. package/templates/nextjs/base/components/navs/navbar.tsx +9 -2
  16. package/templates/nextjs/base/components/ui/checkbox.tsx +26 -0
  17. package/templates/nextjs/base/components/ui/input.tsx +21 -0
  18. package/templates/nextjs/base/components/ui/radio-group.tsx +36 -0
  19. package/templates/nextjs/base/components/ui/select.tsx +139 -0
  20. package/templates/nextjs/base/components/ui/sheet.tsx +139 -0
  21. package/templates/nextjs/base/components/ui/tabs.tsx +53 -0
  22. package/templates/nextjs/base/components/ui/textarea.tsx +20 -0
  23. package/templates/nextjs/base/docker-compose.yml +9 -0
  24. package/templates/nextjs/base/eslint.config.mjs +5 -9
  25. package/templates/nextjs/base/next.config.ts +4 -0
  26. package/templates/nextjs/base/package.json +7 -4
  27. package/templates/nextjs/base/styles/theme.css +2 -0
  28. package/templates/nextjs/base/tsconfig.json +2 -2
  29. package/templates/nextjs/optional/analytics/files/components/analytics.tsx +16 -0
  30. package/templates/nextjs/optional/analytics/files/components/web-vitals.tsx +16 -0
  31. package/templates/nextjs/optional/analytics/inject/app__layout.tsx +7 -0
  32. package/templates/nextjs/optional/analytics/pkg.json +5 -0
  33. package/templates/nextjs/optional/dark-mode/files/components/theme-toggle.tsx +21 -0
  34. package/templates/nextjs/optional/dark-mode/inject/app__layout.tsx +8 -0
  35. package/templates/nextjs/optional/dark-mode/pkg.json +5 -0
  36. package/templates/nextjs/optional/i18n-dict/files/components/navs/navbar-mobile.tsx +60 -26
  37. package/templates/nextjs/optional/i18n-dict/files/components/navs/navbar.tsx +8 -2
  38. package/templates/nextjs/optional/i18n-dict/files/{middleware.ts → proxy.ts} +8 -2
  39. package/templates/nextjs/optional/i18n-dict/inject/app__layout.tsx +34 -0
  40. package/templates/nextjs/optional/sections/blog/files/app/[lang]/blogs/(list)/[category]/main-page.tsx +15 -0
  41. package/templates/nextjs/optional/sections/blog/files/app/[lang]/blogs/(list)/[category]/page.tsx +38 -0
  42. package/templates/nextjs/optional/sections/blog/files/app/[lang]/blogs/(list)/layout.tsx +28 -0
  43. package/templates/nextjs/optional/sections/blog/files/app/[lang]/blogs/detail/[slugNews]/blog-detail-view.tsx +122 -0
  44. package/templates/nextjs/optional/sections/blog/files/app/[lang]/blogs/detail/[slugNews]/page.tsx +73 -0
  45. package/templates/nextjs/optional/sections/blog/files/app/api/blogs/route.ts +14 -0
  46. package/templates/nextjs/optional/sections/blog/files/components/blogs/blog-component.tsx +67 -0
  47. package/templates/nextjs/optional/sections/blog/files/components/blogs/blog-view-desktop.tsx +121 -0
  48. package/templates/nextjs/optional/sections/blog/files/components/blogs/blog-view-mobile.tsx +90 -0
  49. package/templates/nextjs/optional/sections/blog/files/components/navs/layout-blogs.tsx +51 -0
  50. package/templates/nextjs/optional/sections/blog/files/components/sections/blog-section-view.tsx +171 -0
  51. package/templates/nextjs/optional/sections/blog/files/components/sections/blog-section.tsx +13 -174
  52. package/templates/nextjs/optional/sections/blog/files/hooks/use-mobile.ts +19 -0
  53. package/templates/nextjs/optional/sections/blog/files/lib/blog-api.ts +336 -0
  54. package/templates/nextjs/optional/sections/blog/files/lib/sanitize.ts +25 -0
  55. package/templates/nextjs/optional/sections/blog/files/styles/prose.css +40 -0
  56. package/templates/nextjs/optional/sections/blog/inject/constants__common.ts +1 -1
  57. package/templates/nextjs/optional/sections/blog/pkg.json +10 -0
@@ -0,0 +1,139 @@
1
+ "use client";
2
+ import * as React from "react";
3
+ import * as SelectPrimitive from "@radix-ui/react-select";
4
+ import { CheckIcon, ChevronDownIcon, ChevronUpIcon } from "lucide-react";
5
+ import { cn } from "@/lib/utils";
6
+
7
+ const Select = SelectPrimitive.Root;
8
+ const SelectGroup = SelectPrimitive.Group;
9
+ const SelectValue = SelectPrimitive.Value;
10
+
11
+ const SelectTrigger = React.forwardRef<
12
+ React.ElementRef<typeof SelectPrimitive.Trigger>,
13
+ React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger>
14
+ >(({ className, children, ...props }, ref) => (
15
+ <SelectPrimitive.Trigger
16
+ ref={ref}
17
+ className={cn(
18
+ "flex h-9 w-full items-center justify-between whitespace-nowrap rounded-md border border-input bg-transparent px-3 py-2 text-sm shadow-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-1 focus:ring-ring disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1",
19
+ className,
20
+ )}
21
+ {...props}
22
+ >
23
+ {children}
24
+ <SelectPrimitive.Icon asChild>
25
+ <ChevronDownIcon className="h-4 w-4 opacity-50" />
26
+ </SelectPrimitive.Icon>
27
+ </SelectPrimitive.Trigger>
28
+ ));
29
+ SelectTrigger.displayName = SelectPrimitive.Trigger.displayName;
30
+
31
+ const SelectScrollUpButton = React.forwardRef<
32
+ React.ElementRef<typeof SelectPrimitive.ScrollUpButton>,
33
+ React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollUpButton>
34
+ >(({ className, ...props }, ref) => (
35
+ <SelectPrimitive.ScrollUpButton
36
+ ref={ref}
37
+ className={cn("flex cursor-default items-center justify-center py-1", className)}
38
+ {...props}
39
+ >
40
+ <ChevronUpIcon className="h-4 w-4" />
41
+ </SelectPrimitive.ScrollUpButton>
42
+ ));
43
+ SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName;
44
+
45
+ const SelectScrollDownButton = React.forwardRef<
46
+ React.ElementRef<typeof SelectPrimitive.ScrollDownButton>,
47
+ React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollDownButton>
48
+ >(({ className, ...props }, ref) => (
49
+ <SelectPrimitive.ScrollDownButton
50
+ ref={ref}
51
+ className={cn("flex cursor-default items-center justify-center py-1", className)}
52
+ {...props}
53
+ >
54
+ <ChevronDownIcon className="h-4 w-4" />
55
+ </SelectPrimitive.ScrollDownButton>
56
+ ));
57
+ SelectScrollDownButton.displayName = SelectPrimitive.ScrollDownButton.displayName;
58
+
59
+ const SelectContent = React.forwardRef<
60
+ React.ElementRef<typeof SelectPrimitive.Content>,
61
+ React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content>
62
+ >(({ className, children, position = "popper", ...props }, ref) => (
63
+ <SelectPrimitive.Portal>
64
+ <SelectPrimitive.Content
65
+ ref={ref}
66
+ className={cn(
67
+ "relative z-50 max-h-96 min-w-[8rem] overflow-hidden rounded-md border bg-popover 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 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
68
+ position === "popper" &&
69
+ "data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
70
+ className,
71
+ )}
72
+ position={position}
73
+ {...props}
74
+ >
75
+ <SelectScrollUpButton />
76
+ <SelectPrimitive.Viewport
77
+ className={cn(
78
+ "p-1",
79
+ position === "popper" && "h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]",
80
+ )}
81
+ >
82
+ {children}
83
+ </SelectPrimitive.Viewport>
84
+ <SelectScrollDownButton />
85
+ </SelectPrimitive.Content>
86
+ </SelectPrimitive.Portal>
87
+ ));
88
+ SelectContent.displayName = SelectPrimitive.Content.displayName;
89
+
90
+ const SelectLabel = React.forwardRef<
91
+ React.ElementRef<typeof SelectPrimitive.Label>,
92
+ React.ComponentPropsWithoutRef<typeof SelectPrimitive.Label>
93
+ >(({ className, ...props }, ref) => (
94
+ <SelectPrimitive.Label ref={ref} className={cn("px-2 py-1.5 text-sm font-semibold", className)} {...props} />
95
+ ));
96
+ SelectLabel.displayName = SelectPrimitive.Label.displayName;
97
+
98
+ const SelectItem = React.forwardRef<
99
+ React.ElementRef<typeof SelectPrimitive.Item>,
100
+ React.ComponentPropsWithoutRef<typeof SelectPrimitive.Item>
101
+ >(({ className, children, ...props }, ref) => (
102
+ <SelectPrimitive.Item
103
+ ref={ref}
104
+ className={cn(
105
+ "relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-2 pr-8 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
106
+ className,
107
+ )}
108
+ {...props}
109
+ >
110
+ <span className="absolute right-2 flex h-3.5 w-3.5 items-center justify-center">
111
+ <SelectPrimitive.ItemIndicator>
112
+ <CheckIcon className="h-4 w-4" />
113
+ </SelectPrimitive.ItemIndicator>
114
+ </span>
115
+ <SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
116
+ </SelectPrimitive.Item>
117
+ ));
118
+ SelectItem.displayName = SelectPrimitive.Item.displayName;
119
+
120
+ const SelectSeparator = React.forwardRef<
121
+ React.ElementRef<typeof SelectPrimitive.Separator>,
122
+ React.ComponentPropsWithoutRef<typeof SelectPrimitive.Separator>
123
+ >(({ className, ...props }, ref) => (
124
+ <SelectPrimitive.Separator ref={ref} className={cn("-mx-1 my-1 h-px bg-muted", className)} {...props} />
125
+ ));
126
+ SelectSeparator.displayName = SelectPrimitive.Separator.displayName;
127
+
128
+ export {
129
+ Select,
130
+ SelectGroup,
131
+ SelectValue,
132
+ SelectTrigger,
133
+ SelectContent,
134
+ SelectLabel,
135
+ SelectItem,
136
+ SelectSeparator,
137
+ SelectScrollUpButton,
138
+ SelectScrollDownButton,
139
+ };
@@ -0,0 +1,139 @@
1
+ "use client";
2
+
3
+ import * as React from "react";
4
+ import * as SheetPrimitive from "@radix-ui/react-dialog";
5
+ import { XIcon } from "lucide-react";
6
+
7
+ import { cn } from "@/lib/utils";
8
+
9
+ function Sheet({ ...props }: React.ComponentProps<typeof SheetPrimitive.Root>) {
10
+ return <SheetPrimitive.Root data-slot="sheet" {...props} />;
11
+ }
12
+
13
+ function SheetTrigger({
14
+ ...props
15
+ }: React.ComponentProps<typeof SheetPrimitive.Trigger>) {
16
+ return <SheetPrimitive.Trigger data-slot="sheet-trigger" {...props} />;
17
+ }
18
+
19
+ function SheetClose({
20
+ ...props
21
+ }: React.ComponentProps<typeof SheetPrimitive.Close>) {
22
+ return <SheetPrimitive.Close data-slot="sheet-close" {...props} />;
23
+ }
24
+
25
+ function SheetPortal({
26
+ ...props
27
+ }: React.ComponentProps<typeof SheetPrimitive.Portal>) {
28
+ return <SheetPrimitive.Portal data-slot="sheet-portal" {...props} />;
29
+ }
30
+
31
+ function SheetOverlay({
32
+ className,
33
+ ...props
34
+ }: React.ComponentProps<typeof SheetPrimitive.Overlay>) {
35
+ return (
36
+ <SheetPrimitive.Overlay
37
+ data-slot="sheet-overlay"
38
+ className={cn(
39
+ "data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
40
+ className
41
+ )}
42
+ {...props}
43
+ />
44
+ );
45
+ }
46
+
47
+ function SheetContent({
48
+ className,
49
+ children,
50
+ side = "right",
51
+ ...props
52
+ }: React.ComponentProps<typeof SheetPrimitive.Content> & {
53
+ side?: "top" | "right" | "bottom" | "left";
54
+ }) {
55
+ return (
56
+ <SheetPortal>
57
+ <SheetOverlay />
58
+ <SheetPrimitive.Content
59
+ data-slot="sheet-content"
60
+ className={cn(
61
+ "bg-background data-[state=open]:animate-in data-[state=closed]:animate-out fixed z-50 flex flex-col gap-4 shadow-lg transition ease-in-out data-[state=closed]:duration-300 data-[state=open]:duration-500",
62
+ side === "right" &&
63
+ "data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right inset-y-0 right-0 h-full w-3/4 border-l sm:max-w-sm",
64
+ side === "left" &&
65
+ "data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left inset-y-0 left-0 h-full w-3/4 border-r sm:max-w-sm",
66
+ side === "top" &&
67
+ "data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top inset-x-0 top-0 h-auto border-b",
68
+ side === "bottom" &&
69
+ "data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom inset-x-0 bottom-0 h-auto border-t",
70
+ className
71
+ )}
72
+ {...props}
73
+ >
74
+ {children}
75
+ <SheetPrimitive.Close className="hidden ring-offset-background focus:ring-ring data-[state=open]:bg-secondary absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none">
76
+ <XIcon className="size-4" />
77
+ <span className="sr-only">Close</span>
78
+ </SheetPrimitive.Close>
79
+ </SheetPrimitive.Content>
80
+ </SheetPortal>
81
+ );
82
+ }
83
+
84
+ function SheetHeader({ className, ...props }: React.ComponentProps<"div">) {
85
+ return (
86
+ <div
87
+ data-slot="sheet-header"
88
+ className={cn("flex flex-col gap-1.5 p-4", className)}
89
+ {...props}
90
+ />
91
+ );
92
+ }
93
+
94
+ function SheetFooter({ className, ...props }: React.ComponentProps<"div">) {
95
+ return (
96
+ <div
97
+ data-slot="sheet-footer"
98
+ className={cn("mt-auto flex flex-col gap-2 p-4", className)}
99
+ {...props}
100
+ />
101
+ );
102
+ }
103
+
104
+ function SheetTitle({
105
+ className,
106
+ ...props
107
+ }: React.ComponentProps<typeof SheetPrimitive.Title>) {
108
+ return (
109
+ <SheetPrimitive.Title
110
+ data-slot="sheet-title"
111
+ className={cn("text-foreground font-semibold", className)}
112
+ {...props}
113
+ />
114
+ );
115
+ }
116
+
117
+ function SheetDescription({
118
+ className,
119
+ ...props
120
+ }: React.ComponentProps<typeof SheetPrimitive.Description>) {
121
+ return (
122
+ <SheetPrimitive.Description
123
+ data-slot="sheet-description"
124
+ className={cn("text-muted-foreground text-sm", className)}
125
+ {...props}
126
+ />
127
+ );
128
+ }
129
+
130
+ export {
131
+ Sheet,
132
+ SheetTrigger,
133
+ SheetClose,
134
+ SheetContent,
135
+ SheetHeader,
136
+ SheetFooter,
137
+ SheetTitle,
138
+ SheetDescription,
139
+ };
@@ -0,0 +1,53 @@
1
+ "use client";
2
+ import * as React from "react";
3
+ import * as TabsPrimitive from "@radix-ui/react-tabs";
4
+ import { cn } from "@/lib/utils";
5
+
6
+ const Tabs = TabsPrimitive.Root;
7
+
8
+ const TabsList = React.forwardRef<
9
+ React.ElementRef<typeof TabsPrimitive.List>,
10
+ React.ComponentPropsWithoutRef<typeof TabsPrimitive.List>
11
+ >(({ className, ...props }, ref) => (
12
+ <TabsPrimitive.List
13
+ ref={ref}
14
+ className={cn(
15
+ "inline-flex h-9 items-center justify-center rounded-lg bg-muted p-1 text-muted-foreground",
16
+ className,
17
+ )}
18
+ {...props}
19
+ />
20
+ ));
21
+ TabsList.displayName = TabsPrimitive.List.displayName;
22
+
23
+ const TabsTrigger = React.forwardRef<
24
+ React.ElementRef<typeof TabsPrimitive.Trigger>,
25
+ React.ComponentPropsWithoutRef<typeof TabsPrimitive.Trigger>
26
+ >(({ className, ...props }, ref) => (
27
+ <TabsPrimitive.Trigger
28
+ ref={ref}
29
+ className={cn(
30
+ "inline-flex items-center justify-center whitespace-nowrap rounded-md px-3 py-1 text-sm font-medium ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow",
31
+ className,
32
+ )}
33
+ {...props}
34
+ />
35
+ ));
36
+ TabsTrigger.displayName = TabsPrimitive.Trigger.displayName;
37
+
38
+ const TabsContent = React.forwardRef<
39
+ React.ElementRef<typeof TabsPrimitive.Content>,
40
+ React.ComponentPropsWithoutRef<typeof TabsPrimitive.Content>
41
+ >(({ className, ...props }, ref) => (
42
+ <TabsPrimitive.Content
43
+ ref={ref}
44
+ className={cn(
45
+ "mt-2 ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
46
+ className,
47
+ )}
48
+ {...props}
49
+ />
50
+ ));
51
+ TabsContent.displayName = TabsPrimitive.Content.displayName;
52
+
53
+ export { Tabs, TabsList, TabsTrigger, TabsContent };
@@ -0,0 +1,20 @@
1
+ import * as React from "react";
2
+ import { cn } from "@/lib/utils";
3
+
4
+ export interface TextareaProps extends React.TextareaHTMLAttributes<HTMLTextAreaElement> {}
5
+
6
+ const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(({ className, ...props }, ref) => {
7
+ return (
8
+ <textarea
9
+ className={cn(
10
+ "flex min-h-[60px] w-full rounded-md border border-input bg-transparent px-3 py-2 text-base shadow-sm placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
11
+ className,
12
+ )}
13
+ ref={ref}
14
+ {...props}
15
+ />
16
+ );
17
+ });
18
+ Textarea.displayName = "Textarea";
19
+
20
+ export { Textarea };
@@ -0,0 +1,9 @@
1
+ services:
2
+ web:
3
+ build: .
4
+ ports:
5
+ - "3000:3000"
6
+ environment:
7
+ - NODE_ENV=production
8
+ - NEXT_PUBLIC_SITE_URL=${NEXT_PUBLIC_SITE_URL}
9
+ restart: unless-stopped
@@ -1,16 +1,12 @@
1
- import { dirname } from "path";
2
- import { fileURLToPath } from "url";
3
- import { FlatCompat } from "@eslint/eslintrc";
1
+ import nextConfig from "eslint-config-next";
4
2
 
5
- const __filename = fileURLToPath(import.meta.url);
6
- const __dirname = dirname(__filename);
7
-
8
- const compat = new FlatCompat({ baseDirectory: __dirname });
3
+ // Extract @typescript-eslint plugin already loaded by nextConfig (avoids duplicate import)
4
+ const tsPlugin = nextConfig.find((c) => c.plugins?.["@typescript-eslint"])?.plugins["@typescript-eslint"];
9
5
 
10
6
  const eslintConfig = [
11
- { ignores: [".next/**", "node_modules/**"] },
12
- ...compat.extends("next/core-web-vitals", "next/typescript"),
7
+ ...nextConfig,
13
8
  {
9
+ ...(tsPlugin && { plugins: { "@typescript-eslint": tsPlugin } }),
14
10
  rules: {
15
11
  // Enforce no unused variables — common source of tech debt
16
12
  "@typescript-eslint/no-unused-vars": ["error", { argsIgnorePattern: "^_" }],
@@ -1,6 +1,10 @@
1
+ import path from "path";
1
2
  import type { NextConfig } from "next";
2
3
 
3
4
  const nextConfig: NextConfig = {
5
+ // Pin workspace root to this project dir — prevents Next.js from
6
+ // traversing up into a parent monorepo and breaking Turbopack font resolution
7
+ outputFileTracingRoot: path.resolve(__dirname),
4
8
  experimental: {
5
9
  optimizePackageImports: ["lucide-react"],
6
10
  },
@@ -3,7 +3,7 @@
3
3
  "version": "0.1.0",
4
4
  "private": true,
5
5
  "scripts": {
6
- "dev": "next dev --turbopack",
6
+ "dev": "next dev",
7
7
  "build": "next build",
8
8
  "start": "next start",
9
9
  "lint": "next lint",
@@ -25,15 +25,18 @@
25
25
  },
26
26
  "dependencies": {
27
27
  "@radix-ui/react-accordion": "^1.2.12",
28
+ "@radix-ui/react-checkbox": "^1.1.4",
28
29
  "@radix-ui/react-dialog": "^1.1.14",
29
30
  "@radix-ui/react-dropdown-menu": "^2.1.16",
31
+ "@radix-ui/react-radio-group": "^1.2.3",
32
+ "@radix-ui/react-select": "^2.1.6",
30
33
  "@radix-ui/react-slot": "^1.2.3",
34
+ "@radix-ui/react-tabs": "^1.1.3",
31
35
  "class-variance-authority": "^0.7.1",
32
36
  "clsx": "^2.1.1",
33
37
  "lucide-react": "^0.537.0",
34
38
  "motion": "^12.0.0",
35
- "next": "15.3.0",
36
- "next-themes": "^0.4.6",
39
+ "next": "16.2.3",
37
40
  "react": "^19.0.0",
38
41
  "react-dom": "^19.0.0",
39
42
  "sonner": "^2.0.0",
@@ -49,7 +52,7 @@
49
52
  "@types/react": "^19",
50
53
  "@types/react-dom": "^19",
51
54
  "eslint": "^9",
52
- "eslint-config-next": "15.3.0",
55
+ "eslint-config-next": "16.2.3",
53
56
  "husky": "^9.0.0",
54
57
  "lint-staged": "^15.0.0",
55
58
  "prettier": "^3.0.0",
@@ -33,6 +33,8 @@
33
33
  --brand-gradient-to: #ffffff;
34
34
  }
35
35
 
36
+ /* Dark mode overrides — activated by the dark-mode optional module (next-themes).
37
+ These variables have no effect unless ThemeProvider is installed. */
36
38
  .dark {
37
39
  --background: oklch(0.1 0.02 250);
38
40
  --foreground: oklch(0.985 0.001 106.423);
@@ -11,11 +11,11 @@
11
11
  "moduleResolution": "bundler",
12
12
  "resolveJsonModule": true,
13
13
  "isolatedModules": true,
14
- "jsx": "preserve",
14
+ "jsx": "react-jsx",
15
15
  "incremental": true,
16
16
  "plugins": [{ "name": "next" }],
17
17
  "paths": { "@/*": ["./*"] }
18
18
  },
19
- "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
19
+ "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts", ".next/dev/types/**/*.ts"],
20
20
  "exclude": ["node_modules"]
21
21
  }
@@ -0,0 +1,16 @@
1
+ import { GoogleTagManager, GoogleAnalytics } from "@next/third-parties/google";
2
+
3
+ // Analytics component — enabled via environment variables.
4
+ // Set NEXT_PUBLIC_GTM_ID and/or NEXT_PUBLIC_GA_ID in .env to activate.
5
+ // Sentry: install @sentry/nextjs and follow their wizard for full setup.
6
+ export function Analytics() {
7
+ const gtmId = process.env.NEXT_PUBLIC_GTM_ID;
8
+ const gaId = process.env.NEXT_PUBLIC_GA_ID;
9
+
10
+ return (
11
+ <>
12
+ {gtmId && <GoogleTagManager gtmId={gtmId} />}
13
+ {gaId && <GoogleAnalytics gaId={gaId} />}
14
+ </>
15
+ );
16
+ }
@@ -0,0 +1,16 @@
1
+ "use client";
2
+ import { useReportWebVitals } from "next/web-vitals";
3
+
4
+ // Reports Core Web Vitals to the console (development) or your analytics endpoint.
5
+ // Replace the console.log with your analytics send call in production.
6
+ export function WebVitals() {
7
+ useReportWebVitals((metric) => {
8
+ if (process.env.NODE_ENV === "development") {
9
+ console.log(metric);
10
+ }
11
+ // Example: send to GA4
12
+ // gtag("event", metric.name, { value: Math.round(metric.name === "CLS" ? metric.value * 1000 : metric.value), ... });
13
+ });
14
+
15
+ return null;
16
+ }
@@ -0,0 +1,7 @@
1
+ MARKER:__PROVIDERS_IMPORT__
2
+ import { Analytics } from "@/components/analytics";
3
+ import { WebVitals } from "@/components/web-vitals";
4
+ ---
5
+ MARKER:__PROVIDERS_WRAP_END__
6
+ <Analytics />
7
+ <WebVitals />
@@ -0,0 +1,5 @@
1
+ {
2
+ "dependencies": {
3
+ "@next/third-parties": "^15.0.0"
4
+ }
5
+ }
@@ -0,0 +1,21 @@
1
+ "use client";
2
+ import { useTheme } from "next-themes";
3
+ import { MoonIcon, SunIcon } from "lucide-react";
4
+ import { Button } from "@/components/ui/button";
5
+
6
+ // Drop this component anywhere in your layout to expose a dark/light toggle button.
7
+ export function ThemeToggle() {
8
+ const { theme, setTheme } = useTheme();
9
+
10
+ return (
11
+ <Button
12
+ variant="ghost"
13
+ size="icon"
14
+ aria-label="Toggle theme"
15
+ onClick={() => setTheme(theme === "dark" ? "light" : "dark")}
16
+ >
17
+ <SunIcon className="h-4 w-4 rotate-0 scale-100 transition-transform dark:-rotate-90 dark:scale-0" />
18
+ <MoonIcon className="absolute h-4 w-4 rotate-90 scale-0 transition-transform dark:rotate-0 dark:scale-100" />
19
+ </Button>
20
+ );
21
+ }
@@ -0,0 +1,8 @@
1
+ MARKER:__PROVIDERS_IMPORT__
2
+ import { ThemeProvider } from "next-themes";
3
+ ---
4
+ MARKER:__PROVIDERS_WRAP_START__
5
+ <ThemeProvider attribute="class" defaultTheme="light" enableSystem>
6
+ ---
7
+ MARKER:__PROVIDERS_WRAP_END__
8
+ </ThemeProvider>
@@ -0,0 +1,5 @@
1
+ {
2
+ "dependencies": {
3
+ "next-themes": "^0.4.6"
4
+ }
5
+ }
@@ -1,41 +1,75 @@
1
1
  "use client";
2
- import { useState } from "react";
3
2
  import Link from "next/link";
4
3
  import { Menu, X } from "lucide-react";
5
- import { NAV_LINKS } from "@/constants/common";
4
+ import { motion } from "motion/react";
5
+ import { NAV_LINKS, SITE_NAME } from "@/constants/common";
6
6
  import { Button } from "@/components/ui/button";
7
- import { cn } from "@/lib/utils";
7
+ import {
8
+ Sheet,
9
+ SheetTrigger,
10
+ SheetContent,
11
+ SheetClose,
12
+ SheetHeader,
13
+ SheetFooter,
14
+ SheetTitle,
15
+ } from "@/components/ui/sheet";
8
16
  import { useDictionary } from "@/lib/dict-context";
9
17
 
10
18
  export default function NavbarMobile() {
11
- const [open, setOpen] = useState(false);
12
19
  const dict = useDictionary();
13
20
 
14
21
  return (
15
22
  <div className="md:hidden">
16
- <button onClick={() => setOpen(!open)} aria-label="Toggle menu">
17
- {open ? <X className="h-5 w-5" /> : <Menu className="h-5 w-5" />}
18
- </button>
23
+ <Sheet>
24
+ {/* Hamburger trigger */}
25
+ <SheetTrigger asChild>
26
+ <Button variant="ghost" size="icon" aria-label="Open menu">
27
+ <Menu className="h-5 w-5" />
28
+ </Button>
29
+ </SheetTrigger>
19
30
 
20
- {/* Slide-down mobile menu */}
21
- <div
22
- className={cn(
23
- "absolute inset-x-0 top-16 flex flex-col gap-4 border-b bg-background p-6 transition-all duration-200",
24
- open ? "visible opacity-100" : "invisible pointer-events-none opacity-0"
25
- )}
26
- >
27
- {NAV_LINKS.map((link) => (
28
- <Link
29
- key={link.href}
30
- href={link.href}
31
- onClick={() => setOpen(false)}
32
- className="text-sm font-medium"
33
- >
34
- {dict.nav[link.href.slice(1) as keyof typeof dict.nav] ?? link.label}
35
- </Link>
36
- ))}
37
- <Button className="w-full">{dict.nav.getStarted}</Button>
38
- </div>
31
+ <SheetContent>
32
+ {/* Header: logo + close button */}
33
+ <SheetHeader className="flex-row items-center justify-between">
34
+ <SheetTitle className="text-base font-bold text-primary not-sr-only">
35
+ {SITE_NAME}
36
+ </SheetTitle>
37
+ <SheetClose asChild>
38
+ <Button variant="ghost" size="icon" aria-label="Close menu">
39
+ <X className="h-5 w-5" />
40
+ </Button>
41
+ </SheetClose>
42
+ </SheetHeader>
43
+
44
+ {/* Nav links — staggered fade-in */}
45
+ <nav className="flex flex-1 flex-col gap-1 px-2">
46
+ {NAV_LINKS.map((link, i) => (
47
+ <motion.div
48
+ key={link.href}
49
+ initial={{ opacity: 0, x: 16 }}
50
+ animate={{ opacity: 1, x: 0 }}
51
+ transition={{ delay: i * 0.07, duration: 0.25 }}
52
+ >
53
+ <SheetClose asChild>
54
+ <Link
55
+ href={link.href}
56
+ className="block rounded-md px-3 py-3 text-base font-medium text-foreground transition-colors hover:bg-accent hover:text-accent-foreground"
57
+ >
58
+ {dict.nav[link.href.slice(1) as keyof typeof dict.nav] ?? link.label}
59
+ </Link>
60
+ </SheetClose>
61
+ </motion.div>
62
+ ))}
63
+ </nav>
64
+
65
+ {/* CTA */}
66
+ <SheetFooter>
67
+ <SheetClose asChild>
68
+ <Button className="w-full">{dict.nav.getStarted}</Button>
69
+ </SheetClose>
70
+ </SheetFooter>
71
+ </SheetContent>
72
+ </Sheet>
39
73
  </div>
40
74
  );
41
75
  }