feat: use path-based date URLs for nutrition page

Migrate /fitness/nutrition?date=YYYY-MM-DD to /fitness/nutrition/YYYY-MM-DD
using SvelteKit optional param [[date=fitnessDate]]. Replace date nav
buttons with anchor tags for native browser navigation. Today resolves to
the clean /fitness/nutrition path without a date segment.
This commit is contained in:
2026-04-10 08:47:09 +02:00
parent 636f02d110
commit 768c09eeb1
4 changed files with 28 additions and 27 deletions
+5
View File
@@ -0,0 +1,5 @@
import type { ParamMatcher } from '@sveltejs/kit';
export const match: ParamMatcher = (param) => {
return /^\d{4}-\d{2}-\d{2}$/.test(param);
};
+3 -1
View File
@@ -64,7 +64,9 @@
const activePath = $derived(`/fitness/${s.workout}/${s.active}`); const activePath = $derived(`/fitness/${s.workout}/${s.active}`);
const isOnActivePage = $derived($page.url.pathname === activePath); const isOnActivePage = $derived($page.url.pathname === activePath);
const isNutritionPage = $derived( const isNutritionPage = $derived(
$page.url.pathname === `/fitness/${s.nutrition}` || $page.url.pathname === `/fitness/${s.nutrition}/` $page.url.pathname.startsWith(`/fitness/${s.nutrition}`) &&
!$page.url.pathname.startsWith(`/fitness/${s.nutrition}/food`) &&
!$page.url.pathname.startsWith(`/fitness/${s.nutrition}/meals`)
); );
/** @param {number} secs */ /** @param {number} secs */
@@ -6,8 +6,8 @@ import { Recipe } from '$models/Recipe';
import { RoundOffCache } from '$models/RoundOffCache'; import { RoundOffCache } from '$models/RoundOffCache';
import mongoose from 'mongoose'; import mongoose from 'mongoose';
export const load: PageServerLoad = async ({ fetch, url, locals }) => { export const load: PageServerLoad = async ({ fetch, params, locals }) => {
const dateParam = url.searchParams.get('date') || new Date().toISOString().slice(0, 10); const dateParam = params.date || new Date().toISOString().slice(0, 10);
// Run all independent work in parallel: 3 API calls + workout kcal DB query // Run all independent work in parallel: 3 API calls + workout kcal DB query
const dayStart = new Date(dateParam + 'T00:00:00.000Z'); const dayStart = new Date(dateParam + 'T00:00:00.000Z');
@@ -29,17 +29,17 @@
return d.toLocaleDateString(isEn ? 'en-US' : 'de-DE', { weekday: 'short', day: 'numeric', month: 'short' }); return d.toLocaleDateString(isEn ? 'en-US' : 'de-DE', { weekday: 'short', day: 'numeric', month: 'short' });
}); });
async function navigateDate(offset) { function dateOffset(offset) {
const d = new Date(currentDate + 'T12:00:00'); const d = new Date(currentDate + 'T12:00:00');
d.setDate(d.getDate() + offset); d.setDate(d.getDate() + offset);
currentDate = d.toISOString().slice(0, 10); return d.toISOString().slice(0, 10);
await loadEntries();
} }
async function goToday() { const prevDate = $derived(dateOffset(-1));
currentDate = todayStr; const nextDate = $derived(dateOffset(1));
await loadEntries(); const prevHref = $derived(prevDate === todayStr ? `/fitness/${s.nutrition}` : `/fitness/${s.nutrition}/${prevDate}`);
} const nextHref = $derived(nextDate === todayStr ? `/fitness/${s.nutrition}` : `/fitness/${s.nutrition}/${nextDate}`);
const todayHref = $derived(`/fitness/${s.nutrition}`);
// --- Entries --- // --- Entries ---
// svelte-ignore state_referenced_locally // svelte-ignore state_referenced_locally
@@ -47,10 +47,6 @@
// svelte-ignore state_referenced_locally // svelte-ignore state_referenced_locally
let recipeImages = $state(data.recipeImages ?? {}); let recipeImages = $state(data.recipeImages ?? {});
async function loadEntries() {
await goto(`/fitness/${s.nutrition}?date=${currentDate}`, { replaceState: true, noScroll: true });
}
// Keep reactive with server data when navigating // Keep reactive with server data when navigating
$effect(() => { $effect(() => {
entries = data.foodLog?.entries ?? []; entries = data.foodLog?.entries ?? [];
@@ -748,7 +744,7 @@
}) })
}); });
await goto(`/fitness/${s.nutrition}?date=${currentDate}`, { replaceState: true, noScroll: true }); await goto(`/fitness/${s.nutrition}/${currentDate}`, { replaceState: true, noScroll: true });
selectedCmMeal = null; selectedCmMeal = null;
closeFabModal(); closeFabModal();
toast.success(isEn ? `Logged "${meal.name}"` : `"${meal.name}" eingetragen`); toast.success(isEn ? `Logged "${meal.name}"` : `"${meal.name}" eingetragen`);
@@ -793,7 +789,7 @@
}) })
}); });
await goto(`/fitness/${s.nutrition}?date=${currentDate}`, { replaceState: true, noScroll: true }); await goto(`/fitness/${s.nutrition}/${currentDate}`, { replaceState: true, noScroll: true });
selectedCmMeal = null; selectedCmMeal = null;
cancelAdd(); cancelAdd();
toast.success(isEn ? `Logged "${meal.name}"` : `"${meal.name}" eingetragen`); toast.success(isEn ? `Logged "${meal.name}"` : `"${meal.name}" eingetragen`);
@@ -1160,18 +1156,18 @@
<div class="nutrition-page"> <div class="nutrition-page">
<!-- Date Navigator --> <!-- Date Navigator -->
<div class="date-nav"> <div class="date-nav">
<button class="date-btn" onclick={() => navigateDate(-1)} aria-label="Previous day"> <a class="date-btn" href={prevHref} aria-label="Previous day" data-sveltekit-replacestate data-sveltekit-noscroll>
<ChevronLeft size={20} /> <ChevronLeft size={20} />
</button> </a>
<button class="date-display" onclick={goToday} class:is-today={isToday}> <span class="date-display" class:is-today={isToday}>
{displayDate} {displayDate}
{#if isToday}<span class="today-badge">{t('today', lang)}</span>{/if} {#if isToday}<span class="today-badge">{t('today', lang)}</span>{/if}
</button> </span>
<button class="date-btn" onclick={() => navigateDate(1)} aria-label="Next day"> <a class="date-btn" href={nextHref} aria-label="Next day" data-sveltekit-replacestate data-sveltekit-noscroll>
<ChevronRight size={20} /> <ChevronRight size={20} />
</button> </a>
{#if !isToday} {#if !isToday}
<button class="go-today-btn" onclick={goToday}>{t('today', lang)}</button> <a class="go-today-btn" href={todayHref} data-sveltekit-replacestate data-sveltekit-noscroll>{t('today', lang)}</a>
{/if} {/if}
</div> </div>
@@ -1923,6 +1919,7 @@
border-radius: 50%; border-radius: 50%;
display: flex; display: flex;
align-items: center; align-items: center;
text-decoration: none;
transition: color 0.15s, background 0.15s; transition: color 0.15s, background 0.15s;
} }
.date-btn:hover { .date-btn:hover {
@@ -1935,7 +1932,6 @@
color: var(--color-text-primary); color: var(--color-text-primary);
font-size: 1.05rem; font-size: 1.05rem;
font-weight: 700; font-weight: 700;
cursor: pointer;
padding: 0.35rem 0.75rem; padding: 0.35rem 0.75rem;
border-radius: 8px; border-radius: 8px;
display: flex; display: flex;
@@ -1944,9 +1940,6 @@
transition: background 0.15s; transition: background 0.15s;
letter-spacing: -0.01em; letter-spacing: -0.01em;
} }
.date-display:hover {
background: var(--color-bg-elevated);
}
.today-badge { .today-badge {
font-size: 0.6rem; font-size: 0.6rem;
font-weight: 700; font-weight: 700;
@@ -1966,6 +1959,7 @@
background: color-mix(in srgb, var(--color-primary) 10%, transparent); background: color-mix(in srgb, var(--color-primary) 10%, transparent);
border: 1px solid color-mix(in srgb, var(--color-primary) 25%, transparent); border: 1px solid color-mix(in srgb, var(--color-primary) 25%, transparent);
padding: 0.25rem 0.6rem; padding: 0.25rem 0.6rem;
text-decoration: none;
border-radius: 6px; border-radius: 6px;
cursor: pointer; cursor: pointer;
transition: background 0.15s; transition: background 0.15s;