+
🏆
- {tournamentActive
- ? `Tournament Active - ${tournamentType ?? "Unknown Type"}`
- : "No Active Tournament"}
+ {`Tournament Active - ${tournamentType ?? "Unknown Type"}`}
{gameList.length} match{gameList.length !== 1 ? "es" : ""} running
@@ -470,9 +536,31 @@ export default function SpectatePage() {
{showKnockoutBracket ? (
knockoutBracket.length === 0 ? (
-
- Knockout seeding is still in progress. The bracket will appear
- once tournament data is available.
+
+
+ Knockout seeding is still in progress. The bracket will
+ appear once tournament data is available.
+
+ {seedingMatches.length > 0 && (
+
+ {seedingMatches.map((match) => {
+ return (
+
+ );
+ })}
+
+ )}
) : (
@@ -489,74 +577,18 @@ export default function SpectatePage() {
{round.matches.map((match) => {
- const isSelected =
- match.matchId !== null &&
- selectedGame === match.matchId;
- const canSelect = match.matchId !== null;
- const borderClass =
- match.status === "completed"
- ? "border-green-700/80"
- : "border-gray-700";
-
- const Tag = canSelect ? "button" : "div";
-
return (
-
- setSelectedGame(
- isSelected ? null : match.matchId,
- ),
- type: "button" as const,
- }
- : {})}
- className={`w-full rounded-xl border bg-gray-950/70 p-3 text-left transition-colors ${borderClass} ${
- canSelect
- ? isSelected
- ? "ring-1 ring-blue-400"
- : "hover:border-blue-700/80"
- : ""
- }`}
- >
-
-
-
-
-
- {match.matchId !== null
- ? `Match #${match.matchId}`
- : "Awaiting match"}
-
-
- {match.status === "completed"
- ? match.resultKind === "draw"
- ? "Tie"
- : "Complete"
- : match.status === "live"
- ? "Live"
- : "Pending"}
-
-
-
-
+ matchId={match.matchId}
+ player1={match.player1}
+ player2={match.player2}
+ currentTurnColor={match.currentTurnColor}
+ status={match.status}
+ resultKind={match.resultKind}
+ selectedGame={selectedGame}
+ onSelect={setSelectedGame}
+ />
);
})}
@@ -570,54 +602,38 @@ export default function SpectatePage() {
No active matches
) : (
-
+
{gameList.map((g) => {
const live = liveGames.get(g.id);
+ const resultKind = live?.result?.kind ?? null;
+ const status = resultKind
+ ? "completed"
+ : live
+ ? "live"
+ : "scheduled";
return (
-
+ status={status}
+ resultKind={resultKind}
+ selectedGame={selectedGame}
+ onSelect={setSelectedGame}
+ className="w-full sm:w-[calc(50%-0.375rem)] xl:w-[calc(33.333%-0.5rem)]"
+ />
);
})}
)}
-
- {!selectedGameData ? (
-
-
🎯
-
- {gameList.length > 0
- ? "Click a match above to see the board"
- : "Active matches will appear here"}
-
-
- ) : (
+ {selectedGameData && (
+
<>
{selectedGameData.result && (
>
- )}
-
+
+ )}
@@ -692,6 +708,87 @@ function BracketPlayerRow({
);
}
+function MatchSelectorCard({
+ matchId,
+ player1,
+ player2,
+ currentTurnColor,
+ status,
+ resultKind,
+ selectedGame,
+ onSelect,
+ className = "w-full",
+}: {
+ matchId: number | null;
+ player1: string | null;
+ player2: string | null;
+ currentTurnColor: 1 | 2 | null;
+ status: "scheduled" | "live" | "completed";
+ resultKind: KnockoutMatchView["resultKind"];
+ selectedGame: number | null;
+ onSelect: (matchId: number | null) => void;
+ className?: string;
+}) {
+ const isSelected = matchId !== null && selectedGame === matchId;
+ const canSelect = matchId !== null;
+ const borderClass =
+ status === "completed" ? "border-green-700/80" : "border-gray-700";
+ const Tag = canSelect ? "button" : "div";
+
+ return (
+
onSelect(isSelected ? null : matchId),
+ type: "button" as const,
+ }
+ : {})}
+ className={`${className} rounded-xl border bg-gray-950/70 p-3 text-left transition-colors ${borderClass} ${
+ canSelect
+ ? isSelected
+ ? "ring-1 ring-blue-400"
+ : "hover:border-blue-700/80"
+ : ""
+ }`}
+ >
+
+
+
+
+
+ {matchId !== null ? `Match #${matchId}` : "Awaiting match"}
+
+
+ {status === "completed"
+ ? resultKind === "draw"
+ ? "Tie"
+ : "Complete"
+ : status === "live"
+ ? "Live"
+ : "Pending"}
+
+
+
+
+ );
+}
+
function isKnockoutDataMessage(raw: string) {
return !raw.includes(":") && (raw.includes(",") || raw.includes("|"));
}
diff --git a/connect4-ui/components/Nav.tsx b/connect4-ui/components/Nav.tsx
index 26ad938..9256910 100644
--- a/connect4-ui/components/Nav.tsx
+++ b/connect4-ui/components/Nav.tsx
@@ -9,7 +9,8 @@ import { cmd } from "@/lib/protocol";
export default function Nav() {
const pathname = usePathname();
const router = useRouter();
- const { status, role, username, send, becomePlayer, disconnect } = useConnection();
+ const { status, role, username, send, becomePlayer, disconnect } =
+ useConnection();
const [showPlayerModal, setShowPlayerModal] = useState(false);
const [nextUsername, setNextUsername] = useState(username);
const isConnectionPage = pathname === "/";
@@ -47,21 +48,18 @@ export default function Nav() {
{!isConnectionPage && (
<>
-
-
+ {role !== "player" && (
+
+ )}