AV launch: global color scheme to match logo — gold #ffb800 primary, red #d60701 secondary, warm dark bars (1a1208 / 161310) replacing the inherited BB blue palette. Swapped 21 root tokens + 112 component files.
This commit is contained in:
@@ -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-[#3b82f6] 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-[#ffb800] 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-[#3b82f6] 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-[#ffb800] rounded text-xs font-medium hover:bg-blue-500 transition-colors">+ New Client</button>
|
||||
</div>
|
||||
|
||||
{/* Success Toast */}
|
||||
@@ -116,9 +116,9 @@ 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-[#3b82f6] 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-[#ffb800] text-white' : 'bg-[#111] border border-[#252525] 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-[#3b82f6]" />
|
||||
<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]" />
|
||||
</div>
|
||||
|
||||
{/* New Client 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-[#3b82f6] ${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-[#ffb800] ${errors[f.key] ? 'border-red-500/60' : 'border-[#252525]'}`}
|
||||
/>
|
||||
{errors[f.key] && <p className="mt-1 text-xs text-red-400">{errors[f.key]}</p>}
|
||||
</div>
|
||||
@@ -156,16 +156,16 @@ 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-[#3b82f6]" />
|
||||
className="w-full px-2 py-1.5 bg-[#0a0a0a] border border-[#252525] rounded text-xs text-white focus:outline-none focus:border-[#ffb800]" />
|
||||
</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-[#3b82f6]" />
|
||||
className="w-full px-2 py-1.5 bg-[#0a0a0a] border border-[#252525] rounded text-xs text-white focus:outline-none focus:border-[#ffb800]" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<button type="submit" disabled={saving} className="px-3 py-1.5 bg-[#3b82f6] rounded text-xs font-medium hover:bg-blue-500 disabled:opacity-50">{saving ? 'Saving…' : 'Create Client'}</button>
|
||||
<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>
|
||||
</div>
|
||||
</form>
|
||||
@@ -197,7 +197,7 @@ export default function ClientsPage() {
|
||||
<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>
|
||||
</td>
|
||||
<td className="px-4 py-2">
|
||||
<Link href={`/admin/accounting/clients/${c.id}`} className="text-xs text-[#3b82f6] hover:underline">View</Link>
|
||||
<Link href={`/admin/accounting/clients/${c.id}`} className="text-xs text-[#ffb800] hover:underline">View</Link>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
|
||||
@@ -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-[#3b82f6] 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-[#ffb800] border-t-transparent rounded-full animate-spin" /></div>;
|
||||
if (!user) return null;
|
||||
|
||||
return (
|
||||
@@ -84,7 +84,7 @@ export default function CommissionsPage() {
|
||||
<span>Tier progress</span><span>{pct}%</span>
|
||||
</div>
|
||||
<div className="h-1.5 bg-[#1a1a1a] rounded-full overflow-hidden">
|
||||
<div className="h-full bg-[#3b82f6] rounded-full transition-all" style={{ width: `${pct}%` }} />
|
||||
<div className="h-full bg-[#ffb800] rounded-full transition-all" style={{ width: `${pct}%` }} />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
@@ -111,7 +111,7 @@ export default function CommissionsPage() {
|
||||
) : commissions.map((c: any) => (
|
||||
<tr key={c.id} className="border-b border-[#1a1a1a] hover:bg-[#161616]">
|
||||
<td className="px-3 py-2 text-white text-xs">{c.rmp_sales_staff?.full_name ?? '—'}</td>
|
||||
<td className="px-3 py-2 text-[#3b82f6] text-xs font-mono">{c.rmp_invoices?.invoice_number ?? '—'}</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-[#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>
|
||||
|
||||
@@ -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-[#3b82f6] 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-[#ffb800] border-t-transparent rounded-full animate-spin" /></div>;
|
||||
if (!user) return null;
|
||||
|
||||
return (
|
||||
@@ -54,14 +54,14 @@ 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-[#3b82f6] rounded text-xs font-medium hover:bg-blue-500">+ Upload Document</button>
|
||||
<button onClick={() => setShowForm(true)} className="px-3 py-1.5 bg-[#ffb800] 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-[#3b82f6] 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-[#ffb800] text-white' : 'bg-[#111] border border-[#252525] 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-[#3b82f6] 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-[#ffb800] text-white' : 'bg-[#111] border border-[#252525] text-[#888]'}`}>{y}</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -84,31 +84,31 @@ export default function DocumentsPage() {
|
||||
</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-[#3b82f6]" />
|
||||
<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]" />
|
||||
</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-[#3b82f6]" />
|
||||
<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]" />
|
||||
</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-[#3b82f6]" />
|
||||
<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]" />
|
||||
</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-[#3b82f6]" />
|
||||
<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]" />
|
||||
</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-[#3b82f6]" />
|
||||
<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]" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<button type="submit" disabled={saving} className="px-3 py-1.5 bg-[#3b82f6] rounded text-xs font-medium hover:bg-blue-500 disabled:opacity-50">{saving ? 'Saving…' : 'Save Document'}</button>
|
||||
<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>
|
||||
</div>
|
||||
</form>
|
||||
@@ -136,7 +136,7 @@ export default function DocumentsPage() {
|
||||
<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-[#3b82f6] hover:underline">Download</a>}
|
||||
{d.file_url && <a href={d.file_url} target="_blank" rel="noopener noreferrer" className="text-xs text-[#ffb800] hover:underline">Download</a>}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
|
||||
@@ -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-[#3b82f6] 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-[#ffb800] 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-[#3b82f6] rounded text-xs font-medium hover:bg-blue-500">+ Log Expense</button>
|
||||
<button onClick={() => setShowForm(true)} className="px-3 py-1.5 bg-[#ffb800] rounded text-xs font-medium hover:bg-blue-500">+ Log Expense</button>
|
||||
</div>
|
||||
|
||||
{/* Success Toast */}
|
||||
@@ -132,16 +132,16 @@ 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-[#3b82f6]" />
|
||||
<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]" />
|
||||
</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-[#3b82f6] ${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-[#ffb800] ${errors.amount ? 'border-red-500/60' : 'border-[#252525]'}`} />
|
||||
{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-[#3b82f6] ${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-[#ffb800] ${errors.expense_date ? 'border-red-500/60' : 'border-[#252525]'}`} />
|
||||
{errors.expense_date && <p className="mt-1 text-xs text-red-400">{errors.expense_date}</p>}
|
||||
</div>
|
||||
<div>
|
||||
@@ -152,11 +152,11 @@ export default function ExpensesPage() {
|
||||
</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-[#3b82f6]" />
|
||||
<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]" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<button type="submit" disabled={saving} className="px-3 py-1.5 bg-[#3b82f6] rounded text-xs font-medium hover:bg-blue-500 disabled:opacity-50">{saving ? 'Saving…' : 'Log Expense'}</button>
|
||||
<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>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
@@ -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-[#3b82f6] 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-[#ffb800] 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-[#3b82f6] 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-[#ffb800] rounded text-xs font-medium hover:bg-blue-500">+ New Invoice (via Order)</Link>
|
||||
</div>
|
||||
|
||||
{/* Success Toast */}
|
||||
@@ -108,7 +108,7 @@ 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-[#3b82f6] 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-[#ffb800] text-white' : 'bg-[#111] border border-[#252525] text-[#888] hover:text-white'}`}>{s || 'All'}</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -168,7 +168,7 @@ function InvoicesContent() {
|
||||
<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-[#3b82f6] font-mono text-xs">{inv.invoice_number}</td>
|
||||
<td className="px-3 py-2 text-[#ffb800] 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>
|
||||
@@ -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-[#3b82f6] 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-[#ffb800] border-t-transparent rounded-full animate-spin" /></div>}>
|
||||
<InvoicesContent />
|
||||
</Suspense>
|
||||
);
|
||||
|
||||
@@ -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-[#3b82f6] 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-[#ffb800] 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-[#3b82f6] rounded text-xs font-medium hover:bg-blue-500">+ New Order</button>
|
||||
<button onClick={() => setShowForm(true)} className="px-3 py-1.5 bg-[#ffb800] rounded text-xs font-medium hover:bg-blue-500">+ New Order</button>
|
||||
</div>
|
||||
|
||||
{/* Success Toast */}
|
||||
@@ -136,7 +136,7 @@ function OrdersContent() {
|
||||
<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-[#3b82f6]" />
|
||||
<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]" />
|
||||
</div>
|
||||
|
||||
{/* New Order 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-[#3b82f6] ${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-[#ffb800] ${errors.client_id ? 'border-red-500/60' : 'border-[#252525]'}`}>
|
||||
<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-[#3b82f6] ${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-[#ffb800] ${errors.site ? 'border-red-500/60' : 'border-[#252525]'}`}>
|
||||
<option value="">Select site…</option>
|
||||
{SITES.map(s => <option key={s} value={s}>{s}</option>)}
|
||||
</select>
|
||||
@@ -170,57 +170,57 @@ 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-[#3b82f6]">
|
||||
<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]">
|
||||
<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-[#3b82f6]" />
|
||||
<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]" />
|
||||
</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-[#3b82f6]" />
|
||||
<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]" />
|
||||
</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-[#3b82f6] ${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-[#ffb800] ${errors.end_date ? 'border-red-500/60' : 'border-[#252525]'}`} />
|
||||
{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-[#3b82f6] ${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-[#ffb800] ${errors.total_amount_cents ? 'border-red-500/60' : 'border-[#252525]'}`} />
|
||||
{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-[#3b82f6]">
|
||||
<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]">
|
||||
{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-[#3b82f6]" />
|
||||
<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]" />
|
||||
</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-[#3b82f6]">
|
||||
<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]">
|
||||
<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-[#3b82f6]" />
|
||||
<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]" />
|
||||
</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-[#3b82f6]" />
|
||||
<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]" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<button type="submit" disabled={saving} className="px-3 py-1.5 bg-[#3b82f6] rounded text-xs font-medium hover:bg-blue-500 disabled:opacity-50">{saving ? 'Saving…' : 'Create Order'}</button>
|
||||
<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>
|
||||
</div>
|
||||
</form>
|
||||
@@ -245,7 +245,7 @@ function OrdersContent() {
|
||||
<tr key={o.id} className="border-b border-[#1a1a1a] hover:bg-[#161616]">
|
||||
<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-[#3b82f6] text-xs font-mono">{o.internal_order_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-[#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>
|
||||
@@ -255,7 +255,7 @@ function OrdersContent() {
|
||||
<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>
|
||||
</td>
|
||||
<td className="px-3 py-2">
|
||||
<Link href={`/admin/accounting/orders/${o.id}`} className="text-xs text-[#3b82f6] hover:underline">View</Link>
|
||||
<Link href={`/admin/accounting/orders/${o.id}`} className="text-xs text-[#ffb800] 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-[#3b82f6] 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-[#ffb800] border-t-transparent rounded-full animate-spin" /></div>}>
|
||||
<OrdersContent />
|
||||
</Suspense>
|
||||
);
|
||||
|
||||
@@ -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-[#3b82f6] border-t-transparent rounded-full animate-spin" />
|
||||
<div className="w-8 h-8 border-2 border-[#ffb800] border-t-transparent rounded-full animate-spin" />
|
||||
</div>
|
||||
);
|
||||
if (!user) return null;
|
||||
@@ -59,11 +59,11 @@ 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-[#1a2535] border border-[#2a3a50] rounded text-xs text-[#3b82f6] hover:bg-[#0d1a2d] transition-colors">+ New Client</Link>
|
||||
<Link href="/admin/accounting/orders" className="px-3 py-1.5 bg-[#1a2535] border border-[#2a3a50] rounded text-xs text-[#3b82f6] hover:bg-[#0d1a2d] transition-colors">+ New Order</Link>
|
||||
<Link href="/admin/accounting/invoices" className="px-3 py-1.5 bg-[#1a2535] border border-[#2a3a50] rounded text-xs text-[#3b82f6] hover:bg-[#0d1a2d] transition-colors">+ New Invoice</Link>
|
||||
<Link href="/admin/accounting/expenses" className="px-3 py-1.5 bg-[#1a2535] border border-[#2a3a50] rounded text-xs text-[#3b82f6] hover:bg-[#0d1a2d] transition-colors">+ Log Expense</Link>
|
||||
<Link href="/admin/accounting/reconciliation" className="px-3 py-1.5 bg-[#1a2535] border border-[#2a3a50] rounded text-xs text-[#3b82f6] hover:bg-[#0d1a2d] transition-colors">Upload Bank CSV</Link>
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -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-[#3b82f6] transition-colors">
|
||||
<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">
|
||||
{l.label}
|
||||
</Link>
|
||||
))}
|
||||
|
||||
@@ -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-[#3b82f6] 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-[#ffb800] 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-[#3b82f6] rounded text-xs font-medium hover:bg-blue-500 cursor-pointer ${uploading ? 'opacity-50' : ''}`}>
|
||||
<label className={`px-3 py-1.5 bg-[#ffb800] 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>
|
||||
|
||||
@@ -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-[#3b82f6] 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-[#ffb800] border-t-transparent rounded-full animate-spin" /></div>;
|
||||
if (!user) return null;
|
||||
|
||||
return (
|
||||
@@ -43,12 +43,12 @@ export default function ReportsPage() {
|
||||
<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" />
|
||||
<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-[#3b82f6] rounded text-xs font-medium hover:bg-blue-500 disabled:opacity-50">Run Report</button>
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{loadingData ? (
|
||||
<div className="flex items-center justify-center py-20"><div className="w-8 h-8 border-2 border-[#3b82f6] 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-[#ffb800] border-t-transparent rounded-full animate-spin" /></div>
|
||||
) : report ? (
|
||||
<div className="space-y-4">
|
||||
{/* P&L Summary */}
|
||||
|
||||
@@ -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-[#3b82f6] 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-[#ffb800] 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-[#3b82f6] rounded text-xs font-medium hover:bg-blue-500">+ Add Salesperson</button>
|
||||
<button onClick={() => setShowForm(true)} className="px-3 py-1.5 bg-[#ffb800] rounded text-xs font-medium hover:bg-blue-500">+ Add Salesperson</button>
|
||||
</div>
|
||||
|
||||
{/* Success Toast */}
|
||||
@@ -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-[#3b82f6] ${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-[#ffb800] ${errors[f.key] ? 'border-red-500/60' : 'border-[#252525]'}`}
|
||||
/>
|
||||
{errors[f.key] && <p className="mt-1 text-xs text-red-400">{errors[f.key]}</p>}
|
||||
</div>
|
||||
@@ -149,7 +149,7 @@ 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-[#3b82f6] hover:underline">+ Add Tier</button>
|
||||
<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>
|
||||
</div>
|
||||
{tiers.map((tier, i) => (
|
||||
<div key={i} className="flex gap-2 mb-2 items-start">
|
||||
@@ -167,7 +167,7 @@ export default function StaffPage() {
|
||||
))}
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<button type="submit" disabled={saving} className="px-3 py-1.5 bg-[#3b82f6] rounded text-xs font-medium hover:bg-blue-500 disabled:opacity-50">{saving ? 'Saving…' : 'Create'}</button>
|
||||
<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>
|
||||
</div>
|
||||
</form>
|
||||
@@ -197,7 +197,7 @@ export default function StaffPage() {
|
||||
<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-[#3b82f6] hover:underline">View</Link>
|
||||
<Link href={`/admin/accounting/staff/${s.id}`} className="text-xs text-[#ffb800] hover:underline">View</Link>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
|
||||
@@ -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-[#3b82f6] 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-[#ffb800] border-t-transparent rounded-full animate-spin" /></div>;
|
||||
if (!user) return null;
|
||||
|
||||
const selectedProduct = products.find((p: any) => p.id === form.product_id);
|
||||
@@ -88,7 +88,7 @@ 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-[#3b82f6] rounded text-xs font-medium hover:bg-blue-500">+ New IO Extension</button>
|
||||
<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>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-2 flex-wrap">
|
||||
@@ -108,7 +108,7 @@ export default function AdOpsIOsPage() {
|
||||
<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-[#3b82f6]" />
|
||||
<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]" />
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-xs text-[#888] block mb-1">Client</label>
|
||||
@@ -140,11 +140,11 @@ export default function AdOpsIOsPage() {
|
||||
</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-[#3b82f6]" />
|
||||
<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]" />
|
||||
</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-[#3b82f6]" />
|
||||
<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]" />
|
||||
</div>
|
||||
<div className="flex items-center gap-3 pt-4">
|
||||
<label className="flex items-center gap-2 text-xs text-[#888] cursor-pointer">
|
||||
@@ -159,14 +159,14 @@ 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-[#3b82f6]" />
|
||||
<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]" />
|
||||
</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-[#3b82f6]" />
|
||||
<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]" />
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-xs text-[#888] block mb-1">Scheduled Send Date</label>
|
||||
@@ -174,23 +174,23 @@ export default function AdOpsIOsPage() {
|
||||
</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-[#3b82f6]" />
|
||||
<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]" />
|
||||
</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-[#3b82f6]" />
|
||||
<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]" />
|
||||
</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-[#3b82f6]" />
|
||||
<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]" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<button type="submit" disabled={saving} className="px-3 py-1.5 bg-[#3b82f6] rounded text-xs font-medium hover:bg-blue-500 disabled:opacity-50">{saving ? 'Saving…' : 'Create IO Extension'}</button>
|
||||
<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>
|
||||
</div>
|
||||
</form>
|
||||
@@ -212,7 +212,7 @@ export default function AdOpsIOsPage() {
|
||||
<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-[#3b82f6] font-mono text-xs">{io.existing_io_id}</td>
|
||||
<td className="px-3 py-2 text-[#ffb800] 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-[#3b82f6] hover:underline">View</Link>
|
||||
<Link href={`/admin/adops/ios/${io.id}`} className="text-xs text-[#ffb800] 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>
|
||||
)}
|
||||
|
||||
@@ -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-[#3b82f6] 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-[#ffb800] border-t-transparent rounded-full animate-spin" /></div>;
|
||||
if (!user) return null;
|
||||
|
||||
return (
|
||||
@@ -50,7 +50,7 @@ export default function AdOpsNotificationsPage() {
|
||||
) : notifications.map((n: any) => (
|
||||
<tr key={n.id} className="border-b border-[#1a1a1a] hover:bg-[#161616]">
|
||||
<td className="px-3 py-2 text-[#888] text-xs">{n.notification_type?.replace(/_/g, ' ')}</td>
|
||||
<td className="px-3 py-2 text-[#3b82f6] font-mono text-xs">{n.io_extension_id?.slice(0, 8) ?? '—'}</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-[#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>
|
||||
|
||||
@@ -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-[#3b82f6] 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-[#ffb800] border-t-transparent rounded-full animate-spin" /></div>;
|
||||
if (!user) return null;
|
||||
|
||||
return (
|
||||
@@ -87,7 +87,7 @@ export default function AdOpsDashboardPage() {
|
||||
<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">
|
||||
<h2 className="text-sm font-semibold">Active Flights</h2>
|
||||
<Link href="/admin/adops/ios" className="text-xs text-[#3b82f6] hover:underline">View All IOs →</Link>
|
||||
<Link href="/admin/adops/ios" className="text-xs text-[#ffb800] hover:underline">View All IOs →</Link>
|
||||
</div>
|
||||
<table className="w-full text-sm">
|
||||
<thead>
|
||||
@@ -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-[#3b82f6] hover:underline">View</Link>
|
||||
<Link href={`/admin/adops/ios/${f.id}`} className="text-xs text-[#ffb800] hover:underline">View</Link>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
@@ -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-[#3b82f6] hover:underline">Preview</a>}
|
||||
{e.email_html_url && <a href={e.email_html_url} target="_blank" rel="noopener noreferrer" className="text-xs text-[#ffb800] hover:underline">Preview</a>}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
|
||||
@@ -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-[#3b82f6] 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-[#ffb800] border-t-transparent rounded-full animate-spin" /></div>;
|
||||
if (!user) return null;
|
||||
|
||||
return (
|
||||
@@ -60,7 +60,7 @@ 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-[#3b82f6] rounded text-xs font-medium hover:bg-blue-500">+ Add Product</button>
|
||||
<button onClick={() => setShowForm(true)} className="px-3 py-1.5 bg-[#ffb800] rounded text-xs font-medium hover:bg-blue-500">+ Add Product</button>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-2">
|
||||
@@ -90,23 +90,23 @@ export default function AdOpsProductsPage() {
|
||||
</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-[#3b82f6]" />
|
||||
<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]" />
|
||||
</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-[#3b82f6]" />
|
||||
<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]" />
|
||||
</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-[#3b82f6]" />
|
||||
<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]" />
|
||||
</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-[#3b82f6]" />
|
||||
<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]" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<button type="submit" disabled={saving} className="px-3 py-1.5 bg-[#3b82f6] rounded text-xs font-medium hover:bg-blue-500 disabled:opacity-50">{saving ? 'Saving…' : 'Add Product'}</button>
|
||||
<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>
|
||||
</div>
|
||||
</form>
|
||||
@@ -137,7 +137,7 @@ export default function AdOpsProductsPage() {
|
||||
<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>
|
||||
</td>
|
||||
<td className="px-3 py-2">
|
||||
<button onClick={() => toggleActive(p.id, p.active)} className="text-xs text-[#3b82f6] hover:underline">{p.active ? 'Deactivate' : 'Activate'}</button>
|
||||
<button onClick={() => toggleActive(p.id, p.active)} className="text-xs text-[#ffb800] hover:underline">{p.active ? 'Deactivate' : 'Activate'}</button>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
|
||||
@@ -64,7 +64,7 @@ interface MonthlyTrend {
|
||||
// ─── Mock / derived data helpers ──────────────────────────────────────────────
|
||||
|
||||
const CATEGORY_COLORS = [
|
||||
'#3b82f6', '#f59e0b', '#10b981', '#8b5cf6',
|
||||
'#ffb800', '#f59e0b', '#10b981', '#8b5cf6',
|
||||
'#ef4444', '#06b6d4', '#f97316', '#84cc16',
|
||||
];
|
||||
|
||||
@@ -78,7 +78,7 @@ interface StatCardProps {
|
||||
icon: React.ReactNode;
|
||||
}
|
||||
|
||||
function StatCard({ label, value, sub, accent = '#3b82f6', icon }: StatCardProps) {
|
||||
function StatCard({ label, value, sub, accent = '#ffb800', icon }: StatCardProps) {
|
||||
return (
|
||||
<div className="bg-[#111] border border-[#252525] rounded-lg p-5 flex items-start gap-4">
|
||||
<div
|
||||
@@ -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-[#3b82f6] border-t-transparent rounded-full animate-spin" />
|
||||
<div className="w-8 h-8 border-2 border-[#ffb800] border-t-transparent rounded-full animate-spin" />
|
||||
<p className="text-[#555] text-sm font-body">Loading analytics…</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -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-[#3b82f6] text-xs font-body uppercase tracking-wider transition-colors">
|
||||
<Link href="/admin/articles" className="text-[#555] hover:text-[#ffb800] 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-[#3b82f6] text-xs font-body uppercase tracking-wider transition-colors">
|
||||
<Link href="/admin/users" className="text-[#555] hover:text-[#ffb800] 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-[#3b82f6] text-xs font-body uppercase tracking-wider transition-colors">
|
||||
<Link href="/admin/import" className="text-[#555] hover:text-[#ffb800] 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-[#3b82f6] text-[#888] hover:text-[#3b82f6] text-xs font-body rounded transition-colors"
|
||||
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"
|
||||
>
|
||||
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="#3b82f6"
|
||||
accent="#ffb800"
|
||||
/>
|
||||
<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="#3b82f6"
|
||||
accent="#ffb800"
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -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-[#3b82f6] border-t-transparent rounded-full animate-spin" />
|
||||
<div className="w-6 h-6 border-2 border-[#ffb800] border-t-transparent rounded-full animate-spin" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -409,7 +409,7 @@ export default function AdminArticlesPage() {
|
||||
<div className="bg-[#111111] border-b border-[#252525]">
|
||||
<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-[#3b82f6] transition-colors text-sm">Home</Link>
|
||||
<Link href="/home-page" className="text-[#555] hover:text-[#ffb800] 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-[#3b82f6] hover:bg-[#2563eb] rounded px-3 py-2 transition-colors">
|
||||
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">
|
||||
<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-[#3b82f6] border border-[#252525] hover:border-[#3b82f6] rounded px-3 py-2 transition-colors">
|
||||
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">
|
||||
<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>
|
||||
@@ -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-[#3b82f6] placeholder-[#444]"
|
||||
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]"
|
||||
/>
|
||||
</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-[#3b82f6]">
|
||||
className="bg-[#111] border border-[#252525] text-[#cccccc] text-sm rounded px-3 py-2 focus:outline-none focus:border-[#ffb800]">
|
||||
<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-[#1e3a5f] rounded-lg px-4 py-3">
|
||||
<span className="text-[#3b82f6] text-sm font-bold">{selectedIds.size} selected</span>
|
||||
<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-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-[#3b82f6]">
|
||||
className="bg-[#111] border border-[#333] text-[#cccccc] text-sm rounded px-3 py-1.5 focus:outline-none focus:border-[#ffb800]">
|
||||
<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-[#3b82f6] hover:bg-[#2563eb] disabled:bg-[#1e3a5f] 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-[#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">
|
||||
{bulkProcessing ? (
|
||||
<div className="w-3.5 h-3.5 border-2 border-white border-t-transparent rounded-full animate-spin" />
|
||||
) : null}
|
||||
@@ -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-[#3b82f6] bg-[#0d1a2e] px-1.5 py-0.5 rounded">{article.category}</span>
|
||||
<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 px-1.5 py-0.5 rounded ${article.source === 'imported' ? 'text-purple-400 bg-purple-400/10' : 'text-[#3b82f6] bg-[#3b82f6]/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-[#ffb800] bg-[#ffb800]/10'}`}>
|
||||
{article.source === 'imported' ? 'WP' : 'Native'}
|
||||
</span>
|
||||
</div>
|
||||
@@ -612,7 +612,7 @@ export default function AdminArticlesPage() {
|
||||
</button>
|
||||
<button
|
||||
onClick={() => openEdit(article)}
|
||||
className="p-1.5 text-[#555] hover:text-[#3b82f6] transition-colors rounded hover:bg-[#1a1a1a]"
|
||||
className="p-1.5 text-[#555] hover:text-[#ffb800] transition-colors rounded hover:bg-[#1a1a1a]"
|
||||
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" />
|
||||
@@ -634,7 +634,7 @@ export default function AdminArticlesPage() {
|
||||
type="checkbox"
|
||||
checked={articles.length > 0 && selectedIds.size === articles.length}
|
||||
onChange={toggleSelectAll}
|
||||
className="w-4 h-4 rounded border-[#333] bg-[#1a1a1a] accent-[#3b82f6] cursor-pointer"
|
||||
className="w-4 h-4 rounded border-[#333] bg-[#1a1a1a] accent-[#ffb800] 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-[#3b82f6] border-t-transparent rounded-full animate-spin" />
|
||||
<div className="w-6 h-6 border-2 border-[#ffb800] border-t-transparent rounded-full animate-spin" />
|
||||
</div>
|
||||
) : articles.length === 0 ? (
|
||||
<div className="text-center py-16">
|
||||
@@ -668,7 +668,7 @@ 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-[#3b82f6] cursor-pointer flex-shrink-0"
|
||||
className="w-4 h-4 rounded border-[#333] bg-[#1a1a1a] accent-[#ffb800] cursor-pointer flex-shrink-0"
|
||||
/>
|
||||
|
||||
{/* Thumbnail + Title */}
|
||||
@@ -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-[#3b82f6] bg-[#0d1a2e] px-1.5 py-0.5 rounded">
|
||||
<span className="text-[10px] font-bold uppercase tracking-wider text-[#ffb800] 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-[#3b82f6] bg-[#3b82f6]/10'
|
||||
article.source === 'imported' ? 'text-purple-400 bg-purple-400/10' : 'text-[#ffb800] bg-[#ffb800]/10'
|
||||
}`}>
|
||||
{article.source === 'imported' ? 'WP' : 'Native'}
|
||||
</span>
|
||||
@@ -741,7 +741,7 @@ export default function AdminArticlesPage() {
|
||||
<button
|
||||
onClick={() => openEdit(article)}
|
||||
title="Edit"
|
||||
className="p-1.5 text-[#555] hover:text-[#3b82f6] transition-colors rounded hover:bg-[#1a1a1a]">
|
||||
className="p-1.5 text-[#555] hover:text-[#ffb800] transition-colors rounded hover:bg-[#1a1a1a]">
|
||||
<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" />
|
||||
@@ -839,7 +839,7 @@ export default function AdminArticlesPage() {
|
||||
<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 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-[#3b82f6] bg-[#3b82f6]/10 px-1.5 py-0.5 rounded">Native</span>
|
||||
<span className="text-[10px] font-bold uppercase tracking-wider text-[#ffb800] bg-[#ffb800]/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-[#3b82f6] placeholder-[#444]"
|
||||
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]"
|
||||
/>
|
||||
</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-[#3b82f6] placeholder-[#444] resize-none"
|
||||
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"
|
||||
/>
|
||||
</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-[#3b82f6] placeholder-[#444]"
|
||||
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]"
|
||||
/>
|
||||
</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-[#3b82f6] placeholder-[#444]"
|
||||
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]"
|
||||
/>
|
||||
</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-[#3b82f6]">
|
||||
className="w-full bg-[#1a1a1a] border border-[#333] text-[#cccccc] text-sm rounded px-3 py-2 focus:outline-none focus:border-[#ffb800]">
|
||||
<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-[#3b82f6] placeholder-[#444]"
|
||||
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]"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
@@ -939,7 +939,7 @@ 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-[#3b82f6] placeholder-[#444]"
|
||||
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]"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -966,7 +966,7 @@ export default function AdminArticlesPage() {
|
||||
<button
|
||||
onClick={handleNewArticleSave}
|
||||
disabled={newSaving || !newTitle.trim()}
|
||||
className="flex items-center gap-2 bg-[#3b82f6] hover:bg-[#2563eb] disabled:bg-[#1e3a5f] disabled:cursor-not-allowed text-white text-sm font-bold px-5 py-2 rounded transition-colors">
|
||||
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">
|
||||
{newSaving ? (
|
||||
<div className="w-3.5 h-3.5 border-2 border-white border-t-transparent rounded-full animate-spin" />
|
||||
) : (
|
||||
@@ -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-[#3b82f6]"
|
||||
className="w-full bg-[#1a1a1a] border border-[#333] text-[#cccccc] text-sm rounded px-3 py-2 focus:outline-none focus:border-[#ffb800]"
|
||||
/>
|
||||
</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-[#3b82f6]"
|
||||
className="w-full bg-[#1a1a1a] border border-[#333] text-[#cccccc] text-sm rounded px-3 py-2 focus:outline-none focus:border-[#ffb800]"
|
||||
/>
|
||||
</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-[#3b82f6]"
|
||||
className="w-full bg-[#1a1a1a] border border-[#333] text-[#cccccc] text-sm rounded px-3 py-2 focus:outline-none focus:border-[#ffb800]"
|
||||
/>
|
||||
</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-[#3b82f6]">
|
||||
className="w-full bg-[#1a1a1a] border border-[#333] text-[#cccccc] text-sm rounded px-3 py-2 focus:outline-none focus:border-[#ffb800]">
|
||||
<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-[#1e3a5f] rounded-lg p-4">
|
||||
<div className="bg-[#0d1a2e] border border-[#4a3500] 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-[#3b82f6]">
|
||||
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" className="text-[#ffb800]">
|
||||
<circle cx="12" cy="12" r="10" /><polyline points="12 6 12 12 16 14" />
|
||||
</svg>
|
||||
<label className="text-xs text-[#3b82f6] font-bold uppercase tracking-wider">Schedule Publish</label>
|
||||
<label className="text-xs text-[#ffb800] 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-[#3b82f6] [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-[#ffb800] [color-scheme:dark]"
|
||||
/>
|
||||
{editScheduledAt && (
|
||||
<div className="flex items-center justify-between mt-2">
|
||||
@@ -1074,7 +1074,7 @@ 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-[#3b82f6] bg-[#3b82f6]/10'
|
||||
editingArticle.source === 'imported' ? 'text-purple-400 bg-purple-400/10' : 'text-[#ffb800] bg-[#ffb800]/10'
|
||||
}`}>
|
||||
{editingArticle.source === 'imported' ? 'WordPress Import' : 'Native'}
|
||||
</span>
|
||||
@@ -1090,7 +1090,7 @@ export default function AdminArticlesPage() {
|
||||
<button
|
||||
onClick={handleEditSave}
|
||||
disabled={editSaving || !editTitle.trim()}
|
||||
className="flex items-center gap-2 bg-[#3b82f6] hover:bg-[#2563eb] disabled:bg-[#1e3a5f] disabled:cursor-not-allowed text-white text-sm font-bold px-5 py-2 rounded transition-colors">
|
||||
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">
|
||||
{editSaving ? (
|
||||
<div className="w-3.5 h-3.5 border-2 border-white border-t-transparent rounded-full animate-spin" />
|
||||
) : null}
|
||||
|
||||
@@ -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-[#3b82f6] border-t-transparent rounded-full animate-spin" />
|
||||
<div className="w-6 h-6 border-2 border-[#ffb800] border-t-transparent rounded-full animate-spin" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -382,7 +382,7 @@ export default function AuditLogPage() {
|
||||
<StatCard
|
||||
label="Total Events"
|
||||
value={logs.length}
|
||||
accent="bg-[#3b82f6]/10 text-[#3b82f6]"
|
||||
accent="bg-[#ffb800]/10 text-[#ffb800]"
|
||||
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,7 +433,7 @@ 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-[#3b82f6] font-body"
|
||||
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"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -445,7 +445,7 @@ export default function AuditLogPage() {
|
||||
onClick={() => setActionFilter(f)}
|
||||
className={`px-3 py-1.5 rounded-md text-xs font-body font-semibold capitalize transition-all ${
|
||||
actionFilter === f
|
||||
? 'bg-[#3b82f6] text-white'
|
||||
? 'bg-[#ffb800] text-white'
|
||||
: 'text-[#555] hover:text-[#888]'
|
||||
}`}
|
||||
>
|
||||
@@ -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-[#3b82f6] border-t-transparent rounded-full animate-spin" />
|
||||
<div className="w-6 h-6 border-2 border-[#ffb800] border-t-transparent rounded-full animate-spin" />
|
||||
<p className="text-[#555] text-sm font-body">Loading audit logs…</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -496,7 +496,7 @@ export default function AuditLogPage() {
|
||||
</div>
|
||||
<p className="text-[#555] text-sm font-body">No audit log entries found</p>
|
||||
{searchQuery && (
|
||||
<button onClick={() => setSearchQuery('')} className="text-[#3b82f6] text-xs font-body hover:underline">
|
||||
<button onClick={() => setSearchQuery('')} className="text-[#ffb800] text-xs font-body hover:underline">
|
||||
Clear search
|
||||
</button>
|
||||
)}
|
||||
@@ -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-[#3b82f6] transition-colors p-1"
|
||||
className="text-[#444] hover:text-[#ffb800] 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">
|
||||
|
||||
@@ -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-[#3b82f6] border-t-transparent rounded-full animate-spin" />
|
||||
<div className="w-8 h-8 border-2 border-[#ffb800] border-t-transparent rounded-full animate-spin" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -200,7 +200,7 @@ export default function BannedTermsPage() {
|
||||
Scan Published Posts
|
||||
</button>
|
||||
<button onClick={() => { setForm(EMPTY_FORM); setEditingId(null); setShowAddForm(true); }}
|
||||
className="flex items-center gap-2 px-4 py-2 bg-[#3b82f6] 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-[#ffb800] 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>
|
||||
@@ -240,12 +240,12 @@ export default function BannedTermsPage() {
|
||||
<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-[#3b82f6]" 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]" 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-[#3b82f6]">
|
||||
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]">
|
||||
<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-[#3b82f6]">
|
||||
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]">
|
||||
<option value="both">Both Sites</option>
|
||||
<option value="avbeat">AV Beat only</option>
|
||||
<option value="avbeat">AV Beat only</option>
|
||||
@@ -262,11 +262,11 @@ 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-[#3b82f6] 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-[#ffb800] resize-none" />
|
||||
</div>
|
||||
<div className="md:col-span-2 flex gap-3">
|
||||
<button type="submit" disabled={saving}
|
||||
className="px-5 py-2 bg-[#3b82f6] 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-[#ffb800] 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); }}
|
||||
@@ -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-[#3b82f6] w-48" />
|
||||
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" />
|
||||
<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-[#3b82f6]">
|
||||
className="bg-[#111] border border-[#252525] rounded-lg px-3 py-2 text-[#888] text-sm font-body focus:outline-none focus:border-[#ffb800]">
|
||||
<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-[#3b82f6]">
|
||||
className="bg-[#111] border border-[#252525] rounded-lg px-3 py-2 text-[#888] text-sm font-body focus:outline-none focus:border-[#ffb800]">
|
||||
<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-[#3b82f6]">
|
||||
className="bg-[#111] border border-[#252525] rounded-lg px-3 py-2 text-[#888] text-sm font-body focus:outline-none focus:border-[#ffb800]">
|
||||
<option value="">All Status</option>
|
||||
<option value="true">Active</option>
|
||||
<option value="false">Inactive</option>
|
||||
@@ -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-[#3b82f6] hover:text-blue-300 font-body transition-colors">Edit</button>
|
||||
<button onClick={() => handleEdit(term)} className="text-xs text-[#ffb800] 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'}
|
||||
|
||||
@@ -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-[#60a5fa]">{pct(b.clicks, b.impressions)}</span>
|
||||
<span className="text-[#ffd25a]">{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-[#3b82f6] focus:outline-none"
|
||||
className="rounded border border-[#1f2937] bg-[#070a10] p-2 text-[#e5e7eb] focus:border-[#ffb800] 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-[#3b82f6] focus:outline-none"
|
||||
className="rounded border border-[#1f2937] bg-[#070a10] p-2 text-[#e5e7eb] focus:border-[#ffb800] 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-[#3b82f6] focus:outline-none"
|
||||
className="rounded border border-[#1f2937] bg-[#070a10] p-2 text-[#e5e7eb] focus:border-[#ffb800] 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-[#3b82f6] focus:outline-none"
|
||||
className="rounded border border-[#1f2937] bg-[#070a10] p-2 text-[#e5e7eb] focus:border-[#ffb800] 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-[#3b82f6] focus:outline-none"
|
||||
className="rounded border border-[#1f2937] bg-[#070a10] p-2 text-[#e5e7eb] focus:border-[#ffb800] 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-[#2563eb] disabled:opacity-40 disabled:cursor-not-allowed"
|
||||
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"
|
||||
>
|
||||
Save changes
|
||||
</button>
|
||||
@@ -219,7 +219,7 @@ export default function BannerRows({
|
||||
</div>
|
||||
<Link
|
||||
href={`/admin/banners/${b.id}/analytics`}
|
||||
className="text-sm text-[#60a5fa] hover:underline"
|
||||
className="text-sm text-[#ffd25a] hover:underline"
|
||||
>
|
||||
View detailed analytics →
|
||||
</Link>
|
||||
|
||||
@@ -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="#60a5fa" />
|
||||
<ChartCard title="Impressions per day" data={imp} stroke="#ffd25a" />
|
||||
<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-[#60a5fa]">{r.count.toLocaleString()}</td>
|
||||
<td className="py-2 text-right text-[#ffd25a]">{r.count.toLocaleString()}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
|
||||
@@ -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-[#60a5fa] hover:underline">
|
||||
<Link href="/admin/banners" className="text-sm text-[#ffd25a] 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-[#60a5fa] hover:underline" href={b.click_url} target="_blank" rel="noopener noreferrer">
|
||||
<a className="text-[#ffd25a] hover:underline" href={b.click_url} target="_blank" rel="noopener noreferrer">
|
||||
{b.click_url.slice(0, 80)}{b.click_url.length > 80 ? "…" : ""}
|
||||
</a>
|
||||
)}
|
||||
|
||||
@@ -113,7 +113,7 @@ export default async function AdminBannersPage() {
|
||||
after the next refresh.
|
||||
</p>
|
||||
</div>
|
||||
<Link href="/admin" className="text-sm text-[#60a5fa] hover:underline">
|
||||
<Link href="/admin" className="text-sm text-[#ffd25a] hover:underline">
|
||||
← Admin home
|
||||
</Link>
|
||||
</header>
|
||||
|
||||
@@ -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-[#3b82f6] border-t-transparent rounded-full animate-spin" />
|
||||
<div className="w-8 h-8 border-2 border-[#ffb800] 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-[#3b82f6] 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-[#ffb800] hover:bg-blue-500 disabled:opacity-50 disabled:cursor-not-allowed text-white text-sm font-body font-semibold rounded-lg transition-colors"
|
||||
>
|
||||
{scanning ? (
|
||||
<>
|
||||
@@ -262,7 +262,7 @@ export default function CompanyCoveragePage() {
|
||||
onClick={() => setActiveTab(tab)}
|
||||
className={`px-4 py-1.5 rounded-md text-sm font-body font-semibold capitalize transition-colors ${
|
||||
activeTab === tab
|
||||
? 'bg-[#3b82f6] text-white'
|
||||
? 'bg-[#ffb800] text-white'
|
||||
: 'text-[#888] hover:text-white'
|
||||
}`}
|
||||
>
|
||||
@@ -282,7 +282,7 @@ 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-[#3b82f6] hover:text-blue-300 font-body underline">
|
||||
<button onClick={handleScan} disabled={scanning} className="mt-3 text-xs text-[#ffb800] hover:text-blue-300 font-body underline">
|
||||
Run scan now →
|
||||
</button>
|
||||
)}
|
||||
@@ -374,7 +374,7 @@ export default function CompanyCoveragePage() {
|
||||
<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-[#3b82f6]" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24">
|
||||
<svg className="w-4 h-4 text-[#ffb800]" 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>
|
||||
@@ -422,7 +422,7 @@ export default function CompanyCoveragePage() {
|
||||
</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-[#3b82f6] font-body font-mono break-all">{settings.reminder_email}</p>
|
||||
<p className="text-xs text-[#ffb800] 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>
|
||||
|
||||
@@ -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-[#3b82f6] border-t-transparent rounded-full animate-spin" />
|
||||
<div className="w-8 h-8 border-2 border-[#ffb800] border-t-transparent rounded-full animate-spin" />
|
||||
</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-[#3b82f6]' : 'bg-[#333]'}`}
|
||||
className={`relative w-11 h-6 rounded-full transition-colors ${settings.autopilot_enabled ? 'bg-[#ffb800]' : '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-[#3b82f6] 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-[#ffb800] 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>
|
||||
@@ -337,7 +337,7 @@ 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-[#3b82f6] 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-[#ffb800] text-white' : 'bg-[#111] border border-[#252525] text-[#555] hover:text-white'}`}>
|
||||
{s === 'generated' ? 'Pending Review' : s}
|
||||
</button>
|
||||
))}
|
||||
@@ -430,7 +430,7 @@ 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-[#3b82f6] 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-[#ffb800] 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>
|
||||
@@ -443,17 +443,17 @@ export default function CompanyTrackerPage() {
|
||||
<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-[#3b82f6]" />
|
||||
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]" />
|
||||
</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-[#3b82f6]" />
|
||||
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]" />
|
||||
</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-[#3b82f6]">
|
||||
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]">
|
||||
<option value="both">Both Sites</option>
|
||||
<option value="avbeat">AV Beat only</option>
|
||||
<option value="avbeat">AV Beat only</option>
|
||||
@@ -462,14 +462,14 @@ 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-[#3b82f6]">
|
||||
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]">
|
||||
<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-[#3b82f6] hover:bg-blue-500 text-white text-sm font-body font-semibold rounded-lg transition-colors">Add Company</button>
|
||||
<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>
|
||||
</div>
|
||||
</form>
|
||||
@@ -492,12 +492,12 @@ export default function CompanyTrackerPage() {
|
||||
) : companies.map(company => (
|
||||
<tr key={company.id} className="hover:bg-[#0d0d0d] 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-[#3b82f6] 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-[#ffb800] 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-[#3b82f6]' : 'bg-[#333]'}`}>
|
||||
className={`relative w-9 h-5 rounded-full transition-colors ${company.auto_story_enabled ? 'bg-[#ffb800]' : '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>
|
||||
@@ -506,7 +506,7 @@ export default function CompanyTrackerPage() {
|
||||
<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-[#3b82f6] hover:text-blue-300 font-body transition-colors">Force Crawl</button>
|
||||
className="text-xs text-[#ffb800] 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>
|
||||
@@ -567,14 +567,14 @@ 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-[#3b82f6]' : 'bg-[#333]'}`}>
|
||||
className={`relative w-11 h-6 rounded-full transition-colors ${settings.autopilot_enabled ? 'bg-[#ffb800]' : '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-[#3b82f6]" />
|
||||
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]" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -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-[#3b82f6]' : 'bg-[#333]'}`}>
|
||||
className={`relative w-11 h-6 rounded-full transition-colors ${trackerSettings.auto_story_global_enabled ? 'bg-[#ffb800]' : '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-[#3b82f6]' : 'bg-[#333]'}`}>
|
||||
className={`relative w-11 h-6 rounded-full transition-colors ${trackerSettings.auto_publish_enabled ? 'bg-[#ffb800]' : '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-[#3b82f6]" />
|
||||
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]" />
|
||||
</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-[#3b82f6]" />
|
||||
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]" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button onClick={handleSaveSettings} disabled={savingSettings}
|
||||
className="px-6 py-2.5 bg-[#3b82f6] 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-[#ffb800] 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>
|
||||
|
||||
@@ -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-[#3b82f6]"
|
||||
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]"
|
||||
>
|
||||
{Object.entries(SITE_LABELS).map(([key, val]) => (
|
||||
<option key={key} value={key}>{val.label}</option>
|
||||
@@ -195,14 +195,14 @@ 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-[#3b82f6]"
|
||||
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]"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
onClick={handleGenerate}
|
||||
disabled={loading}
|
||||
className="flex-1 bg-[#3b82f6] hover:bg-[#2563eb] disabled:opacity-50 text-white text-sm font-body font-semibold py-2 px-4 rounded-lg transition-colors"
|
||||
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"
|
||||
>
|
||||
{loading ? 'Generating…' : 'Generate Article'}
|
||||
</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-[#3b82f6] border-t-transparent rounded-full animate-spin" />
|
||||
<div className="w-8 h-8 border-2 border-[#ffb800] border-t-transparent rounded-full animate-spin" />
|
||||
<p className="text-[#555] text-sm font-body">Loading content status…</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -90,7 +90,7 @@ function StatCard({
|
||||
label,
|
||||
value,
|
||||
sub,
|
||||
accent = '#3b82f6',
|
||||
accent = '#ffb800',
|
||||
icon,
|
||||
trend,
|
||||
}: {
|
||||
@@ -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-[#3b82f6] border-t-transparent rounded-full animate-spin" />
|
||||
<div className="w-8 h-8 border-2 border-[#ffb800] border-t-transparent rounded-full animate-spin" />
|
||||
<p className="text-[#555] text-sm font-body">Loading analytics…</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -280,7 +280,7 @@ 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-[#3b82f6] text-white'
|
||||
? 'bg-[#ffb800] text-white'
|
||||
: 'text-[#555] hover:text-white border border-[#252525] hover:border-[#444]'
|
||||
}`}
|
||||
>
|
||||
@@ -317,7 +317,7 @@ export default function EmailAnalyticsDashboard() {
|
||||
label="Active Subscribers"
|
||||
value={fmt(stats.activeSubscribers)}
|
||||
sub={`${fmt(stats.unsubscribed)} unsubscribed`}
|
||||
accent="#3b82f6"
|
||||
accent="#ffb800"
|
||||
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">
|
||||
@@ -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="#3b82f6" strokeWidth={1} radius={[3, 3, 0, 0]} />
|
||||
<Bar dataKey="sent" name="Sent" fill="#3b82f620" stroke="#ffb800" 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>
|
||||
@@ -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="#3b82f6" stopOpacity={0.2} />
|
||||
<stop offset="95%" stopColor="#3b82f6" stopOpacity={0} />
|
||||
<stop offset="5%" stopColor="#ffb800" stopOpacity={0.2} />
|
||||
<stop offset="95%" stopColor="#ffb800" 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="#3b82f6"
|
||||
stroke="#ffb800"
|
||||
strokeWidth={2}
|
||||
fill="url(#openGrad)"
|
||||
dot={{ r: 3, fill: '#3b82f6' }}
|
||||
dot={{ r: 3, fill: '#ffb800' }}
|
||||
activeDot={{ r: 5 }}
|
||||
/>
|
||||
<Area
|
||||
@@ -463,7 +463,7 @@ export default function EmailAnalyticsDashboard() {
|
||||
fill="#8b5cf618"
|
||||
dot={false}
|
||||
/>
|
||||
<Bar dataKey="net" name="Net New" fill="#3b82f6" radius={[3, 3, 0, 0]} />
|
||||
<Bar dataKey="net" name="Net New" fill="#ffb800" radius={[3, 3, 0, 0]} />
|
||||
</ComposedChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
@@ -503,7 +503,7 @@ export default function EmailAnalyticsDashboard() {
|
||||
label: 'Open Rate',
|
||||
yours: stats.avgOpenRate,
|
||||
benchmark: 21.5,
|
||||
color: '#3b82f6',
|
||||
color: '#ffb800',
|
||||
unit: '%',
|
||||
},
|
||||
{
|
||||
|
||||
@@ -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-[#3b82f6] 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-[#ffb800] border-t-transparent rounded-full animate-spin" /></div>;
|
||||
if (!user) return null;
|
||||
|
||||
return (
|
||||
@@ -86,7 +86,7 @@ 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-[#3b82f6] rounded text-xs font-medium hover:bg-blue-500">+ Add Domain</button>
|
||||
<button onClick={() => setShowAddForm(true)} className="px-3 py-1.5 bg-[#ffb800] rounded text-xs font-medium hover:bg-blue-500">+ Add Domain</button>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-2">
|
||||
@@ -109,7 +109,7 @@ function DomainsContent() {
|
||||
</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-[#3b82f6]" />
|
||||
<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]" />
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-xs text-[#888] block mb-1">Type</label>
|
||||
@@ -119,7 +119,7 @@ function DomainsContent() {
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<button type="submit" disabled={saving} className="px-3 py-1.5 bg-[#3b82f6] rounded text-xs font-medium hover:bg-blue-500 disabled:opacity-50">{saving ? 'Saving…' : 'Add Domain'}</button>
|
||||
<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>
|
||||
</div>
|
||||
</form>
|
||||
@@ -197,7 +197,7 @@ 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-[#1a2535] border border-[#2a3a50] rounded text-xs text-[#3b82f6] hover:bg-[#0d1a2d] disabled:opacity-50">
|
||||
<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">
|
||||
{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">
|
||||
@@ -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-[#3b82f6] 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-[#ffb800] border-t-transparent rounded-full animate-spin" /></div>}>
|
||||
<DomainsContent />
|
||||
</Suspense>
|
||||
);
|
||||
|
||||
@@ -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-[#3b82f6] 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-[#ffb800] border-t-transparent rounded-full animate-spin" /></div>;
|
||||
if (!user) return null;
|
||||
|
||||
return (
|
||||
@@ -77,7 +77,7 @@ export default function EmailDashboard() {
|
||||
<div key={site} className="bg-[#111] border border-[#252525] 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-[#3b82f6] hover:underline">Manage →</Link>
|
||||
<Link href={`/admin/email/domains?site=${site}`} className="text-xs text-[#ffb800] 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-[#3b82f6] transition-colors">
|
||||
<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">
|
||||
{l.label}
|
||||
</Link>
|
||||
))}
|
||||
|
||||
@@ -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-[#3b82f6] 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-[#ffb800] border-t-transparent rounded-full animate-spin" /></div>;
|
||||
if (!user) return null;
|
||||
|
||||
return (
|
||||
|
||||
@@ -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-[#3b82f6] 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-[#ffb800] border-t-transparent rounded-full animate-spin" /></div>;
|
||||
if (!user) return null;
|
||||
|
||||
return (
|
||||
@@ -84,14 +84,14 @@ 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-[#3b82f6] rounded text-xs font-medium hover:bg-blue-500">+ Add Suppression</button>
|
||||
<button onClick={() => setShowAddForm(true)} className="px-3 py-1.5 bg-[#ffb800] 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-[#3b82f6] 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-[#ffb800] text-white' : 'bg-[#111] border border-[#252525] text-[#888]'}`}>{s}</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -119,7 +119,7 @@ export default function SuppressionsPage() {
|
||||
<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-[#3b82f6]" />
|
||||
<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]" />
|
||||
</div>
|
||||
|
||||
{showAddForm && (
|
||||
@@ -128,7 +128,7 @@ export default function SuppressionsPage() {
|
||||
<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-[#3b82f6]" />
|
||||
<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]" />
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-xs text-[#888] block mb-1">Type</label>
|
||||
@@ -138,11 +138,11 @@ export default function SuppressionsPage() {
|
||||
</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-[#3b82f6]" />
|
||||
<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]" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<button type="submit" disabled={saving} className="px-3 py-1.5 bg-[#3b82f6] rounded text-xs font-medium hover:bg-blue-500 disabled:opacity-50">{saving ? 'Saving…' : 'Add'}</button>
|
||||
<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>
|
||||
</div>
|
||||
</form>
|
||||
@@ -186,13 +186,13 @@ export default function SuppressionsPage() {
|
||||
<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">
|
||||
<h2 className="text-sm font-semibold">Suppression Keywords</h2>
|
||||
<button onClick={() => setShowKeywordForm(true)} className="text-xs text-[#3b82f6] hover:underline">+ Add Keyword</button>
|
||||
<button onClick={() => setShowKeywordForm(true)} className="text-xs text-[#ffb800] hover:underline">+ Add Keyword</button>
|
||||
</div>
|
||||
{showKeywordForm && (
|
||||
<form onSubmit={handleAddKeyword} className="px-4 py-3 border-b border-[#1a1a1a] 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-[#3b82f6]" />
|
||||
<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]" />
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-xs text-[#888] block mb-1">Type</label>
|
||||
@@ -201,7 +201,7 @@ export default function SuppressionsPage() {
|
||||
<option value="hostile">hostile</option>
|
||||
</select>
|
||||
</div>
|
||||
<button type="submit" disabled={saving} className="px-3 py-1.5 bg-[#3b82f6] rounded text-xs font-medium hover:bg-blue-500 disabled:opacity-50">Add</button>
|
||||
<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>
|
||||
</form>
|
||||
)}
|
||||
@@ -216,7 +216,7 @@ export default function SuppressionsPage() {
|
||||
<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>
|
||||
</td>
|
||||
<td className="px-4 py-2">
|
||||
<button onClick={() => toggleKeyword(k.id, k.active)} className="text-xs text-[#3b82f6] hover:underline">{k.active ? 'Deactivate' : 'Activate'}</button>
|
||||
<button onClick={() => toggleKeyword(k.id, k.active)} className="text-xs text-[#ffb800] hover:underline">{k.active ? 'Deactivate' : 'Activate'}</button>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
|
||||
@@ -167,7 +167,7 @@ const EMPTY_STATS: ForumStats = {
|
||||
},
|
||||
};
|
||||
|
||||
const CATEGORY_COLORS = ['#3b82f6', '#f59e0b', '#10b981', '#8b5cf6', '#ef4444', '#06b6d4', '#f97316', '#84cc16'];
|
||||
const CATEGORY_COLORS = ['#ffb800', '#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-[#3b82f6] border-t-transparent rounded-full animate-spin" />
|
||||
<div className="w-8 h-8 border-2 border-[#ffb800] border-t-transparent rounded-full animate-spin" />
|
||||
<p className="text-[#555] text-sm font-body">Loading forum dashboard…</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -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-[#3b82f6] 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-[#ffb800] 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" />
|
||||
@@ -407,7 +407,7 @@ export default function ForumDashboardPage() {
|
||||
iconType="circle"
|
||||
iconSize={8}
|
||||
/>
|
||||
<Line type="monotone" dataKey="threads" stroke="#3b82f6" strokeWidth={2} dot={{ fill: '#3b82f6', r: 3 }} name="New Threads" />
|
||||
<Line type="monotone" dataKey="threads" stroke="#ffb800" strokeWidth={2} dot={{ fill: '#ffb800', r: 3 }} name="New Threads" />
|
||||
<Line type="monotone" dataKey="replies" stroke="#10b981" strokeWidth={2} dot={{ fill: '#10b981', r: 3 }} name="New Replies" />
|
||||
</LineChart>
|
||||
</ResponsiveContainer>
|
||||
@@ -425,7 +425,7 @@ export default function ForumDashboardPage() {
|
||||
<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-[#3b82f6] hover:text-blue-300 font-body transition-colors">View forum →</Link>
|
||||
<Link href="/forum" className="text-xs text-[#ffb800] hover:text-blue-300 font-body transition-colors">View forum →</Link>
|
||||
</div>
|
||||
{topCategories.length === 0 ? (
|
||||
<div className="px-5 py-10 text-center">
|
||||
@@ -475,7 +475,7 @@ export default function ForumDashboardPage() {
|
||||
<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]">
|
||||
{idx + 1}
|
||||
</div>
|
||||
<div className="w-8 h-8 rounded-full bg-gradient-to-br from-[#3b82f6] to-[#8b5cf6] flex items-center justify-center flex-shrink-0">
|
||||
<div className="w-8 h-8 rounded-full bg-gradient-to-br from-[#ffb800] 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>
|
||||
@@ -523,7 +523,7 @@ export default function ForumDashboardPage() {
|
||||
iconType="circle"
|
||||
iconSize={8}
|
||||
/>
|
||||
<Bar dataKey="thread_count" fill="#3b82f6" radius={[3, 3, 0, 0]} name="Threads" />
|
||||
<Bar dataKey="thread_count" fill="#ffb800" radius={[3, 3, 0, 0]} name="Threads" />
|
||||
<Bar dataKey="post_count" fill="#10b981" radius={[3, 3, 0, 0]} name="Posts" />
|
||||
</BarChart>
|
||||
</ResponsiveContainer>
|
||||
@@ -539,7 +539,7 @@ export default function ForumDashboardPage() {
|
||||
<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-[#3b82f6] hover:text-blue-300 font-body transition-colors">Open moderation →</Link>
|
||||
<Link href="/admin/forum-moderation" className="text-xs text-[#ffb800] 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]">
|
||||
{[
|
||||
|
||||
@@ -126,7 +126,7 @@ function ThreadRow({ thread, onAction, loadingId }: ThreadRowProps) {
|
||||
<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-[#3b82f6] transition-colors truncate max-w-xs"
|
||||
className="text-white font-body font-semibold text-sm hover:text-[#ffb800] transition-colors truncate max-w-xs"
|
||||
target="_blank"
|
||||
>
|
||||
{thread.title}
|
||||
@@ -243,7 +243,7 @@ function ReplyRow({ reply, onAction, loadingId }: ReplyRowProps) {
|
||||
{reply.forum_threads && (
|
||||
<Link
|
||||
href={`/forum/thread/${reply.forum_threads.id}`}
|
||||
className="text-[#3b82f6] font-body text-xs hover:underline truncate max-w-xs"
|
||||
className="text-[#ffb800] font-body text-xs hover:underline truncate max-w-xs"
|
||||
target="_blank"
|
||||
>
|
||||
↗ {reply.forum_threads.title}
|
||||
@@ -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-[#3b82f6] border-t-transparent rounded-full animate-spin" />
|
||||
<div className="w-8 h-8 border-2 border-[#ffb800] border-t-transparent rounded-full animate-spin" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -435,10 +435,10 @@ export default function ForumModerationPage() {
|
||||
)}
|
||||
|
||||
{/* Page Header */}
|
||||
<div className="bg-[#1a2535] border-b border-[#2a3a50]">
|
||||
<div className="bg-[#1a1208] 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-[#3b82f6] transition-colors">Admin</Link>
|
||||
<Link href="/admin" className="hover:text-[#ffb800] transition-colors">Admin</Link>
|
||||
<span className="mx-2">›</span>
|
||||
<span className="text-[#aab4c4]">Forum Moderation</span>
|
||||
</nav>
|
||||
@@ -470,7 +470,7 @@ export default function ForumModerationPage() {
|
||||
onClick={() => { setActiveTab(tab); setPage(1); }}
|
||||
className={`px-5 py-2 text-sm font-body font-semibold rounded-md transition-colors capitalize ${
|
||||
activeTab === tab
|
||||
? 'bg-[#3b82f6] text-white'
|
||||
? 'bg-[#ffb800] text-white'
|
||||
: 'text-[#888] hover:text-white'
|
||||
}`}
|
||||
>
|
||||
@@ -488,7 +488,7 @@ 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-[#3b82f6] text-white'
|
||||
? 'bg-[#ffb800] text-white'
|
||||
: 'bg-[#1a1a1a] text-[#888] hover:bg-[#252525] hover:text-white border border-[#252525]'
|
||||
}`}
|
||||
>
|
||||
@@ -501,7 +501,7 @@ 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-[#3b82f6] text-white'
|
||||
? 'bg-[#ffb800] text-white'
|
||||
: 'bg-[#1a1a1a] text-[#888] hover:bg-[#252525] hover:text-white border border-[#252525]'
|
||||
}`}
|
||||
>
|
||||
@@ -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-[#3b82f6] border-t-transparent rounded-full animate-spin" />
|
||||
<div className="w-7 h-7 border-2 border-[#ffb800] border-t-transparent rounded-full animate-spin" />
|
||||
</div>
|
||||
) : activeTab === 'threads' ? (
|
||||
<>
|
||||
|
||||
@@ -137,12 +137,12 @@ export default function AdminForumSeedPage() {
|
||||
<>
|
||||
<Header />
|
||||
<main className="min-h-screen bg-[#111111]">
|
||||
<div className="bg-[#1a2535] border-b border-[#2a3a50]">
|
||||
<div className="bg-[#1a1208] 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-[#3b82f6] transition-colors">Admin</Link>
|
||||
<Link href="/admin" className="hover:text-[#ffb800] transition-colors">Admin</Link>
|
||||
<span className="mx-2">›</span>
|
||||
<Link href="/forum" className="hover:text-[#3b82f6] transition-colors">Forum</Link>
|
||||
<Link href="/forum" className="hover:text-[#ffb800] transition-colors">Forum</Link>
|
||||
<span className="mx-2">›</span>
|
||||
<span className="text-[#aab4c4]">Seed Tool</span>
|
||||
</nav>
|
||||
@@ -161,13 +161,13 @@ export default function AdminForumSeedPage() {
|
||||
<div className="flex border-b border-[#252525]">
|
||||
<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-[#3b82f6]' : '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-[#ffb800]' : '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-[#3b82f6]' : '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-[#ffb800]' : 'text-[#666] hover:text-[#aaa]'}`}
|
||||
>
|
||||
JSON Schema
|
||||
</button>
|
||||
@@ -182,14 +182,14 @@ 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-[#3b82f6] resize-none"
|
||||
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"
|
||||
spellCheck={false}
|
||||
/>
|
||||
<div className="flex gap-3 mt-3">
|
||||
<button
|
||||
onClick={handleImport}
|
||||
disabled={importing}
|
||||
className="bg-[#3b82f6] hover:bg-[#2563eb] disabled:opacity-50 text-white font-body font-semibold text-sm px-5 py-2 rounded-lg transition-colors"
|
||||
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"
|
||||
>
|
||||
{importing ? 'Importing...' : 'Import Threads'}
|
||||
</button>
|
||||
@@ -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-[#3b82f6] font-mono text-xs px-2 py-1 rounded">{slug}</code>
|
||||
<code key={slug} className="bg-[#0d0d0d] border border-[#333] text-[#ffb800] font-mono text-xs px-2 py-1 rounded">{slug}</code>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
@@ -268,17 +268,17 @@ export default function AdminForumSeedPage() {
|
||||
<div className="bg-[#1a1a1a] border border-[#252525] 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-[#3b82f6] hover:underline font-body text-sm">
|
||||
<Link href="/forum" className="flex items-center gap-2 text-[#ffb800] hover:underline font-body text-sm">
|
||||
<span>→</span> View Forum
|
||||
</Link>
|
||||
<Link href="/admin" className="flex items-center gap-2 text-[#3b82f6] hover:underline font-body text-sm">
|
||||
<Link href="/admin" className="flex items-center gap-2 text-[#ffb800] hover:underline font-body text-sm">
|
||||
<span>→</span> Admin Dashboard
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Tips */}
|
||||
<div className="bg-[#1a2535] border border-[#2a3a50] rounded-lg p-4">
|
||||
<div className="bg-[#1a1208] 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>
|
||||
|
||||
@@ -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-[#3b82f6] border-t-transparent rounded-full animate-spin" />
|
||||
<div className="w-6 h-6 border-2 border-[#ffb800] border-t-transparent rounded-full animate-spin" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -145,7 +145,7 @@ export default function WPImportPage() {
|
||||
<div className="bg-[#111111] border-b border-[#252525]">
|
||||
<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-[#3b82f6] transition-colors text-sm">
|
||||
<Link href="/home-page" className="text-[#555] hover:text-[#ffb800] 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-[#3b82f6] hover:underline">
|
||||
<a href="https://www.avbeat.com" target="_blank" rel="noopener noreferrer" className="text-[#ffb800] 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-[#3b82f6]">
|
||||
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" className="text-[#ffb800]">
|
||||
<circle cx="12" cy="12" r="10" /><path d="M12 8v4l3 3" />
|
||||
</svg>
|
||||
{posts.length} posts imported
|
||||
@@ -178,7 +178,7 @@ export default function WPImportPage() {
|
||||
{/* Import Controls */}
|
||||
<div className="bg-[#111111] border border-[#252525] 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-[#3b82f6]">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" className="text-[#ffb800]">
|
||||
<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-[#3b82f6] disabled:opacity-50">
|
||||
className="bg-[#1a1a1a] border border-[#333] text-[#cccccc] text-sm rounded px-3 py-2 focus:outline-none focus:border-[#ffb800] 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-[#3b82f6] hover:bg-[#2563eb] disabled:bg-[#1e3a5f] disabled:cursor-not-allowed text-white text-sm font-bold px-5 py-2 rounded transition-colors">
|
||||
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">
|
||||
{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-[#1e3a5f] rounded p-4">
|
||||
<div className="mt-4 bg-[#0d1a2e] border border-[#4a3500] rounded p-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-5 h-5 border-2 border-[#3b82f6] border-t-transparent rounded-full animate-spin flex-shrink-0" />
|
||||
<div className="w-5 h-5 border-2 border-[#ffb800] border-t-transparent rounded-full animate-spin flex-shrink-0" />
|
||||
<div>
|
||||
<p className="text-[#3b82f6] text-sm font-bold">Import in progress</p>
|
||||
<p className="text-[#ffb800] 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-[#1a2535] rounded-full overflow-hidden">
|
||||
<div className="h-full bg-[#3b82f6] rounded-full animate-pulse w-2/3" />
|
||||
<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>
|
||||
</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-[#3b82f6]' },
|
||||
{ label: 'Pages', value: importResult.pagesProcessed, color: 'text-[#ffb800]' },
|
||||
{ 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">
|
||||
@@ -284,7 +284,7 @@ export default function WPImportPage() {
|
||||
onClick={() => setActiveTab(tab)}
|
||||
className={`px-5 py-2.5 text-sm font-bold uppercase tracking-wider border-b-2 transition-colors ${
|
||||
activeTab === tab
|
||||
? 'border-[#3b82f6] text-[#3b82f6]'
|
||||
? 'border-[#ffb800] text-[#ffb800]'
|
||||
: 'border-transparent text-[#666] hover:text-[#999]'
|
||||
}`}>
|
||||
{tab === 'posts' ? `Imported Posts (${posts.length})` : `Import History (${logs.length})`}
|
||||
@@ -306,14 +306,14 @@ 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-[#3b82f6] placeholder-[#444]"
|
||||
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]"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{loadingData ? (
|
||||
<div className="flex items-center justify-center py-16">
|
||||
<div className="w-6 h-6 border-2 border-[#3b82f6] border-t-transparent rounded-full animate-spin" />
|
||||
<div className="w-6 h-6 border-2 border-[#ffb800] border-t-transparent rounded-full animate-spin" />
|
||||
</div>
|
||||
) : filteredPosts.length === 0 ? (
|
||||
<div className="text-center py-16 bg-[#111] border border-[#252525] rounded-lg">
|
||||
@@ -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-[#3b82f6] bg-[#0d1a2e] px-1.5 py-0.5 rounded">
|
||||
<span className="text-[10px] font-bold uppercase tracking-wider text-[#ffb800] 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-[#3b82f6] transition-colors p-1"
|
||||
className="text-[#555] hover:text-[#ffb800] 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,7 +402,7 @@ export default function WPImportPage() {
|
||||
<div>
|
||||
{loadingData ? (
|
||||
<div className="flex items-center justify-center py-16">
|
||||
<div className="w-6 h-6 border-2 border-[#3b82f6] border-t-transparent rounded-full animate-spin" />
|
||||
<div className="w-6 h-6 border-2 border-[#ffb800] border-t-transparent rounded-full animate-spin" />
|
||||
</div>
|
||||
) : logs.length === 0 ? (
|
||||
<div className="text-center py-16 bg-[#111] border border-[#252525] rounded-lg">
|
||||
|
||||
@@ -67,7 +67,7 @@ function fmtDate(dateStr: string): string {
|
||||
|
||||
// ─── Sub-components ───────────────────────────────────────────────────────────
|
||||
|
||||
function StatCard({ label, value, sub, accent = '#3b82f6', icon }: {
|
||||
function StatCard({ label, value, sub, accent = '#ffb800', icon }: {
|
||||
label: string; value: string; sub: string; accent?: string; icon: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
@@ -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-[#3b82f6] border-t-transparent rounded-full animate-spin" />
|
||||
<div className="w-8 h-8 border-2 border-[#ffb800] border-t-transparent rounded-full animate-spin" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -308,7 +308,7 @@ export default function NewsletterAnalyticsPage() {
|
||||
label="Avg Open Rate"
|
||||
value={pct(avgOpenRate)}
|
||||
sub={`across ${digestData.length} digests`}
|
||||
accent="#3b82f6"
|
||||
accent="#ffb800"
|
||||
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" />
|
||||
@@ -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="#3b82f6" strokeWidth={2} dot={{ r: 3, fill: '#3b82f6' }} activeDot={{ r: 5 }} />
|
||||
<Line type="monotone" dataKey="Open Rate" stroke="#ffb800" strokeWidth={2} dot={{ r: 3, fill: '#ffb800' }} activeDot={{ r: 5 }} />
|
||||
<Line type="monotone" dataKey="Click Rate" stroke="#10b981" strokeWidth={2} dot={{ r: 3, fill: '#10b981' }} activeDot={{ r: 5 }} />
|
||||
</LineChart>
|
||||
</ResponsiveContainer>
|
||||
@@ -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="#3b82f6" radius={[3, 3, 0, 0]} />
|
||||
<Bar dataKey="subscribers" name="Total Subscribers" fill="#ffb800" radius={[3, 3, 0, 0]} />
|
||||
<Bar dataKey="unsubscribes" name="Unsubscribes" fill="#ef4444" radius={[3, 3, 0, 0]} />
|
||||
</BarChart>
|
||||
</ResponsiveContainer>
|
||||
|
||||
@@ -252,7 +252,7 @@ export default function CampaignEditorPage() {
|
||||
article_blocks: payload.article_blocks,
|
||||
layout: 'featured',
|
||||
style: 'dark',
|
||||
accent_color: '#3b82f6',
|
||||
accent_color: '#ffb800',
|
||||
is_preset: false,
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -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-[#3b82f6] border-t-transparent rounded-full animate-spin" />
|
||||
<div className="w-8 h-8 border-2 border-[#ffb800] border-t-transparent rounded-full animate-spin" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -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-[#3b82f6] hover:bg-[#2563eb] 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-[#ffb800] hover:bg-[#d99700] 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>
|
||||
@@ -430,12 +430,12 @@ export default function AdminNewsletterPage() {
|
||||
<div className="flex gap-1 mb-6 bg-[#111] border border-[#252525] 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-[#3b82f6] 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-[#ffb800] 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-[#3b82f6] 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-[#ffb800] text-white' : 'text-[#888] hover:text-white'}`}>
|
||||
Send Digest
|
||||
</button>
|
||||
</div>
|
||||
@@ -457,7 +457,7 @@ export default function AdminNewsletterPage() {
|
||||
<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-[#3b82f6] 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-[#ffb800] text-white' : 'text-[#888] hover:text-white'}`}
|
||||
>
|
||||
{s}
|
||||
</button>
|
||||
@@ -478,9 +478,9 @@ export default function AdminNewsletterPage() {
|
||||
|
||||
{/* Bulk Actions Bar */}
|
||||
{someSelected && (
|
||||
<div className="flex items-center justify-between bg-[#1a2535] border border-[#1e3a5f] rounded-lg px-4 py-3 mb-4">
|
||||
<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 gap-3">
|
||||
<span className="text-sm font-body font-semibold text-[#3b82f6]">
|
||||
<span className="text-sm font-body font-semibold text-[#ffb800]">
|
||||
{selectedIds.size} selected
|
||||
</span>
|
||||
<button
|
||||
@@ -512,7 +512,7 @@ export default function AdminNewsletterPage() {
|
||||
Delete
|
||||
</button>
|
||||
{bulkLoading && (
|
||||
<div className="w-4 h-4 border-2 border-[#3b82f6] border-t-transparent rounded-full animate-spin" />
|
||||
<div className="w-4 h-4 border-2 border-[#ffb800] border-t-transparent rounded-full animate-spin" />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
@@ -522,7 +522,7 @@ export default function AdminNewsletterPage() {
|
||||
<div className="bg-[#111] border border-[#252525] rounded-lg overflow-hidden">
|
||||
{loadingData ? (
|
||||
<div className="flex items-center justify-center py-16">
|
||||
<div className="w-6 h-6 border-2 border-[#3b82f6] border-t-transparent rounded-full animate-spin" />
|
||||
<div className="w-6 h-6 border-2 border-[#ffb800] border-t-transparent rounded-full animate-spin" />
|
||||
</div>
|
||||
) : subscribers.length === 0 ? (
|
||||
<div className="text-center py-16">
|
||||
@@ -540,7 +540,7 @@ export default function AdminNewsletterPage() {
|
||||
type="checkbox"
|
||||
checked={allSelected}
|
||||
onChange={toggleSelectAll}
|
||||
className="w-4 h-4 rounded border-[#444] bg-[#1a1a1a] accent-[#3b82f6] cursor-pointer"
|
||||
className="w-4 h-4 rounded border-[#444] bg-[#1a1a1a] accent-[#ffb800] cursor-pointer"
|
||||
/>
|
||||
</th>
|
||||
<th className="text-left px-4 py-3 text-xs font-body font-bold text-[#555] uppercase tracking-wider">Email</th>
|
||||
@@ -561,14 +561,14 @@ export default function AdminNewsletterPage() {
|
||||
type="checkbox"
|
||||
checked={selectedIds.has(sub.id)}
|
||||
onChange={() => toggleSelect(sub.id)}
|
||||
className="w-4 h-4 rounded border-[#444] bg-[#1a1a1a] accent-[#3b82f6] cursor-pointer"
|
||||
className="w-4 h-4 rounded border-[#444] bg-[#1a1a1a] accent-[#ffb800] 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-[#1a2535] text-[#3b82f6] px-2 py-0.5 rounded-full font-body">{t}</span>
|
||||
<span key={t} className="text-xs bg-[#1a1208] text-[#ffb800] 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>
|
||||
@@ -616,21 +616,21 @@ export default function AdminNewsletterPage() {
|
||||
{activeTab === 'send-digest' && (
|
||||
<div className="max-w-2xl">
|
||||
{/* SMTP warning if not configured */}
|
||||
<div className="bg-[#1a2535] border border-[#1e3a5f] rounded-lg p-4 mb-6 flex gap-3">
|
||||
<svg className="w-5 h-5 text-[#3b82f6] flex-shrink-0 mt-0.5" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24">
|
||||
<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">
|
||||
<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-[#3b82f6]">SMTP_HOST</code>, <code className="text-[#3b82f6]">SMTP_USER</code>, <code className="text-[#3b82f6]">SMTP_PASS</code>, and <code className="text-[#3b82f6]">SMTP_FROM_EMAIL</code> in your environment variables to enable sending.
|
||||
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.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Applied template badge */}
|
||||
{appliedTemplate && (
|
||||
<div className="flex items-center justify-between bg-[#1a2535] border border-[#1e3a5f] rounded-lg px-4 py-3 mb-4">
|
||||
<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 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-[#3b82f6] border border-[#1e3a5f] rounded-lg hover:bg-[#1a2535] transition-colors"
|
||||
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"
|
||||
>
|
||||
<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,7 +725,7 @@ export default function AdminNewsletterPage() {
|
||||
<button
|
||||
type="button"
|
||||
onClick={addArticle}
|
||||
className="text-xs text-[#3b82f6] hover:text-blue-400 font-body font-semibold transition-colors"
|
||||
className="text-xs text-[#ffb800] hover:text-blue-400 font-body font-semibold transition-colors"
|
||||
>
|
||||
+ Add Article
|
||||
</button>
|
||||
@@ -807,7 +807,7 @@ export default function AdminNewsletterPage() {
|
||||
<div className="flex items-center justify-between px-6 py-4 border-b border-[#252525] 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-[#3b82f6] font-body hover:text-blue-400 transition-colors">
|
||||
<Link href="/admin/newsletter/templates" className="text-xs text-[#ffb800] 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-[#3b82f6] border-t-transparent rounded-full animate-spin" />
|
||||
<div className="w-6 h-6 border-2 border-[#ffb800] 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-[#3b82f6] text-sm font-body hover:text-blue-400 mt-2 inline-block">
|
||||
<Link href="/admin/newsletter/templates" className="text-[#ffb800] 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-[#3b82f6] hover:bg-[#1a2535] transition-colors group"
|
||||
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"
|
||||
>
|
||||
<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 }}>
|
||||
|
||||
@@ -42,7 +42,7 @@ const STYLE_OPTIONS = [
|
||||
];
|
||||
|
||||
const ACCENT_COLORS = [
|
||||
'#3b82f6', '#ef4444', '#10b981', '#8b5cf6', '#f59e0b', '#ec4899', '#06b6d4', '#f97316',
|
||||
'#ffb800', '#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: '#3b82f6',
|
||||
accent_color: '#ffb800',
|
||||
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-[#3b82f6] border-t-transparent rounded-full animate-spin" />
|
||||
<div className="w-8 h-8 border-2 border-[#ffb800] border-t-transparent rounded-full animate-spin" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -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-[#1a2535] text-[#3b82f6] px-2 py-0.5 rounded-full font-body">{presets.length}</span>
|
||||
<span className="text-xs bg-[#1a1208] text-[#ffb800] 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-[#3b82f6] border-t-transparent rounded-full animate-spin" />
|
||||
<div className="w-6 h-6 border-2 border-[#ffb800] 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,7 +298,7 @@ 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-[#1a2535] text-[#3b82f6] px-2 py-0.5 rounded-full font-body">{custom.length}</span>
|
||||
<span className="text-xs bg-[#1a1208] text-[#ffb800] 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">
|
||||
@@ -306,7 +306,7 @@ export default function NewsletterTemplatesPage() {
|
||||
<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-[#3b82f6] text-sm font-body font-semibold hover:text-blue-400 transition-colors">
|
||||
<button onClick={openNew} className="text-[#ffb800] text-sm font-body font-semibold hover:text-blue-400 transition-colors">
|
||||
Create your first template →
|
||||
</button>
|
||||
</div>
|
||||
@@ -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-[#3b82f6] bg-[#1a2535]' : 'border-[#252525] bg-[#0d0d0d] hover:border-[#333]'}`}>
|
||||
<span className={`mt-0.5 ${form.layout === opt.value ? 'text-[#3b82f6]' : 'text-[#555]'}`}>
|
||||
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]'}`}>
|
||||
{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-[#3b82f6] bg-[#1a2535] 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-[#ffb800] bg-[#1a1208] text-white' : 'border-[#252525] bg-[#0d0d0d] text-[#888] hover:border-[#333]'}`}>
|
||||
{opt.label}
|
||||
</button>
|
||||
))}
|
||||
@@ -469,7 +469,7 @@ 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-[#3b82f6] hover:text-blue-400 font-body font-semibold transition-colors">
|
||||
<button type="button" onClick={addBlock} className="text-xs text-[#ffb800] hover:text-blue-400 font-body font-semibold transition-colors">
|
||||
+ Add Slot
|
||||
</button>
|
||||
</div>
|
||||
@@ -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-[#3b82f6]"
|
||||
className="w-3 h-3 accent-[#ffb800]"
|
||||
/>
|
||||
<span className="text-xs font-body text-[#888]">Featured</span>
|
||||
</label>
|
||||
@@ -597,7 +597,7 @@ export default function NewsletterTemplatesPage() {
|
||||
{previewTemplate.article_blocks.map((b, i) => (
|
||||
<div key={i} className="flex items-center gap-2 bg-[#0d0d0d] 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-[#3b82f6] font-body bg-[#1a2535] px-1.5 py-0.5 rounded">Featured</span>}
|
||||
{b.featured && <span className="text-xs text-[#ffb800] font-body bg-[#1a1208] 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>
|
||||
@@ -668,12 +668,12 @@ function TemplateCard({ template, onEdit, onDelete, onPreview, deletingId }: Tem
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
onClick={() => onPreview(template)}
|
||||
className="flex-1 py-1.5 text-xs font-body font-semibold text-[#3b82f6] border border-[#1e3a5f] rounded-lg hover:bg-[#1a2535] transition-colors">
|
||||
className="flex-1 py-1.5 text-xs font-body font-semibold text-[#ffb800] border border-[#4a3500] rounded-lg hover:bg-[#1a1208] 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-[#1a2535] border border-[#1e3a5f] rounded-lg hover:bg-[#1e3a5f] transition-colors">
|
||||
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">
|
||||
Use
|
||||
</Link>
|
||||
{!template.is_preset && (
|
||||
|
||||
@@ -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-[#3b82f6] hover:text-blue-300 transition-colors"
|
||||
className="text-xs font-body font-bold text-[#ffb800] 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-[#3b82f6] border-t-transparent rounded-full animate-spin mx-auto mb-3" />
|
||||
<div className="w-8 h-8 border-2 border-[#ffb800] 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-[#3b82f6] border border-[#3b82f6]/30 rounded hover:bg-[#3b82f6]/10 transition-colors"
|
||||
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"
|
||||
>
|
||||
Mark all read
|
||||
</button>
|
||||
@@ -558,7 +558,7 @@ export default function NotificationsPage() {
|
||||
label="Unread"
|
||||
value={stats.unread}
|
||||
icon={<BellIcon className="w-4 h-4" />}
|
||||
color="text-[#3b82f6] bg-[#3b82f6]/5 border-[#3b82f6]/30"
|
||||
color="text-[#ffb800] bg-[#ffb800]/5 border-[#ffb800]/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-[#3b82f6] text-white'
|
||||
? 'bg-[#ffb800] text-white'
|
||||
: 'text-[#555] hover:text-[#888] hover:bg-[#111]'
|
||||
}`}
|
||||
>
|
||||
|
||||
@@ -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-[#3b82f6] hover:bg-[#0d1a2d] transition-all group">
|
||||
<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">
|
||||
<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-[#3b82f6] transition-colors">{label}</p>
|
||||
<p className="text-sm font-body font-semibold text-white group-hover:text-[#ffb800] 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-[#3b82f6] 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-[#ffb800] 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-[#3b82f6] border-t-transparent rounded-full animate-spin" />
|
||||
<div className="w-8 h-8 border-2 border-[#ffb800] border-t-transparent rounded-full animate-spin" />
|
||||
<p className="text-[#555] text-sm font-body">Loading dashboard…</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -375,7 +375,7 @@ export default function AdminDashboardPage() {
|
||||
<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">
|
||||
<h2 className="text-sm font-display font-bold text-white">Recent Activity</h2>
|
||||
<Link href="/admin/articles" className="text-xs text-[#3b82f6] hover:text-blue-300 font-body transition-colors">View all →</Link>
|
||||
<Link href="/admin/articles" className="text-xs text-[#ffb800] hover:text-blue-300 font-body transition-colors">View all →</Link>
|
||||
</div>
|
||||
<div className="divide-y divide-[#1a1a1a]">
|
||||
{recentActivity.length === 0 ? (
|
||||
|
||||
@@ -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-[#3b82f6] focus:outline-none"
|
||||
className="w-full rounded border border-[#1f2937] bg-[#070a10] p-2 text-sm text-[#e5e7eb] focus:border-[#ffb800] focus:outline-none"
|
||||
rows={2}
|
||||
/>
|
||||
<div className="mt-3 flex flex-wrap gap-3 text-sm">
|
||||
|
||||
@@ -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-[#60a5fa] hover:underline">
|
||||
<Link href="/admin/review-queue" className="text-sm text-[#ffd25a] 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-[#60a5fa]">
|
||||
<div className="flex items-center gap-3 text-xs uppercase tracking-wider text-[#ffd25a]">
|
||||
<span>{persona?.name || "—"}</span>
|
||||
<span className="text-[#4b5563]">·</span>
|
||||
<span>{art.category}</span>
|
||||
|
||||
@@ -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-[#3b82f6] transition-colors"
|
||||
className="rounded border border-[#1f2937] bg-[#0b0f17] p-5 hover:border-[#ffb800] 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-[#60a5fa]">
|
||||
<div className="text-xs uppercase tracking-wider text-[#ffd25a]">
|
||||
{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-[#2563eb]"
|
||||
className="rounded bg-[#1d4ed8] px-3 py-1.5 font-medium text-white hover:bg-[#d99700]"
|
||||
>
|
||||
Review
|
||||
</Link>
|
||||
|
||||
@@ -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-[#3b82f6] 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-[#ffb800] border-t-transparent rounded-full animate-spin" /></div>;
|
||||
if (!user) return null;
|
||||
|
||||
return (
|
||||
@@ -83,14 +83,14 @@ export default function RmpHubPage() {
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
<Link href={section?.href} className="block w-full text-center px-3 py-1.5 bg-[#1a2535] border border-[#2a3a50] rounded text-xs text-[#3b82f6] hover:bg-[#0d1a2d] transition-colors">
|
||||
<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">
|
||||
Open {section?.title}
|
||||
</Link>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="bg-[#0d1117] border border-[#1a2535] rounded-lg p-4">
|
||||
<div className="bg-[#0d1117] border border-[#1a1208] rounded-lg p-4">
|
||||
<p className="text-xs text-[#555] mb-3">Quick Navigation</p>
|
||||
<RmpAdminNav />
|
||||
</div>
|
||||
|
||||
@@ -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-[#3b82f6] border-t-transparent rounded-full animate-spin" />
|
||||
<div className="w-8 h-8 border-2 border-[#ffb800] 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-[#3b82f6] bg-[#0d1a2d]' : 'border-[#252525]'
|
||||
selectedRole === role ? 'border-[#ffb800] bg-[#0d1a2d]' : 'border-[#252525]'
|
||||
}`}
|
||||
>
|
||||
<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}`}>
|
||||
@@ -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-[#3b82f6] text-white'
|
||||
? 'bg-[#ffb800] text-white'
|
||||
: 'text-[#888] hover:text-white'
|
||||
}`}
|
||||
>
|
||||
@@ -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-[#3b82f6]"
|
||||
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]"
|
||||
/>
|
||||
</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-[#3b82f6]"
|
||||
className="bg-[#0d0d0d] border border-[#252525] rounded-lg px-3 py-2 text-sm text-white font-body focus:outline-none focus:border-[#ffb800]"
|
||||
>
|
||||
<option value="all">All Roles</option>
|
||||
{ROLES.map((r) => (
|
||||
@@ -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-[#3b82f6] disabled:opacity-50"
|
||||
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"
|
||||
>
|
||||
{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-[#3b82f6] border-t-transparent rounded-full animate-spin" />
|
||||
<div className="w-4 h-4 border-2 border-[#ffb800] border-t-transparent rounded-full animate-spin" />
|
||||
)}
|
||||
</div>
|
||||
</td>
|
||||
@@ -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-[#3b82f6] 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-[#ffb800] 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,7 +593,7 @@ export default function AdminRolesPage() {
|
||||
{isEditing ? (
|
||||
<button
|
||||
onClick={() => setEditedPermissions((prev) => ({ ...prev, [key]: !val }))}
|
||||
className={`relative w-9 h-5 rounded-full transition-colors ${val ? 'bg-[#3b82f6]' : 'bg-[#333]'}`}
|
||||
className={`relative w-9 h-5 rounded-full transition-colors ${val ? 'bg-[#ffb800]' : '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>
|
||||
@@ -644,7 +644,7 @@ 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-[#3b82f6] text-white'
|
||||
? 'bg-[#ffb800] text-white'
|
||||
: 'bg-[#1a1a1a] text-[#888] hover:text-white'
|
||||
}`}
|
||||
>
|
||||
|
||||
@@ -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-[#3b82f6] border-t-transparent rounded-full animate-spin" />
|
||||
<div className="w-6 h-6 border-2 border-[#ffb800] 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-[#3b82f6] border-[#3b82f6]'
|
||||
? 'text-[#ffb800] border-[#ffb800]'
|
||||
: '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-[#3b82f6] border-t-transparent rounded-full animate-spin" />
|
||||
<div className="w-6 h-6 border-2 border-[#ffb800] 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-[#3b82f6] 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-[#ffb800] 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-[#3b82f6] text-xs font-bold flex-shrink-0">
|
||||
<div className="w-7 h-7 rounded-full bg-[#1f2937] flex items-center justify-center text-[#ffb800] 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-[#3b82f6]"
|
||||
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]"
|
||||
/>
|
||||
</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-[#3b82f6] 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-[#ffb800] 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-[#3b82f6] 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-[#ffb800] 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-[#3b82f6]"
|
||||
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]"
|
||||
>
|
||||
{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-[#3b82f6] 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-[#ffb800] 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-[#3b82f6] 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-[#ffb800] resize-none"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center gap-3 pt-1">
|
||||
|
||||
@@ -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-[#3b82f6] border-t-transparent rounded-full animate-spin" />
|
||||
<div className="w-6 h-6 border-2 border-[#ffb800] 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-[#3b82f6] hover:bg-[#2563eb] text-white text-sm font-semibold px-4 py-2 rounded-lg transition-colors"
|
||||
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"
|
||||
>
|
||||
<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-[#3b82f6] border-[#3b82f6]'
|
||||
? 'text-[#ffb800] border-[#ffb800]'
|
||||
: '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-[#3b82f6]"
|
||||
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]"
|
||||
/>
|
||||
</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-[#3b82f6]"
|
||||
className="bg-[#111827] border border-[#1f2937] text-[#ccc] text-sm rounded-lg px-3 py-2 focus:outline-none focus:border-[#ffb800]"
|
||||
>
|
||||
<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-[#3b82f6] border-t-transparent rounded-full animate-spin" />
|
||||
<div className="w-6 h-6 border-2 border-[#ffb800] 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-[#3b82f6] 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-[#ffb800] 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" />
|
||||
) : (
|
||||
@@ -418,7 +418,7 @@ export default function AdminUsersPage() {
|
||||
<button
|
||||
onClick={() => openEditRole(u)}
|
||||
disabled={processingIds.has(u.id)}
|
||||
className="text-[#555] hover:text-[#3b82f6] transition-colors text-xs font-medium px-2 py-1 rounded hover:bg-[#1f2937] disabled:opacity-50"
|
||||
className="text-[#555] hover:text-[#ffb800] 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-[#3b82f6] border-t-transparent rounded-full animate-spin" />
|
||||
<div className="w-6 h-6 border-2 border-[#ffb800] 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-[#3b82f6] hover:text-[#60a5fa] text-sm transition-colors"
|
||||
className="mt-3 text-[#ffb800] hover:text-[#ffd25a] text-sm transition-colors"
|
||||
>
|
||||
Send your first invitation →
|
||||
</button>
|
||||
@@ -491,7 +491,7 @@ 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-[#3b82f6] text-xs font-bold flex-shrink-0">
|
||||
<div className="w-7 h-7 rounded-full bg-[#1f2937] flex items-center justify-center text-[#ffb800] text-xs font-bold flex-shrink-0">
|
||||
{inv.email[0].toUpperCase()}
|
||||
</div>
|
||||
<span className="text-white text-sm">{inv.email}</span>
|
||||
@@ -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-[#3b82f6]"
|
||||
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]"
|
||||
/>
|
||||
</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-[#3b82f6]"
|
||||
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]"
|
||||
>
|
||||
<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-[#3b82f6] 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-[#ffb800] 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-[#3b82f6] hover:bg-[#2563eb] 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-[#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"
|
||||
>
|
||||
{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-[#3b82f6] 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-[#ffb800] 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-[#3b82f6] bg-[#3b82f6]/10'
|
||||
? 'border-[#ffb800] bg-[#ffb800]/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-[#3b82f6]' : 'border-[#444]'
|
||||
editRole === r.value ? 'border-[#ffb800]' : 'border-[#444]'
|
||||
}`}>
|
||||
{editRole === r.value && <span className="w-1.5 h-1.5 rounded-full bg-[#3b82f6]" />}
|
||||
{editRole === r.value && <span className="w-1.5 h-1.5 rounded-full bg-[#ffb800]" />}
|
||||
</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-[#3b82f6] hover:bg-[#2563eb] 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-[#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"
|
||||
>
|
||||
{editSaving ? (
|
||||
<>
|
||||
|
||||
Reference in New Issue
Block a user