1
0
Fork 0
polaris-task-force/src/components/frontend/locker/LoadoutCard.tsx
Z8MB1E 6e717684fb feat(locker): add equipment editor and wardrobe tab
- Add EquipmentEditorDialog for per-category attachments, skins, and custom names
- Add Wardrobe component with soldier image and clickable equipment slots
- Add WardrobePanel with loadout selector and current equipment display
- Add WardrobeSlotPicker dialog for equipping items from locker
- Add equip/unequip loadout functionality with equipped badge
- Add equipment schema to LockerStorages (attachments, skin, customName)
- Add wardrobe slot layout with hotspot positions for each equipment type
- Add locker:item-attach, locker:item-detach, locker:item-skin event types
- Update LoadoutCard to show equipped status and equipment list
- Update LoadoutsPanel to pass ownedItems for equipment display
2026-08-02 18:49:59 -04:00

91 lines
3.3 KiB
TypeScript

"use client";
import { CheckCircle2Icon, PencilIcon, TrashIcon } from "lucide-react";
import type { Asset, Loadout } from "@/payload-types";
import { Button } from "@/components/ui/button";
import { Item, ItemContent, ItemHeader, ItemTitle } from "@/components/ui/item";
import { getAssetTypeLabel, LOADOUT_SLOTS, type LockerGridItem, type LoadoutSlots } from "@/lib/locker";
function resolveSlotValue(value: LoadoutSlots[keyof LoadoutSlots]): Asset | null {
if (value == null) return null;
if (typeof value === "object" && "id" in value) return value as Asset;
return null;
}
function slotDisplayName(asset: Asset, ownedItems: LockerGridItem[]): string {
const custom = ownedItems.find((i) => i.assetId === asset.id)?.equipment?.customName?.trim();
return custom || asset.name;
}
interface LoadoutCardProps {
loadout: Loadout;
ownedItems: LockerGridItem[];
onEdit: () => void;
onDelete: () => void;
onEquip: () => void;
}
export function LoadoutCard({ loadout, ownedItems, onEdit, onDelete, onEquip }: LoadoutCardProps) {
const slots = (loadout.slots ?? {}) as LoadoutSlots;
const filled = LOADOUT_SLOTS.filter((slot) => {
const v = slots[slot.name];
return v != null && (typeof v !== "object" || (typeof v === "object" && "id" in v));
});
return (
<Item variant="outline">
<ItemHeader>
<ItemTitle className="flex items-center gap-2">
{loadout.name}
{loadout.equipped && (
<span className="inline-flex items-center gap-1 text-xs font-medium text-amber-400">
<CheckCircle2Icon className="size-3.5" />
Equipped
</span>
)}
</ItemTitle>
{loadout.description && (
<span className="text-xs text-muted-foreground">{loadout.description}</span>
)}
<div className="flex items-center gap-1">
<Button
variant={loadout.equipped ? "default" : "outline"}
size="sm"
onClick={onEquip}
>
{loadout.equipped ? "Unequip" : "Equip"}
</Button>
<Button variant="outline" size="sm" onClick={onEdit}>
<PencilIcon className="size-3.5" />
Edit
</Button>
<Button variant="outline" size="sm" onClick={onDelete}>
<TrashIcon className="size-3.5" />
</Button>
</div>
</ItemHeader>
<ItemContent>
<div className="flex flex-wrap gap-1.5">
{filled.length === 0 && (
<span className="text-xs text-muted-foreground">No equipment assigned.</span>
)}
{filled.map((slot) => {
const value = slots[slot.name];
const asset = resolveSlotValue(value);
if (!asset) return null;
return (
<span
key={slot.name}
className="inline-flex items-center gap-1.5 text-xs bg-muted px-2.5 py-1 rounded-md"
>
<span className="text-muted-foreground">{slot.label}:</span>
<span className="font-medium">{slotDisplayName(asset, ownedItems)}</span>
<span className="text-muted-foreground">({getAssetTypeLabel(asset.assetType)})</span>
</span>
);
})}
</div>
</ItemContent>
</Item>
);
}