Add inline edit for participant names on admin sports season page

Adds a pencil icon button next to each participant's delete button. Clicking
it switches the row into edit mode with an input field, a save (checkmark) and
cancel (X) button. Escape also cancels. Submits via intent="update-name" and
validates uniqueness within the season before persisting.

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

View file

@ -1,3 +1,4 @@
import { useState, useEffect } from "react";
import { Form, Link } from "react-router"; import { Form, Link } from "react-router";
import type { Route } from "./+types/admin.sports-seasons.$id.participants"; import type { Route } from "./+types/admin.sports-seasons.$id.participants";
@ -8,6 +9,7 @@ import {
findParticipantByName, findParticipantByName,
createParticipant, createParticipant,
deleteParticipant, deleteParticipant,
updateParticipant,
} from "~/models/participant"; } from "~/models/participant";
import { Button } from "~/components/ui/button"; import { Button } from "~/components/ui/button";
import { Input } from "~/components/ui/input"; import { Input } from "~/components/ui/input";
@ -28,7 +30,7 @@ import {
TableHeader, TableHeader,
TableRow, TableRow,
} from "~/components/ui/table"; } from "~/components/ui/table";
import { Plus, Trash2, ArrowLeft } from "lucide-react"; import { Plus, Trash2, ArrowLeft, Pencil, Check, X } from "lucide-react";
export function meta({ data }: Route.MetaArgs): Route.MetaDescriptors { export function meta({ data }: Route.MetaArgs): Route.MetaDescriptors {
return [{ title: `Participants — ${data?.sportsSeason?.name ?? "Sports Season"} - Brackt Admin` }]; return [{ title: `Participants — ${data?.sportsSeason?.name ?? "Sports Season"} - Brackt Admin` }];
@ -62,6 +64,33 @@ export async function action({ request, params }: Route.ActionArgs) {
return { success: true }; return { success: true };
} }
if (intent === "update-name") {
const participantId = formData.get("participantId");
const newName = formData.get("newName");
if (typeof participantId !== "string") {
return { error: "Invalid participant" };
}
if (typeof newName !== "string" || !newName.trim()) {
return { error: "Name is required" };
}
const trimmedName = newName.trim();
const existing = await findParticipantByName(params.id, trimmedName);
if (existing && existing.id !== participantId) {
return { error: `"${trimmedName}" already exists in this season.` };
}
try {
await updateParticipant(participantId, { name: trimmedName });
return { success: true, intent: "update-name" };
} catch (error) {
logger.error("Error updating participant:", error);
return { error: "Failed to update participant. Please try again." };
}
}
// Bulk add participants // Bulk add participants
if (intent === "bulk") { if (intent === "bulk") {
const bulkNames = formData.get("bulkNames"); const bulkNames = formData.get("bulkNames");
@ -142,10 +171,19 @@ 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 [editingName, setEditingName] = useState("");
// 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(() => {
if (actionData?.success && actionData?.intent === "update-name") {
setEditingId(null);
setEditingName("");
}
}, [actionData]);
return ( return (
<div className="p-8"> <div className="p-8">
<div className="max-w-4xl"> <div className="max-w-4xl">
@ -273,26 +311,87 @@ export default function ManageParticipants({ loaderData, actionData }: Route.Com
<TableHeader> <TableHeader>
<TableRow> <TableRow>
<TableHead>Name</TableHead> <TableHead>Name</TableHead>
<TableHead className="w-[50px]" /> <TableHead className="w-[100px]" />
</TableRow> </TableRow>
</TableHeader> </TableHeader>
<TableBody> <TableBody>
{participants.map((participant) => ( {participants.map((participant) => (
<TableRow key={participant.id}> <TableRow key={participant.id}>
<TableCell className="font-medium">{participant.name}</TableCell> <TableCell className="font-medium">
{editingId === participant.id ? (
<Input
value={editingName}
onChange={(e) => setEditingName(e.target.value)}
className="h-8"
autoFocus
onKeyDown={(e) => {
if (e.key === "Escape") {
setEditingId(null);
setEditingName("");
}
}}
/>
) : (
participant.name
)}
</TableCell>
<TableCell> <TableCell>
<Form method="post"> {editingId === participant.id ? (
<input type="hidden" name="intent" value="delete" /> <div className="flex gap-1">
<input type="hidden" name="participantId" value={participant.id} /> <Form method="post">
<Button <input type="hidden" name="intent" value="update-name" />
type="submit" <input type="hidden" name="participantId" value={participant.id} />
variant="ghost" <input type="hidden" name="newName" value={editingName} />
size="sm" <Button
className="h-8 w-8 p-0 text-destructive hover:text-destructive" type="submit"
> variant="ghost"
<Trash2 className="h-4 w-4" /> size="sm"
</Button> className="h-8 w-8 p-0 text-emerald-500 hover:text-emerald-400"
</Form> >
<Check className="h-4 w-4" />
</Button>
</Form>
<Button
type="button"
variant="ghost"
size="sm"
className="h-8 w-8 p-0"
onClick={() => {
setEditingId(null);
setEditingName("");
}}
>
<X className="h-4 w-4" />
</Button>
</div>
) : (
<div className="flex gap-1">
<Button
type="button"
variant="ghost"
size="sm"
className="h-8 w-8 p-0"
onClick={() => {
setEditingId(participant.id);
setEditingName(participant.name);
}}
>
<Pencil className="h-4 w-4" />
</Button>
<Form method="post">
<input type="hidden" name="intent" value="delete" />
<input type="hidden" name="participantId" value={participant.id} />
<Button
type="submit"
variant="ghost"
size="sm"
className="h-8 w-8 p-0 text-destructive hover:text-destructive"
>
<Trash2 className="h-4 w-4" />
</Button>
</Form>
</div>
)}
</TableCell> </TableCell>
</TableRow> </TableRow>
))} ))}