@leanspec/ui 0.2.3 → 0.2.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.
- package/dist/standalone/packages/web/.next/BUILD_ID +1 -1
- package/dist/standalone/packages/web/.next/build-manifest.json +2 -2
- package/dist/standalone/packages/web/.next/prerender-manifest.json +3 -3
- package/dist/standalone/packages/web/.next/server/app/_global-error.html +2 -2
- package/dist/standalone/packages/web/.next/server/app/_global-error.rsc +1 -1
- package/dist/standalone/packages/web/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
- package/dist/standalone/packages/web/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
- package/dist/standalone/packages/web/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
- package/dist/standalone/packages/web/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/dist/standalone/packages/web/.next/server/app/_not-found/page.js.nft.json +1 -1
- package/dist/standalone/packages/web/.next/server/app/_not-found.html +2 -2
- package/dist/standalone/packages/web/.next/server/app/_not-found.rsc +2 -2
- package/dist/standalone/packages/web/.next/server/app/_not-found.segments/_full.segment.rsc +2 -2
- package/dist/standalone/packages/web/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
- package/dist/standalone/packages/web/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
- package/dist/standalone/packages/web/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
- package/dist/standalone/packages/web/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
- package/dist/standalone/packages/web/.next/server/app/api/projects/[id]/specs/route.js.nft.json +1 -1
- package/dist/standalone/packages/web/.next/server/app/api/projects/route.js.nft.json +1 -1
- package/dist/standalone/packages/web/.next/server/app/api/revalidate/route.js.nft.json +1 -1
- package/dist/standalone/packages/web/.next/server/app/api/specs/[id]/dependency-graph/route.js.nft.json +1 -1
- package/dist/standalone/packages/web/.next/server/app/api/specs/[id]/route.js.nft.json +1 -1
- package/dist/standalone/packages/web/.next/server/app/api/specs/[id]/subspecs/[file]/route.js.nft.json +1 -1
- package/dist/standalone/packages/web/.next/server/app/api/stats/route.js.nft.json +1 -1
- package/dist/standalone/packages/web/.next/server/app/board/page.js.nft.json +1 -1
- package/dist/standalone/packages/web/.next/server/app/board.html +1 -1
- package/dist/standalone/packages/web/.next/server/app/board.rsc +2 -2
- package/dist/standalone/packages/web/.next/server/app/board.segments/_full.segment.rsc +2 -2
- package/dist/standalone/packages/web/.next/server/app/board.segments/_index.segment.rsc +1 -1
- package/dist/standalone/packages/web/.next/server/app/board.segments/_tree.segment.rsc +1 -1
- package/dist/standalone/packages/web/.next/server/app/board.segments/board/__PAGE__.segment.rsc +1 -1
- package/dist/standalone/packages/web/.next/server/app/board.segments/board.segment.rsc +1 -1
- package/dist/standalone/packages/web/.next/server/app/page.js.nft.json +1 -1
- package/dist/standalone/packages/web/.next/server/app/specs/[id]/page.js.nft.json +1 -1
- package/dist/standalone/packages/web/.next/server/app/specs/[id]/page_client-reference-manifest.js +1 -1
- package/dist/standalone/packages/web/.next/server/app/specs/page.js.nft.json +1 -1
- package/dist/standalone/packages/web/.next/server/app/stats/page.js.nft.json +1 -1
- package/dist/standalone/packages/web/.next/server/chunks/[root-of-the-server]__2e0f9179._.js +1 -1
- package/dist/standalone/packages/web/.next/server/chunks/[root-of-the-server]__577d6d08._.js +1 -1
- package/dist/standalone/packages/web/.next/server/chunks/[root-of-the-server]__e54bc4b8._.js +1 -1
- package/dist/standalone/packages/web/.next/server/chunks/[root-of-the-server]__f8978f3e._.js +1 -1
- package/dist/standalone/packages/web/.next/server/chunks/ssr/[root-of-the-server]__be46bb7c._.js +1 -1
- package/dist/standalone/packages/web/.next/server/chunks/ssr/_7dedc302._.js +1 -1
- package/dist/standalone/packages/web/.next/server/chunks/ssr/_ad71cd8c._.js +1 -1
- package/dist/standalone/packages/web/.next/server/chunks/ssr/_c5a5c652._.js +1 -1
- package/dist/standalone/packages/web/.next/server/pages/404.html +2 -2
- package/dist/standalone/packages/web/.next/server/pages/500.html +2 -2
- package/dist/standalone/packages/web/.next/server/server-reference-manifest.js +1 -1
- package/dist/standalone/packages/web/.next/server/server-reference-manifest.json +1 -1
- package/dist/{static/chunks/0de258404bcae76f.js → standalone/packages/web/.next/static/chunks/8864b47e107cbe63.js} +1 -1
- package/dist/{static/chunks/09ff02250dd56621.js → standalone/packages/web/.next/static/chunks/a2889ecda42c83e7.js} +1 -1
- package/dist/standalone/packages/web/.next/static/chunks/c22619397bb8368e.js +1 -0
- package/dist/standalone/packages/web/components.json +20 -0
- package/dist/standalone/packages/web/drizzle/0000_reflective_thena.sql +59 -0
- package/dist/standalone/packages/web/drizzle/0001_fresh_carmella_unuscione.sql +1 -0
- package/dist/standalone/packages/web/drizzle/meta/0000_snapshot.json +427 -0
- package/dist/standalone/packages/web/drizzle/meta/0001_snapshot.json +436 -0
- package/dist/standalone/packages/web/drizzle/meta/_journal.json +20 -0
- package/dist/standalone/packages/web/drizzle.config.ts +10 -0
- package/dist/standalone/packages/web/eslint.config.mjs +18 -0
- package/dist/standalone/packages/web/next.config.ts +7 -0
- package/dist/standalone/packages/web/package.json +1 -1
- package/dist/standalone/packages/web/postcss.config.mjs +8 -0
- package/dist/standalone/packages/web/src/app/api/projects/[id]/specs/route.ts +23 -0
- package/dist/standalone/packages/web/src/app/api/projects/route.ts +19 -0
- package/dist/standalone/packages/web/src/app/api/revalidate/route.ts +63 -0
- package/dist/standalone/packages/web/src/app/api/specs/[id]/dependency-graph/route.test.ts +51 -0
- package/dist/standalone/packages/web/src/app/api/specs/[id]/dependency-graph/route.ts +171 -0
- package/dist/standalone/packages/web/src/app/api/specs/[id]/route.ts +36 -0
- package/dist/standalone/packages/web/src/app/api/specs/[id]/subspecs/[file]/route.ts +46 -0
- package/dist/standalone/packages/web/src/app/api/stats/route.ts +19 -0
- package/dist/standalone/packages/web/src/app/board/board-client.tsx +162 -0
- package/dist/standalone/packages/web/src/app/board/loading.tsx +43 -0
- package/dist/standalone/packages/web/src/app/board/page.tsx +18 -0
- package/dist/standalone/packages/web/src/app/dashboard-client.tsx +364 -0
- package/dist/standalone/packages/web/src/app/error.tsx +43 -0
- package/dist/standalone/packages/web/src/app/globals.css +531 -0
- package/dist/standalone/packages/web/src/app/home-client.tsx +277 -0
- package/dist/standalone/packages/web/src/app/layout.tsx +70 -0
- package/dist/standalone/packages/web/src/app/loading.tsx +87 -0
- package/dist/standalone/packages/web/src/app/not-found.tsx +27 -0
- package/dist/standalone/packages/web/src/app/page.tsx +18 -0
- package/dist/standalone/packages/web/src/app/specs/[id]/loading.tsx +5 -0
- package/dist/standalone/packages/web/src/app/specs/[id]/page.tsx +43 -0
- package/dist/standalone/packages/web/src/app/specs/page.tsx +18 -0
- package/dist/standalone/packages/web/src/app/specs/specs-client.tsx +425 -0
- package/dist/standalone/packages/web/src/app/stats/page.tsx +18 -0
- package/dist/standalone/packages/web/src/app/stats/stats-client.tsx +283 -0
- package/dist/standalone/packages/web/src/components/back-to-top.tsx +46 -0
- package/dist/standalone/packages/web/src/components/empty-state.tsx +52 -0
- package/dist/standalone/packages/web/src/components/main-sidebar.tsx +175 -0
- package/dist/standalone/packages/web/src/components/markdown-link.test.ts +96 -0
- package/dist/standalone/packages/web/src/components/markdown-link.tsx +95 -0
- package/dist/standalone/packages/web/src/components/navigation.tsx +210 -0
- package/dist/standalone/packages/web/src/components/priority-badge.tsx +53 -0
- package/dist/standalone/packages/web/src/components/quick-search.tsx +180 -0
- package/dist/standalone/packages/web/src/components/skeletons.tsx +119 -0
- package/dist/standalone/packages/web/src/components/spec-dependency-graph.tsx +369 -0
- package/dist/standalone/packages/web/src/components/spec-detail-client.tsx +372 -0
- package/dist/standalone/packages/web/src/components/spec-detail-loading-shell.tsx +42 -0
- package/dist/standalone/packages/web/src/components/spec-detail-wrapper.tsx +70 -0
- package/dist/standalone/packages/web/src/components/spec-metadata.tsx +136 -0
- package/dist/standalone/packages/web/src/components/spec-sidebar.tsx +127 -0
- package/dist/standalone/packages/web/src/components/spec-timeline.tsx +186 -0
- package/dist/standalone/packages/web/src/components/specs-nav-sidebar.tsx +561 -0
- package/dist/standalone/packages/web/src/components/status-badge.tsx +53 -0
- package/dist/standalone/packages/web/src/components/sub-spec-tabs.tsx +143 -0
- package/dist/standalone/packages/web/src/components/table-of-contents.tsx +130 -0
- package/dist/standalone/packages/web/src/components/theme-provider.tsx +11 -0
- package/dist/standalone/packages/web/src/components/theme-toggle.tsx +37 -0
- package/dist/standalone/packages/web/src/components/ui/avatar.tsx +50 -0
- package/dist/standalone/packages/web/src/components/ui/badge.tsx +36 -0
- package/dist/standalone/packages/web/src/components/ui/breadcrumb.tsx +110 -0
- package/dist/standalone/packages/web/src/components/ui/button.tsx +57 -0
- package/dist/standalone/packages/web/src/components/ui/card.tsx +76 -0
- package/dist/standalone/packages/web/src/components/ui/command.tsx +153 -0
- package/dist/standalone/packages/web/src/components/ui/dialog.tsx +122 -0
- package/dist/standalone/packages/web/src/components/ui/input.tsx +24 -0
- package/dist/standalone/packages/web/src/components/ui/select.tsx +159 -0
- package/dist/standalone/packages/web/src/components/ui/separator.tsx +31 -0
- package/dist/standalone/packages/web/src/components/ui/skeleton.tsx +15 -0
- package/dist/standalone/packages/web/src/components/ui/tabs.tsx +55 -0
- package/dist/standalone/packages/web/src/components/ui/toast.tsx +30 -0
- package/dist/standalone/packages/web/src/components/ui/tooltip.tsx +32 -0
- package/dist/standalone/packages/web/src/lib/date-utils.ts +76 -0
- package/dist/standalone/packages/web/src/lib/db/index.ts +42 -0
- package/dist/standalone/packages/web/src/lib/db/migrate.ts +18 -0
- package/dist/standalone/packages/web/src/lib/db/queries.ts +114 -0
- package/dist/standalone/packages/web/src/lib/db/schema.ts +123 -0
- package/dist/standalone/packages/web/src/lib/db/seed.ts +156 -0
- package/dist/standalone/packages/web/src/lib/db/service-queries.ts +216 -0
- package/dist/standalone/packages/web/src/lib/dependency-graph.ts +105 -0
- package/dist/standalone/packages/web/src/lib/specs/service.ts +120 -0
- package/dist/standalone/packages/web/src/lib/specs/sources/database-source.ts +94 -0
- package/dist/standalone/packages/web/src/lib/specs/sources/filesystem-source.ts +249 -0
- package/dist/standalone/packages/web/src/lib/specs/types.ts +55 -0
- package/dist/standalone/packages/web/src/lib/stores/specs-sidebar-store.ts +152 -0
- package/dist/standalone/packages/web/src/lib/sub-specs.ts +171 -0
- package/dist/standalone/packages/web/src/lib/utils.ts +17 -0
- package/dist/standalone/packages/web/src/types/specs.ts +18 -0
- package/dist/standalone/packages/web/tailwind.config.ts +58 -0
- package/dist/standalone/packages/web/tsconfig.json +34 -0
- package/dist/standalone/packages/web/vitest.config.ts +14 -0
- package/dist/standalone/specs/100-release-process-typecheck-failure/README.md +266 -0
- package/dist/standalone/specs/101-sidebar-scroll-position-drift/README.md +100 -0
- package/package.json +5 -3
- package/dist/BUILD_ID +0 -1
- package/dist/static/chunks/a3e649fcddd3d715.js +0 -1
- /package/dist/{static/Z_BxkB0KJViCNbULN1S5p → standalone/packages/web/.next/static/DDHAmoL_2Poji-whvFjry}/_buildManifest.js +0 -0
- /package/dist/{static/Z_BxkB0KJViCNbULN1S5p → standalone/packages/web/.next/static/DDHAmoL_2Poji-whvFjry}/_clientMiddlewareManifest.json +0 -0
- /package/dist/{static/Z_BxkB0KJViCNbULN1S5p → standalone/packages/web/.next/static/DDHAmoL_2Poji-whvFjry}/_ssgManifest.js +0 -0
- /package/dist/{static → standalone/packages/web/.next/static}/chunks/0c19c69aa7625475.js +0 -0
- /package/dist/{static → standalone/packages/web/.next/static}/chunks/116800b03245a1e5.js +0 -0
- /package/dist/{static → standalone/packages/web/.next/static}/chunks/19e80edf527aef5c.js +0 -0
- /package/dist/{static → standalone/packages/web/.next/static}/chunks/2ece90370908f56c.js +0 -0
- /package/dist/{static → standalone/packages/web/.next/static}/chunks/36fd2dddb486f6bc.js +0 -0
- /package/dist/{static → standalone/packages/web/.next/static}/chunks/5c2072ad938de8ed.js +0 -0
- /package/dist/{static → standalone/packages/web/.next/static}/chunks/6577fe797a336bab.js +0 -0
- /package/dist/{static → standalone/packages/web/.next/static}/chunks/6a05a93ec8fa7b83.js +0 -0
- /package/dist/{static → standalone/packages/web/.next/static}/chunks/7f732ea69e643219.js +0 -0
- /package/dist/{static → standalone/packages/web/.next/static}/chunks/a02c1f50ff00204f.js +0 -0
- /package/dist/{static → standalone/packages/web/.next/static}/chunks/a45464b9776dd88e.js +0 -0
- /package/dist/{static → standalone/packages/web/.next/static}/chunks/a6dad97d9634a72d.js +0 -0
- /package/dist/{static → standalone/packages/web/.next/static}/chunks/ae04dcd433be6dab.js +0 -0
- /package/dist/{static → standalone/packages/web/.next/static}/chunks/b20313408e970968.css +0 -0
- /package/dist/{static → standalone/packages/web/.next/static}/chunks/c46095e1a421d93f.js +0 -0
- /package/dist/{static → standalone/packages/web/.next/static}/chunks/c48dd4c72d7c5ef4.js +0 -0
- /package/dist/{static → standalone/packages/web/.next/static}/chunks/c557ac675be79771.js +0 -0
- /package/dist/{static → standalone/packages/web/.next/static}/chunks/dca0c854c59234cd.js +0 -0
- /package/dist/{static → standalone/packages/web/.next/static}/chunks/df1731c03abf1aee.css +0 -0
- /package/dist/{static → standalone/packages/web/.next/static}/chunks/dfd41488ad062cd5.js +0 -0
- /package/dist/{static → standalone/packages/web/.next/static}/chunks/ebd89051637b9a47.js +0 -0
- /package/dist/{static → standalone/packages/web/.next/static}/chunks/f3ec9fd77a8618b1.js +0 -0
- /package/dist/{static → standalone/packages/web/.next/static}/chunks/turbopack-7450632b40b2e378.js +0 -0
- /package/dist/{public → standalone/packages/web/public}/f864aa7e7061c0600e35cf3d879b27cf.txt +0 -0
- /package/dist/{public → standalone/packages/web/public}/favicon.ico +0 -0
- /package/dist/{public → standalone/packages/web/public}/file.svg +0 -0
- /package/dist/{public → standalone/packages/web/public}/github-mark-white.svg +0 -0
- /package/dist/{public → standalone/packages/web/public}/github-mark.svg +0 -0
- /package/dist/{public → standalone/packages/web/public}/globe.svg +0 -0
- /package/dist/{public → standalone/packages/web/public}/icon.svg +0 -0
- /package/dist/{public → standalone/packages/web/public}/logo-dark-bg.svg +0 -0
- /package/dist/{public → standalone/packages/web/public}/logo-with-bg.svg +0 -0
- /package/dist/{public → standalone/packages/web/public}/logo.svg +0 -0
- /package/dist/{public → standalone/packages/web/public}/next.svg +0 -0
- /package/dist/{public → standalone/packages/web/public}/vercel.svg +0 -0
- /package/dist/{public → standalone/packages/web/public}/window.svg +0 -0
|
@@ -0,0 +1,364 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import Link from 'next/link';
|
|
4
|
+
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
|
5
|
+
import { Button } from '@/components/ui/button';
|
|
6
|
+
import {
|
|
7
|
+
FileText,
|
|
8
|
+
CheckCircle2,
|
|
9
|
+
PlayCircle,
|
|
10
|
+
Clock,
|
|
11
|
+
LayoutGrid,
|
|
12
|
+
List,
|
|
13
|
+
TrendingUp,
|
|
14
|
+
Calendar
|
|
15
|
+
} from 'lucide-react';
|
|
16
|
+
import { StatusBadge } from '@/components/status-badge';
|
|
17
|
+
import { PriorityBadge } from '@/components/priority-badge';
|
|
18
|
+
import { Badge } from '@/components/ui/badge';
|
|
19
|
+
import { extractH1Title } from '@/lib/utils';
|
|
20
|
+
|
|
21
|
+
interface Spec {
|
|
22
|
+
id: string;
|
|
23
|
+
specNumber: number | null;
|
|
24
|
+
specName: string;
|
|
25
|
+
title: string | null;
|
|
26
|
+
status: string | null;
|
|
27
|
+
priority: string | null;
|
|
28
|
+
tags: string[] | null;
|
|
29
|
+
createdAt: Date | null;
|
|
30
|
+
updatedAt: Date | null;
|
|
31
|
+
contentMd?: string;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
interface Stats {
|
|
35
|
+
totalSpecs: number;
|
|
36
|
+
completionRate: number;
|
|
37
|
+
specsByStatus: { status: string; count: number }[];
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
interface DashboardClientProps {
|
|
41
|
+
initialSpecs: Spec[];
|
|
42
|
+
initialStats: Stats;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function formatRelativeTime(date: Date | null): string {
|
|
46
|
+
if (!date) return 'Unknown';
|
|
47
|
+
|
|
48
|
+
const now = new Date();
|
|
49
|
+
const diffMs = now.getTime() - date.getTime();
|
|
50
|
+
const diffHours = Math.floor(diffMs / (1000 * 60 * 60));
|
|
51
|
+
const diffDays = Math.floor(diffHours / 24);
|
|
52
|
+
|
|
53
|
+
if (diffHours < 1) return 'Just now';
|
|
54
|
+
if (diffHours < 24) return `${diffHours} hour${diffHours > 1 ? 's' : ''} ago`;
|
|
55
|
+
if (diffDays < 7) return `${diffDays} day${diffDays > 1 ? 's' : ''} ago`;
|
|
56
|
+
if (diffDays < 30) return `${Math.floor(diffDays / 7)} week${Math.floor(diffDays / 7) > 1 ? 's' : ''} ago`;
|
|
57
|
+
return date.toLocaleDateString();
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function SpecListItem({ spec }: { spec: Spec }) {
|
|
61
|
+
// Extract H1 title from content if available, fallback to title or name
|
|
62
|
+
const h1Title = spec.contentMd ? extractH1Title(spec.contentMd) : null;
|
|
63
|
+
const displayTitle = h1Title || spec.title || spec.specName;
|
|
64
|
+
|
|
65
|
+
return (
|
|
66
|
+
<Link
|
|
67
|
+
href={`/specs/${spec.specNumber}`}
|
|
68
|
+
className="block p-3 rounded-lg hover:bg-accent transition-colors"
|
|
69
|
+
title={spec.specName} /* Show name as tooltip */
|
|
70
|
+
>
|
|
71
|
+
<div className="flex items-start justify-between gap-2">
|
|
72
|
+
<div className="flex-1 min-w-0">
|
|
73
|
+
<div className="flex items-center gap-2 mb-1">
|
|
74
|
+
{spec.specNumber && (
|
|
75
|
+
<span className="text-sm font-mono text-muted-foreground shrink-0">
|
|
76
|
+
#{spec.specNumber.toString().padStart(3, '0')}
|
|
77
|
+
</span>
|
|
78
|
+
)}
|
|
79
|
+
<h4 className="text-sm font-medium truncate">
|
|
80
|
+
{displayTitle}
|
|
81
|
+
</h4>
|
|
82
|
+
</div>
|
|
83
|
+
{/* Show spec name as secondary if it differs from title */}
|
|
84
|
+
{displayTitle !== spec.specName && (
|
|
85
|
+
<div className="text-xs text-muted-foreground mb-1">
|
|
86
|
+
{spec.specName}
|
|
87
|
+
</div>
|
|
88
|
+
)}
|
|
89
|
+
{spec.tags && spec.tags.length > 0 && (
|
|
90
|
+
<div className="flex flex-wrap gap-1">
|
|
91
|
+
{spec.tags.slice(0, 3).map(tag => (
|
|
92
|
+
<Badge key={tag} variant="secondary" className="text-xs">
|
|
93
|
+
{tag}
|
|
94
|
+
</Badge>
|
|
95
|
+
))}
|
|
96
|
+
</div>
|
|
97
|
+
)}
|
|
98
|
+
</div>
|
|
99
|
+
<div className="flex flex-col items-end gap-1 shrink-0">
|
|
100
|
+
{spec.status && <StatusBadge status={spec.status} className="text-xs scale-90" />}
|
|
101
|
+
{spec.priority && <PriorityBadge priority={spec.priority} className="text-xs scale-90" />}
|
|
102
|
+
</div>
|
|
103
|
+
</div>
|
|
104
|
+
</Link>
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function ActivityItem({ spec, action, time }: { spec: Spec; action: string; time: Date | null }) {
|
|
109
|
+
const h1Title = spec.contentMd ? extractH1Title(spec.contentMd) : null;
|
|
110
|
+
const displayTitle = h1Title || spec.title || spec.specName;
|
|
111
|
+
|
|
112
|
+
return (
|
|
113
|
+
<div className="flex items-start gap-3 py-2">
|
|
114
|
+
<div className="w-2 h-2 rounded-full bg-primary mt-2 shrink-0" />
|
|
115
|
+
<div className="flex-1 min-w-0">
|
|
116
|
+
<p className="text-sm">
|
|
117
|
+
<Link
|
|
118
|
+
href={`/specs/${spec.specNumber}`}
|
|
119
|
+
className="font-medium hover:underline"
|
|
120
|
+
title={spec.specName}
|
|
121
|
+
>
|
|
122
|
+
#{spec.specNumber?.toString().padStart(3, '0')} {displayTitle}
|
|
123
|
+
</Link>
|
|
124
|
+
{' '}
|
|
125
|
+
<span className="text-muted-foreground">{action}</span>
|
|
126
|
+
</p>
|
|
127
|
+
<p className="text-xs text-muted-foreground mt-0.5">
|
|
128
|
+
{formatRelativeTime(time)}
|
|
129
|
+
</p>
|
|
130
|
+
</div>
|
|
131
|
+
</div>
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export function DashboardClient({ initialSpecs, initialStats }: DashboardClientProps) {
|
|
136
|
+
const stats = initialStats;
|
|
137
|
+
|
|
138
|
+
// Get specs by status
|
|
139
|
+
const inProgressSpecs = initialSpecs
|
|
140
|
+
.filter(spec => spec.status === 'in-progress')
|
|
141
|
+
.sort((a, b) => (b.specNumber || 0) - (a.specNumber || 0))
|
|
142
|
+
.slice(0, 5);
|
|
143
|
+
|
|
144
|
+
const plannedSpecs = initialSpecs
|
|
145
|
+
.filter(spec => spec.status === 'planned')
|
|
146
|
+
.sort((a, b) => (b.specNumber || 0) - (a.specNumber || 0))
|
|
147
|
+
.slice(0, 5);
|
|
148
|
+
|
|
149
|
+
const recentlyAdded = initialSpecs
|
|
150
|
+
.sort((a, b) => {
|
|
151
|
+
if (!a.createdAt) return 1;
|
|
152
|
+
if (!b.createdAt) return -1;
|
|
153
|
+
return b.createdAt.getTime() - a.createdAt.getTime();
|
|
154
|
+
})
|
|
155
|
+
.slice(0, 5);
|
|
156
|
+
|
|
157
|
+
const recentActivity = initialSpecs
|
|
158
|
+
.filter(spec => spec.updatedAt)
|
|
159
|
+
.sort((a, b) => {
|
|
160
|
+
if (!a.updatedAt) return 1;
|
|
161
|
+
if (!b.updatedAt) return -1;
|
|
162
|
+
return b.updatedAt.getTime() - a.updatedAt.getTime();
|
|
163
|
+
})
|
|
164
|
+
.slice(0, 10);
|
|
165
|
+
|
|
166
|
+
const completeCount = stats.specsByStatus.find(s => s.status === 'complete')?.count || 0;
|
|
167
|
+
|
|
168
|
+
return (
|
|
169
|
+
<div className="min-h-screen bg-background p-4 sm:p-8">
|
|
170
|
+
<div className="max-w-7xl mx-auto space-y-6 sm:space-y-8">
|
|
171
|
+
{/* Header */}
|
|
172
|
+
<div>
|
|
173
|
+
<h1 className="text-3xl sm:text-4xl font-bold tracking-tight">Dashboard</h1>
|
|
174
|
+
<p className="text-muted-foreground mt-2">
|
|
175
|
+
Project overview and recent activity
|
|
176
|
+
</p>
|
|
177
|
+
</div>
|
|
178
|
+
|
|
179
|
+
{/* Stats Cards */}
|
|
180
|
+
<div className="grid grid-cols-2 lg:grid-cols-4 gap-3 sm:gap-4">
|
|
181
|
+
{/* Total Specs */}
|
|
182
|
+
<Card className="relative overflow-hidden">
|
|
183
|
+
<div className="absolute inset-0 bg-gradient-to-br from-blue-500/10 to-transparent" />
|
|
184
|
+
<CardHeader className="relative pb-3">
|
|
185
|
+
<div className="flex items-center justify-between">
|
|
186
|
+
<CardTitle className="text-sm font-medium text-muted-foreground">
|
|
187
|
+
Total Specs
|
|
188
|
+
</CardTitle>
|
|
189
|
+
<FileText className="h-5 w-5 text-blue-600" />
|
|
190
|
+
</div>
|
|
191
|
+
</CardHeader>
|
|
192
|
+
<CardContent className="relative">
|
|
193
|
+
<div className="text-3xl font-bold">{stats.totalSpecs}</div>
|
|
194
|
+
</CardContent>
|
|
195
|
+
</Card>
|
|
196
|
+
|
|
197
|
+
{/* In Progress */}
|
|
198
|
+
<Card className="relative overflow-hidden">
|
|
199
|
+
<div className="absolute inset-0 bg-gradient-to-br from-orange-500/10 to-transparent" />
|
|
200
|
+
<CardHeader className="relative pb-3">
|
|
201
|
+
<div className="flex items-center justify-between">
|
|
202
|
+
<CardTitle className="text-sm font-medium text-muted-foreground">
|
|
203
|
+
In Progress
|
|
204
|
+
</CardTitle>
|
|
205
|
+
<PlayCircle className="h-5 w-5 text-orange-600" />
|
|
206
|
+
</div>
|
|
207
|
+
</CardHeader>
|
|
208
|
+
<CardContent className="relative">
|
|
209
|
+
<div className="text-3xl font-bold">
|
|
210
|
+
{stats.specsByStatus.find(s => s.status === 'in-progress')?.count || 0}
|
|
211
|
+
</div>
|
|
212
|
+
</CardContent>
|
|
213
|
+
</Card>
|
|
214
|
+
|
|
215
|
+
{/* Completed */}
|
|
216
|
+
<Card className="relative overflow-hidden">
|
|
217
|
+
<div className="absolute inset-0 bg-gradient-to-br from-green-500/10 to-transparent" />
|
|
218
|
+
<CardHeader className="relative pb-3">
|
|
219
|
+
<div className="flex items-center justify-between">
|
|
220
|
+
<CardTitle className="text-sm font-medium text-muted-foreground">
|
|
221
|
+
Completed
|
|
222
|
+
</CardTitle>
|
|
223
|
+
<CheckCircle2 className="h-5 w-5 text-green-600" />
|
|
224
|
+
</div>
|
|
225
|
+
</CardHeader>
|
|
226
|
+
<CardContent className="relative">
|
|
227
|
+
<div className="text-3xl font-bold">{completeCount}</div>
|
|
228
|
+
<p className="text-xs text-muted-foreground mt-1 flex items-center gap-1">
|
|
229
|
+
<TrendingUp className="h-3 w-3" />
|
|
230
|
+
{stats.completionRate.toFixed(1)}% completion rate
|
|
231
|
+
</p>
|
|
232
|
+
</CardContent>
|
|
233
|
+
</Card>
|
|
234
|
+
|
|
235
|
+
{/* Planned */}
|
|
236
|
+
<Card className="relative overflow-hidden">
|
|
237
|
+
<div className="absolute inset-0 bg-gradient-to-br from-purple-500/10 to-transparent" />
|
|
238
|
+
<CardHeader className="relative pb-3">
|
|
239
|
+
<div className="flex items-center justify-between">
|
|
240
|
+
<CardTitle className="text-sm font-medium text-muted-foreground">
|
|
241
|
+
Planned
|
|
242
|
+
</CardTitle>
|
|
243
|
+
<Clock className="h-5 w-5 text-purple-600" />
|
|
244
|
+
</div>
|
|
245
|
+
</CardHeader>
|
|
246
|
+
<CardContent className="relative">
|
|
247
|
+
<div className="text-3xl font-bold">
|
|
248
|
+
{stats.specsByStatus.find(s => s.status === 'planned')?.count || 0}
|
|
249
|
+
</div>
|
|
250
|
+
</CardContent>
|
|
251
|
+
</Card>
|
|
252
|
+
</div>
|
|
253
|
+
|
|
254
|
+
{/* Main Content Grid */}
|
|
255
|
+
<div className="grid grid-cols-1 lg:grid-cols-3 gap-4 sm:gap-6">
|
|
256
|
+
{/* Recently Added */}
|
|
257
|
+
<Card>
|
|
258
|
+
<CardHeader>
|
|
259
|
+
<CardTitle className="text-lg flex items-center gap-2">
|
|
260
|
+
<Calendar className="h-5 w-5 text-blue-600" />
|
|
261
|
+
Recently Added
|
|
262
|
+
</CardTitle>
|
|
263
|
+
</CardHeader>
|
|
264
|
+
<CardContent className="space-y-1">
|
|
265
|
+
{recentlyAdded.slice(0, 5).map(spec => (
|
|
266
|
+
<SpecListItem key={spec.id} spec={spec} />
|
|
267
|
+
))}
|
|
268
|
+
</CardContent>
|
|
269
|
+
</Card>
|
|
270
|
+
|
|
271
|
+
{/* Planned Specs */}
|
|
272
|
+
<Card>
|
|
273
|
+
<CardHeader>
|
|
274
|
+
<CardTitle className="text-lg flex items-center gap-2">
|
|
275
|
+
<Clock className="h-5 w-5 text-purple-600" />
|
|
276
|
+
Planned ({plannedSpecs.length})
|
|
277
|
+
</CardTitle>
|
|
278
|
+
</CardHeader>
|
|
279
|
+
<CardContent className="space-y-1">
|
|
280
|
+
{plannedSpecs.length > 0 ? (
|
|
281
|
+
plannedSpecs.map(spec => (
|
|
282
|
+
<SpecListItem key={spec.id} spec={spec} />
|
|
283
|
+
))
|
|
284
|
+
) : (
|
|
285
|
+
<p className="text-sm text-muted-foreground py-4 text-center">
|
|
286
|
+
No planned specs
|
|
287
|
+
</p>
|
|
288
|
+
)}
|
|
289
|
+
</CardContent>
|
|
290
|
+
</Card>
|
|
291
|
+
|
|
292
|
+
{/* In Progress Specs */}
|
|
293
|
+
<Card>
|
|
294
|
+
<CardHeader>
|
|
295
|
+
<CardTitle className="text-lg flex items-center gap-2">
|
|
296
|
+
<PlayCircle className="h-5 w-5 text-orange-600" />
|
|
297
|
+
In Progress ({inProgressSpecs.length})
|
|
298
|
+
</CardTitle>
|
|
299
|
+
</CardHeader>
|
|
300
|
+
<CardContent className="space-y-1">
|
|
301
|
+
{inProgressSpecs.length > 0 ? (
|
|
302
|
+
inProgressSpecs.map(spec => (
|
|
303
|
+
<SpecListItem key={spec.id} spec={spec} />
|
|
304
|
+
))
|
|
305
|
+
) : (
|
|
306
|
+
<p className="text-sm text-muted-foreground py-4 text-center">
|
|
307
|
+
No specs in progress
|
|
308
|
+
</p>
|
|
309
|
+
)}
|
|
310
|
+
</CardContent>
|
|
311
|
+
</Card>
|
|
312
|
+
</div>
|
|
313
|
+
|
|
314
|
+
{/* Activity Timeline */}
|
|
315
|
+
<Card>
|
|
316
|
+
<CardHeader>
|
|
317
|
+
<CardTitle className="text-lg">Recent Activity</CardTitle>
|
|
318
|
+
</CardHeader>
|
|
319
|
+
<CardContent>
|
|
320
|
+
<div className="border-l-2 border-muted pl-4 space-y-1">
|
|
321
|
+
{recentActivity.map(spec => (
|
|
322
|
+
<ActivityItem
|
|
323
|
+
key={spec.id}
|
|
324
|
+
spec={spec}
|
|
325
|
+
action="updated"
|
|
326
|
+
time={spec.updatedAt}
|
|
327
|
+
/>
|
|
328
|
+
))}
|
|
329
|
+
</div>
|
|
330
|
+
</CardContent>
|
|
331
|
+
</Card>
|
|
332
|
+
|
|
333
|
+
{/* Quick Actions */}
|
|
334
|
+
<Card>
|
|
335
|
+
<CardHeader>
|
|
336
|
+
<CardTitle className="text-lg">Quick Actions</CardTitle>
|
|
337
|
+
</CardHeader>
|
|
338
|
+
<CardContent>
|
|
339
|
+
<div className="flex flex-wrap gap-3">
|
|
340
|
+
<Button asChild>
|
|
341
|
+
<Link href="/specs">
|
|
342
|
+
<List className="h-4 w-4 mr-2" />
|
|
343
|
+
View All Specs
|
|
344
|
+
</Link>
|
|
345
|
+
</Button>
|
|
346
|
+
<Button variant="outline" asChild>
|
|
347
|
+
<Link href="/specs?view=board">
|
|
348
|
+
<LayoutGrid className="h-4 w-4 mr-2" />
|
|
349
|
+
Board View
|
|
350
|
+
</Link>
|
|
351
|
+
</Button>
|
|
352
|
+
<Button variant="outline" asChild>
|
|
353
|
+
<Link href="/stats">
|
|
354
|
+
<TrendingUp className="h-4 w-4 mr-2" />
|
|
355
|
+
View Stats
|
|
356
|
+
</Link>
|
|
357
|
+
</Button>
|
|
358
|
+
</div>
|
|
359
|
+
</CardContent>
|
|
360
|
+
</Card>
|
|
361
|
+
</div>
|
|
362
|
+
</div>
|
|
363
|
+
);
|
|
364
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useEffect } from 'react';
|
|
4
|
+
import { Button } from '@/components/ui/button';
|
|
5
|
+
|
|
6
|
+
export default function Error({
|
|
7
|
+
error,
|
|
8
|
+
reset,
|
|
9
|
+
}: {
|
|
10
|
+
error: Error & { digest?: string };
|
|
11
|
+
reset: () => void;
|
|
12
|
+
}) {
|
|
13
|
+
useEffect(() => {
|
|
14
|
+
// Log the error to an error reporting service
|
|
15
|
+
console.error('Application error:', error);
|
|
16
|
+
}, [error]);
|
|
17
|
+
|
|
18
|
+
return (
|
|
19
|
+
<div className="min-h-screen bg-background flex items-center justify-center px-4">
|
|
20
|
+
<div className="text-center space-y-6 max-w-md">
|
|
21
|
+
<div className="space-y-2">
|
|
22
|
+
<h1 className="text-6xl font-bold text-destructive">500</h1>
|
|
23
|
+
<h2 className="text-2xl font-semibold">Something went wrong!</h2>
|
|
24
|
+
<p className="text-muted-foreground">
|
|
25
|
+
An unexpected error occurred. Please try again or contact support if the problem persists.
|
|
26
|
+
</p>
|
|
27
|
+
{error.digest && (
|
|
28
|
+
<p className="text-xs text-muted-foreground font-mono bg-muted px-3 py-2 rounded">
|
|
29
|
+
Error ID: {error.digest}
|
|
30
|
+
</p>
|
|
31
|
+
)}
|
|
32
|
+
</div>
|
|
33
|
+
|
|
34
|
+
<div className="flex gap-4 justify-center">
|
|
35
|
+
<Button onClick={reset}>Try Again</Button>
|
|
36
|
+
<Button variant="outline" onClick={() => window.location.href = '/'}>
|
|
37
|
+
Go Home
|
|
38
|
+
</Button>
|
|
39
|
+
</div>
|
|
40
|
+
</div>
|
|
41
|
+
</div>
|
|
42
|
+
);
|
|
43
|
+
}
|