diff --git a/apps/mana/apps/web/src/lib/components/dashboard/widget-registry.ts b/apps/mana/apps/web/src/lib/components/dashboard/widget-registry.ts index a80a0b8a2..6782c699e 100644 --- a/apps/mana/apps/web/src/lib/components/dashboard/widget-registry.ts +++ b/apps/mana/apps/web/src/lib/components/dashboard/widget-registry.ts @@ -32,6 +32,7 @@ import NutritionProgressWidget from '$lib/modules/core/widgets/NutritionProgress import PlantWateringWidget from '$lib/modules/core/widgets/PlantWateringWidget.svelte'; import CyclesWidget from '$lib/modules/core/widgets/CyclesWidget.svelte'; import NewsUnreadWidget from '$lib/modules/news/widgets/NewsUnreadWidget.svelte'; +import BodyStatsWidget from '$lib/modules/body/widgets/BodyStatsWidget.svelte'; import DayTimelineWidget from './widgets/DayTimelineWidget.svelte'; import ActivityFeedWidget from './widgets/ActivityFeedWidget.svelte'; @@ -60,4 +61,5 @@ export const widgetComponents: Record = { 'activity-feed': ActivityFeedWidget, cycles: CyclesWidget, 'news-unread': NewsUnreadWidget, + 'body-stats': BodyStatsWidget, }; diff --git a/apps/mana/apps/web/src/lib/i18n/index.ts b/apps/mana/apps/web/src/lib/i18n/index.ts index 68569838b..6a9be78e0 100644 --- a/apps/mana/apps/web/src/lib/i18n/index.ts +++ b/apps/mana/apps/web/src/lib/i18n/index.ts @@ -50,6 +50,7 @@ function registerLocale(lang: SupportedLocale) { help, cycles, news, + body, ] = await Promise.all([ import(`./locales/apps/${lang}.json`), import(`./locales/common/${lang}.json`), @@ -85,6 +86,7 @@ function registerLocale(lang: SupportedLocale) { import(`./locales/help/${lang}.json`), import(`./locales/cycles/${lang}.json`), import(`./locales/news/${lang}.json`), + import(`./locales/body/${lang}.json`), ]); return { @@ -122,6 +124,7 @@ function registerLocale(lang: SupportedLocale) { help: help.default, cycles: cycles.default, news: news.default, + body: body.default, }; }); } 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 new file mode 100644 index 000000000..45ba89013 --- /dev/null +++ b/apps/mana/apps/web/src/lib/i18n/locales/body/de.json @@ -0,0 +1,38 @@ +{ + "title": "Body", + "subtitle": "Training & Körper in einem Modul", + "activeWorkout": "Aktives Workout", + "finish": "Beenden", + "readyToTrain": "Bereit für ein Workout?", + "startHint": "Starte eine neue Session und logge deine Sätze", + "startWorkout": "Workout starten", + "weight": "Gewicht", + "dailyCheck": "Heute", + "recent": "Letzte Workouts", + "noWorkouts": "Noch keine Sessions", + "log": "Loggen", + "measurement": { + "weight": "Gewicht", + "bodyfat": "Körperfett", + "muscle": "Muskelmasse", + "chest": "Brust", + "waist": "Taille", + "hips": "Hüfte", + "thigh": "Oberschenkel", + "arm": "Arm", + "calf": "Wade", + "neck": "Hals" + }, + "check": { + "energy": "Energie", + "sleep": "Schlaf", + "soreness": "Muskelkater", + "mood": "Stimmung" + }, + "phase": { + "cut": "Cut", + "bulk": "Bulk", + "maintenance": "Maintenance", + "recomp": "Recomp" + } +} 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 new file mode 100644 index 000000000..a557bbf7e --- /dev/null +++ b/apps/mana/apps/web/src/lib/i18n/locales/body/en.json @@ -0,0 +1,38 @@ +{ + "title": "Body", + "subtitle": "Training & body tracking in one module", + "activeWorkout": "Active workout", + "finish": "Finish", + "readyToTrain": "Ready to train?", + "startHint": "Start a new session and log your sets", + "startWorkout": "Start workout", + "weight": "Weight", + "dailyCheck": "Today", + "recent": "Recent workouts", + "noWorkouts": "No sessions yet", + "log": "Log", + "measurement": { + "weight": "Weight", + "bodyfat": "Body fat", + "muscle": "Muscle mass", + "chest": "Chest", + "waist": "Waist", + "hips": "Hips", + "thigh": "Thigh", + "arm": "Arm", + "calf": "Calf", + "neck": "Neck" + }, + "check": { + "energy": "Energy", + "sleep": "Sleep", + "soreness": "Soreness", + "mood": "Mood" + }, + "phase": { + "cut": "Cut", + "bulk": "Bulk", + "maintenance": "Maintenance", + "recomp": "Recomp" + } +} diff --git a/apps/mana/apps/web/src/lib/i18n/locales/body/es.json b/apps/mana/apps/web/src/lib/i18n/locales/body/es.json new file mode 100644 index 000000000..38b57f5dd --- /dev/null +++ b/apps/mana/apps/web/src/lib/i18n/locales/body/es.json @@ -0,0 +1,38 @@ +{ + "title": "Body", + "subtitle": "Entrenamiento y cuerpo en un solo módulo", + "activeWorkout": "Entrenamiento activo", + "finish": "Terminar", + "readyToTrain": "¿Listo para entrenar?", + "startHint": "Inicia una nueva sesión y registra tus series", + "startWorkout": "Iniciar entrenamiento", + "weight": "Peso", + "dailyCheck": "Hoy", + "recent": "Últimos entrenamientos", + "noWorkouts": "Aún sin sesiones", + "log": "Registrar", + "measurement": { + "weight": "Peso", + "bodyfat": "Grasa corporal", + "muscle": "Masa muscular", + "chest": "Pecho", + "waist": "Cintura", + "hips": "Caderas", + "thigh": "Muslo", + "arm": "Brazo", + "calf": "Pantorrilla", + "neck": "Cuello" + }, + "check": { + "energy": "Energía", + "sleep": "Sueño", + "soreness": "Agujetas", + "mood": "Estado de ánimo" + }, + "phase": { + "cut": "Definición", + "bulk": "Volumen", + "maintenance": "Mantenimiento", + "recomp": "Recomposición" + } +} diff --git a/apps/mana/apps/web/src/lib/i18n/locales/body/fr.json b/apps/mana/apps/web/src/lib/i18n/locales/body/fr.json new file mode 100644 index 000000000..e341ff1c4 --- /dev/null +++ b/apps/mana/apps/web/src/lib/i18n/locales/body/fr.json @@ -0,0 +1,38 @@ +{ + "title": "Body", + "subtitle": "Entraînement et corps dans un seul module", + "activeWorkout": "Séance en cours", + "finish": "Terminer", + "readyToTrain": "Prêt à t'entraîner ?", + "startHint": "Démarre une nouvelle séance et enregistre tes séries", + "startWorkout": "Démarrer la séance", + "weight": "Poids", + "dailyCheck": "Aujourd'hui", + "recent": "Dernières séances", + "noWorkouts": "Pas encore de séance", + "log": "Enregistrer", + "measurement": { + "weight": "Poids", + "bodyfat": "Masse grasse", + "muscle": "Masse musculaire", + "chest": "Poitrine", + "waist": "Tour de taille", + "hips": "Hanches", + "thigh": "Cuisse", + "arm": "Bras", + "calf": "Mollet", + "neck": "Cou" + }, + "check": { + "energy": "Énergie", + "sleep": "Sommeil", + "soreness": "Courbatures", + "mood": "Humeur" + }, + "phase": { + "cut": "Sèche", + "bulk": "Prise de masse", + "maintenance": "Maintenance", + "recomp": "Recomposition" + } +} diff --git a/apps/mana/apps/web/src/lib/i18n/locales/body/it.json b/apps/mana/apps/web/src/lib/i18n/locales/body/it.json new file mode 100644 index 000000000..62fbd64ae --- /dev/null +++ b/apps/mana/apps/web/src/lib/i18n/locales/body/it.json @@ -0,0 +1,38 @@ +{ + "title": "Body", + "subtitle": "Allenamento e corpo in un solo modulo", + "activeWorkout": "Allenamento attivo", + "finish": "Termina", + "readyToTrain": "Pronto ad allenarti?", + "startHint": "Inizia una nuova sessione e registra le serie", + "startWorkout": "Inizia allenamento", + "weight": "Peso", + "dailyCheck": "Oggi", + "recent": "Ultimi allenamenti", + "noWorkouts": "Nessuna sessione", + "log": "Registra", + "measurement": { + "weight": "Peso", + "bodyfat": "Grasso corporeo", + "muscle": "Massa muscolare", + "chest": "Petto", + "waist": "Vita", + "hips": "Fianchi", + "thigh": "Coscia", + "arm": "Braccio", + "calf": "Polpaccio", + "neck": "Collo" + }, + "check": { + "energy": "Energia", + "sleep": "Sonno", + "soreness": "Dolori muscolari", + "mood": "Umore" + }, + "phase": { + "cut": "Definizione", + "bulk": "Massa", + "maintenance": "Mantenimento", + "recomp": "Ricomposizione" + } +} diff --git a/apps/mana/apps/web/src/lib/i18n/locales/dashboard/de.json b/apps/mana/apps/web/src/lib/i18n/locales/dashboard/de.json index 2d5b4b673..562c5cbf0 100644 --- a/apps/mana/apps/web/src/lib/i18n/locales/dashboard/de.json +++ b/apps/mana/apps/web/src/lib/i18n/locales/dashboard/de.json @@ -154,6 +154,10 @@ "news_unread": { "title": "News", "description": "Top-Artikel aus deinem kuratierten Feed" + }, + "body_stats": { + "title": "Body", + "description": "Aktuelles Gewicht und Trainings-Status" } } } diff --git a/apps/mana/apps/web/src/lib/i18n/locales/dashboard/en.json b/apps/mana/apps/web/src/lib/i18n/locales/dashboard/en.json index 14bcd9bfd..7944fef33 100644 --- a/apps/mana/apps/web/src/lib/i18n/locales/dashboard/en.json +++ b/apps/mana/apps/web/src/lib/i18n/locales/dashboard/en.json @@ -154,6 +154,10 @@ "news_unread": { "title": "News", "description": "Top articles from your curated feed" + }, + "body_stats": { + "title": "Body", + "description": "Latest weight and training status" } } } diff --git a/apps/mana/apps/web/src/lib/i18n/locales/dashboard/es.json b/apps/mana/apps/web/src/lib/i18n/locales/dashboard/es.json index e1ad3765b..cd9eca43d 100644 --- a/apps/mana/apps/web/src/lib/i18n/locales/dashboard/es.json +++ b/apps/mana/apps/web/src/lib/i18n/locales/dashboard/es.json @@ -149,6 +149,10 @@ "news_unread": { "title": "Noticias", "description": "Artículos destacados de tu feed curado" + }, + "body_stats": { + "title": "Body", + "description": "Peso actual y estado del entrenamiento" } } } diff --git a/apps/mana/apps/web/src/lib/i18n/locales/dashboard/fr.json b/apps/mana/apps/web/src/lib/i18n/locales/dashboard/fr.json index 5a2c055c2..92baf6ae8 100644 --- a/apps/mana/apps/web/src/lib/i18n/locales/dashboard/fr.json +++ b/apps/mana/apps/web/src/lib/i18n/locales/dashboard/fr.json @@ -149,6 +149,10 @@ "news_unread": { "title": "Actualités", "description": "Articles phares de ton fil personnalisé" + }, + "body_stats": { + "title": "Body", + "description": "Poids actuel et statut de l'entraînement" } } } diff --git a/apps/mana/apps/web/src/lib/i18n/locales/dashboard/it.json b/apps/mana/apps/web/src/lib/i18n/locales/dashboard/it.json index 2ca19bc21..26f755ec7 100644 --- a/apps/mana/apps/web/src/lib/i18n/locales/dashboard/it.json +++ b/apps/mana/apps/web/src/lib/i18n/locales/dashboard/it.json @@ -149,6 +149,10 @@ "news_unread": { "title": "Notizie", "description": "Articoli in evidenza dal tuo feed curato" + }, + "body_stats": { + "title": "Body", + "description": "Peso attuale e stato dell'allenamento" } } } diff --git a/apps/mana/apps/web/src/lib/modules/body/ListView.svelte b/apps/mana/apps/web/src/lib/modules/body/ListView.svelte new file mode 100644 index 000000000..7794be11f --- /dev/null +++ b/apps/mana/apps/web/src/lib/modules/body/ListView.svelte @@ -0,0 +1,216 @@ + + + + + {$_('body.title', { default: 'Body' })} - Mana + + +
+
+
+

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

+

+ {$_('body.subtitle', { default: 'Training & Körper in einem Modul' })} +

+
+ {#if activePhase} +
+ {$_(`body.phase.${activePhase.kind}`, { default: activePhase.kind })} +
+ {/if} +
+ +
+ {#if activeWorkout} + + {:else} +
+
+

{$_('body.readyToTrain', { default: 'Bereit für ein Workout?' })}

+

+ {$_('body.startHint', { default: 'Starte eine neue Session und logge deine Sätze' })} +

+
+ +
+ {/if} +
+ +
+

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

+ + +
+ +
+

{$_('body.dailyCheck', { default: 'Heute' })}

+ +
+ +
+

{$_('body.recent', { default: 'Letzte Workouts' })}

+ +
+
+ + diff --git a/apps/mana/apps/web/src/lib/modules/body/components/DailyCheckCard.svelte b/apps/mana/apps/web/src/lib/modules/body/components/DailyCheckCard.svelte new file mode 100644 index 000000000..776d9c238 --- /dev/null +++ b/apps/mana/apps/web/src/lib/modules/body/components/DailyCheckCard.svelte @@ -0,0 +1,96 @@ + + + +
+ {#each FIELDS as field (field.key)} +
+
{$_(`body.check.${field.key}`, { default: field.label })}
+
+ {#each [1, 2, 3, 4, 5] as n (n)} + {@const active = valueOf(field.key) === n} + + {/each} +
+
+ {/each} +
+ + diff --git a/apps/mana/apps/web/src/lib/modules/body/components/MeasurementForm.svelte b/apps/mana/apps/web/src/lib/modules/body/components/MeasurementForm.svelte new file mode 100644 index 000000000..06b19e27f --- /dev/null +++ b/apps/mana/apps/web/src/lib/modules/body/components/MeasurementForm.svelte @@ -0,0 +1,111 @@ + + + +
{ + e.preventDefault(); + submit(); + }} +> + + +
+ + {unit === 'percent' ? '%' : unit} +
+ + +
+ + diff --git a/apps/mana/apps/web/src/lib/modules/body/components/RecentWorkouts.svelte b/apps/mana/apps/web/src/lib/modules/body/components/RecentWorkouts.svelte new file mode 100644 index 000000000..8657ef493 --- /dev/null +++ b/apps/mana/apps/web/src/lib/modules/body/components/RecentWorkouts.svelte @@ -0,0 +1,84 @@ + + + +{#if recent.length === 0} +

{$_('body.noWorkouts', { default: 'Noch keine Sessions' })}

+{:else} + +{/if} + + diff --git a/apps/mana/apps/web/src/lib/modules/body/components/SetRow.svelte b/apps/mana/apps/web/src/lib/modules/body/components/SetRow.svelte new file mode 100644 index 000000000..80ca16774 --- /dev/null +++ b/apps/mana/apps/web/src/lib/modules/body/components/SetRow.svelte @@ -0,0 +1,147 @@ + + + +
+ + + + + + + {#if set.rpe !== null} + RPE {set.rpe} + {/if} + + +
+ + diff --git a/apps/mana/apps/web/src/lib/modules/body/components/WeightChart.svelte b/apps/mana/apps/web/src/lib/modules/body/components/WeightChart.svelte new file mode 100644 index 000000000..e56e7f5a2 --- /dev/null +++ b/apps/mana/apps/web/src/lib/modules/body/components/WeightChart.svelte @@ -0,0 +1,136 @@ + + + +
+ {#if series.length === 0} +

Noch keine Daten

+ {:else} +
+
+ {latest.value} {latest.unit === 'percent' ? '%' : latest.unit} +
+ {#if series.length > 1} +
0} class:negative={delta < 0}> + {delta > 0 ? '+' : ''}{delta.toFixed(1)} +
+ {/if} +
+ + + + {#each series as m, i (m.id)} + {@const range = extent.max - extent.min || 1} + {@const stepX = (width - padX * 2) / Math.max(series.length - 1, 1)} + {@const x = padX + i * stepX} + {@const y = padY + (height - padY * 2) * (1 - (m.value - extent.min) / range)} + + {/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 new file mode 100644 index 000000000..f39fe8408 --- /dev/null +++ b/apps/mana/apps/web/src/lib/modules/body/components/WorkoutLogger.svelte @@ -0,0 +1,228 @@ + + + +
+
+

{$_('body.activeWorkout', { default: 'Aktives Workout' })}

+ +
+ + {#each [...setsByExercise.entries()] as [exerciseId, exSets] (exerciseId)} +
+

{exerciseName(exerciseId)}

+
+ {#each exSets as set, i (set.id)} + + {/each} +
+
+ {/each} + +
{ + e.preventDefault(); + addSet(); + }} + > + + + + + + + + + +
+
+ + diff --git a/apps/mana/apps/web/src/lib/modules/body/widgets/BodyStatsWidget.svelte b/apps/mana/apps/web/src/lib/modules/body/widgets/BodyStatsWidget.svelte new file mode 100644 index 000000000..8e2567c90 --- /dev/null +++ b/apps/mana/apps/web/src/lib/modules/body/widgets/BodyStatsWidget.svelte @@ -0,0 +1,160 @@ + + +
+
+

+ + {$_('body.title', { default: 'Body' })} +

+ Öffnen → +
+ + {#if loading} +
+
+
+
+ {:else} +
+ +
+
+ {$_('body.weight', { default: 'Gewicht' })} +
+ {#if latest} +
+ {latest.value} + {latest.unit} + {#if previous} + 0} + class:text-muted-foreground={delta === 0} + > + {delta > 0 ? '+' : ''}{delta.toFixed(1)} + + {/if} +
+ {:else} + + {/if} +
+ + + {#if activeWorkout} + +
+ {$_('body.activeWorkout', { default: 'Aktives Workout' })} +
+
+ {todaySets.length} sets · {todayVolume} kg vol +
+
+ {:else if todaySets.length > 0} +
+
Heute
+
{todaySets.length} sets · {todayVolume} kg vol
+
+ {:else} + + {$_('body.startWorkout', { default: 'Workout starten' })} + + {/if} +
+ {/if} +
diff --git a/apps/mana/apps/web/src/lib/types/dashboard.ts b/apps/mana/apps/web/src/lib/types/dashboard.ts index dc365401a..0416bd075 100644 --- a/apps/mana/apps/web/src/lib/types/dashboard.ts +++ b/apps/mana/apps/web/src/lib/types/dashboard.ts @@ -31,7 +31,8 @@ export type WidgetType = | 'day-timeline' // TimeBlocks: chronological day timeline | 'activity-feed' // TimeBlocks: recent activity across modules | 'cycles' // Cycles: current phase + days until next period - | 'news-unread'; // News: latest unread curated articles + | 'news-unread' // News: latest unread curated articles + | 'body-stats'; // Body: latest weight + active workout summary /** * Widget size - maps to CSS Grid columns @@ -132,6 +133,7 @@ export interface WidgetMeta { | 'nutriphi' | 'planta' | 'cycles' + | 'body' | 'mana-auth'; } @@ -351,6 +353,15 @@ export const WIDGET_REGISTRY: WidgetMeta[] = [ defaultSize: 'small', allowMultiple: false, }, + { + type: 'body-stats', + nameKey: 'dashboard.widgets.body_stats.title', + descriptionKey: 'dashboard.widgets.body_stats.description', + icon: '💪', + defaultSize: 'small', + allowMultiple: false, + requiredBackend: 'body', + }, ]; /** diff --git a/apps/mana/apps/web/src/routes/(app)/body/+layout.svelte b/apps/mana/apps/web/src/routes/(app)/body/+layout.svelte new file mode 100644 index 000000000..3358a0b92 --- /dev/null +++ b/apps/mana/apps/web/src/routes/(app)/body/+layout.svelte @@ -0,0 +1,25 @@ + + +{@render children()} diff --git a/apps/mana/apps/web/src/routes/(app)/body/+page.svelte b/apps/mana/apps/web/src/routes/(app)/body/+page.svelte new file mode 100644 index 000000000..e3b716be0 --- /dev/null +++ b/apps/mana/apps/web/src/routes/(app)/body/+page.svelte @@ -0,0 +1,9 @@ + + + + Body - Mana + + +