diff --git a/src/app/actions/projects.ts b/src/app/actions/projects.ts index 0391870..72e13a3 100644 --- a/src/app/actions/projects.ts +++ b/src/app/actions/projects.ts @@ -33,6 +33,7 @@ export async function createProject(input: { repoOwner: string; repoName: string; color: string; + kind?: ProjectRecord["kind"]; }): Promise<{ id: string }> { await requireOwner(); const pb = await getServerPb(); @@ -51,6 +52,7 @@ export async function createProject(input: { slug: uniqueSlug(slugify(name), siblings.map((p) => p.slug)), description: input.description.slice(0, 20000), status: "active", + kind: input.kind ?? "project", color: input.color || "#2f6dad", repo_owner: input.repoOwner.trim(), repo_name: input.repoName.trim(), diff --git a/src/app/admin/projects/page.tsx b/src/app/admin/projects/page.tsx index a6fc905..57394da 100644 --- a/src/app/admin/projects/page.tsx +++ b/src/app/admin/projects/page.tsx @@ -21,7 +21,7 @@ export default async function AdminProjectsPage() { {rows.map(({ project, client, cardCount }) => ( @@ -50,6 +50,13 @@ export default async function AdminProjectsPage() { Not linked )} + + {project.kind === "prototype" ? ( + Prototype + ) : ( + Project + )} + {cardCount} diff --git a/src/app/page.tsx b/src/app/page.tsx index 7c3af7a..11059d3 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -6,13 +6,38 @@ import { AppShell } from "@/components/shell/app-shell"; import { TopBar } from "@/components/shell/top-bar"; import { Pill } from "@/components/ui/pill"; import { getSession } from "@/lib/auth/session"; -import { loadProjectSummaries } from "@/lib/data/board"; +import { loadProjectSummaries, type ProjectSummary } from "@/lib/data/board"; +import type { ClientRecord } from "@/lib/pb/types"; + +interface ClientGroup { + client: ClientRecord; + summaries: ProjectSummary[]; +} + +/** Preserves the incoming (project-name) order within each client's group. */ +function groupByClient(summaries: ProjectSummary[]): ClientGroup[] { + const groups = new Map(); + + for (const summary of summaries) { + const group = groups.get(summary.client.id) ?? { + client: summary.client, + summaries: [], + }; + group.summaries.push(summary); + groups.set(summary.client.id, group); + } + + return [...groups.values()].sort((a, b) => + a.client.name.localeCompare(b.client.name) + ); +} export default async function HomePage() { const session = await getSession(); if (!session) redirect("/login"); const summaries = await loadProjectSummaries(); + const groups = groupByClient(summaries); return ( @@ -26,7 +51,7 @@ export default async function HomePage() { />
- {summaries.length === 0 ? ( + {groups.length === 0 ? (

No projects yet

@@ -36,47 +61,71 @@ export default async function HomePage() {

) : ( -
    - {summaries.map(({ project, client, openCards, inboxCards }) => ( -
  • - -
    +
    + {groups.map((group) => ( +
    + {/* Only the owner sees more than one group — a client manager's + own name over their own projects would be noise. */} + {session.role === "owner" && ( +
    -
    -

    - {project.name} -

    -

    - {client.name} -

    -
    - {inboxCards > 0 && ( - - - {inboxCards} - - )} +

    + {group.client.name} +

    + )} -
    - - {openCards} - - - open {openCards === 1 ? "card" : "cards"} - -
    - -
  • +
      + {group.summaries.map(({ project, client, openCards, inboxCards }) => ( +
    • + +
      + +
      +

      + {project.name} +

      +

      + {client.name} +

      +
      + {project.kind === "prototype" && ( + Prototype + )} + {inboxCards > 0 && ( + + + {inboxCards} + + )} +
      + +
      + + {openCards} + + + open {openCards === 1 ? "card" : "cards"} + +
      + +
    • + ))} +
    + ))} -
+
)}
diff --git a/src/components/admin/project-form.tsx b/src/components/admin/project-form.tsx index c540994..e81b1be 100644 --- a/src/components/admin/project-form.tsx +++ b/src/components/admin/project-form.tsx @@ -5,7 +5,7 @@ import { Plus } from "lucide-react"; import { createProject } from "@/app/actions/projects"; import { DEFAULT_BRAND_COLOR } from "@/lib/brand-default"; -import type { ClientRecord } from "@/lib/pb/types"; +import type { ClientRecord, ProjectRecord } from "@/lib/pb/types"; export function NewProjectForm({ clients }: { clients: ClientRecord[] }) { const [open, setOpen] = useState(false); @@ -14,6 +14,7 @@ export function NewProjectForm({ clients }: { clients: ClientRecord[] }) { const [description, setDescription] = useState(""); const [repoOwner, setRepoOwner] = useState(""); const [repoName, setRepoName] = useState(""); + const [kind, setKind] = useState("project"); const [pending, startTransition] = useTransition(); const [error, setError] = useState(null); @@ -37,11 +38,13 @@ export function NewProjectForm({ clients }: { clients: ClientRecord[] }) { repoOwner, repoName, color: DEFAULT_BRAND_COLOR, + kind, }); setName(""); setDescription(""); setRepoOwner(""); setRepoName(""); + setKind("project"); setOpen(false); } catch (e) { setError(e instanceof Error ? e.message : "Could not create that project"); @@ -132,6 +135,23 @@ export function NewProjectForm({ clients }: { clients: ClientRecord[] }) { + + {error && (

{error}