150 lines
5.2 KiB
TypeScript
150 lines
5.2 KiB
TypeScript
"use client";
|
|
|
|
import Link from "next/link";
|
|
import { Badge } from "@/components/ui/badge";
|
|
import { Card, CardContent, CardFooter, CardHeader } from "@/components/ui/card";
|
|
import { Button } from "@/components/ui/button";
|
|
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,
|
|
Users,
|
|
ArrowUpRight,
|
|
} from "lucide-react";
|
|
|
|
interface ProjectCardProps {
|
|
project: {
|
|
id: string;
|
|
name: string;
|
|
code: string;
|
|
status: string;
|
|
description?: string | null;
|
|
deadline?: Date | 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;
|
|
};
|
|
};
|
|
}
|
|
|
|
const STATUS_STYLES: Record<string, string> = {
|
|
ACTIVE: "bg-green-900/60 text-green-300",
|
|
ON_HOLD: "bg-yellow-900/60 text-yellow-300",
|
|
COMPLETED: "bg-blue-900/60 text-blue-300",
|
|
ARCHIVED: "bg-zinc-800 text-zinc-500",
|
|
};
|
|
|
|
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;
|
|
|
|
return (
|
|
<Card className="group hover:border-zinc-700 transition-all flex flex-col">
|
|
<CardHeader className="pb-3">
|
|
<div className="flex items-start justify-between gap-2">
|
|
<div>
|
|
<div className="flex items-center gap-2 mb-0.5">
|
|
<span className="font-mono text-xs text-zinc-500">{project.code}</span>
|
|
{project.client && (
|
|
<span className="text-xs text-zinc-600">• {project.client.company}</span>
|
|
)}
|
|
</div>
|
|
<h3 className="font-semibold text-base leading-tight text-white">{project.name}</h3>
|
|
</div>
|
|
<span
|
|
className={cn(
|
|
"text-xs px-2 py-0.5 rounded-full border-0 font-medium shrink-0",
|
|
STATUS_STYLES[project.status] ?? STATUS_STYLES.ACTIVE
|
|
)}
|
|
>
|
|
{project.status.replace("_", " ")}
|
|
</span>
|
|
</div>
|
|
|
|
{project.description && (
|
|
<p className="text-sm text-zinc-400 line-clamp-2 mt-1">
|
|
{project.description}
|
|
</p>
|
|
)}
|
|
</CardHeader>
|
|
|
|
<CardContent className="flex-1 pb-3 space-y-3">
|
|
{/* Progress */}
|
|
{total > 0 && (
|
|
<div className="space-y-1.5">
|
|
<div className="flex items-center justify-between text-xs text-muted-foreground">
|
|
<span>Shot progress</span>
|
|
<span className="text-foreground font-medium">{approved}/{total} approved</span>
|
|
</div>
|
|
<Progress value={progress} className="h-1.5" />
|
|
</div>
|
|
)}
|
|
|
|
{/* Stats */}
|
|
<div className="grid grid-cols-3 gap-2 text-xs">
|
|
<div className="flex flex-col items-center rounded-lg bg-zinc-800 py-2">
|
|
<Layers className="h-3.5 w-3.5 text-zinc-400 mb-1" />
|
|
<span className="font-semibold text-white">{total}</span>
|
|
<span className="text-zinc-400">shots</span>
|
|
</div>
|
|
<div className="flex flex-col items-center rounded-lg bg-zinc-800 py-2">
|
|
<CheckCircle2 className="h-3.5 w-3.5 text-green-400 mb-1" />
|
|
<span className="font-semibold text-white">{approved}</span>
|
|
<span className="text-zinc-400">done</span>
|
|
</div>
|
|
<div className="flex flex-col items-center rounded-lg bg-zinc-800 py-2">
|
|
<Film className="h-3.5 w-3.5 text-blue-400 mb-1" />
|
|
<span className="font-semibold text-white">
|
|
{project.shotStats?.inProgress ?? 0}
|
|
</span>
|
|
<span className="text-zinc-400">active</span>
|
|
</div>
|
|
</div>
|
|
</CardContent>
|
|
|
|
<CardFooter className="pt-2 border-t border-zinc-800 flex items-center justify-between">
|
|
<div className="flex items-center gap-2 text-xs text-zinc-400">
|
|
{project.producer ? (
|
|
<>
|
|
<Avatar className="h-5 w-5">
|
|
{project.producer.image && <AvatarImage src={project.producer.image} />}
|
|
<AvatarFallback className="text-[9px]">
|
|
{getInitials(project.producer.name)}
|
|
</AvatarFallback>
|
|
</Avatar>
|
|
<span>{project.producer.name ?? "Producer"}</span>
|
|
</>
|
|
) : (
|
|
<>
|
|
<Clock className="h-3 w-3" />
|
|
<span>{formatRelativeDate(project.createdAt)}</span>
|
|
</>
|
|
)}
|
|
{project.deadline && (
|
|
<span className="text-amber-400 ml-2">
|
|
Due {formatRelativeDate(project.deadline)}
|
|
</span>
|
|
)}
|
|
</div>
|
|
|
|
<Button variant="ghost" size="sm" asChild className="h-7 text-xs gap-1">
|
|
<Link href={`/projects/${project.id}`}>
|
|
Open
|
|
<ArrowUpRight className="h-3 w-3" />
|
|
</Link>
|
|
</Button>
|
|
</CardFooter>
|
|
</Card>
|
|
);
|
|
}
|