'use client'; import React, { useState, useEffect, useCallback } from 'react'; import Link from 'next/link'; import { useAuth } from '@/contexts/AuthContext'; import { useRouter } from 'next/navigation'; interface BannedTerm { id: string; term: string; ban_level: 'soft' | 'hard'; site_scope: 'avbeat' | 'avbeat' | 'both'; is_active: boolean; notes: string | null; created_at: string; added_by_profile?: { full_name: string } | null; } interface FormState { term: string; ban_level: 'soft' | 'hard'; site_scope: 'avbeat' | 'avbeat' | 'both'; notes: string; } const EMPTY_FORM: FormState = { term: '', ban_level: 'soft', site_scope: 'both', notes: '' }; export default function BannedTermsPage() { const { user, loading } = useAuth(); const router = useRouter(); const [terms, setTerms] = useState([]); const [loadingData, setLoadingData] = useState(true); const [showAddForm, setShowAddForm] = useState(false); const [editingId, setEditingId] = useState(null); const [form, setForm] = useState(EMPTY_FORM); const [saving, setSaving] = useState(false); const [actionLoading, setActionLoading] = useState(null); const [toast, setToast] = useState<{ message: string; type: 'success' | 'error' } | null>(null); // Filters const [searchTerm, setSearchTerm] = useState(''); const [filterBanLevel, setFilterBanLevel] = useState(''); const [filterSiteScope, setFilterSiteScope] = useState(''); const [filterActive, setFilterActive] = useState(''); // Retroactive scan const [scanning, setScanning] = useState(false); const [scanResults, setScanResults] = useState(null); const showToast = (message: string, type: 'success' | 'error' = 'success') => { setToast({ message, type }); setTimeout(() => setToast(null), 4000); }; const fetchTerms = useCallback(async () => { setLoadingData(true); try { const params = new URLSearchParams(); if (searchTerm) params.set('search', searchTerm); if (filterBanLevel) params.set('ban_level', filterBanLevel); if (filterSiteScope) params.set('site_scope', filterSiteScope); if (filterActive !== '') params.set('active', filterActive); const res = await fetch(`/api/admin/banned-terms?${params}`); const data = await res.json(); setTerms(data.terms || []); } catch { showToast('Failed to load banned terms', 'error'); } finally { setLoadingData(false); } }, [searchTerm, filterBanLevel, filterSiteScope, filterActive]); useEffect(() => { if (!loading && !user) router.push('/login?next=' + encodeURIComponent(window.location.pathname)); }, [user, loading, router]); useEffect(() => { if (user) fetchTerms(); }, [user, fetchTerms]); const handleSave = async (e: React.FormEvent) => { e.preventDefault(); if (!form.term.trim()) { showToast('Term is required', 'error'); return; } setSaving(true); try { if (editingId) { const res = await fetch('/api/admin/banned-terms', { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ id: editingId, ...form }), }); if (!res.ok) throw new Error((await res.json()).error); showToast('Term updated'); } else { const res = await fetch('/api/admin/banned-terms', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(form), }); if (!res.ok) throw new Error((await res.json()).error); showToast('Term added'); } setForm(EMPTY_FORM); setShowAddForm(false); setEditingId(null); fetchTerms(); } catch (err: any) { showToast(err.message || 'Failed to save', 'error'); } finally { setSaving(false); } }; const handleDeactivate = async (id: string, currentActive: boolean) => { setActionLoading(id); try { const res = await fetch('/api/admin/banned-terms', { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ id, is_active: !currentActive }), }); if (!res.ok) throw new Error((await res.json()).error); showToast(currentActive ? 'Term deactivated' : 'Term reactivated'); fetchTerms(); } catch (err: any) { showToast(err.message || 'Failed', 'error'); } finally { setActionLoading(null); } }; const handleDelete = async (id: string) => { if (!confirm('Delete this banned term permanently?')) return; setActionLoading(id); try { const res = await fetch(`/api/admin/banned-terms?id=${id}`, { method: 'DELETE' }); if (!res.ok) throw new Error((await res.json()).error); showToast('Term deleted'); fetchTerms(); } catch (err: any) { showToast(err.message || 'Failed', 'error'); } finally { setActionLoading(null); } }; const handleEdit = (term: BannedTerm) => { setForm({ term: term.term, ban_level: term.ban_level, site_scope: term.site_scope, notes: term.notes || '' }); setEditingId(term.id); setShowAddForm(true); }; const handleRetroactiveScan = async () => { setScanning(true); setScanResults(null); try { const res = await fetch('/api/admin/banned-terms-scan', { method: 'POST' }); const data = await res.json(); setScanResults(data.matches || []); showToast(`Scan complete: ${data.matches?.length || 0} matches found`); } catch { showToast('Scan failed', 'error'); } finally { setScanning(false); } }; const exportCSV = () => { const headers = ['Term', 'Ban Level', 'Site Scope', 'Active', 'Date Added', 'Notes']; const rows = terms.map(t => [t.term, t.ban_level, t.site_scope, t.is_active ? 'Yes' : 'No', new Date(t.created_at).toLocaleDateString(), t.notes || '']); const csv = [headers, ...rows].map(r => r.map(v => `"${String(v).replace(/"/g, '""')}"`).join(',')).join('\n'); const blob = new Blob([csv], { type: 'text/csv' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = 'banned-terms.csv'; a.click(); URL.revokeObjectURL(url); }; if (loading || loadingData) { return (
); } return (
{toast && (
{toast.message}
)}
{/* Header */}
Admin Banned Terms

Banned Terms

Manage restricted company names and keywords — visible to administrators only

{/* Retroactive scan results */} {scanResults !== null && (

Retroactive Scan Results — {scanResults.length} match{scanResults.length !== 1 ? 'es' : ''} found

{scanResults.length === 0 ? (

No published posts contain currently active banned terms.

) : (
{scanResults.map((r: any, i: number) => (
{r.matched_term} {r.title} {r.author} {new Date(r.published_at).toLocaleDateString()} {r.site_id === 2 ? 'AV' : 'BB'}
))}
)}
)} {/* Add/Edit Form */} {showAddForm && (

{editingId ? 'Edit Term' : 'Add Banned Term'}

setForm(f => ({ ...f, term: e.target.value }))} placeholder="e.g. Sony, Acme Corp" className="w-full bg-[#0a0a0a] border border-[#333] rounded-lg px-3 py-2.5 text-white text-sm font-body focus:outline-none focus:border-[#1D4ED8]" required />