1
0
Fork 0
polaris-task-force/src/components/frontend/locker/WardrobePanel.tsx
Z8MB1E 947a5c1bbf feat(ui): searchable combobox across data-driven dropdowns
- new ui/combobox primitive (popover plus cmdk): search input, scrolling
  list, trigger-width popover, per-option disabled and hint lines
- converts every data-driven single-value select site-wide (blueprints,
  factions, accounts, assignments, NPCs, structures, locker items, seats,
  missions, ranks, resources, mod lists, event targets, sprints)
- long multi-select checkbox menus (event log types, kanban labels) gain
  search inputs following the resource picker pattern
- static short enum selects stay as plain selects
2026-09-17 16:28:17 -04:00

210 lines
7.5 KiB
TypeScript

"use client";
import { useState } from "react";
import { useRouter } from "next/navigation";
import type { Asset, Loadout } from "@/payload-types";
import { Button } from "@/components/ui/button";
import { Combobox } from "@/components/ui/combobox";
import { Item, ItemContent, ItemHeader, ItemTitle } from "@/components/ui/item";
import { Wardrobe } from "./Wardrobe";
import { WardrobeSlotPicker } from "./WardrobeSlotPicker";
import {
getAssetTypeLabel,
getLoadoutSlot,
getWardrobeSlot,
type LoadoutSlots,
type LockerGridItem,
WARDROBE_SLOT_LAYOUT,
} from "@/lib/locker";
import { equipLoadout, setLoadoutSlot } from "@/app/(frontend)/locker/actions";
import { getAssetIcon } from "./LockerGrid";
interface WardrobePanelProps {
userName: string;
loadouts: Loadout[];
equippedLoadout: Loadout | null;
items: LockerGridItem[];
}
function resolveSlotAsset(
value: LoadoutSlots[keyof LoadoutSlots],
items: LockerGridItem[],
): Asset | null {
if (value == null) return null;
if (typeof value === "object" && "id" in value) return value as Asset;
const id = value as number;
return items.find((i) => i.assetId === id)?.asset ?? null;
}
function resolveSlotDisplayName(asset: Asset, items: LockerGridItem[]): string {
const custom = items.find((i) => i.assetId === asset.id)?.equipment?.customName?.trim();
return custom || asset.name;
}
export function WardrobePanel({ userName, loadouts, equippedLoadout, items }: WardrobePanelProps) {
const router = useRouter();
const [pickerSlot, setPickerSlot] = useState<string | null>(null);
const [busy, setBusy] = useState(false);
const slots = (equippedLoadout?.slots ?? {}) as LoadoutSlots;
async function handleEquip(loadoutId: number) {
setBusy(true);
const res = await equipLoadout(loadoutId);
setBusy(false);
if (res.success) router.refresh();
}
async function handlePick(slotName: string, assetId: number | null) {
if (!equippedLoadout) return { success: false, error: "No loadout equipped." };
setBusy(true);
const res = await setLoadoutSlot(equippedLoadout.id, slotName, assetId);
setBusy(false);
if (res.success) {
setPickerSlot(null);
router.refresh();
}
return res;
}
const pickerLayout = pickerSlot ? getWardrobeSlot(pickerSlot) : null;
const pickerConfig = pickerSlot ? getLoadoutSlot(pickerSlot) : null;
const pickerValue = pickerSlot ? (slots[pickerSlot as keyof LoadoutSlots] ?? null) : null;
const filled = equippedLoadout
? WARDROBE_SLOT_LAYOUT.filter((layout) => slots[layout.name] != null)
: [];
return (
<div className="grid grid-cols-1 gap-6 lg:grid-cols-[minmax(0,40rem)_minmax(0,1fr)]">
<Item variant="outline" className="self-start">
<ItemHeader>
<ItemTitle>Character</ItemTitle>
</ItemHeader>
<ItemContent>
<Wardrobe
loadout={equippedLoadout ?? ({ name: "Empty", slots: {} } as unknown as Loadout)}
items={items}
onSlotClick={(name) => setPickerSlot(name)}
/>
<p className="mt-3 text-center text-sm text-muted-foreground">
{equippedLoadout
? "Select a slot block to equip gear from your locker."
: "Equip a loadout to start dressing your character."}
</p>
</ItemContent>
</Item>
<div className="flex flex-col gap-4">
<Item variant="outline">
<ItemHeader>
<ItemTitle>Equipped Loadout</ItemTitle>
</ItemHeader>
<ItemContent>
{loadouts.length === 0 ? (
<p className="text-sm text-muted-foreground">
You have no loadouts yet. Head to the Loadouts tab to create one, then come back
here to equip it.
</p>
) : (
<div className="flex flex-col gap-3">
<div className="flex items-center gap-2">
<Combobox
className="flex-1"
value={equippedLoadout ? String(equippedLoadout.id) : ""}
onValueChange={(v) => handleEquip(Number(v))}
disabled={busy}
placeholder={
equippedLoadout ? equippedLoadout.name : "Select a loadout to equip"
}
searchPlaceholder="Search loadouts..."
options={loadouts.map((l) => ({
value: String(l.id),
label: l.name,
}))}
/>
{equippedLoadout && (
<Button
variant="outline"
size="sm"
onClick={() => setPickerSlot("helmet")}
disabled={busy}
>
Equip Gear
</Button>
)}
</div>
{!equippedLoadout && (
<p className="text-sm text-muted-foreground">
Choose a loadout above. It becomes the active loadout that the wardrobe
reflects.
</p>
)}
</div>
)}
</ItemContent>
</Item>
<Item variant="outline">
<ItemHeader>
<ItemTitle>Current Equipment</ItemTitle>
</ItemHeader>
<ItemContent>
{filled.length === 0 ? (
<p className="text-sm text-muted-foreground">
Nothing equipped. Select a loadout above and click its equipment slots.
</p>
) : (
<div className="grid grid-cols-1 sm:grid-cols-2 gap-1.5">
{filled.map((layout) => {
const asset = resolveSlotAsset(slots[layout.name], items);
if (!asset) return null;
return (
<button
key={layout.name}
type="button"
onClick={() => setPickerSlot(layout.name)}
className="flex items-center gap-2 rounded-md border border-border px-3 py-2 text-left text-sm hover:bg-muted transition-colors"
>
<span className="text-amber-400">
{getAssetIcon(asset.assetType, "size-4")}
</span>
<span className="flex-1 min-w-0">
<span className="block text-xs text-muted-foreground">{layout.label}</span>
<span className="block font-medium truncate">
{resolveSlotDisplayName(asset, items)}
</span>
</span>
<span className="text-xs text-muted-foreground">
{getAssetTypeLabel(asset.assetType)}
</span>
</button>
);
})}
</div>
)}
</ItemContent>
</Item>
<p className="text-sm text-muted-foreground">
{userName}, you have {items.length.toLocaleString()} distinct items in your locker.
</p>
</div>
{pickerLayout && pickerConfig && (
<WardrobeSlotPicker
open={!!pickerSlot}
onOpenChange={(open) => {
if (!open) setPickerSlot(null);
}}
slot={pickerConfig}
items={items}
currentValue={pickerValue}
busy={busy}
onPick={(assetId) => handlePick(pickerLayout.name, assetId)}
/>
)}
</div>
);
}