* 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>
262 lines
No EOL
11 KiB
Markdown
262 lines
No EOL
11 KiB
Markdown
# 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.) |