Files
Codewalkers/apps/web/src/components/InitiativeList.tsx
Lukas May 069eb66acb feat: Add project filter to listInitiatives
Add findByProjectId to InitiativeRepository using a subquery on the
initiative_projects junction table. Extend the listInitiatives tRPC
procedure to accept an optional projectId filter that composes with
the existing status filter. Add a project dropdown to the initiatives
page alongside the status filter.
2026-03-04 11:40:32 +01:00

116 lines
3.3 KiB
TypeScript

import { AlertCircle, Plus } from "lucide-react";
import { motion } from "motion/react";
import { Button } from "@/components/ui/button";
import { Card } from "@/components/ui/card";
import { Skeleton } from "@/components/Skeleton";
import { InitiativeCard } from "@/components/InitiativeCard";
import { trpc } from "@/lib/trpc";
interface InitiativeListProps {
statusFilter?: "all" | "active" | "completed" | "archived";
projectFilter?: string;
onCreateNew: () => void;
onViewInitiative: (id: string) => void;
}
export function InitiativeList({
statusFilter = "all",
projectFilter,
onCreateNew,
onViewInitiative,
}: InitiativeListProps) {
const queryInput = (() => {
const hasStatus = statusFilter !== "all";
if (!hasStatus && !projectFilter) return undefined;
return {
...(hasStatus && { status: statusFilter as "active" | "completed" | "archived" }),
...(projectFilter && { projectId: projectFilter }),
};
})();
const initiativesQuery = trpc.listInitiatives.useQuery(queryInput);
// Loading state
if (initiativesQuery.isLoading) {
return (
<div className="space-y-3">
{Array.from({ length: 3 }).map((_, i) => (
<Card key={i} className="p-4">
<div className="flex items-center justify-between">
<Skeleton className="h-5 w-48" />
<Skeleton className="h-7 w-7 rounded" />
</div>
<div className="mt-1.5 flex items-center gap-3">
<Skeleton className="h-2 w-2 rounded-full" />
<Skeleton className="h-4 w-20" />
<Skeleton className="ml-auto h-2 w-24" />
</div>
</Card>
))}
</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, i) => (
<motion.div
key={initiative.id}
initial={{ opacity: 0, y: 12 }}
animate={{ opacity: 1, y: 0 }}
transition={{
duration: 0.3,
delay: Math.min(i * 0.05, 0.3),
ease: [0, 0, 0.2, 1],
}}
>
<InitiativeCard
initiative={initiative}
onClick={() => onViewInitiative(initiative.id)}
/>
</motion.div>
))}
</div>
);
}