1
0
Fork 0
polaris-task-force/src/components/frontend/locker/LoadoutEditor.tsx

212 lines
6.3 KiB
TypeScript

"use client";
import { useEffect, useState } from "react";
import { useRouter } from "next/navigation";
import type { Loadout } from "@/payload-types";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Textarea } from "@/components/ui/textarea";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import {
LOADOUT_SLOTS,
type LockerGridItem,
type LoadoutSlots,
} from "@/lib/locker";
import {
createLoadout,
setLoadoutSlot,
updateLoadout,
} from "@/app/(frontend)/locker/actions";
interface LoadoutEditorProps {
mode: "create" | "edit";
loadout?: Loadout | null;
ownedItems: LockerGridItem[];
open: boolean;
onOpenChange: (open: boolean) => void;
}
function assetIdOf(value: unknown): string {
if (value == null) return "";
if (typeof value === "object" && "id" in value) return String(value.id);
return String(value);
}
export function LoadoutEditor({
mode,
loadout,
ownedItems,
open,
onOpenChange,
}: LoadoutEditorProps) {
const router = useRouter();
const [name, setName] = useState("");
const [description, setDescription] = useState("");
const [slots, setSlots] = useState<Record<string, string>>({});
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
if (!open) return;
setLoading(false);
setName(loadout?.name ?? "");
setDescription(loadout?.description ?? "");
const initial: Record<string, string> = {};
if (loadout) {
const current = (loadout.slots ?? {}) as LoadoutSlots;
for (const slot of LOADOUT_SLOTS) {
initial[slot.name] = assetIdOf(current[slot.name]);
}
}
setSlots(initial);
setError(null);
}, [open, loadout]);
const ownedByType = (assetTypes: readonly string[]) => {
const types = new Set(assetTypes);
const seen = new Set<number>();
return ownedItems.filter((item) => {
if (seen.has(item.assetId)) return false;
if (!types.has(item.asset.assetType as string)) return false;
seen.add(item.assetId);
return true;
});
};
async function handleSubmit() {
if (!name.trim()) {
setError("A loadout name is required.");
return;
}
setLoading(true);
setError(null);
if (mode === "create") {
const res = await createLoadout({
name: name.trim(),
description: description.trim() || undefined,
});
if (!res.success || res.data == null) {
setError(res.error ?? "Failed to create loadout.");
setLoading(false);
return;
}
const loadoutId = res.data;
for (const slot of LOADOUT_SLOTS) {
const value = slots[slot.name];
if (value) {
await setLoadoutSlot(loadoutId, slot.name, Number(value));
}
}
setLoading(false);
onOpenChange(false);
router.refresh();
return;
}
if (!loadout) return;
const res = await updateLoadout(loadout.id, {
name: name.trim(),
description: description.trim() || undefined,
});
if (!res.success) {
setError(res.error ?? "Failed to update loadout.");
setLoading(false);
return;
}
for (const slot of LOADOUT_SLOTS) {
const value = slots[slot.name];
await setLoadoutSlot(loadout.id, slot.name, value ? Number(value) : null);
}
setLoading(false);
onOpenChange(false);
router.refresh();
}
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="max-h-[85vh] overflow-y-auto">
<DialogHeader>
<DialogTitle>{mode === "create" ? "New Loadout" : "Edit Loadout"}</DialogTitle>
<DialogDescription>
Define a loadout and assign assets from your locker to each equipment slot.
</DialogDescription>
</DialogHeader>
<div className="flex flex-col gap-4">
<div className="flex flex-col gap-1.5">
<Label>Name</Label>
<Input value={name} onChange={(e) => setName(e.target.value)} placeholder="e.g., CQB" />
</div>
<div className="flex flex-col gap-1.5">
<Label>Description</Label>
<Textarea
value={description}
onChange={(e) => setDescription(e.target.value)}
placeholder="Optional notes"
rows={2}
/>
</div>
<div className="flex flex-col gap-3">
{LOADOUT_SLOTS.map((slot) => {
const options = ownedByType(slot.assetTypes);
return (
<div key={slot.name} className="flex flex-col gap-1.5">
<Label>{slot.label}</Label>
<Select
value={slots[slot.name] ?? ""}
onValueChange={(v) =>
setSlots((prev) => ({ ...prev, [slot.name]: v === "none" ? "" : v }))
}
>
<SelectTrigger>
<SelectValue placeholder={slot.description} />
</SelectTrigger>
<SelectContent>
<SelectItem value="none">Empty</SelectItem>
{options.map((item) => (
<SelectItem key={item.assetId} value={String(item.assetId)}>
{item.asset.name}
</SelectItem>
))}
</SelectContent>
</Select>
{options.length === 0 && (
<span className="text-xs text-muted-foreground">
No matching assets in your locker.
</span>
)}
</div>
);
})}
</div>
{error && <p className="text-sm text-red-400">{error}</p>}
</div>
<DialogFooter>
<Button variant="outline" onClick={() => onOpenChange(false)}>
Cancel
</Button>
<Button onClick={handleSubmit} disabled={loading}>
{loading ? "Saving..." : "Save"}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}