fix: address final review issues (sportsSeasonId authority, participant validation, CRLF, unused prop)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Chris Parsons 2026-04-12 14:41:41 -07:00
parent 057f935c88
commit 57cc66ca36
5 changed files with 22 additions and 11 deletions

View file

@ -44,14 +44,12 @@ type Stage = "idle" | "preview" | "saving";
interface BatchResultEntryProps { interface BatchResultEntryProps {
participants: Participant[]; participants: Participant[];
eventId: string;
sportsSeasonId: string; sportsSeasonId: string;
existingResultParticipantIds: Set<string>; existingResultParticipantIds: Set<string>;
} }
export function BatchResultEntry({ export function BatchResultEntry({
participants, participants,
eventId: _eventId,
sportsSeasonId, sportsSeasonId,
existingResultParticipantIds, existingResultParticipantIds,
}: BatchResultEntryProps) { }: BatchResultEntryProps) {
@ -165,7 +163,7 @@ export function BatchResultEntry({
} }
function handleConfirm() { function handleConfirm() {
if (!allResolved) return; if (!allResolved || rows.length === 0) return;
setStage("saving"); setStage("saving");
const results = rows const results = rows
.filter((r) => r.participantId !== null) .filter((r) => r.participantId !== null)

View file

@ -78,6 +78,14 @@ describe("parseResultsText", () => {
{ placement: 1, rawName: "Gerwyn Price" }, { placement: 1, rawName: "Gerwyn Price" },
]); ]);
}); });
it("handles \\r\\n line endings", () => {
const input = "1. Gerwyn Price\r\n2. Luke Littler";
expect(parseResultsText(input)).toEqual([
{ placement: 1, rawName: "Gerwyn Price" },
{ placement: 2, rawName: "Luke Littler" },
]);
});
}); });
describe("multi-line mixed formats", () => { describe("multi-line mixed formats", () => {

View file

@ -21,7 +21,7 @@ export interface ParsedLine {
export function parseResultsText(text: string): ParsedLine[] { export function parseResultsText(text: string): ParsedLine[] {
const results: ParsedLine[] = []; const results: ParsedLine[] = [];
for (const rawLine of text.split("\n")) { for (const rawLine of text.split(/\r?\n/)) {
const line = rawLine.trim(); const line = rawLine.trim();
if (!line) continue; if (!line) continue;

View file

@ -363,6 +363,14 @@ export async function action({ request, params }: Route.ActionArgs) {
return { error: "No results provided" }; return { error: "No results provided" };
} }
// Validate that all participant IDs belong to this sports season
const allSeasonParticipants = await findParticipantsBySportsSeasonId(params.id);
const validParticipantIds = new Set(allSeasonParticipants.map((p) => p.id));
const invalidEntries = incoming.filter((r) => !validParticipantIds.has(r.participantId));
if (invalidEntries.length > 0) {
return { error: "Some participant IDs do not belong to this sports season" };
}
try { try {
const existingResults = await getEventResults(params.eventId); const existingResults = await getEventResults(params.eventId);
const existingIds = new Set(existingResults.map((r) => r.participantId)); const existingIds = new Set(existingResults.map((r) => r.participantId));
@ -389,20 +397,15 @@ export async function action({ request, params }: Route.ActionArgs) {
if (intent === "create-participant") { if (intent === "create-participant") {
const name = formData.get("name"); const name = formData.get("name");
const sportsSeasonId = formData.get("sportsSeasonId");
if (typeof name !== "string" || !name.trim()) { if (typeof name !== "string" || !name.trim()) {
return { error: "Participant name is required" }; return { error: "Participant name is required" };
} }
if (typeof sportsSeasonId !== "string" || !sportsSeasonId.trim()) {
return { error: "Sports season ID is required" };
}
try { try {
const participant = await createParticipant({ const participant = await createParticipant({
name: name.trim(), name: name.trim(),
sportsSeasonId, sportsSeasonId: params.id,
}); });
return { return {
success: `Participant "${participant.name}" created.`, success: `Participant "${participant.name}" created.`,
@ -410,6 +413,9 @@ export async function action({ request, params }: Route.ActionArgs) {
}; };
} catch (error) { } catch (error) {
logger.error("Error creating participant:", error); logger.error("Error creating participant:", error);
if (error instanceof Error && error.message.includes("participants_sports_season_name_unique")) {
return { error: `A participant named "${name.trim()}" already exists in this sports season.` };
}
return { error: "Failed to create participant" }; return { error: "Failed to create participant" };
} }
} }

View file

@ -417,7 +417,6 @@ export default function EventResults({
{event.isQualifyingEvent && event.eventType !== "final_standings" && event.eventType !== "playoff_game" && !event.isComplete && ( {event.isQualifyingEvent && event.eventType !== "final_standings" && event.eventType !== "playoff_game" && !event.isComplete && (
<BatchResultEntry <BatchResultEntry
participants={participants} participants={participants}
eventId={event.id}
sportsSeasonId={sportsSeason.id} sportsSeasonId={sportsSeason.id}
existingResultParticipantIds={ existingResultParticipantIds={
new Set(results.map((r: { participant: { id: string } }) => r.participant.id)) new Set(results.map((r: { participant: { id: string } }) => r.participant.id))