diff --git a/app/components/BatchResultEntry.tsx b/app/components/BatchResultEntry.tsx index 3bbffd0..9152639 100644 --- a/app/components/BatchResultEntry.tsx +++ b/app/components/BatchResultEntry.tsx @@ -44,14 +44,12 @@ type Stage = "idle" | "preview" | "saving"; interface BatchResultEntryProps { participants: Participant[]; - eventId: string; sportsSeasonId: string; existingResultParticipantIds: Set; } export function BatchResultEntry({ participants, - eventId: _eventId, sportsSeasonId, existingResultParticipantIds, }: BatchResultEntryProps) { @@ -165,7 +163,7 @@ export function BatchResultEntry({ } function handleConfirm() { - if (!allResolved) return; + if (!allResolved || rows.length === 0) return; setStage("saving"); const results = rows .filter((r) => r.participantId !== null) diff --git a/app/lib/__tests__/parse-results-text.test.ts b/app/lib/__tests__/parse-results-text.test.ts index 496933e..69056fb 100644 --- a/app/lib/__tests__/parse-results-text.test.ts +++ b/app/lib/__tests__/parse-results-text.test.ts @@ -78,6 +78,14 @@ describe("parseResultsText", () => { { 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", () => { diff --git a/app/lib/parse-results-text.ts b/app/lib/parse-results-text.ts index 8723f31..2d4732b 100644 --- a/app/lib/parse-results-text.ts +++ b/app/lib/parse-results-text.ts @@ -21,7 +21,7 @@ export interface ParsedLine { export function parseResultsText(text: string): ParsedLine[] { const results: ParsedLine[] = []; - for (const rawLine of text.split("\n")) { + for (const rawLine of text.split(/\r?\n/)) { const line = rawLine.trim(); if (!line) continue; diff --git a/app/routes/admin.sports-seasons.$id.events.$eventId.server.ts b/app/routes/admin.sports-seasons.$id.events.$eventId.server.ts index 70a2765..e1b3515 100644 --- a/app/routes/admin.sports-seasons.$id.events.$eventId.server.ts +++ b/app/routes/admin.sports-seasons.$id.events.$eventId.server.ts @@ -363,6 +363,14 @@ export async function action({ request, params }: Route.ActionArgs) { 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 { const existingResults = await getEventResults(params.eventId); 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") { const name = formData.get("name"); - const sportsSeasonId = formData.get("sportsSeasonId"); if (typeof name !== "string" || !name.trim()) { return { error: "Participant name is required" }; } - if (typeof sportsSeasonId !== "string" || !sportsSeasonId.trim()) { - return { error: "Sports season ID is required" }; - } - try { const participant = await createParticipant({ name: name.trim(), - sportsSeasonId, + sportsSeasonId: params.id, }); return { success: `Participant "${participant.name}" created.`, @@ -410,6 +413,9 @@ export async function action({ request, params }: Route.ActionArgs) { }; } catch (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" }; } } diff --git a/app/routes/admin.sports-seasons.$id.events.$eventId.tsx b/app/routes/admin.sports-seasons.$id.events.$eventId.tsx index 8eda02f..b963899 100644 --- a/app/routes/admin.sports-seasons.$id.events.$eventId.tsx +++ b/app/routes/admin.sports-seasons.$id.events.$eventId.tsx @@ -417,7 +417,6 @@ export default function EventResults({ {event.isQualifyingEvent && event.eventType !== "final_standings" && event.eventType !== "playoff_game" && !event.isComplete && ( r.participant.id))