feat(fitness): surface period projection on stats page

PeriodTracker gains an optional mode prop ('entry' | 'projection' |
'full') that gates which sections render. The measure page keeps the
full tracker for the user's own cycle (logging plus calendar). The
stats page now mirrors it in projection mode and is the sole home
for shared cycles, which used to clutter the measure page.
This commit is contained in:
2026-04-21 12:53:11 +02:00
parent 56d438631b
commit 5b7f23b8be
7 changed files with 37 additions and 24 deletions
@@ -1,13 +1,12 @@
import type { PageServerLoad } from './$types';
export const load: PageServerLoad = async ({ fetch }) => {
const [latestRes, listRes, goalRes, periodRes, shareRes, sharedRes] = await Promise.all([
const [latestRes, listRes, goalRes, periodRes, shareRes] = await Promise.all([
fetch('/api/fitness/measurements/latest'),
fetch('/api/fitness/measurements?limit=200'),
fetch('/api/fitness/goal'),
fetch('/api/fitness/period').catch(() => null),
fetch('/api/fitness/period/share').catch(() => null),
fetch('/api/fitness/period/shared').catch(() => null)
fetch('/api/fitness/period/share').catch(() => null)
]);
return {
@@ -15,7 +14,6 @@ export const load: PageServerLoad = async ({ fetch }) => {
measurements: await listRes.json(),
profile: goalRes.ok ? await goalRes.json() : {},
periods: periodRes?.ok ? (await periodRes.json()).entries : [],
periodSharedWith: shareRes?.ok ? (await shareRes.json()).sharedWith : [],
sharedPeriods: sharedRes?.ok ? (await sharedRes.json()).shared : []
periodSharedWith: shareRes?.ok ? (await shareRes.json()).sharedWith : []
};
};
@@ -353,10 +353,6 @@
<PeriodTracker periods={data.periods ?? []} {lang} sharedWith={data.periodSharedWith ?? []} />
{/if}
{#each data.sharedPeriods ?? [] as shared (shared.owner)}
<PeriodTracker periods={shared.entries} {lang} readOnly ownerName={shared.owner} />
{/each}
<div class="page-footer-actions">
<button type="button" class="edit-profile-link" onclick={openProfileEdit}>
<UserCog size={14} />
@@ -2,17 +2,21 @@ import type { PageServerLoad } from './$types';
export const load: PageServerLoad = async ({ fetch, locals }) => {
const session = await locals.auth();
const [res, goalRes, heatmapRes, nutritionRes, latestRes] = await Promise.all([
const [res, goalRes, heatmapRes, nutritionRes, latestRes, periodRes, sharedRes] = 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/measurements/latest')
fetch('/api/fitness/measurements/latest'),
fetch('/api/fitness/period').catch(() => null),
fetch('/api/fitness/period/shared').catch(() => null)
]);
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;
const latest = latestRes.ok ? await latestRes.json() : {};
return { session, stats, goal, muscleHeatmap, nutritionStats, latest };
const periods = periodRes?.ok ? (await periodRes.json()).entries : [];
const sharedPeriods = sharedRes?.ok ? (await sharedRes.json()).shared : [];
return { session, stats, goal, muscleHeatmap, nutritionStats, latest, periods, sharedPeriods };
};
@@ -5,6 +5,7 @@
import MuscleHeatmap from '$lib/components/fitness/MuscleHeatmap.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 PeriodTracker from '$lib/components/fitness/PeriodTracker.svelte';
import { onMount } from 'svelte';
import { detectFitnessLang, fitnessSlugs, t } from '$lib/js/fitnessI18n';
import { toast } from '$lib/js/toast.svelte';
@@ -429,6 +430,14 @@
</div>
</section>
{/if}
{#if data.goal?.sex === 'female'}
<PeriodTracker periods={data.periods ?? []} {lang} mode="projection" />
{/if}
{#each data.sharedPeriods ?? [] as shared (shared.owner)}
<PeriodTracker periods={shared.entries} {lang} readOnly ownerName={shared.owner} mode="projection" />
{/each}
</div>
<style>