mancala/apps/frontend/src/components/game/SeriesStrip.tsx
Halit Aksoy 79f68416ee feat(frontend): show the series above the game
From the second game of a series, a strip shows the series score and a
box per game in the winner's colour, with the stores, who moved first
and how it ended; a finished game opens its replay in a new tab. The
game over card shows the series score too. Spectators see names.
2026-10-17 02:15:53 +03:00

79 lines
3.6 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import * as React from "react";
import { FunctionComponent } from "react";
import { useTranslation } from "react-i18next";
import { getSeriesScore, Seat, SeriesGame } from "@mancala/core";
// The most boxes the strip shows; earlier games collapse into a "+N" box.
const MAX_BOXES = 8;
// Above the board from the second game of a series (docs/specs/rematch, R3): the series score and one box per game,
// each with its winner's colour, the stores from the bottom player's side, a dot for who moved first and how it
// ended. Finished games open their replay in a new tab; the game being played is marked.
const SeriesStrip: FunctionComponent<{
// Finished games, oldest first.
games: SeriesGame[],
// A game of the series is being played after them.
hasCurrentGame: boolean,
bottomSeat: Seat,
bottomName: string,
topName: string,
}> = ({ games, hasCurrentGame, bottomSeat, bottomName, topName }) => {
const { t } = useTranslation();
const score = getSeriesScore(games, bottomSeat);
const finishedBoxes = MAX_BOXES - (hasCurrentGame ? 1 : 0);
const hiddenCount = games.length > finishedBoxes ? games.length - finishedBoxes + 1 : 0;
const shown = games.slice(hiddenCount);
const renderGame = (game: SeriesGame, index: number) => {
const number = hiddenCount + index + 1;
const bottomStore = bottomSeat === "player1" ? game.player1Store : game.player2Store;
const topStore = bottomSeat === "player1" ? game.player2Store : game.player1Store;
const winner = game.result === "player1" || game.result === "player2"
? (game.result === bottomSeat ? "mine" : "theirs") : "draw";
const ending = game.endReason === "timeout" ? t("EndedOnTime") : game.endReason === "left" ? t("EndedByLeaving") : undefined;
const bottomFirst = game.firstSeat === bottomSeat;
const firstMark = <span className="sfirst" title={t("MovedFirst")} />;
const label = [
t("SeriesGameNumber", { number }),
`${bottomName} ${bottomStore} – ${topStore} ${topName}`,
`${t("MovedFirst")}: ${bottomFirst ? bottomName : topName}`,
ending,
].filter(Boolean).join(", ");
return (
<li key={game.gameId}>
<a className={"sg " + winner} href={`/game/${game.gameId}`} target="_blank" rel="noopener" aria-label={label}>
<span className="sn">{ending ? `${number} · ${ending}` : number}</span>
<span className="ss">{bottomFirst && firstMark}{bottomStore}–{topStore}{!bottomFirst && firstMark}</span>
</a>
</li>
);
};
return (
<nav className="series" aria-label={t("Series")}>
<div className="sscore">
<span className="slabel">{t("Series")}</span>
<span className="sname">{bottomName}</span>
<b>{score.wins} – {score.losses}</b>
<span className="sname">{topName}</span>
</div>
<ol className="sgames">
{hiddenCount > 0 && (
<li><span className="sg more" title={t("SeriesEarlierGames", { count: hiddenCount })}>+{hiddenCount}</span></li>
)}
{shown.map(renderGame)}
{hasCurrentGame && (
<li aria-current="true">
<span className="sg now">
<span className="sn">{games.length + 1}</span>
<span className="ss">{t("SeriesCurrentGame")}</span>
</span>
</li>
)}
</ol>
</nav>
);
};
export default SeriesStrip;