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:
@@ -1,5 +1,4 @@
|
|||||||
import { MoreHorizontal, Eye, Bot } from "lucide-react";
|
import { MoreHorizontal, Eye, Bot } from "lucide-react";
|
||||||
import type { Initiative } from "@codewalk-district/shared";
|
|
||||||
import { Card } from "@/components/ui/card";
|
import { Card } from "@/components/ui/card";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import {
|
import {
|
||||||
@@ -13,8 +12,18 @@ import { StatusBadge } from "@/components/StatusBadge";
|
|||||||
import { ProgressBar } from "@/components/ProgressBar";
|
import { ProgressBar } from "@/components/ProgressBar";
|
||||||
import { trpc } from "@/lib/trpc";
|
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 {
|
interface InitiativeCardProps {
|
||||||
initiative: Initiative;
|
initiative: SerializedInitiative;
|
||||||
onView: () => void;
|
onView: () => void;
|
||||||
onSpawnArchitect: (mode: "discuss" | "breakdown") => void;
|
onSpawnArchitect: (mode: "discuss" | "breakdown") => void;
|
||||||
onDelete: () => void;
|
onDelete: () => void;
|
||||||
|
|||||||
90
packages/web/src/components/InitiativeList.tsx
Normal file
90
packages/web/src/components/InitiativeList.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user