Files
Codewalkers/packages/web/src/components/InitiativeList.tsx
Lukas May 895c96435c 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
2026-02-04 21:04:43 +01:00

91 lines
2.5 KiB
TypeScript

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>
);
}