feat(gmw): public features #7-14 — scam domains, top channels, hourly heatmap, category drill-down, coverage stats, channel culture glossary, term KB, edit history
ALSO fixes: dashboard.repository still JOINed dropped user_reputations table (listUsers/getUserDetail crash).
This commit is contained in:
1 parent
2a8f6d9062
commit
00e8d68ce5
38 files changed
+1542
-25
No files matched your search
@@ -0,0 +1,128 @@
|
||||
"use client";
|
||||
|
||||
import { ChevronRight } from "lucide-react";
|
||||
import { Badge, GlassPanel } from "@/components/primitives";
|
||||
import { SectionHeader } from "@/components/shared";
|
||||
import { formatNumber, formatRelativeTime } from "@/lib/format";
|
||||
import type { CategoryAction, ModerationTrends } from "@/lib/types";
|
||||
|
||||
const SEVERITY_TONE: Record<
|
||||
string,
|
||||
"signal" | "amber" | "vermilion" | "neutral"
|
||||
> = {
|
||||
critical: "vermilion",
|
||||
high: "vermilion",
|
||||
medium: "amber",
|
||||
low: "signal",
|
||||
none: "neutral",
|
||||
};
|
||||
|
||||
interface CategoryDrilldownProps {
|
||||
trends: ModerationTrends;
|
||||
selected?: string | null;
|
||||
actions?: CategoryAction[];
|
||||
loading?: boolean;
|
||||
onSelect: (category: string | null) => void;
|
||||
}
|
||||
|
||||
export function CategoryDrilldown({
|
||||
trends,
|
||||
selected,
|
||||
actions,
|
||||
loading,
|
||||
onSelect,
|
||||
}: CategoryDrilldownProps) {
|
||||
const maxCat = trends.categories.reduce((m, c) => Math.max(m, c.count), 0);
|
||||
|
||||
return (
|
||||
<GlassPanel className="lg:col-span-3">
|
||||
<SectionHeader eyebrow="drill-down" title="Flag Category" />
|
||||
{selected ? (
|
||||
<div className="mb-3 flex items-center gap-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => onSelect(null)}
|
||||
className="text-xs text-ink-soft hover:text-ink"
|
||||
>
|
||||
← Back to all categories
|
||||
</button>
|
||||
<span className="text-xs text-ink-faint">
|
||||
/ {selected} (
|
||||
{loading ? "loading…" : formatNumber(actions?.length ?? 0)} actions)
|
||||
</span>
|
||||
</div>
|
||||
) : (
|
||||
<p className="mb-2 text-xs text-ink-faint">
|
||||
Click a category to list the underlying moderation actions.
|
||||
</p>
|
||||
)}
|
||||
|
||||
{!selected ? (
|
||||
<div className="space-y-2">
|
||||
{trends.categories.map((c) => {
|
||||
const pct = maxCat > 0 ? Math.max(2, (c.count / maxCat) * 100) : 0;
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
key={c.name}
|
||||
onClick={() => onSelect(c.name)}
|
||||
className="flex w-full items-center gap-3 text-left text-sm"
|
||||
>
|
||||
<span className="w-36 shrink-0 truncate text-ink-soft">
|
||||
{c.name}
|
||||
</span>
|
||||
<div className="h-2 flex-1 overflow-hidden rounded-full bg-white/5">
|
||||
<div
|
||||
className="h-full rounded-full bg-signal"
|
||||
style={{ width: `${pct}%` }}
|
||||
/>
|
||||
</div>
|
||||
<span className="mono w-10 text-right text-ink">
|
||||
{formatNumber(c.count)}
|
||||
</span>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-2">
|
||||
{loading && <p className="text-xs text-ink-faint">Loading…</p>}
|
||||
{!loading && actions && actions.length === 0 && (
|
||||
<p className="text-xs text-ink-faint">
|
||||
No actions in this category.
|
||||
</p>
|
||||
)}
|
||||
{actions?.slice(0, 12).map((a) => (
|
||||
<div key={a.id} className="flex items-start gap-2 text-sm">
|
||||
<Badge tone={SEVERITY_TONE[a.severity ?? "none"]}>
|
||||
{a.severity ?? "none"}
|
||||
</Badge>
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="flex flex-wrap items-baseline gap-x-2">
|
||||
<span className="font-medium text-ink">{a.action_type}</span>
|
||||
{a.username && (
|
||||
<span className="text-ink-soft">@{a.username}</span>
|
||||
)}
|
||||
<span className="text-ink-faint mono text-xs">
|
||||
{a.created_at ? formatRelativeTime(a.created_at) : ""}
|
||||
</span>
|
||||
</div>
|
||||
{a.content && (
|
||||
<p className="mt-0.5 line-clamp-2 text-ink-faint">
|
||||
{a.content}
|
||||
</p>
|
||||
)}
|
||||
{a.reason && (
|
||||
<p className="mt-0.5 line-clamp-1 text-xs text-ink-faint">
|
||||
Reason: {a.reason}
|
||||
</p>
|
||||
)}
|
||||
<ChevronRight className="mt-1 size-3 text-ink-faint/50" />
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</GlassPanel>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user