// frontend/src/pages/Tournament.jsx import { CalendarDays, Loader2, Network, Settings } from 'lucide-react'; import { useEffect, useRef, useState } from 'react'; import { useOutletContext, useParams } from 'react-router-dom'; import BracketView from '../components/Bracket/BracketView'; import TournamentForm from '../components/Forms/TournamentForm'; import ScheduleView from '../components/Schedule/ScheduleView'; import ScoreModal from '../components/Tournament/ScoreModal'; import Modal from '../components/UI/Modal'; import api, { WS_URL } from '../services/api'; export default function Tournament() { const { id } = useParams(); const { setNavTitle, setNavSubtitle, isAdmin } = useOutletContext(); const [matches, setMatches] = useState([]); const [view, setView] = useState(() => localStorage.getItem('volley_view') || 'bracket'); const [loading, setLoading] = useState(true); const [showSettings, setShowSettings] = useState(false); const [scoreMatch, setScoreMatch] = useState(null); const wsRef = useRef(null); const handleViewChange = (newView) => { setView(newView); localStorage.setItem('volley_view', newView); }; const processMatches = (rawMatches, courts, teams) => { if (!rawMatches) return []; const courtMap = Object.fromEntries(courts.map(c => [c.id, c.name])); const teamMap = Object.fromEntries(teams.map(t => [t.id, t.name])); const incoming = {}; rawMatches.forEach(m => { const num = m.match_number; if (m.winner_next_match_id) { (incoming[m.winner_next_match_id] = incoming[m.winner_next_match_id] || []).push({ label: `Winner of #${num}`, id: m.id }); } if (m.loser_next_match_id) { (incoming[m.loser_next_match_id] = incoming[m.loser_next_match_id] || []).push({ label: `Loser of #${num}`, id: m.id }); } }); return rawMatches.map(m => { const sources = incoming[m.id] || []; let sourceIndex = 0; const p1 = m.p1_team_id ? teamMap[m.p1_team_id] : (sources[sourceIndex++]?.label || 'TBD'); const p2 = m.p2_team_id ? teamMap[m.p2_team_id] : (sources[sourceIndex++]?.label || 'TBD'); const hasTeams = !!(m.p1_team_id && m.p2_team_id); const isFinished = m.status === "Finished"; const isReady = hasTeams && !isFinished; return { ...m, bracket: m.bracket_type, round: m.round_number, number: m.match_number, p1, p2, p1_is_real: !!m.p1_team_id, p2_is_real: !!m.p2_team_id, isReady, court: courtMap[m.court_id] || 'TBD', time: m.start_time ? new Date(m.start_time).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }) : '', p1_sets: m.sets?.filter(s => s.p1 > s.p2).length || 0, p2_sets: m.sets?.filter(s => s.p2 > s.p1).length || 0, hasTeams, isFinished }; }); }; const fetchData = async () => { try { const res = await api.get(`/tournaments/${id}`); setNavTitle(res.name); setNavSubtitle(new Date(res.timestamp).toLocaleDateString()); setMatches(processMatches(res.matches, res.courts, res.teams)); } catch (err) { console.error(err); } finally { setLoading(false); } }; useEffect(() => { fetchData(); if (wsRef.current) return; const connect = () => { const ws = new WebSocket(WS_URL); wsRef.current = ws; ws.onmessage = (e) => { const msg = JSON.parse(e.data); if (msg.type === 'tournament_update' && msg.id === id) fetchData(); }; ws.onclose = () => { wsRef.current = null; }; }; connect(); return () => { if (wsRef.current?.readyState === 1) wsRef.current.close(); wsRef.current = null; }; }, [id]); const handleDeleteTournament = async (tId) => { if (window.confirm("Purge this tournament?")) { await api.delete(`/tournaments/${tId}`); window.location.href = '/'; } }; if (loading) return