init
This commit is contained in:
@@ -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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user