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",
|
"name": "homepage",
|
||||||
"version": "1.46.3",
|
"version": "1.46.4",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -312,6 +312,7 @@ const translations: Translations = {
|
|||||||
body_fat_pct: { en: 'Body Fat (%)', de: 'Körperfett (%)' },
|
body_fat_pct: { en: 'Body Fat (%)', de: 'Körperfett (%)' },
|
||||||
history: { en: 'History', de: 'Verlauf' },
|
history: { en: 'History', de: 'Verlauf' },
|
||||||
past_measurements: { en: 'Past measurements', de: 'Frühere Messungen' },
|
past_measurements: { en: 'Past measurements', de: 'Frühere Messungen' },
|
||||||
|
show_more: { en: 'Show more', de: 'Mehr anzeigen' },
|
||||||
|
|
||||||
// SetTable
|
// SetTable
|
||||||
set_header: { en: 'SET', de: 'SATZ' },
|
set_header: { en: 'SET', de: 'SATZ' },
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import type { PageServerLoad } from './$types';
|
|||||||
export const load: PageServerLoad = async ({ fetch }) => {
|
export const load: PageServerLoad = async ({ fetch }) => {
|
||||||
const [latestRes, listRes, goalRes, periodRes, shareRes] = await Promise.all([
|
const [latestRes, listRes, goalRes, periodRes, shareRes] = await Promise.all([
|
||||||
fetch('/api/fitness/measurements/latest'),
|
fetch('/api/fitness/measurements/latest'),
|
||||||
fetch('/api/fitness/measurements?limit=200'),
|
fetch('/api/fitness/measurements?limit=10'),
|
||||||
fetch('/api/fitness/goal'),
|
fetch('/api/fitness/goal'),
|
||||||
fetch('/api/fitness/period').catch(() => null),
|
fetch('/api/fitness/period').catch(() => null),
|
||||||
fetch('/api/fitness/period/share').catch(() => null)
|
fetch('/api/fitness/period/share').catch(() => null)
|
||||||
|
|||||||
@@ -27,6 +27,9 @@
|
|||||||
let latest = $state(data.latest ? { ...data.latest } : {});
|
let latest = $state(data.latest ? { ...data.latest } : {});
|
||||||
// svelte-ignore state_referenced_locally
|
// svelte-ignore state_referenced_locally
|
||||||
let measurements = $state(data.measurements?.measurements ? [...data.measurements.measurements] : []);
|
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);
|
let showWeightHistory = $state(false);
|
||||||
|
|
||||||
// Profile fields (sex, height, birth year) — stored in FitnessGoal
|
// Profile fields (sex, height, birth year) — stored in FitnessGoal
|
||||||
@@ -105,6 +108,27 @@
|
|||||||
{ label: t('r_calf', lang), key: 'rightCalf', value: latestBp.rightCalf }
|
{ 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 */
|
/** @param {string} id */
|
||||||
async function deleteMeasurement(id) {
|
async function deleteMeasurement(id) {
|
||||||
if (!await confirm(t('delete_measurement_confirm', lang))) return;
|
if (!await confirm(t('delete_measurement_confirm', lang))) return;
|
||||||
@@ -112,6 +136,7 @@
|
|||||||
const res = await fetch(`/api/fitness/measurements/${id}`, { method: 'DELETE' });
|
const res = await fetch(`/api/fitness/measurements/${id}`, { method: 'DELETE' });
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
measurements = measurements.filter((m) => m._id !== id);
|
measurements = measurements.filter((m) => m._id !== id);
|
||||||
|
measurementsTotal = Math.max(0, measurementsTotal - 1);
|
||||||
try {
|
try {
|
||||||
const latestRes = await fetch('/api/fitness/measurements/latest');
|
const latestRes = await fetch('/api/fitness/measurements/latest');
|
||||||
if (latestRes.ok) latest = await latestRes.json();
|
if (latestRes.ok) latest = await latestRes.json();
|
||||||
@@ -309,6 +334,7 @@
|
|||||||
if (latestRes.ok) latest = await latestRes.json();
|
if (latestRes.ok) latest = await latestRes.json();
|
||||||
} catch {}
|
} catch {}
|
||||||
measurements = [created.measurement ?? created, ...measurements];
|
measurements = [created.measurement ?? created, ...measurements];
|
||||||
|
measurementsTotal = measurementsTotal + 1;
|
||||||
resetForm();
|
resetForm();
|
||||||
toast.success(lang === 'en' ? 'Measurement saved' : 'Messung gespeichert');
|
toast.success(lang === 'en' ? 'Measurement saved' : 'Messung gespeichert');
|
||||||
} else {
|
} else {
|
||||||
@@ -571,6 +597,12 @@
|
|||||||
</div>
|
</div>
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</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>
|
</section>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
@@ -1136,6 +1168,35 @@
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 0.4rem;
|
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 {
|
.history-item {
|
||||||
background: var(--color-surface);
|
background: var(--color-surface);
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
|
|||||||
Reference in New Issue
Block a user