From f50c38728ad78df39d2fc895be244919f6fabc63 Mon Sep 17 00:00:00 2001 From: Christopher Rotnes Date: Sun, 17 May 2026 18:49:48 +0200 Subject: [PATCH] =?UTF-8?q?fix(#253):=20resolve=20all=208=20ESLint=20warni?= =?UTF-8?q?ngs=20=E2=80=94=200=20problems=20on=20npm=20run=20lint?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Split bodymap.jsx → bodymap.js (constants/utils) + bodymap.jsx (components only) to fix 5 fast-refresh warnings; updated all 20 import sites - Moved useTheme + ThemeCtx from theme.jsx to hooks.js; theme.jsx now only exports ThemeProvider (1 fast-refresh warning) - Moved useNavHints from PageShell.jsx to hooks.js (1 fast-refresh warning) - Replaced standalone setState-in-effect useEffect in App.jsx with inline checks inside the existing Supabase auth event handlers (1 warning) - Bumped version to 1.5.14 Co-Authored-By: Claude Sonnet 4.6 --- CHANGELOG.md | 9 ++ CLAUDE.md | 20 ++-- app/package.json | 2 +- app/src/App.jsx | 10 +- app/src/components/BodyPanel.jsx | 3 +- app/src/components/ChangelogModal.jsx | 2 +- app/src/components/ExFlyt.jsx | 2 +- app/src/components/GruppetimeEditor.jsx | 2 +- app/src/components/History.jsx | 2 +- app/src/components/IntroModal.jsx | 2 +- app/src/components/MuscleMap.jsx | 2 +- app/src/components/MuscleMapConfirm.jsx | 2 +- app/src/components/MuscleMapResult.jsx | 2 +- app/src/components/MusclePicker.jsx | 2 +- app/src/components/OvelsePicker.jsx | 2 +- app/src/components/PageShell.jsx | 27 +---- app/src/components/Planlegger.jsx | 3 +- app/src/components/Report.jsx | 3 +- app/src/components/SessionEditPanel.jsx | 2 +- app/src/components/Settings.jsx | 4 +- app/src/components/TemplateSessionEditor.jsx | 2 +- app/src/lib/__tests__/bodymap.test.js | 2 +- app/src/lib/__tests__/prompts.test.js | 2 +- app/src/lib/bodymap.js | 115 +++++++++++++++++++ app/src/lib/bodymap.jsx | 115 +------------------ app/src/lib/hooks.js | 32 +++++- app/src/lib/muscleMapReducer.js | 2 +- app/src/lib/prompts.js | 2 +- app/src/lib/utils.js | 2 +- app/src/theme.jsx | 7 +- 30 files changed, 202 insertions(+), 182 deletions(-) create mode 100644 app/src/lib/bodymap.js diff --git a/CHANGELOG.md b/CHANGELOG.md index b112438..bf48c48 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,15 @@ All notable changes to Workout Lens are documented here. +## [1.5.14] — 2026-05-17 + +### Developer / Infrastructure +- **Resolve all ESLint warnings (issue #253)** — `npm run lint` now exits with 0 problems. Changes are purely structural with no runtime impact: + - Split `bodymap.jsx` into `bodymap.js` (constants, `MUSCLES`, `SHAPES`, `EX_DB`, `calcMuscles`, `useIsMobile`, color constants) and `bodymap.jsx` (only `BodySVG` + `HeatmapBodySVG`); fixes 5 fast-refresh warnings. + - Moved `useTheme` and `ThemeCtx` from `theme.jsx` to `hooks.js`; `theme.jsx` now exports only `ThemeProvider`; fixes 1 fast-refresh warning. + - Moved `useNavHints` from `PageShell.jsx` to `hooks.js`; fixes 1 fast-refresh warning. + - Removed the standalone `useEffect` in `App.jsx` that called `setIntroOpen` synchronously — merged the intro check into the existing Supabase auth event handlers; fixes 1 `set-state-in-effect` warning. + ## [1.5.13] — 2026-05-16 ### Developer / Infrastructure diff --git a/CLAUDE.md b/CLAUDE.md index f27540b..b3a9021 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -51,7 +51,7 @@ Canonical definitions for domain terms. When a term is ambiguous in an issue or | Term | Definition | |---|---| -| **Muscle ID** | One of 17 fixed string keys (e.g. `chest`, `lats`, `quads`). The canonical identifier used in the DB, prompts, and bodymap. Full list in `MUSCLES` in `bodymap.jsx`. | +| **Muscle ID** | One of 17 fixed string keys (e.g. `chest`, `lats`, `quads`). The canonical identifier used in the DB, prompts, and bodymap. Full list in `MUSCLES` in `bodymap.js`. | | **Primary muscle** | A muscle directly targeted by an exercise. `muscle_activations.activation_type = 'primary'`. Shown as solid green on the body map. | | **Secondary muscle** | A muscle engaged in a supporting/stabilising role. `activation_type = 'secondary'`. Shown as blue diagonal hatch on the body map. | | **Muscle activation** | A DB record linking a session exercise to a muscle ID with a type. Stored in `muscle_activations`. | @@ -83,14 +83,14 @@ Canonical definitions for domain terms. When a term is ambiguous in an issue or chest, shoulders_front, shoulders_side, biceps, forearms, abs, obliques, quads, calves traps, rear_delts, lats, triceps, lower_back, glutes, hamstrings, calves_back ``` -Each has a `view` (front/back) and Norwegian `label` in the `MUSCLES` object in `app/src/lib/bodymap.jsx`. +Each has a `view` (front/back) and Norwegian `label` in the `MUSCLES` object in `app/src/lib/bodymap.js`. ## Carbon design system Uses `@carbon/react` and `@carbon/icons-react`. IBM Plex fonts (Sans, Mono, Serif, Condensed) bundled locally in `app/public/fonts/` — no Google Fonts, no CDN. - `app/src/styles/carbon-tokens.css` — all Carbon CSS variables for g10 (light) and g100 (dark) themes, plus `@font-face` declarations; font URLs use `/fonts/...` (Vite public-dir absolute paths) -- `app/src/theme.jsx` — `ThemeProvider` sets `data-theme="g10"` or `data-theme="g100"` on ``, persists to `localStorage`. Default (no saved preference): respects OS `prefers-color-scheme` — dark OS → g100, light OS → g10. This is intentional; the app does not force dark mode on first visit. +- `app/src/theme.jsx` — exports only `ThemeProvider`, which sets `data-theme="g10"` or `data-theme="g100"` on ``, persists to `localStorage`. Default (no saved preference): respects OS `prefers-color-scheme` — dark OS → g100, light OS → g10. `ThemeCtx` lives in `hooks.js`; `useTheme` is imported from there, not from `theme.jsx`. - `Login.jsx` → Carbon `TextInput`, `Button`, `InlineNotification`, `Email` icon; `getDailyQuote()` renders a date-aware motivational quote below the subtitle — English only (hardcoded; language preference is unknown before login); keyed by `MM-DD` for special dates (`01-01`, `12-24`), falls back to a per-weekday quote; 13px italic `var(--cds-text-secondary)` - `MuscleMap.jsx` → orchestrator (352 lines): `useReducer` + all 4 `useEffect` hooks + `addImage`/`handleFiles`/`analyze`/`confirm`/`recommend` callbacks + step-indicator strip; delegates rendering to `MuscleMapUpload`, `MuscleMapConfirm`, `MuscleMapResult`; exports `initialState`, `reducer`, `localDateStr`. Sub-components: `MuscleMapUpload.jsx` (dropzone, image grid, ghost shortcuts, analyze CTA); `MuscleMapConfirm.jsx` (layer-02 wrapper, today/other-day pill, date picker, gym-class selector, exercise list, confidence dots, save CTA; includes `getConfidenceColor`); `MuscleMapResult.jsx` (KPI strip, save status, body map, muscle chips, exercise list, recommendations). - `History.jsx` → orchestrator (525 lines): all state (sessions, selectedDate, muscleFilter, sessionEdits Map, classHistory Map), all callbacks, session-row header rendering; delegates expanded panel to `SessionEditPanel` and calendar to `MonthGrid`. `MonthGrid.jsx` (103 lines): 7-column CSS grid heatmap, today/selected outlines, interactive day buttons; includes `calHeatColor`. `SessionEditPanel.jsx` (218 lines): gym-class selector, `BodyPanel`, hover-detail card, exercise list via `ExerciseRowWithAutocomplete`, re-upload button, class-history panel, dirty-state save/discard bar; imports `checkGymCalendarConflict` directly. Per-session edit state is `Map` (no global `editMode` boolean); `PageHeading` has `minHeight: 72` to prevent layout shift; all date formatting via `Intl.DateTimeFormat`. @@ -105,7 +105,7 @@ Uses `@carbon/react` and `@carbon/icons-react`. IBM Plex fonts (Sans, Mono, Seri - `BodySVG` / `HeatmapBodySVG` muscle highlights: primary → `var(--heat-4)` solid green, secondary → diagonal blue hatch (`#001d6c` base + `#4589ff` lines). `HeatmapBodySVG` accepts `onHover(id|null)` and `hovered` props — when `onHover` is provided the internal floating tooltip is suppressed and the caller manages the detail card. - `Home.jsx` → `SectionLabel` + `PageHeading` headings; last session card with gym-class identity hero; 7-day weekly strip with heat colors — clicking a day that has a session navigates to History pre-selected on that date; `fetchThisWeekSessions` in `db.js` - `Report.jsx` → `SectionLabel` eyebrow with period + active day filters on two separate `display:block` spans; three separate `flexWrap: wrap` filter rows (period / weekdays / session types) with `1px solid var(--border-subtle-wl)` top borders between groups; "Nullstill filter" always rendered (opacity-toggled); KPI tiles → heatmap body → hover detail → heat legend → frequency table → gap callout card (with `AccentChip` per untrained muscle) → recommendation button → recs list; when all primary muscles trained shows positive fallback message; when some muscles secondary-only shows those as blue tags; recommendation rows have 3px accent left strip + round `+` button that saves the exercise inline via `saveLibraryExercise`; "Oppdater anbefalinger" ghost button (`Renew` icon) below the recs list — re-runs Claude call and overwrites the cache entry; no `StickyCta`; recs are persisted in the shared `recommendation_cache` Supabase table (see data model) and restored on mount/filter-change via `fetchRecsCache`; prefill prop applied on mount via `useRef` — supports `periodDays`, `selectedDays`, `selectedTypes`, `weekday`, `sessionType`; `KpiTile` (42px Plex Light value); `muscleLastDate` in useMemo -- `PageShell.jsx` → exports: `SectionLabel` (mono 12px, 0.16em tracking, 3px `var(--accent)` left border; accepts optional `renderIcon` prop — renders the Carbon icon at 14px before the label text), `PageHeading` (Cond 700 28px), `PageTitle` (alias for SectionLabel), `AccentChip` (magenta pill: `var(--accent-bg-14)` bg, `var(--accent-soft)` text), `StickyCta` (sticky bottom bar with top border), `BackButton`, `useNavHints()` hook (returns `[hints: boolean, toggle(val): void]`; reads/writes `localStorage` key `wl-nav-hints`, defaults `true`; syncs across all instances in the same tab via a `wl-nav-hints-change` custom event); `NavBtn` is a `forwardRef` component accepting `l1` and `l2` props — renders a 2-line Plex Condensed (8px) label below the icon; nav bar height is 56px; nav icons in order: Camera → RecentlyViewed → Analytics → EventSchedule (Planlegger) → Notebook (Sett-sammen) → Settings — 6 icons each 48px wide; theme toggle and logout removed from header (now in Settings view); `ChangelogModal` no longer rendered here +- `PageShell.jsx` → exports: `SectionLabel` (mono 12px, 0.16em tracking, 3px `var(--accent)` left border; accepts optional `renderIcon` prop — renders the Carbon icon at 14px before the label text), `PageHeading` (Cond 700 28px), `PageTitle` (alias for SectionLabel), `AccentChip` (magenta pill: `var(--accent-bg-14)` bg, `var(--accent-soft)` text), `StickyCta` (sticky bottom bar with top border), `BackButton`; `NavBtn` is a `forwardRef` component accepting `l1` and `l2` props — renders a 2-line Plex Condensed (8px) label below the icon; nav bar height is 56px; nav icons in order: Camera → RecentlyViewed → Analytics → EventSchedule (Planlegger) → Notebook (Sett-sammen) → Settings — 6 icons each 48px wide; theme toggle and logout removed from header (now in Settings view); `ChangelogModal` no longer rendered here. `useNavHints` moved to `app/src/lib/hooks.js` (issue #253) - `carbon-tokens.css` → added `--heat-1..5` green scale (#044317 → #42be65); WL custom tokens: `--accent` (#ee2c80 magenta), `--surface-card`, `--border-subtle-wl`, `--text-muted-wl`, `--accent-bg-08/14/30`, `--accent-soft`, `--r-card` (16px), `--r-pill` (999px), `--r-tile` (10px), `--cond` (IBM Plex Sans Condensed), `--exercise` (#7af2a4 green, g10 override #1a8c4e), `--exercise-soft` (rgba 12%), `--exercise-mid` (rgba 35%); g10 light-mode overrides for all WL tokens - `app.css` → global `html, body { overflow-x: hidden }` to prevent horizontal viewport bleed from chip rows; do not use `overflow: hidden` on direct parents of `flexWrap: wrap` chip containers — it clips instead of scrolling ### Hard rules (must not regress) @@ -247,13 +247,15 @@ recommendation_cache ## Key architecture decisions - **i18n:** `app/src/lib/i18n.js` initialises `i18next` with `fallbackLng: "nb"` and three resource bundles (`nb`, `en`, `fa`). All components use `useTranslation()` for strings. All locale-aware date/time rendering uses `Intl.DateTimeFormat` with a `getIntlLocale()` helper that maps `"nb" → "no"` (the IETF tag `Intl` expects). Never use hardcoded locale strings like `"no-NO"` or `date-fns` locale objects — they break when the user switches language. The `i18n` singleton can be imported directly (`import i18n from "../lib/i18n"`) for `i18n.language` access outside hooks. RTL (`dir="rtl"`) is applied to `` automatically on language change. -- **Shared muscle/SVG module:** `app/src/lib/bodymap.jsx` exports `MUSCLES`, `SHAPES`, `EX_DB`, color constants (`PRIMARY_FILL`, `PRIMARY_HOVER`, `PRIMARY_STROKE`, heat vars), `calcMuscles`, `BodySVG`, `HeatmapBodySVG` (accepts `onHover(id|null)` and `hovered` props — when `onHover` is set the internal tooltip is suppressed), and `useIsMobile`. Do not duplicate these in component files. -- **Shared hooks:** `app/src/lib/hooks.js` — exports `useDebouncedSearch(delayMs?)` (returns `{ search, setSearch, debouncedSearch }`; debounced value is raw — trim/lowercase at use-site) and `useFetch(fn, deps?)` (returns `{ data, loading, error, setData }`; `setData` allows optimistic updates; use only for data fetched once per mount without mutation-driven refetches). Import from here; do not copy the timer pattern locally. +- **Shared muscle/SVG module — split into two files (issue #253):** + - `app/src/lib/bodymap.js` — all non-component exports: `MUSCLES`, `SHAPES`, `EX_DB`, `BODY_PATH`, `BODY_POLY`, color constants (`PRIMARY_FILL`, `PRIMARY_HOVER`, `PRIMARY_STROKE`), `calcMuscles`, `useIsMobile`. Import from here for constants and utilities. + - `app/src/lib/bodymap.jsx` — only `BodySVG` and `HeatmapBodySVG` components (imports from `bodymap.js`). Import using the explicit `.jsx` extension when importing these components. Do not re-add non-component exports here. +- **Shared hooks:** `app/src/lib/hooks.js` — exports `useDebouncedSearch(delayMs?)` (returns `{ search, setSearch, debouncedSearch }`; debounced value is raw — trim/lowercase at use-site), `useFetch(fn, deps?)` (returns `{ data, loading, error, setData }`; `setData` allows optimistic updates; use only for data fetched once per mount without mutation-driven refetches), `ThemeCtx` (the React context used by `ThemeProvider`), `useTheme()` (returns `{ theme, setTheme }` from `ThemeCtx`), and `useNavHints()` (returns `[hints: boolean, toggle(val): void]`; reads/writes `localStorage` key `wl-nav-hints`). Import from here; do not copy these patterns locally. - **Shared utilities:** `app/src/lib/utils.js` — exports `toBase64`, `getMediaType`, `buildMuscleMapFromExercises` (with EX_DB fallback, for confirm/edit steps), `buildMuscleMapFromSession` (reads saved DB session for History read mode), `buildRecMuscleMap` (for recommendation body maps), `isInvalidNum` (validates sets/reps as integers 1–99), `callClaude(body)` (authenticated fetch to `/api/claude` — returns raw `Response`; always call `await res.json()` to read the body), `inferMusclesFromName(name)` (calls Claude Sonnet text API to infer muscle IDs for a single exercise name — returns `{ primary, secondary }` or `null`; handles markdown code fences defensively), `extractMuscles(session)` (splits `muscle_activations` into primary/secondary Sets, removes primary from secondary), `toWeekIso(date)` (Date → `"2026-W19"` ISO week string), `weekIsoToMonday(weekIso)` (`"2026-W19"` → Monday `Date`), `isoWeekMonday(date)` (Date → Monday `Date` of that ISO week, local time), `toIsoDate(date)` (Date → `"yyyy-MM-dd"` string using local time getters — replaces `date-fns` `format`), `getIntlLocale()` (maps `i18n.language` to the IETF tag `Intl` expects, e.g. `"nb" → "no"`). Do not redefine these locally in component files. - **Shared Claude config:** `app/src/lib/prompts.js` — exports `CLAUDE_MODEL_VISION` (sonnet-4-6, for image analysis), `CLAUDE_MODEL_TEXT` (sonnet-4-6, for recommendations), `RECS_PROMPT_VERSION` (integer — bump whenever `buildPeriodRecommendPrompt` or the model changes; old cache entries are swept by the weekly cleanup job; **must also be bumped in `app/api/recsCacheCleanup.js`** — a CI test in `app/api/__tests__/recsVersion.test.js` fails if the two values drift), `ANALYZE_PROMPT`, `buildRecommendPrompt(trained, untrained)`, `buildPeriodRecommendPrompt(periodDays, sessionCount, trainedLabels, untrainedLabels)`, `buildMuscleInferencePrompt(name)` (cheap text-only call for single-exercise muscle inference — strips `<>` from the name and wraps it in `` XML tags per Anthropic's prompt injection boundary pattern; 3 regression tests in `prompts.test.js`). All model IDs and prompt text live here; update in one place. - Claude returns muscle IDs directly in JSON — local keyword matching (EX_DB) was abandoned because Norwegian abbreviations and whiteboard variants didn't match reliably. EX_DB is kept only as fallback for manually added exercises. - SVG body uses `BODY_PATH` (bezier curves, viewBox `0 0 160 360`) — improved silhouette with curved shoulders, arms, waist and hips. Still simplified, not anatomically precise. `SHAPES` entries are either ellipses (`{ cx, cy, rx, ry }`) or SVG paths (`{ d }`); the render loop handles both. Key muscles with path shapes: `traps` (trapezoid with neck notch), `lats` (wing paths). `BodySVG` renders primary muscles as solid green glow, secondary as diagonal blue stripes (``). -- `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. +- `useIsMobile(breakpoint=500)` — exported hook from `bodymap.js`. 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, 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. - **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 `