first commit
This commit is contained in:
193
frontend/app/main/planning/projects/projectCard/projectCard.tsx
Normal file
193
frontend/app/main/planning/projects/projectCard/projectCard.tsx
Normal file
@@ -0,0 +1,193 @@
|
||||
"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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user