"use client"; import Link from "next/link"; import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; import { Progress } from "@/components/ui/progress"; import { cn, getInitials, formatRelativeDate } from "@/lib/utils"; import { Film, Layers, CheckCircle2, Clock, ArrowUpRight } from "lucide-react"; interface ProjectCardProps { project: { id: string; name: string; code: string; status: string; description?: string | null; /** The column is `dueDate`; `deadline` is kept for older callers. */ dueDate?: Date | string | null; deadline?: Date | string | null; createdAt: Date; client?: { company: string } | null; producer?: { name: string | null; image: string | null } | null; _count?: { shots: number }; shotStats?: { total: number; approved: number; inProgress: number; }; /** Still chosen from the project's own shots (lib/project-poster.ts). */ poster?: { url: string; shotCode: string; isKeyShot: boolean } | null; }; } const STATUS_STYLES: Record = { ACTIVE: "bg-green-900/70 text-green-300", ON_HOLD: "bg-yellow-900/70 text-yellow-300", COMPLETED: "bg-blue-900/70 text-blue-300", ARCHIVED: "bg-zinc-900/80 text-zinc-400", }; /** * A project card is scanned, not read: in a grid of twenty the frame is what * the eye picks out, so the still leads and the title sits on it. Every card is * the same height whatever it holds — a ragged grid reads as broken, and the * description is the only part that varies, so it is given fixed room rather * than allowed to push the rest around. */ export function ProjectCard({ project }: ProjectCardProps) { const total = project.shotStats?.total ?? project._count?.shots ?? 0; const approved = project.shotStats?.approved ?? 0; const progress = total > 0 ? Math.round((approved / total) * 100) : 0; const due = project.dueDate ?? project.deadline; const isArchived = project.status === "ARCHIVED"; return ( {/* Poster */}
{project.poster ? ( ) : ( // Same footprint as a real poster, so a project without stills does // not leave a hole in the grid.
)} {/* Scrim: the title has to hold on a bright frame as well as a dark one. */}
{project.status.replace("_", " ")}
{project.code} {project.client && ( • {project.client.company} )}

{project.name}

{/* Body — fixed room so every card lands the same height */}

{project.description || No description}

Shot progress {total > 0 ? `${approved}/${total} approved` : "No shots yet"}
{total} shots
{approved} done
{project.shotStats?.inProgress ?? 0} active
{/* Pinned to the bottom so the footers line up across the row. */}
{project.producer ? ( <> {project.producer.image && } {getInitials(project.producer.name)} {project.producer.name ?? "Producer"} ) : ( <> {formatRelativeDate(project.createdAt)} )}
{due ? ( Due {formatRelativeDate(due as Date)} ) : ( )}
); }