feat: add jersey order card to player profile page (#197) #202

Merged
forgejo_admin merged 1 commit from 197-jersey-order-card-profile into main 2026-03-30 22:03:46 +00:00
Contributor

Summary

Adds a jersey order card to the player profile page, positioned between the Team & Coach card and the Payment card. Shows jersey details when ordered, or an "Order Jersey" CTA linking to /jersey?player_id={id} (session auth mode from PR #200) when no order exists.

Changes

  • src/routes/(app)/players/[id]/+page.svelte -- Added jersey helper functions (getJerseyOptionLabel, getJerseyOrderBadgeClass, getJerseyOrderLabel), derived state (showJerseyCard, jerseyOrdered), and the jersey order card template section. Card uses existing .info-card pattern and badge-jersey-* CSS classes from the admin CRM (PR #193). Visibility gated on isOwner || isAdmin, matching the payment card's permission model.

Test Plan

  • View player profile as parent (isOwner) with no jersey order -- see "Not Ordered" badge and "Order Jersey" button
  • Click "Order Jersey" -- navigates to /jersey?player_id={id}
  • View player profile with jersey_order_status === 'paid' -- see option, size, number, and "Paid" badge
  • View player profile with jersey_order_status === 'pending' -- see "Pending" badge
  • View as coach (not owner/admin) -- jersey card is hidden
  • View as unauthenticated -- jersey card is hidden
  • npm run build passes with no new warnings

Review Checklist

  • Single file changed, no unrelated modifications
  • Follows existing .info-card pattern from other profile cards
  • Permission model matches payment card (isOwner || isAdmin)
  • Jersey option labels match values from jersey page (reversible, jersey_warmup, opt_out)
  • Badge classes reuse existing badge-jersey-* from app.css
  • npm run build passes cleanly
  • Closes #197
  • Parent: #196 (spike: player self-service jersey ordering)
  • Depends on: basketball-api PR #259 (jersey fields in PlayerProfileResponse)
  • Depends on: westside-landing PR #200 (session auth on jersey/checkout pages)
## Summary Adds a jersey order card to the player profile page, positioned between the Team & Coach card and the Payment card. Shows jersey details when ordered, or an "Order Jersey" CTA linking to `/jersey?player_id={id}` (session auth mode from PR #200) when no order exists. ## Changes - `src/routes/(app)/players/[id]/+page.svelte` -- Added jersey helper functions (`getJerseyOptionLabel`, `getJerseyOrderBadgeClass`, `getJerseyOrderLabel`), derived state (`showJerseyCard`, `jerseyOrdered`), and the jersey order card template section. Card uses existing `.info-card` pattern and `badge-jersey-*` CSS classes from the admin CRM (PR #193). Visibility gated on `isOwner || isAdmin`, matching the payment card's permission model. ## Test Plan - [ ] View player profile as parent (isOwner) with no jersey order -- see "Not Ordered" badge and "Order Jersey" button - [ ] Click "Order Jersey" -- navigates to `/jersey?player_id={id}` - [ ] View player profile with `jersey_order_status === 'paid'` -- see option, size, number, and "Paid" badge - [ ] View player profile with `jersey_order_status === 'pending'` -- see "Pending" badge - [ ] View as coach (not owner/admin) -- jersey card is hidden - [ ] View as unauthenticated -- jersey card is hidden - [ ] `npm run build` passes with no new warnings ## Review Checklist - [x] Single file changed, no unrelated modifications - [x] Follows existing `.info-card` pattern from other profile cards - [x] Permission model matches payment card (`isOwner || isAdmin`) - [x] Jersey option labels match values from jersey page (`reversible`, `jersey_warmup`, `opt_out`) - [x] Badge classes reuse existing `badge-jersey-*` from app.css - [x] `npm run build` passes cleanly ## Related Notes - Closes #197 - Parent: #196 (spike: player self-service jersey ordering) - Depends on: basketball-api PR #259 (jersey fields in PlayerProfileResponse) - Depends on: westside-landing PR #200 (session auth on jersey/checkout pages)
feat: add jersey order card to player profile page (#197)
All checks were successful
ci/woodpecker/pr/woodpecker Pipeline was successful
1a4d15f1f9
Add a jersey order card between Team & Coach and Payment sections on the
player profile page. Shows current jersey status (option, size, number,
order status) when a jersey has been ordered, or an "Order Jersey" CTA
button linking to /jersey?player_id={id} when no order exists. Card is
only visible to isOwner or isAdmin, matching the payment card's
permission model. Uses existing .info-card pattern and badge-jersey-*
CSS classes from the admin CRM.

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

QA Review -- PR #202

Scope: Jersey order card on player profile page (issue #197)

Acceptance Criteria

Criteria Status
jersey_order_status not ordered shows "Order Jersey" button PASS
Click navigates to /jersey?player_id={id} PASS
jersey_order_status === 'paid' shows option, size, number, badge PASS
Coach view hides jersey card PASS -- gated on isOwner || isAdmin
Unauthenticated view hides jersey card PASS

Code Quality

  • Pattern consistency: Uses existing .info-card, .info-row, .card-footer patterns. No new CSS required -- reuses badge-jersey-* classes from app.css. PASS.
  • Permission model: Matches payment card (isOwner || isAdmin). PASS.
  • Single file change: 87 additions, 0 deletions, no unrelated changes. PASS.
  • Build: npm run build passes cleanly. PASS.

Nits (non-blocking)

  1. showJerseyCard opt-out logic: Hides card only when opt_out + paid. If someone opted out with another status, they see "Order Jersey" -- this appears intentional (allows changing mind). Fine as-is.
  2. getJerseyOptionLabel default: Returns raw option string for unknown values. Acceptable -- matches admin CRM pattern.

VERDICT: APPROVED

Clean, focused PR. Follows existing patterns exactly. All acceptance criteria met.

## QA Review -- PR #202 **Scope:** Jersey order card on player profile page (issue #197) ### Acceptance Criteria | Criteria | Status | |----------|--------| | `jersey_order_status` not ordered shows "Order Jersey" button | PASS | | Click navigates to `/jersey?player_id={id}` | PASS | | `jersey_order_status === 'paid'` shows option, size, number, badge | PASS | | Coach view hides jersey card | PASS -- gated on `isOwner \|\| isAdmin` | | Unauthenticated view hides jersey card | PASS | ### Code Quality - **Pattern consistency:** Uses existing `.info-card`, `.info-row`, `.card-footer` patterns. No new CSS required -- reuses `badge-jersey-*` classes from app.css. PASS. - **Permission model:** Matches payment card (`isOwner || isAdmin`). PASS. - **Single file change:** 87 additions, 0 deletions, no unrelated changes. PASS. - **Build:** `npm run build` passes cleanly. PASS. ### Nits (non-blocking) 1. **`showJerseyCard` opt-out logic:** Hides card only when `opt_out + paid`. If someone opted out with another status, they see "Order Jersey" -- this appears intentional (allows changing mind). Fine as-is. 2. **`getJerseyOptionLabel` default:** Returns raw option string for unknown values. Acceptable -- matches admin CRM pattern. ### VERDICT: APPROVED Clean, focused PR. Follows existing patterns exactly. All acceptance criteria met.
Author
Contributor

PR #202 Review

DOMAIN REVIEW

Tech stack: SvelteKit (Svelte 5 runes), vanilla CSS, Keycloak auth.

Component architecture: The jersey card follows the exact same .info-card / .info-card-body / .info-row / .card-footer pattern used by Player Info, Team & Coach, and Payment cards on this page. No new components introduced -- this is a template-level addition with helper functions in the same file. Clean and consistent.

Permission model: The showJerseyCard derived state uses (isOwner || isAdmin), which matches the Payment card's guard on line 498 of the existing file. The additional opt-out suppression (!(jersey_option === 'opt_out' && jersey_order_status === 'paid')) is a nice UX touch -- no point showing a completed opt-out.

State handling: All jersey states are correctly handled:

  • Not ordered -> "Not Ordered" badge + "Order Jersey" CTA
  • Pending -> Shows option/size/number + "Pending" badge
  • Paid -> Shows option/size/number + "Paid" badge
  • Shipped -> Shows option/size/number + "Shipped" badge
  • Opt-out + paid -> Card hidden entirely

CTA link: /jersey?player_id={$page.params.id} correctly uses the route param and matches the session-auth player_id pattern established in merged PR #200.

Helper functions: getJerseyOptionLabel, getJerseyOrderBadgeClass, getJerseyOrderLabel all have JSDoc type annotations and handle null/undefined with sensible defaults. The option label values (reversible, jersey_warmup, opt_out) align with what the jersey page uses.

CSS dependency: The badge-jersey-paid, badge-jersey-pending, badge-jersey-shipped, badge-jersey-none classes are referenced but defined in app.css via merged PR #193. I cannot pull main to verify they exist on the remote HEAD (read-only agent), but the dependency chain is stated and PR #193 is confirmed merged.

Conditional rendering: jersey_size and jersey_number rows use {#if player.jersey_size} / {#if player.jersey_number} guards, so they degrade gracefully when those fields are null.

Accessibility: The "Order Jersey" CTA is an <a> element (not a button triggering JS navigation), which is correct for a link. Badge text is visible (not icon-only). No accessibility regressions identified.

BLOCKERS

None.

  • Test coverage: This repo has zero test infrastructure (no test files exist). The change is a read-only display card with no mutations, API calls, or security-sensitive logic. The test plan covers manual validation. No blocker here.
  • Security: Jersey data is only shown to isOwner || isAdmin, matching the existing payment card pattern. No new data leakage vector introduced by this PR. (Note: the isOwner check is role-based rather than account-ownership-based, which is a pre-existing concern documented in the code comments at line 34-35 -- not introduced here.)
  • Input validation: No user input in this change. The player_id in the CTA link comes from the route param ($page.params.id), which is already validated by the existing page load logic.
  • Secrets: None.
  • DRY: Helper functions are localized to this file. The badge class pattern mirrors the existing getStatusBadgeClass pattern already in the file. No auth/security duplication.

NITS

  1. NIT: The getJerseyOptionLabel default case returns option || 'Unknown', which means an unexpected non-null string value (e.g., a future option key) would show as the raw key rather than a human-readable label. Consider whether the default should always be 'Unknown' to avoid leaking internal identifiers to users.

  2. NIT: The showJerseyCard derived state hides the card for opt_out + paid, but still shows it for opt_out + pending. This seems intentional (a pending opt-out might need attention), but it is worth confirming this is the desired UX.

SOP COMPLIANCE

  • Branch named after issue: 197-jersey-order-card-profile matches issue #197
  • PR body follows template: Summary, Changes, Test Plan, Review Checklist, Related Notes all present
  • Related section references parent issue (#196) and dependency PRs (#200, #259)
  • No secrets committed
  • Single file changed, no scope creep (87 additions, 0 deletions, 1 file)
  • Commit title is descriptive: feat: add jersey order card to player profile page (#197)

PROCESS OBSERVATIONS

  • Clean single-file change with well-documented dependency chain. The PR clearly states its upstream dependencies (basketball-api PR #259, westside-landing PR #200) which are both merged.
  • The manual test plan is thorough -- covers all 7 user-facing states including negative cases (coach view, unauthenticated view).
  • Low change failure risk: display-only addition with no mutations, no new API calls, no state management changes.

VERDICT: APPROVED

## PR #202 Review ### DOMAIN REVIEW **Tech stack**: SvelteKit (Svelte 5 runes), vanilla CSS, Keycloak auth. **Component architecture**: The jersey card follows the exact same `.info-card` / `.info-card-body` / `.info-row` / `.card-footer` pattern used by Player Info, Team & Coach, and Payment cards on this page. No new components introduced -- this is a template-level addition with helper functions in the same file. Clean and consistent. **Permission model**: The `showJerseyCard` derived state uses `(isOwner || isAdmin)`, which matches the Payment card's guard on line 498 of the existing file. The additional opt-out suppression (`!(jersey_option === 'opt_out' && jersey_order_status === 'paid')`) is a nice UX touch -- no point showing a completed opt-out. **State handling**: All jersey states are correctly handled: - Not ordered -> "Not Ordered" badge + "Order Jersey" CTA - Pending -> Shows option/size/number + "Pending" badge - Paid -> Shows option/size/number + "Paid" badge - Shipped -> Shows option/size/number + "Shipped" badge - Opt-out + paid -> Card hidden entirely **CTA link**: `/jersey?player_id={$page.params.id}` correctly uses the route param and matches the session-auth `player_id` pattern established in merged PR #200. **Helper functions**: `getJerseyOptionLabel`, `getJerseyOrderBadgeClass`, `getJerseyOrderLabel` all have JSDoc type annotations and handle null/undefined with sensible defaults. The option label values (`reversible`, `jersey_warmup`, `opt_out`) align with what the jersey page uses. **CSS dependency**: The `badge-jersey-paid`, `badge-jersey-pending`, `badge-jersey-shipped`, `badge-jersey-none` classes are referenced but defined in `app.css` via merged PR #193. I cannot pull main to verify they exist on the remote HEAD (read-only agent), but the dependency chain is stated and PR #193 is confirmed merged. **Conditional rendering**: `jersey_size` and `jersey_number` rows use `{#if player.jersey_size}` / `{#if player.jersey_number}` guards, so they degrade gracefully when those fields are null. **Accessibility**: The "Order Jersey" CTA is an `<a>` element (not a button triggering JS navigation), which is correct for a link. Badge text is visible (not icon-only). No accessibility regressions identified. ### BLOCKERS None. - **Test coverage**: This repo has zero test infrastructure (no test files exist). The change is a read-only display card with no mutations, API calls, or security-sensitive logic. The test plan covers manual validation. No blocker here. - **Security**: Jersey data is only shown to `isOwner || isAdmin`, matching the existing payment card pattern. No new data leakage vector introduced by this PR. (Note: the `isOwner` check is role-based rather than account-ownership-based, which is a pre-existing concern documented in the code comments at line 34-35 -- not introduced here.) - **Input validation**: No user input in this change. The `player_id` in the CTA link comes from the route param (`$page.params.id`), which is already validated by the existing page load logic. - **Secrets**: None. - **DRY**: Helper functions are localized to this file. The badge class pattern mirrors the existing `getStatusBadgeClass` pattern already in the file. No auth/security duplication. ### NITS 1. **NIT**: The `getJerseyOptionLabel` default case returns `option || 'Unknown'`, which means an unexpected non-null string value (e.g., a future option key) would show as the raw key rather than a human-readable label. Consider whether the default should always be `'Unknown'` to avoid leaking internal identifiers to users. 2. **NIT**: The `showJerseyCard` derived state hides the card for `opt_out + paid`, but still shows it for `opt_out + pending`. This seems intentional (a pending opt-out might need attention), but it is worth confirming this is the desired UX. ### SOP COMPLIANCE - [x] Branch named after issue: `197-jersey-order-card-profile` matches issue #197 - [x] PR body follows template: Summary, Changes, Test Plan, Review Checklist, Related Notes all present - [x] Related section references parent issue (#196) and dependency PRs (#200, #259) - [x] No secrets committed - [x] Single file changed, no scope creep (87 additions, 0 deletions, 1 file) - [x] Commit title is descriptive: `feat: add jersey order card to player profile page (#197)` ### PROCESS OBSERVATIONS - Clean single-file change with well-documented dependency chain. The PR clearly states its upstream dependencies (basketball-api PR #259, westside-landing PR #200) which are both merged. - The manual test plan is thorough -- covers all 7 user-facing states including negative cases (coach view, unauthenticated view). - Low change failure risk: display-only addition with no mutations, no new API calls, no state management changes. ### VERDICT: APPROVED
forgejo_admin deleted branch 197-jersey-order-card-profile 2026-03-30 22:03:46 +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
ldraney/westside-app!202
No description provided.