create-frootkit 1.2.8 → 1.2.10

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 (52) hide show
  1. package/dist/addons/database.d.ts +13 -5
  2. package/dist/addons/database.d.ts.map +1 -1
  3. package/dist/addons/database.js +496 -94
  4. package/dist/addons/database.js.map +1 -1
  5. package/dist/scaffold.d.ts.map +1 -1
  6. package/dist/scaffold.js +5 -1
  7. package/dist/scaffold.js.map +1 -1
  8. package/package.json +1 -1
  9. package/templates/auth/src/app/api/auth/route.ts +17 -12
  10. package/templates/auth/src/app/dashboard/layout.tsx +14 -9
  11. package/templates/auth/src/app/dashboard/page.tsx +55 -24
  12. package/templates/auth/src/app/layout.tsx +3 -3
  13. package/templates/auth/src/app/login/page.tsx +5 -1
  14. package/templates/auth/src/app/page.tsx +103 -45
  15. package/templates/auth/src/app/signup/page.tsx +5 -1
  16. package/templates/auth/src/components/AuthForm.tsx +34 -19
  17. package/templates/auth/src/components/DashboardNav.tsx +97 -56
  18. package/templates/auth/src/components/MetricCard.tsx +13 -6
  19. package/templates/auth/src/components/Navbar.tsx +13 -12
  20. package/templates/auth/src/lib/auth.ts +123 -0
  21. package/templates/auth/src/styles.css +27 -5
  22. package/templates/blog/src/app/api/posts/route.ts +10 -34
  23. package/templates/blog/src/app/blog/[slug]/page.tsx +22 -21
  24. package/templates/blog/src/app/layout.tsx +4 -4
  25. package/templates/blog/src/app/page.tsx +19 -17
  26. package/templates/blog/src/components/Footer.tsx +6 -6
  27. package/templates/blog/src/components/Navbar.tsx +17 -15
  28. package/templates/blog/src/components/Newsletter.tsx +11 -9
  29. package/templates/blog/src/components/PostCard.tsx +29 -27
  30. package/templates/blog/src/data/posts.ts +48 -0
  31. package/templates/blog/src/styles.css +14 -5
  32. package/templates/ecommerce/src/app/api/products/route.ts +10 -26
  33. package/templates/ecommerce/src/app/cart/page.tsx +3 -3
  34. package/templates/ecommerce/src/app/layout.tsx +2 -2
  35. package/templates/ecommerce/src/app/page.tsx +41 -18
  36. package/templates/ecommerce/src/app/products/[id]/page.tsx +37 -28
  37. package/templates/ecommerce/src/components/CartDrawer.tsx +20 -17
  38. package/templates/ecommerce/src/components/Footer.tsx +6 -6
  39. package/templates/ecommerce/src/components/Navbar.tsx +21 -14
  40. package/templates/ecommerce/src/components/ProductCard.tsx +22 -15
  41. package/templates/ecommerce/src/data/products.ts +55 -11
  42. package/templates/ecommerce/src/styles.css +29 -5
  43. package/templates/empty/src/app/layout.tsx +5 -5
  44. package/templates/empty/src/app/page.tsx +10 -9
  45. package/templates/empty/src/styles.css +13 -15
  46. package/templates/starter/src/app/layout.tsx +4 -4
  47. package/templates/starter/src/app/page.tsx +41 -33
  48. package/templates/starter/src/components/FeatureCard.tsx +5 -5
  49. package/templates/starter/src/components/Footer.tsx +6 -6
  50. package/templates/starter/src/components/Header.tsx +18 -14
  51. package/templates/starter/src/components/TerminalPreview.tsx +30 -30
  52. package/templates/starter/src/styles.css +14 -5
@@ -6,24 +6,32 @@ export function AuthForm({ mode }: AuthFormProps) {
6
6
  const isLogin = mode === 'login';
7
7
 
8
8
  return (
9
- <div class="rounded-xl border border-[#1a2333] bg-[#0a0f18] p-8 shadow-xl">
9
+ <div class="rounded-xl border border-white/[0.08] bg-[#0f1117] p-7 sm:p-8 shadow-2xl shadow-black/50 relative overflow-hidden">
10
+ {/* Top ambient highlight */}
11
+ <div class="absolute inset-x-0 top-0 h-px bg-gradient-to-r from-transparent via-indigo-500/40 to-transparent" />
12
+
10
13
  <div class="mb-6">
11
- <h1 class="text-2xl font-bold text-white mb-1.5">
12
- {isLogin ? 'Sign in to workspace' : 'Create workspace account'}
14
+ <h1 class="text-xl font-semibold text-white tracking-tight mb-1">
15
+ {isLogin ? 'Sign in to workspace' : 'Create your account'}
13
16
  </h1>
14
- <p class="text-slate-400 text-xs">
17
+ <p class="text-zinc-400 text-xs">
15
18
  {isLogin ? 'Need an account? ' : 'Already registered? '}
16
- <a href={isLogin ? '/signup' : '/login'} class="text-emerald-400 font-semibold hover:underline">
19
+ <a href={isLogin ? '/signup' : '/login'} class="text-indigo-400 font-medium hover:text-indigo-300 transition-colors">
17
20
  {isLogin ? 'Sign up' : 'Sign in'}
18
21
  </a>
19
22
  </p>
20
23
  </div>
21
24
 
22
25
  {isLogin && (
23
- <div class="mb-6 p-3.5 rounded-lg bg-[#06090f] border border-[#1a2333] text-xs text-slate-300">
24
- <span class="text-emerald-400 font-bold block mb-1">Demo Credentials:</span>
25
- <div class="font-mono text-slate-300 text-[11px]">
26
- admin@frootkit.dev <span class="text-slate-500 mx-1">•</span> password123
26
+ <div class="mb-5 p-3 rounded-lg bg-white/[0.02] border border-white/[0.06] text-xs">
27
+ <div class="flex items-center gap-1.5 text-zinc-400 text-[11px] mb-1">
28
+ <span class="w-1.5 h-1.5 rounded-full bg-emerald-400" />
29
+ <span class="font-medium text-zinc-300">Quick Demo Credentials:</span>
30
+ </div>
31
+ <div class="font-mono text-zinc-300 text-[11px] bg-white/[0.03] px-2 py-1 rounded flex items-center justify-between">
32
+ <span>admin@frootkit.dev</span>
33
+ <span class="text-zinc-600">•</span>
34
+ <span>password123</span>
27
35
  </div>
28
36
  </div>
29
37
  )}
@@ -31,7 +39,7 @@ export function AuthForm({ mode }: AuthFormProps) {
31
39
  <form action={`/api/auth?action=${mode}`} method="POST" class="space-y-4">
32
40
  {!isLogin && (
33
41
  <div>
34
- <label class="block text-xs font-medium text-slate-300 mb-1.5" for="name">
42
+ <label class="block text-xs font-medium text-zinc-300 mb-1.5" for="name">
35
43
  Full Name
36
44
  </label>
37
45
  <input
@@ -40,13 +48,13 @@ export function AuthForm({ mode }: AuthFormProps) {
40
48
  name="name"
41
49
  placeholder="Developer Name"
42
50
  required
43
- class="w-full px-3.5 py-2 rounded-lg bg-[#06090f] border border-[#1a2333] text-white text-sm focus:outline-none focus:border-emerald-500 transition-colors"
51
+ class="w-full px-3 py-2 rounded-lg bg-[#08090a] border border-white/[0.08] text-white text-xs placeholder:text-zinc-600 focus:outline-none focus:border-indigo-500 focus:ring-1 focus:ring-indigo-500 transition-colors"
44
52
  />
45
53
  </div>
46
54
  )}
47
55
 
48
56
  <div>
49
- <label class="block text-xs font-medium text-slate-300 mb-1.5" for="email">
57
+ <label class="block text-xs font-medium text-zinc-300 mb-1.5" for="email">
50
58
  Email Address
51
59
  </label>
52
60
  <input
@@ -56,14 +64,21 @@ export function AuthForm({ mode }: AuthFormProps) {
56
64
  placeholder="admin@frootkit.dev"
57
65
  defaultValue={isLogin ? 'admin@frootkit.dev' : ''}
58
66
  required
59
- class="w-full px-3.5 py-2 rounded-lg bg-[#06090f] border border-[#1a2333] text-white text-sm focus:outline-none focus:border-emerald-500 transition-colors"
67
+ class="w-full px-3 py-2 rounded-lg bg-[#08090a] border border-white/[0.08] text-white text-xs placeholder:text-zinc-600 focus:outline-none focus:border-indigo-500 focus:ring-1 focus:ring-indigo-500 transition-colors"
60
68
  />
61
69
  </div>
62
70
 
63
71
  <div>
64
- <label class="block text-xs font-medium text-slate-300 mb-1.5" for="password">
65
- Password
66
- </label>
72
+ <div class="flex items-center justify-between mb-1.5">
73
+ <label class="block text-xs font-medium text-zinc-300" for="password">
74
+ Password
75
+ </label>
76
+ {isLogin && (
77
+ <span class="text-[11px] text-zinc-500">
78
+ min 8 characters
79
+ </span>
80
+ )}
81
+ </div>
67
82
  <input
68
83
  id="password"
69
84
  type="password"
@@ -71,16 +86,16 @@ export function AuthForm({ mode }: AuthFormProps) {
71
86
  placeholder="••••••••••••"
72
87
  defaultValue={isLogin ? 'password123' : ''}
73
88
  required
74
- class="w-full px-3.5 py-2 rounded-lg bg-[#06090f] border border-[#1a2333] text-white text-sm focus:outline-none focus:border-emerald-500 transition-colors"
89
+ class="w-full px-3 py-2 rounded-lg bg-[#08090a] border border-white/[0.08] text-white text-xs placeholder:text-zinc-600 focus:outline-none focus:border-indigo-500 focus:ring-1 focus:ring-indigo-500 transition-colors"
75
90
  />
76
91
  </div>
77
92
 
78
93
  <button
79
94
  type="submit"
80
- class="w-full py-2.5 rounded-lg bg-emerald-500 text-slate-950 font-semibold text-sm hover:bg-emerald-400 transition-colors mt-2 flex items-center justify-center gap-2"
95
+ class="w-full py-2.5 rounded-lg bg-white text-zinc-950 font-medium text-xs hover:bg-zinc-200 transition-all mt-3 flex items-center justify-center gap-2 group shadow-sm"
81
96
  >
82
97
  <span>{isLogin ? 'Sign In' : 'Create Account'}</span>
83
- <span>→</span>
98
+ <span class="text-zinc-500 group-hover:translate-x-0.5 transition-transform">→</span>
84
99
  </button>
85
100
  </form>
86
101
  </div>
@@ -1,80 +1,121 @@
1
1
  export function DashboardNav() {
2
2
  return (
3
- <aside class="w-64 border-r border-[#1a2333] bg-[#0a0f18] p-5 flex flex-col justify-between shrink-0 min-h-screen">
4
- <div>
5
- {/* Brand Header */}
6
- <div class="flex items-center gap-3 font-bold text-white text-base mb-6 px-1">
7
- <div class="w-8 h-8 rounded-lg bg-emerald-500/10 border border-emerald-500/30 flex items-center justify-center text-emerald-400 shrink-0">
8
- <svg width="18" height="18" style="width: 18px; height: 18px; min-width: 18px; min-height: 18px;" class="w-4.5 h-4.5 shrink-0" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
9
- <path stroke-linecap="round" stroke-linejoin="round" d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10" />
3
+ <aside class="w-60 border-r border-white/[0.08] bg-[#0d0e12] p-3 flex flex-col justify-between shrink-0 min-h-screen">
4
+ <div class="space-y-6">
5
+ {/* Workspace Brand / Org Selector */}
6
+ <div class="px-2 pt-1">
7
+ <div class="flex items-center justify-between p-1.5 rounded-lg hover:bg-white/[0.04] transition-colors cursor-pointer border border-transparent hover:border-white/[0.06]">
8
+ <div class="flex items-center gap-2.5 min-w-0">
9
+ <div class="w-6 h-6 rounded-md bg-indigo-500/15 border border-indigo-500/30 flex items-center justify-center text-indigo-400 shrink-0">
10
+ <svg style="width: 13px; height: 13px; min-width: 13px; min-height: 13px;" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
11
+ <path d="M12 2L2 7l10 5 10-5-10-5zM2 17l10 5 10-5M2 12l10 5 10-5" />
12
+ </svg>
13
+ </div>
14
+ <div class="truncate">
15
+ <div class="text-xs font-semibold text-zinc-200 truncate">Orbit Workspace</div>
16
+ <div class="text-[10px] text-zinc-500 truncate">Production Cluster</div>
17
+ </div>
18
+ </div>
19
+ <svg style="width: 12px; height: 12px; min-width: 12px; min-height: 12px;" class="text-zinc-500 shrink-0" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
20
+ <path stroke-linecap="round" stroke-linejoin="round" d="M19 9l-7 7-7-7" />
10
21
  </svg>
11
22
  </div>
12
- <span class="tracking-tight">Workspace</span>
13
23
  </div>
14
24
 
15
- {/* Navigation Links */}
16
- <nav class="space-y-1 text-sm font-medium">
17
- <a
18
- href="/dashboard"
19
- class="flex items-center gap-3 px-3 py-2 rounded-lg bg-emerald-500/10 text-emerald-400 border border-emerald-500/25 transition-colors"
20
- >
21
- <svg width="18" height="18" style="width: 18px; height: 18px; min-width: 18px; min-height: 18px;" class="w-4.5 h-4.5 shrink-0 text-emerald-400" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2">
22
- <path stroke-linecap="round" stroke-linejoin="round" d="M4 6a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2V6zM14 6a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2V6zM4 16a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2v-2zM14 16a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2v-2z" />
23
- </svg>
24
- <span>Overview</span>
25
- </a>
25
+ {/* Navigation Sections */}
26
+ <div class="space-y-4">
27
+ <div>
28
+ <div class="px-2.5 mb-1.5 text-[10px] font-semibold text-zinc-500 uppercase tracking-wider">
29
+ Platform
30
+ </div>
31
+ <nav class="space-y-0.5 text-xs font-medium">
32
+ <a
33
+ href="/dashboard"
34
+ class="flex items-center gap-2.5 px-2.5 py-1.5 rounded-md bg-white/[0.08] text-white border border-white/[0.08] shadow-[inset_0_1px_0_0_rgba(255,255,255,0.06)]"
35
+ >
36
+ <span class="w-4 h-4 flex items-center justify-center shrink-0 text-indigo-400">
37
+ <svg style="width: 14px; height: 14px; min-width: 14px; min-height: 14px;" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
38
+ <rect x="3" y="3" width="7" height="7" />
39
+ <rect x="14" y="3" width="7" height="7" />
40
+ <rect x="14" y="14" width="7" height="7" />
41
+ <rect x="3" y="14" width="7" height="7" />
42
+ </svg>
43
+ </span>
44
+ <span>Overview</span>
45
+ </a>
26
46
 
27
- <a
28
- href="/dashboard#analytics"
29
- class="flex items-center gap-3 px-3 py-2 rounded-lg text-slate-400 hover:text-white hover:bg-white/[0.04] transition-colors"
30
- >
31
- <svg width="18" height="18" style="width: 18px; height: 18px; min-width: 18px; min-height: 18px;" class="w-4.5 h-4.5 shrink-0 text-slate-400" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2">
32
- <path stroke-linecap="round" stroke-linejoin="round" d="M13 7h8m0 0v8m0-8l-8 8-4-4-6 6" />
33
- </svg>
34
- <span>Telemetry</span>
35
- </a>
47
+ <a
48
+ href="/dashboard#analytics"
49
+ class="flex items-center gap-2.5 px-2.5 py-1.5 rounded-md text-zinc-400 hover:text-zinc-200 hover:bg-white/[0.04] transition-colors"
50
+ >
51
+ <span class="w-4 h-4 flex items-center justify-center shrink-0 text-zinc-500">
52
+ <svg style="width: 14px; height: 14px; min-width: 14px; min-height: 14px;" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
53
+ <polyline points="22 12 18 12 15 21 9 3 6 12 2 12" />
54
+ </svg>
55
+ </span>
56
+ <span>Telemetry</span>
57
+ </a>
36
58
 
37
- <a
38
- href="/dashboard#team"
39
- class="flex items-center gap-3 px-3 py-2 rounded-lg text-slate-400 hover:text-white hover:bg-white/[0.04] transition-colors"
40
- >
41
- <svg width="18" height="18" style="width: 18px; height: 18px; min-width: 18px; min-height: 18px;" class="w-4.5 h-4.5 shrink-0 text-slate-400" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2">
42
- <path stroke-linecap="round" stroke-linejoin="round" d="M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197M13 7a4 4 0 11-8 0 4 4 0 018 0z" />
43
- </svg>
44
- <span>Members</span>
45
- </a>
59
+ <a
60
+ href="/dashboard#team"
61
+ class="flex items-center gap-2.5 px-2.5 py-1.5 rounded-md text-zinc-400 hover:text-zinc-200 hover:bg-white/[0.04] transition-colors"
62
+ >
63
+ <span class="w-4 h-4 flex items-center justify-center shrink-0 text-zinc-500">
64
+ <svg style="width: 14px; height: 14px; min-width: 14px; min-height: 14px;" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
65
+ <path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2" />
66
+ <circle cx="9" cy="7" r="4" />
67
+ <path d="M23 21v-2a4 4 0 0 0-3-3.87" />
68
+ <path d="M16 3.13a4 4 0 0 1 0 7.75" />
69
+ </svg>
70
+ </span>
71
+ <span>Members</span>
72
+ </a>
73
+ </nav>
74
+ </div>
46
75
 
47
- <a
48
- href="/api/auth"
49
- class="flex items-center gap-3 px-3 py-2 rounded-lg text-slate-400 hover:text-white hover:bg-white/[0.04] transition-colors"
50
- >
51
- <svg width="18" height="18" style="width: 18px; height: 18px; min-width: 18px; min-height: 18px;" class="w-4.5 h-4.5 shrink-0 text-slate-400" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2">
52
- <path stroke-linecap="round" stroke-linejoin="round" d="M15 7a2 2 0 012 2m4 0a6 6 0 01-7.743 5.743L11 17H9v2H7v2H4a1 1 0 01-1-1v-2.586a1 1 0 01.293-.707l5.964-5.964A6 6 0 1121 9z" />
53
- </svg>
54
- <span>Session API</span>
55
- </a>
56
- </nav>
76
+ <div>
77
+ <div class="px-2.5 mb-1.5 text-[10px] font-semibold text-zinc-500 uppercase tracking-wider">
78
+ Developer
79
+ </div>
80
+ <nav class="space-y-0.5 text-xs font-medium">
81
+ <a
82
+ href="/api/auth"
83
+ class="flex items-center gap-2.5 px-2.5 py-1.5 rounded-md text-zinc-400 hover:text-zinc-200 hover:bg-white/[0.04] transition-colors"
84
+ >
85
+ <span class="w-4 h-4 flex items-center justify-center shrink-0 text-zinc-500">
86
+ <svg style="width: 14px; height: 14px; min-width: 14px; min-height: 14px;" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
87
+ <polyline points="16 18 22 12 16 6" />
88
+ <polyline points="8 6 2 12 8 18" />
89
+ </svg>
90
+ </span>
91
+ <span>Session API</span>
92
+ </a>
93
+ </nav>
94
+ </div>
95
+ </div>
57
96
  </div>
58
97
 
59
98
  {/* User profile & Sign out */}
60
- <div class="pt-4 border-t border-[#1a2333] flex items-center justify-between">
99
+ <div class="p-2 border-t border-white/[0.06] flex items-center justify-between">
61
100
  <div class="flex items-center gap-2.5 min-w-0">
62
- <div class="w-8 h-8 rounded-full bg-emerald-500/10 border border-emerald-500/30 flex items-center justify-center font-bold text-xs text-emerald-400 font-mono shrink-0">
101
+ <div class="w-7 h-7 rounded-full bg-gradient-to-br from-indigo-500 to-purple-600 flex items-center justify-center font-medium text-[11px] text-white shrink-0">
63
102
  AD
64
103
  </div>
65
- <div class="text-xs truncate">
66
- <div class="font-semibold text-white truncate">admin@frootkit.dev</div>
67
- <div class="text-slate-400 text-[11px]">Administrator</div>
104
+ <div class="text-xs min-w-0 truncate">
105
+ <div class="font-medium text-zinc-200 truncate">admin@frootkit.dev</div>
106
+ <div class="text-zinc-500 text-[10px] truncate">Owner</div>
68
107
  </div>
69
108
  </div>
70
- <form action="/api/auth?action=logout" method="POST" class="shrink-0 ml-2">
109
+ <form action="/api/auth?action=logout" method="POST" class="shrink-0 ml-1">
71
110
  <button
72
111
  type="submit"
73
- class="p-1.5 rounded-md text-slate-400 hover:text-rose-400 hover:bg-rose-500/10 transition-colors"
112
+ class="p-1.5 rounded-md text-zinc-500 hover:text-zinc-200 hover:bg-white/[0.06] transition-colors"
74
113
  title="Sign out"
75
114
  >
76
- <svg width="16" height="16" style="width: 16px; height: 16px; min-width: 16px; min-height: 16px;" class="w-4 h-4 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2">
77
- <path stroke-linecap="round" stroke-linejoin="round" d="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1" />
115
+ <svg style="width: 14px; height: 14px; min-width: 14px; min-height: 14px;" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
116
+ <path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4" />
117
+ <polyline points="16 17 21 12 16 7" />
118
+ <line x1="21" y1="12" x2="9" y2="12" />
78
119
  </svg>
79
120
  </button>
80
121
  </form>
@@ -3,26 +3,33 @@ export interface MetricCardProps {
3
3
  value: string;
4
4
  change: string;
5
5
  isPositive: boolean;
6
+ context?: string;
6
7
  }
7
8
 
8
- export function MetricCard({ label, value, change, isPositive }: MetricCardProps) {
9
+ export function MetricCard({ label, value, change, isPositive, context = 'vs last 30d' }: MetricCardProps) {
9
10
  return (
10
- <div class="rounded-xl border border-[#1a2333] bg-[#0a0f18] p-5 flex flex-col justify-between shadow-sm">
11
- <div class="text-xs font-medium text-slate-400 mb-3 tracking-wide">
11
+ <div class="rounded-xl border border-white/[0.08] bg-[#0f1117] p-4 sm:p-5 flex flex-col justify-between shadow-[inset_0_1px_0_0_rgba(255,255,255,0.05)] transition-all hover:border-white/[0.14]">
12
+ <div class="text-xs font-medium text-zinc-400 mb-3 tracking-normal">
12
13
  {label}
13
14
  </div>
14
15
  <div class="flex items-baseline justify-between gap-3">
15
- <div class="text-2xl font-bold text-white tracking-tight">{value}</div>
16
+ <div class="text-2xl sm:text-3xl font-semibold text-white tracking-tight tabular-nums">
17
+ {value}
18
+ </div>
16
19
  <span
17
- class={`text-xs font-semibold px-2 py-0.5 rounded-md border shrink-0 ${
20
+ class={`inline-flex items-center gap-1 text-[11px] font-medium px-2 py-0.5 rounded-full border shrink-0 ${
18
21
  isPositive
19
22
  ? 'bg-emerald-500/10 text-emerald-400 border-emerald-500/20'
20
23
  : 'bg-rose-500/10 text-rose-400 border-rose-500/20'
21
24
  }`}
22
25
  >
23
- {change}
26
+ <span>{isPositive ? '↑' : '↓'}</span>
27
+ <span>{change}</span>
24
28
  </span>
25
29
  </div>
30
+ <div class="mt-2 text-[11px] text-zinc-500">
31
+ {context}
32
+ </div>
26
33
  </div>
27
34
  );
28
35
  }
@@ -1,29 +1,30 @@
1
1
  export function Navbar() {
2
2
  return (
3
- <header class="border-b border-[#1a2333] bg-[#06090f]/95 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-base tracking-tight hover:text-emerald-400 transition-colors">
6
- <div class="w-8 h-8 rounded-lg bg-emerald-500/10 border border-emerald-500/30 flex items-center justify-center text-emerald-400 shrink-0">
7
- <svg width="18" height="18" class="w-4.5 h-4.5 shrink-0" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
8
- <path stroke-linecap="round" stroke-linejoin="round" d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z" />
3
+ <header class="border-b border-white/[0.08] bg-[#08090a]/80 backdrop-blur-md sticky top-0 z-50">
4
+ <div class="max-w-6xl mx-auto px-6 h-14 flex items-center justify-between">
5
+ <a href="/" class="flex items-center gap-2.5 font-semibold text-zinc-100 text-sm tracking-tight group">
6
+ <div class="w-7 h-7 rounded-md bg-indigo-500/10 border border-indigo-500/25 flex items-center justify-center text-indigo-400 group-hover:border-indigo-400/50 transition-colors shrink-0">
7
+ <svg style="width: 14px; height: 14px;" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
8
+ <rect x="3" y="11" width="18" height="11" rx="2" ry="2" />
9
+ <path d="M7 11V7a5 5 0 0 1 10 0v4" />
9
10
  </svg>
10
11
  </div>
11
- <span>Froot<span class="text-emerald-400">Auth</span></span>
12
+ <span>Froot<span class="text-indigo-400">Auth</span></span>
12
13
  </a>
13
14
 
14
- <nav class="flex items-center gap-6 text-sm font-medium">
15
- <a href="/dashboard" class="text-slate-300 hover:text-white transition-colors">
15
+ <nav class="flex items-center gap-5 text-xs font-medium">
16
+ <a href="/dashboard" class="text-zinc-400 hover:text-zinc-200 transition-colors">
16
17
  Dashboard
17
18
  </a>
18
- <a href="/login" class="text-slate-300 hover:text-white transition-colors">
19
+ <a href="/login" class="text-zinc-400 hover:text-zinc-200 transition-colors">
19
20
  Sign In
20
21
  </a>
21
22
  <a
22
23
  href="/signup"
23
- class="px-4 py-2 rounded-lg bg-emerald-500 text-slate-950 font-semibold hover:bg-emerald-400 transition-colors text-xs flex items-center gap-1.5"
24
+ class="px-3 py-1.5 rounded-md bg-white text-zinc-950 font-medium hover:bg-zinc-200 transition-all text-xs shadow-sm flex items-center gap-1.5"
24
25
  >
25
26
  <span>Get Started</span>
26
- <span>→</span>
27
+ <span class="text-zinc-500">→</span>
27
28
  </a>
28
29
  </nav>
29
30
  </div>
@@ -129,6 +129,129 @@ export function clearSessionCookie(): string {
129
129
  return 'froot_session=; Path=/; HttpOnly; SameSite=Lax; Max-Age=0';
130
130
  }
131
131
 
132
+ /**
133
+ * Finds user record by email from DB (with fallback to in-memory).
134
+ */
135
+ export async function findUserByEmail(email: string): Promise<{ user: User; passwordHash: string } | null> {
136
+ try {
137
+ const { db } = await import('../db/index.js');
138
+ const { users } = await import('../db/schema.js');
139
+ const { eq } = await import('drizzle-orm');
140
+ const rows = await db.select().from(users).where(eq(users.email, email));
141
+ if (rows && rows.length > 0) {
142
+ const u = rows[0];
143
+ return {
144
+ user: {
145
+ id: u.id,
146
+ email: u.email,
147
+ name: u.name,
148
+ role: u.role as 'user' | 'admin',
149
+ createdAt: u.createdAt,
150
+ },
151
+ passwordHash: u.passwordHash,
152
+ };
153
+ }
154
+ } catch {}
155
+ return USERS_STORE.get(email) || null;
156
+ }
157
+
158
+ /**
159
+ * Finds user record by ID.
160
+ */
161
+ export async function findUserById(id: string): Promise<User | null> {
162
+ try {
163
+ const { db } = await import('../db/index.js');
164
+ const { users } = await import('../db/schema.js');
165
+ const { eq } = await import('drizzle-orm');
166
+ const rows = await db.select().from(users).where(eq(users.id, id));
167
+ if (rows && rows.length > 0) {
168
+ const u = rows[0];
169
+ return {
170
+ id: u.id,
171
+ email: u.email,
172
+ name: u.name,
173
+ role: u.role as 'user' | 'admin',
174
+ createdAt: u.createdAt,
175
+ };
176
+ }
177
+ } catch {}
178
+ for (const entry of USERS_STORE.values()) {
179
+ if (entry.user.id === id) return entry.user;
180
+ }
181
+ return null;
182
+ }
183
+
184
+ /**
185
+ * Saves a new user to DB and in-memory store.
186
+ */
187
+ export async function saveUser(user: User, passwordHash: string): Promise<void> {
188
+ try {
189
+ const { db } = await import('../db/index.js');
190
+ const { users } = await import('../db/schema.js');
191
+ await db.insert(users).values({
192
+ id: user.id,
193
+ email: user.email,
194
+ name: user.name,
195
+ role: user.role,
196
+ passwordHash,
197
+ createdAt: user.createdAt,
198
+ });
199
+ } catch {}
200
+ USERS_STORE.set(user.email, { user, passwordHash });
201
+ }
202
+
203
+ /**
204
+ * Saves a session token to DB and in-memory store.
205
+ */
206
+ export async function saveSession(session: Session): Promise<void> {
207
+ try {
208
+ const { db } = await import('../db/index.js');
209
+ const { sessions } = await import('../db/schema.js');
210
+ await db.insert(sessions).values({
211
+ id: session.id,
212
+ token: session.token,
213
+ userId: session.userId,
214
+ expiresAt: session.expiresAt,
215
+ });
216
+ } catch {}
217
+ SESSIONS_STORE.set(session.token, session);
218
+ }
219
+
220
+ /**
221
+ * Finds session by token.
222
+ */
223
+ export async function findSession(token: string): Promise<Session | null> {
224
+ try {
225
+ const { db } = await import('../db/index.js');
226
+ const { sessions } = await import('../db/schema.js');
227
+ const { eq } = await import('drizzle-orm');
228
+ const rows = await db.select().from(sessions).where(eq(sessions.token, token));
229
+ if (rows && rows.length > 0) {
230
+ const s = rows[0];
231
+ return {
232
+ id: s.id,
233
+ token: s.token,
234
+ userId: s.userId,
235
+ expiresAt: s.expiresAt,
236
+ };
237
+ }
238
+ } catch {}
239
+ return SESSIONS_STORE.get(token) || null;
240
+ }
241
+
242
+ /**
243
+ * Deletes a session by token.
244
+ */
245
+ export async function deleteSession(token: string): Promise<void> {
246
+ try {
247
+ const { db } = await import('../db/index.js');
248
+ const { sessions } = await import('../db/schema.js');
249
+ const { eq } = await import('drizzle-orm');
250
+ await db.delete(sessions).where(eq(sessions.token, token));
251
+ } catch {}
252
+ SESSIONS_STORE.delete(token);
253
+ }
254
+
132
255
  // Pre-populate demo user: admin@frootkit.dev / password123
133
256
  (async () => {
134
257
  const demoHash = await hashPassword('password123');
@@ -4,28 +4,50 @@
4
4
  --font-sans: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
5
5
  --font-mono: 'JetBrains Mono', ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
6
6
 
7
- --color-brand-primary: #10b981;
8
- --color-brand-hover: #34d399;
7
+ --color-brand-primary: #6366f1;
8
+ --color-brand-hover: #4f46e5;
9
9
  }
10
10
 
11
11
  :root {
12
12
  color-scheme: dark;
13
+ --bg-canvas: #08090a;
14
+ --bg-surface: #0f1117;
15
+ --bg-elevated: #151822;
16
+ --border-subtle: rgba(255, 255, 255, 0.08);
17
+ --border-strong: rgba(255, 255, 255, 0.16);
18
+ --text-primary: #f4f4f5;
19
+ --text-secondary: #9496a1;
20
+ --text-muted: #5c6370;
13
21
  }
14
22
 
15
23
  html {
16
- background-color: #06090f;
17
- color: #f8fafc;
24
+ background-color: var(--bg-canvas);
25
+ color: var(--text-primary);
18
26
  font-family: var(--font-sans);
19
27
  scroll-behavior: smooth;
20
28
  -webkit-font-smoothing: antialiased;
29
+ -moz-osx-font-smoothing: grayscale;
21
30
  }
22
31
 
23
- /* SVG safety: prevents flex ballooning across all browsers */
32
+ /* Strict SVG safety to prevent flex ballooning */
24
33
  svg {
25
34
  flex-shrink: 0;
26
35
  max-width: 100%;
27
36
  }
28
37
 
38
+ /* Linear-style subtle card highlight */
39
+ .linear-card {
40
+ background-color: var(--bg-surface);
41
+ border: 1px solid var(--border-subtle);
42
+ box-shadow: inset 0 1px 0 0 rgba(255, 255, 255, 0.06);
43
+ border-radius: 0.75rem;
44
+ transition: border-color 0.15s ease, background-color 0.15s ease, box-shadow 0.15s ease;
45
+ }
46
+
47
+ .linear-card:hover {
48
+ border-color: var(--border-strong);
49
+ }
50
+
29
51
  /* Accessibility: Mandatory Reduced Motion */
30
52
  @media (prefers-reduced-motion: reduce) {
31
53
  *, *::before, *::after {
@@ -1,36 +1,12 @@
1
- import { POSTS } from '../../../data/posts.js';
2
-
3
- export async function GET(req: Request): Promise<Response> {
4
- const url = new URL(req.url);
5
- const tag = url.searchParams.get('tag');
6
- const q = url.searchParams.get('q')?.toLowerCase();
7
-
8
- let results = [...POSTS];
9
-
10
- if (tag) {
11
- results = results.filter((p) => p.tags.some((t) => t.toLowerCase() === tag.toLowerCase()));
12
- }
13
-
14
- if (q) {
15
- results = results.filter(
16
- (p) =>
17
- p.title.toLowerCase().includes(q) ||
18
- p.summary.toLowerCase().includes(q) ||
19
- p.category.toLowerCase().includes(q)
20
- );
21
- }
22
-
23
- return Response.json(
24
- {
25
- posts: results,
26
- total: results.length,
27
- timestamp: new Date().toISOString(),
1
+ import { getPosts } from '../../../data/posts.js';
2
+
3
+ export async function GET(): Promise<Response> {
4
+ const posts = await getPosts();
5
+ return Response.json(posts, {
6
+ status: 200,
7
+ headers: {
8
+ 'content-type': 'application/json',
9
+ 'cache-control': 'public, max-age=60',
28
10
  },
29
- {
30
- status: 200,
31
- headers: {
32
- 'content-type': 'application/json',
33
- },
34
- }
35
- );
11
+ });
36
12
  }