Tennis League
Workplace tennis league scoring, scheduling, and standings.
아직 감지된 업데이트가 없습니다. GitHub 릴리스가 나오면 여기에 보입니다.
개인 계정의 공개 저장소 소유자는 GitHub로 로그인해 운영자를 확인하고 소개를 직접 관리할 수 있습니다. 관리자 승인은 필요하지 않습니다.
로그인한 뒤 확인 파일을 만들어 이 저장소의 기본 브랜치에 30분 안에 커밋해 주세요. 다른 브랜치나 포크의 파일은 인정하지 않습니다. 파일을 올린 뒤 아래에서 확인하면 관리자 승인 없이 관리할 수 있습니다. 자격을 유지하려면 파일을 남겨 두세요.
로그인이 필요하면 로그인 화면으로 이동합니다. 로그인 후 이곳으로 돌아와 원하는 확인 버튼을 다시 눌러 주세요.
프로젝트 폴더에서 /nomorevibe verify를 실행하면 소유권을 확인하고 소개를 직접 관리할 수 있습니다.
1분 만에 확인하기메이커가 쓴 소개는 위의 한 줄이 전부입니다. 더 자세한 내용은 저장소 README에서 읽을 수 있습니다. 저장소 README ↗
README 전체를 불러오는 중입니다.
README 전문 보기 ↗A personal, mobile-first gym reference for a beginner on a lean bulk: how to set up and use each machine, form cues, muscles worked, training programs, eating to gain weight, and a private training log. It is a fully static site (no server, no database, no accounts) and is designed to work offline once installed.
See PLAN.md for the architecture, decisions and build phases.
Requires Node.js 22.12 or newer and npm.
npm install
npm run dev # http://localhost:3000
| Command | What it does |
|---|---|
npm run dev | Development server with hot reload |
npm run validate | Checks every piece of content against its schema and the cross-checks (below) |
npm run typecheck | TypeScript, strict mode |
npm run lint | ESLint; any warning fails |
npm run test | Unit tests (Vitest) |
npm run format | Formats code with Prettier |
npm run build | validate, a static export to out/, then the two post-build steps (below) |
npm run preview | Serves out/ at http://localhost:4173, the way a static host would |
npm run check | Everything above in order; run this before committing |
npm run icons | Redraws the app icons in public/icons/ (only needed after changing the design) |
After next build, two scripts finish the export:
scripts/postbuild.ts copies Next.js's prefetch files to the flat names its client requests,
so in-app links don't 404 on a plain static host.scripts/generate-sw.ts writes out/sw.js, the service worker, listing every file in out/
(see Offline and installing).npm run build writes a complete static site to out/. It must be served over HTTPS (the
service worker won't register otherwise; localhost is the exception) and from the root of a
domain, because every link and cached path starts with /.
vercel.json sends Cache-Control: no-cache for /sw.js, so no cache along the way holds on
to an old copy and new versions reach phones promptly. It also sets the manifest's content type.out/. URLs end in / (/about/), and each is a folder with an index.html. Set the same
no-cache header on /sw.js if the host lets you.username.github.io/<repo>/, which isn't a domain root.
Use a custom domain, or deploy somewhere else.Your stats and training log are never part of the build: they exist only in the browser of the device you entered them on, so deploying publicly shares the guide, not your data.
All content is typed TypeScript under src/content/. Zod schemas are the single source of
truth: the TypeScript types are inferred from them, so a type and its validation can't disagree.
src/content/
├─ schema/ Zod schemas (exercise, program, muscle, nutrition, glossary, shared enums)
├─ muscles.ts The 24 muscles, with plain-English names. Every page uses these ids.
├─ cues.ts Canonical cue phrases ("Brace your core") and what they mean
├─ progression.ts Progression rules (double progression etc.), written once
├─ rep-schemes.ts The only set/rep/rest prescriptions used anywhere, plus the week 1–2 ramp
├─ coverage.ts The list of exercises the site must contain
├─ exercises/<folder>/<slug>.ts One file per exercise
├─ exercises/index.ts Imports every exercise file
├─ programs/ Training programs
├─ nutrition/ Foods and meal ideas (meal totals are computed from foods)
├─ glossary.ts Glossary terms
├─ validate.ts All cross-checks (pure functions, unit-tested)
└─ data.ts Validated content for pages; throws at build time if anything is wrong
chest, upper-chest, front-delts, side-delts, rear-delts, rotator-cuff, lats,
upper-back, traps, lower-back, biceps, brachialis, forearms, triceps, abs,
obliques, glutes, glute-med, quads, hamstrings, adductors, hip-flexors, calves,
soleus. Names and descriptions live in src/content/muscles.ts.
npm run validate checksThe build fails if any of these is true:
id or slug, a filename doesn't match its slug, a file is in the wrong
category folder, or a file isn't listed in exercises/index.ts.alternatives, pairsWellWith or program slot points at an exercise that doesn't exist,
or an alternative doesn't train any of the exercise's primary muscles (as one of its own
primary or secondary muscles).weightGainFit, or a muscle is listed as both primary and secondary.coverage.ts is missing (once COVERAGE_MODE is "strict").Pick a slug: lowercase words joined by hyphens, e.g. pendulum-squat. The slug is the URL
(/exercises/pendulum-squat/) and the filename.
Create the file in the folder for its category: chest, back, shoulders, legs
(legs and glutes), arms, core or cardio.
// src/content/exercises/legs/pendulum-squat.ts
import { defineExercise } from "../../schema/exercise";
export default defineExercise({
id: "pendulum-squat", // never change this once you've logged sets against it
slug: "pendulum-squat",
name: "Pendulum squat",
// Your editor now autocompletes every remaining field and underlines any that are missing.
});
The quickest start is to copy an existing entry of the same equipment type and rewrite every field for the new machine.
Register it in src/content/exercises/index.ts: add an import and put it in the array.
Run npm run validate. It names every field that's missing or wrong, with its path, e.g.
exercise "pendulum-squat" → setup.2.instruction: must be at least 30 characters.
Link it up: add its slug to the alternatives of similar exercises so it shows up as a
substitute.
| Field | What to write |
|---|---|
id, slug | Usually the same. id is permanent; slug can change. |
alsoKnownAs | Other names people use; these make search work ("butterfly" finds the pec deck). |
summary | One line for cards and search results (≤160 characters). |
description | What it is, and how to recognise it in the gym. |
equipmentType | machine, cable, barbell, dumbbell, bodyweight or smith |
loadType | How resistance is set: stack, plates, dumbbells, bodyweight, assisted (less = harder), cardio |
unilateral | true if you do one side at a time |
category, pushPull, difficulty | See the enums in src/content/schema/shared.ts |
primaryMuscles, secondaryMuscles | Muscle ids from the list above; a muscle can't be in both |
setup[] | Ordered { label, instruction, rightWhen? }: seat, pads, pin, grip, feet, how to tell it's right |
execution[] | Ordered steps of one rep |
postureCues[] | { area, cue }, area being spine, shoulder-blades, neck, core, hips, knees, elbows, wrists or feet |
quickCues[] | 3–5 short cues for Quick view |
breathing | { inhale, exhale, note? } |
tempo | Reps: seconds for lower, pauseBottom, lift, pauseTop + note. Holds and cardio: guidance |
rangeOfMotion | { full, stopAt, ifLimited? } |
Use the canonical cue phrases from src/content/cues.ts word for word ("brace your core",
"shoulder blades back and down"), and the rep schemes from rep-schemes.ts, so every page speaks
the same language.
Programs live in src/content/programs/ (one file each, registered in index.ts). A program has
a weeklyLayout (all seven days; session: null is a rest day, optional: true marks an
optional recovery day) and sessions, each an ordered list of slots:
{ exercise: "hack-squat", scheme: "main-compound", sets: 3, fromWeek: 3, beforeThenUse: "leg-press" }
scheme is one of the rep schemes in rep-schemes.ts, and sets must fit its range. An
exercise above beginner difficulty must start at week 3 or later and name a beginner
beforeThenUse; the validator enforces both. The Today view applies the week 1–2 ramp and the
early-weeks stand-ins automatically.
Foods and meal ideas live in src/content/nutrition/. A food's calories must roughly agree with
its protein, carbs and fat (4/4/9 kcal per gram), and a meal's totals are always computed from
its foods.
Search (src/lib/search.ts) matches the name, alsoKnownAs, muscles, body part and
equipment, and tolerates typos. If people at your gym call a machine something else, add that
name to its alsoKnownAs. Gym shorthand that applies to many entries ("db" for dumbbell, "rdl")
lives in the SYNONYMS table at the top of search.ts. Add a test for any new phrase in
search.test.ts so the ranking stays right.
Put the image in public/media/ (e.g. public/media/leg-press.jpg, ideally under 200 KB) and set:
media: {
image: { src: "/media/leg-press.jpg", alt: "The leg press at my gym, seat reclined, feet mid-platform" },
videoUrl: null,
},
Empty media slots render nothing, so there's never an "image coming soon" box.
Settings, training logs, bodyweight, favourites, today's list, saved machine settings and the
rest timer are stored in localStorage on the device and nowhere else.
| Key | Contents |
|---|---|
gym:settings | Units, theme, your stats, program, timer |
gym:sessions | Logged workouts |
gym:bodyweight | Weigh-ins |
gym:favourites | Starred exercise ids |
gym:today | Today's workout list |
gym:machine-settings | Your seat/pad positions per machine |
gym:timer | The running rest timer |
Each value is an envelope, { "v": <version>, "data": … }, validated with zod/mini on every
read (src/lib/storage/). Unreadable data is never silently deleted: it is copied to
<key>:corrupt:<timestamp> and the app falls back to defaults. Settings shows when this has
happened and lets you download the unreadable copies.
Changing a stored shape: edit the schema in src/lib/storage/schemas.ts, bump version in
src/lib/storage/definitions.ts, and add a migration function that converts the previous version's
data. Saved data is upgraded the next time it's read. Add a test for the migration in
store.test.ts.
Clearing your browser data, or losing your phone, loses everything above. Settings → Download a
backup saves every store, envelope and all, as spotter-backup-<date>.json. Restore from a
backup reads the whole file and checks every part before changing anything, upgrades older
versions with the same migrations, then offers:
The same file moves your data between devices, or from the browser into the installed app.
The production build is a PWA: once a page has loaded, the whole site keeps working without a connection.
out/
in the background (the build prints the count and size). Next.js's small per-page data files are
cached as you browse instead; when one isn't cached and you're offline, a tapped link falls back
to a full page load, which comes from the cache.out/. When a new version has
downloaded, a banner offers Update or Later. Nothing reloads unless you tap Update, so a
set you're typing is never lost. Old caches are deleted once the new version takes over.npm run build, then npm run preview, open http://localhost:4173, stop the preview server and
reload. In Chrome DevTools, Application → Service workers and Cache storage show what's
installed, and Application → Storage → Clear site data removes it.To install it on a phone:
The PNG icons are drawn by scripts/generate-icons.ts, with no image library needed. To change
the design, edit the colours and shapes in that script, run npm run icons, and update
public/icons/icon.svg (the browser-tab icon) to match.
ESLint stays on 9.x because the plugins bundled with eslint-config-next don't declare
ESLint 10 support yet.
TypeScript stays on 6.0.x because typescript-eslint supports TypeScript below 6.1.
Dependency versions are pinned exactly in package.json.
Links use AppLink (src/components/ui/AppLink.tsx), not next/link; ESLint enforces
it. Next.js normally prefetches every link that scrolls into view, which ties up a phone's main
thread while the page starts. AppLink turns that off, and IntentPrefetch in the root layout
prefetches a page when you touch, hover over or tab to its link instead.
Lighthouse: build, run npm run preview, then in another terminal:
npx [email protected] http://localhost:4173/ --view
On a slower laptop the report warns that the CPU is slower than Lighthouse expects, and the
performance score comes out low. Lighthouse's
throttling docs
explain how to pick --throttling.cpuSlowdownMultiplier for your machine. For the real figure,
run PageSpeed Insights on the deployed site.
commonMistakes[] | { mistake, whyItMatters, fix }, at least 3 |
safety[], bailOut | Safety notes, and how to get out of a rep you can't finish |
beginnerStart | How to find a starting weight, plus the first prescription (dose) |
progression | A rule id from progression.ts, the increment, and exercise-specific notes |
variations[] (optional) | Grip or equipment variations that share this entry |
alternatives[], pairsWellWith[] | Slugs of other entries |
weightGainFit | Cardio entries only: how it fits a weight-gain goal |
media | { image: null, videoUrl: null } until you add your own (below) |