import { useCallback, useState } from 'react' import { useParams, Link } from 'react-router-dom' import { Loading, ErrorState } from '../../../components/PageState.jsx' import { useAsync } from '../../../lib/useAsync.js' import { dateTime, ago } from '../../../lib/format.js' import { api } from '../../../api/client.js' import { useAuth } from '../../../contexts/AuthContext.jsx' const ACTION_TABS = [ { key: 'warn', label: 'Warnings' }, { key: 'mute', label: 'Mutes' }, { key: 'kick', label: 'Kicks' }, { key: 'ban', label: 'Bans' }, ] function fmtDuration(seconds) { if (!seconds) return null if (seconds % 86400 === 0) return `${seconds / 86400}d` if (seconds % 3600 === 0) return `${seconds / 3600}h` if (seconds % 60 === 0) return `${seconds / 60}m` return `${seconds}s` } export default function ModerationUser() { const { discordId } = useParams() const { user } = useAuth() const isAdmin = user?.role === 'admin' const [tab, setTab] = useState('warn') const [tick, setTick] = useState(0) const reload = useCallback(() => setTick((t) => t + 1), []) const { loading, error, data } = useAsync( () => Promise.all([ api.admin.modUser(discordId), api.admin.modUserActions(discordId, { limit: 200 }), api.admin.modUserNotes(discordId), api.admin.getUserAppeals(discordId), ]), [discordId, tick], ) if (loading) return if (error) return const [summary, actions, notes, appeals] = data const counts = summary.counts || {} const tabActions = actions.filter((a) => a.action_type === tab) let tabBody if (tab === 'notes') { tabBody = } else if (tab === 'appeals') { tabBody = } else { tabBody = } return (
← Back to moderation {/* Header */}
{summary.tag || '(unknown user)'} {summary.linked_account && ( site account: {summary.linked_account.username} )}
{discordId}
{ACTION_TABS.map((t) => ( ))}
{/* Tabs */}
{ACTION_TABS.map((t) => ( setTab(t.key)}> {t.label} ({counts[t.key] || 0}) ))} setTab('notes')}> Notes ({summary.notes_count || 0}) setTab('appeals')}> Appeals ({appeals.length})
{tabBody}
) } function Count({ label, value }) { return (
{value}
{label}
) } function TabButton({ active, onClick, children }) { return ( ) } function ActionTable({ rows, showDuration }) { return (
{showDuration && } {rows.length === 0 && ( )} {rows.map((a) => ( {showDuration && } ))}
Reason ActorDurationWhen
Nothing here.
{a.reason || '—'} {a.is_automated ? ( Automated ) : ( {a.staff_tag || a.staff_user_id} )} {fmtDuration(a.duration_seconds) || '—'}{dateTime(a.created_at)}
) } const APPEAL_STATUS_STYLE = { pending: { color: '#e0b070', background: 'rgba(224,176,112,0.12)', border: '1px solid rgba(224,176,112,0.4)' }, under_review: { color: '#7fa8d0', background: 'rgba(127,168,208,0.14)', border: '1px solid rgba(127,168,208,0.4)' }, approved: { color: '#7fd0a4', background: 'rgba(95,185,138,0.16)', border: '1px solid rgba(95,185,138,0.4)' }, denied: { color: '#d98b84', background: 'rgba(217,139,132,0.16)', border: '1px solid rgba(217,139,132,0.4)' }, withdrawn: { color: '#9fb0c6', background: 'rgba(127,153,189,0.14)', border: '1px solid var(--line)' }, } const APPEAL_STATUS_LABEL = { pending: 'Pending', under_review: 'Under review', approved: 'Approved', denied: 'Denied', withdrawn: 'Withdrawn', } function AppealsTab({ rows }) { return (
{rows.length === 0 && ( )} {rows.map((a) => ( ))}
Action Appeal Staff response Status Reversal When
No appeals from this user.
{a.action_type} {a.submitted_text} {a.staff_response || '—'} {APPEAL_STATUS_LABEL[a.status] || a.status} {a.reversal_status === 'done' && Lifted} {a.reversal_status === 'failed' && Failed} {(!a.reversal_status || a.reversal_status === 'none') && '—'} {ago(a.submitted_at)}
) } function NotesTab({ discordId, notes, isAdmin, onAdded }) { const [body, setBody] = useState('') const [visibility, setVisibility] = useState('staff_only') const [busy, setBusy] = useState(false) const [err, setErr] = useState('') async function add() { if (!body.trim()) return setBusy(true) setErr('') try { await api.admin.addModNote(discordId, { body: body.trim(), visibility }) setBody('') setVisibility('staff_only') onAdded() } catch (e) { setErr(e.message || 'Could not save the note.') } finally { setBusy(false) } } return (
{err &&

{err}

}