"use client"; import { AlertTriangle, Ban, CheckCircle2, Clock, Filter, MessageSquareWarning, MicOff, ShieldAlert, Trash2, UserX, XCircle, } from "lucide-react"; import { useEffect, useState } from "react"; import { useAmbient } from "@/components/ambient/ambient-context"; import { Donut } from "@/components/charts"; import { Badge, GlassPanel, Select, type SelectOption, } from "@/components/primitives"; import { ErrorState, LoadingState, MetricTile, SectionHeader, } from "@/components/shared"; import { useModerationActions, useModerationStats } from "@/hooks"; import { formatNumber } from "@/lib/format"; import type { ModerationAction, ModerationActionType, ModerationStats, } from "@/lib/types"; const ACTION_ICON: Record = { delete_message: , mute_user: , warn_user: , kick_user: , ban_user: , }; const ACTION_LABEL: Record = { delete_message: "Delete", mute_user: "Mute", warn_user: "Warn", kick_user: "Kick", ban_user: "Ban", }; export function ModerationView({ initialStats, initialActions, }: { initialStats?: ModerationStats; initialActions?: ModerationAction[]; }) { const { data: stats, isLoading, error } = useModerationStats(initialStats); const [statusFilter, setStatusFilter] = useState(""); const [typeFilter, setTypeFilter] = useState(""); const { data: actions } = useModerationActions( statusFilter || undefined, typeFilter || undefined, !statusFilter && !typeFilter ? initialActions : undefined, ); const failedRate = stats ? stats.failed_rate * 100 : 0; const byAction = stats?.by_action ?? {}; const segments = Object.entries(byAction).map(([k, _v]) => ({ value: 1, color: k === "ban_user" || k === "kick_user" ? "var(--color-vermilion)" : k === "warn_user" ? "var(--color-amber)" : "var(--color-signal)", label: k, })); const ambient = useAmbient(); useEffect(() => { ambient.set( failedRate > 20 ? "vermilion" : failedRate > 5 ? "amber" : "signal", 0.3 + Math.min(0.4, failedRate / 50), "moderation", ); }, [failedRate, ambient]); if (error && !stats) return ; if (!stats && isLoading) return ; if (!stats) return ; const statusOpts: SelectOption[] = [ { value: "", label: "All statuses" }, { value: "pending", label: "Pending" }, { value: "executed", label: "Executed" }, { value: "failed", label: "Failed" }, ]; const typeOpts: SelectOption[] = [ { value: "", label: "All actions" }, ...Object.keys(byAction).map((k) => ({ value: k, label: ACTION_LABEL[k as ModerationActionType] ?? k, })), ]; return (
} /> } /> 0 ? "vermilion" : "neutral"} icon={} /> 0 ? "amber" : "neutral"} icon={} />
{Object.entries(byAction).map(([k, v]) => { const count = typeof v === "number" ? v : null; return (
{ACTION_ICON[k as ModerationActionType]} {ACTION_LABEL[k as ModerationActionType] ?? k} {count !== null && ( {count} )}
); })} {Object.keys(byAction).length === 0 && (
No actions recorded yet.
)}
} />
{(actions ?? []).map((a) => ( ))} {(actions ?? []).length === 0 && (
No matching actions.
)}
); } function ActionRow({ a }: { a: ModerationAction }) { const tone = a.status === "executed" ? "signal" : a.status === "failed" ? "vermilion" : "amber"; const icon = ACTION_ICON[a.action_type] ?? ( ); return (
{icon}
{a.username ?? "unknown"} {a.status} {a.created_at ? new Date(a.created_at).toLocaleString() : "—"}
{a.reason && (
“{a.reason}”
)} {a.content && (
{a.content}
)} {a.error && (
{a.error}
)}
); }