194 lines
6.4 KiB
TypeScript
194 lines
6.4 KiB
TypeScript
"use client";
|
||
|
||
import { calculateDurationInDays } from "@/app/utils/function/projectDuration";
|
||
import {
|
||
Card,
|
||
CardHeader,
|
||
CardTitle,
|
||
CardContent,
|
||
CardDescription,
|
||
} from "ikoncomponents";
|
||
import { Badge } from "ikoncomponents";
|
||
import { Button } from "ikoncomponents";
|
||
import { CalendarDays, Clock, Building2, UserRound, ArrowRight, Loader2 } from "lucide-react";
|
||
import { useRouter } from "next/navigation";
|
||
import { useState } from "react";
|
||
|
||
export type ProjectCardProps = {
|
||
projectIdentifier: string;
|
||
projectName: string;
|
||
projectDescription: string;
|
||
projectStatus: string;
|
||
projectClient:string;
|
||
productType: string;
|
||
contractedStartDate: string ;
|
||
contractedEndDate: string ;
|
||
projectManager:string;
|
||
};
|
||
|
||
// A small palette of accent themes. Only the icons, left border and the solid
|
||
// button are colored — the panels stay neutral so text is always readable.
|
||
// Each entry carries explicit light + dark classes and uses 600-weight fills
|
||
// with white text so contrast holds in both themes. Classes are static strings
|
||
// (not interpolated) so Tailwind keeps them in the build.
|
||
const ACCENTS = [
|
||
{
|
||
border: "border-l-blue-500",
|
||
icon: "text-blue-600 dark:text-blue-400",
|
||
button:
|
||
"bg-blue-600 text-white hover:bg-blue-700 dark:bg-blue-600 dark:hover:bg-blue-500",
|
||
},
|
||
{
|
||
border: "border-l-emerald-500",
|
||
icon: "text-emerald-600 dark:text-emerald-400",
|
||
button:
|
||
"bg-emerald-600 text-white hover:bg-emerald-700 dark:bg-emerald-600 dark:hover:bg-emerald-500",
|
||
},
|
||
{
|
||
border: "border-l-violet-500",
|
||
icon: "text-violet-600 dark:text-violet-400",
|
||
button:
|
||
"bg-violet-600 text-white hover:bg-violet-700 dark:bg-violet-600 dark:hover:bg-violet-500",
|
||
},
|
||
{
|
||
border: "border-l-amber-500",
|
||
icon: "text-amber-600 dark:text-amber-400",
|
||
button:
|
||
"bg-amber-600 text-white hover:bg-amber-700 dark:bg-amber-600 dark:hover:bg-amber-500",
|
||
},
|
||
{
|
||
border: "border-l-rose-500",
|
||
icon: "text-rose-600 dark:text-rose-400",
|
||
button:
|
||
"bg-rose-600 text-white hover:bg-rose-700 dark:bg-rose-600 dark:hover:bg-rose-500",
|
||
},
|
||
{
|
||
border: "border-l-cyan-500",
|
||
icon: "text-cyan-600 dark:text-cyan-400",
|
||
button:
|
||
"bg-cyan-600 text-white hover:bg-cyan-700 dark:bg-cyan-600 dark:hover:bg-cyan-500",
|
||
},
|
||
] as const;
|
||
|
||
export default function ProjectCard({
|
||
projectIdentifier,
|
||
projectName,
|
||
projectDescription,
|
||
projectStatus,
|
||
projectClient,
|
||
productType,
|
||
projectManager,
|
||
contractedStartDate,
|
||
contractedEndDate,
|
||
}: ProjectCardProps) {
|
||
|
||
const router = useRouter();
|
||
|
||
// Shows a spinner on the button from the moment it's clicked until this card
|
||
// unmounts (i.e. the project details route has taken over). The details page
|
||
// then renders its own spinner while it finishes loading.
|
||
const [isNavigating, setIsNavigating] = useState(false);
|
||
|
||
const duration = calculateDurationInDays(contractedStartDate,contractedEndDate);
|
||
|
||
const handleViewDetails = () => {
|
||
setIsNavigating(true);
|
||
router.push(`/main/planning/projects/${projectIdentifier}`);
|
||
};
|
||
|
||
// Pick a stable accent per project so the same card is always the same color.
|
||
const accent = (() => {
|
||
const key = projectIdentifier || projectName || "";
|
||
let hash = 0;
|
||
for (let i = 0; i < key.length; i++) {
|
||
hash = (hash * 31 + key.charCodeAt(i)) >>> 0;
|
||
}
|
||
return ACCENTS[hash % ACCENTS.length];
|
||
})();
|
||
|
||
// Map a project status to a badge style so the state reads at a glance.
|
||
const statusVariant = (() => {
|
||
switch (projectStatus?.toLowerCase()) {
|
||
case "completed":
|
||
return "verified" as const;
|
||
case "active":
|
||
case "ongoing":
|
||
return "default" as const;
|
||
default:
|
||
return "secondary" as const;
|
||
}
|
||
})();
|
||
|
||
return (
|
||
<Card
|
||
className={`group flex flex-col rounded-2xl border border-l-4 ${accent.border} shadow-sm transition-all hover:-translate-y-0.5 hover:shadow-lg`}
|
||
>
|
||
|
||
<CardHeader className="flex flex-row items-start justify-between gap-3 pb-3">
|
||
<div className="min-w-0">
|
||
<CardTitle className="truncate text-lg font-semibold leading-tight">
|
||
{projectName}
|
||
</CardTitle>
|
||
</div>
|
||
|
||
<Badge variant={statusVariant} className="shrink-0 text-xs px-3 py-1">
|
||
{projectStatus || "Unknown"}
|
||
</Badge>
|
||
</CardHeader>
|
||
|
||
<CardContent className="flex flex-1 flex-col space-y-4">
|
||
|
||
{/* Description */}
|
||
<CardDescription className="line-clamp-2 min-h-10 text-sm">
|
||
{projectDescription || "No description provided."}
|
||
</CardDescription>
|
||
|
||
{/* Meta — neutral panel, colored icons, full-width rows so values stay readable */}
|
||
<div className="space-y-2.5 rounded-xl border bg-muted/40 p-3 text-sm">
|
||
<div className="flex items-center justify-between gap-3">
|
||
<div className="flex min-w-0 items-center gap-2 text-muted-foreground">
|
||
<CalendarDays className={`h-4 w-4 shrink-0 ${accent.icon}`} />
|
||
<span className="truncate text-foreground">
|
||
{contractedStartDate || "to"} – {contractedEndDate || "—"}
|
||
</span>
|
||
</div>
|
||
<div className="flex shrink-0 items-center gap-1.5 text-muted-foreground">
|
||
<Clock className={`h-4 w-4 shrink-0 ${accent.icon}`} />
|
||
<span className="text-foreground">{duration}d</span>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="flex items-center gap-2 text-muted-foreground">
|
||
<Building2 className={`h-4 w-4 shrink-0 ${accent.icon}`} />
|
||
<span className="truncate text-foreground">{projectClient || "—"}</span>
|
||
</div>
|
||
|
||
<div className="flex items-center gap-2 text-muted-foreground">
|
||
<UserRound className={`h-4 w-4 shrink-0 ${accent.icon}`} />
|
||
<span className="truncate text-foreground">{projectManager || "—"}</span>
|
||
</div>
|
||
</div>
|
||
|
||
<Button
|
||
onClick={handleViewDetails}
|
||
disabled={isNavigating}
|
||
className={`mt-auto flex w-full items-center justify-center gap-2 border-0 ${accent.button}`}
|
||
>
|
||
{isNavigating ? (
|
||
<>
|
||
Loading
|
||
<Loader2 className="h-4 w-4 animate-spin" />
|
||
</>
|
||
) : (
|
||
<>
|
||
View Details
|
||
<ArrowRight className="h-4 w-4 transition-transform group-hover:translate-x-0.5" />
|
||
</>
|
||
)}
|
||
</Button>
|
||
|
||
</CardContent>
|
||
</Card>
|
||
);
|
||
}
|