calorie-ai-android/web/src/DashboardPage.svelte
Daniel 33d50258ce
Some checks failed
Android CI / build (push) Successful in 1m52s
Web CI / test (push) Successful in 54s
Android Release / release-apk (push) Failing after 21s
Improve plan UX and split web UI components
2026-05-21 03:40:47 +02:00

109 lines
6 KiB
Svelte

<script>
import Stat from './Stat.svelte';
export let today;
export let activeCalorieTarget;
export let targetPercent;
export let macroTotal;
export let produceToday;
export let todayEntries = [];
export let todayActivities = [];
export let activityToday;
export let weekAverage;
export let selectPage;
</script>
<section class="space-y-4">
<div class="grid gap-3 sm:grid-cols-2 xl:grid-cols-5">
<article class="sm:col-span-2 rounded-2xl bg-gradient-to-br from-blue-600 to-indigo-700 p-5 text-white shadow-sm">
<div class="text-xs font-semibold uppercase tracking-widest text-blue-200">Today</div>
<div id="todayCalories" class="mt-1 text-4xl font-black tracking-tight">{today.calories} kcal</div>
{#if activeCalorieTarget}
<div class="mt-2 h-2 overflow-hidden rounded-full bg-white/20"><div class="h-full rounded-full bg-white" style="width:{targetPercent}%"></div></div>
<div class="mt-1 text-xs font-semibold text-blue-100">{Math.max(0, activeCalorieTarget - today.calories)} kcal left of {activeCalorieTarget}</div>
{/if}
<div id="todayMacroText" class="mt-2 text-sm font-medium text-blue-100">P {today.protein}g &nbsp;·&nbsp; C {today.carbs}g &nbsp;·&nbsp; F {today.fat}g</div>
<div id="todayProduceText" class="text-sm text-blue-200">Fruit {today.fruit.toFixed(1)} &nbsp;·&nbsp; Veg {today.vegetables.toFixed(1)}</div>
</article>
<Stat label="Meals today" value={today.meals} note={today.meals === 1 ? 'meal logged' : 'meals logged'} />
<Stat label="7-day average" value={`${weekAverage} kcal`} note="Calories per day" />
<Stat label="Activity today" value={`${Math.round(activityToday.calories)} kcal`} note={`${activityToday.steps} steps`} />
</div>
<div class="grid gap-4 lg:grid-cols-2">
<section class="card">
<h3 class="card-title">Macros today</h3>
<div class="space-y-3 p-4">
{#each [['Protein', today.protein, 'bg-blue-500'], ['Carbs', today.carbs, 'bg-amber-400'], ['Fat', today.fat, 'bg-violet-500']] as [label, val, cls]}
<div>
<div class="mb-1.5 flex justify-between text-sm">
<span class="font-semibold text-slate-700">{label}</span>
<span class="text-slate-500">{val}g &nbsp;·&nbsp; {Math.round(val / macroTotal * 100)}%</span>
</div>
<div class="h-2.5 overflow-hidden rounded-full bg-slate-100">
<div class="h-full rounded-full transition-all {cls}" style="width:{Math.round(val / macroTotal * 100)}%"></div>
</div>
</div>
{/each}
<div class="border-t border-slate-100 pt-2 text-xs text-slate-400">{today.calories} kcal total &nbsp;·&nbsp; produce {produceToday.toFixed(1)} / 5</div>
</div>
</section>
<section class="card">
<h3 class="card-title">Today's meals</h3>
<div class="divide-y divide-slate-100">
{#each todayEntries as entry}
<div class="flex items-center justify-between px-4 py-3">
<div class="min-w-0">
<div class="truncate text-sm font-semibold text-slate-800">{entry.mealName || entry.description}</div>
<div class="text-xs text-slate-400">{entry.mealType} · {entry.time}</div>
</div>
<div class="ml-3 shrink-0 text-sm font-bold text-slate-600">{entry.calories} kcal</div>
</div>
{:else}
<div class="px-4 py-8 text-center text-sm text-slate-400">No meals logged today</div>
{/each}
</div>
{#if todayEntries.length}
<div class="border-t border-slate-100 px-4 py-2">
<button class="text-xs font-semibold text-blue-600 hover:text-blue-700" type="button" on:click={() => selectPage('log')}>+ Log another meal</button>
</div>
{:else}
<div class="border-t border-slate-100 px-4 py-2">
<button class="text-xs font-semibold text-blue-600 hover:text-blue-700" type="button" on:click={() => selectPage('log')}>Log your first meal today</button>
</div>
{/if}
</section>
<section class="card lg:col-span-2">
<h3 class="card-title">Synced activity</h3>
<div class="divide-y divide-slate-100">
{#each todayActivities.slice(0, 5) as activity}
<div class="flex items-center justify-between px-4 py-3">
<div class="min-w-0">
<div class="truncate text-sm font-semibold text-slate-800">{activity.title || activity.type || 'Activity'}</div>
<div class="text-xs text-slate-400">{activity.sourceName || activity.source || 'External'} · {String(activity.startAt || '').slice(11, 16) || 'All day'}</div>
</div>
<div class="ml-3 shrink-0 text-right text-sm font-bold text-slate-600">
{Math.round(Number(activity.calories || 0))} kcal
{#if activity.steps}<div class="text-xs font-medium text-slate-400">{activity.steps} steps</div>{/if}
</div>
</div>
{:else}
<div class="grid gap-4 p-4 md:grid-cols-[1fr_1.1fr] md:items-center">
<div class="rounded-2xl bg-blue-50 p-4 text-left">
<div class="text-xs font-black uppercase tracking-widest text-blue-700">Connect Garmin</div>
<h4 class="mt-1 text-lg font-black text-slate-950">Use the Android app to sync your watch.</h4>
<p class="mt-2 text-sm leading-6 text-slate-600">The web dashboard shows synced activity, but Garmin pairing runs through Garmin Connect and Android Health Connect.</p>
</div>
<ol class="grid gap-2 text-sm leading-6 text-slate-600">
<li><strong class="text-slate-900">1.</strong> Sync your watch in Garmin Connect on Android.</li>
<li><strong class="text-slate-900">2.</strong> In Garmin Connect, enable sharing to Health Connect.</li>
<li><strong class="text-slate-900">3.</strong> Open Calorie AI on Android and tap Settings -> Sync Health Connect.</li>
</ol>
</div>
{/each}
</div>
</section>
</div>
</section>