From 111fa91427aea87f6ff88cc6ce7f29b2362f467f Mon Sep 17 00:00:00 2001 From: Alexander Bocken Date: Wed, 8 Apr 2026 16:47:21 +0200 Subject: [PATCH] feat: replace browser confirm() with reusable ConfirmDialog component Promise-based modal dialog with backdrop, keyboard support, and animations, replacing all 18 native confirm() call sites across fitness, cospend, recipes, and tasks pages. --- package.json | 2 +- src/lib/components/ConfirmDialog.svelte | 108 ++++++++++++++++++ .../components/cospend/PaymentModal.svelte | 3 +- .../components/fitness/PeriodTracker.svelte | 3 +- .../recipes/CreateIngredientList.svelte | 9 +- .../components/recipes/CreateStepList.svelte | 9 +- src/lib/js/confirmDialog.svelte.ts | 55 +++++++++ src/routes/+layout.svelte | 4 +- .../payments/+page.svelte | 3 +- .../payments/edit/[id]/+page.svelte | 3 +- .../recurring/+page.svelte | 3 +- .../to-try/+page.svelte | 3 +- .../[id]/+page.svelte | 5 +- .../[measure=fitnessMeasure]/+page.svelte | 3 +- .../edit/[id]/+page.svelte | 3 +- .../[nutrition=fitnessNutrition]/+page.svelte | 3 +- .../meals/+page.svelte | 3 +- .../[active=fitnessActive]/+page.svelte | 3 +- src/routes/tasks/+page.svelte | 3 +- src/routes/tasks/rewards/+page.svelte | 3 +- 20 files changed, 206 insertions(+), 25 deletions(-) create mode 100644 src/lib/components/ConfirmDialog.svelte create mode 100644 src/lib/js/confirmDialog.svelte.ts diff --git a/package.json b/package.json index 60918e7..8c88391 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "homepage", - "version": "1.13.4", + "version": "1.14.0", "private": true, "type": "module", "scripts": { diff --git a/src/lib/components/ConfirmDialog.svelte b/src/lib/components/ConfirmDialog.svelte new file mode 100644 index 0000000..cd58dd2 --- /dev/null +++ b/src/lib/components/ConfirmDialog.svelte @@ -0,0 +1,108 @@ + + + + +{#if dialog.open} +
dialog.respond(false)} role="presentation"> +
e.stopPropagation()} role="alertdialog" aria-modal="true"> + {#if dialog.title} +

{dialog.title}

+ {/if} +

{dialog.message}

+
+ + +
+
+
+{/if} + + diff --git a/src/lib/components/cospend/PaymentModal.svelte b/src/lib/components/cospend/PaymentModal.svelte index 198a801..17a842f 100644 --- a/src/lib/components/cospend/PaymentModal.svelte +++ b/src/lib/components/cospend/PaymentModal.svelte @@ -7,6 +7,7 @@ import { getCategoryEmoji } from '$lib/utils/categories'; import { formatCurrency as formatCurrencyUtil } from '$lib/utils/formatters'; import { detectCospendLang, cospendRoot, t, locale, splitDescription, paymentCategoryName } from '$lib/js/cospendI18n'; + import { confirm } from '$lib/js/confirmDialog.svelte'; let { paymentId, onclose, onpaymentDeleted } = $props(); @@ -110,7 +111,7 @@ let deleting = $state(false); async function deletePayment() { - if (!confirm(t('delete_payment_confirm', lang))) { + if (!await confirm(t('delete_payment_confirm', lang))) { return; } diff --git a/src/lib/components/fitness/PeriodTracker.svelte b/src/lib/components/fitness/PeriodTracker.svelte index d06e9c1..684870a 100644 --- a/src/lib/components/fitness/PeriodTracker.svelte +++ b/src/lib/components/fitness/PeriodTracker.svelte @@ -2,6 +2,7 @@ import { t } from '$lib/js/fitnessI18n'; import { Trash2, Plus, Pencil, UserPlus, X, ChevronLeft, ChevronRight } from '@lucide/svelte'; import { toast } from '$lib/js/toast.svelte'; + import { confirm } from '$lib/js/confirmDialog.svelte'; import ProfilePicture from '$lib/components/cospend/ProfilePicture.svelte'; /** @@ -523,7 +524,7 @@ /** @param {string} id */ async function deletePeriod(id) { - if (!confirm(t('delete_period_confirm', lang))) return; + if (!await confirm(t('delete_period_confirm', lang))) return; try { const res = await fetch(`/api/fitness/period/${id}`, { method: 'DELETE' }); if (res.ok) { diff --git a/src/lib/components/recipes/CreateIngredientList.svelte b/src/lib/components/recipes/CreateIngredientList.svelte index 6acda7f..25bbfce 100644 --- a/src/lib/components/recipes/CreateIngredientList.svelte +++ b/src/lib/components/recipes/CreateIngredientList.svelte @@ -7,6 +7,7 @@ import Plus from '$lib/assets/icons/Plus.svelte' import Check from '$lib/assets/icons/Check.svelte' import "$lib/css/action_button.css" +import { confirm } from '$lib/js/confirmDialog.svelte' import { do_on_key } from '$lib/components/recipes/do_on_key.js' import { portions } from '$lib/js/portions_store.js' @@ -138,8 +139,8 @@ function handleSelect(recipe: any, options: any) { showSelector = false; } -export function removeReference(list_index: number) { - const confirmed = confirm(t[lang].confirmDeleteReference); +export async function removeReference(list_index: number) { + const confirmed = await confirm(t[lang].confirmDeleteReference); if (confirmed) { ingredients.splice(list_index, 1); ingredients = ingredients; @@ -265,9 +266,9 @@ export function add_new_ingredient(){ ingredients[list_index].list.push({ ...new_ingredient}) ingredients = ingredients //tells svelte to update dom } -export function remove_list(list_index: number){ +export async function remove_list(list_index: number){ if(ingredients[list_index].list.length > 1){ - const response = confirm(t[lang].confirmDeleteList); + const response = await confirm(t[lang].confirmDeleteList); if(!response){ return } diff --git a/src/lib/components/recipes/CreateStepList.svelte b/src/lib/components/recipes/CreateStepList.svelte index 399c2d3..8baddd9 100644 --- a/src/lib/components/recipes/CreateStepList.svelte +++ b/src/lib/components/recipes/CreateStepList.svelte @@ -8,6 +8,7 @@ import Check from '$lib/assets/icons/Check.svelte' import "$lib/css/action_button.css" import { do_on_key } from '$lib/components/recipes/do_on_key.js' +import { confirm } from '$lib/js/confirmDialog.svelte' import BaseRecipeSelector from '$lib/components/recipes/BaseRecipeSelector.svelte' let { lang = 'de' as 'de' | 'en', instructions = $bindable(), add_info = $bindable() } = $props<{ lang?: 'de' | 'en', instructions: any, add_info: any }>(); @@ -134,8 +135,8 @@ function handleSelect(recipe: any, options: any) { showSelector = false; } -export function removeReference(list_index: number) { - const confirmed = confirm(t[lang].confirmDeleteReference); +export async function removeReference(list_index: number) { + const confirmed = await confirm(t[lang].confirmDeleteReference); if (confirmed) { instructions.splice(list_index, 1); instructions = instructions; @@ -219,9 +220,9 @@ function get_sublist_index(sublist_name: string, list: any[]){ } return -1 } -export function remove_list(list_index: number){ +export async function remove_list(list_index: number){ if(instructions[list_index].steps.length > 1){ - const response = confirm(t[lang].confirmDeleteList); + const response = await confirm(t[lang].confirmDeleteList); if(!response){ return } diff --git a/src/lib/js/confirmDialog.svelte.ts b/src/lib/js/confirmDialog.svelte.ts new file mode 100644 index 0000000..13473dc --- /dev/null +++ b/src/lib/js/confirmDialog.svelte.ts @@ -0,0 +1,55 @@ +interface ConfirmState { + open: boolean; + title: string; + message: string; + confirmText: string; + cancelText: string; + destructive: boolean; + resolve: ((value: boolean) => void) | null; +} + +let state = $state({ + open: false, + title: '', + message: '', + confirmText: 'OK', + cancelText: 'Cancel', + destructive: false, + resolve: null, +}); + +export function getConfirmDialog() { + return { + get open() { return state.open; }, + get title() { return state.title; }, + get message() { return state.message; }, + get confirmText() { return state.confirmText; }, + get cancelText() { return state.cancelText; }, + get destructive() { return state.destructive; }, + respond(value: boolean) { + state.resolve?.(value); + state = { ...state, open: false, resolve: null }; + } + }; +} + +interface ConfirmOptions { + title?: string; + confirmText?: string; + cancelText?: string; + destructive?: boolean; +} + +export function confirm(message: string, options: ConfirmOptions = {}): Promise { + return new Promise((resolve) => { + state = { + open: true, + message, + title: options.title ?? '', + confirmText: options.confirmText ?? 'OK', + cancelText: options.cancelText ?? 'Cancel', + destructive: options.destructive ?? true, + resolve, + }; + }); +} diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte index 507e105..8b30a0e 100644 --- a/src/routes/+layout.svelte +++ b/src/routes/+layout.svelte @@ -2,6 +2,7 @@ import '../app.css'; import { onNavigate } from '$app/navigation'; import Toast from '$lib/components/Toast.svelte'; + import ConfirmDialog from '$lib/components/ConfirmDialog.svelte'; let { children } = $props(); onNavigate((navigation) => { @@ -22,4 +23,5 @@ {@render children()} - \ No newline at end of file + + \ No newline at end of file diff --git a/src/routes/[cospendRoot=cospendRoot]/payments/+page.svelte b/src/routes/[cospendRoot=cospendRoot]/payments/+page.svelte index c1e788b..ce0578a 100644 --- a/src/routes/[cospendRoot=cospendRoot]/payments/+page.svelte +++ b/src/routes/[cospendRoot=cospendRoot]/payments/+page.svelte @@ -5,6 +5,7 @@ import ProfilePicture from '$lib/components/cospend/ProfilePicture.svelte'; import { getCategoryEmoji } from '$lib/utils/categories'; import { toast } from '$lib/js/toast.svelte'; + import { confirm } from '$lib/js/confirmDialog.svelte'; import { isSettlementPayment, getSettlementIcon, getSettlementReceiver } from '$lib/utils/settlements'; import AddButton from '$lib/components/AddButton.svelte'; import { detectCospendLang, cospendRoot, t, locale, splitDescription, paymentCategoryName } from '$lib/js/cospendI18n'; @@ -82,7 +83,7 @@ } async function deletePayment(/** @type {string} */ paymentId) { - if (!confirm(t('delete_payment_confirm', lang))) { + if (!await confirm(t('delete_payment_confirm', lang))) { return; } diff --git a/src/routes/[cospendRoot=cospendRoot]/payments/edit/[id]/+page.svelte b/src/routes/[cospendRoot=cospendRoot]/payments/edit/[id]/+page.svelte index 8d8cff2..64e9307 100644 --- a/src/routes/[cospendRoot=cospendRoot]/payments/edit/[id]/+page.svelte +++ b/src/routes/[cospendRoot=cospendRoot]/payments/edit/[id]/+page.svelte @@ -3,6 +3,7 @@ import { goto } from '$app/navigation'; import { page } from '$app/stores'; import { detectCospendLang, cospendRoot, locale, t, getCategoryOptionsI18n } from '$lib/js/cospendI18n'; + import { confirm } from '$lib/js/confirmDialog.svelte'; import FormSection from '$lib/components/FormSection.svelte'; import ImageUpload from '$lib/components/ImageUpload.svelte'; import SaveFab from '$lib/components/SaveFab.svelte'; @@ -264,7 +265,7 @@ let deleting = $state(false); async function deletePayment() { - if (!confirm('Are you sure you want to delete this payment? This action cannot be undone.')) { + if (!await confirm('Are you sure you want to delete this payment? This action cannot be undone.')) { return; } diff --git a/src/routes/[cospendRoot=cospendRoot]/recurring/+page.svelte b/src/routes/[cospendRoot=cospendRoot]/recurring/+page.svelte index 1873e7a..4a3a47c 100644 --- a/src/routes/[cospendRoot=cospendRoot]/recurring/+page.svelte +++ b/src/routes/[cospendRoot=cospendRoot]/recurring/+page.svelte @@ -3,6 +3,7 @@ import { getCategoryEmoji } from '$lib/utils/categories'; import ProfilePicture from '$lib/components/cospend/ProfilePicture.svelte'; import { toast } from '$lib/js/toast.svelte'; + import { confirm } from '$lib/js/confirmDialog.svelte'; import AddButton from '$lib/components/AddButton.svelte'; import { formatCurrency } from '$lib/utils/formatters'; import Toggle from '$lib/components/Toggle.svelte'; @@ -65,7 +66,7 @@ } async function deleteRecurringPayment(/** @type {string} */ paymentId, /** @type {string} */ title) { - if (!confirm(`${t('delete_recurring_confirm', lang)} "${title}"?`)) { + if (!await confirm(`${t('delete_recurring_confirm', lang)} "${title}"?`)) { return; } diff --git a/src/routes/[recipeLang=recipeLang]/to-try/+page.svelte b/src/routes/[recipeLang=recipeLang]/to-try/+page.svelte index d3c690a..840b4dd 100644 --- a/src/routes/[recipeLang=recipeLang]/to-try/+page.svelte +++ b/src/routes/[recipeLang=recipeLang]/to-try/+page.svelte @@ -1,5 +1,6 @@