diff --git a/package.json b/package.json index 6a16b588..81d20972 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "homepage", - "version": "1.40.3", + "version": "1.40.4", "private": true, "type": "module", "scripts": { diff --git a/src/routes/fitness/[measure=fitnessMeasure]/+page.svelte b/src/routes/fitness/[measure=fitnessMeasure]/+page.svelte index a380adce..5c6a26f6 100644 --- a/src/routes/fitness/[measure=fitnessMeasure]/+page.svelte +++ b/src/routes/fitness/[measure=fitnessMeasure]/+page.svelte @@ -6,11 +6,9 @@ import { confirm } from '$lib/js/confirmDialog.svelte'; import SaveFab from '$lib/components/SaveFab.svelte'; import DatePicker from '$lib/components/DatePicker.svelte'; - import { BODY_PART_CARDS, bodyPartSlug } from '$lib/js/fitnessBodyParts'; const lang = $derived(detectFitnessLang($page.url.pathname)); const measureSlug = $derived(lang === 'en' ? 'measure' : 'messen'); - const historySlug = $derived(lang === 'en' ? 'history' : 'verlauf'); import { getWorkout } from '$lib/js/workout.svelte'; import PeriodTracker from '$lib/components/fitness/PeriodTracker.svelte'; @@ -99,26 +97,6 @@ { label: t('r_calf', lang), key: 'rightCalf', value: latestBp.rightCalf } ]); - /** @param {import('$lib/js/fitnessBodyParts').BodyPartCard} c */ - function currentValue(c) { - if (c.paired) { - const l = /** @type {number|undefined} */ (latestBp[c.dbLeft]); - const r = /** @type {number|undefined} */ (latestBp[c.dbRight]); - return { left: l ?? null, right: r ?? null }; - } - const v = /** @type {number|undefined} */ (latestBp[c.db]); - return { value: v ?? null }; - } - - /** @param {import('$lib/js/fitnessBodyParts').BodyPartCard} c */ - function hasAny(c) { - const v = currentValue(c); - if (c.paired) return v.left != null || v.right != null; - return v.value != null; - } - - const cardsWithData = $derived(BODY_PART_CARDS.filter(hasAny)); - /** @param {string} id */ async function deleteMeasurement(id) { if (!await confirm(t('delete_measurement_confirm', lang))) return; @@ -340,55 +318,6 @@ {/if} - {#if cardsWithData.length > 0} - - {t('body_parts', lang)} - - {#each cardsWithData as card (card.key)} - {@const cv = currentValue(card)} - - - {#if card.img && card.img.endsWith('.svg')} - - {:else if card.img} - - {:else} - - {/if} - - - {t(card.labelKey, lang)} - {#if card.paired} - {#if cv.left != null && cv.right != null && cv.left === cv.right} - {cv.left.toFixed(1)}cm - {:else if cv.left != null && cv.right != null} - - L {cv.left.toFixed(1)} - · - R {cv.right.toFixed(1)} - cm - - {:else if cv.left != null} - L {cv.left.toFixed(1)}cm - {:else if cv.right != null} - R {cv.right.toFixed(1)}cm - {/if} - {:else if cv.value != null} - {cv.value.toFixed(1)}cm - {/if} - - - {/each} - - - {/if} - {#if measurements.length > 0} showWeightHistory = !showWeightHistory}> @@ -919,156 +848,6 @@ white-space: nowrap; } - /* Body parts (latest) */ - .bp-grid { - display: grid; - grid-template-columns: repeat(3, minmax(0, 1fr)); - gap: 0.6rem; - } - .bp-card { - display: flex; - flex-direction: column; - align-items: center; - text-align: center; - gap: 0.35rem; - padding: 0.7rem 0.5rem 0.6rem; - background: var(--color-surface); - border: 1px solid var(--color-border); - border-radius: var(--radius-lg); - cursor: pointer; - color: inherit; - font: inherit; - text-decoration: none; - position: relative; - transition: border-color var(--transition-normal), box-shadow var(--transition-normal), transform var(--transition-normal); - } - .bp-card:hover { - border-color: var(--color-primary); - box-shadow: var(--shadow-sm); - } - .bp-img-wrap { - display: grid; - place-items: center; - width: 3.25rem; - height: 3.25rem; - flex-shrink: 0; - border-radius: 50%; - background: var(--color-bg-secondary); - color: var(--color-text-secondary); - } - .bp-img { - width: 2.4rem; - height: 2.4rem; - object-fit: contain; - } - .bp-img-svg { - mask-image: var(--bp-svg-src); - -webkit-mask-image: var(--bp-svg-src); - mask-size: contain; - -webkit-mask-size: contain; - mask-repeat: no-repeat; - -webkit-mask-repeat: no-repeat; - mask-position: center; - -webkit-mask-position: center; - background-color: var(--color-text-primary); - } - @media (prefers-color-scheme: dark) { - img.bp-img { filter: invert(1); } - } - :global(:root[data-theme="dark"]) img.bp-img { filter: invert(1); } - :global(:root[data-theme="light"]) img.bp-img { filter: none; } - .bp-meta { - display: flex; - flex-direction: column; - align-items: center; - gap: 0.1rem; - min-width: 0; - width: 100%; - } - .bp-label { - font-size: 0.65rem; - font-weight: 600; - text-transform: uppercase; - letter-spacing: 0.06em; - color: var(--color-text-secondary); - } - .bp-value { - font-size: 1rem; - font-weight: 700; - font-variant-numeric: tabular-nums; - color: var(--color-text-primary); - letter-spacing: -0.01em; - } - .bp-value.paired { - font-size: 0.78rem; - display: inline-flex; - flex-wrap: wrap; - align-items: baseline; - justify-content: center; - gap: 0.2rem; - } - .bp-value em { - font-style: normal; - font-weight: 600; - font-size: 0.62rem; - color: var(--color-text-tertiary); - margin-right: 0.15rem; - letter-spacing: 0.05em; - } - .bp-side { - white-space: nowrap; - } - .bp-side-sep { - color: var(--color-text-tertiary); - } - .bp-unit { - margin-left: 0.2rem; - font-size: 0.65rem; - font-weight: 600; - color: var(--color-text-tertiary); - } - @media (max-width: 420px) { - .bp-grid { gap: 0.45rem; } - .bp-card { padding: 0.55rem 0.35rem; } - .bp-img-wrap { width: 2.6rem; height: 2.6rem; } - .bp-img { width: 1.9rem; height: 1.9rem; } - .bp-label { font-size: 0.58rem; } - .bp-value { font-size: 0.88rem; } - .bp-value.paired { font-size: 0.7rem; } - } - @media (min-width: 768px) { - .bp-grid { gap: 0.85rem; } - .bp-card { - flex-direction: row; - align-items: center; - text-align: left; - gap: 0.85rem; - padding: 0.9rem 1rem; - } - .bp-img-wrap { - width: 3.75rem; - height: 3.75rem; - } - .bp-img { - width: 2.75rem; - height: 2.75rem; - } - .bp-meta { - align-items: flex-start; - text-align: left; - gap: 0.25rem; - } - .bp-value.paired { - justify-content: flex-start; - } - .bp-label { - font-size: 0.68rem; - } - .bp-value { - font-size: 1.15rem; - } - } - /* History */ .history-toggle { display: flex; diff --git a/src/routes/fitness/[stats=fitnessStats]/+page.server.ts b/src/routes/fitness/[stats=fitnessStats]/+page.server.ts index 9fd7cb01..26a0a224 100644 --- a/src/routes/fitness/[stats=fitnessStats]/+page.server.ts +++ b/src/routes/fitness/[stats=fitnessStats]/+page.server.ts @@ -2,15 +2,17 @@ import type { PageServerLoad } from './$types'; export const load: PageServerLoad = async ({ fetch, locals }) => { const session = await locals.auth(); - const [res, goalRes, heatmapRes, nutritionRes] = await Promise.all([ + const [res, goalRes, heatmapRes, nutritionRes, latestRes] = await Promise.all([ fetch('/api/fitness/stats/overview'), fetch('/api/fitness/goal'), fetch('/api/fitness/stats/muscle-heatmap?weeks=8'), - fetch('/api/fitness/stats/nutrition') + fetch('/api/fitness/stats/nutrition'), + fetch('/api/fitness/measurements/latest') ]); const stats = await res.json(); const goal = goalRes.ok ? await goalRes.json() : { weeklyWorkouts: null, streak: 0 }; const muscleHeatmap = heatmapRes.ok ? await heatmapRes.json() : { weeks: [], totals: {}, muscleGroups: [] }; const nutritionStats = nutritionRes.ok ? await nutritionRes.json() : null; - return { session, stats, goal, muscleHeatmap, nutritionStats }; + const latest = latestRes.ok ? await latestRes.json() : {}; + return { session, stats, goal, muscleHeatmap, nutritionStats, latest }; }; diff --git a/src/routes/fitness/[stats=fitnessStats]/+page.svelte b/src/routes/fitness/[stats=fitnessStats]/+page.svelte index 383727d7..06b53dfe 100644 --- a/src/routes/fitness/[stats=fitnessStats]/+page.svelte +++ b/src/routes/fitness/[stats=fitnessStats]/+page.svelte @@ -3,14 +3,17 @@ import { page } from '$app/stores'; import FitnessChart from '$lib/components/fitness/FitnessChart.svelte'; import MuscleHeatmap from '$lib/components/fitness/MuscleHeatmap.svelte'; - import { Dumbbell, Route, Flame, Weight, Beef, Droplet, Wheat, Scale, Target, Info } from '@lucide/svelte'; + import { Dumbbell, Route, Flame, Weight, Beef, Droplet, Wheat, Scale, Target, Info, Ruler } from '@lucide/svelte'; import FitnessStreakAura from '$lib/components/fitness/FitnessStreakAura.svelte'; import { onMount } from 'svelte'; import { detectFitnessLang, fitnessSlugs, t } from '$lib/js/fitnessI18n'; import { toast } from '$lib/js/toast.svelte'; import StatsRingGraph from '$lib/components/fitness/StatsRingGraph.svelte'; + import { BODY_PART_CARDS, bodyPartSlug } from '$lib/js/fitnessBodyParts'; const lang = $derived(detectFitnessLang($page.url.pathname)); + const statsSlug = $derived(lang === 'en' ? 'stats' : 'statistik'); + const historySlug = $derived(lang === 'en' ? 'history' : 'verlauf'); let { data } = $props(); @@ -37,6 +40,28 @@ const stats = $derived(data.stats ?? {}); + const latestBp = $derived(data.latest?.measurements?.value ?? {}); + + /** @param {import('$lib/js/fitnessBodyParts').BodyPartCard} c */ + function currentValue(c) { + if (c.paired) { + const l = /** @type {number|undefined} */ (latestBp[c.dbLeft]); + const r = /** @type {number|undefined} */ (latestBp[c.dbRight]); + return { left: l ?? null, right: r ?? null }; + } + const v = /** @type {number|undefined} */ (latestBp[c.db]); + return { value: v ?? null }; + } + + /** @param {import('$lib/js/fitnessBodyParts').BodyPartCard} c */ + function hasAny(c) { + const v = currentValue(c); + if (c.paired) return v.left != null || v.right != null; + return v.value != null; + } + + const cardsWithData = $derived(BODY_PART_CARDS.filter(hasAny)); + let goalStreak = $derived(data.goal?.streak ?? 0); let goalWeekly = $derived(data.goal?.weeklyWorkouts ?? null); let showBalanceInfo = $state(false); @@ -356,6 +381,55 @@ + + {#if cardsWithData.length > 0} + + {t('body_parts', lang)} + + {#each cardsWithData as card (card.key)} + {@const cv = currentValue(card)} + + + {#if card.img && card.img.endsWith('.svg')} + + {:else if card.img} + + {:else} + + {/if} + + + {t(card.labelKey, lang)} + {#if card.paired} + {#if cv.left != null && cv.right != null && cv.left === cv.right} + {cv.left.toFixed(1)}cm + {:else if cv.left != null && cv.right != null} + + L {cv.left.toFixed(1)} + · + R {cv.right.toFixed(1)} + cm + + {:else if cv.left != null} + L {cv.left.toFixed(1)}cm + {:else if cv.right != null} + R {cv.right.toFixed(1)}cm + {/if} + {:else if cv.value != null} + {cv.value.toFixed(1)}cm + {/if} + + + {/each} + + + {/if} diff --git a/src/routes/fitness/[measure=fitnessMeasure]/[history=fitnessHistory]/[part]/+page.server.ts b/src/routes/fitness/[stats=fitnessStats]/[history=fitnessHistory]/[part]/+page.server.ts similarity index 100% rename from src/routes/fitness/[measure=fitnessMeasure]/[history=fitnessHistory]/[part]/+page.server.ts rename to src/routes/fitness/[stats=fitnessStats]/[history=fitnessHistory]/[part]/+page.server.ts diff --git a/src/routes/fitness/[measure=fitnessMeasure]/[history=fitnessHistory]/[part]/+page.svelte b/src/routes/fitness/[stats=fitnessStats]/[history=fitnessHistory]/[part]/+page.svelte similarity index 98% rename from src/routes/fitness/[measure=fitnessMeasure]/[history=fitnessHistory]/[part]/+page.svelte rename to src/routes/fitness/[stats=fitnessStats]/[history=fitnessHistory]/[part]/+page.svelte index 3d938939..03fd3a01 100644 --- a/src/routes/fitness/[measure=fitnessMeasure]/[history=fitnessHistory]/[part]/+page.svelte +++ b/src/routes/fitness/[stats=fitnessStats]/[history=fitnessHistory]/[part]/+page.svelte @@ -7,6 +7,7 @@ let { data } = $props(); const lang = $derived(detectFitnessLang($page.url.pathname)); + const statsSlug = $derived(lang === 'en' ? 'stats' : 'statistik'); const measureSlug = $derived(lang === 'en' ? 'measure' : 'messen'); const card = $derived(data.card); @@ -112,7 +113,7 @@ - +