OpenAthlete
AGPLv3 endurance training platform: EU-oriented hosting, transparent CTL/ATL/TSB logic in code, self-hosting, and full export. Built in Grenoble.
翻译准备中
尚未检测到更新。GitHub 发布版本后会显示在这里。
个人 GitHub 账号下公开仓库的所有者可以登录并验证所有权,自行管理介绍,无需管理员批准。
登录并生成验证文件,在 30 分钟内将其提交到此仓库的默认分支。其他分支或分叉仓库中的文件无效。然后在下方检查文件,即可无需管理员批准直接管理项目。请保留文件以维持管理权限。
如需登录,将前往登录页面。登录后请回到此处,再次点击所选的验证按钮。
在项目文件夹中运行 /nomorevibe verify,即可验证所有权并自行管理介绍。
一分钟完成验证📱 A progressive web app built with React, TypeScript, and Vite. Uses a pure reducer state machine to enforce snooker rules, Web Audio API for offline sounds, and Supabase for cloud history.
翻译准备中
取自仓库 README
正在加载完整 README。 以下为已保存的摘录。
A snooker scorer that knows the rules, so you don't have to argue about them.
Keeping score on paper works right up until someone pots a red on a foul, or the black gets re-spotted, or nobody can remember whose visit it is. SnookerBee is a phone-sized referee: it enforces the sequence, computes the penalties, and stays out of the way the rest of the time.
It runs offline, installs to your home screen, and survives iOS deciding to close it mid-frame.
Built for landscape. The scoring layout needs the width, so portrait shows a rotate prompt rather than a squeezed table.查看完整 README ↗A snooker scorer that knows the rules, so you don't have to argue about them.
▶ Open the app · installs to your home screen · works offline
Keeping score on paper works right up until someone pots a red on a foul, or the black gets re-spotted, or nobody can remember whose visit it is. SnookerBee is a phone-sized referee: it enforces the sequence, computes the penalties, and stays out of the way the rest of the time.
It runs offline, installs to your home screen, and survives iOS deciding to close it mid-frame.
Built for landscape. The scoring layout needs the width, so portrait shows a rotate prompt rather than a squeezed table.
A pure reducer — no side effects, fully testable — that holds the frame together:
| Mode | |
|---|---|
| 1 v 1 | Standard head-to-head |
| Teams | 2v2 or 3v3, alternating visits |
| Free-for-All | 2–8 players, solo scoring |
| Century | Race to exactly 50 or 100 |
10 or 15 reds. Best of 1, 3 or 5 — odd only, so a match can't end level.
Each frame rotates who breaks, and the rest follow in the order you set: 1,2,3 → 2,1,3 → 3,1,2. Everyone plays after someone different.
A few decisions that aren't obvious from the outside:
Durations are subtracted, not counted. There's no ticking timer. Start times are recorded and elapsed time is derived from them, so backgrounding the app can't lose time, undo can't rewind the clock, and nothing re-renders once a second for the length of a match.
Matches survive the app dying. iOS terminates backgrounded web apps. State is written to localStorage as you play, so the dashboard can offer to resume, and a finished match is held until a save actually succeeds.
Sharing is preview-first. iOS drops user activation across an await, so generating a PNG and then calling navigator.share() silently fails. The card is shown first; the share button is a fresh gesture with the file already in hand.
A version can't ship without release notes. npm run build runs scripts/check-release.mjs first. It fails the build — and so the Vercel deploy — unless package.json matches the newest entry in src/changelog.ts, versions run newest → oldest, and every entry is dated and lists a change. The app reads the same file for its What's new page.
Colour lives on the content, not behind it. Selected states tint text and icons rather than filling the element, which keeps one thing loud per screen instead of five.
| Layer | Choice |
|---|---|
| UI | React 19, TypeScript, Vite 8 |
| Routing | React Router 7 |
| State | useReducer state machine, no external store |
| Styling | Vanilla CSS with design tokens, themed by class |
| Offline | vite-plugin-pwa + Workbox |
| Audio | Web Audio API oscillators |
| Backend | Supabase (Postgres + Google OAuth, Row Level Security) |
| Testing | Vitest — rules engine, results, frame analysis, player stats |
| Lint | oxlint |
| CI | GitHub Actions — lint, test and build on every push and PR |
| Hosting | Vercel, gated by a release-notes check |
Requires Node 18+.
git clone https://github.com/arshad0126/snookerbee.git
cd snookerbee
npm install
npm run dev
Then open the printed URL — usually http://localhost:5173 — and turn your phone (or the browser window) sideways.
| Script | |
|---|---|
npm run dev | Dev server with HMR |
npm test | Vitest suite |
npm run lint | oxlint |
npm run build | Release-notes check, typecheck and production build |
npm run preview | Serve the built output |
Cloud sync is optional. Without Supabase credentials the app runs in guest mode and keeps history on the device.
Set these in .env locally, or in your host's environment settings:
VITE_SUPABASE_URL=your_supabase_project_url
VITE_SUPABASE_ANON_KEY=your_supabase_anon_public_key
Vite inlines these at build time, so a deployment missing them produces a bundle that can't reach the database — rebuild after changing them.
Create a Supabase project, open the SQL Editor, and run docs/schema.sql.
It is additive and safe to run against a database that already holds matches. If you set the project up before Century mode existed, running it again adds the two new tables and the milestone columns without touching anything else.
Row Level Security is on for every table — a signed-in user can only reach their own rows.
-- Matches
create table matches (
id uuid primary key default gen_random_uuid(),
user_id uuid references auth.users(id) on delete cascade,
mode text not null,
reds_count int not null,
best_of int not null,
created_at timestamptz default now(),
duration_ms bigint not null,
winner_name text not null
);
alter table matches enable row level security;
create policy "Users can CRUD their own matches" on matches
for all using (auth.uid() = user_id);
-- Players within a match
create table match_players (
id uuid primary key default gen_random_uuid(),
match_id uuid references matches(id) on delete cascade,
player_name text not null,
team_name text,
total_score int default 0,
highest_break int default 0,
frames_won int default 0,
fouls_committed int default 0,
time_spent_ms bigint default 0
);
alter table match_players enable row level security;
create policy "Users can CRUD their own match players" on match_players
for all using (
match_id in (select id from matches where user_id = auth.uid())
);
-- Frame-by-frame action logs
create table match_frames (
id uuid primary key default gen_random_uuid(),
match_id uuid references matches(id) on delete cascade,
frame_number int not null,
duration_ms bigint,
action_log jsonb
);
alter table match_frames enable row level security;
create policy "Users can CRUD their own match frames" on match_frames
for all using (
match_id in (select id from matches where user_id = auth.uid())
);
src/
├── engine/ Rules engine — reducer, validators, types, constants (+ tests)
├── components/ Screens, plus ui/ for the shared component library
├── lib/ Supabase, local storage, audio, share cards (+ tests)
├── hooks/ Auth and theme
├── styles/ Design tokens and stylesheets
└── changelog.ts Release notes — the build checks it, the app shows it
The engine has no React in it. reducer.ts takes a state and an action and returns a state, which is why the rules can be reasoned about — and corrected — without touching the interface.
Built for the table, not the spreadsheet.