Commit graph

20 commits

Author SHA1 Message Date
Chris Parsons
01dacbce27
Add Storybook stories for new components from staged changes (#321) 2026-04-23 22:09:25 -07:00
Chris Parsons
9ed0282fd0
New design (#309)
* Redesign home page with new layout and component system

- Two-column layout (My Leagues 2/3, Upcoming Events 1/3) with mobile stack
- LeagueRow: square avatar, gradient draft highlight, rank/points display, progress bar
- MyLeaguesCard, CreateLeagueCard with shared SectionCardHeader
- UpcomingEventsCard: vertical timeline with grouped multi-league events
- Shared gradient system: BracktGradients SVG defs, GradientIcon wrapper, brand.ts constants
- Button default variant updated to green→cyan gradient
- Navbar: plain nav links with gradient hover, support/admin icon buttons
- Accessibility fixes: semantic h2 headings, aria-label on LeagueAvatar and nav elements
- Storybook stories for all new components

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* Responsive league row layout and mobile polish

- League rows stack avatar+name on top, stats full-width below on mobile
- Stats spread to right side on sm+ screens with border separator on mobile
- Tighter padding on mobile (px-3/py-3), full padding on sm+
- Card headers and content use px-3 sm:px-6 to reduce mobile gutters
- Two-column home layout deferred to lg breakpoint (tablet gets stacked)
- Active leagues sorted by completion percentage descending
- Default rank 1 / 0 points for active leagues with no scoring events yet
- Fix ordinal bug for 11th/12th/13th; add aria-labels to rank change indicators
- Remove dead StatDivider className prop

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* Improve claude file.

* Add StandingsPreview card component with podium row styling

- New StandingsPreview component with gold/silver/bronze row tints for
  top 3, team avatar, and LeagueRow-style stat columns (Ranking + Points)
  with rank and 7-day point change indicators
- Fix GradientIcon in Storybook by adding BracktGradients decorator to
  preview.tsx (renamed from .ts to support JSX)
- Fix degenerate SVG gradient on horizontal strokes by switching
  BracktGradients to gradientUnits="userSpaceOnUse" with Lucide-space
  coordinates (0→24)
- Revert erroneous fill: url(#gradient) from GradientIcon; stroke-only
  fix was sufficient once gradientUnits was corrected

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* Update components on league homepage.

* Finish up league page styling.

* Work on standings page.

* Add story for RecentScoresCard

* Update Point Progression Chart.

* Sort point progression legend by ranking and add team links to standings rows

* Fix standings discrepancy on change.

* Create draft cell component.

* Update draft board page

* Draft room improvements.

* Update some draft room styling.

* Fix context menu missing.

* Move tab navigation and autodraft to header row, narrow sidebar

* Virtualize available participants list, memoize draft room props

Adds @tanstack/react-virtual to replace separate mobile/desktop lists
with a single unified virtual scroll loop. Also memoizes miniDraftGrid
and availableParticipantsSectionProps, and switches pick lookup from
Array.find to a Map for O(1) access.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* Update draft room UI.

* More draft room fixes.

* Draft room tweaks.

* Fix Rosters page.

* Queue Section fixes.

* Mobile Draft fixes.

* Fix draft board page.

* Create bracket look.

* Bracket work.

* Finish bracket page.

* Homepage initial styling

* homepage copy

* Add privacy policy. Fixes #88.

* how to play copy

* rules copy

* Fix brackets on homepage.

* Add footer to website.

* Glow on dots.

* Landing page copy.

* Fix sidebar.

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-23 13:14:55 -07:00
Chris Parsons
611e1ccf0a
Show tied ranks with T prefix across standings and Discord (#239)
* Show tied ranks with T prefix across standings and Discord notifications, fixes #197

- Add buildTiedRankChecker() utility to standings-display.ts; replaces
  four copies of inline rank-count logic spread across components,
  the league home route, and the Discord service
- Prefix shared ranks with "T" (e.g. "T3") in StandingsTable (league
  panel), StandingsTable (full standings), the league home preview, and
  Discord webhook notifications
- Add useMemo wrapping in StandingsTable components so tie detection
  does not recompute on every render
- Fix compareTeamsForRanking to round total points to hundredths before
  comparing, preventing floating-point noise from producing spurious
  non-ties in the standings

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* Fix discord test to expect escaped period in rank display

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-27 20:45:15 -07:00
Chris Parsons
e4285876ad Fix oxlint errors. 2026-03-22 20:41:44 -07:00
Chris Parsons
b672c50d78
Add sortable Sport and Participant columns to team breakdown table (#208)
- Sort by sport (secondary: participant) or participant name
- Extract SortableHeader component to reduce header button repetition
- Use toSorted() instead of slice().sort() (oxlint fix)
- Use localeCompare with sensitivity: 'base' for case-insensitive string sorts

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-22 20:30:17 -07:00
Chris Parsons
194f9087a3
Add sortable columns to draft picks table on manager page (#207)
* Add sortable columns to draft picks table on manager page

Pick # column sorts by pick number (asc by default), Points column
sorts by actual points then projected as tiebreaker (desc by default).
Clicking a column header toggles sort direction with visual indicators.

https://claude.ai/code/session_01XBnm7eKxerR7WjwrqqJPwe

* Fix code review issues in TeamScoreBreakdown sortable table

- Move SortIndicator out of render (avoids remount on every render)
- Extract sortPicks helper outside component
- Restore default sort: sport name then pick number (was lost in prior commit)
- Replace unicode arrow chars with Lucide ArrowUp/ArrowDown/ArrowUpDown icons
- Add aria-sort attributes to sortable column headers for accessibility
- Change w-[90px] to min-w-[90px] on Pick # column to avoid clipping
- Flatten nested div inside Points sort button

https://claude.ai/code/session_01XBnm7eKxerR7WjwrqqJPwe

* Fix pick sort to use pure pick number (matches tests)

The sport-grouped tiebreaker conflicted with the test contract for the
pick column. Tests expect ascending pick number = [1, 2, 3] regardless
of sport, so revert sortPicks to sort by pickNumber only for that column.

https://claude.ai/code/session_01XBnm7eKxerR7WjwrqqJPwe

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-03-22 19:20:01 -07:00
Chris Parsons
8c5389909d
Redesign standings page with sortable table, 7-day change, and chart repositioned (#205)
* Redesign standings page with sortable table, 7-day change, and chart repositioned

- Move point progression chart below the standings table
- Replace separate Points/Projected/Placement columns with:
  - Single stacked "actual / projected" Points column (shared PointsDisplay component)
  - "7-Day Change" column showing points gained + rank change over past 7 days
- Remove Placement breakdown column
- Sort ties alphabetically by team name (rank sort only)
- All columns are sortable via new reusable useSortableData hook
- Add sevenDayPointChange to TeamStandingWithChange type and model

https://claude.ai/code/session_01CuCKFVYbpsKSQoFDcTYfY7

* Code review fixes: module-level comparators, correct type on SortableHead, handle negative point change, remove redundant spread

- Move comparators object to module level (closes over nothing, no useMemo needed)
- Use SortConfig<StandingRow> on SortableHead instead of inline duplicate type
- SevenDayChange: handle negative pointChange with correct sign and color
- Remove redundant sevenDayPointChange explicit assignment (already in ...standing spread)

https://claude.ai/code/session_01CuCKFVYbpsKSQoFDcTYfY7

* Update StandingsTable tests to match redesigned component

- Remove showPlacementBreakdown prop (no longer exists on component)
- Replace Placement Breakdown test suite with 7-Day Change column tests
- Update header assertion: "Placements" → "7-Day Change"
- Add tests for positive/negative point change display and rank change indicators
- Remove accessibility test for placement title attributes

https://claude.ai/code/session_01CuCKFVYbpsKSQoFDcTYfY7

* Fix movement indicator arrow count assertion to exclude sort header arrows

queryAllByText(/↑|↓/) was matching the active sort column's ↑ indicator.
Narrow to /[↑↓]\d/ so only movement indicators (↑1, ↓1) are counted.

https://claude.ai/code/session_01CuCKFVYbpsKSQoFDcTYfY7

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-03-22 11:05:13 -07:00
Chris Parsons
4bffa40606
Fix oxlint warnings: no-shadow, consistent-function-scoping, no-non-null-assertion, and others (#196)
* Fix no-shadow and consistent-function-scoping lint violations

Resolves all 11 no-shadow and 16 consistent-function-scoping oxlint
warnings and promotes both rules to errors in .oxlintrc.json.

no-shadow: renamed Drizzle callback params (sports→s, matches→m,
seasons→s) to avoid shadowing outer imports; removed shadowed
destructures (eq, inArray) from where callbacks; renamed inner
template→bracketTemplate, prev→currentTimers, season→ss, name→teamName
(with name: teamName fix to preserve semantics).

consistent-function-scoping: moved formatDate, getRankBadge,
getMovementIndicator, getPositionBadge, getStatusBadge, toDateStr,
elo (×2), weightedPick, sortByMatchNumber (×2) to module scope;
moved formatTime (×2), isValidLeagueName, getDraftTimes,
makeSeasonQueues to file scope in test files.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* Fix no-non-null-assertion lint violations and promote to error

Eliminates all 208 no-non-null-assertion warnings across 38 files.
Promotes typescript/no-non-null-assertion from warn to error in
.oxlintrc.json.

Fix patterns applied:
- Map.get(key)! after .has() check → extract with get() + null guard
- Map.get(key)! on pre-populated count maps → ?? 0 default
- .set(id, map.get(id)! + 1) increment → ?? 0 before adding
- participant1Id!/participant2Id! on DB matches → ?? "" fallback
- array.find()! in tests → guard + throw or expect().toBeDefined()
- bracketTemplateCache.get(id)! → null guard extract
- Various nullable field accesses → optional chain or ?? default

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* Fix prefer-add-event-listener, no-unassigned-import, require-module-specifiers

Resolves all 9 remaining non-console lint warnings and promotes all
three rules to errors in .oxlintrc.json.

- prefer-add-event-listener: converted onchange/onclick/onload
  assignments to addEventListener in useDraftNotifications.ts and
  admin.data-sync.tsx; stored changeHandler ref for proper cleanup
  with removeEventListener
- no-unassigned-import: configured rule with allow list for legitimate
  side-effect imports (*.css, @testing-library/jest-dom,
  @testing-library/cypress/add-commands)
- require-module-specifiers: removed redundant `export {}` from
  cypress/support/e2e.ts (file already has an import)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* Fix TypeScript errors from no-non-null-assertion fixes

Two fixes introduced by the non-null assertion cleanup produced type
errors:

- scoring-event.ts: `?? ""` was wrong type for a participant object map;
  restructured to explicit null guards so TypeScript can narrow correctly
- standings-sync/index.ts: `?? null` after name-match lookup lost the
  truthy guarantee, causing TS18047 on the write-back block; added
  `participant &&` guard before accessing its properties

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* Add npm run typecheck as Stop hook in Claude settings

Runs a full project typecheck at the end of each Claude turn so type
errors surface as feedback before the next message.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-21 10:59:51 -07:00
Chris Parsons
e2b178221a
Add oxlint linting setup with zero errors (#194)
* Add oxlint and fix all lint errors

- Install oxlint, add .oxlintrc.json with rules for TypeScript/React
- Add npm run lint / lint:fix scripts
- Add Claude PostToolUse hook to run oxlint on every edited file
- Fix 101 errors: unused vars/imports, eqeqeq, prefer-const, no-new-array
- Fix no-array-index-key (use stable keys or suppress positional cases)
- Fix exhaustive-deps missing dependency in useEffect
- Promote exhaustive-deps and no-array-index-key to errors
- Fix Map.get() !== null bug in $leagueId.server.ts (should be !== undefined)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* Fix no-explicit-any warnings and upgrade tsconfig to ES2023

- Replace all `any` types with proper types or `unknown` across ~20 files
- Add typed socket payload interfaces in draft route and useDraftSocket
- Use any[] with eslint-disable for socket.io callbacks (legitimate escape hatch)
- Bump all tsconfigs from ES2022 → ES2023 to support toSorted/toReversed
- Fix cascading type errors uncovered by removing any: Map.get narrowing,
  participant relation types, ChartDataPoint, Partial<NewSeason> indexing
- Add ParticipantResultWithParticipant type to participant-result model
- Fix test fixtures to match updated interfaces (DraftCell, ParticipantResult)
- Fix duplicate getQPStandings import in sportsSeasonId.server.ts

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* Promote no-explicit-any to error

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-21 09:44:05 -07:00
Chris Parsons
063834d8e6
Display team owner names in standings views (#184)
* Show team name + username in standings, extract TeamNameDisplay component

- Add TeamNameDisplay component that renders team name (as link) with owner username below, matching the league homepage style
- Update StandingsTable to use TeamNameDisplay with owner username shown below team name
- Update league homepage standings section to use TeamNameDisplay
- Add ownerName/teamOwnerId fields to TeamStanding type
- Extend getSeasonStandings to include teamOwnerId from team relation
- Fetch and attach owner display names in the full standings page loader

https://claude.ai/code/session_01EYgGnuTBaRVdBDapJRTxDZ

* Address code review: type hygiene, explicit types, parallel fetching, style fix

- Remove teamOwnerId from TeamStanding type (was an internal server concern, not display data)
- Remove teamOwnerId from getSeasonStandings return value
- Add TeamStandingWithChange interface extending TeamStanding with sevenDayRankChange/sevenDayOldRank fields
- Annotate getSevenDayStandingsChange with explicit Promise<TeamStandingWithChange[]> return type
- Re-export TeamStandingWithChange from models/standings.ts
- Standings loader: query teams table directly for ownerIds instead of relying on teamOwnerId in standings data
- Standings loader: parallelize all independent queries (standings, teams, progressionData, seasonComplete, completionPercentage) with Promise.all
- Restore font-medium on StandingsTable team TableCell

https://claude.ai/code/session_01EYgGnuTBaRVdBDapJRTxDZ

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-03-19 20:19:28 -07:00
Chris Parsons
af99f6d002
Fix bracket point averaging and partial-score display in standings/breakdown (#159)
* Fix bracket point averaging and partial-score display in standings/breakdown

Bracket sports (UCL, NBA, NFL, etc.) use averaged points for tied
positions (e.g. QF losers all share avg of 5th-8th = 20 pts), but
both calculateTeamProjectedScore and getTeamScoreBreakdown were using
raw pointsFor5th (25) instead of the bracket-averaged value. This caused
the team breakdown page to show incorrect actual points (25 vs 20) and
the standings page to show incorrect actualPoints/projectedPoints totals.

Additionally, participants with isPartialScore=true (still alive in a
bracket with a provisional floor position) were being displayed with a
final placement badge (e.g. "5th") instead of "Pending".

Changes:
- calculateTeamProjectedScore: use calculateBracketPoints for bracket
  sports; handle isPartialScore participants separately (floor in
  actualPoints, incremental EV in projectedPoints, Math.max guard for
  EV < floor edge case)
- getTeamScoreBreakdown: same bracket averaging fix; pass isPartialScore
  through to picks; use explicit finalPosition != null && > 0 check
- TeamScoreBreakdown component: show Pending badge for isPartialScore
  participants; display actual/floor + EV row for all incomplete picks;
  add "actual / projected" column header subtitle
- 9 new unit tests covering all scoring branches including the EV-below-
  floor clamp edge case

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* Fix typecheck: cast DUMMY_EV_ROW to any in test fixture

ParticipantEV has additional required fields (id, participantId, etc.)
that aren't needed for the mock — cast to any to satisfy the type checker.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-17 14:00:32 -07:00
Chris Parsons
33f9ad6ebe
Simplify team breakdown page layout and fix code quality issues (#155)
- Replace per-sport cards with a single flat table; add Sport column (linked to sport season) before Participant
- Replace three summary stat cards with a compact "X remaining" line in the header
- Format pick numbers as round.pick notation (e.g. 2.02 for pick 16 in a 14-team league)
- Add getNumTeamsInSeason model function to compute pick-within-round correctly
- Remove dead bySport/totalPoints from standings model and component interface
- Narrow standing prop type to only currentRank (placementCounts was unused)
- Guard against numTeams === 0 edge case in pick notation
- Fix 0.0 → 0.00 decimal inconsistency
- Fix unsafe finalPosition! non-null assertion (null treated as Did Not Score)
- Update tests to match current UI; add coverage for new edge cases

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-17 09:20:41 -07:00
Chris Parsons
e9798263b5
Add rules page, rewrite how-to-play, and fix scoring display (#20)
- Add new /rules route with official league rules covering rosters,
  scoring, major-based QP scoring, tiebreakers, draft, and season rules
- Rewrite how-to-play page with a tutorial/marketing tone, highlighting
  the Fischer increment draft clock as a novel feature
- Add Rules link to navbar (desktop and mobile)
- Align QP values in both pages with DEFAULT_QP_VALUES in code
- Fix tiebreaker description to match placement-count logic in code
- Update all fantasy point displays from toFixed(1) to toFixed(2) across
  StandingsTable, TeamScoreBreakdown, and PointProgressionChart
- Update tests to match new two-decimal-place point display format

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-21 21:58:51 -08:00
Chris Parsons
1584d34b89
Redesign to dark-mode-only with navy palette and accent colors (#13)
Removes light mode entirely in favour of a permanent dark theme with a
navy-tinted background and three signature accents (electric blue,
amber/gold, coral) exposed as CSS custom properties and Tailwind
utilities (bg-electric, text-amber-accent, text-coral-accent).

- Set class="dark" on <html> and apply Clerk dark base theme
- Rewrite app.css: single :root palette (oklch navy values), custom
  --electric / --amber-accent / --coral-accent variables, remove
  duplicate .dark block and light-mode bg-white/bg-gray-950 rule
- Install @clerk/themes for Clerk dark modal support
- Replace hardcoded Tailwind colors across 30+ files:
  - Draft grid cells: blue-50/blue-950 → electric/15, green-50/950 → emerald/10
  - Timer: green-600/yellow-600/red-600 → emerald-400/amber-accent/coral-accent
  - Status badges: blue-50/green-50/gray-50 → electric/emerald/muted variants
  - Success messages: green-500/15 text-green-700 dark:text-green-400 → emerald-500/15 text-emerald-400
  - Info cards: blue-50 dark:bg-blue-950 → electric/10
  - Warning cards: yellow-500 → amber-accent variants
  - Medal/placement badges: yellow-500/orange-600 → amber-accent/coral-accent
  - Movement indicators: green-600/red-600 → emerald-400/coral-accent
  - Connection dots: green-500/red-500 → emerald-500/coral-accent
- Remove dark:hidden/dark:block logo toggle in welcome.tsx (always dark)
- Update DraftGrid test assertions to match new class names

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-20 19:26:11 -08:00
Chris Parsons
9211cad7d1 fix: correct test expectations for ICM edge cases and duplicate text queries
- ICM tests: fix single participant, zero probabilities, and 2-participant
  tests to account for simulation only filling min(participants, 8) positions
- TeamScoreBreakdown tests: use getAllByText for "150.0" which now appears
  in both the header and the Actual Points summary card

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-17 14:24:07 -08:00
Chris Parsons
24126fe389 fix: add missing projectedPoints and actualPoints to TeamScoreBreakdown test fixtures
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-14 23:08:06 -08:00
Chris Parsons
7970cb6a9c feat: add FIFA World Cup 48-team bracket template with group stage and projected scoring
- Add FIFA_48 bracket template with 12 groups of 4, knockout stage of 32
- Add tournament groups schema, model, and GroupStageDisplay component
- Add projected/expected value scoring to standings and team breakdowns
- Add docker-compose for local PostgreSQL development
- Move DRAFT_ORDER_IMPLEMENTATION.md to plans directory

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-14 22:30:12 -08:00
Chris Parsons
6ef829f667 feat: add recharts library and implement PointProgressionChart component for historical views
- Added recharts dependency to package.json for data visualization.
- Implemented PointProgressionChart component to display team point progression over time.
- Updated scoring-system.md to reflect completion of historical views and added implementation notes.
- Created unit tests for PointProgressionChart to ensure correct rendering and data handling.
- Developed season completion detection and percentage calculation functions in season-helpers.server.ts.
- Added tests for season completion logic and point progression data formatting.
- Enhanced league loader to fetch necessary data for current season and teams.
2025-11-14 21:18:34 -08:00
Chris Parsons
f17699e4c5 feat: Implement team breakdown pages with detailed score breakdown
- Created `TeamScoreBreakdown` component to display all drafted participants, their placements, and points.
- Added new route for team breakdown at `/leagues/:leagueId/standings/:seasonId/teams/:teamId`.
- Enhanced `getTeamScoreBreakdown` model to include `sportsSeasonId` for grouping.
- Updated `StandingsTable` to include clickable team name links.
- Added functionality to finalize brackets in playoff events, including error handling and recalculating standings.
- Implemented tests for `TeamScoreBreakdown` component to ensure proper rendering and functionality.
- Updated routing to support new team breakdown feature and ensure seamless navigation.
2025-11-14 20:01:21 -08:00
Chris Parsons
0385ca220c feat: add standings page and enhance standings functionality
- Added a new route for league standings: `/leagues/:leagueId/standings/:seasonId`
- Implemented the `StandingsTable` component to display team standings with ranking, points, and placement breakdown.
- Integrated tiebreaker logic for ranking teams based on total points and placements.
- Enhanced the league home page with a link to view standings.
- Updated scoring system documentation to reflect the new standings features.
- Added comprehensive tests for tiebreaker logic and `StandingsTable` component.
- Created shared types for standings to be used in both server and client code.
2025-11-13 13:24:03 -08:00