create-landing-app 0.2.8 → 0.3.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/prompts.js +7 -3
- package/dist/scaffold.js +5 -2
- package/package.json +1 -1
- package/templates/nextjs/base/.dockerignore +6 -0
- package/templates/nextjs/base/.editorconfig +15 -0
- package/templates/nextjs/base/.env.example +9 -2
- package/templates/nextjs/base/.husky/pre-push +8 -10
- package/templates/nextjs/base/CLAUDE.md +169 -0
- package/templates/nextjs/base/Dockerfile +3 -9
- package/templates/nextjs/base/Makefile +25 -0
- package/templates/nextjs/base/app/layout.tsx +6 -9
- package/templates/nextjs/base/app/sitemap.ts +15 -0
- package/templates/nextjs/base/commitlint.config.mjs +6 -22
- package/templates/nextjs/base/components/navs/navbar-mobile.tsx +60 -27
- package/templates/nextjs/base/components/navs/navbar.tsx +9 -2
- package/templates/nextjs/base/components/ui/checkbox.tsx +26 -0
- package/templates/nextjs/base/components/ui/input.tsx +21 -0
- package/templates/nextjs/base/components/ui/radio-group.tsx +36 -0
- package/templates/nextjs/base/components/ui/select.tsx +139 -0
- package/templates/nextjs/base/components/ui/sheet.tsx +139 -0
- package/templates/nextjs/base/components/ui/tabs.tsx +53 -0
- package/templates/nextjs/base/components/ui/textarea.tsx +20 -0
- package/templates/nextjs/base/docker-compose.yml +9 -0
- package/templates/nextjs/base/eslint.config.mjs +5 -9
- package/templates/nextjs/base/next.config.ts +4 -0
- package/templates/nextjs/base/package.json +7 -4
- package/templates/nextjs/base/styles/theme.css +2 -0
- package/templates/nextjs/base/tsconfig.json +2 -2
- package/templates/nextjs/optional/analytics/files/components/analytics.tsx +16 -0
- package/templates/nextjs/optional/analytics/files/components/web-vitals.tsx +16 -0
- package/templates/nextjs/optional/analytics/inject/app__layout.tsx +7 -0
- package/templates/nextjs/optional/analytics/pkg.json +5 -0
- package/templates/nextjs/optional/dark-mode/files/components/theme-toggle.tsx +21 -0
- package/templates/nextjs/optional/dark-mode/inject/app__layout.tsx +8 -0
- package/templates/nextjs/optional/dark-mode/pkg.json +5 -0
- package/templates/nextjs/optional/i18n-dict/files/components/navs/navbar-mobile.tsx +60 -26
- package/templates/nextjs/optional/i18n-dict/files/components/navs/navbar.tsx +8 -2
- package/templates/nextjs/optional/i18n-dict/files/{middleware.ts → proxy.ts} +8 -2
- package/templates/nextjs/optional/i18n-dict/inject/app__layout.tsx +34 -0
- package/templates/nextjs/optional/sections/blog/files/app/[lang]/blogs/(list)/[category]/main-page.tsx +15 -0
- package/templates/nextjs/optional/sections/blog/files/app/[lang]/blogs/(list)/[category]/page.tsx +38 -0
- package/templates/nextjs/optional/sections/blog/files/app/[lang]/blogs/(list)/layout.tsx +28 -0
- package/templates/nextjs/optional/sections/blog/files/app/[lang]/blogs/detail/[slugNews]/blog-detail-view.tsx +122 -0
- package/templates/nextjs/optional/sections/blog/files/app/[lang]/blogs/detail/[slugNews]/page.tsx +73 -0
- package/templates/nextjs/optional/sections/blog/files/app/api/blogs/route.ts +14 -0
- package/templates/nextjs/optional/sections/blog/files/components/blogs/blog-component.tsx +58 -0
- package/templates/nextjs/optional/sections/blog/files/components/blogs/blog-view-desktop.tsx +121 -0
- package/templates/nextjs/optional/sections/blog/files/components/blogs/blog-view-mobile.tsx +90 -0
- package/templates/nextjs/optional/sections/blog/files/components/navs/layout-blogs.tsx +51 -0
- package/templates/nextjs/optional/sections/blog/files/components/sections/blog-section-view.tsx +171 -0
- package/templates/nextjs/optional/sections/blog/files/components/sections/blog-section.tsx +13 -174
- package/templates/nextjs/optional/sections/blog/files/hooks/use-mobile.ts +19 -0
- package/templates/nextjs/optional/sections/blog/files/lib/blog-api.ts +336 -0
- package/templates/nextjs/optional/sections/blog/files/lib/sanitize.ts +25 -0
- package/templates/nextjs/optional/sections/blog/files/styles/prose.css +40 -0
- package/templates/nextjs/optional/sections/blog/inject/constants__common.ts +1 -1
- 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 };
|
|
@@ -1,16 +1,12 @@
|
|
|
1
|
-
import
|
|
2
|
-
import { fileURLToPath } from "url";
|
|
3
|
-
import { FlatCompat } from "@eslint/eslintrc";
|
|
1
|
+
import nextConfig from "eslint-config-next";
|
|
4
2
|
|
|
5
|
-
|
|
6
|
-
const
|
|
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
|
-
|
|
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
|
|
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": "
|
|
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": "
|
|
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": "
|
|
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,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
|
+
}
|
|
@@ -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 {
|
|
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 {
|
|
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
|
-
<
|
|
17
|
-
{
|
|
18
|
-
|
|
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
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
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
|
}
|