An app for practical shooters: import your match results (PractiScore HTML and PDF for IPSC / Steel Challenge, Google Sheets CSV for Tiro FBI, WinMSS and FAT PDF for IPSC), track your progress match by match with KPIs and charts, and optionally keep a log of the firearms you used and the rounds you fired.
npm install
cp .env.example .env.local # fill in your Supabase credentials
# apply ALL the migrations in supabase/migrations/ from the SQL Editor,
# in numeric order (0001 → 0020).
npm run dev # http://localhost:3000Next.js 16 (App Router) · React 19 · TypeScript · Supabase (Postgres + Auth + RLS) · Tailwind 4 · Vitest.
| Command | Description |
|---|---|
npm run dev |
Dev server |
npm run build |
Production build |
npm test |
Tests (parsers, importer, stats, claim, round estimator) |
npm run test:watch |
Tests in watch mode |
- Multi-format import: PractiScore HTML (IPSC + Steel Challenge) and Google Sheets CSV for Tiro FBI. Format is detected automatically.
- Multiple identities per user: the same shooter may be spelled differently in each discipline ("Last, First" in IPSC vs "Last First" in Tiro FBI); a user can claim several identities.
- Claim auto-detection: on import, the app suggests the shooters that look like the logged-in user (matching on name tokens + member number).
- Dashboard with KPIs: matches shot, average %, best %, best placement, average percentile, consistency (standard deviation), trend (linear regression over the full history), cadence (matches/month).
- Progress chart in SVG (no libraries) with a per-match tooltip.
- Optional firearm catalog: register your firearms and assign them per match to keep a round count (auto-derived for Tiro FBI = 45 and Steel = 25 × stages loaded; manual for IPSC).
- Theming: light / dark / system, persisted (next-themes).
Functional documentation lives in docs/:
src/
├── app/
│ ├── [locale]/ # localized routes (route groups: (app) and (auth))
│ │ ├── (app)/
│ │ │ ├── about/
│ │ │ ├── activity/
│ │ │ ├── ammo/
│ │ │ ├── dashboard/
│ │ │ ├── firearms/ # catalog and per-firearm detail
│ │ │ ├── import/
│ │ │ └── matches/
│ │ └── (auth)/ # login + signup
│ ├── auth/ # callback, confirm, signout (not localized)
│ └── q/[code]/ # short QR redirect per firearm
├── components/ # reusable UI + StatsOverview, PerformanceChart, etc.
├── lib/
│ ├── actions/ # shared Server Actions (claim, firearms)
│ ├── db/ # data access layer (matches, shooters, firearms, ...)
│ ├── firearms/ # per-discipline round estimator
│ ├── import/ # import logic + auto-claim
│ ├── parsers/ # external file parsers (PractiScore HTML/PDF, WinMSS, FAT, Steel, FBI CSV)
│ ├── stats/ # KPI computation (pure function, tested)
│ ├── supabase/ # server / browser / middleware clients
│ ├── types/ # parser domain types
│ ├── disciplines.ts # discipline constants + helpers
│ ├── redirects.ts # redirectWithError helper
│ └── utils.ts # cn, formatters
└── proxy.ts # per-request session refresh
supabase/migrations/ # schema SQL (numbered 0001…0020)
docs/ # functional documentation
tests/ # vitest + real PractiScore and FBI fixtures
profiles·disciplines·divisions— foundation.shooters— shooter identities (a user can claim several).matches·match_entries·stages·stage_results— match data (shared across users; UNIQUE NULLS NOT DISTINCT on matches to avoid duplicates when region is NULL).firearms·match_firearm_log— private per user, RLS by owner.
See docs/data-model.md for the details.
Pull Requests are welcome. Before sending one, read CONTRIBUTING.md — it covers local setup, our commit conventions (we use conventional commits so release-please can compute versions automatically), and the PR flow.
Found a bug or have an idea? Open an issue. For file import bugs, attach the file that fails — without it we can't reproduce.
AGPL v3 or later. In short: you can use, modify and redistribute the code freely; if you host it as a public service (your own or a fork), your modified version has to be available under the AGPL too. This keeps community improvements open to the community.