create-spine 1.0.2 → 1.0.4

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 (42) hide show
  1. package/final-test/AGENTS.md +5 -0
  2. package/final-test/CLAUDE.md +1 -0
  3. package/final-test/README.md +37 -0
  4. package/final-test/app/Refund/page.tsx +340 -0
  5. package/final-test/app/api/auth/[...nextauth]/route.ts +6 -0
  6. package/final-test/app/api/generate/route.ts +57 -0
  7. package/final-test/app/dashboard/page.tsx +713 -0
  8. package/final-test/app/faq/page.tsx +343 -0
  9. package/final-test/app/globals.css +130 -0
  10. package/final-test/app/layout.tsx +33 -0
  11. package/final-test/app/page.tsx +650 -0
  12. package/final-test/app/pricing/page.tsx +23 -0
  13. package/final-test/app/privacy/page.tsx +328 -0
  14. package/final-test/app/terms/page.tsx +12 -0
  15. package/final-test/components/ui/badge.tsx +52 -0
  16. package/final-test/components/ui/button.tsx +58 -0
  17. package/final-test/components/ui/card.tsx +103 -0
  18. package/final-test/components/ui/separator.tsx +25 -0
  19. package/final-test/components/ui/textarea.tsx +18 -0
  20. package/final-test/components/ui/ui/footer/page.tsx +103 -0
  21. package/final-test/components/ui/ui/navbar/navbar.tsx +0 -0
  22. package/final-test/components.json +25 -0
  23. package/final-test/desktop.ini +6 -0
  24. package/final-test/eslint.config.mjs +18 -0
  25. package/final-test/lib/auth.ts +19 -0
  26. package/final-test/lib/prisma.ts +7 -0
  27. package/final-test/lib/utils.ts +6 -0
  28. package/final-test/next.config.ts +7 -0
  29. package/final-test/package-lock.json +9794 -0
  30. package/final-test/package.json +35 -0
  31. package/final-test/postcss.config.mjs +7 -0
  32. package/final-test/public/X.svg +1 -0
  33. package/final-test/public/github.svg +2 -0
  34. package/final-test/public/google.svg +2 -0
  35. package/final-test/src/components/ui/badge.tsx +58 -0
  36. package/final-test/src/components/ui/button.tsx +90 -0
  37. package/final-test/src/components/ui/card.tsx +103 -0
  38. package/final-test/src/components/ui/textarea.tsx +29 -0
  39. package/final-test/src/lib/utils.ts +6 -0
  40. package/final-test/tsconfig.json +34 -0
  41. package/index.js +3 -6
  42. package/package.json +1 -1
@@ -0,0 +1,713 @@
1
+ "use client"
2
+
3
+ /**
4
+ * Spine — Dashboard
5
+ * Self-contained demo with realistic mock data wired to Spine's actual product
6
+ * mechanics (daily AI-generation quota, stacks, scaffolds, files).
7
+ *
8
+ * To connect real data:
9
+ * - Replace STATS / RECENT_PROJECTS / USAGE_DATA / NOTIFICATIONS with API/DB reads
10
+ * - Replace "Alex Chen" with session.user.name (NextAuth) in Sidebar + Topbar
11
+ * - Wire "Generate new scaffold" to the same /api/generate flow as the landing page
12
+ * - Wire each row's Download button to the real scaffold ZIP endpoint
13
+ */
14
+
15
+ import { useState, useEffect, useMemo } from "react"
16
+ import {
17
+ Zap, Package, FileCode, FolderOpen, LayoutDashboard, FolderGit2, Layers,
18
+ KeyRound, CreditCard, Settings, Bell, Search, Menu, X, ChevronDown, ChevronRight,
19
+ LogOut, User, TrendingUp, Plus, Download, Copy, Check, Circle, Terminal,
20
+ } from "lucide-react"
21
+ import { Button } from "@/components/ui/button"
22
+ import { Badge } from "@/components/ui/badge"
23
+ import { Card, CardContent } from "@/components/ui/card"
24
+ import { Avatar, AvatarFallback } from "@/components/ui/avatar"
25
+ import { Separator } from "@/components/ui/separator"
26
+ import { Input } from "@/components/ui/input"
27
+ import { AreaChart, Area, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer } from "recharts"
28
+
29
+ /* Tiny className joiner so this file doesn't depend on "@/lib/utils" being present */
30
+ const cx = (...c) => c.filter(Boolean).join(" ")
31
+
32
+ /* ════════════════════════════════════════════════════════════════
33
+ MOCK DATA
34
+ ════════════════════════════════════════════════════════════════ */
35
+ const NAV_ITEMS = [
36
+ { id: "overview", label: "Overview", icon: LayoutDashboard },
37
+ { id: "projects", label: "Projects", icon: FolderGit2 },
38
+ { id: "templates", label: "Templates", icon: Layers },
39
+ { id: "api-keys", label: "API Keys", icon: KeyRound },
40
+ { id: "billing", label: "Billing", icon: CreditCard },
41
+ { id: "settings", label: "Settings", icon: Settings },
42
+ ]
43
+
44
+ const STACK_META = {
45
+ nextjs: { label: "Next.js Fullstack", text: "text-blue-400", bg: "bg-blue-950", border: "border-blue-800" },
46
+ mern: { label: "MERN Stack", text: "text-violet-400", bg: "bg-violet-950", border: "border-violet-800" },
47
+ micro: { label: "Microservice", text: "text-amber-400", bg: "bg-amber-950", border: "border-amber-800" },
48
+ hono: { label: "Hono Edge API", text: "text-orange-400", bg: "bg-orange-950", border: "border-orange-800" },
49
+ }
50
+
51
+ const STATS = {
52
+ generationsToday: 4,
53
+ dailyLimit: 5,
54
+ scaffoldsTotal: 128,
55
+ scaffoldsDelta: 12,
56
+ filesTotal: 1024,
57
+ filesDelta: 86,
58
+ activeProjects: 18,
59
+ stacksInUse: 4,
60
+ }
61
+
62
+ const RECENT_PROJECTS = [
63
+ { id: 1, name: "task-manager-api", stack: "nextjs", files: 11, created: "2 hours ago" },
64
+ { id: 2, name: "blog-cms-backend", stack: "mern", files: 9, created: "Yesterday" },
65
+ { id: 3, name: "shop-checkout-service", stack: "hono", files: 8, created: "3 days ago" },
66
+ { id: 4, name: "internal-tools-api", stack: "micro", files: 6, created: "5 days ago" },
67
+ { id: 5, name: "saas-billing-api", stack: "nextjs", files: 13, created: "1 week ago" },
68
+ ]
69
+
70
+ const QUICKSTART = [
71
+ { id: 1, label: "Install the Spine CLI", done: true },
72
+ { id: 2, label: "Generate your first scaffold", done: true },
73
+ { id: 3, label: "Connect a GitHub repo", done: false },
74
+ { id: 4, label: "Upgrade to Pro for unlimited runs", done: false },
75
+ ]
76
+
77
+ const NOTIFICATIONS = [
78
+ { id: 1, title: "Your task-manager-api scaffold finished generating.", time: "2 hours ago", unread: true },
79
+ { id: 2, title: "New template available: Hono Edge API v2.", time: "1 day ago", unread: true },
80
+ { id: 3, title: "Your weekly usage summary is ready.", time: "3 days ago", unread: false },
81
+ ]
82
+
83
+ const RAW_USAGE = [2,1,3,2,4,3,2,3,4,2,5,3,4,3,4,5,3,6,4,5,4,5,6,4,7,5,6,5,7,6]
84
+ const USAGE_DATA = RAW_USAGE.map((value, i) => {
85
+ const d = new Date()
86
+ d.setDate(d.getDate() - (RAW_USAGE.length - 1 - i))
87
+ return { label: d.toLocaleDateString("en-US", { month: "short", day: "numeric" }), value }
88
+ })
89
+
90
+ /* ════════════════════════════════════════════════════════════════
91
+ HOOKS
92
+ ════════════════════════════════════════════════════════════════ */
93
+ function useCountUp(target, duration = 900) {
94
+ const [value, setValue] = useState(0)
95
+ useEffect(() => {
96
+ let raf
97
+ const start = performance.now()
98
+ function tick(now) {
99
+ const progress = Math.min((now - start) / duration, 1)
100
+ const eased = 1 - Math.pow(1 - progress, 3)
101
+ setValue(Math.round(target * eased))
102
+ if (progress < 1) raf = requestAnimationFrame(tick)
103
+ }
104
+ raf = requestAnimationFrame(tick)
105
+ return () => cancelAnimationFrame(raf)
106
+ }, [target, duration])
107
+ return value
108
+ }
109
+
110
+ /* ════════════════════════════════════════════════════════════════
111
+ SMALL PIECES
112
+ ════════════════════════════════════════════════════════════════ */
113
+ function Logo() {
114
+ return (
115
+ <div className="flex items-center gap-2.5">
116
+ <svg width="20" height="20" viewBox="0 0 24 24" fill="none">
117
+ <line x1="12" y1="1" x2="12" y2="23" stroke="#34d399" strokeWidth="2" strokeLinecap="round" />
118
+ <line x1="12" y1="5" x2="18" y2="3" stroke="#34d399" strokeWidth="1.4" strokeLinecap="round" />
119
+ <line x1="12" y1="5" x2="6" y2="3" stroke="#34d399" strokeWidth="1.4" strokeLinecap="round" opacity="0.45" />
120
+ <line x1="12" y1="11" x2="18" y2="9" stroke="#34d399" strokeWidth="1.4" strokeLinecap="round" />
121
+ <line x1="12" y1="11" x2="6" y2="9" stroke="#34d399" strokeWidth="1.4" strokeLinecap="round" opacity="0.45" />
122
+ <line x1="12" y1="17" x2="18" y2="15" stroke="#34d399" strokeWidth="1.4" strokeLinecap="round" />
123
+ <line x1="12" y1="17" x2="6" y2="15" stroke="#34d399" strokeWidth="1.4" strokeLinecap="round" opacity="0.45" />
124
+ </svg>
125
+ <span className="font-bold text-base tracking-tight text-zinc-50">Spine</span>
126
+ </div>
127
+ )
128
+ }
129
+
130
+ function StackBadge({ stackId }) {
131
+ const meta = STACK_META[stackId]
132
+ if (!meta) return null
133
+ return (
134
+ <Badge variant="outline" className={cx("font-mono text-xs whitespace-nowrap", meta.bg, meta.border, meta.text)}>
135
+ {meta.label}
136
+ </Badge>
137
+ )
138
+ }
139
+
140
+ function UsageTooltip({ active, payload, label }) {
141
+ if (!active || !payload || !payload.length) return null
142
+ return (
143
+ <div className="rounded-lg border border-zinc-700 bg-zinc-900 px-3 py-2 shadow-2xl">
144
+ <p className="text-xs text-zinc-500 font-mono mb-0.5">{label}</p>
145
+ <p className="text-sm font-semibold text-emerald-400">{payload[0].value} scaffolds</p>
146
+ </div>
147
+ )
148
+ }
149
+
150
+ /* ════════════════════════════════════════════════════════════════
151
+ SIDEBAR
152
+ ════════════════════════════════════════════════════════════════ */
153
+ function SidebarContent({ activeNav, onNavigate, onClose }) {
154
+ return (
155
+ <div className="flex flex-col h-full">
156
+ <div className="h-14 flex items-center justify-between px-5 border-b border-zinc-800 shrink-0">
157
+ <Logo />
158
+ {onClose && (
159
+ <button onClick={onClose} className="text-zinc-500 hover:text-zinc-200 transition-colors lg:hidden" aria-label="Close menu">
160
+ <X className="w-4 h-4" />
161
+ </button>
162
+ )}
163
+ </div>
164
+
165
+ <nav className="flex-1 overflow-y-auto py-4 px-3">
166
+ <p className="px-2.5 text-xs font-semibold text-zinc-600 tracking-widest uppercase mb-2">Workspace</p>
167
+ <div className="flex flex-col gap-0.5">
168
+ {NAV_ITEMS.map((item) => {
169
+ const active = activeNav === item.id
170
+ const Icon = item.icon
171
+ return (
172
+ <button
173
+ key={item.id}
174
+ onClick={() => onNavigate(item.id)}
175
+ className={cx(
176
+ "flex items-center gap-2.5 px-2.5 py-2 rounded-lg text-sm transition-colors text-left border",
177
+ active
178
+ ? "bg-emerald-950 text-emerald-400 border-emerald-800"
179
+ : "text-zinc-400 hover:text-zinc-100 hover:bg-zinc-800/60 border-transparent"
180
+ )}
181
+ >
182
+ <Icon className="w-4 h-4 shrink-0" />
183
+ {item.label}
184
+ </button>
185
+ )
186
+ })}
187
+ </div>
188
+ </nav>
189
+
190
+ <div className="p-3 border-t border-zinc-800 shrink-0">
191
+ <div className="flex items-center gap-2 px-2 py-2 mb-2">
192
+ <span className="relative flex h-2 w-2 shrink-0">
193
+ <span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-emerald-400 opacity-60" />
194
+ <span className="relative inline-flex rounded-full h-2 w-2 bg-emerald-400" />
195
+ </span>
196
+ <span className="text-xs text-zinc-500">All systems operational</span>
197
+ </div>
198
+ <Separator className="bg-zinc-800 mb-2" />
199
+ <div className="flex items-center gap-2.5 px-2 py-1.5">
200
+ <Avatar className="h-8 w-8 border border-zinc-700 shrink-0">
201
+ <AvatarFallback className="bg-emerald-950 text-emerald-400 text-xs font-semibold">AC</AvatarFallback>
202
+ </Avatar>
203
+ <div className="min-w-0 flex-1">
204
+ <p className="text-xs font-medium text-zinc-200 truncate">Alex Chen</p>
205
+ <p className="text-xs text-zinc-600 truncate">Free plan</p>
206
+ </div>
207
+ </div>
208
+ </div>
209
+ </div>
210
+ )
211
+ }
212
+
213
+ /* ════════════════════════════════════════════════════════════════
214
+ TOPBAR
215
+ ════════════════════════════════════════════════════════════════ */
216
+ function Topbar({ onMenuClick, notifOpen, setNotifOpen, userMenuOpen, setUserMenuOpen }) {
217
+ return (
218
+ <header className="sticky top-0 z-30 h-14 flex items-center gap-2 sm:gap-3 px-4 md:px-6 bg-black/80 backdrop-blur-md border-b border-zinc-800 shrink-0">
219
+ <button onClick={onMenuClick} className="text-zinc-400 hover:text-zinc-100 transition-colors lg:hidden shrink-0" aria-label="Open menu">
220
+ <Menu className="w-5 h-5" />
221
+ </button>
222
+
223
+ <h1 className="text-sm font-semibold text-zinc-100">Overview</h1>
224
+
225
+ <div className="flex-1" />
226
+
227
+ <div className="relative hidden sm:block">
228
+ <Search className="absolute left-2.5 top-1/2 -translate-y-1/2 w-3.5 h-3.5 text-zinc-500 pointer-events-none" />
229
+ <Input
230
+ placeholder="Search projects..."
231
+ className="h-9 w-40 lg:w-64 pl-8 bg-zinc-900 border-zinc-800 text-sm placeholder:text-zinc-600 focus-visible:ring-emerald-600"
232
+ />
233
+ </div>
234
+
235
+ <Button size="sm" className="hidden sm:inline-flex bg-emerald-500 hover:bg-emerald-400 text-black font-semibold text-xs h-8 gap-1.5">
236
+ <Plus className="w-3.5 h-3.5" /> New scaffold
237
+ </Button>
238
+
239
+ <div className="relative">
240
+ <button
241
+ onClick={() => { setNotifOpen((v) => !v); setUserMenuOpen(false) }}
242
+ className="relative text-zinc-400 hover:text-zinc-100 transition-colors w-8 h-8 flex items-center justify-center rounded-lg hover:bg-zinc-800/60"
243
+ aria-label="Notifications"
244
+ >
245
+ <Bell className="w-4 h-4" />
246
+ <span className="absolute top-1.5 right-1.5 w-1.5 h-1.5 rounded-full bg-emerald-400" />
247
+ </button>
248
+ {notifOpen && (
249
+ <>
250
+ <div className="fixed inset-0 z-40" onClick={() => setNotifOpen(false)} />
251
+ <div className="absolute right-0 mt-2 w-72 bg-zinc-900 border border-zinc-800 rounded-lg shadow-2xl z-50 overflow-hidden">
252
+ <div className="px-4 py-3 border-b border-zinc-800">
253
+ <p className="text-xs font-semibold text-zinc-300">Notifications</p>
254
+ </div>
255
+ <div className="max-h-64 overflow-y-auto">
256
+ {NOTIFICATIONS.map((n) => (
257
+ <div key={n.id} className="px-4 py-3 border-b border-zinc-800 last:border-0 hover:bg-zinc-800/40 transition-colors">
258
+ <div className="flex items-start gap-2">
259
+ {n.unread && <span className="w-1.5 h-1.5 rounded-full bg-emerald-400 mt-1.5 shrink-0" />}
260
+ <div className={cx(!n.unread && "pl-3.5")}>
261
+ <p className="text-xs text-zinc-300 leading-relaxed">{n.title}</p>
262
+ <p className="text-xs text-zinc-600 mt-0.5">{n.time}</p>
263
+ </div>
264
+ </div>
265
+ </div>
266
+ ))}
267
+ </div>
268
+ </div>
269
+ </>
270
+ )}
271
+ </div>
272
+
273
+ <div className="relative">
274
+ <button onClick={() => { setUserMenuOpen((v) => !v); setNotifOpen(false) }} className="flex items-center gap-1.5" aria-label="Account menu">
275
+ <Avatar className="h-8 w-8 border border-zinc-700">
276
+ <AvatarFallback className="bg-emerald-950 text-emerald-400 text-xs font-semibold">AC</AvatarFallback>
277
+ </Avatar>
278
+ <ChevronDown className="w-3.5 h-3.5 text-zinc-500 hidden sm:block" />
279
+ </button>
280
+ {userMenuOpen && (
281
+ <>
282
+ <div className="fixed inset-0 z-40" onClick={() => setUserMenuOpen(false)} />
283
+ <div className="absolute right-0 mt-2 w-52 bg-zinc-900 border border-zinc-800 rounded-lg shadow-2xl z-50 overflow-hidden py-1.5">
284
+ <div className="px-3.5 py-2 mb-1 border-b border-zinc-800">
285
+ <p className="text-xs font-medium text-zinc-200">Alex Chen</p>
286
+ <p className="text-xs text-zinc-600">alex@example.com</p>
287
+ </div>
288
+ {[{ icon: User, label: "Profile" }, { icon: Settings, label: "Settings" }, { icon: CreditCard, label: "Billing" }].map((item) => {
289
+ const ItemIcon = item.icon
290
+ return (
291
+ <button
292
+ key={item.label}
293
+ className="w-full flex items-center gap-2.5 px-3.5 py-2 text-xs text-zinc-400 hover:text-zinc-100 hover:bg-zinc-800/60 transition-colors text-left"
294
+ >
295
+ <ItemIcon className="w-3.5 h-3.5" /> {item.label}
296
+ </button>
297
+ )
298
+ })}
299
+ <Separator className="bg-zinc-800 my-1.5" />
300
+ <button className="w-full flex items-center gap-2.5 px-3.5 py-2 text-xs text-red-400 hover:bg-red-950/40 transition-colors text-left">
301
+ <LogOut className="w-3.5 h-3.5" /> Log out
302
+ </button>
303
+ </div>
304
+ </>
305
+ )}
306
+ </div>
307
+ </header>
308
+ )
309
+ }
310
+
311
+ /* ════════════════════════════════════════════════════════════════
312
+ STAT CARD
313
+ ════════════════════════════════════════════════════════════════ */
314
+ function StatCard({ icon: Icon, label, value, tag, tone = "positive" }) {
315
+ const count = useCountUp(value)
316
+ return (
317
+ <Card className="bg-zinc-900 border-zinc-800 hover:border-zinc-700 transition-colors">
318
+ <CardContent className="p-5">
319
+ <div className="flex items-center justify-between mb-4">
320
+ <div className="w-9 h-9 rounded-lg bg-emerald-950 border border-emerald-800 flex items-center justify-center">
321
+ <Icon className="w-4 h-4 text-emerald-400" />
322
+ </div>
323
+ {tag && (
324
+ <span
325
+ className={cx(
326
+ "inline-flex items-center gap-1 text-xs font-medium px-1.5 py-0.5 rounded-md font-mono",
327
+ tone === "positive" ? "text-emerald-400 bg-emerald-950/60" : "text-zinc-500 bg-zinc-800/60"
328
+ )}
329
+ >
330
+ {tone === "positive" && <TrendingUp className="w-3 h-3" />}
331
+ {tag}
332
+ </span>
333
+ )}
334
+ </div>
335
+ <p className="text-2xl font-extrabold tracking-tight tabular-nums">{count.toLocaleString()}</p>
336
+ <p className="text-xs text-zinc-500 mt-1">{label}</p>
337
+ </CardContent>
338
+ </Card>
339
+ )
340
+ }
341
+
342
+ /* ════════════════════════════════════════════════════════════════
343
+ USAGE CARD — signature terminal-chrome panel (mirrors the landing
344
+ page's generator output window)
345
+ ════════════════════════════════════════════════════════════════ */
346
+ function UsageCard({ period, setPeriod, data }) {
347
+ const total = data.reduce((a, b) => a + b.value, 0)
348
+ const avg = (total / data.length).toFixed(1)
349
+ const peak = Math.max(...data.map((d) => d.value))
350
+
351
+ return (
352
+ <Card className="bg-zinc-950 border-zinc-800 overflow-hidden h-full">
353
+ <div className="flex items-center justify-between px-4 py-2.5 border-b border-zinc-800 bg-zinc-900">
354
+ <div className="flex items-center gap-2">
355
+ <span className="w-2.5 h-2.5 rounded-full bg-red-500/70" />
356
+ <span className="w-2.5 h-2.5 rounded-full bg-yellow-500/70" />
357
+ <span className="w-2.5 h-2.5 rounded-full bg-emerald-500/70" />
358
+ <span className="text-xs text-zinc-500 font-mono ml-2">usage — scaffolds/day</span>
359
+ </div>
360
+ <div className="flex items-center gap-1 bg-zinc-950 border border-zinc-800 rounded-md p-0.5">
361
+ {["7", "14", "30"].map((p) => (
362
+ <button
363
+ key={p}
364
+ onClick={() => setPeriod(p)}
365
+ className={cx(
366
+ "px-2 py-1 rounded text-xs font-mono transition-colors",
367
+ period === p ? "bg-zinc-800 text-emerald-400" : "text-zinc-500 hover:text-zinc-300"
368
+ )}
369
+ >
370
+ {p}D
371
+ </button>
372
+ ))}
373
+ </div>
374
+ </div>
375
+ <CardContent className="p-5 pt-6">
376
+ <div style={{ width: "100%", height: 220 }}>
377
+ <ResponsiveContainer width="100%" height="100%">
378
+ <AreaChart data={data} margin={{ top: 8, right: 8, left: -20, bottom: 0 }}>
379
+ <defs>
380
+ <linearGradient id="usageFill" x1="0" y1="0" x2="0" y2="1">
381
+ <stop offset="0%" stopColor="#34d399" stopOpacity={0.35} />
382
+ <stop offset="100%" stopColor="#34d399" stopOpacity={0} />
383
+ </linearGradient>
384
+ </defs>
385
+ <CartesianGrid stroke="#27272a" strokeDasharray="3 3" vertical={false} />
386
+ <XAxis dataKey="label" stroke="#52525b" fontSize={10} tickLine={false} axisLine={false} interval="preserveStartEnd" />
387
+ <YAxis stroke="#52525b" fontSize={10} tickLine={false} axisLine={false} width={24} allowDecimals={false} />
388
+ <Tooltip content={<UsageTooltip />} cursor={{ stroke: "#3f3f46", strokeWidth: 1 }} />
389
+ <Area type="monotone" dataKey="value" stroke="#34d399" strokeWidth={2} fill="url(#usageFill)" />
390
+ </AreaChart>
391
+ </ResponsiveContainer>
392
+ </div>
393
+ <div className="flex items-center gap-5 mt-2 pt-4 border-t border-zinc-900">
394
+ <div>
395
+ <p className="text-xs text-zinc-600">Total in range</p>
396
+ <p className="text-sm font-mono font-semibold text-zinc-200">{total}</p>
397
+ </div>
398
+ <div>
399
+ <p className="text-xs text-zinc-600">Daily average</p>
400
+ <p className="text-sm font-mono font-semibold text-zinc-200">{avg}</p>
401
+ </div>
402
+ <div>
403
+ <p className="text-xs text-zinc-600">Peak day</p>
404
+ <p className="text-sm font-mono font-semibold text-emerald-400">{peak}</p>
405
+ </div>
406
+ </div>
407
+ </CardContent>
408
+ </Card>
409
+ )
410
+ }
411
+
412
+ /* ════════════════════════════════════════════════════════════════
413
+ PLAN CARD
414
+ ════════════════════════════════════════════════════════════════ */
415
+ function PlanCard({ used, limit }) {
416
+ const pct = Math.min(100, Math.round((used / limit) * 100))
417
+ const nearLimit = pct >= 80
418
+ const left = Math.max(0, limit - used)
419
+
420
+ return (
421
+ <Card className="bg-zinc-900 border-zinc-800 h-full">
422
+ <CardContent className="p-6 flex flex-col h-full">
423
+ <div className="flex items-center justify-between mb-1">
424
+ <p className="text-xs font-semibold text-zinc-500 tracking-widest uppercase">Current plan</p>
425
+ <Badge variant="outline" className="bg-zinc-950 border-zinc-700 text-zinc-400 text-xs">Free</Badge>
426
+ </div>
427
+ <p className="text-lg font-bold tracking-tight mb-5">Free Forever</p>
428
+
429
+ <div className="mb-1.5 flex items-baseline justify-between">
430
+ <span className="text-xs text-zinc-500">Daily generations</span>
431
+ <span className={cx("text-xs font-mono", nearLimit ? "text-amber-400" : "text-zinc-400")}>{used} / {limit}</span>
432
+ </div>
433
+ <div className="h-1.5 w-full rounded-full bg-zinc-800 overflow-hidden mb-1.5">
434
+ <div
435
+ className={cx("h-full rounded-full transition-all duration-500", nearLimit ? "bg-amber-400" : "bg-emerald-400")}
436
+ style={{ width: `${pct}%` }}
437
+ />
438
+ </div>
439
+ <p className="text-xs text-zinc-600 mb-6">{left} left today · resets at midnight UTC</p>
440
+
441
+ <div className="flex flex-col gap-2 mb-6 flex-1">
442
+ {["Unlimited AI generations", "Private saved templates", "Custom stack presets", "Team sharing"].map((f) => (
443
+ <div key={f} className="flex items-center gap-2">
444
+ <Check className="w-3.5 h-3.5 text-emerald-400 shrink-0" />
445
+ <span className="text-xs text-zinc-400">{f}</span>
446
+ </div>
447
+ ))}
448
+ </div>
449
+
450
+ <Button className="w-full bg-emerald-500 hover:bg-emerald-400 text-black font-semibold">Upgrade to Pro — $9/mo</Button>
451
+ </CardContent>
452
+ </Card>
453
+ )
454
+ }
455
+
456
+ /* ════════════════════════════════════════════════════════════════
457
+ PROJECTS TABLE
458
+ ════════════════════════════════════════════════════════════════ */
459
+ function ProjectsTable({ projects }) {
460
+ return (
461
+ <Card className="bg-zinc-900 border-zinc-800 h-full">
462
+ <CardContent className="p-6">
463
+ <div className="flex items-center justify-between mb-5">
464
+ <div>
465
+ <p className="text-xs text-zinc-500 tracking-widest uppercase mb-1">Recent activity</p>
466
+ <h3 className="text-base font-bold tracking-tight">Your projects</h3>
467
+ </div>
468
+ <button className="text-xs text-zinc-500 hover:text-zinc-200 transition-colors inline-flex items-center gap-1">
469
+ View all <ChevronRight className="w-3.5 h-3.5" />
470
+ </button>
471
+ </div>
472
+
473
+ {projects.length === 0 ? (
474
+ <div className="flex flex-col items-center justify-center text-center py-16 px-6 border border-dashed border-zinc-800 rounded-lg">
475
+ <div className="w-10 h-10 rounded-lg bg-emerald-950 border border-emerald-800 flex items-center justify-center mb-3">
476
+ <Terminal className="w-4 h-4 text-emerald-400" />
477
+ </div>
478
+ <p className="text-sm text-zinc-400 mb-1">No scaffolds yet</p>
479
+ <p className="text-xs text-zinc-600 max-w-xs">
480
+ Run <span className="text-emerald-400 font-mono">npx create-spine@latest</span> or generate one from this dashboard.
481
+ </p>
482
+ </div>
483
+ ) : (
484
+ <div className="overflow-x-auto">
485
+ <table className="w-full text-sm">
486
+ <thead>
487
+ <tr className="border-b border-zinc-800 text-left">
488
+ <th className="px-2 pb-2.5 font-medium text-zinc-500 text-xs whitespace-nowrap">Project</th>
489
+ <th className="px-2 pb-2.5 font-medium text-zinc-500 text-xs whitespace-nowrap">Stack</th>
490
+ <th className="px-2 pb-2.5 font-medium text-zinc-500 text-xs text-right whitespace-nowrap">Files</th>
491
+ <th className="px-2 pb-2.5 font-medium text-zinc-500 text-xs whitespace-nowrap">Created</th>
492
+ <th className="px-2 pb-2.5 font-medium text-zinc-500 text-xs text-right whitespace-nowrap">Download</th>
493
+ </tr>
494
+ </thead>
495
+ <tbody>
496
+ {projects.map((p) => (
497
+ <tr key={p.id} className="border-b border-zinc-900 last:border-0 hover:bg-zinc-800/30 transition-colors">
498
+ <td className="px-2 py-3">
499
+ <div className="flex items-center gap-2.5">
500
+ <div className="w-7 h-7 rounded-md bg-zinc-800 flex items-center justify-center shrink-0">
501
+ <FolderGit2 className="w-3.5 h-3.5 text-zinc-400" />
502
+ </div>
503
+ <span className="font-medium text-zinc-200 whitespace-nowrap">{p.name}</span>
504
+ </div>
505
+ </td>
506
+ <td className="px-2 py-3">
507
+ <StackBadge stackId={p.stack} />
508
+ </td>
509
+ <td className="px-2 py-3 text-right font-mono text-zinc-400 text-xs">{p.files}</td>
510
+ <td className="px-2 py-3 text-zinc-500 text-xs whitespace-nowrap">{p.created}</td>
511
+ <td className="px-2 py-3 text-right">
512
+ <button
513
+ className="inline-flex items-center justify-center w-7 h-7 rounded-md text-zinc-500 hover:text-emerald-400 hover:bg-emerald-950/50 transition-colors"
514
+ title="Download ZIP"
515
+ >
516
+ <Download className="w-3.5 h-3.5" />
517
+ </button>
518
+ </td>
519
+ </tr>
520
+ ))}
521
+ </tbody>
522
+ </table>
523
+ </div>
524
+ )}
525
+ </CardContent>
526
+ </Card>
527
+ )
528
+ }
529
+
530
+ /* ════════════════════════════════════════════════════════════════
531
+ QUICK START
532
+ ════════════════════════════════════════════════════════════════ */
533
+ function QuickStartCard({ items }) {
534
+ const [copied, setCopied] = useState(false)
535
+ const doneCount = items.filter((i) => i.done).length
536
+
537
+ const copy = () => {
538
+ navigator.clipboard?.writeText("npx create-spine@latest").catch(() => {})
539
+ setCopied(true)
540
+ setTimeout(() => setCopied(false), 1800)
541
+ }
542
+
543
+ return (
544
+ <Card className="bg-zinc-900 border-zinc-800 h-full">
545
+ <CardContent className="p-6 flex flex-col h-full">
546
+ <div className="flex items-center justify-between mb-1">
547
+ <p className="text-xs text-zinc-500 tracking-widest uppercase">Getting started</p>
548
+ <span className="text-xs font-mono text-zinc-500">{doneCount}/{items.length}</span>
549
+ </div>
550
+ <h3 className="text-base font-bold tracking-tight mb-5">Quick start</h3>
551
+
552
+ <div className="flex flex-col gap-3.5 mb-5">
553
+ {items.map((item) => (
554
+ <div key={item.id} className="flex items-start gap-2.5">
555
+ {item.done ? (
556
+ <div className="w-4 h-4 rounded-full bg-emerald-500 flex items-center justify-center mt-0.5 shrink-0">
557
+ <Check className="w-2.5 h-2.5 text-black" strokeWidth={3} />
558
+ </div>
559
+ ) : (
560
+ <Circle className="w-4 h-4 text-zinc-700 mt-0.5 shrink-0" />
561
+ )}
562
+ <span className={cx("text-sm", item.done ? "text-zinc-500 line-through decoration-zinc-700" : "text-zinc-300")}>
563
+ {item.label}
564
+ </span>
565
+ </div>
566
+ ))}
567
+ </div>
568
+
569
+ <div className="mt-auto flex items-center gap-2 bg-zinc-950 border border-zinc-800 rounded-lg px-3 py-2">
570
+ <span className="text-emerald-400 font-mono text-xs">$</span>
571
+ <span className="text-zinc-400 font-mono text-xs flex-1 truncate">npx create-spine@latest</span>
572
+ <button onClick={copy} className="text-zinc-500 hover:text-zinc-200 transition-colors shrink-0" title="Copy command" aria-label="Copy command">
573
+ {copied ? <Check className="w-3.5 h-3.5 text-emerald-400" /> : <Copy className="w-3.5 h-3.5" />}
574
+ </button>
575
+ </div>
576
+ </CardContent>
577
+ </Card>
578
+ )
579
+ }
580
+
581
+ /* ════════════════════════════════════════════════════════════════
582
+ MAIN DASHBOARD
583
+ ════════════════════════════════════════════════════════════════ */
584
+ export default function Dashboard() {
585
+ const [mobileNavOpen, setMobileNavOpen] = useState(false)
586
+ const [notifOpen, setNotifOpen] = useState(false)
587
+ const [userMenuOpen, setUserMenuOpen] = useState(false)
588
+ const [activeNav, setActiveNav] = useState("overview")
589
+ const [period, setPeriod] = useState("14")
590
+
591
+ useEffect(() => {
592
+ function onKey(e) {
593
+ if (e.key === "Escape") {
594
+ setMobileNavOpen(false)
595
+ setNotifOpen(false)
596
+ setUserMenuOpen(false)
597
+ }
598
+ }
599
+ window.addEventListener("keydown", onKey)
600
+ return () => window.removeEventListener("keydown", onKey)
601
+ }, [])
602
+
603
+ useEffect(() => {
604
+ document.body.style.overflow = mobileNavOpen ? "hidden" : ""
605
+ return () => { document.body.style.overflow = "" }
606
+ }, [mobileNavOpen])
607
+
608
+ const chartData = useMemo(() => USAGE_DATA.slice(-Number(period)), [period])
609
+
610
+ const greeting = useMemo(() => {
611
+ const h = new Date().getHours()
612
+ if (h < 12) return "Good morning"
613
+ if (h < 18) return "Good afternoon"
614
+ return "Good evening"
615
+ }, [])
616
+
617
+ const dateLabel = useMemo(
618
+ () => new Date().toLocaleDateString("en-US", { weekday: "long", year: "numeric", month: "long", day: "numeric" }),
619
+ []
620
+ )
621
+
622
+ return (
623
+ <div className="bg-black min-h-screen text-zinc-50 font-sans antialiased flex">
624
+ <style>{`
625
+ @media (prefers-reduced-motion: reduce) {
626
+ *, *::before, *::after {
627
+ animation-duration: 0.01ms !important;
628
+ animation-iteration-count: 1 !important;
629
+ transition-duration: 0.01ms !important;
630
+ }
631
+ }
632
+ `}</style>
633
+
634
+ <div
635
+ onClick={() => setMobileNavOpen(false)}
636
+ className={cx(
637
+ "fixed inset-0 bg-black/60 backdrop-blur-sm z-40 transition-opacity duration-300 lg:hidden",
638
+ mobileNavOpen ? "opacity-100 pointer-events-auto" : "opacity-0 pointer-events-none"
639
+ )}
640
+ />
641
+
642
+ <aside
643
+ className={cx(
644
+ "fixed inset-y-0 left-0 z-50 w-72 bg-zinc-950 border-r border-zinc-800 transform transition-transform duration-300 ease-out lg:hidden",
645
+ mobileNavOpen ? "translate-x-0" : "-translate-x-full"
646
+ )}
647
+ >
648
+ <SidebarContent
649
+ activeNav={activeNav}
650
+ onNavigate={(id) => { setActiveNav(id); setMobileNavOpen(false) }}
651
+ onClose={() => setMobileNavOpen(false)}
652
+ />
653
+ </aside>
654
+
655
+ <aside className="hidden lg:flex lg:flex-col w-64 shrink-0 border-r border-zinc-800 bg-zinc-950/40 sticky top-0 h-screen">
656
+ <SidebarContent activeNav={activeNav} onNavigate={setActiveNav} />
657
+ </aside>
658
+
659
+ <div className="flex-1 min-w-0 flex flex-col">
660
+ <Topbar
661
+ onMenuClick={() => setMobileNavOpen(true)}
662
+ notifOpen={notifOpen}
663
+ setNotifOpen={setNotifOpen}
664
+ userMenuOpen={userMenuOpen}
665
+ setUserMenuOpen={setUserMenuOpen}
666
+ />
667
+
668
+ <main className="flex-1 px-4 md:px-6 lg:px-8 py-6 md:py-8 max-w-7xl w-full mx-auto">
669
+ <div className="relative flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4 mb-7">
670
+ <div
671
+ className="absolute -top-10 left-0 pointer-events-none -z-10"
672
+ style={{ width: 420, height: 220, background: "radial-gradient(ellipse at center, rgba(16,185,129,0.07) 0%, transparent 70%)" }}
673
+ />
674
+ <div>
675
+ <h2 className="text-xl md:text-2xl font-extrabold tracking-tight">{greeting}, Alex</h2>
676
+ <p className="text-sm text-zinc-500 mt-1">{dateLabel}</p>
677
+ </div>
678
+ <Button className="bg-emerald-500 hover:bg-emerald-400 text-black font-semibold gap-2 self-start sm:self-auto">
679
+ <Plus className="w-4 h-4" /> Generate new scaffold
680
+ </Button>
681
+ </div>
682
+
683
+ <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4 mb-4">
684
+ <StatCard icon={Zap} label="Generations today" value={STATS.generationsToday} tag={`of ${STATS.dailyLimit} daily`} tone="neutral" />
685
+ <StatCard icon={Package} label="Scaffolds generated" value={STATS.scaffoldsTotal} tag={`+${STATS.scaffoldsDelta} this week`} />
686
+ <StatCard icon={FileCode} label="Files generated" value={STATS.filesTotal} tag={`+${STATS.filesDelta} this week`} />
687
+ <StatCard icon={FolderOpen} label="Active projects" value={STATS.activeProjects} tag={`${STATS.stacksInUse} stacks`} tone="neutral" />
688
+ </div>
689
+
690
+ <div className="grid grid-cols-1 xl:grid-cols-3 gap-4 mb-4">
691
+ <div className="xl:col-span-2">
692
+ <UsageCard period={period} setPeriod={setPeriod} data={chartData} />
693
+ </div>
694
+ <div className="xl:col-span-1">
695
+ <PlanCard used={STATS.generationsToday} limit={STATS.dailyLimit} />
696
+ </div>
697
+ </div>
698
+
699
+ <div className="grid grid-cols-1 xl:grid-cols-3 gap-4 mb-6">
700
+ <div className="xl:col-span-2">
701
+ <ProjectsTable projects={RECENT_PROJECTS} />
702
+ </div>
703
+ <div className="xl:col-span-1">
704
+ <QuickStartCard items={QUICKSTART} />
705
+ </div>
706
+ </div>
707
+
708
+ <p className="text-center text-xs text-zinc-700 mb-2">Spine · spine.io © {new Date().getFullYear()}</p>
709
+ </main>
710
+ </div>
711
+ </div>
712
+ )
713
+ }