212 lines
6.3 KiB
TypeScript
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>
|
|
);
|
|
}
|