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
package/dist/prompts.js CHANGED
@@ -13,8 +13,11 @@ export async function runPrompts() {
13
13
  const contact = await confirm({ message: "Include Contact section? (form + API route)" });
14
14
  if (isCancel(contact))
15
15
  return null;
16
- const docker = await confirm({ message: "Include Docker setup? (for VPS deploy)" });
17
- if (isCancel(docker))
16
+ const darkMode = await confirm({ message: "Enable dark mode toggle?" });
17
+ if (isCancel(darkMode))
18
+ return null;
19
+ const analytics = await confirm({ message: "Include Analytics? (GTM + GA via env vars)" });
20
+ if (isCancel(analytics))
18
21
  return null;
19
22
  return {
20
23
  projectName: String(projectName).trim(),
@@ -24,6 +27,7 @@ export async function runPrompts() {
24
27
  dataFetching: "tanstack-query",
25
28
  blog: Boolean(blog),
26
29
  contact: Boolean(contact),
27
- docker: Boolean(docker),
30
+ darkMode: Boolean(darkMode),
31
+ analytics: Boolean(analytics),
28
32
  };
29
33
  }
package/dist/scaffold.js CHANGED
@@ -32,8 +32,11 @@ export async function scaffold(config, targetDir) {
32
32
  optionals.push("sections/blog");
33
33
  if (config.contact)
34
34
  optionals.push("sections/contact");
35
- if (config.docker)
36
- optionals.push("docker");
35
+ // Optional UI/tracking features — run after sections so layout markers are intact
36
+ if (config.darkMode)
37
+ optionals.push("dark-mode");
38
+ if (config.analytics)
39
+ optionals.push("analytics");
37
40
  // 3. Apply each optional feature (files + inject markers)
38
41
  for (const opt of optionals) {
39
42
  const optDir = path.join(TEMPLATES_ROOT, "optional", opt);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-landing-app",
3
- "version": "0.2.8",
3
+ "version": "0.3.1",
4
4
  "description": "Create a production-ready Next.js landing page with one command",
5
5
  "type": "module",
6
6
  "bin": {
@@ -0,0 +1,6 @@
1
+ node_modules
2
+ .next
3
+ .git
4
+ *.log
5
+ .env
6
+ .env.local
@@ -0,0 +1,15 @@
1
+ root = true
2
+
3
+ [*]
4
+ indent_style = space
5
+ indent_size = 2
6
+ end_of_line = lf
7
+ charset = utf-8
8
+ trim_trailing_whitespace = true
9
+ insert_final_newline = true
10
+
11
+ [*.md]
12
+ trim_trailing_whitespace = false
13
+
14
+ [Makefile]
15
+ indent_style = tab
@@ -1,8 +1,14 @@
1
1
  # Site
2
2
  NEXT_PUBLIC_SITE_URL=https://yourdomain.com
3
3
 
4
- # Analytics (optional)
5
- NEXT_PUBLIC_GOOGLE_ANALYTICS_ID=
4
+ # Analytics (optional) — install analytics optional module to activate
5
+ NEXT_PUBLIC_GTM_ID=
6
+ NEXT_PUBLIC_GA_ID=
7
+
8
+ # Blog (optional) — set to your API base URL to replace built-in mock data
9
+ # Expected endpoints: GET /blogs?applicationFilter=..., GET /blogs/:slug, GET /categories
10
+ NEXT_PUBLIC_BLOG_API=
11
+ BLOG_API_APPLICATION=ndachain
6
12
 
7
13
  # S3 / Storage (optional)
8
14
  NEXT_PUBLIC_S3_DOMAIN=
@@ -28,17 +28,15 @@ else
28
28
  fi
29
29
  echo ""
30
30
 
31
- # ── Step 3: Docker build + security scan (opt-in with RUN_SCAN=true) ──
32
- if [ "${RUN_SCAN}" = "true" ]; then
33
- if ! command -v docker > /dev/null 2>&1; then
34
- echo "${YELLOW}[3/3] Security scan skipped (Docker not found)${NC}"
35
- else
36
- echo "${GREEN}[3/3] Running Docker build + security scan...${NC}"
37
- FAIL_ON_VULN=false bash ./scripts/build-and-scan.sh
38
- echo "${GREEN}✅ Scan completed${NC}"
39
- fi
31
+ # ── Step 3: Docker build + security scan (always runs, skip with SKIP_SCAN=true) ──
32
+ if [ "${SKIP_SCAN}" = "true" ]; then
33
+ echo "${YELLOW}[3/3] Security scan skipped (SKIP_SCAN=true)${NC}"
34
+ elif ! command -v docker > /dev/null 2>&1; then
35
+ echo "${YELLOW}[3/3] Security scan skipped (Docker not found)${NC}"
40
36
  else
41
- echo "${YELLOW}[3/3] Security scan skipped (set RUN_SCAN=true to enable)${NC}"
37
+ echo "${GREEN}[3/3] Running Docker build + security scan...${NC}"
38
+ FAIL_ON_VULN=false bash ./scripts/build-and-scan.sh
39
+ echo "${GREEN}✅ Scan completed${NC}"
42
40
  fi
43
41
 
44
42
  echo ""
@@ -0,0 +1,169 @@
1
+ # CLAUDE.md
2
+
3
+ Project-specific instructions for Claude Code when working on a generated `create-landing-app` project.
4
+
5
+ ## Stack
6
+
7
+ | Layer | Technology |
8
+ |---|---|
9
+ | Framework | Next.js 16.2 (App Router, Turbopack) |
10
+ | Language | TypeScript 5 (strict mode) |
11
+ | Styling | Tailwind CSS v4 + CSS variables |
12
+ | UI Components | Radix UI primitives + shadcn/ui pattern |
13
+ | Animation | Motion (Framer Motion v12) |
14
+ | Icons | Lucide React |
15
+ | Fonts | Inter (Google Fonts via `next/font`) |
16
+ | Toasts | Sonner |
17
+ | Package manager | npm (or bun/pnpm depending on scaffold choice) |
18
+
19
+ ## Project Structure
20
+
21
+ ```
22
+ app/
23
+ layout.tsx Root layout — add providers, fonts here
24
+ page.tsx Home page — compose sections here
25
+ globals.css Tailwind base + global resets
26
+ sitemap.ts Auto-generated sitemap
27
+ robots.ts robots.txt config
28
+ not-found.tsx Custom 404 page
29
+
30
+ components/
31
+ navs/ Navigation components
32
+ navbar.tsx Desktop navbar
33
+ navbar-mobile.tsx Mobile drawer menu
34
+ sections/ Page section components
35
+ hero-section.tsx
36
+ features-section.tsx
37
+ footer-section.tsx
38
+ ui/ shadcn/ui base components (Radix-based)
39
+ button.tsx
40
+ dialog.tsx
41
+ dropdown-menu.tsx
42
+ accordion.tsx
43
+ input.tsx checkbox.tsx radio-group.tsx select.tsx tabs.tsx textarea.tsx
44
+ sonner.tsx Toast wrapper
45
+ providers.tsx Client-side provider wrapper
46
+
47
+ constants/
48
+ common.ts Nav links, site config, social links
49
+
50
+ lib/
51
+ metadata.ts createMetadata() factory — use for all page metadata
52
+ utils.ts cn() — Tailwind class merge utility
53
+
54
+ styles/
55
+ theme.css CSS variables (colors, radius) — edit here to rebrand
56
+
57
+ types/ Shared TypeScript type definitions
58
+ public/ Static assets (og-image.png, favicon, etc.)
59
+ scripts/
60
+ lighthouse-check.sh Manual Lighthouse CI runner
61
+ build-and-scan.sh Docker build + Trivy security scan
62
+ ```
63
+
64
+ ## Development Commands
65
+
66
+ ```bash
67
+ npm run dev # Dev server (Turbopack — fast HMR)
68
+ npm run build # Production build
69
+ npm run start # Serve production build
70
+ npm run lint # ESLint check
71
+ npm run lint:fix # ESLint auto-fix
72
+ npm run format # Prettier write (all files)
73
+ npm run format:check # Prettier dry-run
74
+ npm run lighthouse # Run Lighthouse CI audit manually
75
+ npm run build-and-scan # Docker build + Trivy vulnerability scan
76
+ ```
77
+
78
+ ## Code Conventions
79
+
80
+ ### TypeScript
81
+ - **No `any` types** — use `unknown` + type narrowing or proper interfaces
82
+ - Keep component files under **200 lines** — extract sub-components when larger
83
+ - Use named exports for components; default export only for Next.js pages/layouts
84
+
85
+ ### Styling
86
+ - Use `cn()` from `lib/utils.ts` for all conditional class merging — never string interpolation
87
+ - **CSS variables** for all design tokens — defined in `styles/theme.css`
88
+ - Rebrand by editing `--brand-*` and `--primary` variables in `theme.css`
89
+ - Dark mode variables are in the `.dark {}` block — only active when `dark-mode` optional module is installed
90
+
91
+ ### Metadata
92
+ - **Always** use `createMetadata()` from `lib/metadata.ts` for page `<head>` metadata
93
+ - Set `NEXT_PUBLIC_SITE_URL` in `.env.local` — used for canonical URLs and OG images
94
+ - Place `og-image.png` (1200×630) in `/public/`
95
+
96
+ ### Components
97
+ - Place new page sections in `components/sections/`
98
+ - Place new nav components in `components/navs/`
99
+ - Place Radix-based UI primitives in `components/ui/`
100
+ - All animations via Motion (`motion/react`) — avoid CSS transitions for interactive elements
101
+
102
+ ### Providers
103
+ - Add client-side providers in `components/providers.tsx`
104
+ - The root `layout.tsx` has `// __PROVIDERS_IMPORT__` and `// __PROVIDERS_WRAP_START/END__` markers — these are used by the CLI injector; do not remove them
105
+
106
+ ## Environment Variables
107
+
108
+ ```bash
109
+ # .env.local (never commit this file)
110
+ NEXT_PUBLIC_SITE_URL=https://yourdomain.com # Required — used in metadata & sitemap
111
+ NEXT_PUBLIC_GTM_ID= # Google Tag Manager (analytics module)
112
+ NEXT_PUBLIC_GA_ID= # Google Analytics (analytics module)
113
+ NEXT_PUBLIC_BLOG_API= # External blog API base URL (blog module)
114
+ BLOG_API_APPLICATION=ndachain # Application filter required by the blog API
115
+ NEXT_PUBLIC_S3_DOMAIN= # CDN domain for media assets
116
+ ```
117
+
118
+ ## Optional Modules (installed via CLI)
119
+
120
+ These are injected at scaffold time — each module merges files and deps into the base template:
121
+
122
+ | Module | What it adds |
123
+ |---|---|
124
+ | `i18n-dict` | `dictionaries/en.json` + `vi.json`, `app/[lang]/` routing, middleware, language switcher |
125
+ | `zustand` | `store/ui-store.ts`, Zustand provider |
126
+ | `tanstack-query` | `lib/query-client.ts`, `lib/custom-fetch.ts`, QueryClientProvider |
127
+ | `docker` | `Dockerfile`, `docker-compose.yml`, `.dockerignore`, `scripts/build-and-scan.sh` |
128
+ | `analytics` | GTM/GA4 script injection, env var wiring |
129
+ | `dark-mode` | `next-themes` ThemeProvider, theme toggle component |
130
+ | `blog` | Blog listing + detail pages, `lib/blog-api.ts`, optional external API |
131
+ | `contact` | Contact form section with validation |
132
+ | `about` | About section with team/mission layout |
133
+
134
+ ## Git Hooks (Husky)
135
+
136
+ | Hook | Runs |
137
+ |---|---|
138
+ | `pre-commit` | Prettier + ESLint on staged files (via lint-staged) |
139
+ | `commit-msg` | commitlint — enforces Conventional Commits |
140
+ | `pre-push` | `next build` → Lighthouse CI → Docker scan (if Dockerfile exists) |
141
+
142
+ Skip Lighthouse on push: `SKIP_LIGHTHOUSE=true git push`
143
+ Skip Docker scan: `SKIP_SCAN=true git push`
144
+
145
+ ## Commit Convention
146
+
147
+ ```
148
+ feat(hero): add animated gradient background
149
+ fix(navbar): close mobile menu on route change
150
+ refactor(blog): extract post card into separate component
151
+ chore: update dependencies
152
+ ```
153
+
154
+ Allowed types: `feat` `fix` `docs` `style` `refactor` `perf` `test` `build` `ci` `chore` `revert`
155
+
156
+ ## Deployment
157
+
158
+ ```bash
159
+ # Docker (standalone output)
160
+ docker build -t my-app .
161
+ docker run -p 3000:3000 my-app
162
+
163
+ # Or
164
+ docker compose up
165
+
166
+ # Vercel / Netlify — just connect the repo, no config needed
167
+ ```
168
+
169
+ The Docker image runs `node .next/standalone/server.js` — not `next start`.
@@ -2,12 +2,12 @@
2
2
  FROM node:20-alpine AS deps
3
3
  RUN corepack enable
4
4
  WORKDIR /app
5
- COPY package.json yarn.lock* package-lock.json* pnpm-lock.yaml* bun.lock* bun.lockb* ./
5
+ COPY package.json yarn.lock* package-lock.json* pnpm-lock.yaml* bun.lockb* ./
6
6
  RUN \
7
7
  if [ -f yarn.lock ]; then yarn --frozen-lockfile; \
8
8
  elif [ -f package-lock.json ]; then npm ci; \
9
9
  elif [ -f pnpm-lock.yaml ]; then pnpm i --frozen-lockfile; \
10
- elif [ -f bun.lock ] || [ -f bun.lockb ]; then npm i -g bun && bun install --frozen-lockfile; \
10
+ elif [ -f bun.lockb ]; then bun install --frozen-lockfile; \
11
11
  else echo "No lockfile found." && exit 1; \
12
12
  fi
13
13
 
@@ -16,13 +16,7 @@ FROM node:20-alpine AS builder
16
16
  WORKDIR /app
17
17
  COPY --from=deps /app/node_modules ./node_modules
18
18
  COPY . .
19
- RUN \
20
- if [ -f yarn.lock ]; then yarn build; \
21
- elif [ -f package-lock.json ]; then npm run build; \
22
- elif [ -f pnpm-lock.yaml ]; then pnpm run build; \
23
- elif [ -f bun.lock ] || [ -f bun.lockb ]; then npm i -g bun && bun run build; \
24
- else npm run build; \
25
- fi
19
+ RUN yarn build
26
20
 
27
21
  # Stage 3: Production runner (standalone output)
28
22
  FROM node:20-alpine AS runner
@@ -0,0 +1,25 @@
1
+ .PHONY: dev build start lint format lighthouse docker-build docker-run
2
+
3
+ dev:
4
+ npm run dev
5
+
6
+ build:
7
+ npm run build
8
+
9
+ start:
10
+ npm run start
11
+
12
+ lint:
13
+ npm run lint
14
+
15
+ format:
16
+ npm run format
17
+
18
+ lighthouse:
19
+ npm run lighthouse
20
+
21
+ docker-build:
22
+ docker build -t __PROJECT_NAME__ .
23
+
24
+ docker-run:
25
+ docker run -p 3000:3000 __PROJECT_NAME__
@@ -1,13 +1,12 @@
1
1
  import type { Metadata } from "next";
2
2
  import { Inter } from "next/font/google";
3
- import { ThemeProvider } from "next-themes";
4
3
  import { Toaster } from "@/components/ui/sonner";
5
4
  import Providers from "@/components/providers";
6
5
  import { createMetadata } from "@/lib/metadata";
7
6
  import "./globals.css";
8
7
  // __PROVIDERS_IMPORT__
9
8
 
10
- const inter = Inter({ variable: "--font-inter", subsets: ["latin"] });
9
+ const inter = Inter({ variable: "--font-inter", subsets: ["latin"], display: "swap" });
11
10
 
12
11
  export const metadata: Metadata = createMetadata({
13
12
  title: "__PROJECT_NAME__",
@@ -18,13 +17,11 @@ export default function RootLayout({ children }: { children: React.ReactNode })
18
17
  return (
19
18
  <html lang="en" suppressHydrationWarning>
20
19
  <body className={`${inter.variable} font-sans antialiased`}>
21
- <ThemeProvider attribute="class" defaultTheme="light" enableSystem>
22
- <Providers>
23
- {/* __PROVIDERS_WRAP_START__ */}
24
- {children}
25
- {/* __PROVIDERS_WRAP_END__ */}
26
- </Providers>
27
- </ThemeProvider>
20
+ <Providers>
21
+ {/* __PROVIDERS_WRAP_START__ */}
22
+ {children}
23
+ {/* __PROVIDERS_WRAP_END__ */}
24
+ </Providers>
28
25
  <Toaster />
29
26
  </body>
30
27
  </html>
@@ -0,0 +1,15 @@
1
+ import type { MetadataRoute } from "next";
2
+
3
+ const BASE_URL = process.env.NEXT_PUBLIC_SITE_URL ?? "https://yourdomain.com";
4
+
5
+ // Add additional routes as your project grows
6
+ export default function sitemap(): MetadataRoute.Sitemap {
7
+ return [
8
+ {
9
+ url: BASE_URL,
10
+ lastModified: new Date(),
11
+ changeFrequency: "monthly",
12
+ priority: 1,
13
+ },
14
+ ];
15
+ }
@@ -2,30 +2,14 @@
2
2
  const config = {
3
3
  extends: ["@commitlint/config-conventional"],
4
4
  rules: {
5
- // Allowed commit types
6
- "type-enum": [
7
- 2,
8
- "always",
9
- [
10
- "feat", // new feature
11
- "fix", // bug fix
12
- "docs", // documentation only
13
- "style", // formatting, whitespace
14
- "refactor", // code change that's not feat or fix
15
- "perf", // performance improvement
16
- "test", // tests
17
- "build", // build system, deps
18
- "ci", // CI/CD
19
- "chore", // other changes
20
- "revert", // revert a commit
21
- ],
22
- ],
23
- // Subject line max length
5
+ // Downgrade type/subject requirements to warnings only
6
+ "type-empty": [1, "never"],
7
+ "subject-empty": [1, "never"],
8
+ "type-enum": [1, "always", ["feat", "fix", "docs", "style", "refactor", "perf", "test", "build", "ci", "chore", "revert"]],
9
+ // Only hard rules: keep messages readable
24
10
  "subject-max-length": [2, "always", 100],
25
- // No period at end of subject
26
11
  "subject-full-stop": [2, "never", "."],
27
- // Subject must start lowercase
28
- "subject-case": [2, "never", ["sentence-case", "start-case", "pascal-case", "upper-case"]],
12
+ "subject-case": [0],
29
13
  },
30
14
  };
31
15
 
@@ -1,39 +1,72 @@
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
 
9
17
  export default function NavbarMobile() {
10
- const [open, setOpen] = useState(false);
11
-
12
18
  return (
13
19
  <div className="md:hidden">
14
- <button onClick={() => setOpen(!open)} aria-label="Toggle menu">
15
- {open ? <X className="h-5 w-5" /> : <Menu className="h-5 w-5" />}
16
- </button>
20
+ <Sheet>
21
+ {/* Hamburger trigger */}
22
+ <SheetTrigger asChild>
23
+ <Button variant="ghost" size="icon" aria-label="Open menu">
24
+ <Menu className="h-5 w-5" />
25
+ </Button>
26
+ </SheetTrigger>
27
+
28
+ <SheetContent>
29
+ {/* Header: logo + close button */}
30
+ <SheetHeader className="flex-row items-center justify-between">
31
+ <SheetTitle className="text-base font-bold text-primary not-sr-only">
32
+ {SITE_NAME}
33
+ </SheetTitle>
34
+ <SheetClose asChild>
35
+ <Button variant="ghost" size="icon" aria-label="Close menu">
36
+ <X className="h-5 w-5" />
37
+ </Button>
38
+ </SheetClose>
39
+ </SheetHeader>
40
+
41
+ {/* Nav links — staggered fade-in */}
42
+ <nav className="flex flex-1 flex-col gap-1 px-2">
43
+ {NAV_LINKS.map((link, i) => (
44
+ <motion.div
45
+ key={link.href}
46
+ initial={{ opacity: 0, x: 16 }}
47
+ animate={{ opacity: 1, x: 0 }}
48
+ transition={{ delay: i * 0.07, duration: 0.25 }}
49
+ >
50
+ <SheetClose asChild>
51
+ <Link
52
+ href={link.href}
53
+ className="block rounded-md px-3 py-3 text-base font-medium text-foreground transition-colors hover:bg-accent hover:text-accent-foreground"
54
+ >
55
+ {link.label}
56
+ </Link>
57
+ </SheetClose>
58
+ </motion.div>
59
+ ))}
60
+ </nav>
17
61
 
18
- {/* Slide-down mobile menu */}
19
- <div
20
- className={cn(
21
- "absolute inset-x-0 top-16 flex flex-col gap-4 border-b bg-background p-6 transition-all duration-200",
22
- open ? "visible opacity-100" : "invisible pointer-events-none opacity-0"
23
- )}
24
- >
25
- {NAV_LINKS.map((link) => (
26
- <Link
27
- key={link.href}
28
- href={link.href}
29
- onClick={() => setOpen(false)}
30
- className="text-sm font-medium"
31
- >
32
- {link.label}
33
- </Link>
34
- ))}
35
- <Button className="w-full">Get started</Button>
36
- </div>
62
+ {/* CTA */}
63
+ <SheetFooter>
64
+ <SheetClose asChild>
65
+ <Button className="w-full">Get started</Button>
66
+ </SheetClose>
67
+ </SheetFooter>
68
+ </SheetContent>
69
+ </Sheet>
37
70
  </div>
38
71
  );
39
72
  }
@@ -9,12 +9,15 @@ import { cn } from "@/lib/utils";
9
9
 
10
10
  export default function Navbar() {
11
11
  const [isVisible, setIsVisible] = useState(true);
12
+ const [isScrolled, setIsScrolled] = useState(false);
12
13
  const lastScrollY = useRef(0);
13
14
 
14
15
  useEffect(() => {
15
16
  const controlNavbar = () => {
16
17
  const currentScrollY = window.scrollY;
17
18
 
19
+ setIsScrolled(currentScrollY > 10);
20
+
18
21
  if (currentScrollY < lastScrollY.current || currentScrollY < 10) {
19
22
  setIsVisible(true); // scrolling up OR near top → show
20
23
  } else {
@@ -31,8 +34,12 @@ export default function Navbar() {
31
34
  return (
32
35
  <header
33
36
  className={cn(
34
- "fixed top-0 z-50 w-full border-b border-border/40 bg-background/80 backdrop-blur-sm header-shadow transition-transform duration-300",
35
- isVisible ? "translate-y-0" : "-translate-y-[calc(100%+20px)]"
37
+ "fixed top-0 z-50 w-full transition-all duration-700 ease-in-out",
38
+ isVisible ? "translate-y-0" : "-translate-y-[calc(100%+20px)]",
39
+ // Transparent at top; apply blur + border when scrolled
40
+ isScrolled
41
+ ? "border-b border-border/40 bg-background/80 backdrop-blur-md shadow-sm"
42
+ : "border-b border-transparent bg-transparent",
36
43
  )}
37
44
  >
38
45
  <div className="content-container flex h-16 items-center justify-between">
@@ -0,0 +1,26 @@
1
+ "use client";
2
+ import * as React from "react";
3
+ import * as CheckboxPrimitive from "@radix-ui/react-checkbox";
4
+ import { CheckIcon } from "lucide-react";
5
+ import { cn } from "@/lib/utils";
6
+
7
+ const Checkbox = React.forwardRef<
8
+ React.ElementRef<typeof CheckboxPrimitive.Root>,
9
+ React.ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root>
10
+ >(({ className, ...props }, ref) => (
11
+ <CheckboxPrimitive.Root
12
+ ref={ref}
13
+ className={cn(
14
+ "peer h-4 w-4 shrink-0 rounded-sm border border-primary shadow focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground",
15
+ className,
16
+ )}
17
+ {...props}
18
+ >
19
+ <CheckboxPrimitive.Indicator className={cn("flex items-center justify-center text-current")}>
20
+ <CheckIcon className="h-4 w-4" />
21
+ </CheckboxPrimitive.Indicator>
22
+ </CheckboxPrimitive.Root>
23
+ ));
24
+ Checkbox.displayName = CheckboxPrimitive.Root.displayName;
25
+
26
+ export { Checkbox };
@@ -0,0 +1,21 @@
1
+ import * as React from "react";
2
+ import { cn } from "@/lib/utils";
3
+
4
+ export interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {}
5
+
6
+ const Input = React.forwardRef<HTMLInputElement, InputProps>(({ className, type, ...props }, ref) => {
7
+ return (
8
+ <input
9
+ type={type}
10
+ className={cn(
11
+ "flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground 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",
12
+ className,
13
+ )}
14
+ ref={ref}
15
+ {...props}
16
+ />
17
+ );
18
+ });
19
+ Input.displayName = "Input";
20
+
21
+ export { Input };
@@ -0,0 +1,36 @@
1
+ "use client";
2
+ import * as React from "react";
3
+ import * as RadioGroupPrimitive from "@radix-ui/react-radio-group";
4
+ import { CircleIcon } from "lucide-react";
5
+ import { cn } from "@/lib/utils";
6
+
7
+ const RadioGroup = React.forwardRef<
8
+ React.ElementRef<typeof RadioGroupPrimitive.Root>,
9
+ React.ComponentPropsWithoutRef<typeof RadioGroupPrimitive.Root>
10
+ >(({ className, ...props }, ref) => {
11
+ return <RadioGroupPrimitive.Root className={cn("grid gap-2", className)} {...props} ref={ref} />;
12
+ });
13
+ RadioGroup.displayName = RadioGroupPrimitive.Root.displayName;
14
+
15
+ const RadioGroupItem = React.forwardRef<
16
+ React.ElementRef<typeof RadioGroupPrimitive.Item>,
17
+ React.ComponentPropsWithoutRef<typeof RadioGroupPrimitive.Item>
18
+ >(({ className, ...props }, ref) => {
19
+ return (
20
+ <RadioGroupPrimitive.Item
21
+ ref={ref}
22
+ className={cn(
23
+ "aspect-square h-4 w-4 rounded-full border border-primary text-primary shadow focus:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50",
24
+ className,
25
+ )}
26
+ {...props}
27
+ >
28
+ <RadioGroupPrimitive.Indicator className="flex items-center justify-center">
29
+ <CircleIcon className="h-3.5 w-3.5 fill-primary" />
30
+ </RadioGroupPrimitive.Indicator>
31
+ </RadioGroupPrimitive.Item>
32
+ );
33
+ });
34
+ RadioGroupItem.displayName = RadioGroupPrimitive.Item.displayName;
35
+
36
+ export { RadioGroup, RadioGroupItem };