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 {
participants: Participant[];
eventId: string;
sportsSeasonId: string;
existingResultParticipantIds: Set<string>;
}
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)

View file

@ -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", () => {

View file

@ -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;

View file

@ -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" };
}
}

View file

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