diff --git a/apps/mana/apps/web/src/lib/i18n/locales/body/de.json b/apps/mana/apps/web/src/lib/i18n/locales/body/de.json index 45ba89013..5331444b6 100644 --- a/apps/mana/apps/web/src/lib/i18n/locales/body/de.json +++ b/apps/mana/apps/web/src/lib/i18n/locales/body/de.json @@ -33,6 +33,37 @@ "cut": "Cut", "bulk": "Bulk", "maintenance": "Maintenance", - "recomp": "Recomp" + "recomp": "Recomp", + "start": "Phase starten", + "startNew": "Phase starten", + "end": "Beenden" + }, + "progression": "Progression", + "routines": { + "title": "Routinen", + "start": "Start", + "empty": "Noch keine Routinen" + }, + "exercisePicker": { + "title": "Übung wählen", + "pick": "Übung wählen", + "search": "Suchen…", + "empty": "Nichts gefunden", + "create": "Neue Übung anlegen" + }, + "muscle": { + "chest": "Brust", + "back": "Rücken", + "shoulders": "Schultern", + "biceps": "Bizeps", + "triceps": "Trizeps", + "forearms": "Unterarme", + "core": "Core", + "quads": "Quadrizeps", + "hamstrings": "Hamstrings", + "glutes": "Gesäß", + "calves": "Waden", + "cardio": "Cardio", + "fullbody": "Ganzkörper" } } diff --git a/apps/mana/apps/web/src/lib/i18n/locales/body/en.json b/apps/mana/apps/web/src/lib/i18n/locales/body/en.json index a557bbf7e..7741c5e2c 100644 --- a/apps/mana/apps/web/src/lib/i18n/locales/body/en.json +++ b/apps/mana/apps/web/src/lib/i18n/locales/body/en.json @@ -33,6 +33,37 @@ "cut": "Cut", "bulk": "Bulk", "maintenance": "Maintenance", - "recomp": "Recomp" + "recomp": "Recomp", + "start": "Start phase", + "startNew": "Start phase", + "end": "End" + }, + "progression": "Progression", + "routines": { + "title": "Routines", + "start": "Start", + "empty": "No routines yet" + }, + "exercisePicker": { + "title": "Pick an exercise", + "pick": "Pick exercise", + "search": "Search…", + "empty": "Nothing found", + "create": "New exercise" + }, + "muscle": { + "chest": "Chest", + "back": "Back", + "shoulders": "Shoulders", + "biceps": "Biceps", + "triceps": "Triceps", + "forearms": "Forearms", + "core": "Core", + "quads": "Quads", + "hamstrings": "Hamstrings", + "glutes": "Glutes", + "calves": "Calves", + "cardio": "Cardio", + "fullbody": "Full body" } } diff --git a/apps/mana/apps/web/src/lib/modules/body/ListView.svelte b/apps/mana/apps/web/src/lib/modules/body/ListView.svelte index 7794be11f..80cff3ad7 100644 --- a/apps/mana/apps/web/src/lib/modules/body/ListView.svelte +++ b/apps/mana/apps/web/src/lib/modules/body/ListView.svelte @@ -11,6 +11,7 @@ import type { Observable } from 'dexie'; import type { BodyExercise, + BodyRoutine, BodyWorkout, BodySet, BodyMeasurement, @@ -24,8 +25,12 @@ import WeightChart from './components/WeightChart.svelte'; import DailyCheckCard from './components/DailyCheckCard.svelte'; import RecentWorkouts from './components/RecentWorkouts.svelte'; + import RoutineManager from './components/RoutineManager.svelte'; + import PhaseManager from './components/PhaseManager.svelte'; + import ExerciseProgressionChart from './components/ExerciseProgressionChart.svelte'; const exercises$: Observable = getContext('bodyExercises'); + const routines$: Observable = getContext('bodyRoutines'); const workouts$: Observable = getContext('bodyWorkouts'); const sets$: Observable = getContext('bodySets'); const measurements$: Observable = getContext('bodyMeasurements'); @@ -33,6 +38,7 @@ const phases$: Observable = getContext('bodyPhases'); let exercises = $state([]); + let routines = $state([]); let workouts = $state([]); let sets = $state([]); let measurements = $state([]); @@ -43,6 +49,10 @@ const sub = exercises$.subscribe((v) => (exercises = v)); return () => sub.unsubscribe(); }); + $effect(() => { + const sub = routines$.subscribe((v) => (routines = v)); + return () => sub.unsubscribe(); + }); $effect(() => { const sub = workouts$.subscribe((v) => (workouts = v)); return () => sub.unsubscribe(); @@ -85,13 +95,12 @@ {$_('body.subtitle', { default: 'Training & Körper in einem Modul' })}

- {#if activePhase} -
- {$_(`body.phase.${activePhase.kind}`, { default: activePhase.kind })} -
- {/if} +
+ +
+
{#if activeWorkout} @@ -110,6 +119,15 @@ {/if}
+
+ +
+ +
+

{$_('body.progression', { default: 'Progression' })}

+ +
+

{$_('body.weight', { default: 'Gewicht' })}

@@ -151,28 +169,6 @@ color: hsl(var(--color-muted-foreground)); margin-top: 0.125rem; } - .phase-pill { - padding: 0.25rem 0.75rem; - border-radius: 999px; - font-size: 0.75rem; - font-weight: 600; - text-transform: uppercase; - letter-spacing: 0.05em; - background: hsl(var(--color-muted)); - color: hsl(var(--color-foreground)); - } - .phase-pill[data-kind='cut'] { - background: hsl(0 84% 60% / 0.15); - color: hsl(0 84% 50%); - } - .phase-pill[data-kind='bulk'] { - background: hsl(142 71% 45% / 0.15); - color: hsl(142 71% 38%); - } - .phase-pill[data-kind='maintenance'] { - background: hsl(217 91% 60% / 0.15); - color: hsl(217 91% 50%); - } .card { display: flex; flex-direction: column; diff --git a/apps/mana/apps/web/src/lib/modules/body/collections.ts b/apps/mana/apps/web/src/lib/modules/body/collections.ts index 89133d32c..d03f9a2ab 100644 --- a/apps/mana/apps/web/src/lib/modules/body/collections.ts +++ b/apps/mana/apps/web/src/lib/modules/body/collections.ts @@ -92,7 +92,37 @@ export const BODY_GUEST_SEED = { isPreset: true, }, ] satisfies LocalBodyExercise[], - bodyRoutines: [] satisfies LocalBodyRoutine[], + bodyRoutines: [ + { + id: 'body-routine-fullbody', + name: 'Full Body Starter', + description: 'Squat, Bench, Row — 3× pro Woche, der klassische Einstieg', + exerciseIds: ['body-exercise-squat', 'body-exercise-bench', 'body-exercise-row'], + order: 0, + isArchived: false, + }, + { + id: 'body-routine-upper', + name: 'Upper Day', + description: 'Push + Pull für den Oberkörper', + exerciseIds: [ + 'body-exercise-bench', + 'body-exercise-ohp', + 'body-exercise-row', + 'body-exercise-pullup', + ], + order: 1, + isArchived: false, + }, + { + id: 'body-routine-lower', + name: 'Lower Day', + description: 'Beine und Posterior Chain', + exerciseIds: ['body-exercise-squat', 'body-exercise-deadlift'], + order: 2, + isArchived: false, + }, + ] satisfies LocalBodyRoutine[], bodyWorkouts: [] satisfies LocalBodyWorkout[], bodySets: [] satisfies LocalBodySet[], bodyMeasurements: [] satisfies LocalBodyMeasurement[], diff --git a/apps/mana/apps/web/src/lib/modules/body/components/ExercisePicker.svelte b/apps/mana/apps/web/src/lib/modules/body/components/ExercisePicker.svelte new file mode 100644 index 000000000..294bf5a0c --- /dev/null +++ b/apps/mana/apps/web/src/lib/modules/body/components/ExercisePicker.svelte @@ -0,0 +1,405 @@ + + + + + + + + diff --git a/apps/mana/apps/web/src/lib/modules/body/components/ExerciseProgressionChart.svelte b/apps/mana/apps/web/src/lib/modules/body/components/ExerciseProgressionChart.svelte new file mode 100644 index 000000000..c8e370b5e --- /dev/null +++ b/apps/mana/apps/web/src/lib/modules/body/components/ExerciseProgressionChart.svelte @@ -0,0 +1,158 @@ + + + +
+ {#if !resolvedExercise || timeline.length === 0} +

Noch keine Sets geloggt

+ {:else} +
+
{resolvedExercise.name}
+
+ e1RM {latest.value} + kg + {#if timeline.length > 1} + 0} class:negative={delta < 0}> + {delta > 0 ? '+' : ''}{delta.toFixed(0)} + + {/if} +
+
+ + + + {#each timeline as p, i (p.date)} + {@const range = extent.max - extent.min || 1} + {@const stepX = (width - padX * 2) / Math.max(timeline.length - 1, 1)} + {@const x = padX + i * stepX} + {@const y = padY + (height - padY * 2) * (1 - (p.value - extent.min) / range)} + + {/each} + + {/if} +
+ + diff --git a/apps/mana/apps/web/src/lib/modules/body/components/PhaseManager.svelte b/apps/mana/apps/web/src/lib/modules/body/components/PhaseManager.svelte new file mode 100644 index 000000000..4303287c2 --- /dev/null +++ b/apps/mana/apps/web/src/lib/modules/body/components/PhaseManager.svelte @@ -0,0 +1,243 @@ + + + +
+ {#if activePhase} +
+
+
+ {$_(`body.phase.${activePhase.kind}`, { default: activePhase.kind })} +
+
+ seit {activePhase.startDate} + {#if activePhase.targetWeight} + · Ziel: {activePhase.targetWeight}kg + {/if} +
+
+ +
+ {:else if opening} +
(e.preventDefault(), start())}> +
+ {#each KINDS as k (k)} + + {/each} +
+
+ + +
+
+ + +
+
+ {:else} + + {/if} +
+ + diff --git a/apps/mana/apps/web/src/lib/modules/body/components/RoutineManager.svelte b/apps/mana/apps/web/src/lib/modules/body/components/RoutineManager.svelte new file mode 100644 index 000000000..84c1ecee3 --- /dev/null +++ b/apps/mana/apps/web/src/lib/modules/body/components/RoutineManager.svelte @@ -0,0 +1,253 @@ + + + +
+
+

{$_('body.routines.title', { default: 'Routinen' })}

+ +
+ + {#if creating} +
+ +
+ {#each exercises.filter((e) => !e.isArchived) as ex (ex.id)} + + {/each} +
+ +
+ {/if} + + {#if activeRoutines.length === 0} +

{$_('body.routines.empty', { default: 'Noch keine Routinen' })}

+ {:else} +
    + {#each activeRoutines as r (r.id)} +
  • +
    +
    {r.name}
    +
    + {r.exerciseIds.map(exerciseName).join(' · ')} +
    +
    +
    + + +
    +
  • + {/each} +
+ {/if} +
+ + diff --git a/apps/mana/apps/web/src/lib/modules/body/components/WorkoutLogger.svelte b/apps/mana/apps/web/src/lib/modules/body/components/WorkoutLogger.svelte index f39fe8408..523d274e7 100644 --- a/apps/mana/apps/web/src/lib/modules/body/components/WorkoutLogger.svelte +++ b/apps/mana/apps/web/src/lib/modules/body/components/WorkoutLogger.svelte @@ -13,7 +13,9 @@ import { _ } from 'svelte-i18n'; import type { BodyExercise, BodySet, BodyWorkout } from '../types'; import { bodyStore } from '../stores/body.svelte'; + import { getLastSetByExercise, relativeDays } from '../queries'; import SetRow from './SetRow.svelte'; + import ExercisePicker from './ExercisePicker.svelte'; interface Props { workout: BodyWorkout; @@ -26,6 +28,11 @@ let weight = $state(0); let reps = $state(8); let isWarmup = $state(false); + let pickerOpen = $state(false); + + let lastSets = $derived(getLastSetByExercise(sets)); + let selectedExercise = $derived(exercises.find((e) => e.id === selectedExerciseId) ?? null); + let lastForSelected = $derived(selectedExerciseId ? lastSets.get(selectedExerciseId) : null); // Pre-fill from the most recent set of the selected exercise so the // "next set" form starts at last week's working weight, not zero. @@ -108,11 +115,17 @@ addSet(); }} > - +