From 5b7f23b8befe164c1a29a9719e3892dc68528198 Mon Sep 17 00:00:00 2001 From: Alexander Bocken Date: Tue, 21 Apr 2026 12:53:11 +0200 Subject: [PATCH] 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. --- package.json | 2 +- .../components/fitness/PeriodTracker.svelte | 27 +++++++++++-------- src/lib/js/fitnessI18n.ts | 1 + .../[measure=fitnessMeasure]/+page.server.ts | 8 +++--- .../[measure=fitnessMeasure]/+page.svelte | 4 --- .../[stats=fitnessStats]/+page.server.ts | 10 ++++--- .../fitness/[stats=fitnessStats]/+page.svelte | 9 +++++++ 7 files changed, 37 insertions(+), 24 deletions(-) diff --git a/package.json b/package.json index 797947bf..25b25f3f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "homepage", - "version": "1.40.6", + "version": "1.41.0", "private": true, "type": "module", "scripts": { diff --git a/src/lib/components/fitness/PeriodTracker.svelte b/src/lib/components/fitness/PeriodTracker.svelte index c05bc109..12d6a76f 100644 --- a/src/lib/components/fitness/PeriodTracker.svelte +++ b/src/lib/components/fitness/PeriodTracker.svelte @@ -7,9 +7,11 @@ import ProfilePicture from '$lib/components/cospend/ProfilePicture.svelte'; /** - * @type {{ periods: any[], lang: 'en' | 'de', sharedWith?: string[], readOnly?: boolean, ownerName?: string }} + * @type {{ periods: any[], lang: 'en' | 'de', sharedWith?: string[], readOnly?: boolean, ownerName?: string, mode?: 'entry' | 'projection' | 'full' }} */ - let { periods: initialPeriods = [], lang = 'en', sharedWith: initialSharedWith = [], readOnly = false, ownerName = '' } = $props(); + let { periods: initialPeriods = [], lang = 'en', sharedWith: initialSharedWith = [], readOnly = false, ownerName = '', mode = 'full' } = $props(); + const showEntry = $derived(mode !== 'projection'); + const showProjection = $derived(mode !== 'entry'); // svelte-ignore state_referenced_locally let periods = $state([...initialPeriods]); @@ -592,18 +594,18 @@
{t('current_period', lang)} {t('period_day', lang)} {ongoingDay} - {#if predictions.predictedEndOfOngoing} + {#if showProjection && predictions.predictedEndOfOngoing} {t('predicted_end', lang)} {relativeDate(predictions.predictedEndOfOngoing)} {formatDate(predictions.predictedEndOfOngoing)} {/if} - {#if !readOnly} + {#if showEntry && !readOnly} {/if}
- {#if nextCycle} + {#if showProjection && nextCycle}
{t('ovulation', lang)} @@ -617,13 +619,13 @@
{/if}
- {:else if nextCycle} + {:else if showProjection && nextCycle}
{t('next_period', lang)} {relativeRange(nextCycle.start, nextCycle.end)} {formatDate(nextCycle.start)} — {formatDate(nextCycle.end)} - {#if !readOnly} + {#if showEntry && !readOnly} @@ -641,9 +643,9 @@
- {:else} + {:else if showEntry}
- {t('no_period_data', lang)} + {sorted.length === 0 ? t('no_period_data', lang) : t('no_active_period', lang)} {#if !readOnly}
+ {#if showProjection}
@@ -691,7 +694,9 @@
- {#if completed.length >= 2} + {/if} + + {#if showProjection && completed.length >= 2}
{t('cycle_length', lang)} @@ -710,7 +715,7 @@
{/if} - {#if !readOnly} + {#if showEntry && !readOnly} {#if sorted.length > 0}
diff --git a/src/lib/js/fitnessI18n.ts b/src/lib/js/fitnessI18n.ts index 07378df4..cd725721 100644 --- a/src/lib/js/fitnessI18n.ts +++ b/src/lib/js/fitnessI18n.ts @@ -402,6 +402,7 @@ const translations: Translations = { period_tracker: { en: 'Period Tracker', de: 'Periodentracker' }, current_period: { en: 'Current Period', de: 'Aktuelle Periode' }, no_period_data: { en: 'No period data yet. Log your first period to start tracking.', de: 'Noch keine Periodendaten. Erfasse deine erste Periode.' }, + no_active_period: { en: 'No active period.', de: 'Keine aktive Periode.' }, start_period: { en: 'Start Period', de: 'Periode starten' }, end_period: { en: 'Period Ended', de: 'Periode vorbei' }, period_day: { en: 'Day', de: 'Tag' }, diff --git a/src/routes/fitness/[measure=fitnessMeasure]/+page.server.ts b/src/routes/fitness/[measure=fitnessMeasure]/+page.server.ts index dfaddaf0..b43fc702 100644 --- a/src/routes/fitness/[measure=fitnessMeasure]/+page.server.ts +++ b/src/routes/fitness/[measure=fitnessMeasure]/+page.server.ts @@ -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 : [] }; }; diff --git a/src/routes/fitness/[measure=fitnessMeasure]/+page.svelte b/src/routes/fitness/[measure=fitnessMeasure]/+page.svelte index 5c6a26f6..5d6f6fe4 100644 --- a/src/routes/fitness/[measure=fitnessMeasure]/+page.svelte +++ b/src/routes/fitness/[measure=fitnessMeasure]/+page.svelte @@ -353,10 +353,6 @@ {/if} - {#each data.sharedPeriods ?? [] as shared (shared.owner)} - - {/each} - {/if} + + {#if data.goal?.sex === 'female'} + + {/if} + + {#each data.sharedPeriods ?? [] as shared (shared.owner)} + + {/each}