From 6d3165f40548fd0285cfa82be41a77102b218ee8 Mon Sep 17 00:00:00 2001 From: Alexander Bocken Date: Thu, 23 Apr 2026 13:18:30 +0200 Subject: [PATCH] feat(fitness/measure): paginate past measurements (SSR 10, "Show more" pulls 20) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit SSR now ships only the 10 most recent measurements (down from 200) to cut initial page weight. A "Show more (N/total)" pill appears below the list when more are available; clicking fetches the next 20 via the existing GET endpoint (offset/limit already supported) and appends with dedupe by `_id`. `measurementsTotal` is seeded from the API's `total` field and kept in sync on save (+1) / delete (−1). The button is hidden when the history is collapsed or when `measurements.length >= total`. Added `show_more` i18n string. --- package.json | 2 +- src/lib/js/fitnessI18n.ts | 1 + .../[measure=fitnessMeasure]/+page.server.ts | 2 +- .../[measure=fitnessMeasure]/+page.svelte | 61 +++++++++++++++++++ 4 files changed, 64 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 0a87e26b..c66c6927 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "homepage", - "version": "1.46.3", + "version": "1.46.4", "private": true, "type": "module", "scripts": { diff --git a/src/lib/js/fitnessI18n.ts b/src/lib/js/fitnessI18n.ts index 814ff026..31990b67 100644 --- a/src/lib/js/fitnessI18n.ts +++ b/src/lib/js/fitnessI18n.ts @@ -312,6 +312,7 @@ const translations: Translations = { body_fat_pct: { en: 'Body Fat (%)', de: 'Körperfett (%)' }, history: { en: 'History', de: 'Verlauf' }, past_measurements: { en: 'Past measurements', de: 'Frühere Messungen' }, + show_more: { en: 'Show more', de: 'Mehr anzeigen' }, // SetTable set_header: { en: 'SET', de: 'SATZ' }, diff --git a/src/routes/fitness/[measure=fitnessMeasure]/+page.server.ts b/src/routes/fitness/[measure=fitnessMeasure]/+page.server.ts index b43fc702..01652253 100644 --- a/src/routes/fitness/[measure=fitnessMeasure]/+page.server.ts +++ b/src/routes/fitness/[measure=fitnessMeasure]/+page.server.ts @@ -3,7 +3,7 @@ import type { PageServerLoad } from './$types'; export const load: PageServerLoad = async ({ fetch }) => { const [latestRes, listRes, goalRes, periodRes, shareRes] = await Promise.all([ fetch('/api/fitness/measurements/latest'), - fetch('/api/fitness/measurements?limit=200'), + fetch('/api/fitness/measurements?limit=10'), fetch('/api/fitness/goal'), fetch('/api/fitness/period').catch(() => null), fetch('/api/fitness/period/share').catch(() => null) diff --git a/src/routes/fitness/[measure=fitnessMeasure]/+page.svelte b/src/routes/fitness/[measure=fitnessMeasure]/+page.svelte index 502533d0..af76d8cb 100644 --- a/src/routes/fitness/[measure=fitnessMeasure]/+page.svelte +++ b/src/routes/fitness/[measure=fitnessMeasure]/+page.svelte @@ -27,6 +27,9 @@ let latest = $state(data.latest ? { ...data.latest } : {}); // svelte-ignore state_referenced_locally let measurements = $state(data.measurements?.measurements ? [...data.measurements.measurements] : []); + // svelte-ignore state_referenced_locally + let measurementsTotal = $state(/** @type {number} */ (data.measurements?.total ?? measurements.length)); + let loadingMore = $state(false); let showWeightHistory = $state(false); // Profile fields (sex, height, birth year) — stored in FitnessGoal @@ -105,6 +108,27 @@ { label: t('r_calf', lang), key: 'rightCalf', value: latestBp.rightCalf } ]); + async function loadMore() { + if (loadingMore) return; + loadingMore = true; + try { + const res = await fetch(`/api/fitness/measurements?limit=20&offset=${measurements.length}`); + if (res.ok) { + const body = await res.json(); + const next = Array.isArray(body?.measurements) ? body.measurements : []; + const existing = new Set(measurements.map((/** @type {any} */ m) => m._id)); + const fresh = next.filter((/** @type {any} */ m) => !existing.has(m._id)); + measurements = [...measurements, ...fresh]; + if (typeof body?.total === 'number') measurementsTotal = body.total; + } else { + toast.error(lang === 'en' ? 'Failed to load more' : 'Laden fehlgeschlagen'); + } + } catch { + toast.error(lang === 'en' ? 'Failed to load more' : 'Laden fehlgeschlagen'); + } + loadingMore = false; + } + /** @param {string} id */ async function deleteMeasurement(id) { if (!await confirm(t('delete_measurement_confirm', lang))) return; @@ -112,6 +136,7 @@ const res = await fetch(`/api/fitness/measurements/${id}`, { method: 'DELETE' }); if (res.ok) { measurements = measurements.filter((m) => m._id !== id); + measurementsTotal = Math.max(0, measurementsTotal - 1); try { const latestRes = await fetch('/api/fitness/measurements/latest'); if (latestRes.ok) latest = await latestRes.json(); @@ -309,6 +334,7 @@ if (latestRes.ok) latest = await latestRes.json(); } catch {} measurements = [created.measurement ?? created, ...measurements]; + measurementsTotal = measurementsTotal + 1; resetForm(); toast.success(lang === 'en' ? 'Measurement saved' : 'Messung gespeichert'); } else { @@ -571,6 +597,12 @@ {/each} + {#if showWeightHistory && measurements.length < measurementsTotal} + + {/if} {/if} @@ -1136,6 +1168,35 @@ flex-direction: column; gap: 0.4rem; } + .show-more { + align-self: stretch; + margin-top: 0.5rem; + display: inline-flex; + align-items: center; + justify-content: center; + gap: 0.4rem; + padding: 0.55rem 1rem; + border: 1px dashed var(--color-border); + border-radius: var(--radius-pill); + background: transparent; + color: var(--color-text-secondary); + font-size: 0.8rem; + font-weight: 600; + cursor: pointer; + transition: border-color var(--transition-fast, 120ms), color var(--transition-fast, 120ms), background var(--transition-fast, 120ms); + } + .show-more:hover:not(:disabled) { + border-color: var(--color-primary); + color: var(--color-primary); + background: color-mix(in oklab, var(--color-primary) 6%, transparent); + } + .show-more:disabled { opacity: 0.5; cursor: not-allowed; } + .show-more-count { + font-size: 0.7rem; + font-weight: 500; + color: var(--color-text-tertiary); + font-variant-numeric: tabular-nums; + } .history-item { background: var(--color-surface); border-radius: 8px;