尚未检测到更新。GitHub 发布版本后会显示在这里。
个人 GitHub 账号下公开仓库的所有者可以登录并验证所有权,自行管理介绍,无需管理员批准。
登录并生成验证文件,在 30 分钟内将其提交到此仓库的默认分支。其他分支或分叉仓库中的文件无效。然后在下方检查文件,即可无需管理员批准直接管理项目。请保留文件以维持管理权限。
如需登录,将前往登录页面。登录后请回到此处,再次点击所选的验证按钮。
在项目文件夹中运行 /nomorevibe verify,即可验证所有权并自行管理介绍。
一分钟完成验证制作者的介绍仅有上面的一句话。 更多详情请阅读仓库 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) |