import { useState, type FormEvent } from 'react';
import { Ban, Gavel, Plus, ShieldAlert } from 'lucide-react';
import { useApiResource, useServer } from '../context/server-context';
import { useI18n } from '../lib/i18n';
import { Badge, Button, FeatureUnavailable, Field, Input, PageHeader, Panel, ResourceState, Select, TableShell, Textarea, Toast, formatDate } from '../components/ui';
export function ModerationPage() {
const { t, locale } = useI18n();
const { api, features } = useServer();
const enabled = features?.moderationEnabled === true;
const resource = useApiResource(() => api.moderationCases(), [], ['moderation'], enabled);
const [player, setPlayer] = useState('');
const [type, setType] = useState('WARN');
const [reason, setReason] = useState('');
const [message, setMessage] = useState('');
const [duration, setDuration] = useState('');
const [previewing, setPreviewing] = useState(false);
const [busy, setBusy] = useState(false);
const [revoking, setRevoking] = useState<{ caseId: string; actionId: string }>();
const [revokeReason, setRevokeReason] = useState('');
const [toast, setToast] = useState<{ message: string; tone: 'good' | 'danger' }>();
const submit = async (event: FormEvent) => {
event.preventDefault();
if (!player.trim() || !reason.trim()) return;
if (!previewing) { setPreviewing(true); return; }
setBusy(true);
try {
const moderationCase = await api.createModerationCase({ playerId: player.trim(), action: type,
reason: reason.trim(), message: message.trim() || reason.trim(), duration: duration.trim() || undefined });
setToast({ message: `${t('moderation.case')} · ${moderationCase.id}`, tone: 'good' });
setPlayer(''); setReason(''); setMessage(''); setDuration(''); setPreviewing(false); resource.reload();
} catch (error) {
setToast({ message: error instanceof Error ? error.message : String(error), tone: 'danger' });
} finally { setBusy(false); }
};
const revoke = async () => {
if (!revoking || !revokeReason.trim()) return;
setBusy(true);
try {
await api.revokeModerationAction(revoking.caseId, revoking.actionId, revokeReason.trim());
setToast({ message: t('moderation.revoked'), tone: 'good' }); setRevoking(undefined); setRevokeReason(''); resource.reload();
} catch (error) { setToast({ message: error instanceof Error ? error.message : String(error), tone: 'danger' }); }
finally { setBusy(false); }
};
return <div className="page"><PageHeader title={t('moderation.title')} description={t('moderation.description')} />
{!enabled && <FeatureUnavailable>{t('moderation.featureOff')}</FeatureUnavailable>}
<div className="moderation-layout">
<Panel title={<><Plus size={18} />{t('moderation.newCase')}</>}>
<form className="form-stack" onSubmit={submit}>
<Field label={t('moderation.player')}><Input value={player} onChange={(event) => { setPlayer(event.target.value); setPreviewing(false); }} required /></Field>
<Field label={t('moderation.type')}><Select value={type} onChange={(event) => { setType(event.target.value); setPreviewing(false); }}><option>WARN</option><option>MUTE</option><option>KICK</option><option>TEMPBAN</option><option>BAN</option></Select></Field>
<Field label={t('moderation.duration')}><Input value={duration} onChange={(event) => { setDuration(event.target.value); setPreviewing(false); }} placeholder="2h" /></Field>
<Field label={t('moderation.reason')}><Textarea value={reason} onChange={(event) => { setReason(event.target.value); setPreviewing(false); }} required /></Field>
<Field label={t('moderation.message')} hint={t('moderation.messageHint')}><Textarea value={message}
onChange={(event) => { setMessage(event.target.value); setPreviewing(false); }} /></Field>
{previewing && <div className="confirm-box confirm-box--danger"><ShieldAlert /><div><strong>{type} · {player}</strong>
<p>{reason}{duration ? ` · ${duration}` : ''}</p><small>{message.trim() || reason}</small></div></div>}
<Button type="submit" variant={previewing ? 'danger' : 'primary'} disabled={busy || !enabled || !player.trim() || !reason.trim()}><Gavel size={16} />{previewing ? t('common.confirm') : t('moderation.create')}</Button>
</form>
</Panel>
<Panel title={<><Ban size={18} />{t('moderation.title')}</>}>
{revoking && <div className="confirm-box confirm-box--danger"><ShieldAlert /><div className="form-stack"><strong>{t('moderation.revoke')}</strong><Input value={revokeReason} onChange={(event) => setRevokeReason(event.target.value)} placeholder={t('common.reason')} /><div className="button-row"><Button variant="danger" disabled={busy || !revokeReason.trim()} onClick={() => void revoke()}>{t('common.confirm')}</Button><Button variant="ghost" onClick={() => setRevoking(undefined)}>{t('common.cancel')}</Button></div></div></div>}
<ResourceState loading={resource.loading} error={resource.error} empty={resource.data?.items.length === 0} onRetry={resource.reload}><TableShell><thead><tr><th>{t('moderation.case')}</th><th>{t('moderation.player')}</th><th>{t('moderation.type')}</th><th>{t('moderation.state')}</th><th>{t('moderation.expiry')}</th><th /></tr></thead><tbody>{resource.data?.items.map((item) => <tr key={item.id}><td><code>{item.id}</code><small className="table-subline">{formatDate(item.createdAt, locale)}</small></td><td><strong>{item.playerName ?? item.playerUuid}</strong><small className="table-subline">{item.message ?? item.reason}</small><small className="table-subline">{item.reason}</small></td><td><Badge tone="warn">{item.type}</Badge></td><td><Badge tone={item.state === 'ACTIVE' ? 'danger' : 'neutral'}>{item.state}</Badge></td><td>{formatDate(item.expiresAt, locale)}</td><td>{item.state === 'ACTIVE' && item.history?.at(-1)?.id && <Button variant="ghost" onClick={() => setRevoking({ caseId: item.id, actionId: item.history!.at(-1)!.id })}>{t('moderation.revoke')}</Button>}</td></tr>)}</tbody></TableShell></ResourceState>
</Panel>
</div>{toast && <Toast {...toast} onClose={() => setToast(undefined)} />}</div>;
}
import { useState, type FormEvent } from 'react';
import { Ban, Gavel, Plus, ShieldAlert } from 'lucide-react';
import { useApiResource, useServer } from '../context/server-context';
import { useI18n } from '../lib/i18n';
import { Badge, Button, FeatureUnavailable, Field, Input, PageHeader, Panel, ResourceState, Select, TableShell, Textarea, Toast, formatDate } from '../components/ui';
export function ModerationPage() {
const { t, locale } = useI18n();
const { api, features } = useServer();
const enabled = features?.moderationEnabled === true;
const resource = useApiResource(() => api.moderationCases(), [], ['moderation'], enabled);
const [player, setPlayer] = useState('');
const [type, setType] = useState('WARN');
const [reason, setReason] = useState('');
const [message, setMessage] = useState('');
const [duration, setDuration] = useState('');
const [previewing, setPreviewing] = useState(false);
const [busy, setBusy] = useState(false);
const [revoking, setRevoking] = useState<{ caseId: string; actionId: string }>();
const [revokeReason, setRevokeReason] = useState('');
const [toast, setToast] = useState<{ message: string; tone: 'good' | 'danger' }>();
const submit = async (event: FormEvent) => {
event.preventDefault();
if (!player.trim() || !reason.trim()) return;
if (!previewing) { setPreviewing(true); return; }
setBusy(true);
try {
const moderationCase = await api.createModerationCase({ playerId: player.trim(), action: type,
reason: reason.trim(), message: message.trim() || reason.trim(), duration: duration.trim() || undefined });
setToast({ message: `${t('moderation.case')} · ${moderationCase.id}`, tone: 'good' });
setPlayer(''); setReason(''); setMessage(''); setDuration(''); setPreviewing(false); resource.reload();
} catch (error) {
setToast({ message: error instanceof Error ? error.message : String(error), tone: 'danger' });
} finally { setBusy(false); }
};
const revoke = async () => {
if (!revoking || !revokeReason.trim()) return;
setBusy(true);
try {
await api.revokeModerationAction(revoking.caseId, revoking.actionId, revokeReason.trim());
setToast({ message: t('moderation.revoked'), tone: 'good' }); setRevoking(undefined); setRevokeReason(''); resource.reload();
} catch (error) { setToast({ message: error instanceof Error ? error.message : String(error), tone: 'danger' }); }
finally { setBusy(false); }
};
return <div className="page"><PageHeader title={t('moderation.title')} description={t('moderation.description')} />
{!enabled && <FeatureUnavailable>{t('moderation.featureOff')}</FeatureUnavailable>}
<div className="moderation-layout">
<Panel title={<><Plus size={18} />{t('moderation.newCase')}</>}>
<form className="form-stack" onSubmit={submit}>
<Field label={t('moderation.player')}><Input value={player} onChange={(event) => { setPlayer(event.target.value); setPreviewing(false); }} required /></Field>
<Field label={t('moderation.type')}><Select value={type} onChange={(event) => { setType(event.target.value); setPreviewing(false); }}><option>WARN</option><option>MUTE</option><option>KICK</option><option>TEMPBAN</option><option>BAN</option></Select></Field>
<Field label={t('moderation.duration')}><Input value={duration} onChange={(event) => { setDuration(event.target.value); setPreviewing(false); }} placeholder="2h" /></Field>
<Field label={t('moderation.reason')}><Textarea value={reason} onChange={(event) => { setReason(event.target.value); setPreviewing(false); }} required /></Field>
<Field label={t('moderation.message')} hint={t('moderation.messageHint')}><Textarea value={message}
onChange={(event) => { setMessage(event.target.value); setPreviewing(false); }} /></Field>
{previewing && <div className="confirm-box confirm-box--danger"><ShieldAlert /><div><strong>{type} · {player}</strong>
<p>{reason}{duration ? ` · ${duration}` : ''}</p><small>{message.trim() || reason}</small></div></div>}
<Button type="submit" variant={previewing ? 'danger' : 'primary'} disabled={busy || !enabled || !player.trim() || !reason.trim()}><Gavel size={16} />{previewing ? t('common.confirm') : t('moderation.create')}</Button>
</form>
</Panel>
<Panel title={<><Ban size={18} />{t('moderation.title')}</>}>
{revoking && <div className="confirm-box confirm-box--danger"><ShieldAlert /><div className="form-stack"><strong>{t('moderation.revoke')}</strong><Input value={revokeReason} onChange={(event) => setRevokeReason(event.target.value)} placeholder={t('common.reason')} /><div className="button-row"><Button variant="danger" disabled={busy || !revokeReason.trim()} onClick={() => void revoke()}>{t('common.confirm')}</Button><Button variant="ghost" onClick={() => setRevoking(undefined)}>{t('common.cancel')}</Button></div></div></div>}
<ResourceState loading={resource.loading} error={resource.error} empty={resource.data?.items.length === 0} onRetry={resource.reload}><TableShell><thead><tr><th>{t('moderation.case')}</th><th>{t('moderation.player')}</th><th>{t('moderation.type')}</th><th>{t('moderation.state')}</th><th>{t('moderation.expiry')}</th><th /></tr></thead><tbody>{resource.data?.items.map((item) => <tr key={item.id}><td><code>{item.id}</code><small className="table-subline">{formatDate(item.createdAt, locale)}</small></td><td><strong>{item.playerName ?? item.playerUuid}</strong><small className="table-subline">{item.message ?? item.reason}</small><small className="table-subline">{item.reason}</small></td><td><Badge tone="warn">{item.type}</Badge></td><td><Badge tone={item.state === 'ACTIVE' ? 'danger' : 'neutral'}>{item.state}</Badge></td><td>{formatDate(item.expiresAt, locale)}</td><td>{item.state === 'ACTIVE' && item.history?.at(-1)?.id && <Button variant="ghost" onClick={() => setRevoking({ caseId: item.id, actionId: item.history!.at(-1)!.id })}>{t('moderation.revoke')}</Button>}</td></tr>)}</tbody></TableShell></ResourceState>
</Panel>
</div>{toast && <Toast {...toast} onClose={() => setToast(undefined)} />}</div>;
}