diff --git a/frontend/src/App.css b/frontend/src/App.css index 38c9bcc..b76c0eb 100644 --- a/frontend/src/App.css +++ b/frontend/src/App.css @@ -544,6 +544,16 @@ body { to { transform: rotate(360deg); } } +@keyframes confirmFadeIn { + from { opacity: 0; } + to { opacity: 1; } +} + +@keyframes confirmSlideUp { + from { opacity: 0; transform: translateY(12px) scale(0.97); } + to { opacity: 1; transform: translateY(0) scale(1); } +} + /* Tooltip with enhanced styling */ .tooltip { position: relative; diff --git a/frontend/src/App.js b/frontend/src/App.js index 8f9d068..a542057 100644 --- a/frontend/src/App.js +++ b/frontend/src/App.js @@ -8,6 +8,7 @@ import AuditLog from './components/AuditLog'; import NvdSyncModal from './components/NvdSyncModal'; import NavDrawer from './components/NavDrawer'; import CalendarWidget from './components/CalendarWidget'; +import ConfirmModal from './components/ConfirmModal'; import VulnerabilityTriagePage from './components/pages/ReportingPage'; import KnowledgeBasePage from './components/pages/KnowledgeBasePage'; import ExportsPage from './components/pages/ExportsPage'; @@ -241,6 +242,9 @@ export default function App() { const [archiveList, setArchiveList] = useState([]); const [archiveListLoading, setArchiveListLoading] = useState(false); + // Confirmation modal state — replaces window.confirm() + const [pendingConfirm, setPendingConfirm] = useState(null); + const toggleCVEExpand = (cveId) => { setExpandedCVEs(prev => ({ ...prev, [cveId]: !prev[cveId] })); }; @@ -532,26 +536,30 @@ export default function App() { }; const handleDeleteDocument = async (docId, cveId, vendor) => { - if (!window.confirm('Are you sure you want to delete this document?')) { - return; - } + setPendingConfirm({ + title: 'Delete Document', + message: 'Are you sure you want to delete this document?', + confirmText: 'Delete', + onConfirm: async () => { + setPendingConfirm(null); + try { + const response = await fetch(`${API_BASE}/documents/${docId}`, { + method: 'DELETE', + credentials: 'include' + }); - try { - const response = await fetch(`${API_BASE}/documents/${docId}`, { - method: 'DELETE', - credentials: 'include' - }); + if (!response.ok) throw new Error('Failed to delete document'); - if (!response.ok) throw new Error('Failed to delete document'); - - alert('Document deleted successfully!'); - const key = `${cveId}-${vendor}`; - delete cveDocuments[key]; - await fetchDocuments(cveId, vendor); - fetchCVEs(); - } catch (err) { - alert(`Error: ${err.message}`); - } + alert('Document deleted successfully!'); + const key = `${cveId}-${vendor}`; + delete cveDocuments[key]; + await fetchDocuments(cveId, vendor); + fetchCVEs(); + } catch (err) { + alert(`Error: ${err.message}`); + } + }, + }); }; const handleEditCVE = (cve) => { @@ -644,65 +652,73 @@ export default function App() { }; const handleDeleteCVEEntry = async (cve) => { - if (!window.confirm(`Are you sure you want to delete the "${cve.vendor}" entry for ${cve.cve_id}? This will also delete all associated documents.`)) { - return; - } + setPendingConfirm({ + title: 'Delete Vendor Entry', + message: `Are you sure you want to delete the "${cve.vendor}" entry for ${cve.cve_id}? This will also delete all associated documents.`, + confirmText: 'Delete', + onConfirm: async () => { + setPendingConfirm(null); + try { + const url = `${API_BASE}/cves/${cve.id}`; + console.log('DELETE request to:', url); + const response = await fetch(url, { + method: 'DELETE', + credentials: 'include' + }); - try { - const url = `${API_BASE}/cves/${cve.id}`; - console.log('DELETE request to:', url); - const response = await fetch(url, { - method: 'DELETE', - credentials: 'include' - }); + if (!response.ok) { + const contentType = response.headers.get('content-type'); + if (contentType && contentType.includes('application/json')) { + const data = await response.json(); + throw new Error(data.error || 'Failed to delete CVE entry'); + } else { + throw new Error(`Server returned ${response.status} ${response.statusText}. Check API_BASE configuration.`); + } + } - if (!response.ok) { - const contentType = response.headers.get('content-type'); - if (contentType && contentType.includes('application/json')) { - const data = await response.json(); - throw new Error(data.error || 'Failed to delete CVE entry'); - } else { - throw new Error(`Server returned ${response.status} ${response.statusText}. Check API_BASE configuration.`); + alert(`Deleted ${cve.vendor} entry for ${cve.cve_id}`); + fetchCVEs(); + fetchVendors(); + } catch (err) { + alert(`Error: ${err.message}`); } - } - - alert(`Deleted ${cve.vendor} entry for ${cve.cve_id}`); - fetchCVEs(); - fetchVendors(); - } catch (err) { - alert(`Error: ${err.message}`); - } + }, + }); }; const handleDeleteEntireCVE = async (cveId, vendorCount) => { - if (!window.confirm(`Are you sure you want to delete ALL ${vendorCount} vendor entries for ${cveId}? This will permanently remove all associated documents and files.`)) { - return; - } + setPendingConfirm({ + title: 'Delete Entire CVE', + message: `Are you sure you want to delete ALL ${vendorCount} vendor entries for ${cveId}? This will permanently remove all associated documents and files.`, + confirmText: 'Delete All', + onConfirm: async () => { + setPendingConfirm(null); + try { + const url = `${API_BASE}/cves/by-cve-id/${encodeURIComponent(cveId)}`; + console.log('DELETE request to:', url); + const response = await fetch(url, { + method: 'DELETE', + credentials: 'include' + }); - try { - const url = `${API_BASE}/cves/by-cve-id/${encodeURIComponent(cveId)}`; - console.log('DELETE request to:', url); - const response = await fetch(url, { - method: 'DELETE', - credentials: 'include' - }); + if (!response.ok) { + const contentType = response.headers.get('content-type'); + if (contentType && contentType.includes('application/json')) { + const data = await response.json(); + throw new Error(data.error || 'Failed to delete CVE'); + } else { + throw new Error(`Server returned ${response.status} ${response.statusText}. Check API_BASE configuration.`); + } + } - if (!response.ok) { - const contentType = response.headers.get('content-type'); - if (contentType && contentType.includes('application/json')) { - const data = await response.json(); - throw new Error(data.error || 'Failed to delete CVE'); - } else { - throw new Error(`Server returned ${response.status} ${response.statusText}. Check API_BASE configuration.`); + alert(`Deleted all entries for ${cveId}`); + fetchCVEs(); + fetchVendors(); + } catch (err) { + alert(`Error: ${err.message}`); } - } - - alert(`Deleted all entries for ${cveId}`); - fetchCVEs(); - fetchVendors(); - } catch (err) { - alert(`Error: ${err.message}`); - } + }, + }); }; const handleAddTicket = async (e) => { @@ -770,18 +786,25 @@ export default function App() { }; const handleDeleteTicket = async (ticket) => { - if (!window.confirm(`Delete ticket ${ticket.ticket_key}?`)) return; - try { - const response = await fetch(`${API_BASE}/jira-tickets/${ticket.id}`, { - method: 'DELETE', - credentials: 'include' - }); - if (!response.ok) throw new Error('Failed to delete ticket'); - alert('Ticket deleted'); - fetchJiraTickets(); - } catch (err) { - alert(`Error: ${err.message}`); - } + setPendingConfirm({ + title: 'Delete Ticket', + message: `Delete ticket ${ticket.ticket_key}?`, + confirmText: 'Delete', + onConfirm: async () => { + setPendingConfirm(null); + try { + const response = await fetch(`${API_BASE}/jira-tickets/${ticket.id}`, { + method: 'DELETE', + credentials: 'include' + }); + if (!response.ok) throw new Error('Failed to delete ticket'); + alert('Ticket deleted'); + fetchJiraTickets(); + } catch (err) { + alert(`Error: ${err.message}`); + } + }, + }); }; const openAddTicketForCVE = (cve_id, vendor) => { @@ -855,18 +878,25 @@ export default function App() { }; const handleDeleteArcherTicket = async (ticket) => { - if (!window.confirm(`Delete Archer ticket ${ticket.exc_number}?`)) return; - try { - const response = await fetch(`${API_BASE}/archer-tickets/${ticket.id}`, { - method: 'DELETE', - credentials: 'include' - }); - if (!response.ok) throw new Error('Failed to delete Archer ticket'); - alert('Archer ticket deleted'); - fetchArcherTickets(); - } catch (err) { - alert(`Error: ${err.message}`); - } + setPendingConfirm({ + title: 'Delete Archer Ticket', + message: `Delete Archer ticket ${ticket.exc_number}?`, + confirmText: 'Delete', + onConfirm: async () => { + setPendingConfirm(null); + try { + const response = await fetch(`${API_BASE}/archer-tickets/${ticket.id}`, { + method: 'DELETE', + credentials: 'include' + }); + if (!response.ok) throw new Error('Failed to delete Archer ticket'); + alert('Archer ticket deleted'); + fetchArcherTickets(); + } catch (err) { + alert(`Error: ${err.message}`); + } + }, + }); }; const openAddArcherTicketForCVE = (cve_id, vendor) => { @@ -2420,6 +2450,17 @@ export default function App() { } {/* End Three Column Layout */} + + {/* Confirmation Modal */} + setPendingConfirm(null)} + /> ); diff --git a/frontend/src/components/ConfirmModal.js b/frontend/src/components/ConfirmModal.js new file mode 100644 index 0000000..bef188f --- /dev/null +++ b/frontend/src/components/ConfirmModal.js @@ -0,0 +1,171 @@ +import React, { useEffect, useRef } from 'react'; +import { AlertTriangle } from 'lucide-react'; + +/** + * ConfirmModal — themed replacement for window.confirm(). + * + * Props: + * open {boolean} Whether the modal is visible + * title {string} Heading text (e.g. "Delete Document") + * message {string|ReactNode} Body text / description + * confirmText {string} Label for the confirm button (default "Confirm") + * cancelText {string} Label for the cancel button (default "Cancel") + * variant {"danger"|"warning"|"default"} Controls accent color (default "danger") + * onConfirm {function} Called when user confirms + * onCancel {function} Called when user cancels or presses Escape + */ +export default function ConfirmModal({ + open, + title = 'Confirm', + message = 'Are you sure?', + confirmText = 'Confirm', + cancelText = 'Cancel', + variant = 'danger', + onConfirm, + onCancel, +}) { + const confirmRef = useRef(null); + + // Focus the confirm button when the modal opens and handle Escape key + useEffect(() => { + if (!open) return; + + // Small delay so the DOM is painted before we focus + const timer = setTimeout(() => confirmRef.current?.focus(), 50); + + const handleKey = (e) => { + if (e.key === 'Escape') onCancel?.(); + }; + document.addEventListener('keydown', handleKey); + return () => { + clearTimeout(timer); + document.removeEventListener('keydown', handleKey); + }; + }, [open, onCancel]); + + if (!open) return null; + + const accentMap = { + danger: { color: '#EF4444', bg: 'rgba(239,68,68,0.10)', bgHover: 'rgba(239,68,68,0.18)', border: 'rgba(239,68,68,0.3)' }, + warning: { color: '#F59E0B', bg: 'rgba(245,158,11,0.10)', bgHover: 'rgba(245,158,11,0.18)', border: 'rgba(245,158,11,0.3)' }, + default: { color: '#0EA5E9', bg: 'rgba(14,165,233,0.10)', bgHover: 'rgba(14,165,233,0.18)', border: 'rgba(14,165,233,0.3)' }, + }; + const accent = accentMap[variant] || accentMap.danger; + + return ( +
{ if (e.target === e.currentTarget) onCancel?.(); }} + > +
+ {/* Header */} +
+
+ +
+
+ {title} +
+
+ + {/* Body */} +
+ {message} +
+ + {/* Actions */} +
+ + +
+
+
+ ); +} diff --git a/frontend/src/components/KnowledgeBaseModal.js b/frontend/src/components/KnowledgeBaseModal.js index 7e6ee05..fa89f25 100644 --- a/frontend/src/components/KnowledgeBaseModal.js +++ b/frontend/src/components/KnowledgeBaseModal.js @@ -1,5 +1,6 @@ import React, { useState, useEffect } from 'react'; import { X, Loader, AlertCircle, CheckCircle, Upload as UploadIcon, Download, FileText, Trash2 } from 'lucide-react'; +import ConfirmModal from './ConfirmModal'; const API_BASE = process.env.REACT_APP_API_BASE || 'http://localhost:3001/api'; @@ -12,7 +13,7 @@ export default function KnowledgeBaseModal({ onClose, onUpdate }) { const [result, setResult] = useState(null); const [existingArticles, setExistingArticles] = useState([]); const [error, setError] = useState(''); - + const [pendingConfirm, setPendingConfirm] = useState(null); // Fetch existing articles on mount useEffect(() => { fetchExistingArticles(); @@ -117,27 +118,31 @@ export default function KnowledgeBaseModal({ onClose, onUpdate }) { }; const handleDelete = async (id, articleTitle) => { - if (!window.confirm(`Are you sure you want to delete "${articleTitle}"?`)) { - return; - } + setPendingConfirm({ + title: 'Delete Article', + message: `Are you sure you want to delete "${articleTitle}"?`, + confirmText: 'Delete', + onConfirm: async () => { + setPendingConfirm(null); + try { + const response = await fetch(`${API_BASE}/knowledge-base/${id}`, { + method: 'DELETE', + credentials: 'include' + }); - try { - const response = await fetch(`${API_BASE}/knowledge-base/${id}`, { - method: 'DELETE', - credentials: 'include' - }); + if (!response.ok) throw new Error('Delete failed'); - if (!response.ok) throw new Error('Delete failed'); + // Refresh the list + await fetchExistingArticles(); - // Refresh the list - await fetchExistingArticles(); - - // Notify parent to refresh - if (onUpdate) onUpdate(); - } catch (err) { - console.error('Error deleting article:', err); - setError('Failed to delete article'); - } + // Notify parent to refresh + if (onUpdate) onUpdate(); + } catch (err) { + console.error('Error deleting article:', err); + setError('Failed to delete article'); + } + }, + }); }; const resetForm = () => { @@ -379,6 +384,17 @@ export default function KnowledgeBaseModal({ onClose, onUpdate }) { )} + + {/* Confirmation Modal */} + setPendingConfirm(null)} + /> ); } diff --git a/frontend/src/components/UserManagement.js b/frontend/src/components/UserManagement.js index e34a9f8..12af745 100644 --- a/frontend/src/components/UserManagement.js +++ b/frontend/src/components/UserManagement.js @@ -1,6 +1,10 @@ +// ⚠️ CONVENTION: This component uses Tailwind utility classes (e.g. bg-white, rounded-lg, hover:bg-gray-50) +// instead of inline styles or App.css global classes. This is the legacy modal kept for UserMenu quick-access; +// the themed replacement lives in AdminPage.js. import React, { useState, useEffect } from 'react'; import { X, Plus, Edit2, Trash2, Loader, AlertCircle, CheckCircle, User, Mail, Shield } from 'lucide-react'; import { useAuth } from '../contexts/AuthContext'; +import ConfirmModal from './ConfirmModal'; const API_BASE = process.env.REACT_APP_API_BASE || 'http://localhost:3001/api'; @@ -35,6 +39,7 @@ export default function UserManagement({ onClose }) { }); const [formError, setFormError] = useState(''); const [formSuccess, setFormSuccess] = useState(''); + const [pendingConfirm, setPendingConfirm] = useState(null); useEffect(() => { fetchUsers(); @@ -55,29 +60,10 @@ export default function UserManagement({ onClose }) { } }; - const confirmGroupChange = (targetUser, newGroup) => { - let message = `Are you sure you want to change ${targetUser.username}'s group from ${targetUser.group} to ${newGroup}?`; - - // Extra warning when downgrading an Admin user - if (targetUser.group === 'Admin' && newGroup !== 'Admin') { - message += `\n\n⚠️ WARNING: You are removing Admin privileges from ${targetUser.username}. They will lose full system access.`; - } - - return window.confirm(message); - }; - - const handleSubmit = async (e) => { - e.preventDefault(); + const doSubmit = async () => { setFormError(''); setFormSuccess(''); - // If editing and group changed, show confirmation dialog - if (editingUser && formData.group !== editingUser.group) { - if (!confirmGroupChange(editingUser, formData.group)) { - return; - } - } - try { const url = editingUser ? `${API_BASE}/users/${editingUser.id}` @@ -117,6 +103,31 @@ export default function UserManagement({ onClose }) { } }; + const handleSubmit = (e) => { + e.preventDefault(); + + // If editing and group changed, show confirmation modal + if (editingUser && formData.group !== editingUser.group) { + let message = `Are you sure you want to change ${editingUser.username}'s group from ${editingUser.group} to ${formData.group}?`; + if (editingUser.group === 'Admin' && formData.group !== 'Admin') { + message += ` WARNING: You are removing Admin privileges from ${editingUser.username}. They will lose full system access.`; + } + setPendingConfirm({ + title: 'Change User Group', + message, + confirmText: 'Change Group', + variant: editingUser.group === 'Admin' ? 'danger' : 'warning', + onConfirm: () => { + setPendingConfirm(null); + doSubmit(); + }, + }); + return; + } + + doSubmit(); + }; + const handleEdit = (user) => { setEditingUser(user); setFormData({ @@ -131,26 +142,30 @@ export default function UserManagement({ onClose }) { }; const handleDelete = async (userId) => { - if (!window.confirm('Are you sure you want to delete this user?')) { - return; - } + setPendingConfirm({ + title: 'Delete User', + message: 'Are you sure you want to delete this user?', + confirmText: 'Delete', + onConfirm: async () => { + setPendingConfirm(null); + try { + const response = await fetch(`${API_BASE}/users/${userId}`, { + method: 'DELETE', + credentials: 'include' + }); - try { - const response = await fetch(`${API_BASE}/users/${userId}`, { - method: 'DELETE', - credentials: 'include' - }); + const data = await response.json(); - const data = await response.json(); + if (!response.ok) { + throw new Error(data.error || 'Delete failed'); + } - if (!response.ok) { - throw new Error(data.error || 'Delete failed'); - } - - fetchUsers(); - } catch (err) { - alert(err.message); - } + fetchUsers(); + } catch (err) { + alert(err.message); + } + }, + }); }; const handleToggleActive = async (user) => { @@ -418,6 +433,17 @@ export default function UserManagement({ onClose }) { )} + + {/* Confirmation Modal */} + setPendingConfirm(null)} + /> ); } diff --git a/frontend/src/components/pages/AdminPage.js b/frontend/src/components/pages/AdminPage.js index 6488309..15aabd6 100644 --- a/frontend/src/components/pages/AdminPage.js +++ b/frontend/src/components/pages/AdminPage.js @@ -1,8 +1,8 @@ import React, { useState, useEffect, useCallback } from 'react'; import { Shield, Clock, Activity, Plus, Edit2, Trash2, Loader, AlertCircle, CheckCircle, X, ChevronLeft, ChevronRight, Search, Users, FileText } from 'lucide-react'; import { useAuth } from '../../contexts/AuthContext'; +import ConfirmModal from '../ConfirmModal'; -// ⚠️ CONVENTION: Use relative API path, not absolute URL. Should be: process.env.REACT_APP_API_BASE || '' const API_BASE = process.env.REACT_APP_API_BASE || 'http://localhost:3001/api'; const TABS = [ @@ -115,6 +115,7 @@ function UserManagementPanel() { const [formData, setFormData] = useState({ username: '', email: '', password: '', group: 'Read_Only' }); const [formError, setFormError] = useState(''); const [successMessage, setSuccessMessage] = useState(''); + const [pendingConfirm, setPendingConfirm] = useState(null); const fetchUsers = useCallback(async () => { setLoading(true); @@ -200,19 +201,26 @@ function UserManagementPanel() { }; const handleDelete = async (userId) => { - if (!window.confirm('Are you sure you want to delete this user?')) return; - try { - const res = await fetch(`${API_BASE}/users/${userId}`, { - method: 'DELETE', - credentials: 'include', - }); - const data = await res.json(); - if (!res.ok) throw new Error(data.error || 'Delete failed'); - setSuccessMessage('User deleted'); - fetchUsers(); - } catch (err) { - alert(err.message); - } + setPendingConfirm({ + title: 'Delete User', + message: 'Are you sure you want to delete this user?', + confirmText: 'Delete', + onConfirm: async () => { + setPendingConfirm(null); + try { + const res = await fetch(`${API_BASE}/users/${userId}`, { + method: 'DELETE', + credentials: 'include', + }); + const data = await res.json(); + if (!res.ok) throw new Error(data.error || 'Delete failed'); + setSuccessMessage('User deleted'); + fetchUsers(); + } catch (err) { + alert(err.message); + } + }, + }); }; const handleToggleActive = async (user) => { @@ -567,6 +575,17 @@ function UserManagementPanel() { )} )} + + {/* Confirmation Modal */} + setPendingConfirm(null)} + /> ); } diff --git a/frontend/src/components/pages/ComplianceDetailPanel.js b/frontend/src/components/pages/ComplianceDetailPanel.js index e382621..1c97344 100644 --- a/frontend/src/components/pages/ComplianceDetailPanel.js +++ b/frontend/src/components/pages/ComplianceDetailPanel.js @@ -1,5 +1,6 @@ import React, { useState, useEffect, useCallback } from 'react'; import { X, MessageSquare, Send, Loader, AlertCircle, Clock, Shield, Trash2 } from 'lucide-react'; +import ConfirmModal from '../ConfirmModal'; const API_BASE = process.env.REACT_APP_API_BASE || 'http://localhost:3001/api'; @@ -45,6 +46,7 @@ export default function ComplianceDetailPanel({ hostname, onClose, onNoteAdded, const [selectedMetrics, setSelectedMetrics] = useState([]); const [submitting, setSubmitting] = useState(false); const [noteError, setNoteError] = useState(null); + const [pendingConfirm, setPendingConfirm] = useState(null); const fetchDetail = useCallback(async () => { setLoading(true); @@ -91,19 +93,26 @@ export default function ComplianceDetailPanel({ hostname, onClose, onNoteAdded, }; const handleDeleteNote = async (noteId, hasGroup) => { - if (!window.confirm('Delete this note?')) return; - try { - const url = hasGroup - ? `${API_BASE}/compliance/notes/${noteId}?group=true` - : `${API_BASE}/compliance/notes/${noteId}`; - const res = await fetch(url, { method: 'DELETE', credentials: 'include' }); - const data = await res.json(); - if (!res.ok) throw new Error(data.error || 'Failed to delete note'); - await fetchDetail(); - if (onNoteAdded) onNoteAdded(); - } catch (err) { - setNoteError(err.message); - } + setPendingConfirm({ + title: 'Delete Note', + message: 'Delete this note?', + confirmText: 'Delete', + onConfirm: async () => { + setPendingConfirm(null); + try { + const url = hasGroup + ? `${API_BASE}/compliance/notes/${noteId}?group=true` + : `${API_BASE}/compliance/notes/${noteId}`; + const res = await fetch(url, { method: 'DELETE', credentials: 'include' }); + const data = await res.json(); + if (!res.ok) throw new Error(data.error || 'Failed to delete note'); + await fetchDetail(); + if (onNoteAdded) onNoteAdded(); + } catch (err) { + setNoteError(err.message); + } + }, + }); }; const activeMetrics = detail?.metrics?.filter(m => m.status === 'active') || []; @@ -371,6 +380,17 @@ export default function ComplianceDetailPanel({ hostname, onClose, onNoteAdded, )} + + {/* Confirmation Modal */} + setPendingConfirm(null)} + /> ); } diff --git a/frontend/src/components/pages/KnowledgeBasePage.js b/frontend/src/components/pages/KnowledgeBasePage.js index 719e388..06004bf 100644 --- a/frontend/src/components/pages/KnowledgeBasePage.js +++ b/frontend/src/components/pages/KnowledgeBasePage.js @@ -6,11 +6,12 @@ import React, { useState, useEffect, useCallback, useMemo } from 'react'; import { BookOpen, Search, Upload, RefreshCw, Loader, - AlertCircle, FileText, File, Trash2, X, + AlertCircle, FileText, File, Trash2, X, // ⚠️ CONVENTION: FileText and File are imported but unused — remove if not needed } from 'lucide-react'; import { useAuth } from '../../contexts/AuthContext'; import KnowledgeBaseModal from '../KnowledgeBaseModal'; import KnowledgeBaseViewer from '../KnowledgeBaseViewer'; +import ConfirmModal from '../ConfirmModal'; // ⚠️ CONVENTION: ConfirmModal is imported but never used — either integrate it into handleDelete or remove this import const API_BASE = process.env.REACT_APP_API_BASE || 'http://localhost:3001/api'; const GREEN = '#10B981'; @@ -216,6 +217,7 @@ export default function KnowledgeBasePage() { const [activeCategory, setActiveCategory] = useState('All'); const [selected, setSelected] = useState(null); const [showUpload, setShowUpload] = useState(false); + const [pendingConfirm, setPendingConfirm] = useState(null); // ------------------------------------------------------------------------- // Fetch @@ -241,17 +243,24 @@ export default function KnowledgeBasePage() { // Delete // ------------------------------------------------------------------------- const handleDelete = useCallback(async (article) => { - if (!window.confirm(`Delete "${article.title}"? This cannot be undone.`)) return; - try { - const res = await fetch(`${API_BASE}/knowledge-base/${article.id}`, { - method: 'DELETE', credentials: 'include', - }); - if (!res.ok) throw new Error('Delete failed'); - setArticles(prev => prev.filter(a => a.id !== article.id)); - if (selected?.id === article.id) setSelected(null); - } catch (err) { - alert(`Failed to delete: ${err.message}`); - } + setPendingConfirm({ + title: 'Delete Article', + message: `Delete "${article.title}"? This cannot be undone.`, + confirmText: 'Delete', + onConfirm: async () => { + setPendingConfirm(null); + try { + const res = await fetch(`${API_BASE}/knowledge-base/${article.id}`, { + method: 'DELETE', credentials: 'include', + }); + if (!res.ok) throw new Error('Delete failed'); + setArticles(prev => prev.filter(a => a.id !== article.id)); + if (selected?.id === article.id) setSelected(null); + } catch (err) { + alert(`Failed to delete: ${err.message}`); + } + }, + }); }, [selected]); // ------------------------------------------------------------------------- @@ -479,6 +488,17 @@ export default function KnowledgeBasePage() { onUpdate={() => { fetchArticles(); setShowUpload(false); }} /> )} + + {/* Confirmation Modal */} + setPendingConfirm(null)} + /> ); }