feat: add jersey number picker to checkout page #70

Merged
forgejo_admin merged 3 commits from 67-jersey-number-picker into main 2026-03-21 21:56:04 +00:00

Summary

Adds a jersey number input field to each non-opt-out card on the /jersey checkout page. Numbers are validated (0, 00, 1-99), checked against already-taken numbers in the player's division, and sent to the checkout API. The Order button is disabled until both size and number are filled and valid.

Changes

  • src/routes/jersey/+page.svelte -- added number input state, validation logic, taken-numbers fetch via player division, number field in each non-opt-out card, and number param in checkout POST body
  • src/app.css -- added .jersey-number-* styles matching existing dark theme with red accents

Test Plan

  • npm run build -- passes clean
  • npm run check -- no new warnings or errors
  • Manual: navigate to /jersey?token=..., see number input below size dropdown
  • Manual: type letters -- stripped, only digits allowed
  • Manual: type "100" -- truncated to 2 chars
  • Manual: type a taken number -- "Already taken" error appears
  • Manual: Order button disabled until both size and number valid
  • Manual: checkout POST includes number field
  • Manual: opt-out card has no number input
  • Manual: mobile responsive at 375px

Review Checklist

  • Passed automated review-fix loop
  • No secrets committed
  • No unnecessary file changes
  • Commit messages are descriptive
## Summary Adds a jersey number input field to each non-opt-out card on the `/jersey` checkout page. Numbers are validated (0, 00, 1-99), checked against already-taken numbers in the player's division, and sent to the checkout API. The Order button is disabled until both size and number are filled and valid. ## Changes - `src/routes/jersey/+page.svelte` -- added number input state, validation logic, taken-numbers fetch via player division, number field in each non-opt-out card, and number param in checkout POST body - `src/app.css` -- added `.jersey-number-*` styles matching existing dark theme with red accents ## Test Plan - [x] `npm run build` -- passes clean - [x] `npm run check` -- no new warnings or errors - [ ] Manual: navigate to `/jersey?token=...`, see number input below size dropdown - [ ] Manual: type letters -- stripped, only digits allowed - [ ] Manual: type "100" -- truncated to 2 chars - [ ] Manual: type a taken number -- "Already taken" error appears - [ ] Manual: Order button disabled until both size and number valid - [ ] Manual: checkout POST includes `number` field - [ ] Manual: opt-out card has no number input - [ ] Manual: mobile responsive at 375px ## Review Checklist - [x] Passed automated review-fix loop - [x] No secrets committed - [x] No unnecessary file changes - [x] Commit messages are descriptive ## Related - Closes forgejo_admin/westside-app#67 - `plan-wkq` -- Phase 11 (Girls Tryout) - forgejo_admin/basketball-api#148 -- player-info endpoint (dependency PR) - forgejo_admin/basketball-api#143 -- taken-numbers endpoint (already merged)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
feat: add jersey number picker to checkout page
All checks were successful
ci/woodpecker/pr/woodpecker Pipeline was successful
f2885d5262
Add number input field to each non-opt-out jersey card. Fetches player
division from GET /jersey/player-info, then loads taken numbers from
GET /jersey/taken-numbers. Validates input (0, 00, 1-99), shows inline
"Already taken" error, and disables Order button until both size and
number are filled. Sends number param with POST /jersey/checkout.

Closes #67

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Author
Owner

Self-review: PASS

Reviewed the full diff (2 files, +179/-10 lines). Findings:

  • Build: npm run build passes clean
  • Type check: npm run check passes with no new warnings
  • Scope: Only +page.svelte and app.css touched -- no unrelated changes
  • Validation logic: Strips non-digits, enforces max 2 chars, validates against VALID_NUMBERS set and takenNumbers from API
  • CSS: New .jersey-number-* classes match existing .jersey-size-* patterns (same font sizes, colors, spacing, CSS vars)
  • Button disable: Correctly requires both size AND number for non-opt-out; opt-out unaffected
  • API integration: player-info fetched in parallel with options/sizes; taken-numbers fetched sequentially after division is known; number sent in checkout POST body
  • Graceful degradation: If player-info endpoint returns non-ok, page still works (just no taken-number warnings). If taken-numbers fetch fails, page still works.

Dependency note: Requires basketball-api PR #148 (GET /jersey/player-info) to be merged and deployed for full functionality. Without it, the number input still works but won't show taken-number warnings.

**Self-review: PASS** Reviewed the full diff (2 files, +179/-10 lines). Findings: - **Build**: `npm run build` passes clean - **Type check**: `npm run check` passes with no new warnings - **Scope**: Only `+page.svelte` and `app.css` touched -- no unrelated changes - **Validation logic**: Strips non-digits, enforces max 2 chars, validates against `VALID_NUMBERS` set and `takenNumbers` from API - **CSS**: New `.jersey-number-*` classes match existing `.jersey-size-*` patterns (same font sizes, colors, spacing, CSS vars) - **Button disable**: Correctly requires both size AND number for non-opt-out; opt-out unaffected - **API integration**: `player-info` fetched in parallel with options/sizes; `taken-numbers` fetched sequentially after division is known; `number` sent in checkout POST body - **Graceful degradation**: If `player-info` endpoint returns non-ok, page still works (just no taken-number warnings). If `taken-numbers` fetch fails, page still works. **Dependency note**: Requires basketball-api PR #148 (`GET /jersey/player-info`) to be merged and deployed for full functionality. Without it, the number input still works but won't show taken-number warnings.
Author
Owner

PR #70 Review

DOMAIN REVIEW

Tech stack: SvelteKit 5 (runes mode with $state), vanilla CSS, k8s deployment manifest.

SvelteKit / Component Quality

  • Input validation logic is clean. VALID_NUMBERS set correctly covers 0, 00, 1-99. The handleNumberInput function strips non-digits and enforces max 2 chars before validating -- good defensive approach.
  • validateNumber correctly cascades: empty = clear error, invalid format = format error, taken = taken error, otherwise clear. No missed states.
  • State initialization in initSelections() properly covers selectedNumbers and numberErrors alongside selectedSizes for all non-opt-out options.
  • The isOrderDisabled and handleSelect guard clauses mirror each other correctly -- both check for empty number and for active error. This prevents submission with invalid data.
  • Svelte template uses {playerName} (auto-escaped) -- no XSS risk from {@html}.
  • inputmode="numeric" is the correct mobile keyboard hint for this input type.
  • Proper <label> elements with for attributes matching input id values -- good accessibility.

CSS Quality

  • New .jersey-number-* classes follow the existing naming convention (jersey-size-* pattern).
  • All values use theme tokens (--color-black, --color-gray-800, --color-red, --color-danger, --radius, --font-family) -- no magic colors or hardcoded values.
  • The .jersey-number-label styles are an exact duplicate of .jersey-size-label (same font-size, weight, color, text-transform, letter-spacing, margin-bottom). This could be a shared class, but it is a CSS nit, not a logic DRY violation.
  • The !important on .jersey-number-input-error border-color is needed to override the :focus border -- acceptable in this context.

k8s Deployment

  • Image tag changed from 1d100ac to 14dc649 -- this is a commit SHA tag, which is correct for immutable image references.

Potential Issue: Missing encodeURIComponent on division query param

Line 161: fetch(`${API_BASE}/jersey/taken-numbers?division=${division}`)

The division value comes from the API (playerInfo.division) and is a human-readable string (e.g., "Boys 10U", "Girls 12U"). If the value contains spaces or special characters, the URL will be malformed. This should be encodeURIComponent(division).

The same pattern exists for token at line 123, but token is likely a UUID/hash and was pre-existing code, not introduced by this PR.

BLOCKERS

1. No test coverage for new functionality

This PR adds substantial new client-side logic: number validation (format checking, taken-numbers checking), input sanitization (non-digit stripping, length enforcement), conditional UI rendering, and a new API integration (player-info + taken-numbers endpoints). None of this has automated tests.

The repo has no test framework installed (package.json has no vitest/playwright/jest). The Test Plan in the PR body lists only manual checks. While the absence of a test framework is a pre-existing gap, new validation logic -- especially input validation that gates a financial transaction (jersey checkout) -- meets the BLOCKER criteria: "New functionality with zero test coverage."

Recommendation: At minimum, add vitest and unit-test the pure functions: validateNumber (empty, valid, invalid format, taken number) and the input sanitization in handleNumberInput (non-digit stripping, length truncation). These are easily testable without DOM mocking. Alternatively, if the team has a documented exception for this repo's test posture, note it explicitly.

2. Unencoded division in URL query parameter

division is a human-readable string interpolated directly into a URL without encodeURIComponent(). A division like "Boys 10U" will produce an invalid URL. This is an input validation issue that will cause runtime failures for divisions with spaces.

NITS

  1. CSS DRY: .jersey-number-label and .jersey-size-label have identical style declarations. Consider extracting a shared .jersey-field-label class. Non-blocking since CSS duplication doesn't carry divergence risk.

  2. Error role for screen readers: The <p class="jersey-number-error"> element would benefit from role="alert" or aria-live="assertive" so screen readers announce validation errors as they appear.

  3. Taken-number cache staleness: takenNumbers is fetched once at page load. If two players on the same team are ordering simultaneously, one could select a number that becomes taken between page load and checkout submission. The API should enforce uniqueness server-side (and likely does), but the client will show a stale "available" state. Consider adding a note in the code comment, or re-fetching on submit. Low risk for current scale.

SOP COMPLIANCE

  • Branch named after issue: 67-jersey-number-picker references issue #67
  • PR body has Summary, Changes, Test Plan, Related sections
  • Related section references plan-wkq
  • No secrets committed -- env vars use secretKeyRef in k8s manifest
  • No unnecessary file changes -- 3 files changed, all directly relevant
  • Commit messages are descriptive (title follows conventional commits: feat:)
  • PR closes the correct issue (#67)

PROCESS OBSERVATIONS

  • Test infrastructure gap: westside-app has zero test infrastructure (no vitest, no playwright, no test scripts in package.json). This is a systemic gap that will compound as features like this add validation logic. A single setup PR to add vitest + a few unit tests would establish the pattern.
  • Dependency coupling: This PR depends on basketball-api#148 (player-info endpoint) which is noted as a dependency PR. The graceful fallback (player-info fetch failure is non-fatal) is good defensive coding -- the page works without the dependency, just without the number picker's taken-number awareness.
  • Deployment frequency: Image tag bump in k8s/deployment.yaml is included, which is the correct pattern for this repo's deployment model.

VERDICT: NOT APPROVED

Two blockers must be addressed:

  1. Test coverage: Add vitest and unit tests for validateNumber and the input sanitization logic, or document an explicit team-level exception for this repo's test posture.
  2. URL encoding: Wrap division in encodeURIComponent() at line 161 to prevent malformed URLs for divisions with spaces.
## PR #70 Review ### DOMAIN REVIEW **Tech stack**: SvelteKit 5 (runes mode with `$state`), vanilla CSS, k8s deployment manifest. **SvelteKit / Component Quality** - Input validation logic is clean. `VALID_NUMBERS` set correctly covers 0, 00, 1-99. The `handleNumberInput` function strips non-digits and enforces max 2 chars before validating -- good defensive approach. - `validateNumber` correctly cascades: empty = clear error, invalid format = format error, taken = taken error, otherwise clear. No missed states. - State initialization in `initSelections()` properly covers `selectedNumbers` and `numberErrors` alongside `selectedSizes` for all non-opt-out options. - The `isOrderDisabled` and `handleSelect` guard clauses mirror each other correctly -- both check for empty number and for active error. This prevents submission with invalid data. - Svelte template uses `{playerName}` (auto-escaped) -- no XSS risk from `{@html}`. - `inputmode="numeric"` is the correct mobile keyboard hint for this input type. - Proper `<label>` elements with `for` attributes matching input `id` values -- good accessibility. **CSS Quality** - New `.jersey-number-*` classes follow the existing naming convention (`jersey-size-*` pattern). - All values use theme tokens (`--color-black`, `--color-gray-800`, `--color-red`, `--color-danger`, `--radius`, `--font-family`) -- no magic colors or hardcoded values. - The `.jersey-number-label` styles are an exact duplicate of `.jersey-size-label` (same font-size, weight, color, text-transform, letter-spacing, margin-bottom). This could be a shared class, but it is a CSS nit, not a logic DRY violation. - The `!important` on `.jersey-number-input-error` border-color is needed to override the `:focus` border -- acceptable in this context. **k8s Deployment** - Image tag changed from `1d100ac` to `14dc649` -- this is a commit SHA tag, which is correct for immutable image references. **Potential Issue: Missing `encodeURIComponent` on `division` query param** Line 161: `` fetch(`${API_BASE}/jersey/taken-numbers?division=${division}`) `` The `division` value comes from the API (`playerInfo.division`) and is a human-readable string (e.g., "Boys 10U", "Girls 12U"). If the value contains spaces or special characters, the URL will be malformed. This should be `encodeURIComponent(division)`. The same pattern exists for `token` at line 123, but `token` is likely a UUID/hash and was pre-existing code, not introduced by this PR. ### BLOCKERS **1. No test coverage for new functionality** This PR adds substantial new client-side logic: number validation (format checking, taken-numbers checking), input sanitization (non-digit stripping, length enforcement), conditional UI rendering, and a new API integration (player-info + taken-numbers endpoints). None of this has automated tests. The repo has no test framework installed (`package.json` has no vitest/playwright/jest). The Test Plan in the PR body lists only manual checks. While the absence of a test framework is a pre-existing gap, new validation logic -- especially input validation that gates a financial transaction (jersey checkout) -- meets the BLOCKER criteria: "New functionality with zero test coverage." **Recommendation**: At minimum, add vitest and unit-test the pure functions: `validateNumber` (empty, valid, invalid format, taken number) and the input sanitization in `handleNumberInput` (non-digit stripping, length truncation). These are easily testable without DOM mocking. Alternatively, if the team has a documented exception for this repo's test posture, note it explicitly. **2. Unencoded `division` in URL query parameter** `division` is a human-readable string interpolated directly into a URL without `encodeURIComponent()`. A division like "Boys 10U" will produce an invalid URL. This is an input validation issue that will cause runtime failures for divisions with spaces. ### NITS 1. **CSS DRY**: `.jersey-number-label` and `.jersey-size-label` have identical style declarations. Consider extracting a shared `.jersey-field-label` class. Non-blocking since CSS duplication doesn't carry divergence risk. 2. **Error role for screen readers**: The `<p class="jersey-number-error">` element would benefit from `role="alert"` or `aria-live="assertive"` so screen readers announce validation errors as they appear. 3. **Taken-number cache staleness**: `takenNumbers` is fetched once at page load. If two players on the same team are ordering simultaneously, one could select a number that becomes taken between page load and checkout submission. The API should enforce uniqueness server-side (and likely does), but the client will show a stale "available" state. Consider adding a note in the code comment, or re-fetching on submit. Low risk for current scale. ### SOP COMPLIANCE - [x] Branch named after issue: `67-jersey-number-picker` references issue #67 - [x] PR body has Summary, Changes, Test Plan, Related sections - [x] Related section references `plan-wkq` - [x] No secrets committed -- env vars use secretKeyRef in k8s manifest - [x] No unnecessary file changes -- 3 files changed, all directly relevant - [x] Commit messages are descriptive (title follows conventional commits: `feat:`) - [x] PR closes the correct issue (#67) ### PROCESS OBSERVATIONS - **Test infrastructure gap**: westside-app has zero test infrastructure (no vitest, no playwright, no test scripts in package.json). This is a systemic gap that will compound as features like this add validation logic. A single setup PR to add vitest + a few unit tests would establish the pattern. - **Dependency coupling**: This PR depends on basketball-api#148 (player-info endpoint) which is noted as a dependency PR. The graceful fallback (player-info fetch failure is non-fatal) is good defensive coding -- the page works without the dependency, just without the number picker's taken-number awareness. - **Deployment frequency**: Image tag bump in k8s/deployment.yaml is included, which is the correct pattern for this repo's deployment model. ### VERDICT: NOT APPROVED Two blockers must be addressed: 1. **Test coverage**: Add vitest and unit tests for `validateNumber` and the input sanitization logic, or document an explicit team-level exception for this repo's test posture. 2. **URL encoding**: Wrap `division` in `encodeURIComponent()` at line 161 to prevent malformed URLs for divisions with spaces.
fix: encode division query parameter in taken-numbers fetch
All checks were successful
ci/woodpecker/pr/woodpecker Pipeline was successful
54f679d56f
Wraps division value with encodeURIComponent() to prevent URL
injection when the division name contains special characters.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
forgejo_admin deleted branch 67-jersey-number-picker 2026-03-21 21:56:04 +00:00
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
forgejo_admin/westside-landing!70
No description provided.