# 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 ```bash # 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: ```typescript 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_draft` → `draft` → `active` → `completed` - 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: ```typescript // 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.)