145 lines
4.6 KiB
TypeScript
145 lines
4.6 KiB
TypeScript
import { Flex, Box } from "@mantine/core";
|
|
import { Match } from "@/features/matches/types";
|
|
import { MatchCard } from "./match-card";
|
|
import { useEffect, useRef } from "react";
|
|
|
|
interface BracketProps {
|
|
rounds: Match[][];
|
|
orders: Record<number, number>;
|
|
showControls?: boolean;
|
|
}
|
|
|
|
export const Bracket: React.FC<BracketProps> = ({
|
|
rounds,
|
|
orders,
|
|
showControls,
|
|
}) => {
|
|
const containerRef = useRef<HTMLDivElement>(null);
|
|
const svgRef = useRef<SVGSVGElement>(null);
|
|
|
|
useEffect(() => {
|
|
const updateConnectorLines = () => {
|
|
if (!containerRef.current || !svgRef.current) return;
|
|
|
|
const svg = svgRef.current;
|
|
const container = containerRef.current;
|
|
const flexContainer = container.querySelector('.bracket-flex-container') as HTMLElement;
|
|
if (!flexContainer) return;
|
|
|
|
svg.innerHTML = '';
|
|
|
|
const flexRect = flexContainer.getBoundingClientRect();
|
|
const containerRect = container.getBoundingClientRect();
|
|
|
|
svg.style.width = `${flexContainer.scrollWidth}px`;
|
|
svg.style.height = `${flexContainer.scrollHeight}px`;
|
|
|
|
rounds.forEach((round, roundIndex) => {
|
|
if (roundIndex === rounds.length - 1) return;
|
|
|
|
const nextRound = rounds[roundIndex + 1];
|
|
|
|
round.forEach((match, matchIndex) => {
|
|
if (match.bye) return;
|
|
|
|
const matchElement = container.querySelector(`[data-match-lid="${match.lid}"]`) as HTMLElement;
|
|
if (!matchElement) return;
|
|
|
|
const nextMatches = nextRound.filter(nextMatch =>
|
|
!nextMatch.bye && (
|
|
orders[nextMatch.home_from_lid] === match.order ||
|
|
orders[nextMatch.away_from_lid] === match.order
|
|
)
|
|
);
|
|
|
|
nextMatches.forEach(nextMatch => {
|
|
const nextMatchElement = container.querySelector(`[data-match-lid="${nextMatch.lid}"]`) as HTMLElement;
|
|
if (!nextMatchElement) return;
|
|
|
|
const matchRect = matchElement.getBoundingClientRect();
|
|
const nextMatchRect = nextMatchElement.getBoundingClientRect();
|
|
|
|
const startX = matchRect.right - flexRect.left;
|
|
const startY = matchRect.top + matchRect.height / 2 - flexRect.top;
|
|
const endX = nextMatchRect.left - flexRect.left;
|
|
const endY = nextMatchRect.top + nextMatchRect.height / 2 - flexRect.top;
|
|
|
|
const midX = startX + (endX - startX) * 0.5;
|
|
|
|
const path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
|
|
const pathData = `M ${startX} ${startY} L ${midX} ${startY} L ${midX} ${endY} L ${endX} ${endY}`;
|
|
|
|
path.setAttribute('d', pathData);
|
|
path.setAttribute('stroke', 'var(--mantine-color-default-border)');
|
|
path.setAttribute('stroke-width', '2');
|
|
path.setAttribute('fill', 'none');
|
|
path.setAttribute('stroke-linecap', 'round');
|
|
path.setAttribute('stroke-linejoin', 'round');
|
|
|
|
svg.appendChild(path);
|
|
});
|
|
});
|
|
});
|
|
};
|
|
|
|
updateConnectorLines();
|
|
|
|
const handleUpdate = () => {
|
|
requestAnimationFrame(updateConnectorLines);
|
|
};
|
|
|
|
const scrollContainer = containerRef.current?.closest('.mantine-ScrollArea-viewport');
|
|
scrollContainer?.addEventListener('scroll', handleUpdate);
|
|
window.addEventListener('resize', handleUpdate);
|
|
|
|
return () => {
|
|
scrollContainer?.removeEventListener('scroll', handleUpdate);
|
|
window.removeEventListener('resize', handleUpdate);
|
|
};
|
|
}, [rounds, orders]);
|
|
|
|
return (
|
|
<Box pos="relative" ref={containerRef}>
|
|
<svg
|
|
ref={svgRef}
|
|
style={{
|
|
position: 'absolute',
|
|
top: 0,
|
|
left: 0,
|
|
width: '100%',
|
|
height: '100%',
|
|
pointerEvents: 'none',
|
|
zIndex: 0,
|
|
}}
|
|
/>
|
|
<Flex direction="row" gap={24} justify="left" pos="relative" style={{ zIndex: 1 }} className="bracket-flex-container">
|
|
{rounds.map((round, roundIndex) => (
|
|
<Flex
|
|
key={roundIndex}
|
|
direction="column"
|
|
align="center"
|
|
pos="relative"
|
|
gap={24}
|
|
justify="space-around"
|
|
p={24}
|
|
>
|
|
{round.map((match) =>
|
|
match.bye ? (
|
|
<div key={match.lid}></div>
|
|
) : (
|
|
<div key={match.lid}>
|
|
<MatchCard
|
|
match={match}
|
|
orders={orders}
|
|
showControls={showControls}
|
|
/>
|
|
</div>
|
|
)
|
|
)}
|
|
</Flex>
|
|
))}
|
|
</Flex>
|
|
</Box>
|
|
);
|
|
};
|