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:
@@ -0,0 +1,5 @@
|
|||||||
|
import type { ParamMatcher } from '@sveltejs/kit';
|
||||||
|
|
||||||
|
export const match: ParamMatcher = (param) => {
|
||||||
|
return /^\d{4}-\d{2}-\d{2}$/.test(param);
|
||||||
|
};
|
||||||
@@ -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 */
|
||||||
|
|||||||
+2
-2
@@ -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');
|
||||||
+18
-24
@@ -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;
|
||||||
Reference in New Issue
Block a user