Initial commit
This commit is contained in:
@@ -0,0 +1,149 @@
|
||||
"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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user