Files
kickkingsadmin/src/components/match-history-battle-card.tsx
T
2026-07-26 18:31:43 +00:00

274 lines
9.5 KiB
TypeScript

"use client";
import Link from "next/link";
import type { ReactNode } from "react";
import { ClickableUserId } from "@/components/clickable-user-id";
import { formatRcLabelFromCoinsBigInt } from "@/lib/coins-rc";
/** Ledger rows are per player; both players pay → show `1.0 RC x 2 = 2.0 RC`. */
function rcPerPlayerTimesTwoLine(coinStr: string | null): string {
if (coinStr == null || String(coinStr).trim() === "") return "—";
try {
const perPlayer = BigInt(String(coinStr).trim());
const bothPlayers = perPlayer * BigInt(2);
return `${formatRcLabelFromCoinsBigInt(perPlayer)} x 2 = ${formatRcLabelFromCoinsBigInt(bothPlayers)}`;
} catch {
return "—";
}
}
function entryCombinedPerPlayerTimesTwoLine(
feeCoins: string | null,
holdCoins: string | null,
): string {
if (
feeCoins == null ||
holdCoins == null ||
String(feeCoins).trim() === "" ||
String(holdCoins).trim() === ""
) {
return "—";
}
try {
const perPlayer =
BigInt(String(feeCoins).trim()) + BigInt(String(holdCoins).trim());
const bothPlayers = perPlayer * BigInt(2);
return `${formatRcLabelFromCoinsBigInt(perPlayer)} x 2 = ${formatRcLabelFromCoinsBigInt(bothPlayers)}`;
} catch {
return "—";
}
}
type PlayerSide = {
id: number | null;
username: string | null;
cc: number | null;
rc: number | null;
color: "red" | "blue";
};
type Props = {
matchId: number;
statusLabel: string;
createdAtLabel: ReactNode;
/** Ledger `entry_fee` / player as coin string; null if unknown. */
entryFeeCoins: string | null;
/** Ledger `entry_hold` / player as coin string; null if unknown. */
entryHoldPerPlayerCoins: string | null;
/** Preformatted `prize_cc` for display (e.g. with grouping). */
prizeCcLabel: string;
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 != null ? (
<ClickableUserId
id={side.id}
className="cursor-pointer border-0 bg-transparent p-0 text-[11px] font-semibold text-inherit underline decoration-white/50 underline-offset-2 hover:decoration-white"
>
{side.id}
</ClickableUserId>
) : (
"—"
)}
</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,
entryFeeCoins,
entryHoldPerPlayerCoins,
prizeCcLabel,
left,
right,
winnerId,
}: Props) {
const entryLine = entryCombinedPerPlayerTimesTwoLine(
entryFeeCoins,
entryHoldPerPlayerCoins,
);
const entryHoldLine = rcPerPlayerTimesTwoLine(entryHoldPerPlayerCoins);
const entryFeeLine = rcPerPlayerTimesTwoLine(entryFeeCoins);
const prizeCcDisplay = prizeCcLabel.trim() === "" ? "—" : prizeCcLabel;
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 min-w-0 flex-1 flex-wrap gap-2">
<span className="rounded bg-zinc-900/10 px-2 py-1 dark:bg-zinc-100/10">
Time {createdAtLabel}
</span>
<span className="rounded bg-zinc-900/10 px-2 py-1 dark:bg-zinc-100/10">
Entry {entryLine}
</span>
<span className="rounded bg-zinc-900/10 px-2 py-1 dark:bg-zinc-100/10">
Entry hold {entryHoldLine}
</span>
<span className="rounded bg-zinc-900/10 px-2 py-1 dark:bg-zinc-100/10">
Prize {prizeCcDisplay} CC
</span>
<span className="rounded-md border border-emerald-200/90 bg-emerald-50 px-2 py-1 text-[11px] font-medium text-emerald-600 tabular-nums dark:border-emerald-300/35 dark:bg-emerald-400/15 dark:text-emerald-200">
Entry fee {entryFeeLine}
</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>
);
}