diff --git a/src/lib/api/client.ts b/src/lib/api/client.ts
index 4dc8b5f..29456be 100644
--- a/src/lib/api/client.ts
+++ b/src/lib/api/client.ts
@@ -5,6 +5,26 @@ import { network } from '$lib/offline/network.svelte';
const BASE = '/api';
+export class ApiError extends Error {
+ status: number;
+ detail: unknown;
+ constructor(message: string, status: number, detail: unknown) {
+ super(message);
+ this.name = 'ApiError';
+ this.status = status;
+ this.detail = detail;
+ }
+}
+
+export function extractErrorMessage(err: unknown, fallback = 'Something went wrong'): string {
+ if (err instanceof ApiError) {
+ const d = err.detail as Record | null;
+ if (typeof d?.detail === 'string') return d.detail;
+ }
+ if (err instanceof Error) return err.message;
+ return fallback;
+}
+
let isRefreshing = false;
let refreshPromise: Promise | null = null;
@@ -121,7 +141,7 @@ export async function apiPost(path: string, body: unknown): Promise {
return {} as T;
}
const err = await res.json().catch(() => ({}));
- throw Object.assign(new Error(`POST ${path} failed: ${res.status}`), { status: res.status, detail: err });
+ throw new ApiError(`POST ${path} failed: ${res.status}`, res.status, err);
}
return res.json();
}
diff --git a/src/lib/components/diary/MacroSummary.svelte b/src/lib/components/diary/MacroSummary.svelte
index 127bf0d..cb116fe 100644
--- a/src/lib/components/diary/MacroSummary.svelte
+++ b/src/lib/components/diary/MacroSummary.svelte
@@ -5,6 +5,8 @@
import { updateUserSettings } from "$lib/api/settings";
import { useQueryClient } from "@tanstack/svelte-query";
import Sheet from "$lib/components/ui/Sheet.svelte";
+ import Button from "$lib/components/ui/Button.svelte";
+ import Input from "$lib/components/ui/Input.svelte";
interface Props {
diary: Diary;
@@ -192,16 +194,8 @@
well?
-
-
+
+
@@ -220,16 +214,17 @@
>
{/if}
- {
- const v = e.currentTarget.value;
+ const v = (e.currentTarget as HTMLInputElement).value;
form.calories_goal = v === "" ? null : Number(v);
}}
- class="w-full bg-zinc-800 border border-zinc-700 rounded-xl px-4 py-2.5 text-zinc-100 focus:outline-none focus:border-green-500 transition-colors"
/>
Clear to auto-calculate from macro goals
@@ -242,22 +237,19 @@
>{field.label}
({field.unit})
-
{/each}
-
diff --git a/src/lib/components/diary/MealCard.svelte b/src/lib/components/diary/MealCard.svelte
index 31734ab..da675e8 100644
--- a/src/lib/components/diary/MealCard.svelte
+++ b/src/lib/components/diary/MealCard.svelte
@@ -7,6 +7,8 @@
import { deleteEntry } from "$lib/api/entries";
import { goto } from "$app/navigation";
import Sheet from "$lib/components/ui/Sheet.svelte";
+ import Button from "$lib/components/ui/Button.svelte";
+ import Input from "$lib/components/ui/Input.svelte";
interface Props {
meal: Meal;
@@ -92,6 +94,8 @@
>
(collapsed = !collapsed)}
+ aria-expanded={!collapsed}
+ aria-label="{meal.name}, {collapsed ? 'expand' : 'collapse'}"
class="flex-1 flex items-center gap-2 text-left min-w-0"
>
- handleSearch(e.currentTarget.value)}
+ oninput={(e) => handleSearch((e.currentTarget as HTMLInputElement).value)}
onkeydown={(e) => {
if (e.key === "Enter") {
const first = productsQuery.data?.[0];
if (first) selectProduct(first);
}
}}
- class="w-full bg-zinc-900 border border-zinc-700 rounded-xl pl-9 pr-4 py-2.5 text-sm text-zinc-100 placeholder-zinc-500 focus:outline-none focus:border-green-500 transition-colors"
+ class="text-sm placeholder-zinc-500 pl-9"
/>
@@ -295,54 +300,24 @@
{/if}
-
-
e.preventDefault()}
- onclick={() => {
- grams = Math.max(1, grams - 10);
- }}
- class="w-11 h-11 rounded-xl bg-zinc-800 hover:bg-zinc-700 transition-colors text-lg font-medium flex items-center justify-center"
- >−
-
e.currentTarget.select()}
- onkeydown={(e) => {
- if (e.key === "Enter") handleAddEntry();
- }}
- class="flex-1 bg-zinc-800 border border-zinc-700 rounded-xl px-4 py-2.5 text-center text-xl font-semibold focus:outline-none focus:border-green-500 transition-colors"
- />
-
e.preventDefault()}
- onclick={() => {
- grams = grams + 10;
- }}
- class="w-11 h-11 rounded-xl bg-zinc-800 hover:bg-zinc-700 transition-colors text-lg font-medium flex items-center justify-center"
- >+
+
+
{#if error}
{error}
{/if}
-
{submitting
? "Adding…"
: network.online
? "Add to meal"
: "Add to meal (offline)"}
-
+
{/if}
diff --git a/src/routes/(app)/diary/[date]/add-meal/+page.svelte b/src/routes/(app)/diary/[date]/add-meal/+page.svelte
index c3d4c6e..4ebfaa8 100644
--- a/src/routes/(app)/diary/[date]/add-meal/+page.svelte
+++ b/src/routes/(app)/diary/[date]/add-meal/+page.svelte
@@ -8,6 +8,8 @@
import { listPresets } from "$lib/api/presets";
import type { Preset } from "$lib/types/api";
import TopBar from "$lib/components/ui/TopBar.svelte";
+ import Button from "$lib/components/ui/Button.svelte";
+ import Input from "$lib/components/ui/Input.svelte";
import { kcal, g } from "$lib/utils/format";
import { today } from "$lib/utils/date";
@@ -45,6 +47,12 @@
queryFn: () => listPresets(30),
}));
+ const filteredPresets = $derived(
+ (presetsQuery.data ?? []).filter(
+ (p) => !presetDebounced || p.name.toLowerCase().includes(presetDebounced.toLowerCase()),
+ ),
+ );
+
async function handleCreateNew(e: SubmitEvent) {
e.preventDefault();
submitting = true;
@@ -99,39 +107,36 @@
-
-
+
{submitting
? "Creating…"
: network.online
? "Create meal"
: "Create meal (offline)"}
-
+
{:else}
- handlePresetSearch(e.currentTarget.value)}
+ oninput={(e) => handlePresetSearch((e.currentTarget as HTMLInputElement).value)}
autofocus
- class="w-full bg-zinc-900 border border-zinc-700 rounded-xl px-4 py-2.5 text-sm text-zinc-100 placeholder-zinc-500 focus:outline-none focus:border-green-500 transition-colors"
+ class="text-sm placeholder-zinc-500"
/>
@@ -145,18 +150,14 @@
{/each}
- {:else if (presetsQuery.data ?? []).filter((p) => !presetDebounced || p.name
- .toLowerCase()
- .includes(presetDebounced.toLowerCase())).length === 0}
+ {:else if filteredPresets.length === 0}
No presets yet
Save a meal as preset from the diary view
{:else}
- {#each (presetsQuery.data ?? []).filter((p) => !presetDebounced || p.name
- .toLowerCase()
- .includes(presetDebounced.toLowerCase())) as preset (preset.id)}
+ {#each filteredPresets as preset (preset.id)}
-
handleFromPreset(preset)}
diff --git a/src/routes/(app)/diary/[date]/edit-entry/[entry_id]/+page.svelte b/src/routes/(app)/diary/[date]/edit-entry/[entry_id]/+page.svelte
index ef27e06..f9c0921 100644
--- a/src/routes/(app)/diary/[date]/edit-entry/[entry_id]/+page.svelte
+++ b/src/routes/(app)/diary/[date]/edit-entry/[entry_id]/+page.svelte
@@ -6,6 +6,8 @@
import { offlineEditEntry } from "$lib/offline/mutations";
import { network } from "$lib/offline/network.svelte";
import TopBar from "$lib/components/ui/TopBar.svelte";
+ import Button from "$lib/components/ui/Button.svelte";
+ import GramsStepper from "$lib/components/ui/GramsStepper.svelte";
import { today } from "$lib/utils/date";
const date = $derived(
@@ -133,53 +135,27 @@
-
- e.preventDefault()}
- onclick={() => {
- grams = Math.max(1, grams - 10);
- }}
- class="w-12 h-12 rounded-xl bg-zinc-900 hover:bg-zinc-800 transition-colors text-xl font-medium flex items-center justify-center"
- >−
- e.currentTarget.select()}
- onkeydown={(e) => {
- if (e.key === "Enter") handleSave();
- }}
- class="flex-1 bg-zinc-900 border border-zinc-700 rounded-xl px-4 py-3 text-center text-2xl font-semibold focus:outline-none focus:border-green-500 transition-colors"
- />
- e.preventDefault()}
- onclick={() => {
- grams = grams + 10;
- }}
- class="w-12 h-12 rounded-xl bg-zinc-900 hover:bg-zinc-800 transition-colors text-xl font-medium flex items-center justify-center"
- >+
-
+
-
{saving
? "Saving…"
: network.online
? "Save changes"
: "Save changes (offline)"}
-
+
{/if}
diff --git a/src/routes/(app)/presets/+page.svelte b/src/routes/(app)/presets/+page.svelte
index 3b1faad..4de8b94 100644
--- a/src/routes/(app)/presets/+page.svelte
+++ b/src/routes/(app)/presets/+page.svelte
@@ -15,6 +15,9 @@
import { kcal, g } from "$lib/utils/format";
import TopBar from "$lib/components/ui/TopBar.svelte";
import Sheet from "$lib/components/ui/Sheet.svelte";
+ import Button from "$lib/components/ui/Button.svelte";
+ import Input from "$lib/components/ui/Input.svelte";
+ import GramsStepper from "$lib/components/ui/GramsStepper.svelte";
import type { Preset, PresetEntry, Product } from "$lib/types/api";
const queryClient = useQueryClient();
@@ -248,12 +251,13 @@
d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"
/>
- handleSearch(e.currentTarget.value)}
- class="w-full bg-zinc-900 border border-zinc-700 rounded-xl pl-9 pr-4 py-2.5 text-sm text-zinc-100 placeholder-zinc-500 focus:outline-none focus:border-green-500 transition-colors"
+ oninput={(e) => handleSearch((e.currentTarget as HTMLInputElement).value)}
+ class="text-sm placeholder-zinc-500 pl-9"
/>
@@ -511,21 +515,17 @@
title="Create new preset"
>
@@ -536,21 +536,17 @@
title="Rename preset"
>
@@ -565,44 +561,11 @@
{editingEntry.entry.product.name}
-
- e.preventDefault()}
- onclick={() => {
- editGrams = Math.max(1, editGrams - 10);
- }}
- class="w-11 h-11 rounded-xl bg-zinc-800 hover:bg-zinc-700 transition-colors text-lg font-medium flex items-center justify-center"
- >−
- e.currentTarget.select()}
- class="flex-1 bg-zinc-800 border border-zinc-700 rounded-xl px-4 py-2.5 text-center text-xl font-semibold focus:outline-none focus:border-green-500 transition-colors"
- />
- e.preventDefault()}
- onclick={() => {
- editGrams = editGrams + 10;
- }}
- class="w-11 h-11 rounded-xl bg-zinc-800 hover:bg-zinc-700 transition-colors text-lg font-medium flex items-center justify-center"
- >+
-
+
-
+
{editSaving ? "Saving…" : "Save"}
-
+
{/if}
@@ -620,14 +583,16 @@
{#if selectedProduct === null}
-
handleProductSearch(e.currentTarget.value)}
+ oninput={(e) => handleProductSearch((e.currentTarget as HTMLInputElement).value)}
autofocus
- class="w-full bg-zinc-800 border border-zinc-700 rounded-xl px-4 py-2.5 text-sm text-zinc-100 placeholder-zinc-500 focus:outline-none focus:border-green-500 transition-colors"
+ class="text-sm placeholder-zinc-500"
/>
{#if productsQuery.isPending && productDebouncedQ}
Searching…
@@ -673,52 +638,15 @@
-
- e.preventDefault()}
- onclick={() => {
- addGrams = Math.max(1, addGrams - 10);
- }}
- class="w-11 h-11 rounded-xl bg-zinc-800 hover:bg-zinc-700 transition-colors text-lg font-medium flex items-center justify-center"
- >−
- e.currentTarget.select()}
- class="flex-1 bg-zinc-800 border border-zinc-700 rounded-xl px-4 py-2.5 text-center text-xl font-semibold focus:outline-none focus:border-green-500 transition-colors"
- />
- e.preventDefault()}
- onclick={() => {
- addGrams = addGrams + 10;
- }}
- class="w-11 h-11 rounded-xl bg-zinc-800 hover:bg-zinc-700 transition-colors text-lg font-medium flex items-center justify-center"
- >+
-
+
- (selectedProduct = null)}
- class="flex-1 bg-zinc-800 hover:bg-zinc-700 rounded-xl py-3 text-sm font-medium transition-colors"
- >
+ (selectedProduct = null)}>
Back
-
-
+
+
{addSaving ? "Adding…" : "Add"}
-
+
{/if}
diff --git a/src/routes/(app)/products/new/+page.svelte b/src/routes/(app)/products/new/+page.svelte
index 5fb3f36..15ba991 100644
--- a/src/routes/(app)/products/new/+page.svelte
+++ b/src/routes/(app)/products/new/+page.svelte
@@ -2,6 +2,8 @@
import { page } from "$app/state";
import { createProduct } from "$lib/api/products";
import TopBar from "$lib/components/ui/TopBar.svelte";
+ import Button from "$lib/components/ui/Button.svelte";
+ import Input from "$lib/components/ui/Input.svelte";
let name = $state(page.url.searchParams.get("name") ?? "");
let barcode = $state(page.url.searchParams.get("barcode") ?? "");
@@ -52,15 +54,15 @@
-
@@ -69,7 +71,7 @@
-
-
-
-
@@ -138,13 +136,9 @@
{error}
{/if}
-
+
{submitting ? "Saving…" : "Save product"}
-
+
diff --git a/src/routes/(app)/settings/+page.svelte b/src/routes/(app)/settings/+page.svelte
index 97236b4..14b97bb 100644
--- a/src/routes/(app)/settings/+page.svelte
+++ b/src/routes/(app)/settings/+page.svelte
@@ -4,6 +4,7 @@
import { updateDiary } from "$lib/api/diary";
import TopBar from "$lib/components/ui/TopBar.svelte";
import Sheet from "$lib/components/ui/Sheet.svelte";
+ import Button from "$lib/components/ui/Button.svelte";
import { today } from "$lib/utils/date";
const queryClient = useQueryClient();
@@ -101,7 +102,7 @@
>
-
Calories
+
Calories
{#if form.calories_goal === null}
-
{field.label}
+
{field.label}
{field.unit} per day
{error}
{/if}
-
{saving ? "Saving…" : saved ? "Saved!" : "Save"}
-
+
{/if}
@@ -174,15 +178,7 @@
Override today's diary macro goals with these values as well?
- (syncDiaryOpen = false)}
- class="flex-1 bg-zinc-800 hover:bg-zinc-700 rounded-xl py-3 text-sm font-medium transition-colors"
- >No
- Yes
+ (syncDiaryOpen = false)}>No
+ Yes
diff --git a/src/routes/(auth)/login/+page.svelte b/src/routes/(auth)/login/+page.svelte
index 5a5abbf..8f31523 100644
--- a/src/routes/(auth)/login/+page.svelte
+++ b/src/routes/(auth)/login/+page.svelte
@@ -4,6 +4,8 @@
import { auth } from "$lib/auth/store.svelte";
import { onMount } from "svelte";
import { today } from "$lib/utils/date";
+ import Button from "$lib/components/ui/Button.svelte";
+ import Input from "$lib/components/ui/Input.svelte";
let username = $state("");
let password = $state("");
@@ -39,13 +41,12 @@
for="username"
class="block text-sm font-medium text-zinc-400 mb-1">Username
-
@@ -54,13 +55,12 @@
for="password"
class="block text-sm font-medium text-zinc-400 mb-1">Password
-
@@ -68,13 +68,9 @@
{error}
{/if}
-
+
{loading ? "Signing in…" : "Sign in"}
-
+
diff --git a/src/routes/(auth)/register/+page.svelte b/src/routes/(auth)/register/+page.svelte
index a10ea8a..d847d68 100644
--- a/src/routes/(auth)/register/+page.svelte
+++ b/src/routes/(auth)/register/+page.svelte
@@ -3,6 +3,9 @@
import { goto } from "$app/navigation";
import { onMount } from "svelte";
import { PUBLIC_TURNSTILE_SITE_KEY } from "$env/static/public";
+ import { extractErrorMessage } from "$lib/api/client";
+ import Button from "$lib/components/ui/Button.svelte";
+ import Input from "$lib/components/ui/Input.svelte";
let username = $state("");
let password = $state("");
@@ -52,8 +55,7 @@
await register(username, password, captchaToken);
goto(`/settings`);
} catch (err: unknown) {
- const e2 = err as { detail?: { detail?: string } };
- error = e2.detail?.detail ?? "Registration failed";
+ error = extractErrorMessage(err, "Registration failed");
} finally {
loading = false;
}
@@ -70,13 +72,12 @@
for="username"
class="block text-sm font-medium text-zinc-400 mb-1">Username
-
@@ -85,13 +86,12 @@
for="password"
class="block text-sm font-medium text-zinc-400 mb-1">Password
-
@@ -101,13 +101,12 @@
class="block text-sm font-medium text-zinc-400 mb-1"
>Confirm password
-
@@ -117,13 +116,9 @@
{error}
{/if}
-
+
{loading ? "Creating account…" : "Create account"}
-
+