Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,13 @@ ADMIN_SECRET=""
ADMIN_PASSWORD=""
ADMIN_EMAIL=""

# ── Seed / test bypass ──────────────────────────────────────────────────────
# Guards POST /api/admin/seed (loads the official 774 LGAs) and lets test
# scripts create LGAs past the 774-total cap on POST /api/lga/register by
# sending it as an `x-seed-secret` header. Leave unset in production once
# real chairmen are registering — there's no legitimate reason to exceed 774.
SEED_SECRET=""

# ── File uploads (Cloudinary) ───────────────────────────────────────────────
# Create a free account at https://cloudinary.com and copy these from your
# dashboard. Used for project images/video and press release attachments —
Expand Down Expand Up @@ -54,4 +61,3 @@ CONTACT_EMAIL=""
# ── Misc ─────────────────────────────────────────────────────────────────────
NEXT_PUBLIC_APP_URL="http://localhost:3000"
OPENAI_API_KEY=""
SEED_SECRET=""
82 changes: 41 additions & 41 deletions app/(admin)/admin/analytics/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -43,24 +43,24 @@ function StatCard({ icon: Icon, label, value, sub, color = "text-green-600" }: {
sub?: string; color?: string;
}) {
return (
<div className="bg-white/5 border border-white/10 rounded-2xl p-5">
<div className="bg-white border border-slate-200 rounded-2xl p-5 shadow-sm">
<div className="flex items-center gap-3 mb-3">
<div className="h-9 w-9 rounded-xl bg-green-600/20 flex items-center justify-center">
<div className="h-9 w-9 rounded-xl bg-green-100 flex items-center justify-center">
<Icon className={`h-4 w-4 ${color}`} />
</div>
<p className="text-sm text-green-200/70">{label}</p>
<p className="text-sm text-slate-500">{label}</p>
</div>
<p className="text-3xl font-extrabold text-white">{value}</p>
{sub && <p className="text-xs text-green-300/50 mt-1">{sub}</p>}
<p className="text-3xl font-extrabold text-slate-900">{value}</p>
{sub && <p className="text-xs text-slate-400 mt-1">{sub}</p>}
</div>
);
}

function nairaK(koboStr: string) {
const naira = (koboStr && koboStr !== "0" ? Number(BigInt(koboStr)) : 0) / 100;
if (naira >= 1_000_000) return `₦${(naira / 1_000_000).toFixed(1)}M`;
if (naira >= 1_000) return `₦${(naira / 1_000).toFixed(1)}K`;
return `₦${naira.toFixed(2)}`;
if (naira >= 1_000_000) return `${(naira / 1_000_000).toFixed(1)}M`;
if (naira >= 1_000) return `${(naira / 1_000).toFixed(1)}K`;
return `${naira.toFixed(2)}`;
}

export default function AdminAnalyticsPage() {
Expand All @@ -78,14 +78,14 @@ export default function AdminAnalyticsPage() {
if (loading) {
return (
<div className="max-w-6xl mx-auto px-4 sm:px-6 py-8 space-y-4">
{[0,1,2].map((i) => <div key={i} className="h-32 bg-white/5 rounded-2xl animate-pulse" />)}
{[0,1,2].map((i) => <div key={i} className="h-32 bg-slate-100 rounded-2xl animate-pulse" />)}
</div>
);
}

if (!data) {
return (
<div className="max-w-6xl mx-auto px-4 sm:px-6 py-8 text-center text-green-200/60">
<div className="max-w-6xl mx-auto px-4 sm:px-6 py-8 text-center text-slate-400">
Failed to load analytics.
</div>
);
Expand All @@ -102,10 +102,10 @@ export default function AdminAnalyticsPage() {
<div className="max-w-6xl mx-auto px-4 sm:px-6 py-8">
<div className="mb-6">
<div className="flex items-center gap-2 mb-1">
<Activity className="h-5 w-5 text-green-400" />
<h1 className="text-2xl font-bold text-white">Platform Analytics</h1>
<Activity className="h-5 w-5 text-green-600" />
<h1 className="text-2xl font-bold text-slate-900">Platform Analytics</h1>
</div>
<p className="text-green-200/60 text-sm">Real-time overview of platform activity</p>
<p className="text-slate-500 text-sm">Real-time overview of platform activity</p>
</div>

{/* Key metrics */}
Expand All @@ -123,22 +123,22 @@ export default function AdminAnalyticsPage() {
{/* Charts */}
<div className="grid lg:grid-cols-2 gap-6 mb-8">
{/* Monthly revenue bar chart */}
<div className="bg-white/5 border border-white/10 rounded-2xl p-5">
<h2 className="text-sm font-semibold text-green-200/80 mb-4">Monthly Revenue (₦)</h2>
<div className="bg-white border border-slate-200 rounded-2xl p-5 shadow-sm">
<h2 className="text-sm font-semibold text-slate-700 mb-4">Monthly Revenue ()</h2>
{revenueChartData.length === 0 ? (
<div className="h-48 flex items-center justify-center text-sm text-green-200/40">
<div className="h-48 flex items-center justify-center text-sm text-slate-400">
No revenue data yet
</div>
) : (
<ResponsiveContainer width="100%" height={200}>
<BarChart data={revenueChartData} margin={{ top: 0, right: 8, bottom: 0, left: -20 }}>
<CartesianGrid strokeDasharray="3 3" stroke="rgba(255,255,255,0.06)" />
<XAxis dataKey="month" tick={{ fontSize: 10, fill: "#6ee7b7" }} />
<YAxis tick={{ fontSize: 10, fill: "#6ee7b7" }}
tickFormatter={(v) => `₦${(Number(v) / 1000).toFixed(0)}K`} />
<CartesianGrid strokeDasharray="3 3" stroke="#f1f5f9" />
<XAxis dataKey="month" tick={{ fontSize: 10, fill: "#94a3b8" }} />
<YAxis tick={{ fontSize: 10, fill: "#94a3b8" }}
tickFormatter={(v) => `${(Number(v) / 1000).toFixed(0)}K`} />
<Tooltip
contentStyle={{ background: "#0f2d1a", border: "1px solid rgba(255,255,255,0.1)", borderRadius: 12, color: "#fff" }}
formatter={(v) => [`₦${Number(v).toLocaleString()}`, "Revenue"]}
contentStyle={{ background: "#fff", border: "1px solid #e2e8f0", borderRadius: 12, color: "#0f172a" }}
formatter={(v) => [`${Number(v).toLocaleString()}`, "Revenue"]}
/>
<Bar dataKey="revenue" fill="#15803d" radius={[4,4,0,0]} />
</BarChart>
Expand All @@ -147,8 +147,8 @@ export default function AdminAnalyticsPage() {
</div>

{/* LGA status pie */}
<div className="bg-white/5 border border-white/10 rounded-2xl p-5">
<h2 className="text-sm font-semibold text-green-200/80 mb-4">LGA Status Breakdown</h2>
<div className="bg-white border border-slate-200 rounded-2xl p-5 shadow-sm">
<h2 className="text-sm font-semibold text-slate-700 mb-4">LGA Status Breakdown</h2>
<ResponsiveContainer width="100%" height={200}>
<PieChart>
<Pie data={lgaStatusData} dataKey="value" nameKey="name"
Expand All @@ -158,7 +158,7 @@ export default function AdminAnalyticsPage() {
))}
</Pie>
<Tooltip
contentStyle={{ background: "#0f2d1a", border: "1px solid rgba(255,255,255,0.1)", borderRadius: 12, color: "#fff" }}
contentStyle={{ background: "#fff", border: "1px solid #e2e8f0", borderRadius: 12, color: "#0f172a" }}
formatter={(v, name) => [v, name]}
/>
</PieChart>
Expand All @@ -167,37 +167,37 @@ export default function AdminAnalyticsPage() {
</div>

{/* User role breakdown */}
<div className="bg-white/5 border border-white/10 rounded-2xl p-5 mb-6">
<h2 className="text-sm font-semibold text-green-200/80 mb-4">User Breakdown by Role</h2>
<div className="bg-white border border-slate-200 rounded-2xl p-5 mb-6 shadow-sm">
<h2 className="text-sm font-semibold text-slate-700 mb-4">User Breakdown by Role</h2>
<div className="grid grid-cols-2 sm:grid-cols-5 gap-3">
{userRoleData.map(({ name, value }) => (
<div key={name} className="text-center p-3 rounded-xl bg-white/5">
<p className="text-xl font-bold text-white">{value.toLocaleString()}</p>
<p className="text-xs text-green-200/50 mt-1">{name.replace("_", " ")}</p>
<div key={name} className="text-center p-3 rounded-xl bg-slate-50">
<p className="text-xl font-bold text-slate-900">{value.toLocaleString()}</p>
<p className="text-xs text-slate-500 mt-1">{name.replace("_", " ")}</p>
</div>
))}
</div>
</div>

{/* Top LGAs by posts */}
<div className="bg-white/5 border border-white/10 rounded-2xl overflow-hidden">
<div className="px-5 py-4 border-b border-white/10 flex items-center gap-2">
<TrendingUp className="h-4 w-4 text-green-400" />
<h2 className="text-sm font-semibold text-green-200/80">Top LGAs by Content</h2>
<div className="bg-white border border-slate-200 rounded-2xl overflow-hidden shadow-sm">
<div className="px-5 py-4 border-b border-slate-100 flex items-center gap-2">
<TrendingUp className="h-4 w-4 text-green-600" />
<h2 className="text-sm font-semibold text-slate-700">Top LGAs by Content</h2>
</div>
{data.topLgasByPosts.length === 0 ? (
<div className="py-10 text-center text-sm text-green-200/40">No data yet</div>
<div className="py-10 text-center text-sm text-slate-400">No data yet</div>
) : (
<div className="divide-y divide-white/5">
<div className="divide-y divide-slate-100">
{data.topLgasByPosts.map((lga, idx) => (
<div key={lga.id} className="flex items-center gap-4 px-5 py-4">
<span className="text-sm font-bold text-green-400/60 w-5 text-center">{idx + 1}</span>
<span className="text-sm font-bold text-green-600/70 w-5 text-center">{idx + 1}</span>
<div className="flex-1 min-w-0">
<p className="text-sm font-semibold text-white truncate">{lga.lgaName} LGA</p>
<p className="text-xs text-green-200/40 mt-0.5">{lga.state} · {lga.status}</p>
<p className="text-sm font-semibold text-slate-900 truncate">{lga.lgaName} LGA</p>
<p className="text-xs text-slate-400 mt-0.5">{lga.state} · {lga.status}</p>
</div>
<div className="flex items-center gap-1 text-sm font-bold text-green-300">
<FileText className="h-3.5 w-3.5 text-green-400/60" />
<div className="flex items-center gap-1 text-sm font-bold text-green-700">
<FileText className="h-3.5 w-3.5 text-green-600/70" />
{lga._count.posts}
</div>
</div>
Expand Down
16 changes: 8 additions & 8 deletions app/(admin)/admin/audit-reports/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -119,10 +119,10 @@ export default function AdminAuditReportsPage() {
<div className="mb-6 flex items-start justify-between gap-4">
<div>
<div className="flex items-center gap-2 mb-1">
<ClipboardList className="h-5 w-5 text-green-400" />
<h1 className="text-2xl font-bold text-white">Audit Reports</h1>
<ClipboardList className="h-5 w-5 text-green-600" />
<h1 className="text-2xl font-bold text-slate-900">Audit Reports</h1>
</div>
<p className="text-green-200/60 text-sm">{total.toLocaleString()} audit report records</p>
<p className="text-slate-500 text-sm">{total.toLocaleString()} audit report records</p>
</div>
<button
onClick={() => setShowForm(true)}
Expand All @@ -138,7 +138,7 @@ export default function AdminAuditReportsPage() {
<input
type="text" placeholder="Search LGA or report title…"
value={search} onChange={(e) => setSearch(e.target.value)}
className="w-full pl-9 pr-4 py-2.5 rounded-xl border border-white/20 bg-white/10 text-white placeholder:text-green-200/40 text-sm focus:outline-none focus:border-green-400"
className="w-full pl-9 pr-4 py-2.5 rounded-xl border border-slate-200 bg-white text-slate-800 placeholder:text-slate-400 text-sm focus:outline-none focus:border-green-400"
/>
</div>

Expand Down Expand Up @@ -192,13 +192,13 @@ export default function AdminAuditReportsPage() {

{/* Pagination */}
{Math.ceil(total / PAGE) > 1 && (
<div className="mt-5 flex items-center justify-between text-white">
<p className="text-sm text-green-200/60">Showing {page * PAGE + 1}–{Math.min((page + 1) * PAGE, total)} of {total}</p>
<div className="mt-5 flex items-center justify-between">
<p className="text-sm text-slate-500">Showing {page * PAGE + 1}–{Math.min((page + 1) * PAGE, total)} of {total}</p>
<div className="flex gap-2">
<button onClick={() => setPage((p) => Math.max(0, p - 1))} disabled={page === 0}
className="px-4 py-2 rounded-xl border border-white/20 text-sm disabled:opacity-40 hover:bg-white/10">← Prev</button>
className="px-4 py-2 rounded-xl border border-slate-200 text-slate-600 text-sm disabled:opacity-40 hover:bg-slate-50">← Prev</button>
<button onClick={() => setPage((p) => p + 1)} disabled={(page + 1) * PAGE >= total}
className="px-4 py-2 rounded-xl border border-white/20 text-sm disabled:opacity-40 hover:bg-white/10">Next →</button>
className="px-4 py-2 rounded-xl border border-slate-200 text-slate-600 text-sm disabled:opacity-40 hover:bg-slate-50">Next →</button>
</div>
</div>
)}
Expand Down
2 changes: 1 addition & 1 deletion app/(admin)/admin/lgas/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -637,7 +637,7 @@ export default function AdminLGAsPage() {
<div className="flex gap-2">
<button onClick={() => setPage((p) => Math.max(0, p - 1))} disabled={page === 0}
className="px-4 py-2 rounded-xl border border-slate-200 text-sm disabled:opacity-40 hover:border-green-400 transition-colors">
 Prev
← Prev
</button>
<button onClick={() => setPage((p) => p + 1)} disabled={(page + 1) * PAGE >= total}
className="px-4 py-2 rounded-xl border border-slate-200 text-sm disabled:opacity-40 hover:border-green-400 transition-colors">
Expand Down
6 changes: 3 additions & 3 deletions app/(admin)/admin/live-streams/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -125,15 +125,15 @@ export default function AdminLiveStreamsPage() {
<div className="mb-6 flex items-start justify-between gap-4">
<div>
<div className="flex items-center gap-2 mb-1">
<Radio className="h-5 w-5 text-green-400" />
<h1 className="text-2xl font-bold text-white">Live Streams</h1>
<Radio className="h-5 w-5 text-green-600" />
<h1 className="text-2xl font-bold text-slate-900">Live Streams</h1>
{liveCount > 0 && (
<span className="flex items-center gap-1 px-2 py-0.5 rounded-full bg-red-500 text-white text-xs font-bold">
<span className="h-1.5 w-1.5 rounded-full bg-white animate-pulse" /> {liveCount} LIVE
</span>
)}
</div>
<p className="text-green-200/60 text-sm">{streams.length} total streams</p>
<p className="text-slate-500 text-sm">{streams.length} total streams</p>
</div>
<button onClick={() => setShowForm(true)}
className="flex items-center gap-2 px-4 py-2.5 rounded-xl bg-green-600 hover:bg-green-700 text-white text-sm font-semibold">
Expand Down
18 changes: 9 additions & 9 deletions app/(admin)/admin/press-releases/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -155,13 +155,13 @@ export default function AdminPressReleasesPage() {
<div className="mb-6 flex items-start justify-between gap-4">
<div className="min-w-0">
<div className="flex items-center gap-2 mb-1">
<Newspaper className="h-5 w-5 text-green-400 shrink-0" />
<h1 className="text-2xl font-bold text-white">Press Releases</h1>
<Newspaper className="h-5 w-5 text-green-600 shrink-0" />
<h1 className="text-2xl font-bold text-slate-900">Press Releases</h1>
{pendingCount > 0 && (
<span className="px-2 py-0.5 rounded-full bg-amber-500 text-white text-xs font-bold">{pendingCount} pending</span>
)}
</div>
<p className="text-green-200/60 text-sm">{total.toLocaleString()} total records</p>
<p className="text-slate-500 text-sm">{total.toLocaleString()} total records</p>
</div>
<button onClick={() => setShowForm(true)}
className="flex items-center gap-2 px-4 py-2.5 rounded-xl bg-green-600 hover:bg-green-700 text-white text-sm font-semibold shrink-0">
Expand All @@ -174,13 +174,13 @@ export default function AdminPressReleasesPage() {
<div className="relative flex-1 min-w-[200px]">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-slate-400" />
<input type="text" placeholder="Search title or entity…" value={q} onChange={(e) => setQ(e.target.value)}
className="w-full pl-9 pr-4 py-2.5 rounded-xl border border-white/20 bg-white/10 text-white placeholder:text-green-200/40 text-sm focus:outline-none focus:border-green-400" />
className="w-full pl-9 pr-4 py-2.5 rounded-xl border border-slate-200 bg-white text-slate-800 placeholder:text-slate-400 text-sm focus:outline-none focus:border-green-400" />
</div>
<div className="flex gap-2 flex-wrap">
{(["", "PENDING", "PUBLISHED", "REJECTED"] as const).map((s) => (
<button key={s} onClick={() => { setStatusFilter(s); setPage(0); }}
className={`px-3 py-2 rounded-xl text-xs font-semibold border transition-colors ${
statusFilter === s ? "bg-green-600 text-white border-green-600" : "border-white/20 text-green-200/70 hover:bg-white/10"
statusFilter === s ? "bg-green-600 text-white border-green-600" : "border-slate-200 text-slate-500 hover:bg-slate-50"
}`}>{s === "" ? "All" : s.charAt(0) + s.slice(1).toLowerCase()}</button>
))}
</div>
Expand Down Expand Up @@ -265,13 +265,13 @@ export default function AdminPressReleasesPage() {

{/* Pagination */}
{Math.ceil(total / PAGE) > 1 && (
<div className="mt-5 flex items-center justify-between text-white">
<p className="text-sm text-green-200/60">Showing {page * PAGE + 1}–{Math.min((page + 1) * PAGE, total)} of {total}</p>
<div className="mt-5 flex items-center justify-between">
<p className="text-sm text-slate-500">Showing {page * PAGE + 1}–{Math.min((page + 1) * PAGE, total)} of {total}</p>
<div className="flex gap-2">
<button onClick={() => setPage((p) => Math.max(0, p - 1))} disabled={page === 0}
className="px-4 py-2 rounded-xl border border-white/20 text-sm disabled:opacity-40 hover:bg-white/10">← Prev</button>
className="px-4 py-2 rounded-xl border border-slate-200 text-slate-600 text-sm disabled:opacity-40 hover:bg-slate-50">← Prev</button>
<button onClick={() => setPage((p) => p + 1)} disabled={(page + 1) * PAGE >= total}
className="px-4 py-2 rounded-xl border border-white/20 text-sm disabled:opacity-40 hover:bg-white/10">Next →</button>
className="px-4 py-2 rounded-xl border border-slate-200 text-slate-600 text-sm disabled:opacity-40 hover:bg-slate-50">Next →</button>
</div>
</div>
)}
Expand Down
Loading
Loading