Files
avbeat-com/src/app/admin/forum-dashboard/page.tsx
Local Administrator 8042024c4a feat(brand): AV BEAT 2026 rebrand — blue/navy/white identity
- New AvBeatLogo inline-SVG component (rounded-square emblem with
  forward-leaning AV monogram + horizontal beam detail) at
  src/components/AvBeatLogo.tsx, three variants (full/wordmark/icon).
- Header.tsx now uses the responsive AV BEAT logo lockup in the
  top-left, links to /, full lockup on desktop (emblem+wordmark+tagline),
  emblem+wordmark on tablet, emblem-only on mobile.
- Removed DualSpeedTicker (the 'RECENT FORUM POSTS' ticker strip);
  the news ticker / glow chrome does not match the new aesthetic and the
  forum row was the explicit removal target.
- Tailwind theme + globals.css now expose the AV BEAT 2026 palette as
  semantic tokens: --brand-blue (#1D4ED8), --brand-blue-bright (#38BDF8),
  --brand-navy (#0F172A), --brand-bg (#F8FAFC), --brand-surface, --brand-border,
  --brand-text, --brand-text-muted. Legacy --color-* aliases re-point to the
  new tokens so any inline-styled component re-themes for free.
- Site-wide hex sweep migrates 2,769 hardcoded color literals across 144
  files from the old dark-broadcast palette to the new tokens (orange
  -> blue, dark-brown -> white surface / navy text, cream -> navy).
- Layout body class flipped from 'bb-neon' to 'bg-brand-bg text-brand-text'
  so the dark glow chrome no longer leaks through the new light theme.
2026-06-03 12:07:18 +00:00

566 lines
26 KiB
TypeScript

'use client';
import React, { useState, useEffect, useCallback } from 'react';
import Link from 'next/link';
import { useAuth } from '@/contexts/AuthContext';
import { useRouter } from 'next/navigation';
import {
BarChart,
Bar,
XAxis,
YAxis,
CartesianGrid,
Tooltip,
ResponsiveContainer,
LineChart,
Line,
Legend,
} from 'recharts';
// ─── Types ────────────────────────────────────────────────────────────────────
interface WeeklyGrowth {
week: string;
threads: number;
replies: number;
}
interface TopCategory {
id: string;
name: string;
icon: string;
thread_count: number;
post_count: number;
}
interface ActiveUser {
author_id: string;
author_name: string;
threads: number;
replies: number;
total: number;
}
interface EngagementMetrics {
totalThreads: number;
totalReplies: number;
totalViews: number;
totalVotes: number;
avgRepliesPerThread: number;
engagementRate: number;
newThreadsWeek: number;
newRepliesWeek: number;
}
interface ModerationStats {
flaggedThreads: number;
lockedThreads: number;
pinnedThreads: number;
featuredThreads: number;
flaggedReplies: number;
hiddenReplies: number;
}
interface ForumStats {
weeklyGrowth: WeeklyGrowth[];
topCategories: TopCategory[];
activeUsers: ActiveUser[];
engagement: EngagementMetrics;
moderation: ModerationStats;
}
// ─── Helpers ──────────────────────────────────────────────────────────────────
function fmt(n: number): string {
if (n >= 1000000) return `${(n / 1000000).toFixed(1)}M`;
if (n >= 1000) return `${(n / 1000).toFixed(1)}K`;
return String(n);
}
// ─── Stat Card ────────────────────────────────────────────────────────────────
interface StatCardProps {
label: string;
value: number | string;
sub?: string;
accentColor: string;
bgColor: string;
icon: React.ReactNode;
badge?: { text: string; color: string };
}
function StatCard({ label, value, sub, accentColor, bgColor, icon, badge }: StatCardProps) {
return (
<div className="bg-[#111] border border-[#DCE6F2] rounded-lg p-5 flex flex-col gap-3 hover:border-[#333] transition-colors">
<div className="flex items-start justify-between">
<div className={`w-10 h-10 rounded-lg flex items-center justify-center ${bgColor}`}>
<span className={accentColor}>{icon}</span>
</div>
{badge && (
<span className={`text-xs font-body font-bold px-2 py-0.5 rounded-full ${badge.color}`}>
{badge.text}
</span>
)}
</div>
<div>
<p className="text-2xl font-display font-bold text-white">{value}</p>
<p className="text-sm text-[#888] font-body mt-0.5">{label}</p>
{sub && <p className="text-xs text-[#555] font-body mt-1">{sub}</p>}
</div>
</div>
);
}
// ─── Custom Tooltip ───────────────────────────────────────────────────────────
function ChartTooltip({ active, payload, label }: any) {
if (!active || !payload?.length) return null;
return (
<div className="bg-[#FFFFFF] border border-[#333] rounded-lg px-3 py-2 text-xs font-body shadow-xl">
{label && <p className="text-[#888] mb-1">{label}</p>}
{payload.map((entry: any, i: number) => (
<p key={i} style={{ color: entry.color }} className="leading-5">
{entry.name}: <span className="font-bold text-white">{entry.value}</span>
</p>
))}
</div>
);
}
// ─── Section Header ───────────────────────────────────────────────────────────
function SectionHeader({ title, subtitle, action }: { title: string; subtitle?: string; action?: React.ReactNode }) {
return (
<div className="mb-5 flex items-start justify-between">
<div>
<h2 className="text-white text-base font-bold font-display uppercase tracking-wider">{title}</h2>
{subtitle && <p className="text-[#555] text-xs font-body mt-0.5">{subtitle}</p>}
</div>
{action}
</div>
);
}
// ─── Main Page ────────────────────────────────────────────────────────────────
const EMPTY_STATS: ForumStats = {
weeklyGrowth: [],
topCategories: [],
activeUsers: [],
engagement: {
totalThreads: 0,
totalReplies: 0,
totalViews: 0,
totalVotes: 0,
avgRepliesPerThread: 0,
engagementRate: 0,
newThreadsWeek: 0,
newRepliesWeek: 0,
},
moderation: {
flaggedThreads: 0,
lockedThreads: 0,
pinnedThreads: 0,
featuredThreads: 0,
flaggedReplies: 0,
hiddenReplies: 0,
},
};
const CATEGORY_COLORS = ['#1D4ED8', '#f59e0b', '#10b981', '#8b5cf6', '#ef4444', '#06b6d4', '#f97316', '#84cc16'];
export default function ForumDashboardPage() {
const { user, loading } = useAuth();
const router = useRouter();
const [stats, setStats] = useState<ForumStats>(EMPTY_STATS);
const [loadingData, setLoadingData] = useState(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
if (!loading && !user) router.push('/login?next=' + encodeURIComponent(window.location.pathname));
}, [user, loading, router]);
const fetchStats = useCallback(async () => {
setLoadingData(true);
setError(null);
try {
const res = await fetch('/api/admin/forum-stats');
if (!res.ok) throw new Error('Failed to fetch forum stats');
const data = await res.json();
setStats(data);
} catch (err: any) {
setError(err.message ?? 'Unknown error');
} finally {
setLoadingData(false);
}
}, []);
useEffect(() => {
if (user) fetchStats();
}, [user, fetchStats]);
if (loading || loadingData) {
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-[#1D4ED8] border-t-transparent rounded-full animate-spin" />
<p className="text-[#555] text-sm font-body">Loading forum dashboard</p>
</div>
</div>
);
}
if (!user) return null;
const { engagement, moderation, weeklyGrowth, topCategories, activeUsers } = stats;
const totalFlagged = moderation.flaggedThreads + moderation.flaggedReplies;
return (
<div className="min-h-screen bg-[#0a0a0a]">
<div className="max-w-7xl mx-auto px-4 py-8">
{/* Header */}
<div className="mb-8 flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
<div>
<div className="flex items-center gap-2 mb-1">
<Link href="/admin" className="text-[#555] hover:text-white text-sm font-body transition-colors">Admin</Link>
<svg className="w-3.5 h-3.5 text-[#333]" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" d="M9 5l7 7-7 7" />
</svg>
<span className="text-[#888] text-sm font-body">Forum Dashboard</span>
</div>
<h1 className="text-2xl font-display font-bold text-white">Forum Dashboard</h1>
<p className="text-[#555] text-sm font-body mt-1">Thread growth, engagement, and moderation overview</p>
</div>
<div className="flex items-center gap-3">
<button
onClick={fetchStats}
className="flex items-center gap-2 px-3 py-2 bg-[#111] border border-[#DCE6F2] rounded-lg text-xs text-[#888] hover:text-white hover:border-[#333] transition-all font-body"
>
<svg className="w-3.5 h-3.5" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
</svg>
Refresh
</button>
<Link
href="/admin/forum-moderation"
className="flex items-center gap-2 px-3 py-2 bg-[#1D4ED8] 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" />
</svg>
Moderation
</Link>
</div>
</div>
{/* Error Banner */}
{error && (
<div className="mb-6 bg-red-500/10 border border-red-500/30 rounded-lg px-4 py-3 flex items-center gap-3">
<svg className="w-4 h-4 text-red-400 flex-shrink-0" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24">
<circle cx="12" cy="12" r="10" /><path strokeLinecap="round" strokeLinejoin="round" d="M12 8v4m0 4h.01" />
</svg>
<p className="text-red-400 text-sm font-body">{error}</p>
<button onClick={fetchStats} className="ml-auto text-xs text-red-400 hover:text-red-300 underline font-body">Retry</button>
</div>
)}
{/* Flagged Content Alert */}
{totalFlagged > 0 && (
<div className="mb-6 bg-red-500/10 border border-red-500/30 rounded-lg px-4 py-3 flex items-center gap-3">
<svg className="w-4 h-4 text-red-400 flex-shrink-0" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" d="M3 21v-4m0 0V5a2 2 0 012-2h6.5l1 1H21l-3 6 3 6h-8.5l-1-1H5a2 2 0 00-2 2zm9-13.5V9" />
</svg>
<p className="text-red-400 text-sm font-body">
<span className="font-bold">{totalFlagged} flagged item{totalFlagged !== 1 ? 's' : ''}</span> require moderation review
</p>
<Link href="/admin/forum-moderation" className="ml-auto text-xs text-red-400 hover:text-red-300 underline font-body">Review now </Link>
</div>
)}
{/* Engagement Metric Cards */}
<section className="mb-8">
<h2 className="text-xs font-body font-bold text-[#555] uppercase tracking-widest mb-4">Engagement Metrics</h2>
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
<StatCard
label="Total Threads"
value={fmt(engagement.totalThreads)}
sub={`+${engagement.newThreadsWeek} this week`}
accentColor="text-blue-400"
bgColor="bg-blue-500/10"
icon={
<svg className="w-5 h-5" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" d="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z" />
</svg>
}
/>
<StatCard
label="Total Replies"
value={fmt(engagement.totalReplies)}
sub={`+${engagement.newRepliesWeek} this week`}
accentColor="text-green-400"
bgColor="bg-green-500/10"
icon={
<svg className="w-5 h-5" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" d="M3 10h10a8 8 0 018 8v2M3 10l6 6m-6-6l6-6" />
</svg>
}
/>
<StatCard
label="Total Views"
value={fmt(engagement.totalViews)}
sub="Across all threads"
accentColor="text-purple-400"
bgColor="bg-purple-500/10"
icon={
<svg className="w-5 h-5" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
<path strokeLinecap="round" strokeLinejoin="round" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z" />
</svg>
}
/>
<StatCard
label="Total Votes"
value={fmt(engagement.totalVotes)}
sub="Upvotes cast"
accentColor="text-amber-400"
bgColor="bg-amber-500/10"
icon={
<svg className="w-5 h-5" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" d="M5 15l7-7 7 7" />
</svg>
}
/>
<StatCard
label="Avg Replies/Thread"
value={engagement.avgRepliesPerThread}
sub="Discussion depth"
accentColor="text-cyan-400"
bgColor="bg-cyan-500/10"
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 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z" />
</svg>
}
/>
<StatCard
label="Engagement Rate"
value={`${engagement.engagementRate}%`}
sub="Threads with replies"
accentColor="text-pink-400"
bgColor="bg-pink-500/10"
icon={
<svg className="w-5 h-5" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" d="M13 7h8m0 0v8m0-8l-8 8-4-4-6 6" />
</svg>
}
/>
<StatCard
label="Flagged Content"
value={totalFlagged}
sub={`${moderation.flaggedThreads} threads · ${moderation.flaggedReplies} replies`}
accentColor="text-red-400"
bgColor="bg-red-500/10"
badge={totalFlagged > 0 ? { text: 'Needs review', color: 'text-red-400 bg-red-400/10' } : undefined}
icon={
<svg className="w-5 h-5" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" d="M3 21v-4m0 0V5a2 2 0 012-2h6.5l1 1H21l-3 6 3 6h-8.5l-1-1H5a2 2 0 00-2 2zm9-13.5V9" />
</svg>
}
/>
<StatCard
label="Locked Threads"
value={moderation.lockedThreads}
sub={`${moderation.pinnedThreads} pinned · ${moderation.featuredThreads} featured`}
accentColor="text-orange-400"
bgColor="bg-orange-500/10"
icon={
<svg className="w-5 h-5" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z" />
</svg>
}
/>
</div>
</section>
{/* Thread Growth Chart */}
<section className="mb-8">
<div className="bg-[#111] border border-[#DCE6F2] rounded-lg p-6">
<SectionHeader
title="Thread Growth"
subtitle="Weekly new threads and replies over the past 8 weeks"
/>
{weeklyGrowth.length === 0 ? (
<div className="h-48 flex items-center justify-center">
<p className="text-[#555] text-sm font-body">No data available</p>
</div>
) : (
<ResponsiveContainer width="100%" height={240}>
<LineChart data={weeklyGrowth} margin={{ top: 5, right: 10, left: -20, bottom: 5 }}>
<CartesianGrid strokeDasharray="3 3" stroke="#FFFFFF" />
<XAxis dataKey="week" tick={{ fill: '#555', fontSize: 11 }} axisLine={false} tickLine={false} />
<YAxis tick={{ fill: '#555', fontSize: 11 }} axisLine={false} tickLine={false} />
<Tooltip content={<ChartTooltip />} />
<Legend
wrapperStyle={{ fontSize: '11px', color: '#888', paddingTop: '12px' }}
iconType="circle"
iconSize={8}
/>
<Line type="monotone" dataKey="threads" stroke="#1D4ED8" strokeWidth={2} dot={{ fill: '#1D4ED8', r: 3 }} name="New Threads" />
<Line type="monotone" dataKey="replies" stroke="#10b981" strokeWidth={2} dot={{ fill: '#10b981', r: 3 }} name="New Replies" />
</LineChart>
</ResponsiveContainer>
)}
</div>
</section>
{/* Two-column: Top Categories + Active Users */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-8">
{/* Top Categories */}
<div className="bg-[#111] border border-[#DCE6F2] rounded-lg overflow-hidden">
<div className="px-5 py-4 border-b border-[#FFFFFF] flex items-center justify-between">
<div>
<h2 className="text-sm font-display font-bold text-white">Top Categories</h2>
<p className="text-xs text-[#555] font-body mt-0.5">By thread count</p>
</div>
<Link href="/forum" className="text-xs text-[#1D4ED8] hover:text-blue-300 font-body transition-colors">View forum </Link>
</div>
{topCategories.length === 0 ? (
<div className="px-5 py-10 text-center">
<p className="text-[#555] text-sm font-body">No categories found</p>
</div>
) : (
<div className="divide-y divide-[#FFFFFF]">
{topCategories.map((cat, idx) => {
const maxThreads = topCategories[0]?.thread_count ?? 1;
const pct = maxThreads > 0 ? Math.round((cat.thread_count / maxThreads) * 100) : 0;
const color = CATEGORY_COLORS[idx % CATEGORY_COLORS.length];
return (
<div key={cat.id} className="px-5 py-3 hover:bg-[#F8FAFC] transition-colors">
<div className="flex items-center gap-3 mb-1.5">
<span className="text-base leading-none">{cat.icon}</span>
<span className="text-sm text-white font-body flex-1 truncate">{cat.name}</span>
<span className="text-xs text-[#888] font-body font-semibold">{cat.thread_count} threads</span>
<span className="text-xs text-[#555] font-body">{cat.post_count} posts</span>
</div>
<div className="h-1.5 bg-[#FFFFFF] rounded-full overflow-hidden ml-7">
<div
className="h-full rounded-full transition-all duration-500"
style={{ width: `${pct}%`, backgroundColor: color }}
/>
</div>
</div>
);
})}
</div>
)}
</div>
{/* Active Users */}
<div className="bg-[#111] border border-[#DCE6F2] rounded-lg overflow-hidden">
<div className="px-5 py-4 border-b border-[#FFFFFF]">
<h2 className="text-sm font-display font-bold text-white">Active Users</h2>
<p className="text-xs text-[#555] font-body mt-0.5">Most active in the last 30 days</p>
</div>
{activeUsers.length === 0 ? (
<div className="px-5 py-10 text-center">
<p className="text-[#555] text-sm font-body">No active users found</p>
</div>
) : (
<div className="divide-y divide-[#FFFFFF]">
{activeUsers.map((u, idx) => (
<div key={u.author_id} className="px-5 py-3 flex items-center gap-3 hover:bg-[#F8FAFC] transition-colors">
<div className="w-6 h-6 rounded-full bg-[#FFFFFF] 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-[#1D4ED8] 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>
</div>
<div className="flex-1 min-w-0">
<p className="text-sm text-white font-body truncate">{u.author_name}</p>
<p className="text-xs text-[#555] font-body">{u.threads} threads · {u.replies} replies</p>
</div>
<div className="text-right flex-shrink-0">
<p className="text-sm font-bold text-white font-body">{u.total}</p>
<p className="text-xs text-[#555] font-body">posts</p>
</div>
</div>
))}
</div>
)}
</div>
</div>
{/* Category Bar Chart */}
{topCategories.length > 0 && (
<section className="mb-8">
<div className="bg-[#111] border border-[#DCE6F2] rounded-lg p-6">
<SectionHeader
title="Category Activity"
subtitle="Thread and post distribution across categories"
/>
<ResponsiveContainer width="100%" height={220}>
<BarChart data={topCategories} margin={{ top: 5, right: 10, left: -20, bottom: 5 }}>
<CartesianGrid strokeDasharray="3 3" stroke="#FFFFFF" />
<XAxis
dataKey="name"
tick={{ fill: '#555', fontSize: 10 }}
axisLine={false}
tickLine={false}
interval={0}
angle={-25}
textAnchor="end"
height={50}
/>
<YAxis tick={{ fill: '#555', fontSize: 11 }} axisLine={false} tickLine={false} />
<Tooltip content={<ChartTooltip />} />
<Legend
wrapperStyle={{ fontSize: '11px', color: '#888', paddingTop: '8px' }}
iconType="circle"
iconSize={8}
/>
<Bar dataKey="thread_count" fill="#1D4ED8" radius={[3, 3, 0, 0]} name="Threads" />
<Bar dataKey="post_count" fill="#10b981" radius={[3, 3, 0, 0]} name="Posts" />
</BarChart>
</ResponsiveContainer>
</div>
</section>
)}
{/* Moderation Stats */}
<section className="mb-8">
<div className="bg-[#111] border border-[#DCE6F2] rounded-lg overflow-hidden">
<div className="px-5 py-4 border-b border-[#FFFFFF] flex items-center justify-between">
<div>
<h2 className="text-sm font-display font-bold text-white">Moderation Stats</h2>
<p className="text-xs text-[#555] font-body mt-0.5">Current moderation state across all forum content</p>
</div>
<Link href="/admin/forum-moderation" className="text-xs text-[#1D4ED8] 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-[#FFFFFF]">
{[
{ label: 'Flagged Threads', value: moderation.flaggedThreads, color: 'text-red-400', bg: 'bg-red-500/10' },
{ label: 'Flagged Replies', value: moderation.flaggedReplies, color: 'text-red-400', bg: 'bg-red-500/10' },
{ label: 'Locked Threads', value: moderation.lockedThreads, color: 'text-orange-400', bg: 'bg-orange-500/10' },
{ label: 'Pinned Threads', value: moderation.pinnedThreads, color: 'text-yellow-400', bg: 'bg-yellow-500/10' },
{ label: 'Featured Threads', value: moderation.featuredThreads, color: 'text-blue-400', bg: 'bg-blue-500/10' },
{ label: 'Hidden Replies', value: moderation.hiddenReplies, color: 'text-[#888]', bg: 'bg-[#FFFFFF]' },
].map(({ label, value, color, bg }) => (
<div key={label} className="px-4 py-5 flex flex-col items-center gap-1 text-center">
<p className={`text-2xl font-bold font-display ${color}`}>{value}</p>
<p className="text-xs text-[#555] font-body leading-tight">{label}</p>
</div>
))}
</div>
</div>
</section>
</div>
</div>
);
}