40227-vm/frontend/src/components/common/ConfirmationDialog.tsx
2026-06-17 21:45:57 +02:00

116 lines
4.2 KiB
TypeScript

import { AlertTriangle, Info } from 'lucide-react';
import type { ReactNode } from 'react';
import { Button } from '@/components/ui/button';
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog';
import { cn } from '@/lib/utils';
type ConfirmationDialogTone = 'danger' | 'warning' | 'info';
interface ConfirmationDialogProps {
readonly open: boolean;
readonly title: string;
readonly description: ReactNode;
readonly confirmLabel: string;
readonly cancelLabel?: string;
readonly loading?: boolean;
readonly loadingLabel?: string;
readonly disabled?: boolean;
readonly tone?: ConfirmationDialogTone;
readonly icon?: ReactNode;
readonly onCancel: () => void;
readonly onConfirm: () => void;
}
const toneClasses: Record<ConfirmationDialogTone, {
readonly panelAccent: string;
readonly iconWrap: string;
readonly confirmButton: string;
}> = {
danger: {
panelAccent: 'from-red-500/12 via-rose-500/8 to-transparent border-red-500/20',
iconWrap: 'border-red-500/30 bg-gradient-to-br from-red-500/25 to-rose-600/15 text-red-200 shadow-red-500/20',
confirmButton: 'bg-gradient-to-r from-red-500 to-rose-600 text-white hover:from-red-400 hover:to-rose-500 shadow-lg shadow-red-500/20',
},
warning: {
panelAccent: 'from-amber-500/12 via-orange-500/8 to-transparent border-amber-500/20',
iconWrap: 'border-amber-500/30 bg-gradient-to-br from-amber-400/25 to-orange-500/15 text-amber-200 shadow-amber-500/20',
confirmButton: 'bg-gradient-to-r from-amber-400 to-orange-500 text-slate-950 hover:from-amber-300 hover:to-orange-400 shadow-lg shadow-amber-500/20',
},
info: {
panelAccent: 'from-blue-500/12 via-cyan-500/8 to-transparent border-blue-500/20',
iconWrap: 'border-blue-500/30 bg-gradient-to-br from-blue-500/25 to-cyan-500/15 text-blue-200 shadow-blue-500/20',
confirmButton: 'bg-gradient-to-r from-blue-500 to-cyan-500 text-white hover:from-blue-400 hover:to-cyan-400 shadow-lg shadow-blue-500/20',
},
};
export function ConfirmationDialog({
open,
title,
description,
confirmLabel,
cancelLabel = 'Cancel',
loading = false,
loadingLabel,
disabled = false,
tone = 'danger',
icon,
onCancel,
onConfirm,
}: ConfirmationDialogProps) {
const fallbackIcon = tone === 'info' ? <Info size={22} /> : <AlertTriangle size={22} />;
const classes = toneClasses[tone];
return (
<Dialog open={open} onOpenChange={(nextOpen) => {
if (!nextOpen && !loading) {
onCancel();
}
}}
>
<DialogContent className="overflow-hidden rounded-2xl border-slate-700/60 bg-slate-900/95 p-0 text-white shadow-2xl shadow-black/40 backdrop-blur-xl sm:max-w-lg">
<DialogHeader className={cn('gap-0 border-b bg-gradient-to-br p-6 text-left', classes.panelAccent)}>
<div className="flex items-start gap-4">
<div className={cn('flex h-12 w-12 shrink-0 items-center justify-center rounded-xl border shadow-lg', classes.iconWrap)}>
{icon ?? fallbackIcon}
</div>
<div className="space-y-2 pt-0.5">
<DialogTitle className="text-xl font-semibold leading-tight text-white">{title}</DialogTitle>
<DialogDescription className="text-sm leading-6 text-slate-300">
{description}
</DialogDescription>
</div>
</div>
</DialogHeader>
<DialogFooter className="gap-3 border-t border-slate-800/80 bg-slate-950/35 p-5 sm:space-x-0">
<Button
type="button"
onClick={onCancel}
disabled={loading}
className="h-11 rounded-xl border border-slate-700/70 bg-slate-800/70 px-5 text-slate-200 hover:bg-slate-700/80"
>
{cancelLabel}
</Button>
<Button
type="button"
onClick={onConfirm}
disabled={disabled || loading}
loading={loading}
loadingLabel={loadingLabel ?? confirmLabel}
className={cn(classes.confirmButton, 'h-11 rounded-xl px-5 disabled:cursor-not-allowed disabled:opacity-60')}
>
{confirmLabel}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}