Files
2026-03-19 09:09:55 +01:00

91 lines
4.0 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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
# Start
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)
- **Traefik** — external reverse proxy; services expose via labels only
- **Email:** Set `SMTP_HOST`, `SMTP_PORT`, `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