Skip to content

Commit 9e45054

Browse files
committed
feat: add capital project launchpad workflow
1 parent 83516cd commit 9e45054

57 files changed

Lines changed: 5082 additions & 275 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.env.example

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,6 @@ NEXT_PUBLIC_WORKOS_REDIRECT_URI=http://localhost:3000/auth/callback
55
NEXT_PUBLIC_CONVEX_URL=https://your-project.convex.cloud
66
CONVEX_DEPLOYMENT=dev:your-deployment-name
77
CONVEX_DEPLOY_KEY=
8+
OPENAI_API_KEY=sk_your_openai_api_key
9+
OPENAI_MODEL=gpt-5.5
10+
OPENAI_TIMEOUT_MS=30000

app/(app)/app/page.tsx

Lines changed: 179 additions & 81 deletions
Original file line numberDiff line numberDiff line change
@@ -1,110 +1,208 @@
11
import Link from "next/link";
22
import { fetchQuery } from "convex/nextjs";
3+
import {
4+
ArrowRightIcon,
5+
CheckCircle2Icon,
6+
FileCheck2Icon,
7+
FolderKanbanIcon,
8+
ListChecksIcon,
9+
PackageCheckIcon,
10+
SparklesIcon,
11+
} from "lucide-react";
312
import { api } from "@/convex/_generated/api";
413
import { Badge } from "@/components/ui/badge";
514
import { buttonVariants } from "@/components/ui/button";
6-
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
15+
import {
16+
CompactPanel,
17+
SectionKicker,
18+
StatusDot,
19+
WorkflowBoard,
20+
WorkflowStateStrip,
21+
} from "@/components/app/workflow-primitives";
722
import { getAppContext } from "@/lib/server/auth";
8-
import { cn } from "@/lib/utils";
23+
import { cn, formatTimestamp } from "@/lib/utils";
924

1025
export default async function AppOverviewPage() {
1126
const context = await getAppContext();
12-
const projects = await fetchQuery(
13-
api.projects.listProjects,
27+
const summary = await fetchQuery(
28+
api.projects.dashboardSummary,
1429
{
1530
workspaceId: context.activeWorkspace.id,
1631
organizationId: context.activeWorkspace.organizationId,
1732
},
1833
{ token: context.auth.accessToken },
1934
);
2035

36+
const metrics = [
37+
{
38+
label: "Projects",
39+
value: summary.projectCount,
40+
icon: FolderKanbanIcon,
41+
},
42+
{
43+
label: "Intakes started",
44+
value: summary.intakeCount,
45+
icon: ListChecksIcon,
46+
},
47+
{
48+
label: "Pending review",
49+
value: summary.pendingReviewCount,
50+
icon: SparklesIcon,
51+
},
52+
{
53+
label: "Approved outputs",
54+
value: summary.approvedOutputCount,
55+
icon: FileCheck2Icon,
56+
},
57+
];
58+
const flowCards = [
59+
{
60+
label: "Project dashboard",
61+
detail: "Create and track owner projects.",
62+
icon: FolderKanbanIcon,
63+
href: `/w/${context.activeWorkspace.slug}/projects`,
64+
},
65+
{
66+
label: "Project intake",
67+
detail: "Capture scope, budget, timing, and constraints.",
68+
icon: ListChecksIcon,
69+
},
70+
{
71+
label: "Delivery recommendation",
72+
detail: "Compare method fit and review decision drivers.",
73+
icon: SparklesIcon,
74+
},
75+
{
76+
label: "Procurement package",
77+
detail: "Generate, edit, review, approve, and export.",
78+
icon: PackageCheckIcon,
79+
},
80+
];
81+
2182
return (
22-
<div className="flex flex-col gap-6">
23-
<section className="page-surface grid gap-6 p-6 lg:grid-cols-[1.25fr_0.75fr]">
24-
<div className="space-y-4">
25-
<Badge variant="secondary">Authenticated app shell</Badge>
26-
<div className="space-y-2">
27-
<h1 className="text-3xl font-semibold tracking-tight">
28-
{context.viewer.firstName ?? context.viewer.name ?? "Welcome back"}
29-
</h1>
30-
<p className="max-w-2xl text-sm leading-6 text-muted-foreground">
31-
Your active organization is mirrored from WorkOS into Convex as a workspace. The starter keeps tenancy in WorkOS and reserves Convex for app-specific data.
32-
</p>
33-
</div>
34-
<div className="flex flex-col gap-3 sm:flex-row">
83+
<div className="flex flex-col gap-5">
84+
<WorkflowBoard className="p-5 md:p-6">
85+
<div className="grid gap-5 lg:grid-cols-[0.95fr_1.4fr] lg:items-end">
86+
<div className="space-y-4">
87+
<div className="flex flex-wrap items-center gap-3">
88+
<SectionKicker>Variation 1</SectionKicker>
89+
<Badge variant="secondary">App flow & core states</Badge>
90+
</div>
91+
<div>
92+
<h1 className="font-heading text-5xl font-bold uppercase leading-[0.92] tracking-wide md:text-6xl">
93+
Capital Project Launchpad
94+
</h1>
95+
<p className="mt-3 max-w-2xl text-sm leading-6 text-muted-foreground">
96+
{context.activeWorkspace.name} moves project setup through delivery recommendation, procurement drafting, review, approval, and export.
97+
</p>
98+
</div>
3599
<Link
36-
href={`/w/${context.activeWorkspace.slug}`}
100+
href={`/w/${context.activeWorkspace.slug}/projects`}
37101
className={cn(buttonVariants())}
38102
>
39-
Open workspace
40-
</Link>
41-
<Link
42-
href="/settings/profile"
43-
className={cn(buttonVariants({ variant: "outline" }))}
44-
>
45-
Open profile widget
103+
Open project board
104+
<ArrowRightIcon data-icon="inline-end" />
46105
</Link>
47106
</div>
107+
108+
<div className="grid gap-3 md:grid-cols-2">
109+
{flowCards.map(({ label, detail, icon: Icon, href }, index) => {
110+
const body = (
111+
<>
112+
<div className="flex items-center justify-between gap-3">
113+
<span className="flex size-7 items-center justify-center rounded-full bg-[color:var(--brand-orange)] font-mono text-xs font-semibold text-white">
114+
{index + 1}
115+
</span>
116+
<Icon className="size-4 text-[color:var(--brand-navy)]" />
117+
</div>
118+
<div>
119+
<div className="font-semibold">{label}</div>
120+
<div className="mt-1 text-xs leading-5 text-muted-foreground">
121+
{detail}
122+
</div>
123+
</div>
124+
</>
125+
);
126+
127+
return href ? (
128+
<Link
129+
key={label}
130+
href={href}
131+
className="flex min-h-32 flex-col justify-between rounded-md border border-border bg-card/80 p-3 text-sm hover:bg-card"
132+
>
133+
{body}
134+
</Link>
135+
) : (
136+
<div
137+
key={label}
138+
className="flex min-h-32 flex-col justify-between rounded-md border border-border bg-card/80 p-3 text-sm"
139+
>
140+
{body}
141+
</div>
142+
);
143+
})}
144+
</div>
48145
</div>
49-
<Card className="border-border/70 bg-background/70">
50-
<CardHeader>
51-
<CardTitle>Current tenant</CardTitle>
52-
<CardDescription>
53-
Role-aware navigation comes from WorkOS session claims.
54-
</CardDescription>
55-
</CardHeader>
56-
<CardContent className="space-y-3 text-sm text-muted-foreground">
57-
<div className="flex items-center justify-between">
58-
<span>Workspace</span>
59-
<span className="font-medium text-foreground">
60-
{context.activeWorkspace.name}
61-
</span>
62-
</div>
63-
<div className="flex items-center justify-between">
64-
<span>Role</span>
65-
<Badge variant="outline">{context.role}</Badge>
66-
</div>
67-
<div className="flex items-center justify-between">
68-
<span>Projects</span>
69-
<span className="font-medium text-foreground">{projects.length}</span>
146+
</WorkflowBoard>
147+
148+
<WorkflowStateStrip
149+
states={[
150+
{ label: "Draft generated", detail: `${summary.intakeCount} intakes started`, status: "draft" },
151+
{ label: "Review complete", detail: `${summary.pendingReviewCount} pending review`, status: "reviewed" },
152+
{ label: "Approved / locked", detail: `${summary.approvedOutputCount} export-ready`, status: "approved", locked: true },
153+
]}
154+
/>
155+
156+
<section className="grid gap-3 md:grid-cols-4">
157+
{metrics.map(({ label, value, icon: Icon }) => (
158+
<CompactPanel key={label} className="min-h-28">
159+
<div className="flex items-start justify-between gap-3">
160+
<div>
161+
<div className="text-sm text-muted-foreground">{label}</div>
162+
<div className="mt-3 font-heading text-4xl font-bold tracking-wide">
163+
{value}
164+
</div>
165+
</div>
166+
<div className="flex size-8 items-center justify-center rounded-md bg-primary text-primary-foreground">
167+
<Icon />
168+
</div>
70169
</div>
71-
</CardContent>
72-
</Card>
170+
</CompactPanel>
171+
))}
73172
</section>
74173

75-
<section className="grid gap-4 md:grid-cols-3">
76-
<Card className="page-surface">
77-
<CardHeader>
78-
<CardTitle>Realtime sample data</CardTitle>
79-
<CardDescription>
80-
`/w/[slug]` hydrates a Convex project panel with typed mutations.
81-
</CardDescription>
82-
</CardHeader>
83-
<CardContent className="text-3xl font-semibold">{projects.length}</CardContent>
84-
</Card>
85-
<Card className="page-surface">
86-
<CardHeader>
87-
<CardTitle>Accessible workspaces</CardTitle>
88-
<CardDescription>
89-
The workspace switcher uses WorkOS memberships and session switching.
90-
</CardDescription>
91-
</CardHeader>
92-
<CardContent className="text-3xl font-semibold">
93-
{context.workspaces.length}
94-
</CardContent>
95-
</Card>
96-
<Card className="page-surface">
97-
<CardHeader>
98-
<CardTitle>Admin controls</CardTitle>
99-
<CardDescription>
100-
Widgets are loaded on demand and token issuance stays server-side.
101-
</CardDescription>
102-
</CardHeader>
103-
<CardContent className="text-3xl font-semibold">
104-
{context.role === "owner" || context.role === "admin" ? "Enabled" : "Member"}
105-
</CardContent>
106-
</Card>
107-
</section>
174+
<CompactPanel>
175+
<div className="mb-3 flex items-center justify-between gap-3">
176+
<div>
177+
<SectionKicker>Generation log</SectionKicker>
178+
<h2 className="mt-1 font-heading text-xl font-bold uppercase tracking-wide">
179+
Recent system activity
180+
</h2>
181+
</div>
182+
<Badge variant="outline">{summary.recentEvents.length}</Badge>
183+
</div>
184+
<div className="grid gap-2">
185+
{summary.recentEvents.length === 0 ? (
186+
<div className="flex items-center gap-3 rounded-md border border-dashed border-border bg-background/60 p-4 text-sm text-muted-foreground">
187+
<CheckCircle2Icon className="size-4 text-[color:var(--brand-teal)]" />
188+
<span>No generation events yet.</span>
189+
</div>
190+
) : (
191+
summary.recentEvents.map((event) => (
192+
<div
193+
key={event._id}
194+
className="flex flex-col gap-2 rounded-md border border-border bg-background/70 p-3 text-sm md:flex-row md:items-center"
195+
>
196+
<StatusDot status={event.status} className="font-medium capitalize" />
197+
<div className="font-medium capitalize">{event.promptType}</div>
198+
<div className="text-muted-foreground md:ml-auto">
199+
{formatTimestamp(event.createdAt)}
200+
</div>
201+
</div>
202+
))
203+
)}
204+
</div>
205+
</CompactPanel>
108206
</div>
109207
);
110208
}

app/(app)/w/[slug]/page.tsx

Lines changed: 2 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -1,55 +1,11 @@
1-
import { preloadQuery } from "convex/nextjs";
2-
import { notFound, redirect } from "next/navigation";
3-
import { api } from "@/convex/_generated/api";
4-
import { Card, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
5-
import { ProjectWorkspacePanel } from "@/components/app/project-workspace-panel";
6-
import { getAppContext } from "@/lib/server/auth";
7-
import type { WorkspaceSummary } from "@/lib/types";
1+
import { redirect } from "next/navigation";
82

93
export default async function WorkspacePage({
104
params,
115
}: {
126
params: Promise<{ slug: string }>;
137
}) {
148
const { slug } = await params;
15-
const context = await getAppContext();
169

17-
if (slug !== context.activeWorkspace.slug) {
18-
const isKnownWorkspace = context.workspaces.some(
19-
(workspace: WorkspaceSummary) => workspace.slug === slug,
20-
);
21-
22-
if (!isKnownWorkspace) {
23-
notFound();
24-
}
25-
26-
redirect("/app");
27-
}
28-
29-
const preloadedProjects = await preloadQuery(
30-
api.projects.listProjects,
31-
{
32-
workspaceId: context.activeWorkspace.id,
33-
organizationId: context.activeWorkspace.organizationId,
34-
},
35-
{ token: context.auth.accessToken },
36-
);
37-
38-
return (
39-
<div className="flex flex-col gap-6">
40-
<Card className="page-surface">
41-
<CardHeader>
42-
<CardTitle className="text-3xl">Workspace projects</CardTitle>
43-
<CardDescription>
44-
This panel is the sample domain for the starter: org-scoped projects backed by Convex queries and mutations.
45-
</CardDescription>
46-
</CardHeader>
47-
</Card>
48-
49-
<ProjectWorkspacePanel
50-
workspaceId={context.activeWorkspace.id}
51-
preloadedProjects={preloadedProjects}
52-
/>
53-
</div>
54-
);
10+
redirect(`/w/${slug}/projects`);
5511
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import { fetchQuery } from "convex/nextjs";
2+
import { api } from "@/convex/_generated/api";
3+
import { DeliveryWorkspace } from "@/components/app/delivery-workspace";
4+
import { ProjectWorkflowNav } from "@/components/app/project-workflow-nav";
5+
import { getProjectRouteContext } from "@/lib/server/project-route";
6+
7+
export default async function DeliveryPage({
8+
params,
9+
}: {
10+
params: Promise<{ slug: string; projectSlug: string }>;
11+
}) {
12+
const { slug, projectSlug } = await params;
13+
const context = await getProjectRouteContext({
14+
workspaceSlug: slug,
15+
projectSlug,
16+
returnTo: `/w/${slug}/projects/${projectSlug}/delivery`,
17+
});
18+
const analyses = await fetchQuery(
19+
api.deliveryAnalyses.listByProject,
20+
{ projectId: context.project._id },
21+
{ token: context.auth.accessToken },
22+
);
23+
24+
return (
25+
<div className="flex flex-col gap-6">
26+
<ProjectWorkflowNav
27+
workspaceSlug={slug}
28+
projectSlug={projectSlug}
29+
projectName={context.project.name}
30+
active="delivery"
31+
status={context.project.status}
32+
/>
33+
<DeliveryWorkspace projectId={context.project._id} analyses={analyses} />
34+
</div>
35+
);
36+
}

0 commit comments

Comments
 (0)