diff --git a/dev-dist/sw.js b/dev-dist/sw.js
index 7a0a0a8..957bc46 100644
--- a/dev-dist/sw.js
+++ b/dev-dist/sw.js
@@ -81,7 +81,7 @@ define(['./workbox-5357ef54'], function (workbox) {
[
{
url: 'index.html',
- revision: '0.3rdphr8mv9',
+ revision: '0.vjm3pcb1ifo',
},
],
{},
diff --git a/src/components/CompetitorListItem/CompetitorListItem.test.tsx b/src/components/CompetitorListItem/CompetitorListItem.test.tsx
new file mode 100644
index 0000000..80c1410
--- /dev/null
+++ b/src/components/CompetitorListItem/CompetitorListItem.test.tsx
@@ -0,0 +1,93 @@
+import '@testing-library/jest-dom';
+import { render, screen } from '@testing-library/react';
+import { Person } from '@wca/helpers';
+import { MemoryRouter } from 'react-router-dom';
+import { CompetitorListItem } from './CompetitorListItem';
+
+// Mock the translation hook
+jest.mock('react-i18next', () => ({
+ useTranslation: () => ({
+ t: (key: string) => key,
+ }),
+}));
+
+// Mock the AssignmentCodeCell component to avoid tailwind colors import issues
+jest.mock('@/components/AssignmentCodeCell', () => ({
+ AssignmentCodeCell: ({
+ children,
+ assignmentCode,
+ }: {
+ children?: React.ReactNode;
+ assignmentCode?: string;
+ }) =>
{children || assignmentCode}
,
+}));
+
+const mockPerson: Person = {
+ registrantId: 1,
+ name: 'John Doe',
+ wcaUserId: 123,
+ wcaId: '2023DOEJ01',
+ countryIso2: 'US',
+ gender: 'm',
+ birthdate: '1990-01-01',
+ email: 'john@example.com',
+ avatar: {
+ url: 'https://example.com/full.jpg',
+ thumbUrl: 'https://example.com/thumb.jpg',
+ },
+ roles: [],
+ assignments: [],
+ personalBests: [],
+ extensions: [],
+ registration: {
+ wcaRegistrationId: 1,
+ eventIds: ['333'],
+ status: 'accepted',
+ isCompeting: true,
+ },
+};
+
+function renderWithRouter(ui: React.ReactElement) {
+ return render(ui, {
+ wrapper: MemoryRouter,
+ });
+}
+
+describe('CompetitorListItem', () => {
+ it('renders basic competitor item', () => {
+ renderWithRouter();
+
+ expect(screen.getByText('John Doe')).toBeInTheDocument();
+ expect(screen.getByRole('link')).toHaveAttribute('href', expect.stringContaining('persons/1'));
+ });
+
+ it('renders highlighted competitor with avatar', () => {
+ renderWithRouter();
+
+ expect(screen.getByText('John Doe')).toBeInTheDocument();
+ expect(screen.getByText('competition.competitors.viewMyAssignments')).toBeInTheDocument();
+ expect(screen.getByRole('img')).toHaveAttribute('src', 'https://example.com/thumb.jpg');
+ });
+
+ it('renders bookmarked competitor', () => {
+ renderWithRouter();
+
+ expect(screen.getByText('John Doe')).toBeInTheDocument();
+ expect(screen.getByText('', { selector: '.fa-bookmark' })).toBeInTheDocument();
+ });
+
+ it('renders competitor with assignment', () => {
+ renderWithRouter();
+
+ expect(screen.getByText('John Doe')).toBeInTheDocument();
+ // The AssignmentCodeCell should be rendered but we won't test its internals
+ });
+
+ it('does not show bookmark when assignment is present', () => {
+ renderWithRouter(
+ ,
+ );
+
+ expect(screen.queryByText('', { selector: '.fa-bookmark' })).not.toBeInTheDocument();
+ });
+});
diff --git a/src/containers/Competitors/CompetitorListItem.tsx b/src/components/CompetitorListItem/CompetitorListItem.tsx
similarity index 100%
rename from src/containers/Competitors/CompetitorListItem.tsx
rename to src/components/CompetitorListItem/CompetitorListItem.tsx
diff --git a/src/components/CompetitorListItem/index.ts b/src/components/CompetitorListItem/index.ts
new file mode 100644
index 0000000..1462c53
--- /dev/null
+++ b/src/components/CompetitorListItem/index.ts
@@ -0,0 +1 @@
+export * from './CompetitorListItem';
diff --git a/src/components/DesktopGroupView/DesktopGroupView.tsx b/src/components/DesktopGroupView/DesktopGroupView.tsx
new file mode 100644
index 0000000..f328c0d
--- /dev/null
+++ b/src/components/DesktopGroupView/DesktopGroupView.tsx
@@ -0,0 +1,73 @@
+import { Room } from '@wca/helpers';
+import { Fragment } from 'react';
+import { Link } from 'react-router-dom';
+import { AssignmentCodeCell } from '@/components/AssignmentCodeCell';
+import { Container } from '@/components/Container';
+import { GroupAssignmentCodeRank } from '@/lib/constants';
+import { byName } from '@/lib/utils';
+import { ExtendedPerson } from '@/types/group.types';
+
+interface DesktopGroupViewProps {
+ rooms: Room[];
+ personsInActivity: ExtendedPerson[];
+ children?: React.ReactNode;
+}
+
+export const DesktopGroupView = ({ rooms, personsInActivity, children }: DesktopGroupViewProps) => {
+ return (
+
+ {children}
+
+ {rooms.map((stage) => (
+
+ {stage.name}
+
+ ))}
+ {GroupAssignmentCodeRank.filter((assignmentCode) =>
+ personsInActivity?.some((person) => person.assignment?.assignmentCode === assignmentCode),
+ ).map((assignmentCode) => {
+ const personsInActivityWithAssignment =
+ personsInActivity?.filter(
+ (person) => person.assignment?.assignmentCode === assignmentCode,
+ ) || [];
+ return (
+
+
+
+ {rooms.map((room) => (
+
+ {personsInActivityWithAssignment
+ ?.filter((person) => person.room?.id === room.id)
+ ?.sort(byName)
+ .map((person) => (
+
+ {person.name}
+
+ ))}
+
+ ))}
+
+ );
+ })}
+
+
+ );
+};
diff --git a/src/components/DesktopGroupView/index.ts b/src/components/DesktopGroupView/index.ts
new file mode 100644
index 0000000..8caf85f
--- /dev/null
+++ b/src/components/DesktopGroupView/index.ts
@@ -0,0 +1 @@
+export * from './DesktopGroupView';
diff --git a/src/components/GroupButtonMenu/GroupButtonMenu.test.tsx b/src/components/GroupButtonMenu/GroupButtonMenu.test.tsx
new file mode 100644
index 0000000..497afe4
--- /dev/null
+++ b/src/components/GroupButtonMenu/GroupButtonMenu.test.tsx
@@ -0,0 +1,108 @@
+import '@testing-library/jest-dom';
+import { render, screen } from '@testing-library/react';
+import { Competition } from '@wca/helpers';
+import { MemoryRouter } from 'react-router-dom';
+import { GroupButtonMenu } from './GroupButtonMenu';
+
+// Mock the translation hook
+jest.mock('react-i18next', () => ({
+ useTranslation: () => ({
+ t: (key: string) => key,
+ }),
+}));
+
+// Mock react-router-dom
+const mockNavigate = jest.fn();
+jest.mock('react-router-dom', () => ({
+ ...jest.requireActual('react-router-dom'),
+ useNavigate: () => mockNavigate,
+ useParams: () => ({ competitionId: 'TestComp2023' }),
+}));
+
+// Mock activity codes helper
+jest.mock('@/lib/activityCodes', () => ({
+ prevActivityCode: (_wcif: Competition, activityCode: string) => {
+ if (activityCode === '333-r1-g2') return '333-r1-g1';
+ return null;
+ },
+ nextActivityCode: (_wcif: Competition, activityCode: string) => {
+ if (activityCode === '333-r1-g2') return '333-r1-g3';
+ return null;
+ },
+}));
+
+const mockWcif: Competition = {
+ formatVersion: '1.0',
+ id: 'TestComp2023',
+ name: 'Test Competition 2023',
+ shortName: 'TestComp2023',
+ persons: [],
+ events: [],
+ schedule: {
+ startDate: '2023-01-01',
+ numberOfDays: 1,
+ venues: [],
+ },
+ competitorLimit: 100,
+ extensions: [],
+};
+
+function renderWithRouter(ui: React.ReactElement) {
+ return render(ui, {
+ wrapper: MemoryRouter,
+ });
+}
+
+describe('GroupButtonMenu', () => {
+ beforeEach(() => {
+ mockNavigate.mockClear();
+
+ // Clear event listeners
+ const _events: Record = {};
+ document.addEventListener = jest.fn((_event, _cb) => {
+ // No-op for tests
+ });
+ document.removeEventListener = jest.fn((_event) => {
+ // No-op for tests
+ });
+ });
+
+ it('renders navigation buttons', () => {
+ renderWithRouter();
+
+ expect(screen.getByText('competition.groups.previousGroup')).toBeInTheDocument();
+ expect(screen.getByText('competition.groups.nextGroup')).toBeInTheDocument();
+ });
+
+ it('enables both buttons when prev and next exist', () => {
+ renderWithRouter();
+
+ const prevButton = screen.getByRole('link', { name: /previousGroup/ });
+ const nextButton = screen.getByRole('link', { name: /nextGroup/ });
+
+ expect(prevButton).not.toHaveClass('pointer-events-none');
+ expect(nextButton).not.toHaveClass('pointer-events-none');
+ expect(prevButton).toHaveAttribute('href', '/competitions/TestComp2023/events/333-r1/1');
+ expect(nextButton).toHaveAttribute('href', '/competitions/TestComp2023/events/333-r1/3');
+ });
+
+ it('disables buttons when no prev/next available', () => {
+ renderWithRouter();
+
+ const prevButton = screen.getByRole('link', { name: /previousGroup/ });
+ const nextButton = screen.getByRole('link', { name: /nextGroup/ });
+
+ expect(prevButton).toHaveClass('pointer-events-none opacity-25');
+ expect(nextButton).toHaveClass('pointer-events-none opacity-25');
+ });
+
+ it('handles missing wcif gracefully', () => {
+ renderWithRouter();
+
+ const prevButton = screen.getByRole('link', { name: /previousGroup/ });
+ const nextButton = screen.getByRole('link', { name: /nextGroup/ });
+
+ expect(prevButton).toHaveClass('pointer-events-none opacity-25');
+ expect(nextButton).toHaveClass('pointer-events-none opacity-25');
+ });
+});
diff --git a/src/components/GroupButtonMenu/GroupButtonMenu.tsx b/src/components/GroupButtonMenu/GroupButtonMenu.tsx
new file mode 100644
index 0000000..fc39243
--- /dev/null
+++ b/src/components/GroupButtonMenu/GroupButtonMenu.tsx
@@ -0,0 +1,86 @@
+import { Competition } from '@wca/helpers';
+import classNames from 'classnames';
+import { useCallback, useEffect } from 'react';
+import { useTranslation } from 'react-i18next';
+import { Link, useNavigate, useParams } from 'react-router-dom';
+import { nextActivityCode, prevActivityCode } from '@/lib/activityCodes';
+
+interface GroupButtonMenuProps {
+ wcif?: Competition;
+ activityCode: string;
+}
+
+export const GroupButtonMenu = ({ wcif, activityCode }: GroupButtonMenuProps) => {
+ const { t } = useTranslation();
+ const navigate = useNavigate();
+ const { competitionId } = useParams();
+
+ const prev = wcif && prevActivityCode(wcif, activityCode);
+ const next = wcif && nextActivityCode(wcif, activityCode);
+
+ const prevUrl = `/competitions/${competitionId}/events/${prev?.split?.('-g')?.[0]}/${
+ prev?.split?.('-g')?.[1]
+ }`;
+ const nextUrl = `/competitions/${competitionId}/events/${next?.split?.('-g')?.[0]}/${
+ next?.split?.('-g')?.[1]
+ }`;
+
+ const goToPrev = useCallback(() => {
+ if (prev) {
+ navigate(prevUrl);
+ }
+ }, [prev, navigate, prevUrl]);
+
+ const goToNext = useCallback(() => {
+ if (next) {
+ navigate(nextUrl);
+ }
+ }, [next, navigate, nextUrl]);
+
+ useEffect(() => {
+ const handleKeydown = (event: KeyboardEvent) => {
+ if (event.key === 'ArrowLeft') {
+ goToPrev();
+ }
+
+ if (event.key === 'ArrowRight') {
+ goToNext();
+ }
+ };
+
+ document.addEventListener('keydown', handleKeydown);
+
+ return () => {
+ document.removeEventListener('keydown', handleKeydown);
+ };
+ }, [wcif, activityCode, goToPrev, goToNext]);
+
+ return (
+
+
+
+ {t('competition.groups.previousGroup')}
+
+
+ {t('competition.groups.nextGroup')}
+
+
+
+ );
+};
diff --git a/src/components/GroupButtonMenu/index.ts b/src/components/GroupButtonMenu/index.ts
new file mode 100644
index 0000000..b3f819e
--- /dev/null
+++ b/src/components/GroupButtonMenu/index.ts
@@ -0,0 +1 @@
+export * from './GroupButtonMenu';
diff --git a/src/components/GroupHeader/GroupHeader.tsx b/src/components/GroupHeader/GroupHeader.tsx
new file mode 100644
index 0000000..c524be8
--- /dev/null
+++ b/src/components/GroupHeader/GroupHeader.tsx
@@ -0,0 +1,75 @@
+import { Room, Round, Venue } from '@wca/helpers';
+import { Fragment } from 'react';
+import { ActivityRow } from '@/components';
+import { Breadcrumbs } from '@/components/Breadcrumbs/Breadcrumbs';
+import { CutoffTimeLimitPanel } from '@/components/CutoffTimeLimitPanel';
+import { hasActivities } from '@/lib/activities';
+import { activityCodeToName } from '@/lib/activityCodes';
+import { useWCIF } from '@/providers/WCIFProvider';
+
+interface GroupHeaderProps {
+ round?: Round;
+ activityCode: string;
+ rooms: (Room & { venue: Venue })[];
+ children?: React.ReactNode;
+}
+
+export const GroupHeader = ({ round, activityCode, rooms, children }: GroupHeaderProps) => {
+ const { competitionId } = useWCIF();
+
+ const activityName = activityCodeToName(activityCode);
+ const activityNameSplit = activityName.split(', ');
+
+ const roundName = activityNameSplit.slice(0, 2).join(', ');
+ const groupName = activityNameSplit ? activityNameSplit.slice(-1).join('') : undefined;
+
+ return (
+
+
+
+
+
+ {children}
+
+ {round && }
+
+
+ {rooms?.filter(hasActivities(activityCode)).map((room) => {
+ const activity = room.activities
+ .flatMap((ra) => ra.childActivities)
+ .find((a) => a.activityCode === activityCode);
+
+ if (!activity) {
+ return null;
+ }
+ const venue = room.venue;
+ const timeZone = venue.timezone;
+
+ return (
+
+ {/* {multistage && {stage.name}:
}
+
+ {activity && formatDateTimeRange(minStartTime, maxEndTime)}
+
*/}
+
+
+ );
+ })}
+
+
+ );
+};
diff --git a/src/components/GroupHeader/index.ts b/src/components/GroupHeader/index.ts
new file mode 100644
index 0000000..8484d3b
--- /dev/null
+++ b/src/components/GroupHeader/index.ts
@@ -0,0 +1 @@
+export * from './GroupHeader';
diff --git a/src/components/MobileGroupView/MobileGroupView.tsx b/src/components/MobileGroupView/MobileGroupView.tsx
new file mode 100644
index 0000000..a88565f
--- /dev/null
+++ b/src/components/MobileGroupView/MobileGroupView.tsx
@@ -0,0 +1,80 @@
+import { Competition } from '@wca/helpers';
+import { Fragment } from 'react';
+import { Link } from 'react-router-dom';
+import { AssignmentCodeCell } from '@/components/AssignmentCodeCell';
+import { Container } from '@/components/Container';
+import { GroupAssignmentCodeRank } from '@/lib/constants';
+import { byName } from '@/lib/utils';
+import { ExtendedPerson } from '@/types/group.types';
+
+interface MobileGroupViewProps {
+ wcif?: Competition;
+ personsInActivity: ExtendedPerson[];
+ multistage: boolean;
+ children?: React.ReactNode;
+}
+
+export const MobileGroupView = ({
+ wcif,
+ personsInActivity,
+ multistage,
+ children,
+}: MobileGroupViewProps) => {
+ return (
+
+ {children}
+
+ {GroupAssignmentCodeRank.filter((assignmentCode) =>
+ personsInActivity?.some((person) => person.assignment?.assignmentCode === assignmentCode),
+ ).map((assignmentCode) => {
+ const personsInActivityWithAssignment =
+ personsInActivity?.filter(
+ (person) => person.assignment?.assignmentCode === assignmentCode,
+ ) || [];
+
+ return (
+
+
+
+
+
+ {personsInActivityWithAssignment
+ .sort((a, b) => {
+ const stageSort = (a.stage?.name || '').localeCompare(b.stage?.name || '');
+ return stageSort !== 0 ? stageSort : byName(a, b);
+ })
+ ?.map((person) => (
+
+
{person.name}
+ {multistage && (
+
+ {person.stage && person.stage.name}
+
+ )}
+
+ ))}
+
+
+
+
+ );
+ })}
+
+
+ );
+};
diff --git a/src/components/MobileGroupView/index.ts b/src/components/MobileGroupView/index.ts
new file mode 100644
index 0000000..7ba28e1
--- /dev/null
+++ b/src/components/MobileGroupView/index.ts
@@ -0,0 +1 @@
+export * from './MobileGroupView';
diff --git a/src/components/UserRow/UserRow.test.tsx b/src/components/UserRow/UserRow.test.tsx
new file mode 100644
index 0000000..2b5b559
--- /dev/null
+++ b/src/components/UserRow/UserRow.test.tsx
@@ -0,0 +1,57 @@
+import '@testing-library/jest-dom';
+import { render, screen } from '@testing-library/react';
+import { UserRow } from './UserRow';
+
+const mockUser: User = {
+ id: 1,
+ name: 'John Doe',
+ email: 'john@example.com',
+ wca_id: '2023DOEJ01',
+ avatar: {
+ thumb_url: 'https://example.com/avatar.jpg',
+ },
+ delegate_status: 'none',
+};
+
+const mockUserNoWcaId: User = {
+ id: 2,
+ name: 'Jane Smith',
+ email: 'jane@example.com',
+ wca_id: '',
+ delegate_status: 'none',
+};
+
+describe('UserRow', () => {
+ it('renders user with WCA ID correctly', () => {
+ render();
+
+ expect(screen.getByText('John Doe')).toBeInTheDocument();
+ expect(screen.getByText('2023DOEJ01')).toBeInTheDocument();
+ expect(screen.getByRole('img', { name: 'John Doe' })).toHaveAttribute(
+ 'src',
+ 'https://example.com/avatar.jpg',
+ );
+ expect(screen.getByRole('link')).toHaveAttribute(
+ 'href',
+ 'https://www.worldcubeassociation.org/persons/2023DOEJ01',
+ );
+ expect(screen.getByRole('link')).toHaveAttribute('target', '_blank');
+ });
+
+ it('renders user without WCA ID correctly', () => {
+ render();
+
+ expect(screen.getByText('Jane Smith')).toBeInTheDocument();
+ expect(screen.getByRole('link')).toHaveAttribute('href', '');
+ expect(
+ screen.queryByRole('button', { name: /fa-arrow-up-right-from-square/ }),
+ ).not.toBeInTheDocument();
+ });
+
+ it('renders with missing avatar', () => {
+ const userWithoutAvatar = { ...mockUser, avatar: undefined };
+ render();
+
+ expect(screen.getByRole('img')).not.toHaveAttribute('src');
+ });
+});
diff --git a/src/pages/Competition/Information/UserRow.tsx b/src/components/UserRow/UserRow.tsx
similarity index 90%
rename from src/pages/Competition/Information/UserRow.tsx
rename to src/components/UserRow/UserRow.tsx
index 9942b9b..96ae2d7 100644
--- a/src/pages/Competition/Information/UserRow.tsx
+++ b/src/components/UserRow/UserRow.tsx
@@ -1,4 +1,8 @@
-export const UserRow = ({ user }: { user: User }) => {
+interface UserRowProps {
+ user: User;
+}
+
+export const UserRow = ({ user }: UserRowProps) => {
const avatarUrl = user.avatar?.thumb_url;
return (
diff --git a/src/components/UserRow/index.ts b/src/components/UserRow/index.ts
new file mode 100644
index 0000000..dcd43a0
--- /dev/null
+++ b/src/components/UserRow/index.ts
@@ -0,0 +1 @@
+export * from './UserRow';
diff --git a/src/components/index.ts b/src/components/index.ts
index 87e3260..642cf8c 100644
--- a/src/components/index.ts
+++ b/src/components/index.ts
@@ -6,12 +6,18 @@ export * from './Button';
export * from './CompetitionList';
export * from './CompetitionListItem';
export * from './CompetitionSelect';
+export * from './CompetitorListItem';
export * from './Container';
export * from './CutoffTimeLimitPanel';
+export * from './DesktopGroupView';
export * from './DisclaimerText';
export * from './ErrorFallback';
export * from './ExternalLink';
+export * from './GroupButtonMenu';
+export * from './GroupHeader';
export * from './LastFetchedAt';
export * from './LinkButton';
+export * from './MobileGroupView';
export * from './Notebox';
export * from './PinCompetitionButton';
+export * from './UserRow';
diff --git a/src/containers/Competitors/Competitors.tsx b/src/containers/Competitors/Competitors.tsx
index 627f100..8fecc6f 100644
--- a/src/containers/Competitors/Competitors.tsx
+++ b/src/containers/Competitors/Competitors.tsx
@@ -2,12 +2,12 @@ import { Competition, Person } from '@wca/helpers';
import { useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useParams } from 'react-router-dom';
+import { CompetitorListItem } from '@/components';
import { usePinnedPersons } from '@/hooks/UsePinnedPersons';
import { useOngoingActivities } from '@/hooks/useOngoingActivities';
import { acceptedRegistration } from '@/lib/person';
import { byName } from '@/lib/utils';
import { useAuth } from '@/providers/AuthProvider';
-import { CompetitorListItem } from './CompetitorListItem';
export const Competitors = ({ wcif }: { wcif: Competition }) => {
const { t } = useTranslation();
diff --git a/src/pages/Competition/ByGroup/Group.tsx b/src/pages/Competition/ByGroup/Group.tsx
index e1e2556..22b9893 100644
--- a/src/pages/Competition/ByGroup/Group.tsx
+++ b/src/pages/Competition/ByGroup/Group.tsx
@@ -1,27 +1,12 @@
import { ActivityCode } from '@wca/helpers';
-import classNames from 'classnames';
-import { Fragment, useCallback, useEffect } from 'react';
-import { useTranslation } from 'react-i18next';
-import { Link, useNavigate, useParams } from 'react-router-dom';
-import { ActivityRow } from '@/components';
-import { AssignmentCodeCell } from '@/components/AssignmentCodeCell';
-import { Breadcrumbs } from '@/components/Breadcrumbs/Breadcrumbs';
-import { Container } from '@/components/Container';
-import { CutoffTimeLimitPanel } from '@/components/CutoffTimeLimitPanel';
-import { getAllRoundActivities, getRoomData, getRooms, hasActivities } from '@/lib/activities';
-import {
- activityCodeToName,
- matchesActivityCode,
- nextActivityCode,
- prevActivityCode,
- toRoundAttemptId,
-} from '@/lib/activityCodes';
-import { GroupAssignmentCodeRank } from '@/lib/constants';
+import { useParams } from 'react-router-dom';
+import { DesktopGroupView, GroupButtonMenu, GroupHeader, MobileGroupView } from '@/components';
+import { getAllRoundActivities, getRoomData, getRooms } from '@/lib/activities';
+import { matchesActivityCode, toRoundAttemptId } from '@/lib/activityCodes';
import { getAllEvents } from '@/lib/events';
-import { byName } from '@/lib/utils';
import { useWCIF } from '@/providers/WCIFProvider';
-const useCommon = () => {
+export const useCommon = () => {
const { wcif } = useWCIF();
const { roundId, groupNumber } = useParams();
const activityCode = `${roundId}-g${groupNumber}` as ActivityCode;
@@ -88,270 +73,25 @@ const useCommon = () => {
};
export default function Group() {
- return (
- <>
-
-
- >
- );
-}
-
-export const GroupHeader = () => {
- const { competitionId } = useWCIF();
- const { round, activityCode, rooms } = useCommon();
-
- const activityName = activityCodeToName(activityCode);
- const activityNameSplit = activityName.split(', ');
-
- const roundName = activityNameSplit.slice(0, 2).join(', ');
- const groupName = activityNameSplit ? activityNameSplit.slice(-1).join('') : undefined;
-
- return (
-
-
-
-
-
-
-
- {round && }
-
-
- {rooms?.filter(hasActivities(activityCode)).map((room) => {
- const activity = room.activities
- .flatMap((ra) => ra.childActivities)
- .find((a) => a.activityCode === activityCode);
-
- if (!activity) {
- return null;
- }
- const venue = room.venue;
- const timeZone = venue.timezone;
-
- return (
-
- {/* {multistage && {stage.name}:
}
-
- {activity && formatDateTimeRange(minStartTime, maxEndTime)}
-
*/}
-
-
- );
- })}
-
-
- );
-};
-
-export const MobileGroupView = () => {
- const { wcif, personsInActivity, multistage } = useCommon();
-
- return (
-
-
-
- {GroupAssignmentCodeRank.filter((assignmentCode) =>
- personsInActivity?.some((person) => person.assignment?.assignmentCode === assignmentCode),
- ).map((assignmentCode) => {
- const personsInActivityWithAssignment =
- personsInActivity?.filter(
- (person) => person.assignment?.assignmentCode === assignmentCode,
- ) || [];
-
- return (
-
-
-
-
-
- {personsInActivityWithAssignment
- .sort((a, b) => {
- const stageSort = (a.stage?.name || '').localeCompare(b.stage?.name || '');
- return stageSort !== 0 ? stageSort : byName(a, b);
- })
- ?.map((person) => (
-
-
{person.name}
- {multistage && (
-
- {person.stage && person.stage.name}
-
- )}
-
- ))}
-
-
-
-
- );
- })}
-
-
- );
-};
-
-const DesktopGroupView = () => {
- const { rooms, personsInActivity } = useCommon();
-
- return (
-
-
-
- {rooms.map((stage) => (
-
- {stage.name}
-
- ))}
- {GroupAssignmentCodeRank.filter((assignmentCode) =>
- personsInActivity?.some((person) => person.assignment?.assignmentCode === assignmentCode),
- ).map((assignmentCode) => {
- const personsInActivityWithAssignment =
- personsInActivity?.filter(
- (person) => person.assignment?.assignmentCode === assignmentCode,
- ) || [];
- return (
-
-
+ const { wcif, personsInActivity, multistage, round, activityCode, rooms } = useCommon();
- {rooms.map((room) => (
-
- {personsInActivityWithAssignment
- ?.filter((person) => person.room?.id === room.id)
- ?.sort(byName)
- .map((person) => (
-
- {person.name}
-
- ))}
-
- ))}
-
- );
- })}
-
-
+ const groupHeader = (
+
+
+
);
-};
-
-export const GroupButtonMenu = () => {
- const { t } = useTranslation();
- const navigate = useNavigate();
- const { competitionId } = useParams();
- const { wcif, activityCode } = useCommon();
-
- const prev = wcif && prevActivityCode(wcif, activityCode);
- const next = wcif && nextActivityCode(wcif, activityCode);
-
- const prevUrl = `/competitions/${competitionId}/events/${prev?.split?.('-g')?.[0]}/${
- prev?.split?.('-g')?.[1]
- }`;
- const nextUrl = `/competitions/${competitionId}/events/${next?.split?.('-g')?.[0]}/${
- next?.split?.('-g')?.[1]
- }`;
-
- const goToPrev = useCallback(() => {
- if (prev) {
- navigate(prevUrl);
- }
- }, [prev, navigate, prevUrl]);
-
- const goToNext = useCallback(() => {
- if (next) {
- navigate(nextUrl);
- }
- }, [next, navigate, nextUrl]);
-
- useEffect(() => {
- const handleKeydown = (event: KeyboardEvent) => {
- if (event.key === 'ArrowLeft') {
- goToPrev();
- }
-
- if (event.key === 'ArrowRight') {
- goToNext();
- }
- };
-
- document.addEventListener('keydown', handleKeydown);
-
- return () => {
- document.removeEventListener('keydown', handleKeydown);
- };
- }, [wcif, activityCode, goToPrev, goToNext]);
return (
-
-
-
- {t('competition.groups.previousGroup')}
-
-
- {t('competition.groups.nextGroup')}
-
-
-
+ <>
+
+ {groupHeader}
+
+
+ {groupHeader}
+
+ >
);
-};
+}
diff --git a/src/pages/Competition/Information/index.tsx b/src/pages/Competition/Information/index.tsx
index ee9b44b..a7e561d 100644
--- a/src/pages/Competition/Information/index.tsx
+++ b/src/pages/Competition/Information/index.tsx
@@ -1,9 +1,8 @@
import { useEffect } from 'react';
import { useParams } from 'react-router-dom';
-import { Container, ExternalLink } from '@/components';
+import { Container, ExternalLink, UserRow } from '@/components';
import { useCompetition } from '@/hooks/queries/useCompetition';
import { useWCIF } from '@/providers/WCIFProvider';
-import { UserRow } from './UserRow';
export default function Information() {
const { setTitle, wcif } = useWCIF();
diff --git a/src/types/group.types.ts b/src/types/group.types.ts
new file mode 100644
index 0000000..216e533
--- /dev/null
+++ b/src/types/group.types.ts
@@ -0,0 +1,9 @@
+import { Assignment, Competition, Person, Room, Venue } from '@wca/helpers';
+
+export interface ExtendedPerson extends Person {
+ assignment?: Assignment;
+ activity?: any;
+ room?: Room & { venue: Venue };
+ stage?: { name: string; color: string };
+ wcif?: Competition;
+}