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 @@