create-landing-app 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +21 -0
- package/dist/install.js +18 -0
- package/dist/prompts.js +62 -0
- package/dist/scaffold.js +159 -0
- package/dist/utils/__tests__/merge-json.test.js +144 -0
- package/dist/utils/__tests__/replace-tokens.test.js +212 -0
- package/dist/utils/copy-dir.js +22 -0
- package/dist/utils/merge-json.js +19 -0
- package/dist/utils/replace-tokens.js +8 -0
- package/package.json +48 -0
- package/templates/nextjs/base/.env.example +8 -0
- package/templates/nextjs/base/.github/workflows/ci.yml +40 -0
- package/templates/nextjs/base/.husky/commit-msg +7 -0
- package/templates/nextjs/base/.husky/pre-commit +3 -0
- package/templates/nextjs/base/.husky/pre-push +46 -0
- package/templates/nextjs/base/.lighthouserc.json +28 -0
- package/templates/nextjs/base/.prettierignore +11 -0
- package/templates/nextjs/base/.prettierrc.json +10 -0
- package/templates/nextjs/base/Dockerfile +42 -0
- package/templates/nextjs/base/app/globals.css +82 -0
- package/templates/nextjs/base/app/layout.tsx +32 -0
- package/templates/nextjs/base/app/not-found.tsx +13 -0
- package/templates/nextjs/base/app/page.tsx +15 -0
- package/templates/nextjs/base/app/robots.ts +9 -0
- package/templates/nextjs/base/commitlint.config.mjs +32 -0
- package/templates/nextjs/base/components/navs/navbar-mobile.tsx +39 -0
- package/templates/nextjs/base/components/navs/navbar.tsx +39 -0
- package/templates/nextjs/base/components/providers.tsx +12 -0
- package/templates/nextjs/base/components/sections/features-section.tsx +78 -0
- package/templates/nextjs/base/components/sections/footer-section.tsx +98 -0
- package/templates/nextjs/base/components/sections/hero-section.tsx +74 -0
- package/templates/nextjs/base/components/ui/accordion.tsx +47 -0
- package/templates/nextjs/base/components/ui/button.tsx +44 -0
- package/templates/nextjs/base/components/ui/dialog.tsx +61 -0
- package/templates/nextjs/base/components/ui/dropdown-menu.tsx +55 -0
- package/templates/nextjs/base/components/ui/sonner.tsx +6 -0
- package/templates/nextjs/base/components.json +19 -0
- package/templates/nextjs/base/constants/common.ts +15 -0
- package/templates/nextjs/base/eslint.config.mjs +25 -0
- package/templates/nextjs/base/lib/metadata.ts +36 -0
- package/templates/nextjs/base/lib/utils.ts +7 -0
- package/templates/nextjs/base/next.config.ts +33 -0
- package/templates/nextjs/base/package.json +61 -0
- package/templates/nextjs/base/postcss.config.mjs +7 -0
- package/templates/nextjs/base/scripts/build-and-scan.sh +127 -0
- package/templates/nextjs/base/scripts/lighthouse-check.sh +86 -0
- package/templates/nextjs/base/styles/theme.css +63 -0
- package/templates/nextjs/base/tsconfig.json +21 -0
- package/templates/nextjs/base/types/index.ts +16 -0
- package/templates/nextjs/optional/docker/files/.dockerignore +6 -0
- package/templates/nextjs/optional/docker/files/Dockerfile +36 -0
- package/templates/nextjs/optional/docker/files/docker-compose.yml +9 -0
- package/templates/nextjs/optional/i18n-dict/files/app/[lang]/layout.tsx +19 -0
- package/templates/nextjs/optional/i18n-dict/files/app/[lang]/page.tsx +15 -0
- package/templates/nextjs/optional/i18n-dict/files/components/navs/language-switcher.tsx +39 -0
- package/templates/nextjs/optional/i18n-dict/files/components/navs/navbar-mobile.tsx +41 -0
- package/templates/nextjs/optional/i18n-dict/files/components/navs/navbar.tsx +41 -0
- package/templates/nextjs/optional/i18n-dict/files/components/providers.tsx +16 -0
- package/templates/nextjs/optional/i18n-dict/files/components/sections/features-section.tsx +80 -0
- package/templates/nextjs/optional/i18n-dict/files/components/sections/footer-section.tsx +98 -0
- package/templates/nextjs/optional/i18n-dict/files/dictionaries/en.json +21 -0
- package/templates/nextjs/optional/i18n-dict/files/dictionaries/vi.json +21 -0
- package/templates/nextjs/optional/i18n-dict/files/get-dictionary.ts +10 -0
- package/templates/nextjs/optional/i18n-dict/files/i18n-config.ts +6 -0
- package/templates/nextjs/optional/i18n-dict/files/lib/dict-context.tsx +23 -0
- package/templates/nextjs/optional/i18n-dict/files/middleware.ts +31 -0
- package/templates/nextjs/optional/i18n-dict/pkg.json +9 -0
- package/templates/nextjs/optional/sections/about/files/components/sections/about-section.tsx +36 -0
- package/templates/nextjs/optional/sections/about/inject/app__[lang]__page.tsx +5 -0
- package/templates/nextjs/optional/sections/about/inject/app__page.tsx +5 -0
- package/templates/nextjs/optional/sections/about/inject/constants__common.ts +2 -0
- package/templates/nextjs/optional/sections/blog/files/components/sections/blog-section.tsx +191 -0
- package/templates/nextjs/optional/sections/blog/inject/app__[lang]__page.tsx +5 -0
- package/templates/nextjs/optional/sections/blog/inject/app__page.tsx +5 -0
- package/templates/nextjs/optional/sections/blog/inject/constants__common.ts +2 -0
- package/templates/nextjs/optional/sections/contact/files/components/sections/contact-section.tsx +79 -0
- package/templates/nextjs/optional/sections/contact/inject/app__[lang]__page.tsx +5 -0
- package/templates/nextjs/optional/sections/contact/inject/app__page.tsx +5 -0
- package/templates/nextjs/optional/sections/contact/inject/constants__common.ts +2 -0
- package/templates/nextjs/optional/tanstack-query/files/lib/custom-fetch.ts +9 -0
- package/templates/nextjs/optional/tanstack-query/files/lib/query-client.ts +21 -0
- package/templates/nextjs/optional/tanstack-query/inject/components__providers.tsx +9 -0
- package/templates/nextjs/optional/tanstack-query/pkg.json +5 -0
- package/templates/nextjs/optional/zustand/files/store/ui-store.ts +16 -0
- package/templates/nextjs/optional/zustand/inject/components__providers.tsx +3 -0
- package/templates/nextjs/optional/zustand/pkg.json +5 -0
- package/templates/nextjs/themes/dark.css +36 -0
- package/templates/nextjs/themes/forest.css +58 -0
- package/templates/nextjs/themes/ocean.css +58 -0
- package/templates/nextjs/themes/pila.css +75 -0
- package/templates/nextjs/themes/purple.css +58 -0
package/package.json
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "create-landing-app",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Create a production-ready Next.js landing page with one command",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"create-landing-app": "./dist/index.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"dist",
|
|
11
|
+
"templates"
|
|
12
|
+
],
|
|
13
|
+
"scripts": {
|
|
14
|
+
"build": "tsc -p tsconfig.json",
|
|
15
|
+
"dev": "tsc -p tsconfig.json --watch",
|
|
16
|
+
"prepublishOnly": "yarn build && rm -rf ./templates && cp -r ../../templates ./templates",
|
|
17
|
+
"test": "node --experimental-vm-modules node_modules/.bin/jest"
|
|
18
|
+
},
|
|
19
|
+
"keywords": [
|
|
20
|
+
"nextjs",
|
|
21
|
+
"landing-page",
|
|
22
|
+
"boilerplate",
|
|
23
|
+
"scaffold",
|
|
24
|
+
"create-app",
|
|
25
|
+
"tailwindcss",
|
|
26
|
+
"typescript",
|
|
27
|
+
"starter"
|
|
28
|
+
],
|
|
29
|
+
"license": "MIT",
|
|
30
|
+
"repository": {
|
|
31
|
+
"type": "git",
|
|
32
|
+
"url": "https://github.com/LamHana/landing-page-boilerplate"
|
|
33
|
+
},
|
|
34
|
+
"homepage": "https://github.com/LamHana/landing-page-boilerplate#readme",
|
|
35
|
+
"dependencies": {
|
|
36
|
+
"@clack/prompts": "^0.9.0",
|
|
37
|
+
"kolorist": "^1.8.0",
|
|
38
|
+
"execa": "^9.0.0"
|
|
39
|
+
},
|
|
40
|
+
"devDependencies": {
|
|
41
|
+
"@types/jest": "^29.5.0",
|
|
42
|
+
"@types/node": "^20.0.0",
|
|
43
|
+
"jest": "^29.5.0",
|
|
44
|
+
"ts-jest": "^29.1.0",
|
|
45
|
+
"typescript": "^5.0.0"
|
|
46
|
+
},
|
|
47
|
+
"engines": { "node": ">=18.0.0" }
|
|
48
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main, dev]
|
|
6
|
+
pull_request:
|
|
7
|
+
branches: [main, dev]
|
|
8
|
+
|
|
9
|
+
jobs:
|
|
10
|
+
lint:
|
|
11
|
+
runs-on: ubuntu-latest
|
|
12
|
+
steps:
|
|
13
|
+
- uses: actions/checkout@v4
|
|
14
|
+
- uses: actions/setup-node@v4
|
|
15
|
+
with: { node-version: 20 }
|
|
16
|
+
- run: yarn install --frozen-lockfile
|
|
17
|
+
- run: yarn format:check
|
|
18
|
+
- run: yarn lint
|
|
19
|
+
|
|
20
|
+
lighthouse:
|
|
21
|
+
runs-on: ubuntu-latest
|
|
22
|
+
needs: lint
|
|
23
|
+
steps:
|
|
24
|
+
- uses: actions/checkout@v4
|
|
25
|
+
- uses: actions/setup-node@v4
|
|
26
|
+
with: { node-version: 20 }
|
|
27
|
+
- run: yarn install --frozen-lockfile
|
|
28
|
+
- run: yarn build
|
|
29
|
+
- run: yarn lighthouse
|
|
30
|
+
env:
|
|
31
|
+
LHCI_GITHUB_APP_TOKEN: ${{ secrets.LHCI_GITHUB_APP_TOKEN }}
|
|
32
|
+
|
|
33
|
+
security-scan:
|
|
34
|
+
runs-on: ubuntu-latest
|
|
35
|
+
needs: lint
|
|
36
|
+
steps:
|
|
37
|
+
- uses: actions/checkout@v4
|
|
38
|
+
- run: FAIL_ON_VULN=true bash ./scripts/build-and-scan.sh
|
|
39
|
+
env:
|
|
40
|
+
APP_NAME: __PROJECT_NAME__
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
#!/bin/sh
|
|
2
|
+
set -e
|
|
3
|
+
|
|
4
|
+
RED='\033[0;31m'
|
|
5
|
+
GREEN='\033[0;32m'
|
|
6
|
+
YELLOW='\033[1;33m'
|
|
7
|
+
NC='\033[0m'
|
|
8
|
+
|
|
9
|
+
echo ""
|
|
10
|
+
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
11
|
+
echo " Pre-push quality checks"
|
|
12
|
+
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
13
|
+
echo ""
|
|
14
|
+
|
|
15
|
+
# ── Step 1: Next.js build (always runs) ──────────────────
|
|
16
|
+
echo "${GREEN}[1/3] Building Next.js...${NC}"
|
|
17
|
+
npm run build
|
|
18
|
+
echo "${GREEN}✅ Build passed${NC}"
|
|
19
|
+
echo ""
|
|
20
|
+
|
|
21
|
+
# ── Step 2: Lighthouse CI (skip with SKIP_LIGHTHOUSE=true) ──
|
|
22
|
+
if [ "${SKIP_LIGHTHOUSE}" = "true" ]; then
|
|
23
|
+
echo "${YELLOW}[2/3] Lighthouse skipped (SKIP_LIGHTHOUSE=true)${NC}"
|
|
24
|
+
else
|
|
25
|
+
echo "${GREEN}[2/3] Running Lighthouse CI...${NC}"
|
|
26
|
+
bash ./scripts/lighthouse-check.sh
|
|
27
|
+
echo "${GREEN}✅ Lighthouse passed${NC}"
|
|
28
|
+
fi
|
|
29
|
+
echo ""
|
|
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
|
|
40
|
+
else
|
|
41
|
+
echo "${YELLOW}[3/3] Security scan skipped (set RUN_SCAN=true to enable)${NC}"
|
|
42
|
+
fi
|
|
43
|
+
|
|
44
|
+
echo ""
|
|
45
|
+
echo "${GREEN}✅ All pre-push checks passed. Pushing...${NC}"
|
|
46
|
+
echo ""
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
{
|
|
2
|
+
"ci": {
|
|
3
|
+
"collect": {
|
|
4
|
+
"url": ["http://localhost:3000"],
|
|
5
|
+
"numberOfRuns": 1,
|
|
6
|
+
"settings": {
|
|
7
|
+
"preset": "desktop",
|
|
8
|
+
"throttlingMethod": "simulate"
|
|
9
|
+
}
|
|
10
|
+
},
|
|
11
|
+
"assert": {
|
|
12
|
+
"assertions": {
|
|
13
|
+
"categories:performance": ["warn", { "minScore": 0.8 }],
|
|
14
|
+
"categories:accessibility": ["error", { "minScore": 0.9 }],
|
|
15
|
+
"categories:best-practices": ["warn", { "minScore": 0.9 }],
|
|
16
|
+
"categories:seo": ["warn", { "minScore": 0.9 }],
|
|
17
|
+
"first-contentful-paint": ["warn", { "maxNumericValue": 2000 }],
|
|
18
|
+
"largest-contentful-paint": ["error", { "maxNumericValue": 3000 }],
|
|
19
|
+
"total-blocking-time": ["warn", { "maxNumericValue": 300 }],
|
|
20
|
+
"cumulative-layout-shift": ["error", { "maxNumericValue": 0.1 }],
|
|
21
|
+
"interactive": ["warn", { "maxNumericValue": 4000 }]
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
"upload": {
|
|
25
|
+
"target": "temporary-public-storage"
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# Stage 1: Install dependencies
|
|
2
|
+
FROM node:20-alpine AS deps
|
|
3
|
+
RUN corepack enable
|
|
4
|
+
WORKDIR /app
|
|
5
|
+
COPY package.json yarn.lock* package-lock.json* pnpm-lock.yaml* bun.lock* bun.lockb* ./
|
|
6
|
+
RUN \
|
|
7
|
+
if [ -f yarn.lock ]; then yarn --frozen-lockfile; \
|
|
8
|
+
elif [ -f package-lock.json ]; then npm ci; \
|
|
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; \
|
|
11
|
+
else echo "No lockfile found." && exit 1; \
|
|
12
|
+
fi
|
|
13
|
+
|
|
14
|
+
# Stage 2: Build
|
|
15
|
+
FROM node:20-alpine AS builder
|
|
16
|
+
WORKDIR /app
|
|
17
|
+
COPY --from=deps /app/node_modules ./node_modules
|
|
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
|
|
26
|
+
|
|
27
|
+
# Stage 3: Production runner (standalone output)
|
|
28
|
+
FROM node:20-alpine AS runner
|
|
29
|
+
WORKDIR /app
|
|
30
|
+
ENV NODE_ENV=production
|
|
31
|
+
ENV PORT=3000
|
|
32
|
+
|
|
33
|
+
# Run as non-root user for security (OWASP A05)
|
|
34
|
+
RUN addgroup --system --gid 1001 nodejs && adduser --system --uid 1001 nextjs
|
|
35
|
+
USER nextjs
|
|
36
|
+
|
|
37
|
+
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
|
|
38
|
+
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
|
|
39
|
+
COPY --from=builder --chown=nextjs:nodejs /app/public ./public
|
|
40
|
+
|
|
41
|
+
EXPOSE 3000
|
|
42
|
+
CMD ["node", "server.js"]
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
@import "tailwindcss";
|
|
2
|
+
@import "tw-animate-css";
|
|
3
|
+
@import "../styles/theme.css"; /* theme variables — edit this file to rebrand */
|
|
4
|
+
|
|
5
|
+
@custom-variant dark (&:is(.dark *));
|
|
6
|
+
|
|
7
|
+
/* Map Tailwind v4 color tokens to CSS variables */
|
|
8
|
+
@theme inline {
|
|
9
|
+
--color-background: var(--background);
|
|
10
|
+
--color-foreground: var(--foreground);
|
|
11
|
+
--color-primary: var(--primary);
|
|
12
|
+
--color-primary-foreground: var(--primary-foreground);
|
|
13
|
+
--color-secondary: var(--secondary);
|
|
14
|
+
--color-secondary-foreground: var(--secondary-foreground);
|
|
15
|
+
--color-muted: var(--muted);
|
|
16
|
+
--color-muted-foreground: var(--muted-foreground);
|
|
17
|
+
--color-accent: var(--accent);
|
|
18
|
+
--color-accent-foreground: var(--accent-foreground);
|
|
19
|
+
--color-destructive: var(--destructive);
|
|
20
|
+
--color-border: var(--border);
|
|
21
|
+
--color-input: var(--input);
|
|
22
|
+
--color-ring: var(--ring);
|
|
23
|
+
--color-card: var(--card);
|
|
24
|
+
--color-card-foreground: var(--card-foreground);
|
|
25
|
+
--color-popover: var(--popover);
|
|
26
|
+
--color-popover-foreground: var(--popover-foreground);
|
|
27
|
+
--radius-sm: calc(var(--radius) - 4px);
|
|
28
|
+
--radius-md: calc(var(--radius) - 2px);
|
|
29
|
+
--radius-lg: var(--radius);
|
|
30
|
+
--radius-xl: calc(var(--radius) + 4px);
|
|
31
|
+
--breakpoint-tablet: 1024px;
|
|
32
|
+
--breakpoint-mobile: 768px;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
@layer base {
|
|
36
|
+
* { @apply border-border outline-ring/50; }
|
|
37
|
+
body { @apply bg-background text-foreground; }
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/* Hide scrollbar */
|
|
41
|
+
::-webkit-scrollbar { display: none; }
|
|
42
|
+
html { -ms-overflow-style: none; scrollbar-width: none; }
|
|
43
|
+
body { overflow-x: hidden; }
|
|
44
|
+
|
|
45
|
+
/* Layout container — use this on all sections for consistent max-width */
|
|
46
|
+
.content-container {
|
|
47
|
+
max-width: 1440px;
|
|
48
|
+
margin-left: auto;
|
|
49
|
+
margin-right: auto;
|
|
50
|
+
padding-left: 20px;
|
|
51
|
+
padding-right: 20px;
|
|
52
|
+
}
|
|
53
|
+
@media (max-width: 1024px) {
|
|
54
|
+
.content-container { padding-left: 40px !important; padding-right: 40px !important; }
|
|
55
|
+
}
|
|
56
|
+
@media (max-width: 768px) {
|
|
57
|
+
.content-container { padding-left: 20px !important; padding-right: 20px !important; }
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/* Brand utilities — sourced from theme.css vars */
|
|
61
|
+
.btn-primary {
|
|
62
|
+
background-color: var(--brand-action);
|
|
63
|
+
border-radius: 8px;
|
|
64
|
+
box-shadow: 0px 1px 2px rgba(10, 13, 18, 0.05);
|
|
65
|
+
}
|
|
66
|
+
.btn-outline {
|
|
67
|
+
border: 1px solid var(--brand-border);
|
|
68
|
+
background: var(--background);
|
|
69
|
+
border-radius: var(--radius-md);
|
|
70
|
+
}
|
|
71
|
+
.bg-footer {
|
|
72
|
+
background: linear-gradient(279deg, var(--brand-footer-from) 1.61%, var(--brand-footer-to) 100.79%);
|
|
73
|
+
}
|
|
74
|
+
.text-gradient-brand {
|
|
75
|
+
background: linear-gradient(90deg, var(--brand-gradient-from), var(--brand-gradient-to));
|
|
76
|
+
background-clip: text;
|
|
77
|
+
-webkit-background-clip: text;
|
|
78
|
+
-webkit-text-fill-color: transparent;
|
|
79
|
+
}
|
|
80
|
+
.header-shadow {
|
|
81
|
+
box-shadow: 0 0px 24px 0px rgba(0, 0, 0, 0.1), 0 4px 6px -4px rgba(0, 0, 0, 0.1);
|
|
82
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { Metadata } from "next";
|
|
2
|
+
import { Inter } from "next/font/google";
|
|
3
|
+
import { ThemeProvider } from "next-themes";
|
|
4
|
+
import { Toaster } from "@/components/ui/sonner";
|
|
5
|
+
import Providers from "@/components/providers";
|
|
6
|
+
import { createMetadata } from "@/lib/metadata";
|
|
7
|
+
import "./globals.css";
|
|
8
|
+
// __PROVIDERS_IMPORT__
|
|
9
|
+
|
|
10
|
+
const inter = Inter({ variable: "--font-inter", subsets: ["latin"] });
|
|
11
|
+
|
|
12
|
+
export const metadata: Metadata = createMetadata({
|
|
13
|
+
title: "__PROJECT_NAME__",
|
|
14
|
+
description: "A modern landing page",
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
export default function RootLayout({ children }: { children: React.ReactNode }) {
|
|
18
|
+
return (
|
|
19
|
+
<html lang="en" suppressHydrationWarning>
|
|
20
|
+
<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>
|
|
28
|
+
<Toaster />
|
|
29
|
+
</body>
|
|
30
|
+
</html>
|
|
31
|
+
);
|
|
32
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import Link from "next/link";
|
|
2
|
+
|
|
3
|
+
export default function NotFound() {
|
|
4
|
+
return (
|
|
5
|
+
<div className="flex min-h-screen flex-col items-center justify-center gap-4">
|
|
6
|
+
<h1 className="text-6xl font-bold text-primary">404</h1>
|
|
7
|
+
<p className="text-muted-foreground">Page not found</p>
|
|
8
|
+
<Link href="/" className="btn-primary px-6 py-2 text-white rounded-lg">
|
|
9
|
+
Back to home
|
|
10
|
+
</Link>
|
|
11
|
+
</div>
|
|
12
|
+
);
|
|
13
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import HeroSection from "@/components/sections/hero-section";
|
|
2
|
+
import FeaturesSection from "@/components/sections/features-section";
|
|
3
|
+
import FooterSection from "@/components/sections/footer-section";
|
|
4
|
+
// __PAGE_IMPORTS__
|
|
5
|
+
|
|
6
|
+
export default function Home() {
|
|
7
|
+
return (
|
|
8
|
+
<main>
|
|
9
|
+
<HeroSection />
|
|
10
|
+
<FeaturesSection />
|
|
11
|
+
{/* __PAGE_SECTIONS__ */}
|
|
12
|
+
<FooterSection />
|
|
13
|
+
</main>
|
|
14
|
+
);
|
|
15
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { MetadataRoute } from "next";
|
|
2
|
+
|
|
3
|
+
export default function robots(): MetadataRoute.Robots {
|
|
4
|
+
const baseUrl = process.env.NEXT_PUBLIC_SITE_URL ?? "https://yourdomain.com";
|
|
5
|
+
return {
|
|
6
|
+
rules: { userAgent: "*", allow: "/" },
|
|
7
|
+
sitemap: `${baseUrl}/sitemap.xml`,
|
|
8
|
+
};
|
|
9
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/** @type {import('@commitlint/types').UserConfig} */
|
|
2
|
+
const config = {
|
|
3
|
+
extends: ["@commitlint/config-conventional"],
|
|
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
|
|
24
|
+
"subject-max-length": [2, "always", 100],
|
|
25
|
+
// No period at end of subject
|
|
26
|
+
"subject-full-stop": [2, "never", "."],
|
|
27
|
+
// Subject must start lowercase
|
|
28
|
+
"subject-case": [2, "never", ["sentence-case", "start-case", "pascal-case", "upper-case"]],
|
|
29
|
+
},
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
export default config;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { useState } from "react";
|
|
3
|
+
import Link from "next/link";
|
|
4
|
+
import { Menu, X } from "lucide-react";
|
|
5
|
+
import { NAV_LINKS } from "@/constants/common";
|
|
6
|
+
import { Button } from "@/components/ui/button";
|
|
7
|
+
import { cn } from "@/lib/utils";
|
|
8
|
+
|
|
9
|
+
export default function NavbarMobile() {
|
|
10
|
+
const [open, setOpen] = useState(false);
|
|
11
|
+
|
|
12
|
+
return (
|
|
13
|
+
<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>
|
|
17
|
+
|
|
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>
|
|
37
|
+
</div>
|
|
38
|
+
);
|
|
39
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import Link from "next/link";
|
|
2
|
+
import { NAV_LINKS, SITE_NAME } from "@/constants/common";
|
|
3
|
+
import NavbarMobile from "./navbar-mobile";
|
|
4
|
+
import { Button } from "@/components/ui/button";
|
|
5
|
+
// __NAVBAR_IMPORT__
|
|
6
|
+
|
|
7
|
+
// Server component — no animation needed at nav level
|
|
8
|
+
export default function Navbar() {
|
|
9
|
+
return (
|
|
10
|
+
<header className="sticky top-0 z-50 w-full border-b border-border/40 bg-background/80 backdrop-blur-sm header-shadow">
|
|
11
|
+
<div className="content-container flex h-16 items-center justify-between">
|
|
12
|
+
{/* Logo */}
|
|
13
|
+
<Link href="/" className="flex items-center gap-2 text-xl font-bold text-primary">
|
|
14
|
+
{SITE_NAME}
|
|
15
|
+
</Link>
|
|
16
|
+
|
|
17
|
+
{/* Desktop nav */}
|
|
18
|
+
<nav className="hidden items-center gap-6 md:flex">
|
|
19
|
+
{NAV_LINKS.map((link) => (
|
|
20
|
+
<Link
|
|
21
|
+
key={link.href}
|
|
22
|
+
href={link.href}
|
|
23
|
+
className="text-sm font-medium text-muted-foreground transition-colors hover:text-foreground"
|
|
24
|
+
>
|
|
25
|
+
{link.label}
|
|
26
|
+
</Link>
|
|
27
|
+
))}
|
|
28
|
+
</nav>
|
|
29
|
+
|
|
30
|
+
{/* CTA + mobile toggle */}
|
|
31
|
+
<div className="flex items-center gap-3">
|
|
32
|
+
{/* __NAVBAR_EXTRAS__ */}
|
|
33
|
+
<Button className="hidden md:flex">Get started</Button>
|
|
34
|
+
<NavbarMobile />
|
|
35
|
+
</div>
|
|
36
|
+
</div>
|
|
37
|
+
</header>
|
|
38
|
+
);
|
|
39
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { motion } from "motion/react";
|
|
3
|
+
import { Zap, Shield, Palette, Globe, Code, Rocket } from "lucide-react";
|
|
4
|
+
|
|
5
|
+
const FEATURES = [
|
|
6
|
+
{
|
|
7
|
+
icon: Zap,
|
|
8
|
+
title: "Lightning fast",
|
|
9
|
+
description: "Built on Next.js 15 with Turbopack. Dev server starts in milliseconds.",
|
|
10
|
+
},
|
|
11
|
+
{
|
|
12
|
+
icon: Shield,
|
|
13
|
+
title: "Secure by default",
|
|
14
|
+
description: "Best-practice security headers, no inline scripts, CSP-ready.",
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
icon: Palette,
|
|
18
|
+
title: "Fully themeable",
|
|
19
|
+
description: "5 beautiful presets. One CSS file to rebrand completely.",
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
icon: Globe,
|
|
23
|
+
title: "i18n ready",
|
|
24
|
+
description: "Optional dictionary-based translation. Zero extra dependencies.",
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
icon: Code,
|
|
28
|
+
title: "Clean code",
|
|
29
|
+
description: "TypeScript, ESLint, component-first architecture. Easy to extend.",
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
icon: Rocket,
|
|
33
|
+
title: "Deploy anywhere",
|
|
34
|
+
description: "Docker-ready standalone output. Ships to any VPS in minutes.",
|
|
35
|
+
},
|
|
36
|
+
];
|
|
37
|
+
|
|
38
|
+
export default function FeaturesSection() {
|
|
39
|
+
return (
|
|
40
|
+
<section id="features" className="bg-secondary/30 py-24">
|
|
41
|
+
<div className="content-container">
|
|
42
|
+
{/* Header */}
|
|
43
|
+
<motion.div
|
|
44
|
+
initial={{ opacity: 0, y: 30 }}
|
|
45
|
+
whileInView={{ opacity: 1, y: 0 }}
|
|
46
|
+
viewport={{ once: true }}
|
|
47
|
+
transition={{ duration: 0.6 }}
|
|
48
|
+
className="mb-16 space-y-4 text-center"
|
|
49
|
+
>
|
|
50
|
+
<h2 className="text-4xl font-bold">Everything you need</h2>
|
|
51
|
+
<p className="mx-auto max-w-2xl text-lg text-muted-foreground">
|
|
52
|
+
Built with the best tools in the ecosystem. No bloat, just what matters.
|
|
53
|
+
</p>
|
|
54
|
+
</motion.div>
|
|
55
|
+
|
|
56
|
+
{/* Feature grid */}
|
|
57
|
+
<div className="grid gap-8 sm:grid-cols-2 lg:grid-cols-3">
|
|
58
|
+
{FEATURES.map((feature, i) => (
|
|
59
|
+
<motion.div
|
|
60
|
+
key={feature.title}
|
|
61
|
+
initial={{ opacity: 0, y: 30 }}
|
|
62
|
+
whileInView={{ opacity: 1, y: 0 }}
|
|
63
|
+
viewport={{ once: true }}
|
|
64
|
+
transition={{ duration: 0.5, delay: i * 0.08 }}
|
|
65
|
+
className="group rounded-2xl border border-border bg-background p-6 transition-all duration-300 hover:border-primary/40 hover:shadow-md"
|
|
66
|
+
>
|
|
67
|
+
<div className="mb-4 flex h-12 w-12 items-center justify-center rounded-xl bg-accent transition-colors group-hover:bg-primary/10">
|
|
68
|
+
<feature.icon className="h-6 w-6 text-primary" />
|
|
69
|
+
</div>
|
|
70
|
+
<h3 className="mb-2 text-lg font-semibold">{feature.title}</h3>
|
|
71
|
+
<p className="text-sm leading-relaxed text-muted-foreground">{feature.description}</p>
|
|
72
|
+
</motion.div>
|
|
73
|
+
))}
|
|
74
|
+
</div>
|
|
75
|
+
</div>
|
|
76
|
+
</section>
|
|
77
|
+
);
|
|
78
|
+
}
|