nexus-5-frontend-3/src/lib/components/admin/DeleteConfirmModal.svelte
2026-01-26 11:30:40 -05:00

143 lines
3.6 KiB
Svelte

<script lang="ts">
import { fade, scale } from 'svelte/transition';
import IconSpinner from '$lib/components/icons/IconSpinner.svelte';
interface Props {
open: boolean;
title?: string;
message?: string;
confirmLabel?: string;
cancelLabel?: string;
loading?: boolean;
onconfirm: () => Promise<void> | void;
oncancel: () => void;
}
let {
open = $bindable(false),
title = 'Confirm Delete',
message = 'Are you sure you want to delete this item? This action cannot be undone.',
confirmLabel = 'Delete',
cancelLabel = 'Cancel',
loading = false,
onconfirm,
oncancel
}: Props = $props();
let error = $state('');
let isSubmitting = $state(false);
async function handleConfirm() {
if (!onconfirm) return;
isSubmitting = true;
error = '';
try {
await onconfirm();
open = false;
} catch (err) {
error = err instanceof Error ? err.message : 'Failed to perform action';
} finally {
isSubmitting = false;
}
}
function handleCancel() {
if (isSubmitting) return;
error = '';
open = false;
oncancel?.();
}
function handleKeydown(event: KeyboardEvent) {
if (event.key === 'Escape' && open && !isSubmitting) {
handleCancel();
}
}
function handleBackdropClick(event: MouseEvent) {
if (event.target === event.currentTarget && !isSubmitting) {
handleCancel();
}
}
</script>
<svelte:window onkeydown={handleKeydown} />
{#if open}
<!-- Backdrop -->
<div
class="fixed inset-0 z-50 flex items-center justify-center bg-black/50 p-4"
transition:fade={{ duration: 150 }}
onclick={handleBackdropClick}
role="presentation"
>
<!-- Modal -->
<div
class="w-full max-w-sm rounded-xl border border-theme bg-theme p-6 shadow-theme-lg"
transition:scale={{ duration: 150, start: 0.95 }}
role="alertdialog"
aria-modal="true"
aria-labelledby="modal-title"
aria-describedby="modal-description"
>
{#if error}
<div class="mb-4 rounded-lg border border-danger bg-danger p-3 text-sm text-danger">
{error}
</div>
{/if}
<div class="text-center">
<!-- Warning Icon -->
<div class="mx-auto mb-4 flex h-12 w-12 items-center justify-center rounded-full bg-danger">
<svg
class="h-6 w-6 text-danger"
style="fill: none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"
/>
</svg>
</div>
<h3 id="modal-title" class="mb-2 text-lg font-semibold text-theme">
{title}
</h3>
<p id="modal-description" class="mb-6 text-sm text-theme-secondary">
{message}
</p>
<div class="flex gap-3">
<button
type="button"
onclick={handleCancel}
disabled={isSubmitting || loading}
class="flex-1 rounded-lg border border-theme bg-theme px-4 py-2 text-sm font-medium text-theme transition-colors hover:bg-black/5 disabled:cursor-not-allowed disabled:opacity-50 dark:hover:bg-white/10"
>
{cancelLabel}
</button>
<button
type="button"
onclick={handleConfirm}
disabled={isSubmitting || loading}
class="btn-danger flex-1 disabled:cursor-not-allowed disabled:opacity-50"
>
{#if isSubmitting || loading}
<span class="flex items-center justify-center gap-2">
<IconSpinner class="h-4 w-4" />
<span>Deleting...</span>
</span>
{:else}
{confirmLabel}
{/if}
</button>
</div>
</div>
</div>
</div>
{/if}