create-frootkit 1.1.4 → 1.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (109) hide show
  1. package/README.md +50 -16
  2. package/dist/addons/database.d.ts +3 -2
  3. package/dist/addons/database.d.ts.map +1 -1
  4. package/dist/addons/database.js +472 -13
  5. package/dist/addons/database.js.map +1 -1
  6. package/dist/index.js +93 -18
  7. package/dist/index.js.map +1 -1
  8. package/dist/scaffold.d.ts +1 -0
  9. package/dist/scaffold.d.ts.map +1 -1
  10. package/dist/scaffold.js +3 -3
  11. package/dist/scaffold.js.map +1 -1
  12. package/dist/template.d.ts +4 -5
  13. package/dist/template.d.ts.map +1 -1
  14. package/dist/template.js +6 -404
  15. package/dist/template.js.map +1 -1
  16. package/dist/templates/auth.d.ts +3 -0
  17. package/dist/templates/auth.d.ts.map +1 -0
  18. package/dist/templates/auth.js +487 -0
  19. package/dist/templates/auth.js.map +1 -0
  20. package/dist/templates/blog.d.ts +3 -0
  21. package/dist/templates/blog.d.ts.map +1 -0
  22. package/dist/templates/blog.js +369 -0
  23. package/dist/templates/blog.js.map +1 -0
  24. package/dist/templates/common.d.ts +5 -0
  25. package/dist/templates/common.d.ts.map +1 -0
  26. package/dist/templates/common.js +116 -0
  27. package/dist/templates/common.js.map +1 -0
  28. package/dist/templates/ecommerce.d.ts +3 -0
  29. package/dist/templates/ecommerce.d.ts.map +1 -0
  30. package/dist/templates/ecommerce.js +474 -0
  31. package/dist/templates/ecommerce.js.map +1 -0
  32. package/dist/templates/empty.d.ts +3 -0
  33. package/dist/templates/empty.d.ts.map +1 -0
  34. package/dist/templates/empty.js +85 -0
  35. package/dist/templates/empty.js.map +1 -0
  36. package/dist/templates/index.d.ts +23 -0
  37. package/dist/templates/index.d.ts.map +1 -0
  38. package/dist/templates/index.js +100 -0
  39. package/dist/templates/index.js.map +1 -0
  40. package/dist/templates/starter.d.ts +3 -0
  41. package/dist/templates/starter.d.ts.map +1 -0
  42. package/dist/templates/starter.js +308 -0
  43. package/dist/templates/starter.js.map +1 -0
  44. package/dist/templates/types.d.ts +13 -0
  45. package/dist/templates/types.d.ts.map +1 -0
  46. package/dist/templates/types.js +5 -0
  47. package/dist/templates/types.js.map +1 -0
  48. package/package.json +2 -1
  49. package/templates/auth/README.md +11 -0
  50. package/templates/auth/package.json +24 -0
  51. package/templates/auth/src/app/api/auth/route.ts +148 -0
  52. package/templates/auth/src/app/dashboard/layout.tsx +34 -0
  53. package/templates/auth/src/app/dashboard/page.tsx +72 -0
  54. package/templates/auth/src/app/layout.tsx +31 -0
  55. package/templates/auth/src/app/login/page.tsx +18 -0
  56. package/templates/auth/src/app/page.tsx +60 -0
  57. package/templates/auth/src/app/signup/page.tsx +18 -0
  58. package/templates/auth/src/components/AuthForm.tsx +77 -0
  59. package/templates/auth/src/components/DashboardNav.tsx +51 -0
  60. package/templates/auth/src/components/MetricCard.tsx +26 -0
  61. package/templates/auth/src/components/Navbar.tsx +29 -0
  62. package/templates/auth/src/lib/auth.ts +145 -0
  63. package/templates/auth/src/styles.css +19 -0
  64. package/templates/auth/tsconfig.json +13 -0
  65. package/templates/blog/README.md +11 -0
  66. package/templates/blog/package.json +24 -0
  67. package/templates/blog/src/app/api/posts/route.ts +36 -0
  68. package/templates/blog/src/app/blog/[slug]/page.tsx +74 -0
  69. package/templates/blog/src/app/layout.tsx +31 -0
  70. package/templates/blog/src/app/page.tsx +44 -0
  71. package/templates/blog/src/components/Footer.tsx +16 -0
  72. package/templates/blog/src/components/Navbar.tsx +31 -0
  73. package/templates/blog/src/components/Newsletter.tsx +32 -0
  74. package/templates/blog/src/components/PostCard.tsx +47 -0
  75. package/templates/blog/src/data/posts.ts +75 -0
  76. package/templates/blog/src/styles.css +19 -0
  77. package/templates/blog/tsconfig.json +13 -0
  78. package/templates/ecommerce/README.md +11 -0
  79. package/templates/ecommerce/package.json +24 -0
  80. package/templates/ecommerce/src/app/api/checkout/route.ts +19 -0
  81. package/templates/ecommerce/src/app/api/products/route.ts +28 -0
  82. package/templates/ecommerce/src/app/cart/page.tsx +22 -0
  83. package/templates/ecommerce/src/app/layout.tsx +31 -0
  84. package/templates/ecommerce/src/app/page.tsx +59 -0
  85. package/templates/ecommerce/src/app/products/[id]/page.tsx +88 -0
  86. package/templates/ecommerce/src/components/CartDrawer.tsx +64 -0
  87. package/templates/ecommerce/src/components/Footer.tsx +16 -0
  88. package/templates/ecommerce/src/components/Navbar.tsx +32 -0
  89. package/templates/ecommerce/src/components/ProductCard.tsx +52 -0
  90. package/templates/ecommerce/src/data/products.ts +87 -0
  91. package/templates/ecommerce/src/styles.css +19 -0
  92. package/templates/ecommerce/tsconfig.json +13 -0
  93. package/templates/empty/README.md +10 -0
  94. package/templates/empty/package.json +24 -0
  95. package/templates/empty/src/app/layout.tsx +29 -0
  96. package/templates/empty/src/app/page.tsx +38 -0
  97. package/templates/empty/src/styles.css +19 -0
  98. package/templates/empty/tsconfig.json +13 -0
  99. package/templates/starter/README.md +22 -0
  100. package/templates/starter/package.json +24 -0
  101. package/templates/starter/src/app/api/health/route.ts +23 -0
  102. package/templates/starter/src/app/layout.tsx +31 -0
  103. package/templates/starter/src/app/page.tsx +122 -0
  104. package/templates/starter/src/components/FeatureCard.tsx +25 -0
  105. package/templates/starter/src/components/Footer.tsx +16 -0
  106. package/templates/starter/src/components/Header.tsx +45 -0
  107. package/templates/starter/src/components/TerminalPreview.tsx +56 -0
  108. package/templates/starter/src/styles.css +19 -0
  109. package/templates/starter/tsconfig.json +13 -0
@@ -0,0 +1,34 @@
1
+ import type { ComponentChildren } from 'preact';
2
+ import { DashboardNav } from '../../components/DashboardNav.js';
3
+
4
+ export default function DashboardLayout({ children }: { children: ComponentChildren }) {
5
+ return (
6
+ <div class="min-h-screen flex bg-[#030712] text-slate-100">
7
+ <DashboardNav />
8
+ <div class="flex-1 flex flex-col min-w-0">
9
+ <header class="h-16 border-b border-slate-800/80 px-8 flex items-center justify-between bg-slate-950/40 backdrop-blur-md">
10
+ <div class="flex items-center gap-3">
11
+ <h1 class="text-sm font-bold text-white">Production Cluster</h1>
12
+ <span class="px-2 py-0.5 rounded-full bg-emerald-500/10 text-emerald-400 text-[10px] font-mono border border-emerald-500/20">
13
+ US-East (N. Virginia)
14
+ </span>
15
+ </div>
16
+
17
+ <div class="flex items-center gap-4 text-xs">
18
+ <span class="text-slate-400">Logged in as <strong class="text-white">admin@frootkit.dev</strong></span>
19
+ <a
20
+ href="/api/auth"
21
+ class="px-2.5 py-1 rounded-lg bg-slate-900 border border-slate-800 text-slate-300 font-mono hover:text-emerald-400"
22
+ >
23
+ Session API
24
+ </a>
25
+ </div>
26
+ </header>
27
+
28
+ <div class="p-8 flex-1 overflow-y-auto">
29
+ {children}
30
+ </div>
31
+ </div>
32
+ </div>
33
+ );
34
+ }
@@ -0,0 +1,72 @@
1
+ import { MetricCard } from '../../components/MetricCard.js';
2
+
3
+ export const metadata = {
4
+ title: 'Workspace Overview — FrootSaaS',
5
+ };
6
+
7
+ export default function DashboardOverviewPage() {
8
+ const metrics = [
9
+ { label: 'Monthly Recurring Revenue', value: '$42,850', change: '+14.2%', isPositive: true },
10
+ { label: 'Active Subscribers', value: '1,280', change: '+8.1%', isPositive: true },
11
+ { label: 'API Requests (24h)', value: '3.42M', change: '+22.4%', isPositive: true },
12
+ { label: 'Average Latency', value: '0.42ms', change: '-12.0%', isPositive: true },
13
+ ];
14
+
15
+ const recentUsers = [
16
+ { name: 'Sarah Connor', email: 'sarah@frootkit.dev', role: 'Owner', date: 'Just now' },
17
+ { name: 'John Doe', email: 'john@acme.corp', role: 'Developer', date: '2 hours ago' },
18
+ { name: 'Alex Wong', email: 'alex@startup.io', role: 'Designer', date: '1 day ago' },
19
+ { name: 'Maria Garcia', email: 'maria@scale.dev', role: 'Admin', date: '3 days ago' },
20
+ ];
21
+
22
+ return (
23
+ <div class="space-y-8">
24
+ <div>
25
+ <h2 class="text-2xl font-bold text-white mb-1">Metrics & Operations</h2>
26
+ <p class="text-xs text-slate-400">Real-time performance and subscriber growth analytics.</p>
27
+ </div>
28
+
29
+ <div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-6">
30
+ {metrics.map((m) => (
31
+ <MetricCard
32
+ key={m.label}
33
+ label={m.label}
34
+ value={m.value}
35
+ change={m.change}
36
+ isPositive={m.isPositive}
37
+ />
38
+ ))}
39
+ </div>
40
+
41
+ <div class="rounded-2xl border border-slate-800 bg-slate-900/40 p-6 backdrop-blur-sm">
42
+ <h3 class="text-sm font-bold text-white mb-4">Recent Workspace Activity</h3>
43
+ <div class="overflow-x-auto">
44
+ <table class="w-full text-left text-xs">
45
+ <thead>
46
+ <tr class="border-b border-slate-800 text-slate-500 font-mono">
47
+ <th class="pb-3 font-semibold">User</th>
48
+ <th class="pb-3 font-semibold">Email</th>
49
+ <th class="pb-3 font-semibold">Role</th>
50
+ <th class="pb-3 font-semibold">Time</th>
51
+ </tr>
52
+ </thead>
53
+ <tbody class="divide-y divide-slate-800/60">
54
+ {recentUsers.map((u) => (
55
+ <tr key={u.email} class="hover:bg-slate-900/60">
56
+ <td class="py-3 font-medium text-white">{u.name}</td>
57
+ <td class="py-3 text-slate-400 font-mono">{u.email}</td>
58
+ <td class="py-3">
59
+ <span class="px-2 py-0.5 rounded-full bg-slate-800 text-slate-300 font-mono text-[10px]">
60
+ {u.role}
61
+ </span>
62
+ </td>
63
+ <td class="py-3 text-slate-500">{u.date}</td>
64
+ </tr>
65
+ ))}
66
+ </tbody>
67
+ </table>
68
+ </div>
69
+ </div>
70
+ </div>
71
+ );
72
+ }
@@ -0,0 +1,31 @@
1
+ import type { ComponentChildren } from 'preact';
2
+
3
+ export interface RootLayoutProps {
4
+ children: ComponentChildren;
5
+ metadata?: {
6
+ title?: string;
7
+ description?: string;
8
+ };
9
+ }
10
+
11
+ export default function RootLayout({ children, metadata }: RootLayoutProps) {
12
+ const title = metadata?.title || 'FrootSaaS — Modern Cloud Platform';
13
+ const description =
14
+ metadata?.description ||
15
+ 'Fullstack SaaS application with authentication and real-time dashboard powered by Preact and FrootKit.';
16
+
17
+ return (
18
+ <html lang="en" class="dark scroll-smooth">
19
+ <head>
20
+ <meta charSet="UTF-8" />
21
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
22
+ <title>{title}</title>
23
+ <meta name="description" content={description} />
24
+ <link rel="stylesheet" href="/styles.css" />
25
+ </head>
26
+ <body class="bg-[#030712] text-slate-100 min-h-screen antialiased selection:bg-emerald-500/20 selection:text-emerald-300">
27
+ {children}
28
+ </body>
29
+ </html>
30
+ );
31
+ }
@@ -0,0 +1,18 @@
1
+ import { Navbar } from '../../components/Navbar.js';
2
+ import { AuthForm } from '../../components/AuthForm.js';
3
+
4
+ export const metadata = {
5
+ title: 'Sign In — FrootSaaS',
6
+ };
7
+
8
+ export default function LoginPage() {
9
+ return (
10
+ <div class="min-h-screen flex flex-col justify-between">
11
+ <Navbar />
12
+
13
+ <main class="max-w-md mx-auto px-6 py-16 flex-1 w-full flex flex-col justify-center">
14
+ <AuthForm mode="login" />
15
+ </main>
16
+ </div>
17
+ );
18
+ }
@@ -0,0 +1,60 @@
1
+ import { Navbar } from '../components/Navbar.js';
2
+
3
+ export const metadata = {
4
+ title: 'FrootSaaS — Next Generation Cloud Infrastructure',
5
+ description: 'Deploy lightning fast web services with enterprise grade observability.',
6
+ };
7
+
8
+ export default function SaaSMarketingPage() {
9
+ return (
10
+ <div class="min-h-screen flex flex-col justify-between">
11
+ <Navbar />
12
+
13
+ <main class="max-w-6xl mx-auto px-6 py-20 flex-1 flex flex-col justify-center">
14
+ <div class="text-center max-w-3xl mx-auto mb-16">
15
+ <div class="inline-flex items-center gap-2 px-3.5 py-1.5 rounded-full bg-emerald-500/10 border border-emerald-500/25 text-xs font-semibold text-emerald-400 mb-8 shadow-sm">
16
+ <span class="w-1.5 h-1.5 rounded-full bg-emerald-400 animate-pulse" />
17
+ Fullstack Authentication & Dashboard
18
+ </div>
19
+
20
+ <h1 class="text-4xl sm:text-6xl font-black text-white tracking-tight mb-6 leading-tight">
21
+ The Complete Platform for{' '}
22
+ <span class="bg-gradient-to-r from-emerald-400 via-teal-300 to-cyan-400 bg-clip-text text-transparent">
23
+ Modern SaaS
24
+ </span>
25
+ </h1>
26
+
27
+ <p class="text-slate-300 text-lg sm:text-xl leading-relaxed mb-10 max-w-2xl mx-auto">
28
+ Real Web Crypto password hashing, secure session cookies, protected routes, and an administrative dashboard layout ready out of the box.
29
+ </p>
30
+
31
+ <div class="flex flex-wrap items-center justify-center gap-4">
32
+ <a
33
+ href="/signup"
34
+ class="px-6 py-3 rounded-xl bg-emerald-500 text-slate-950 font-bold hover:bg-emerald-400 transition-all shadow-lg shadow-emerald-500/25 hover:-translate-y-0.5"
35
+ >
36
+ Start Free Trial &rarr;
37
+ </a>
38
+ <a
39
+ href="/dashboard"
40
+ class="px-6 py-3 rounded-xl bg-slate-900 border border-slate-800 text-slate-200 font-semibold hover:bg-slate-800 transition-all hover:-translate-y-0.5"
41
+ >
42
+ View Live Dashboard
43
+ </a>
44
+ </div>
45
+ </div>
46
+ </main>
47
+
48
+ <footer class="border-t border-slate-900 bg-slate-950/60 py-8 text-center text-xs text-slate-500">
49
+ <div class="max-w-6xl mx-auto px-6 flex justify-between">
50
+ <div>FrootSaaS &copy; {new Date().getFullYear()} OrbitNorth LLC</div>
51
+ <div class="flex gap-4">
52
+ <a href="/login" class="hover:text-slate-400">Sign In</a>
53
+ <a href="/signup" class="hover:text-slate-400">Sign Up</a>
54
+ <a href="/dashboard" class="hover:text-slate-400">Dashboard</a>
55
+ </div>
56
+ </div>
57
+ </footer>
58
+ </div>
59
+ );
60
+ }
@@ -0,0 +1,18 @@
1
+ import { Navbar } from '../../components/Navbar.js';
2
+ import { AuthForm } from '../../components/AuthForm.js';
3
+
4
+ export const metadata = {
5
+ title: 'Create Account — FrootSaaS',
6
+ };
7
+
8
+ export default function SignupPage() {
9
+ return (
10
+ <div class="min-h-screen flex flex-col justify-between">
11
+ <Navbar />
12
+
13
+ <main class="max-w-md mx-auto px-6 py-16 flex-1 w-full flex flex-col justify-center">
14
+ <AuthForm mode="signup" />
15
+ </main>
16
+ </div>
17
+ );
18
+ }
@@ -0,0 +1,77 @@
1
+ export interface AuthFormProps {
2
+ mode: 'login' | 'signup';
3
+ }
4
+
5
+ export function AuthForm({ mode }: AuthFormProps) {
6
+ const isLogin = mode === 'login';
7
+
8
+ return (
9
+ <div class="rounded-3xl border border-slate-800 bg-slate-900/60 p-8 backdrop-blur-xl shadow-2xl">
10
+ <div class="text-center mb-8">
11
+ <h1 class="text-2xl font-bold text-white mb-2">
12
+ {isLogin ? 'Sign in to your account' : 'Create your account'}
13
+ </h1>
14
+ <p class="text-slate-400 text-xs">
15
+ {isLogin ? "Don't have an account? " : 'Already have an account? '}
16
+ <a href={isLogin ? '/signup' : '/login'} class="text-emerald-400 font-semibold hover:underline">
17
+ {isLogin ? 'Sign up' : 'Sign in'}
18
+ </a>
19
+ </p>
20
+ </div>
21
+
22
+ {isLogin && (
23
+ <div class="mb-6 p-3 rounded-xl bg-emerald-500/10 border border-emerald-500/20 text-xs text-emerald-300 flex items-center justify-between">
24
+ <div>
25
+ <span class="font-bold">Demo:</span> admin@frootkit.dev &bull; password123
26
+ </div>
27
+ </div>
28
+ )}
29
+
30
+ <form action={`/api/auth?action=${mode}`} method="POST" class="space-y-4">
31
+ {!isLogin && (
32
+ <div>
33
+ <label class="block text-xs font-medium text-slate-300 mb-1.5">Full Name</label>
34
+ <input
35
+ type="text"
36
+ name="name"
37
+ placeholder="Sarah Connor"
38
+ required
39
+ class="w-full px-4 py-2.5 rounded-xl bg-slate-950 border border-slate-800 text-white text-sm focus:outline-none focus:border-emerald-500"
40
+ />
41
+ </div>
42
+ )}
43
+
44
+ <div>
45
+ <label class="block text-xs font-medium text-slate-300 mb-1.5">Email Address</label>
46
+ <input
47
+ type="email"
48
+ name="email"
49
+ placeholder="admin@frootkit.dev"
50
+ defaultValue={isLogin ? 'admin@frootkit.dev' : ''}
51
+ required
52
+ class="w-full px-4 py-2.5 rounded-xl bg-slate-950 border border-slate-800 text-white text-sm focus:outline-none focus:border-emerald-500"
53
+ />
54
+ </div>
55
+
56
+ <div>
57
+ <label class="block text-xs font-medium text-slate-300 mb-1.5">Password</label>
58
+ <input
59
+ type="password"
60
+ name="password"
61
+ placeholder="••••••••••••"
62
+ defaultValue={isLogin ? 'password123' : ''}
63
+ required
64
+ class="w-full px-4 py-2.5 rounded-xl bg-slate-950 border border-slate-800 text-white text-sm focus:outline-none focus:border-emerald-500"
65
+ />
66
+ </div>
67
+
68
+ <button
69
+ type="submit"
70
+ class="w-full py-3 rounded-xl bg-emerald-500 text-slate-950 font-bold text-sm hover:bg-emerald-400 transition-colors shadow-lg shadow-emerald-500/20 mt-2"
71
+ >
72
+ {isLogin ? 'Sign In &rarr;' : 'Create Account &rarr;'}
73
+ </button>
74
+ </form>
75
+ </div>
76
+ );
77
+ }
@@ -0,0 +1,51 @@
1
+ export function DashboardNav() {
2
+ const links = [
3
+ { label: 'Overview', href: '/dashboard', icon: '📊' },
4
+ { label: 'Analytics', href: '/dashboard#analytics', icon: '📈' },
5
+ { label: 'Team', href: '/dashboard#team', icon: '👥' },
6
+ { label: 'API Keys', href: '/api/auth', icon: '🔑' },
7
+ ];
8
+
9
+ return (
10
+ <aside class="w-64 border-r border-slate-800/80 bg-slate-950/80 p-6 flex flex-col justify-between shrink-0">
11
+ <div>
12
+ <div class="flex items-center gap-2.5 font-bold text-white text-lg mb-8">
13
+ <span class="w-7 h-7 rounded-lg bg-emerald-500 flex items-center justify-center text-slate-950 font-black text-xs">
14
+ F
15
+ </span>
16
+ <span>Workspace</span>
17
+ </div>
18
+
19
+ <nav class="space-y-1 text-sm font-medium">
20
+ {links.map((link) => (
21
+ <a
22
+ key={link.label}
23
+ href={link.href}
24
+ class="flex items-center gap-3 px-3.5 py-2.5 rounded-xl text-slate-300 hover:text-white hover:bg-slate-900/80 transition-colors"
25
+ >
26
+ <span>{link.icon}</span>
27
+ <span>{link.label}</span>
28
+ </a>
29
+ ))}
30
+ </nav>
31
+ </div>
32
+
33
+ <div class="pt-6 border-t border-slate-800/80 flex items-center justify-between">
34
+ <div class="flex items-center gap-2.5">
35
+ <div class="w-8 h-8 rounded-full bg-emerald-500/20 border border-emerald-500/30 flex items-center justify-center font-bold text-xs text-emerald-400">
36
+ SC
37
+ </div>
38
+ <div class="text-xs">
39
+ <div class="font-bold text-white">Sarah Connor</div>
40
+ <div class="text-slate-500">Pro Plan</div>
41
+ </div>
42
+ </div>
43
+ <form action="/api/auth?action=logout" method="POST">
44
+ <button type="submit" class="text-slate-500 hover:text-rose-400 text-xs transition-colors" title="Sign out">
45
+ &rarr;
46
+ </button>
47
+ </form>
48
+ </div>
49
+ </aside>
50
+ );
51
+ }
@@ -0,0 +1,26 @@
1
+ export interface MetricCardProps {
2
+ label: string;
3
+ value: string;
4
+ change: string;
5
+ isPositive: boolean;
6
+ }
7
+
8
+ export function MetricCard({ label, value, change, isPositive }: MetricCardProps) {
9
+ return (
10
+ <div class="rounded-2xl border border-slate-800 bg-slate-900/40 p-6 backdrop-blur-sm">
11
+ <div class="text-xs font-medium text-slate-400 mb-2">{label}</div>
12
+ <div class="flex items-baseline justify-between">
13
+ <div class="text-2xl sm:text-3xl font-black text-white">{value}</div>
14
+ <span
15
+ class={`text-xs font-mono font-bold px-2 py-0.5 rounded-full ${
16
+ isPositive
17
+ ? 'bg-emerald-500/10 text-emerald-400 border border-emerald-500/20'
18
+ : 'bg-rose-500/10 text-rose-400 border border-rose-500/20'
19
+ }`}
20
+ >
21
+ {change}
22
+ </span>
23
+ </div>
24
+ </div>
25
+ );
26
+ }
@@ -0,0 +1,29 @@
1
+ export function Navbar() {
2
+ return (
3
+ <header class="border-b border-slate-800/80 bg-slate-950/70 backdrop-blur-md sticky top-0 z-50">
4
+ <div class="max-w-6xl mx-auto px-6 h-16 flex items-center justify-between">
5
+ <a href="/" class="flex items-center gap-2.5 font-bold text-white text-lg tracking-tight hover:opacity-90">
6
+ <span class="w-8 h-8 rounded-xl bg-gradient-to-tr from-emerald-500 to-cyan-400 flex items-center justify-center text-slate-950 font-black text-sm">
7
+ F
8
+ </span>
9
+ <span>Froot<span class="text-emerald-400">SaaS</span></span>
10
+ </a>
11
+
12
+ <nav class="flex items-center gap-6 text-sm font-medium">
13
+ <a href="/dashboard" class="text-slate-300 hover:text-white transition-colors">
14
+ Dashboard
15
+ </a>
16
+ <a href="/login" class="text-slate-300 hover:text-white transition-colors">
17
+ Sign In
18
+ </a>
19
+ <a
20
+ href="/signup"
21
+ class="px-4 py-2 rounded-xl bg-emerald-500 text-slate-950 font-bold hover:bg-emerald-400 transition-all shadow-md shadow-emerald-500/20"
22
+ >
23
+ Get Started
24
+ </a>
25
+ </nav>
26
+ </div>
27
+ </header>
28
+ );
29
+ }
@@ -0,0 +1,145 @@
1
+ import { webcrypto } from 'node:crypto';
2
+
3
+ const crypto = globalThis.crypto || webcrypto;
4
+
5
+ export interface User {
6
+ id: string;
7
+ email: string;
8
+ name: string;
9
+ role: 'user' | 'admin';
10
+ createdAt: string;
11
+ }
12
+
13
+ export interface Session {
14
+ id: string;
15
+ token: string;
16
+ userId: string;
17
+ expiresAt: string;
18
+ }
19
+
20
+ // In-memory fallback stores (used when no database is attached)
21
+ const USERS_STORE = new Map<string, { user: User; passwordHash: string }>();
22
+ const SESSIONS_STORE = new Map<string, Session>();
23
+
24
+ /**
25
+ * Derives a PBKDF2 HMAC-SHA256 password hash using the standard Web Crypto API.
26
+ * Format: pbkdf2:sha256:100000:<saltHex>:<hashHex>
27
+ */
28
+ export async function hashPassword(password: string): Promise<string> {
29
+ const salt = crypto.getRandomValues(new Uint8Array(16));
30
+ const enc = new TextEncoder();
31
+ const keyMaterial = await crypto.subtle.importKey(
32
+ 'raw',
33
+ enc.encode(password),
34
+ { name: 'PBKDF2' },
35
+ false,
36
+ ['deriveBits']
37
+ );
38
+
39
+ const iterations = 100000;
40
+ const derivedBits = await crypto.subtle.deriveBits(
41
+ {
42
+ name: 'PBKDF2',
43
+ salt,
44
+ iterations,
45
+ hash: 'SHA-256',
46
+ },
47
+ keyMaterial,
48
+ 256
49
+ );
50
+
51
+ const saltHex = Buffer.from(salt).toString('hex');
52
+ const hashHex = Buffer.from(derivedBits).toString('hex');
53
+
54
+ return `pbkdf2:sha256:${iterations}:${saltHex}:${hashHex}`;
55
+ }
56
+
57
+ /**
58
+ * Verifies a plain text password against a stored PBKDF2 hash.
59
+ */
60
+ export async function verifyPassword(password: string, storedHash: string): Promise<boolean> {
61
+ const parts = storedHash.split(':');
62
+ if (parts.length !== 5 || parts[0] !== 'pbkdf2' || parts[1] !== 'sha256') {
63
+ return false;
64
+ }
65
+
66
+ const iterations = parseInt(parts[2]!, 10);
67
+ const salt = Buffer.from(parts[3]!, 'hex');
68
+ const expectedHashHex = parts[4]!;
69
+
70
+ const enc = new TextEncoder();
71
+ const keyMaterial = await crypto.subtle.importKey(
72
+ 'raw',
73
+ enc.encode(password),
74
+ { name: 'PBKDF2' },
75
+ false,
76
+ ['deriveBits']
77
+ );
78
+
79
+ const derivedBits = await crypto.subtle.deriveBits(
80
+ {
81
+ name: 'PBKDF2',
82
+ salt,
83
+ iterations,
84
+ hash: 'SHA-256',
85
+ },
86
+ keyMaterial,
87
+ 256
88
+ );
89
+
90
+ const derivedHashHex = Buffer.from(derivedBits).toString('hex');
91
+ return derivedHashHex === expectedHashHex;
92
+ }
93
+
94
+ /**
95
+ * Creates a new cryptographically secure session token.
96
+ */
97
+ export function createSessionToken(): string {
98
+ const bytes = crypto.getRandomValues(new Uint8Array(32));
99
+ return Buffer.from(bytes).toString('hex');
100
+ }
101
+
102
+ /**
103
+ * Parses cookies from an incoming request header string.
104
+ */
105
+ export function parseCookies(header?: string | null): Record<string, string> {
106
+ if (!header) return {};
107
+ const cookies: Record<string, string> = {};
108
+ const pairs = header.split(';');
109
+ for (const pair of pairs) {
110
+ const [key, val] = pair.trim().split('=');
111
+ if (key && val) {
112
+ cookies[key.trim()] = decodeURIComponent(val.trim());
113
+ }
114
+ }
115
+ return cookies;
116
+ }
117
+
118
+ /**
119
+ * Creates a Set-Cookie header for a session.
120
+ */
121
+ export function createSessionCookie(token: string, maxAgeSeconds: number = 86400 * 7): string {
122
+ return `froot_session=${encodeURIComponent(token)}; Path=/; HttpOnly; SameSite=Lax; Max-Age=${maxAgeSeconds}`;
123
+ }
124
+
125
+ /**
126
+ * Clears the session cookie.
127
+ */
128
+ export function clearSessionCookie(): string {
129
+ return 'froot_session=; Path=/; HttpOnly; SameSite=Lax; Max-Age=0';
130
+ }
131
+
132
+ // Pre-populate demo user: admin@frootkit.dev / password123
133
+ (async () => {
134
+ const demoHash = await hashPassword('password123');
135
+ const demoUser: User = {
136
+ id: 'usr_demo_admin',
137
+ email: 'admin@frootkit.dev',
138
+ name: 'Sarah Connor',
139
+ role: 'admin',
140
+ createdAt: new Date().toISOString(),
141
+ };
142
+ USERS_STORE.set(demoUser.email, { user: demoUser, passwordHash: demoHash });
143
+ })();
144
+
145
+ export { USERS_STORE, SESSIONS_STORE };
@@ -0,0 +1,19 @@
1
+ @import "tailwindcss";
2
+
3
+ @theme {
4
+ --color-brand-50: #ecfdf5;
5
+ --color-brand-100: #d1fae5;
6
+ --color-brand-200: #a7f3d0;
7
+ --color-brand-300: #6ee7b7;
8
+ --color-brand-400: #34d399;
9
+ --color-brand-500: #10b981;
10
+ --color-brand-600: #059669;
11
+ --color-brand-700: #047857;
12
+ --color-brand-800: #065f46;
13
+ --color-brand-900: #064e3b;
14
+ --color-brand-950: #022c22;
15
+ }
16
+
17
+ :root {
18
+ color-scheme: dark;
19
+ }
@@ -0,0 +1,13 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2024",
4
+ "module": "NodeNext",
5
+ "moduleResolution": "NodeNext",
6
+ "jsx": "react-jsx",
7
+ "jsxImportSource": "preact",
8
+ "strict": true,
9
+ "skipLibCheck": true,
10
+ "noEmit": true
11
+ },
12
+ "include": ["src/**/*"]
13
+ }
@@ -0,0 +1,11 @@
1
+ # {{PROJECT_NAME}} — Blog & Content
2
+
3
+ A fast content publishing platform built with **FrootKit**.
4
+
5
+ ## 🚀 Features
6
+
7
+ - Dynamic articles with `src/app/blog/[slug]/page.tsx`
8
+ - Pre-rendered static pages using `generateStaticParams()`
9
+ - Search and filtering via `src/app/api/posts/route.ts`
10
+ - Clean component separation (`Navbar`, `PostCard`, `Newsletter`, `Footer`)
11
+ - Ready for Drizzle / Prisma database integration!
@@ -0,0 +1,24 @@
1
+ {
2
+ "name": "{{PROJECT_NAME}}",
3
+ "version": "1.0.0",
4
+ "type": "module",
5
+ "scripts": {
6
+ "dev": "frootkit dev",
7
+ "build": "frootkit build",
8
+ "start": "frootkit start"
9
+ },
10
+ "dependencies": {
11
+ "frootkit": "npm:froot-kit@^1.3.1",
12
+ "froot-kit": "^1.3.1",
13
+ "frootjs": "^1.0.6",
14
+ "preact": "^11.1.0",
15
+ "preact-render-to-string": "^6.8.2"
16
+ },
17
+ "devDependencies": {
18
+ "@tailwindcss/cli": "^4.0.0",
19
+ "tailwindcss": "^4.0.0",
20
+ "@types/node": "^22.10.0",
21
+ "tsx": "^4.19.2",
22
+ "typescript": "^5.8.0"
23
+ }
24
+ }