1
0
Fork 0
polaris-task-force/src/components/frontend/intelligence/CampaignCard.tsx

134 lines
5.8 KiB
TypeScript

"use client";
import Link from "next/link";
import type { Campaign } from "@/payload-types";
import { cn } from "@/lib/utils";
import { ClockIcon, CrosshairIcon, UsersIcon } from "lucide-react";
interface CampaignCardCampaign extends Pick<
Campaign,
"id" | "name" | "summary" | "status" | "startDate" | "endDate"
> {
readonly coverImageUrl: string | null;
readonly missionCount: number;
readonly avgPlayers: number;
readonly avgDurationMinutes: number;
}
type CampaignCardInput = CampaignCardCampaign | Campaign;
interface CampaignCardProps {
readonly campaign: CampaignCardInput;
}
const statusColors: Record<Campaign["status"], string> = {
concept: "bg-muted text-muted-foreground",
inProgress: "bg-blue-500/20 text-blue-400",
completed: "bg-emerald-500/20 text-emerald-400",
};
const statusLabels: Record<Campaign["status"], string> = {
concept: "Concept",
inProgress: "In Progress",
completed: "Completed",
};
export function CampaignCard({ campaign }: CampaignCardProps) {
const coverImageUrl =
"coverImageUrl" in campaign
? campaign.coverImageUrl
: typeof campaign.coverImage === "object" && campaign.coverImage !== null
? (campaign.coverImage.url ?? null)
: null;
const missionCount = "missionCount" in campaign ? campaign.missionCount : 0;
const avgPlayers = "avgPlayers" in campaign ? campaign.avgPlayers : 0;
const avgDurationMinutes = "avgDurationMinutes" in campaign ? campaign.avgDurationMinutes : 0;
const roundedDuration = Math.round(avgDurationMinutes);
const durationHours = Math.floor(roundedDuration / 60);
const durationMinutes = roundedDuration % 60;
const durationLabel =
roundedDuration >= 60 ? `${durationHours}h ${durationMinutes}m` : `${roundedDuration}m`;
const playersLabel = Number.isInteger(avgPlayers) ? avgPlayers.toString() : avgPlayers.toFixed(1);
const owner = (campaign as any).owner;
const ownerName =
owner && typeof owner === "object" && "displayName" in owner
? owner.displayName
: "Unknown";
return (
<Link
href={`/intelligence/campaigns/${campaign.id}`}
aria-label={`Open campaign ${campaign.name}`}
className="block rounded-lg focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background"
>
<article className="group relative aspect-[4/3] overflow-hidden rounded-lg border border-border/70 bg-card transition-colors duration-300 group-hover:border-foreground/30">
<div className="absolute inset-0 scale-100 transition-transform duration-500 ease-out group-hover:scale-110 motion-reduce:transition-none motion-reduce:group-hover:scale-100">
{coverImageUrl !== null ? (
<img
src={coverImageUrl}
alt=""
aria-hidden="true"
className="absolute inset-0 size-full object-cover opacity-30 transition-opacity duration-500 ease-out group-hover:opacity-50 motion-reduce:transition-none"
/>
) : (
<div
aria-hidden="true"
className="absolute inset-0 bg-gradient-to-br from-muted to-muted/50 opacity-30 transition-opacity duration-500 ease-out group-hover:opacity-50 motion-reduce:transition-none"
/>
)}
</div>
<div
aria-hidden="true"
className="absolute inset-0 bg-gradient-to-t from-black/80 via-black/40 to-transparent"
/>
<span
className={cn(
"absolute right-3 top-3 z-10 rounded-full px-2 py-1 text-[10px] font-semibold uppercase tracking-wider",
statusColors[campaign.status],
)}
>
{statusLabels[campaign.status]}
</span>
<div className="absolute inset-0 z-10 flex items-center justify-center px-6 pb-6 text-center">
<div className="flex max-w-full flex-col items-center gap-2">
<h2 className="text-balance text-center text-xl font-bold text-white drop-shadow-lg">
{campaign.name}
</h2>
<p className="text-sm uppercase tracking-wider text-white/70 drop-shadow">
{statusLabels[campaign.status]}
</p>
</div>
</div>
<footer className="absolute inset-x-0 bottom-0 z-20 flex translate-y-4 items-center bg-black/60 px-2 py-2.5 opacity-0 backdrop-blur-sm transition-all duration-300 ease-out group-hover:translate-y-0 group-hover:opacity-100 motion-reduce:transition-none">
<div className="flex min-w-0 flex-1 items-center justify-center gap-1.5 border-r border-white/15 text-xs text-white/80">
<CrosshairIcon aria-hidden="true" className="size-3.5 shrink-0 text-white/60" />
<span className="sr-only">Missions</span>
<span className="truncate">{missionCount}</span>
</div>
<div className="flex min-w-0 flex-1 items-center justify-center gap-1.5 border-r border-white/15 text-xs text-white/80">
<UsersIcon aria-hidden="true" className="size-3.5 shrink-0 text-white/60" />
<span className="sr-only">Avg players</span>
<span className="truncate">{playersLabel}</span>
</div>
<div className="flex min-w-0 flex-1 items-center justify-center gap-1.5 text-xs text-white/80">
<ClockIcon aria-hidden="true" className="size-3.5 shrink-0 text-white/60" />
<span className="sr-only">Avg duration</span>
<span className="truncate">{durationLabel}</span>
</div>
<div className="flex min-w-0 flex-1 items-center justify-center gap-1.5 text-xs text-white/80">
<div className="text-right">
<div className="truncate">{ownerName}</div>
<span className="text-[10px] uppercase tracking-wider text-white/60">Owner</span>
</div>
</div>
</footer>
</article>
</Link>
);
}