Add interactive demo components and feature sections to homepage
- Introduced `DemoSection`, showcasing live feature demos with interactive cards and test credentials for admin and auth flows. - Added `FeatureGrid` with dynamic animations, highlighting major application features like RBAC, documentation, and deployment readiness. - Built reusable `FeatureCard` for feature details, including icons, descriptions, and CTAs. - Implemented `TechStackSection` to display modern tools and technologies used in the stack with tooltips. - Updated dependencies: added `framer-motion`, `lucide-react`, and `react-syntax-highlighter`.
This commit is contained in:
185
frontend/package-lock.json
generated
185
frontend/package-lock.json
generated
@@ -22,10 +22,12 @@
|
||||
"@radix-ui/react-slot": "^1.2.4",
|
||||
"@radix-ui/react-tabs": "^1.1.13",
|
||||
"@tanstack/react-query": "^5.90.5",
|
||||
"@types/react-syntax-highlighter": "^15.5.13",
|
||||
"axios": "^1.13.1",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"date-fns": "^4.1.0",
|
||||
"framer-motion": "^12.23.24",
|
||||
"gray-matter": "^4.0.3",
|
||||
"lucide-react": "^0.552.0",
|
||||
"next": "^15.5.6",
|
||||
@@ -34,6 +36,7 @@
|
||||
"react-dom": "^19.0.0",
|
||||
"react-hook-form": "^7.66.0",
|
||||
"react-markdown": "^10.1.0",
|
||||
"react-syntax-highlighter": "^16.1.0",
|
||||
"recharts": "^2.15.4",
|
||||
"rehype-autolink-headings": "^7.1.0",
|
||||
"rehype-highlight": "^7.0.2",
|
||||
@@ -5185,6 +5188,12 @@
|
||||
"@types/pg": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/prismjs": {
|
||||
"version": "1.26.5",
|
||||
"resolved": "https://registry.npmjs.org/@types/prismjs/-/prismjs-1.26.5.tgz",
|
||||
"integrity": "sha512-AUZTa7hQ2KY5L7AmtSiqxlhWxb4ina0yd8hNbl4TWuqnv/pFP0nDMb3YrfSBf4hJVGLh2YEIBfKaBW/9UEl6IQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/react": {
|
||||
"version": "19.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.2.tgz",
|
||||
@@ -5204,6 +5213,15 @@
|
||||
"@types/react": "^19.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/react-syntax-highlighter": {
|
||||
"version": "15.5.13",
|
||||
"resolved": "https://registry.npmjs.org/@types/react-syntax-highlighter/-/react-syntax-highlighter-15.5.13.tgz",
|
||||
"integrity": "sha512-uLGJ87j6Sz8UaBAooU0T6lWJ0dBmjZgN1PZTrj05TNql2/XpC6+4HhMT5syIdFUUt+FASfCeLLv4kBygNU+8qA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/react": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/shimmer": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/shimmer/-/shimmer-1.2.0.tgz",
|
||||
@@ -8757,6 +8775,19 @@
|
||||
"reusify": "^1.0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/fault": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/fault/-/fault-1.0.4.tgz",
|
||||
"integrity": "sha512-CJ0HCB5tL5fYTEA7ToAq5+kTwd++Borf1/bifxd9iT70QcXr4MRrO3Llf8Ifs70q+SJcGHFtnIE/Nw6giCtECA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"format": "^0.2.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/wooorm"
|
||||
}
|
||||
},
|
||||
"node_modules/fb-watchman": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz",
|
||||
@@ -8910,6 +8941,14 @@
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/format": {
|
||||
"version": "0.2.2",
|
||||
"resolved": "https://registry.npmjs.org/format/-/format-0.2.2.tgz",
|
||||
"integrity": "sha512-wzsgA6WOq+09wrU1tsJ09udeR/YZRaeArL9e1wPbFg3GG2yDnC2ldKpxs4xunpFF9DgqCqOIra3bc1HWrJ37Ww==",
|
||||
"engines": {
|
||||
"node": ">=0.4.x"
|
||||
}
|
||||
},
|
||||
"node_modules/forwarded-parse": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/forwarded-parse/-/forwarded-parse-2.1.2.tgz",
|
||||
@@ -8917,6 +8956,33 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/framer-motion": {
|
||||
"version": "12.23.24",
|
||||
"resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.23.24.tgz",
|
||||
"integrity": "sha512-HMi5HRoRCTou+3fb3h9oTLyJGBxHfW+HnNE25tAXOvVx/IvwMHK0cx7IR4a2ZU6sh3IX1Z+4ts32PcYBOqka8w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"motion-dom": "^12.23.23",
|
||||
"motion-utils": "^12.23.6",
|
||||
"tslib": "^2.4.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@emotion/is-prop-valid": "*",
|
||||
"react": "^18.0.0 || ^19.0.0",
|
||||
"react-dom": "^18.0.0 || ^19.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@emotion/is-prop-valid": {
|
||||
"optional": true
|
||||
},
|
||||
"react": {
|
||||
"optional": true
|
||||
},
|
||||
"react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/fs.realpath": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
|
||||
@@ -9456,6 +9522,19 @@
|
||||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
},
|
||||
"node_modules/hast-util-parse-selector": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/hast-util-parse-selector/-/hast-util-parse-selector-4.0.0.tgz",
|
||||
"integrity": "sha512-wkQCkSYoOGCRKERFWcxMVMOcYE2K1AaNLU8DXS9arxnLOUEWbOXKXiJUNzEpqZ3JOKpnha3jkFrumEjVliDe7A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/hast": "^3.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
},
|
||||
"node_modules/hast-util-to-jsx-runtime": {
|
||||
"version": "2.3.6",
|
||||
"resolved": "https://registry.npmjs.org/hast-util-to-jsx-runtime/-/hast-util-to-jsx-runtime-2.3.6.tgz",
|
||||
@@ -9525,6 +9604,23 @@
|
||||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
},
|
||||
"node_modules/hastscript": {
|
||||
"version": "9.0.1",
|
||||
"resolved": "https://registry.npmjs.org/hastscript/-/hastscript-9.0.1.tgz",
|
||||
"integrity": "sha512-g7df9rMFX/SPi34tyGCyUBREQoKkapwdY/T04Qn9TDWfHhAYt4/I0gMVirzK5wEzeUqIjEB+LXC/ypb7Aqno5w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/hast": "^3.0.0",
|
||||
"comma-separated-tokens": "^2.0.0",
|
||||
"hast-util-parse-selector": "^4.0.0",
|
||||
"property-information": "^7.0.0",
|
||||
"space-separated-tokens": "^2.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
},
|
||||
"node_modules/highlight.js": {
|
||||
"version": "11.11.1",
|
||||
"resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-11.11.1.tgz",
|
||||
@@ -9534,6 +9630,12 @@
|
||||
"node": ">=12.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/highlightjs-vue": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/highlightjs-vue/-/highlightjs-vue-1.0.0.tgz",
|
||||
"integrity": "sha512-PDEfEF102G23vHmPhLyPboFCD+BkMGu+GuJe2d9/eH4FsCwvgBpnc9n0pGE+ffKdph38s6foEZiEjdgHdzp+IA==",
|
||||
"license": "CC0-1.0"
|
||||
},
|
||||
"node_modules/html-encoding-sniffer": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-4.0.0.tgz",
|
||||
@@ -13199,6 +13301,21 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/motion-dom": {
|
||||
"version": "12.23.23",
|
||||
"resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.23.23.tgz",
|
||||
"integrity": "sha512-n5yolOs0TQQBRUFImrRfs/+6X4p3Q4n1dUEqt/H58Vx7OW6RF+foWEgmTVDhIWJIMXOuNNL0apKH2S16en9eiA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"motion-utils": "^12.23.6"
|
||||
}
|
||||
},
|
||||
"node_modules/motion-utils": {
|
||||
"version": "12.23.6",
|
||||
"resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-12.23.6.tgz",
|
||||
"integrity": "sha512-eAWoPgr4eFEOFfg2WjIsMoqJTW6Z8MTUCgn/GZ3VRpClWBdnbjryiA3ZSNLyxCTmCQx4RmYX6jX1iWHbenUPNQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/mrmime": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.1.tgz",
|
||||
@@ -14216,6 +14333,15 @@
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/prismjs": {
|
||||
"version": "1.30.0",
|
||||
"resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.30.0.tgz",
|
||||
"integrity": "sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/progress": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz",
|
||||
@@ -14553,6 +14679,49 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/react-syntax-highlighter": {
|
||||
"version": "16.1.0",
|
||||
"resolved": "https://registry.npmjs.org/react-syntax-highlighter/-/react-syntax-highlighter-16.1.0.tgz",
|
||||
"integrity": "sha512-E40/hBiP5rCNwkeBN1vRP+xow1X0pndinO+z3h7HLsHyjztbyjfzNWNKuAsJj+7DLam9iT4AaaOZnueCU+Nplg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.28.4",
|
||||
"highlight.js": "^10.4.1",
|
||||
"highlightjs-vue": "^1.0.0",
|
||||
"lowlight": "^1.17.0",
|
||||
"prismjs": "^1.30.0",
|
||||
"refractor": "^5.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 16.20.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">= 0.14.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-syntax-highlighter/node_modules/highlight.js": {
|
||||
"version": "10.7.3",
|
||||
"resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-10.7.3.tgz",
|
||||
"integrity": "sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A==",
|
||||
"license": "BSD-3-Clause",
|
||||
"engines": {
|
||||
"node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/react-syntax-highlighter/node_modules/lowlight": {
|
||||
"version": "1.20.0",
|
||||
"resolved": "https://registry.npmjs.org/lowlight/-/lowlight-1.20.0.tgz",
|
||||
"integrity": "sha512-8Ktj+prEb1RoCPkEOrPMYUN/nCggB7qAWe3a7OpMjWQkh3l2RD5wKRQ+o8Q8YuI9RG/xs95waaI/E6ym/7NsTw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"fault": "^1.0.0",
|
||||
"highlight.js": "~10.7.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/wooorm"
|
||||
}
|
||||
},
|
||||
"node_modules/react-transition-group": {
|
||||
"version": "4.4.5",
|
||||
"resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz",
|
||||
@@ -14658,6 +14827,22 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/refractor": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/refractor/-/refractor-5.0.0.tgz",
|
||||
"integrity": "sha512-QXOrHQF5jOpjjLfiNk5GFnWhRXvxjUVnlFxkeDmewR5sXkr3iM46Zo+CnRR8B+MDVqkULW4EcLVcRBNOPXHosw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/hast": "^3.0.0",
|
||||
"@types/prismjs": "^1.0.0",
|
||||
"hastscript": "^9.0.0",
|
||||
"parse-entities": "^4.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/wooorm"
|
||||
}
|
||||
},
|
||||
"node_modules/regexp.prototype.flags": {
|
||||
"version": "1.5.4",
|
||||
"resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz",
|
||||
|
||||
@@ -35,10 +35,12 @@
|
||||
"@radix-ui/react-slot": "^1.2.4",
|
||||
"@radix-ui/react-tabs": "^1.1.13",
|
||||
"@tanstack/react-query": "^5.90.5",
|
||||
"@types/react-syntax-highlighter": "^15.5.13",
|
||||
"axios": "^1.13.1",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"date-fns": "^4.1.0",
|
||||
"framer-motion": "^12.23.24",
|
||||
"gray-matter": "^4.0.3",
|
||||
"lucide-react": "^0.552.0",
|
||||
"next": "^15.5.6",
|
||||
@@ -47,6 +49,7 @@
|
||||
"react-dom": "^19.0.0",
|
||||
"react-hook-form": "^7.66.0",
|
||||
"react-markdown": "^10.1.0",
|
||||
"react-syntax-highlighter": "^16.1.0",
|
||||
"recharts": "^2.15.4",
|
||||
"rehype-autolink-headings": "^7.1.0",
|
||||
"rehype-highlight": "^7.0.2",
|
||||
|
||||
@@ -1,100 +1,95 @@
|
||||
import Image from "next/image";
|
||||
/**
|
||||
* Homepage / Landing Page
|
||||
* Main landing page for the FastNext Template project
|
||||
* Showcases features, tech stack, and provides demos for developers
|
||||
*/
|
||||
|
||||
import { Header } from '@/components/home/Header';
|
||||
import { HeroSection } from '@/components/home/HeroSection';
|
||||
import { ContextSection } from '@/components/home/ContextSection';
|
||||
import { AnimatedTerminal } from '@/components/home/AnimatedTerminal';
|
||||
import { FeatureGrid } from '@/components/home/FeatureGrid';
|
||||
import { DemoSection } from '@/components/home/DemoSection';
|
||||
import { StatsSection } from '@/components/home/StatsSection';
|
||||
import { TechStackSection } from '@/components/home/TechStackSection';
|
||||
import { PhilosophySection } from '@/components/home/PhilosophySection';
|
||||
import { QuickStartCode } from '@/components/home/QuickStartCode';
|
||||
import { CTASection } from '@/components/home/CTASection';
|
||||
|
||||
export default function Home() {
|
||||
return (
|
||||
<div className="grid grid-rows-[20px_1fr_20px] items-center justify-items-center min-h-screen p-8 pb-20 gap-16 sm:p-20 font-[family-name:var(--font-geist-sans)]">
|
||||
<main className="flex flex-col gap-8 row-start-2 items-center sm:items-start">
|
||||
<Image
|
||||
className="dark:invert"
|
||||
src="/next.svg"
|
||||
alt="Next.js logo"
|
||||
width={180}
|
||||
height={38}
|
||||
priority
|
||||
/>
|
||||
<ol className="list-inside list-decimal text-sm text-center sm:text-left font-[family-name:var(--font-geist-mono)]">
|
||||
<li className="mb-2">
|
||||
Get started by editing{" "}
|
||||
<code className="bg-black/[.05] dark:bg-white/[.06] px-1 py-0.5 rounded font-semibold">
|
||||
src/app/page.tsx
|
||||
</code>
|
||||
.
|
||||
</li>
|
||||
<li>Save and see your changes instantly.</li>
|
||||
</ol>
|
||||
<div className="min-h-screen">
|
||||
{/* Header Navigation */}
|
||||
<Header />
|
||||
|
||||
<div className="flex gap-4 items-center flex-col sm:flex-row">
|
||||
{/* Main Content */}
|
||||
<main>
|
||||
{/* Hero Section with CTAs */}
|
||||
<HeroSection />
|
||||
|
||||
{/* What is this template? */}
|
||||
<ContextSection />
|
||||
|
||||
{/* Animated Terminal with Quick Start */}
|
||||
<AnimatedTerminal />
|
||||
|
||||
{/* 6 Feature Cards Grid */}
|
||||
<FeatureGrid />
|
||||
|
||||
{/* Interactive Demo Cards */}
|
||||
<DemoSection />
|
||||
|
||||
{/* Statistics with Animated Counters */}
|
||||
<StatsSection />
|
||||
|
||||
{/* Tech Stack Grid */}
|
||||
<TechStackSection />
|
||||
|
||||
{/* For Developers, By Developers */}
|
||||
<PhilosophySection />
|
||||
|
||||
{/* Quick Start Code Block */}
|
||||
<QuickStartCode />
|
||||
|
||||
{/* Final CTA Section */}
|
||||
<CTASection />
|
||||
</main>
|
||||
|
||||
{/* Footer */}
|
||||
<footer className="border-t bg-muted/30">
|
||||
<div className="container mx-auto px-6 py-8">
|
||||
<div className="flex flex-col md:flex-row items-center justify-between gap-4">
|
||||
<div className="text-sm text-muted-foreground">
|
||||
© {new Date().getFullYear()} FastNext Template. MIT Licensed.
|
||||
</div>
|
||||
<div className="flex items-center gap-6 text-sm text-muted-foreground">
|
||||
<a
|
||||
className="rounded-full border border-solid border-transparent transition-colors flex items-center justify-center bg-foreground text-background gap-2 hover:bg-[#383838] dark:hover:bg-[#ccc] text-sm sm:text-base h-10 sm:h-12 px-4 sm:px-5"
|
||||
href="https://vercel.com/new?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
||||
href="https://github.com/your-org/fast-next-template"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="hover:text-foreground transition-colors"
|
||||
>
|
||||
<Image
|
||||
className="dark:invert"
|
||||
src="/vercel.svg"
|
||||
alt="Vercel logomark"
|
||||
width={20}
|
||||
height={20}
|
||||
/>
|
||||
Deploy now
|
||||
GitHub
|
||||
</a>
|
||||
<a
|
||||
className="rounded-full border border-solid border-black/[.08] dark:border-white/[.145] transition-colors flex items-center justify-center hover:bg-[#f2f2f2] dark:hover:bg-[#1a1a1a] hover:border-transparent text-sm sm:text-base h-10 sm:h-12 px-4 sm:px-5 sm:min-w-44"
|
||||
href="https://nextjs.org/docs?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
||||
href="https://github.com/your-org/fast-next-template#documentation"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="hover:text-foreground transition-colors"
|
||||
>
|
||||
Read our docs
|
||||
Documentation
|
||||
</a>
|
||||
<a
|
||||
href="https://github.com/your-org/fast-next-template/issues"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="hover:text-foreground transition-colors"
|
||||
>
|
||||
Report Issue
|
||||
</a>
|
||||
</div>
|
||||
</main>
|
||||
<footer className="row-start-3 flex gap-6 flex-wrap items-center justify-center">
|
||||
<a
|
||||
className="flex items-center gap-2 hover:underline hover:underline-offset-4"
|
||||
href="https://nextjs.org/learn?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<Image
|
||||
aria-hidden
|
||||
src="/file.svg"
|
||||
alt="File icon"
|
||||
width={16}
|
||||
height={16}
|
||||
/>
|
||||
Learn
|
||||
</a>
|
||||
<a
|
||||
className="flex items-center gap-2 hover:underline hover:underline-offset-4"
|
||||
href="https://vercel.com/templates?framework=next.js&utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<Image
|
||||
aria-hidden
|
||||
src="/window.svg"
|
||||
alt="Window icon"
|
||||
width={16}
|
||||
height={16}
|
||||
/>
|
||||
Examples
|
||||
</a>
|
||||
<a
|
||||
className="flex items-center gap-2 hover:underline hover:underline-offset-4"
|
||||
href="https://nextjs.org?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<Image
|
||||
aria-hidden
|
||||
src="/globe.svg"
|
||||
alt="Globe icon"
|
||||
width={16}
|
||||
height={16}
|
||||
/>
|
||||
Go to nextjs.org →
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
);
|
||||
|
||||
140
frontend/src/components/home/AnimatedTerminal.tsx
Normal file
140
frontend/src/components/home/AnimatedTerminal.tsx
Normal file
@@ -0,0 +1,140 @@
|
||||
/**
|
||||
* Animated Terminal
|
||||
* Terminal with typing animation showing installation/setup commands
|
||||
*/
|
||||
|
||||
'use client';
|
||||
|
||||
import { useEffect, useState, useRef } from 'react';
|
||||
import { motion } from 'framer-motion';
|
||||
import { Terminal, Play } from 'lucide-react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import Link from 'next/link';
|
||||
|
||||
const commands = [
|
||||
{ text: '# Clone the repository', delay: 0 },
|
||||
{ text: '$ git clone https://github.com/your-org/fast-next-template.git', delay: 800 },
|
||||
{ text: '$ cd fast-next-template', delay: 1600 },
|
||||
{ text: '', delay: 2200 },
|
||||
{ text: '# Start with Docker (one command)', delay: 2400 },
|
||||
{ text: '$ docker-compose up', delay: 3200 },
|
||||
{ text: '', delay: 4000 },
|
||||
{ text: '✓ Backend running at http://localhost:8000', delay: 4200, isSuccess: true },
|
||||
{ text: '✓ Frontend running at http://localhost:3000', delay: 4400, isSuccess: true },
|
||||
{ text: '✓ Admin panel at http://localhost:3000/admin', delay: 4600, isSuccess: true },
|
||||
{ text: '✓ API docs at http://localhost:8000/docs', delay: 4800, isSuccess: true },
|
||||
];
|
||||
|
||||
export function AnimatedTerminal() {
|
||||
const [displayedLines, setDisplayedLines] = useState<typeof commands>([]);
|
||||
const [isAnimating, setIsAnimating] = useState(false);
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const hasAnimated = useRef(false);
|
||||
|
||||
useEffect(() => {
|
||||
// Only animate once when component enters viewport
|
||||
if (hasAnimated.current) return;
|
||||
|
||||
const observer = new IntersectionObserver(
|
||||
([entry]) => {
|
||||
if (entry.isIntersecting && !hasAnimated.current) {
|
||||
hasAnimated.current = true;
|
||||
setIsAnimating(true);
|
||||
animateCommands();
|
||||
}
|
||||
},
|
||||
{ threshold: 0.3 }
|
||||
);
|
||||
|
||||
if (containerRef.current) {
|
||||
observer.observe(containerRef.current);
|
||||
}
|
||||
|
||||
return () => observer.disconnect();
|
||||
}, []);
|
||||
|
||||
const animateCommands = () => {
|
||||
commands.forEach((cmd) => {
|
||||
setTimeout(() => {
|
||||
setDisplayedLines((prev) => [...prev, cmd]);
|
||||
}, cmd.delay);
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<section className="container mx-auto px-6 py-16 md:py-24" ref={containerRef}>
|
||||
<motion.div
|
||||
className="mx-auto max-w-4xl"
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: '-100px' }}
|
||||
transition={{ duration: 0.6 }}
|
||||
>
|
||||
{/* Title */}
|
||||
<div className="text-center mb-8">
|
||||
<h2 className="text-3xl md:text-4xl font-bold mb-4">Get Started in Seconds</h2>
|
||||
<p className="text-lg text-muted-foreground">
|
||||
Clone, run, and start building. No complex setup required.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Terminal Window */}
|
||||
<div className="rounded-lg border bg-card shadow-lg overflow-hidden">
|
||||
{/* Terminal Header */}
|
||||
<div className="flex items-center gap-2 border-b bg-muted/50 px-4 py-3">
|
||||
<div className="flex gap-1.5">
|
||||
<div className="h-3 w-3 rounded-full bg-red-500" aria-hidden="true" />
|
||||
<div className="h-3 w-3 rounded-full bg-yellow-500" aria-hidden="true" />
|
||||
<div className="h-3 w-3 rounded-full bg-green-500" aria-hidden="true" />
|
||||
</div>
|
||||
<div className="flex items-center gap-2 ml-4 text-sm text-muted-foreground">
|
||||
<Terminal className="h-4 w-4" aria-hidden="true" />
|
||||
<span className="font-mono">bash</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Terminal Content */}
|
||||
<div className="bg-slate-950 p-6 font-mono text-sm overflow-x-auto" style={{ minHeight: '400px' }}>
|
||||
<div className="space-y-2">
|
||||
{displayedLines.map((line, index) => (
|
||||
<motion.div
|
||||
key={index}
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
transition={{ duration: 0.2 }}
|
||||
className={`${
|
||||
line.isSuccess
|
||||
? 'text-green-400'
|
||||
: line.text.startsWith('#')
|
||||
? 'text-slate-500'
|
||||
: line.text.startsWith('$')
|
||||
? 'text-blue-400'
|
||||
: 'text-slate-300'
|
||||
}`}
|
||||
>
|
||||
{line.text || '\u00A0'}
|
||||
{index === displayedLines.length - 1 && isAnimating && !line.isSuccess && (
|
||||
<span className="inline-block w-2 h-4 ml-1 bg-slate-400 animate-pulse" aria-hidden="true" />
|
||||
)}
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* CTA Below Terminal */}
|
||||
<div className="mt-8 text-center">
|
||||
<p className="text-muted-foreground mb-4">
|
||||
Or try the live demo without installing anything
|
||||
</p>
|
||||
<Button asChild size="lg" variant="outline" className="gap-2">
|
||||
<Link href="/login">
|
||||
<Play className="h-5 w-5" aria-hidden="true" />
|
||||
Try Live Demo
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</motion.div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
124
frontend/src/components/home/CTASection.tsx
Normal file
124
frontend/src/components/home/CTASection.tsx
Normal file
@@ -0,0 +1,124 @@
|
||||
/**
|
||||
* CTA Section
|
||||
* Final call-to-action footer section
|
||||
*/
|
||||
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import Link from 'next/link';
|
||||
import { motion } from 'framer-motion';
|
||||
import { Github, Star, Play, ArrowRight } from 'lucide-react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { DemoCredentialsModal } from './DemoCredentialsModal';
|
||||
|
||||
export function CTASection() {
|
||||
const [demoModalOpen, setDemoModalOpen] = useState(false);
|
||||
|
||||
return (
|
||||
<section className="relative overflow-hidden bg-gradient-to-br from-primary/10 via-background to-background">
|
||||
{/* Background Pattern */}
|
||||
<div
|
||||
className="absolute inset-0 -z-10 bg-[radial-gradient(circle_at_50%_50%,rgba(var(--primary-rgb,120,119,198),0.1),transparent_70%)]"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
|
||||
<div className="container mx-auto px-6 py-24 md:py-32">
|
||||
<motion.div
|
||||
className="mx-auto max-w-3xl text-center space-y-8"
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: '-100px' }}
|
||||
transition={{ duration: 0.6 }}
|
||||
>
|
||||
{/* Headline */}
|
||||
<h2 className="text-4xl md:text-5xl font-bold tracking-tight">
|
||||
Start Building,{' '}
|
||||
<span className="bg-gradient-to-r from-primary to-primary/60 bg-clip-text text-transparent">
|
||||
Not Boilerplating
|
||||
</span>
|
||||
</h2>
|
||||
|
||||
{/* Subtext */}
|
||||
<p className="text-xl text-muted-foreground leading-relaxed">
|
||||
Clone the repository, read the docs, and ship features on day one.{' '}
|
||||
<span className="text-foreground font-medium">Free forever, MIT licensed.</span>
|
||||
</p>
|
||||
|
||||
{/* CTAs */}
|
||||
<div className="flex flex-col sm:flex-row items-center justify-center gap-4 pt-4">
|
||||
<Button
|
||||
asChild
|
||||
size="lg"
|
||||
className="gap-2 text-base group"
|
||||
>
|
||||
<a
|
||||
href="https://github.com/your-org/fast-next-template"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<Github className="h-5 w-5" aria-hidden="true" />
|
||||
Get Started on GitHub
|
||||
<div className="inline-flex items-center gap-1 ml-2 rounded-full bg-background/20 px-2 py-0.5 text-xs">
|
||||
<Star className="h-3 w-3 fill-current" aria-hidden="true" />
|
||||
Star
|
||||
</div>
|
||||
</a>
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => setDemoModalOpen(true)}
|
||||
size="lg"
|
||||
variant="outline"
|
||||
className="gap-2 text-base group"
|
||||
>
|
||||
<Play className="h-5 w-5 group-hover:scale-110 transition-transform" aria-hidden="true" />
|
||||
Try Live Demo
|
||||
</Button>
|
||||
<Button
|
||||
asChild
|
||||
size="lg"
|
||||
variant="ghost"
|
||||
className="gap-2 text-base group"
|
||||
>
|
||||
<a
|
||||
href="https://github.com/your-org/fast-next-template#documentation"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Read Documentation
|
||||
<ArrowRight className="h-4 w-4 group-hover:translate-x-1 transition-transform" aria-hidden="true" />
|
||||
</a>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Extra Info */}
|
||||
<motion.div
|
||||
className="pt-8 text-sm text-muted-foreground"
|
||||
initial={{ opacity: 0 }}
|
||||
whileInView={{ opacity: 1 }}
|
||||
viewport={{ once: true }}
|
||||
transition={{ duration: 0.5, delay: 0.2 }}
|
||||
>
|
||||
<p>
|
||||
Need help getting started? Check out the{' '}
|
||||
<Link href="/dev" className="text-primary hover:underline">
|
||||
component showcase
|
||||
</Link>
|
||||
{' '}or explore the{' '}
|
||||
<Link href="/admin" className="text-primary hover:underline">
|
||||
admin dashboard demo
|
||||
</Link>
|
||||
.
|
||||
</p>
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
</div>
|
||||
|
||||
{/* Demo Credentials Modal */}
|
||||
<DemoCredentialsModal
|
||||
open={demoModalOpen}
|
||||
onClose={() => setDemoModalOpen(false)}
|
||||
/>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
59
frontend/src/components/home/ContextSection.tsx
Normal file
59
frontend/src/components/home/ContextSection.tsx
Normal file
@@ -0,0 +1,59 @@
|
||||
/**
|
||||
* Context Section
|
||||
* Explains what the template is and what you get out of the box
|
||||
*/
|
||||
|
||||
'use client';
|
||||
|
||||
import { motion } from 'framer-motion';
|
||||
import { CheckCircle2 } from 'lucide-react';
|
||||
|
||||
export function ContextSection() {
|
||||
const features = [
|
||||
'Clone & Deploy in < 5 minutes',
|
||||
'97% Test Coverage (743 tests)',
|
||||
'12+ Documentation Guides',
|
||||
'Zero Commercial Dependencies',
|
||||
];
|
||||
|
||||
return (
|
||||
<section className="container mx-auto px-6 py-16 md:py-24">
|
||||
<div className="mx-auto max-w-3xl">
|
||||
<motion.div
|
||||
className="text-center space-y-6"
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: '-100px' }}
|
||||
transition={{ duration: 0.6 }}
|
||||
>
|
||||
<h2 className="text-3xl md:text-4xl font-bold">
|
||||
What You Get Out of the Box
|
||||
</h2>
|
||||
<p className="text-lg text-muted-foreground leading-relaxed">
|
||||
This isn't a boilerplate generator or a paid SaaS template. It's a complete,
|
||||
production-ready codebase you can clone and customize. Everything you need to build
|
||||
modern web applications without reinventing authentication, authorization, and admin
|
||||
infrastructure.
|
||||
</p>
|
||||
|
||||
{/* Feature Badges */}
|
||||
<div className="flex flex-wrap justify-center gap-3 pt-4">
|
||||
{features.map((feature, index) => (
|
||||
<motion.div
|
||||
key={feature}
|
||||
className="inline-flex items-center gap-2 rounded-full border bg-card px-4 py-2 text-sm font-medium"
|
||||
initial={{ opacity: 0, scale: 0.9 }}
|
||||
whileInView={{ opacity: 1, scale: 1 }}
|
||||
viewport={{ once: true }}
|
||||
transition={{ duration: 0.3, delay: index * 0.1 }}
|
||||
>
|
||||
<CheckCircle2 className="h-4 w-4 text-green-600" aria-hidden="true" />
|
||||
<span>{feature}</span>
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
</motion.div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
141
frontend/src/components/home/DemoCredentialsModal.tsx
Normal file
141
frontend/src/components/home/DemoCredentialsModal.tsx
Normal file
@@ -0,0 +1,141 @@
|
||||
/**
|
||||
* Demo Credentials Modal
|
||||
* Displays demo login credentials for testing the live application
|
||||
*/
|
||||
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import Link from 'next/link';
|
||||
import { Copy, Check } from 'lucide-react';
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from '@/components/ui/dialog';
|
||||
import { Button } from '@/components/ui/button';
|
||||
|
||||
interface DemoCredentialsModalProps {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
export function DemoCredentialsModal({ open, onClose }: DemoCredentialsModalProps) {
|
||||
const [copiedRegular, setCopiedRegular] = useState(false);
|
||||
const [copiedAdmin, setCopiedAdmin] = useState(false);
|
||||
|
||||
const regularCredentials = 'demo@example.com\nDemo123!';
|
||||
const adminCredentials = 'admin@example.com\nAdmin123!';
|
||||
|
||||
const copyToClipboard = async (text: string, type: 'regular' | 'admin') => {
|
||||
try {
|
||||
await navigator.clipboard.writeText(text);
|
||||
if (type === 'regular') {
|
||||
setCopiedRegular(true);
|
||||
setTimeout(() => setCopiedRegular(false), 2000);
|
||||
} else {
|
||||
setCopiedAdmin(true);
|
||||
setTimeout(() => setCopiedAdmin(false), 2000);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to copy:', err);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onClose}>
|
||||
<DialogContent className="sm:max-w-md">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Try the Live Demo</DialogTitle>
|
||||
<DialogDescription>
|
||||
Use these credentials to explore the template's features. Both accounts are pre-configured with sample data.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="space-y-4 my-4">
|
||||
{/* Regular User Credentials */}
|
||||
<div className="rounded-lg border p-4 space-y-3 bg-card">
|
||||
<div className="flex items-center justify-between">
|
||||
<p className="font-semibold text-sm">Regular User</p>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => copyToClipboard(regularCredentials, 'regular')}
|
||||
className="h-8 gap-2"
|
||||
>
|
||||
{copiedRegular ? (
|
||||
<Check className="h-4 w-4 text-green-600" aria-hidden="true" />
|
||||
) : (
|
||||
<Copy className="h-4 w-4" aria-hidden="true" />
|
||||
)}
|
||||
<span className="sr-only">Copy regular user credentials</span>
|
||||
{copiedRegular ? 'Copied!' : 'Copy'}
|
||||
</Button>
|
||||
</div>
|
||||
<div className="font-mono text-sm space-y-1 text-muted-foreground">
|
||||
<p className="flex items-center gap-2">
|
||||
<span className="text-xs font-sans text-muted-foreground/70">Email:</span>
|
||||
<span className="text-foreground">demo@example.com</span>
|
||||
</p>
|
||||
<p className="flex items-center gap-2">
|
||||
<span className="text-xs font-sans text-muted-foreground/70">Password:</span>
|
||||
<span className="text-foreground">Demo123!</span>
|
||||
</p>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Access settings, organizations, and user features
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Admin User Credentials */}
|
||||
<div className="rounded-lg border p-4 space-y-3 bg-card">
|
||||
<div className="flex items-center justify-between">
|
||||
<p className="font-semibold text-sm">Admin User (Superuser)</p>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => copyToClipboard(adminCredentials, 'admin')}
|
||||
className="h-8 gap-2"
|
||||
>
|
||||
{copiedAdmin ? (
|
||||
<Check className="h-4 w-4 text-green-600" aria-hidden="true" />
|
||||
) : (
|
||||
<Copy className="h-4 w-4" aria-hidden="true" />
|
||||
)}
|
||||
<span className="sr-only">Copy admin user credentials</span>
|
||||
{copiedAdmin ? 'Copied!' : 'Copy'}
|
||||
</Button>
|
||||
</div>
|
||||
<div className="font-mono text-sm space-y-1 text-muted-foreground">
|
||||
<p className="flex items-center gap-2">
|
||||
<span className="text-xs font-sans text-muted-foreground/70">Email:</span>
|
||||
<span className="text-foreground">admin@example.com</span>
|
||||
</p>
|
||||
<p className="flex items-center gap-2">
|
||||
<span className="text-xs font-sans text-muted-foreground/70">Password:</span>
|
||||
<span className="text-foreground">Admin123!</span>
|
||||
</p>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Full admin panel access: user management, analytics, bulk operations
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<DialogFooter className="flex flex-col sm:flex-row gap-2">
|
||||
<Button variant="outline" onClick={onClose} className="w-full sm:w-auto">
|
||||
Close
|
||||
</Button>
|
||||
<Button asChild className="w-full sm:w-auto">
|
||||
<Link href="/login" onClick={onClose}>
|
||||
Go to Login →
|
||||
</Link>
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
97
frontend/src/components/home/DemoSection.tsx
Normal file
97
frontend/src/components/home/DemoSection.tsx
Normal file
@@ -0,0 +1,97 @@
|
||||
/**
|
||||
* Demo Section
|
||||
* Interactive demo cards showing live features with demo credentials
|
||||
*/
|
||||
|
||||
'use client';
|
||||
|
||||
import Link from 'next/link';
|
||||
import { motion } from 'framer-motion';
|
||||
import { Play, Layers, ShieldCheck } from 'lucide-react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
|
||||
const demos = [
|
||||
{
|
||||
icon: Layers,
|
||||
title: 'Component Showcase',
|
||||
description:
|
||||
'Browse the complete design system, UI components, and interactive examples built with shadcn/ui and TailwindCSS',
|
||||
href: '/dev',
|
||||
cta: 'View Components',
|
||||
variant: 'outline' as const,
|
||||
},
|
||||
{
|
||||
icon: ShieldCheck,
|
||||
title: 'Authentication Flow',
|
||||
description:
|
||||
'Test the complete auth flow: login, session management, password reset. Full security implementation with JWT and refresh tokens',
|
||||
href: '/login',
|
||||
credentials: 'demo@example.com / Demo123!',
|
||||
cta: 'Try Auth Demo',
|
||||
variant: 'default' as const,
|
||||
},
|
||||
{
|
||||
icon: Play,
|
||||
title: 'Admin Dashboard',
|
||||
description:
|
||||
'Experience the admin panel with user management, real-time analytics charts, bulk operations, and session monitoring',
|
||||
href: '/admin',
|
||||
credentials: 'admin@example.com / Admin123!',
|
||||
cta: 'Launch Admin',
|
||||
variant: 'outline' as const,
|
||||
},
|
||||
];
|
||||
|
||||
export function DemoSection() {
|
||||
return (
|
||||
<section className="container mx-auto px-6 py-16 md:py-24 bg-muted/30">
|
||||
<motion.div
|
||||
className="text-center mb-12"
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: '-100px' }}
|
||||
transition={{ duration: 0.6 }}
|
||||
>
|
||||
<h2 className="text-3xl md:text-4xl font-bold mb-4">See It In Action</h2>
|
||||
<p className="text-lg text-muted-foreground max-w-2xl mx-auto">
|
||||
Explore the template's capabilities with live demos. Login with demo credentials to test features.
|
||||
</p>
|
||||
</motion.div>
|
||||
|
||||
<div className="grid md:grid-cols-3 gap-6 max-w-6xl mx-auto">
|
||||
{demos.map((demo, index) => (
|
||||
<motion.div
|
||||
key={demo.title}
|
||||
className="rounded-lg border bg-card p-6 hover:shadow-lg transition-shadow"
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: '-100px' }}
|
||||
transition={{ duration: 0.5, delay: index * 0.1 }}
|
||||
whileHover={{ y: -4 }}
|
||||
>
|
||||
{/* Icon */}
|
||||
<div className="mb-4 inline-flex h-12 w-12 items-center justify-center rounded-lg bg-gradient-to-br from-primary to-primary/60">
|
||||
<demo.icon className="h-6 w-6 text-primary-foreground" aria-hidden="true" />
|
||||
</div>
|
||||
|
||||
<h3 className="text-xl font-semibold mb-2">{demo.title}</h3>
|
||||
<p className="text-muted-foreground mb-4 leading-relaxed">
|
||||
{demo.description}
|
||||
</p>
|
||||
|
||||
{demo.credentials && (
|
||||
<div className="mb-4 p-3 rounded-md bg-muted font-mono text-xs border">
|
||||
<div className="text-muted-foreground mb-1">Demo Credentials:</div>
|
||||
<div className="text-foreground">{demo.credentials}</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Button asChild className="w-full" variant={demo.variant}>
|
||||
<Link href={demo.href}>{demo.cta} →</Link>
|
||||
</Button>
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
63
frontend/src/components/home/FeatureCard.tsx
Normal file
63
frontend/src/components/home/FeatureCard.tsx
Normal file
@@ -0,0 +1,63 @@
|
||||
/**
|
||||
* Feature Card
|
||||
* Reusable feature card component with icon, title, description, and CTA link
|
||||
*/
|
||||
|
||||
'use client';
|
||||
|
||||
import Link from 'next/link';
|
||||
import { motion } from 'framer-motion';
|
||||
import { ArrowRight, LucideIcon } from 'lucide-react';
|
||||
|
||||
interface FeatureCardProps {
|
||||
icon: LucideIcon;
|
||||
title: string;
|
||||
description: string;
|
||||
highlight: string;
|
||||
ctaText: string;
|
||||
ctaHref: string;
|
||||
}
|
||||
|
||||
export function FeatureCard({
|
||||
icon: Icon,
|
||||
title,
|
||||
description,
|
||||
highlight,
|
||||
ctaText,
|
||||
ctaHref,
|
||||
}: FeatureCardProps) {
|
||||
return (
|
||||
<motion.div
|
||||
className="group relative rounded-lg border bg-card p-6 hover:shadow-lg transition-all"
|
||||
whileHover={{ y: -4 }}
|
||||
transition={{ duration: 0.2 }}
|
||||
>
|
||||
{/* Icon with Gradient Background */}
|
||||
<div className="mb-4 inline-flex h-12 w-12 items-center justify-center rounded-lg bg-gradient-to-br from-primary to-primary/60">
|
||||
<Icon className="h-6 w-6 text-primary-foreground" aria-hidden="true" />
|
||||
</div>
|
||||
|
||||
{/* Title */}
|
||||
<h3 className="text-xl font-semibold mb-2">{title}</h3>
|
||||
|
||||
{/* Highlight Badge */}
|
||||
<div className="mb-3">
|
||||
<span className="inline-block rounded-full bg-primary/10 px-3 py-1 text-xs font-medium text-primary">
|
||||
{highlight}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Description */}
|
||||
<p className="text-muted-foreground mb-4 leading-relaxed">{description}</p>
|
||||
|
||||
{/* CTA Link */}
|
||||
<Link
|
||||
href={ctaHref}
|
||||
className="inline-flex items-center gap-1 text-sm font-medium text-primary hover:underline group-hover:gap-2 transition-all"
|
||||
>
|
||||
{ctaText}
|
||||
<ArrowRight className="h-4 w-4" aria-hidden="true" />
|
||||
</Link>
|
||||
</motion.div>
|
||||
);
|
||||
}
|
||||
117
frontend/src/components/home/FeatureGrid.tsx
Normal file
117
frontend/src/components/home/FeatureGrid.tsx
Normal file
@@ -0,0 +1,117 @@
|
||||
/**
|
||||
* Feature Grid
|
||||
* Grid layout displaying 6 main features with stagger animation
|
||||
*/
|
||||
|
||||
'use client';
|
||||
|
||||
import { motion } from 'framer-motion';
|
||||
import { Shield, Users, BarChart3, BookOpen, Server, Code } from 'lucide-react';
|
||||
import { FeatureCard } from './FeatureCard';
|
||||
|
||||
const features = [
|
||||
{
|
||||
icon: Shield,
|
||||
title: 'Authentication & Security',
|
||||
description:
|
||||
'JWT authentication with refresh tokens, session management, password reset flow, rate limiting, CSRF protection, and comprehensive security tests preventing common attacks (CVE-2015-9235, session hijacking)',
|
||||
highlight: 'Battle-tested security',
|
||||
ctaText: 'View Auth Flow',
|
||||
ctaHref: '/login',
|
||||
},
|
||||
{
|
||||
icon: Users,
|
||||
title: 'Multi-Tenant Organizations',
|
||||
description:
|
||||
'Complete organization system with 3-tier RBAC (Owner/Admin/Member). Invite members, manage permissions, and scope data access per organization—all batteries included',
|
||||
highlight: 'Multi-tenancy built-in',
|
||||
ctaText: 'See Organizations',
|
||||
ctaHref: '/admin/organizations',
|
||||
},
|
||||
{
|
||||
icon: BarChart3,
|
||||
title: 'Admin Dashboard',
|
||||
description:
|
||||
'Full-featured admin panel with user management, real-time analytics charts, bulk operations, session monitoring, and role-based access controls',
|
||||
highlight: 'Enterprise-ready admin',
|
||||
ctaText: 'Try Admin Panel',
|
||||
ctaHref: '/admin',
|
||||
},
|
||||
{
|
||||
icon: BookOpen,
|
||||
title: 'Complete Documentation',
|
||||
description:
|
||||
'12+ documentation guides covering architecture, design system, testing patterns, deployment, and AI code generation guidelines. Interactive API docs with Swagger and ReDoc',
|
||||
highlight: 'Developer-first docs',
|
||||
ctaText: 'Browse Docs',
|
||||
ctaHref: 'https://github.com/your-org/fast-next-template#documentation',
|
||||
},
|
||||
{
|
||||
icon: Server,
|
||||
title: 'Production Ready',
|
||||
description:
|
||||
'Docker deployment configs, database migrations with Alembic helpers, connection pooling, health checks, monitoring setup, and production security headers',
|
||||
highlight: 'Deploy with confidence',
|
||||
ctaText: 'Deployment Guide',
|
||||
ctaHref: 'https://github.com/your-org/fast-next-template#deployment',
|
||||
},
|
||||
{
|
||||
icon: Code,
|
||||
title: 'Developer Experience',
|
||||
description:
|
||||
'Auto-generated TypeScript API client from OpenAPI spec, hot reload in development, migration helpers (python migrate.py auto), VS Code settings, and comprehensive component library',
|
||||
highlight: 'Delightful DX',
|
||||
ctaText: 'Explore Components',
|
||||
ctaHref: '/dev',
|
||||
},
|
||||
];
|
||||
|
||||
const containerVariants = {
|
||||
hidden: { opacity: 0 },
|
||||
show: {
|
||||
opacity: 1,
|
||||
transition: {
|
||||
staggerChildren: 0.15,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const itemVariants = {
|
||||
hidden: { opacity: 0, y: 20 },
|
||||
show: { opacity: 1, y: 0 },
|
||||
};
|
||||
|
||||
export function FeatureGrid() {
|
||||
return (
|
||||
<section className="container mx-auto px-6 py-16 md:py-24">
|
||||
<motion.div
|
||||
className="text-center mb-12"
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: '-100px' }}
|
||||
transition={{ duration: 0.6 }}
|
||||
>
|
||||
<h2 className="text-3xl md:text-4xl font-bold mb-4">
|
||||
Comprehensive Features, No Assembly Required
|
||||
</h2>
|
||||
<p className="text-lg text-muted-foreground max-w-2xl mx-auto">
|
||||
Everything you need to build production-grade web applications. Clone, customize, and ship.
|
||||
</p>
|
||||
</motion.div>
|
||||
|
||||
<motion.div
|
||||
className="grid gap-6 md:grid-cols-2 lg:grid-cols-3"
|
||||
variants={containerVariants}
|
||||
initial="hidden"
|
||||
whileInView="show"
|
||||
viewport={{ once: true, margin: '-100px' }}
|
||||
>
|
||||
{features.map((feature) => (
|
||||
<motion.div key={feature.title} variants={itemVariants}>
|
||||
<FeatureCard {...feature} />
|
||||
</motion.div>
|
||||
))}
|
||||
</motion.div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
155
frontend/src/components/home/Header.tsx
Normal file
155
frontend/src/components/home/Header.tsx
Normal file
@@ -0,0 +1,155 @@
|
||||
/**
|
||||
* Homepage Header
|
||||
* Navigation header for the landing page with demo credentials modal
|
||||
*/
|
||||
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import Link from 'next/link';
|
||||
import { Menu, X, Github, Star } from 'lucide-react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { DemoCredentialsModal } from './DemoCredentialsModal';
|
||||
import { Sheet, SheetContent, SheetTrigger } from '@/components/ui/sheet';
|
||||
|
||||
export function Header() {
|
||||
const [demoModalOpen, setDemoModalOpen] = useState(false);
|
||||
const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
|
||||
|
||||
const navLinks = [
|
||||
{ href: '/dev', label: 'Components' },
|
||||
{ href: '/admin', label: 'Admin Demo' },
|
||||
];
|
||||
|
||||
return (
|
||||
<>
|
||||
<header className="sticky top-0 z-50 w-full border-b bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60">
|
||||
<div className="container mx-auto flex h-16 items-center justify-between px-6">
|
||||
{/* Logo */}
|
||||
<Link href="/" className="font-bold text-xl hover:opacity-80 transition-opacity">
|
||||
<span className="bg-gradient-to-r from-primary to-primary/60 bg-clip-text text-transparent">
|
||||
FastNext
|
||||
</span>
|
||||
{' '}
|
||||
<span className="text-foreground">Template</span>
|
||||
</Link>
|
||||
|
||||
{/* Desktop Navigation */}
|
||||
<nav className="hidden md:flex items-center gap-6">
|
||||
{navLinks.map((link) => (
|
||||
<Link
|
||||
key={link.href}
|
||||
href={link.href}
|
||||
className="text-sm font-medium text-muted-foreground hover:text-foreground transition-colors"
|
||||
>
|
||||
{link.label}
|
||||
</Link>
|
||||
))}
|
||||
|
||||
{/* GitHub Link with Star */}
|
||||
<a
|
||||
href="https://github.com/your-org/fast-next-template"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="flex items-center gap-2 text-sm font-medium text-muted-foreground hover:text-foreground transition-colors"
|
||||
>
|
||||
<Github className="h-4 w-4" aria-hidden="true" />
|
||||
<span>GitHub</span>
|
||||
<span className="inline-flex items-center gap-1 rounded-full bg-muted px-2 py-0.5 text-xs">
|
||||
<Star className="h-3 w-3 fill-current" aria-hidden="true" />
|
||||
<span aria-label="GitHub stars">Star</span>
|
||||
</span>
|
||||
</a>
|
||||
|
||||
{/* CTAs */}
|
||||
<Button
|
||||
onClick={() => setDemoModalOpen(true)}
|
||||
variant="default"
|
||||
size="sm"
|
||||
>
|
||||
Try Demo
|
||||
</Button>
|
||||
<Button
|
||||
asChild
|
||||
variant="outline"
|
||||
size="sm"
|
||||
>
|
||||
<Link href="/login">Login</Link>
|
||||
</Button>
|
||||
</nav>
|
||||
|
||||
{/* Mobile Menu Toggle */}
|
||||
<Sheet open={mobileMenuOpen} onOpenChange={setMobileMenuOpen}>
|
||||
<SheetTrigger asChild className="md:hidden">
|
||||
<Button variant="ghost" size="icon" aria-label="Toggle menu">
|
||||
{mobileMenuOpen ? (
|
||||
<X className="h-5 w-5" aria-hidden="true" />
|
||||
) : (
|
||||
<Menu className="h-5 w-5" aria-hidden="true" />
|
||||
)}
|
||||
</Button>
|
||||
</SheetTrigger>
|
||||
<SheetContent side="right" className="w-[300px] sm:w-[400px]">
|
||||
<nav className="flex flex-col gap-4 mt-8">
|
||||
{navLinks.map((link) => (
|
||||
<Link
|
||||
key={link.href}
|
||||
href={link.href}
|
||||
onClick={() => setMobileMenuOpen(false)}
|
||||
className="text-lg font-medium hover:text-primary transition-colors"
|
||||
>
|
||||
{link.href}
|
||||
</Link>
|
||||
))}
|
||||
|
||||
{/* GitHub Link */}
|
||||
<a
|
||||
href="https://github.com/your-org/fast-next-template"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
onClick={() => setMobileMenuOpen(false)}
|
||||
className="flex items-center gap-2 text-lg font-medium hover:text-primary transition-colors"
|
||||
>
|
||||
<Github className="h-5 w-5" aria-hidden="true" />
|
||||
<span>GitHub</span>
|
||||
<span className="inline-flex items-center gap-1 rounded-full bg-muted px-2 py-0.5 text-xs ml-auto">
|
||||
<Star className="h-3 w-3 fill-current" aria-hidden="true" />
|
||||
Star
|
||||
</span>
|
||||
</a>
|
||||
|
||||
<div className="border-t pt-4 mt-4 space-y-3">
|
||||
<Button
|
||||
onClick={() => {
|
||||
setMobileMenuOpen(false);
|
||||
setDemoModalOpen(true);
|
||||
}}
|
||||
variant="default"
|
||||
className="w-full"
|
||||
>
|
||||
Try Demo
|
||||
</Button>
|
||||
<Button
|
||||
asChild
|
||||
variant="outline"
|
||||
className="w-full"
|
||||
>
|
||||
<Link href="/login" onClick={() => setMobileMenuOpen(false)}>
|
||||
Login
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</nav>
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{/* Demo Credentials Modal */}
|
||||
<DemoCredentialsModal
|
||||
open={demoModalOpen}
|
||||
onClose={() => setDemoModalOpen(false)}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
150
frontend/src/components/home/HeroSection.tsx
Normal file
150
frontend/src/components/home/HeroSection.tsx
Normal file
@@ -0,0 +1,150 @@
|
||||
/**
|
||||
* Hero Section
|
||||
* Main hero section with headline, subheadline, CTAs, and gradient background
|
||||
*/
|
||||
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import Link from 'next/link';
|
||||
import { motion } from 'framer-motion';
|
||||
import { ArrowRight, Github, Play } from 'lucide-react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { DemoCredentialsModal } from './DemoCredentialsModal';
|
||||
|
||||
export function HeroSection() {
|
||||
const [demoModalOpen, setDemoModalOpen] = useState(false);
|
||||
|
||||
return (
|
||||
<section className="relative overflow-hidden">
|
||||
{/* Gradient Background */}
|
||||
<div className="absolute inset-0 -z-10 bg-gradient-to-br from-primary/5 via-background to-background" aria-hidden="true" />
|
||||
<div
|
||||
className="absolute inset-0 -z-10 bg-[radial-gradient(circle_at_30%_20%,rgba(var(--primary-rgb,120,119,198),0.1),transparent_50%)]"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<div
|
||||
className="absolute inset-0 -z-10 bg-[radial-gradient(circle_at_70%_80%,rgba(var(--primary-rgb,120,119,198),0.05),transparent_50%)]"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
|
||||
<div className="container mx-auto px-6 py-24 md:py-32 lg:py-40">
|
||||
<div className="mx-auto max-w-4xl text-center space-y-8">
|
||||
{/* Badge */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.5 }}
|
||||
>
|
||||
<div className="inline-flex items-center gap-2 rounded-full border bg-background/50 px-4 py-1.5 text-sm backdrop-blur">
|
||||
<span className="inline-block h-2 w-2 rounded-full bg-green-500 animate-pulse" aria-hidden="true" />
|
||||
<span className="font-medium">MIT Licensed</span>
|
||||
<span className="text-muted-foreground">•</span>
|
||||
<span className="font-medium">97% Test Coverage</span>
|
||||
<span className="text-muted-foreground">•</span>
|
||||
<span className="font-medium">Production Ready</span>
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
{/* Headline */}
|
||||
<motion.h1
|
||||
className="text-4xl font-bold tracking-tight sm:text-5xl md:text-6xl lg:text-7xl"
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.5, delay: 0.1 }}
|
||||
>
|
||||
<span className="block">Everything You Need to Build</span>
|
||||
<span className="block bg-gradient-to-r from-primary via-primary/80 to-primary/60 bg-clip-text text-transparent">
|
||||
Modern Web Applications
|
||||
</span>
|
||||
</motion.h1>
|
||||
|
||||
{/* Subheadline */}
|
||||
<motion.p
|
||||
className="mx-auto max-w-2xl text-lg md:text-xl text-muted-foreground leading-relaxed"
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.5, delay: 0.2 }}
|
||||
>
|
||||
Production-ready FastAPI + Next.js template with authentication, multi-tenancy, and
|
||||
comprehensive admin panel. Built by developers, for developers.{' '}
|
||||
<span className="text-foreground font-medium">Start building features on day one.</span>
|
||||
</motion.p>
|
||||
|
||||
{/* CTAs */}
|
||||
<motion.div
|
||||
className="flex flex-col sm:flex-row items-center justify-center gap-4"
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.5, delay: 0.3 }}
|
||||
>
|
||||
<Button
|
||||
size="lg"
|
||||
onClick={() => setDemoModalOpen(true)}
|
||||
className="gap-2 text-base group"
|
||||
>
|
||||
<Play className="h-5 w-5 group-hover:scale-110 transition-transform" aria-hidden="true" />
|
||||
Try Live Demo
|
||||
</Button>
|
||||
<Button
|
||||
asChild
|
||||
size="lg"
|
||||
variant="outline"
|
||||
className="gap-2 text-base group"
|
||||
>
|
||||
<a
|
||||
href="https://github.com/your-org/fast-next-template"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<Github className="h-5 w-5" aria-hidden="true" />
|
||||
View on GitHub
|
||||
<ArrowRight className="h-4 w-4 group-hover:translate-x-1 transition-transform" aria-hidden="true" />
|
||||
</a>
|
||||
</Button>
|
||||
<Button
|
||||
asChild
|
||||
size="lg"
|
||||
variant="ghost"
|
||||
className="gap-2 text-base group"
|
||||
>
|
||||
<Link href="/dev">
|
||||
Explore Components
|
||||
<ArrowRight className="h-4 w-4 group-hover:translate-x-1 transition-transform" aria-hidden="true" />
|
||||
</Link>
|
||||
</Button>
|
||||
</motion.div>
|
||||
|
||||
{/* Quick Stats */}
|
||||
<motion.div
|
||||
className="flex flex-wrap items-center justify-center gap-6 pt-8 text-sm text-muted-foreground"
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
transition={{ duration: 0.5, delay: 0.4 }}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-2xl font-bold text-foreground">97%</span>
|
||||
<span>Test Coverage</span>
|
||||
</div>
|
||||
<div className="h-4 w-px bg-border" aria-hidden="true" />
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-2xl font-bold text-foreground">743</span>
|
||||
<span>Passing Tests</span>
|
||||
</div>
|
||||
<div className="h-4 w-px bg-border" aria-hidden="true" />
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-2xl font-bold text-foreground">0</span>
|
||||
<span>Flaky Tests</span>
|
||||
</div>
|
||||
</motion.div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Demo Credentials Modal */}
|
||||
<DemoCredentialsModal
|
||||
open={demoModalOpen}
|
||||
onClose={() => setDemoModalOpen(false)}
|
||||
/>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
103
frontend/src/components/home/PhilosophySection.tsx
Normal file
103
frontend/src/components/home/PhilosophySection.tsx
Normal file
@@ -0,0 +1,103 @@
|
||||
/**
|
||||
* Philosophy Section
|
||||
* "For Developers, By Developers" section explaining why this template exists
|
||||
*/
|
||||
|
||||
'use client';
|
||||
|
||||
import { motion } from 'framer-motion';
|
||||
import { X, Check } from 'lucide-react';
|
||||
|
||||
const wontFind = [
|
||||
'Vendor lock-in to specific services',
|
||||
'Hidden costs or upgrade prompts',
|
||||
'Poorly documented "magic"',
|
||||
'Outdated dependencies',
|
||||
];
|
||||
|
||||
const willFind = [
|
||||
'Production patterns that actually work',
|
||||
'Comprehensive test coverage (not aspirational)',
|
||||
'Clear, honest documentation',
|
||||
'Active development and improvements',
|
||||
];
|
||||
|
||||
export function PhilosophySection() {
|
||||
return (
|
||||
<section className="container mx-auto px-6 py-16 md:py-24">
|
||||
<div className="mx-auto max-w-4xl">
|
||||
<motion.div
|
||||
className="text-center mb-12"
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: '-100px' }}
|
||||
transition={{ duration: 0.6 }}
|
||||
>
|
||||
<h2 className="text-3xl md:text-4xl font-bold mb-6">
|
||||
Why This Template Exists
|
||||
</h2>
|
||||
<div className="space-y-4 text-lg text-muted-foreground leading-relaxed">
|
||||
<p>
|
||||
We built this template after rebuilding the same authentication, authorization, and
|
||||
admin infrastructure for the fifth time. Instead of yet another tutorial or boilerplate
|
||||
generator, we created a complete, tested, documented codebase that you can clone and
|
||||
customize.
|
||||
</p>
|
||||
<p className="text-foreground font-semibold text-xl">
|
||||
No vendor lock-in. No subscriptions. No license restrictions.
|
||||
</p>
|
||||
<p>
|
||||
Just clean, modern code with patterns that scale. MIT licensed forever.
|
||||
</p>
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
<div className="grid md:grid-cols-2 gap-8 mt-12">
|
||||
{/* What You Won't Find */}
|
||||
<motion.div
|
||||
className="rounded-lg border bg-card p-6"
|
||||
initial={{ opacity: 0, x: -20 }}
|
||||
whileInView={{ opacity: 1, x: 0 }}
|
||||
viewport={{ once: true, margin: '-100px' }}
|
||||
transition={{ duration: 0.5 }}
|
||||
>
|
||||
<h3 className="text-xl font-semibold mb-4 flex items-center gap-2">
|
||||
<X className="h-5 w-5 text-red-600" aria-hidden="true" />
|
||||
What You Won't Find Here
|
||||
</h3>
|
||||
<ul className="space-y-3">
|
||||
{wontFind.map((item) => (
|
||||
<li key={item} className="flex items-start gap-3">
|
||||
<X className="h-5 w-5 text-red-600 flex-shrink-0 mt-0.5" aria-hidden="true" />
|
||||
<span className="text-muted-foreground">{item}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</motion.div>
|
||||
|
||||
{/* What You Will Find */}
|
||||
<motion.div
|
||||
className="rounded-lg border bg-card p-6"
|
||||
initial={{ opacity: 0, x: 20 }}
|
||||
whileInView={{ opacity: 1, x: 0 }}
|
||||
viewport={{ once: true, margin: '-100px' }}
|
||||
transition={{ duration: 0.5 }}
|
||||
>
|
||||
<h3 className="text-xl font-semibold mb-4 flex items-center gap-2">
|
||||
<Check className="h-5 w-5 text-green-600" aria-hidden="true" />
|
||||
What You Will Find
|
||||
</h3>
|
||||
<ul className="space-y-3">
|
||||
{willFind.map((item) => (
|
||||
<li key={item} className="flex items-start gap-3">
|
||||
<Check className="h-5 w-5 text-green-600 flex-shrink-0 mt-0.5" aria-hidden="true" />
|
||||
<span className="text-muted-foreground">{item}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</motion.div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
101
frontend/src/components/home/QuickStartCode.tsx
Normal file
101
frontend/src/components/home/QuickStartCode.tsx
Normal file
@@ -0,0 +1,101 @@
|
||||
/**
|
||||
* Quick Start Code Block
|
||||
* Code snippet showing quick start commands with copy functionality
|
||||
*/
|
||||
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { motion } from 'framer-motion';
|
||||
import { Check, Copy } from 'lucide-react';
|
||||
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
|
||||
import { vscDarkPlus } from 'react-syntax-highlighter/dist/esm/styles/prism';
|
||||
import { Button } from '@/components/ui/button';
|
||||
|
||||
const codeString = `# Clone and start with Docker
|
||||
git clone https://github.com/your-org/fast-next-template.git
|
||||
cd fast-next-template
|
||||
docker-compose up
|
||||
|
||||
# Or set up locally
|
||||
cd backend && python -m venv .venv && source .venv/bin/activate
|
||||
pip install -r requirements.txt
|
||||
cd ../frontend && npm install`;
|
||||
|
||||
export function QuickStartCode() {
|
||||
const [copied, setCopied] = useState(false);
|
||||
|
||||
const copyToClipboard = async () => {
|
||||
try {
|
||||
await navigator.clipboard.writeText(codeString);
|
||||
setCopied(true);
|
||||
setTimeout(() => setCopied(false), 2000);
|
||||
} catch (err) {
|
||||
console.error('Failed to copy:', err);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<section className="container mx-auto px-6 py-16 md:py-24">
|
||||
<motion.div
|
||||
className="mx-auto max-w-4xl"
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: '-100px' }}
|
||||
transition={{ duration: 0.6 }}
|
||||
>
|
||||
<div className="text-center mb-8">
|
||||
<h2 className="text-3xl md:text-4xl font-bold mb-4">5-Minute Setup</h2>
|
||||
<p className="text-lg text-muted-foreground">
|
||||
Clone, run, and start building. It's that simple.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="relative rounded-lg border bg-card shadow-lg overflow-hidden">
|
||||
{/* Header with Copy Button */}
|
||||
<div className="flex items-center justify-between border-b bg-muted/50 px-4 py-3">
|
||||
<div className="flex items-center gap-2 text-sm text-muted-foreground font-mono">
|
||||
<span className="inline-block h-2 w-2 rounded-full bg-green-500" aria-hidden="true" />
|
||||
<span>bash</span>
|
||||
</div>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={copyToClipboard}
|
||||
className="gap-2"
|
||||
>
|
||||
{copied ? (
|
||||
<>
|
||||
<Check className="h-4 w-4 text-green-600" aria-hidden="true" />
|
||||
<span>Copied!</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Copy className="h-4 w-4" aria-hidden="true" />
|
||||
<span>Copy</span>
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Code Block */}
|
||||
<div className="overflow-x-auto">
|
||||
<SyntaxHighlighter
|
||||
language="bash"
|
||||
style={vscDarkPlus}
|
||||
customStyle={{
|
||||
margin: 0,
|
||||
padding: '1.5rem',
|
||||
background: 'transparent',
|
||||
fontSize: '0.875rem',
|
||||
}}
|
||||
wrapLines
|
||||
>
|
||||
{codeString}
|
||||
</SyntaxHighlighter>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
134
frontend/src/components/home/StatsSection.tsx
Normal file
134
frontend/src/components/home/StatsSection.tsx
Normal file
@@ -0,0 +1,134 @@
|
||||
/**
|
||||
* Stats Section
|
||||
* Animated statistics with counters
|
||||
*/
|
||||
|
||||
'use client';
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
import { motion, useInView } from 'framer-motion';
|
||||
import { useRef } from 'react';
|
||||
import { CheckCircle2, TestTube, Zap, FileCode } from 'lucide-react';
|
||||
|
||||
interface Stat {
|
||||
icon: React.ComponentType<{ className?: string }>;
|
||||
value: number;
|
||||
suffix: string;
|
||||
label: string;
|
||||
description: string;
|
||||
}
|
||||
|
||||
const stats: Stat[] = [
|
||||
{
|
||||
icon: Zap,
|
||||
value: 97,
|
||||
suffix: '%',
|
||||
label: 'Test Coverage',
|
||||
description: 'Comprehensive testing across backend and frontend',
|
||||
},
|
||||
{
|
||||
icon: TestTube,
|
||||
value: 743,
|
||||
suffix: '',
|
||||
label: 'Passing Tests',
|
||||
description: 'Backend, frontend unit, and E2E tests',
|
||||
},
|
||||
{
|
||||
icon: CheckCircle2,
|
||||
value: 0,
|
||||
suffix: '',
|
||||
label: 'Flaky Tests',
|
||||
description: 'Production-stable test suite',
|
||||
},
|
||||
{
|
||||
icon: FileCode,
|
||||
value: 30,
|
||||
suffix: '+',
|
||||
label: 'API Endpoints',
|
||||
description: 'Fully documented with OpenAPI',
|
||||
},
|
||||
];
|
||||
|
||||
function AnimatedCounter({ value, suffix }: { value: number; suffix: string }) {
|
||||
const [count, setCount] = useState(0);
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
const isInView = useInView(ref, { once: true, margin: '-100px' });
|
||||
|
||||
useEffect(() => {
|
||||
if (!isInView) return;
|
||||
|
||||
const duration = 2000; // 2 seconds
|
||||
const steps = 60;
|
||||
const increment = value / steps;
|
||||
let current = 0;
|
||||
|
||||
const timer = setInterval(() => {
|
||||
current += increment;
|
||||
if (current >= value) {
|
||||
setCount(value);
|
||||
clearInterval(timer);
|
||||
} else {
|
||||
setCount(Math.floor(current));
|
||||
}
|
||||
}, duration / steps);
|
||||
|
||||
return () => clearInterval(timer);
|
||||
}, [isInView, value]);
|
||||
|
||||
return (
|
||||
<div ref={ref} className="text-5xl md:text-6xl font-bold">
|
||||
{count}
|
||||
{suffix}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function StatsSection() {
|
||||
return (
|
||||
<section className="container mx-auto px-6 py-16 md:py-24">
|
||||
<motion.div
|
||||
className="text-center mb-12"
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: '-100px' }}
|
||||
transition={{ duration: 0.6 }}
|
||||
>
|
||||
<h2 className="text-3xl md:text-4xl font-bold mb-4">Built with Quality in Mind</h2>
|
||||
<p className="text-lg text-muted-foreground max-w-2xl mx-auto">
|
||||
Not just another template. Comprehensive testing, documentation, and production-ready patterns.
|
||||
</p>
|
||||
</motion.div>
|
||||
|
||||
<div className="grid grid-cols-2 lg:grid-cols-4 gap-8 max-w-6xl mx-auto">
|
||||
{stats.map((stat, index) => (
|
||||
<motion.div
|
||||
key={stat.label}
|
||||
className="text-center space-y-4"
|
||||
initial={{ opacity: 0, scale: 0.9 }}
|
||||
whileInView={{ opacity: 1, scale: 1 }}
|
||||
viewport={{ once: true, margin: '-100px' }}
|
||||
transition={{ duration: 0.5, delay: index * 0.1 }}
|
||||
>
|
||||
{/* Icon */}
|
||||
<div className="flex justify-center">
|
||||
<div className="inline-flex h-16 w-16 items-center justify-center rounded-full bg-primary/10">
|
||||
<stat.icon className="h-8 w-8 text-primary" aria-hidden="true" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Animated Counter */}
|
||||
<div className="bg-gradient-to-br from-primary to-primary/60 bg-clip-text text-transparent">
|
||||
<AnimatedCounter value={stat.value} suffix={stat.suffix} />
|
||||
</div>
|
||||
|
||||
{/* Label */}
|
||||
<div>
|
||||
<div className="font-semibold text-lg mb-1">{stat.label}</div>
|
||||
<div className="text-sm text-muted-foreground">{stat.description}</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
104
frontend/src/components/home/TechStackSection.tsx
Normal file
104
frontend/src/components/home/TechStackSection.tsx
Normal file
@@ -0,0 +1,104 @@
|
||||
/**
|
||||
* Tech Stack Section
|
||||
* Displays technology logos/badges with tooltips
|
||||
*/
|
||||
|
||||
'use client';
|
||||
|
||||
import { motion } from 'framer-motion';
|
||||
|
||||
interface Tech {
|
||||
name: string;
|
||||
description: string;
|
||||
color: string;
|
||||
}
|
||||
|
||||
const technologies: Tech[] = [
|
||||
{
|
||||
name: 'FastAPI',
|
||||
description: 'Async Python web framework, auto-docs, type hints',
|
||||
color: 'from-teal-500 to-green-600',
|
||||
},
|
||||
{
|
||||
name: 'Next.js 15',
|
||||
description: 'React 19, App Router, Server Components',
|
||||
color: 'from-slate-900 to-slate-700',
|
||||
},
|
||||
{
|
||||
name: 'PostgreSQL',
|
||||
description: 'Reliable, scalable SQL database',
|
||||
color: 'from-blue-600 to-blue-800',
|
||||
},
|
||||
{
|
||||
name: 'TypeScript',
|
||||
description: 'End-to-end type safety',
|
||||
color: 'from-blue-500 to-blue-700',
|
||||
},
|
||||
{
|
||||
name: 'Docker',
|
||||
description: 'Containerized deployment',
|
||||
color: 'from-blue-400 to-blue-600',
|
||||
},
|
||||
{
|
||||
name: 'TailwindCSS',
|
||||
description: 'Utility-first styling with OKLCH colors',
|
||||
color: 'from-cyan-500 to-blue-500',
|
||||
},
|
||||
{
|
||||
name: 'shadcn/ui',
|
||||
description: 'Accessible component library (New York variant)',
|
||||
color: 'from-slate-800 to-slate-600',
|
||||
},
|
||||
{
|
||||
name: 'Playwright',
|
||||
description: 'Reliable E2E testing (zero flaky tests)',
|
||||
color: 'from-green-600 to-emerald-700',
|
||||
},
|
||||
];
|
||||
|
||||
export function TechStackSection() {
|
||||
return (
|
||||
<section className="container mx-auto px-6 py-16 md:py-24 bg-muted/30">
|
||||
<motion.div
|
||||
className="text-center mb-12"
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: '-100px' }}
|
||||
transition={{ duration: 0.6 }}
|
||||
>
|
||||
<h2 className="text-3xl md:text-4xl font-bold mb-4">
|
||||
Modern, Type-Safe, Production-Grade Stack
|
||||
</h2>
|
||||
<p className="text-lg text-muted-foreground max-w-2xl mx-auto">
|
||||
Built with the best tools for full-stack development. Async architecture, type safety, and developer experience.
|
||||
</p>
|
||||
</motion.div>
|
||||
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 max-w-5xl mx-auto">
|
||||
{technologies.map((tech, index) => (
|
||||
<motion.div
|
||||
key={tech.name}
|
||||
className="group relative"
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: '-100px' }}
|
||||
transition={{ duration: 0.4, delay: index * 0.05 }}
|
||||
whileHover={{ scale: 1.05 }}
|
||||
>
|
||||
<div className="rounded-lg border bg-card p-6 text-center hover:shadow-lg transition-all">
|
||||
{/* Tech Badge */}
|
||||
<div className={`inline-block rounded-full bg-gradient-to-r ${tech.color} px-4 py-2 text-white font-semibold text-sm mb-2`}>
|
||||
{tech.name}
|
||||
</div>
|
||||
|
||||
{/* Hover Tooltip */}
|
||||
<div className="opacity-0 group-hover:opacity-100 transition-opacity">
|
||||
<p className="text-xs text-muted-foreground mt-2">{tech.description}</p>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
33
frontend/src/hooks/usePrefersReducedMotion.ts
Normal file
33
frontend/src/hooks/usePrefersReducedMotion.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
/**
|
||||
* Hook to detect if user prefers reduced motion
|
||||
* Respects prefers-reduced-motion media query for accessibility
|
||||
*/
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
export function usePrefersReducedMotion(): boolean {
|
||||
const [prefersReducedMotion, setPrefersReducedMotion] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
// Check if window is defined (SSR safety)
|
||||
if (typeof window === 'undefined') {
|
||||
return;
|
||||
}
|
||||
|
||||
const mediaQuery = window.matchMedia('(prefers-reduced-motion: reduce)');
|
||||
setPrefersReducedMotion(mediaQuery.matches);
|
||||
|
||||
// Listen for changes
|
||||
const handleChange = (event: MediaQueryListEvent) => {
|
||||
setPrefersReducedMotion(event.matches);
|
||||
};
|
||||
|
||||
mediaQuery.addEventListener('change', handleChange);
|
||||
|
||||
return () => {
|
||||
mediaQuery.removeEventListener('change', handleChange);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return prefersReducedMotion;
|
||||
}
|
||||
Reference in New Issue
Block a user