diary/today'
'
This commit is contained in:
parent
4bad3fff5b
commit
9f41c87309
1 changed files with 153 additions and 2 deletions
|
|
@ -1,9 +1,160 @@
|
|||
<script lang="ts">
|
||||
import { createQuery, useQueryClient } from '@tanstack/svelte-query';
|
||||
import { getDiary } from '$lib/api/diary';
|
||||
import { getCachedDiary, cacheDiary } from '$lib/offline/db';
|
||||
import { addDays, formatDisplay, today } from '$lib/utils/date';
|
||||
import { goto } from '$app/navigation';
|
||||
import { today } from '$lib/utils/date';
|
||||
import { onMount } from 'svelte';
|
||||
import MacroSummary from '$lib/components/diary/MacroSummary.svelte';
|
||||
import MealCard from '$lib/components/diary/MealCard.svelte';
|
||||
import DateNav from '$lib/components/diary/DateNav.svelte';
|
||||
import CalendarPicker from '$lib/components/diary/CalendarPicker.svelte';
|
||||
import Sheet from '$lib/components/ui/Sheet.svelte';
|
||||
import CommandPalette from '$lib/components/ui/CommandPalette.svelte';
|
||||
import type { Command } from '$lib/components/ui/CommandPalette.svelte';
|
||||
|
||||
const date = $derived(today());
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
let calendarOpen = $state(false);
|
||||
let commandOpen = $state(false);
|
||||
|
||||
const commands = $derived<Command[]>([
|
||||
...(diaryQuery.data?.meals ?? []).map(meal => ({
|
||||
id: `entry-${meal.id}`,
|
||||
label: `Add entry → ${meal.name}`,
|
||||
keywords: ['e', 'entry', 'food', 'add'],
|
||||
action: () => goto(`/diary/${date}/add-entry?meal_id=${meal.id}`)
|
||||
})),
|
||||
{
|
||||
id: 'add-meal',
|
||||
label: 'Add meal',
|
||||
keywords: ['m', 'meal'],
|
||||
action: () => goto(`/diary/${date}/add-meal?diary_id=${diaryQuery.data?.id}`)
|
||||
}
|
||||
]);
|
||||
|
||||
onMount(() => {
|
||||
goto(`/diary/${today()}`, { replaceState: true });
|
||||
function handleKeydown(e: KeyboardEvent) {
|
||||
const tag = (e.target as HTMLElement).tagName;
|
||||
const editable = (e.target as HTMLElement).isContentEditable;
|
||||
if (tag === 'INPUT' || tag === 'TEXTAREA' || tag === 'SELECT' || editable) return;
|
||||
if (e.key === '/' || e.key === ':') {
|
||||
e.preventDefault();
|
||||
commandOpen = true;
|
||||
}
|
||||
}
|
||||
document.addEventListener('keydown', handleKeydown);
|
||||
return () => document.removeEventListener('keydown', handleKeydown);
|
||||
});
|
||||
|
||||
const diaryQuery = createQuery(() => ({
|
||||
queryKey: ['diary', date],
|
||||
queryFn: async () => {
|
||||
if (!navigator.onLine) {
|
||||
const inMemory = queryClient.getQueryData<import('$lib/types/api').Diary>(['diary', date]);
|
||||
if (inMemory) return inMemory;
|
||||
const cached = await getCachedDiary(date);
|
||||
if (cached) return cached;
|
||||
throw new Error('Offline and no cached data');
|
||||
}
|
||||
const diary = await getDiary(date);
|
||||
await cacheDiary(JSON.parse(JSON.stringify(diary)));
|
||||
return diary;
|
||||
}
|
||||
}));
|
||||
|
||||
function goDate(delta: number) {
|
||||
goto(`/diary/${addDays(date, delta)}`);
|
||||
}
|
||||
|
||||
function handleDateSelect(d: string) {
|
||||
calendarOpen = false;
|
||||
goto(`/diary/${d}`);
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="flex flex-col h-screen">
|
||||
<!-- Header -->
|
||||
<header class="px-4 pt-[calc(0.75rem+var(--safe-top))] pb-3 bg-zinc-950 sticky top-0 z-10 border-b border-zinc-800">
|
||||
<DateNav
|
||||
{date}
|
||||
label={formatDisplay(date)}
|
||||
onPrev={() => goDate(-1)}
|
||||
onNext={() => goDate(1)}
|
||||
isToday={true}
|
||||
onDateClick={() => calendarOpen = true}
|
||||
/>
|
||||
</header>
|
||||
|
||||
<!-- Content -->
|
||||
<main class="flex-1 overflow-y-auto px-4 py-4 pb-[calc(5rem+var(--safe-bottom))] lg:pb-6">
|
||||
{#if diaryQuery.isPending}
|
||||
<!-- Skeleton -->
|
||||
<div class="animate-pulse space-y-4 lg:grid lg:grid-cols-[300px_1fr] lg:gap-6 lg:space-y-0 lg:items-start">
|
||||
<div class="h-28 bg-zinc-800 rounded-2xl"></div>
|
||||
<div class="space-y-4">
|
||||
<div class="h-40 bg-zinc-800 rounded-2xl"></div>
|
||||
<div class="h-40 bg-zinc-800 rounded-2xl"></div>
|
||||
</div>
|
||||
</div>
|
||||
{:else if diaryQuery.isError}
|
||||
<div class="text-center text-zinc-500 mt-20">
|
||||
<p class="text-lg">Could not load diary</p>
|
||||
<p class="text-sm mt-1">{diaryQuery.error.message}</p>
|
||||
<button
|
||||
onclick={() => diaryQuery.refetch()}
|
||||
class="mt-4 px-4 py-2 bg-zinc-800 rounded-xl text-sm"
|
||||
>
|
||||
Retry
|
||||
</button>
|
||||
</div>
|
||||
{:else if diaryQuery.data}
|
||||
{@const diary = diaryQuery.data}
|
||||
|
||||
<div class="space-y-4 lg:grid lg:grid-cols-[300px_1fr] lg:gap-6 lg:space-y-0 lg:items-start">
|
||||
<!-- Left: macros summary (sticky on desktop) -->
|
||||
<div class="lg:sticky lg:top-[4.5rem]">
|
||||
<MacroSummary macros={diary} />
|
||||
</div>
|
||||
|
||||
<!-- Right: meals -->
|
||||
<div class="space-y-4">
|
||||
{#each diary.meals as meal (meal.id)}
|
||||
<MealCard {meal} {date} diaryId={diary.id} />
|
||||
{/each}
|
||||
|
||||
<!-- Add meal button -->
|
||||
<button
|
||||
onclick={() => goto(`/diary/${date}/add-meal?diary_id=${diaryQuery.data?.id}`)}
|
||||
class="w-full border border-dashed border-zinc-700 rounded-2xl py-4 text-zinc-500 hover:text-zinc-300 hover:border-zinc-500 transition-colors text-sm font-medium"
|
||||
>
|
||||
+ Add meal
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</main>
|
||||
|
||||
<!-- FAB: Add entry (mobile only) -->
|
||||
{#if diaryQuery.data}
|
||||
<button
|
||||
onclick={() => {
|
||||
const firstMeal = diaryQuery.data?.meals[0];
|
||||
if (firstMeal) goto(`/diary/${date}/add-entry?meal_id=${firstMeal.id}`);
|
||||
}}
|
||||
class="lg:hidden fixed bottom-[calc(4rem+var(--safe-bottom))] right-4 z-30 w-14 h-14 rounded-full bg-green-600 hover:bg-green-500 shadow-lg flex items-center justify-center transition-colors"
|
||||
aria-label="Add entry"
|
||||
>
|
||||
<svg class="w-7 h-7" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4" />
|
||||
</svg>
|
||||
</button>
|
||||
{/if}
|
||||
|
||||
<Sheet open={calendarOpen} onclose={() => calendarOpen = false}>
|
||||
<CalendarPicker selected={date} onSelect={handleDateSelect} />
|
||||
</Sheet>
|
||||
|
||||
<CommandPalette {commands} open={commandOpen} onclose={() => commandOpen = false} />
|
||||
</div>
|
||||
|
|
|
|||
Loading…
Reference in a new issue