Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,12 @@

All notable changes to Workout Lens are documented here.

## [1.2.3] — 2026-05-11

### Added
- **First-login intro guide (#162)** — a 5-slide Carbon `Modal` (`passiveModal`) appears automatically for new users when `wl-intro-seen` is not set in localStorage. Each slide shows a 64px Carbon icon (`Camera`, `RecentlyViewed`, `Analytics`, `EventSchedule`, `Book`), a `PageHeading` title, and a body paragraph. Navigation: ghost "Hopp over" (any step) closes and sets the key; secondary "Forrige" + primary "Neste" step through slides 1–5; "Kom i gang" on the final slide closes and sets the key; the close (×) button also sets the key. A step indicator ("Steg N av 5") updates on every step; a replay hint appears on step 5 only. Settings → Om appen gains a ghost "Vis introduksjonsguide" button (`Information` icon) that clears `wl-intro-seen` and re-opens the modal from step 1. All strings translated in `nb`, `en`, and `fa`.
- **Theme FOUC fixes** — eliminated flash-of-unstyled-content on initial page load: (1) a blocking inline script in `index.html` sets `data-theme` on `<html>` before the JS bundle executes; (2) `ThemeProvider` also sets `data-theme` synchronously inside its `useState` lazy initialiser so the attribute is present before React's first commit.

## [1.2.2] — 2026-05-10

### Developer
Expand Down
3 changes: 2 additions & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -203,7 +203,8 @@ recommendation_cache
- `useIsMobile(breakpoint=500)` — exported hook from `bodymap.jsx`. Below breakpoint: single body view with Front/Bak toggle. Above: side-by-side. Consumed via `BodyPanel` — do not use directly in page components.
- **Shared exercise row:** `app/src/components/ExerciseRow.jsx` — renders one editable exercise row (checkbox, inline name edit, sets/reps inputs, delete). Props: `exercise`, `onChange(updates)`, `onDelete()`, `layer` ("layer-01"/"layer-02"), `validateNumbers`, `autoFocusName`, `onNameBlur` (optional callback fired when the name input blurs — used by `ExerciseRowWithAutocomplete` to trigger muscle inference). The outer row div has no click handler — only the Checkbox toggles `enabled` (prevents accidental untick when editing fields). Used by `MuscleMap.jsx`, `History.jsx`, and `TemplateSessionEditor.jsx`.
- **Planlegger:** `app/src/components/Planlegger.jsx` — weekly training planner view (issue #59). State: `weekOffset` (±week navigation), `assignments` (`{ [dow 1-7]: template | null }`), `templates`, `weekSessions` (logged sessions for the visible ISO week — issue #143), `pickerDow`, `saving`, `saveError`, `hoveredMuscle`. Computed via `useMemo`: `monday`, `weekIso`, `weekLabel` (built inline with `Intl.DateTimeFormat` for the locale-aware month abbreviation + `t("planlegger.weekLabel", ...)`), `untrainedThisWeekIds` (muscle IDs not trained in any logged session for the visible ISO week — derived from `weekSessions` via `extractMuscles`; issue #143), `projectedExerciseMap` (union of all assigned templates' exercises via `buildMuscleMapFromExercises`), `sessionCount`, `muscleGroupCount`, `untrainedMuscleIds`, `showForslag` (≥2 untrained muscles), `forslagTemplates` (up to 3 templates from library covering untrained muscles). Layout: week nav chevrons → `PageHeading` → `SectionLabel "IKKE TRENT DENNE UKEN"` → wrap row of mono pill chips (History-style: `var(--r-pill)`, `var(--border-subtle-wl)`, `var(--text-muted-wl)`, `var(--cds-font-mono)` 11px) listing muscles not yet trained that week (or a single mono message when all 17 are trained) → `SectionLabel "PROJISERT DEKNING"` → projected `HeatmapBodySVG` (side-by-side/toggle) → fixed-height 48px hover-detail container (always rendered, prevents layout shift) → optional Forslag card → `SectionLabel "UKESPLAN"` → 7 × DayRow → inline `TemplatePicker` bottom-sheet overlay. No sticky save/delete bar — plan auto-saves on every add/remove; `deleteWeekPlan` is called automatically when all slots are cleared. Persists via `fetchWeekPlan` / `saveWeekPlan` / `deleteWeekPlan` in `db.js`; loads logged sessions via `fetchSessionsForWeek` in parallel with the plan fetch. Duration (`N MIN`) omitted — `session_templates` has no duration column.
- **Settings:** `app/src/components/Settings.jsx` — settings view reachable via the gear icon in the header (issue #123). Sections in order: (1) Språk — `RadioButtonGroup` for nb/en/fa; calls `i18n.changeLanguage()` + persists to `localStorage`; (2) Utseende — Carbon `Toggle` for dark/light theme + Carbon `Toggle` for nav hints (`useNavHints()`) with a live `BodyPanel` preview (fixed sample: primary `chest, quads, lats`; secondary `shoulders_front, hamstrings, triceps`); (3) Kontakt — feedback text + GitHub link; (4) Om appen — version number + "Vis endringslogg" opening `ChangelogModal`; (5) Konto — logged-in email (read-only) + danger logout button. `ChangelogModal` is no longer rendered in `PageShell` — it lives here exclusively.
- **IntroModal:** `app/src/components/IntroModal.jsx` — one-time 5-slide onboarding modal (issue #162). Controlled by `open`/`onClose` props from `App.jsx`. Resets `step` to 0 via `useEffect` whenever `open` becomes true. `dismiss()` sets `localStorage` key `wl-intro-seen=1` then calls `onClose()`; the ×-close button and "Hopp over" also call `dismiss()`. Slide data is a static constant array of `{ Icon, titleKey, bodyKey }`. Step indicator and replay hint rendered in body below slide content. Responsive via an inline `<style>` block: max-width 560px on desktop, full-viewport on ≤500px. Wrapped in `<Theme>` matching the current app theme.
- **Settings:** `app/src/components/Settings.jsx` — settings view reachable via the gear icon in the header (issue #123). Accepts optional `onShowIntro` prop from `App.jsx`. Sections in order: (1) Språk — `RadioButtonGroup` for nb/en/fa; calls `i18n.changeLanguage()` + persists to `localStorage`; (2) Utseende — Carbon `Toggle` for dark/light theme + Carbon `Toggle` for nav hints (`useNavHints()`) with a live `BodyPanel` preview (fixed sample: primary `chest, quads, lats`; secondary `shoulders_front, hamstrings, triceps`); (3) Kontakt — feedback text + GitHub link; (4) Om appen — version number + ghost "Vis introduksjonsguide" button (calls `onShowIntro`) + changelog accordion; (5) Konto — logged-in email (read-only) + danger logout button. `ChangelogModal` is no longer rendered in `PageShell` — it lives here exclusively.
- **BodyPanel:** `app/src/components/BodyPanel.jsx` — shared front/back body map. Manages its own `mobileView` toggle state internally. Props: `primary[]`, `secondary[]`, `muscleMap`, `marginBottom`. Replaces the duplicated mobile/desktop render pattern that previously existed in `MuscleMap`, `History`, and `TemplateSessionEditor`.
- **MusclePicker:** `app/src/components/MusclePicker.jsx` — interactive body map where clicking a muscle cycles off → primary → secondary → off. Props: `primary[]`, `secondary[]`, `onChange({ primary, secondary })`, `instanceId` (unique suffix to avoid SVG filter ID collisions). Used inside `ExerciseForm.jsx`.
- **ExerciseForm:** `app/src/components/ExerciseForm.jsx` — form for creating/editing a library exercise (name, default sets/reps, MusclePicker). Props: `initial`, `onSave(fields)`, `onCancel()`, `saving`. On name field blur, fires `inferMusclesFromName` if no muscles are set yet — shows `InlineLoading` spinner → finished flourish → static AI label. Shows a red warning when name is filled but muscles are still empty. Extracted from inline definition in `Bibliotek.jsx`.
Expand Down
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@ Photograph a handwritten gym whiteboard workout, and the app tells you which mus
8. **Library** — build a named exercise library with click-to-toggle muscle selection; AI muscle inference fires when you type an exercise name and leave the field — muscles are filled in automatically and marked "Muskler satt av AI"; create session templates (e.g. "CrossFit - Anna - mandag") as reusable collections of library exercises
9. **Weekly planner** — assign templates to each day of the week; an "Ikke trent denne uken" chip row lists the muscles you have not yet trained in logged sessions for the visible ISO week (History-style mono pills); a live "Projisert dekning" heatmap body map shows projected cumulative muscle coverage from the assigned templates; a Forslag card flags muscle groups with no planned coverage; plan is saved to Supabase and reloaded on next visit
10. **Language** — switch between Norsk, English and فارسی (RTL) at any time from Settings; all UI strings, date formats, and month names update instantly
11. **Settings** — language selector (top), theme toggle (dark/light) + nav hints toggle with live body map preview, contact, changelog, and account section: display name input + sign-out (bottom)
11. **Settings** — language selector (top), theme toggle (dark/light) + nav hints toggle with live body map preview, contact, Om appen section (version + "Vis introduksjonsguide" replay button + changelog accordion), and account section: display name input + sign-out (bottom)
12. **First-login intro guide** — a 5-slide modal appears automatically on first login (gated by `localStorage` key `wl-intro-seen`); walks through Upload → History → Report → Planner → Library; skippable and replayable from Settings
12. **Joint class history** — when a gym-linked session is expanded in History, a "Kolleger i denne klassen" panel shows co-instructor sessions for the same class slot (display name + exercise list). All sessions are always visible to co-instructors at the same gym — this cross-instructor transparency is the core value of the shared view

## Tech stack
Expand Down
8 changes: 8 additions & 0 deletions app/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,14 @@
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Workout Lens</title>
<script>
(function () {
var s = localStorage.getItem('carbon-theme');
var t = (s === 'g10' || s === 'g100') ? s
: (window.matchMedia('(prefers-color-scheme: dark)').matches ? 'g100' : 'g10');
document.documentElement.setAttribute('data-theme', t);
})();
</script>
</head>
<body>
<div id="root"></div>
Expand Down
22 changes: 21 additions & 1 deletion app/public/locales/en/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -268,6 +268,25 @@
"7": "SUN"
}
},
"intro": {
"modalHeading": "Get started with Workout Lens",
"stepIndicator": "Step {{current}} of {{total}}",
"skip": "Skip",
"prev": "Previous",
"next": "Next",
"start": "Get started",
"replayHint": "You can always reopen this guide from Settings.",
"slide1Title": "Upload a training session",
"slide1Body": "Take a photo of the gym whiteboard or add exercises manually. Claude recognises the exercises and maps which muscles you trained.",
"slide2Title": "View your history",
"slide2Body": "All your sessions are collected in History. Edit exercises, change the date, and see which muscles were activated.",
"slide3Title": "Analyse your training",
"slide3Body": "Period reports show which muscles you hit regularly, which you neglect, and give you personalised recommendations.",
"slide4Title": "Plan the week",
"slide4Body": "Build weekly plans from your templates and see projected muscle coverage before you train.",
"slide5Title": "Build a library",
"slide5Body": "Save your own exercises with muscle mappings and build templates you can reuse week after week."
},
"settings": {
"heading": "Settings",
"appearance": "Appearance",
Expand Down Expand Up @@ -295,7 +314,8 @@
"myGym": "My gym",
"myGymMembership": "Sporty Thon Senter Ski",
"myGymFutureHint": "Coming soon: choose your gym and see shared session history with other instructors.",
"dataSharingNote": "All sessions you log are visible to other instructors at the same gym. This is necessary to give you insight into what your colleagues are training, and is part of the service's purpose."
"dataSharingNote": "All sessions you log are visible to other instructors at the same gym. This is necessary to give you insight into what your colleagues are training, and is part of the service's purpose.",
"showIntroGuide": "Show intro guide"
},
"report": {
"heroMuscles_one": "{{count}} muscle",
Expand Down
22 changes: 21 additions & 1 deletion app/public/locales/fa/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -268,6 +268,25 @@
"7": "یکشنبه"
}
},
"intro": {
"modalHeading": "شروع با Workout Lens",
"stepIndicator": "مرحله {{current}} از {{total}}",
"skip": "رد کردن",
"prev": "قبلی",
"next": "بعدی",
"start": "شروع",
"replayHint": "می‌توانید این راهنما را از تنظیمات دوباره مشاهده کنید.",
"slide1Title": "آپلود جلسه تمرینی",
"slide1Body": "از تخته تمرین عکس بگیرید یا تمرین‌ها را دستی وارد کنید. Claude تمرین‌ها را شناسایی می‌کند و نشان می‌دهد کدام عضلات را تمرین دادید.",
"slide2Title": "مشاهده تاریخچه",
"slide2Body": "تمام جلسات در تاریخچه جمع‌آوری می‌شود. می‌توانید تمرین‌ها را ویرایش کنید، تاریخ را تغییر دهید و عضلات فعال‌شده را ببینید.",
"slide3Title": "تحلیل تمرین",
"slide3Body": "گزارش‌های دوره نشان می‌دهد کدام عضلات را منظم تمرین می‌دهید، کدام را فراموش می‌کنید و پیشنهادهای شخصی ارائه می‌دهد.",
"slide4Title": "برنامه‌ریزی هفته",
"slide4Body": "با قالب‌هایتان برنامه هفتگی بسازید و پوشش عضلانی پیش‌بینی‌شده را قبل از تمرین ببینید.",
"slide5Title": "ساختن کتابخانه",
"slide5Body": "تمرین‌های خود را با نگاشت عضلات ذخیره کنید و قالب‌هایی بسازید که هفته به هفته می‌توانید از آن‌ها استفاده کنید."
},
"settings": {
"heading": "تنظیمات",
"appearance": "ظاهر",
Expand Down Expand Up @@ -295,7 +314,8 @@
"myGym": "باشگاه من",
"myGymMembership": "Sporty Thon Senter Ski",
"myGymFutureHint": "به زودی: انتخاب باشگاه و مشاهده تاریخچه مشترک با سایر مربیان.",
"dataSharingNote": "تمام جلسات ثبت‌شده برای سایر مربیان در همان مرکز ورزشی قابل مشاهده است. این برای ارائه بینش درباره تمرین همکاران ضروری است."
"dataSharingNote": "تمام جلسات ثبت‌شده برای سایر مربیان در همان مرکز ورزشی قابل مشاهده است. این برای ارائه بینش درباره تمرین همکاران ضروری است.",
"showIntroGuide": "نمایش راهنمای معارفه"
},
"report": {
"heroMuscles_one": "{{count}} عضله",
Expand Down
22 changes: 21 additions & 1 deletion app/public/locales/nb/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -268,6 +268,25 @@
"7": "SØN"
}
},
"intro": {
"modalHeading": "Kom i gang med Workout Lens",
"stepIndicator": "Steg {{current}} av {{total}}",
"skip": "Hopp over",
"prev": "Forrige",
"next": "Neste",
"start": "Kom i gang",
"replayHint": "Du kan alltid åpne denne guiden igjen under Innstillinger.",
"slide1Title": "Last opp treningsøkt",
"slide1Body": "Ta bilde av treningstavla eller legg inn øvelser manuelt. Claude gjenkjenner øvelsene og kartlegger hvilke muskler du har trent.",
"slide2Title": "Se historikken din",
"slide2Body": "Alle øktene dine samles i Historikk. Du kan redigere øvelser, endre dato og se hvilke muskler som ble aktivert.",
"slide3Title": "Analyser treningen",
"slide3Body": "Perioderapporter viser hvilke muskler du treffer jevnlig, hvilke du glemmer, og gir deg personlige anbefalinger.",
"slide4Title": "Planlegg uken",
"slide4Body": "Bygg ukesplaner fra malene dine og se projisert muskeldekning før du trener.",
"slide5Title": "Bygg et bibliotek",
"slide5Body": "Lagre dine egne øvelser med muskeltilknytning og bygg maler du kan gjenbruke uke etter uke."
},
"settings": {
"heading": "Innstillinger",
"appearance": "Utseende",
Expand Down Expand Up @@ -295,7 +314,8 @@
"myGym": "Min gym",
"myGymMembership": "Sporty Thon Senter Ski",
"myGymFutureHint": "Fremtidig: velg gym og se felles økthistorikk med andre instruktører.",
"dataSharingNote": "Alle økter du logger er synlige for andre instruktører ved samme treningssenter. Dette er nødvendig for å gi deg innsikt i hva kollegene dine trener, og er en del av tjenestens formål."
"dataSharingNote": "Alle økter du logger er synlige for andre instruktører ved samme treningssenter. Dette er nødvendig for å gi deg innsikt i hva kollegene dine trener, og er en del av tjenestens formål.",
"showIntroGuide": "Vis introduksjonsguide"
},
"report": {
"heroMuscles_one": "{{count}} muskel",
Expand Down
14 changes: 13 additions & 1 deletion app/src/App.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import TemplatePicker from "./components/TemplatePicker";
import TemplateSessionEditor from "./components/TemplateSessionEditor";
import Settings from "./components/Settings";
import Planlegger from "./components/Planlegger";
import IntroModal from "./components/IntroModal";

function App() {
const [session, setSession] = useState(undefined);
Expand All @@ -21,6 +22,7 @@ function App() {
const [historyInitialDate, setHistoryInitialDate] = useState(null);
const [bibliotekInitialTab, setBibliotekInitialTab] = useState(0);
const [reportPrefill, setReportPrefill] = useState(null);
const [introOpen, setIntroOpen] = useState(false);

useEffect(() => {
supabase.auth.getSession().then(({ data: { session } }) => {
Expand All @@ -34,6 +36,15 @@ function App() {
return () => subscription.unsubscribe();
}, []);

useEffect(() => {
if (session && !localStorage.getItem("wl-intro-seen")) setIntroOpen(true);
}, [session]);

function handleShowIntro() {
localStorage.removeItem("wl-intro-seen");
setIntroOpen(true);
}

if (session === undefined) return null;
if (!session) return <Login />;

Expand Down Expand Up @@ -76,7 +87,7 @@ function App() {
}}
/>;
else if (view === "settings")
content = <Settings />;
content = <Settings onShowIntro={handleShowIntro} />;
else if (view === "template-editor" && templateEditorState)
content = <TemplateSessionEditor
template={templateEditorState.template}
Expand Down Expand Up @@ -107,6 +118,7 @@ function App() {
return (
<NavContext.Provider value={navValue}>
{content}
{introOpen && <IntroModal open={true} onClose={() => setIntroOpen(false)} />}
</NavContext.Provider>
);
}
Expand Down
Loading
Loading