feat(17-02): create InitiativeCard component
- Renders initiative name, StatusBadge, ProgressBar, and phase count - Fetches phase stats per-card via trpc.listPhases (self-contained) - Spawn Architect dropdown with discuss/breakdown modes - More actions menu with edit/duplicate/archive/delete - Responsive: stacks vertically on mobile, hides phase count text - Card clickable with stopPropagation on action buttons
This commit is contained in:
118
packages/web/src/components/InitiativeCard.tsx
Normal file
118
packages/web/src/components/InitiativeCard.tsx
Normal file
@@ -0,0 +1,118 @@
|
|||||||
|
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 {
|
||||||
|
DropdownMenu,
|
||||||
|
DropdownMenuContent,
|
||||||
|
DropdownMenuItem,
|
||||||
|
DropdownMenuTrigger,
|
||||||
|
DropdownMenuSeparator,
|
||||||
|
} from "@/components/ui/dropdown-menu";
|
||||||
|
import { StatusBadge } from "@/components/StatusBadge";
|
||||||
|
import { ProgressBar } from "@/components/ProgressBar";
|
||||||
|
import { trpc } from "@/lib/trpc";
|
||||||
|
|
||||||
|
interface InitiativeCardProps {
|
||||||
|
initiative: Initiative;
|
||||||
|
onView: () => void;
|
||||||
|
onSpawnArchitect: (mode: "discuss" | "breakdown") => void;
|
||||||
|
onDelete: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function InitiativeCard({
|
||||||
|
initiative,
|
||||||
|
onView,
|
||||||
|
onSpawnArchitect,
|
||||||
|
onDelete,
|
||||||
|
}: InitiativeCardProps) {
|
||||||
|
// Each card fetches its own phase stats (N+1 acceptable for v1 small counts)
|
||||||
|
const phasesQuery = trpc.listPhases.useQuery({
|
||||||
|
initiativeId: initiative.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
const phases = phasesQuery.data ?? [];
|
||||||
|
const completedCount = phases.filter((p) => p.status === "completed").length;
|
||||||
|
const totalCount = phases.length;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card
|
||||||
|
className="cursor-pointer p-4 transition-colors hover:bg-accent/50"
|
||||||
|
onClick={onView}
|
||||||
|
>
|
||||||
|
<div className="flex flex-col gap-3 md:flex-row md:items-center md:justify-between">
|
||||||
|
{/* Left: Initiative name */}
|
||||||
|
<div className="min-w-0 flex-shrink-0">
|
||||||
|
<span className="text-base font-bold">{initiative.name}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Middle: Status + Progress + Phase count */}
|
||||||
|
<div className="flex flex-1 items-center gap-4">
|
||||||
|
<StatusBadge status={initiative.status} />
|
||||||
|
<ProgressBar
|
||||||
|
completed={completedCount}
|
||||||
|
total={totalCount}
|
||||||
|
className="w-32"
|
||||||
|
/>
|
||||||
|
<span className="hidden text-sm text-muted-foreground md:inline">
|
||||||
|
{completedCount}/{totalCount} phases
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Right: Action buttons */}
|
||||||
|
<div
|
||||||
|
className="flex items-center gap-2"
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
>
|
||||||
|
<Button variant="outline" size="sm" onClick={onView}>
|
||||||
|
<Eye className="mr-1 h-4 w-4" />
|
||||||
|
View
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
{/* Spawn Architect Dropdown */}
|
||||||
|
<DropdownMenu>
|
||||||
|
<DropdownMenuTrigger asChild>
|
||||||
|
<Button variant="outline" size="sm">
|
||||||
|
<Bot className="mr-1 h-4 w-4" />
|
||||||
|
Spawn Architect
|
||||||
|
</Button>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent align="end">
|
||||||
|
<DropdownMenuItem
|
||||||
|
onClick={() => onSpawnArchitect("discuss")}
|
||||||
|
>
|
||||||
|
Discuss
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem
|
||||||
|
onClick={() => onSpawnArchitect("breakdown")}
|
||||||
|
>
|
||||||
|
Breakdown
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
|
||||||
|
{/* More Actions Menu */}
|
||||||
|
<DropdownMenu>
|
||||||
|
<DropdownMenuTrigger asChild>
|
||||||
|
<Button variant="ghost" size="icon">
|
||||||
|
<MoreHorizontal className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent align="end">
|
||||||
|
<DropdownMenuItem onClick={onView}>Edit</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem>Duplicate</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem>Archive</DropdownMenuItem>
|
||||||
|
<DropdownMenuSeparator />
|
||||||
|
<DropdownMenuItem
|
||||||
|
className="text-destructive"
|
||||||
|
onClick={onDelete}
|
||||||
|
>
|
||||||
|
Delete
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user