diff --git a/frontend/src/components/ConfirmModal.tsx b/frontend/src/components/ConfirmModal.tsx new file mode 100644 index 0000000..5afa55f --- /dev/null +++ b/frontend/src/components/ConfirmModal.tsx @@ -0,0 +1,118 @@ +import { useEffect, useRef } from 'react' +import { AlertTriangle } from 'lucide-react' + +interface ConfirmModalProps { + open: boolean + title: string + message: string + confirmLabel?: string + confirmVariant?: 'danger' | 'primary' + onConfirm: () => void + onCancel: () => void +} + +export default function ConfirmModal({ + open, + title, + message, + confirmLabel = 'Delete', + confirmVariant = 'danger', + onConfirm, + onCancel, +}: ConfirmModalProps) { + const cancelRef = useRef(null) + const dialogRef = useRef(null) + + useEffect(() => { + if (!open) return + // Focus cancel button when modal opens + cancelRef.current?.focus() + + const handleKeyDown = (e: KeyboardEvent) => { + if (e.key === 'Escape') { + onCancel() + return + } + // Trap focus inside the modal + if (e.key === 'Tab' && dialogRef.current) { + const focusable = dialogRef.current.querySelectorAll( + 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])' + ) + const first = focusable[0] + const last = focusable[focusable.length - 1] + if (e.shiftKey) { + if (document.activeElement === first) { + e.preventDefault() + last?.focus() + } + } else { + if (document.activeElement === last) { + e.preventDefault() + first?.focus() + } + } + } + } + + document.addEventListener('keydown', handleKeyDown) + return () => document.removeEventListener('keydown', handleKeyDown) + }, [open, onCancel]) + + if (!open) return null + + return ( +
+
+
+
+
+ +
+
+

+ {title} +

+

+ {message} +

+
+
+ +
+ + +
+
+
+
+ ) +} diff --git a/frontend/src/pages/Admin.tsx b/frontend/src/pages/Admin.tsx index f5e1231..0d28f26 100644 --- a/frontend/src/pages/Admin.tsx +++ b/frontend/src/pages/Admin.tsx @@ -4,6 +4,7 @@ import { toast } from 'sonner' import { UserPlus, Trash2, Pencil, ChevronDown, ChevronUp, ChevronRight, Settings, RefreshCw, CheckCircle2, XCircle, Clock, DollarSign, Layers, AlertTriangle, Upload, FileBox, Plus, X, LayoutDashboard } from 'lucide-react' import { Link } from 'react-router-dom' import api from '../api/client' +import ConfirmModal from '../components/ConfirmModal' import TemplateEditor from '../components/admin/TemplateEditor' import PricingTierTable from '../components/admin/PricingTierTable' import OutputTypeTable from '../components/admin/OutputTypeTable' @@ -191,6 +192,7 @@ export default function AdminPage() { const [smtpDraft, setSmtpDraft] = useState>({}) const smtp = { ...settings, ...smtpDraft } as Settings + const [confirmState, setConfirmState] = useState<{ open: boolean; title: string; message: string; onConfirm: () => void }>({ open: false, title: '', message: '', onConfirm: () => {} }) const [showTenantDashboardModal, setShowTenantDashboardModal] = useState(false) const { data: tenantDefaultWidgets } = useQuery({ queryKey: ['tenant-default-dashboard'], @@ -276,7 +278,17 @@ export default function AdminPage() { {user.is_active ? 'active' : 'inactive'} @@ -1484,6 +1506,14 @@ function AssetLibraryPanel() { })} )} + + setConfirmState((s) => ({ ...s, open: false }))} + /> ) } diff --git a/frontend/src/pages/Billing.tsx b/frontend/src/pages/Billing.tsx index 5ff7b85..b2a8320 100644 --- a/frontend/src/pages/Billing.tsx +++ b/frontend/src/pages/Billing.tsx @@ -6,6 +6,7 @@ import { getInvoices, createInvoice, updateInvoiceStatus, deleteInvoice, downloadInvoicePdf, type Invoice, type InvoiceCreate, } from '../api/billing' +import ConfirmModal from '../components/ConfirmModal' // ── Helpers ─────────────────────────────────────────────────────────────── @@ -37,36 +38,46 @@ function NewInvoiceModal({ onClose, onCreate }: { onClose: () => void; onCreate: return (
-
-
-

New Invoice

- +
+
+

New Invoice

+
- + setIssuedAt(e.target.value)} - className="w-full px-3 py-2 text-sm border border-gray-200 rounded-lg focus:outline-none focus:ring-1 focus:ring-blue-500" + className="w-full px-3 py-2 text-sm border border-border-default rounded-lg focus:outline-none focus:ring-1 focus:ring-accent" />
- +