This commit is contained in:
NextJS
2026-05-05 20:02:40 +00:00
commit 0f6e979aac
51 changed files with 10161 additions and 0 deletions
@@ -0,0 +1,206 @@
import Link from "next/link";
type PlayerSide = {
id: number | null;
username: string | null;
cc: number | null;
rc: number | null;
color: "red" | "blue";
};
type Props = {
matchId: number;
statusLabel: string;
createdAtLabel: string;
entryFee: number | null;
prizeCc: number | null;
left: PlayerSide;
right: PlayerSide;
/** May be string when `bigint` is JSON-serialized. */
winnerId: number | string | null;
};
function idsMatch(
a: number | string | null | undefined,
b: number | string | null | undefined,
): boolean {
if (a == null || b == null) return false;
const na = typeof a === "number" ? a : Number(a);
const nb = typeof b === "number" ? b : Number(b);
return Number.isFinite(na) && Number.isFinite(nb) && na === nb;
}
function sideStyles(color: PlayerSide["color"]) {
if (color === "red") {
return {
panel:
"border-rose-500/60 bg-gradient-to-br from-rose-700/80 via-fuchsia-700/70 to-violet-700/80",
tag: "bg-rose-500/90 text-rose-50",
iconBg: "bg-rose-500/30 text-rose-100",
score: "text-rose-100",
};
}
return {
panel:
"border-sky-500/60 bg-gradient-to-br from-sky-700/80 via-indigo-700/70 to-violet-700/80",
tag: "bg-sky-500/90 text-sky-50",
iconBg: "bg-sky-500/30 text-sky-100",
score: "text-sky-100",
};
}
function initials(name: string | null): string {
if (!name || name.trim() === "") return "?";
const compact = name.trim().slice(0, 2);
return compact.toUpperCase();
}
function playerLabel(side: PlayerSide): string {
if (side.id == null) return "Open slot";
if (side.username && side.username.trim() !== "") return side.username;
return `Player ${side.id}`;
}
function winnerLabel(
winnerId: number | string | null,
left: PlayerSide,
right: PlayerSide,
): string {
if (winnerId == null || winnerId === "") return "No winner yet";
if (idsMatch(winnerId, left.id)) return `${playerLabel(left)} won`;
if (idsMatch(winnerId, right.id)) return `${playerLabel(right)} won`;
return `Winner #${winnerId}`;
}
function PlayerPanel({
side,
isWinner,
isDimmed,
}: {
side: PlayerSide;
isWinner: boolean;
isDimmed: boolean;
}) {
const s = sideStyles(side.color);
return (
<div
className={`relative flex min-h-24 flex-col justify-between rounded-xl border px-2.5 py-2 shadow-lg shadow-black/20 transition ${s.panel} ${
isWinner ? "ring-2 ring-amber-300 shadow-amber-200/30" : ""
} ${isDimmed ? "opacity-50" : "opacity-100"}`}
>
{isWinner ? (
<span className="absolute -top-2 right-2 rounded bg-amber-300 px-1.5 py-0.5 text-[10px] font-black uppercase tracking-wider text-amber-950">
Winner
</span>
) : null}
<span
className={`inline-flex w-fit rounded-md px-2 py-0.5 text-[10px] font-bold uppercase tracking-wider ${s.tag}`}
>
{side.color === "red" ? "Home" : "Away"}
</span>
<div className="mt-1.5 flex items-center gap-2">
<div
className={`grid h-8 w-8 place-items-center rounded-full text-xs font-bold ${s.iconBg}`}
>
{initials(side.username)}
</div>
<div className="min-w-0">
<p className="truncate text-xs font-extrabold uppercase tracking-wide text-white">
{playerLabel(side)}
</p>
<p className={`text-[11px] font-semibold tabular-nums ${s.score}`}>
ID: {side.id ?? "—"}
</p>
</div>
</div>
<div className="mt-1.5 flex items-center gap-1.5 text-[10px] font-semibold text-white/85">
<span className="rounded bg-black/25 px-1.5 py-0.5">
CC {side.cc ?? "—"}
</span>
<span className="rounded bg-black/25 px-1.5 py-0.5">
RC {side.rc ?? "—"}
</span>
</div>
</div>
);
}
export function MatchHistoryBattleCard({
matchId,
statusLabel,
createdAtLabel,
entryFee,
prizeCc,
left,
right,
winnerId,
}: Props) {
const hasWinner =
winnerId != null &&
winnerId !== "" &&
(idsMatch(winnerId, left.id) || idsMatch(winnerId, right.id));
const winnerIsRight = hasWinner && idsMatch(winnerId, right.id);
const displayLeft = winnerIsRight ? right : left;
const displayRight = winnerIsRight ? left : right;
const leftIsWinner = hasWinner && idsMatch(displayLeft.id, winnerId);
const rightIsWinner = hasWinner && idsMatch(displayRight.id, winnerId);
const noWinner = !hasWinner;
return (
<article className="rounded-2xl border border-zinc-200 bg-gradient-to-b from-zinc-100 to-zinc-200 p-2.5 shadow-sm dark:border-zinc-700 dark:from-zinc-900 dark:to-zinc-950">
<div className="mb-1.5 flex items-center justify-between gap-2">
<span className="rounded-md bg-zinc-900 px-2 py-1 text-[11px] font-bold uppercase tracking-wider text-white dark:bg-zinc-50 dark:text-zinc-900">
Match #{matchId}
</span>
<span className="rounded-md bg-amber-400/90 px-2 py-1 text-[11px] font-extrabold uppercase tracking-wider text-amber-950">
{statusLabel}
</span>
</div>
<div className="grid grid-cols-[1fr_auto_1fr] items-center gap-2">
<PlayerPanel
side={displayLeft}
isWinner={leftIsWinner}
isDimmed={noWinner ? true : !leftIsWinner}
/>
<div className="flex flex-col items-center gap-2">
<div className="rounded-lg border-2 border-amber-300 bg-amber-200/95 px-2 py-0.5 text-[11px] font-black uppercase tracking-wider text-amber-950">
Final
</div>
<div className="rounded-lg border border-zinc-400 bg-zinc-100 px-2 py-1 text-[11px] font-semibold text-zinc-700 dark:border-zinc-600 dark:bg-zinc-800 dark:text-zinc-200">
{winnerLabel(winnerId, displayLeft, displayRight)}
</div>
</div>
<PlayerPanel
side={displayRight}
isWinner={rightIsWinner}
isDimmed={noWinner ? true : !rightIsWinner}
/>
</div>
<div className="mt-2 flex flex-wrap items-center justify-between gap-2 border-t border-zinc-300/80 pt-1.5 text-xs font-medium text-zinc-700 dark:border-zinc-700 dark:text-zinc-300">
<div className="flex flex-wrap gap-2">
<span className="rounded bg-zinc-900/10 px-2 py-1 dark:bg-zinc-100/10">
Entry {entryFee ?? "—"}
</span>
<span className="rounded bg-zinc-900/10 px-2 py-1 dark:bg-zinc-100/10">
Prize {prizeCc ?? "—"} CC
</span>
<span className="rounded bg-zinc-900/10 px-2 py-1 dark:bg-zinc-100/10">
{createdAtLabel}
</span>
</div>
<Link
href={`/match-logs/${matchId}`}
target="_blank"
rel="noopener noreferrer"
className="rounded-md border border-zinc-400 bg-white px-3 py-1.5 text-xs font-semibold text-zinc-800 shadow-sm transition hover:bg-zinc-100 dark:border-zinc-600 dark:bg-zinc-900 dark:text-zinc-100 dark:hover:bg-zinc-800"
>
Show logs
</Link>
</div>
</article>
);
}