From 474e75ff663908522b3c8dccb29c801a60737987 Mon Sep 17 00:00:00 2001 From: Cailyn Sinclair Date: Sun, 15 Mar 2026 13:10:29 -0700 Subject: [PATCH 1/2] fix: show stages for single-room competitions --- .../PersonalSchedule/Assignments.tsx | 4 +- src/containers/Schedule/Schedule.tsx | 9 ++- src/lib/activities.test.ts | 81 +++++++++++++++++++ src/lib/activities.ts | 10 +++ 4 files changed, 100 insertions(+), 4 deletions(-) create mode 100644 src/lib/activities.test.ts diff --git a/src/containers/PersonalSchedule/Assignments.tsx b/src/containers/PersonalSchedule/Assignments.tsx index da559d5..63df975 100644 --- a/src/containers/PersonalSchedule/Assignments.tsx +++ b/src/containers/PersonalSchedule/Assignments.tsx @@ -6,7 +6,7 @@ import { useNow } from '@/hooks/useNow/useNow'; import { parseActivityCodeFlexible } from '@/lib/activityCodes'; import { isActivityWithRoomOrParent } from '@/lib/typeguards'; import { byDate, roundTime } from '@/lib/utils'; -import { getRoomData, getRooms } from '../../lib/activities'; +import { getRoomData, hasMultipleScheduleLocations } from '../../lib/activities'; import { ExtraAssignment } from './PersonalExtraAssignment'; import { PersonalNormalAssignment } from './PersonalNormalAssignment'; import { getGroupedAssignmentsByDate } from './utils'; @@ -21,7 +21,7 @@ const key = (compId: string, id) => `${compId}-${id}`; export function Assignments({ wcif, person, showStationNumber }: AssignmentsProps) { const { t } = useTranslation(); - const showRoom = useMemo(() => wcif && getRooms(wcif).length > 1, [wcif]); + const showRoom = useMemo(() => hasMultipleScheduleLocations(wcif), [wcif]); const { collapsedDates, setCollapsedDates, toggleDate } = useCollapse( key(wcif.id, person.registrantId), diff --git a/src/containers/Schedule/Schedule.tsx b/src/containers/Schedule/Schedule.tsx index 2c37f38..542f95c 100644 --- a/src/containers/Schedule/Schedule.tsx +++ b/src/containers/Schedule/Schedule.tsx @@ -2,7 +2,12 @@ import { Competition } from '@wca/helpers'; import { useCallback, useEffect, useMemo } from 'react'; import { ActivityRow } from '@/components'; import { useCollapse } from '@/hooks/UseCollapse'; -import { getRoomData, getRooms, getScheduledDays, getVenueForActivity } from '@/lib/activities'; +import { + getRoomData, + getScheduledDays, + getVenueForActivity, + hasMultipleScheduleLocations, +} from '@/lib/activities'; import { ActivityWithRoomOrParent } from '@/lib/types'; const key = (compId: string) => `${compId}-schedule`; @@ -85,7 +90,7 @@ export const ScheduleContainer = ({ wcif }: ScheduleContainerProps) => { // eslint-disable-next-line react-hooks/exhaustive-deps }, [scheduleDays]); - const showRoom = useMemo(() => wcif && getRooms(wcif).length > 1, [wcif]); + const showRoom = useMemo(() => hasMultipleScheduleLocations(wcif), [wcif]); return (
diff --git a/src/lib/activities.test.ts b/src/lib/activities.test.ts new file mode 100644 index 0000000..48810f2 --- /dev/null +++ b/src/lib/activities.test.ts @@ -0,0 +1,81 @@ +import { Competition } from '@wca/helpers'; +import { hasMultipleScheduleLocations } from './activities'; + +const baseCompetition = { + formatVersion: '1.0', + id: 'TestComp2026', + name: 'Test Comp 2026', + shortName: 'Test Comp', + events: [], + persons: [], + competitorLimit: 0, + extensions: [], +} as const; + +describe('hasMultipleScheduleLocations', () => { + it('returns false for a single room without stage metadata', () => { + const wcif = { + ...baseCompetition, + schedule: { + numberOfDays: 1, + startDate: '2026-03-15', + venues: [ + { + id: 1, + name: 'Venue', + timezone: 'America/Los_Angeles', + rooms: [ + { + id: 10, + name: 'Main Room', + color: '#123456', + activities: [], + extensions: [], + }, + ], + }, + ], + }, + } as unknown as Competition; + + expect(hasMultipleScheduleLocations(wcif)).toBe(false); + }); + + it('returns true for a single room with multiple stages in the natshelper extension', () => { + const wcif = { + ...baseCompetition, + schedule: { + numberOfDays: 1, + startDate: '2026-03-15', + venues: [ + { + id: 1, + name: 'Venue', + timezone: 'America/Los_Angeles', + rooms: [ + { + id: 10, + name: 'Main Room', + color: '#123456', + activities: [], + extensions: [ + { + id: 'org.cubingusa.natshelper.v1.Room', + data: { + stages: [ + { id: 1, name: 'Stage A', color: '#ff0000' }, + { id: 2, name: 'Stage B', color: '#00ff00' }, + ], + }, + }, + ], + }, + ], + }, + ], + }, + } as unknown as Competition; + + expect(hasMultipleScheduleLocations(wcif)).toBe(true); + }); +}); diff --git a/src/lib/activities.ts b/src/lib/activities.ts index 8fb068b..f172122 100644 --- a/src/lib/activities.ts +++ b/src/lib/activities.ts @@ -31,6 +31,16 @@ export const getRooms = ( })), ); +export const hasMultipleScheduleLocations = (wcif: Competition): boolean => { + const rooms = getRooms(wcif); + + if (rooms.length > 1) { + return true; + } + + return rooms.some((room) => (getNatsHelperRoomExtension(room)?.stages?.length || 0) > 1); +}; + /** * Returns the activity's child activities with a reference to the parent activity */ From da7c41a13ebb58a793a15152f759399bd820181c Mon Sep 17 00:00:00 2001 From: Cailyn Sinclair Date: Sat, 4 Apr 2026 12:06:43 -0700 Subject: [PATCH 2/2] nits: design, added login card --- .codex | 0 AGENTS.md | 115 +----------------- .../CompetitionList/CompetitionList.tsx | 4 +- .../CompetitionSelect/CompetitionSelect.tsx | 1 + .../LoggedOutPromptCard.test.tsx | 38 ++++++ .../LoggedOutPromptCard.tsx | 27 ++++ src/components/LoggedOutPromptCard/index.ts | 1 + src/components/index.ts | 1 + src/i18n/en/translation.yaml | 6 +- src/layouts/RootLayout/Header.tsx | 6 +- src/pages/Home/index.tsx | 14 +-- 11 files changed, 87 insertions(+), 126 deletions(-) create mode 100644 .codex create mode 100644 src/components/LoggedOutPromptCard/LoggedOutPromptCard.test.tsx create mode 100644 src/components/LoggedOutPromptCard/LoggedOutPromptCard.tsx create mode 100644 src/components/LoggedOutPromptCard/index.ts diff --git a/.codex b/.codex new file mode 100644 index 0000000..e69de29 diff --git a/AGENTS.md b/AGENTS.md index 49e96f9..c70d3dd 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,113 +1,2 @@ -Instruction file for the AI agent working on the WCA competition groups web application. - -## Project Overview - -This is a React + TypeScript web application for viewing WCA (World Cube Association) competition groups digitally. The project uses Vite for development/build, Apollo Client for GraphQL, React Query for data fetching, and TailwindCSS/Styled Components for styling. The AI agent working on this project should understand the modular, component-driven structure and adhere to the repository’s coding and testing practices. - ---- - -## Code Layout - -- `src/` — Main source code. - - `pages/` — Route-level components. - - `components/` — Reusable React components. - - `containers/` — Higher-level components that manage state and logic. These use components and are used in pages. - - `hooks/` — Custom React hooks. - - `providers/` — Context providers for global state management. - - `lib/` — Utility functions and helpers. - - `lib/api.ts` — Data fetching and API abstraction layers. -- `public/` — Static assets. -- `package.json` — Project scripts and dependencies. -- `vite.config.ts` — Vite configuration. - ---- - -## Setup & Dependencies - -- **Node.js version:** Use the version compatible with Yarn 1.22+ and the dependencies in `package.json`. -- **Install dependencies:** - ```bash - yarn - ``` -- **No special environment variables** are required for development or testing by default. - ---- - -## Building & Running - -- **Development server:** - ```bash - yarn dev - ``` -- **Production build:** - ```bash - yarn build - ``` -- **Preview production build:** - ```bash - yarn serve - ``` - ---- - -## Testing - -- **Run all tests:** - ```bash - yarn test - ``` -- **Testing libraries:** Jest and React Testing Library. -- **Before committing, always run the tests** and ensure **all tests pass**. The AI agent must run the full test suite after changes. -- All new features and bug fixes should include or update relevant tests. - ---- - -## Linting & Formatting - -- **Lint code:** - ```bash - yarn lint - ``` -- **Type-check code:** - ```bash - yarn check:type - ``` -- **Formatting:** Prettier is used for code formatting. ESLint is used for linting. Import sorting is handled by Prettier plugins. -- The AI agent should fix any lint or type errors it introduces. (CI will fail if errors are present.) - ---- - -## Coding Conventions - -- **TypeScript:** All code must be type-safe. Use/extend types in `src/types` as needed. -- **Styling:** Use TailwindCSS for utility-first styles; use Styled Components for component-scoped styles. -- **State Management:** Use React Query for server state and Apollo Client for GraphQL APIs. -- **Data Fetching:** Abstract API logic into `/lib/api.ts`. -- **Routing:** Use React Router v6. -- **Testing:** All new logic must have corresponding Jest/RTL tests. -- **Internationalization:** Use `i18next` and `react-i18next` for translations. -- **Documentation:** Update `README.md` and add comments where necessary. -- **Function and variable names:** Should be clear and descriptive. -- **Components:** Prefer functional components and hooks. - ---- - -## Commit & PR Guidelines - -- **Commits:** Use clear, descriptive commit messages. Conventional Commits format is preferred (`feat: ...`, `fix: ...`, `refactor: ...`). -- **Pull Requests:** Include a summary of changes and reasoning. Reference issues if applicable (e.g., “Closes #123”). -- **CI:** Tests and linting run on every PR. Ensure all checks pass before finalizing. - ---- - -## Additional Instructions - -- **Do not modify** files in `public/` unless the task explicitly requires it. -- **Do not update dependencies** in `package.json` without approval. -- **New libraries:** Prefer existing dependencies or standard approaches first. -- **New files:** The agent can create new files for features or tests, but all new code should be covered by tests. -- **Comments:** Add comments to explain complex logic; maintainability is valued. - ---- - -If unsure, review the `README.md`, existing code, or add clarifying comments in your PR. +Use space-y instead of mt-2 for better spacing between elements. +Always use spacing in multiples of 2 unless you need to use odd spacing for a specific reason. This helps maintain visual consistency across the app. diff --git a/src/components/CompetitionList/CompetitionList.tsx b/src/components/CompetitionList/CompetitionList.tsx index ff7ed46..ab277b8 100644 --- a/src/components/CompetitionList/CompetitionList.tsx +++ b/src/components/CompetitionList/CompetitionList.tsx @@ -30,8 +30,8 @@ export function CompetitionListFragment({ } return ( -
- {title} +
+ {title} {loading ? :
} {!!competitions.length && (
    diff --git a/src/components/CompetitionSelect/CompetitionSelect.tsx b/src/components/CompetitionSelect/CompetitionSelect.tsx index 074431a..2d55d30 100644 --- a/src/components/CompetitionSelect/CompetitionSelect.tsx +++ b/src/components/CompetitionSelect/CompetitionSelect.tsx @@ -45,6 +45,7 @@ export const CompetitionSelect = ({ onSelect, className }: CompetitionSelectProp 'bg-panel', // Borders + focus 'border border-tertiary-weak', + 'px-1.5', state.isFocused ? 'ring-1 ring-blue-500 dark:ring-blue-400 border-blue-500 dark:border-blue-400' : '', diff --git a/src/components/LoggedOutPromptCard/LoggedOutPromptCard.test.tsx b/src/components/LoggedOutPromptCard/LoggedOutPromptCard.test.tsx new file mode 100644 index 0000000..13ad727 --- /dev/null +++ b/src/components/LoggedOutPromptCard/LoggedOutPromptCard.test.tsx @@ -0,0 +1,38 @@ +import '@testing-library/jest-dom'; +import { render, screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import { useTranslation } from 'react-i18next'; +import { LoggedOutPromptCard } from './LoggedOutPromptCard'; + +jest.mock('react-i18next', () => ({ + useTranslation: jest.fn(), +})); + +describe('LoggedOutPromptCard', () => { + it('renders the login prompt and triggers the login callback', async () => { + const user = userEvent.setup(); + const onLogin = jest.fn(); + + const messages: Record = { + 'home.loggedOutCard.eyebrow': 'Personalized view', + 'home.loggedOutCard.title': 'Log in to see your competitions', + 'home.loggedOutCard.description': + 'Sign in with your WCA account to load your competition list and personalized schedule shortcuts.', + 'common.login': 'Login', + }; + + jest.mocked(useTranslation).mockReturnValue({ + t: (key: string) => messages[key] ?? key, + i18n: {} as never, + ready: true, + } as unknown as ReturnType); + + render(); + + expect(screen.getByRole('heading', { name: /log in to see your competitions/i })).toBeVisible(); + + await user.click(screen.getByRole('button', { name: /login/i })); + + expect(onLogin).toHaveBeenCalledTimes(1); + }); +}); diff --git a/src/components/LoggedOutPromptCard/LoggedOutPromptCard.tsx b/src/components/LoggedOutPromptCard/LoggedOutPromptCard.tsx new file mode 100644 index 0000000..0989235 --- /dev/null +++ b/src/components/LoggedOutPromptCard/LoggedOutPromptCard.tsx @@ -0,0 +1,27 @@ +import { useTranslation } from 'react-i18next'; +import { Button } from '@/components/Button'; + +interface LoggedOutPromptCardProps { + onLogin: () => void; +} + +export function LoggedOutPromptCard({ onLogin }: LoggedOutPromptCardProps) { + const { t } = useTranslation(); + + return ( +
    +
    +
    +

    {t('home.loggedOutCard.title')}

    +

    {t('home.loggedOutCard.description')}

    +
    +
    +