Skip to content

Commit e668a68

Browse files
committed
feat: build project dashboard create flow
1 parent 71458a7 commit e668a68

32 files changed

Lines changed: 1659 additions & 337 deletions

app/(app)/app/page.tsx

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,10 @@ import { Badge } from "@/components/ui/badge";
55
import { buttonVariants } from "@/components/ui/button";
66
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
77
import { getAppContext } from "@/lib/server/auth";
8-
import { buildWorkspacePath } from "@/lib/routes";
8+
import {
9+
buildProfileSettingsPath,
10+
buildWorkspaceProjectsPath,
11+
} from "@/lib/routes";
912
import { cn } from "@/lib/utils";
1013

1114
export default async function AppOverviewPage() {
@@ -33,13 +36,13 @@ export default async function AppOverviewPage() {
3336
</div>
3437
<div className="flex flex-col gap-3 sm:flex-row">
3538
<Link
36-
href={buildWorkspacePath(context.activeWorkspace.slug)}
39+
href={buildWorkspaceProjectsPath(context.activeWorkspace.slug)}
3740
className={cn(buttonVariants())}
3841
>
3942
Open workspace
4043
</Link>
4144
<Link
42-
href="/settings/profile"
45+
href={buildProfileSettingsPath()}
4346
className={cn(buttonVariants({ variant: "outline" }))}
4447
>
4548
Open profile widget
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import { WorkspaceSectionPlaceholder } from "@/components/app/workspace-section-placeholder";
2+
import { requireWorkspaceRouteContext } from "@/lib/server/auth";
3+
4+
export default async function WorkspacePackagesPage({
5+
params,
6+
}: {
7+
params: Promise<{ slug: string }>;
8+
}) {
9+
const { slug } = await params;
10+
const context = await requireWorkspaceRouteContext(slug);
11+
12+
return (
13+
<WorkspaceSectionPlaceholder
14+
workspace={context.routeWorkspace}
15+
role={context.role}
16+
eyebrow="Procurement packages"
17+
title="Packages"
18+
description="Generated RFQ/RFP packages, scoring matrices, rubrics, and committee packets will be surfaced here in a later phase."
19+
emptyTitle="Package workspace is not built yet"
20+
emptyDescription="No procurement generation, editing, review, approval, or export workflows are included in Phase 2A."
21+
/>
22+
);
23+
}

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

Lines changed: 6 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,40 +1,16 @@
1-
import { preloadQuery } from "convex/nextjs";
2-
import { api } from "@/convex/_generated/api";
3-
import { Card, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
4-
import { ProjectWorkspacePanel } from "@/components/app/project-workspace-panel";
1+
import { redirect } from "next/navigation";
52
import { requireWorkspaceRouteContext } from "@/lib/server/auth";
3+
import { buildWorkspaceProjectsPath } from "@/lib/routes";
64

75
export default async function WorkspacePage({
86
params,
97
}: {
108
params: Promise<{ slug: string }>;
119
}) {
1210
const { slug } = await params;
13-
const context = await requireWorkspaceRouteContext(slug);
11+
const context = await requireWorkspaceRouteContext(slug, {
12+
returnTo: buildWorkspaceProjectsPath(slug),
13+
});
1414

15-
const preloadedProjects = await preloadQuery(
16-
api.projects.listProjects,
17-
{
18-
workspaceId: context.activeWorkspace.id,
19-
},
20-
{ token: context.auth.accessToken },
21-
);
22-
23-
return (
24-
<div className="flex flex-col gap-6">
25-
<Card className="page-surface">
26-
<CardHeader>
27-
<CardTitle className="text-3xl">Workspace projects</CardTitle>
28-
<CardDescription>
29-
This panel is the sample domain for the starter: org-scoped projects backed by Convex queries and mutations.
30-
</CardDescription>
31-
</CardHeader>
32-
</Card>
33-
34-
<ProjectWorkspacePanel
35-
workspaceId={context.activeWorkspace.id}
36-
preloadedProjects={preloadedProjects}
37-
/>
38-
</div>
39-
);
15+
redirect(buildWorkspaceProjectsPath(context.routeWorkspace.slug));
4016
}
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
import Link from "next/link";
2+
import {
3+
BrandedCard,
4+
PageHeader,
5+
StatusBadge,
6+
} from "@/components/app/app-primitives";
7+
import { Button } from "@/components/ui/button";
8+
import { getProjectStageLabel } from "@/lib/frontend-contracts";
9+
import { buildWorkspaceProjectsPath } from "@/lib/routes";
10+
import { requireProjectRouteContext } from "@/lib/server/auth";
11+
import { formatTimestamp } from "@/lib/utils";
12+
13+
export default async function ProjectIntakePlaceholderPage({
14+
params,
15+
}: {
16+
params: Promise<{ slug: string; projectSlug: string }>;
17+
}) {
18+
const { slug, projectSlug } = await params;
19+
const context = await requireProjectRouteContext({
20+
workspaceSlug: slug,
21+
projectSlug,
22+
});
23+
const project = context.project;
24+
25+
return (
26+
<div className="flex flex-col gap-6">
27+
<PageHeader
28+
eyebrow="Project intake"
29+
title={project.name}
30+
description="The persistent intake wizard is the next phase. This protected route confirms the new project was created and routed correctly."
31+
action={
32+
<Button
33+
render={<Link href={buildWorkspaceProjectsPath(context.routeWorkspace.slug)} />}
34+
variant="outline"
35+
>
36+
Back to projects
37+
</Button>
38+
}
39+
/>
40+
41+
<BrandedCard className="grid gap-5 p-6 md:grid-cols-[1fr_18rem]">
42+
<div className="space-y-4">
43+
<StatusBadge>
44+
{getProjectStageLabel(project)}
45+
</StatusBadge>
46+
<div>
47+
<h2 className="text-lg font-semibold text-[color:var(--cpl-navy)]">
48+
Intake route ready
49+
</h2>
50+
<p className="mt-2 max-w-2xl text-sm leading-6 text-muted-foreground">
51+
This page is intentionally limited to route confirmation. Intake
52+
fields, autosave, delivery recommendations, procurement packages,
53+
approvals, and exports remain out of scope for Phase 2B.
54+
</p>
55+
</div>
56+
</div>
57+
58+
<dl className="grid gap-3 rounded-lg border border-[color:var(--cpl-soft-border)] bg-muted/35 p-4 text-sm">
59+
<div>
60+
<dt className="text-xs font-semibold uppercase tracking-[0.14em] text-muted-foreground">
61+
Project ID
62+
</dt>
63+
<dd className="mt-1 font-medium text-[color:var(--cpl-navy)]">
64+
{project.slug}
65+
</dd>
66+
</div>
67+
<div>
68+
<dt className="text-xs font-semibold uppercase tracking-[0.14em] text-muted-foreground">
69+
Program
70+
</dt>
71+
<dd className="mt-1 text-muted-foreground">
72+
{project.programDepartment ?? "Not assigned"}
73+
</dd>
74+
</div>
75+
<div>
76+
<dt className="text-xs font-semibold uppercase tracking-[0.14em] text-muted-foreground">
77+
Updated
78+
</dt>
79+
<dd className="mt-1 text-muted-foreground">
80+
{formatTimestamp(project.updatedAt)}
81+
</dd>
82+
</div>
83+
</dl>
84+
</BrandedCard>
85+
</div>
86+
);
87+
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
"use client";
2+
3+
import { Button } from "@/components/ui/button";
4+
import { PageHeader, PermissionNotice } from "@/components/app/app-primitives";
5+
6+
export default function WorkspaceProjectsError({
7+
error,
8+
reset,
9+
}: {
10+
error: Error & { digest?: string };
11+
reset: () => void;
12+
}) {
13+
const isPermissionError =
14+
error.message.toLowerCase().includes("access denied") ||
15+
error.message.toLowerCase().includes("permission");
16+
17+
return (
18+
<div className="flex flex-col gap-6">
19+
<PageHeader
20+
eyebrow="Project dashboard"
21+
title="Projects"
22+
description="The project list could not be loaded for this workspace."
23+
action={
24+
<Button type="button" variant="outline" onClick={reset}>
25+
Retry
26+
</Button>
27+
}
28+
/>
29+
<PermissionNotice
30+
title={isPermissionError ? "Permission denied" : "Project list unavailable"}
31+
description={
32+
isPermissionError
33+
? "Your current workspace context does not grant access to these projects."
34+
: "Refresh the route or retry after the project service is available."
35+
}
36+
/>
37+
</div>
38+
);
39+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import { LoadingState, PageHeader } from "@/components/app/app-primitives";
2+
3+
export default function WorkspaceProjectsLoading() {
4+
return (
5+
<div className="flex flex-col gap-6">
6+
<PageHeader
7+
eyebrow="Project dashboard"
8+
title="Projects"
9+
description="Loading active organization projects."
10+
/>
11+
<LoadingState label="Loading project list" />
12+
</div>
13+
);
14+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import { ProjectDashboard } from "@/components/app/project-workspace-panel";
2+
import { requireWorkspaceRouteContext } from "@/lib/server/auth";
3+
4+
export default async function WorkspaceProjectsPage({
5+
params,
6+
}: {
7+
params: Promise<{ slug: string }>;
8+
}) {
9+
const { slug } = await params;
10+
const context = await requireWorkspaceRouteContext(slug);
11+
12+
return (
13+
<ProjectDashboard
14+
workspaceId={context.routeWorkspace.id}
15+
workspaceName={context.routeWorkspace.name}
16+
workspaceSlug={context.routeWorkspace.slug}
17+
/>
18+
);
19+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import { WorkspaceSectionPlaceholder } from "@/components/app/workspace-section-placeholder";
2+
import { requireWorkspaceRouteContext } from "@/lib/server/auth";
3+
4+
export default async function WorkspaceReportsPage({
5+
params,
6+
}: {
7+
params: Promise<{ slug: string }>;
8+
}) {
9+
const { slug } = await params;
10+
const context = await requireWorkspaceRouteContext(slug);
11+
12+
return (
13+
<WorkspaceSectionPlaceholder
14+
workspace={context.routeWorkspace}
15+
role={context.role}
16+
eyebrow="Portfolio reports"
17+
title="Reports"
18+
description="Reporting will summarize project setup, delivery recommendations, package status, and approval readiness after live project data is available."
19+
emptyTitle="Reports are not built yet"
20+
emptyDescription="This section currently exists to validate protected navigation and workspace routing only."
21+
/>
22+
);
23+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import { WorkspaceSectionPlaceholder } from "@/components/app/workspace-section-placeholder";
2+
import { requireWorkspaceRouteContext } from "@/lib/server/auth";
3+
4+
export default async function WorkspaceSettingsPage({
5+
params,
6+
}: {
7+
params: Promise<{ slug: string }>;
8+
}) {
9+
const { slug } = await params;
10+
const context = await requireWorkspaceRouteContext(slug);
11+
12+
return (
13+
<WorkspaceSectionPlaceholder
14+
workspace={context.routeWorkspace}
15+
role={context.role}
16+
eyebrow="Workspace settings"
17+
title="Settings"
18+
description="Workspace-level configuration will stay aligned with WorkOS organization, role, and permission boundaries."
19+
emptyTitle="Workspace settings are not built yet"
20+
emptyDescription="Profile and admin WorkOS widget routes remain available from the user menu for this phase."
21+
/>
22+
);
23+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import { WorkspaceSectionPlaceholder } from "@/components/app/workspace-section-placeholder";
2+
import { requireWorkspaceRouteContext } from "@/lib/server/auth";
3+
4+
export default async function WorkspaceTemplatesPage({
5+
params,
6+
}: {
7+
params: Promise<{ slug: string }>;
8+
}) {
9+
const { slug } = await params;
10+
const context = await requireWorkspaceRouteContext(slug);
11+
12+
return (
13+
<WorkspaceSectionPlaceholder
14+
workspace={context.routeWorkspace}
15+
role={context.role}
16+
eyebrow="Workspace templates"
17+
title="Templates"
18+
description="Reusable procurement and intake templates will live here after the core project dashboard is in place."
19+
emptyTitle="Template library is not built yet"
20+
emptyDescription="Phase 2A only proves navigation and tenant context for this section."
21+
/>
22+
);
23+
}

0 commit comments

Comments
 (0)