95 lines
4.1 KiB
Markdown
95 lines
4.1 KiB
Markdown
# CLAUDE.md
|
||
|
||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||
|
||
## What This Is
|
||
|
||
MulliganMates is a social golf score tracking PWA. Multiple players in a group track scores per hole in real-time. Anyone in a group can enter or edit scores for any player. Built as a self-hosted Next.js app running in Docker Compose with a single PostgreSQL database.
|
||
|
||
## Commands
|
||
|
||
All commands run inside Docker (Node.js is not installed on the host):
|
||
|
||
```bash
|
||
# Development (with Mailpit for email)
|
||
docker compose --profile dev up
|
||
|
||
# Production
|
||
docker compose up -d
|
||
|
||
# Install a new npm package
|
||
docker run --rm -v $(pwd):/app -w /app node:22-alpine npm install <package>
|
||
|
||
# Add a shadcn/ui component
|
||
docker run --rm -v $(pwd):/app -w /app node:22-alpine npx shadcn@latest add <component>
|
||
|
||
# Type-check / build
|
||
docker run --rm -v $(pwd):/app -w /app node:22-alpine npm run build
|
||
|
||
# Generate Drizzle migrations (after schema changes)
|
||
docker run --rm -v $(pwd):/app -w /app -e DATABASE_URL=... node:22-alpine npx drizzle-kit generate
|
||
|
||
# Apply migrations manually
|
||
docker run --rm -v $(pwd):/app -w /app -e DATABASE_URL=... node:22-alpine npx drizzle-kit migrate
|
||
```
|
||
|
||
## Architecture
|
||
|
||
**Stack:** Next.js 15 (App Router) · TypeScript · Tailwind CSS v4 · shadcn/ui v4 · better-auth · Drizzle ORM · SSE · nodemailer · Serwist (PWA)
|
||
|
||
**Routing groups:**
|
||
- `app/(auth)/` — unauthenticated pages (login only)
|
||
- `app/(app)/` — authenticated app shell with bottom navigation
|
||
|
||
**Auth (`lib/auth.ts`):**
|
||
- better-auth with `magicLink` plugin (`disableSignUp: true` for invite-only)
|
||
- `drizzleAdapter` pointing to `lib/db/schema.ts` tables: `user`, `session`, `verification`
|
||
- Additional field: `handicapIndex` on user
|
||
- Server-side session: `auth.api.getSession({ headers: await headers() })`
|
||
- Magic link to invite new users: create user with `db.insert(user)` first, then `auth.api.signInMagicLink()`
|
||
|
||
**Database (`lib/db/`):**
|
||
- `index.ts` — `drizzle(postgres(DATABASE_URL))` export
|
||
- `schema.ts` — all table definitions (camelCase column names)
|
||
- `migrations/` — generated SQL files applied via `instrumentation.ts` on startup
|
||
- **All Drizzle field names are camelCase** (`holesCount`, `courseHandicap`, `holeNumber`, `strokeIndex`)
|
||
|
||
**Real-time scores:**
|
||
- `app/api/rounds/[id]/stream/route.ts` — SSE Route Handler polling `scores` table every 3s
|
||
- Client connects via `new EventSource('/api/rounds/{id}/stream')`
|
||
- Payload: `ScoreRow[]` with `{ holeId, playerId, strokes }`
|
||
|
||
**Round lifecycle:** `lobby → active → completed`
|
||
- Lobby: players join/are invited; no scoring
|
||
- Active: started by round creator; scoring enabled
|
||
- Completed: closed by creator; read-only
|
||
|
||
## Database Schema Key Points
|
||
|
||
- better-auth tables use `text` PKs (nanoid); app tables use `uuid` PKs (`gen_random_uuid()`)
|
||
- `scores` has unique constraint on `(roundId, holeId, playerId)` — upsert pattern
|
||
- `roundPlayers` snapshots `handicapIndex` and `courseHandicap` at join time
|
||
- Migrations run automatically on app startup via `instrumentation.ts`
|
||
|
||
## UI Conventions
|
||
|
||
- shadcn/ui v4 uses `@base-ui/react` — **no `asChild` prop on Button**. Use `buttonVariants()` + `<Link>` instead
|
||
- Net score vs par (WHS handicap-adjusted) is always **primary** display, bold
|
||
- Raw strokes vs course par is secondary
|
||
- Score entry defaults to hole par; +/− buttons; tap number for direct input
|
||
- Bottom navigation: Rounds · History · Profile (+ Admin tab for admin user)
|
||
- Mobile-first: minimum 44×44px tap targets, swipe between holes
|
||
|
||
**Handicap calculation (WHS):**
|
||
`course_handicap = handicap_index × (slope_rating ÷ 113) + (course_rating − par)`
|
||
See `lib/handicap.ts` for `strokesReceived()`, `netStrokes()`, `formatVsPar()`, `scoreColorClass()`
|
||
|
||
## Infrastructure
|
||
|
||
- **Docker Compose** — `app` + `db` (postgres:17-alpine) + `mailpit` (dev profile)
|
||
- **Traefik** — external reverse proxy; services expose via labels only
|
||
- **Email dev:** Mailpit (`docker compose --profile dev up`)
|
||
- **Email prod:** Set `SMTP_HOST`, `SMTP_USER`, `SMTP_PASS` in `.env`
|
||
- Copy `.env.example` → `.env` before first run
|
||
- `BETTER_AUTH_URL` must equal the app's public URL for magic links to work
|