Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions docs/AI_USAGE.md
Original file line number Diff line number Diff line change
Expand Up @@ -590,6 +590,14 @@
- 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.

## The size of the drawings (`src/components/elements/ElementDiagram.tsx`, `src/components/elements/ResultsTable.tsx`, `src/lib/imageUrl.ts`, `docs/SUSTAINABILITY.md`)

- **Tool:** Claude Code
- **What the team did:** Took the peer feedback that the drawings on the element page are shown far larger than their real size (issue #174), and asked for a check of every page that shows a drawing, with the numbers written down.
- **What Claude did:** Measured first, with a real browser at widths from 375 to 1920 px, and the files themselves (836 files, at most 400 px wide, 5.64 MB in total, 12.7 kB at most). The element page showed the drawing up to 1812 px wide. Then put a maximum width of 400 px on the two `<img>` elements that exist (the element page and the cards of the Elements page below 768 px), gave both a `width` and a `height`, added a test for the two attributes, and wrote the pages, sizes and bytes in `docs/SUSTAINABILITY.md`.
- **What was decided:** No `srcset` and no smaller copies: the files are already small and only one size exists. The `height` is a typical one (200), because the drawings differ in height.
- **Limits:** The numbers are from one machine and one element page, not from a repeated measurement. The height is not exact, so the page can still move a little when a drawing loads.

## A shorter README (`README.md`, `docs/`)

- **Tool:** Claude Code
Expand Down
17 changes: 17 additions & 0 deletions docs/SUSTAINABILITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,23 @@ Catalog drawings use WebP. Result-card images are lazy-loaded and decoded asynch

**Where:** image files in [data/images/](../data/images/); `<img loading="lazy">` in [ResultsTable.tsx](../src/components/elements/ResultsTable.tsx); HTTP cache headers in [app.ts](../backend/src/server/app.ts); the image endpoint is also described in [API.md](./API.md#images).

### Show the drawings at their real size

The drawings are at most **400 px wide** (652 of the 836 files are exactly 400; the height is between 71 and 499 px) and **5.64 MB in total** (6.7 kB on average, 12.7 kB at most; the 7.4 MB in the peer feedback is the size on disk, counted in blocks). Only two places show a drawing, and neither shows it larger than the file:

| Page | What shows a drawing | Shown size | Transferred |
| ------------------------------------------ | --------------------------------- | ----------------------------------------------------------------------------------- | ----------------------------------------------- |
| Element page | One drawing | At most 400 px wide (before: up to 1812 px on a 1920 px screen, 4.5 times the file) | One file, 6.3 kB in the example measured |
| Elements, below 768 px (cards) | One drawing per card, lazy-loaded | At most 400 px wide, and at most 224 px high | 20 files per page of results, about 27 to 54 kB |
| Elements, 768 px and wider (table) | No drawing | None | None |
| Variants list, Your gymnasts, Gymnast page | No drawing | None | None |

A smaller file per screen width (`srcset`) was considered and not done: the files are already small (6.7 kB on average), there is only one size of each drawing, and a smaller copy would save a few kB per page and would need new drawings. Both `<img>` elements have `width` and `height`, so the space is reserved before the file has loaded. The height is a typical one (200), because it differs between the drawings, so the page can move a little when a drawing is much taller or lower than that.

**Measured** with a real browser on the running app, at widths from 375 to 1920 px (the size shown, and the bytes of the files that were loaded).

**Where:** `DRAWING_WIDTH` and `DRAWING_HEIGHT` in [imageUrl.ts](../src/lib/imageUrl.ts); [ElementDiagram.tsx](../src/components/elements/ElementDiagram.tsx) and [ResultsTable.tsx](../src/components/elements/ResultsTable.tsx).

### Keep catalog data out of the frontend bundle

The catalog JSON is no longer imported into the frontend to build the element detail view; the page fetches the selected item from the GraphQL API instead. The project history records the JavaScript bundle changing from **665 kB to 522 kB** after this move. On the current version, `npm run build` reports a JavaScript bundle of **548.06 kB (170.79 kB gzip)**. These are build-output sizes, not per-visit measurements; the historical before/after comparison and current build can differ because the code and build tooling have changed.
Expand Down
6 changes: 4 additions & 2 deletions src/components/elements/ElementDiagram.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Card, CardContent } from '@/components/ui/card'
import { displayCode, type ElementDetail } from '@/lib/catalog'
import { imageUrlForApp } from '@/lib/imageUrl'
import { DRAWING_HEIGHT, DRAWING_WIDTH, imageUrlForApp } from '@/lib/imageUrl'

export default function ElementDiagram({
element,
Expand All @@ -10,11 +10,13 @@ export default function ElementDiagram({
return (
<div className="m-0">
{/* The drawings are black lines, so they keep a white ground in both themes. */}
<Card className="w-full bg-white p-0">
<Card className="w-full max-w-[400px] bg-white p-0">
<CardContent className="p-0">
<img
src={imageUrlForApp(element.imageUrl)}
alt={`Illustration for ${displayCode(element, element.variants.length > 0)}${element.name ? `: ${element.name}` : ''}`}
width={DRAWING_WIDTH}
height={DRAWING_HEIGHT}
className="block h-auto max-h-[75svh] w-full object-contain"
/>
</CardContent>
Expand Down
6 changes: 4 additions & 2 deletions src/components/elements/ResultsTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { Link } from 'react-router-dom'
import DifficultyBadge from '@/components/ui/DifficultyBadge'
import { getDifficultyLetter } from '@/lib/difficulty'
import { groupLabel, type ElementListRow } from '@/lib/elementList'
import { imageUrlForApp } from '@/lib/imageUrl'
import { DRAWING_HEIGHT, DRAWING_WIDTH, imageUrlForApp } from '@/lib/imageUrl'

// On a wide screen the results are a table. On a phone and a small screen (below
// the "md" breakpoint) they are cards below each other instead, because four
Expand Down Expand Up @@ -85,12 +85,14 @@ function ResultCards({ rows }: { rows: ElementListRow[] }) {
<img
src={imageUrlForApp(row.imageUrl)}
alt={`Illustration for ${row.code}${row.name ? `: ${row.name}` : ''}`}
width={DRAWING_WIDTH}
height={DRAWING_HEIGHT}
loading="lazy"
decoding="async"
onError={(event) => {
event.currentTarget.hidden = true
}}
className="mt-4 block h-auto max-h-56 w-full rounded-md bg-white object-contain"
className="mt-4 block h-auto max-h-56 w-full max-w-[400px] rounded-md bg-white object-contain"
/>
</li>
))}
Expand Down
10 changes: 10 additions & 0 deletions src/lib/imageUrl.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,15 @@
const IMAGE_PATH = '/images/'

/**
* The drawings are at most 400 px wide (652 of the 836 files are exactly 400; measured
* on all of them) and between 71 and 499 px high, 196 on average. Nothing shows them
* wider than 400 px, because a larger size is only a blurry enlargement. The height is a
* typical one: with the width it only reserves the space of the drawing before it has
* loaded, so the page does not jump.
*/
export const DRAWING_WIDTH = 400
export const DRAWING_HEIGHT = 200

/**
* Route backend image URLs through the frontend's base path. The backend URL
* still supplies the apparatus and filename, while the app base may differ
Expand Down
3 changes: 3 additions & 0 deletions src/tests/components/elements/ResultsTable.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -234,6 +234,9 @@ describe('ResultsTable: the cards (a phone and a small screen)', () => {
const image = /<img[^>]*>/.exec(plainCard)?.[0] ?? ''
expect(image).toContain(`src="${plain.imageUrl}"`)
expect(image).toContain('loading="lazy"')
// The size of the file, so the card does not grow when the drawing has loaded.
expect(image).toContain('width="400"')
expect(image).toContain('height="200"')
expect(image.split(/[ "]/)).not.toContain('hidden')
expect(plainCard).toContain(`alt="Illustration for ${plain.code}"`)
expect(namedCard).toContain(
Expand Down
3 changes: 3 additions & 0 deletions src/tests/pages/ElementDetailPage.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,9 @@ describe('ElementDetailPage', () => {
expect(screen.getByRole('img').getAttribute('src')).toBe(
'http://images.test/UB/UB-2.305-a.webp',
)
// The size of the file, so the page does not jump when the drawing has loaded.
expect(screen.getByRole('img').getAttribute('width')).toBe('400')
expect(screen.getByRole('img').getAttribute('height')).toBe('200')
expect(
screen.getByRole('link', { name: /2\.305-b/ }).getAttribute('href'),
).toBe('/elements/UB-2.305-b')
Expand Down