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.
- 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 +8 -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 +67 -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
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
|
|
17
|
-
if (isCancel(
|
|
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
|
-
|
|
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
|
-
|
|
36
|
-
|
|
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,8 +1,14 @@
|
|
|
1
1
|
# Site
|
|
2
2
|
NEXT_PUBLIC_SITE_URL=https://yourdomain.com
|
|
3
3
|
|
|
4
|
-
# Analytics (optional)
|
|
5
|
-
|
|
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 (
|
|
32
|
-
if [ "${
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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 "${
|
|
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.
|
|
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.
|
|
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
|
-
<
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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
|
-
//
|
|
6
|
-
"type-
|
|
7
|
-
|
|
8
|
-
|
|
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
|
-
|
|
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 {
|
|
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
|
|
|
9
17
|
export default function NavbarMobile() {
|
|
10
|
-
const [open, setOpen] = useState(false);
|
|
11
|
-
|
|
12
18
|
return (
|
|
13
19
|
<div className="md:hidden">
|
|
14
|
-
<
|
|
15
|
-
{
|
|
16
|
-
|
|
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
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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
|
|
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 };
|