This commit is contained in:
FURRO404
2026-06-21 01:06:40 -07:00
parent 3a7b72b01b
commit 9465ee05d1
3 changed files with 137 additions and 65 deletions
+59 -63
View File
@@ -2,7 +2,7 @@ import { useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react'
import Tree, { prewarmTreeCanvas } from '../Tree/Tree'
import FallingLeaves from '../Tree/FallingLeaves'
import ReplayCanvasPanel from './ReplayCanvas'
import { buildBracket, parentPosition } from './bracket'
import { buildBracket, computeBracketLayout, feederParent, layoutKey } from './bracket'
const numberFormat = new Intl.NumberFormat('en-GB')
const dateFormat = new Intl.DateTimeFormat('en-GB', {
@@ -3840,66 +3840,59 @@ function TournamentMatchCard({ match, navigate }) {
)
}
// Map each match to the match it feeds into in the next column, so we can draw a
// connector between them. Parent is found by slot position (faithful to the real
// tree even with byes hidden); falls back to proportional index if the exact
// parent slot was itself a hidden bye.
function feederParent(match, columnIndex, columns) {
const cur = columns[columnIndex]
const next = columns[columnIndex + 1]
if (!next) return null
const pPos = parentPosition(Number(match.position), cur.matches.length, next.matches.length)
const exact = next.matches.find((m) => Number(m.position) === pPos)
if (exact) return exact
const order = cur.matches.indexOf(match)
const idx = Math.min(next.matches.length - 1, Math.floor((order * next.matches.length) / cur.matches.length))
return next.matches[idx] || null
}
function TournamentBracketSide({ side, navigate }) {
const gridRef = useRef(null)
const nodeRefs = useRef(new Map())
// `tops` places each match; once placed we read back the DOM to draw connectors.
const [layout, setLayout] = useState({ tops: new Map(), height: 0 })
const [connectors, setConnectors] = useState({ width: 0, height: 0, paths: [] })
const nodeKey = (columnIndex, match) => `${columnIndex}:${match.match_id}`
// Pass 1: measure card heights, then position every match centred on its feeders.
useLayoutEffect(() => {
const grid = gridRef.current
if (!grid) return undefined
const measure = () => {
const gridBox = grid.getBoundingClientRect()
const boxOf = (key) => {
const el = nodeRefs.current.get(key)
if (!el) return null
const b = el.getBoundingClientRect()
return {
left: b.left - gridBox.left,
right: b.right - gridBox.left,
mid: b.top - gridBox.top + b.height / 2,
}
}
const paths = []
for (let c = 0; c < side.columns.length - 1; c += 1) {
for (const match of side.columns[c].matches) {
const parent = feederParent(match, c, side.columns)
if (!parent) continue
const from = boxOf(nodeKey(c, match))
const to = boxOf(nodeKey(c + 1, parent))
if (!from || !to) continue
const midX = (from.right + to.left) / 2
paths.push(`M ${from.right} ${from.mid} H ${midX} V ${to.mid} H ${to.left}`)
}
}
setConnectors({ width: grid.scrollWidth, height: grid.scrollHeight, paths })
const relayout = () => {
const heights = new Map()
for (const [key, el] of nodeRefs.current) heights.set(key, el.offsetHeight)
const next = computeBracketLayout(side.columns, heights)
setLayout({ tops: next.tops, height: next.height })
}
measure()
const observer = new ResizeObserver(measure)
relayout()
const observer = new ResizeObserver(relayout)
observer.observe(grid)
return () => observer.disconnect()
}, [side])
// Pass 2: with matches positioned, read real edges and draw elbow connectors.
useLayoutEffect(() => {
const grid = gridRef.current
if (!grid) return
const gridBox = grid.getBoundingClientRect()
const boxOf = (key) => {
const el = nodeRefs.current.get(key)
if (!el) return null
const b = el.getBoundingClientRect()
return {
left: b.left - gridBox.left,
right: b.right - gridBox.left,
mid: b.top - gridBox.top + b.height / 2,
}
}
const paths = []
for (let c = 0; c < side.columns.length - 1; c += 1) {
for (const match of side.columns[c].matches) {
const parent = feederParent(match, c, side.columns)
if (!parent) continue
const from = boxOf(layoutKey(c, match))
const to = boxOf(layoutKey(c + 1, parent))
if (!from || !to) continue
const midX = (from.right + to.left) / 2
paths.push(`M ${from.right} ${from.mid} H ${midX} V ${to.mid} H ${to.left}`)
}
}
setConnectors({ width: grid.scrollWidth, height: grid.scrollHeight, paths })
}, [side, layout])
return (
<div>
<h3 className="mb-3 text-sm font-semibold uppercase tracking-wide text-fury-cyan">{side.label}</h3>
@@ -3917,23 +3910,26 @@ function TournamentBracketSide({ side, navigate }) {
</svg>
{side.columns.map((column, columnIndex) => (
<div className="tournament-round-column" key={column.id}>
<p className="text-xs font-semibold uppercase tracking-wide text-text-muted">
<p className="mb-3 text-xs font-semibold uppercase tracking-wide text-text-muted">
{column.label}
</p>
<div className="flex flex-1 flex-col justify-around gap-3">
{column.matches.map((match) => (
<div
className="tournament-match-node"
key={`${match.type_bracket}-${match.match_id}`}
ref={(el) => {
const key = nodeKey(columnIndex, match)
if (el) nodeRefs.current.set(key, el)
else nodeRefs.current.delete(key)
}}
>
<TournamentMatchCard match={match} navigate={navigate} />
</div>
))}
<div className="tournament-round-track" style={{ height: layout.height || undefined }}>
{column.matches.map((match) => {
const key = layoutKey(columnIndex, match)
return (
<div
className="tournament-match-node"
key={`${match.type_bracket}-${match.match_id}`}
ref={(el) => {
if (el) nodeRefs.current.set(key, el)
else nodeRefs.current.delete(key)
}}
style={{ top: layout.tops.get(key) ?? 0 }}
>
<TournamentMatchCard match={match} navigate={navigate} />
</div>
)
})}
</div>
</div>
))}