feat(17-02): create InitiativeList component

- Fetches initiatives via trpc.listInitiatives with optional status filter
- Handles loading, error, empty, and populated states
- Empty state shows "No initiatives yet" with CTA button
- Error state shows message with retry button
- Renders vertical stack of InitiativeCards with space-y-3 gap
- Fix: SerializedInitiative type for tRPC Date→string serialization
This commit is contained in:
Lukas May
2026-02-04 21:04:43 +01:00
parent f6caa5df1a
commit 895c96435c
2 changed files with 101 additions and 2 deletions

View File

@@ -1,5 +1,4 @@
import { MoreHorizontal, Eye, Bot } from "lucide-react";
import type { Initiative } from "@codewalk-district/shared";
import { Card } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import {
@@ -13,8 +12,18 @@ import { StatusBadge } from "@/components/StatusBadge";
import { ProgressBar } from "@/components/ProgressBar";
import { trpc } from "@/lib/trpc";
/** Initiative shape as returned by tRPC (Date serialized to string over JSON) */
export interface SerializedInitiative {
id: string;
name: string;
description: string | null;
status: "active" | "completed" | "archived";
createdAt: string;
updatedAt: string;
}
interface InitiativeCardProps {
initiative: Initiative;
initiative: SerializedInitiative;
onView: () => void;
onSpawnArchitect: (mode: "discuss" | "breakdown") => void;
onDelete: () => void;

View File

@@ -0,0 +1,90 @@
import { AlertCircle, Plus } from "lucide-react";
import { Button } from "@/components/ui/button";
import { InitiativeCard } from "@/components/InitiativeCard";
import { trpc } from "@/lib/trpc";
interface InitiativeListProps {
statusFilter?: "all" | "active" | "completed" | "archived";
onCreateNew: () => void;
onViewInitiative: (id: string) => void;
onSpawnArchitect: (
initiativeId: string,
mode: "discuss" | "breakdown",
) => void;
onDeleteInitiative: (id: string) => void;
}
export function InitiativeList({
statusFilter = "all",
onCreateNew,
onViewInitiative,
onSpawnArchitect,
onDeleteInitiative,
}: InitiativeListProps) {
const initiativesQuery = trpc.listInitiatives.useQuery(
statusFilter === "all" ? undefined : { status: statusFilter },
);
// Loading state
if (initiativesQuery.isLoading) {
return (
<div className="flex items-center justify-center py-12 text-muted-foreground">
Loading initiatives...
</div>
);
}
// Error state
if (initiativesQuery.isError) {
return (
<div className="flex flex-col items-center justify-center gap-4 py-12">
<AlertCircle className="h-8 w-8 text-destructive" />
<p className="text-sm text-destructive">
Failed to load initiatives: {initiativesQuery.error.message}
</p>
<Button
variant="outline"
size="sm"
onClick={() => initiativesQuery.refetch()}
>
Retry
</Button>
</div>
);
}
const initiatives = initiativesQuery.data ?? [];
// Empty state
if (initiatives.length === 0) {
return (
<div className="flex flex-col items-center justify-center gap-4 py-16">
<p className="text-lg font-medium text-muted-foreground">
No initiatives yet
</p>
<p className="text-sm text-muted-foreground">
Create your first initiative to start planning and executing work.
</p>
<Button onClick={onCreateNew}>
<Plus className="mr-1 h-4 w-4" />
New Initiative
</Button>
</div>
);
}
// Populated state
return (
<div className="space-y-3">
{initiatives.map((initiative) => (
<InitiativeCard
key={initiative.id}
initiative={initiative}
onView={() => onViewInitiative(initiative.id)}
onSpawnArchitect={(mode) => onSpawnArchitect(initiative.id, mode)}
onDelete={() => onDeleteInitiative(initiative.id)}
/>
))}
</div>
);
}