From 30a447a3eaabd8073582d3fbaea38d7a5de74f4b Mon Sep 17 00:00:00 2001 From: hsiegeln <37154749+hsiegeln@users.noreply.github.com> Date: Sat, 18 Apr 2026 22:22:19 +0200 Subject: [PATCH] refactor(client): requireProfile() + asyncFetch wrapper MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit requireProfile(): - src/lib/client/profile.svelte.ts: neuer Helper, returnt das aktive Profile oder null nach standardisiertem alertAction - 5x in recipes/[id]/+page.svelte: setRating, toggleFavorite, logCooked, addComment, toggleWishlist verlieren je 7 Zeilen Guard-Klausel - profile-Variable im Closure macht den ! am profileStore.active obsolet asyncFetch(): - src/lib/client/api-fetch-wrapper.ts: returnt Response auf 2xx, null nach alertAction auf Fehler - 4 Call-Sites umgestellt: saveRecipe + saveTitle (recipes/[id]), saveEdit (admin/domains), rename (admin/profiles) - admin/domains add() bewusst nicht migriert — inline-Error-UX statt Modal Findings aus REVIEW-2026-04-18.md (Quick-Win 5) und redundancy.md --- src/lib/client/api-fetch-wrapper.ts | 25 +++++ src/lib/client/profile.svelte.ts | 15 +++ src/routes/admin/domains/+page.svelte | 32 +++---- src/routes/admin/profiles/+page.svelte | 26 +++-- src/routes/recipes/[id]/+page.svelte | 126 +++++++++---------------- 5 files changed, 114 insertions(+), 110 deletions(-) create mode 100644 src/lib/client/api-fetch-wrapper.ts diff --git a/src/lib/client/api-fetch-wrapper.ts b/src/lib/client/api-fetch-wrapper.ts new file mode 100644 index 0000000..3604f7b --- /dev/null +++ b/src/lib/client/api-fetch-wrapper.ts @@ -0,0 +1,25 @@ +import { alertAction } from '$lib/client/confirm.svelte'; + +/** + * Fetch wrapper for actions where a non-OK response should pop a modal + * via alertAction(). Returns the Response on 2xx, or null after showing + * the alert. Caller should `if (!res) return;` after the call. + * + * Use this for *interactive* actions (rename, delete, save). For form + * submissions where the error should appear inline next to the field + * (e.g. admin/domains add()), keep manual handling. + */ +export async function asyncFetch( + url: string, + init: RequestInit | undefined, + errorTitle: string +): Promise { + const res = await fetch(url, init); + if (res.ok) return res; + const body = (await res.json().catch(() => null)) as { message?: string } | null; + await alertAction({ + title: errorTitle, + message: body?.message ?? `HTTP ${res.status}` + }); + return null; +} diff --git a/src/lib/client/profile.svelte.ts b/src/lib/client/profile.svelte.ts index d0b8e32..d8ba289 100644 --- a/src/lib/client/profile.svelte.ts +++ b/src/lib/client/profile.svelte.ts @@ -1,4 +1,5 @@ import type { Profile } from '$lib/types'; +import { alertAction } from '$lib/client/confirm.svelte'; const STORAGE_KEY = 'kochwas.activeProfileId'; @@ -60,3 +61,17 @@ class ProfileStore { } export const profileStore = new ProfileStore(); + +/** + * Returns the active profile, or null after showing the standard + * "kein Profil gewählt" dialog. Use as the first line of any per-profile + * action so we don't repeat the guard at every call-site. + */ +export async function requireProfile(): Promise { + if (profileStore.active) return profileStore.active; + await alertAction({ + title: 'Kein Profil gewählt', + message: 'Tippe oben rechts auf „Profil wählen", dann klappt die Aktion.' + }); + return null; +} diff --git a/src/routes/admin/domains/+page.svelte b/src/routes/admin/domains/+page.svelte index 77ad96e..da2a010 100644 --- a/src/routes/admin/domains/+page.svelte +++ b/src/routes/admin/domains/+page.svelte @@ -2,7 +2,8 @@ import { onMount } from 'svelte'; import { Pencil, Check, X, Globe } from 'lucide-svelte'; import type { AllowedDomain } from '$lib/types'; - import { confirmAction, alertAction } from '$lib/client/confirm.svelte'; + import { confirmAction } from '$lib/client/confirm.svelte'; + import { asyncFetch } from '$lib/client/api-fetch-wrapper'; import { requireOnline } from '$lib/client/require-online'; let domains = $state([]); @@ -64,22 +65,19 @@ if (!requireOnline('Das Speichern')) return; saving = true; try { - const res = await fetch(`/api/domains/${d.id}`, { - method: 'PATCH', - headers: { 'content-type': 'application/json' }, - body: JSON.stringify({ - domain: editDomain.trim(), - display_name: editLabel.trim() || null - }) - }); - if (!res.ok) { - const body = await res.json().catch(() => ({})); - await alertAction({ - title: 'Speichern fehlgeschlagen', - message: body.message ?? `HTTP ${res.status}` - }); - return; - } + const res = await asyncFetch( + `/api/domains/${d.id}`, + { + method: 'PATCH', + headers: { 'content-type': 'application/json' }, + body: JSON.stringify({ + domain: editDomain.trim(), + display_name: editLabel.trim() || null + }) + }, + 'Speichern fehlgeschlagen' + ); + if (!res) return; cancelEdit(); await load(); } finally { diff --git a/src/routes/admin/profiles/+page.svelte b/src/routes/admin/profiles/+page.svelte index 52010aa..b398589 100644 --- a/src/routes/admin/profiles/+page.svelte +++ b/src/routes/admin/profiles/+page.svelte @@ -1,6 +1,7 @@