Files
sendou.ink/apps/web-react/app/features/tournament-bracket/components/Bracket/RoundHeader.tsx

155 lines
3.7 KiB
TypeScript

import clsx from "clsx";
import { differenceInMinutes } from "date-fns";
import { LocaleTime } from "~/components/LocaleTime";
import type { TournamentRoundMaps } from "~/db/tables-json";
import { useTournament } from "~/features/tournament/tournament-context";
import { resolveLeagueRoundStartDate } from "~/features/tournament/tournament-utils";
import { useAutoRerender } from "~/hooks/useAutoRerender";
import { databaseTimestampToDate } from "~/utils/dates";
import type { Unpacked } from "~/utils/types";
import * as Deadline from "../../core/Deadline";
import type { TournamentData } from "../../core/Tournament.server";
import styles from "./bracket.module.css";
export function RoundHeader({
roundId,
bracketIdx,
name,
bestOf,
showInfos,
maps,
roundStartedAt = null,
matches = [],
}: {
roundId: number;
bracketIdx: number;
name: string;
bestOf?: number;
showInfos?: boolean;
maps?: TournamentRoundMaps | null;
roundStartedAt?: number | null;
matches?: Array<Unpacked<TournamentData["data"]["match"]>>;
}) {
const leagueRoundStartDate = useLeagueWeekStart(bracketIdx, roundId);
const countPrefix = maps?.type === "PLAY_ALL" ? "Play all " : "Bo";
const pickBanSuffix =
maps?.pickBan === "COUNTERPICK"
? " (C)"
: maps?.pickBan === "BAN_2"
? " (B)"
: "";
return (
<div>
<div className={styles.elimRoundHeader}>{name}</div>
{showInfos && bestOf && !leagueRoundStartDate ? (
<div className={styles.elimRoundHeaderInfos}>
<div>
{countPrefix}
{bestOf}
{pickBanSuffix}
</div>
{roundStartedAt && matches && matches.length > 0 ? (
<RoundTimer
startedAt={roundStartedAt}
bestOf={bestOf}
matches={matches}
/>
) : null}
</div>
) : leagueRoundStartDate ? (
<LeagueRoundStartDate date={leagueRoundStartDate} />
) : (
<div className={clsx(styles.elimRoundHeaderInfos, "invisible")}>
Hidden
</div>
)}
</div>
);
}
function LeagueRoundStartDate({ date }: { date: Date }) {
return (
<div className={styles.elimRoundHeaderInfos}>
<div>
<LocaleTime
date={date}
options={{
month: "numeric",
day: "numeric",
}}
inline
/>{" "}
→
</div>
</div>
);
}
function RoundTimer({
startedAt,
bestOf,
matches,
}: {
startedAt: number;
bestOf: number;
matches: Array<Unpacked<TournamentData["data"]["match"]>>;
}) {
const now = useAutoRerender("minute", {
alignTo: databaseTimestampToDate(startedAt),
});
const elapsedMinutes = differenceInMinutes(
now,
databaseTimestampToDate(startedAt),
);
const matchStatuses = matches
.filter((match) => match.startedAt)
.map((match) => {
const matchElapsedMinutes = differenceInMinutes(
now,
databaseTimestampToDate(match.startedAt!),
);
const gamesCompleted =
(match.opponent1?.score ?? 0) + (match.opponent2?.score ?? 0);
return Deadline.matchStatus({
elapsedMinutes: matchElapsedMinutes,
gamesCompleted,
maxGamesCount: bestOf,
});
});
const worstStatus = matchStatuses.includes("error")
? "error"
: matchStatuses.includes("warning")
? "warning"
: "normal";
const displayText = elapsedMinutes >= 60 ? "1h+" : `${elapsedMinutes}m`;
const statusColor =
worstStatus === "error"
? "var(--color-error)"
: worstStatus === "warning"
? "var(--color-warning)"
: "var(--color-text)";
return <div style={{ color: statusColor }}>{displayText}</div>;
}
function useLeagueWeekStart(bracketIdx: number, roundId: number) {
const tournament = useTournament();
if (bracketIdx !== 0 || !tournament.isLeagueDivision) return null;
return resolveLeagueRoundStartDate(
tournament,
tournament.bracketByIdx(bracketIdx) ?? undefined,
roundId,
);
}