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.
This commit is contained in:
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "homepage",
|
||||
"version": "1.13.4",
|
||||
"version": "1.14.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
<script>
|
||||
import { getConfirmDialog } from '$lib/js/confirmDialog.svelte';
|
||||
|
||||
const dialog = getConfirmDialog();
|
||||
|
||||
function onKeydown(e) {
|
||||
if (!dialog.open) return;
|
||||
if (e.key === 'Escape') dialog.respond(false);
|
||||
if (e.key === 'Enter') dialog.respond(true);
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:window onkeydown={onKeydown} />
|
||||
|
||||
{#if dialog.open}
|
||||
<div class="confirm-backdrop" onclick={() => dialog.respond(false)} role="presentation">
|
||||
<div class="confirm-dialog" onclick={(e) => e.stopPropagation()} role="alertdialog" aria-modal="true">
|
||||
{#if dialog.title}
|
||||
<h3 class="confirm-title">{dialog.title}</h3>
|
||||
{/if}
|
||||
<p class="confirm-message">{dialog.message}</p>
|
||||
<div class="confirm-actions">
|
||||
<button class="confirm-btn cancel" onclick={() => dialog.respond(false)}>
|
||||
{dialog.cancelText}
|
||||
</button>
|
||||
<button
|
||||
class="confirm-btn confirm"
|
||||
class:destructive={dialog.destructive}
|
||||
onclick={() => dialog.respond(true)}
|
||||
>
|
||||
{dialog.confirmText}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
.confirm-backdrop {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
z-index: 10000;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
animation: fade-in 150ms ease-out;
|
||||
}
|
||||
.confirm-dialog {
|
||||
background: var(--color-surface);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: 12px;
|
||||
padding: 1.25rem 1.5rem;
|
||||
max-width: 360px;
|
||||
width: calc(100vw - 2rem);
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
|
||||
animation: scale-in 150ms ease-out;
|
||||
}
|
||||
.confirm-title {
|
||||
margin: 0 0 0.5rem;
|
||||
font-size: 1rem;
|
||||
font-weight: 700;
|
||||
color: var(--color-text-primary);
|
||||
}
|
||||
.confirm-message {
|
||||
margin: 0 0 1.25rem;
|
||||
font-size: 0.85rem;
|
||||
line-height: 1.5;
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
.confirm-actions {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
.confirm-btn {
|
||||
padding: 0.45rem 1rem;
|
||||
border-radius: 8px;
|
||||
font-size: 0.85rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
border: none;
|
||||
transition: opacity 150ms;
|
||||
}
|
||||
.confirm-btn:hover {
|
||||
opacity: 0.85;
|
||||
}
|
||||
.confirm-btn.cancel {
|
||||
background: var(--color-bg-tertiary);
|
||||
color: var(--color-text-primary);
|
||||
}
|
||||
.confirm-btn.confirm {
|
||||
background: var(--color-primary);
|
||||
color: var(--color-text-on-primary);
|
||||
}
|
||||
.confirm-btn.confirm.destructive {
|
||||
background: var(--nord11);
|
||||
color: white;
|
||||
}
|
||||
@keyframes fade-in {
|
||||
from { opacity: 0; }
|
||||
to { opacity: 1; }
|
||||
}
|
||||
@keyframes scale-in {
|
||||
from { opacity: 0; transform: scale(0.95); }
|
||||
to { opacity: 1; transform: scale(1); }
|
||||
}
|
||||
</style>
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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<ConfirmState>({
|
||||
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<boolean> {
|
||||
return new Promise((resolve) => {
|
||||
state = {
|
||||
open: true,
|
||||
message,
|
||||
title: options.title ?? '',
|
||||
confirmText: options.confirmText ?? 'OK',
|
||||
cancelText: options.cancelText ?? 'Cancel',
|
||||
destructive: options.destructive ?? true,
|
||||
resolve,
|
||||
};
|
||||
});
|
||||
}
|
||||
@@ -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 @@
|
||||
</script>
|
||||
|
||||
{@render children()}
|
||||
<Toast />
|
||||
<Toast />
|
||||
<ConfirmDialog />
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<script>
|
||||
import ToTryCard from '$lib/components/recipes/ToTryCard.svelte';
|
||||
import { confirm } from '$lib/js/confirmDialog.svelte';
|
||||
|
||||
let { data } = $props();
|
||||
|
||||
@@ -102,7 +103,7 @@
|
||||
/** @param {any} id */
|
||||
async function handleDelete(id) {
|
||||
const msg = isEnglish ? 'Delete this recipe?' : 'Dieses Rezept löschen?';
|
||||
if (!confirm(msg)) return;
|
||||
if (!await confirm(msg)) return;
|
||||
|
||||
const res = await fetch(`/api/${data.recipeLang}/to-try`, {
|
||||
method: 'DELETE',
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import { page } from '$app/stores';
|
||||
import { Clock, Weight, Trophy, Trash2, Pencil, Plus, Upload, Route, X, RefreshCw, Gauge, Flame, Info } from '@lucide/svelte';
|
||||
import { detectFitnessLang, fitnessSlugs, t } from '$lib/js/fitnessI18n';
|
||||
import { confirm } from '$lib/js/confirmDialog.svelte';
|
||||
import { toast } from '$lib/js/toast.svelte';
|
||||
|
||||
const lang = $derived(detectFitnessLang($page.url.pathname));
|
||||
@@ -216,7 +217,7 @@
|
||||
}
|
||||
|
||||
async function deleteSession() {
|
||||
if (!confirm(t('delete_session_confirm', lang))) return;
|
||||
if (!await confirm(t('delete_session_confirm', lang))) return;
|
||||
deleting = true;
|
||||
try {
|
||||
const res = await fetch(`/api/fitness/sessions/${session._id}`, { method: 'DELETE' });
|
||||
@@ -446,7 +447,7 @@
|
||||
|
||||
/** @param {number} exIdx */
|
||||
async function removeGpx(exIdx) {
|
||||
if (!confirm(t('remove_gps_confirm', lang))) return;
|
||||
if (!await confirm(t('remove_gps_confirm', lang))) return;
|
||||
try {
|
||||
const res = await fetch(`/api/fitness/sessions/${session._id}/gpx`, {
|
||||
method: 'DELETE',
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import { Pencil, Trash2, ChevronRight, Venus, Mars } from '@lucide/svelte';
|
||||
import { detectFitnessLang, t } from '$lib/js/fitnessI18n';
|
||||
import { toast } from '$lib/js/toast.svelte';
|
||||
import { confirm } from '$lib/js/confirmDialog.svelte';
|
||||
|
||||
const lang = $derived(detectFitnessLang($page.url.pathname));
|
||||
const measureSlug = $derived(lang === 'en' ? 'measure' : 'messen');
|
||||
@@ -93,7 +94,7 @@
|
||||
|
||||
/** @param {string} id */
|
||||
async function deleteMeasurement(id) {
|
||||
if (!confirm(t('delete_measurement_confirm', lang))) return;
|
||||
if (!await confirm(t('delete_measurement_confirm', lang))) return;
|
||||
try {
|
||||
const res = await fetch(`/api/fitness/measurements/${id}`, { method: 'DELETE' });
|
||||
if (res.ok) {
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import { goto } from '$app/navigation';
|
||||
import { detectFitnessLang, t } from '$lib/js/fitnessI18n';
|
||||
import { toast } from '$lib/js/toast.svelte';
|
||||
import { confirm } from '$lib/js/confirmDialog.svelte';
|
||||
import { Trash2 } from '@lucide/svelte';
|
||||
import SaveFab from '$lib/components/SaveFab.svelte';
|
||||
|
||||
@@ -85,7 +86,7 @@
|
||||
}
|
||||
|
||||
async function deleteMeasurement() {
|
||||
if (!confirm(t('delete_measurement_confirm', lang))) return;
|
||||
if (!await confirm(t('delete_measurement_confirm', lang))) return;
|
||||
deleting = true;
|
||||
try {
|
||||
const res = await fetch(`/api/fitness/measurements/${m._id}`, { method: 'DELETE' });
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
import AddButton from '$lib/components/AddButton.svelte';
|
||||
import FoodSearch from '$lib/components/fitness/FoodSearch.svelte';
|
||||
import { toast } from '$lib/js/toast.svelte';
|
||||
import { confirm } from '$lib/js/confirmDialog.svelte';
|
||||
import { getDRI, NUTRIENT_META } from '$lib/data/dailyReferenceIntake';
|
||||
|
||||
const lang = $derived(detectFitnessLang($page.url.pathname));
|
||||
@@ -725,7 +726,7 @@
|
||||
}
|
||||
|
||||
async function deleteEntry(id) {
|
||||
if (!confirm(t('delete_entry_confirm', lang))) return;
|
||||
if (!await confirm(t('delete_entry_confirm', lang))) return;
|
||||
try {
|
||||
const res = await fetch(`/api/fitness/food-log/${id}`, { method: 'DELETE' });
|
||||
if (res.ok) {
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
import { ChevronLeft, Plus, Trash2, Pencil, UtensilsCrossed, X } from '@lucide/svelte';
|
||||
import { detectFitnessLang, fitnessSlugs, t } from '$lib/js/fitnessI18n';
|
||||
import { toast } from '$lib/js/toast.svelte';
|
||||
import { confirm } from '$lib/js/confirmDialog.svelte';
|
||||
import FoodSearch from '$lib/components/fitness/FoodSearch.svelte';
|
||||
|
||||
const lang = $derived(detectFitnessLang($page.url.pathname));
|
||||
@@ -127,7 +128,7 @@
|
||||
}
|
||||
|
||||
async function deleteMeal(meal) {
|
||||
if (!confirm(t('delete_meal_confirm', lang))) return;
|
||||
if (!await confirm(t('delete_meal_confirm', lang))) return;
|
||||
try {
|
||||
const res = await fetch(`/api/fitness/custom-meals/${meal._id}`, { method: 'DELETE' });
|
||||
if (res.ok) {
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import { page } from '$app/stores';
|
||||
import { Trash2, Play, Pause, Trophy, Clock, Dumbbell, Route, RefreshCw, Check, ChevronUp, ChevronDown, Flame, MapPin, Volume2, X, Timer, Plus, GripVertical } from '@lucide/svelte';
|
||||
import { detectFitnessLang, fitnessSlugs, t } from '$lib/js/fitnessI18n';
|
||||
import { confirm } from '$lib/js/confirmDialog.svelte';
|
||||
|
||||
const lang = $derived(detectFitnessLang($page.url.pathname));
|
||||
const sl = $derived(fitnessSlugs(lang));
|
||||
@@ -161,7 +162,7 @@
|
||||
}
|
||||
|
||||
async function deleteInterval(/** @type {string} */ id) {
|
||||
if (!confirm(t('delete_interval_confirm', lang))) return;
|
||||
if (!await confirm(t('delete_interval_confirm', lang))) return;
|
||||
await fetch(`/api/fitness/intervals/${id}`, { method: 'DELETE' });
|
||||
if (selectedIntervalId === id) selectedIntervalId = null;
|
||||
await fetchIntervalTemplates();
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<script>
|
||||
import { invalidateAll } from '$app/navigation';
|
||||
import { confirm } from '$lib/js/confirmDialog.svelte';
|
||||
import { formatDistanceToNow, isPast, isToday, differenceInDays, format } from 'date-fns';
|
||||
import { de } from 'date-fns/locale';
|
||||
import { Plus, Check, Pencil, Trash2, Tag, Users, RotateCcw, Calendar,
|
||||
@@ -124,7 +125,7 @@
|
||||
|
||||
/** @param {any} task */
|
||||
async function deleteTask(task) {
|
||||
if (!confirm(`"${task.title}" wirklich löschen?`)) return;
|
||||
if (!await confirm(`"${task.title}" wirklich löschen?`)) return;
|
||||
const res = await fetch(`/api/tasks/${task._id}`, { method: 'DELETE' });
|
||||
if (res.ok) await refreshTasks();
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<script>
|
||||
import { invalidateAll } from '$app/navigation';
|
||||
import { confirm } from '$lib/js/confirmDialog.svelte';
|
||||
import { STICKERS, getStickerById, getRarityColor } from '$lib/utils/stickers';
|
||||
import { formatDistanceToNow } from 'date-fns';
|
||||
import { de } from 'date-fns/locale';
|
||||
@@ -74,7 +75,7 @@
|
||||
}
|
||||
|
||||
async function clearHistory() {
|
||||
if (!confirm('Deinen gesamten Verlauf und alle Sticker wirklich löschen? Das kann nicht rückgängig gemacht werden.')) return;
|
||||
if (!await confirm('Deinen gesamten Verlauf und alle Sticker wirklich löschen? Das kann nicht rückgängig gemacht werden.')) return;
|
||||
const res = await fetch('/api/tasks/stats', { method: 'DELETE' });
|
||||
if (res.ok) await invalidateAll();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user