Simplify edit state and surface edit errors to user

- Combine editingId + editingName into single editing: { id, name } | null state object
- Extract cancelEdit() helper called in 3 places instead of repeating setEditing(null)
- Tag all update-name action responses with intent: "update-name" so errors can be
  identified and routed correctly
- Show update-name errors inline below the edit input
- Guard single-add and bulk-add error displays against showing update-name errors

https://claude.ai/code/session_01Q94DWQ1HZEB9PJhGMArU8D
This commit is contained in:
Claude 2026-04-02 00:51:55 +00:00
parent e43fd17de4
commit 93fe319830
No known key found for this signature in database

View file

@ -69,17 +69,17 @@ export async function action({ request, params }: Route.ActionArgs) {
const newName = formData.get("newName"); const newName = formData.get("newName");
if (typeof participantId !== "string") { if (typeof participantId !== "string") {
return { error: "Invalid participant" }; return { error: "Invalid participant", intent: "update-name" };
} }
if (typeof newName !== "string" || !newName.trim()) { if (typeof newName !== "string" || !newName.trim()) {
return { error: "Name is required" }; return { error: "Name is required", intent: "update-name" };
} }
const trimmedName = newName.trim(); const trimmedName = newName.trim();
const existing = await findParticipantByName(params.id, trimmedName); const existing = await findParticipantByName(params.id, trimmedName);
if (existing && existing.id !== participantId) { if (existing && existing.id !== participantId) {
return { error: `"${trimmedName}" already exists in this season.` }; return { error: `"${trimmedName}" already exists in this season.`, intent: "update-name" };
} }
try { try {
@ -87,7 +87,7 @@ export async function action({ request, params }: Route.ActionArgs) {
return { success: true, intent: "update-name" }; return { success: true, intent: "update-name" };
} catch (error) { } catch (error) {
logger.error("Error updating participant:", error); logger.error("Error updating participant:", error);
return { error: "Failed to update participant. Please try again." }; return { error: "Failed to update participant. Please try again.", intent: "update-name" };
} }
} }
@ -171,16 +171,16 @@ export async function action({ request, params }: Route.ActionArgs) {
export default function ManageParticipants({ loaderData, actionData }: Route.ComponentProps) { export default function ManageParticipants({ loaderData, actionData }: Route.ComponentProps) {
const { sportsSeason, participants } = loaderData; const { sportsSeason, participants } = loaderData;
const [editingId, setEditingId] = useState<string | null>(null); const [editing, setEditing] = useState<{ id: string; name: string } | null>(null);
const [editingName, setEditingName] = useState("");
const cancelEdit = () => setEditing(null);
// Use success state to reset forms by changing the key // Use success state to reset forms by changing the key
const formKey = actionData?.success ? Date.now() : 'static'; const formKey = actionData?.success ? Date.now() : 'static';
useEffect(() => { useEffect(() => {
if (actionData?.success && actionData?.intent === "update-name") { if (actionData?.success && actionData?.intent === "update-name") {
setEditingId(null); cancelEdit();
setEditingName("");
} }
}, [actionData]); }, [actionData]);
@ -221,7 +221,7 @@ export default function ManageParticipants({ loaderData, actionData }: Route.Com
/> />
</div> </div>
{actionData?.error && !actionData?.count && ( {actionData?.error && !actionData?.count && actionData?.intent !== "update-name" && (
<div className="bg-destructive/15 text-destructive px-4 py-3 rounded-md text-sm"> <div className="bg-destructive/15 text-destructive px-4 py-3 rounded-md text-sm">
{actionData.error} {actionData.error}
</div> </div>
@ -267,7 +267,7 @@ export default function ManageParticipants({ loaderData, actionData }: Route.Com
</p> </p>
</div> </div>
{actionData?.error && actionData?.count === undefined && ( {actionData?.error && actionData?.count === undefined && actionData?.intent !== "update-name" && (
<div className="bg-destructive/15 text-destructive px-4 py-3 rounded-md text-sm"> <div className="bg-destructive/15 text-destructive px-4 py-3 rounded-md text-sm">
{actionData.error} {actionData.error}
</div> </div>
@ -318,30 +318,30 @@ export default function ManageParticipants({ loaderData, actionData }: Route.Com
{participants.map((participant) => ( {participants.map((participant) => (
<TableRow key={participant.id}> <TableRow key={participant.id}>
<TableCell className="font-medium"> <TableCell className="font-medium">
{editingId === participant.id ? ( {editing?.id === participant.id ? (
<div className="space-y-1">
<Input <Input
value={editingName} value={editing.name}
onChange={(e) => setEditingName(e.target.value)} onChange={(e) => setEditing({ id: participant.id, name: e.target.value })}
className="h-8" className="h-8"
autoFocus autoFocus
onKeyDown={(e) => { onKeyDown={(e) => { if (e.key === "Escape") cancelEdit(); }}
if (e.key === "Escape") {
setEditingId(null);
setEditingName("");
}
}}
/> />
{actionData?.error && actionData?.intent === "update-name" && (
<p className="text-xs text-destructive">{actionData.error}</p>
)}
</div>
) : ( ) : (
participant.name participant.name
)} )}
</TableCell> </TableCell>
<TableCell> <TableCell>
{editingId === participant.id ? ( {editing?.id === participant.id ? (
<div className="flex gap-1"> <div className="flex gap-1">
<Form method="post"> <Form method="post">
<input type="hidden" name="intent" value="update-name" /> <input type="hidden" name="intent" value="update-name" />
<input type="hidden" name="participantId" value={participant.id} /> <input type="hidden" name="participantId" value={participant.id} />
<input type="hidden" name="newName" value={editingName} /> <input type="hidden" name="newName" value={editing.name} />
<Button <Button
type="submit" type="submit"
variant="ghost" variant="ghost"
@ -356,10 +356,7 @@ export default function ManageParticipants({ loaderData, actionData }: Route.Com
variant="ghost" variant="ghost"
size="sm" size="sm"
className="h-8 w-8 p-0" className="h-8 w-8 p-0"
onClick={() => { onClick={cancelEdit}
setEditingId(null);
setEditingName("");
}}
> >
<X className="h-4 w-4" /> <X className="h-4 w-4" />
</Button> </Button>
@ -371,10 +368,7 @@ export default function ManageParticipants({ loaderData, actionData }: Route.Com
variant="ghost" variant="ghost"
size="sm" size="sm"
className="h-8 w-8 p-0" className="h-8 w-8 p-0"
onClick={() => { onClick={() => setEditing({ id: participant.id, name: participant.name })}
setEditingId(participant.id);
setEditingName(participant.name);
}}
> >
<Pencil className="h-4 w-4" /> <Pencil className="h-4 w-4" />
</Button> </Button>