import { useState } from 'react' import { useNavigate } from 'react-router-dom' import { Loading, ErrorState } from '../../../components/PageState.jsx' import { useAsync } from '../../../lib/useAsync.js' import { ago, dateTime } from '../../../lib/format.js' import { api } from '../../../api/client.js' const WINDOWS = [ { key: '24h', label: 'Last 24h' }, { key: '7d', label: 'Last 7 days' }, { key: '30d', label: 'Last 30 days' }, ] const TYPES = [ { key: null, label: 'All' }, { key: 'ban', label: 'Bans' }, { key: 'kick', label: 'Kicks' }, { key: 'mute', label: 'Mutes' }, { key: 'warn', label: 'Warnings' }, ] const MOD_TILES = [ { key: 'ban', label: 'Bans' }, { key: 'kick', label: 'Kicks' }, { key: 'mute', label: 'Mutes' }, { key: 'warn', label: 'Warnings' }, ] // Second tile row → jumps the events panel to the matching stream. const EVENT_TILES = [ { key: 'joins', label: 'Joins', tab: 'members' }, { key: 'leaves', label: 'Leaves', tab: 'members' }, { key: 'filter_hits', label: 'Filter hits', tab: 'filter' }, { key: 'spam_hits', label: 'Spam hits', tab: 'spam' }, ] const EVENT_TABS = [ { key: 'members', label: 'Members' }, { key: 'filter', label: 'Filter hits' }, { key: 'spam', label: 'Spam hits' }, ] export default function Moderation() { const navigate = useNavigate() const [win, setWin] = useState('24h') const [typeFilter, setTypeFilter] = useState(null) const [eventTab, setEventTab] = useState('members') const { loading, error, data } = useAsync( () => Promise.all([ api.admin.modSummary(), api.admin.modRecent({ limit: 100 }), api.admin.modMembers({ limit: 50 }), api.admin.modFilterHits({ limit: 50 }), api.admin.modSpamHits({ limit: 50 }), ]), [], ) if (loading) return if (error) return const [summary, recent, members, filterHits, spamHits] = data const counts = summary.windows?.[win] || {} const feed = typeFilter ? recent.filter((r) => r.action_type === typeFilter) : recent const goUser = (id) => navigate(`/admin/moderation/user/${id}`) return (
{/* Window selector */}
{WINDOWS.map((w) => ( ))}
{/* Moderation-action tiles (click filters the recent-actions feed) */}
{MOD_TILES.map((t) => ( setTypeFilter(typeFilter === t.key ? null : t.key)} /> ))}
{/* Event tiles (click jumps the events panel to that stream) */}
{EVENT_TILES.map((t) => ( setEventTab(t.tab)} /> ))}

Counts are for the selected window. Member, filter, and spam events are captured live by the bot.

{/* Recent moderation actions */}

Recent actions

{TYPES.map((t) => ( ))}
{feed.length === 0 && ( )} {feed.map((a) => ( ))}
Action Target Staff Reason When
No matching actions.
{a.action_type} goUser(a.target_user_id)}>{a.target_tag || a.target_user_id} {a.linked_account && site: {a.linked_account.username}} {a.is_automated ? Automated : {a.staff_tag || a.staff_user_id}} {a.reason || '—'} {ago(a.created_at)}
{/* Event streams panel */}

Events

{EVENT_TABS.map((t) => ( ))}
{eventTab === 'members' && } {eventTab === 'filter' && } {eventTab === 'spam' && }
) } function Tile({ value, label, sub, active, onClick }) { return ( ) } function MembersTable({ rows, onUser }) { return (
{rows.length === 0 && } {rows.map((m) => ( ))}
Event User Invite When
No member events yet.
{m.event_type} onUser(m.discord_user_id)}>{m.username || m.discord_user_id} {m.invite_code ? ( {m.invite_code}{m.inviter_tag ? ` · by ${m.inviter_tag}` : ''} ) : '—'} {ago(m.created_at)}
) } function FilterTable({ rows, onUser }) { return (
{rows.length === 0 && } {rows.map((f) => ( ))}
Type User Matched Action When
No filter hits yet.
{f.hit_type} onUser(f.discord_user_id)}>{f.username || f.discord_user_id} {f.matched || '—'} {f.action_taken} {ago(f.created_at)}
) } const SPAM_LABEL = { rate_limit: 'Rate limit', mass_mention: 'Mass mention', mass_emoji: 'Mass emoji' } function SpamTable({ rows, onUser }) { return (
{rows.length === 0 && } {rows.map((s) => ( ))}
Type User When
No spam hits yet.
{SPAM_LABEL[s.spam_type] || s.spam_type} onUser(s.discord_user_id)}>{s.username || s.discord_user_id} {ago(s.created_at)}
) } // User lookup: search by Discord id or a historical username snapshot. function UserSearch({ onPick }) { const [term, setTerm] = useState('') const [results, setResults] = useState(null) const [busy, setBusy] = useState(false) async function run(e) { e.preventDefault() const q = term.trim() if (!q) return setBusy(true) try { setResults(await api.admin.modSearch(q)) } finally { setBusy(false) } } return (
setTerm(e.target.value)} style={{ maxWidth: 360 }} />
{results && results.length === 0 && (

No moderated users match “{term}”.

)} {results && results.length > 0 && (
{results.map((r) => ( onPick(r.target_user_id)}> ))}
{r.target_tag || '(unknown tag)'} {r.target_user_id} {r.action_count} action{Number(r.action_count) === 1 ? '' : 's'} last {ago(r.last_seen)}
)}
) } const activePill = { background: 'var(--blue)', color: 'var(--ink)', borderColor: 'var(--accent)' } const rowHead = { display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 12, flexWrap: 'wrap', marginBottom: 12 } const h2 = { margin: 0, fontSize: '1.25rem', color: 'var(--head)' } const muted = { color: 'var(--muted)' }