feat(admin): guilds console + user sanctions timeline
Co-authored-by: Cursor <[email protected]>
This commit is contained in:
1 parent
1e77398a7c
commit
a384c9a8bb
10 files changed
+3861
-2991
No files matched your search
@@ -0,0 +1,87 @@
|
||||
import Link from "next/link";
|
||||
import type { SanctionEvent } from "../_lib/load-user-sanctions";
|
||||
|
||||
function fmt(ts: number): string {
|
||||
if (!ts) return "—";
|
||||
return new Date(ts * 1000).toISOString().slice(0, 16).replace("T", " ");
|
||||
}
|
||||
|
||||
const KIND_CLASS: Record<SanctionEvent["kind"], string> = {
|
||||
ban: "bg-[var(--admin-error-subtle)] text-[var(--admin-error)]",
|
||||
lift: "bg-[var(--admin-success-subtle)] text-[var(--admin-success)]",
|
||||
mute: "bg-[var(--admin-accent)]/20 text-[var(--admin-accent)]",
|
||||
unmute: "bg-[var(--admin-success-subtle)] text-[var(--admin-success)]",
|
||||
cfh: "bg-[var(--admin-accent)]/15 text-[var(--admin-text)]",
|
||||
staff: "bg-[var(--admin-border)] text-[var(--admin-text-muted)]",
|
||||
status: "bg-[var(--admin-accent)]/25 text-[var(--admin-accent)]",
|
||||
};
|
||||
|
||||
export function UserSanctionTimeline({
|
||||
events,
|
||||
muteUntil,
|
||||
}: {
|
||||
events: SanctionEvent[];
|
||||
muteUntil: number;
|
||||
}) {
|
||||
const now = Math.floor(Date.now() / 1000);
|
||||
const muted = muteUntil > now;
|
||||
|
||||
return (
|
||||
<div className="rounded-lg border bg-card p-4 space-y-4">
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<h3 className="text-sm font-semibold m-0">Sanctions timeline</h3>
|
||||
{muted ? (
|
||||
<span className="text-xs font-bold px-2 py-0.5 rounded-full bg-[var(--admin-accent)]/20 text-[var(--admin-accent)]">
|
||||
Muted until {fmt(muteUntil)}
|
||||
</span>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
{events.length === 0 ? (
|
||||
<p className="text-sm text-muted-foreground m-0">
|
||||
No bans, mutes, CFH, or staff actions recorded for this user.
|
||||
</p>
|
||||
) : (
|
||||
<ol className="space-y-3 m-0 p-0 list-none">
|
||||
{events.map((ev) => (
|
||||
<li
|
||||
key={ev.id}
|
||||
className="flex gap-3 border-b border-[var(--admin-border)] pb-3 last:border-0 last:pb-0"
|
||||
>
|
||||
<span
|
||||
className={`shrink-0 mt-0.5 text-[0.65rem] font-bold uppercase tracking-wide px-2 py-0.5 rounded-full ${KIND_CLASS[ev.kind]}`}
|
||||
>
|
||||
{ev.kind}
|
||||
</span>
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="flex flex-wrap items-baseline gap-x-2 gap-y-0.5">
|
||||
{ev.href ? (
|
||||
<Link
|
||||
href={ev.href}
|
||||
className="text-sm font-medium hover:underline"
|
||||
>
|
||||
{ev.label}
|
||||
</Link>
|
||||
) : (
|
||||
<span className="text-sm font-medium">{ev.label}</span>
|
||||
)}
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{fmt(ev.at)}
|
||||
</span>
|
||||
{ev.actor ? (
|
||||
<span className="text-xs text-muted-foreground">
|
||||
by {ev.actor}
|
||||
</span>
|
||||
) : null}
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground m-0 mt-0.5 break-words">
|
||||
{ev.detail}
|
||||
</p>
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
</ol>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user