* 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>
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:
- server.ts - Main entry point that creates the HTTP server and initializes Socket.IO
- server/app.ts - Express app with React Router request handler and database context
- server/socket.ts - Socket.IO server for real-time draft updates
- 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.tsusing 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" ),
- IMPORTANT: React Router 7 requires ALL routes to be explicitly registered in
- 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 inapp/components/ui/
Real-time Features
Socket.IO is used for live draft updates:
- Join Draft: Clients join a room for their season (
join-draftevent) - Timer Updates: Server broadcasts
timer-updateevery second during active drafts - Pick Made: Broadcasts
pick-madewhen a participant is drafted - Autodraft: Broadcasts
autodraft-updatedwhen teams enable/disable autodraft - Team Connection: Broadcasts
team-connected/team-disconnectedfor 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
- League - Container for multiple seasons, has commissioners
- Season - Single year/iteration of a league with specific settings
- Status:
pre_draft→draft→active→completed - Stores draft settings (rounds, timer settings, etc.)
- Status:
- Team - Belongs to a season, owned by a user
- Draft Slot - Defines draft order for teams in a season
- Draft Pick - Record of each pick made during draft
- Draft Queue - Pre-ordered list of participants a team wants to draft
- Draft Timer - Active timer for current team on the clock
- Autodraft Settings - Per-team settings for automatic drafting
Sports Data System
- Sport - Base sport (e.g., NFL, NBA) with type (team/individual)
- Sports Season - Specific season of a sport (e.g., "2024 NFL Season")
- Participant - Individual athlete or team that can be drafted
- Season Template - Reusable configuration for league seasons
- Season Sport - Links a fantasy season to sports it covers
- 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
clerkIdfor ownership validation - Admin checks via
isAdminboolean 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:
-
scoringEvents.eventDate(datecolumn,YYYY-MM-DDstring) — 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. -
playoffMatchGames.scheduledAt(timestampcolumn) — set on individual games within a bracket match. Bracket events (e.g. a playoff series) often haveeventDate = nullon the scoring event itself, with each game's exact time stored here instead.
Pattern for date-range queries: Use a two-step approach:
selectDistinctevent IDs via left joins throughplayoffMatches → playoffMatchGames, filtering witheventDate IN [dates] OR (scheduledAt >= dayStart AND scheduledAt <= dayEnd)findManyon 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
currentPickNumberon season - Timer system in
server/timer.tshandles 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.mdfor 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!
- 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
- Example:
- THEN: Create file in
app/routes/with corresponding name - Export loader/action if needed
- 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
- Update
database/schema.tswith new tables/columns - Run
npm run db:generateto create migration - Run
npm run db:migrateto apply migration - Add model functions in
app/models/for new queries - Update TypeScript types if needed
Adding Socket.IO Events
- Define event types in
server/socket.d.tsinterfaces - Add event handler in
server/socket.ts - Emit events from server code using
getSocketIO().to(room).emit() - 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:productionscript) - 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
ownerIdfield (Clerk user ID) - League commissioners stored separately in
commissionerstable - Season status drives UI visibility (draft order, draft controls, etc.)