feat(ui): custom dialog replaces all remaining window.alert() calls
All checks were successful
Build & Publish Docker Image / build-and-push (push) Successful in 53s

alertAction({title, message}) returns Promise<void> and renders the
same ConfirmDialog with infoOnly:true — single OK button, no Abbrechen.
Replaces:
- 'Bitte Profil wählen.' (recipe rating / favorite / cooked / comment)
- 'Bitte Profil wählen, um zu liken.' (wishlist)
- 'Profil konnte nicht angelegt werden' (ProfileSwitcher)
- 'Umbenennen fehlgeschlagen' (admin/profiles)
- 'Speichern fehlgeschlagen' (preview)

No window.alert() or window.confirm() left in the codebase.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-17 17:23:07 +02:00
parent 1b9928f806
commit 4f7c76c908
7 changed files with 59 additions and 18 deletions

View File

@@ -4,6 +4,8 @@ export type ConfirmOptions = {
confirmLabel?: string; confirmLabel?: string;
cancelLabel?: string; cancelLabel?: string;
destructive?: boolean; destructive?: boolean;
/** If true, hide the cancel button — used for simple info/alert dialogs. */
infoOnly?: boolean;
}; };
type PendingRequest = ConfirmOptions & { type PendingRequest = ConfirmOptions & {
@@ -39,3 +41,14 @@ export function confirmAction(options: ConfirmOptions): Promise<boolean> {
if (typeof window === 'undefined') return Promise.resolve(false); if (typeof window === 'undefined') return Promise.resolve(false);
return confirmStore.ask(options); return confirmStore.ask(options);
} }
/**
* Show a modal info dialog with a single OK button. Resolves when dismissed.
* Use instead of window.alert().
*/
export function alertAction(options: Omit<ConfirmOptions, 'destructive' | 'cancelLabel' | 'infoOnly'>): Promise<void> {
if (typeof window === 'undefined') return Promise.resolve();
return confirmStore
.ask({ ...options, infoOnly: true, confirmLabel: options.confirmLabel ?? 'OK' })
.then(() => undefined);
}

View File

@@ -41,13 +41,15 @@
<p class="message">{p.message}</p> <p class="message">{p.message}</p>
{/if} {/if}
<div class="actions"> <div class="actions">
<button {#if !p.infoOnly}
type="button" <button
class="btn cancel" type="button"
onclick={() => confirmStore.answer(false)} class="btn cancel"
> onclick={() => confirmStore.answer(false)}
{p.cancelLabel ?? 'Abbrechen'} >
</button> {p.cancelLabel ?? 'Abbrechen'}
</button>
{/if}
<button <button
type="button" type="button"
class="btn confirm" class="btn confirm"

View File

@@ -1,5 +1,6 @@
<script lang="ts"> <script lang="ts">
import { profileStore } from '$lib/client/profile.svelte'; import { profileStore } from '$lib/client/profile.svelte';
import { alertAction } from '$lib/client/confirm.svelte';
let showModal = $state(false); let showModal = $state(false);
let newName = $state(''); let newName = $state('');
@@ -13,7 +14,10 @@
newName = ''; newName = '';
showModal = false; showModal = false;
} catch (e) { } catch (e) {
alert((e as Error).message); await alertAction({
title: 'Profil konnte nicht angelegt werden',
message: (e as Error).message
});
} }
} }
</script> </script>

View File

@@ -1,6 +1,6 @@
<script lang="ts"> <script lang="ts">
import { profileStore } from '$lib/client/profile.svelte'; import { profileStore } from '$lib/client/profile.svelte';
import { confirmAction } from '$lib/client/confirm.svelte'; import { confirmAction, alertAction } from '$lib/client/confirm.svelte';
let newName = $state(''); let newName = $state('');
let newEmoji = $state('🍳'); let newEmoji = $state('🍳');
@@ -31,7 +31,10 @@
}); });
if (!res.ok) { if (!res.ok) {
const body = await res.json().catch(() => ({})); const body = await res.json().catch(() => ({}));
alert(`Fehler: ${body.message ?? res.status}`); await alertAction({
title: 'Umbenennen fehlgeschlagen',
message: body.message ?? `HTTP ${res.status}`
});
return; return;
} }
await profileStore.load(); await profileStore.load();

View File

@@ -2,6 +2,7 @@
import { page } from '$app/stores'; import { page } from '$app/stores';
import { goto } from '$app/navigation'; import { goto } from '$app/navigation';
import RecipeView from '$lib/components/RecipeView.svelte'; import RecipeView from '$lib/components/RecipeView.svelte';
import { alertAction } from '$lib/client/confirm.svelte';
import type { Recipe } from '$lib/types'; import type { Recipe } from '$lib/types';
let targetUrl = $state(($page.url.searchParams.get('url') ?? '').trim()); let targetUrl = $state(($page.url.searchParams.get('url') ?? '').trim());
@@ -45,7 +46,10 @@
saving = false; saving = false;
if (!res.ok) { if (!res.ok) {
const body = await res.json().catch(() => ({})); const body = await res.json().catch(() => ({}));
alert(`Speichern fehlgeschlagen: ${body.message ?? res.status}`); await alertAction({
title: 'Speichern fehlgeschlagen',
message: body.message ?? `HTTP ${res.status}`
});
return; return;
} }
const body = await res.json(); const body = await res.json();

View File

@@ -4,7 +4,7 @@
import RecipeView from '$lib/components/RecipeView.svelte'; import RecipeView from '$lib/components/RecipeView.svelte';
import StarRating from '$lib/components/StarRating.svelte'; import StarRating from '$lib/components/StarRating.svelte';
import { profileStore } from '$lib/client/profile.svelte'; import { profileStore } from '$lib/client/profile.svelte';
import { confirmAction } from '$lib/client/confirm.svelte'; import { confirmAction, alertAction } from '$lib/client/confirm.svelte';
import type { CommentRow } from '$lib/server/recipes/actions'; import type { CommentRow } from '$lib/server/recipes/actions';
let { data } = $props(); let { data } = $props();
@@ -40,7 +40,10 @@
async function setRating(stars: number) { async function setRating(stars: number) {
if (!profileStore.active) { if (!profileStore.active) {
alert('Bitte erst Profil wählen.'); await alertAction({
title: 'Kein Profil gewählt',
message: 'Tippe oben rechts auf „Profil wählen", dann klappt die Aktion.'
});
return; return;
} }
await fetch(`/api/recipes/${data.recipe.id}/rating`, { await fetch(`/api/recipes/${data.recipe.id}/rating`, {
@@ -55,7 +58,10 @@
async function toggleFavorite() { async function toggleFavorite() {
if (!profileStore.active) { if (!profileStore.active) {
alert('Bitte erst Profil wählen.'); await alertAction({
title: 'Kein Profil gewählt',
message: 'Tippe oben rechts auf „Profil wählen", dann klappt die Aktion.'
});
return; return;
} }
const method = isFav ? 'DELETE' : 'PUT'; const method = isFav ? 'DELETE' : 'PUT';
@@ -69,7 +75,10 @@
async function logCooked() { async function logCooked() {
if (!profileStore.active) { if (!profileStore.active) {
alert('Bitte erst Profil wählen.'); await alertAction({
title: 'Kein Profil gewählt',
message: 'Tippe oben rechts auf „Profil wählen", dann klappt die Aktion.'
});
return; return;
} }
const res = await fetch(`/api/recipes/${data.recipe.id}/cooked`, { const res = await fetch(`/api/recipes/${data.recipe.id}/cooked`, {
@@ -83,7 +92,10 @@
async function addComment() { async function addComment() {
if (!profileStore.active) { if (!profileStore.active) {
alert('Bitte erst Profil wählen.'); await alertAction({
title: 'Kein Profil gewählt',
message: 'Tippe oben rechts auf „Profil wählen", dann klappt die Aktion.'
});
return; return;
} }
const text = newComment.trim(); const text = newComment.trim();

View File

@@ -1,7 +1,7 @@
<script lang="ts"> <script lang="ts">
import { onMount } from 'svelte'; import { onMount } from 'svelte';
import { profileStore } from '$lib/client/profile.svelte'; import { profileStore } from '$lib/client/profile.svelte';
import { confirmAction } from '$lib/client/confirm.svelte'; import { confirmAction, alertAction } from '$lib/client/confirm.svelte';
import type { WishlistEntry, SortKey } from '$lib/server/wishlist/repository'; import type { WishlistEntry, SortKey } from '$lib/server/wishlist/repository';
let entries = $state<WishlistEntry[]>([]); let entries = $state<WishlistEntry[]>([]);
@@ -27,7 +27,10 @@
async function toggleLike(entry: WishlistEntry) { async function toggleLike(entry: WishlistEntry) {
if (!profileStore.active) { if (!profileStore.active) {
alert('Bitte Profil wählen, um zu liken.'); await alertAction({
title: 'Kein Profil gewählt',
message: 'Tippe oben rechts auf „Profil wählen", um zu liken.'
});
return; return;
} }
const method = entry.liked_by_me ? 'DELETE' : 'PUT'; const method = entry.liked_by_me ? 'DELETE' : 'PUT';