AV: precise recolor to spec — warm dark backgrounds (#1c1815/#231d18/#26201a/#15110e) + gold #F0A623 / orange #D85A30 accents + warm text (#FBEFE0/#C9BBA8/#8a7e6e). 138 component files + 36 token swaps.

This commit is contained in:
2026-06-02 23:46:00 +00:00
parent 01c1abf99d
commit 2876ac6ecd
141 changed files with 2020 additions and 2020 deletions

View File

@@ -91,7 +91,7 @@ export default function ClientsPage() {
setForm({ company_name: '', contact_name: '', contact_email: '', contact_phone: '', billing_address: '', notes: '' });
}
if (loading) return <div className="min-h-screen bg-[#0a0a0a] flex items-center justify-center"><div className="w-8 h-8 border-2 border-[#ffb800] border-t-transparent rounded-full animate-spin" /></div>;
if (loading) return <div className="min-h-screen bg-[#0a0a0a] flex items-center justify-center"><div className="w-8 h-8 border-2 border-[#F0A623] border-t-transparent rounded-full animate-spin" /></div>;
if (!user) return null;
return (
@@ -102,7 +102,7 @@ export default function ClientsPage() {
<h1 className="text-xl font-bold">Clients</h1>
<Link href="/admin/accounting" className="text-xs text-[#555] hover:text-[#888]"> Accounting</Link>
</div>
<button onClick={() => setShowForm(true)} className="px-3 py-1.5 bg-[#ffb800] rounded text-xs font-medium hover:bg-blue-500 transition-colors">+ New Client</button>
<button onClick={() => setShowForm(true)} className="px-3 py-1.5 bg-[#F0A623] rounded text-xs font-medium hover:bg-blue-500 transition-colors">+ New Client</button>
</div>
{/* Success Toast */}
@@ -116,14 +116,14 @@ export default function ClientsPage() {
{/* Filters */}
<div className="flex gap-2 flex-wrap">
{['active','inactive','all'].map(s => (
<button key={s} onClick={() => setFilter(s)} className={`px-3 py-1 rounded text-xs capitalize ${filter === s ? 'bg-[#ffb800] text-white' : 'bg-[#111] border border-[#252525] text-[#888] hover:text-white'}`}>{s}</button>
<button key={s} onClick={() => setFilter(s)} className={`px-3 py-1 rounded text-xs capitalize ${filter === s ? 'bg-[#F0A623] text-white' : 'bg-[#111] border border-[#3a322b] text-[#888] hover:text-white'}`}>{s}</button>
))}
<input value={search} onChange={e => setSearch(e.target.value)} placeholder="Search clients…" className="ml-auto px-3 py-1 bg-[#111] border border-[#252525] rounded text-xs text-white placeholder-[#555] focus:outline-none focus:border-[#ffb800]" />
<input value={search} onChange={e => setSearch(e.target.value)} placeholder="Search clients…" className="ml-auto px-3 py-1 bg-[#111] border border-[#3a322b] rounded text-xs text-white placeholder-[#555] focus:outline-none focus:border-[#F0A623]" />
</div>
{/* New Client Form */}
{showForm && (
<form onSubmit={handleCreate} noValidate className="bg-[#111] border border-[#252525] rounded-lg p-4 space-y-3">
<form onSubmit={handleCreate} noValidate className="bg-[#111] border border-[#3a322b] rounded-lg p-4 space-y-3">
<h3 className="text-sm font-semibold">New Client</h3>
{errors._form && (
@@ -148,7 +148,7 @@ export default function ClientsPage() {
type={f.key === 'contact_email' ? 'email' : 'text'}
value={(form as any)[f.key]}
onChange={e => handleChange(f.key, e.target.value)}
className={`w-full px-2 py-1.5 bg-[#0a0a0a] border rounded text-xs text-white focus:outline-none focus:border-[#ffb800] ${errors[f.key] ? 'border-red-500/60' : 'border-[#252525]'}`}
className={`w-full px-2 py-1.5 bg-[#0a0a0a] border rounded text-xs text-white focus:outline-none focus:border-[#F0A623] ${errors[f.key] ? 'border-red-500/60' : 'border-[#3a322b]'}`}
/>
{errors[f.key] && <p className="mt-1 text-xs text-red-400">{errors[f.key]}</p>}
</div>
@@ -156,26 +156,26 @@ export default function ClientsPage() {
<div className="col-span-2">
<label className="text-xs text-[#888] block mb-1">Billing Address</label>
<input value={form.billing_address} onChange={e => handleChange('billing_address', e.target.value)}
className="w-full px-2 py-1.5 bg-[#0a0a0a] border border-[#252525] rounded text-xs text-white focus:outline-none focus:border-[#ffb800]" />
className="w-full px-2 py-1.5 bg-[#0a0a0a] border border-[#3a322b] rounded text-xs text-white focus:outline-none focus:border-[#F0A623]" />
</div>
<div className="col-span-2">
<label className="text-xs text-[#888] block mb-1">Notes</label>
<textarea value={form.notes} onChange={e => handleChange('notes', e.target.value)} rows={2}
className="w-full px-2 py-1.5 bg-[#0a0a0a] border border-[#252525] rounded text-xs text-white focus:outline-none focus:border-[#ffb800]" />
className="w-full px-2 py-1.5 bg-[#0a0a0a] border border-[#3a322b] rounded text-xs text-white focus:outline-none focus:border-[#F0A623]" />
</div>
</div>
<div className="flex gap-2">
<button type="submit" disabled={saving} className="px-3 py-1.5 bg-[#ffb800] rounded text-xs font-medium hover:bg-blue-500 disabled:opacity-50">{saving ? 'Saving…' : 'Create Client'}</button>
<button type="button" onClick={handleCancel} className="px-3 py-1.5 bg-[#1a1a1a] border border-[#252525] rounded text-xs text-[#888] hover:text-white">Cancel</button>
<button type="submit" disabled={saving} className="px-3 py-1.5 bg-[#F0A623] rounded text-xs font-medium hover:bg-blue-500 disabled:opacity-50">{saving ? 'Saving…' : 'Create Client'}</button>
<button type="button" onClick={handleCancel} className="px-3 py-1.5 bg-[#231d18] border border-[#3a322b] rounded text-xs text-[#888] hover:text-white">Cancel</button>
</div>
</form>
)}
{/* Table */}
<div className="bg-[#111] border border-[#252525] rounded-lg overflow-hidden">
<div className="bg-[#111] border border-[#3a322b] rounded-lg overflow-hidden">
<table className="w-full text-sm">
<thead>
<tr className="border-b border-[#1a1a1a]">
<tr className="border-b border-[#231d18]">
{['Company','Contact','Email','Active Orders','Total Revenue','Status',''].map(h => (
<th key={h} className="px-4 py-2 text-left text-xs text-[#555] font-medium">{h}</th>
))}
@@ -187,17 +187,17 @@ export default function ClientsPage() {
) : clients.length === 0 ? (
<tr><td colSpan={7} className="px-4 py-8 text-center text-[#555] text-xs">No clients found</td></tr>
) : clients.map(c => (
<tr key={c.id} className="border-b border-[#1a1a1a] hover:bg-[#161616]">
<tr key={c.id} className="border-b border-[#231d18] hover:bg-[#231d18]">
<td className="px-4 py-2 text-white font-medium">{c.company_name}</td>
<td className="px-4 py-2 text-[#ccc]">{c.contact_name}</td>
<td className="px-4 py-2 text-[#888]">{c.contact_email}</td>
<td className="px-4 py-2 text-[#ccc]">{c.active_orders ?? 0}</td>
<td className="px-4 py-2 text-green-400">{fmt(c.total_revenue ?? 0)}</td>
<td className="px-4 py-2">
<span className={`px-2 py-0.5 rounded-full text-xs ${c.status === 'active' ? 'bg-green-400/10 text-green-400' : 'bg-[#1a1a1a] text-[#555]'}`}>{c.status}</span>
<span className={`px-2 py-0.5 rounded-full text-xs ${c.status === 'active' ? 'bg-green-400/10 text-green-400' : 'bg-[#231d18] text-[#555]'}`}>{c.status}</span>
</td>
<td className="px-4 py-2">
<Link href={`/admin/accounting/clients/${c.id}`} className="text-xs text-[#ffb800] hover:underline">View</Link>
<Link href={`/admin/accounting/clients/${c.id}`} className="text-xs text-[#F0A623] hover:underline">View</Link>
</td>
</tr>
))}

View File

@@ -41,7 +41,7 @@ export default function CommissionsPage() {
load();
}
if (loading) return <div className="min-h-screen bg-[#0a0a0a] flex items-center justify-center"><div className="w-8 h-8 border-2 border-[#ffb800] border-t-transparent rounded-full animate-spin" /></div>;
if (loading) return <div className="min-h-screen bg-[#0a0a0a] flex items-center justify-center"><div className="w-8 h-8 border-2 border-[#F0A623] border-t-transparent rounded-full animate-spin" /></div>;
if (!user) return null;
return (
@@ -53,10 +53,10 @@ export default function CommissionsPage() {
<Link href="/admin/accounting" className="text-xs text-[#555] hover:text-[#888]"> Accounting</Link>
</div>
<div className="flex gap-2">
<select value={year} onChange={e => setYear(Number(e.target.value))} className="px-2 py-1 bg-[#111] border border-[#252525] rounded text-xs text-[#888] focus:outline-none">
<select value={year} onChange={e => setYear(Number(e.target.value))} className="px-2 py-1 bg-[#111] border border-[#3a322b] rounded text-xs text-[#888] focus:outline-none">
{[2024, 2025, 2026, 2027].map(y => <option key={y} value={y}>{y}</option>)}
</select>
<select value={filterStaff} onChange={e => setFilterStaff(e.target.value)} className="px-2 py-1 bg-[#111] border border-[#252525] rounded text-xs text-[#888] focus:outline-none">
<select value={filterStaff} onChange={e => setFilterStaff(e.target.value)} className="px-2 py-1 bg-[#111] border border-[#3a322b] rounded text-xs text-[#888] focus:outline-none">
<option value="">All Staff</option>
{staff.map((s: any) => <option key={s.id} value={s.id}>{s.full_name}</option>)}
</select>
@@ -68,7 +68,7 @@ export default function CommissionsPage() {
{summary.map((s: any) => {
const pct = s.next_threshold_cents ? Math.min(100, Math.round((s.ytd_sales_cents / s.next_threshold_cents) * 100)) : 100;
return (
<div key={s.staff_id} className="bg-[#111] border border-[#252525] rounded-lg p-4 space-y-2">
<div key={s.staff_id} className="bg-[#111] border border-[#3a322b] rounded-lg p-4 space-y-2">
<div className="flex items-center justify-between">
<p className="text-sm font-semibold text-white">{s.full_name}</p>
<span className="text-xs text-[#888]">{s.current_tier}</span>
@@ -83,8 +83,8 @@ export default function CommissionsPage() {
<div className="flex justify-between text-xs text-[#555] mb-1">
<span>Tier progress</span><span>{pct}%</span>
</div>
<div className="h-1.5 bg-[#1a1a1a] rounded-full overflow-hidden">
<div className="h-full bg-[#ffb800] rounded-full transition-all" style={{ width: `${pct}%` }} />
<div className="h-1.5 bg-[#231d18] rounded-full overflow-hidden">
<div className="h-full bg-[#F0A623] rounded-full transition-all" style={{ width: `${pct}%` }} />
</div>
</div>
)}
@@ -94,10 +94,10 @@ export default function CommissionsPage() {
</div>
{/* Detail table */}
<div className="bg-[#111] border border-[#252525] rounded-lg overflow-x-auto">
<div className="bg-[#111] border border-[#3a322b] rounded-lg overflow-x-auto">
<table className="w-full text-sm">
<thead>
<tr className="border-b border-[#1a1a1a]">
<tr className="border-b border-[#231d18]">
{['Staff','Invoice','Sale Amount','Rate','Commission','YTD at Time','Paid',''].map(h => (
<th key={h} className="px-3 py-2 text-left text-xs text-[#555] font-medium">{h}</th>
))}
@@ -109,9 +109,9 @@ export default function CommissionsPage() {
) : commissions.length === 0 ? (
<tr><td colSpan={8} className="px-4 py-8 text-center text-[#555] text-xs">No commission records</td></tr>
) : commissions.map((c: any) => (
<tr key={c.id} className="border-b border-[#1a1a1a] hover:bg-[#161616]">
<tr key={c.id} className="border-b border-[#231d18] hover:bg-[#231d18]">
<td className="px-3 py-2 text-white text-xs">{c.rmp_sales_staff?.full_name ?? '—'}</td>
<td className="px-3 py-2 text-[#ffb800] text-xs font-mono">{c.rmp_invoices?.invoice_number ?? '—'}</td>
<td className="px-3 py-2 text-[#F0A623] text-xs font-mono">{c.rmp_invoices?.invoice_number ?? '—'}</td>
<td className="px-3 py-2 text-[#ccc] text-xs">{fmt(c.sale_amount_cents ?? 0)}</td>
<td className="px-3 py-2 text-[#888] text-xs">{((c.commission_rate ?? 0) * 100).toFixed(1)}%</td>
<td className="px-3 py-2 text-green-400 text-xs">{fmt(c.commission_amount_cents ?? 0)}</td>

View File

@@ -43,7 +43,7 @@ export default function DocumentsPage() {
const years = Array.from({ length: 5 }, (_, i) => new Date().getFullYear() - i);
if (loading) return <div className="min-h-screen bg-[#0a0a0a] flex items-center justify-center"><div className="w-8 h-8 border-2 border-[#ffb800] border-t-transparent rounded-full animate-spin" /></div>;
if (loading) return <div className="min-h-screen bg-[#0a0a0a] flex items-center justify-center"><div className="w-8 h-8 border-2 border-[#F0A623] border-t-transparent rounded-full animate-spin" /></div>;
if (!user) return null;
return (
@@ -54,70 +54,70 @@ export default function DocumentsPage() {
<h1 className="text-xl font-bold">Document Vault</h1>
<Link href="/admin/accounting" className="text-xs text-[#555] hover:text-[#888]"> Accounting</Link>
</div>
<button onClick={() => setShowForm(true)} className="px-3 py-1.5 bg-[#ffb800] rounded text-xs font-medium hover:bg-blue-500">+ Upload Document</button>
<button onClick={() => setShowForm(true)} className="px-3 py-1.5 bg-[#F0A623] rounded text-xs font-medium hover:bg-blue-500">+ Upload Document</button>
</div>
{/* Year tabs */}
<div className="flex gap-1 flex-wrap">
<button onClick={() => setFilterYear('')} className={`px-3 py-1 rounded text-xs ${!filterYear ? 'bg-[#ffb800] text-white' : 'bg-[#111] border border-[#252525] text-[#888]'}`}>All Years</button>
<button onClick={() => setFilterYear('')} className={`px-3 py-1 rounded text-xs ${!filterYear ? 'bg-[#F0A623] text-white' : 'bg-[#111] border border-[#3a322b] text-[#888]'}`}>All Years</button>
{years.map(y => (
<button key={y} onClick={() => setFilterYear(String(y))} className={`px-3 py-1 rounded text-xs ${filterYear === String(y) ? 'bg-[#ffb800] text-white' : 'bg-[#111] border border-[#252525] text-[#888]'}`}>{y}</button>
<button key={y} onClick={() => setFilterYear(String(y))} className={`px-3 py-1 rounded text-xs ${filterYear === String(y) ? 'bg-[#F0A623] text-white' : 'bg-[#111] border border-[#3a322b] text-[#888]'}`}>{y}</button>
))}
</div>
<div className="flex gap-2">
<select value={filterType} onChange={e => setFilterType(e.target.value)} className="px-2 py-1 bg-[#111] border border-[#252525] rounded text-xs text-[#888] focus:outline-none">
<select value={filterType} onChange={e => setFilterType(e.target.value)} className="px-2 py-1 bg-[#111] border border-[#3a322b] rounded text-xs text-[#888] focus:outline-none">
<option value="">All Types</option>
{DOC_TYPES.map(t => <option key={t} value={t}>{t.replace(/_/g, ' ')}</option>)}
</select>
</div>
{showForm && (
<form onSubmit={handleCreate} className="bg-[#111] border border-[#252525] rounded-lg p-4 space-y-3">
<form onSubmit={handleCreate} className="bg-[#111] border border-[#3a322b] rounded-lg p-4 space-y-3">
<h3 className="text-sm font-semibold">Upload Document</h3>
<div className="grid grid-cols-2 gap-3">
<div>
<label className="text-xs text-[#888] block mb-1">Type</label>
<select value={form.document_type} onChange={e => setForm(p => ({ ...p, document_type: e.target.value }))} className="w-full px-2 py-1.5 bg-[#0a0a0a] border border-[#252525] rounded text-xs text-white focus:outline-none">
<select value={form.document_type} onChange={e => setForm(p => ({ ...p, document_type: e.target.value }))} className="w-full px-2 py-1.5 bg-[#0a0a0a] border border-[#3a322b] rounded text-xs text-white focus:outline-none">
{DOC_TYPES.map(t => <option key={t} value={t}>{t.replace(/_/g, ' ')}</option>)}
</select>
</div>
<div>
<label className="text-xs text-[#888] block mb-1">Title *</label>
<input required value={form.title} onChange={e => setForm(p => ({ ...p, title: e.target.value }))} className="w-full px-2 py-1.5 bg-[#0a0a0a] border border-[#252525] rounded text-xs text-white focus:outline-none focus:border-[#ffb800]" />
<input required value={form.title} onChange={e => setForm(p => ({ ...p, title: e.target.value }))} className="w-full px-2 py-1.5 bg-[#0a0a0a] border border-[#3a322b] rounded text-xs text-white focus:outline-none focus:border-[#F0A623]" />
</div>
{form.document_type === 'sales_agreement' && (
<>
<div>
<label className="text-xs text-[#888] block mb-1">Person *</label>
<input required value={form.person} onChange={e => setForm(p => ({ ...p, person: e.target.value }))} className="w-full px-2 py-1.5 bg-[#0a0a0a] border border-[#252525] rounded text-xs text-white focus:outline-none focus:border-[#ffb800]" />
<input required value={form.person} onChange={e => setForm(p => ({ ...p, person: e.target.value }))} className="w-full px-2 py-1.5 bg-[#0a0a0a] border border-[#3a322b] rounded text-xs text-white focus:outline-none focus:border-[#F0A623]" />
</div>
<div>
<label className="text-xs text-[#888] block mb-1">Year *</label>
<input type="number" required value={form.year} onChange={e => setForm(p => ({ ...p, year: e.target.value }))} className="w-full px-2 py-1.5 bg-[#0a0a0a] border border-[#252525] rounded text-xs text-white focus:outline-none focus:border-[#ffb800]" />
<input type="number" required value={form.year} onChange={e => setForm(p => ({ ...p, year: e.target.value }))} className="w-full px-2 py-1.5 bg-[#0a0a0a] border border-[#3a322b] rounded text-xs text-white focus:outline-none focus:border-[#F0A623]" />
</div>
</>
)}
<div className="col-span-2">
<label className="text-xs text-[#888] block mb-1">File URL</label>
<input value={form.file_url} onChange={e => setForm(p => ({ ...p, file_url: e.target.value }))} placeholder="https://…" className="w-full px-2 py-1.5 bg-[#0a0a0a] border border-[#252525] rounded text-xs text-white focus:outline-none focus:border-[#ffb800]" />
<input value={form.file_url} onChange={e => setForm(p => ({ ...p, file_url: e.target.value }))} placeholder="https://…" className="w-full px-2 py-1.5 bg-[#0a0a0a] border border-[#3a322b] rounded text-xs text-white focus:outline-none focus:border-[#F0A623]" />
</div>
<div className="col-span-2">
<label className="text-xs text-[#888] block mb-1">Notes</label>
<textarea value={form.notes} onChange={e => setForm(p => ({ ...p, notes: e.target.value }))} rows={2} className="w-full px-2 py-1.5 bg-[#0a0a0a] border border-[#252525] rounded text-xs text-white focus:outline-none focus:border-[#ffb800]" />
<textarea value={form.notes} onChange={e => setForm(p => ({ ...p, notes: e.target.value }))} rows={2} className="w-full px-2 py-1.5 bg-[#0a0a0a] border border-[#3a322b] rounded text-xs text-white focus:outline-none focus:border-[#F0A623]" />
</div>
</div>
<div className="flex gap-2">
<button type="submit" disabled={saving} className="px-3 py-1.5 bg-[#ffb800] rounded text-xs font-medium hover:bg-blue-500 disabled:opacity-50">{saving ? 'Saving…' : 'Save Document'}</button>
<button type="button" onClick={() => setShowForm(false)} className="px-3 py-1.5 bg-[#1a1a1a] border border-[#252525] rounded text-xs text-[#888] hover:text-white">Cancel</button>
<button type="submit" disabled={saving} className="px-3 py-1.5 bg-[#F0A623] rounded text-xs font-medium hover:bg-blue-500 disabled:opacity-50">{saving ? 'Saving…' : 'Save Document'}</button>
<button type="button" onClick={() => setShowForm(false)} className="px-3 py-1.5 bg-[#231d18] border border-[#3a322b] rounded text-xs text-[#888] hover:text-white">Cancel</button>
</div>
</form>
)}
<div className="bg-[#111] border border-[#252525] rounded-lg overflow-hidden">
<div className="bg-[#111] border border-[#3a322b] rounded-lg overflow-hidden">
<table className="w-full text-sm">
<thead>
<tr className="border-b border-[#1a1a1a]">
<tr className="border-b border-[#231d18]">
{['Type','Title','Person','Year','Date',''].map(h => (
<th key={h} className="px-4 py-2 text-left text-xs text-[#555] font-medium">{h}</th>
))}
@@ -129,14 +129,14 @@ export default function DocumentsPage() {
) : docs.length === 0 ? (
<tr><td colSpan={6} className="px-4 py-8 text-center text-[#555] text-xs">No documents found</td></tr>
) : docs.map((d: any) => (
<tr key={d.id} className="border-b border-[#1a1a1a] hover:bg-[#161616]">
<tr key={d.id} className="border-b border-[#231d18] hover:bg-[#231d18]">
<td className="px-4 py-2 text-[#888] text-xs capitalize">{d.document_type?.replace(/_/g, ' ')}</td>
<td className="px-4 py-2 text-white text-xs">{d.title}</td>
<td className="px-4 py-2 text-[#ccc] text-xs">{d.person ?? '—'}</td>
<td className="px-4 py-2 text-[#888] text-xs">{d.year ?? '—'}</td>
<td className="px-4 py-2 text-[#555] text-xs">{d.uploaded_at ? new Date(d.uploaded_at).toLocaleDateString() : '—'}</td>
<td className="px-4 py-2 flex gap-2">
{d.file_url && <a href={d.file_url} target="_blank" rel="noopener noreferrer" className="text-xs text-[#ffb800] hover:underline">Download</a>}
{d.file_url && <a href={d.file_url} target="_blank" rel="noopener noreferrer" className="text-xs text-[#F0A623] hover:underline">Download</a>}
</td>
</tr>
))}

View File

@@ -84,7 +84,7 @@ export default function ExpensesPage() {
setForm({ vendor: '', amount: '', category: 'other', description: '', expense_date: new Date().toISOString().split('T')[0], receipt_source: 'manual' });
}
if (loading) return <div className="min-h-screen bg-[#0a0a0a] flex items-center justify-center"><div className="w-8 h-8 border-2 border-[#ffb800] border-t-transparent rounded-full animate-spin" /></div>;
if (loading) return <div className="min-h-screen bg-[#0a0a0a] flex items-center justify-center"><div className="w-8 h-8 border-2 border-[#F0A623] border-t-transparent rounded-full animate-spin" /></div>;
if (!user) return null;
return (
@@ -95,7 +95,7 @@ export default function ExpensesPage() {
<h1 className="text-xl font-bold">Expenses</h1>
<Link href="/admin/accounting" className="text-xs text-[#555] hover:text-[#888]"> Accounting</Link>
</div>
<button onClick={() => setShowForm(true)} className="px-3 py-1.5 bg-[#ffb800] rounded text-xs font-medium hover:bg-blue-500">+ Log Expense</button>
<button onClick={() => setShowForm(true)} className="px-3 py-1.5 bg-[#F0A623] rounded text-xs font-medium hover:bg-blue-500">+ Log Expense</button>
</div>
{/* Success Toast */}
@@ -107,11 +107,11 @@ export default function ExpensesPage() {
)}
<div className="flex gap-2 flex-wrap">
<select value={filterCat} onChange={e => setFilterCat(e.target.value)} className="px-2 py-1 bg-[#111] border border-[#252525] rounded text-xs text-[#888] focus:outline-none">
<select value={filterCat} onChange={e => setFilterCat(e.target.value)} className="px-2 py-1 bg-[#111] border border-[#3a322b] rounded text-xs text-[#888] focus:outline-none">
<option value="">All Categories</option>
{CATEGORIES.map(c => <option key={c} value={c}>{c.replace(/_/g, ' ')}</option>)}
</select>
<select value={filterReconciled} onChange={e => setFilterReconciled(e.target.value)} className="px-2 py-1 bg-[#111] border border-[#252525] rounded text-xs text-[#888] focus:outline-none">
<select value={filterReconciled} onChange={e => setFilterReconciled(e.target.value)} className="px-2 py-1 bg-[#111] border border-[#3a322b] rounded text-xs text-[#888] focus:outline-none">
<option value="">All</option>
<option value="true">Reconciled</option>
<option value="false">Unreconciled</option>
@@ -119,7 +119,7 @@ export default function ExpensesPage() {
</div>
{showForm && (
<form onSubmit={handleCreate} noValidate className="bg-[#111] border border-[#252525] rounded-lg p-4 space-y-3">
<form onSubmit={handleCreate} noValidate className="bg-[#111] border border-[#3a322b] rounded-lg p-4 space-y-3">
<h3 className="text-sm font-semibold">Log Expense</h3>
{errors._form && (
@@ -132,40 +132,40 @@ export default function ExpensesPage() {
<div className="grid grid-cols-2 md:grid-cols-3 gap-3">
<div>
<label className="text-xs text-[#888] block mb-1">Vendor</label>
<input type="text" value={form.vendor} onChange={e => handleChange('vendor', e.target.value)} className="w-full px-2 py-1.5 bg-[#0a0a0a] border border-[#252525] rounded text-xs text-white focus:outline-none focus:border-[#ffb800]" />
<input type="text" value={form.vendor} onChange={e => handleChange('vendor', e.target.value)} className="w-full px-2 py-1.5 bg-[#0a0a0a] border border-[#3a322b] rounded text-xs text-white focus:outline-none focus:border-[#F0A623]" />
</div>
<div>
<label className="text-xs text-[#888] block mb-1">Amount ($) <span className="text-red-400">*</span></label>
<input type="number" step="0.01" value={form.amount} onChange={e => handleChange('amount', e.target.value)} className={`w-full px-2 py-1.5 bg-[#0a0a0a] border rounded text-xs text-white focus:outline-none focus:border-[#ffb800] ${errors.amount ? 'border-red-500/60' : 'border-[#252525]'}`} />
<input type="number" step="0.01" value={form.amount} onChange={e => handleChange('amount', e.target.value)} className={`w-full px-2 py-1.5 bg-[#0a0a0a] border rounded text-xs text-white focus:outline-none focus:border-[#F0A623] ${errors.amount ? 'border-red-500/60' : 'border-[#3a322b]'}`} />
{errors.amount && <p className="mt-1 text-xs text-red-400">{errors.amount}</p>}
</div>
<div>
<label className="text-xs text-[#888] block mb-1">Date <span className="text-red-400">*</span></label>
<input type="date" value={form.expense_date} onChange={e => handleChange('expense_date', e.target.value)} className={`w-full px-2 py-1.5 bg-[#0a0a0a] border rounded text-xs text-white focus:outline-none focus:border-[#ffb800] ${errors.expense_date ? 'border-red-500/60' : 'border-[#252525]'}`} />
<input type="date" value={form.expense_date} onChange={e => handleChange('expense_date', e.target.value)} className={`w-full px-2 py-1.5 bg-[#0a0a0a] border rounded text-xs text-white focus:outline-none focus:border-[#F0A623] ${errors.expense_date ? 'border-red-500/60' : 'border-[#3a322b]'}`} />
{errors.expense_date && <p className="mt-1 text-xs text-red-400">{errors.expense_date}</p>}
</div>
<div>
<label className="text-xs text-[#888] block mb-1">Category</label>
<select value={form.category} onChange={e => handleChange('category', e.target.value)} className="w-full px-2 py-1.5 bg-[#0a0a0a] border border-[#252525] rounded text-xs text-white focus:outline-none">
<select value={form.category} onChange={e => handleChange('category', e.target.value)} className="w-full px-2 py-1.5 bg-[#0a0a0a] border border-[#3a322b] rounded text-xs text-white focus:outline-none">
{CATEGORIES.map(c => <option key={c} value={c}>{c.replace(/_/g, ' ')}</option>)}
</select>
</div>
<div className="col-span-2">
<label className="text-xs text-[#888] block mb-1">Description</label>
<input value={form.description} onChange={e => handleChange('description', e.target.value)} className="w-full px-2 py-1.5 bg-[#0a0a0a] border border-[#252525] rounded text-xs text-white focus:outline-none focus:border-[#ffb800]" />
<input value={form.description} onChange={e => handleChange('description', e.target.value)} className="w-full px-2 py-1.5 bg-[#0a0a0a] border border-[#3a322b] rounded text-xs text-white focus:outline-none focus:border-[#F0A623]" />
</div>
</div>
<div className="flex gap-2">
<button type="submit" disabled={saving} className="px-3 py-1.5 bg-[#ffb800] rounded text-xs font-medium hover:bg-blue-500 disabled:opacity-50">{saving ? 'Saving…' : 'Log Expense'}</button>
<button type="button" onClick={handleCancel} className="px-3 py-1.5 bg-[#1a1a1a] border border-[#252525] rounded text-xs text-[#888] hover:text-white">Cancel</button>
<button type="submit" disabled={saving} className="px-3 py-1.5 bg-[#F0A623] rounded text-xs font-medium hover:bg-blue-500 disabled:opacity-50">{saving ? 'Saving…' : 'Log Expense'}</button>
<button type="button" onClick={handleCancel} className="px-3 py-1.5 bg-[#231d18] border border-[#3a322b] rounded text-xs text-[#888] hover:text-white">Cancel</button>
</div>
</form>
)}
<div className="bg-[#111] border border-[#252525] rounded-lg overflow-x-auto">
<div className="bg-[#111] border border-[#3a322b] rounded-lg overflow-x-auto">
<table className="w-full text-sm">
<thead>
<tr className="border-b border-[#1a1a1a]">
<tr className="border-b border-[#231d18]">
{['Date','Vendor','Amount','Category','Source','Reconciled','Description'].map(h => (
<th key={h} className="px-3 py-2 text-left text-xs text-[#555] font-medium">{h}</th>
))}
@@ -177,7 +177,7 @@ export default function ExpensesPage() {
) : expenses.length === 0 ? (
<tr><td colSpan={7} className="px-4 py-8 text-center text-[#555] text-xs">No expenses found</td></tr>
) : expenses.map((ex: any) => (
<tr key={ex.id} className="border-b border-[#1a1a1a] hover:bg-[#161616]">
<tr key={ex.id} className="border-b border-[#231d18] hover:bg-[#231d18]">
<td className="px-3 py-2 text-[#888] text-xs">{ex.expense_date}</td>
<td className="px-3 py-2 text-white text-xs">{ex.vendor ?? '—'}</td>
<td className="px-3 py-2 text-red-400 text-xs">{ex.amount_cents ? fmt(ex.amount_cents) : '—'}</td>

View File

@@ -10,7 +10,7 @@ const STATUS_COLORS: Record<string, string> = {
paid: 'bg-green-400/10 text-green-400',
pending: 'bg-yellow-400/10 text-yellow-400',
overdue: 'bg-red-400/10 text-red-400',
cancelled: 'bg-[#1a1a1a] text-[#555]',
cancelled: 'bg-[#231d18] text-[#555]',
};
function InvoicesContent() {
@@ -83,7 +83,7 @@ function InvoicesContent() {
setPayForm({ method: 'check', paid_date: new Date().toISOString().split('T')[0], wire_reference: '' });
}
if (loading) return <div className="min-h-screen bg-[#0a0a0a] flex items-center justify-center"><div className="w-8 h-8 border-2 border-[#ffb800] border-t-transparent rounded-full animate-spin" /></div>;
if (loading) return <div className="min-h-screen bg-[#0a0a0a] flex items-center justify-center"><div className="w-8 h-8 border-2 border-[#F0A623] border-t-transparent rounded-full animate-spin" /></div>;
if (!user) return null;
return (
@@ -94,7 +94,7 @@ function InvoicesContent() {
<h1 className="text-xl font-bold">Invoices</h1>
<Link href="/admin/accounting" className="text-xs text-[#555] hover:text-[#888]"> Accounting</Link>
</div>
<Link href="/admin/accounting/orders" className="px-3 py-1.5 bg-[#ffb800] rounded text-xs font-medium hover:bg-blue-500">+ New Invoice (via Order)</Link>
<Link href="/admin/accounting/orders" className="px-3 py-1.5 bg-[#F0A623] rounded text-xs font-medium hover:bg-blue-500">+ New Invoice (via Order)</Link>
</div>
{/* Success Toast */}
@@ -108,14 +108,14 @@ function InvoicesContent() {
{/* Filters */}
<div className="flex gap-2 flex-wrap">
{['','pending','paid','overdue','cancelled'].map(s => (
<button key={s} onClick={() => setFilterStatus(s)} className={`px-3 py-1 rounded text-xs capitalize ${filterStatus === s ? 'bg-[#ffb800] text-white' : 'bg-[#111] border border-[#252525] text-[#888] hover:text-white'}`}>{s || 'All'}</button>
<button key={s} onClick={() => setFilterStatus(s)} className={`px-3 py-1 rounded text-xs capitalize ${filterStatus === s ? 'bg-[#F0A623] text-white' : 'bg-[#111] border border-[#3a322b] text-[#888] hover:text-white'}`}>{s || 'All'}</button>
))}
</div>
{/* Mark Paid Modal */}
{markPaidId && (
<div className="fixed inset-0 bg-black/60 flex items-center justify-center z-50">
<form onSubmit={handleMarkPaid} noValidate className="bg-[#111] border border-[#252525] rounded-lg p-5 w-80 space-y-3">
<form onSubmit={handleMarkPaid} noValidate className="bg-[#111] border border-[#3a322b] rounded-lg p-5 w-80 space-y-3">
<h3 className="text-sm font-semibold">Mark Invoice as Paid</h3>
{payErrors._form && (
@@ -127,35 +127,35 @@ function InvoicesContent() {
<div>
<label className="text-xs text-[#888] block mb-1">Payment Method</label>
<select value={payForm.method} onChange={e => handlePayChange('method', e.target.value)} className="w-full px-2 py-1.5 bg-[#0a0a0a] border border-[#252525] rounded text-xs text-white focus:outline-none">
<select value={payForm.method} onChange={e => handlePayChange('method', e.target.value)} className="w-full px-2 py-1.5 bg-[#0a0a0a] border border-[#3a322b] rounded text-xs text-white focus:outline-none">
{['stripe','wire','check','other'].map(m => <option key={m} value={m}>{m}</option>)}
</select>
</div>
<div>
<label className="text-xs text-[#888] block mb-1">Paid Date <span className="text-red-400">*</span></label>
<input type="date" value={payForm.paid_date} onChange={e => handlePayChange('paid_date', e.target.value)} className={`w-full px-2 py-1.5 bg-[#0a0a0a] border rounded text-xs text-white focus:outline-none ${payErrors.paid_date ? 'border-red-500/60' : 'border-[#252525]'}`} />
<input type="date" value={payForm.paid_date} onChange={e => handlePayChange('paid_date', e.target.value)} className={`w-full px-2 py-1.5 bg-[#0a0a0a] border rounded text-xs text-white focus:outline-none ${payErrors.paid_date ? 'border-red-500/60' : 'border-[#3a322b]'}`} />
{payErrors.paid_date && <p className="mt-1 text-xs text-red-400">{payErrors.paid_date}</p>}
</div>
{payForm.method === 'wire' && (
<div>
<label className="text-xs text-[#888] block mb-1">Wire Reference <span className="text-red-400">*</span></label>
<input value={payForm.wire_reference} onChange={e => handlePayChange('wire_reference', e.target.value)} className={`w-full px-2 py-1.5 bg-[#0a0a0a] border rounded text-xs text-white focus:outline-none ${payErrors.wire_reference ? 'border-red-500/60' : 'border-[#252525]'}`} />
<input value={payForm.wire_reference} onChange={e => handlePayChange('wire_reference', e.target.value)} className={`w-full px-2 py-1.5 bg-[#0a0a0a] border rounded text-xs text-white focus:outline-none ${payErrors.wire_reference ? 'border-red-500/60' : 'border-[#3a322b]'}`} />
{payErrors.wire_reference && <p className="mt-1 text-xs text-red-400">{payErrors.wire_reference}</p>}
</div>
)}
<div className="flex gap-2">
<button type="submit" disabled={saving} className="px-3 py-1.5 bg-green-600 rounded text-xs font-medium hover:bg-green-500 disabled:opacity-50">{saving ? 'Saving…' : 'Mark Paid'}</button>
<button type="button" onClick={handleCloseModal} className="px-3 py-1.5 bg-[#1a1a1a] border border-[#252525] rounded text-xs text-[#888] hover:text-white">Cancel</button>
<button type="button" onClick={handleCloseModal} className="px-3 py-1.5 bg-[#231d18] border border-[#3a322b] rounded text-xs text-[#888] hover:text-white">Cancel</button>
</div>
</form>
</div>
)}
{/* Table */}
<div className="bg-[#111] border border-[#252525] rounded-lg overflow-x-auto">
<div className="bg-[#111] border border-[#3a322b] rounded-lg overflow-x-auto">
<table className="w-full text-sm">
<thead>
<tr className="border-b border-[#1a1a1a]">
<tr className="border-b border-[#231d18]">
{['Invoice#','Client','Site','Order#','Amount','Status','Due Date','Salesperson',''].map(h => (
<th key={h} className="px-3 py-2 text-left text-xs text-[#555] font-medium whitespace-nowrap">{h}</th>
))}
@@ -167,14 +167,14 @@ function InvoicesContent() {
) : invoices.length === 0 ? (
<tr><td colSpan={9} className="px-4 py-8 text-center text-[#555] text-xs">No invoices found</td></tr>
) : invoices.map((inv: any) => (
<tr key={inv.id} className="border-b border-[#1a1a1a] hover:bg-[#161616]">
<td className="px-3 py-2 text-[#ffb800] font-mono text-xs">{inv.invoice_number}</td>
<tr key={inv.id} className="border-b border-[#231d18] hover:bg-[#231d18]">
<td className="px-3 py-2 text-[#F0A623] font-mono text-xs">{inv.invoice_number}</td>
<td className="px-3 py-2 text-white text-xs">{inv.rmp_clients?.company_name ?? '—'}</td>
<td className="px-3 py-2 text-[#888] text-xs capitalize">{inv.site}</td>
<td className="px-3 py-2 text-[#888] text-xs font-mono">{inv.rmp_orders?.internal_order_number ?? '—'}</td>
<td className="px-3 py-2 text-green-400 text-xs">{inv.amount_cents ? fmt(inv.amount_cents) : '—'}</td>
<td className="px-3 py-2">
<span className={`px-2 py-0.5 rounded-full text-xs ${STATUS_COLORS[inv.status] ?? 'bg-[#1a1a1a] text-[#555]'}`}>{inv.status}</span>
<span className={`px-2 py-0.5 rounded-full text-xs ${STATUS_COLORS[inv.status] ?? 'bg-[#231d18] text-[#555]'}`}>{inv.status}</span>
</td>
<td className="px-3 py-2 text-[#888] text-xs">{inv.due_date ?? '—'}</td>
<td className="px-3 py-2 text-[#ccc] text-xs">{inv.rmp_sales_staff?.full_name ?? '—'}</td>
@@ -195,7 +195,7 @@ function InvoicesContent() {
export default function InvoicesPage() {
return (
<Suspense fallback={<div className="min-h-screen bg-[#0a0a0a] flex items-center justify-center"><div className="w-8 h-8 border-2 border-[#ffb800] border-t-transparent rounded-full animate-spin" /></div>}>
<Suspense fallback={<div className="min-h-screen bg-[#0a0a0a] flex items-center justify-center"><div className="w-8 h-8 border-2 border-[#F0A623] border-t-transparent rounded-full animate-spin" /></div>}>
<InvoicesContent />
</Suspense>
);

View File

@@ -104,7 +104,7 @@ function OrdersContent() {
setForm({ client_id: '', site: '', product_id: '', ad_unit: '', description: '', start_date: '', end_date: '', total_amount_cents: '', currency: 'USD', invoicing_schedule: 'single', next_invoice_date: '', staff_id: '', po_number: '', notes: '' });
}
if (loading) return <div className="min-h-screen bg-[#0a0a0a] flex items-center justify-center"><div className="w-8 h-8 border-2 border-[#ffb800] border-t-transparent rounded-full animate-spin" /></div>;
if (loading) return <div className="min-h-screen bg-[#0a0a0a] flex items-center justify-center"><div className="w-8 h-8 border-2 border-[#F0A623] border-t-transparent rounded-full animate-spin" /></div>;
if (!user) return null;
return (
@@ -115,7 +115,7 @@ function OrdersContent() {
<h1 className="text-xl font-bold">Orders</h1>
<Link href="/admin/accounting" className="text-xs text-[#555] hover:text-[#888]"> Accounting</Link>
</div>
<button onClick={() => setShowForm(true)} className="px-3 py-1.5 bg-[#ffb800] rounded text-xs font-medium hover:bg-blue-500">+ New Order</button>
<button onClick={() => setShowForm(true)} className="px-3 py-1.5 bg-[#F0A623] rounded text-xs font-medium hover:bg-blue-500">+ New Order</button>
</div>
{/* Success Toast */}
@@ -128,20 +128,20 @@ function OrdersContent() {
{/* Filters */}
<div className="flex gap-2 flex-wrap items-center">
<select value={filterSite} onChange={e => setFilterSite(e.target.value)} className="px-2 py-1 bg-[#111] border border-[#252525] rounded text-xs text-[#888] focus:outline-none">
<select value={filterSite} onChange={e => setFilterSite(e.target.value)} className="px-2 py-1 bg-[#111] border border-[#3a322b] rounded text-xs text-[#888] focus:outline-none">
<option value="">All Sites</option>
{SITES.map(s => <option key={s} value={s}>{s}</option>)}
</select>
<select value={filterStatus} onChange={e => setFilterStatus(e.target.value)} className="px-2 py-1 bg-[#111] border border-[#252525] rounded text-xs text-[#888] focus:outline-none">
<select value={filterStatus} onChange={e => setFilterStatus(e.target.value)} className="px-2 py-1 bg-[#111] border border-[#3a322b] rounded text-xs text-[#888] focus:outline-none">
<option value="">All Statuses</option>
{STATUSES.map(s => <option key={s} value={s}>{s}</option>)}
</select>
<input value={filterClient} onChange={e => setFilterClient(e.target.value)} placeholder="Filter by client…" className="px-2 py-1 bg-[#111] border border-[#252525] rounded text-xs text-white placeholder-[#555] focus:outline-none focus:border-[#ffb800]" />
<input value={filterClient} onChange={e => setFilterClient(e.target.value)} placeholder="Filter by client…" className="px-2 py-1 bg-[#111] border border-[#3a322b] rounded text-xs text-white placeholder-[#555] focus:outline-none focus:border-[#F0A623]" />
</div>
{/* New Order Form */}
{showForm && (
<form onSubmit={handleCreate} noValidate className="bg-[#111] border border-[#252525] rounded-lg p-4 space-y-3">
<form onSubmit={handleCreate} noValidate className="bg-[#111] border border-[#3a322b] rounded-lg p-4 space-y-3">
<h3 className="text-sm font-semibold">New Order</h3>
{errors._form && (
@@ -154,7 +154,7 @@ function OrdersContent() {
<div className="grid grid-cols-2 md:grid-cols-3 gap-3">
<div>
<label className="text-xs text-[#888] block mb-1">Client <span className="text-red-400">*</span></label>
<select value={form.client_id} onChange={e => handleChange('client_id', e.target.value)} className={`w-full px-2 py-1.5 bg-[#0a0a0a] border rounded text-xs text-white focus:outline-none focus:border-[#ffb800] ${errors.client_id ? 'border-red-500/60' : 'border-[#252525]'}`}>
<select value={form.client_id} onChange={e => handleChange('client_id', e.target.value)} className={`w-full px-2 py-1.5 bg-[#0a0a0a] border rounded text-xs text-white focus:outline-none focus:border-[#F0A623] ${errors.client_id ? 'border-red-500/60' : 'border-[#3a322b]'}`}>
<option value="">Select client</option>
{clients.map((c: any) => <option key={c.id} value={c.id}>{c.company_name}</option>)}
</select>
@@ -162,7 +162,7 @@ function OrdersContent() {
</div>
<div>
<label className="text-xs text-[#888] block mb-1">Site <span className="text-red-400">*</span></label>
<select value={form.site} onChange={e => handleChange('site', e.target.value)} className={`w-full px-2 py-1.5 bg-[#0a0a0a] border rounded text-xs text-white focus:outline-none focus:border-[#ffb800] ${errors.site ? 'border-red-500/60' : 'border-[#252525]'}`}>
<select value={form.site} onChange={e => handleChange('site', e.target.value)} className={`w-full px-2 py-1.5 bg-[#0a0a0a] border rounded text-xs text-white focus:outline-none focus:border-[#F0A623] ${errors.site ? 'border-red-500/60' : 'border-[#3a322b]'}`}>
<option value="">Select site</option>
{SITES.map(s => <option key={s} value={s}>{s}</option>)}
</select>
@@ -170,67 +170,67 @@ function OrdersContent() {
</div>
<div>
<label className="text-xs text-[#888] block mb-1">Product</label>
<select value={form.product_id} onChange={e => handleChange('product_id', e.target.value)} className="w-full px-2 py-1.5 bg-[#0a0a0a] border border-[#252525] rounded text-xs text-white focus:outline-none focus:border-[#ffb800]">
<select value={form.product_id} onChange={e => handleChange('product_id', e.target.value)} className="w-full px-2 py-1.5 bg-[#0a0a0a] border border-[#3a322b] rounded text-xs text-white focus:outline-none focus:border-[#F0A623]">
<option value="">Select product</option>
{products.map((p: any) => <option key={p.id} value={p.id}>{p.name}</option>)}
</select>
</div>
<div>
<label className="text-xs text-[#888] block mb-1">Ad Unit</label>
<input value={form.ad_unit} onChange={e => handleChange('ad_unit', e.target.value)} className="w-full px-2 py-1.5 bg-[#0a0a0a] border border-[#252525] rounded text-xs text-white focus:outline-none focus:border-[#ffb800]" />
<input value={form.ad_unit} onChange={e => handleChange('ad_unit', e.target.value)} className="w-full px-2 py-1.5 bg-[#0a0a0a] border border-[#3a322b] rounded text-xs text-white focus:outline-none focus:border-[#F0A623]" />
</div>
<div>
<label className="text-xs text-[#888] block mb-1">Start Date</label>
<input type="date" value={form.start_date} onChange={e => handleChange('start_date', e.target.value)} className="w-full px-2 py-1.5 bg-[#0a0a0a] border border-[#252525] rounded text-xs text-white focus:outline-none focus:border-[#ffb800]" />
<input type="date" value={form.start_date} onChange={e => handleChange('start_date', e.target.value)} className="w-full px-2 py-1.5 bg-[#0a0a0a] border border-[#3a322b] rounded text-xs text-white focus:outline-none focus:border-[#F0A623]" />
</div>
<div>
<label className="text-xs text-[#888] block mb-1">End Date</label>
<input type="date" value={form.end_date} onChange={e => handleChange('end_date', e.target.value)} className={`w-full px-2 py-1.5 bg-[#0a0a0a] border rounded text-xs text-white focus:outline-none focus:border-[#ffb800] ${errors.end_date ? 'border-red-500/60' : 'border-[#252525]'}`} />
<input type="date" value={form.end_date} onChange={e => handleChange('end_date', e.target.value)} className={`w-full px-2 py-1.5 bg-[#0a0a0a] border rounded text-xs text-white focus:outline-none focus:border-[#F0A623] ${errors.end_date ? 'border-red-500/60' : 'border-[#3a322b]'}`} />
{errors.end_date && <p className="mt-1 text-xs text-red-400">{errors.end_date}</p>}
</div>
<div>
<label className="text-xs text-[#888] block mb-1">Total Amount ($)</label>
<input type="number" step="0.01" value={form.total_amount_cents} onChange={e => handleChange('total_amount_cents', e.target.value)} className={`w-full px-2 py-1.5 bg-[#0a0a0a] border rounded text-xs text-white focus:outline-none focus:border-[#ffb800] ${errors.total_amount_cents ? 'border-red-500/60' : 'border-[#252525]'}`} />
<input type="number" step="0.01" value={form.total_amount_cents} onChange={e => handleChange('total_amount_cents', e.target.value)} className={`w-full px-2 py-1.5 bg-[#0a0a0a] border rounded text-xs text-white focus:outline-none focus:border-[#F0A623] ${errors.total_amount_cents ? 'border-red-500/60' : 'border-[#3a322b]'}`} />
{errors.total_amount_cents && <p className="mt-1 text-xs text-red-400">{errors.total_amount_cents}</p>}
</div>
<div>
<label className="text-xs text-[#888] block mb-1">Invoicing Schedule</label>
<select value={form.invoicing_schedule} onChange={e => handleChange('invoicing_schedule', e.target.value)} className="w-full px-2 py-1.5 bg-[#0a0a0a] border border-[#252525] rounded text-xs text-white focus:outline-none focus:border-[#ffb800]">
<select value={form.invoicing_schedule} onChange={e => handleChange('invoicing_schedule', e.target.value)} className="w-full px-2 py-1.5 bg-[#0a0a0a] border border-[#3a322b] rounded text-xs text-white focus:outline-none focus:border-[#F0A623]">
{SCHEDULES.map(s => <option key={s} value={s}>{s}</option>)}
</select>
</div>
<div>
<label className="text-xs text-[#888] block mb-1">Next Invoice Date</label>
<input type="date" value={form.next_invoice_date} onChange={e => handleChange('next_invoice_date', e.target.value)} className="w-full px-2 py-1.5 bg-[#0a0a0a] border border-[#252525] rounded text-xs text-white focus:outline-none focus:border-[#ffb800]" />
<input type="date" value={form.next_invoice_date} onChange={e => handleChange('next_invoice_date', e.target.value)} className="w-full px-2 py-1.5 bg-[#0a0a0a] border border-[#3a322b] rounded text-xs text-white focus:outline-none focus:border-[#F0A623]" />
</div>
<div>
<label className="text-xs text-[#888] block mb-1">Salesperson</label>
<select value={form.staff_id} onChange={e => handleChange('staff_id', e.target.value)} className="w-full px-2 py-1.5 bg-[#0a0a0a] border border-[#252525] rounded text-xs text-white focus:outline-none focus:border-[#ffb800]">
<select value={form.staff_id} onChange={e => handleChange('staff_id', e.target.value)} className="w-full px-2 py-1.5 bg-[#0a0a0a] border border-[#3a322b] rounded text-xs text-white focus:outline-none focus:border-[#F0A623]">
<option value="">None</option>
{staff.map((s: any) => <option key={s.id} value={s.id}>{s.full_name}</option>)}
</select>
</div>
<div>
<label className="text-xs text-[#888] block mb-1">PO Number</label>
<input value={form.po_number} onChange={e => handleChange('po_number', e.target.value)} className="w-full px-2 py-1.5 bg-[#0a0a0a] border border-[#252525] rounded text-xs text-white focus:outline-none focus:border-[#ffb800]" />
<input value={form.po_number} onChange={e => handleChange('po_number', e.target.value)} className="w-full px-2 py-1.5 bg-[#0a0a0a] border border-[#3a322b] rounded text-xs text-white focus:outline-none focus:border-[#F0A623]" />
</div>
<div className="col-span-2 md:col-span-3">
<label className="text-xs text-[#888] block mb-1">Notes</label>
<textarea value={form.notes} onChange={e => handleChange('notes', e.target.value)} rows={2} className="w-full px-2 py-1.5 bg-[#0a0a0a] border border-[#252525] rounded text-xs text-white focus:outline-none focus:border-[#ffb800]" />
<textarea value={form.notes} onChange={e => handleChange('notes', e.target.value)} rows={2} className="w-full px-2 py-1.5 bg-[#0a0a0a] border border-[#3a322b] rounded text-xs text-white focus:outline-none focus:border-[#F0A623]" />
</div>
</div>
<div className="flex gap-2">
<button type="submit" disabled={saving} className="px-3 py-1.5 bg-[#ffb800] rounded text-xs font-medium hover:bg-blue-500 disabled:opacity-50">{saving ? 'Saving…' : 'Create Order'}</button>
<button type="button" onClick={handleCancel} className="px-3 py-1.5 bg-[#1a1a1a] border border-[#252525] rounded text-xs text-[#888] hover:text-white">Cancel</button>
<button type="submit" disabled={saving} className="px-3 py-1.5 bg-[#F0A623] rounded text-xs font-medium hover:bg-blue-500 disabled:opacity-50">{saving ? 'Saving…' : 'Create Order'}</button>
<button type="button" onClick={handleCancel} className="px-3 py-1.5 bg-[#231d18] border border-[#3a322b] rounded text-xs text-[#888] hover:text-white">Cancel</button>
</div>
</form>
)}
{/* Table */}
<div className="bg-[#111] border border-[#252525] rounded-lg overflow-x-auto">
<div className="bg-[#111] border border-[#3a322b] rounded-lg overflow-x-auto">
<table className="w-full text-sm">
<thead>
<tr className="border-b border-[#1a1a1a]">
<tr className="border-b border-[#231d18]">
{['Client','PO#','Order#','Site','Product','Ad Unit','Dates','Amount','Status',''].map(h => (
<th key={h} className="px-3 py-2 text-left text-xs text-[#555] font-medium whitespace-nowrap">{h}</th>
))}
@@ -242,20 +242,20 @@ function OrdersContent() {
) : orders.length === 0 ? (
<tr><td colSpan={10} className="px-4 py-8 text-center text-[#555] text-xs">No orders found</td></tr>
) : orders.map((o: any) => (
<tr key={o.id} className="border-b border-[#1a1a1a] hover:bg-[#161616]">
<tr key={o.id} className="border-b border-[#231d18] hover:bg-[#231d18]">
<td className="px-3 py-2 text-white text-xs">{o.rmp_clients?.company_name ?? '—'}</td>
<td className="px-3 py-2 text-[#888] text-xs">{o.po_number ?? '—'}</td>
<td className="px-3 py-2 text-[#ffb800] text-xs font-mono">{o.internal_order_number}</td>
<td className="px-3 py-2 text-[#F0A623] text-xs font-mono">{o.internal_order_number}</td>
<td className="px-3 py-2 text-[#888] text-xs capitalize">{o.site}</td>
<td className="px-3 py-2 text-[#ccc] text-xs">{o.rmp_products?.name ?? o.ad_unit ?? '—'}</td>
<td className="px-3 py-2 text-[#888] text-xs">{o.ad_unit ?? '—'}</td>
<td className="px-3 py-2 text-[#888] text-xs whitespace-nowrap">{o.start_date} {o.end_date ?? '∞'}</td>
<td className="px-3 py-2 text-green-400 text-xs">{o.total_amount_cents ? fmt(o.total_amount_cents) : '—'}</td>
<td className="px-3 py-2">
<span className={`px-2 py-0.5 rounded-full text-xs ${o.status === 'active' ? 'bg-green-400/10 text-green-400' : o.status === 'cancelled' ? 'bg-red-400/10 text-red-400' : 'bg-[#1a1a1a] text-[#555]'}`}>{o.status}</span>
<span className={`px-2 py-0.5 rounded-full text-xs ${o.status === 'active' ? 'bg-green-400/10 text-green-400' : o.status === 'cancelled' ? 'bg-red-400/10 text-red-400' : 'bg-[#231d18] text-[#555]'}`}>{o.status}</span>
</td>
<td className="px-3 py-2">
<Link href={`/admin/accounting/orders/${o.id}`} className="text-xs text-[#ffb800] hover:underline">View</Link>
<Link href={`/admin/accounting/orders/${o.id}`} className="text-xs text-[#F0A623] hover:underline">View</Link>
</td>
</tr>
))}
@@ -269,7 +269,7 @@ function OrdersContent() {
export default function OrdersPage() {
return (
<Suspense fallback={<div className="min-h-screen bg-[#0a0a0a] flex items-center justify-center"><div className="w-8 h-8 border-2 border-[#ffb800] border-t-transparent rounded-full animate-spin" /></div>}>
<Suspense fallback={<div className="min-h-screen bg-[#0a0a0a] flex items-center justify-center"><div className="w-8 h-8 border-2 border-[#F0A623] border-t-transparent rounded-full animate-spin" /></div>}>
<OrdersContent />
</Suspense>
);

View File

@@ -44,7 +44,7 @@ export default function AccountingDashboard() {
if (loading || loadingData) return (
<div className="min-h-screen bg-[#0a0a0a] flex items-center justify-center">
<div className="w-8 h-8 border-2 border-[#ffb800] border-t-transparent rounded-full animate-spin" />
<div className="w-8 h-8 border-2 border-[#F0A623] border-t-transparent rounded-full animate-spin" />
</div>
);
if (!user) return null;
@@ -59,18 +59,18 @@ export default function AccountingDashboard() {
<p className="text-[#888] text-sm mt-1">Relevant Media Properties, LLC</p>
</div>
<div className="flex gap-2 flex-wrap">
<Link href="/admin/accounting/clients" className="px-3 py-1.5 bg-[#1a1208] border border-[#2a3a50] rounded text-xs text-[#ffb800] hover:bg-[#0d1a2d] transition-colors">+ New Client</Link>
<Link href="/admin/accounting/orders" className="px-3 py-1.5 bg-[#1a1208] border border-[#2a3a50] rounded text-xs text-[#ffb800] hover:bg-[#0d1a2d] transition-colors">+ New Order</Link>
<Link href="/admin/accounting/invoices" className="px-3 py-1.5 bg-[#1a1208] border border-[#2a3a50] rounded text-xs text-[#ffb800] hover:bg-[#0d1a2d] transition-colors">+ New Invoice</Link>
<Link href="/admin/accounting/expenses" className="px-3 py-1.5 bg-[#1a1208] border border-[#2a3a50] rounded text-xs text-[#ffb800] hover:bg-[#0d1a2d] transition-colors">+ Log Expense</Link>
<Link href="/admin/accounting/reconciliation" className="px-3 py-1.5 bg-[#1a1208] border border-[#2a3a50] rounded text-xs text-[#ffb800] hover:bg-[#0d1a2d] transition-colors">Upload Bank CSV</Link>
<Link href="/admin/accounting/clients" className="px-3 py-1.5 bg-[#231d18] border border-[#2a3a50] rounded text-xs text-[#F0A623] hover:bg-[#0d1a2d] transition-colors">+ New Client</Link>
<Link href="/admin/accounting/orders" className="px-3 py-1.5 bg-[#231d18] border border-[#2a3a50] rounded text-xs text-[#F0A623] hover:bg-[#0d1a2d] transition-colors">+ New Order</Link>
<Link href="/admin/accounting/invoices" className="px-3 py-1.5 bg-[#231d18] border border-[#2a3a50] rounded text-xs text-[#F0A623] hover:bg-[#0d1a2d] transition-colors">+ New Invoice</Link>
<Link href="/admin/accounting/expenses" className="px-3 py-1.5 bg-[#231d18] border border-[#2a3a50] rounded text-xs text-[#F0A623] hover:bg-[#0d1a2d] transition-colors">+ Log Expense</Link>
<Link href="/admin/accounting/reconciliation" className="px-3 py-1.5 bg-[#231d18] border border-[#2a3a50] rounded text-xs text-[#F0A623] hover:bg-[#0d1a2d] transition-colors">Upload Bank CSV</Link>
</div>
</div>
{/* Stat Cards */}
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-6 gap-3">
{STAT_CARDS.map(c => (
<Link key={c.key} href={c.href} className="bg-[#111] border border-[#252525] rounded-lg p-4 hover:border-[#333] transition-colors">
<Link key={c.key} href={c.href} className="bg-[#111] border border-[#3a322b] rounded-lg p-4 hover:border-[#333] transition-colors">
<p className={`text-lg font-bold ${c.color}`}>{fmt(stats[c.key] ?? 0)}</p>
<p className="text-xs text-[#888] mt-1">{c.label}</p>
</Link>
@@ -78,13 +78,13 @@ export default function AccountingDashboard() {
</div>
{/* Site Revenue Table */}
<div className="bg-[#111] border border-[#252525] rounded-lg overflow-hidden">
<div className="px-4 py-3 border-b border-[#252525]">
<div className="bg-[#111] border border-[#3a322b] rounded-lg overflow-hidden">
<div className="px-4 py-3 border-b border-[#3a322b]">
<h2 className="text-sm font-semibold text-white">Revenue by Site</h2>
</div>
<table className="w-full text-sm">
<thead>
<tr className="border-b border-[#1a1a1a]">
<tr className="border-b border-[#231d18]">
{['Site','Revenue MTD','Revenue YTD','Outstanding','Active Orders'].map(h => (
<th key={h} className="px-4 py-2 text-left text-xs text-[#555] font-medium">{h}</th>
))}
@@ -94,7 +94,7 @@ export default function AccountingDashboard() {
{siteRevenue.length === 0 ? (
<tr><td colSpan={5} className="px-4 py-6 text-center text-[#555] text-xs">No data yet</td></tr>
) : siteRevenue.map((row: any) => (
<tr key={row.site} className="border-b border-[#1a1a1a] hover:bg-[#161616]">
<tr key={row.site} className="border-b border-[#231d18] hover:bg-[#231d18]">
<td className="px-4 py-2 text-white font-medium capitalize">{row.site}</td>
<td className="px-4 py-2 text-green-400">{fmt(row.revenue_mtd ?? 0)}</td>
<td className="px-4 py-2 text-green-400">{fmt(row.revenue_ytd ?? 0)}</td>
@@ -107,13 +107,13 @@ export default function AccountingDashboard() {
</div>
{/* Top Clients */}
<div className="bg-[#111] border border-[#252525] rounded-lg overflow-hidden">
<div className="px-4 py-3 border-b border-[#252525]">
<div className="bg-[#111] border border-[#3a322b] rounded-lg overflow-hidden">
<div className="px-4 py-3 border-b border-[#3a322b]">
<h2 className="text-sm font-semibold text-white">Top 5 Clients YTD</h2>
</div>
<table className="w-full text-sm">
<thead>
<tr className="border-b border-[#1a1a1a]">
<tr className="border-b border-[#231d18]">
{['Client','Revenue YTD','Active Orders'].map(h => (
<th key={h} className="px-4 py-2 text-left text-xs text-[#555] font-medium">{h}</th>
))}
@@ -123,7 +123,7 @@ export default function AccountingDashboard() {
{topClients.length === 0 ? (
<tr><td colSpan={3} className="px-4 py-6 text-center text-[#555] text-xs">No data yet</td></tr>
) : topClients.map((c: any) => (
<tr key={c.id} className="border-b border-[#1a1a1a] hover:bg-[#161616]">
<tr key={c.id} className="border-b border-[#231d18] hover:bg-[#231d18]">
<td className="px-4 py-2 text-white">{c.company_name}</td>
<td className="px-4 py-2 text-green-400">{fmt(c.revenue_ytd ?? 0)}</td>
<td className="px-4 py-2 text-[#ccc]">{c.active_orders ?? 0}</td>
@@ -146,7 +146,7 @@ export default function AccountingDashboard() {
{ label: 'Reports', href: '/admin/accounting/reports' },
{ label: 'Documents', href: '/admin/accounting/documents' },
].map(l => (
<Link key={l.href} href={l.href} className="bg-[#111] border border-[#252525] rounded-lg p-3 text-center text-sm text-[#888] hover:text-white hover:border-[#ffb800] transition-colors">
<Link key={l.href} href={l.href} className="bg-[#111] border border-[#3a322b] rounded-lg p-3 text-center text-sm text-[#888] hover:text-white hover:border-[#F0A623] transition-colors">
{l.label}
</Link>
))}

View File

@@ -48,7 +48,7 @@ export default function ReconciliationPage() {
load();
}
if (loading) return <div className="min-h-screen bg-[#0a0a0a] flex items-center justify-center"><div className="w-8 h-8 border-2 border-[#ffb800] border-t-transparent rounded-full animate-spin" /></div>;
if (loading) return <div className="min-h-screen bg-[#0a0a0a] flex items-center justify-center"><div className="w-8 h-8 border-2 border-[#F0A623] border-t-transparent rounded-full animate-spin" /></div>;
if (!user) return null;
return (
@@ -59,7 +59,7 @@ export default function ReconciliationPage() {
<h1 className="text-xl font-bold">Bank Reconciliation</h1>
<Link href="/admin/accounting" className="text-xs text-[#555] hover:text-[#888]"> Accounting</Link>
</div>
<label className={`px-3 py-1.5 bg-[#ffb800] rounded text-xs font-medium hover:bg-blue-500 cursor-pointer ${uploading ? 'opacity-50' : ''}`}>
<label className={`px-3 py-1.5 bg-[#F0A623] rounded text-xs font-medium hover:bg-blue-500 cursor-pointer ${uploading ? 'opacity-50' : ''}`}>
{uploading ? 'Uploading…' : 'Upload Bank CSV'}
<input type="file" accept=".csv" className="hidden" onChange={handleUpload} disabled={uploading} />
</label>
@@ -78,10 +78,10 @@ export default function ReconciliationPage() {
</div>
)}
<div className="bg-[#111] border border-[#252525] rounded-lg overflow-x-auto">
<div className="bg-[#111] border border-[#3a322b] rounded-lg overflow-x-auto">
<table className="w-full text-sm">
<thead>
<tr className="border-b border-[#1a1a1a]">
<tr className="border-b border-[#231d18]">
{['Date','Description','Amount','Type','Matched To','Reconciled','Action'].map(h => (
<th key={h} className="px-3 py-2 text-left text-xs text-[#555] font-medium">{h}</th>
))}
@@ -93,7 +93,7 @@ export default function ReconciliationPage() {
) : transactions.length === 0 ? (
<tr><td colSpan={7} className="px-4 py-8 text-center text-[#555] text-xs">No transactions. Upload a CSV to begin.</td></tr>
) : transactions.map((tx: any) => (
<tr key={tx.id} className="border-b border-[#1a1a1a] hover:bg-[#161616]">
<tr key={tx.id} className="border-b border-[#231d18] hover:bg-[#231d18]">
<td className="px-3 py-2 text-[#888] text-xs">{tx.transaction_date}</td>
<td className="px-3 py-2 text-white text-xs max-w-xs truncate">{tx.description}</td>
<td className={`px-3 py-2 text-xs font-medium ${tx.type === 'credit' ? 'text-green-400' : 'text-red-400'}`}>{fmt(Math.abs(tx.amount_cents ?? 0))}</td>

View File

@@ -28,7 +28,7 @@ export default function ReportsPage() {
useEffect(() => { if (user) loadReport(); }, [user]);
if (loading) return <div className="min-h-screen bg-[#0a0a0a] flex items-center justify-center"><div className="w-8 h-8 border-2 border-[#ffb800] border-t-transparent rounded-full animate-spin" /></div>;
if (loading) return <div className="min-h-screen bg-[#0a0a0a] flex items-center justify-center"><div className="w-8 h-8 border-2 border-[#F0A623] border-t-transparent rounded-full animate-spin" /></div>;
if (!user) return null;
return (
@@ -40,41 +40,41 @@ export default function ReportsPage() {
<Link href="/admin/accounting" className="text-xs text-[#555] hover:text-[#888]"> Accounting</Link>
</div>
<div className="flex gap-2 items-center">
<input type="date" value={startDate} onChange={e => setStartDate(e.target.value)} className="px-2 py-1 bg-[#111] border border-[#252525] rounded text-xs text-white focus:outline-none" />
<input type="date" value={startDate} onChange={e => setStartDate(e.target.value)} className="px-2 py-1 bg-[#111] border border-[#3a322b] rounded text-xs text-white focus:outline-none" />
<span className="text-[#555] text-xs">to</span>
<input type="date" value={endDate} onChange={e => setEndDate(e.target.value)} className="px-2 py-1 bg-[#111] border border-[#252525] rounded text-xs text-white focus:outline-none" />
<button onClick={loadReport} disabled={loadingData} className="px-3 py-1.5 bg-[#ffb800] rounded text-xs font-medium hover:bg-blue-500 disabled:opacity-50">Run Report</button>
<input type="date" value={endDate} onChange={e => setEndDate(e.target.value)} className="px-2 py-1 bg-[#111] border border-[#3a322b] rounded text-xs text-white focus:outline-none" />
<button onClick={loadReport} disabled={loadingData} className="px-3 py-1.5 bg-[#F0A623] rounded text-xs font-medium hover:bg-blue-500 disabled:opacity-50">Run Report</button>
</div>
</div>
{loadingData ? (
<div className="flex items-center justify-center py-20"><div className="w-8 h-8 border-2 border-[#ffb800] border-t-transparent rounded-full animate-spin" /></div>
<div className="flex items-center justify-center py-20"><div className="w-8 h-8 border-2 border-[#F0A623] border-t-transparent rounded-full animate-spin" /></div>
) : report ? (
<div className="space-y-4">
{/* P&L Summary */}
<div className="grid grid-cols-3 gap-3">
<div className="bg-[#111] border border-[#252525] rounded-lg p-4">
<div className="bg-[#111] border border-[#3a322b] rounded-lg p-4">
<p className="text-xs text-[#555]">Total Revenue</p>
<p className="text-2xl font-bold text-green-400 mt-1">{fmt(report.total_revenue_cents ?? 0)}</p>
</div>
<div className="bg-[#111] border border-[#252525] rounded-lg p-4">
<div className="bg-[#111] border border-[#3a322b] rounded-lg p-4">
<p className="text-xs text-[#555]">Total Expenses</p>
<p className="text-2xl font-bold text-red-400 mt-1">{fmt(report.total_expenses_cents ?? 0)}</p>
</div>
<div className="bg-[#111] border border-[#252525] rounded-lg p-4">
<div className="bg-[#111] border border-[#3a322b] rounded-lg p-4">
<p className="text-xs text-[#555]">Net Profit</p>
<p className={`text-2xl font-bold mt-1 ${(report.net_profit_cents ?? 0) >= 0 ? 'text-blue-400' : 'text-red-400'}`}>{fmt(report.net_profit_cents ?? 0)}</p>
</div>
</div>
{/* Revenue by Site */}
<div className="bg-[#111] border border-[#252525] rounded-lg overflow-hidden">
<div className="px-4 py-3 border-b border-[#252525]"><h2 className="text-sm font-semibold">Revenue by Site</h2></div>
<div className="bg-[#111] border border-[#3a322b] rounded-lg overflow-hidden">
<div className="px-4 py-3 border-b border-[#3a322b]"><h2 className="text-sm font-semibold">Revenue by Site</h2></div>
<table className="w-full text-sm">
<thead><tr className="border-b border-[#1a1a1a]">{['Site','Revenue'].map(h => <th key={h} className="px-4 py-2 text-left text-xs text-[#555]">{h}</th>)}</tr></thead>
<thead><tr className="border-b border-[#231d18]">{['Site','Revenue'].map(h => <th key={h} className="px-4 py-2 text-left text-xs text-[#555]">{h}</th>)}</tr></thead>
<tbody>
{(report.by_site ?? []).map((r: any) => (
<tr key={r.site} className="border-b border-[#1a1a1a]">
<tr key={r.site} className="border-b border-[#231d18]">
<td className="px-4 py-2 text-white text-xs capitalize">{r.site}</td>
<td className="px-4 py-2 text-green-400 text-xs">{fmt(r.revenue_cents ?? 0)}</td>
</tr>
@@ -84,13 +84,13 @@ export default function ReportsPage() {
</div>
{/* Top Clients */}
<div className="bg-[#111] border border-[#252525] rounded-lg overflow-hidden">
<div className="px-4 py-3 border-b border-[#252525]"><h2 className="text-sm font-semibold">Top 10 Clients</h2></div>
<div className="bg-[#111] border border-[#3a322b] rounded-lg overflow-hidden">
<div className="px-4 py-3 border-b border-[#3a322b]"><h2 className="text-sm font-semibold">Top 10 Clients</h2></div>
<table className="w-full text-sm">
<thead><tr className="border-b border-[#1a1a1a]">{['Client','Revenue'].map(h => <th key={h} className="px-4 py-2 text-left text-xs text-[#555]">{h}</th>)}</tr></thead>
<thead><tr className="border-b border-[#231d18]">{['Client','Revenue'].map(h => <th key={h} className="px-4 py-2 text-left text-xs text-[#555]">{h}</th>)}</tr></thead>
<tbody>
{(report.top_clients ?? []).map((c: any) => (
<tr key={c.id} className="border-b border-[#1a1a1a]">
<tr key={c.id} className="border-b border-[#231d18]">
<td className="px-4 py-2 text-white text-xs">{c.company_name}</td>
<td className="px-4 py-2 text-green-400 text-xs">{fmt(c.revenue_cents ?? 0)}</td>
</tr>
@@ -100,13 +100,13 @@ export default function ReportsPage() {
</div>
{/* Expenses by Category */}
<div className="bg-[#111] border border-[#252525] rounded-lg overflow-hidden">
<div className="px-4 py-3 border-b border-[#252525]"><h2 className="text-sm font-semibold">Expenses by Category</h2></div>
<div className="bg-[#111] border border-[#3a322b] rounded-lg overflow-hidden">
<div className="px-4 py-3 border-b border-[#3a322b]"><h2 className="text-sm font-semibold">Expenses by Category</h2></div>
<table className="w-full text-sm">
<thead><tr className="border-b border-[#1a1a1a]">{['Category','Amount'].map(h => <th key={h} className="px-4 py-2 text-left text-xs text-[#555]">{h}</th>)}</tr></thead>
<thead><tr className="border-b border-[#231d18]">{['Category','Amount'].map(h => <th key={h} className="px-4 py-2 text-left text-xs text-[#555]">{h}</th>)}</tr></thead>
<tbody>
{(report.by_category ?? []).map((c: any) => (
<tr key={c.category} className="border-b border-[#1a1a1a]">
<tr key={c.category} className="border-b border-[#231d18]">
<td className="px-4 py-2 text-white text-xs">{c.category?.replace(/_/g, ' ')}</td>
<td className="px-4 py-2 text-red-400 text-xs">{fmt(c.amount_cents ?? 0)}</td>
</tr>

View File

@@ -97,7 +97,7 @@ export default function StaffPage() {
if (errors[errKey]) setErrors(p => { const n = { ...p }; delete n[errKey]; return n; });
}
if (loading) return <div className="min-h-screen bg-[#0a0a0a] flex items-center justify-center"><div className="w-8 h-8 border-2 border-[#ffb800] border-t-transparent rounded-full animate-spin" /></div>;
if (loading) return <div className="min-h-screen bg-[#0a0a0a] flex items-center justify-center"><div className="w-8 h-8 border-2 border-[#F0A623] border-t-transparent rounded-full animate-spin" /></div>;
if (!user) return null;
return (
@@ -108,7 +108,7 @@ export default function StaffPage() {
<h1 className="text-xl font-bold">Sales Staff</h1>
<Link href="/admin/accounting" className="text-xs text-[#555] hover:text-[#888]"> Accounting</Link>
</div>
<button onClick={() => setShowForm(true)} className="px-3 py-1.5 bg-[#ffb800] rounded text-xs font-medium hover:bg-blue-500">+ Add Salesperson</button>
<button onClick={() => setShowForm(true)} className="px-3 py-1.5 bg-[#F0A623] rounded text-xs font-medium hover:bg-blue-500">+ Add Salesperson</button>
</div>
{/* Success Toast */}
@@ -120,7 +120,7 @@ export default function StaffPage() {
)}
{showForm && (
<form onSubmit={handleCreate} noValidate className="bg-[#111] border border-[#252525] rounded-lg p-4 space-y-3">
<form onSubmit={handleCreate} noValidate className="bg-[#111] border border-[#3a322b] rounded-lg p-4 space-y-3">
<h3 className="text-sm font-semibold">New Salesperson</h3>
{errors._form && (
@@ -140,7 +140,7 @@ export default function StaffPage() {
type={f.key === 'email' ? 'email' : 'text'}
value={(form as any)[f.key]}
onChange={e => handleChange(f.key, e.target.value)}
className={`w-full px-2 py-1.5 bg-[#0a0a0a] border rounded text-xs text-white focus:outline-none focus:border-[#ffb800] ${errors[f.key] ? 'border-red-500/60' : 'border-[#252525]'}`}
className={`w-full px-2 py-1.5 bg-[#0a0a0a] border rounded text-xs text-white focus:outline-none focus:border-[#F0A623] ${errors[f.key] ? 'border-red-500/60' : 'border-[#3a322b]'}`}
/>
{errors[f.key] && <p className="mt-1 text-xs text-red-400">{errors[f.key]}</p>}
</div>
@@ -149,34 +149,34 @@ export default function StaffPage() {
<div>
<div className="flex items-center justify-between mb-2">
<label className="text-xs text-[#888]">Commission Tiers ({new Date().getFullYear()})</label>
<button type="button" onClick={() => setTiers(t => [...t, { tier_order: t.length + 1, threshold_cents: '', rate: '' }])} className="text-xs text-[#ffb800] hover:underline">+ Add Tier</button>
<button type="button" onClick={() => setTiers(t => [...t, { tier_order: t.length + 1, threshold_cents: '', rate: '' }])} className="text-xs text-[#F0A623] hover:underline">+ Add Tier</button>
</div>
{tiers.map((tier, i) => (
<div key={i} className="flex gap-2 mb-2 items-start">
<span className="text-xs text-[#555] w-12 pt-5">Tier {tier.tier_order}</span>
<div className="flex-1">
<label className="text-xs text-[#555] block mb-0.5">Threshold ($, blank = no cap)</label>
<input type="number" step="0.01" value={tier.threshold_cents} onChange={e => handleTierChange(i, 'threshold_cents', e.target.value)} placeholder="100000" className="w-full px-2 py-1 bg-[#0a0a0a] border border-[#252525] rounded text-xs text-white focus:outline-none" />
<input type="number" step="0.01" value={tier.threshold_cents} onChange={e => handleTierChange(i, 'threshold_cents', e.target.value)} placeholder="100000" className="w-full px-2 py-1 bg-[#0a0a0a] border border-[#3a322b] rounded text-xs text-white focus:outline-none" />
</div>
<div className="flex-1">
<label className="text-xs text-[#555] block mb-0.5">Rate (%) <span className="text-red-400">*</span></label>
<input type="number" step="0.1" value={tier.rate} onChange={e => handleTierChange(i, 'rate', e.target.value)} placeholder="25" className={`w-full px-2 py-1 bg-[#0a0a0a] border rounded text-xs text-white focus:outline-none ${errors[`tier_rate_${i}`] ? 'border-red-500/60' : 'border-[#252525]'}`} />
<input type="number" step="0.1" value={tier.rate} onChange={e => handleTierChange(i, 'rate', e.target.value)} placeholder="25" className={`w-full px-2 py-1 bg-[#0a0a0a] border rounded text-xs text-white focus:outline-none ${errors[`tier_rate_${i}`] ? 'border-red-500/60' : 'border-[#3a322b]'}`} />
{errors[`tier_rate_${i}`] && <p className="mt-1 text-xs text-red-400">{errors[`tier_rate_${i}`]}</p>}
</div>
</div>
))}
</div>
<div className="flex gap-2">
<button type="submit" disabled={saving} className="px-3 py-1.5 bg-[#ffb800] rounded text-xs font-medium hover:bg-blue-500 disabled:opacity-50">{saving ? 'Saving…' : 'Create'}</button>
<button type="button" onClick={handleCancel} className="px-3 py-1.5 bg-[#1a1a1a] border border-[#252525] rounded text-xs text-[#888] hover:text-white">Cancel</button>
<button type="submit" disabled={saving} className="px-3 py-1.5 bg-[#F0A623] rounded text-xs font-medium hover:bg-blue-500 disabled:opacity-50">{saving ? 'Saving…' : 'Create'}</button>
<button type="button" onClick={handleCancel} className="px-3 py-1.5 bg-[#231d18] border border-[#3a322b] rounded text-xs text-[#888] hover:text-white">Cancel</button>
</div>
</form>
)}
<div className="bg-[#111] border border-[#252525] rounded-lg overflow-hidden">
<div className="bg-[#111] border border-[#3a322b] rounded-lg overflow-hidden">
<table className="w-full text-sm">
<thead>
<tr className="border-b border-[#1a1a1a]">
<tr className="border-b border-[#231d18]">
{['Name','Email','Status','Commission YTD','Outstanding',''].map(h => (
<th key={h} className="px-4 py-2 text-left text-xs text-[#555] font-medium">{h}</th>
))}
@@ -188,16 +188,16 @@ export default function StaffPage() {
) : staff.length === 0 ? (
<tr><td colSpan={6} className="px-4 py-8 text-center text-[#555] text-xs">No staff found</td></tr>
) : staff.map((s: any) => (
<tr key={s.id} className={`border-b border-[#1a1a1a] hover:bg-[#161616] ${s.status === 'terminated' ? 'opacity-50' : ''}`}>
<tr key={s.id} className={`border-b border-[#231d18] hover:bg-[#231d18] ${s.status === 'terminated' ? 'opacity-50' : ''}`}>
<td className="px-4 py-2 text-white">{s.full_name}</td>
<td className="px-4 py-2 text-[#888] text-xs">{s.email}</td>
<td className="px-4 py-2">
<span className={`px-2 py-0.5 rounded-full text-xs ${s.status === 'active' ? 'bg-green-400/10 text-green-400' : s.status === 'terminated' ? 'bg-red-400/10 text-red-400' : 'bg-[#1a1a1a] text-[#555]'}`}>{s.status}</span>
<span className={`px-2 py-0.5 rounded-full text-xs ${s.status === 'active' ? 'bg-green-400/10 text-green-400' : s.status === 'terminated' ? 'bg-red-400/10 text-red-400' : 'bg-[#231d18] text-[#555]'}`}>{s.status}</span>
</td>
<td className="px-4 py-2 text-green-400 text-xs">{fmt(s.commission_ytd ?? 0)}</td>
<td className="px-4 py-2 text-yellow-400 text-xs">{fmt(s.commission_outstanding ?? 0)}</td>
<td className="px-4 py-2">
<Link href={`/admin/accounting/staff/${s.id}`} className="text-xs text-[#ffb800] hover:underline">View</Link>
<Link href={`/admin/accounting/staff/${s.id}`} className="text-xs text-[#F0A623] hover:underline">View</Link>
</td>
</tr>
))}

View File

@@ -9,7 +9,7 @@ const FLIGHT_COLORS: Record<string, string> = {
scheduled: 'bg-blue-400/10 text-blue-400',
paused: 'bg-gray-600/20 text-gray-400',
expired: 'bg-red-400/10 text-red-400',
cancelled: 'bg-[#1a1a1a] text-[#555]',
cancelled: 'bg-[#231d18] text-[#555]',
};
export default function AdOpsIOsPage() {
@@ -73,7 +73,7 @@ export default function AdOpsIOsPage() {
load();
}
if (loading) return <div className="min-h-screen bg-[#0a0a0a] flex items-center justify-center"><div className="w-8 h-8 border-2 border-[#ffb800] border-t-transparent rounded-full animate-spin" /></div>;
if (loading) return <div className="min-h-screen bg-[#0a0a0a] flex items-center justify-center"><div className="w-8 h-8 border-2 border-[#F0A623] border-t-transparent rounded-full animate-spin" /></div>;
if (!user) return null;
const selectedProduct = products.find((p: any) => p.id === form.product_id);
@@ -88,63 +88,63 @@ export default function AdOpsIOsPage() {
<h1 className="text-xl font-bold">Insertion Orders</h1>
<Link href="/admin/adops" className="text-xs text-[#555] hover:text-[#888]"> Ad Operations</Link>
</div>
<button onClick={() => setShowForm(true)} className="px-3 py-1.5 bg-[#ffb800] rounded text-xs font-medium hover:bg-blue-500">+ New IO Extension</button>
<button onClick={() => setShowForm(true)} className="px-3 py-1.5 bg-[#F0A623] rounded text-xs font-medium hover:bg-blue-500">+ New IO Extension</button>
</div>
<div className="flex gap-2 flex-wrap">
<select value={filterStatus} onChange={e => setFilterStatus(e.target.value)} className="px-2 py-1 bg-[#111] border border-[#252525] rounded text-xs text-[#888] focus:outline-none">
<select value={filterStatus} onChange={e => setFilterStatus(e.target.value)} className="px-2 py-1 bg-[#111] border border-[#3a322b] rounded text-xs text-[#888] focus:outline-none">
<option value="">All Statuses</option>
{['scheduled','active','paused','expired','cancelled'].map(s => <option key={s} value={s}>{s}</option>)}
</select>
<select value={filterSite} onChange={e => setFilterSite(e.target.value)} className="px-2 py-1 bg-[#111] border border-[#252525] rounded text-xs text-[#888] focus:outline-none">
<select value={filterSite} onChange={e => setFilterSite(e.target.value)} className="px-2 py-1 bg-[#111] border border-[#3a322b] rounded text-xs text-[#888] focus:outline-none">
<option value="">All Sites</option>
{['avbeat','avbeat','backlotbeat','broadcastengineering'].map(s => <option key={s} value={s}>{s}</option>)}
</select>
</div>
{showForm && (
<form onSubmit={handleCreate} className="bg-[#111] border border-[#252525] rounded-lg p-4 space-y-3">
<form onSubmit={handleCreate} className="bg-[#111] border border-[#3a322b] rounded-lg p-4 space-y-3">
<h3 className="text-sm font-semibold">New IO Extension</h3>
<div className="grid grid-cols-2 md:grid-cols-3 gap-3">
<div>
<label className="text-xs text-[#888] block mb-1">Existing IO ID *</label>
<input required value={form.existing_io_id} onChange={e => setForm(p => ({ ...p, existing_io_id: e.target.value }))} className="w-full px-2 py-1.5 bg-[#0a0a0a] border border-[#252525] rounded text-xs text-white focus:outline-none focus:border-[#ffb800]" />
<input required value={form.existing_io_id} onChange={e => setForm(p => ({ ...p, existing_io_id: e.target.value }))} className="w-full px-2 py-1.5 bg-[#0a0a0a] border border-[#3a322b] rounded text-xs text-white focus:outline-none focus:border-[#F0A623]" />
</div>
<div>
<label className="text-xs text-[#888] block mb-1">Client</label>
<select value={form.client_id} onChange={e => setForm(p => ({ ...p, client_id: e.target.value }))} className="w-full px-2 py-1.5 bg-[#0a0a0a] border border-[#252525] rounded text-xs text-white focus:outline-none">
<select value={form.client_id} onChange={e => setForm(p => ({ ...p, client_id: e.target.value }))} className="w-full px-2 py-1.5 bg-[#0a0a0a] border border-[#3a322b] rounded text-xs text-white focus:outline-none">
<option value="">Select</option>
{clients.map((c: any) => <option key={c.id} value={c.id}>{c.company_name}</option>)}
</select>
</div>
<div>
<label className="text-xs text-[#888] block mb-1">Salesperson</label>
<select value={form.staff_id} onChange={e => setForm(p => ({ ...p, staff_id: e.target.value }))} className="w-full px-2 py-1.5 bg-[#0a0a0a] border border-[#252525] rounded text-xs text-white focus:outline-none">
<select value={form.staff_id} onChange={e => setForm(p => ({ ...p, staff_id: e.target.value }))} className="w-full px-2 py-1.5 bg-[#0a0a0a] border border-[#3a322b] rounded text-xs text-white focus:outline-none">
<option value="">None</option>
{staff.map((s: any) => <option key={s.id} value={s.id}>{s.full_name}</option>)}
</select>
</div>
<div>
<label className="text-xs text-[#888] block mb-1">Start Date *</label>
<input type="date" required value={form.start_date} onChange={e => setForm(p => ({ ...p, start_date: e.target.value }))} className="w-full px-2 py-1.5 bg-[#0a0a0a] border border-[#252525] rounded text-xs text-white focus:outline-none" />
<input type="date" required value={form.start_date} onChange={e => setForm(p => ({ ...p, start_date: e.target.value }))} className="w-full px-2 py-1.5 bg-[#0a0a0a] border border-[#3a322b] rounded text-xs text-white focus:outline-none" />
</div>
<div>
<label className="text-xs text-[#888] block mb-1">End Date</label>
<input type="date" value={form.end_date} onChange={e => setForm(p => ({ ...p, end_date: e.target.value }))} className="w-full px-2 py-1.5 bg-[#0a0a0a] border border-[#252525] rounded text-xs text-white focus:outline-none" />
<input type="date" value={form.end_date} onChange={e => setForm(p => ({ ...p, end_date: e.target.value }))} className="w-full px-2 py-1.5 bg-[#0a0a0a] border border-[#3a322b] rounded text-xs text-white focus:outline-none" />
</div>
<div>
<label className="text-xs text-[#888] block mb-1">Flight Status</label>
<select value={form.flight_status} onChange={e => setForm(p => ({ ...p, flight_status: e.target.value }))} className="w-full px-2 py-1.5 bg-[#0a0a0a] border border-[#252525] rounded text-xs text-white focus:outline-none">
<select value={form.flight_status} onChange={e => setForm(p => ({ ...p, flight_status: e.target.value }))} className="w-full px-2 py-1.5 bg-[#0a0a0a] border border-[#3a322b] rounded text-xs text-white focus:outline-none">
{['scheduled','active','paused'].map(s => <option key={s} value={s}>{s}</option>)}
</select>
</div>
<div>
<label className="text-xs text-[#888] block mb-1">Banner Image URL</label>
<input value={form.banner_image_url} onChange={e => setForm(p => ({ ...p, banner_image_url: e.target.value }))} className="w-full px-2 py-1.5 bg-[#0a0a0a] border border-[#252525] rounded text-xs text-white focus:outline-none focus:border-[#ffb800]" />
<input value={form.banner_image_url} onChange={e => setForm(p => ({ ...p, banner_image_url: e.target.value }))} className="w-full px-2 py-1.5 bg-[#0a0a0a] border border-[#3a322b] rounded text-xs text-white focus:outline-none focus:border-[#F0A623]" />
</div>
<div>
<label className="text-xs text-[#888] block mb-1">Click-Through URL</label>
<input value={form.click_through_url} onChange={e => setForm(p => ({ ...p, click_through_url: e.target.value }))} className="w-full px-2 py-1.5 bg-[#0a0a0a] border border-[#252525] rounded text-xs text-white focus:outline-none focus:border-[#ffb800]" />
<input value={form.click_through_url} onChange={e => setForm(p => ({ ...p, click_through_url: e.target.value }))} className="w-full px-2 py-1.5 bg-[#0a0a0a] border border-[#3a322b] rounded text-xs text-white focus:outline-none focus:border-[#F0A623]" />
</div>
<div className="flex items-center gap-3 pt-4">
<label className="flex items-center gap-2 text-xs text-[#888] cursor-pointer">
@@ -159,47 +159,47 @@ export default function AdOpsIOsPage() {
{form.is_comp && (
<div>
<label className="text-xs text-[#888] block mb-1">Comp Reason</label>
<input value={form.comp_reason} onChange={e => setForm(p => ({ ...p, comp_reason: e.target.value }))} className="w-full px-2 py-1.5 bg-[#0a0a0a] border border-[#252525] rounded text-xs text-white focus:outline-none focus:border-[#ffb800]" />
<input value={form.comp_reason} onChange={e => setForm(p => ({ ...p, comp_reason: e.target.value }))} className="w-full px-2 py-1.5 bg-[#0a0a0a] border border-[#3a322b] rounded text-xs text-white focus:outline-none focus:border-[#F0A623]" />
</div>
)}
{isEmailProduct && (
<>
<div>
<label className="text-xs text-[#888] block mb-1">Email HTML URL</label>
<input value={form.email_html_url} onChange={e => setForm(p => ({ ...p, email_html_url: e.target.value }))} className="w-full px-2 py-1.5 bg-[#0a0a0a] border border-[#252525] rounded text-xs text-white focus:outline-none focus:border-[#ffb800]" />
<input value={form.email_html_url} onChange={e => setForm(p => ({ ...p, email_html_url: e.target.value }))} className="w-full px-2 py-1.5 bg-[#0a0a0a] border border-[#3a322b] rounded text-xs text-white focus:outline-none focus:border-[#F0A623]" />
</div>
<div>
<label className="text-xs text-[#888] block mb-1">Scheduled Send Date</label>
<input type="date" value={form.email_scheduled_date} onChange={e => setForm(p => ({ ...p, email_scheduled_date: e.target.value }))} className="w-full px-2 py-1.5 bg-[#0a0a0a] border border-[#252525] rounded text-xs text-white focus:outline-none" />
<input type="date" value={form.email_scheduled_date} onChange={e => setForm(p => ({ ...p, email_scheduled_date: e.target.value }))} className="w-full px-2 py-1.5 bg-[#0a0a0a] border border-[#3a322b] rounded text-xs text-white focus:outline-none" />
</div>
<div>
<label className="text-xs text-[#888] block mb-1">List Segment</label>
<input value={form.email_list_segment} onChange={e => setForm(p => ({ ...p, email_list_segment: e.target.value }))} className="w-full px-2 py-1.5 bg-[#0a0a0a] border border-[#252525] rounded text-xs text-white focus:outline-none focus:border-[#ffb800]" />
<input value={form.email_list_segment} onChange={e => setForm(p => ({ ...p, email_list_segment: e.target.value }))} className="w-full px-2 py-1.5 bg-[#0a0a0a] border border-[#3a322b] rounded text-xs text-white focus:outline-none focus:border-[#F0A623]" />
</div>
</>
)}
{isAdvertorial && (
<div>
<label className="text-xs text-[#888] block mb-1">Page URL (permanent)</label>
<input value={form.page_url} onChange={e => setForm(p => ({ ...p, page_url: e.target.value }))} className="w-full px-2 py-1.5 bg-[#0a0a0a] border border-[#252525] rounded text-xs text-white focus:outline-none focus:border-[#ffb800]" />
<input value={form.page_url} onChange={e => setForm(p => ({ ...p, page_url: e.target.value }))} className="w-full px-2 py-1.5 bg-[#0a0a0a] border border-[#3a322b] rounded text-xs text-white focus:outline-none focus:border-[#F0A623]" />
</div>
)}
<div className="col-span-2 md:col-span-3">
<label className="text-xs text-[#888] block mb-1">Notes</label>
<textarea value={form.notes} onChange={e => setForm(p => ({ ...p, notes: e.target.value }))} rows={2} className="w-full px-2 py-1.5 bg-[#0a0a0a] border border-[#252525] rounded text-xs text-white focus:outline-none focus:border-[#ffb800]" />
<textarea value={form.notes} onChange={e => setForm(p => ({ ...p, notes: e.target.value }))} rows={2} className="w-full px-2 py-1.5 bg-[#0a0a0a] border border-[#3a322b] rounded text-xs text-white focus:outline-none focus:border-[#F0A623]" />
</div>
</div>
<div className="flex gap-2">
<button type="submit" disabled={saving} className="px-3 py-1.5 bg-[#ffb800] rounded text-xs font-medium hover:bg-blue-500 disabled:opacity-50">{saving ? 'Saving…' : 'Create IO Extension'}</button>
<button type="button" onClick={() => setShowForm(false)} className="px-3 py-1.5 bg-[#1a1a1a] border border-[#252525] rounded text-xs text-[#888] hover:text-white">Cancel</button>
<button type="submit" disabled={saving} className="px-3 py-1.5 bg-[#F0A623] rounded text-xs font-medium hover:bg-blue-500 disabled:opacity-50">{saving ? 'Saving…' : 'Create IO Extension'}</button>
<button type="button" onClick={() => setShowForm(false)} className="px-3 py-1.5 bg-[#231d18] border border-[#3a322b] rounded text-xs text-[#888] hover:text-white">Cancel</button>
</div>
</form>
)}
<div className="bg-[#111] border border-[#252525] rounded-lg overflow-x-auto">
<div className="bg-[#111] border border-[#3a322b] rounded-lg overflow-x-auto">
<table className="w-full text-sm">
<thead>
<tr className="border-b border-[#1a1a1a]">
<tr className="border-b border-[#231d18]">
{['IO ID','Client','Product','Start','End','Status','Comp','Paid','Actions'].map(h => (
<th key={h} className="px-3 py-2 text-left text-xs text-[#555] font-medium whitespace-nowrap">{h}</th>
))}
@@ -211,8 +211,8 @@ export default function AdOpsIOsPage() {
) : ios.length === 0 ? (
<tr><td colSpan={9} className="px-4 py-8 text-center text-[#555] text-xs">No IO extensions found</td></tr>
) : ios.map((io: any) => (
<tr key={io.id} className="border-b border-[#1a1a1a] hover:bg-[#161616]">
<td className="px-3 py-2 text-[#ffb800] font-mono text-xs">{io.existing_io_id}</td>
<tr key={io.id} className="border-b border-[#231d18] hover:bg-[#231d18]">
<td className="px-3 py-2 text-[#F0A623] font-mono text-xs">{io.existing_io_id}</td>
<td className="px-3 py-2 text-white text-xs">{io.rmp_clients?.company_name ?? '—'}</td>
<td className="px-3 py-2 text-[#ccc] text-xs">{io.rmp_products?.name ?? '—'}</td>
<td className="px-3 py-2 text-[#888] text-xs">{io.start_date}</td>
@@ -227,7 +227,7 @@ export default function AdOpsIOsPage() {
<span className={`px-2 py-0.5 rounded-full text-xs ${io.is_paid ? 'bg-green-400/10 text-green-400' : 'bg-yellow-400/10 text-yellow-400'}`}>{io.is_paid ? 'Paid' : 'Unpaid'}</span>
</td>
<td className="px-3 py-2 flex gap-2">
<Link href={`/admin/adops/ios/${io.id}`} className="text-xs text-[#ffb800] hover:underline">View</Link>
<Link href={`/admin/adops/ios/${io.id}`} className="text-xs text-[#F0A623] hover:underline">View</Link>
{io.flight_status === 'expired' && (
<button onClick={() => { const r = prompt('Reactivation reason?'); if (r) updateStatus(io.id, 'active', r); }} className="text-xs text-green-400 hover:underline">Reactivate</button>
)}

View File

@@ -22,7 +22,7 @@ export default function AdOpsNotificationsPage() {
useEffect(() => { if (user) load(); }, [user, load]);
if (loading) return <div className="min-h-screen bg-[#0a0a0a] flex items-center justify-center"><div className="w-8 h-8 border-2 border-[#ffb800] border-t-transparent rounded-full animate-spin" /></div>;
if (loading) return <div className="min-h-screen bg-[#0a0a0a] flex items-center justify-center"><div className="w-8 h-8 border-2 border-[#F0A623] border-t-transparent rounded-full animate-spin" /></div>;
if (!user) return null;
return (
@@ -33,10 +33,10 @@ export default function AdOpsNotificationsPage() {
<Link href="/admin/adops" className="text-xs text-[#555] hover:text-[#888]"> Ad Operations</Link>
</div>
<div className="bg-[#111] border border-[#252525] rounded-lg overflow-x-auto">
<div className="bg-[#111] border border-[#3a322b] rounded-lg overflow-x-auto">
<table className="w-full text-sm">
<thead>
<tr className="border-b border-[#1a1a1a]">
<tr className="border-b border-[#231d18]">
{['Type','IO','Recipient','Via','Sent At','Status','Message'].map(h => (
<th key={h} className="px-3 py-2 text-left text-xs text-[#555] font-medium">{h}</th>
))}
@@ -48,9 +48,9 @@ export default function AdOpsNotificationsPage() {
) : notifications.length === 0 ? (
<tr><td colSpan={7} className="px-4 py-8 text-center text-[#555] text-xs">No notifications yet</td></tr>
) : notifications.map((n: any) => (
<tr key={n.id} className="border-b border-[#1a1a1a] hover:bg-[#161616]">
<tr key={n.id} className="border-b border-[#231d18] hover:bg-[#231d18]">
<td className="px-3 py-2 text-[#888] text-xs">{n.notification_type?.replace(/_/g, ' ')}</td>
<td className="px-3 py-2 text-[#ffb800] font-mono text-xs">{n.io_extension_id?.slice(0, 8) ?? '—'}</td>
<td className="px-3 py-2 text-[#F0A623] font-mono text-xs">{n.io_extension_id?.slice(0, 8) ?? '—'}</td>
<td className="px-3 py-2 text-[#ccc] text-xs">{n.recipient_email ?? n.recipient_phone ?? '—'}</td>
<td className="px-3 py-2 text-[#555] text-xs">{n.sent_via}</td>
<td className="px-3 py-2 text-[#555] text-xs">{n.sent_at ? new Date(n.sent_at).toLocaleString() : '—'}</td>

View File

@@ -9,7 +9,7 @@ const FLIGHT_COLORS: Record<string, string> = {
scheduled: 'bg-blue-400/10 text-blue-400 border-blue-400/20',
paused: 'bg-gray-600/20 text-gray-400 border-gray-600/20',
expired: 'bg-red-400/10 text-red-400 border-red-400/20',
cancelled: 'bg-[#1a1a1a] text-[#555] border-[#252525]',
cancelled: 'bg-[#231d18] text-[#555] border-[#3a322b]',
};
export default function AdOpsDashboardPage() {
@@ -43,7 +43,7 @@ export default function AdOpsDashboardPage() {
useEffect(() => { if (user) load(); }, [user, load]);
if (loading) return <div className="min-h-screen bg-[#0a0a0a] flex items-center justify-center"><div className="w-8 h-8 border-2 border-[#ffb800] border-t-transparent rounded-full animate-spin" /></div>;
if (loading) return <div className="min-h-screen bg-[#0a0a0a] flex items-center justify-center"><div className="w-8 h-8 border-2 border-[#F0A623] border-t-transparent rounded-full animate-spin" /></div>;
if (!user) return null;
return (
@@ -64,7 +64,7 @@ export default function AdOpsDashboardPage() {
{ label: 'Unpaid Active', key: 'unpaid_active', color: 'text-orange-400' },
{ label: 'Email Blasts Scheduled', key: 'email_scheduled', color: 'text-purple-400' },
].map(c => (
<div key={c.key} className="bg-[#111] border border-[#252525] rounded-lg p-4">
<div key={c.key} className="bg-[#111] border border-[#3a322b] rounded-lg p-4">
<p className={`text-2xl font-bold ${c.color}`}>{stats?.[c.key] ?? 0}</p>
<p className="text-xs text-[#888] mt-1">{c.label}</p>
</div>
@@ -73,25 +73,25 @@ export default function AdOpsDashboardPage() {
{/* Filters */}
<div className="flex gap-2 flex-wrap">
<select value={filterSite} onChange={e => setFilterSite(e.target.value)} className="px-2 py-1 bg-[#111] border border-[#252525] rounded text-xs text-[#888] focus:outline-none">
<select value={filterSite} onChange={e => setFilterSite(e.target.value)} className="px-2 py-1 bg-[#111] border border-[#3a322b] rounded text-xs text-[#888] focus:outline-none">
<option value="">All Sites</option>
{['avbeat','avbeat','backlotbeat','broadcastengineering'].map(s => <option key={s} value={s}>{s}</option>)}
</select>
<select value={filterStatus} onChange={e => setFilterStatus(e.target.value)} className="px-2 py-1 bg-[#111] border border-[#252525] rounded text-xs text-[#888] focus:outline-none">
<select value={filterStatus} onChange={e => setFilterStatus(e.target.value)} className="px-2 py-1 bg-[#111] border border-[#3a322b] rounded text-xs text-[#888] focus:outline-none">
<option value="">All Statuses</option>
{['active','scheduled','paused','expired','cancelled'].map(s => <option key={s} value={s}>{s}</option>)}
</select>
</div>
{/* Active Flights Table */}
<div className="bg-[#111] border border-[#252525] rounded-lg overflow-x-auto">
<div className="px-4 py-3 border-b border-[#252525] flex items-center justify-between">
<div className="bg-[#111] border border-[#3a322b] rounded-lg overflow-x-auto">
<div className="px-4 py-3 border-b border-[#3a322b] flex items-center justify-between">
<h2 className="text-sm font-semibold">Active Flights</h2>
<Link href="/admin/adops/ios" className="text-xs text-[#ffb800] hover:underline">View All IOs </Link>
<Link href="/admin/adops/ios" className="text-xs text-[#F0A623] hover:underline">View All IOs </Link>
</div>
<table className="w-full text-sm">
<thead>
<tr className="border-b border-[#1a1a1a]">
<tr className="border-b border-[#231d18]">
{['Client','Product','Site','Start','End','Status','Salesperson','Paid',''].map(h => (
<th key={h} className="px-3 py-2 text-left text-xs text-[#555] font-medium whitespace-nowrap">{h}</th>
))}
@@ -103,7 +103,7 @@ export default function AdOpsDashboardPage() {
) : flights.length === 0 ? (
<tr><td colSpan={9} className="px-4 py-8 text-center text-[#555] text-xs">No flights found</td></tr>
) : flights.map((f: any) => (
<tr key={f.id} className="border-b border-[#1a1a1a] hover:bg-[#161616]">
<tr key={f.id} className="border-b border-[#231d18] hover:bg-[#231d18]">
<td className="px-3 py-2 text-white text-xs">{f.rmp_clients?.company_name ?? '—'}</td>
<td className="px-3 py-2 text-[#ccc] text-xs">{f.rmp_products?.name ?? '—'}</td>
<td className="px-3 py-2 text-[#888] text-xs capitalize">{f.rmp_orders?.site ?? '—'}</td>
@@ -117,7 +117,7 @@ export default function AdOpsDashboardPage() {
<span className={`px-2 py-0.5 rounded-full text-xs ${f.is_paid ? 'bg-green-400/10 text-green-400' : f.is_comp ? 'bg-blue-400/10 text-blue-400' : 'bg-yellow-400/10 text-yellow-400'}`}>{f.is_comp ? 'Comp' : f.is_paid ? 'Paid' : 'Unpaid'}</span>
</td>
<td className="px-3 py-2">
<Link href={`/admin/adops/ios/${f.id}`} className="text-xs text-[#ffb800] hover:underline">View</Link>
<Link href={`/admin/adops/ios/${f.id}`} className="text-xs text-[#F0A623] hover:underline">View</Link>
</td>
</tr>
))}
@@ -126,13 +126,13 @@ export default function AdOpsDashboardPage() {
</div>
{/* Email Schedule */}
<div className="bg-[#111] border border-[#252525] rounded-lg overflow-hidden">
<div className="px-4 py-3 border-b border-[#252525]">
<div className="bg-[#111] border border-[#3a322b] rounded-lg overflow-hidden">
<div className="px-4 py-3 border-b border-[#3a322b]">
<h2 className="text-sm font-semibold">Email Distribution Schedule</h2>
</div>
<table className="w-full text-sm">
<thead>
<tr className="border-b border-[#1a1a1a]">
<tr className="border-b border-[#231d18]">
{['Client','Type','Site','Scheduled Date','Status',''].map(h => (
<th key={h} className="px-3 py-2 text-left text-xs text-[#555] font-medium">{h}</th>
))}
@@ -142,7 +142,7 @@ export default function AdOpsDashboardPage() {
{emailSchedule.length === 0 ? (
<tr><td colSpan={6} className="px-4 py-6 text-center text-[#555] text-xs">No email blasts scheduled</td></tr>
) : emailSchedule.map((e: any) => (
<tr key={e.id} className="border-b border-[#1a1a1a] hover:bg-[#161616]">
<tr key={e.id} className="border-b border-[#231d18] hover:bg-[#231d18]">
<td className="px-3 py-2 text-white text-xs">{e.rmp_clients?.company_name ?? '—'}</td>
<td className="px-3 py-2 text-[#888] text-xs">{e.rmp_products?.product_type?.replace(/_/g, ' ')}</td>
<td className="px-3 py-2 text-[#888] text-xs capitalize">{e.rmp_orders?.site ?? '—'}</td>
@@ -151,7 +151,7 @@ export default function AdOpsDashboardPage() {
<span className={`px-2 py-0.5 rounded-full text-xs ${e.email_sent_at ? 'bg-green-400/10 text-green-400' : 'bg-blue-400/10 text-blue-400'}`}>{e.email_sent_at ? 'Sent' : 'Pending'}</span>
</td>
<td className="px-3 py-2">
{e.email_html_url && <a href={e.email_html_url} target="_blank" rel="noopener noreferrer" className="text-xs text-[#ffb800] hover:underline">Preview</a>}
{e.email_html_url && <a href={e.email_html_url} target="_blank" rel="noopener noreferrer" className="text-xs text-[#F0A623] hover:underline">Preview</a>}
</td>
</tr>
))}

View File

@@ -49,7 +49,7 @@ export default function AdOpsProductsPage() {
load();
}
if (loading) return <div className="min-h-screen bg-[#0a0a0a] flex items-center justify-center"><div className="w-8 h-8 border-2 border-[#ffb800] border-t-transparent rounded-full animate-spin" /></div>;
if (loading) return <div className="min-h-screen bg-[#0a0a0a] flex items-center justify-center"><div className="w-8 h-8 border-2 border-[#F0A623] border-t-transparent rounded-full animate-spin" /></div>;
if (!user) return null;
return (
@@ -60,62 +60,62 @@ export default function AdOpsProductsPage() {
<h1 className="text-xl font-bold">Product Catalog</h1>
<Link href="/admin/adops" className="text-xs text-[#555] hover:text-[#888]"> Ad Operations</Link>
</div>
<button onClick={() => setShowForm(true)} className="px-3 py-1.5 bg-[#ffb800] rounded text-xs font-medium hover:bg-blue-500">+ Add Product</button>
<button onClick={() => setShowForm(true)} className="px-3 py-1.5 bg-[#F0A623] rounded text-xs font-medium hover:bg-blue-500">+ Add Product</button>
</div>
<div className="flex gap-2">
<select value={filterSite} onChange={e => setFilterSite(e.target.value)} className="px-2 py-1 bg-[#111] border border-[#252525] rounded text-xs text-[#888] focus:outline-none">
<select value={filterSite} onChange={e => setFilterSite(e.target.value)} className="px-2 py-1 bg-[#111] border border-[#3a322b] rounded text-xs text-[#888] focus:outline-none">
<option value="">All Sites</option>
{SITES.map(s => <option key={s} value={s}>{s}</option>)}
</select>
</div>
{showForm && (
<form onSubmit={handleCreate} className="bg-[#111] border border-[#252525] rounded-lg p-4 space-y-3">
<form onSubmit={handleCreate} className="bg-[#111] border border-[#3a322b] rounded-lg p-4 space-y-3">
<h3 className="text-sm font-semibold">New Product</h3>
<div className="grid grid-cols-2 md:grid-cols-3 gap-3">
<div>
<label className="text-xs text-[#888] block mb-1">Site *</label>
<select required value={form.site} onChange={e => setForm(p => ({ ...p, site: e.target.value }))} className="w-full px-2 py-1.5 bg-[#0a0a0a] border border-[#252525] rounded text-xs text-white focus:outline-none">
<select required value={form.site} onChange={e => setForm(p => ({ ...p, site: e.target.value }))} className="w-full px-2 py-1.5 bg-[#0a0a0a] border border-[#3a322b] rounded text-xs text-white focus:outline-none">
<option value="">Select</option>
{SITES.map(s => <option key={s} value={s}>{s}</option>)}
</select>
</div>
<div>
<label className="text-xs text-[#888] block mb-1">Product Type *</label>
<select required value={form.product_type} onChange={e => setForm(p => ({ ...p, product_type: e.target.value }))} className="w-full px-2 py-1.5 bg-[#0a0a0a] border border-[#252525] rounded text-xs text-white focus:outline-none">
<select required value={form.product_type} onChange={e => setForm(p => ({ ...p, product_type: e.target.value }))} className="w-full px-2 py-1.5 bg-[#0a0a0a] border border-[#3a322b] rounded text-xs text-white focus:outline-none">
<option value="">Select</option>
{PRODUCT_TYPES.map(t => <option key={t} value={t}>{t.replace(/_/g, ' ')}</option>)}
</select>
</div>
<div>
<label className="text-xs text-[#888] block mb-1">Name *</label>
<input required value={form.name} onChange={e => setForm(p => ({ ...p, name: e.target.value }))} className="w-full px-2 py-1.5 bg-[#0a0a0a] border border-[#252525] rounded text-xs text-white focus:outline-none focus:border-[#ffb800]" />
<input required value={form.name} onChange={e => setForm(p => ({ ...p, name: e.target.value }))} className="w-full px-2 py-1.5 bg-[#0a0a0a] border border-[#3a322b] rounded text-xs text-white focus:outline-none focus:border-[#F0A623]" />
</div>
<div>
<label className="text-xs text-[#888] block mb-1">Dimensions</label>
<input value={form.dimensions} onChange={e => setForm(p => ({ ...p, dimensions: e.target.value }))} placeholder="e.g. 300x250" className="w-full px-2 py-1.5 bg-[#0a0a0a] border border-[#252525] rounded text-xs text-white focus:outline-none focus:border-[#ffb800]" />
<input value={form.dimensions} onChange={e => setForm(p => ({ ...p, dimensions: e.target.value }))} placeholder="e.g. 300x250" className="w-full px-2 py-1.5 bg-[#0a0a0a] border border-[#3a322b] rounded text-xs text-white focus:outline-none focus:border-[#F0A623]" />
</div>
<div>
<label className="text-xs text-[#888] block mb-1">Duration (days)</label>
<input type="number" value={form.duration_days} onChange={e => setForm(p => ({ ...p, duration_days: e.target.value }))} className="w-full px-2 py-1.5 bg-[#0a0a0a] border border-[#252525] rounded text-xs text-white focus:outline-none focus:border-[#ffb800]" />
<input type="number" value={form.duration_days} onChange={e => setForm(p => ({ ...p, duration_days: e.target.value }))} className="w-full px-2 py-1.5 bg-[#0a0a0a] border border-[#3a322b] rounded text-xs text-white focus:outline-none focus:border-[#F0A623]" />
</div>
<div className="col-span-2 md:col-span-3">
<label className="text-xs text-[#888] block mb-1">Description</label>
<input value={form.description} onChange={e => setForm(p => ({ ...p, description: e.target.value }))} className="w-full px-2 py-1.5 bg-[#0a0a0a] border border-[#252525] rounded text-xs text-white focus:outline-none focus:border-[#ffb800]" />
<input value={form.description} onChange={e => setForm(p => ({ ...p, description: e.target.value }))} className="w-full px-2 py-1.5 bg-[#0a0a0a] border border-[#3a322b] rounded text-xs text-white focus:outline-none focus:border-[#F0A623]" />
</div>
</div>
<div className="flex gap-2">
<button type="submit" disabled={saving} className="px-3 py-1.5 bg-[#ffb800] rounded text-xs font-medium hover:bg-blue-500 disabled:opacity-50">{saving ? 'Saving…' : 'Add Product'}</button>
<button type="button" onClick={() => setShowForm(false)} className="px-3 py-1.5 bg-[#1a1a1a] border border-[#252525] rounded text-xs text-[#888] hover:text-white">Cancel</button>
<button type="submit" disabled={saving} className="px-3 py-1.5 bg-[#F0A623] rounded text-xs font-medium hover:bg-blue-500 disabled:opacity-50">{saving ? 'Saving…' : 'Add Product'}</button>
<button type="button" onClick={() => setShowForm(false)} className="px-3 py-1.5 bg-[#231d18] border border-[#3a322b] rounded text-xs text-[#888] hover:text-white">Cancel</button>
</div>
</form>
)}
<div className="bg-[#111] border border-[#252525] rounded-lg overflow-x-auto">
<div className="bg-[#111] border border-[#3a322b] rounded-lg overflow-x-auto">
<table className="w-full text-sm">
<thead>
<tr className="border-b border-[#1a1a1a]">
<tr className="border-b border-[#231d18]">
{['Site','Type','Name','Dimensions','Duration','Active',''].map(h => (
<th key={h} className="px-3 py-2 text-left text-xs text-[#555] font-medium">{h}</th>
))}
@@ -127,17 +127,17 @@ export default function AdOpsProductsPage() {
) : products.length === 0 ? (
<tr><td colSpan={7} className="px-4 py-8 text-center text-[#555] text-xs">No products found</td></tr>
) : products.map((p: any) => (
<tr key={p.id} className={`border-b border-[#1a1a1a] hover:bg-[#161616] ${!p.active ? 'opacity-50' : ''}`}>
<tr key={p.id} className={`border-b border-[#231d18] hover:bg-[#231d18] ${!p.active ? 'opacity-50' : ''}`}>
<td className="px-3 py-2 text-[#888] text-xs capitalize">{p.site}</td>
<td className="px-3 py-2 text-[#555] text-xs">{p.product_type?.replace(/_/g, ' ')}</td>
<td className="px-3 py-2 text-white text-xs">{p.name}</td>
<td className="px-3 py-2 text-[#888] text-xs">{p.dimensions ?? '—'}</td>
<td className="px-3 py-2 text-[#888] text-xs">{p.duration_days ? `${p.duration_days}d` : '—'}</td>
<td className="px-3 py-2">
<span className={`px-2 py-0.5 rounded-full text-xs ${p.active ? 'bg-green-400/10 text-green-400' : 'bg-[#1a1a1a] text-[#555]'}`}>{p.active ? 'Active' : 'Inactive'}</span>
<span className={`px-2 py-0.5 rounded-full text-xs ${p.active ? 'bg-green-400/10 text-green-400' : 'bg-[#231d18] text-[#555]'}`}>{p.active ? 'Active' : 'Inactive'}</span>
</td>
<td className="px-3 py-2">
<button onClick={() => toggleActive(p.id, p.active)} className="text-xs text-[#ffb800] hover:underline">{p.active ? 'Deactivate' : 'Activate'}</button>
<button onClick={() => toggleActive(p.id, p.active)} className="text-xs text-[#F0A623] hover:underline">{p.active ? 'Deactivate' : 'Activate'}</button>
</td>
</tr>
))}

View File

@@ -64,7 +64,7 @@ interface MonthlyTrend {
// ─── Mock / derived data helpers ──────────────────────────────────────────────
const CATEGORY_COLORS = [
'#ffb800', '#f59e0b', '#10b981', '#8b5cf6',
'#F0A623', '#f59e0b', '#10b981', '#8b5cf6',
'#ef4444', '#06b6d4', '#f97316', '#84cc16',
];
@@ -78,9 +78,9 @@ interface StatCardProps {
icon: React.ReactNode;
}
function StatCard({ label, value, sub, accent = '#ffb800', icon }: StatCardProps) {
function StatCard({ label, value, sub, accent = '#F0A623', icon }: StatCardProps) {
return (
<div className="bg-[#111] border border-[#252525] rounded-lg p-5 flex items-start gap-4">
<div className="bg-[#111] border border-[#3a322b] rounded-lg p-5 flex items-start gap-4">
<div
className="w-10 h-10 rounded-lg flex items-center justify-center flex-shrink-0"
style={{ background: `${accent}18` }}
@@ -112,7 +112,7 @@ function SectionHeader({ title, subtitle }: { title: string; subtitle?: string }
function ChartTooltip({ active, payload, label }: any) {
if (!active || !payload?.length) return null;
return (
<div className="bg-[#1a1a1a] border border-[#333] rounded-lg px-3 py-2 text-xs font-body shadow-xl">
<div className="bg-[#231d18] border border-[#333] rounded-lg px-3 py-2 text-xs font-body shadow-xl">
{label && <p className="text-[#888] mb-1">{label}</p>}
{payload.map((entry: any, i: number) => (
<p key={i} style={{ color: entry.color }} className="leading-5">
@@ -252,7 +252,7 @@ export default function AdminAnalyticsPage() {
return (
<div className="min-h-screen bg-[#0a0a0a] flex items-center justify-center">
<div className="flex flex-col items-center gap-3">
<div className="w-8 h-8 border-2 border-[#ffb800] border-t-transparent rounded-full animate-spin" />
<div className="w-8 h-8 border-2 border-[#F0A623] border-t-transparent rounded-full animate-spin" />
<p className="text-[#555] text-sm font-body">Loading analytics</p>
</div>
</div>
@@ -266,7 +266,7 @@ export default function AdminAnalyticsPage() {
return (
<div className="min-h-screen bg-[#0a0a0a] text-white">
{/* ── Page Header ─────────────────────────────────────────────────── */}
<div className="bg-[#111] border-b border-[#252525]">
<div className="bg-[#111] border-b border-[#3a322b]">
<div className="max-w-7xl mx-auto px-4 sm:px-6 py-5 flex items-center justify-between gap-4">
<div>
<h1 className="text-white text-xl font-bold font-heading uppercase tracking-wider">Analytics</h1>
@@ -275,20 +275,20 @@ export default function AdminAnalyticsPage() {
</p>
</div>
<div className="flex items-center gap-2 flex-wrap">
<Link href="/admin/articles" className="text-[#555] hover:text-[#ffb800] text-xs font-body uppercase tracking-wider transition-colors">
<Link href="/admin/articles" className="text-[#555] hover:text-[#F0A623] text-xs font-body uppercase tracking-wider transition-colors">
Articles
</Link>
<span className="text-[#333]">·</span>
<Link href="/admin/users" className="text-[#555] hover:text-[#ffb800] text-xs font-body uppercase tracking-wider transition-colors">
<Link href="/admin/users" className="text-[#555] hover:text-[#F0A623] text-xs font-body uppercase tracking-wider transition-colors">
Users
</Link>
<span className="text-[#333]">·</span>
<Link href="/admin/import" className="text-[#555] hover:text-[#ffb800] text-xs font-body uppercase tracking-wider transition-colors">
<Link href="/admin/import" className="text-[#555] hover:text-[#F0A623] text-xs font-body uppercase tracking-wider transition-colors">
WP Import
</Link>
<button
onClick={fetchAnalytics}
className="ml-2 px-3 py-1.5 bg-[#1a1a1a] border border-[#333] hover:border-[#ffb800] text-[#888] hover:text-[#ffb800] text-xs font-body rounded transition-colors"
className="ml-2 px-3 py-1.5 bg-[#231d18] border border-[#333] hover:border-[#F0A623] text-[#888] hover:text-[#F0A623] text-xs font-body rounded transition-colors"
>
Refresh
</button>
@@ -312,7 +312,7 @@ export default function AdminAnalyticsPage() {
label="Total Articles"
value={metrics?.totalArticles ?? 0}
icon={<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/></svg>}
accent="#ffb800"
accent="#F0A623"
/>
<StatCard
label="Published"
@@ -361,7 +361,7 @@ export default function AdminAnalyticsPage() {
value={`${publishRate}%`}
sub="Published / total"
icon={<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><line x1="18" y1="20" x2="18" y2="10"/><line x1="12" y1="20" x2="12" y2="4"/><line x1="6" y1="20" x2="6" y2="14"/></svg>}
accent="#ffb800"
accent="#F0A623"
/>
</div>
</section>
@@ -369,7 +369,7 @@ export default function AdminAnalyticsPage() {
{/* ── Monthly Trend ────────────────────────────────────────────── */}
<section>
<SectionHeader title="Monthly Trends" subtitle="Published, imported, and native articles over the last 6 months" />
<div className="bg-[#111] border border-[#252525] rounded-lg p-5">
<div className="bg-[#111] border border-[#3a322b] rounded-lg p-5">
{monthlyTrends.length > 0 ? (
<ResponsiveContainer width="100%" height={240}>
<LineChart data={monthlyTrends} margin={{ top: 4, right: 8, left: -20, bottom: 0 }}>
@@ -395,7 +395,7 @@ export default function AdminAnalyticsPage() {
{/* Author Contributions */}
<section>
<SectionHeader title="Author Contributions" subtitle="Top contributors by article count" />
<div className="bg-[#111] border border-[#252525] rounded-lg p-5">
<div className="bg-[#111] border border-[#3a322b] rounded-lg p-5">
{authorStats.length > 0 ? (
<ResponsiveContainer width="100%" height={280}>
<BarChart
@@ -429,7 +429,7 @@ export default function AdminAnalyticsPage() {
{/* Top Categories */}
<section>
<SectionHeader title="Top-Performing Categories" subtitle="Article distribution by category" />
<div className="bg-[#111] border border-[#252525] rounded-lg p-5">
<div className="bg-[#111] border border-[#3a322b] rounded-lg p-5">
{categoryStats.length > 0 ? (
<>
<ResponsiveContainer width="100%" height={200}>
@@ -481,7 +481,7 @@ export default function AdminAnalyticsPage() {
<SectionHeader title="Import Success Rates" subtitle="Publishing success across content sources" />
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
{importStats.map((stat) => (
<div key={stat.label} className="bg-[#111] border border-[#252525] rounded-lg p-5">
<div key={stat.label} className="bg-[#111] border border-[#3a322b] rounded-lg p-5">
<div className="flex items-center justify-between mb-4">
<p className="text-white text-sm font-bold font-heading uppercase tracking-wider">{stat.label}</p>
<span
@@ -526,7 +526,7 @@ export default function AdminAnalyticsPage() {
{/* ── Author Detail Table ──────────────────────────────────────── */}
<section>
<SectionHeader title="Author Detail Breakdown" subtitle="Per-author article status distribution" />
<div className="bg-[#111] border border-[#252525] rounded-lg overflow-hidden">
<div className="bg-[#111] border border-[#3a322b] rounded-lg overflow-hidden">
<div className="overflow-x-auto">
<table className="w-full text-sm font-body">
<thead>
@@ -550,7 +550,7 @@ export default function AdminAnalyticsPage() {
return (
<tr
key={a.author_name}
className={`border-b border-[#1a1a1a] hover:bg-[#151515] transition-colors ${i % 2 === 0 ? '' : 'bg-[#0d0d0d]'}`}
className={`border-b border-[#231d18] hover:bg-[#151515] transition-colors ${i % 2 === 0 ? '' : 'bg-[#1c1815]'}`}
>
<td className="px-4 py-3 text-white font-medium">{a.author_name}</td>
<td className="px-4 py-3 text-right text-[#888]">{a.total}</td>

View File

@@ -33,7 +33,7 @@ const STATUS_COLORS: Record<string, string> = {
published: 'text-green-400 bg-green-400/10',
draft: 'text-yellow-400 bg-yellow-400/10',
pending: 'text-orange-400 bg-orange-400/10',
archived: 'text-[#555] bg-[#1a1a1a]',
archived: 'text-[#555] bg-[#231d18]',
};
const STATUS_LABELS: Record<string, string> = {
@@ -384,7 +384,7 @@ export default function AdminArticlesPage() {
if (loading || (!user && !loading)) {
return (
<div className="min-h-screen bg-[#0a0a0a] flex items-center justify-center">
<div className="w-6 h-6 border-2 border-[#ffb800] border-t-transparent rounded-full animate-spin" />
<div className="w-6 h-6 border-2 border-[#F0A623] border-t-transparent rounded-full animate-spin" />
</div>
);
}
@@ -406,10 +406,10 @@ export default function AdminArticlesPage() {
)}
{/* Page Header */}
<div className="bg-[#111111] border-b border-[#252525]">
<div className="bg-[#1c1815] border-b border-[#3a322b]">
<div className="max-w-7xl mx-auto px-4 py-5">
<div className="flex items-center gap-3 mb-1">
<Link href="/home-page" className="text-[#555] hover:text-[#ffb800] transition-colors text-sm">Home</Link>
<Link href="/home-page" className="text-[#555] hover:text-[#F0A623] transition-colors text-sm">Home</Link>
<span className="text-[#333]">/</span>
<span className="text-[#888] text-sm">Admin</span>
<span className="text-[#333]">/</span>
@@ -423,13 +423,13 @@ export default function AdminArticlesPage() {
<div className="flex items-center gap-2">
<button
onClick={() => setShowNewArticle(true)}
className="flex items-center gap-1.5 text-xs font-bold text-white bg-[#ffb800] hover:bg-[#d99700] rounded px-3 py-2 transition-colors">
className="flex items-center gap-1.5 text-xs font-bold text-white bg-[#F0A623] hover:bg-[#BA7517] rounded px-3 py-2 transition-colors">
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5"><line x1="12" y1="5" x2="12" y2="19" /><line x1="5" y1="12" x2="19" y2="12" /></svg>
New Article
</button>
<Link
href="/admin/import"
className="flex items-center gap-1.5 text-xs text-[#666] hover:text-[#ffb800] border border-[#252525] hover:border-[#ffb800] rounded px-3 py-2 transition-colors">
className="flex items-center gap-1.5 text-xs text-[#666] hover:text-[#F0A623] border border-[#3a322b] hover:border-[#F0A623] rounded px-3 py-2 transition-colors">
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" /><polyline points="7 10 12 15 17 10" /><line x1="12" y1="15" x2="12" y2="3" /></svg>
WP Import
</Link>
@@ -471,7 +471,7 @@ export default function AdminArticlesPage() {
{ label: 'Drafts', value: counts.draft, color: 'text-yellow-400' },
{ label: 'Archived', value: counts.archived, color: 'text-[#555]' },
].map((s) => (
<div key={s.label} className="bg-[#111] border border-[#252525] rounded-lg p-4 text-center">
<div key={s.label} className="bg-[#111] border border-[#3a322b] rounded-lg p-4 text-center">
<p className={`text-2xl font-bold ${s.color}`}>{s.value}</p>
<p className="text-[#555] text-xs mt-0.5 uppercase tracking-wider">{s.label}</p>
</div>
@@ -479,7 +479,7 @@ export default function AdminArticlesPage() {
</div>
{/* Status Tabs */}
<div className="flex items-center gap-1 border-b border-[#252525] overflow-x-auto">
<div className="flex items-center gap-1 border-b border-[#3a322b] overflow-x-auto">
{tabs.map((tab) => (
<button
key={tab.key}
@@ -492,7 +492,7 @@ export default function AdminArticlesPage() {
{tab.label}
{tab.key !== 'all' && (
<span className={`text-[10px] px-1.5 py-0.5 rounded-full ${
activeTab === tab.key ? 'bg-current/10' : 'bg-[#1a1a1a]'
activeTab === tab.key ? 'bg-current/10' : 'bg-[#231d18]'
}`}>
{counts[tab.key]}
</span>
@@ -512,13 +512,13 @@ export default function AdminArticlesPage() {
placeholder="Search articles..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="w-full bg-[#111] border border-[#252525] text-[#cccccc] text-sm rounded pl-9 pr-4 py-2 focus:outline-none focus:border-[#ffb800] placeholder-[#444]"
className="w-full bg-[#111] border border-[#3a322b] text-[#cccccc] text-sm rounded pl-9 pr-4 py-2 focus:outline-none focus:border-[#F0A623] placeholder-[#444]"
/>
</div>
<select
value={sourceFilter}
onChange={(e) => setSourceFilter(e.target.value as SourceFilter)}
className="bg-[#111] border border-[#252525] text-[#cccccc] text-sm rounded px-3 py-2 focus:outline-none focus:border-[#ffb800]">
className="bg-[#111] border border-[#3a322b] text-[#cccccc] text-sm rounded px-3 py-2 focus:outline-none focus:border-[#F0A623]">
<option value="all">All Sources</option>
<option value="imported">Imported (WP)</option>
<option value="native">Native</option>
@@ -527,13 +527,13 @@ export default function AdminArticlesPage() {
{/* Bulk Actions Bar */}
{selectedIds.size > 0 && (
<div className="flex items-center gap-3 bg-[#0d1a2e] border border-[#4a3500] rounded-lg px-4 py-3">
<span className="text-[#ffb800] text-sm font-bold">{selectedIds.size} selected</span>
<div className="flex items-center gap-3 bg-[#0d1a2e] border border-[#412402] rounded-lg px-4 py-3">
<span className="text-[#F0A623] text-sm font-bold">{selectedIds.size} selected</span>
<div className="flex-1" />
<select
value={bulkAction}
onChange={(e) => setBulkAction(e.target.value as BulkAction | '')}
className="bg-[#111] border border-[#333] text-[#cccccc] text-sm rounded px-3 py-1.5 focus:outline-none focus:border-[#ffb800]">
className="bg-[#111] border border-[#333] text-[#cccccc] text-sm rounded px-3 py-1.5 focus:outline-none focus:border-[#F0A623]">
<option value="">Bulk Action</option>
<option value="publish">Publish</option>
<option value="pending">Submit for Review</option>
@@ -544,7 +544,7 @@ export default function AdminArticlesPage() {
<button
onClick={handleBulkAction}
disabled={!bulkAction || bulkProcessing}
className="flex items-center gap-1.5 bg-[#ffb800] hover:bg-[#d99700] disabled:bg-[#4a3500] disabled:cursor-not-allowed text-white text-sm font-bold px-4 py-1.5 rounded transition-colors">
className="flex items-center gap-1.5 bg-[#F0A623] hover:bg-[#BA7517] disabled:bg-[#412402] disabled:cursor-not-allowed text-white text-sm font-bold px-4 py-1.5 rounded transition-colors">
{bulkProcessing ? (
<div className="w-3.5 h-3.5 border-2 border-white border-t-transparent rounded-full animate-spin" />
) : null}
@@ -568,15 +568,15 @@ export default function AdminArticlesPage() {
<span className="text-orange-400 text-sm font-bold">Approval Queue</span>
<span className="text-[#555] text-xs ml-1"> Review and approve or reject each submission</span>
</div>
<div className="divide-y divide-[#1a1a1a]">
<div className="divide-y divide-[#231d18]">
{pendingArticles.map((article) => (
<div key={article.id} className={`flex items-start gap-4 px-4 py-4 hover:bg-[#141414] transition-colors ${processingIds.has(article.id) ? 'opacity-50 pointer-events-none' : ''}`}>
{article.featured_image ? (
<div className="flex-shrink-0 w-16 h-12 rounded overflow-hidden bg-[#1a1a1a]">
<div className="flex-shrink-0 w-16 h-12 rounded overflow-hidden bg-[#231d18]">
<AppImage src={article.featured_image} alt={article.featured_image_alt || article.title} width={64} height={48} className="w-full h-full object-cover" />
</div>
) : (
<div className="flex-shrink-0 w-16 h-12 rounded bg-[#1a1a1a] flex items-center justify-center">
<div className="flex-shrink-0 w-16 h-12 rounded bg-[#231d18] flex items-center justify-center">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" className="text-[#333]">
<rect x="3" y="3" width="18" height="18" rx="2" /><circle cx="8.5" cy="8.5" r="1.5" /><polyline points="21 15 16 10 5 21" />
</svg>
@@ -587,9 +587,9 @@ export default function AdminArticlesPage() {
<div className="flex items-center gap-3 mt-1">
<span className="text-[11px] text-[#555]">{article.author_name || 'Unknown author'}</span>
{article.category && (
<span className="text-[10px] font-bold uppercase tracking-wider text-[#ffb800] bg-[#0d1a2e] px-1.5 py-0.5 rounded">{article.category}</span>
<span className="text-[10px] font-bold uppercase tracking-wider text-[#F0A623] bg-[#0d1a2e] px-1.5 py-0.5 rounded">{article.category}</span>
)}
<span className={`text-[10px] font-bold uppercase tracking-wider px-1.5 py-0.5 rounded ${article.source === 'imported' ? 'text-purple-400 bg-purple-400/10' : 'text-[#ffb800] bg-[#ffb800]/10'}`}>
<span className={`text-[10px] font-bold uppercase tracking-wider px-1.5 py-0.5 rounded ${article.source === 'imported' ? 'text-purple-400 bg-purple-400/10' : 'text-[#F0A623] bg-[#F0A623]/10'}`}>
{article.source === 'imported' ? 'WP' : 'Native'}
</span>
</div>
@@ -606,13 +606,13 @@ export default function AdminArticlesPage() {
</button>
<button
onClick={() => handleStatusChange(article, 'draft')}
className="flex items-center gap-1.5 text-xs font-bold text-[#666] border border-[#333] hover:border-[#555] hover:bg-[#1a1a1a] px-3 py-1.5 rounded transition-colors">
className="flex items-center gap-1.5 text-xs font-bold text-[#666] border border-[#333] hover:border-[#555] hover:bg-[#231d18] px-3 py-1.5 rounded transition-colors">
<svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5"><line x1="18" y1="6" x2="6" y2="18" /><line x1="6" y1="6" x2="18" y2="18" /></svg>
Reject
</button>
<button
onClick={() => openEdit(article)}
className="p-1.5 text-[#555] hover:text-[#ffb800] transition-colors rounded hover:bg-[#1a1a1a]"
className="p-1.5 text-[#555] hover:text-[#F0A623] transition-colors rounded hover:bg-[#231d18]"
title="Edit">
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
<path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7" />
@@ -627,14 +627,14 @@ export default function AdminArticlesPage() {
)}
{/* Articles Table */}
<div className="bg-[#111] border border-[#252525] rounded-lg overflow-hidden">
<div className="bg-[#111] border border-[#3a322b] rounded-lg overflow-hidden">
{/* Table Header */}
<div className="flex items-center gap-3 px-4 py-3 border-b border-[#252525] bg-[#0d0d0d]">
<div className="flex items-center gap-3 px-4 py-3 border-b border-[#3a322b] bg-[#1c1815]">
<input
type="checkbox"
checked={articles.length > 0 && selectedIds.size === articles.length}
onChange={toggleSelectAll}
className="w-4 h-4 rounded border-[#333] bg-[#1a1a1a] accent-[#ffb800] cursor-pointer"
className="w-4 h-4 rounded border-[#333] bg-[#231d18] accent-[#F0A623] cursor-pointer"
/>
<span className="text-[10px] text-[#555] uppercase tracking-wider font-bold flex-1">Article</span>
<span className="text-[10px] text-[#555] uppercase tracking-wider font-bold w-24 hidden md:block">Category</span>
@@ -646,7 +646,7 @@ export default function AdminArticlesPage() {
{loadingData ? (
<div className="flex items-center justify-center py-16">
<div className="w-6 h-6 border-2 border-[#ffb800] border-t-transparent rounded-full animate-spin" />
<div className="w-6 h-6 border-2 border-[#F0A623] border-t-transparent rounded-full animate-spin" />
</div>
) : articles.length === 0 ? (
<div className="text-center py-16">
@@ -656,7 +656,7 @@ export default function AdminArticlesPage() {
<p className="text-[#555] text-sm">No articles found</p>
</div>
) : (
<div className="divide-y divide-[#1a1a1a]">
<div className="divide-y divide-[#231d18]">
{articles.map((article) => (
<div
key={article.id}
@@ -668,13 +668,13 @@ export default function AdminArticlesPage() {
type="checkbox"
checked={selectedIds.has(article.id)}
onChange={() => toggleSelect(article.id)}
className="w-4 h-4 rounded border-[#333] bg-[#1a1a1a] accent-[#ffb800] cursor-pointer flex-shrink-0"
className="w-4 h-4 rounded border-[#333] bg-[#231d18] accent-[#F0A623] cursor-pointer flex-shrink-0"
/>
{/* Thumbnail + Title */}
<div className="flex items-center gap-3 flex-1 min-w-0">
{article.featured_image ? (
<div className="flex-shrink-0 w-12 h-9 rounded overflow-hidden bg-[#1a1a1a]">
<div className="flex-shrink-0 w-12 h-9 rounded overflow-hidden bg-[#231d18]">
<AppImage
src={article.featured_image}
alt={article.featured_image_alt || article.title}
@@ -684,7 +684,7 @@ export default function AdminArticlesPage() {
/>
</div>
) : (
<div className="flex-shrink-0 w-12 h-9 rounded bg-[#1a1a1a] flex items-center justify-center">
<div className="flex-shrink-0 w-12 h-9 rounded bg-[#231d18] flex items-center justify-center">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" className="text-[#333]">
<rect x="3" y="3" width="18" height="18" rx="2" /><circle cx="8.5" cy="8.5" r="1.5" /><polyline points="21 15 16 10 5 21" />
</svg>
@@ -699,7 +699,7 @@ export default function AdminArticlesPage() {
{/* Category */}
<div className="w-24 hidden md:block">
{article.category ? (
<span className="text-[10px] font-bold uppercase tracking-wider text-[#ffb800] bg-[#0d1a2e] px-1.5 py-0.5 rounded">
<span className="text-[10px] font-bold uppercase tracking-wider text-[#F0A623] bg-[#0d1a2e] px-1.5 py-0.5 rounded">
{article.category}
</span>
) : (
@@ -710,7 +710,7 @@ export default function AdminArticlesPage() {
{/* Source */}
<div className="w-20 hidden lg:block">
<span className={`text-[10px] font-bold uppercase tracking-wider px-1.5 py-0.5 rounded ${
article.source === 'imported' ? 'text-purple-400 bg-purple-400/10' : 'text-[#ffb800] bg-[#ffb800]/10'
article.source === 'imported' ? 'text-purple-400 bg-purple-400/10' : 'text-[#F0A623] bg-[#F0A623]/10'
}`}>
{article.source === 'imported' ? 'WP' : 'Native'}
</span>
@@ -730,7 +730,7 @@ export default function AdminArticlesPage() {
{/* Status */}
<div className="w-28">
<span className={`text-[10px] font-bold uppercase tracking-wider px-1.5 py-0.5 rounded ${STATUS_COLORS[article.status] || 'text-[#555] bg-[#1a1a1a]'}`}>
<span className={`text-[10px] font-bold uppercase tracking-wider px-1.5 py-0.5 rounded ${STATUS_COLORS[article.status] || 'text-[#555] bg-[#231d18]'}`}>
{article.status === 'pending' ? 'Pending' : article.status}
</span>
</div>
@@ -741,7 +741,7 @@ export default function AdminArticlesPage() {
<button
onClick={() => openEdit(article)}
title="Edit"
className="p-1.5 text-[#555] hover:text-[#ffb800] transition-colors rounded hover:bg-[#1a1a1a]">
className="p-1.5 text-[#555] hover:text-[#F0A623] transition-colors rounded hover:bg-[#231d18]">
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
<path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7" />
<path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z" />
@@ -753,7 +753,7 @@ export default function AdminArticlesPage() {
<button
onClick={() => handleStatusChange(article, 'pending')}
title="Submit for Review"
className="p-1.5 text-[#555] hover:text-orange-400 transition-colors rounded hover:bg-[#1a1a1a]">
className="p-1.5 text-[#555] hover:text-orange-400 transition-colors rounded hover:bg-[#231d18]">
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
<circle cx="12" cy="12" r="10" /><line x1="12" y1="8" x2="12" y2="12" /><line x1="12" y1="16" x2="12.01" y2="16" />
</svg>
@@ -765,7 +765,7 @@ export default function AdminArticlesPage() {
<button
onClick={() => handleStatusChange(article, 'published')}
title="Approve & Publish"
className="p-1.5 text-[#555] hover:text-green-400 transition-colors rounded hover:bg-[#1a1a1a]">
className="p-1.5 text-[#555] hover:text-green-400 transition-colors rounded hover:bg-[#231d18]">
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
<path d="M22 11.08V12a10 10 0 1 1-5.93-9.14" /><polyline points="22 4 12 14.01 9 11.01" />
</svg>
@@ -777,7 +777,7 @@ export default function AdminArticlesPage() {
<button
onClick={() => handleStatusChange(article, 'draft')}
title="Reject (back to draft)"
className="p-1.5 text-[#555] hover:text-red-400 transition-colors rounded hover:bg-[#1a1a1a]">
className="p-1.5 text-[#555] hover:text-red-400 transition-colors rounded hover:bg-[#231d18]">
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
<line x1="18" y1="6" x2="6" y2="18" /><line x1="6" y1="6" x2="18" y2="18" />
</svg>
@@ -789,7 +789,7 @@ export default function AdminArticlesPage() {
<button
onClick={() => handleStatusChange(article, 'published')}
title="Publish"
className="p-1.5 text-[#555] hover:text-green-400 transition-colors rounded hover:bg-[#1a1a1a]">
className="p-1.5 text-[#555] hover:text-green-400 transition-colors rounded hover:bg-[#231d18]">
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
<path d="M22 11.08V12a10 10 0 1 1-5.93-9.14" /><polyline points="22 4 12 14.01 9 11.01" />
</svg>
@@ -801,7 +801,7 @@ export default function AdminArticlesPage() {
<button
onClick={() => handleStatusChange(article, 'archived')}
title="Archive"
className="p-1.5 text-[#555] hover:text-yellow-400 transition-colors rounded hover:bg-[#1a1a1a]">
className="p-1.5 text-[#555] hover:text-yellow-400 transition-colors rounded hover:bg-[#231d18]">
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
<polyline points="21 8 21 21 3 21 3 8" /><rect x="1" y="3" width="22" height="5" /><line x1="10" y1="12" x2="14" y2="12" />
</svg>
@@ -812,7 +812,7 @@ export default function AdminArticlesPage() {
<button
onClick={() => handleDelete(article)}
title="Delete"
className="p-1.5 text-[#555] hover:text-red-400 transition-colors rounded hover:bg-[#1a1a1a]">
className="p-1.5 text-[#555] hover:text-red-400 transition-colors rounded hover:bg-[#231d18]">
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
<polyline points="3 6 5 6 21 6" />
<path d="M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6" />
@@ -834,12 +834,12 @@ export default function AdminArticlesPage() {
{/* New Article Modal */}
{showNewArticle && (
<div className="fixed inset-0 z-50 flex items-start justify-center p-4 bg-black/80 overflow-y-auto">
<div className="bg-[#111] border border-[#252525] rounded-xl w-full max-w-4xl shadow-2xl my-8">
<div className="bg-[#111] border border-[#3a322b] rounded-xl w-full max-w-4xl shadow-2xl my-8">
{/* Modal Header */}
<div className="flex items-center justify-between px-6 py-4 border-b border-[#252525] sticky top-0 bg-[#111] z-10 rounded-t-xl">
<div className="flex items-center justify-between px-6 py-4 border-b border-[#3a322b] sticky top-0 bg-[#111] z-10 rounded-t-xl">
<div className="flex items-center gap-3">
<h2 className="text-base font-bold text-white font-heading">New Article</h2>
<span className="text-[10px] font-bold uppercase tracking-wider text-[#ffb800] bg-[#ffb800]/10 px-1.5 py-0.5 rounded">Native</span>
<span className="text-[10px] font-bold uppercase tracking-wider text-[#F0A623] bg-[#F0A623]/10 px-1.5 py-0.5 rounded">Native</span>
</div>
<button
onClick={() => setShowNewArticle(false)}
@@ -859,7 +859,7 @@ export default function AdminArticlesPage() {
value={newTitle}
onChange={(e) => setNewTitle(e.target.value)}
placeholder="Enter article title..."
className="w-full bg-[#1a1a1a] border border-[#333] text-white text-lg font-bold rounded px-4 py-3 focus:outline-none focus:border-[#ffb800] placeholder-[#444]"
className="w-full bg-[#231d18] border border-[#333] text-white text-lg font-bold rounded px-4 py-3 focus:outline-none focus:border-[#F0A623] placeholder-[#444]"
/>
</div>
@@ -871,7 +871,7 @@ export default function AdminArticlesPage() {
onChange={(e) => setNewExcerpt(e.target.value)}
placeholder="Brief summary shown in article listings..."
rows={2}
className="w-full bg-[#1a1a1a] border border-[#333] text-[#cccccc] text-sm rounded px-3 py-2.5 focus:outline-none focus:border-[#ffb800] placeholder-[#444] resize-none"
className="w-full bg-[#231d18] border border-[#333] text-[#cccccc] text-sm rounded px-3 py-2.5 focus:outline-none focus:border-[#F0A623] placeholder-[#444] resize-none"
/>
</div>
@@ -894,7 +894,7 @@ export default function AdminArticlesPage() {
value={newAuthor}
onChange={(e) => setNewAuthor(e.target.value)}
placeholder="Author name"
className="w-full bg-[#1a1a1a] border border-[#333] text-[#cccccc] text-sm rounded px-3 py-2 focus:outline-none focus:border-[#ffb800] placeholder-[#444]"
className="w-full bg-[#231d18] border border-[#333] text-[#cccccc] text-sm rounded px-3 py-2 focus:outline-none focus:border-[#F0A623] placeholder-[#444]"
/>
</div>
<div>
@@ -904,7 +904,7 @@ export default function AdminArticlesPage() {
value={newCategory}
onChange={(e) => setNewCategory(e.target.value)}
placeholder="e.g. Technology"
className="w-full bg-[#1a1a1a] border border-[#333] text-[#cccccc] text-sm rounded px-3 py-2 focus:outline-none focus:border-[#ffb800] placeholder-[#444]"
className="w-full bg-[#231d18] border border-[#333] text-[#cccccc] text-sm rounded px-3 py-2 focus:outline-none focus:border-[#F0A623] placeholder-[#444]"
/>
</div>
<div>
@@ -912,7 +912,7 @@ export default function AdminArticlesPage() {
<select
value={newStatus}
onChange={(e) => setNewStatus(e.target.value as 'draft' | 'pending' | 'published')}
className="w-full bg-[#1a1a1a] border border-[#333] text-[#cccccc] text-sm rounded px-3 py-2 focus:outline-none focus:border-[#ffb800]">
className="w-full bg-[#231d18] border border-[#333] text-[#cccccc] text-sm rounded px-3 py-2 focus:outline-none focus:border-[#F0A623]">
<option value="draft">Draft</option>
<option value="pending">Submit for Review</option>
<option value="published">Publish Now</option>
@@ -929,7 +929,7 @@ export default function AdminArticlesPage() {
value={newFeaturedImage}
onChange={(e) => setNewFeaturedImage(e.target.value)}
placeholder="https://..."
className="w-full bg-[#1a1a1a] border border-[#333] text-[#cccccc] text-sm rounded px-3 py-2 focus:outline-none focus:border-[#ffb800] placeholder-[#444]"
className="w-full bg-[#231d18] border border-[#333] text-[#cccccc] text-sm rounded px-3 py-2 focus:outline-none focus:border-[#F0A623] placeholder-[#444]"
/>
</div>
<div>
@@ -939,21 +939,21 @@ export default function AdminArticlesPage() {
value={newFeaturedImageAlt}
onChange={(e) => setNewFeaturedImageAlt(e.target.value)}
placeholder="Describe the image..."
className="w-full bg-[#1a1a1a] border border-[#333] text-[#cccccc] text-sm rounded px-3 py-2 focus:outline-none focus:border-[#ffb800] placeholder-[#444]"
className="w-full bg-[#231d18] border border-[#333] text-[#cccccc] text-sm rounded px-3 py-2 focus:outline-none focus:border-[#F0A623] placeholder-[#444]"
/>
</div>
</div>
{/* Featured image preview */}
{newFeaturedImage && (
<div className="rounded-lg overflow-hidden border border-[#252525] max-h-48">
<div className="rounded-lg overflow-hidden border border-[#3a322b] max-h-48">
<img src={newFeaturedImage} alt={newFeaturedImageAlt || 'Featured image preview'} className="w-full h-48 object-cover" />
</div>
)}
</div>
{/* Modal Footer */}
<div className="flex items-center justify-between px-6 py-4 border-t border-[#252525] bg-[#0d0d0d] rounded-b-xl">
<div className="flex items-center justify-between px-6 py-4 border-t border-[#3a322b] bg-[#1c1815] rounded-b-xl">
<p className="text-[11px] text-[#444]">
{newStatus === 'draft' ? 'Saved as draft — not publicly visible' : newStatus === 'pending' ? 'Submitted for editorial review' : 'Will be published immediately'}
</p>
@@ -966,7 +966,7 @@ export default function AdminArticlesPage() {
<button
onClick={handleNewArticleSave}
disabled={newSaving || !newTitle.trim()}
className="flex items-center gap-2 bg-[#ffb800] hover:bg-[#d99700] disabled:bg-[#4a3500] disabled:cursor-not-allowed text-white text-sm font-bold px-5 py-2 rounded transition-colors">
className="flex items-center gap-2 bg-[#F0A623] hover:bg-[#BA7517] disabled:bg-[#412402] disabled:cursor-not-allowed text-white text-sm font-bold px-5 py-2 rounded transition-colors">
{newSaving ? (
<div className="w-3.5 h-3.5 border-2 border-white border-t-transparent rounded-full animate-spin" />
) : (
@@ -983,8 +983,8 @@ export default function AdminArticlesPage() {
{/* Edit Modal */}
{editingArticle && (
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/70">
<div className="bg-[#111] border border-[#252525] rounded-xl w-full max-w-lg shadow-2xl">
<div className="flex items-center justify-between px-5 py-4 border-b border-[#252525]">
<div className="bg-[#111] border border-[#3a322b] rounded-xl w-full max-w-lg shadow-2xl">
<div className="flex items-center justify-between px-5 py-4 border-b border-[#3a322b]">
<h2 className="text-base font-bold text-white font-heading">Edit Article</h2>
<button
onClick={() => setEditingArticle(null)}
@@ -1002,7 +1002,7 @@ export default function AdminArticlesPage() {
type="text"
value={editTitle}
onChange={(e) => setEditTitle(e.target.value)}
className="w-full bg-[#1a1a1a] border border-[#333] text-[#cccccc] text-sm rounded px-3 py-2 focus:outline-none focus:border-[#ffb800]"
className="w-full bg-[#231d18] border border-[#333] text-[#cccccc] text-sm rounded px-3 py-2 focus:outline-none focus:border-[#F0A623]"
/>
</div>
<div className="grid grid-cols-2 gap-4">
@@ -1012,7 +1012,7 @@ export default function AdminArticlesPage() {
type="text"
value={editAuthor}
onChange={(e) => setEditAuthor(e.target.value)}
className="w-full bg-[#1a1a1a] border border-[#333] text-[#cccccc] text-sm rounded px-3 py-2 focus:outline-none focus:border-[#ffb800]"
className="w-full bg-[#231d18] border border-[#333] text-[#cccccc] text-sm rounded px-3 py-2 focus:outline-none focus:border-[#F0A623]"
/>
</div>
<div>
@@ -1021,7 +1021,7 @@ export default function AdminArticlesPage() {
type="text"
value={editCategory}
onChange={(e) => setEditCategory(e.target.value)}
className="w-full bg-[#1a1a1a] border border-[#333] text-[#cccccc] text-sm rounded px-3 py-2 focus:outline-none focus:border-[#ffb800]"
className="w-full bg-[#231d18] border border-[#333] text-[#cccccc] text-sm rounded px-3 py-2 focus:outline-none focus:border-[#F0A623]"
/>
</div>
</div>
@@ -1030,7 +1030,7 @@ export default function AdminArticlesPage() {
<select
value={editStatus}
onChange={(e) => setEditStatus(e.target.value)}
className="w-full bg-[#1a1a1a] border border-[#333] text-[#cccccc] text-sm rounded px-3 py-2 focus:outline-none focus:border-[#ffb800]">
className="w-full bg-[#231d18] border border-[#333] text-[#cccccc] text-sm rounded px-3 py-2 focus:outline-none focus:border-[#F0A623]">
<option value="draft">Draft</option>
<option value="pending">Pending Review</option>
<option value="published">Published</option>
@@ -1040,19 +1040,19 @@ export default function AdminArticlesPage() {
{/* Scheduling — only shown for draft status */}
{editStatus === 'draft' && (
<div className="bg-[#0d1a2e] border border-[#4a3500] rounded-lg p-4">
<div className="bg-[#0d1a2e] border border-[#412402] rounded-lg p-4">
<div className="flex items-center gap-2 mb-3">
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" className="text-[#ffb800]">
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" className="text-[#F0A623]">
<circle cx="12" cy="12" r="10" /><polyline points="12 6 12 12 16 14" />
</svg>
<label className="text-xs text-[#ffb800] font-bold uppercase tracking-wider">Schedule Publish</label>
<label className="text-xs text-[#F0A623] font-bold uppercase tracking-wider">Schedule Publish</label>
</div>
<input
type="datetime-local"
value={editScheduledAt}
onChange={(e) => setEditScheduledAt(e.target.value)}
min={new Date().toISOString().slice(0, 16)}
className="w-full bg-[#111] border border-[#333] text-[#cccccc] text-sm rounded px-3 py-2 focus:outline-none focus:border-[#ffb800] [color-scheme:dark]"
className="w-full bg-[#111] border border-[#333] text-[#cccccc] text-sm rounded px-3 py-2 focus:outline-none focus:border-[#F0A623] [color-scheme:dark]"
/>
{editScheduledAt && (
<div className="flex items-center justify-between mt-2">
@@ -1074,14 +1074,14 @@ export default function AdminArticlesPage() {
<div className="flex items-center gap-2 pt-1">
<span className="text-xs text-[#555]">Source:</span>
<span className={`text-[10px] font-bold uppercase tracking-wider px-1.5 py-0.5 rounded ${
editingArticle.source === 'imported' ? 'text-purple-400 bg-purple-400/10' : 'text-[#ffb800] bg-[#ffb800]/10'
editingArticle.source === 'imported' ? 'text-purple-400 bg-purple-400/10' : 'text-[#F0A623] bg-[#F0A623]/10'
}`}>
{editingArticle.source === 'imported' ? 'WordPress Import' : 'Native'}
</span>
</div>
</div>
<div className="flex items-center justify-end gap-3 px-5 py-4 border-t border-[#252525]">
<div className="flex items-center justify-end gap-3 px-5 py-4 border-t border-[#3a322b]">
<button
onClick={() => setEditingArticle(null)}
className="text-[#666] hover:text-[#999] text-sm transition-colors px-4 py-2">
@@ -1090,7 +1090,7 @@ export default function AdminArticlesPage() {
<button
onClick={handleEditSave}
disabled={editSaving || !editTitle.trim()}
className="flex items-center gap-2 bg-[#ffb800] hover:bg-[#d99700] disabled:bg-[#4a3500] disabled:cursor-not-allowed text-white text-sm font-bold px-5 py-2 rounded transition-colors">
className="flex items-center gap-2 bg-[#F0A623] hover:bg-[#BA7517] disabled:bg-[#412402] disabled:cursor-not-allowed text-white text-sm font-bold px-5 py-2 rounded transition-colors">
{editSaving ? (
<div className="w-3.5 h-3.5 border-2 border-white border-t-transparent rounded-full animate-spin" />
) : null}

View File

@@ -50,13 +50,13 @@ const ACTION_COLORS: Record<string, string> = {
article_published: 'text-green-400 bg-green-400/10',
article_unpublished: 'text-yellow-400 bg-yellow-400/10',
article_deleted: 'text-red-400 bg-red-400/10',
article_archived: 'text-[#888] bg-[#1a1a1a]',
article_archived: 'text-[#888] bg-[#231d18]',
article_status_changed: 'text-purple-400 bg-purple-400/10',
user_role_changed: 'text-orange-400 bg-orange-400/10',
user_activated: 'text-green-400 bg-green-400/10',
user_deactivated: 'text-red-400 bg-red-400/10',
user_invited: 'text-blue-400 bg-blue-400/10',
user_invitation_revoked: 'text-[#888] bg-[#1a1a1a]',
user_invitation_revoked: 'text-[#888] bg-[#231d18]',
import_completed: 'text-teal-400 bg-teal-400/10',
import_failed: 'text-red-400 bg-red-400/10',
};
@@ -150,7 +150,7 @@ interface StatCardProps {
function StatCard({ label, value, accent, icon }: StatCardProps) {
return (
<div className="bg-[#111] border border-[#252525] rounded-lg p-4 flex items-center gap-4">
<div className="bg-[#111] border border-[#3a322b] rounded-lg p-4 flex items-center gap-4">
<div className={`w-10 h-10 rounded-lg flex items-center justify-center flex-shrink-0 ${accent}`}>
{icon}
</div>
@@ -170,16 +170,16 @@ interface DetailModalProps {
}
function DetailModal({ log, onClose }: DetailModalProps) {
const actionColor = ACTION_COLORS[log.action] || 'text-[#888] bg-[#1a1a1a]';
const actionColor = ACTION_COLORS[log.action] || 'text-[#888] bg-[#231d18]';
const actionLabel = ACTION_LABELS[log.action] || log.action;
const performerName = log.user_profiles?.full_name || log.performed_by_email || 'System';
return (
<div className="fixed inset-0 z-50 flex items-center justify-center p-4">
<div className="absolute inset-0 bg-black/70" onClick={onClose} />
<div className="relative bg-[#111] border border-[#252525] rounded-xl w-full max-w-lg shadow-2xl">
<div className="relative bg-[#111] border border-[#3a322b] rounded-xl w-full max-w-lg shadow-2xl">
{/* Header */}
<div className="flex items-center justify-between p-5 border-b border-[#252525]">
<div className="flex items-center justify-between p-5 border-b border-[#3a322b]">
<div className="flex items-center gap-3">
<div className={`w-8 h-8 rounded-lg flex items-center justify-center ${actionColor}`}>
{ACTION_ICONS[log.action] || getDefaultIcon(log.action)}
@@ -247,7 +247,7 @@ function DetailModal({ log, onClose }: DetailModalProps) {
{log.metadata && Object.keys(log.metadata).length > 0 && (
<div>
<p className="text-[#555] text-xs font-body uppercase tracking-wider mb-1">Metadata</p>
<div className="bg-[#0d0d0d] border border-[#1e1e1e] rounded-lg p-3">
<div className="bg-[#1c1815] border border-[#1e1e1e] rounded-lg p-3">
<pre className="text-[#aaa] text-xs font-mono whitespace-pre-wrap break-all">
{JSON.stringify(log.metadata, null, 2)}
</pre>
@@ -340,7 +340,7 @@ export default function AuditLogPage() {
if (loading) {
return (
<div className="min-h-screen bg-[#0a0a0a] flex items-center justify-center">
<div className="w-6 h-6 border-2 border-[#ffb800] border-t-transparent rounded-full animate-spin" />
<div className="w-6 h-6 border-2 border-[#F0A623] border-t-transparent rounded-full animate-spin" />
</div>
);
}
@@ -368,7 +368,7 @@ export default function AuditLogPage() {
</div>
<button
onClick={fetchLogs}
className="flex items-center gap-2 px-3 py-2 bg-[#111] border border-[#252525] rounded-lg text-[#888] hover:text-white hover:border-[#333] transition-all text-sm font-body"
className="flex items-center gap-2 px-3 py-2 bg-[#111] border border-[#3a322b] rounded-lg text-[#888] hover:text-white hover:border-[#333] transition-all text-sm font-body"
>
<svg className="w-4 h-4" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
@@ -382,7 +382,7 @@ export default function AuditLogPage() {
<StatCard
label="Total Events"
value={logs.length}
accent="bg-[#ffb800]/10 text-[#ffb800]"
accent="bg-[#F0A623]/10 text-[#F0A623]"
icon={
<svg className="w-5 h-5" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2" />
@@ -433,19 +433,19 @@ export default function AuditLogPage() {
placeholder="Search by user, item, or action…"
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="w-full bg-[#111] border border-[#252525] rounded-lg pl-9 pr-4 py-2.5 text-sm text-white placeholder-[#444] focus:outline-none focus:border-[#ffb800] font-body"
className="w-full bg-[#111] border border-[#3a322b] rounded-lg pl-9 pr-4 py-2.5 text-sm text-white placeholder-[#444] focus:outline-none focus:border-[#F0A623] font-body"
/>
</div>
{/* Action Type Filter */}
<div className="flex items-center gap-1 bg-[#111] border border-[#252525] rounded-lg p-1">
<div className="flex items-center gap-1 bg-[#111] border border-[#3a322b] rounded-lg p-1">
{(['all', 'article', 'user', 'import'] as ActionFilter[]).map((f) => (
<button
key={f}
onClick={() => setActionFilter(f)}
className={`px-3 py-1.5 rounded-md text-xs font-body font-semibold capitalize transition-all ${
actionFilter === f
? 'bg-[#ffb800] text-white'
? 'bg-[#F0A623] text-white'
: 'text-[#555] hover:text-[#888]'
}`}
>
@@ -466,9 +466,9 @@ export default function AuditLogPage() {
)}
{/* Table */}
<div className="bg-[#111] border border-[#252525] rounded-xl overflow-hidden">
<div className="bg-[#111] border border-[#3a322b] rounded-xl overflow-hidden">
{/* Table Header */}
<div className="hidden md:grid grid-cols-[2fr_1.5fr_1.5fr_1fr_auto] gap-4 px-5 py-3 border-b border-[#1e1e1e] bg-[#0d0d0d]">
<div className="hidden md:grid grid-cols-[2fr_1.5fr_1.5fr_1fr_auto] gap-4 px-5 py-3 border-b border-[#1e1e1e] bg-[#1c1815]">
<p className="text-[#444] text-xs font-body font-semibold uppercase tracking-wider">Action</p>
<p className="text-[#444] text-xs font-body font-semibold uppercase tracking-wider">Performed By</p>
<p className="text-[#444] text-xs font-body font-semibold uppercase tracking-wider">Affected Item</p>
@@ -480,7 +480,7 @@ export default function AuditLogPage() {
{loadingData && (
<div className="flex items-center justify-center py-16">
<div className="flex flex-col items-center gap-3">
<div className="w-6 h-6 border-2 border-[#ffb800] border-t-transparent rounded-full animate-spin" />
<div className="w-6 h-6 border-2 border-[#F0A623] border-t-transparent rounded-full animate-spin" />
<p className="text-[#555] text-sm font-body">Loading audit logs</p>
</div>
</div>
@@ -489,14 +489,14 @@ export default function AuditLogPage() {
{/* Empty */}
{!loadingData && filteredLogs.length === 0 && (
<div className="flex flex-col items-center justify-center py-16 gap-3">
<div className="w-12 h-12 rounded-full bg-[#1a1a1a] flex items-center justify-center">
<div className="w-12 h-12 rounded-full bg-[#231d18] flex items-center justify-center">
<svg className="w-6 h-6 text-[#333]" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2" />
</svg>
</div>
<p className="text-[#555] text-sm font-body">No audit log entries found</p>
{searchQuery && (
<button onClick={() => setSearchQuery('')} className="text-[#ffb800] text-xs font-body hover:underline">
<button onClick={() => setSearchQuery('')} className="text-[#F0A623] text-xs font-body hover:underline">
Clear search
</button>
)}
@@ -505,7 +505,7 @@ export default function AuditLogPage() {
{/* Rows */}
{!loadingData && filteredLogs.map((log, idx) => {
const actionColor = ACTION_COLORS[log.action] || 'text-[#888] bg-[#1a1a1a]';
const actionColor = ACTION_COLORS[log.action] || 'text-[#888] bg-[#231d18]';
const actionLabel = ACTION_LABELS[log.action] || log.action;
const icon = ACTION_ICONS[log.action] || getDefaultIcon(log.action);
const performerName = log.user_profiles?.full_name || log.performed_by_email || 'System';
@@ -513,8 +513,8 @@ export default function AuditLogPage() {
return (
<div
key={log.id}
className={`grid grid-cols-1 md:grid-cols-[2fr_1.5fr_1.5fr_1fr_auto] gap-3 md:gap-4 px-5 py-4 hover:bg-[#0d0d0d] transition-colors cursor-pointer ${
idx !== filteredLogs.length - 1 ? 'border-b border-[#1a1a1a]' : ''
className={`grid grid-cols-1 md:grid-cols-[2fr_1.5fr_1.5fr_1fr_auto] gap-3 md:gap-4 px-5 py-4 hover:bg-[#1c1815] transition-colors cursor-pointer ${
idx !== filteredLogs.length - 1 ? 'border-b border-[#231d18]' : ''
}`}
onClick={() => setSelectedLog(log)}
>
@@ -530,7 +530,7 @@ export default function AuditLogPage() {
{/* Performed By */}
<div className="flex items-center gap-2 min-w-0">
<div className="w-6 h-6 rounded-full bg-[#1a1a1a] border border-[#252525] flex items-center justify-center flex-shrink-0">
<div className="w-6 h-6 rounded-full bg-[#231d18] border border-[#3a322b] flex items-center justify-center flex-shrink-0">
<span className="text-[#888] text-xs font-body font-bold">
{performerName.charAt(0).toUpperCase()}
</span>
@@ -569,7 +569,7 @@ export default function AuditLogPage() {
<div className="flex items-center">
<button
onClick={(e) => { e.stopPropagation(); setSelectedLog(log); }}
className="text-[#444] hover:text-[#ffb800] transition-colors p-1"
className="text-[#444] hover:text-[#F0A623] transition-colors p-1"
aria-label="View details"
>
<svg className="w-4 h-4" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24">
@@ -593,7 +593,7 @@ export default function AuditLogPage() {
<button
onClick={() => setPage((p) => Math.max(0, p - 1))}
disabled={page === 0}
className="px-3 py-1.5 bg-[#111] border border-[#252525] rounded-lg text-xs font-body text-[#888] hover:text-white hover:border-[#333] disabled:opacity-30 disabled:cursor-not-allowed transition-all"
className="px-3 py-1.5 bg-[#111] border border-[#3a322b] rounded-lg text-xs font-body text-[#888] hover:text-white hover:border-[#333] disabled:opacity-30 disabled:cursor-not-allowed transition-all"
>
Previous
</button>
@@ -601,7 +601,7 @@ export default function AuditLogPage() {
<button
onClick={() => setPage((p) => p + 1)}
disabled={filteredLogs.length < PAGE_SIZE}
className="px-3 py-1.5 bg-[#111] border border-[#252525] rounded-lg text-xs font-body text-[#888] hover:text-white hover:border-[#333] disabled:opacity-30 disabled:cursor-not-allowed transition-all"
className="px-3 py-1.5 bg-[#111] border border-[#3a322b] rounded-lg text-xs font-body text-[#888] hover:text-white hover:border-[#333] disabled:opacity-30 disabled:cursor-not-allowed transition-all"
>
Next
</button>

View File

@@ -162,7 +162,7 @@ export default function BannedTermsPage() {
if (loading || loadingData) {
return (
<div className="min-h-screen bg-[#0a0a0a] flex items-center justify-center">
<div className="w-8 h-8 border-2 border-[#ffb800] border-t-transparent rounded-full animate-spin" />
<div className="w-8 h-8 border-2 border-[#F0A623] border-t-transparent rounded-full animate-spin" />
</div>
);
}
@@ -190,17 +190,17 @@ export default function BannedTermsPage() {
<p className="text-[#555] text-sm font-body mt-1">Manage restricted company names and keywords visible to administrators only</p>
</div>
<div className="flex items-center gap-2">
<button onClick={exportCSV} className="flex items-center gap-2 px-3 py-2 bg-[#1a1a1a] border border-[#333] text-[#888] hover:text-white text-sm font-body rounded-lg transition-colors">
<button onClick={exportCSV} className="flex items-center gap-2 px-3 py-2 bg-[#231d18] border border-[#333] text-[#888] hover:text-white text-sm font-body rounded-lg transition-colors">
<svg className="w-4 h-4" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4" /></svg>
Export CSV
</button>
<button onClick={handleRetroactiveScan} disabled={scanning}
className="flex items-center gap-2 px-3 py-2 bg-[#1a1a1a] border border-[#333] text-[#888] hover:text-white text-sm font-body rounded-lg transition-colors disabled:opacity-50">
className="flex items-center gap-2 px-3 py-2 bg-[#231d18] border border-[#333] text-[#888] hover:text-white text-sm font-body rounded-lg transition-colors disabled:opacity-50">
{scanning ? <div className="w-4 h-4 border-2 border-[#888]/30 border-t-[#888] rounded-full animate-spin" /> : <svg className="w-4 h-4" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" /></svg>}
Scan Published Posts
</button>
<button onClick={() => { setForm(EMPTY_FORM); setEditingId(null); setShowAddForm(true); }}
className="flex items-center gap-2 px-4 py-2 bg-[#ffb800] hover:bg-blue-500 text-white text-sm font-body font-semibold rounded-lg transition-colors">
className="flex items-center gap-2 px-4 py-2 bg-[#F0A623] hover:bg-blue-500 text-white text-sm font-body font-semibold rounded-lg transition-colors">
<svg className="w-4 h-4" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" d="M12 4v16m8-8H4" /></svg>
Add Term
</button>
@@ -209,7 +209,7 @@ export default function BannedTermsPage() {
{/* Retroactive scan results */}
{scanResults !== null && (
<div className="mb-6 bg-[#111] border border-[#252525] rounded-lg p-5">
<div className="mb-6 bg-[#111] border border-[#3a322b] rounded-lg p-5">
<div className="flex items-center justify-between mb-4">
<h3 className="text-sm font-display font-bold text-white">Retroactive Scan Results {scanResults.length} match{scanResults.length !== 1 ? 'es' : ''} found</h3>
<button onClick={() => setScanResults(null)} className="text-[#555] hover:text-white text-xs font-body">Dismiss</button>
@@ -234,18 +234,18 @@ export default function BannedTermsPage() {
{/* Add/Edit Form */}
{showAddForm && (
<div className="mb-6 bg-[#111] border border-[#252525] rounded-lg p-6">
<div className="mb-6 bg-[#111] border border-[#3a322b] rounded-lg p-6">
<h3 className="text-sm font-display font-bold text-white mb-4">{editingId ? 'Edit Term' : 'Add Banned Term'}</h3>
<form onSubmit={handleSave} className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="md:col-span-2">
<label className="block text-xs font-body text-[#888] mb-1.5">Term / Company Name *</label>
<input type="text" value={form.term} onChange={e => 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-[#ffb800]" required />
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-[#F0A623]" required />
</div>
<div>
<label className="block text-xs font-body text-[#888] mb-1.5">Ban Level</label>
<select value={form.ban_level} onChange={e => setForm(f => ({ ...f, ban_level: e.target.value as 'soft' | 'hard' }))}
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-[#ffb800]">
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-[#F0A623]">
<option value="soft">Soft Ban Flag for review (contributor doesn't know)</option>
<option value="hard">Hard Ban — Block submission entirely</option>
</select>
@@ -253,7 +253,7 @@ export default function BannedTermsPage() {
<div>
<label className="block text-xs font-body text-[#888] mb-1.5">Site Scope</label>
<select value={form.site_scope} onChange={e => setForm(f => ({ ...f, site_scope: e.target.value as any }))}
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-[#ffb800]">
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-[#F0A623]">
<option value="both">Both Sites</option>
<option value="avbeat">AV Beat only</option>
<option value="avbeat">AV Beat only</option>
@@ -262,15 +262,15 @@ export default function BannedTermsPage() {
<div className="md:col-span-2">
<label className="block text-xs font-body text-[#888] mb-1.5">Internal Notes (not shown to contributors)</label>
<textarea value={form.notes} onChange={e => setForm(f => ({ ...f, notes: e.target.value }))} rows={2} placeholder="Reason for ban, context..."
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-[#ffb800] resize-none" />
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-[#F0A623] resize-none" />
</div>
<div className="md:col-span-2 flex gap-3">
<button type="submit" disabled={saving}
className="px-5 py-2 bg-[#ffb800] hover:bg-blue-500 text-white text-sm font-body font-semibold rounded-lg transition-colors disabled:opacity-50">
className="px-5 py-2 bg-[#F0A623] hover:bg-blue-500 text-white text-sm font-body font-semibold rounded-lg transition-colors disabled:opacity-50">
{saving ? 'Saving...' : editingId ? 'Update Term' : 'Add Term'}
</button>
<button type="button" onClick={() => { setShowAddForm(false); setEditingId(null); setForm(EMPTY_FORM); }}
className="px-4 py-2 bg-[#1a1a1a] border border-[#333] text-[#888] hover:text-white text-sm font-body rounded-lg transition-colors">
className="px-4 py-2 bg-[#231d18] border border-[#333] text-[#888] hover:text-white text-sm font-body rounded-lg transition-colors">
Cancel
</button>
</div>
@@ -281,22 +281,22 @@ export default function BannedTermsPage() {
{/* Filters */}
<div className="mb-4 flex flex-wrap gap-3">
<input type="text" value={searchTerm} onChange={e => setSearchTerm(e.target.value)} placeholder="Search terms..."
className="bg-[#111] border border-[#252525] rounded-lg px-3 py-2 text-white text-sm font-body focus:outline-none focus:border-[#ffb800] w-48" />
className="bg-[#111] border border-[#3a322b] rounded-lg px-3 py-2 text-white text-sm font-body focus:outline-none focus:border-[#F0A623] w-48" />
<select value={filterBanLevel} onChange={e => setFilterBanLevel(e.target.value)}
className="bg-[#111] border border-[#252525] rounded-lg px-3 py-2 text-[#888] text-sm font-body focus:outline-none focus:border-[#ffb800]">
className="bg-[#111] border border-[#3a322b] rounded-lg px-3 py-2 text-[#888] text-sm font-body focus:outline-none focus:border-[#F0A623]">
<option value="">All Ban Levels</option>
<option value="soft">Soft Ban</option>
<option value="hard">Hard Ban</option>
</select>
<select value={filterSiteScope} onChange={e => setFilterSiteScope(e.target.value)}
className="bg-[#111] border border-[#252525] rounded-lg px-3 py-2 text-[#888] text-sm font-body focus:outline-none focus:border-[#ffb800]">
className="bg-[#111] border border-[#3a322b] rounded-lg px-3 py-2 text-[#888] text-sm font-body focus:outline-none focus:border-[#F0A623]">
<option value="">All Sites</option>
<option value="avbeat">AV Beat</option>
<option value="avbeat">AV Beat</option>
<option value="both">Both</option>
</select>
<select value={filterActive} onChange={e => setFilterActive(e.target.value)}
className="bg-[#111] border border-[#252525] rounded-lg px-3 py-2 text-[#888] text-sm font-body focus:outline-none focus:border-[#ffb800]">
className="bg-[#111] border border-[#3a322b] rounded-lg px-3 py-2 text-[#888] text-sm font-body focus:outline-none focus:border-[#F0A623]">
<option value="">All Status</option>
<option value="true">Active</option>
<option value="false">Inactive</option>
@@ -304,8 +304,8 @@ export default function BannedTermsPage() {
</div>
{/* Terms Table */}
<div className="bg-[#111] border border-[#252525] rounded-lg overflow-hidden">
<div className="px-5 py-4 border-b border-[#1a1a1a] flex items-center justify-between">
<div className="bg-[#111] border border-[#3a322b] rounded-lg overflow-hidden">
<div className="px-5 py-4 border-b border-[#231d18] flex items-center justify-between">
<h2 className="text-sm font-display font-bold text-white">Banned Terms ({terms.length})</h2>
</div>
{terms.length === 0 ? (
@@ -316,15 +316,15 @@ export default function BannedTermsPage() {
<div className="overflow-x-auto">
<table className="w-full">
<thead>
<tr className="border-b border-[#1a1a1a]">
<tr className="border-b border-[#231d18]">
{['Term', 'Ban Level', 'Site Scope', 'Status', 'Date Added', 'Added By', 'Notes', 'Actions'].map(h => (
<th key={h} className="px-4 py-3 text-left text-xs font-body font-bold text-[#555] uppercase tracking-wider">{h}</th>
))}
</tr>
</thead>
<tbody className="divide-y divide-[#1a1a1a]">
<tbody className="divide-y divide-[#231d18]">
{terms.map(term => (
<tr key={term.id} className={`hover:bg-[#0d0d0d] transition-colors ${!term.is_active ? 'opacity-50' : ''}`}>
<tr key={term.id} className={`hover:bg-[#1c1815] transition-colors ${!term.is_active ? 'opacity-50' : ''}`}>
<td className="px-4 py-3">
<span className="text-sm font-body font-semibold text-white">{term.term}</span>
</td>
@@ -339,7 +339,7 @@ export default function BannedTermsPage() {
<span className="text-xs text-[#888] font-body capitalize">{term.site_scope === 'both' ? 'Both Sites' : term.site_scope === 'avbeat' ? 'AV Beat' : 'AV Beat'}</span>
</td>
<td className="px-4 py-3">
<span className={`text-xs px-2 py-0.5 rounded-full font-body ${term.is_active ? 'bg-green-500/10 text-green-400' : 'bg-[#1a1a1a] text-[#555]'}`}>
<span className={`text-xs px-2 py-0.5 rounded-full font-body ${term.is_active ? 'bg-green-500/10 text-green-400' : 'bg-[#231d18] text-[#555]'}`}>
{term.is_active ? 'Active' : 'Inactive'}
</span>
</td>
@@ -348,7 +348,7 @@ export default function BannedTermsPage() {
<td className="px-4 py-3 text-xs text-[#555] font-body max-w-xs truncate">{term.notes || ''}</td>
<td className="px-4 py-3">
<div className="flex items-center gap-2">
<button onClick={() => handleEdit(term)} className="text-xs text-[#ffb800] hover:text-blue-300 font-body transition-colors">Edit</button>
<button onClick={() => handleEdit(term)} className="text-xs text-[#F0A623] hover:text-blue-300 font-body transition-colors">Edit</button>
<button onClick={() => handleDeactivate(term.id, term.is_active)} disabled={actionLoading === term.id}
className="text-xs text-[#888] hover:text-white font-body transition-colors disabled:opacity-50">
{term.is_active ? 'Deactivate' : 'Reactivate'}
@@ -366,7 +366,7 @@ export default function BannedTermsPage() {
</div>
{/* Legend */}
<div className="mt-6 bg-[#111] border border-[#252525] rounded-lg p-5">
<div className="mt-6 bg-[#111] border border-[#3a322b] rounded-lg p-5">
<h3 className="text-xs font-body font-bold text-[#555] uppercase tracking-widest mb-3">Ban Level Reference</h3>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>

View File

@@ -54,7 +54,7 @@ function StatBlock({ label, b }: { label: string; b: StatBucket }) {
<span className="font-semibold">{fmtNum(b.clicks)}</span>
<span className="text-[#6b7280]"> clk</span>
<span className="mx-1 text-[#374151]">·</span>
<span className="text-[#ffd25a]">{pct(b.clicks, b.impressions)}</span>
<span className="text-[#F0A623]">{pct(b.clicks, b.impressions)}</span>
</div>
</div>
);
@@ -140,7 +140,7 @@ export default function BannerRows({
<input
value={current.name}
onChange={(e) => field(b.id, "name", e.target.value)}
className="rounded border border-[#1f2937] bg-[#070a10] p-2 text-[#e5e7eb] focus:border-[#ffb800] focus:outline-none"
className="rounded border border-[#1f2937] bg-[#070a10] p-2 text-[#e5e7eb] focus:border-[#F0A623] focus:outline-none"
/>
</label>
@@ -150,7 +150,7 @@ export default function BannerRows({
value={current.click_url || ""}
onChange={(e) => field(b.id, "click_url", e.target.value)}
placeholder="https://…"
className="rounded border border-[#1f2937] bg-[#070a10] p-2 text-[#e5e7eb] focus:border-[#ffb800] focus:outline-none"
className="rounded border border-[#1f2937] bg-[#070a10] p-2 text-[#e5e7eb] focus:border-[#F0A623] focus:outline-none"
/>
</label>
@@ -160,7 +160,7 @@ export default function BannerRows({
type="date"
value={fmtDate(current.start_date)}
onChange={(e) => field(b.id, "start_date", e.target.value ? new Date(e.target.value).toISOString() : null)}
className="rounded border border-[#1f2937] bg-[#070a10] p-2 text-[#e5e7eb] focus:border-[#ffb800] focus:outline-none"
className="rounded border border-[#1f2937] bg-[#070a10] p-2 text-[#e5e7eb] focus:border-[#F0A623] focus:outline-none"
/>
</label>
@@ -170,7 +170,7 @@ export default function BannerRows({
type="date"
value={fmtDate(current.end_date)}
onChange={(e) => field(b.id, "end_date", e.target.value ? new Date(e.target.value).toISOString() : null)}
className="rounded border border-[#1f2937] bg-[#070a10] p-2 text-[#e5e7eb] focus:border-[#ffb800] focus:outline-none"
className="rounded border border-[#1f2937] bg-[#070a10] p-2 text-[#e5e7eb] focus:border-[#F0A623] focus:outline-none"
/>
</label>
@@ -179,7 +179,7 @@ export default function BannerRows({
<select
value={current.status}
onChange={(e) => field(b.id, "status", e.target.value)}
className="rounded border border-[#1f2937] bg-[#070a10] p-2 text-[#e5e7eb] focus:border-[#ffb800] focus:outline-none"
className="rounded border border-[#1f2937] bg-[#070a10] p-2 text-[#e5e7eb] focus:border-[#F0A623] focus:outline-none"
>
<option value="active">active</option>
<option value="scheduled">scheduled</option>
@@ -210,7 +210,7 @@ export default function BannerRows({
<button
onClick={() => save(b.id)}
disabled={!dirty || pending}
className="rounded bg-[#1d4ed8] px-3 py-1.5 text-sm font-medium text-white hover:bg-[#d99700] disabled:opacity-40 disabled:cursor-not-allowed"
className="rounded bg-[#1d4ed8] px-3 py-1.5 text-sm font-medium text-white hover:bg-[#BA7517] disabled:opacity-40 disabled:cursor-not-allowed"
>
Save changes
</button>
@@ -219,7 +219,7 @@ export default function BannerRows({
</div>
<Link
href={`/admin/banners/${b.id}/analytics`}
className="text-sm text-[#ffd25a] hover:underline"
className="text-sm text-[#F0A623] hover:underline"
>
View detailed analytics
</Link>

View File

@@ -68,7 +68,7 @@ export default function AnalyticsCharts({
</div>
<section className="grid grid-cols-1 md:grid-cols-2 gap-4 mb-6">
<ChartCard title="Impressions per day" data={imp} stroke="#ffd25a" />
<ChartCard title="Impressions per day" data={imp} stroke="#F0A623" />
<ChartCard title="Clicks per day" data={clk} stroke="#34d399" />
</section>
@@ -121,7 +121,7 @@ function TopListCard({ title, rows }: { title: string; rows: TopRow[] }) {
{rows.map((r) => (
<tr key={r.label} className="border-b border-[#1f2937] last:border-0">
<td className="py-2 pr-3 text-[#e5e7eb] truncate max-w-[400px]" title={r.label}>{r.label}</td>
<td className="py-2 text-right text-[#ffd25a]">{r.count.toLocaleString()}</td>
<td className="py-2 text-right text-[#F0A623]">{r.count.toLocaleString()}</td>
</tr>
))}
</tbody>

View File

@@ -142,7 +142,7 @@ export default async function BannerAnalyticsPage({
return (
<main className="mx-auto max-w-6xl px-6 py-10 text-[#e5e7eb]">
<Link href="/admin/banners" className="text-sm text-[#ffd25a] hover:underline">
<Link href="/admin/banners" className="text-sm text-[#F0A623] hover:underline">
Back to banners
</Link>
@@ -159,7 +159,7 @@ export default async function BannerAnalyticsPage({
<p className="text-sm text-[#9ca3af]">
{b.size} · {b.status} ·{" "}
{b.click_url && (
<a className="text-[#ffd25a] hover:underline" href={b.click_url} target="_blank" rel="noopener noreferrer">
<a className="text-[#F0A623] hover:underline" href={b.click_url} target="_blank" rel="noopener noreferrer">
{b.click_url.slice(0, 80)}{b.click_url.length > 80 ? "…" : ""}
</a>
)}

View File

@@ -113,7 +113,7 @@ export default async function AdminBannersPage() {
after the next refresh.
</p>
</div>
<Link href="/admin" className="text-sm text-[#ffd25a] hover:underline">
<Link href="/admin" className="text-sm text-[#F0A623] hover:underline">
Admin home
</Link>
</header>

View File

@@ -171,7 +171,7 @@ export default function CompanyCoveragePage() {
return (
<div className="min-h-screen bg-[#0a0a0a] flex items-center justify-center">
<div className="flex flex-col items-center gap-3">
<div className="w-8 h-8 border-2 border-[#ffb800] border-t-transparent rounded-full animate-spin" />
<div className="w-8 h-8 border-2 border-[#F0A623] border-t-transparent rounded-full animate-spin" />
<p className="text-[#555] text-sm font-body">Loading company coverage</p>
</div>
</div>
@@ -218,7 +218,7 @@ export default function CompanyCoveragePage() {
<button
onClick={handleScan}
disabled={scanning}
className="flex items-center gap-2 px-4 py-2 bg-[#ffb800] hover:bg-blue-500 disabled:opacity-50 disabled:cursor-not-allowed text-white text-sm font-body font-semibold rounded-lg transition-colors"
className="flex items-center gap-2 px-4 py-2 bg-[#F0A623] hover:bg-blue-500 disabled:opacity-50 disabled:cursor-not-allowed text-white text-sm font-body font-semibold rounded-lg transition-colors"
>
{scanning ? (
<>
@@ -255,14 +255,14 @@ export default function CompanyCoveragePage() {
<div className="lg:col-span-2">
{/* Tabs */}
<div className="flex gap-1 mb-4 bg-[#111] border border-[#252525] rounded-lg p-1 w-fit">
<div className="flex gap-1 mb-4 bg-[#111] border border-[#3a322b] rounded-lg p-1 w-fit">
{(['queued', 'published', 'dismissed'] as const).map((tab) => (
<button
key={tab}
onClick={() => setActiveTab(tab)}
className={`px-4 py-1.5 rounded-md text-sm font-body font-semibold capitalize transition-colors ${
activeTab === tab
? 'bg-[#ffb800] text-white'
? 'bg-[#F0A623] text-white'
: 'text-[#888] hover:text-white'
}`}
>
@@ -272,7 +272,7 @@ export default function CompanyCoveragePage() {
</div>
{/* Queue List */}
<div className="bg-[#111] border border-[#252525] rounded-lg overflow-hidden">
<div className="bg-[#111] border border-[#3a322b] rounded-lg overflow-hidden">
{queue.length === 0 ? (
<div className="px-5 py-16 text-center">
<svg className="w-10 h-10 text-[#333] mx-auto mb-3" fill="none" stroke="currentColor" strokeWidth="1.5" viewBox="0 0 24 24">
@@ -282,13 +282,13 @@ export default function CompanyCoveragePage() {
{activeTab === 'queued' ? 'No stories queued — run a scan to detect companies' : `No ${activeTab} stories`}
</p>
{activeTab === 'queued' && (
<button onClick={handleScan} disabled={scanning} className="mt-3 text-xs text-[#ffb800] hover:text-blue-300 font-body underline">
<button onClick={handleScan} disabled={scanning} className="mt-3 text-xs text-[#F0A623] hover:text-blue-300 font-body underline">
Run scan now
</button>
)}
</div>
) : (
<div className="divide-y divide-[#1a1a1a]">
<div className="divide-y divide-[#231d18]">
{queue.map((item) => (
<div key={item.id} className="p-4">
<div className="flex items-start justify-between gap-3">
@@ -332,7 +332,7 @@ export default function CompanyCoveragePage() {
</button>
<button
onClick={() => setExpandedItem(expandedItem === item.id ? null : item.id)}
className="px-3 py-1.5 bg-[#1a1a1a] hover:bg-[#222] text-[#888] text-xs font-body rounded-md transition-colors"
className="px-3 py-1.5 bg-[#231d18] hover:bg-[#222] text-[#888] text-xs font-body rounded-md transition-colors"
>
{expandedItem === item.id ? 'Hide' : 'Preview'}
</button>
@@ -349,7 +349,7 @@ export default function CompanyCoveragePage() {
{/* Expanded Draft Preview */}
{expandedItem === item.id && (
<div className="mt-4 bg-[#0d0d0d] border border-[#1a1a1a] rounded-lg p-4">
<div className="mt-4 bg-[#1c1815] border border-[#231d18] rounded-lg p-4">
<p className="text-xs font-body font-bold text-[#555] uppercase tracking-widest mb-3">AI Draft Preview</p>
<h3 className="text-base font-display font-bold text-white mb-2">{item.ai_draft_title}</h3>
<p className="text-sm text-[#888] font-body italic mb-3">{item.ai_draft_excerpt}</p>
@@ -370,11 +370,11 @@ export default function CompanyCoveragePage() {
<div className="flex flex-col gap-4">
{/* Auto-Pilot Toggle */}
<div className={`bg-[#111] border rounded-lg p-5 transition-colors ${settings.autopilot_enabled ? 'border-green-500/40' : 'border-[#252525]'}`}>
<div className={`bg-[#111] border rounded-lg p-5 transition-colors ${settings.autopilot_enabled ? 'border-green-500/40' : 'border-[#3a322b]'}`}>
<div className="flex items-start justify-between gap-3 mb-3">
<div>
<div className="flex items-center gap-2 mb-1">
<svg className="w-4 h-4 text-[#ffb800]" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24">
<svg className="w-4 h-4 text-[#F0A623]" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" d="M13 10V3L4 14h7v7l9-11h-7z" />
</svg>
<h3 className="text-sm font-display font-bold text-white">Auto-Pilot</h3>
@@ -405,7 +405,7 @@ export default function CompanyCoveragePage() {
<p className="text-xs text-green-400 font-body font-semibold">Auto-Pilot is ON stories publish automatically</p>
</div>
) : (
<div className="flex items-center gap-2 bg-[#1a1a1a] rounded-md px-3 py-2">
<div className="flex items-center gap-2 bg-[#231d18] rounded-md px-3 py-2">
<div className="w-2 h-2 rounded-full bg-[#444] flex-shrink-0" />
<p className="text-xs text-[#666] font-body">Manual review required before publishing</p>
</div>
@@ -413,7 +413,7 @@ export default function CompanyCoveragePage() {
</div>
{/* Reminder Settings */}
<div className="bg-[#111] border border-[#252525] rounded-lg p-5">
<div className="bg-[#111] border border-[#3a322b] rounded-lg p-5">
<div className="flex items-center gap-2 mb-3">
<svg className="w-4 h-4 text-[#888]" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" />
@@ -421,14 +421,14 @@ export default function CompanyCoveragePage() {
<h3 className="text-sm font-display font-bold text-white">Reminder Email</h3>
</div>
<p className="text-xs text-[#555] font-body mb-2">New story suggestions are emailed to:</p>
<div className="bg-[#0d0d0d] border border-[#1a1a1a] rounded-md px-3 py-2">
<p className="text-xs text-[#ffb800] font-body font-mono break-all">{settings.reminder_email}</p>
<div className="bg-[#1c1815] border border-[#231d18] rounded-md px-3 py-2">
<p className="text-xs text-[#F0A623] font-body font-mono break-all">{settings.reminder_email}</p>
</div>
<p className="text-xs text-[#444] font-body mt-2">Reminders are sent when Auto-Pilot is off and new companies are detected.</p>
</div>
{/* Last Scan Info */}
<div className="bg-[#111] border border-[#252525] rounded-lg p-5">
<div className="bg-[#111] border border-[#3a322b] rounded-lg p-5">
<div className="flex items-center gap-2 mb-3">
<svg className="w-4 h-4 text-[#888]" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24">
<circle cx="12" cy="12" r="10" /><path strokeLinecap="round" d="M12 6v6l4 2" />
@@ -456,7 +456,7 @@ export default function CompanyCoveragePage() {
</div>
{/* How It Works */}
<div className="bg-[#111] border border-[#252525] rounded-lg p-5">
<div className="bg-[#111] border border-[#3a322b] rounded-lg p-5">
<h3 className="text-xs font-body font-bold text-[#555] uppercase tracking-widest mb-3">How It Works</h3>
<ol className="space-y-2">
{[
@@ -467,7 +467,7 @@ export default function CompanyCoveragePage() {
'Email reminder sent to coverage contact',
].map((step, i) => (
<li key={i} className="flex items-start gap-2">
<span className="flex-shrink-0 w-4 h-4 rounded-full bg-[#1a1a1a] text-[#555] text-xs font-body font-bold flex items-center justify-center mt-0.5">
<span className="flex-shrink-0 w-4 h-4 rounded-full bg-[#231d18] text-[#555] text-xs font-body font-bold flex items-center justify-center mt-0.5">
{i + 1}
</span>
<span className="text-xs text-[#666] font-body leading-relaxed">{step}</span>

View File

@@ -245,7 +245,7 @@ export default function CompanyTrackerPage() {
if (loading || loadingData) {
return (
<div className="min-h-screen bg-[#0a0a0a] flex items-center justify-center">
<div className="w-8 h-8 border-2 border-[#ffb800] border-t-transparent rounded-full animate-spin" />
<div className="w-8 h-8 border-2 border-[#F0A623] border-t-transparent rounded-full animate-spin" />
</div>
);
}
@@ -279,7 +279,7 @@ export default function CompanyTrackerPage() {
</div>
<div className="flex items-center gap-3">
{/* Auto-Pilot toggle */}
<div className="flex items-center gap-3 bg-[#111] border border-[#252525] rounded-lg px-4 py-2.5">
<div className="flex items-center gap-3 bg-[#111] border border-[#3a322b] rounded-lg px-4 py-2.5">
<div>
<div className="text-xs font-body font-bold text-white">Auto-Pilot</div>
<div className="text-xs text-[#555] font-body">{settings.autopilot_enabled ? 'Stories auto-publish' : 'Manual review'}</div>
@@ -291,13 +291,13 @@ export default function CompanyTrackerPage() {
await fetch('/api/admin/company-coverage', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ action: 'update_settings', autopilot_enabled: newVal, reminder_email: settings.reminder_email }) });
showToast(newVal ? '🚀 Auto-Pilot enabled' : '⏸ Auto-Pilot disabled');
}}
className={`relative w-11 h-6 rounded-full transition-colors ${settings.autopilot_enabled ? 'bg-[#ffb800]' : 'bg-[#333]'}`}
className={`relative w-11 h-6 rounded-full transition-colors ${settings.autopilot_enabled ? 'bg-[#F0A623]' : 'bg-[#333]'}`}
>
<span className={`absolute top-0.5 left-0.5 w-5 h-5 bg-white rounded-full shadow transition-transform ${settings.autopilot_enabled ? 'translate-x-5' : 'translate-x-0'}`} />
</button>
</div>
<button onClick={handleScan} disabled={scanning}
className="flex items-center gap-2 px-4 py-2 bg-[#ffb800] hover:bg-blue-500 disabled:opacity-50 text-white text-sm font-body font-semibold rounded-lg transition-colors">
className="flex items-center gap-2 px-4 py-2 bg-[#F0A623] hover:bg-blue-500 disabled:opacity-50 text-white text-sm font-body font-semibold rounded-lg transition-colors">
{scanning ? <div className="w-4 h-4 border-2 border-white/30 border-t-white rounded-full animate-spin" /> : <svg className="w-4 h-4" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" /></svg>}
{scanning ? 'Scanning…' : 'Scan Articles Now'}
</button>
@@ -313,7 +313,7 @@ export default function CompanyTrackerPage() {
{ label: 'Companies Found', value: settings.last_scan_companies_found },
{ label: 'Tracked Companies', value: companies.length || '—' },
].map(stat => (
<div key={stat.label} className="bg-[#111] border border-[#252525] rounded-lg px-4 py-3">
<div key={stat.label} className="bg-[#111] border border-[#3a322b] rounded-lg px-4 py-3">
<div className="text-lg font-display font-bold text-white">{stat.value}</div>
<div className="text-xs text-[#555] font-body">{stat.label}</div>
</div>
@@ -322,10 +322,10 @@ export default function CompanyTrackerPage() {
)}
{/* Tabs */}
<div className="flex gap-1 mb-6 bg-[#111] border border-[#252525] rounded-lg p-1 w-fit">
<div className="flex gap-1 mb-6 bg-[#111] border border-[#3a322b] rounded-lg p-1 w-fit">
{tabs.map(tab => (
<button key={tab.id} onClick={() => setActiveTab(tab.id)}
className={`px-4 py-2 rounded-md text-sm font-body font-medium transition-colors ${activeTab === tab.id ? 'bg-[#1a1a1a] text-white' : 'text-[#555] hover:text-[#888]'}`}>
className={`px-4 py-2 rounded-md text-sm font-body font-medium transition-colors ${activeTab === tab.id ? 'bg-[#231d18] text-white' : 'text-[#555] hover:text-[#888]'}`}>
{tab.label}
</button>
))}
@@ -337,12 +337,12 @@ export default function CompanyTrackerPage() {
<div className="flex flex-wrap gap-3 mb-4">
{(['generated','pending','approved','published','rejected'] as QueueStatus[]).map(s => (
<button key={s} onClick={() => setQueueStatus(s)}
className={`px-3 py-1.5 rounded-lg text-xs font-body font-medium transition-colors capitalize ${queueStatus === s ? 'bg-[#ffb800] text-white' : 'bg-[#111] border border-[#252525] text-[#555] hover:text-white'}`}>
className={`px-3 py-1.5 rounded-lg text-xs font-body font-medium transition-colors capitalize ${queueStatus === s ? 'bg-[#F0A623] text-white' : 'bg-[#111] border border-[#3a322b] text-[#555] hover:text-white'}`}>
{s === 'generated' ? 'Pending Review' : s}
</button>
))}
<select value={queueSiteFilter} onChange={e => setQueueSiteFilter(e.target.value)}
className="bg-[#111] border border-[#252525] rounded-lg px-3 py-1.5 text-[#555] text-xs font-body focus:outline-none">
className="bg-[#111] border border-[#3a322b] rounded-lg px-3 py-1.5 text-[#555] text-xs font-body focus:outline-none">
<option value="">All Sites</option>
<option value="1">AV Beat</option>
<option value="2">AV Beat</option>
@@ -350,18 +350,18 @@ export default function CompanyTrackerPage() {
</div>
{queue.length === 0 ? (
<div className="bg-[#111] border border-[#252525] rounded-lg p-12 text-center">
<div className="bg-[#111] border border-[#3a322b] rounded-lg p-12 text-center">
<p className="text-[#555] text-sm font-body">No stories in this queue. Run a scan to generate new story suggestions.</p>
</div>
) : (
<div className="space-y-3">
{queue.map(item => (
<div key={item.id} className="bg-[#111] border border-[#252525] rounded-lg overflow-hidden">
<div key={item.id} className="bg-[#111] border border-[#3a322b] rounded-lg overflow-hidden">
<div className="px-5 py-4 flex items-start gap-4">
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2 mb-1 flex-wrap">
<SiteBadge siteId={item.site_id} />
<span className="text-xs px-2 py-0.5 rounded bg-[#1a1a1a] text-[#888] font-body capitalize">{item.story_type.replace(/_/g, ' ')}</span>
<span className="text-xs px-2 py-0.5 rounded bg-[#231d18] text-[#888] font-body capitalize">{item.story_type.replace(/_/g, ' ')}</span>
{item.quality_confidence && (
<span className={`text-xs px-2 py-0.5 rounded font-body ${item.quality_confidence >= 0.85 ? 'bg-green-500/10 text-green-400' : 'bg-yellow-500/10 text-yellow-400'}`}>
{Math.round(item.quality_confidence * 100)}% confidence
@@ -398,7 +398,7 @@ export default function CompanyTrackerPage() {
</div>
</div>
{expandedItem === item.id && item.generated_content && (
<div className="px-5 pb-5 border-t border-[#1a1a1a]">
<div className="px-5 pb-5 border-t border-[#231d18]">
<div className="mt-4 bg-[#0a0a0a] rounded-lg p-4 text-sm text-[#aaa] font-body leading-relaxed max-h-64 overflow-y-auto"
dangerouslySetInnerHTML={{ __html: item.generated_content }} />
<div className="mt-3 flex items-center gap-3">
@@ -430,30 +430,30 @@ export default function CompanyTrackerPage() {
<div>
<div className="flex items-center justify-between mb-4">
<p className="text-sm text-[#555] font-body">{companies.length} companies tracked</p>
<button onClick={() => setShowAddCompany(true)} className="flex items-center gap-2 px-4 py-2 bg-[#ffb800] hover:bg-blue-500 text-white text-sm font-body font-semibold rounded-lg transition-colors">
<button onClick={() => setShowAddCompany(true)} className="flex items-center gap-2 px-4 py-2 bg-[#F0A623] hover:bg-blue-500 text-white text-sm font-body font-semibold rounded-lg transition-colors">
<svg className="w-4 h-4" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" d="M12 4v16m8-8H4" /></svg>
Add Company
</button>
</div>
{showAddCompany && (
<div className="mb-4 bg-[#111] border border-[#252525] rounded-lg p-5">
<div className="mb-4 bg-[#111] border border-[#3a322b] rounded-lg p-5">
<h3 className="text-sm font-display font-bold text-white mb-4">Add Company Manually</h3>
<form onSubmit={handleAddCompany} className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label className="block text-xs font-body text-[#888] mb-1.5">Company Name *</label>
<input type="text" value={addCompanyForm.company_name} onChange={e => setAddCompanyForm(f => ({ ...f, company_name: e.target.value }))} required
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-[#ffb800]" />
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-[#F0A623]" />
</div>
<div>
<label className="block text-xs font-body text-[#888] mb-1.5">Website URL</label>
<input type="text" value={addCompanyForm.company_website} onChange={e => setAddCompanyForm(f => ({ ...f, company_website: e.target.value }))} placeholder="example.com"
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-[#ffb800]" />
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-[#F0A623]" />
</div>
<div>
<label className="block text-xs font-body text-[#888] mb-1.5">Site Scope</label>
<select value={addCompanyForm.site_scope} onChange={e => setAddCompanyForm(f => ({ ...f, site_scope: e.target.value }))}
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-[#ffb800]">
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-[#F0A623]">
<option value="both">Both Sites</option>
<option value="avbeat">AV Beat only</option>
<option value="avbeat">AV Beat only</option>
@@ -462,51 +462,51 @@ export default function CompanyTrackerPage() {
<div>
<label className="block text-xs font-body text-[#888] mb-1.5">Crawl Priority</label>
<select value={addCompanyForm.crawl_priority} onChange={e => setAddCompanyForm(f => ({ ...f, crawl_priority: e.target.value }))}
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-[#ffb800]">
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-[#F0A623]">
<option value="standard">Standard (weekly)</option>
<option value="high">High (every 3 days)</option>
<option value="critical">Critical (daily)</option>
</select>
</div>
<div className="md:col-span-2 flex gap-3">
<button type="submit" className="px-5 py-2 bg-[#ffb800] hover:bg-blue-500 text-white text-sm font-body font-semibold rounded-lg transition-colors">Add Company</button>
<button type="button" onClick={() => setShowAddCompany(false)} className="px-4 py-2 bg-[#1a1a1a] border border-[#333] text-[#888] hover:text-white text-sm font-body rounded-lg transition-colors">Cancel</button>
<button type="submit" className="px-5 py-2 bg-[#F0A623] hover:bg-blue-500 text-white text-sm font-body font-semibold rounded-lg transition-colors">Add Company</button>
<button type="button" onClick={() => setShowAddCompany(false)} className="px-4 py-2 bg-[#231d18] border border-[#333] text-[#888] hover:text-white text-sm font-body rounded-lg transition-colors">Cancel</button>
</div>
</form>
</div>
)}
<div className="bg-[#111] border border-[#252525] rounded-lg overflow-hidden">
<div className="bg-[#111] border border-[#3a322b] rounded-lg overflow-hidden">
<div className="overflow-x-auto">
<table className="w-full">
<thead>
<tr className="border-b border-[#1a1a1a]">
<tr className="border-b border-[#231d18]">
{['Company', 'Website', 'Mentions', 'Sites', 'Auto-Stories', 'Priority', 'Last Crawl', 'Actions'].map(h => (
<th key={h} className="px-4 py-3 text-left text-xs font-body font-bold text-[#555] uppercase tracking-wider">{h}</th>
))}
</tr>
</thead>
<tbody className="divide-y divide-[#1a1a1a]">
<tbody className="divide-y divide-[#231d18]">
{companies.length === 0 ? (
<tr><td colSpan={8} className="px-4 py-12 text-center text-[#555] text-sm font-body">No companies tracked yet. Run a scan to auto-detect companies from published articles.</td></tr>
) : companies.map(company => (
<tr key={company.id} className="hover:bg-[#0d0d0d] transition-colors">
<tr key={company.id} className="hover:bg-[#1c1815] transition-colors">
<td className="px-4 py-3 text-sm font-body font-semibold text-white">{company.company_name}</td>
<td className="px-4 py-3 text-xs text-[#555] font-body">{company.company_website ? <a href={`https://${company.company_website}`} target="_blank" rel="noopener noreferrer" className="text-[#ffb800] hover:underline">{company.company_website}</a> : '—'}</td>
<td className="px-4 py-3 text-xs text-[#555] font-body">{company.company_website ? <a href={`https://${company.company_website}`} target="_blank" rel="noopener noreferrer" className="text-[#F0A623] hover:underline">{company.company_website}</a> : '—'}</td>
<td className="px-4 py-3 text-sm font-body text-white font-bold">{company.mention_count}</td>
<td className="px-4 py-3 text-xs text-[#888] font-body capitalize">{company.site_scope === 'both' ? 'Both' : company.site_scope === 'avbeat' ? 'BB' : 'AV'}</td>
<td className="px-4 py-3">
<button onClick={() => handleToggleCompanyAutoStory(company.id, !company.auto_story_enabled)} disabled={actionLoading === company.id}
className={`relative w-9 h-5 rounded-full transition-colors ${company.auto_story_enabled ? 'bg-[#ffb800]' : 'bg-[#333]'}`}>
className={`relative w-9 h-5 rounded-full transition-colors ${company.auto_story_enabled ? 'bg-[#F0A623]' : 'bg-[#333]'}`}>
<span className={`absolute top-0.5 left-0.5 w-4 h-4 bg-white rounded-full shadow transition-transform ${company.auto_story_enabled ? 'translate-x-4' : 'translate-x-0'}`} />
</button>
</td>
<td className="px-4 py-3"><span className={`text-xs px-2 py-0.5 rounded font-body capitalize ${company.crawl_priority === 'critical' ? 'bg-red-500/10 text-red-400' : company.crawl_priority === 'high' ? 'bg-yellow-500/10 text-yellow-400' : 'bg-[#1a1a1a] text-[#555]'}`}>{company.crawl_priority}</span></td>
<td className="px-4 py-3"><span className={`text-xs px-2 py-0.5 rounded font-body capitalize ${company.crawl_priority === 'critical' ? 'bg-red-500/10 text-red-400' : company.crawl_priority === 'high' ? 'bg-yellow-500/10 text-yellow-400' : 'bg-[#231d18] text-[#555]'}`}>{company.crawl_priority}</span></td>
<td className="px-4 py-3 text-xs text-[#555] font-body">{company.last_crawled ? timeAgo(company.last_crawled) : 'Never'}</td>
<td className="px-4 py-3">
<div className="flex items-center gap-2">
<button onClick={async () => { await fetch('/api/admin/company-coverage', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ action: 'force_crawl', company_id: company.id }) }); showToast('Crawl scheduled'); }}
className="text-xs text-[#ffb800] hover:text-blue-300 font-body transition-colors">Force Crawl</button>
className="text-xs text-[#F0A623] hover:text-blue-300 font-body transition-colors">Force Crawl</button>
<button onClick={() => handleDeleteCompany(company.id)} disabled={actionLoading === company.id}
className="text-xs text-red-500 hover:text-red-400 font-body transition-colors disabled:opacity-50">Remove</button>
</div>
@@ -522,21 +522,21 @@ export default function CompanyTrackerPage() {
{/* ── Crawl Log Tab ── */}
{activeTab === 'crawl-log' && (
<div className="bg-[#111] border border-[#252525] rounded-lg overflow-hidden">
<div className="bg-[#111] border border-[#3a322b] rounded-lg overflow-hidden">
<div className="overflow-x-auto">
<table className="w-full">
<thead>
<tr className="border-b border-[#1a1a1a]">
<tr className="border-b border-[#231d18]">
{['Date', 'Company', 'Source URL', 'Items Found', 'Stories Queued', 'Status'].map(h => (
<th key={h} className="px-4 py-3 text-left text-xs font-body font-bold text-[#555] uppercase tracking-wider">{h}</th>
))}
</tr>
</thead>
<tbody className="divide-y divide-[#1a1a1a]">
<tbody className="divide-y divide-[#231d18]">
{crawlLogs.length === 0 ? (
<tr><td colSpan={6} className="px-4 py-12 text-center text-[#555] text-sm font-body">No crawl history yet.</td></tr>
) : crawlLogs.map(log => (
<tr key={log.id} className="hover:bg-[#0d0d0d] transition-colors">
<tr key={log.id} className="hover:bg-[#1c1815] transition-colors">
<td className="px-4 py-3 text-xs text-[#555] font-body">{new Date(log.crawled_at).toLocaleString()}</td>
<td className="px-4 py-3 text-sm text-white font-body">{log.company_name}</td>
<td className="px-4 py-3 text-xs text-[#555] font-body truncate max-w-xs">{log.source_url || '—'}</td>
@@ -558,7 +558,7 @@ export default function CompanyTrackerPage() {
{/* ── Settings Tab ── */}
{activeTab === 'settings' && (
<div className="max-w-2xl space-y-6">
<div className="bg-[#111] border border-[#252525] rounded-lg p-6">
<div className="bg-[#111] border border-[#3a322b] rounded-lg p-6">
<h3 className="text-sm font-display font-bold text-white mb-4">Auto-Pilot & Reminders</h3>
<div className="space-y-4">
<div className="flex items-center justify-between">
@@ -567,19 +567,19 @@ export default function CompanyTrackerPage() {
<div className="text-xs text-[#555] font-body mt-0.5">When enabled, high-confidence stories publish automatically without review</div>
</div>
<button onClick={() => setSettings(s => ({ ...s, autopilot_enabled: !s.autopilot_enabled }))}
className={`relative w-11 h-6 rounded-full transition-colors ${settings.autopilot_enabled ? 'bg-[#ffb800]' : 'bg-[#333]'}`}>
className={`relative w-11 h-6 rounded-full transition-colors ${settings.autopilot_enabled ? 'bg-[#F0A623]' : 'bg-[#333]'}`}>
<span className={`absolute top-0.5 left-0.5 w-5 h-5 bg-white rounded-full shadow transition-transform ${settings.autopilot_enabled ? 'translate-x-5' : 'translate-x-0'}`} />
</button>
</div>
<div>
<label className="block text-xs font-body text-[#888] mb-1.5">Reminder Email</label>
<input type="email" value={settings.reminder_email} onChange={e => setSettings(s => ({ ...s, reminder_email: e.target.value }))}
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-[#ffb800]" />
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-[#F0A623]" />
</div>
</div>
</div>
<div className="bg-[#111] border border-[#252525] rounded-lg p-6">
<div className="bg-[#111] border border-[#3a322b] rounded-lg p-6">
<h3 className="text-sm font-display font-bold text-white mb-4">Story Generation Settings</h3>
<div className="space-y-4">
<div className="flex items-center justify-between">
@@ -588,7 +588,7 @@ export default function CompanyTrackerPage() {
<div className="text-xs text-[#555] font-body mt-0.5">Master switch for all automated story generation</div>
</div>
<button onClick={() => setTrackerSettings(s => ({ ...s, auto_story_global_enabled: !s.auto_story_global_enabled }))}
className={`relative w-11 h-6 rounded-full transition-colors ${trackerSettings.auto_story_global_enabled ? 'bg-[#ffb800]' : 'bg-[#333]'}`}>
className={`relative w-11 h-6 rounded-full transition-colors ${trackerSettings.auto_story_global_enabled ? 'bg-[#F0A623]' : 'bg-[#333]'}`}>
<span className={`absolute top-0.5 left-0.5 w-5 h-5 bg-white rounded-full shadow transition-transform ${trackerSettings.auto_story_global_enabled ? 'translate-x-5' : 'translate-x-0'}`} />
</button>
</div>
@@ -598,25 +598,25 @@ export default function CompanyTrackerPage() {
<div className="text-xs text-[#555] font-body mt-0.5">Publish stories with confidence {Math.round(trackerSettings.auto_publish_confidence_threshold * 100)}% automatically</div>
</div>
<button onClick={() => setTrackerSettings(s => ({ ...s, auto_publish_enabled: !s.auto_publish_enabled }))}
className={`relative w-11 h-6 rounded-full transition-colors ${trackerSettings.auto_publish_enabled ? 'bg-[#ffb800]' : 'bg-[#333]'}`}>
className={`relative w-11 h-6 rounded-full transition-colors ${trackerSettings.auto_publish_enabled ? 'bg-[#F0A623]' : 'bg-[#333]'}`}>
<span className={`absolute top-0.5 left-0.5 w-5 h-5 bg-white rounded-full shadow transition-transform ${trackerSettings.auto_publish_enabled ? 'translate-x-5' : 'translate-x-0'}`} />
</button>
</div>
<div>
<label className="block text-xs font-body text-[#888] mb-1.5">Mention Threshold (min mentions to qualify for auto-stories)</label>
<input type="number" min={1} max={20} value={trackerSettings.mention_threshold} onChange={e => setTrackerSettings(s => ({ ...s, mention_threshold: parseInt(e.target.value) }))}
className="w-32 bg-[#0a0a0a] border border-[#333] rounded-lg px-3 py-2.5 text-white text-sm font-body focus:outline-none focus:border-[#ffb800]" />
className="w-32 bg-[#0a0a0a] border border-[#333] rounded-lg px-3 py-2.5 text-white text-sm font-body focus:outline-none focus:border-[#F0A623]" />
</div>
<div>
<label className="block text-xs font-body text-[#888] mb-1.5">Max Stories Per Company Per Week</label>
<input type="number" min={1} max={20} value={trackerSettings.max_stories_per_company_per_week} onChange={e => setTrackerSettings(s => ({ ...s, max_stories_per_company_per_week: parseInt(e.target.value) }))}
className="w-32 bg-[#0a0a0a] border border-[#333] rounded-lg px-3 py-2.5 text-white text-sm font-body focus:outline-none focus:border-[#ffb800]" />
className="w-32 bg-[#0a0a0a] border border-[#333] rounded-lg px-3 py-2.5 text-white text-sm font-body focus:outline-none focus:border-[#F0A623]" />
</div>
</div>
</div>
<button onClick={handleSaveSettings} disabled={savingSettings}
className="px-6 py-2.5 bg-[#ffb800] hover:bg-blue-500 text-white text-sm font-body font-semibold rounded-lg transition-colors disabled:opacity-50">
className="px-6 py-2.5 bg-[#F0A623] hover:bg-blue-500 text-white text-sm font-body font-semibold rounded-lg transition-colors disabled:opacity-50">
{savingSettings ? 'Saving...' : 'Save Settings'}
</button>
</div>

View File

@@ -97,7 +97,7 @@ function StatCard({
good?: boolean;
}) {
return (
<div className="bg-[#111] border border-[#252525] rounded-lg p-5 flex flex-col gap-2">
<div className="bg-[#111] border border-[#3a322b] rounded-lg p-5 flex flex-col gap-2">
<p className={`text-2xl font-display font-bold ${accent}`}>{value}</p>
<p className="text-sm text-[#888] font-body">{label}</p>
{sub && <p className="text-xs text-[#555] font-body">{sub}</p>}
@@ -173,7 +173,7 @@ function GeneratePanel({ onGenerated }: { onGenerated: () => void }) {
};
return (
<div className="bg-[#111] border border-[#252525] rounded-lg p-5 flex flex-col gap-4">
<div className="bg-[#111] border border-[#3a322b] rounded-lg p-5 flex flex-col gap-4">
<h3 className="text-sm font-display font-bold text-white">Manual Controls</h3>
<div className="flex flex-col gap-3">
<div>
@@ -181,7 +181,7 @@ function GeneratePanel({ onGenerated }: { onGenerated: () => void }) {
<select
value={site}
onChange={(e) => setSite(e.target.value as RMPSite)}
className="w-full bg-[#1a1a1a] border border-[#333] rounded-lg px-3 py-2 text-sm text-white font-body focus:outline-none focus:border-[#ffb800]"
className="w-full bg-[#231d18] border border-[#333] rounded-lg px-3 py-2 text-sm text-white font-body focus:outline-none focus:border-[#F0A623]"
>
{Object.entries(SITE_LABELS).map(([key, val]) => (
<option key={key} value={key}>{val.label}</option>
@@ -195,21 +195,21 @@ function GeneratePanel({ onGenerated }: { onGenerated: () => void }) {
value={topic}
onChange={(e) => setTopic(e.target.value)}
placeholder="e.g. ATSC 3.0 deployment update"
className="w-full bg-[#1a1a1a] border border-[#333] rounded-lg px-3 py-2 text-sm text-white font-body placeholder-[#444] focus:outline-none focus:border-[#ffb800]"
className="w-full bg-[#231d18] border border-[#333] rounded-lg px-3 py-2 text-sm text-white font-body placeholder-[#444] focus:outline-none focus:border-[#F0A623]"
/>
</div>
<div className="flex gap-2">
<button
onClick={handleGenerate}
disabled={loading}
className="flex-1 bg-[#ffb800] hover:bg-[#d99700] disabled:opacity-50 text-white text-sm font-body font-semibold py-2 px-4 rounded-lg transition-colors"
className="flex-1 bg-[#F0A623] hover:bg-[#BA7517] disabled:opacity-50 text-white text-sm font-body font-semibold py-2 px-4 rounded-lg transition-colors"
>
{loading ? 'Generating…' : 'Generate Article'}
</button>
<button
onClick={handleSEOBatch}
disabled={loading}
className="flex-1 bg-[#1a1a1a] hover:bg-[#252525] disabled:opacity-50 border border-[#333] text-white text-sm font-body font-semibold py-2 px-4 rounded-lg transition-colors"
className="flex-1 bg-[#231d18] hover:bg-[#3a322b] disabled:opacity-50 border border-[#333] text-white text-sm font-body font-semibold py-2 px-4 rounded-lg transition-colors"
>
{loading ? 'Running…' : 'Batch SEO Meta'}
</button>
@@ -279,7 +279,7 @@ export default function ContentStatusPage() {
return (
<div className="min-h-screen bg-[#0a0a0a] flex items-center justify-center">
<div className="flex flex-col items-center gap-3">
<div className="w-8 h-8 border-2 border-[#ffb800] border-t-transparent rounded-full animate-spin" />
<div className="w-8 h-8 border-2 border-[#F0A623] border-t-transparent rounded-full animate-spin" />
<p className="text-[#555] text-sm font-body">Loading content status</p>
</div>
</div>
@@ -296,7 +296,7 @@ export default function ContentStatusPage() {
return (
<div className="min-h-screen bg-[#0a0a0a] text-white">
{/* Header */}
<div className="border-b border-[#1a1a1a] bg-[#0d0d0d] px-6 py-4">
<div className="border-b border-[#231d18] bg-[#1c1815] px-6 py-4">
<div className="max-w-7xl mx-auto flex items-center justify-between">
<div className="flex items-center gap-3">
<Link href="/admin" className="text-[#555] hover:text-white transition-colors text-sm font-body">
@@ -327,7 +327,7 @@ export default function ContentStatusPage() {
</button>
<button
onClick={fetchAll}
className="text-xs font-body px-3 py-1.5 rounded-lg border border-[#333] text-white bg-[#111] hover:bg-[#1a1a1a] transition-colors"
className="text-xs font-body px-3 py-1.5 rounded-lg border border-[#333] text-white bg-[#111] hover:bg-[#231d18] transition-colors"
>
Refresh Now
</button>
@@ -395,7 +395,7 @@ export default function ContentStatusPage() {
{Object.entries(SITE_LABELS).map(([key, cfg]) => {
const count = status?.siteBreakdown?.[key] ?? 0;
return (
<div key={key} className="bg-[#111] border border-[#252525] rounded-lg p-4 flex flex-col gap-2">
<div key={key} className="bg-[#111] border border-[#3a322b] rounded-lg p-4 flex flex-col gap-2">
<p className={`text-xs font-body font-semibold ${cfg.color}`}>{cfg.label}</p>
<p className="text-3xl font-display font-bold text-white">{count}</p>
<p className="text-xs text-[#555] font-body">Target: {cfg.target}</p>
@@ -484,7 +484,7 @@ export default function ContentStatusPage() {
{/* API Reference */}
<section>
<h2 className="text-sm font-display font-bold text-[#888] uppercase tracking-wider mb-4">API Endpoints</h2>
<div className="bg-[#111] border border-[#252525] rounded-lg p-5 grid grid-cols-1 sm:grid-cols-2 gap-3">
<div className="bg-[#111] border border-[#3a322b] rounded-lg p-5 grid grid-cols-1 sm:grid-cols-2 gap-3">
{[
{ method: 'GET', path: '/api/content/audit', desc: 'Full content audit report' },
{ method: 'GET', path: '/api/content/pipeline-status', desc: 'Live pipeline + node status' },

View File

@@ -90,7 +90,7 @@ function StatCard({
label,
value,
sub,
accent = '#ffb800',
accent = '#F0A623',
icon,
trend,
}: {
@@ -102,7 +102,7 @@ function StatCard({
trend?: { value: number; label: string };
}) {
return (
<div className="bg-[#111] border border-[#252525] rounded-xl p-5 flex flex-col gap-3">
<div className="bg-[#111] border border-[#3a322b] rounded-xl p-5 flex flex-col gap-3">
<div className="flex items-start justify-between">
<div
className="w-10 h-10 rounded-lg flex items-center justify-center flex-shrink-0"
@@ -134,7 +134,7 @@ function StatCard({
function ChartTooltip({ active, payload, label }: any) {
if (!active || !payload?.length) return null;
return (
<div className="bg-[#1a1a1a] border border-[#333] rounded-lg px-3 py-2 text-xs font-body shadow-xl">
<div className="bg-[#231d18] border border-[#333] rounded-lg px-3 py-2 text-xs font-body shadow-xl">
{label && <p className="text-[#888] mb-1.5 font-semibold">{label}</p>}
{payload.map((entry: any, i: number) => (
<p key={i} style={{ color: entry.color }} className="leading-5">
@@ -252,7 +252,7 @@ export default function EmailAnalyticsDashboard() {
return (
<div className="min-h-screen bg-[#0a0a0a] flex items-center justify-center">
<div className="flex flex-col items-center gap-3">
<div className="w-8 h-8 border-2 border-[#ffb800] border-t-transparent rounded-full animate-spin" />
<div className="w-8 h-8 border-2 border-[#F0A623] border-t-transparent rounded-full animate-spin" />
<p className="text-[#555] text-sm font-body">Loading analytics</p>
</div>
</div>
@@ -262,7 +262,7 @@ export default function EmailAnalyticsDashboard() {
return (
<div className="min-h-screen bg-[#0a0a0a] text-white">
{/* Header */}
<div className="border-b border-[#1a1a1a] bg-[#0d0d0d] sticky top-0 z-10">
<div className="border-b border-[#231d18] bg-[#1c1815] sticky top-0 z-10">
<div className="max-w-7xl mx-auto px-6 py-4 flex items-center justify-between">
<div className="flex items-center gap-3">
<Link href="/admin" className="text-[#555] hover:text-white transition-colors">
@@ -270,7 +270,7 @@ export default function EmailAnalyticsDashboard() {
<path strokeLinecap="round" strokeLinejoin="round" d="M10.5 19.5L3 12m0 0l7.5-7.5M3 12h18" />
</svg>
</Link>
<div className="w-px h-5 bg-[#252525]" />
<div className="w-px h-5 bg-[#3a322b]" />
<span className="text-white text-sm font-body font-semibold">Email Analytics</span>
</div>
<div className="flex items-center gap-2">
@@ -280,17 +280,17 @@ export default function EmailAnalyticsDashboard() {
onClick={() => setTimeRange(r)}
className={`px-3 py-1.5 text-xs font-semibold font-body rounded-lg transition-colors ${
timeRange === r
? 'bg-[#ffb800] text-white'
: 'text-[#555] hover:text-white border border-[#252525] hover:border-[#444]'
? 'bg-[#F0A623] text-white'
: 'text-[#555] hover:text-white border border-[#3a322b] hover:border-[#444]'
}`}
>
{r === '3m' ? '3 Mo' : r === '6m' ? '6 Mo' : 'All'}
</button>
))}
<div className="w-px h-6 bg-[#252525] mx-1" />
<div className="w-px h-6 bg-[#3a322b] mx-1" />
<Link
href="/admin/newsletter/analytics"
className="flex items-center gap-1.5 px-3 py-1.5 text-xs font-body font-semibold text-[#888] hover:text-white border border-[#252525] rounded-lg transition-colors"
className="flex items-center gap-1.5 px-3 py-1.5 text-xs font-body font-semibold text-[#888] hover:text-white border border-[#3a322b] rounded-lg transition-colors"
>
<svg className="w-3.5 h-3.5" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" d="M3 10h18M3 14h18M10 3v18M14 3v18" />
@@ -317,7 +317,7 @@ export default function EmailAnalyticsDashboard() {
label="Active Subscribers"
value={fmt(stats.activeSubscribers)}
sub={`${fmt(stats.unsubscribed)} unsubscribed`}
accent="#ffb800"
accent="#F0A623"
trend={{ value: 12, label: 'vs last month' }}
icon={
<svg className="w-5 h-5" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24">
@@ -365,7 +365,7 @@ export default function EmailAnalyticsDashboard() {
</div>
{/* Send Volume Chart */}
<div className="bg-[#111] border border-[#252525] rounded-xl p-6">
<div className="bg-[#111] border border-[#3a322b] rounded-xl p-6">
<SectionHeader
title="Send Volume Over Time"
subtitle="Total emails sent, opens, and clicks per month"
@@ -378,7 +378,7 @@ export default function EmailAnalyticsDashboard() {
<YAxis tick={{ fill: '#555', fontSize: 10 }} tickLine={false} axisLine={false} tickFormatter={(v) => fmt(v)} />
<Tooltip content={<ChartTooltip />} />
<Legend wrapperStyle={{ fontSize: 11, color: '#888', paddingTop: 12 }} />
<Bar dataKey="sent" name="Sent" fill="#3b82f620" stroke="#ffb800" strokeWidth={1} radius={[3, 3, 0, 0]} />
<Bar dataKey="sent" name="Sent" fill="#3b82f620" stroke="#F0A623" strokeWidth={1} radius={[3, 3, 0, 0]} />
<Bar dataKey="opens" name="Opens" fill="#10b98120" stroke="#10b981" strokeWidth={1} radius={[3, 3, 0, 0]} />
<Bar dataKey="clicks" name="Clicks" fill="#f59e0b20" stroke="#f59e0b" strokeWidth={1} radius={[3, 3, 0, 0]} />
</ComposedChart>
@@ -387,7 +387,7 @@ export default function EmailAnalyticsDashboard() {
</div>
{/* Open Rate & CTR Trend */}
<div className="bg-[#111] border border-[#252525] rounded-xl p-6">
<div className="bg-[#111] border border-[#3a322b] rounded-xl p-6">
<SectionHeader
title="Open Rate & Click-Through Rate Trend"
subtitle="Monthly engagement rates as a percentage of recipients"
@@ -397,8 +397,8 @@ export default function EmailAnalyticsDashboard() {
<AreaChart data={filteredRates} margin={{ top: 4, right: 8, left: -10, bottom: 0 }}>
<defs>
<linearGradient id="openGrad" x1="0" y1="0" x2="0" y2="1">
<stop offset="5%" stopColor="#ffb800" stopOpacity={0.2} />
<stop offset="95%" stopColor="#ffb800" stopOpacity={0} />
<stop offset="5%" stopColor="#F0A623" stopOpacity={0.2} />
<stop offset="95%" stopColor="#F0A623" stopOpacity={0} />
</linearGradient>
<linearGradient id="clickGrad" x1="0" y1="0" x2="0" y2="1">
<stop offset="5%" stopColor="#10b981" stopOpacity={0.2} />
@@ -418,10 +418,10 @@ export default function EmailAnalyticsDashboard() {
<Area
type="monotone"
dataKey="Open Rate"
stroke="#ffb800"
stroke="#F0A623"
strokeWidth={2}
fill="url(#openGrad)"
dot={{ r: 3, fill: '#ffb800' }}
dot={{ r: 3, fill: '#F0A623' }}
activeDot={{ r: 5 }}
/>
<Area
@@ -441,7 +441,7 @@ export default function EmailAnalyticsDashboard() {
{/* Subscriber Growth */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
{/* Growth Over Time */}
<div className="bg-[#111] border border-[#252525] rounded-xl p-6">
<div className="bg-[#111] border border-[#3a322b] rounded-xl p-6">
<SectionHeader
title="Subscriber Growth"
subtitle="Total subscribers and monthly net change"
@@ -463,14 +463,14 @@ export default function EmailAnalyticsDashboard() {
fill="#8b5cf618"
dot={false}
/>
<Bar dataKey="net" name="Net New" fill="#ffb800" radius={[3, 3, 0, 0]} />
<Bar dataKey="net" name="Net New" fill="#F0A623" radius={[3, 3, 0, 0]} />
</ComposedChart>
</ResponsiveContainer>
</div>
</div>
{/* New vs Unsubscribed */}
<div className="bg-[#111] border border-[#252525] rounded-xl p-6">
<div className="bg-[#111] border border-[#3a322b] rounded-xl p-6">
<SectionHeader
title="New Subscribers vs Unsubscribes"
subtitle="Monthly acquisition vs churn"
@@ -492,7 +492,7 @@ export default function EmailAnalyticsDashboard() {
</div>
{/* Benchmark Comparison */}
<div className="bg-[#111] border border-[#252525] rounded-xl p-6">
<div className="bg-[#111] border border-[#3a322b] rounded-xl p-6">
<SectionHeader
title="Performance vs Industry Benchmarks"
subtitle="How your metrics compare to email marketing industry averages"
@@ -503,7 +503,7 @@ export default function EmailAnalyticsDashboard() {
label: 'Open Rate',
yours: stats.avgOpenRate,
benchmark: 21.5,
color: '#ffb800',
color: '#F0A623',
unit: '%',
},
{
@@ -584,7 +584,7 @@ export default function EmailAnalyticsDashboard() {
<div className="flex flex-wrap gap-3 pb-4">
<Link
href="/admin/newsletter"
className="flex items-center gap-2 px-4 py-2 bg-[#111] border border-[#252525] rounded-lg text-sm font-body text-[#888] hover:text-white hover:border-[#444] transition-colors"
className="flex items-center gap-2 px-4 py-2 bg-[#111] border border-[#3a322b] rounded-lg text-sm font-body text-[#888] hover:text-white hover:border-[#444] transition-colors"
>
<svg className="w-4 h-4" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" d="M21.75 6.75v10.5a2.25 2.25 0 01-2.25 2.25h-15a2.25 2.25 0 01-2.25-2.25V6.75m19.5 0A2.25 2.25 0 0019.5 4.5h-15a2.25 2.25 0 00-2.25 2.25m19.5 0v.243a2.25 2.25 0 01-1.07 1.916l-7.5 4.615a2.25 2.25 0 01-2.36 0L3.32 8.91a2.25 2.25 0 01-1.07-1.916V6.75" />
@@ -593,7 +593,7 @@ export default function EmailAnalyticsDashboard() {
</Link>
<Link
href="/admin/newsletter/analytics"
className="flex items-center gap-2 px-4 py-2 bg-[#111] border border-[#252525] rounded-lg text-sm font-body text-[#888] hover:text-white hover:border-[#444] transition-colors"
className="flex items-center gap-2 px-4 py-2 bg-[#111] border border-[#3a322b] rounded-lg text-sm font-body text-[#888] hover:text-white hover:border-[#444] transition-colors"
>
<svg className="w-4 h-4" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" d="M3 13.125C3 12.504 3.504 12 4.125 12h2.25c.621 0 1.125.504 1.125 1.125v6.75C7.5 20.496 6.996 21 6.375 21h-2.25A1.125 1.125 0 013 19.875v-6.75zM9.75 8.625c0-.621.504-1.125 1.125-1.125h2.25c.621 0 1.125.504 1.125 1.125v11.25c0 .621-.504 1.125-1.125 1.125h-2.25a1.125 1.125 0 01-1.125-1.125V8.625zM16.5 4.125c0-.621.504-1.125 1.125-1.125h2.25C20.496 3 21 3.504 21 4.125v15.75c0 .621-.504 1.125-1.125 1.125h-2.25a1.125 1.125 0 01-1.125-1.125V4.125z" />
@@ -602,7 +602,7 @@ export default function EmailAnalyticsDashboard() {
</Link>
<Link
href="/admin/newsletter/templates"
className="flex items-center gap-2 px-4 py-2 bg-[#111] border border-[#252525] rounded-lg text-sm font-body text-[#888] hover:text-white hover:border-[#444] transition-colors"
className="flex items-center gap-2 px-4 py-2 bg-[#111] border border-[#3a322b] rounded-lg text-sm font-body text-[#888] hover:text-white hover:border-[#444] transition-colors"
>
<svg className="w-4 h-4" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" d="M19.5 14.25v-2.625a3.375 3.375 0 00-3.375-3.375h-1.5A1.125 1.125 0 0113.5 7.125v-1.5a3.375 3.375 0 00-3.375-3.375H8.25m0 12.75h7.5m-7.5 3H12M10.5 2.25H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 00-9-9z" />

View File

@@ -75,7 +75,7 @@ function DomainsContent() {
load();
}
if (loading) return <div className="min-h-screen bg-[#0a0a0a] flex items-center justify-center"><div className="w-8 h-8 border-2 border-[#ffb800] border-t-transparent rounded-full animate-spin" /></div>;
if (loading) return <div className="min-h-screen bg-[#0a0a0a] flex items-center justify-center"><div className="w-8 h-8 border-2 border-[#F0A623] border-t-transparent rounded-full animate-spin" /></div>;
if (!user) return null;
return (
@@ -86,41 +86,41 @@ function DomainsContent() {
<h1 className="text-xl font-bold">Sending Domains</h1>
<Link href="/admin/email" className="text-xs text-[#555] hover:text-[#888]"> Email</Link>
</div>
<button onClick={() => setShowAddForm(true)} className="px-3 py-1.5 bg-[#ffb800] rounded text-xs font-medium hover:bg-blue-500">+ Add Domain</button>
<button onClick={() => setShowAddForm(true)} className="px-3 py-1.5 bg-[#F0A623] rounded text-xs font-medium hover:bg-blue-500">+ Add Domain</button>
</div>
<div className="flex gap-2">
<select value={filterSite} onChange={e => setFilterSite(e.target.value)} className="px-2 py-1 bg-[#111] border border-[#252525] rounded text-xs text-[#888] focus:outline-none">
<select value={filterSite} onChange={e => setFilterSite(e.target.value)} className="px-2 py-1 bg-[#111] border border-[#3a322b] rounded text-xs text-[#888] focus:outline-none">
<option value="">All Sites</option>
{SITES.map(s => <option key={s} value={s}>{s}</option>)}
</select>
</div>
{showAddForm && (
<form onSubmit={handleAddDomain} className="bg-[#111] border border-[#252525] rounded-lg p-4 space-y-3">
<form onSubmit={handleAddDomain} className="bg-[#111] border border-[#3a322b] rounded-lg p-4 space-y-3">
<h3 className="text-sm font-semibold">Add Rotation Domain</h3>
<div className="grid grid-cols-2 gap-3">
<div>
<label className="text-xs text-[#888] block mb-1">Site *</label>
<select required value={addForm.site} onChange={e => setAddForm(p => ({ ...p, site: e.target.value }))} className="w-full px-2 py-1.5 bg-[#0a0a0a] border border-[#252525] rounded text-xs text-white focus:outline-none">
<select required value={addForm.site} onChange={e => setAddForm(p => ({ ...p, site: e.target.value }))} className="w-full px-2 py-1.5 bg-[#0a0a0a] border border-[#3a322b] rounded text-xs text-white focus:outline-none">
<option value="">Select</option>
{SITES.map(s => <option key={s} value={s}>{s}</option>)}
</select>
</div>
<div>
<label className="text-xs text-[#888] block mb-1">Domain *</label>
<input required value={addForm.domain} onChange={e => setAddForm(p => ({ ...p, domain: e.target.value }))} placeholder="mail.example.com" className="w-full px-2 py-1.5 bg-[#0a0a0a] border border-[#252525] rounded text-xs text-white focus:outline-none focus:border-[#ffb800]" />
<input required value={addForm.domain} onChange={e => setAddForm(p => ({ ...p, domain: e.target.value }))} placeholder="mail.example.com" className="w-full px-2 py-1.5 bg-[#0a0a0a] border border-[#3a322b] rounded text-xs text-white focus:outline-none focus:border-[#F0A623]" />
</div>
<div>
<label className="text-xs text-[#888] block mb-1">Type</label>
<select value={addForm.domain_type} onChange={e => setAddForm(p => ({ ...p, domain_type: e.target.value }))} className="w-full px-2 py-1.5 bg-[#0a0a0a] border border-[#252525] rounded text-xs text-white focus:outline-none">
<select value={addForm.domain_type} onChange={e => setAddForm(p => ({ ...p, domain_type: e.target.value }))} className="w-full px-2 py-1.5 bg-[#0a0a0a] border border-[#3a322b] rounded text-xs text-white focus:outline-none">
{['subdomain','sister_domain','primary'].map(t => <option key={t} value={t}>{t}</option>)}
</select>
</div>
</div>
<div className="flex gap-2">
<button type="submit" disabled={saving} className="px-3 py-1.5 bg-[#ffb800] rounded text-xs font-medium hover:bg-blue-500 disabled:opacity-50">{saving ? 'Saving…' : 'Add Domain'}</button>
<button type="button" onClick={() => setShowAddForm(false)} className="px-3 py-1.5 bg-[#1a1a1a] border border-[#252525] rounded text-xs text-[#888] hover:text-white">Cancel</button>
<button type="submit" disabled={saving} className="px-3 py-1.5 bg-[#F0A623] rounded text-xs font-medium hover:bg-blue-500 disabled:opacity-50">{saving ? 'Saving…' : 'Add Domain'}</button>
<button type="button" onClick={() => setShowAddForm(false)} className="px-3 py-1.5 bg-[#231d18] border border-[#3a322b] rounded text-xs text-[#888] hover:text-white">Cancel</button>
</div>
</form>
)}
@@ -131,10 +131,10 @@ function DomainsContent() {
<div className="bg-[#111] border border-yellow-400/30 rounded-lg p-5 w-80 space-y-3">
<h3 className="text-sm font-semibold text-yellow-400"> Override DNS Verification</h3>
<p className="text-xs text-[#888]">This allows sending before DNS is verified. Use with caution.</p>
<textarea value={bypassReason} onChange={e => setBypassReason(e.target.value)} placeholder="Reason for bypass…" rows={3} className="w-full px-2 py-1.5 bg-[#0a0a0a] border border-[#252525] rounded text-xs text-white focus:outline-none" />
<textarea value={bypassReason} onChange={e => setBypassReason(e.target.value)} placeholder="Reason for bypass…" rows={3} className="w-full px-2 py-1.5 bg-[#0a0a0a] border border-[#3a322b] rounded text-xs text-white focus:outline-none" />
<div className="flex gap-2">
<button onClick={() => setBypass(showBypass)} disabled={!bypassReason} className="px-3 py-1.5 bg-yellow-600 rounded text-xs font-medium hover:bg-yellow-500 disabled:opacity-50">Set Bypass</button>
<button onClick={() => setShowBypass(null)} className="px-3 py-1.5 bg-[#1a1a1a] border border-[#252525] rounded text-xs text-[#888] hover:text-white">Cancel</button>
<button onClick={() => setShowBypass(null)} className="px-3 py-1.5 bg-[#231d18] border border-[#3a322b] rounded text-xs text-[#888] hover:text-white">Cancel</button>
</div>
</div>
</div>
@@ -142,12 +142,12 @@ function DomainsContent() {
<div className="space-y-2">
{loadingData ? (
<div className="bg-[#111] border border-[#252525] rounded-lg p-8 text-center text-[#555] text-xs">Loading</div>
<div className="bg-[#111] border border-[#3a322b] rounded-lg p-8 text-center text-[#555] text-xs">Loading</div>
) : domains.length === 0 ? (
<div className="bg-[#111] border border-[#252525] rounded-lg p-8 text-center text-[#555] text-xs">No domains found</div>
<div className="bg-[#111] border border-[#3a322b] rounded-lg p-8 text-center text-[#555] text-xs">No domains found</div>
) : domains.map((d: any) => (
<div key={d.id} className="bg-[#111] border border-[#252525] rounded-lg overflow-hidden">
<div className="px-4 py-3 flex items-center gap-3 cursor-pointer hover:bg-[#161616]" onClick={() => setExpanded(expanded === d.id ? null : d.id)}>
<div key={d.id} className="bg-[#111] border border-[#3a322b] rounded-lg overflow-hidden">
<div className="px-4 py-3 flex items-center gap-3 cursor-pointer hover:bg-[#231d18]" onClick={() => setExpanded(expanded === d.id ? null : d.id)}>
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2 flex-wrap">
<span className="text-sm font-medium text-white">{d.domain}</span>
@@ -156,7 +156,7 @@ function DomainsContent() {
<span className={`px-1.5 py-0.5 rounded text-xs ${d.dns_fully_verified ? 'bg-green-400/10 text-green-400' : 'bg-yellow-400/10 text-yellow-400'}`}>{d.dns_fully_verified ? 'DNS OK' : 'DNS Pending'}</span>
{d.blacklisted && <span className="px-1.5 py-0.5 bg-red-400/10 text-red-400 rounded text-xs">Blacklisted</span>}
{d.bypass_verification && <span className="px-1.5 py-0.5 bg-yellow-400/10 text-yellow-400 rounded text-xs">Bypass Active</span>}
<span className={`px-1.5 py-0.5 rounded text-xs ${d.is_active ? 'bg-green-400/10 text-green-400' : 'bg-[#1a1a1a] text-[#555]'}`}>{d.is_active ? 'Active' : 'Inactive'}</span>
<span className={`px-1.5 py-0.5 rounded text-xs ${d.is_active ? 'bg-green-400/10 text-green-400' : 'bg-[#231d18] text-[#555]'}`}>{d.is_active ? 'Active' : 'Inactive'}</span>
</div>
<div className="flex gap-3 mt-1 text-xs text-[#555]">
<span>Health: {d.health_score}/100</span>
@@ -167,7 +167,7 @@ function DomainsContent() {
</div>
{expanded === d.id && (
<div className="border-t border-[#1a1a1a] px-4 py-3 space-y-3">
<div className="border-t border-[#231d18] px-4 py-3 space-y-3">
{/* DNS checks */}
<div className="grid grid-cols-2 gap-x-6">
<DnsRow label="SPF" valid={d.spf_valid} checkedAt={d.spf_checked_at} />
@@ -178,7 +178,7 @@ function DomainsContent() {
</div>
{/* DNS helper panel */}
<div className="bg-[#0a0a0a] border border-[#1a1a1a] rounded p-3 space-y-2">
<div className="bg-[#0a0a0a] border border-[#231d18] rounded p-3 space-y-2">
<p className="text-xs font-semibold text-[#888]">DNS Record Helper (GoDaddy)</p>
<div className="space-y-1 text-xs text-[#555] font-mono">
<p>SPF: <span className="text-[#888]">v=spf1 include:sendgrid.net ~all</span></p>
@@ -197,10 +197,10 @@ function DomainsContent() {
{/* Actions */}
<div className="flex gap-2 flex-wrap">
<button onClick={() => runVerify(d.id)} disabled={verifying === d.id} className="px-3 py-1.5 bg-[#1a1208] border border-[#2a3a50] rounded text-xs text-[#ffb800] hover:bg-[#0d1a2d] disabled:opacity-50">
<button onClick={() => runVerify(d.id)} disabled={verifying === d.id} className="px-3 py-1.5 bg-[#231d18] border border-[#2a3a50] rounded text-xs text-[#F0A623] hover:bg-[#0d1a2d] disabled:opacity-50">
{verifying === d.id ? 'Verifying…' : 'Run Full Verification'}
</button>
<button onClick={() => toggleActive(d.id, d.is_active)} disabled={!d.dns_fully_verified && !d.bypass_verification && !d.is_active} className="px-3 py-1.5 bg-[#1a1a1a] border border-[#252525] rounded text-xs text-[#888] hover:text-white disabled:opacity-30">
<button onClick={() => toggleActive(d.id, d.is_active)} disabled={!d.dns_fully_verified && !d.bypass_verification && !d.is_active} className="px-3 py-1.5 bg-[#231d18] border border-[#3a322b] rounded text-xs text-[#888] hover:text-white disabled:opacity-30">
{d.is_active ? 'Deactivate' : 'Activate'}
</button>
<button onClick={() => setShowBypass(d.id)} className="px-3 py-1.5 bg-yellow-900/30 border border-yellow-400/20 rounded text-xs text-yellow-400 hover:bg-yellow-900/50">
@@ -219,7 +219,7 @@ function DomainsContent() {
export default function EmailDomainsPage() {
return (
<Suspense fallback={<div className="min-h-screen bg-[#0a0a0a] flex items-center justify-center"><div className="w-8 h-8 border-2 border-[#ffb800] border-t-transparent rounded-full animate-spin" /></div>}>
<Suspense fallback={<div className="min-h-screen bg-[#0a0a0a] flex items-center justify-center"><div className="w-8 h-8 border-2 border-[#F0A623] border-t-transparent rounded-full animate-spin" /></div>}>
<DomainsContent />
</Suspense>
);

View File

@@ -8,7 +8,7 @@ const SITES = ['avbeat','avbeat','backlotbeat','broadcastengineering'];
function DnsCheck({ label, valid }: { label: string; valid: boolean | null }) {
return (
<span className={`inline-flex items-center gap-1 text-xs px-1.5 py-0.5 rounded ${valid === true ? 'bg-green-400/10 text-green-400' : valid === false ? 'bg-red-400/10 text-red-400' : 'bg-[#1a1a1a] text-[#555]'}`}>
<span className={`inline-flex items-center gap-1 text-xs px-1.5 py-0.5 rounded ${valid === true ? 'bg-green-400/10 text-green-400' : valid === false ? 'bg-red-400/10 text-red-400' : 'bg-[#231d18] text-[#555]'}`}>
{valid === true ? '✓' : valid === false ? '✗' : '?'} {label}
</span>
);
@@ -42,7 +42,7 @@ export default function EmailDashboard() {
domains: domains.filter((d: any) => d.site === site),
}));
if (loading) return <div className="min-h-screen bg-[#0a0a0a] flex items-center justify-center"><div className="w-8 h-8 border-2 border-[#ffb800] border-t-transparent rounded-full animate-spin" /></div>;
if (loading) return <div className="min-h-screen bg-[#0a0a0a] flex items-center justify-center"><div className="w-8 h-8 border-2 border-[#F0A623] border-t-transparent rounded-full animate-spin" /></div>;
if (!user) return null;
return (
@@ -61,7 +61,7 @@ export default function EmailDashboard() {
{ label: 'Total Suppressions', value: stats?.total_suppressions ?? 0, color: 'text-red-400' },
{ label: 'Blacklisted Domains', value: stats?.blacklisted ?? 0, color: 'text-red-400' },
].map(c => (
<div key={c.label} className="bg-[#111] border border-[#252525] rounded-lg p-4">
<div key={c.label} className="bg-[#111] border border-[#3a322b] rounded-lg p-4">
<p className={`text-2xl font-bold ${c.color}`}>{c.value}</p>
<p className="text-xs text-[#888] mt-1">{c.label}</p>
</div>
@@ -74,10 +74,10 @@ export default function EmailDashboard() {
const primary = siteDomains.find((d: any) => d.is_primary);
const suppCount = stats?.suppressions_by_site?.[site] ?? 0;
return (
<div key={site} className="bg-[#111] border border-[#252525] rounded-lg p-4 space-y-3">
<div key={site} className="bg-[#111] border border-[#3a322b] rounded-lg p-4 space-y-3">
<div className="flex items-center justify-between">
<h3 className="text-sm font-semibold capitalize">{site}</h3>
<Link href={`/admin/email/domains?site=${site}`} className="text-xs text-[#ffb800] hover:underline">Manage </Link>
<Link href={`/admin/email/domains?site=${site}`} className="text-xs text-[#F0A623] hover:underline">Manage </Link>
</div>
<div className="text-xs text-[#888]">
<span className="text-white">{primary?.domain ?? 'No primary'}</span>
@@ -113,7 +113,7 @@ export default function EmailDashboard() {
{ label: 'Suppressions', href: '/admin/email/suppressions' },
{ label: 'Send Log', href: '/admin/email/send-log' },
].map(l => (
<Link key={l.href} href={l.href} className="bg-[#111] border border-[#252525] rounded-lg p-3 text-center text-sm text-[#888] hover:text-white hover:border-[#ffb800] transition-colors">
<Link key={l.href} href={l.href} className="bg-[#111] border border-[#3a322b] rounded-lg p-3 text-center text-sm text-[#888] hover:text-white hover:border-[#F0A623] transition-colors">
{l.label}
</Link>
))}

View File

@@ -29,7 +29,7 @@ export default function SendLogPage() {
useEffect(() => { if (user) load(); }, [user, load]);
if (loading) return <div className="min-h-screen bg-[#0a0a0a] flex items-center justify-center"><div className="w-8 h-8 border-2 border-[#ffb800] border-t-transparent rounded-full animate-spin" /></div>;
if (loading) return <div className="min-h-screen bg-[#0a0a0a] flex items-center justify-center"><div className="w-8 h-8 border-2 border-[#F0A623] border-t-transparent rounded-full animate-spin" /></div>;
if (!user) return null;
return (
@@ -41,20 +41,20 @@ export default function SendLogPage() {
</div>
<div className="flex gap-2 flex-wrap">
<select value={filterSite} onChange={e => setFilterSite(e.target.value)} className="px-2 py-1 bg-[#111] border border-[#252525] rounded text-xs text-[#888] focus:outline-none">
<select value={filterSite} onChange={e => setFilterSite(e.target.value)} className="px-2 py-1 bg-[#111] border border-[#3a322b] rounded text-xs text-[#888] focus:outline-none">
<option value="">All Sites</option>
{SITES.map(s => <option key={s} value={s}>{s}</option>)}
</select>
<select value={filterStatus} onChange={e => setFilterStatus(e.target.value)} className="px-2 py-1 bg-[#111] border border-[#252525] rounded text-xs text-[#888] focus:outline-none">
<select value={filterStatus} onChange={e => setFilterStatus(e.target.value)} className="px-2 py-1 bg-[#111] border border-[#3a322b] rounded text-xs text-[#888] focus:outline-none">
<option value="">All Statuses</option>
{['queued','sent','failed','suppressed'].map(s => <option key={s} value={s}>{s}</option>)}
</select>
</div>
<div className="bg-[#111] border border-[#252525] rounded-lg overflow-x-auto">
<div className="bg-[#111] border border-[#3a322b] rounded-lg overflow-x-auto">
<table className="w-full text-sm">
<thead>
<tr className="border-b border-[#1a1a1a]">
<tr className="border-b border-[#231d18]">
{['Site','Recipient','Subject','Type','Domain','Status','Sent At','Opens','Clicks'].map(h => (
<th key={h} className="px-3 py-2 text-left text-xs text-[#555] font-medium whitespace-nowrap">{h}</th>
))}
@@ -66,14 +66,14 @@ export default function SendLogPage() {
) : logs.length === 0 ? (
<tr><td colSpan={9} className="px-4 py-8 text-center text-[#555] text-xs">No send log entries</td></tr>
) : logs.map((l: any) => (
<tr key={l.id} className="border-b border-[#1a1a1a] hover:bg-[#161616]">
<tr key={l.id} className="border-b border-[#231d18] hover:bg-[#231d18]">
<td className="px-3 py-2 text-[#888] text-xs capitalize">{l.site}</td>
<td className="px-3 py-2 text-white text-xs">{l.recipient_email}</td>
<td className="px-3 py-2 text-[#ccc] text-xs max-w-xs truncate">{l.subject ?? '—'}</td>
<td className="px-3 py-2 text-[#555] text-xs">{l.send_type}</td>
<td className="px-3 py-2 text-[#555] text-xs">{l.rmp_email_sending_domains?.domain ?? '—'}</td>
<td className="px-3 py-2">
<span className={`px-2 py-0.5 rounded-full text-xs ${l.status === 'sent' ? 'bg-green-400/10 text-green-400' : l.status === 'failed' ? 'bg-red-400/10 text-red-400' : l.status === 'suppressed' ? 'bg-yellow-400/10 text-yellow-400' : 'bg-[#1a1a1a] text-[#555]'}`}>{l.status}</span>
<span className={`px-2 py-0.5 rounded-full text-xs ${l.status === 'sent' ? 'bg-green-400/10 text-green-400' : l.status === 'failed' ? 'bg-red-400/10 text-red-400' : l.status === 'suppressed' ? 'bg-yellow-400/10 text-yellow-400' : 'bg-[#231d18] text-[#555]'}`}>{l.status}</span>
</td>
<td className="px-3 py-2 text-[#555] text-xs">{l.sent_at ? new Date(l.sent_at).toLocaleString() : '—'}</td>
<td className="px-3 py-2 text-[#888] text-xs">{l.opens}</td>

View File

@@ -72,7 +72,7 @@ export default function SuppressionsPage() {
load();
}
if (loading) return <div className="min-h-screen bg-[#0a0a0a] flex items-center justify-center"><div className="w-8 h-8 border-2 border-[#ffb800] border-t-transparent rounded-full animate-spin" /></div>;
if (loading) return <div className="min-h-screen bg-[#0a0a0a] flex items-center justify-center"><div className="w-8 h-8 border-2 border-[#F0A623] border-t-transparent rounded-full animate-spin" /></div>;
if (!user) return null;
return (
@@ -84,29 +84,29 @@ export default function SuppressionsPage() {
<Link href="/admin/email" className="text-xs text-[#555] hover:text-[#888]"> Email</Link>
</div>
<div className="flex gap-2">
<button onClick={() => setShowAddForm(true)} className="px-3 py-1.5 bg-[#ffb800] rounded text-xs font-medium hover:bg-blue-500">+ Add Suppression</button>
<button onClick={() => setShowAddForm(true)} className="px-3 py-1.5 bg-[#F0A623] rounded text-xs font-medium hover:bg-blue-500">+ Add Suppression</button>
</div>
</div>
{/* Site selector */}
<div className="flex gap-1 flex-wrap">
{SITES.map(s => (
<button key={s} onClick={() => setSite(s)} className={`px-3 py-1 rounded text-xs capitalize ${site === s ? 'bg-[#ffb800] text-white' : 'bg-[#111] border border-[#252525] text-[#888]'}`}>{s}</button>
<button key={s} onClick={() => setSite(s)} className={`px-3 py-1 rounded text-xs capitalize ${site === s ? 'bg-[#F0A623] text-white' : 'bg-[#111] border border-[#3a322b] text-[#888]'}`}>{s}</button>
))}
</div>
{/* Stats */}
{stats && (
<div className="grid grid-cols-3 gap-3">
<div className="bg-[#111] border border-[#252525] rounded-lg p-3">
<div className="bg-[#111] border border-[#3a322b] rounded-lg p-3">
<p className="text-xl font-bold text-red-400">{stats.total ?? 0}</p>
<p className="text-xs text-[#888] mt-0.5">Total Suppressed</p>
</div>
<div className="bg-[#111] border border-[#252525] rounded-lg p-3">
<div className="bg-[#111] border border-[#3a322b] rounded-lg p-3">
<p className="text-xl font-bold text-yellow-400">{stats.added_this_month ?? 0}</p>
<p className="text-xs text-[#888] mt-0.5">Added This Month</p>
</div>
<div className="bg-[#111] border border-[#252525] rounded-lg p-3">
<div className="bg-[#111] border border-[#3a322b] rounded-lg p-3">
<p className="text-xl font-bold text-orange-400">{stats.hostile_replies ?? 0}</p>
<p className="text-xs text-[#888] mt-0.5">Hostile Replies</p>
</div>
@@ -115,44 +115,44 @@ export default function SuppressionsPage() {
{/* Filters */}
<div className="flex gap-2 flex-wrap">
<select value={filterType} onChange={e => setFilterType(e.target.value)} className="px-2 py-1 bg-[#111] border border-[#252525] rounded text-xs text-[#888] focus:outline-none">
<select value={filterType} onChange={e => setFilterType(e.target.value)} className="px-2 py-1 bg-[#111] border border-[#3a322b] rounded text-xs text-[#888] focus:outline-none">
<option value="">All Types</option>
{SUPP_TYPES.map(t => <option key={t} value={t}>{t.replace(/_/g, ' ')}</option>)}
</select>
<input value={search} onChange={e => setSearch(e.target.value)} placeholder="Search email…" className="px-2 py-1 bg-[#111] border border-[#252525] rounded text-xs text-white placeholder-[#555] focus:outline-none focus:border-[#ffb800]" />
<input value={search} onChange={e => setSearch(e.target.value)} placeholder="Search email…" className="px-2 py-1 bg-[#111] border border-[#3a322b] rounded text-xs text-white placeholder-[#555] focus:outline-none focus:border-[#F0A623]" />
</div>
{showAddForm && (
<form onSubmit={handleAddSuppression} className="bg-[#111] border border-[#252525] rounded-lg p-4 space-y-3">
<form onSubmit={handleAddSuppression} className="bg-[#111] border border-[#3a322b] rounded-lg p-4 space-y-3">
<h3 className="text-sm font-semibold">Add Manual Suppression</h3>
<div className="grid grid-cols-2 gap-3">
<div>
<label className="text-xs text-[#888] block mb-1">Email *</label>
<input required type="email" value={addForm.email} onChange={e => setAddForm(p => ({ ...p, email: e.target.value }))} className="w-full px-2 py-1.5 bg-[#0a0a0a] border border-[#252525] rounded text-xs text-white focus:outline-none focus:border-[#ffb800]" />
<input required type="email" value={addForm.email} onChange={e => setAddForm(p => ({ ...p, email: e.target.value }))} className="w-full px-2 py-1.5 bg-[#0a0a0a] border border-[#3a322b] rounded text-xs text-white focus:outline-none focus:border-[#F0A623]" />
</div>
<div>
<label className="text-xs text-[#888] block mb-1">Type</label>
<select value={addForm.suppression_type} onChange={e => setAddForm(p => ({ ...p, suppression_type: e.target.value }))} className="w-full px-2 py-1.5 bg-[#0a0a0a] border border-[#252525] rounded text-xs text-white focus:outline-none">
<select value={addForm.suppression_type} onChange={e => setAddForm(p => ({ ...p, suppression_type: e.target.value }))} className="w-full px-2 py-1.5 bg-[#0a0a0a] border border-[#3a322b] rounded text-xs text-white focus:outline-none">
{SUPP_TYPES.map(t => <option key={t} value={t}>{t.replace(/_/g, ' ')}</option>)}
</select>
</div>
<div className="col-span-2">
<label className="text-xs text-[#888] block mb-1">Notes</label>
<input value={addForm.notes} onChange={e => setAddForm(p => ({ ...p, notes: e.target.value }))} className="w-full px-2 py-1.5 bg-[#0a0a0a] border border-[#252525] rounded text-xs text-white focus:outline-none focus:border-[#ffb800]" />
<input value={addForm.notes} onChange={e => setAddForm(p => ({ ...p, notes: e.target.value }))} className="w-full px-2 py-1.5 bg-[#0a0a0a] border border-[#3a322b] rounded text-xs text-white focus:outline-none focus:border-[#F0A623]" />
</div>
</div>
<div className="flex gap-2">
<button type="submit" disabled={saving} className="px-3 py-1.5 bg-[#ffb800] rounded text-xs font-medium hover:bg-blue-500 disabled:opacity-50">{saving ? 'Saving…' : 'Add'}</button>
<button type="button" onClick={() => setShowAddForm(false)} className="px-3 py-1.5 bg-[#1a1a1a] border border-[#252525] rounded text-xs text-[#888] hover:text-white">Cancel</button>
<button type="submit" disabled={saving} className="px-3 py-1.5 bg-[#F0A623] rounded text-xs font-medium hover:bg-blue-500 disabled:opacity-50">{saving ? 'Saving…' : 'Add'}</button>
<button type="button" onClick={() => setShowAddForm(false)} className="px-3 py-1.5 bg-[#231d18] border border-[#3a322b] rounded text-xs text-[#888] hover:text-white">Cancel</button>
</div>
</form>
)}
{/* Suppressions table */}
<div className="bg-[#111] border border-[#252525] rounded-lg overflow-x-auto">
<div className="bg-[#111] border border-[#3a322b] rounded-lg overflow-x-auto">
<table className="w-full text-sm">
<thead>
<tr className="border-b border-[#1a1a1a]">
<tr className="border-b border-[#231d18]">
{['Email','Type','Source','Reply Text','Domain','Date',''].map(h => (
<th key={h} className="px-3 py-2 text-left text-xs text-[#555] font-medium">{h}</th>
))}
@@ -164,10 +164,10 @@ export default function SuppressionsPage() {
) : suppressions.length === 0 ? (
<tr><td colSpan={7} className="px-4 py-8 text-center text-[#555] text-xs">No suppressions found</td></tr>
) : suppressions.map((s: any) => (
<tr key={s.id} className="border-b border-[#1a1a1a] hover:bg-[#161616]">
<tr key={s.id} className="border-b border-[#231d18] hover:bg-[#231d18]">
<td className="px-3 py-2 text-white text-xs">{s.email}</td>
<td className="px-3 py-2">
<span className={`px-2 py-0.5 rounded-full text-xs ${s.suppression_type === 'hostile_reply' ? 'bg-red-400/10 text-red-400' : s.suppression_type === 'unsubscribe' ? 'bg-yellow-400/10 text-yellow-400' : 'bg-[#1a1a1a] text-[#555]'}`}>{s.suppression_type?.replace(/_/g, ' ')}</span>
<span className={`px-2 py-0.5 rounded-full text-xs ${s.suppression_type === 'hostile_reply' ? 'bg-red-400/10 text-red-400' : s.suppression_type === 'unsubscribe' ? 'bg-yellow-400/10 text-yellow-400' : 'bg-[#231d18] text-[#555]'}`}>{s.suppression_type?.replace(/_/g, ' ')}</span>
</td>
<td className="px-3 py-2 text-[#555] text-xs">{s.source}</td>
<td className="px-3 py-2 text-[#555] text-xs max-w-xs truncate">{s.reply_text ?? '—'}</td>
@@ -183,40 +183,40 @@ export default function SuppressionsPage() {
</div>
{/* Keyword Management */}
<div className="bg-[#111] border border-[#252525] rounded-lg overflow-hidden">
<div className="px-4 py-3 border-b border-[#252525] flex items-center justify-between">
<div className="bg-[#111] border border-[#3a322b] rounded-lg overflow-hidden">
<div className="px-4 py-3 border-b border-[#3a322b] flex items-center justify-between">
<h2 className="text-sm font-semibold">Suppression Keywords</h2>
<button onClick={() => setShowKeywordForm(true)} className="text-xs text-[#ffb800] hover:underline">+ Add Keyword</button>
<button onClick={() => setShowKeywordForm(true)} className="text-xs text-[#F0A623] hover:underline">+ Add Keyword</button>
</div>
{showKeywordForm && (
<form onSubmit={handleAddKeyword} className="px-4 py-3 border-b border-[#1a1a1a] flex gap-2 items-end">
<form onSubmit={handleAddKeyword} className="px-4 py-3 border-b border-[#231d18] flex gap-2 items-end">
<div>
<label className="text-xs text-[#888] block mb-1">Keyword *</label>
<input required value={keywordForm.keyword} onChange={e => setKeywordForm(p => ({ ...p, keyword: e.target.value }))} className="px-2 py-1.5 bg-[#0a0a0a] border border-[#252525] rounded text-xs text-white focus:outline-none focus:border-[#ffb800]" />
<input required value={keywordForm.keyword} onChange={e => setKeywordForm(p => ({ ...p, keyword: e.target.value }))} className="px-2 py-1.5 bg-[#0a0a0a] border border-[#3a322b] rounded text-xs text-white focus:outline-none focus:border-[#F0A623]" />
</div>
<div>
<label className="text-xs text-[#888] block mb-1">Type</label>
<select value={keywordForm.keyword_type} onChange={e => setKeywordForm(p => ({ ...p, keyword_type: e.target.value }))} className="px-2 py-1.5 bg-[#0a0a0a] border border-[#252525] rounded text-xs text-white focus:outline-none">
<select value={keywordForm.keyword_type} onChange={e => setKeywordForm(p => ({ ...p, keyword_type: e.target.value }))} className="px-2 py-1.5 bg-[#0a0a0a] border border-[#3a322b] rounded text-xs text-white focus:outline-none">
<option value="unsubscribe">unsubscribe</option>
<option value="hostile">hostile</option>
</select>
</div>
<button type="submit" disabled={saving} className="px-3 py-1.5 bg-[#ffb800] rounded text-xs font-medium hover:bg-blue-500 disabled:opacity-50">Add</button>
<button type="button" onClick={() => setShowKeywordForm(false)} className="px-3 py-1.5 bg-[#1a1a1a] border border-[#252525] rounded text-xs text-[#888]">Cancel</button>
<button type="submit" disabled={saving} className="px-3 py-1.5 bg-[#F0A623] rounded text-xs font-medium hover:bg-blue-500 disabled:opacity-50">Add</button>
<button type="button" onClick={() => setShowKeywordForm(false)} className="px-3 py-1.5 bg-[#231d18] border border-[#3a322b] rounded text-xs text-[#888]">Cancel</button>
</form>
)}
<table className="w-full text-sm">
<thead><tr className="border-b border-[#1a1a1a]">{['Keyword','Type','Active',''].map(h => <th key={h} className="px-4 py-2 text-left text-xs text-[#555]">{h}</th>)}</tr></thead>
<thead><tr className="border-b border-[#231d18]">{['Keyword','Type','Active',''].map(h => <th key={h} className="px-4 py-2 text-left text-xs text-[#555]">{h}</th>)}</tr></thead>
<tbody>
{keywords.map((k: any) => (
<tr key={k.id} className="border-b border-[#1a1a1a] hover:bg-[#161616]">
<tr key={k.id} className="border-b border-[#231d18] hover:bg-[#231d18]">
<td className="px-4 py-2 text-white text-xs">{k.keyword}</td>
<td className="px-4 py-2 text-[#888] text-xs">{k.keyword_type}</td>
<td className="px-4 py-2">
<span className={`px-2 py-0.5 rounded-full text-xs ${k.active ? 'bg-green-400/10 text-green-400' : 'bg-[#1a1a1a] text-[#555]'}`}>{k.active ? 'Active' : 'Inactive'}</span>
<span className={`px-2 py-0.5 rounded-full text-xs ${k.active ? 'bg-green-400/10 text-green-400' : 'bg-[#231d18] text-[#555]'}`}>{k.active ? 'Active' : 'Inactive'}</span>
</td>
<td className="px-4 py-2">
<button onClick={() => toggleKeyword(k.id, k.active)} className="text-xs text-[#ffb800] hover:underline">{k.active ? 'Deactivate' : 'Activate'}</button>
<button onClick={() => toggleKeyword(k.id, k.active)} className="text-xs text-[#F0A623] hover:underline">{k.active ? 'Deactivate' : 'Activate'}</button>
</td>
</tr>
))}

View File

@@ -91,7 +91,7 @@ interface StatCardProps {
function StatCard({ label, value, sub, accentColor, bgColor, icon, badge }: StatCardProps) {
return (
<div className="bg-[#111] border border-[#252525] rounded-lg p-5 flex flex-col gap-3 hover:border-[#333] transition-colors">
<div className="bg-[#111] border border-[#3a322b] rounded-lg p-5 flex flex-col gap-3 hover:border-[#333] transition-colors">
<div className="flex items-start justify-between">
<div className={`w-10 h-10 rounded-lg flex items-center justify-center ${bgColor}`}>
<span className={accentColor}>{icon}</span>
@@ -116,7 +116,7 @@ function StatCard({ label, value, sub, accentColor, bgColor, icon, badge }: Stat
function ChartTooltip({ active, payload, label }: any) {
if (!active || !payload?.length) return null;
return (
<div className="bg-[#1a1a1a] border border-[#333] rounded-lg px-3 py-2 text-xs font-body shadow-xl">
<div className="bg-[#231d18] border border-[#333] rounded-lg px-3 py-2 text-xs font-body shadow-xl">
{label && <p className="text-[#888] mb-1">{label}</p>}
{payload.map((entry: any, i: number) => (
<p key={i} style={{ color: entry.color }} className="leading-5">
@@ -167,7 +167,7 @@ const EMPTY_STATS: ForumStats = {
},
};
const CATEGORY_COLORS = ['#ffb800', '#f59e0b', '#10b981', '#8b5cf6', '#ef4444', '#06b6d4', '#f97316', '#84cc16'];
const CATEGORY_COLORS = ['#F0A623', '#f59e0b', '#10b981', '#8b5cf6', '#ef4444', '#06b6d4', '#f97316', '#84cc16'];
export default function ForumDashboardPage() {
const { user, loading } = useAuth();
@@ -204,7 +204,7 @@ export default function ForumDashboardPage() {
return (
<div className="min-h-screen bg-[#0a0a0a] flex items-center justify-center">
<div className="flex flex-col items-center gap-3">
<div className="w-8 h-8 border-2 border-[#ffb800] border-t-transparent rounded-full animate-spin" />
<div className="w-8 h-8 border-2 border-[#F0A623] border-t-transparent rounded-full animate-spin" />
<p className="text-[#555] text-sm font-body">Loading forum dashboard</p>
</div>
</div>
@@ -236,7 +236,7 @@ export default function ForumDashboardPage() {
<div className="flex items-center gap-3">
<button
onClick={fetchStats}
className="flex items-center gap-2 px-3 py-2 bg-[#111] border border-[#252525] rounded-lg text-xs text-[#888] hover:text-white hover:border-[#333] transition-all font-body"
className="flex items-center gap-2 px-3 py-2 bg-[#111] border border-[#3a322b] rounded-lg text-xs text-[#888] hover:text-white hover:border-[#333] transition-all font-body"
>
<svg className="w-3.5 h-3.5" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
@@ -245,7 +245,7 @@ export default function ForumDashboardPage() {
</button>
<Link
href="/admin/forum-moderation"
className="flex items-center gap-2 px-3 py-2 bg-[#ffb800] rounded-lg text-xs text-white hover:bg-blue-500 transition-colors font-body font-semibold"
className="flex items-center gap-2 px-3 py-2 bg-[#F0A623] rounded-lg text-xs text-white hover:bg-blue-500 transition-colors font-body font-semibold"
>
<svg className="w-3.5 h-3.5" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z" />
@@ -386,7 +386,7 @@ export default function ForumDashboardPage() {
{/* Thread Growth Chart */}
<section className="mb-8">
<div className="bg-[#111] border border-[#252525] rounded-lg p-6">
<div className="bg-[#111] border border-[#3a322b] rounded-lg p-6">
<SectionHeader
title="Thread Growth"
subtitle="Weekly new threads and replies over the past 8 weeks"
@@ -398,7 +398,7 @@ export default function ForumDashboardPage() {
) : (
<ResponsiveContainer width="100%" height={240}>
<LineChart data={weeklyGrowth} margin={{ top: 5, right: 10, left: -20, bottom: 5 }}>
<CartesianGrid strokeDasharray="3 3" stroke="#1a1a1a" />
<CartesianGrid strokeDasharray="3 3" stroke="#231d18" />
<XAxis dataKey="week" tick={{ fill: '#555', fontSize: 11 }} axisLine={false} tickLine={false} />
<YAxis tick={{ fill: '#555', fontSize: 11 }} axisLine={false} tickLine={false} />
<Tooltip content={<ChartTooltip />} />
@@ -407,7 +407,7 @@ export default function ForumDashboardPage() {
iconType="circle"
iconSize={8}
/>
<Line type="monotone" dataKey="threads" stroke="#ffb800" strokeWidth={2} dot={{ fill: '#ffb800', r: 3 }} name="New Threads" />
<Line type="monotone" dataKey="threads" stroke="#F0A623" strokeWidth={2} dot={{ fill: '#F0A623', r: 3 }} name="New Threads" />
<Line type="monotone" dataKey="replies" stroke="#10b981" strokeWidth={2} dot={{ fill: '#10b981', r: 3 }} name="New Replies" />
</LineChart>
</ResponsiveContainer>
@@ -419,33 +419,33 @@ export default function ForumDashboardPage() {
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-8">
{/* Top Categories */}
<div className="bg-[#111] border border-[#252525] rounded-lg overflow-hidden">
<div className="px-5 py-4 border-b border-[#1a1a1a] flex items-center justify-between">
<div className="bg-[#111] border border-[#3a322b] rounded-lg overflow-hidden">
<div className="px-5 py-4 border-b border-[#231d18] flex items-center justify-between">
<div>
<h2 className="text-sm font-display font-bold text-white">Top Categories</h2>
<p className="text-xs text-[#555] font-body mt-0.5">By thread count</p>
</div>
<Link href="/forum" className="text-xs text-[#ffb800] hover:text-blue-300 font-body transition-colors">View forum </Link>
<Link href="/forum" className="text-xs text-[#F0A623] hover:text-blue-300 font-body transition-colors">View forum </Link>
</div>
{topCategories.length === 0 ? (
<div className="px-5 py-10 text-center">
<p className="text-[#555] text-sm font-body">No categories found</p>
</div>
) : (
<div className="divide-y divide-[#1a1a1a]">
<div className="divide-y divide-[#231d18]">
{topCategories.map((cat, idx) => {
const maxThreads = topCategories[0]?.thread_count ?? 1;
const pct = maxThreads > 0 ? Math.round((cat.thread_count / maxThreads) * 100) : 0;
const color = CATEGORY_COLORS[idx % CATEGORY_COLORS.length];
return (
<div key={cat.id} className="px-5 py-3 hover:bg-[#0d0d0d] transition-colors">
<div key={cat.id} className="px-5 py-3 hover:bg-[#1c1815] transition-colors">
<div className="flex items-center gap-3 mb-1.5">
<span className="text-base leading-none">{cat.icon}</span>
<span className="text-sm text-white font-body flex-1 truncate">{cat.name}</span>
<span className="text-xs text-[#888] font-body font-semibold">{cat.thread_count} threads</span>
<span className="text-xs text-[#555] font-body">{cat.post_count} posts</span>
</div>
<div className="h-1.5 bg-[#1a1a1a] rounded-full overflow-hidden ml-7">
<div className="h-1.5 bg-[#231d18] rounded-full overflow-hidden ml-7">
<div
className="h-full rounded-full transition-all duration-500"
style={{ width: `${pct}%`, backgroundColor: color }}
@@ -459,8 +459,8 @@ export default function ForumDashboardPage() {
</div>
{/* Active Users */}
<div className="bg-[#111] border border-[#252525] rounded-lg overflow-hidden">
<div className="px-5 py-4 border-b border-[#1a1a1a]">
<div className="bg-[#111] border border-[#3a322b] rounded-lg overflow-hidden">
<div className="px-5 py-4 border-b border-[#231d18]">
<h2 className="text-sm font-display font-bold text-white">Active Users</h2>
<p className="text-xs text-[#555] font-body mt-0.5">Most active in the last 30 days</p>
</div>
@@ -469,13 +469,13 @@ export default function ForumDashboardPage() {
<p className="text-[#555] text-sm font-body">No active users found</p>
</div>
) : (
<div className="divide-y divide-[#1a1a1a]">
<div className="divide-y divide-[#231d18]">
{activeUsers.map((u, idx) => (
<div key={u.author_id} className="px-5 py-3 flex items-center gap-3 hover:bg-[#0d0d0d] transition-colors">
<div className="w-6 h-6 rounded-full bg-[#1a1a1a] flex items-center justify-center flex-shrink-0 text-xs font-bold font-body text-[#555]">
<div key={u.author_id} className="px-5 py-3 flex items-center gap-3 hover:bg-[#1c1815] transition-colors">
<div className="w-6 h-6 rounded-full bg-[#231d18] flex items-center justify-center flex-shrink-0 text-xs font-bold font-body text-[#555]">
{idx + 1}
</div>
<div className="w-8 h-8 rounded-full bg-gradient-to-br from-[#ffb800] to-[#8b5cf6] flex items-center justify-center flex-shrink-0">
<div className="w-8 h-8 rounded-full bg-gradient-to-br from-[#F0A623] to-[#8b5cf6] flex items-center justify-center flex-shrink-0">
<span className="text-xs font-bold text-white font-body">
{u.author_name.charAt(0).toUpperCase()}
</span>
@@ -498,14 +498,14 @@ export default function ForumDashboardPage() {
{/* Category Bar Chart */}
{topCategories.length > 0 && (
<section className="mb-8">
<div className="bg-[#111] border border-[#252525] rounded-lg p-6">
<div className="bg-[#111] border border-[#3a322b] rounded-lg p-6">
<SectionHeader
title="Category Activity"
subtitle="Thread and post distribution across categories"
/>
<ResponsiveContainer width="100%" height={220}>
<BarChart data={topCategories} margin={{ top: 5, right: 10, left: -20, bottom: 5 }}>
<CartesianGrid strokeDasharray="3 3" stroke="#1a1a1a" />
<CartesianGrid strokeDasharray="3 3" stroke="#231d18" />
<XAxis
dataKey="name"
tick={{ fill: '#555', fontSize: 10 }}
@@ -523,7 +523,7 @@ export default function ForumDashboardPage() {
iconType="circle"
iconSize={8}
/>
<Bar dataKey="thread_count" fill="#ffb800" radius={[3, 3, 0, 0]} name="Threads" />
<Bar dataKey="thread_count" fill="#F0A623" radius={[3, 3, 0, 0]} name="Threads" />
<Bar dataKey="post_count" fill="#10b981" radius={[3, 3, 0, 0]} name="Posts" />
</BarChart>
</ResponsiveContainer>
@@ -533,22 +533,22 @@ export default function ForumDashboardPage() {
{/* Moderation Stats */}
<section className="mb-8">
<div className="bg-[#111] border border-[#252525] rounded-lg overflow-hidden">
<div className="px-5 py-4 border-b border-[#1a1a1a] flex items-center justify-between">
<div className="bg-[#111] border border-[#3a322b] rounded-lg overflow-hidden">
<div className="px-5 py-4 border-b border-[#231d18] flex items-center justify-between">
<div>
<h2 className="text-sm font-display font-bold text-white">Moderation Stats</h2>
<p className="text-xs text-[#555] font-body mt-0.5">Current moderation state across all forum content</p>
</div>
<Link href="/admin/forum-moderation" className="text-xs text-[#ffb800] hover:text-blue-300 font-body transition-colors">Open moderation </Link>
<Link href="/admin/forum-moderation" className="text-xs text-[#F0A623] hover:text-blue-300 font-body transition-colors">Open moderation </Link>
</div>
<div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-6 divide-x divide-y sm:divide-y-0 divide-[#1a1a1a]">
<div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-6 divide-x divide-y sm:divide-y-0 divide-[#231d18]">
{[
{ label: 'Flagged Threads', value: moderation.flaggedThreads, color: 'text-red-400', bg: 'bg-red-500/10' },
{ label: 'Flagged Replies', value: moderation.flaggedReplies, color: 'text-red-400', bg: 'bg-red-500/10' },
{ label: 'Locked Threads', value: moderation.lockedThreads, color: 'text-orange-400', bg: 'bg-orange-500/10' },
{ label: 'Pinned Threads', value: moderation.pinnedThreads, color: 'text-yellow-400', bg: 'bg-yellow-500/10' },
{ label: 'Featured Threads', value: moderation.featuredThreads, color: 'text-blue-400', bg: 'bg-blue-500/10' },
{ label: 'Hidden Replies', value: moderation.hiddenReplies, color: 'text-[#888]', bg: 'bg-[#1a1a1a]' },
{ label: 'Hidden Replies', value: moderation.hiddenReplies, color: 'text-[#888]', bg: 'bg-[#231d18]' },
].map(({ label, value, color, bg }) => (
<div key={label} className="px-4 py-5 flex flex-col items-center gap-1 text-center">
<p className={`text-2xl font-bold font-display ${color}`}>{value}</p>

View File

@@ -62,7 +62,7 @@ function FlagModal({ onConfirm, onCancel }: FlagModalProps) {
const [reason, setReason] = useState('');
return (
<div className="fixed inset-0 bg-black/70 flex items-center justify-center z-50 px-4">
<div className="bg-[#1a1a1a] border border-[#333] rounded-xl p-6 w-full max-w-md">
<div className="bg-[#231d18] border border-[#333] rounded-xl p-6 w-full max-w-md">
<h3 className="text-white font-heading font-bold text-lg mb-2">Flag Content</h3>
<p className="text-[#888] font-body text-sm mb-4">Provide a reason for flagging this content (optional).</p>
<textarea
@@ -70,10 +70,10 @@ function FlagModal({ onConfirm, onCancel }: FlagModalProps) {
onChange={e => setReason(e.target.value)}
rows={3}
placeholder="e.g. Spam, harassment, off-topic..."
className="w-full bg-[#0d0d0d] border border-[#333] rounded-lg px-3 py-2 text-white font-body text-sm focus:outline-none focus:border-[#ef4444] resize-none mb-4"
className="w-full bg-[#1c1815] border border-[#333] rounded-lg px-3 py-2 text-white font-body text-sm focus:outline-none focus:border-[#ef4444] resize-none mb-4"
/>
<div className="flex gap-3 justify-end">
<button onClick={onCancel} className="px-4 py-2 text-sm font-body text-[#888] hover:text-white bg-[#252525] hover:bg-[#333] rounded-lg transition-colors">
<button onClick={onCancel} className="px-4 py-2 text-sm font-body text-[#888] hover:text-white bg-[#3a322b] hover:bg-[#333] rounded-lg transition-colors">
Cancel
</button>
<button onClick={() => onConfirm(reason)} className="px-4 py-2 text-sm font-body font-semibold text-white bg-red-600 hover:bg-red-700 rounded-lg transition-colors">
@@ -119,14 +119,14 @@ function ThreadRow({ thread, onAction, loadingId }: ThreadRowProps) {
onCancel={() => setShowFlagModal(false)}
/>
)}
<div className={`bg-[#111] border rounded-lg p-4 transition-colors ${isAiFlagged ? 'border-purple-500/50' : thread.is_flagged ? 'border-red-500/40' : 'border-[#252525]'}`}>
<div className={`bg-[#111] border rounded-lg p-4 transition-colors ${isAiFlagged ? 'border-purple-500/50' : thread.is_flagged ? 'border-red-500/40' : 'border-[#3a322b]'}`}>
<div className="flex items-start gap-3">
<div className="flex-1 min-w-0">
{/* Title + badges */}
<div className="flex flex-wrap items-center gap-2 mb-1">
<Link
href={`/forum/thread/${thread.id}`}
className="text-white font-body font-semibold text-sm hover:text-[#ffb800] transition-colors truncate max-w-xs"
className="text-white font-body font-semibold text-sm hover:text-[#F0A623] transition-colors truncate max-w-xs"
target="_blank"
>
{thread.title}
@@ -159,7 +159,7 @@ function ThreadRow({ thread, onAction, loadingId }: ThreadRowProps) {
onClick={() => onAction(thread.id, thread.is_pinned ? 'unpin' : 'pin')}
className={`px-2.5 py-1 text-xs font-body font-semibold rounded-md transition-colors disabled:opacity-50 ${
thread.is_pinned
? 'bg-amber-400/20 text-amber-400 hover:bg-amber-400/30' :'bg-[#1a1a1a] text-[#888] hover:bg-[#252525] hover:text-amber-400'
? 'bg-amber-400/20 text-amber-400 hover:bg-amber-400/30' :'bg-[#231d18] text-[#888] hover:bg-[#3a322b] hover:text-amber-400'
}`}
>
{thread.is_pinned ? 'Unpin' : 'Pin'}
@@ -169,7 +169,7 @@ function ThreadRow({ thread, onAction, loadingId }: ThreadRowProps) {
onClick={() => onAction(thread.id, thread.is_featured ? 'unfeature' : 'feature')}
className={`px-2.5 py-1 text-xs font-body font-semibold rounded-md transition-colors disabled:opacity-50 ${
thread.is_featured
? 'bg-yellow-400/20 text-yellow-400 hover:bg-yellow-400/30' :'bg-[#1a1a1a] text-[#888] hover:bg-[#252525] hover:text-yellow-400'
? 'bg-yellow-400/20 text-yellow-400 hover:bg-yellow-400/30' :'bg-[#231d18] text-[#888] hover:bg-[#3a322b] hover:text-yellow-400'
}`}
>
{thread.is_featured ? 'Unfeature' : 'Feature'}
@@ -179,7 +179,7 @@ function ThreadRow({ thread, onAction, loadingId }: ThreadRowProps) {
onClick={() => onAction(thread.id, thread.is_locked ? 'unlock' : 'lock')}
className={`px-2.5 py-1 text-xs font-body font-semibold rounded-md transition-colors disabled:opacity-50 ${
thread.is_locked
? 'bg-orange-400/20 text-orange-400 hover:bg-orange-400/30' :'bg-[#1a1a1a] text-[#888] hover:bg-[#252525] hover:text-orange-400'
? 'bg-orange-400/20 text-orange-400 hover:bg-orange-400/30' :'bg-[#231d18] text-[#888] hover:bg-[#3a322b] hover:text-orange-400'
}`}
>
{thread.is_locked ? 'Unlock' : 'Lock'}
@@ -199,7 +199,7 @@ function ThreadRow({ thread, onAction, loadingId }: ThreadRowProps) {
<button
disabled={isLoading}
onClick={() => setShowFlagModal(true)}
className="px-2.5 py-1 text-xs font-body font-semibold rounded-md bg-[#1a1a1a] text-[#888] hover:bg-[#252525] hover:text-red-400 transition-colors disabled:opacity-50"
className="px-2.5 py-1 text-xs font-body font-semibold rounded-md bg-[#231d18] text-[#888] hover:bg-[#3a322b] hover:text-red-400 transition-colors disabled:opacity-50"
>
Flag
</button>
@@ -235,7 +235,7 @@ function ReplyRow({ reply, onAction, loadingId }: ReplyRowProps) {
onCancel={() => setShowFlagModal(false)}
/>
)}
<div className={`bg-[#111] border rounded-lg p-4 transition-colors ${isAiFlagged ? 'border-purple-500/50' : reply.is_flagged ? 'border-red-500/40' : reply.is_hidden ? 'border-[#333] opacity-60' : 'border-[#252525]'}`}>
<div className={`bg-[#111] border rounded-lg p-4 transition-colors ${isAiFlagged ? 'border-purple-500/50' : reply.is_flagged ? 'border-red-500/40' : reply.is_hidden ? 'border-[#333] opacity-60' : 'border-[#3a322b]'}`}>
<div className="flex items-start gap-3">
<div className="flex-1 min-w-0">
{/* Thread link + badges */}
@@ -243,13 +243,13 @@ function ReplyRow({ reply, onAction, loadingId }: ReplyRowProps) {
{reply.forum_threads && (
<Link
href={`/forum/thread/${reply.forum_threads.id}`}
className="text-[#ffb800] font-body text-xs hover:underline truncate max-w-xs"
className="text-[#F0A623] font-body text-xs hover:underline truncate max-w-xs"
target="_blank"
>
{reply.forum_threads.title}
</Link>
)}
{reply.is_hidden && <StatusBadge label="👁 Hidden" color="text-[#888] bg-[#252525]" />}
{reply.is_hidden && <StatusBadge label="👁 Hidden" color="text-[#888] bg-[#3a322b]" />}
{isAiFlagged && <StatusBadge label="🤖 AI Flagged" color="text-purple-400 bg-purple-400/10" />}
{reply.is_flagged && !isAiFlagged && <StatusBadge label="🚩 Flagged" color="text-red-400 bg-red-400/10" />}
</div>
@@ -273,8 +273,8 @@ function ReplyRow({ reply, onAction, loadingId }: ReplyRowProps) {
onClick={() => onAction(reply.id, reply.is_hidden ? 'unhide' : 'hide')}
className={`px-2.5 py-1 text-xs font-body font-semibold rounded-md transition-colors disabled:opacity-50 ${
reply.is_hidden
? 'bg-[#252525] text-[#aaa] hover:bg-[#333]'
: 'bg-[#1a1a1a] text-[#888] hover:bg-[#252525] hover:text-white'
? 'bg-[#3a322b] text-[#aaa] hover:bg-[#333]'
: 'bg-[#231d18] text-[#888] hover:bg-[#3a322b] hover:text-white'
}`}
>
{reply.is_hidden ? 'Unhide' : 'Hide'}
@@ -294,7 +294,7 @@ function ReplyRow({ reply, onAction, loadingId }: ReplyRowProps) {
<button
disabled={isLoading}
onClick={() => setShowFlagModal(true)}
className="px-2.5 py-1 text-xs font-body font-semibold rounded-md bg-[#1a1a1a] text-[#888] hover:bg-[#252525] hover:text-red-400 transition-colors disabled:opacity-50"
className="px-2.5 py-1 text-xs font-body font-semibold rounded-md bg-[#231d18] text-[#888] hover:bg-[#3a322b] hover:text-red-400 transition-colors disabled:opacity-50"
>
Flag
</button>
@@ -398,7 +398,7 @@ export default function ForumModerationPage() {
if (loading) {
return (
<div className="min-h-screen bg-[#0a0a0a] flex items-center justify-center">
<div className="w-8 h-8 border-2 border-[#ffb800] border-t-transparent rounded-full animate-spin" />
<div className="w-8 h-8 border-2 border-[#F0A623] border-t-transparent rounded-full animate-spin" />
</div>
);
}
@@ -435,10 +435,10 @@ export default function ForumModerationPage() {
)}
{/* Page Header */}
<div className="bg-[#1a1208] border-b border-[#2a3a50]">
<div className="bg-[#231d18] border-b border-[#2a3a50]">
<div className="max-w-7xl mx-auto px-4 py-6">
<nav className="text-sm font-body text-[#666] mb-2">
<Link href="/admin" className="hover:text-[#ffb800] transition-colors">Admin</Link>
<Link href="/admin" className="hover:text-[#F0A623] transition-colors">Admin</Link>
<span className="mx-2"></span>
<span className="text-[#aab4c4]">Forum Moderation</span>
</nav>
@@ -453,7 +453,7 @@ export default function ForumModerationPage() {
<span className="text-xs text-purple-400 font-body bg-purple-400/10 border border-purple-400/20 px-3 py-1.5 rounded-lg">
🤖 Claude AI Auto-Moderation Active
</span>
<span className="text-xs text-[#555] font-body bg-[#1a1a1a] border border-[#252525] px-3 py-1.5 rounded-lg">
<span className="text-xs text-[#555] font-body bg-[#231d18] border border-[#3a322b] px-3 py-1.5 rounded-lg">
🛡 Moderator View
</span>
</div>
@@ -463,14 +463,14 @@ export default function ForumModerationPage() {
<div className="max-w-7xl mx-auto px-4 py-6">
{/* Tabs */}
<div className="flex gap-1 bg-[#111] border border-[#252525] rounded-lg p-1 w-fit mb-6">
<div className="flex gap-1 bg-[#111] border border-[#3a322b] rounded-lg p-1 w-fit mb-6">
{(['threads', 'replies'] as Tab[]).map(tab => (
<button
key={tab}
onClick={() => { setActiveTab(tab); setPage(1); }}
className={`px-5 py-2 text-sm font-body font-semibold rounded-md transition-colors capitalize ${
activeTab === tab
? 'bg-[#ffb800] text-white'
? 'bg-[#F0A623] text-white'
: 'text-[#888] hover:text-white'
}`}
>
@@ -488,8 +488,8 @@ export default function ForumModerationPage() {
onClick={() => { setThreadFilter(f.value); setPage(1); }}
className={`px-3 py-1.5 text-xs font-body font-semibold rounded-lg transition-colors ${
threadFilter === f.value
? 'bg-[#ffb800] text-white'
: 'bg-[#1a1a1a] text-[#888] hover:bg-[#252525] hover:text-white border border-[#252525]'
? 'bg-[#F0A623] text-white'
: 'bg-[#231d18] text-[#888] hover:bg-[#3a322b] hover:text-white border border-[#3a322b]'
}`}
>
{f.label}
@@ -501,8 +501,8 @@ export default function ForumModerationPage() {
onClick={() => { setReplyFilter(f.value); setPage(1); }}
className={`px-3 py-1.5 text-xs font-body font-semibold rounded-lg transition-colors ${
replyFilter === f.value
? 'bg-[#ffb800] text-white'
: 'bg-[#1a1a1a] text-[#888] hover:bg-[#252525] hover:text-white border border-[#252525]'
? 'bg-[#F0A623] text-white'
: 'bg-[#231d18] text-[#888] hover:bg-[#3a322b] hover:text-white border border-[#3a322b]'
}`}
>
{f.label}
@@ -515,7 +515,7 @@ export default function ForumModerationPage() {
{/* Content */}
{loadingData ? (
<div className="flex items-center justify-center py-20">
<div className="w-7 h-7 border-2 border-[#ffb800] border-t-transparent rounded-full animate-spin" />
<div className="w-7 h-7 border-2 border-[#F0A623] border-t-transparent rounded-full animate-spin" />
</div>
) : activeTab === 'threads' ? (
<>
@@ -559,7 +559,7 @@ export default function ForumModerationPage() {
<button
disabled={page === 1}
onClick={() => setPage(p => p - 1)}
className="px-4 py-2 text-sm font-body text-[#888] bg-[#1a1a1a] border border-[#252525] rounded-lg hover:bg-[#252525] disabled:opacity-40 transition-colors"
className="px-4 py-2 text-sm font-body text-[#888] bg-[#231d18] border border-[#3a322b] rounded-lg hover:bg-[#3a322b] disabled:opacity-40 transition-colors"
>
Prev
</button>
@@ -567,7 +567,7 @@ export default function ForumModerationPage() {
<button
disabled={page === totalPages}
onClick={() => setPage(p => p + 1)}
className="px-4 py-2 text-sm font-body text-[#888] bg-[#1a1a1a] border border-[#252525] rounded-lg hover:bg-[#252525] disabled:opacity-40 transition-colors"
className="px-4 py-2 text-sm font-body text-[#888] bg-[#231d18] border border-[#3a322b] rounded-lg hover:bg-[#3a322b] disabled:opacity-40 transition-colors"
>
Next
</button>

View File

@@ -136,13 +136,13 @@ export default function AdminForumSeedPage() {
return (
<>
<Header />
<main className="min-h-screen bg-[#111111]">
<div className="bg-[#1a1208] border-b border-[#2a3a50]">
<main className="min-h-screen bg-[#1c1815]">
<div className="bg-[#231d18] border-b border-[#2a3a50]">
<div className="max-w-container mx-auto px-4 py-6">
<nav className="text-sm font-body text-[#666] mb-2">
<Link href="/admin" className="hover:text-[#ffb800] transition-colors">Admin</Link>
<Link href="/admin" className="hover:text-[#F0A623] transition-colors">Admin</Link>
<span className="mx-2"></span>
<Link href="/forum" className="hover:text-[#ffb800] transition-colors">Forum</Link>
<Link href="/forum" className="hover:text-[#F0A623] transition-colors">Forum</Link>
<span className="mx-2"></span>
<span className="text-[#aab4c4]">Seed Tool</span>
</nav>
@@ -157,17 +157,17 @@ export default function AdminForumSeedPage() {
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
{/* Left: Editor */}
<div className="lg:col-span-2">
<div className="bg-[#1a1a1a] border border-[#252525] rounded-lg overflow-hidden">
<div className="flex border-b border-[#252525]">
<div className="bg-[#231d18] border border-[#3a322b] rounded-lg overflow-hidden">
<div className="flex border-b border-[#3a322b]">
<button
onClick={() => setActiveTab('import')}
className={`px-4 py-3 font-body text-sm font-semibold transition-colors ${activeTab === 'import' ? 'text-white border-b-2 border-[#ffb800]' : 'text-[#666] hover:text-[#aaa]'}`}
className={`px-4 py-3 font-body text-sm font-semibold transition-colors ${activeTab === 'import' ? 'text-white border-b-2 border-[#F0A623]' : 'text-[#666] hover:text-[#aaa]'}`}
>
Import Editor
</button>
<button
onClick={() => setActiveTab('json')}
className={`px-4 py-3 font-body text-sm font-semibold transition-colors ${activeTab === 'json' ? 'text-white border-b-2 border-[#ffb800]' : 'text-[#666] hover:text-[#aaa]'}`}
className={`px-4 py-3 font-body text-sm font-semibold transition-colors ${activeTab === 'json' ? 'text-white border-b-2 border-[#F0A623]' : 'text-[#666] hover:text-[#aaa]'}`}
>
JSON Schema
</button>
@@ -182,20 +182,20 @@ export default function AdminForumSeedPage() {
value={seedData}
onChange={e => setSeedData(e.target.value)}
rows={24}
className="w-full bg-[#0d0d0d] border border-[#333] rounded-lg px-3 py-3 text-[#d0d0d0] font-mono text-xs focus:outline-none focus:border-[#ffb800] resize-none"
className="w-full bg-[#1c1815] border border-[#333] rounded-lg px-3 py-3 text-[#d0d0d0] font-mono text-xs focus:outline-none focus:border-[#F0A623] resize-none"
spellCheck={false}
/>
<div className="flex gap-3 mt-3">
<button
onClick={handleImport}
disabled={importing}
className="bg-[#ffb800] hover:bg-[#d99700] disabled:opacity-50 text-white font-body font-semibold text-sm px-5 py-2 rounded-lg transition-colors"
className="bg-[#F0A623] hover:bg-[#BA7517] disabled:opacity-50 text-white font-body font-semibold text-sm px-5 py-2 rounded-lg transition-colors"
>
{importing ? 'Importing...' : 'Import Threads'}
</button>
<button
onClick={() => setSeedData(JSON.stringify(SEED_TEMPLATES, null, 2))}
className="bg-[#252525] hover:bg-[#333] text-[#aaa] font-body text-sm px-4 py-2 rounded-lg transition-colors"
className="bg-[#3a322b] hover:bg-[#333] text-[#aaa] font-body text-sm px-4 py-2 rounded-lg transition-colors"
>
Reset to Templates
</button>
@@ -206,7 +206,7 @@ export default function AdminForumSeedPage() {
{activeTab === 'json' && (
<div className="p-4">
<p className="text-[#888] font-body text-xs mb-3">Required JSON structure for seed data:</p>
<pre className="bg-[#0d0d0d] border border-[#333] rounded-lg p-4 text-[#d0d0d0] font-mono text-xs overflow-auto">
<pre className="bg-[#1c1815] border border-[#333] rounded-lg p-4 text-[#d0d0d0] font-mono text-xs overflow-auto">
{`[
{
"category_slug": "live-production",
@@ -226,7 +226,7 @@ export default function AdminForumSeedPage() {
<h4 className="text-white font-body font-semibold text-sm mb-2">Available Category Slugs</h4>
<div className="flex flex-wrap gap-2">
{['live-production','ip-cloud','audio','cameras','storage-mam','streaming','ai-automation','post-production','nab-ibc','career-jobs','gear-reviews','general'].map(slug => (
<code key={slug} className="bg-[#0d0d0d] border border-[#333] text-[#ffb800] font-mono text-xs px-2 py-1 rounded">{slug}</code>
<code key={slug} className="bg-[#1c1815] border border-[#333] text-[#F0A623] font-mono text-xs px-2 py-1 rounded">{slug}</code>
))}
</div>
</div>
@@ -239,7 +239,7 @@ export default function AdminForumSeedPage() {
<div className="space-y-4">
{/* Import Results */}
{results.length > 0 && (
<div className="bg-[#1a1a1a] border border-[#252525] rounded-lg p-4">
<div className="bg-[#231d18] border border-[#3a322b] rounded-lg p-4">
<h3 className="text-white font-heading font-semibold mb-3">Import Results</h3>
<div className="flex gap-4 mb-3">
<div className="text-center">
@@ -265,20 +265,20 @@ export default function AdminForumSeedPage() {
)}
{/* Quick Links */}
<div className="bg-[#1a1a1a] border border-[#252525] rounded-lg p-4">
<div className="bg-[#231d18] border border-[#3a322b] rounded-lg p-4">
<h3 className="text-white font-heading font-semibold mb-3">Quick Links</h3>
<div className="space-y-2">
<Link href="/forum" className="flex items-center gap-2 text-[#ffb800] hover:underline font-body text-sm">
<Link href="/forum" className="flex items-center gap-2 text-[#F0A623] hover:underline font-body text-sm">
<span></span> View Forum
</Link>
<Link href="/admin" className="flex items-center gap-2 text-[#ffb800] hover:underline font-body text-sm">
<Link href="/admin" className="flex items-center gap-2 text-[#F0A623] hover:underline font-body text-sm">
<span></span> Admin Dashboard
</Link>
</div>
</div>
{/* Tips */}
<div className="bg-[#1a1208] border border-[#2a3a50] rounded-lg p-4">
<div className="bg-[#231d18] border border-[#2a3a50] rounded-lg p-4">
<h3 className="text-white font-heading font-semibold mb-2 text-sm">Tips</h3>
<ul className="text-[#aab4c4] font-body text-xs space-y-1.5">
<li> Threads are imported as real posts they appear immediately in the forum</li>

View File

@@ -134,7 +134,7 @@ export default function WPImportPage() {
if (loading || (!user && !loading)) {
return (
<div className="min-h-screen bg-[#0a0a0a] flex items-center justify-center">
<div className="w-6 h-6 border-2 border-[#ffb800] border-t-transparent rounded-full animate-spin" />
<div className="w-6 h-6 border-2 border-[#F0A623] border-t-transparent rounded-full animate-spin" />
</div>
);
}
@@ -142,10 +142,10 @@ export default function WPImportPage() {
return (
<div className="min-h-screen bg-[#0a0a0a] text-[#cccccc]">
{/* Page Header */}
<div className="bg-[#111111] border-b border-[#252525]">
<div className="bg-[#1c1815] border-b border-[#3a322b]">
<div className="max-w-6xl mx-auto px-4 py-5">
<div className="flex items-center gap-3 mb-1">
<Link href="/home-page" className="text-[#555] hover:text-[#ffb800] transition-colors text-sm">
<Link href="/home-page" className="text-[#555] hover:text-[#F0A623] transition-colors text-sm">
Home
</Link>
<span className="text-[#333]">/</span>
@@ -158,14 +158,14 @@ export default function WPImportPage() {
<h1 className="text-2xl font-bold text-white font-heading">WordPress Post Importer</h1>
<p className="text-[#666] text-sm mt-1">
Import posts from{' '}
<a href="https://www.avbeat.com" target="_blank" rel="noopener noreferrer" className="text-[#ffb800] hover:underline">
<a href="https://www.avbeat.com" target="_blank" rel="noopener noreferrer" className="text-[#F0A623] hover:underline">
avbeat.com
</a>{' '}
via WordPress REST API
</p>
</div>
<div className="flex items-center gap-2 text-xs text-[#555] bg-[#1a1a1a] border border-[#252525] rounded px-3 py-2">
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" className="text-[#ffb800]">
<div className="flex items-center gap-2 text-xs text-[#555] bg-[#231d18] border border-[#3a322b] rounded px-3 py-2">
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" className="text-[#F0A623]">
<circle cx="12" cy="12" r="10" /><path d="M12 8v4l3 3" />
</svg>
{posts.length} posts imported
@@ -176,9 +176,9 @@ export default function WPImportPage() {
<div className="max-w-6xl mx-auto px-4 py-6 space-y-6">
{/* Import Controls */}
<div className="bg-[#111111] border border-[#252525] rounded-lg p-5">
<div className="bg-[#1c1815] border border-[#3a322b] rounded-lg p-5">
<h2 className="text-base font-bold text-white font-heading mb-4 flex items-center gap-2">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" className="text-[#ffb800]">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" className="text-[#F0A623]">
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" /><polyline points="7 10 12 15 17 10" /><line x1="12" y1="15" x2="12" y2="3" />
</svg>
Run Import
@@ -191,7 +191,7 @@ export default function WPImportPage() {
value={maxPages}
onChange={(e) => setMaxPages(Number(e.target.value))}
disabled={isImporting}
className="bg-[#1a1a1a] border border-[#333] text-[#cccccc] text-sm rounded px-3 py-2 focus:outline-none focus:border-[#ffb800] disabled:opacity-50">
className="bg-[#231d18] border border-[#333] text-[#cccccc] text-sm rounded px-3 py-2 focus:outline-none focus:border-[#F0A623] disabled:opacity-50">
<option value={1}>1 page (~20 posts)</option>
<option value={3}>3 pages (~60 posts)</option>
<option value={5}>5 pages (~100 posts)</option>
@@ -204,7 +204,7 @@ export default function WPImportPage() {
<button
onClick={handleImport}
disabled={isImporting}
className="flex items-center gap-2 bg-[#ffb800] hover:bg-[#d99700] disabled:bg-[#4a3500] disabled:cursor-not-allowed text-white text-sm font-bold px-5 py-2 rounded transition-colors">
className="flex items-center gap-2 bg-[#F0A623] hover:bg-[#BA7517] disabled:bg-[#412402] disabled:cursor-not-allowed text-white text-sm font-bold px-5 py-2 rounded transition-colors">
{isImporting ? (
<>
<div className="w-4 h-4 border-2 border-white border-t-transparent rounded-full animate-spin" />
@@ -223,16 +223,16 @@ export default function WPImportPage() {
{/* Import Progress / Result */}
{isImporting && (
<div className="mt-4 bg-[#0d1a2e] border border-[#4a3500] rounded p-4">
<div className="mt-4 bg-[#0d1a2e] border border-[#412402] rounded p-4">
<div className="flex items-center gap-3">
<div className="w-5 h-5 border-2 border-[#ffb800] border-t-transparent rounded-full animate-spin flex-shrink-0" />
<div className="w-5 h-5 border-2 border-[#F0A623] border-t-transparent rounded-full animate-spin flex-shrink-0" />
<div>
<p className="text-[#ffb800] text-sm font-bold">Import in progress</p>
<p className="text-[#F0A623] text-sm font-bold">Import in progress</p>
<p className="text-[#666] text-xs mt-0.5">Fetching posts from avbeat.com and saving to database...</p>
</div>
</div>
<div className="mt-3 h-1.5 bg-[#1a1208] rounded-full overflow-hidden">
<div className="h-full bg-[#ffb800] rounded-full animate-pulse w-2/3" />
<div className="mt-3 h-1.5 bg-[#231d18] rounded-full overflow-hidden">
<div className="h-full bg-[#F0A623] rounded-full animate-pulse w-2/3" />
</div>
</div>
)}
@@ -261,7 +261,7 @@ export default function WPImportPage() {
{[
{ label: 'Fetched', value: importResult.totalFetched, color: 'text-[#cccccc]' },
{ label: 'Imported', value: importResult.totalImported, color: 'text-green-400' },
{ label: 'Pages', value: importResult.pagesProcessed, color: 'text-[#ffb800]' },
{ label: 'Pages', value: importResult.pagesProcessed, color: 'text-[#F0A623]' },
{ label: 'Errors', value: importResult.totalErrors, color: importResult.totalErrors ? 'text-red-400' : 'text-[#555]' },
].map((stat) => (
<div key={stat.label} className="bg-[#111] rounded p-2.5 text-center">
@@ -277,14 +277,14 @@ export default function WPImportPage() {
</div>
{/* Tabs */}
<div className="flex gap-0 border-b border-[#252525]">
<div className="flex gap-0 border-b border-[#3a322b]">
{(['posts', 'logs'] as const).map((tab) => (
<button
key={tab}
onClick={() => setActiveTab(tab)}
className={`px-5 py-2.5 text-sm font-bold uppercase tracking-wider border-b-2 transition-colors ${
activeTab === tab
? 'border-[#ffb800] text-[#ffb800]'
? 'border-[#F0A623] text-[#F0A623]'
: 'border-transparent text-[#666] hover:text-[#999]'
}`}>
{tab === 'posts' ? `Imported Posts (${posts.length})` : `Import History (${logs.length})`}
@@ -306,17 +306,17 @@ export default function WPImportPage() {
placeholder="Search posts..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="w-full bg-[#111] border border-[#252525] text-[#cccccc] text-sm rounded pl-9 pr-4 py-2 focus:outline-none focus:border-[#ffb800] placeholder-[#444]"
className="w-full bg-[#111] border border-[#3a322b] text-[#cccccc] text-sm rounded pl-9 pr-4 py-2 focus:outline-none focus:border-[#F0A623] placeholder-[#444]"
/>
</div>
</div>
{loadingData ? (
<div className="flex items-center justify-center py-16">
<div className="w-6 h-6 border-2 border-[#ffb800] border-t-transparent rounded-full animate-spin" />
<div className="w-6 h-6 border-2 border-[#F0A623] border-t-transparent rounded-full animate-spin" />
</div>
) : filteredPosts.length === 0 ? (
<div className="text-center py-16 bg-[#111] border border-[#252525] rounded-lg">
<div className="text-center py-16 bg-[#111] border border-[#3a322b] rounded-lg">
<svg width="40" height="40" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" className="text-[#333] mx-auto mb-3">
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z" /><polyline points="14 2 14 8 20 8" /><line x1="16" y1="13" x2="8" y2="13" /><line x1="16" y1="17" x2="8" y2="17" /><polyline points="10 9 9 9 8 9" />
</svg>
@@ -327,10 +327,10 @@ export default function WPImportPage() {
) : (
<div className="space-y-2">
{filteredPosts.map((post) => (
<div key={post.id} className="bg-[#111] border border-[#252525] rounded-lg p-4 flex items-start gap-4 hover:border-[#333] transition-colors">
<div key={post.id} className="bg-[#111] border border-[#3a322b] rounded-lg p-4 flex items-start gap-4 hover:border-[#333] transition-colors">
{/* Thumbnail */}
{post.featured_image ? (
<div className="flex-shrink-0 w-16 h-12 rounded overflow-hidden bg-[#1a1a1a]">
<div className="flex-shrink-0 w-16 h-12 rounded overflow-hidden bg-[#231d18]">
<AppImage
src={post.featured_image}
alt={post.featured_image_alt || post.title}
@@ -340,7 +340,7 @@ export default function WPImportPage() {
/>
</div>
) : (
<div className="flex-shrink-0 w-16 h-12 rounded bg-[#1a1a1a] flex items-center justify-center">
<div className="flex-shrink-0 w-16 h-12 rounded bg-[#231d18] flex items-center justify-center">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" className="text-[#333]">
<rect x="3" y="3" width="18" height="18" rx="2" /><circle cx="8.5" cy="8.5" r="1.5" /><polyline points="21 15 16 10 5 21" />
</svg>
@@ -354,7 +354,7 @@ export default function WPImportPage() {
<h3 className="text-sm font-bold text-[#cccccc] leading-snug line-clamp-1">{post.title}</h3>
<div className="flex flex-wrap items-center gap-x-3 gap-y-1 mt-1">
{post.category && (
<span className="text-[10px] font-bold uppercase tracking-wider text-[#ffb800] bg-[#0d1a2e] px-1.5 py-0.5 rounded">
<span className="text-[10px] font-bold uppercase tracking-wider text-[#F0A623] bg-[#0d1a2e] px-1.5 py-0.5 rounded">
{post.category}
</span>
)}
@@ -368,7 +368,7 @@ export default function WPImportPage() {
href={`https://www.avbeat.com/${post.wp_slug}`}
target="_blank"
rel="noopener noreferrer"
className="text-[#555] hover:text-[#ffb800] transition-colors p-1"
className="text-[#555] hover:text-[#F0A623] transition-colors p-1"
title="View on avbeat.com">
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
<path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6" /><polyline points="15 3 21 3 21 9" /><line x1="10" y1="14" x2="21" y2="3" />
@@ -402,16 +402,16 @@ export default function WPImportPage() {
<div>
{loadingData ? (
<div className="flex items-center justify-center py-16">
<div className="w-6 h-6 border-2 border-[#ffb800] border-t-transparent rounded-full animate-spin" />
<div className="w-6 h-6 border-2 border-[#F0A623] border-t-transparent rounded-full animate-spin" />
</div>
) : logs.length === 0 ? (
<div className="text-center py-16 bg-[#111] border border-[#252525] rounded-lg">
<div className="text-center py-16 bg-[#111] border border-[#3a322b] rounded-lg">
<p className="text-[#555] text-sm">No import history yet.</p>
</div>
) : (
<div className="space-y-2">
{logs.map((log) => (
<div key={log.id} className="bg-[#111] border border-[#252525] rounded-lg p-4">
<div key={log.id} className="bg-[#111] border border-[#3a322b] rounded-lg p-4">
<div className="flex items-start justify-between gap-4">
<div className="flex items-center gap-2">
{log.status === 'completed' ? (
@@ -439,7 +439,7 @@ export default function WPImportPage() {
{ label: 'Skipped', value: log.total_skipped },
{ label: 'Errors', value: log.total_errors },
].map((stat) => (
<div key={stat.label} className="bg-[#0d0d0d] rounded p-2 text-center">
<div key={stat.label} className="bg-[#1c1815] rounded p-2 text-center">
<p className="text-base font-bold text-[#cccccc]">{stat.value}</p>
<p className="text-[10px] text-[#444] uppercase tracking-wider">{stat.label}</p>
</div>

View File

@@ -67,11 +67,11 @@ function fmtDate(dateStr: string): string {
// ─── Sub-components ───────────────────────────────────────────────────────────
function StatCard({ label, value, sub, accent = '#ffb800', icon }: {
function StatCard({ label, value, sub, accent = '#F0A623', icon }: {
label: string; value: string; sub: string; accent?: string; icon: React.ReactNode;
}) {
return (
<div className="bg-[#111] border border-[#252525] rounded-lg p-5 flex items-start gap-4">
<div className="bg-[#111] border border-[#3a322b] rounded-lg p-5 flex items-start gap-4">
<div
className="w-10 h-10 rounded-lg flex items-center justify-center flex-shrink-0"
style={{ background: `${accent}18` }}
@@ -90,7 +90,7 @@ function StatCard({ label, value, sub, accent = '#ffb800', icon }: {
function ChartTooltip({ active, payload, label }: any) {
if (!active || !payload?.length) return null;
return (
<div className="bg-[#1a1a1a] border border-[#333] rounded-lg px-3 py-2 text-xs font-body shadow-xl">
<div className="bg-[#231d18] border border-[#333] rounded-lg px-3 py-2 text-xs font-body shadow-xl">
{label && <p className="text-[#888] mb-1">{label}</p>}
{payload.map((entry: any, i: number) => (
<p key={i} style={{ color: entry.color }} className="leading-5">
@@ -243,7 +243,7 @@ export default function NewsletterAnalyticsPage() {
if (loading || loadingData) {
return (
<div className="min-h-screen bg-[#0a0a0a] flex items-center justify-center">
<div className="w-8 h-8 border-2 border-[#ffb800] border-t-transparent rounded-full animate-spin" />
<div className="w-8 h-8 border-2 border-[#F0A623] border-t-transparent rounded-full animate-spin" />
</div>
);
}
@@ -258,7 +258,7 @@ export default function NewsletterAnalyticsPage() {
return (
<div className="min-h-screen bg-[#0a0a0a] text-white">
{/* Header */}
<div className="border-b border-[#1a1a1a] bg-[#0d0d0d] sticky top-0 z-10">
<div className="border-b border-[#231d18] bg-[#1c1815] sticky top-0 z-10">
<div className="max-w-7xl mx-auto px-6 py-4 flex items-center justify-between">
<div className="flex items-center gap-3">
<Link href="/admin" className="text-[#555] hover:text-white transition-colors">
@@ -266,7 +266,7 @@ export default function NewsletterAnalyticsPage() {
<path strokeLinecap="round" strokeLinejoin="round" d="M10.5 19.5L3 12m0 0l7.5-7.5M3 12h18" />
</svg>
</Link>
<div className="w-px h-5 bg-[#252525]" />
<div className="w-px h-5 bg-[#3a322b]" />
<Link href="/admin/newsletter" className="text-[#555] hover:text-white text-sm font-body transition-colors">
Newsletter
</Link>
@@ -278,14 +278,14 @@ export default function NewsletterAnalyticsPage() {
<div className="flex items-center gap-3">
<Link
href="/admin/newsletter/templates"
className="flex items-center gap-1.5 px-3 py-1.5 text-xs font-body font-semibold text-[#888] hover:text-white border border-[#252525] rounded-lg transition-colors"
className="flex items-center gap-1.5 px-3 py-1.5 text-xs font-body font-semibold text-[#888] hover:text-white border border-[#3a322b] rounded-lg transition-colors"
>
<svg className="w-3.5 h-3.5" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" d="M19.5 14.25v-2.625a3.375 3.375 0 00-3.375-3.375h-1.5A1.125 1.125 0 0113.5 7.125v-1.5a3.375 3.375 0 00-3.375-3.375H8.25m0 12.75h7.5m-7.5 3H12M10.5 2.25H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 00-9-9z" />
</svg>
Templates
</Link>
<div className="w-px h-8 bg-[#252525]" />
<div className="w-px h-8 bg-[#3a322b]" />
<div className="text-right">
<p className="text-white text-sm font-bold font-heading">{fmt(totalSubscribers)}</p>
<p className="text-[#555] text-xs font-body">active subscribers</p>
@@ -308,7 +308,7 @@ export default function NewsletterAnalyticsPage() {
label="Avg Open Rate"
value={pct(avgOpenRate)}
sub={`across ${digestData.length} digests`}
accent="#ffb800"
accent="#F0A623"
icon={
<svg className="w-5 h-5" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" d="M2.036 12.322a1.012 1.012 0 010-.639C3.423 7.51 7.36 4.5 12 4.5c4.638 0 8.573 3.007 9.963 7.178.07.207.07.431 0 .639C20.577 16.49 16.64 19.5 12 19.5c-4.638 0-8.573-3.007-9.963-7.178z" />
@@ -352,7 +352,7 @@ export default function NewsletterAnalyticsPage() {
</div>
{/* Open & Click Rate Trend */}
<div className="bg-[#111] border border-[#252525] rounded-lg p-6">
<div className="bg-[#111] border border-[#3a322b] rounded-lg p-6">
<SectionHeader title="Open & Click Rate Trend" subtitle="Rate per digest over time" />
<div className="h-64">
<ResponsiveContainer width="100%" height="100%">
@@ -362,7 +362,7 @@ export default function NewsletterAnalyticsPage() {
<YAxis tick={{ fill: '#555', fontSize: 10 }} tickLine={false} axisLine={false} tickFormatter={(v) => `${v}%`} />
<Tooltip content={<ChartTooltip />} />
<Legend wrapperStyle={{ fontSize: 11, color: '#888' }} />
<Line type="monotone" dataKey="Open Rate" stroke="#ffb800" strokeWidth={2} dot={{ r: 3, fill: '#ffb800' }} activeDot={{ r: 5 }} />
<Line type="monotone" dataKey="Open Rate" stroke="#F0A623" strokeWidth={2} dot={{ r: 3, fill: '#F0A623' }} activeDot={{ r: 5 }} />
<Line type="monotone" dataKey="Click Rate" stroke="#10b981" strokeWidth={2} dot={{ r: 3, fill: '#10b981' }} activeDot={{ r: 5 }} />
</LineChart>
</ResponsiveContainer>
@@ -370,7 +370,7 @@ export default function NewsletterAnalyticsPage() {
</div>
{/* Subscriber Growth Chart */}
<div className="bg-[#111] border border-[#252525] rounded-lg p-6">
<div className="bg-[#111] border border-[#3a322b] rounded-lg p-6">
<SectionHeader title="Subscriber Growth" subtitle="Monthly net new subscribers vs unsubscribes" />
<div className="h-64">
<ResponsiveContainer width="100%" height="100%">
@@ -380,7 +380,7 @@ export default function NewsletterAnalyticsPage() {
<YAxis tick={{ fill: '#555', fontSize: 11 }} tickLine={false} axisLine={false} />
<Tooltip content={<ChartTooltip />} />
<Legend wrapperStyle={{ fontSize: 11, color: '#888' }} />
<Bar dataKey="subscribers" name="Total Subscribers" fill="#ffb800" radius={[3, 3, 0, 0]} />
<Bar dataKey="subscribers" name="Total Subscribers" fill="#F0A623" radius={[3, 3, 0, 0]} />
<Bar dataKey="unsubscribes" name="Unsubscribes" fill="#ef4444" radius={[3, 3, 0, 0]} />
</BarChart>
</ResponsiveContainer>
@@ -388,7 +388,7 @@ export default function NewsletterAnalyticsPage() {
</div>
{/* Per-Digest Performance Table */}
<div className="bg-[#111] border border-[#252525] rounded-lg overflow-hidden">
<div className="bg-[#111] border border-[#3a322b] rounded-lg overflow-hidden">
<div className="px-6 py-5 border-b border-[#1e1e1e]">
<SectionHeader title="Per-Digest Performance" subtitle="Click column headers to sort" />
</div>
@@ -421,7 +421,7 @@ export default function NewsletterAnalyticsPage() {
{sortedDigests.map((digest, i) => (
<tr
key={digest.id}
className={`border-b border-[#1a1a1a] hover:bg-[#161616] transition-colors ${i % 2 === 0 ? '' : 'bg-[#0d0d0d]'}`}
className={`border-b border-[#231d18] hover:bg-[#231d18] transition-colors ${i % 2 === 0 ? '' : 'bg-[#1c1815]'}`}
>
<td className="px-4 py-3 text-white max-w-xs">
<p className="truncate font-medium" title={digest.subject}>{digest.subject}</p>

View File

@@ -252,7 +252,7 @@ export default function CampaignEditorPage() {
article_blocks: payload.article_blocks,
layout: 'featured',
style: 'dark',
accent_color: '#ffb800',
accent_color: '#F0A623',
is_preset: false,
}),
});
@@ -296,7 +296,7 @@ export default function CampaignEditorPage() {
)}
{/* ── Top Bar ── */}
<div className="sticky top-0 z-40 bg-[#0a0a0a]/95 backdrop-blur border-b border-[#1a1a1a]">
<div className="sticky top-0 z-40 bg-[#0a0a0a]/95 backdrop-blur border-b border-[#231d18]">
<div className="max-w-screen-xl mx-auto px-4 h-14 flex items-center justify-between gap-4">
<div className="flex items-center gap-3 min-w-0">
<Link href="/admin/newsletter" className="text-[#555] hover:text-white transition-colors flex-shrink-0">
@@ -313,7 +313,7 @@ export default function CampaignEditorPage() {
className="bg-transparent text-sm font-medium text-white placeholder-[#444] outline-none min-w-0 w-48"
/>
<span className={`text-xs px-2 py-0.5 rounded-full font-medium flex-shrink-0 ${
campaign.status === 'draft' ? 'bg-[#1a1a1a] text-[#666]' :
campaign.status === 'draft' ? 'bg-[#231d18] text-[#666]' :
campaign.status === 'scheduled'? 'bg-yellow-400/10 text-yellow-400' : 'bg-green-400/10 text-green-400'
}`}>
{campaign.status}
@@ -339,7 +339,7 @@ export default function CampaignEditorPage() {
<button
onClick={() => handleSave('draft')}
disabled={saving}
className="px-3 py-1.5 text-xs font-medium bg-[#1a1a1a] hover:bg-[#222] border border-[#2a2a2a] text-[#aaa] rounded-lg transition-colors disabled:opacity-50"
className="px-3 py-1.5 text-xs font-medium bg-[#231d18] hover:bg-[#222] border border-[#3a322b] text-[#aaa] rounded-lg transition-colors disabled:opacity-50"
>
{saving ? 'Saving…' : 'Save Draft'}
</button>
@@ -393,9 +393,9 @@ export default function CampaignEditorPage() {
<div className="space-y-2">
{loadingArticles ? (
Array.from({ length: 5 }).map((_, i) => (
<div key={i} className="bg-[#111] border border-[#1a1a1a] rounded-xl p-4 animate-pulse">
<div key={i} className="bg-[#111] border border-[#231d18] rounded-xl p-4 animate-pulse">
<div className="h-3 bg-[#1e1e1e] rounded w-3/4 mb-2" />
<div className="h-2.5 bg-[#1a1a1a] rounded w-1/2" />
<div className="h-2.5 bg-[#231d18] rounded w-1/2" />
</div>
))
) : articles.length === 0 ? (
@@ -409,7 +409,7 @@ export default function CampaignEditorPage() {
onClick={() => toggleArticle(article)}
className={`group relative bg-[#111] border rounded-xl p-4 cursor-pointer transition-all ${
selected
? 'border-white/20 bg-white/5' :'border-[#1a1a1a] hover:border-[#2a2a2a]'
? 'border-white/20 bg-white/5' :'border-[#231d18] hover:border-[#3a322b]'
}`}
>
<div className="flex items-start gap-3">
@@ -427,7 +427,7 @@ export default function CampaignEditorPage() {
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2 mb-1">
{article.category && (
<span className="text-[10px] font-semibold uppercase tracking-wider text-[#555] bg-[#1a1a1a] px-1.5 py-0.5 rounded">
<span className="text-[10px] font-semibold uppercase tracking-wider text-[#555] bg-[#231d18] px-1.5 py-0.5 rounded">
{article.category}
</span>
)}
@@ -454,7 +454,7 @@ export default function CampaignEditorPage() {
{(activePanel === 'settings' || activePanel === 'articles') && (
<>
{/* Subject Line */}
<div className="bg-[#111] border border-[#1a1a1a] rounded-xl p-4">
<div className="bg-[#111] border border-[#231d18] rounded-xl p-4">
<div className="flex items-center justify-between mb-3">
<h3 className="text-xs font-semibold text-white uppercase tracking-wider">Subject Line</h3>
<span className={`text-[10px] font-mono ${subjectCharCount > 60 ? 'text-yellow-400' : 'text-[#444]'}`}>
@@ -477,7 +477,7 @@ export default function CampaignEditorPage() {
</div>
{/* Preview Text */}
<div className="bg-[#111] border border-[#1a1a1a] rounded-xl p-4">
<div className="bg-[#111] border border-[#231d18] rounded-xl p-4">
<div className="flex items-center justify-between mb-3">
<h3 className="text-xs font-semibold text-white uppercase tracking-wider">Preview Text</h3>
<span className={`text-[10px] font-mono ${previewCharCount > 90 ? 'text-yellow-400' : 'text-[#444]'}`}>
@@ -497,7 +497,7 @@ export default function CampaignEditorPage() {
</div>
{/* Recipient Segment */}
<div className="bg-[#111] border border-[#1a1a1a] rounded-xl p-4">
<div className="bg-[#111] border border-[#231d18] rounded-xl p-4">
<h3 className="text-xs font-semibold text-white uppercase tracking-wider mb-3">Recipient Segment</h3>
<div className="space-y-2">
{SEGMENTS.map((seg) => {
@@ -509,7 +509,7 @@ export default function CampaignEditorPage() {
onClick={() => setCampaign((p) => ({ ...p, segment: seg.id }))}
className={`w-full text-left px-3 py-2.5 rounded-lg border transition-all ${
active
? 'bg-white/5 border-white/20 text-white' :'bg-[#0a0a0a] border-[#1a1a1a] text-[#888] hover:border-[#2a2a2a] hover:text-white'
? 'bg-white/5 border-white/20 text-white' :'bg-[#0a0a0a] border-[#231d18] text-[#888] hover:border-[#3a322b] hover:text-white'
}`}
>
<div className="flex items-center justify-between">
@@ -529,7 +529,7 @@ export default function CampaignEditorPage() {
})}
</div>
{recipientCount > 0 && (
<div className="mt-3 pt-3 border-t border-[#1a1a1a] flex items-center gap-2">
<div className="mt-3 pt-3 border-t border-[#231d18] flex items-center gap-2">
<svg className="w-3.5 h-3.5 text-[#555]" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0z" />
</svg>
@@ -543,7 +543,7 @@ export default function CampaignEditorPage() {
)}
{/* ── Selected Articles ── */}
<div className="bg-[#111] border border-[#1a1a1a] rounded-xl p-4">
<div className="bg-[#111] border border-[#231d18] rounded-xl p-4">
<div className="flex items-center justify-between mb-3">
<h3 className="text-xs font-semibold text-white uppercase tracking-wider">Selected Articles</h3>
<span className="text-[10px] text-[#555]">{campaign.articles.length} article{campaign.articles.length !== 1 ? 's' : ''}</span>
@@ -559,7 +559,7 @@ export default function CampaignEditorPage() {
) : (
<div className="space-y-2">
{campaign.articles.map((article, idx) => (
<div key={article.id} className="bg-[#0a0a0a] border border-[#1a1a1a] rounded-lg p-3">
<div key={article.id} className="bg-[#0a0a0a] border border-[#231d18] rounded-lg p-3">
<div className="flex items-start gap-2">
{/* Order controls */}
<div className="flex flex-col gap-0.5 flex-shrink-0 mt-0.5">
@@ -618,16 +618,16 @@ export default function CampaignEditorPage() {
{/* ── Preview Panel ── */}
{activePanel === 'preview' && (
<div className="bg-[#111] border border-[#1a1a1a] rounded-xl overflow-hidden">
<div className="px-4 py-3 border-b border-[#1a1a1a] flex items-center justify-between">
<div className="bg-[#111] border border-[#231d18] rounded-xl overflow-hidden">
<div className="px-4 py-3 border-b border-[#231d18] flex items-center justify-between">
<h3 className="text-xs font-semibold text-white uppercase tracking-wider">Email Preview</h3>
<span className="text-[10px] text-[#555]">Inbox view</span>
</div>
{/* Inbox row preview */}
<div className="px-4 py-3 border-b border-[#1a1a1a] bg-[#0d0d0d]">
<div className="px-4 py-3 border-b border-[#231d18] bg-[#1c1815]">
<div className="flex items-center gap-2 mb-1">
<div className="w-6 h-6 rounded-full bg-[#1a1a1a] flex items-center justify-center flex-shrink-0">
<div className="w-6 h-6 rounded-full bg-[#231d18] flex items-center justify-center flex-shrink-0">
<span className="text-[8px] font-bold text-[#666]">BB</span>
</div>
<div className="flex-1 min-w-0">
@@ -644,7 +644,7 @@ export default function CampaignEditorPage() {
{/* Email body preview */}
<div className="p-4 space-y-3 max-h-[400px] overflow-y-auto">
{/* Header */}
<div className="bg-[#0a0a0a] rounded-lg p-3 text-center border border-[#1a1a1a]">
<div className="bg-[#0a0a0a] rounded-lg p-3 text-center border border-[#231d18]">
<p className="text-xs font-bold text-white tracking-widest uppercase">AV Beat</p>
{campaign.previewText && (
<p className="text-[10px] text-[#555] mt-1">{campaign.previewText}</p>
@@ -657,7 +657,7 @@ export default function CampaignEditorPage() {
) : (
campaign.articles.map((article, idx) => (
<div key={article.id} className={`rounded-lg border p-3 ${
article.featured ? 'border-white/10 bg-white/3' : 'border-[#1a1a1a] bg-[#0a0a0a]'
article.featured ? 'border-white/10 bg-white/3' : 'border-[#231d18] bg-[#0a0a0a]'
}`}>
{article.featured && (
<span className="text-[9px] font-semibold uppercase tracking-wider text-yellow-400 mb-1 block">Featured Story</span>
@@ -679,7 +679,7 @@ export default function CampaignEditorPage() {
)}
{/* Footer */}
<div className="text-center pt-2 border-t border-[#1a1a1a]">
<div className="text-center pt-2 border-t border-[#231d18]">
<p className="text-[9px] text-[#333]">AV Beat · Unsubscribe · View in browser</p>
</div>
</div>
@@ -687,7 +687,7 @@ export default function CampaignEditorPage() {
)}
{/* ── Publish Summary ── */}
<div className="bg-[#111] border border-[#1a1a1a] rounded-xl p-4">
<div className="bg-[#111] border border-[#231d18] rounded-xl p-4">
<h3 className="text-xs font-semibold text-white uppercase tracking-wider mb-3">Campaign Summary</h3>
<div className="space-y-2">
{[
@@ -710,7 +710,7 @@ export default function CampaignEditorPage() {
<button
onClick={() => handleSave('draft')}
disabled={saving}
className="flex-1 py-2 text-xs font-medium bg-[#1a1a1a] hover:bg-[#222] border border-[#2a2a2a] text-[#aaa] rounded-lg transition-colors disabled:opacity-50"
className="flex-1 py-2 text-xs font-medium bg-[#231d18] hover:bg-[#222] border border-[#3a322b] text-[#aaa] rounded-lg transition-colors disabled:opacity-50"
>
{saving ? 'Saving…' : 'Save Draft'}
</button>

View File

@@ -356,7 +356,7 @@ export default function AdminNewsletterPage() {
if (loading) {
return (
<div className="min-h-screen bg-[#0a0a0a] flex items-center justify-center">
<div className="w-8 h-8 border-2 border-[#ffb800] border-t-transparent rounded-full animate-spin" />
<div className="w-8 h-8 border-2 border-[#F0A623] border-t-transparent rounded-full animate-spin" />
</div>
);
}
@@ -373,7 +373,7 @@ export default function AdminNewsletterPage() {
)}
{/* Header */}
<div className="bg-[#111] border-b border-[#252525] px-6 py-4">
<div className="bg-[#111] border-b border-[#3a322b] px-6 py-4">
<div className="max-w-6xl mx-auto flex items-center justify-between">
<div className="flex items-center gap-3">
<Link href="/admin" className="text-[#555] hover:text-[#888] transition-colors">
@@ -389,7 +389,7 @@ export default function AdminNewsletterPage() {
<div className="flex items-center gap-3">
<Link
href="/admin/newsletter/templates"
className="flex items-center gap-1.5 px-3 py-1.5 text-xs font-body font-semibold text-[#888] hover:text-white border border-[#252525] rounded-lg transition-colors">
className="flex items-center gap-1.5 px-3 py-1.5 text-xs font-body font-semibold text-[#888] hover:text-white border border-[#3a322b] rounded-lg transition-colors">
<svg className="w-3.5 h-3.5" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" d="M19.5 14.25v-2.625a3.375 3.375 0 00-3.375-3.375h-1.5A1.125 1.125 0 0113.5 7.125v-1.5a3.375 3.375 0 00-3.375-3.375H8.25m0 12.75h7.5m-7.5 3H12M10.5 2.25H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 00-9-9z" />
</svg>
@@ -397,7 +397,7 @@ export default function AdminNewsletterPage() {
</Link>
<Link
href="/admin/newsletter/campaign-editor"
className="flex items-center gap-1.5 px-3 py-1.5 text-xs font-body font-semibold text-white bg-[#ffb800] hover:bg-[#d99700] rounded-lg transition-colors">
className="flex items-center gap-1.5 px-3 py-1.5 text-xs font-body font-semibold text-white bg-[#F0A623] hover:bg-[#BA7517] rounded-lg transition-colors">
<svg className="w-3.5 h-3.5" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" d="M12 4v16m8-8H4" />
</svg>
@@ -405,18 +405,18 @@ export default function AdminNewsletterPage() {
</Link>
<Link
href="/admin/newsletter/analytics"
className="flex items-center gap-1.5 px-3 py-1.5 text-xs font-body font-semibold text-[#888] hover:text-white border border-[#252525] rounded-lg transition-colors">
className="flex items-center gap-1.5 px-3 py-1.5 text-xs font-body font-semibold text-[#888] hover:text-white border border-[#3a322b] rounded-lg transition-colors">
<svg className="w-3.5 h-3.5" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" d="M3 13.125C3 12.504 3.504 12 4.125 12h2.25c.621 0 1.125.504 1.125 1.125v6.75C7.5 20.496 6.996 21 6.375 21h-2.25A1.125 1.125 0 013 19.875v-6.75zM9.75 8.625c0-.621.504-1.125 1.125-1.125h2.25c.621 0 1.125.504 1.125 1.125v11.25c0 .621-.504 1.125-1.125 1.125h-2.25a1.125 1.125 0 01-1.125-1.125V8.625zM16.5 4.125c0-.621.504-1.125 1.125-1.125h2.25c.621 0 1.125.504 1.125 1.125v15.75c0 .621-.504 1.125-1.125 1.125h-2.25a1.125 1.125 0 01-1.125-1.125V4.125z" />
</svg>
Analytics
</Link>
<div className="w-px h-8 bg-[#252525]" />
<div className="w-px h-8 bg-[#3a322b]" />
<div className="text-right">
<p className="text-white font-display font-bold text-xl">{activeCount}</p>
<p className="text-[#555] text-xs font-body">active subscribers</p>
</div>
<div className="w-px h-8 bg-[#252525]" />
<div className="w-px h-8 bg-[#3a322b]" />
<div className="text-right">
<p className="text-white font-display font-bold text-xl">{totalCount}</p>
<p className="text-[#555] text-xs font-body">total</p>
@@ -427,15 +427,15 @@ export default function AdminNewsletterPage() {
<div className="max-w-6xl mx-auto px-6 py-6">
{/* Tabs */}
<div className="flex gap-1 mb-6 bg-[#111] border border-[#252525] rounded-lg p-1 w-fit">
<div className="flex gap-1 mb-6 bg-[#111] border border-[#3a322b] rounded-lg p-1 w-fit">
<button
onClick={() => setActiveTab('subscribers')}
className={`px-4 py-2 rounded-md text-sm font-body font-semibold transition-colors ${activeTab === 'subscribers' ? 'bg-[#ffb800] text-white' : 'text-[#888] hover:text-white'}`}>
className={`px-4 py-2 rounded-md text-sm font-body font-semibold transition-colors ${activeTab === 'subscribers' ? 'bg-[#F0A623] text-white' : 'text-[#888] hover:text-white'}`}>
Subscribers
</button>
<button
onClick={() => setActiveTab('send-digest')}
className={`px-4 py-2 rounded-md text-sm font-body font-semibold transition-colors ${activeTab === 'send-digest' ? 'bg-[#ffb800] text-white' : 'text-[#888] hover:text-white'}`}>
className={`px-4 py-2 rounded-md text-sm font-body font-semibold transition-colors ${activeTab === 'send-digest' ? 'bg-[#F0A623] text-white' : 'text-[#888] hover:text-white'}`}>
Send Digest
</button>
</div>
@@ -452,12 +452,12 @@ export default function AdminNewsletterPage() {
onChange={(e) => setSearchQuery(e.target.value)}
className="search-input flex-1 py-2 px-3 text-sm"
/>
<div className="flex gap-1 bg-[#111] border border-[#252525] rounded-lg p-1">
<div className="flex gap-1 bg-[#111] border border-[#3a322b] rounded-lg p-1">
{(['all', 'active', 'unsubscribed'] as const).map((s) => (
<button
key={s}
onClick={() => setStatusFilter(s)}
className={`px-3 py-1.5 rounded-md text-xs font-body font-semibold capitalize transition-colors ${statusFilter === s ? 'bg-[#ffb800] text-white' : 'text-[#888] hover:text-white'}`}
className={`px-3 py-1.5 rounded-md text-xs font-body font-semibold capitalize transition-colors ${statusFilter === s ? 'bg-[#F0A623] text-white' : 'text-[#888] hover:text-white'}`}
>
{s}
</button>
@@ -467,7 +467,7 @@ export default function AdminNewsletterPage() {
<button
onClick={handleExportCSV}
disabled={subscribers.length === 0}
className="flex items-center gap-1.5 px-3 py-2 text-xs font-body font-semibold text-[#888] hover:text-white border border-[#252525] rounded-lg transition-colors disabled:opacity-40 disabled:cursor-not-allowed whitespace-nowrap"
className="flex items-center gap-1.5 px-3 py-2 text-xs font-body font-semibold text-[#888] hover:text-white border border-[#3a322b] rounded-lg transition-colors disabled:opacity-40 disabled:cursor-not-allowed whitespace-nowrap"
>
<svg className="w-3.5 h-3.5" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" d="M3 16.5v2.25A2.25 2.25 0 015.25 21h13.5A2.25 2.25 0 0121 18.75V16.5M16.5 12L12 16.5m0 0L7.5 12m4.5 4.5V3" />
@@ -478,9 +478,9 @@ export default function AdminNewsletterPage() {
{/* Bulk Actions Bar */}
{someSelected && (
<div className="flex items-center justify-between bg-[#1a1208] border border-[#4a3500] rounded-lg px-4 py-3 mb-4">
<div className="flex items-center justify-between bg-[#231d18] border border-[#412402] rounded-lg px-4 py-3 mb-4">
<div className="flex items-center gap-3">
<span className="text-sm font-body font-semibold text-[#ffb800]">
<span className="text-sm font-body font-semibold text-[#F0A623]">
{selectedIds.size} selected
</span>
<button
@@ -512,17 +512,17 @@ export default function AdminNewsletterPage() {
Delete
</button>
{bulkLoading && (
<div className="w-4 h-4 border-2 border-[#ffb800] border-t-transparent rounded-full animate-spin" />
<div className="w-4 h-4 border-2 border-[#F0A623] border-t-transparent rounded-full animate-spin" />
)}
</div>
</div>
)}
{/* Table */}
<div className="bg-[#111] border border-[#252525] rounded-lg overflow-hidden">
<div className="bg-[#111] border border-[#3a322b] rounded-lg overflow-hidden">
{loadingData ? (
<div className="flex items-center justify-center py-16">
<div className="w-6 h-6 border-2 border-[#ffb800] border-t-transparent rounded-full animate-spin" />
<div className="w-6 h-6 border-2 border-[#F0A623] border-t-transparent rounded-full animate-spin" />
</div>
) : subscribers.length === 0 ? (
<div className="text-center py-16">
@@ -534,13 +534,13 @@ export default function AdminNewsletterPage() {
) : (
<table className="w-full">
<thead>
<tr className="border-b border-[#252525]">
<tr className="border-b border-[#3a322b]">
<th className="px-4 py-3 w-10">
<input
type="checkbox"
checked={allSelected}
onChange={toggleSelectAll}
className="w-4 h-4 rounded border-[#444] bg-[#1a1a1a] accent-[#ffb800] cursor-pointer"
className="w-4 h-4 rounded border-[#444] bg-[#231d18] accent-[#F0A623] cursor-pointer"
/>
</th>
<th className="text-left px-4 py-3 text-xs font-body font-bold text-[#555] uppercase tracking-wider">Email</th>
@@ -554,21 +554,21 @@ export default function AdminNewsletterPage() {
{subscribers.map((sub) => (
<tr
key={sub.id}
className={`border-b border-[#1a1a1a] transition-colors ${selectedIds.has(sub.id) ? 'bg-[#0d1a2d]' : 'hover:bg-[#0d0d0d]'}`}
className={`border-b border-[#231d18] transition-colors ${selectedIds.has(sub.id) ? 'bg-[#0d1a2d]' : 'hover:bg-[#1c1815]'}`}
>
<td className="px-4 py-3 w-10">
<input
type="checkbox"
checked={selectedIds.has(sub.id)}
onChange={() => toggleSelect(sub.id)}
className="w-4 h-4 rounded border-[#444] bg-[#1a1a1a] accent-[#ffb800] cursor-pointer"
className="w-4 h-4 rounded border-[#444] bg-[#231d18] accent-[#F0A623] cursor-pointer"
/>
</td>
<td className="px-4 py-3 text-sm font-body text-[#e0e0e0]">{sub.email}</td>
<td className="px-4 py-3 hidden md:table-cell">
<div className="flex flex-wrap gap-1">
{sub.topics?.slice(0, 3).map((t) => (
<span key={t} className="text-xs bg-[#1a1208] text-[#ffb800] px-2 py-0.5 rounded-full font-body">{t}</span>
<span key={t} className="text-xs bg-[#231d18] text-[#F0A623] px-2 py-0.5 rounded-full font-body">{t}</span>
))}
{sub.topics?.length > 3 && (
<span className="text-xs text-[#555] font-body">+{sub.topics.length - 3}</span>
@@ -576,7 +576,7 @@ export default function AdminNewsletterPage() {
</div>
</td>
<td className="px-4 py-3">
<span className={`text-xs font-body font-bold px-2 py-1 rounded-full ${sub.status === 'active' ? 'text-green-400 bg-green-400/10' : 'text-[#555] bg-[#1a1a1a]'}`}>
<span className={`text-xs font-body font-bold px-2 py-1 rounded-full ${sub.status === 'active' ? 'text-green-400 bg-green-400/10' : 'text-[#555] bg-[#231d18]'}`}>
{sub.status}
</span>
</td>
@@ -616,21 +616,21 @@ export default function AdminNewsletterPage() {
{activeTab === 'send-digest' && (
<div className="max-w-2xl">
{/* SMTP warning if not configured */}
<div className="bg-[#1a1208] border border-[#4a3500] rounded-lg p-4 mb-6 flex gap-3">
<svg className="w-5 h-5 text-[#ffb800] flex-shrink-0 mt-0.5" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24">
<div className="bg-[#231d18] border border-[#412402] rounded-lg p-4 mb-6 flex gap-3">
<svg className="w-5 h-5 text-[#F0A623] flex-shrink-0 mt-0.5" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<div>
<p className="text-[#e0e0e0] text-sm font-body font-semibold">SMTP Required</p>
<p className="text-[#777] text-xs font-body mt-0.5">
Set <code className="text-[#ffb800]">SMTP_HOST</code>, <code className="text-[#ffb800]">SMTP_USER</code>, <code className="text-[#ffb800]">SMTP_PASS</code>, and <code className="text-[#ffb800]">SMTP_FROM_EMAIL</code> in your environment variables to enable sending.
Set <code className="text-[#F0A623]">SMTP_HOST</code>, <code className="text-[#F0A623]">SMTP_USER</code>, <code className="text-[#F0A623]">SMTP_PASS</code>, and <code className="text-[#F0A623]">SMTP_FROM_EMAIL</code> in your environment variables to enable sending.
</p>
</div>
</div>
{/* Applied template badge */}
{appliedTemplate && (
<div className="flex items-center justify-between bg-[#1a1208] border border-[#4a3500] rounded-lg px-4 py-3 mb-4">
<div className="flex items-center justify-between bg-[#231d18] border border-[#412402] rounded-lg px-4 py-3 mb-4">
<div className="flex items-center gap-2">
<div className="w-8 h-8 rounded-lg flex items-center justify-center flex-shrink-0 mt-0.5" style={{ backgroundColor: appliedTemplate.accent_color + '20' }}>
<span style={{ color: appliedTemplate.accent_color }}>
@@ -662,7 +662,7 @@ export default function AdminNewsletterPage() {
<p className="text-xs font-body text-[#555]">Fill in the fields below or start from a template</p>
<button
onClick={() => { fetchTemplates(); setShowTemplateDrawer(true); }}
className="flex items-center gap-1.5 px-3 py-1.5 text-xs font-body font-semibold text-[#ffb800] border border-[#4a3500] rounded-lg hover:bg-[#1a1208] transition-colors"
className="flex items-center gap-1.5 px-3 py-1.5 text-xs font-body font-semibold text-[#F0A623] border border-[#412402] rounded-lg hover:bg-[#231d18] transition-colors"
>
<svg className="w-3.5 h-3.5" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" d="M19.5 14.25v-2.625a3.375 3.375 0 00-3.375-3.375h-1.5A1.125 1.125 0 0113.5 7.125v-1.5a3.375 3.375 0 00-3.375-3.375H8.25m0 12.75h7.5m-7.5 3H12M10.5 2.25H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 00-9-9z" />
@@ -725,14 +725,14 @@ export default function AdminNewsletterPage() {
<button
type="button"
onClick={addArticle}
className="text-xs text-[#ffb800] hover:text-blue-400 font-body font-semibold transition-colors"
className="text-xs text-[#F0A623] hover:text-blue-400 font-body font-semibold transition-colors"
>
+ Add Article
</button>
</div>
<div className="space-y-3">
{digestArticles.map((article, index) => (
<div key={index} className="bg-[#111] border border-[#252525] rounded-lg p-4">
<div key={index} className="bg-[#111] border border-[#3a322b] rounded-lg p-4">
<div className="flex items-center justify-between mb-3">
<span className="text-xs font-body text-[#555]">Article {index + 1}</span>
{digestArticles.length > 1 && (
@@ -803,11 +803,11 @@ export default function AdminNewsletterPage() {
{/* Template Picker Drawer */}
{showTemplateDrawer && (
<div className="fixed inset-0 z-50 bg-black/70 flex items-end sm:items-center justify-center p-0 sm:p-4" onClick={() => setShowTemplateDrawer(false)}>
<div className="bg-[#111] border border-[#252525] rounded-t-2xl sm:rounded-xl w-full sm:max-w-2xl max-h-[80vh] overflow-hidden flex flex-col" onClick={(e) => e.stopPropagation()}>
<div className="flex items-center justify-between px-6 py-4 border-b border-[#252525] flex-shrink-0">
<div className="bg-[#111] border border-[#3a322b] rounded-t-2xl sm:rounded-xl w-full sm:max-w-2xl max-h-[80vh] overflow-hidden flex flex-col" onClick={(e) => e.stopPropagation()}>
<div className="flex items-center justify-between px-6 py-4 border-b border-[#3a322b] flex-shrink-0">
<h2 className="font-display font-bold text-white text-base">Choose a Template</h2>
<div className="flex items-center gap-3">
<Link href="/admin/newsletter/templates" className="text-xs text-[#ffb800] font-body hover:text-blue-400 transition-colors">
<Link href="/admin/newsletter/templates" className="text-xs text-[#F0A623] font-body hover:text-blue-400 transition-colors">
Manage templates
</Link>
<button onClick={() => setShowTemplateDrawer(false)} className="text-[#555] hover:text-white transition-colors">
@@ -820,12 +820,12 @@ export default function AdminNewsletterPage() {
<div className="overflow-y-auto flex-1 p-4">
{loadingTemplates ? (
<div className="flex items-center justify-center py-12">
<div className="w-6 h-6 border-2 border-[#ffb800] border-t-transparent rounded-full animate-spin" />
<div className="w-6 h-6 border-2 border-[#F0A623] border-t-transparent rounded-full animate-spin" />
</div>
) : templates.length === 0 ? (
<div className="text-center py-12">
<p className="text-[#555] font-body text-sm">No templates found.</p>
<Link href="/admin/newsletter/templates" className="text-[#ffb800] text-sm font-body hover:text-blue-400 mt-2 inline-block">
<Link href="/admin/newsletter/templates" className="text-[#F0A623] text-sm font-body hover:text-blue-400 mt-2 inline-block">
Create your first template
</Link>
</div>
@@ -835,7 +835,7 @@ export default function AdminNewsletterPage() {
<button
key={tpl.id}
onClick={() => applyTemplate(tpl)}
className="flex items-start gap-3 p-4 bg-[#0d0d0d] border border-[#252525] rounded-xl text-left hover:border-[#ffb800] hover:bg-[#1a1208] transition-colors group"
className="flex items-start gap-3 p-4 bg-[#1c1815] border border-[#3a322b] rounded-xl text-left hover:border-[#F0A623] hover:bg-[#231d18] transition-colors group"
>
<div className="w-8 h-8 rounded-lg flex items-center justify-center flex-shrink-0 mt-0.5" style={{ backgroundColor: tpl.accent_color + '20' }}>
<span style={{ color: tpl.accent_color }}>
@@ -851,8 +851,8 @@ export default function AdminNewsletterPage() {
</div>
{tpl.description && <p className="text-xs text-[#777] font-body line-clamp-1">{tpl.description}</p>}
<div className="flex gap-1.5 mt-1.5">
<span className="text-xs bg-[#1a1a1a] text-[#888] px-1.5 py-0.5 rounded font-body capitalize">{tpl.layout}</span>
<span className="text-xs bg-[#1a1a1a] text-[#888] px-1.5 py-0.5 rounded font-body">{tpl.article_blocks?.length || 0} slots</span>
<span className="text-xs bg-[#231d18] text-[#888] px-1.5 py-0.5 rounded font-body capitalize">{tpl.layout}</span>
<span className="text-xs bg-[#231d18] text-[#888] px-1.5 py-0.5 rounded font-body">{tpl.article_blocks?.length || 0} slots</span>
</div>
</div>
</button>

View File

@@ -42,7 +42,7 @@ const STYLE_OPTIONS = [
];
const ACCENT_COLORS = [
'#ffb800', '#ef4444', '#10b981', '#8b5cf6', '#f59e0b', '#ec4899', '#06b6d4', '#f97316',
'#F0A623', '#ef4444', '#10b981', '#8b5cf6', '#f59e0b', '#ec4899', '#06b6d4', '#f97316',
];
const LAYOUT_ICONS: Record<string, React.ReactNode> = {
@@ -83,7 +83,7 @@ const emptyForm = (): Omit<NewsletterTemplate, 'id' | 'is_preset' | 'created_at'
subject_prefix: '',
header_text: '',
footer_text: '',
accent_color: '#ffb800',
accent_color: '#F0A623',
article_blocks: [{ title: '', excerpt: '', url: '', category: '' }],
});
@@ -226,7 +226,7 @@ export default function NewsletterTemplatesPage() {
if (loading) {
return (
<div className="min-h-screen bg-[#0a0a0a] flex items-center justify-center">
<div className="w-8 h-8 border-2 border-[#ffb800] border-t-transparent rounded-full animate-spin" />
<div className="w-8 h-8 border-2 border-[#F0A623] border-t-transparent rounded-full animate-spin" />
</div>
);
}
@@ -242,7 +242,7 @@ export default function NewsletterTemplatesPage() {
)}
{/* Header */}
<div className="bg-[#111] border-b border-[#252525] px-6 py-4">
<div className="bg-[#111] border-b border-[#3a322b] px-6 py-4">
<div className="max-w-6xl mx-auto flex items-center justify-between">
<div className="flex items-center gap-3">
<Link href="/admin/newsletter" className="text-[#555] hover:text-[#888] transition-colors">
@@ -272,11 +272,11 @@ export default function NewsletterTemplatesPage() {
<div className="mb-8">
<div className="flex items-center gap-2 mb-4">
<h2 className="font-display font-bold text-white text-base">Preset Templates</h2>
<span className="text-xs bg-[#1a1208] text-[#ffb800] px-2 py-0.5 rounded-full font-body">{presets.length}</span>
<span className="text-xs bg-[#231d18] text-[#F0A623] px-2 py-0.5 rounded-full font-body">{presets.length}</span>
</div>
{loadingData ? (
<div className="flex items-center justify-center py-12">
<div className="w-6 h-6 border-2 border-[#ffb800] border-t-transparent rounded-full animate-spin" />
<div className="w-6 h-6 border-2 border-[#F0A623] border-t-transparent rounded-full animate-spin" />
</div>
) : (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
@@ -298,15 +298,15 @@ export default function NewsletterTemplatesPage() {
<div>
<div className="flex items-center gap-2 mb-4">
<h2 className="font-display font-bold text-white text-base">Custom Templates</h2>
<span className="text-xs bg-[#1a1208] text-[#ffb800] px-2 py-0.5 rounded-full font-body">{custom.length}</span>
<span className="text-xs bg-[#231d18] text-[#F0A623] px-2 py-0.5 rounded-full font-body">{custom.length}</span>
</div>
{!loadingData && custom.length === 0 ? (
<div className="bg-[#111] border border-[#252525] rounded-lg p-10 text-center">
<div className="bg-[#111] border border-[#3a322b] rounded-lg p-10 text-center">
<svg className="w-10 h-10 text-[#333] mx-auto mb-3" fill="none" stroke="currentColor" strokeWidth="1.5" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" d="M19.5 14.25v-2.625a3.375 3.375 0 00-3.375-3.375h-1.5A1.125 1.125 0 0113.5 7.125v-1.5a3.375 3.375 0 00-3.375-3.375H8.25m0 12.75h7.5m-7.5 3H12M10.5 2.25H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 00-9-9z" />
</svg>
<p className="text-[#555] font-body text-sm mb-3">No custom templates yet</p>
<button onClick={openNew} className="text-[#ffb800] text-sm font-body font-semibold hover:text-blue-400 transition-colors">
<button onClick={openNew} className="text-[#F0A623] text-sm font-body font-semibold hover:text-blue-400 transition-colors">
Create your first template
</button>
</div>
@@ -330,8 +330,8 @@ export default function NewsletterTemplatesPage() {
{/* Template Form Modal */}
{showForm && (
<div className="fixed inset-0 z-50 bg-black/70 flex items-start justify-center overflow-y-auto py-8 px-4">
<div className="bg-[#111] border border-[#252525] rounded-xl w-full max-w-2xl">
<div className="flex items-center justify-between px-6 py-4 border-b border-[#252525]">
<div className="bg-[#111] border border-[#3a322b] rounded-xl w-full max-w-2xl">
<div className="flex items-center justify-between px-6 py-4 border-b border-[#3a322b]">
<h2 className="font-display font-bold text-white text-base">
{editingId ? 'Edit Template' : 'New Template'}
</h2>
@@ -376,8 +376,8 @@ export default function NewsletterTemplatesPage() {
key={opt.value}
type="button"
onClick={() => setForm((f) => ({ ...f, layout: opt.value as NewsletterTemplate['layout'] }))}
className={`flex items-start gap-3 p-3 rounded-lg border text-left transition-colors ${form.layout === opt.value ? 'border-[#ffb800] bg-[#1a1208]' : 'border-[#252525] bg-[#0d0d0d] hover:border-[#333]'}`}>
<span className={`mt-0.5 ${form.layout === opt.value ? 'text-[#ffb800]' : 'text-[#555]'}`}>
className={`flex items-start gap-3 p-3 rounded-lg border text-left transition-colors ${form.layout === opt.value ? 'border-[#F0A623] bg-[#231d18]' : 'border-[#3a322b] bg-[#1c1815] hover:border-[#333]'}`}>
<span className={`mt-0.5 ${form.layout === opt.value ? 'text-[#F0A623]' : 'text-[#555]'}`}>
{LAYOUT_ICONS[opt.value]}
</span>
<div>
@@ -398,7 +398,7 @@ export default function NewsletterTemplatesPage() {
key={opt.value}
type="button"
onClick={() => setForm((f) => ({ ...f, style: opt.value as NewsletterTemplate['style'] }))}
className={`flex-1 py-2.5 px-3 rounded-lg border text-sm font-body font-semibold transition-colors ${form.style === opt.value ? 'border-[#ffb800] bg-[#1a1208] text-white' : 'border-[#252525] bg-[#0d0d0d] text-[#888] hover:border-[#333]'}`}>
className={`flex-1 py-2.5 px-3 rounded-lg border text-sm font-body font-semibold transition-colors ${form.style === opt.value ? 'border-[#F0A623] bg-[#231d18] text-white' : 'border-[#3a322b] bg-[#1c1815] text-[#888] hover:border-[#333]'}`}>
{opt.label}
</button>
))}
@@ -469,14 +469,14 @@ export default function NewsletterTemplatesPage() {
<div>
<div className="flex items-center justify-between mb-2">
<label className="text-xs font-body font-bold text-[#888] uppercase tracking-wider">Article Block Slots</label>
<button type="button" onClick={addBlock} className="text-xs text-[#ffb800] hover:text-blue-400 font-body font-semibold transition-colors">
<button type="button" onClick={addBlock} className="text-xs text-[#F0A623] hover:text-blue-400 font-body font-semibold transition-colors">
+ Add Slot
</button>
</div>
<p className="text-xs text-[#555] font-body mb-3">Define placeholder slots for articles. These will be pre-filled when using the template in Send Digest.</p>
<div className="space-y-2">
{form.article_blocks.map((block, i) => (
<div key={i} className="bg-[#0d0d0d] border border-[#252525] rounded-lg p-3">
<div key={i} className="bg-[#1c1815] border border-[#3a322b] rounded-lg p-3">
<div className="flex items-center justify-between mb-2">
<span className="text-xs font-body text-[#555]">Slot {i + 1}</span>
<div className="flex items-center gap-3">
@@ -486,7 +486,7 @@ export default function NewsletterTemplatesPage() {
type="checkbox"
checked={!!block.featured}
onChange={(e) => updateBlock(i, 'featured', e.target.checked)}
className="w-3 h-3 accent-[#ffb800]"
className="w-3 h-3 accent-[#F0A623]"
/>
<span className="text-xs font-body text-[#888]">Featured</span>
</label>
@@ -527,7 +527,7 @@ export default function NewsletterTemplatesPage() {
</div>
</div>
<div className="flex items-center justify-end gap-3 px-6 py-4 border-t border-[#252525]">
<div className="flex items-center justify-end gap-3 px-6 py-4 border-t border-[#3a322b]">
<button onClick={() => setShowForm(false)} className="px-4 py-2 text-sm font-body text-[#888] hover:text-white transition-colors">
Cancel
</button>
@@ -545,8 +545,8 @@ export default function NewsletterTemplatesPage() {
{/* Preview Modal */}
{previewTemplate && (
<div className="fixed inset-0 z-50 bg-black/70 flex items-center justify-center p-4" onClick={() => setPreviewTemplate(null)}>
<div className="bg-[#111] border border-[#252525] rounded-xl w-full max-w-lg" onClick={(e) => e.stopPropagation()}>
<div className="flex items-center justify-between px-6 py-4 border-b border-[#252525]">
<div className="bg-[#111] border border-[#3a322b] rounded-xl w-full max-w-lg" onClick={(e) => e.stopPropagation()}>
<div className="flex items-center justify-between px-6 py-4 border-b border-[#3a322b]">
<h2 className="font-display font-bold text-white text-base">Template Preview</h2>
<button onClick={() => setPreviewTemplate(null)} className="text-[#555] hover:text-white transition-colors">
<svg className="w-5 h-5" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24">
@@ -565,27 +565,27 @@ export default function NewsletterTemplatesPage() {
</div>
</div>
<div className="grid grid-cols-3 gap-3 text-center">
<div className="bg-[#0d0d0d] rounded-lg p-3">
<div className="bg-[#1c1815] rounded-lg p-3">
<p className="text-xs text-[#555] font-body mb-1">Layout</p>
<p className="text-sm font-body font-semibold text-white capitalize">{previewTemplate.layout}</p>
</div>
<div className="bg-[#0d0d0d] rounded-lg p-3">
<div className="bg-[#1c1815] rounded-lg p-3">
<p className="text-xs text-[#555] font-body mb-1">Style</p>
<p className="text-sm font-body font-semibold text-white capitalize">{previewTemplate.style}</p>
</div>
<div className="bg-[#0d0d0d] rounded-lg p-3">
<div className="bg-[#1c1815] rounded-lg p-3">
<p className="text-xs text-[#555] font-body mb-1">Slots</p>
<p className="text-sm font-body font-semibold text-white">{previewTemplate.article_blocks?.length || 0}</p>
</div>
</div>
{previewTemplate.subject_prefix && (
<div className="bg-[#0d0d0d] rounded-lg p-3">
<div className="bg-[#1c1815] rounded-lg p-3">
<p className="text-xs text-[#555] font-body mb-1">Subject Prefix</p>
<p className="text-sm font-body text-[#e0e0e0]">{previewTemplate.subject_prefix}</p>
</div>
)}
{previewTemplate.header_text && (
<div className="bg-[#0d0d0d] rounded-lg p-3">
<div className="bg-[#1c1815] rounded-lg p-3">
<p className="text-xs text-[#555] font-body mb-1">Header Text</p>
<p className="text-sm font-body text-[#e0e0e0]">{previewTemplate.header_text}</p>
</div>
@@ -595,9 +595,9 @@ export default function NewsletterTemplatesPage() {
<p className="text-xs text-[#555] font-body mb-2">Article Slots ({previewTemplate.article_blocks.length})</p>
<div className="space-y-1.5">
{previewTemplate.article_blocks.map((b, i) => (
<div key={i} className="flex items-center gap-2 bg-[#0d0d0d] rounded px-3 py-2">
<div key={i} className="flex items-center gap-2 bg-[#1c1815] rounded px-3 py-2">
<span className="text-xs text-[#555] font-body w-12 flex-shrink-0">Slot {i + 1}</span>
{b.featured && <span className="text-xs text-[#ffb800] font-body bg-[#1a1208] px-1.5 py-0.5 rounded">Featured</span>}
{b.featured && <span className="text-xs text-[#F0A623] font-body bg-[#231d18] px-1.5 py-0.5 rounded">Featured</span>}
{b.category && <span className="text-xs text-[#888] font-body">{b.category}</span>}
{b.title && <span className="text-xs text-[#e0e0e0] font-body truncate">{b.title}</span>}
</div>
@@ -614,7 +614,7 @@ export default function NewsletterTemplatesPage() {
{!previewTemplate.is_preset && (
<button
onClick={() => { setPreviewTemplate(null); openEdit(previewTemplate); }}
className="px-4 py-2 text-sm font-body text-[#888] hover:text-white border border-[#252525] rounded-lg transition-colors">
className="px-4 py-2 text-sm font-body text-[#888] hover:text-white border border-[#3a322b] rounded-lg transition-colors">
Edit
</button>
)}
@@ -637,7 +637,7 @@ interface TemplateCardProps {
function TemplateCard({ template, onEdit, onDelete, onPreview, deletingId }: TemplateCardProps) {
return (
<div className="bg-[#111] border border-[#252525] rounded-xl overflow-hidden hover:border-[#333] transition-colors group">
<div className="bg-[#111] border border-[#3a322b] rounded-xl overflow-hidden hover:border-[#333] transition-colors group">
{/* Color bar */}
<div className="h-1" style={{ backgroundColor: template.accent_color }} />
<div className="p-4">
@@ -660,27 +660,27 @@ function TemplateCard({ template, onEdit, onDelete, onPreview, deletingId }: Tem
)}
<div className="flex flex-wrap gap-1.5 mb-4">
<span className="text-xs bg-[#1a1a1a] text-[#888] px-2 py-0.5 rounded-full font-body capitalize">{template.layout}</span>
<span className="text-xs bg-[#1a1a1a] text-[#888] px-2 py-0.5 rounded-full font-body capitalize">{template.style}</span>
<span className="text-xs bg-[#1a1a1a] text-[#888] px-2 py-0.5 rounded-full font-body">{template.article_blocks?.length || 0} slots</span>
<span className="text-xs bg-[#231d18] text-[#888] px-2 py-0.5 rounded-full font-body capitalize">{template.layout}</span>
<span className="text-xs bg-[#231d18] text-[#888] px-2 py-0.5 rounded-full font-body capitalize">{template.style}</span>
<span className="text-xs bg-[#231d18] text-[#888] px-2 py-0.5 rounded-full font-body">{template.article_blocks?.length || 0} slots</span>
</div>
<div className="flex items-center gap-2">
<button
onClick={() => onPreview(template)}
className="flex-1 py-1.5 text-xs font-body font-semibold text-[#ffb800] border border-[#4a3500] rounded-lg hover:bg-[#1a1208] transition-colors">
className="flex-1 py-1.5 text-xs font-body font-semibold text-[#F0A623] border border-[#412402] rounded-lg hover:bg-[#231d18] transition-colors">
Preview
</button>
<Link
href={`/admin/newsletter?template=${template.id}`}
className="flex-1 py-1.5 text-xs font-body font-semibold text-center text-white bg-[#1a1208] border border-[#4a3500] rounded-lg hover:bg-[#4a3500] transition-colors">
className="flex-1 py-1.5 text-xs font-body font-semibold text-center text-white bg-[#231d18] border border-[#412402] rounded-lg hover:bg-[#412402] transition-colors">
Use
</Link>
{!template.is_preset && (
<>
<button
onClick={() => onEdit(template)}
className="p-1.5 text-[#555] hover:text-white border border-[#252525] rounded-lg transition-colors">
className="p-1.5 text-[#555] hover:text-white border border-[#3a322b] rounded-lg transition-colors">
<svg className="w-3.5 h-3.5" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" d="M16.862 4.487l1.687-1.688a1.875 1.875 0 112.652 2.652L10.582 16.07a4.5 4.5 0 01-1.897 1.13L6 18l.8-2.685a4.5 4.5 0 011.13-1.897l8.932-8.931zm0 0L19.5 7.125" />
</svg>
@@ -688,7 +688,7 @@ function TemplateCard({ template, onEdit, onDelete, onPreview, deletingId }: Tem
<button
onClick={() => onDelete(template.id)}
disabled={deletingId === template.id}
className="p-1.5 text-[#555] hover:text-red-400 border border-[#252525] rounded-lg transition-colors disabled:opacity-50">
className="p-1.5 text-[#555] hover:text-red-400 border border-[#3a322b] rounded-lg transition-colors disabled:opacity-50">
<svg className="w-3.5 h-3.5" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" d="M14.74 9l-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 01-2.244 2.077H8.084a2.25 2.25 0 01-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 00-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 013.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 00-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 00-7.5 0" />
</svg>

View File

@@ -166,7 +166,7 @@ function StatCard({ label, value, icon, color, active, onClick }: StatCardProps)
className={`w-full text-left p-4 rounded-lg border transition-all ${
active
? `${color} border-current/30`
: 'bg-[#111] border-[#252525] hover:border-[#333]'
: 'bg-[#111] border-[#3a322b] hover:border-[#333]'
}`}
>
<div className="flex items-center justify-between mb-2">
@@ -191,7 +191,7 @@ function NotificationRow({ notification: n, onMarkRead, onDismiss }: Notificatio
return (
<div
className={`relative p-4 rounded-lg border transition-all ${
n.read ? 'bg-[#0d0d0d] border-[#1e1e1e]' : `${cfg.bg} ${cfg.border}`
n.read ? 'bg-[#1c1815] border-[#1e1e1e]' : `${cfg.bg} ${cfg.border}`
}`}
>
{/* Unread dot */}
@@ -202,7 +202,7 @@ function NotificationRow({ notification: n, onMarkRead, onDismiss }: Notificatio
<div className="flex items-start gap-3">
{/* Icon */}
<div className={`flex-shrink-0 w-8 h-8 rounded-full flex items-center justify-center ${
n.read ? 'bg-[#1a1a1a]' : cfg.badge
n.read ? 'bg-[#231d18]' : cfg.badge
}`}>
<TypeIcon type={n.type} />
</div>
@@ -244,7 +244,7 @@ function NotificationRow({ notification: n, onMarkRead, onDismiss }: Notificatio
{n.actionUrl && (
<Link
href={n.actionUrl}
className="text-xs font-body font-bold text-[#ffb800] hover:text-blue-300 transition-colors"
className="text-xs font-body font-bold text-[#F0A623] hover:text-blue-300 transition-colors"
>
{n.actionLabel || 'View →'}
</Link>
@@ -474,7 +474,7 @@ export default function NotificationsPage() {
return (
<div className="min-h-screen bg-[#0a0a0a] flex items-center justify-center">
<div className="text-center">
<div className="w-8 h-8 border-2 border-[#ffb800] border-t-transparent rounded-full animate-spin mx-auto mb-3" />
<div className="w-8 h-8 border-2 border-[#F0A623] border-t-transparent rounded-full animate-spin mx-auto mb-3" />
<p className="text-[#555] text-sm font-body">Loading notifications</p>
</div>
</div>
@@ -512,7 +512,7 @@ export default function NotificationsPage() {
{stats.unread > 0 && (
<button
onClick={handleMarkAllRead}
className="px-3 py-1.5 text-xs font-body font-bold text-[#ffb800] border border-[#ffb800]/30 rounded hover:bg-[#ffb800]/10 transition-colors"
className="px-3 py-1.5 text-xs font-body font-bold text-[#F0A623] border border-[#F0A623]/30 rounded hover:bg-[#F0A623]/10 transition-colors"
>
Mark all read
</button>
@@ -520,7 +520,7 @@ export default function NotificationsPage() {
<button
onClick={handleRefresh}
disabled={refreshing}
className="flex items-center gap-1.5 px-3 py-1.5 text-xs font-body text-[#888] border border-[#252525] rounded hover:border-[#333] hover:text-white transition-colors disabled:opacity-50"
className="flex items-center gap-1.5 px-3 py-1.5 text-xs font-body text-[#888] border border-[#3a322b] rounded hover:border-[#333] hover:text-white transition-colors disabled:opacity-50"
>
<RefreshIcon className={`w-3.5 h-3.5 ${refreshing ? 'animate-spin' : ''}`} />
Refresh
@@ -558,7 +558,7 @@ export default function NotificationsPage() {
label="Unread"
value={stats.unread}
icon={<BellIcon className="w-4 h-4" />}
color="text-[#ffb800] bg-[#ffb800]/5 border-[#ffb800]/30"
color="text-[#F0A623] bg-[#F0A623]/5 border-[#F0A623]/30"
active={filter === 'unread'}
onClick={() => setFilter(filter === 'unread' ? 'all' : 'unread')}
/>
@@ -580,7 +580,7 @@ export default function NotificationsPage() {
onClick={() => setFilter(f)}
className={`px-3 py-1.5 text-xs font-body font-bold rounded transition-colors ${
filter === f
? 'bg-[#ffb800] text-white'
? 'bg-[#F0A623] text-white'
: 'text-[#555] hover:text-[#888] hover:bg-[#111]'
}`}
>

View File

@@ -44,8 +44,8 @@ function timeAgo(dateStr: string): string {
const STATUS_COLORS: Record<string, string> = {
published: 'text-green-400 bg-green-400/10',
pending: 'text-yellow-400 bg-yellow-400/10',
draft: 'text-[#888] bg-[#1a1a1a]',
archived: 'text-[#555] bg-[#1a1a1a]',
draft: 'text-[#888] bg-[#231d18]',
archived: 'text-[#555] bg-[#231d18]',
imported: 'text-blue-400 bg-blue-400/10',
};
@@ -91,7 +91,7 @@ interface MetricCardProps {
function MetricCard({ label, value, sub, accent, icon, href, badge }: MetricCardProps) {
const content = (
<div className={`bg-[#111] border border-[#252525] rounded-lg p-5 flex flex-col gap-3 hover:border-[#333] transition-colors ${href ? 'cursor-pointer' : ''}`}>
<div className={`bg-[#111] border border-[#3a322b] rounded-lg p-5 flex flex-col gap-3 hover:border-[#333] transition-colors ${href ? 'cursor-pointer' : ''}`}>
<div className="flex items-start justify-between">
<div className={`w-10 h-10 rounded-lg flex items-center justify-center ${accent}`}>
{icon}
@@ -125,15 +125,15 @@ interface QuickActionProps {
function QuickAction({ label, description, href, icon, accent }: QuickActionProps) {
return (
<Link href={href} className="flex items-center gap-3 p-3 bg-[#111] border border-[#252525] rounded-lg hover:border-[#ffb800] hover:bg-[#0d1a2d] transition-all group">
<Link href={href} className="flex items-center gap-3 p-3 bg-[#111] border border-[#3a322b] rounded-lg hover:border-[#F0A623] hover:bg-[#0d1a2d] transition-all group">
<div className={`w-9 h-9 rounded-lg flex items-center justify-center flex-shrink-0 ${accent} group-hover:scale-105 transition-transform`}>
{icon}
</div>
<div className="min-w-0">
<p className="text-sm font-body font-semibold text-white group-hover:text-[#ffb800] transition-colors">{label}</p>
<p className="text-sm font-body font-semibold text-white group-hover:text-[#F0A623] transition-colors">{label}</p>
<p className="text-xs text-[#555] font-body truncate">{description}</p>
</div>
<svg className="w-4 h-4 text-[#444] group-hover:text-[#ffb800] ml-auto flex-shrink-0 transition-colors" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24">
<svg className="w-4 h-4 text-[#444] group-hover:text-[#F0A623] ml-auto flex-shrink-0 transition-colors" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" d="M9 5l7 7-7 7" />
</svg>
</Link>
@@ -239,7 +239,7 @@ export default function AdminDashboardPage() {
return (
<div className="min-h-screen bg-[#0a0a0a] flex items-center justify-center">
<div className="flex flex-col items-center gap-3">
<div className="w-8 h-8 border-2 border-[#ffb800] border-t-transparent rounded-full animate-spin" />
<div className="w-8 h-8 border-2 border-[#F0A623] border-t-transparent rounded-full animate-spin" />
<p className="text-[#555] text-sm font-body">Loading dashboard</p>
</div>
</div>
@@ -345,7 +345,7 @@ export default function AdminDashboardPage() {
<MetricCard
label="Drafts"
value={metrics.draftArticles}
accent="bg-[#1a1a1a]"
accent="bg-[#231d18]"
href="/admin/articles"
icon={
<svg className="w-5 h-5 text-[#888]" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24">
@@ -356,7 +356,7 @@ export default function AdminDashboardPage() {
<MetricCard
label="Archived"
value={metrics.archivedArticles}
accent="bg-[#1a1a1a]"
accent="bg-[#231d18]"
href="/admin/articles"
icon={
<svg className="w-5 h-5 text-[#555]" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24">
@@ -372,20 +372,20 @@ export default function AdminDashboardPage() {
{/* Recent Activity Feed */}
<div className="lg:col-span-2">
<div className="bg-[#111] border border-[#252525] rounded-lg overflow-hidden">
<div className="px-5 py-4 border-b border-[#1a1a1a] flex items-center justify-between">
<div className="bg-[#111] border border-[#3a322b] rounded-lg overflow-hidden">
<div className="px-5 py-4 border-b border-[#231d18] flex items-center justify-between">
<h2 className="text-sm font-display font-bold text-white">Recent Activity</h2>
<Link href="/admin/articles" className="text-xs text-[#ffb800] hover:text-blue-300 font-body transition-colors">View all </Link>
<Link href="/admin/articles" className="text-xs text-[#F0A623] hover:text-blue-300 font-body transition-colors">View all </Link>
</div>
<div className="divide-y divide-[#1a1a1a]">
<div className="divide-y divide-[#231d18]">
{recentActivity.length === 0 ? (
<div className="px-5 py-10 text-center">
<p className="text-[#555] text-sm font-body">No recent activity found</p>
</div>
) : (
recentActivity.map((item) => (
<div key={`${item.source}-${item.id}`} className="px-5 py-3 flex items-start gap-3 hover:bg-[#0d0d0d] transition-colors">
<div className="w-6 h-6 rounded-full bg-[#1a1a1a] flex items-center justify-center flex-shrink-0 mt-0.5">
<div key={`${item.source}-${item.id}`} className="px-5 py-3 flex items-start gap-3 hover:bg-[#1c1815] transition-colors">
<div className="w-6 h-6 rounded-full bg-[#231d18] flex items-center justify-center flex-shrink-0 mt-0.5">
{ACTIVITY_ICONS[item.type] || ACTIVITY_ICONS['draft']}
</div>
<div className="flex-1 min-w-0">
@@ -412,8 +412,8 @@ export default function AdminDashboardPage() {
{/* Quick Actions */}
<div className="flex flex-col gap-4">
<div className="bg-[#111] border border-[#252525] rounded-lg overflow-hidden">
<div className="px-5 py-4 border-b border-[#1a1a1a]">
<div className="bg-[#111] border border-[#3a322b] rounded-lg overflow-hidden">
<div className="px-5 py-4 border-b border-[#231d18]">
<h2 className="text-sm font-display font-bold text-white">Quick Actions</h2>
</div>
<div className="p-4 flex flex-col gap-2">
@@ -587,21 +587,21 @@ export default function AdminDashboardPage() {
</div>
{/* Status Summary Mini-Card */}
<div className="bg-[#111] border border-[#252525] rounded-lg p-4">
<div className="bg-[#111] border border-[#3a322b] rounded-lg p-4">
<h3 className="text-xs font-body font-bold text-[#555] uppercase tracking-widest mb-3">Content Status</h3>
<div className="space-y-2">
{[
{ label: 'Published', value: metrics.publishedArticles, color: 'bg-green-400', total: metrics.totalArticles },
{ label: 'Pending', value: metrics.pendingApprovals, color: 'bg-yellow-400', total: metrics.totalArticles },
{ label: 'Draft', value: metrics.draftArticles, color: 'bg-[#444]', total: metrics.totalArticles },
{ label: 'Archived', value: metrics.archivedArticles, color: 'bg-[#2a2a2a]', total: metrics.totalArticles },
{ label: 'Archived', value: metrics.archivedArticles, color: 'bg-[#3a322b]', total: metrics.totalArticles },
].map(({ label, value, color, total }) => (
<div key={label}>
<div className="flex items-center justify-between mb-1">
<span className="text-xs text-[#888] font-body">{label}</span>
<span className="text-xs text-white font-body font-semibold">{value}</span>
</div>
<div className="h-1.5 bg-[#1a1a1a] rounded-full overflow-hidden">
<div className="h-1.5 bg-[#231d18] rounded-full overflow-hidden">
<div
className={`h-full rounded-full ${color} transition-all duration-500`}
style={{ width: total > 0 ? `${Math.round((value / total) * 100)}%` : '0%' }}

View File

@@ -41,7 +41,7 @@ export default function ReviewActions({ id, currentStatus }: Props) {
value={note}
onChange={(e) => setNote(e.target.value)}
placeholder="Reviewer note (optional)"
className="w-full rounded border border-[#1f2937] bg-[#070a10] p-2 text-sm text-[#e5e7eb] focus:border-[#ffb800] focus:outline-none"
className="w-full rounded border border-[#1f2937] bg-[#070a10] p-2 text-sm text-[#e5e7eb] focus:border-[#F0A623] focus:outline-none"
rows={2}
/>
<div className="mt-3 flex flex-wrap gap-3 text-sm">

View File

@@ -41,12 +41,12 @@ export default async function ReviewDetailPage({ params }: { params: Promise<{ i
return (
<main className="mx-auto max-w-6xl px-6 py-10 text-[#e5e7eb]">
<Link href="/admin/review-queue" className="text-sm text-[#ffd25a] hover:underline">
<Link href="/admin/review-queue" className="text-sm text-[#F0A623] hover:underline">
Back to queue
</Link>
<header className="mt-4 mb-6 border-b border-[#222] pb-4">
<div className="flex items-center gap-3 text-xs uppercase tracking-wider text-[#ffd25a]">
<div className="flex items-center gap-3 text-xs uppercase tracking-wider text-[#F0A623]">
<span>{persona?.name || "—"}</span>
<span className="text-[#4b5563]">·</span>
<span>{art.category}</span>

View File

@@ -70,7 +70,7 @@ export default async function ReviewQueuePage() {
return (
<li
key={r.id}
className="rounded border border-[#1f2937] bg-[#0b0f17] p-5 hover:border-[#ffb800] transition-colors"
className="rounded border border-[#1f2937] bg-[#0b0f17] p-5 hover:border-[#F0A623] transition-colors"
>
<div className="flex items-start gap-4">
{persona?.avatar_url ? (
@@ -85,7 +85,7 @@ export default async function ReviewQueuePage() {
<div className="h-14 w-14 rounded-full bg-[#1f2937]" />
)}
<div className="flex-1 min-w-0">
<div className="text-xs uppercase tracking-wider text-[#ffd25a]">
<div className="text-xs uppercase tracking-wider text-[#F0A623]">
{persona?.name || "—"} · {r.category}
</div>
<h2 className="text-xl font-semibold mt-1">{r.title}</h2>
@@ -100,7 +100,7 @@ export default async function ReviewQueuePage() {
<div className="mt-4 flex flex-wrap items-center gap-3 text-xs">
<Link
href={`/admin/review-queue/${r.id}`}
className="rounded bg-[#1d4ed8] px-3 py-1.5 font-medium text-white hover:bg-[#d99700]"
className="rounded bg-[#1d4ed8] px-3 py-1.5 font-medium text-white hover:bg-[#BA7517]"
>
Review
</Link>

View File

@@ -12,7 +12,7 @@ export default function RmpHubPage() {
useEffect(() => { if (!loading && !user) router?.push('/login'); }, [user, loading, router]);
if (loading) return <div className="min-h-screen bg-[#0a0a0a] flex items-center justify-center"><div className="w-8 h-8 border-2 border-[#ffb800] border-t-transparent rounded-full animate-spin" /></div>;
if (loading) return <div className="min-h-screen bg-[#0a0a0a] flex items-center justify-center"><div className="w-8 h-8 border-2 border-[#F0A623] border-t-transparent rounded-full animate-spin" /></div>;
if (!user) return null;
return (
@@ -68,7 +68,7 @@ export default function RmpHubPage() {
],
},
]?.map(section => (
<div key={section?.title} className="bg-[#111] border border-[#252525] rounded-lg p-4 space-y-3">
<div key={section?.title} className="bg-[#111] border border-[#3a322b] rounded-lg p-4 space-y-3">
<div className="flex items-center gap-2">
<span className="text-2xl">{section?.icon}</span>
<div>
@@ -78,19 +78,19 @@ export default function RmpHubPage() {
</div>
<div className="space-y-1">
{section?.links?.map(l => (
<Link key={l?.href} href={l?.href} className="block px-2 py-1 rounded text-xs text-[#888] hover:text-white hover:bg-[#1a1a1a] transition-colors">
<Link key={l?.href} href={l?.href} className="block px-2 py-1 rounded text-xs text-[#888] hover:text-white hover:bg-[#231d18] transition-colors">
{l?.label}
</Link>
))}
</div>
<Link href={section?.href} className="block w-full text-center px-3 py-1.5 bg-[#1a1208] border border-[#2a3a50] rounded text-xs text-[#ffb800] hover:bg-[#0d1a2d] transition-colors">
<Link href={section?.href} className="block w-full text-center px-3 py-1.5 bg-[#231d18] border border-[#2a3a50] rounded text-xs text-[#F0A623] hover:bg-[#0d1a2d] transition-colors">
Open {section?.title}
</Link>
</div>
))}
</div>
<div className="bg-[#0d1117] border border-[#1a1208] rounded-lg p-4">
<div className="bg-[#0d1117] border border-[#231d18] rounded-lg p-4">
<p className="text-xs text-[#555] mb-3">Quick Navigation</p>
<RmpAdminNav />
</div>

View File

@@ -61,7 +61,7 @@ const ROLE_COLORS: Record<string, string> = {
editor: 'text-purple-400 bg-purple-400/10 border-purple-400/20',
moderator: 'text-orange-400 bg-orange-400/10 border-orange-400/20',
contributor: 'text-blue-400 bg-blue-400/10 border-blue-400/20',
reader: 'text-[#888] bg-[#1a1a1a] border-[#333]',
reader: 'text-[#888] bg-[#231d18] border-[#333]',
};
const ROLE_ICONS: Record<string, React.ReactNode> = {
@@ -324,7 +324,7 @@ export default function AdminRolesPage() {
return (
<div className="min-h-screen bg-[#0a0a0a] flex items-center justify-center">
<div className="flex flex-col items-center gap-3">
<div className="w-8 h-8 border-2 border-[#ffb800] border-t-transparent rounded-full animate-spin" />
<div className="w-8 h-8 border-2 border-[#F0A623] border-t-transparent rounded-full animate-spin" />
<p className="text-[#555] text-sm font-body">Loading roles panel</p>
</div>
</div>
@@ -376,7 +376,7 @@ export default function AdminRolesPage() {
key={role}
onClick={() => { setSelectedRole(role === selectedRole ? 'all' : role); setActiveTab('assign'); }}
className={`bg-[#111] border rounded-lg p-4 text-left hover:border-[#333] transition-all ${
selectedRole === role ? 'border-[#ffb800] bg-[#0d1a2d]' : 'border-[#252525]'
selectedRole === role ? 'border-[#F0A623] bg-[#0d1a2d]' : 'border-[#3a322b]'
}`}
>
<div className={`inline-flex items-center gap-1.5 px-2 py-0.5 rounded-full text-xs font-body font-semibold border mb-2 ${color}`}>
@@ -391,7 +391,7 @@ export default function AdminRolesPage() {
</div>
{/* Tabs */}
<div className="flex gap-1 mb-6 bg-[#111] border border-[#252525] rounded-lg p-1 w-fit">
<div className="flex gap-1 mb-6 bg-[#111] border border-[#3a322b] rounded-lg p-1 w-fit">
{([
{ id: 'assign', label: 'Assign Roles', icon: 'M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0z' },
{ id: 'permissions', label: 'Permissions', icon: 'M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z' },
@@ -402,7 +402,7 @@ export default function AdminRolesPage() {
onClick={() => setActiveTab(tab.id)}
className={`flex items-center gap-2 px-4 py-2 rounded-md text-sm font-body font-medium transition-all ${
activeTab === tab.id
? 'bg-[#ffb800] text-white'
? 'bg-[#F0A623] text-white'
: 'text-[#888] hover:text-white'
}`}
>
@@ -416,9 +416,9 @@ export default function AdminRolesPage() {
{/* ── Tab: Assign Roles ── */}
{activeTab === 'assign' && (
<div className="bg-[#111] border border-[#252525] rounded-lg overflow-hidden">
<div className="bg-[#111] border border-[#3a322b] rounded-lg overflow-hidden">
{/* Toolbar */}
<div className="px-5 py-4 border-b border-[#1a1a1a] flex flex-col sm:flex-row gap-3">
<div className="px-5 py-4 border-b border-[#231d18] flex flex-col sm:flex-row gap-3">
<div className="relative flex-1">
<svg className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-[#555]" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
@@ -428,13 +428,13 @@ export default function AdminRolesPage() {
placeholder="Search users…"
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="w-full bg-[#0d0d0d] border border-[#252525] rounded-lg pl-9 pr-4 py-2 text-sm text-white placeholder-[#555] font-body focus:outline-none focus:border-[#ffb800]"
className="w-full bg-[#1c1815] border border-[#3a322b] rounded-lg pl-9 pr-4 py-2 text-sm text-white placeholder-[#555] font-body focus:outline-none focus:border-[#F0A623]"
/>
</div>
<select
value={selectedRole}
onChange={(e) => setSelectedRole(e.target.value)}
className="bg-[#0d0d0d] border border-[#252525] rounded-lg px-3 py-2 text-sm text-white font-body focus:outline-none focus:border-[#ffb800]"
className="bg-[#1c1815] border border-[#3a322b] rounded-lg px-3 py-2 text-sm text-white font-body focus:outline-none focus:border-[#F0A623]"
>
<option value="all">All Roles</option>
{ROLES.map((r) => (
@@ -447,7 +447,7 @@ export default function AdminRolesPage() {
<div className="overflow-x-auto">
<table className="w-full">
<thead>
<tr className="border-b border-[#1a1a1a]">
<tr className="border-b border-[#231d18]">
<th className="px-5 py-3 text-left text-xs font-body font-bold text-[#555] uppercase tracking-wider">User</th>
<th className="px-5 py-3 text-left text-xs font-body font-bold text-[#555] uppercase tracking-wider">Current Role</th>
<th className="px-5 py-3 text-left text-xs font-body font-bold text-[#555] uppercase tracking-wider hidden sm:table-cell">Status</th>
@@ -455,7 +455,7 @@ export default function AdminRolesPage() {
<th className="px-5 py-3 text-left text-xs font-body font-bold text-[#555] uppercase tracking-wider">Assign Role</th>
</tr>
</thead>
<tbody className="divide-y divide-[#1a1a1a]">
<tbody className="divide-y divide-[#231d18]">
{filteredUsers.length === 0 ? (
<tr>
<td colSpan={5} className="px-5 py-10 text-center text-[#555] text-sm font-body">
@@ -464,10 +464,10 @@ export default function AdminRolesPage() {
</tr>
) : (
filteredUsers.map((u) => (
<tr key={u.id} className="hover:bg-[#0d0d0d] transition-colors">
<tr key={u.id} className="hover:bg-[#1c1815] transition-colors">
<td className="px-5 py-3">
<div className="flex items-center gap-3">
<div className="w-8 h-8 rounded-full bg-[#1a1a1a] flex items-center justify-center flex-shrink-0 text-xs font-body font-bold text-[#888]">
<div className="w-8 h-8 rounded-full bg-[#231d18] flex items-center justify-center flex-shrink-0 text-xs font-body font-bold text-[#888]">
{u.avatar_url
? <img src={u.avatar_url} alt={u.full_name} className="w-8 h-8 rounded-full object-cover" />
: getInitials(u.full_name || u.email)
@@ -484,7 +484,7 @@ export default function AdminRolesPage() {
</td>
<td className="px-5 py-3 hidden sm:table-cell">
<span className={`text-xs font-body font-semibold px-2 py-0.5 rounded-full ${
u.is_active ? 'text-green-400 bg-green-400/10' : 'text-[#555] bg-[#1a1a1a]'
u.is_active ? 'text-green-400 bg-green-400/10' : 'text-[#555] bg-[#231d18]'
}`}>
{u.is_active ? 'Active' : 'Inactive'}
</span>
@@ -500,14 +500,14 @@ export default function AdminRolesPage() {
defaultValue={u.role}
onChange={(e) => handleAssignRole(u.id, e.target.value)}
disabled={processingUserId === u.id}
className="bg-[#0d0d0d] border border-[#252525] rounded-lg px-2 py-1.5 text-xs text-white font-body focus:outline-none focus:border-[#ffb800] disabled:opacity-50"
className="bg-[#1c1815] border border-[#3a322b] rounded-lg px-2 py-1.5 text-xs text-white font-body focus:outline-none focus:border-[#F0A623] disabled:opacity-50"
>
{ROLES.map((r) => (
<option key={r} value={r} className="capitalize">{r.charAt(0).toUpperCase() + r.slice(1)}</option>
))}
</select>
{processingUserId === u.id && (
<div className="w-4 h-4 border-2 border-[#ffb800] border-t-transparent rounded-full animate-spin" />
<div className="w-4 h-4 border-2 border-[#F0A623] border-t-transparent rounded-full animate-spin" />
)}
</div>
</td>
@@ -519,7 +519,7 @@ export default function AdminRolesPage() {
</div>
{filteredUsers.length > 0 && (
<div className="px-5 py-3 border-t border-[#1a1a1a]">
<div className="px-5 py-3 border-t border-[#231d18]">
<p className="text-xs text-[#555] font-body">{filteredUsers.length} user{filteredUsers.length !== 1 ? 's' : ''} shown</p>
</div>
)}
@@ -535,9 +535,9 @@ export default function AdminRolesPage() {
const color = ROLE_COLORS[perm.role] || ROLE_COLORS['reader'];
return (
<div key={perm.role} className="bg-[#111] border border-[#252525] rounded-lg overflow-hidden">
<div key={perm.role} className="bg-[#111] border border-[#3a322b] rounded-lg overflow-hidden">
{/* Role Header */}
<div className="px-5 py-4 border-b border-[#1a1a1a] flex items-center justify-between">
<div className="px-5 py-4 border-b border-[#231d18] flex items-center justify-between">
<div className="flex items-center gap-3">
<div className={`inline-flex items-center gap-1.5 px-2.5 py-1 rounded-full text-xs font-body font-semibold border ${color}`}>
{ROLE_ICONS[perm.role]}
@@ -557,7 +557,7 @@ export default function AdminRolesPage() {
<button
onClick={handleSavePermissions}
disabled={savingPermissions}
className="px-3 py-1.5 text-xs font-body font-semibold bg-[#ffb800] hover:bg-blue-500 text-white rounded-lg transition-colors disabled:opacity-50 flex items-center gap-1.5"
className="px-3 py-1.5 text-xs font-body font-semibold bg-[#F0A623] hover:bg-blue-500 text-white rounded-lg transition-colors disabled:opacity-50 flex items-center gap-1.5"
>
{savingPermissions && <div className="w-3 h-3 border-2 border-white border-t-transparent rounded-full animate-spin" />}
Save Changes
@@ -593,12 +593,12 @@ export default function AdminRolesPage() {
{isEditing ? (
<button
onClick={() => setEditedPermissions((prev) => ({ ...prev, [key]: !val }))}
className={`relative w-9 h-5 rounded-full transition-colors ${val ? 'bg-[#ffb800]' : 'bg-[#333]'}`}
className={`relative w-9 h-5 rounded-full transition-colors ${val ? 'bg-[#F0A623]' : 'bg-[#333]'}`}
>
<span className={`absolute top-0.5 w-4 h-4 bg-white rounded-full shadow transition-transform ${val ? 'translate-x-4' : 'translate-x-0.5'}`} />
</button>
) : (
<span className={`w-5 h-5 rounded-full flex items-center justify-center flex-shrink-0 ${val ? 'bg-green-400/10' : 'bg-[#1a1a1a]'}`}>
<span className={`w-5 h-5 rounded-full flex items-center justify-center flex-shrink-0 ${val ? 'bg-green-400/10' : 'bg-[#231d18]'}`}>
{val ? (
<svg className="w-3 h-3 text-green-400" fill="none" stroke="currentColor" strokeWidth="3" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" d="M5 13l4 4L19 7" />
@@ -631,9 +631,9 @@ export default function AdminRolesPage() {
{/* ── Tab: Activity Logs ── */}
{activeTab === 'activity' && (
<div className="bg-[#111] border border-[#252525] rounded-lg overflow-hidden">
<div className="bg-[#111] border border-[#3a322b] rounded-lg overflow-hidden">
{/* Toolbar */}
<div className="px-5 py-4 border-b border-[#1a1a1a] flex flex-col sm:flex-row gap-3 items-start sm:items-center justify-between">
<div className="px-5 py-4 border-b border-[#231d18] flex flex-col sm:flex-row gap-3 items-start sm:items-center justify-between">
<h2 className="text-sm font-display font-bold text-white">Role-Based Activity</h2>
<div className="flex items-center gap-2">
<span className="text-xs text-[#555] font-body">Filter by role:</span>
@@ -644,8 +644,8 @@ export default function AdminRolesPage() {
onClick={() => setActivityRoleFilter(r)}
className={`px-2.5 py-1 rounded-md text-xs font-body font-medium transition-colors capitalize ${
activityRoleFilter === r
? 'bg-[#ffb800] text-white'
: 'bg-[#1a1a1a] text-[#888] hover:text-white'
? 'bg-[#F0A623] text-white'
: 'bg-[#231d18] text-[#888] hover:text-white'
}`}
>
{r}
@@ -656,7 +656,7 @@ export default function AdminRolesPage() {
</div>
{/* Log List */}
<div className="divide-y divide-[#1a1a1a]">
<div className="divide-y divide-[#231d18]">
{activityLogs.length === 0 ? (
<div className="px-5 py-12 text-center">
<svg className="w-10 h-10 text-[#333] mx-auto mb-3" fill="none" stroke="currentColor" strokeWidth="1.5" viewBox="0 0 24 24">
@@ -666,13 +666,13 @@ export default function AdminRolesPage() {
</div>
) : (
activityLogs.map((log) => {
const actionColor = ACTION_COLORS[log.action] || 'text-[#888] bg-[#1a1a1a]';
const actionColor = ACTION_COLORS[log.action] || 'text-[#888] bg-[#231d18]';
const actionLabel = ACTION_LABELS[log.action] || log.action.replace(/_/g, ' ');
const performer = log.user_profiles?.full_name || log.performed_by_email || 'System';
const performerRole = log.user_profiles?.role;
return (
<div key={log.id} className="px-5 py-3.5 flex items-start gap-4 hover:bg-[#0d0d0d] transition-colors">
<div key={log.id} className="px-5 py-3.5 flex items-start gap-4 hover:bg-[#1c1815] transition-colors">
{/* Action badge */}
<div className={`mt-0.5 px-2 py-0.5 rounded text-xs font-body font-semibold flex-shrink-0 ${actionColor}`}>
{actionLabel}
@@ -720,7 +720,7 @@ export default function AdminRolesPage() {
</div>
{activityLogs.length > 0 && (
<div className="px-5 py-3 border-t border-[#1a1a1a]">
<div className="px-5 py-3 border-t border-[#231d18]">
<p className="text-xs text-[#555] font-body">{activityLogs.length} log entries shown</p>
</div>
)}

View File

@@ -223,7 +223,7 @@ export default function AdminSuspensionsPage() {
if (loading) {
return (
<div className="min-h-screen bg-[#0d1117] flex items-center justify-center">
<div className="w-6 h-6 border-2 border-[#ffb800] border-t-transparent rounded-full animate-spin" />
<div className="w-6 h-6 border-2 border-[#F0A623] border-t-transparent rounded-full animate-spin" />
</div>
);
}
@@ -288,7 +288,7 @@ export default function AdminSuspensionsPage() {
onClick={() => setActiveTab(tab)}
className={`px-4 py-2.5 text-sm font-semibold capitalize transition-colors border-b-2 -mb-px ${
activeTab === tab
? 'text-[#ffb800] border-[#ffb800]'
? 'text-[#F0A623] border-[#F0A623]'
: 'text-[#555] border-transparent hover:text-[#888]'
}`}
>
@@ -304,7 +304,7 @@ export default function AdminSuspensionsPage() {
{loadingData ? (
<div className="flex items-center justify-center py-16">
<div className="w-6 h-6 border-2 border-[#ffb800] border-t-transparent rounded-full animate-spin" />
<div className="w-6 h-6 border-2 border-[#F0A623] border-t-transparent rounded-full animate-spin" />
</div>
) : (
<>
@@ -336,7 +336,7 @@ export default function AdminSuspensionsPage() {
<tr key={s.id} className="border-b border-[#1a2030] hover:bg-[#0d1117]/50 transition-colors">
<td className="px-5 py-3.5">
<div className="flex items-center gap-3">
<div className="w-8 h-8 rounded-full bg-[#1f2937] flex items-center justify-center text-[#ffb800] font-bold text-sm flex-shrink-0 overflow-hidden">
<div className="w-8 h-8 rounded-full bg-[#1f2937] flex items-center justify-center text-[#F0A623] font-bold text-sm flex-shrink-0 overflow-hidden">
{s.user?.avatar_url ? (
<img src={s.user.avatar_url} alt={s.user.full_name || s.user.email} className="w-full h-full object-cover" />
) : (
@@ -415,7 +415,7 @@ export default function AdminSuspensionsPage() {
<tr key={s.id} className="border-b border-[#1a2030] hover:bg-[#0d1117]/50 transition-colors">
<td className="px-5 py-3.5">
<div className="flex items-center gap-3">
<div className="w-7 h-7 rounded-full bg-[#1f2937] flex items-center justify-center text-[#ffb800] text-xs font-bold flex-shrink-0">
<div className="w-7 h-7 rounded-full bg-[#1f2937] flex items-center justify-center text-[#F0A623] text-xs font-bold flex-shrink-0">
{(s.user?.full_name?.[0] || s.user?.email?.[0] || '?').toUpperCase()}
</div>
<div className="min-w-0">
@@ -465,7 +465,7 @@ export default function AdminSuspensionsPage() {
placeholder="Search users..."
value={userSearch}
onChange={(e) => setUserSearch(e.target.value)}
className="w-full bg-[#111827] border border-[#1f2937] text-white text-sm rounded-lg pl-9 pr-3 py-2 placeholder-[#444] focus:outline-none focus:border-[#ffb800]"
className="w-full bg-[#111827] border border-[#1f2937] text-white text-sm rounded-lg pl-9 pr-3 py-2 placeholder-[#444] focus:outline-none focus:border-[#F0A623]"
/>
</div>
</div>
@@ -488,7 +488,7 @@ export default function AdminSuspensionsPage() {
<tr key={u.id} className="border-b border-[#1a2030] hover:bg-[#0d1117]/50 transition-colors">
<td className="px-5 py-3.5">
<div className="flex items-center gap-3">
<div className="w-8 h-8 rounded-full bg-[#1f2937] flex items-center justify-center text-[#ffb800] font-bold text-sm flex-shrink-0 overflow-hidden">
<div className="w-8 h-8 rounded-full bg-[#1f2937] flex items-center justify-center text-[#F0A623] font-bold text-sm flex-shrink-0 overflow-hidden">
{u.avatar_url ? (
<img src={u.avatar_url} alt={u.full_name || u.email} className="w-full h-full object-cover" />
) : (
@@ -572,7 +572,7 @@ export default function AdminSuspensionsPage() {
<form onSubmit={handleSuspend} className="px-6 py-5 space-y-4">
{/* User info */}
<div className="flex items-center gap-3 bg-[#0d1117] rounded-lg p-3">
<div className="w-9 h-9 rounded-full bg-[#1f2937] flex items-center justify-center text-[#ffb800] font-bold text-sm flex-shrink-0 overflow-hidden">
<div className="w-9 h-9 rounded-full bg-[#1f2937] flex items-center justify-center text-[#F0A623] font-bold text-sm flex-shrink-0 overflow-hidden">
{selectedUser.avatar_url ? (
<img src={selectedUser.avatar_url} alt={selectedUser.full_name || selectedUser.email} className="w-full h-full object-cover" />
) : (
@@ -637,7 +637,7 @@ export default function AdminSuspensionsPage() {
<select
value={suspendDuration ?? ''}
onChange={(e) => setSuspendDuration(e.target.value ? parseInt(e.target.value) : null)}
className="w-full bg-[#0d1117] border border-[#1f2937] text-white text-sm rounded-lg px-3 py-2.5 focus:outline-none focus:border-[#ffb800]"
className="w-full bg-[#0d1117] border border-[#1f2937] text-white text-sm rounded-lg px-3 py-2.5 focus:outline-none focus:border-[#F0A623]"
>
{DURATION_OPTIONS.filter(d => d.value !== null).map((d) => (
<option key={d.value} value={d.value!}>{d.label}</option>
@@ -657,7 +657,7 @@ export default function AdminSuspensionsPage() {
onChange={(e) => setSuspendReason(e.target.value)}
placeholder="Explain the reason for this action..."
rows={3}
className="w-full bg-[#0d1117] border border-[#1f2937] text-white text-sm rounded-lg px-3 py-2.5 placeholder-[#444] focus:outline-none focus:border-[#ffb800] resize-none"
className="w-full bg-[#0d1117] border border-[#1f2937] text-white text-sm rounded-lg px-3 py-2.5 placeholder-[#444] focus:outline-none focus:border-[#F0A623] resize-none"
/>
</div>
@@ -729,7 +729,7 @@ export default function AdminSuspensionsPage() {
onChange={(e) => setLiftReason(e.target.value)}
placeholder="Reason for lifting this restriction..."
rows={2}
className="w-full bg-[#0d1117] border border-[#1f2937] text-white text-sm rounded-lg px-3 py-2.5 placeholder-[#444] focus:outline-none focus:border-[#ffb800] resize-none"
className="w-full bg-[#0d1117] border border-[#1f2937] text-white text-sm rounded-lg px-3 py-2.5 placeholder-[#444] focus:outline-none focus:border-[#F0A623] resize-none"
/>
</div>
<div className="flex items-center gap-3 pt-1">

View File

@@ -31,14 +31,14 @@ interface Invitation {
type Tab = 'users' | 'invitations';
const ROLE_OPTIONS = [
{ value: 'reader', label: 'Reader', color: 'text-[#888] bg-[#1a1a1a]' },
{ value: 'reader', label: 'Reader', color: 'text-[#888] bg-[#231d18]' },
{ value: 'contributor', label: 'Contributor', color: 'text-blue-400 bg-blue-400/10' },
{ value: 'editor', label: 'Editor', color: 'text-purple-400 bg-purple-400/10' },
{ value: 'admin', label: 'Admin', color: 'text-red-400 bg-red-400/10' },
];
const ROLE_COLORS: Record<string, string> = {
reader: 'text-[#888] bg-[#1a1a1a]',
reader: 'text-[#888] bg-[#231d18]',
contributor: 'text-blue-400 bg-blue-400/10',
editor: 'text-purple-400 bg-purple-400/10',
admin: 'text-red-400 bg-red-400/10',
@@ -47,7 +47,7 @@ const ROLE_COLORS: Record<string, string> = {
const STATUS_COLORS: Record<string, string> = {
pending: 'text-yellow-400 bg-yellow-400/10',
accepted: 'text-green-400 bg-green-400/10',
revoked: 'text-[#555] bg-[#1a1a1a]',
revoked: 'text-[#555] bg-[#231d18]',
expired: 'text-orange-400 bg-orange-400/10',
email_failed: 'text-red-400 bg-red-400/10',
};
@@ -246,7 +246,7 @@ export default function AdminUsersPage() {
if (loading) {
return (
<div className="min-h-screen bg-[#0d1117] flex items-center justify-center">
<div className="w-6 h-6 border-2 border-[#ffb800] border-t-transparent rounded-full animate-spin" />
<div className="w-6 h-6 border-2 border-[#F0A623] border-t-transparent rounded-full animate-spin" />
</div>
);
}
@@ -276,7 +276,7 @@ export default function AdminUsersPage() {
</div>
<button
onClick={() => setShowInviteModal(true)}
className="flex items-center gap-2 bg-[#ffb800] hover:bg-[#d99700] text-white text-sm font-semibold px-4 py-2 rounded-lg transition-colors"
className="flex items-center gap-2 bg-[#F0A623] hover:bg-[#BA7517] text-white text-sm font-semibold px-4 py-2 rounded-lg transition-colors"
>
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" aria-hidden="true">
<path d="M22 16.92v3a2 2 0 0 1-2.18 2 19.79 19.79 0 0 1-8.63-3.07A19.5 19.5 0 0 1 4.69 12 19.79 19.79 0 0 1 1.61 3.41 2 2 0 0 1 3.6 1.22h3a2 2 0 0 1 2 1.72c.127.96.361 1.903.7 2.81a2 2 0 0 1-.45 2.11L7.91 8.96a16 16 0 0 0 6 6l.96-.96a2 2 0 0 1 2.11-.45c.907.339 1.85.573 2.81.7A2 2 0 0 1 21.5 16.5z" />
@@ -306,7 +306,7 @@ export default function AdminUsersPage() {
onClick={() => setActiveTab(tab)}
className={`px-4 py-2.5 text-sm font-semibold capitalize transition-colors border-b-2 -mb-px ${
activeTab === tab
? 'text-[#ffb800] border-[#ffb800]'
? 'text-[#F0A623] border-[#F0A623]'
: 'text-[#555] border-transparent hover:text-[#888]'
}`}
>
@@ -331,14 +331,14 @@ export default function AdminUsersPage() {
placeholder={activeTab === 'users' ? 'Search users...' : 'Search by email...'}
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="w-full bg-[#111827] border border-[#1f2937] text-white text-sm rounded-lg pl-9 pr-3 py-2 placeholder-[#444] focus:outline-none focus:border-[#ffb800]"
className="w-full bg-[#111827] border border-[#1f2937] text-white text-sm rounded-lg pl-9 pr-3 py-2 placeholder-[#444] focus:outline-none focus:border-[#F0A623]"
/>
</div>
{activeTab === 'users' && (
<select
value={roleFilter}
onChange={(e) => setRoleFilter(e.target.value)}
className="bg-[#111827] border border-[#1f2937] text-[#ccc] text-sm rounded-lg px-3 py-2 focus:outline-none focus:border-[#ffb800]"
className="bg-[#111827] border border-[#1f2937] text-[#ccc] text-sm rounded-lg px-3 py-2 focus:outline-none focus:border-[#F0A623]"
>
<option value="all">All Roles</option>
{ROLE_OPTIONS.map((r) => (
@@ -353,7 +353,7 @@ export default function AdminUsersPage() {
<div className="bg-[#111827] border border-[#1f2937] rounded-xl overflow-hidden">
{loadingData ? (
<div className="flex items-center justify-center py-16">
<div className="w-6 h-6 border-2 border-[#ffb800] border-t-transparent rounded-full animate-spin" />
<div className="w-6 h-6 border-2 border-[#F0A623] border-t-transparent rounded-full animate-spin" />
</div>
) : filteredUsers.length === 0 ? (
<div className="text-center py-16 text-[#555]">
@@ -379,7 +379,7 @@ export default function AdminUsersPage() {
<tr key={u.id} className="border-b border-[#1a2030] hover:bg-[#0d1117]/50 transition-colors">
<td className="px-5 py-3.5">
<div className="flex items-center gap-3">
<div className="w-8 h-8 rounded-full bg-[#1f2937] flex items-center justify-center text-[#ffb800] font-bold text-sm flex-shrink-0 overflow-hidden">
<div className="w-8 h-8 rounded-full bg-[#1f2937] flex items-center justify-center text-[#F0A623] font-bold text-sm flex-shrink-0 overflow-hidden">
{u.avatar_url ? (
<img src={u.avatar_url} alt={u.full_name || u.email} className="w-full h-full object-cover" />
) : (
@@ -393,7 +393,7 @@ export default function AdminUsersPage() {
</div>
</td>
<td className="px-4 py-3.5">
<span className={`inline-flex items-center px-2 py-0.5 rounded text-xs font-semibold uppercase tracking-wide ${ROLE_COLORS[u.role] || 'text-[#888] bg-[#1a1a1a]'}`}>
<span className={`inline-flex items-center px-2 py-0.5 rounded text-xs font-semibold uppercase tracking-wide ${ROLE_COLORS[u.role] || 'text-[#888] bg-[#231d18]'}`}>
{u.role}
</span>
</td>
@@ -418,7 +418,7 @@ export default function AdminUsersPage() {
<button
onClick={() => openEditRole(u)}
disabled={processingIds.has(u.id)}
className="text-[#555] hover:text-[#ffb800] transition-colors text-xs font-medium px-2 py-1 rounded hover:bg-[#1f2937] disabled:opacity-50"
className="text-[#555] hover:text-[#F0A623] transition-colors text-xs font-medium px-2 py-1 rounded hover:bg-[#1f2937] disabled:opacity-50"
>
Edit Role
</button>
@@ -455,7 +455,7 @@ export default function AdminUsersPage() {
<div className="bg-[#111827] border border-[#1f2937] rounded-xl overflow-hidden">
{loadingData ? (
<div className="flex items-center justify-center py-16">
<div className="w-6 h-6 border-2 border-[#ffb800] border-t-transparent rounded-full animate-spin" />
<div className="w-6 h-6 border-2 border-[#F0A623] border-t-transparent rounded-full animate-spin" />
</div>
) : filteredInvitations.length === 0 ? (
<div className="text-center py-16 text-[#555]">
@@ -465,7 +465,7 @@ export default function AdminUsersPage() {
<p className="text-sm">No invitations sent yet</p>
<button
onClick={() => setShowInviteModal(true)}
className="mt-3 text-[#ffb800] hover:text-[#ffd25a] text-sm transition-colors"
className="mt-3 text-[#F0A623] hover:text-[#F0A623] text-sm transition-colors"
>
Send your first invitation
</button>
@@ -491,19 +491,19 @@ export default function AdminUsersPage() {
<tr key={inv.id} className="border-b border-[#1a2030] hover:bg-[#0d1117]/50 transition-colors">
<td className="px-5 py-3.5">
<div className="flex items-center gap-2">
<div className="w-7 h-7 rounded-full bg-[#1f2937] flex items-center justify-center text-[#ffb800] text-xs font-bold flex-shrink-0">
<div className="w-7 h-7 rounded-full bg-[#1f2937] flex items-center justify-center text-[#F0A623] text-xs font-bold flex-shrink-0">
{inv.email[0].toUpperCase()}
</div>
<span className="text-white text-sm">{inv.email}</span>
</div>
</td>
<td className="px-4 py-3.5">
<span className={`inline-flex items-center px-2 py-0.5 rounded text-xs font-semibold uppercase tracking-wide ${ROLE_COLORS[inv.role] || 'text-[#888] bg-[#1a1a1a]'}`}>
<span className={`inline-flex items-center px-2 py-0.5 rounded text-xs font-semibold uppercase tracking-wide ${ROLE_COLORS[inv.role] || 'text-[#888] bg-[#231d18]'}`}>
{inv.role}
</span>
</td>
<td className="px-4 py-3.5">
<span className={`inline-flex items-center px-2 py-0.5 rounded text-xs font-semibold capitalize ${STATUS_COLORS[displayStatus] || 'text-[#888] bg-[#1a1a1a]'}`}>
<span className={`inline-flex items-center px-2 py-0.5 rounded text-xs font-semibold capitalize ${STATUS_COLORS[displayStatus] || 'text-[#888] bg-[#231d18]'}`}>
{displayStatus.replace('_', ' ')}
</span>
</td>
@@ -564,7 +564,7 @@ export default function AdminUsersPage() {
value={inviteEmail}
onChange={(e) => setInviteEmail(e.target.value)}
placeholder="contributor@example.com"
className="w-full bg-[#0d1117] border border-[#1f2937] text-white text-sm rounded-lg px-3 py-2.5 placeholder-[#444] focus:outline-none focus:border-[#ffb800]"
className="w-full bg-[#0d1117] border border-[#1f2937] text-white text-sm rounded-lg px-3 py-2.5 placeholder-[#444] focus:outline-none focus:border-[#F0A623]"
/>
</div>
<div>
@@ -574,7 +574,7 @@ export default function AdminUsersPage() {
<select
value={inviteRole}
onChange={(e) => setInviteRole(e.target.value)}
className="w-full bg-[#0d1117] border border-[#1f2937] text-white text-sm rounded-lg px-3 py-2.5 focus:outline-none focus:border-[#ffb800]"
className="w-full bg-[#0d1117] border border-[#1f2937] text-white text-sm rounded-lg px-3 py-2.5 focus:outline-none focus:border-[#F0A623]"
>
<option value="contributor">Contributor can submit articles for review</option>
<option value="editor">Editor can publish and manage articles</option>
@@ -590,7 +590,7 @@ export default function AdminUsersPage() {
onChange={(e) => setInviteMessage(e.target.value)}
placeholder="Add a personal note to the invitation email..."
rows={3}
className="w-full bg-[#0d1117] border border-[#1f2937] text-white text-sm rounded-lg px-3 py-2.5 placeholder-[#444] focus:outline-none focus:border-[#ffb800] resize-none"
className="w-full bg-[#0d1117] border border-[#1f2937] text-white text-sm rounded-lg px-3 py-2.5 placeholder-[#444] focus:outline-none focus:border-[#F0A623] resize-none"
/>
</div>
<div className="bg-[#0d1117] border border-[#1f2937] rounded-lg p-3">
@@ -609,7 +609,7 @@ export default function AdminUsersPage() {
<button
type="submit"
disabled={inviteSending || !inviteEmail.trim()}
className="flex-1 bg-[#ffb800] hover:bg-[#d99700] disabled:opacity-50 disabled:cursor-not-allowed text-white text-sm font-semibold py-2.5 rounded-lg transition-colors flex items-center justify-center gap-2"
className="flex-1 bg-[#F0A623] hover:bg-[#BA7517] disabled:opacity-50 disabled:cursor-not-allowed text-white text-sm font-semibold py-2.5 rounded-lg transition-colors flex items-center justify-center gap-2"
>
{inviteSending ? (
<>
@@ -644,7 +644,7 @@ export default function AdminUsersPage() {
</div>
<div className="px-6 py-5 space-y-4">
<div className="flex items-center gap-3 bg-[#0d1117] rounded-lg p-3">
<div className="w-9 h-9 rounded-full bg-[#1f2937] flex items-center justify-center text-[#ffb800] font-bold text-sm flex-shrink-0 overflow-hidden">
<div className="w-9 h-9 rounded-full bg-[#1f2937] flex items-center justify-center text-[#F0A623] font-bold text-sm flex-shrink-0 overflow-hidden">
{editingUser.avatar_url ? (
<img src={editingUser.avatar_url} alt={editingUser.full_name || editingUser.email} className="w-full h-full object-cover" />
) : (
@@ -666,7 +666,7 @@ export default function AdminUsersPage() {
key={r.value}
className={`flex items-center gap-3 p-3 rounded-lg border cursor-pointer transition-colors ${
editRole === r.value
? 'border-[#ffb800] bg-[#ffb800]/10'
? 'border-[#F0A623] bg-[#F0A623]/10'
: 'border-[#1f2937] hover:border-[#2a3a50]'
}`}
>
@@ -679,9 +679,9 @@ export default function AdminUsersPage() {
className="sr-only"
/>
<span className={`w-3.5 h-3.5 rounded-full border-2 flex items-center justify-center flex-shrink-0 ${
editRole === r.value ? 'border-[#ffb800]' : 'border-[#444]'
editRole === r.value ? 'border-[#F0A623]' : 'border-[#444]'
}`}>
{editRole === r.value && <span className="w-1.5 h-1.5 rounded-full bg-[#ffb800]" />}
{editRole === r.value && <span className="w-1.5 h-1.5 rounded-full bg-[#F0A623]" />}
</span>
<span className={`text-xs font-semibold uppercase tracking-wide px-2 py-0.5 rounded ${r.color}`}>
{r.label}
@@ -700,7 +700,7 @@ export default function AdminUsersPage() {
<button
onClick={handleEditSave}
disabled={editSaving || editRole === editingUser.role}
className="flex-1 bg-[#ffb800] hover:bg-[#d99700] disabled:opacity-50 disabled:cursor-not-allowed text-white text-sm font-semibold py-2.5 rounded-lg transition-colors flex items-center justify-center gap-2"
className="flex-1 bg-[#F0A623] hover:bg-[#BA7517] disabled:opacity-50 disabled:cursor-not-allowed text-white text-sm font-semibold py-2.5 rounded-lg transition-colors flex items-center justify-center gap-2"
>
{editSaving ? (
<>

View File

@@ -223,7 +223,7 @@ export default function WPUsersAdminPage() {
</thead>
<tbody>
{filteredUsers.map(u => (
<tr key={u.id} className="border-b border-[#1a1a1a] hover:bg-[#151515] transition-colors">
<tr key={u.id} className="border-b border-[#231d18] hover:bg-[#151515] transition-colors">
<td className="px-4 py-3 text-[#555] font-mono text-xs">{u.wp_id}</td>
<td className="px-4 py-3">
<span className="text-white font-medium">{u.wp_username}</span>