diff --git a/docs/AI_USAGE.md b/docs/AI_USAGE.md index 05eb3fe..3e7719c 100644 --- a/docs/AI_USAGE.md +++ b/docs/AI_USAGE.md @@ -533,6 +533,14 @@ - **What the team did:** Wrote issue #97: move the pages out of `src/components/`, so it is clear which files are routes and which are parts of pages, and describe the folders in the README. - **What Claude did:** Moved the four pages and their tests with `git mv` (so their history follows them), and moved the three placeholder pages out of `App.tsx` into their own files, so every route has a file in `src/pages/`. Updated the imports, the links in the README and the docs, and the "Project structure" table. Found that `src/styles/gymnastTheme.test.ts` reads the files of the gymnasts and login pages by folder, so after the move it would have stopped checking the two pages without failing; added them to the list. The shared components in `components/ui/` were left where they are (see the issue). After that, asked to move the frontend tests to `src/tests/` as well: moved all 29 with `git mv` into the same folders as the code they test, changed their relative imports to the `@/` alias (so a test does not depend on where it is), and updated the paths that `gymnastTheme.test.ts` reads files from. +## The About page (`src/pages/AboutPage.tsx`, `src/components/about/`, `src/lib/codeOfPoints.ts`) + +- **Tool:** Claude Code +- **What the team did:** Wrote issue #146 and decided what the page is for: an introduction for people who do not know gymnastics, the PDF it comes from with a link, a clear statement that the Code of Points is not ours, the mission (the knowledge of the PDF for search and for calculating scores), and that the site is made especially for trainers, which is why there is a login. Decided that the page must be very simple, work in light and dark mode, and have no buttons of its own. +- **What Claude did:** Found the official PDF on gymnastics.sport and checked that the link answers with a PDF (23.8 MB). Read the front pages of the PDF to check what the page says: that FIG owns the Code, that the final score is the D-score plus the E-score, and that the D-score has difficulty, composition requirements and connection value. Wrote the tests first (the link, the rights text, the glossary, the trainers and login text, the limits of the calculator, headings in order, no buttons), then the page as plain text with links, using only the theme colors. Checked it with axe in both themes and at 320 px. +- **What was found:** The PDF says "Translation and copying are prohibited without prior written approval by FIG". The page says that we do not own the Code of Points, but whether the team may show the elements and drawings is a question for the team and the course staff, and it is not answered here. +- **What the team changed afterwards:** The team asked for changes in the text and for information that Claude had missed in the first version: that the site is especially for trainers and why it has a login, the full mapping from the letters A to J to their numbers, the difference between a routine and a vault and why a vault has no letter, and that the calculator does not support bonuses or subtracting the E-score yet. The team also asked for a very simple page without buttons, in light and dark mode, and checked what Claude wrote against the PDF. + ## The go-to-top link on every page (`src/components/goToTop/GoToTop.tsx`, `src/App.tsx`) - **Tool:** Claude Code @@ -580,4 +588,4 @@ - The result did not make the code shorter: the files under `src/components/gymnasts/` have more lines than before, because of the new files and their comments. What changed is that each decision now has one place. - Changed outside the Gymnast page: the element page (colors, the badge, "Try in a routine", "Add to gymnast"), "Your gymnasts" (the wrapping of the title and the list), `variables.css` (`--card-hover`), and two shared parts that other pages use too: `Button` (the `icon` prop, which changes nothing when it is not given) and `DifficultyBadge` (its colors now follow the theme, also in the results of the Elements page). - Not done on purpose: moving the error messages of the other pages (the elements results, the element page, the login, "Your gymnasts") to the three layers, because those files belong to other pull requests and the change would conflict with them. The menus of the Elements page still have their own copies of the menu classes that `menuStyles.ts` now holds. -- **Limits:** The end-to-end tests need the database and the backend, and are run by hand: the course git server has no runners. A screen reader was not used: that has to be done by a person. Lighthouse was started on a production build in the last round, but the run did not finish (the computer was low on memory), so there are no Lighthouse scores from that round. "Your gymnasts" was fixed after the last check in a browser and has not been measured again, and the results table of the Elements page, which uses the changed badge, has not been looked at in dark mode. The test gymnast was made with the test login in the local database and deleted again. \ No newline at end of file +- **Limits:** The end-to-end tests need the database and the backend, and are run by hand: the course git server has no runners. A screen reader was not used: that has to be done by a person. Lighthouse was started on a production build in the last round, but the run did not finish (the computer was low on memory), so there are no Lighthouse scores from that round. "Your gymnasts" was fixed after the last check in a browser and has not been measured again, and the results table of the Elements page, which uses the changed badge, has not been looked at in dark mode. The test gymnast was made with the test login in the local database and deleted again. diff --git a/src/components/about/AboutSection.tsx b/src/components/about/AboutSection.tsx new file mode 100644 index 0000000..69a918f --- /dev/null +++ b/src/components/about/AboutSection.tsx @@ -0,0 +1,22 @@ +import { useId, type ReactNode } from 'react' + +/** + * One part of the About page: a heading (h2) and plain text under it. The `
` is named + * by its heading, so a screen reader can list the parts and jump between them. + */ +export default function AboutSection({ + title, + children, +}: { + title: string + children: ReactNode +}) { + const headingId = useId() + + return ( +
+

{title}

+ {children} +
+ ) +} diff --git a/src/lib/codeOfPoints.ts b/src/lib/codeOfPoints.ts new file mode 100644 index 0000000..f51ffd5 --- /dev/null +++ b/src/lib/codeOfPoints.ts @@ -0,0 +1,10 @@ +// Where the Code of Points that the catalog comes from can be found. The links were checked on +// 2026-11-06: the file answered 200 with `application/pdf` (23.8 MB), and the page of the +// rules lists it as "1.1 - WAG COP 2025-2028". + +/** The official PDF of FIG: Women's Artistic Gymnastics Code of Points 2025-2028 (English). */ +export const COP_PDF_URL = + 'https://www.gymnastics.sport/publicdir/rules/files/en_1.1%20-%20WAG%20COP%202025-2028.pdf' + +/** The page of FIG with all the rules documents, for the day the file is moved or renamed. */ +export const COP_RULES_URL = 'https://www.gymnastics.sport/rules/' diff --git a/src/pages/AboutPage.tsx b/src/pages/AboutPage.tsx index 6085613..7a08e98 100644 --- a/src/pages/AboutPage.tsx +++ b/src/pages/AboutPage.tsx @@ -1,11 +1,139 @@ +import { Link } from 'react-router-dom' +import AboutSection from '@/components/about/AboutSection' import PageTitle from '@/components/PageTitle' +import { COP_PDF_URL, COP_RULES_URL } from '@/lib/codeOfPoints' -// A placeholder until the page is built. It has its own
, like the real pages. +// A link in the text, underlined so it is not told by its color alone (WCAG 1.4.1). The colors +// are the theme colors, so the page reads in light and in dark mode. +const LINK = + 'font-medium underline underline-offset-4 hover:no-underline focus-visible:rounded-sm focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-(--text-color)' + +/** The words of the site, in the order a newcomer meets them. */ +const GLOSSARY: { term: string; description: string }[] = [ + { + term: 'Apparatus', + description: + 'One of the four events: vault, uneven bars, balance beam and floor exercise.', + }, + { + term: 'Routine', + description: + 'The whole exercise a gymnast performs on one apparatus. On uneven bars, balance beam and floor it is a series of elements. On vault it is one vault (some competitions require two).', + }, + { + term: 'Element', + description: + 'One skill in a routine, such as a turn or a salto. Each has a code, like 2.305, and a drawing. The difficulty is a letter with a fixed value: A = 0.1, B = 0.2, C = 0.3, D = 0.4, E = 0.5, F = 0.6, G = 0.7, H = 0.8, I = 0.9, J = 1.0. The code tells the difficulty: in 2.305 the 3 after the dot means C.', + }, + { + term: 'Vault', + description: + 'A vault has no letter. A letter (0.1 to 1.0) is one part of a D-score, which on the other apparatus is built from eight elements. A vault stands for the whole exercise: its value, between 1.6 and 6.4, is the D-score. So one vault is worth about as much as a whole routine of eight elements, and a routine on vault can be just one vault.', + }, + { + term: 'D-score', + description: + 'The difficulty score. It is made of the values of the eight highest elements, plus composition requirements, connection values and bonuses. On vault it is just the value of the vault.', + }, + { + term: 'E-score', + description: + 'The execution score. It starts at 10.0, and the judges subtract points for faults. We do not support subtracting the E-score yet: the calculator only gives the D-score.', + }, + { + term: 'Final score', + description: 'The D-score plus the E-score, minus any penalties.', + }, + { + term: 'Code of Points', + description: + 'The rulebook of FIG, the international gymnastics federation. It lists all elements and what they are worth. A new one is made every four years.', + }, +] + +/** A plain introduction to the site, to the sport and to the Code of Points the data comes from. */ export default function AboutPage() { return ( -
+
About -

About

+ +
+

About

+ + +

+ The knowledge about elements and their values is in a long PDF. Our + mission is to make it available for search and for calculating + scores, for trainers and their gymnasts. +

+
+ + +

+ A gymnast performs an exercise on each of four apparatus, and judges + score it. These are the words you will meet here. +

+
+ {GLOSSARY.map(({ term, description }) => ( +
+
{term}
+
{description}
+
+ ))} +
+
+ + +

+ All elements and values here come from the{' '} + + Women’s Artistic Gymnastics Code of Points 2025–2028 (PDF, about + 24 MB) + + , published by FIG. If the link stops working, see the{' '} + + rules page of FIG + + . +

+

+ We do not own the Code of Points. It is the property of the FIG, and + all rights belong to FIG. We use it for education in a student + project that is not affiliated with FIG. The catalog may not have + the latest changes, so the official document always decides. +

+
+ + +

+ The site is made especially for trainers. Anyone can{' '} + + search the elements + + . A trainer can{' '} + + log in + {' '} + to keep a profile for each gymnast, with routines and the elements + each can do. That is why there is a login. For now it is a test + login: anyone can log in as any coach, so do not enter real + information. +

+

+ Our{' '} + + calculator + {' '} + adds the eight highest elements of a routine, each counted once. It + does not include composition requirements, connection values or + bonuses (for example the dismount bonus), so it is not an official + score. +

+
+
) } diff --git a/src/styles/global.css b/src/styles/global.css index 0f46ff9..d49730c 100644 --- a/src/styles/global.css +++ b/src/styles/global.css @@ -9,10 +9,16 @@ border-inline: 0; } -/* The titles of the Elements, gymnasts, gymnast and login pages are the same: the size +/* The titles of the Elements, gymnasts, gymnast, About and login pages are the same: the size of h1 comes from index.css (56px, 36px on a narrow window) for all of them. */ #root - :is(.elements-page, .your-gymnasts-page, .gymnast-page, .login-page) + :is( + .elements-page, + .your-gymnasts-page, + .gymnast-page, + .about-page, + .login-page + ) :is(h1, h2) { color: var(--text-color); font-weight: 700; @@ -85,7 +91,7 @@ margin-bottom: 0.75rem; } -#root :is(.your-gymnasts-page, .gymnast-page) h2 { +#root :is(.your-gymnasts-page, .gymnast-page, .about-page) h2 { font-size: clamp(1.375rem, 4vw, 1.75rem); line-height: 1.2; margin: 0; diff --git a/src/tests/pages/AboutPage.test.tsx b/src/tests/pages/AboutPage.test.tsx new file mode 100644 index 0000000..3ddac85 --- /dev/null +++ b/src/tests/pages/AboutPage.test.tsx @@ -0,0 +1,114 @@ +// @vitest-environment jsdom +// The About page is static text. These tests check its structure and accessibility, and the +// parts that must not be lost in a rewrite of the wording: the login text and the limits of +// the calculator. + +import { cleanup, render, screen, within } from '@testing-library/react' +import { MemoryRouter } from 'react-router-dom' +import { afterEach, describe, expect, it } from 'vitest' +import AboutPage from '@/pages/AboutPage' + +afterEach(cleanup) + +const renderPage = () => + render( + + + , + ) + +/** The section with this heading. */ +const section = (name: string) => + screen + .getByRole('heading', { level: 2, name }) + .closest('section') as HTMLElement + +describe('AboutPage: the page', () => { + it('has a title (WCAG 2.4.2) and one h1', () => { + renderPage() + + expect(document.title).toBe('About – WAG CoP Planner') + expect(screen.getAllByRole('heading', { level: 1 })).toHaveLength(1) + expect(screen.getByRole('heading', { level: 1 }).textContent).toBe('About') + }) + + it('has one main landmark and the headings in order: h1, then h2 only', () => { + renderPage() + + expect(screen.getAllByRole('main')).toHaveLength(1) + const levels = screen + .getAllByRole('heading') + .map((heading) => Number(heading.tagName[1])) + expect(levels[0]).toBe(1) + expect(levels.slice(1).every((level) => level === 2)).toBe(true) + expect(levels.length).toBe(5) + }) + + it('has every section named by its heading, so a screen reader can list them', () => { + renderPage() + + for (const heading of screen.getAllByRole('heading', { level: 2 })) { + const region = heading.closest('section')! + expect(region.getAttribute('aria-labelledby')).toBe(heading.id) + } + }) +}) + +describe('AboutPage: trainers and the login', () => { + it('says that the site is made for trainers, and why there is a login', () => { + renderPage() + + const trainers = section('For trainers') + expect(trainers.textContent).toMatch(/log in/i) + expect(trainers.textContent).toMatch(/gymnast/i) + expect(trainers.textContent).toMatch(/routines/i) + }) + + it('is honest that the login is a test login that anyone can use as anyone', () => { + renderPage() + + expect(section('For trainers').textContent).toMatch( + /anyone can log in as any coach/i, + ) + }) + + it('has a link to the login page', () => { + renderPage() + + const link = within(section('For trainers')).getByRole('link', { + name: /^log in$/i, + }) + expect(link.getAttribute('href')).toBe('/login') + }) +}) + +describe('AboutPage: the calculator and the links', () => { + it('says what the calculator counts and what not, so nobody takes the number for an official score', () => { + renderPage() + + const text = section('For trainers').textContent ?? '' + expect(text).toMatch(/eight highest/i) + expect(text).toMatch(/composition requirements/i) + expect(text).toMatch(/connection values/i) + expect(text).toMatch(/not an official score/i) + }) + + it('has links to the search and the calculator', () => { + renderPage() + + expect( + screen + .getByRole('link', { name: /search the elements/i }) + .getAttribute('href'), + ).toBe('/elements') + expect( + screen.getByRole('link', { name: /calculator/i }).getAttribute('href'), + ).toBe('/calculator') + }) + + it('has no buttons: it is text with links', () => { + renderPage() + + expect(screen.queryAllByRole('button')).toHaveLength(0) + }) +})