diff --git a/src/containers/CompetitionResults/CompetitionResults.test.tsx b/src/containers/CompetitionResults/CompetitionResults.test.tsx index 2c7d1d0..38d35fb 100644 --- a/src/containers/CompetitionResults/CompetitionResults.test.tsx +++ b/src/containers/CompetitionResults/CompetitionResults.test.tsx @@ -43,8 +43,8 @@ jest.mock('react-i18next', () => ({ if (key === 'competition.results.average') return 'Avg'; if (key === 'competition.results.best') return 'Best'; if (key === 'competition.results.attempts') return 'Attempts'; - if (key === 'competition.results.liveResultsDelayNote') { - return 'Data is pulled from WCA Live and may be delayed.'; + if (key === 'competition.results.resultsSourceNote') { + return 'Results are merged from WCIF and WCA Live when available. WCA Live data may be delayed.'; } if (key === 'competition.results.allResults') return 'All results'; if (key === 'competition.results.unknownCompetitor') { @@ -273,7 +273,9 @@ describe('CompetitionResultsContainer', () => { expect(screen.queryByRole('link', { name: 'Round 2' })).not.toBeInTheDocument(); expect(screen.queryByRole('table', { name: 'Results' })).not.toBeInTheDocument(); expect( - screen.getByText('Data is pulled from WCA Live and may be delayed.'), + screen.getByText( + 'Results are merged from WCIF and WCA Live when available. WCA Live data may be delayed.', + ), ).toBeInTheDocument(); }); @@ -316,7 +318,9 @@ describe('CompetitionResultsContainer', () => { expect(screen.getAllByText('12.00')).toHaveLength(2); expect(screen.queryByLabelText('Attempts')).not.toBeInTheDocument(); expect( - screen.getByText('Data is pulled from WCA Live and may be delayed.'), + screen.getByText( + 'Results are merged from WCIF and WCA Live when available. WCA Live data may be delayed.', + ), ).toBeInTheDocument(); expect( screen @@ -511,7 +515,9 @@ describe('CompetitionResultsContainer', () => { within(screen.getByRole('row', { name: /2 Nick Silvestri/ })).getAllByText('15.00'), ).toHaveLength(2); expect( - screen.getByText('Data is pulled from WCA Live and may be delayed.'), + screen.getByText( + 'Results are merged from WCIF and WCA Live when available. WCA Live data may be delayed.', + ), ).toBeInTheDocument(); }); diff --git a/src/containers/CompetitionResults/CompetitionResults.tsx b/src/containers/CompetitionResults/CompetitionResults.tsx index bd91ec8..27c8dcb 100644 --- a/src/containers/CompetitionResults/CompetitionResults.tsx +++ b/src/containers/CompetitionResults/CompetitionResults.tsx @@ -274,7 +274,7 @@ export function CompetitionResultsContainer({ roundNumber: selectedRound.roundNumber, })} - +
- +
diff --git a/src/containers/CompetitionResults/resultSources.ts b/src/containers/CompetitionResults/resultSources.ts index 6e008dd..ac0510a 100644 --- a/src/containers/CompetitionResults/resultSources.ts +++ b/src/containers/CompetitionResults/resultSources.ts @@ -5,6 +5,16 @@ import { normalizeResultRecordTag } from './ResultRecordBadge'; const roundTypeOrder = ['0', '1', 'c', '2', 'e', '3', 'b', 'd', 'f']; +const normalizeName = (value: string) => value.trim().toLowerCase(); + +export const findUniquePersonByName = (persons: Person[], name: string) => { + const matchingPersons = persons.filter( + (person) => normalizeName(person.name) === normalizeName(name), + ); + + return matchingPersons.length === 1 ? matchingPersons[0] : undefined; +}; + const compareRoundTypeIds = (a: string, b: string) => { const aIndex = roundTypeOrder.indexOf(a); const bIndex = roundTypeOrder.indexOf(b); @@ -24,12 +34,13 @@ const compareRoundTypeIds = (a: string, b: string) => { return a.localeCompare(b); }; -export const findPersonForApiResult = (persons: Person[], result: WcaCompetitionResult) => - persons.find( - (person) => - (result.wca_id && person.wcaId === result.wca_id) || - person.name.toLocaleLowerCase() === result.name.toLocaleLowerCase(), - ); +export const findPersonForApiResult = (persons: Person[], result: WcaCompetitionResult) => { + if (result.wca_id) { + return persons.find((person) => person.wcaId === result.wca_id); + } + + return findUniquePersonByName(persons, result.name); +}; export const getWcaApiRoundTypeMap = (results: WcaCompetitionResult[]) => { const roundTypeIdsByEventId = new Map(); diff --git a/src/containers/CompetitionResults/resultsProvider.test.ts b/src/containers/CompetitionResults/resultsProvider.test.ts index 6cfc85f..e7f465e 100644 --- a/src/containers/CompetitionResults/resultsProvider.test.ts +++ b/src/containers/CompetitionResults/resultsProvider.test.ts @@ -1,6 +1,7 @@ import { Competition, Person } from '@wca/helpers'; import { WcaLiveCompetitorResult, WcaLiveRound } from '@/hooks/queries/useWcaLive'; import { WcaCompetitionResult } from '@/lib/api'; +import { findPersonForApiResult } from './resultSources'; import { getPersonalResultsFromSources, getRoundResultsFromSources } from './resultsProvider'; jest.mock('@/lib/events', () => ({ @@ -186,6 +187,8 @@ const liveRoundResult = ( person: { id: '2', registrantId: 2, + wcaUserId: 1002, + wcaId: '2010BOBS01', name: 'Bob Solver', }, ...overrides, @@ -222,6 +225,39 @@ const livePersonalResult = ( ...overrides, }); +describe('findPersonForApiResult', () => { + const duplicateNamePerson: Person = { + ...persons[0], + registrantId: 4, + wcaUserId: 1004, + wcaId: '2010ALIC02', + }; + + it('matches an exact WCA ID before considering duplicate names', () => { + expect( + findPersonForApiResult( + [...persons, duplicateNamePerson], + apiResult({ name: 'Alice Solver', wca_id: '2010ALIC02' }), + ), + ).toBe(duplicateNamePerson); + }); + + it('does not fall back to a name when a WCA ID is present but unmatched', () => { + expect( + findPersonForApiResult(persons, apiResult({ name: 'Alice Solver', wca_id: '2099MISS01' })), + ).toBeUndefined(); + }); + + it('does not use an ambiguous name when the API result has no WCA ID', () => { + expect( + findPersonForApiResult( + [...persons, duplicateNamePerson], + apiResult({ name: 'Alice Solver', wca_id: null }), + ), + ).toBeUndefined(); + }); +}); + describe('resultsProvider', () => { describe('getRoundResultsFromSources', () => { it('returns no round results without WCIF or a selected round', () => { @@ -301,6 +337,8 @@ describe('resultsProvider', () => { average: 2200, round_type_id: '1', attempts: [2100, 2200, 2300], + best_index: 2, + worst_index: 0, regional_single_record: 'NR', regional_average_record: 'WR', }), @@ -326,6 +364,12 @@ describe('resultsProvider', () => { singleRecordTag: 'NR', averageRecordTag: 'WR', }); + expect(results.find((result) => result.personId === 2)).not.toHaveProperty( + 'bestAttemptIndex', + ); + expect(results.find((result) => result.personId === 2)).not.toHaveProperty( + 'worstAttemptIndex', + ); expect(results.find((result) => result.personId === 1)).toMatchObject({ ranking: 1, best: 1200, @@ -341,6 +385,98 @@ describe('resultsProvider', () => { }); }); + it('matches live round results by WCA user id when registrant id is missing', () => { + const results = getRoundResultsFromSources({ + wcif, + selectedRound: selectedRound(0), + wcaApiResults: [], + wcaLiveRound: liveRound([ + liveRoundResult({ + id: 'live-alice', + ranking: 4, + best: 900, + average: 1000, + attempts: [{ result: 900 }, { result: 1000 }, { result: 1100 }], + person: { + id: 'missing-registrant-id', + registrantId: null, + wcaUserId: 1001, + wcaId: '2010ALIC01', + name: 'Alice Published Name', + }, + }), + ]), + }); + + expect(results).toHaveLength(3); + expect(results.filter((result) => result.personId === 1)).toHaveLength(1); + expect(results.find((result) => result.personId === 1)).toMatchObject({ + personName: 'Alice Published Name', + ranking: 4, + best: 900, + average: 1000, + attempts: [{ result: 900 }, { result: 1000 }, { result: 1100 }], + }); + }); + + it('leaves a live result unlinked when its name is ambiguous and identifiers are missing', () => { + const duplicateNamePerson: Person = { + ...persons[0], + registrantId: 4, + wcaUserId: 1004, + wcaId: '2010ALIC02', + }; + const duplicateNameWcif = { + ...wcif, + persons: [...persons, duplicateNamePerson], + } as Competition; + const results = getRoundResultsFromSources({ + wcif: duplicateNameWcif, + selectedRound: selectedRound(0), + wcaApiResults: [], + wcaLiveRound: liveRound([ + liveRoundResult({ + id: 'live-ambiguous-alice', + person: { + id: 'missing-identifiers', + registrantId: null, + name: 'Alice Solver', + }, + }), + ]), + }); + + expect(results.find((result) => result.id === 'live-ambiguous-alice')).toMatchObject({ + personId: null, + personName: 'Alice Solver', + }); + }); + + it('does not override an unmatched live identity with a name match', () => { + const results = getRoundResultsFromSources({ + wcif, + selectedRound: selectedRound(0), + wcaApiResults: [], + wcaLiveRound: liveRound([ + liveRoundResult({ + id: 'live-conflicting-alice', + person: { + id: 'conflicting-identifiers', + registrantId: null, + wcaUserId: 9999, + wcaId: '2099MISS01', + name: 'Alice Solver', + }, + }), + ]), + }); + + expect(results.find((result) => result.id === 'live-conflicting-alice')).toMatchObject({ + personId: null, + personName: 'Alice Solver', + }); + }); + it('matches WCA API results by WCA ID before using the displayed name', () => { const results = getRoundResultsFromSources({ wcif, @@ -479,6 +615,23 @@ describe('resultsProvider', () => { ]); }); + it('does not include a same-name API result with a conflicting WCA ID', () => { + const results = getPersonalResultsFromSources({ + wcif, + person: persons[0], + wcaApiResults: [ + apiResult({ + id: 9201, + name: 'Alice Solver', + wca_id: '2010BOBS01', + }), + ], + wcaLiveResults: [], + }); + + expect(results[0].rounds.map((round) => round.roundId)).toEqual(['333-r1']); + }); + it('sorts live personal events and rounds before merging them', () => { const secondRound = livePersonalResult({ id: 'live-personal-333-r2', diff --git a/src/containers/CompetitionResults/resultsProvider.ts b/src/containers/CompetitionResults/resultsProvider.ts index b475ff9..562e6d7 100644 --- a/src/containers/CompetitionResults/resultsProvider.ts +++ b/src/containers/CompetitionResults/resultsProvider.ts @@ -8,7 +8,12 @@ import { PersonalRoundResult } from '../CompetitionPersonalResults/CompetitionPe import { CompetitionRoundResult } from './CompetitionResultsTable'; import { normalizeResultRecordTag } from './ResultRecordBadge'; import { getStoredRoundResults } from './advancement'; -import { getApiRoundResults, getWcaApiResultsByRoundId } from './resultSources'; +import { + findPersonForApiResult, + findUniquePersonByName, + getApiRoundResults, + getWcaApiResultsByRoundId, +} from './resultSources'; import { getRoundRosterResults } from './roundRoster'; type SelectedRound = NonNullable>; @@ -44,22 +49,66 @@ const mergeRoundResultSources = ( return [...resultsByKey.values()]; }; -const getLiveRoundResults = (wcaLiveRound: WcaLiveRound | null | undefined) => +const findPersonForLiveRoundResult = ( + wcif: Competition, + result: WcaLiveRound['results'][number], +): Person | undefined => { + if (result.person.registrantId != null) { + const matchedByRegistrantId = wcif.persons.find( + (person) => person.registrantId === result.person.registrantId, + ); + + if (matchedByRegistrantId) { + return matchedByRegistrantId; + } + } + + if (result.person.wcaUserId != null) { + const matchedByWcaUserId = wcif.persons.find( + (person) => person.wcaUserId === result.person.wcaUserId, + ); + + if (matchedByWcaUserId) { + return matchedByWcaUserId; + } + } + + if (result.person.wcaId) { + const matchedByWcaId = wcif.persons.find((person) => person.wcaId === result.person.wcaId); + + if (matchedByWcaId) { + return matchedByWcaId; + } + } + + const hasStrongIdentifier = + result.person.registrantId != null || + result.person.wcaUserId != null || + Boolean(result.person.wcaId); + + return hasStrongIdentifier ? undefined : findUniquePersonByName(wcif.persons, result.person.name); +}; + +const getLiveRoundResults = (wcif: Competition, wcaLiveRound: WcaLiveRound | null | undefined) => wcaLiveRound?.results .filter((result) => result.attempts.length > 0) - .map((result) => ({ - id: result.id, - personId: result.person.registrantId, - personName: result.person.name, - ranking: result.ranking, - advancing: result.advancing, - advancingQuestionable: result.advancingQuestionable, - attempts: result.attempts, - best: result.best, - average: result.average, - singleRecordTag: normalizeResultRecordTag(result.singleRecordTag), - averageRecordTag: normalizeResultRecordTag(result.averageRecordTag), - })) ?? []; + .map((result) => { + const matchedPerson = findPersonForLiveRoundResult(wcif, result); + + return { + id: result.id, + personId: matchedPerson?.registrantId ?? result.person.registrantId, + personName: result.person.name, + ranking: result.ranking, + advancing: result.advancing, + advancingQuestionable: result.advancingQuestionable, + attempts: result.attempts, + best: result.best, + average: result.average, + singleRecordTag: normalizeResultRecordTag(result.singleRecordTag), + averageRecordTag: normalizeResultRecordTag(result.averageRecordTag), + }; + }) ?? []; export const getRoundResultsFromSources = ({ wcif, @@ -76,7 +125,7 @@ export const getRoundResultsFromSources = ({ return []; } - const liveRoundResults = getLiveRoundResults(wcaLiveRound); + const liveRoundResults = getLiveRoundResults(wcif, wcaLiveRound); const storedRoundResults = getStoredRoundResults( selectedRound.round, getAdvancementConditionForRound(selectedRound.event.rounds, selectedRound.round), @@ -147,8 +196,12 @@ const getLivePersonalResults = ( return { eventId, - eventName: firstResult.round.competitionEvent.event.name, - eventRank: firstResult.round.competitionEvent.event.rank, + eventName: wcifEvent + ? getEventName(eventId, wcifEvent) + : firstResult.round.competitionEvent.event.name, + eventRank: wcifEvent + ? wcif.events.indexOf(wcifEvent) + : firstResult.round.competitionEvent.event.rank, rounds: eventResults .sort((a, b) => a.round.number - b.round.number) .map((result) => ({ @@ -175,9 +228,7 @@ const getApiPersonalResults = ( apiResults: WcaCompetitionResult[], ): EventResults[] => { const personResults = apiResults.filter( - (result) => - (person.wcaId && result.wca_id === person.wcaId) || - result.name.toLocaleLowerCase() === person.name.toLocaleLowerCase(), + (result) => findPersonForApiResult(wcif.persons, result)?.registrantId === person.registrantId, ); const resultsByRoundId = getWcaApiResultsByRoundId(wcif, personResults); @@ -223,11 +274,18 @@ const mergeEventResultSources = (...sources: EventResults[][]): EventResults[] = ); sourceEvent.rounds.forEach((roundResult) => { - roundsById.set(roundResult.roundId, roundResult); + const existingRoundResult = roundsById.get(roundResult.roundId); + + roundsById.set(roundResult.roundId, { + ...existingRoundResult, + ...roundResult, + }); }); eventsById.set(sourceEvent.eventId, { + ...existingEvent, ...sourceEvent, + eventName: existingEvent?.eventName ?? sourceEvent.eventName, eventRank: existingEvent?.eventRank ?? sourceEvent.eventRank, rounds: [...roundsById.values()].sort((a, b) => a.roundNumber - b.roundNumber), }); diff --git a/src/containers/PersonalSchedule/utils.test.ts b/src/containers/PersonalSchedule/utils.test.ts new file mode 100644 index 0000000..50cbbbc --- /dev/null +++ b/src/containers/PersonalSchedule/utils.test.ts @@ -0,0 +1,163 @@ +import { Competition, Person } from '@wca/helpers'; +import { parseActivityCodeFlexible } from '@/lib/activityCodes'; +import { formatBriefActivityName, getAllAssignments, getGroupedAssignmentsByDate } from './utils'; + +jest.mock('@/i18n', () => ({ + __esModule: true, + default: { + t: (key: string) => key, + }, + t: (key: string) => key, +})); + +const wcif = { + id: 'WC2025', + schedule: { + venues: [ + { + id: 1, + name: 'Venue', + timezone: 'America/Los_Angeles', + rooms: [], + }, + ], + }, + events: [], +} as unknown as Competition; + +const worldsAssignmentsPerson = { + registrantId: 215, + assignments: [], + registration: { + eventIds: [], + }, + extensions: [ + { + id: 'com.competitiongroups.worldsassignments', + data: { + assignments: [ + { + staff: 'Stage Stream - Main', + startTime: '2025-07-04T01:00:00Z', + endTime: '2025-07-04T02:00:00Z', + }, + ], + }, + }, + ], +} as unknown as Person; + +const fmcWcif = { + ...wcif, + schedule: { + ...wcif.schedule, + venues: [ + { + ...wcif.schedule.venues[0], + rooms: [ + { + id: 1, + name: 'FMC Room', + color: '#ffffff', + extensions: [], + activities: [ + { + id: 100, + name: 'Fewest Moves Attempt 1', + activityCode: '333fm-r1-a1', + startTime: '2025-07-03T18:00:00Z', + endTime: '2025-07-03T19:00:00Z', + extensions: [], + childActivities: [ + { + id: 101, + name: 'Fewest Moves Attempt 1 Group 1', + activityCode: '333fm-r1-g1-a1', + startTime: '2025-07-03T18:00:00Z', + endTime: '2025-07-03T19:00:00Z', + extensions: [], + childActivities: [], + }, + ], + }, + ], + }, + ], + }, + ], + }, +} as unknown as Competition; + +const fmcPerson = { + registrantId: 216, + assignments: [ + { + activityId: 101, + assignmentCode: 'competitor', + stationNumber: null, + }, + ], + registration: { + eventIds: ['333fm'], + }, + extensions: [], +} as unknown as Person; + +describe('PersonalSchedule utils', () => { + it('creates parse-safe activities for worlds assignments with free-form staff names', () => { + const [assignment] = getAllAssignments(wcif, worldsAssignmentsPerson); + + expect(assignment.activity).toBeDefined(); + const activity = assignment.activity!; + + expect(activity.activityCode).toBe('other-misc'); + expect(() => parseActivityCodeFlexible(activity.activityCode)).not.toThrow(); + expect(formatBriefActivityName(activity)).toBe('Stage Stream - Main'); + }); + + it('includes days that only have worlds assignments', () => { + const [scheduleDay] = getGroupedAssignmentsByDate(wcif, worldsAssignmentsPerson); + + expect(scheduleDay.date).toBe('Thursday, 7/3/2025'); + expect(scheduleDay.assignments).toHaveLength(1); + expect(scheduleDay.assignments[0].assignment.assignmentCode).toBe('Stage Stream - Main'); + }); + + it('keeps standard other activities in the other namespace', () => { + expect(parseActivityCodeFlexible('other-lunch-g2')).toMatchObject({ + eventId: 'other-lunch', + roundNumber: 1, + groupNumber: 2, + attemptNumber: null, + }); + expect(parseActivityCodeFlexible('other-misc')).toMatchObject({ + eventId: 'other-misc', + }); + }); + + it('only includes the FMC attempt activity assigned to the competitor', () => { + const fmcAssignments = getAllAssignments(fmcWcif, fmcPerson).filter(({ activity }) => + activity?.activityCode.startsWith('333fm'), + ); + + expect(fmcAssignments).toHaveLength(1); + expect(fmcAssignments[0].activityId).toBe(101); + }); + + it('does not create FMC assignments without a matching activity assignment', () => { + const personWithoutFmcAssignment = { + ...fmcPerson, + assignments: [], + } as Person; + + expect(getAllAssignments(fmcWcif, personWithoutFmcAssignment)).toHaveLength(0); + }); + + it('includes a day that only has an assigned FMC attempt', () => { + const [scheduleDay] = getGroupedAssignmentsByDate(fmcWcif, fmcPerson); + + expect(scheduleDay.date).toBe('Thursday, 7/3/2025'); + expect(scheduleDay.assignments).toHaveLength(1); + expect(scheduleDay.assignments[0].assignment.activityId).toBe(101); + }); +}); diff --git a/src/containers/PersonalSchedule/utils.ts b/src/containers/PersonalSchedule/utils.ts index 99cf80d..43ad8ff 100644 --- a/src/containers/PersonalSchedule/utils.ts +++ b/src/containers/PersonalSchedule/utils.ts @@ -1,12 +1,27 @@ import { Activity, Assignment, Competition, Person } from '@wca/helpers'; import { getWorldAssignmentsExtension } from '@/extensions/com.competitiongroups.worldsassignments'; import i18n from '@/i18n'; -import { getAllActivities, getRooms } from '@/lib/activities'; +import { getAllActivities } from '@/lib/activities'; import { isUnofficialParsedActivityCode, parseActivityCodeFlexible } from '@/lib/activityCodes'; import { eventById, isOfficialEventId } from '@/lib/events'; import { formatNumericDate, getNumericDateFormatter } from '@/lib/time'; import { byDate } from '@/lib/utils'; +type ActivityWithLocation = Activity & { + room?: { id: number }; + parent?: { room?: { id: number } }; +}; + +const getActivityTimeZone = ( + venues: Competition['schedule']['venues'], + activity: ActivityWithLocation, +) => { + const roomId = activity.room?.id ?? activity.parent?.room?.id; + const venue = venues.find((candidate) => candidate.rooms.some((room) => room.id === roomId)); + + return venue?.timezone ?? venues[0]?.timezone; +}; + export const getNormalAssignments = (wcif: Competition, person: Person) => { const allActivities = getAllActivities(wcif); @@ -53,7 +68,7 @@ const getExtraAssignments = (person: Person) => { activityId: -1, stationNumber: null, activity: { - activityCode: 'other-' + assignment.staff, + activityCode: 'other-misc', startTime: assignment.startTime, endTime: assignment.endTime, childActivities: [], @@ -152,16 +167,6 @@ export const getGroupedAssignmentsByDate = (wcif: Competition, person: Person) = const scheduledDays = allAssignments .map((a) => { - // Commenting out for NAC26, extra assignments are valid even if the day does not have any other assignments - // if (a.type === 'extra') { - // return { - // approxDateTime: 0, - // date: '', - // dateParts: [], - // assignments: [], - // }; - // } - if (!a.activity) { return { approxDateTime: 0, @@ -171,18 +176,14 @@ export const getGroupedAssignmentsByDate = (wcif: Competition, person: Person) = }; } - const roomId = a.activity && 'room' in a.activity && a.activity.room?.id; - const parent = a.activity && 'parent' in a.activity && a.activity.parent; - - const venue = venues.find((v) => v.rooms.some((r) => r.id === roomId || parent)); - const dateTime = new Date(a.activity?.startTime); - const date = formatNumericDate(dateTime, venue?.timezone); + const timeZone = getActivityTimeZone(venues, a.activity); + const date = formatNumericDate(dateTime, timeZone); return { approxDateTime: dateTime.getTime(), date: date, - dateParts: getNumericDateFormatter(venue?.timezone).formatToParts(dateTime), + dateParts: getNumericDateFormatter(timeZone).formatToParts(dateTime), }; }) .filter((v, i, arr) => arr.findIndex(({ date }) => date === v.date) === i); @@ -200,7 +201,6 @@ export const getGroupedAssignmentsByDate = (wcif: Competition, person: Person) = export const getAssignmentsWithParsedDate = (wcif: Competition, person: Person) => { const allAssignments = getAllAssignments(wcif, person); const venues = wcif.schedule.venues; - const rooms = getRooms(wcif); return allAssignments .map((assignment) => { @@ -214,7 +214,10 @@ export const getAssignmentsWithParsedDate = (wcif: Competition, person: Person) return { assignment, activity, - date: formatNumericDate(new Date(activity.startTime), venues[0].timezone), + date: formatNumericDate( + new Date(activity.startTime), + getActivityTimeZone(venues, activity), + ), }; } @@ -226,16 +229,12 @@ export const getAssignmentsWithParsedDate = (wcif: Competition, person: Person) }; } - const roomId = (activity.room || activity.parent?.room)?.id; - - const venue = activity?.room?.id ? rooms.find((r) => r.id === roomId)?.venue : venues[0]; - const dateTime = new Date(activity.startTime); return { assignment, activity, - date: formatNumericDate(dateTime, venue?.timezone), + date: formatNumericDate(dateTime, getActivityTimeZone(venues, activity)), }; } }) diff --git a/src/hooks/queries/useWcaLive.ts b/src/hooks/queries/useWcaLive.ts index 66e9013..d274189 100644 --- a/src/hooks/queries/useWcaLive.ts +++ b/src/hooks/queries/useWcaLive.ts @@ -121,6 +121,8 @@ export interface WcaLiveRoundResult { person: { id: string; registrantId: number | null; + wcaUserId?: number | null; + wcaId?: string | null; name: string; }; } @@ -219,6 +221,8 @@ const WCA_LIVE_ROUND_RESULTS_QUERY = ` person { id registrantId + wcaUserId + wcaId name } } diff --git a/src/i18n/en/translation.yaml b/src/i18n/en/translation.yaml index 33b4f57..aae1be7 100644 --- a/src/i18n/en/translation.yaml +++ b/src/i18n/en/translation.yaml @@ -210,7 +210,7 @@ competition: unknownCompetitor: 'Competitor #{{personId}}' allResults: All results viewLiveResults: View live results - liveResultsDelayNote: Data is pulled from WCA Live and may be delayed. + resultsSourceNote: Results are merged from WCIF and WCA Live when available. WCA Live data may be delayed. personalResults: eventResults: Event results round: