feat(fitness/measure): paginate past measurements (SSR 10, "Show more" pulls 20)
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.
This commit is contained in:
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "homepage",
|
||||
"version": "1.46.3",
|
||||
"version": "1.46.4",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
|
||||
@@ -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' },
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 @@
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{#if showWeightHistory && measurements.length < measurementsTotal}
|
||||
<button type="button" class="show-more" onclick={loadMore} disabled={loadingMore}>
|
||||
{loadingMore ? t('saving', lang) : t('show_more', lang)}
|
||||
<span class="show-more-count">({measurements.length}/{measurementsTotal})</span>
|
||||
</button>
|
||||
{/if}
|
||||
</section>
|
||||
{/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;
|
||||
|
||||
Reference in New Issue
Block a user