brackt/CLAUDE.md
Chris Parsons fa0e798db7
Add "Games to Score" widget to admin dashboard (#183)
* Add "Games to Score" widget to admin dashboard

Shows scoring events for today and tomorrow in a tabbed card, with
status badges (Pending/Scored), a scored/total progress counter, and
direct "Score" / "View" links to each event's results page. Excludes
non-scoring schedule_event types so only actionable events surface.

- app/models/scoring-event.ts: add getEventsForDates() model function
  that queries scoringEvents joined with sportsSeasons/sport for a
  given list of date strings
- app/routes/admin._index.tsx: call getEventsForDates([today, tomorrow])
  in the loader and render the GamesToScore component below the stats row
- app/models/__tests__/scoring-event-dashboard.test.ts: 8 unit tests
  covering empty input, shape mapping, multi-date filtering, completed
  events, and the inArray where conditions

https://claude.ai/code/session_014zKK15o5RJfTaodLvnZpF5

* Fix getEventsForDates to include bracket events by game scheduledAt

The previous implementation only matched on scoringEvents.eventDate,
which is often null for bracket events where individual game times are
set on playoffMatchGames.scheduledAt instead.

Switched to a two-step query mirroring getUpcomingEventsForDraftedParticipants:
1. selectDistinct event IDs via left joins through playoffMatches →
   playoffMatchGames, matching where eventDate IN dates OR any game's
   scheduledAt falls within the date range's timestamp bounds
2. findMany on the matched IDs with sportsSeason + sport relations

Updated tests to cover the two-step flow, leftJoin usage, and the
scheduledAt timestamp bounds appearing in the where clause.

https://claude.ai/code/session_014zKK15o5RJfTaodLvnZpF5

* Document dual game date storage in CLAUDE.md

scoringEvents.eventDate and playoffMatchGames.scheduledAt both hold game
dates depending on the event type. Added a dedicated section under
Important Patterns explaining when each is used and the two-step
selectDistinct + findMany pattern required for correct date queries.

https://claude.ai/code/session_014zKK15o5RJfTaodLvnZpF5

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-03-19 19:25:37 -07:00

11 KiB

CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

Project Overview

Brackt.com is a fantasy sports drafting platform built with React Router 7, featuring real-time draft functionality with Socket.IO, PostgreSQL database with DrizzleORM, and Clerk authentication.

Tech Stack

  • Framework: React Router 7 with SSR
  • Runtime: Node.js with Express server
  • Database: PostgreSQL with DrizzleORM
  • Auth: Clerk (@clerk/react-router)
  • Real-time: Socket.IO (server/socket.ts)
  • Styling: TailwindCSS + ShadCN UI components
  • Testing: Vitest (unit/component) + Cypress (E2E)
  • TypeScript: Strict mode enabled

Development Commands

# Development (HMR enabled)
npm run dev

# Build
npm run build                    # Build both Remix and server
npm run build:remix              # Build React Router app only
npm run build:server             # Build Express server only

# Database
npm run db:generate              # Generate Drizzle migrations
npm run db:migrate               # Run migrations

# Testing
npm test                         # Unit tests (watch mode)
npm run test:run                 # Unit tests (run once)
npm run test:ui                  # Unit tests with UI
npm run test:coverage            # Coverage report
npm run test:e2e                 # Cypress interactive
npm run test:e2e:headless        # Cypress headless
npm run test:all                 # Run all tests

# Type checking
npm run typecheck                # Check all TypeScript files

# Production
npm start                        # Start production server (requires build)
npm run start:production         # Run migrations + start server

Architecture Overview

Server Architecture

The application uses a dual-server architecture:

  1. server.ts - Main entry point that creates the HTTP server and initializes Socket.IO
  2. server/app.ts - Express app with React Router request handler and database context
  3. server/socket.ts - Socket.IO server for real-time draft updates
  4. server/timer.ts - Draft timer system that runs every second to update active draft timers

Important: The timer system (server/timer.ts) must be started for draft functionality. It uses a dedicated database connection and setInterval to manage all active draft timers.

Database Layer

  • Schema: database/schema.ts - All Drizzle table definitions
  • Context: Database connection provided via AsyncLocalStorage context (database/context.ts)
  • Models: app/models/ - Business logic and database queries organized by domain
    • Each model exports typed functions for CRUD operations
    • Models use Drizzle relations for joins
    • Example: app/models/league.ts, app/models/season.ts, app/models/draft-pick.ts

Frontend Architecture

  • Routes: Defined in app/routes.ts using React Router config syntax
    • IMPORTANT: React Router 7 requires ALL routes to be explicitly registered in app/routes.ts
    • When creating a new route file, you MUST also add it to app/routes.ts
    • Example: If you create app/routes/admin.sports-seasons.$id.new-feature.tsx, you must add:
      route(
        "sports-seasons/:id/new-feature",
        "routes/admin.sports-seasons.$id.new-feature.tsx"
      ),
      
  • File-based routing: Route files in app/routes/ map to URL patterns
    • $param = dynamic segment (e.g., $leagueId.tsx)
    • _index.tsx = index route
    • . separator for nested routes (e.g., leagues/$leagueId.settings.tsx)
  • Loaders/Actions: Use React Router's loader/action pattern for data fetching and mutations
  • Components: Organized in app/components/ with UI components in app/components/ui/

Real-time Features

Socket.IO is used for live draft updates:

  1. Join Draft: Clients join a room for their season (join-draft event)
  2. Timer Updates: Server broadcasts timer-update every second during active drafts
  3. Pick Made: Broadcasts pick-made when a participant is drafted
  4. Autodraft: Broadcasts autodraft-updated when teams enable/disable autodraft
  5. Team Connection: Broadcasts team-connected/team-disconnected for presence

Socket Integration: The server uses getSocketIO() from server/socket.ts to emit events. Clients use useSocket() hook (if implemented) to listen for events.

Key Domain Models

Fantasy Draft System

  1. League - Container for multiple seasons, has commissioners
  2. Season - Single year/iteration of a league with specific settings
    • Status: pre_draftdraftactivecompleted
    • Stores draft settings (rounds, timer settings, etc.)
  3. Team - Belongs to a season, owned by a user
  4. Draft Slot - Defines draft order for teams in a season
  5. Draft Pick - Record of each pick made during draft
  6. Draft Queue - Pre-ordered list of participants a team wants to draft
  7. Draft Timer - Active timer for current team on the clock
  8. Autodraft Settings - Per-team settings for automatic drafting

Sports Data System

  1. Sport - Base sport (e.g., NFL, NBA) with type (team/individual)
  2. Sports Season - Specific season of a sport (e.g., "2024 NFL Season")
  3. Participant - Individual athlete or team that can be drafted
  4. Season Template - Reusable configuration for league seasons
  5. Season Sport - Links a fantasy season to sports it covers
  6. Participant Result - Stores final standings/scores for participants

Important Patterns

Database Queries

Always use the model layer instead of direct Drizzle queries in routes:

// Good
import { findLeagueById } from "~/models/league";
const league = await findLeagueById(leagueId);

// Avoid - don't query directly in routes
const league = await db.query.leagues.findFirst({...});

Authentication

  • User data synced from Clerk via webhook (routes/api/webhooks/clerk.ts)
  • Check clerkId for ownership validation
  • Admin checks via isAdmin boolean on users table

Scoring Event Dates

Game/event dates are stored in two different places depending on the event type, and any date-based query must account for both:

  1. scoringEvents.eventDate (date column, YYYY-MM-DD string) — set directly on the event. Used for non-bracket events (majors, final standings) and sometimes for bracket events when the admin sets a single date for the whole round.

  2. playoffMatchGames.scheduledAt (timestamp column) — set on individual games within a bracket match. Bracket events (e.g. a playoff series) often have eventDate = null on the scoring event itself, with each game's exact time stored here instead.

Pattern for date-range queries: Use a two-step approach:

  1. selectDistinct event IDs via left joins through playoffMatches → playoffMatchGames, filtering with eventDate IN [dates] OR (scheduledAt >= dayStart AND scheduledAt <= dayEnd)
  2. findMany on the matched IDs to load full event data with relations

See getEventsForDates and getUpcomingEventsForDraftedParticipants in app/models/scoring-event.ts for the reference implementation. All timestamp bounds use UTC (T00:00:00.000Z / T23:59:59.999Z).

Draft Logic

The draft system implements snake draft:

  • Draft slots define the base order (1, 2, 3, ...)
  • Each round alternates: odd rounds go 1→N, even rounds go N→1
  • Current pick determined by currentPickNumber on season
  • Timer system in server/timer.ts handles autopicks when time expires

Testing Strategy

  • Unit tests: Pure functions and utilities in __tests__/ directories
  • Component tests: React components using React Testing Library
  • E2E tests: Critical user flows in cypress/e2e/
  • Fixtures: Reusable test data in app/test/fixtures/
  • See TESTING.md for comprehensive testing guide

IMPORTANT: Tests are required for new features. When adding any new feature, you must include appropriate tests:

  • New model functions → unit tests in a co-located __tests__/ directory
  • New route loaders/actions → unit or integration tests for the logic
  • New utility functions → unit tests
  • New components with non-trivial logic → component tests
  • Critical user flows → Cypress E2E tests

Do not consider a feature complete until tests are written and passing.

Common Workflows

Adding a New Route

CRITICAL: Both steps 1 and 2 are required - the route will not work if you skip step 1!

  1. FIRST: Add route config to app/routes.ts
    • Example: route("admin/sports-seasons/:id/new-page", "routes/admin.sports-seasons.$id.new-page.tsx")
    • Nested admin routes go inside the route("admin", ...) array
  2. THEN: Create file in app/routes/ with corresponding name
  3. Export loader/action if needed
  4. TypeScript types are auto-generated via react-router typegen

Common mistake: Creating the route file but forgetting to register it in app/routes.ts - this will result in a 404!

Database Changes

  1. Update database/schema.ts with new tables/columns
  2. Run npm run db:generate to create migration
  3. Run npm run db:migrate to apply migration
  4. Add model functions in app/models/ for new queries
  5. Update TypeScript types if needed

Adding Socket.IO Events

  1. Define event types in server/socket.d.ts interfaces
  2. Add event handler in server/socket.ts
  3. Emit events from server code using getSocketIO().to(room).emit()
  4. Listen for events in client components

Admin Features

Admin routes are nested under /admin and require isAdmin: true on the user:

  • Sports management (admin.sports.*)
  • Sports seasons (admin.sports-seasons.*)
  • Season templates (admin.templates.*)
  • Data sync utilities (admin.data-sync.tsx)

File Structure Notes

  • app/: React Router application code
    • components/: React components (UI components in ui/)
    • models/: Database query functions organized by domain
    • routes/: Route handlers (loaders, actions, components)
    • lib/: Utility functions
    • hooks/: Custom React hooks
    • contexts/: React contexts
    • test/: Test setup and fixtures
  • server/: Server-side code (Express, Socket.IO, timers)
  • database/: Drizzle schema and context
  • drizzle/: Generated migrations
  • cypress/: E2E tests

Environment Variables

Required in .env:

  • DATABASE_URL - PostgreSQL connection string
  • Clerk variables for authentication (see .env.example)

Production Deployment

The app is containerized with Docker:

  • Runs migrations on startup (start:production script)
  • Server listens on port 3000 (configurable via PORT env var)
  • Serves static assets from build/client/
  • SSR handled by React Router Express adapter

Notes

  • Draft timer system must be explicitly started (usually in server initialization)
  • Socket.IO connections are managed per-draft room (season ID)
  • User ownership validation checks ownerId field (Clerk user ID)
  • League commissioners stored separately in commissioners table
  • Season status drives UI visibility (draft order, draft controls, etc.)