import React, { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { Text, ScrollArea, Box } from "@mantine/core"; import { BracketData } from "../types"; import { Bracket } from "./bracket"; import MatchDock from "./match-dock"; import useAppShellHeight from "@/hooks/use-appshell-height"; import { Match } from "@/features/matches/types"; import styles from "./styles.module.css"; interface BracketViewProps { bracket: BracketData; showControls?: boolean; groupConfig?: { num_groups: number; advance_per_group: number; }; renderMatch?: (match: Match) => React.ReactNode; bottomOffset?: number; nextUpMatchId?: string; } const BracketView: React.FC = ({ bracket, showControls, groupConfig, renderMatch, bottomOffset, nextUpMatchId }) => { const height = useAppShellHeight(); const viewportRef = useRef(null); const hasAutoScrolled = useRef(false); const orders = useMemo(() => { const map: Record = {}; bracket.winners.flat().forEach(match => map[match.lid] = match.order); bracket.losers.flat().forEach(match => map[match.lid] = match.order); return map; }, [bracket.winners, bracket.losers]); const [selectedLid, setSelectedLid] = useState(null); const handleMatchTap = useCallback((match: Match) => { setSelectedLid((prev) => (prev === match.lid ? null : match.lid)); }, []); const closeDock = useCallback(() => setSelectedLid(null), []); const selectedMatch = useMemo(() => { if (selectedLid == null) return null; return ( [...bracket.winners.flat(), ...bracket.losers.flat()].find( (match) => match.lid === selectedLid ) ?? null ); }, [bracket, selectedLid]); useEffect(() => { if (hasAutoScrolled.current) return; const matches = [...bracket.winners.flat(), ...bracket.losers.flat()].filter( (match) => !match.bye ); const target = nextUpMatchId !== undefined ? matches.find((match) => match.id === nextUpMatchId) : matches.find((match) => match.status === "started") ?? matches.find((match) => match.status === "ready"); if (!target) return; const viewport = viewportRef.current; const element = viewport?.querySelector(`[data-match-lid="${target.lid}"]`); if (!viewport || !element) return; hasAutoScrolled.current = true; const viewportRect = viewport.getBoundingClientRect(); const elementRect = element.getBoundingClientRect(); const left = elementRect.left - viewportRect.left + viewport.scrollLeft - 40; const top = elementRect.top - viewportRect.top + viewport.scrollTop - (viewport.clientHeight - elementRect.height) / 2; viewport.scrollTo({ left: Math.max(0, left), top: Math.max(0, top), behavior: window.matchMedia("(prefers-reduced-motion: reduce)").matches ? "auto" : "smooth", }); }, [bracket, nextUpMatchId]); return
Winners Bracket
{bracket.losers && bracket.losers.length > 0 && bracket.losers.some(round => round.length > 0) && (
Losers Bracket
)} {bottomOffset ?
: null} }; export default BracketView;