Wrap next/link in a shared Link component that ships prefetch=false by default, so no route is ever prefetched (viewport or hover) anymore, and drop the DNS prefetch hint. Removes hidden background requests that were the source of intermittent issues.
84 lines
2.7 KiB
TypeScript
84 lines
2.7 KiB
TypeScript
import Link from "@/components/link";
|
|
import { formatDate } from "@/lib/format-date";
|
|
import type { SanctionEvent } from "../_lib/load-user-sanctions";
|
|
|
|
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 {formatDate(new Date(muteUntil * 1000))}
|
|
</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">
|
|
{formatDate(new Date(ev.at * 1000))}
|
|
</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>
|
|
);
|
|
}
|