1
0
Fork 0
polaris-task-force/src/components/frontend/logistics/ResourceCard.tsx
Z8MB1E 3797cbcc03 feat: add logistics pages for resources and vehicles
- Add resources list page with resource cards and detail pages
- Add vehicles list page with vehicle cards, filters, and detail pages
- Add ResourceCard, VehicleCard, and VehicleFilters components
- Widen structure detail page max-width from 4xl to 7xl
2026-07-31 20:02:25 -04:00

178 lines
5.7 KiB
TypeScript

"use client";
import Link from "next/link";
import {
Item,
ItemContent,
ItemDescription,
ItemHeader,
ItemMedia,
ItemTitle,
} from "@/components/ui/item";
import { convertLexicalToPlaintext } from "@payloadcms/richtext-lexical/plaintext";
import {
BrainIcon,
ChevronRightIcon,
DropletIcon,
HandshakeIcon,
PackageOpenIcon,
ScaleIcon,
ZapIcon,
} from "lucide-react";
interface ResourceCardProps {
resource: {
id: number;
name: string;
codeName: string;
type: "physical" | "fluid" | "energy" | "abstract";
rarity: string;
baseValue: number;
unitOfMeasure: string;
massPerUnit: number;
icon?: { url: string } | number | null;
color?: string | null;
tradeable?: boolean | null;
description?: {
root: {
type: string;
children: {
type: any;
version: number;
[k: string]: unknown;
}[];
direction: ("ltr" | "rtl") | null;
format: "left" | "start" | "center" | "right" | "end" | "justify" | "";
indent: number;
version: number;
};
[k: string]: unknown;
} | null;
};
}
const typeColors: Record<string, string> = {
physical: "bg-stone-500/20 text-stone-400",
fluid: "bg-blue-500/20 text-blue-400",
energy: "bg-amber-500/20 text-amber-400",
abstract: "bg-purple-500/20 text-purple-400",
};
const typeIcons: Record<string, React.ReactNode> = {
physical: <PackageOpenIcon className="size-4" />,
fluid: <DropletIcon className="size-4" />,
energy: <ZapIcon className="size-4" />,
abstract: <BrainIcon className="size-4" />,
};
const rarityColors: Record<string, string> = {
common: "bg-muted text-muted-foreground",
uncommon: "bg-green-500/20 text-green-400",
rare: "bg-blue-500/20 text-blue-400",
epic: "bg-purple-500/20 text-purple-400",
legendary: "bg-amber-500/20 text-amber-400",
exotic: "bg-pink-500/20 text-pink-400",
unique: "bg-red-500/20 text-red-400",
};
const rarityLabels: Record<string, string> = {
common: "Common",
uncommon: "Uncommon",
rare: "Rare",
epic: "Epic",
legendary: "Legendary",
exotic: "Exotic",
unique: "Unique",
};
export function ResourceCard({ resource }: ResourceCardProps) {
const icon =
resource.icon && typeof resource.icon === "object" ? resource.icon : null;
const description = resource.description
? convertLexicalToPlaintext({ data: resource.description })
: null;
const truncatedDescription =
description && description.length > 150
? description.slice(0, 150) + "…"
: description;
return (
<Link href={`/logistics/resources/${resource.id}`}>
<Item
variant="outline"
className="cursor-pointer group transition-all duration-200 hover:scale-[1.01] hover:[box-shadow:inset_0_2px_8px_rgba(255,255,255,0.33)]"
>
<ItemHeader>
<div className="flex items-center gap-2">
{icon ? (
<ItemMedia variant="image">
<img
src={icon.url}
alt={resource.name}
className="size-full object-cover"
/>
</ItemMedia>
) : resource.color ? (
<ItemMedia variant="icon">
<span
className="size-4 rounded-full shrink-0"
style={{ backgroundColor: resource.color }}
/>
</ItemMedia>
) : (
<ItemMedia variant="icon">{typeIcons[resource.type]}</ItemMedia>
)}
<ItemTitle className="flex flex-col">
<span className="font-mono text-xs text-muted-foreground">
{resource.codeName}
</span>
<span>{resource.name}</span>
</ItemTitle>
</div>
<div className="flex items-center gap-2">
<span
className={`text-[10px] font-semibold uppercase tracking-wider px-1.5 py-0.5 rounded ${typeColors[resource.type] ?? "bg-muted text-muted-foreground"}`}
>
{resource.type}
</span>
<span
className={`text-[10px] font-semibold uppercase tracking-wider px-1.5 py-0.5 rounded ${rarityColors[resource.rarity] ?? "bg-muted text-muted-foreground"}`}
>
{rarityLabels[resource.rarity] ?? resource.rarity}
</span>
</div>
</ItemHeader>
<ItemContent>
<div className="flex flex-col gap-3 w-full">
{truncatedDescription && (
<p className="text-sm text-muted-foreground line-clamp-2">
{truncatedDescription}
</p>
)}
<div className="flex flex-wrap items-center gap-x-4 gap-y-1.5 text-xs text-muted-foreground">
<span className="flex items-center gap-1 font-medium text-foreground">
{resource.baseValue.toLocaleString()} credits
</span>
<span className="flex items-center gap-1">
<ScaleIcon className="size-3" />
{resource.massPerUnit.toLocaleString()} kg/{resource.unitOfMeasure}
</span>
{resource.tradeable && (
<span className="flex items-center gap-1">
<HandshakeIcon className="size-3" />
Tradeable
</span>
)}
</div>
</div>
</ItemContent>
<ItemDescription>
{truncatedDescription ?? `${resource.type} resource — ${resource.unitOfMeasure}`}
</ItemDescription>
<span className="text-xs text-muted-foreground group-hover:text-primary transition-colors flex items-center gap-1">
View details <ChevronRightIcon className="size-3" />
</span>
</Item>
</Link>
);
}