Initial commit v1

This commit is contained in:
Rolf
2026-03-18 12:37:17 +01:00
commit 4897d3003f
55 changed files with 14241 additions and 0 deletions
+92
View File
@@ -0,0 +1,92 @@
# 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 + Supabase app running in Docker Compose.
## Commands
All commands run inside Docker (Node.js is not installed on the host):
```bash
# Development (with Studio + Mailpit)
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
docker run --rm -v $(pwd):/app -w /app node:22-alpine npm run build
```
## Architecture
**Stack:** Next.js 15 (App Router) · TypeScript · Tailwind CSS v4 · shadcn/ui · Supabase (self-hosted) · TanStack Query v5 · Zustand · Serwist (PWA)
**Routing groups:**
- `app/(auth)/` — unauthenticated pages (login, confirm)
- `app/(app)/` — authenticated app shell with bottom navigation
**Supabase clients:**
- `lib/supabase/client.ts` — browser client (use in `'use client'` components)
- `lib/supabase/server.ts` — server client + admin client (Server Components, Server Actions, Route Handlers)
- Always use `supabase.auth.getUser()` server-side, never `getSession()` — getUser() cryptographically verifies the JWT
**Auth flow:**
- Invite-only. New users are invited via `supabase.auth.admin.inviteUserByEmail()` (uses service role key).
- Magic link → `/auth/confirm` route handler exchanges the PKCE code for a session.
- `middleware.ts` guards all `/(app)` routes; redirects unauthenticated users to `/login`.
- Admin email is set via `ADMIN_EMAIL` env var; `lib/admin.ts` exposes `isAdminEmail()`.
**Real-time scores:**
- Supabase Postgres Changes subscriptions on the `scores` table, filtered by `round_id`.
- TanStack Query cache is invalidated on change events.
- Score entry uses UPSERT — `unique(round_id, hole_id, player_id)` constraint.
**Round lifecycle:** `lobby → active → completed`
- Lobby: players can be invited and join; no scoring yet.
- Active: started by round creator; scoring enabled; no new players.
- Completed: closed by round creator; read-only.
## Database
Migrations live in `supabase/migrations/` and are applied on DB container startup via `/docker-entrypoint-initdb.d/`.
- `00001_schema.sql` — tables, triggers (auto-create profile, enforce course par sum, update scores.updated_at)
- `00002_rls.sql` — Row Level Security policies for all tables
**Key RLS rules:**
- Scores can be inserted/updated by any round member, but only when `round.status = 'active'`
- `is_round_member(round_id)` helper function used throughout RLS policies
- `service_role` key bypasses RLS — only use server-side, never expose to client
**Course par validation:** A DB trigger fires on `holes` INSERT/UPDATE; once 9 or 18 holes exist for a course, it asserts `SUM(holes.par) = courses.par`.
**Handicap calculation (WHS):**
`course_handicap = handicap_index × (slope_rating ÷ 113) + (course_rating par)`
Stored as a snapshot on `round_players` at join time.
## Infrastructure
- **Docker Compose** — all services defined in `docker-compose.yml`
- **Traefik** — external reverse proxy; services expose themselves via labels only (no port mappings)
- `studio` and `mailpit` services are in the `dev` profile — not started in production
- **Email dev:** Mailpit catches all outgoing email (`SMTP_HOST=mailpit`)
- **Email prod:** Set `SMTP_HOST`, `SMTP_USER`, `SMTP_PASS` to Resend/Mailgun/etc. in `.env`
- Copy `.env.example``.env` and fill all values before first run
## Key Conventions
- Net score vs par (WHS handicap-adjusted) is always the **primary** display, in bold
- Raw strokes vs course par is secondary
- Score entry defaults to hole par; +/ buttons; tap number for picker
- Bottom navigation: Rounds · History · Profile (+ Admin tab for admin user only)
- Mobile-first: minimum 44×44px tap targets, swipe between holes, high-contrast for sunlight