fix(locker): revert to onClick and add error feedback to slot picker
- Revert onPointerDown back to onClick for item selection - Add error state to display server action failures to user - Add pending state to disable buttons during submission - Update onPick to return ActionResult for error handling
This commit is contained in:
parent
bf6b29690f
commit
fc27f11ab8
2 changed files with 33 additions and 21 deletions
|
|
@ -62,7 +62,7 @@ export function WardrobePanel({ userName, loadouts, equippedLoadout, items }: Wa
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handlePick(slotName: string, assetId: number | null) {
|
async function handlePick(slotName: string, assetId: number | null) {
|
||||||
if (!equippedLoadout) return;
|
if (!equippedLoadout) return { success: false, error: "No loadout equipped." };
|
||||||
setBusy(true);
|
setBusy(true);
|
||||||
const res = await setLoadoutSlot(equippedLoadout.id, slotName, assetId);
|
const res = await setLoadoutSlot(equippedLoadout.id, slotName, assetId);
|
||||||
setBusy(false);
|
setBusy(false);
|
||||||
|
|
@ -70,6 +70,7 @@ export function WardrobePanel({ userName, loadouts, equippedLoadout, items }: Wa
|
||||||
setPickerSlot(null);
|
setPickerSlot(null);
|
||||||
router.refresh();
|
router.refresh();
|
||||||
}
|
}
|
||||||
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
const pickerLayout = pickerSlot ? getWardrobeSlot(pickerSlot) : null;
|
const pickerLayout = pickerSlot ? getWardrobeSlot(pickerSlot) : null;
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useMemo } from "react";
|
import { useMemo, useState } from "react";
|
||||||
import type { Asset } from "@/payload-types";
|
import type { Asset } from "@/payload-types";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import {
|
import {
|
||||||
|
|
@ -22,7 +22,7 @@ interface WardrobeSlotPickerProps {
|
||||||
items: LockerGridItem[];
|
items: LockerGridItem[];
|
||||||
currentValue: number | Asset | null;
|
currentValue: number | Asset | null;
|
||||||
busy: boolean;
|
busy: boolean;
|
||||||
onPick: (assetId: number | null) => void;
|
onPick: (assetId: number | null) => Promise<{ success: boolean; error?: string }>;
|
||||||
}
|
}
|
||||||
|
|
||||||
function resolveCurrentId(value: number | Asset | null): number | null {
|
function resolveCurrentId(value: number | Asset | null): number | null {
|
||||||
|
|
@ -40,6 +40,9 @@ export function WardrobeSlotPicker({
|
||||||
busy,
|
busy,
|
||||||
onPick,
|
onPick,
|
||||||
}: WardrobeSlotPickerProps) {
|
}: WardrobeSlotPickerProps) {
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
const [pending, setPending] = useState(false);
|
||||||
|
|
||||||
const options = useMemo(() => {
|
const options = useMemo(() => {
|
||||||
const types = new Set(slot.assetTypes);
|
const types = new Set(slot.assetTypes);
|
||||||
const seen = new Set<number>();
|
const seen = new Set<number>();
|
||||||
|
|
@ -53,6 +56,21 @@ export function WardrobeSlotPicker({
|
||||||
|
|
||||||
const currentId = resolveCurrentId(currentValue);
|
const currentId = resolveCurrentId(currentValue);
|
||||||
|
|
||||||
|
async function handlePick(assetId: number | null) {
|
||||||
|
setError(null);
|
||||||
|
setPending(true);
|
||||||
|
try {
|
||||||
|
const res = await onPick(assetId);
|
||||||
|
if (!res.success) {
|
||||||
|
setError(res.error ?? "Failed to equip item.");
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
setError(e instanceof Error ? e.message : "An unexpected error occurred.");
|
||||||
|
} finally {
|
||||||
|
setPending(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||||
<DialogContent className="max-h-[85vh] overflow-y-auto">
|
<DialogContent className="max-h-[85vh] overflow-y-auto">
|
||||||
|
|
@ -63,6 +81,12 @@ export function WardrobeSlotPicker({
|
||||||
</DialogDescription>
|
</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
|
|
||||||
|
{error && (
|
||||||
|
<div className="rounded-md border border-red-500/50 bg-red-500/10 px-3 py-2 text-sm text-red-400">
|
||||||
|
{error}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<div className="flex flex-col gap-1.5">
|
<div className="flex flex-col gap-1.5">
|
||||||
{options.length === 0 && (
|
{options.length === 0 && (
|
||||||
<p className="text-sm text-muted-foreground py-2">
|
<p className="text-sm text-muted-foreground py-2">
|
||||||
|
|
@ -75,11 +99,8 @@ export function WardrobeSlotPicker({
|
||||||
<button
|
<button
|
||||||
key={entry.assetId}
|
key={entry.assetId}
|
||||||
type="button"
|
type="button"
|
||||||
disabled={busy}
|
disabled={busy || pending}
|
||||||
onPointerDown={(e) => {
|
onClick={() => handlePick(entry.assetId)}
|
||||||
e.preventDefault();
|
|
||||||
onPick(entry.assetId);
|
|
||||||
}}
|
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex items-center gap-3 rounded-md border px-3 py-2 text-left text-sm transition-colors",
|
"flex items-center gap-3 rounded-md border px-3 py-2 text-left text-sm transition-colors",
|
||||||
selected
|
selected
|
||||||
|
|
@ -103,23 +124,13 @@ export function WardrobeSlotPicker({
|
||||||
<DialogFooter>
|
<DialogFooter>
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
disabled={busy}
|
disabled={busy || pending}
|
||||||
onPointerDown={(e) => {
|
onClick={() => onOpenChange(false)}
|
||||||
e.preventDefault();
|
|
||||||
onOpenChange(false);
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
{currentId != null && (
|
{currentId != null && (
|
||||||
<Button
|
<Button variant="ghost" disabled={busy || pending} onClick={() => handlePick(null)}>
|
||||||
variant="ghost"
|
|
||||||
disabled={busy}
|
|
||||||
onPointerDown={(e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
onPick(null);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Clear Slot
|
Clear Slot
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue