'use client'; import { useEffect, useMemo, useState } from 'react'; import Link from 'next/link'; type AdminUser = { id: string; handle: string; displayName?: string | null; email?: string | null; isSuspended: boolean; isSilenced: boolean; suspensionReason?: string | null; silenceReason?: string | null; createdAt: string; }; type AdminPost = { id: string; content: string; createdAt: string; isRemoved: boolean; removedReason?: string | null; author: { id: string; handle: string; displayName?: string | null; }; }; type Report = { id: string; targetType: 'post' | 'user'; targetId: string; reason: string; status: 'open' | 'resolved'; createdAt: string; reporter?: { id: string; handle: string; } | null; target?: AdminPost | AdminUser | null; }; const formatDate = (value: string) => { const date = new Date(value); return date.toLocaleString(); }; export default function AdminPage() { const [isAdmin, setIsAdmin] = useState(null); const [tab, setTab] = useState<'reports' | 'posts' | 'users' | 'settings'>('reports'); const [reports, setReports] = useState([]); const [posts, setPosts] = useState([]); const [users, setUsers] = useState([]); const [loading, setLoading] = useState(false); const [reportStatus, setReportStatus] = useState<'open' | 'resolved' | 'all'>('open'); const [nodeSettings, setNodeSettings] = useState({ name: '', description: '', longDescription: '', rules: '', bannerUrl: '', accentColor: '#00D4AA', }); const [savingSettings, setSavingSettings] = useState(false); const [isUploadingBanner, setIsUploadingBanner] = useState(false); const [bannerUploadError, setBannerUploadError] = useState(null); useEffect(() => { fetch('/api/admin/me') .then((res) => res.json()) .then((data) => setIsAdmin(!!data.isAdmin)) .catch(() => setIsAdmin(false)); }, []); const loadReports = async () => { setLoading(true); try { const res = await fetch(`/api/admin/reports?status=${reportStatus}`); const data = await res.json(); setReports(data.reports || []); } catch { setReports([]); } finally { setLoading(false); } }; const loadPosts = async () => { setLoading(true); try { const res = await fetch('/api/admin/posts?status=all'); const data = await res.json(); setPosts(data.posts || []); } catch { setPosts([]); } finally { setLoading(false); } }; const loadUsers = async () => { setLoading(true); try { const res = await fetch('/api/admin/users'); const data = await res.json(); setUsers(data.users || []); } catch { setUsers([]); } finally { setLoading(false); } }; const loadNodeSettings = async () => { setLoading(true); try { const res = await fetch('/api/node'); const data = await res.json(); setNodeSettings({ name: data.name || '', description: data.description || '', longDescription: data.longDescription || '', rules: data.rules || '', bannerUrl: data.bannerUrl || '', accentColor: data.accentColor || '#00D4AA', }); } catch { // error } finally { setLoading(false); } }; useEffect(() => { if (!isAdmin) return; if (tab === 'reports') loadReports(); if (tab === 'posts') loadPosts(); if (tab === 'users') loadUsers(); if (tab === 'settings') loadNodeSettings(); }, [tab, isAdmin, reportStatus]); const handleReportResolve = async (id: string, status: 'open' | 'resolved') => { const note = status === 'resolved' ? window.prompt('Resolution note (optional):') || '' : ''; await fetch(`/api/admin/reports/${id}`, { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ status, note }), }); loadReports(); }; const handlePostAction = async (id: string, action: 'remove' | 'restore') => { const reason = action === 'remove' ? window.prompt('Reason (optional):') || '' : ''; await fetch(`/api/admin/posts/${id}`, { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ action, reason }), }); if (tab === 'reports') { loadReports(); } else { loadPosts(); } }; const handleSaveSettings = async (override?: typeof nodeSettings) => { const payload = override ?? nodeSettings; setSavingSettings(true); try { const res = await fetch('/api/admin/node', { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload), }); if (res.ok) { alert('Settings saved!'); } else { alert('Failed to save settings.'); } } catch { alert('Failed to save settings.'); } finally { setSavingSettings(false); } }; const handleBannerUpload = async (event: React.ChangeEvent) => { const file = event.target.files?.[0]; event.target.value = ''; if (!file) return; setBannerUploadError(null); setIsUploadingBanner(true); try { const formData = new FormData(); formData.append('file', file); const res = await fetch('/api/media/upload', { method: 'POST', body: formData, }); const data = await res.json(); if (!res.ok || !data.url) { throw new Error(data.error || 'Upload failed'); } const nextSettings = { ...nodeSettings, bannerUrl: data.media?.url || data.url, }; setNodeSettings(nextSettings); await handleSaveSettings(nextSettings); } catch (error) { console.error('Banner upload failed', error); setBannerUploadError('Upload failed. Please try again.'); } finally { setIsUploadingBanner(false); } }; const handleUserAction = async (id: string, action: 'suspend' | 'unsuspend' | 'silence' | 'unsilence') => { const needsReason = action === 'suspend' || action === 'silence'; const reason = needsReason ? window.prompt('Reason (optional):') || '' : ''; await fetch(`/api/admin/users/${id}`, { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ action, reason }), }); loadUsers(); }; const reportCounts = useMemo(() => { return { open: reports.filter((r) => r.status === 'open').length, resolved: reports.filter((r) => r.status === 'resolved').length, }; }, [reports]); if (isAdmin === null) { return (
Checking permissions...
); } if (!isAdmin) { return (

Moderation

You do not have access to this page.

Back to home
); } return (

Moderation Dashboard

Manage reports, posts, and user actions.

Return to feed
{tab === 'reports' && (
{(['open', 'resolved', 'all'] as const).map((status) => ( ))}
Open: {reportCounts.open} Resolved: {reportCounts.resolved}
{loading ? (
Loading reports...
) : reports.length === 0 ? (
No reports found.
) : (
{reports.map((report) => (
{report.status} {report.targetType.toUpperCase()} report
{report.reason}
Reported by {report.reporter?.handle || 'anonymous'} • {formatDate(report.createdAt)}
{report.targetType === 'post' && report.target && 'content' in report.target && (
@{report.target.author.handle}: {report.target.content || '[repost]'}
)} {report.targetType === 'user' && report.target && 'handle' in report.target && (
User: @{report.target.handle}
)}
{report.targetType === 'post' && report.target && 'content' in report.target && ( )} {report.status === 'open' ? ( ) : ( )}
))}
)}
)} {tab === 'posts' && (
{loading ? (
Loading posts...
) : posts.length === 0 ? (
No posts found.
) : (
{posts.map((post) => (
{post.isRemoved ? 'removed' : 'active'} @{post.author.handle} • {formatDate(post.createdAt)}
{post.content || '[repost]'}
{post.removedReason && (
Reason: {post.removedReason}
)}
{post.isRemoved ? ( ) : ( )}
))}
)}
)} {tab === 'users' && (
{loading ? (
Loading users...
) : users.length === 0 ? (
No users found.
) : (
{users.map((user) => (
{user.isSuspended ? 'suspended' : 'active'} {user.isSilenced ? 'silenced' : 'visible'} @{user.handle} • {formatDate(user.createdAt)}
{user.displayName || user.handle}
{user.suspensionReason && (
Suspension: {user.suspensionReason}
)} {user.silenceReason && (
Silence: {user.silenceReason}
)}
View {user.isSuspended ? ( ) : ( )} {user.isSilenced ? ( ) : ( )}
))}
)}
)} {tab === 'settings' && (
Node Settings
{loading ? (
Loading settings...
) : (
setNodeSettings({ ...nodeSettings, name: e.target.value })} placeholder="My Synapsis Node" />