Added refs

This commit is contained in:
2026-03-30 22:09:10 +02:00 Verified
parent 8376ffceab
commit 7fae4b24ca
13 changed files with 1182 additions and 620 deletions
+115
View File
@@ -173,6 +173,106 @@ def update_schedule_times(db: Session, t: models.Tournament):
else: else:
break break
# ==========================================
# --- DYNAMIC REFEREE ASSIGNMENT ---
# ==========================================
all_matches = sorted(list(matches), key=lambda x: (x.start_time, x.court_id))
duty_counts = defaultdict(int)
active_refs = []
def is_busy(outcome_tuple, source_m, start, end):
next_m_id = source_m.winner_next_match_id if outcome_tuple[0] == "W" else source_m.loser_next_match_id
if next_m_id:
next_m = match_map[next_m_id]
next_start = next_m.start_time
if next_start:
next_end = next_start + timedelta(minutes=t.duration)
if not (next_end <= start or next_start >= end):
return True
for r_outcome, r_start, r_end in active_refs:
if r_outcome == outcome_tuple:
if not (r_end <= start or r_start >= end):
return True
return False
for m in all_matches:
if not m.start_time:
continue
m_start = m.start_time
m_end = m_start + timedelta(minutes=t.duration)
if m.bracket_type == BracketTypes.FINALS:
prev_final = next((p for p in all_matches if p.bracket_type == BracketTypes.FINALS and p.winner_next_match_id == m.id), None)
if prev_final:
m.ref_team_id = prev_final.ref_team_id
m.ref_label = prev_final.ref_label
identifier = f"TEAM_{m.ref_team_id}" if m.ref_team_id else prev_final.ref_label
active_refs.append((identifier, m_start, m_end))
continue
best_outcome = None
best_score = float('inf')
for prev_m in all_matches:
if not prev_m.start_time:
continue
prev_end = prev_m.start_time + timedelta(minutes=t.duration)
if prev_end <= m_start:
l_outcome = ("L", prev_m.id)
if not is_busy(l_outcome, prev_m, m_start, m_end):
wait_mins = (m_start - prev_end).total_seconds() / 60.0
score = (duty_counts[l_outcome] * 120) + wait_mins
if prev_m.court_id == m.court_id:
score -= 30
if score < best_score:
best_score = score
best_outcome = (l_outcome, prev_m)
w_outcome = ("W", prev_m.id)
if not is_busy(w_outcome, prev_m, m_start, m_end):
wait_mins = (m_start - prev_end).total_seconds() / 60.0
score = (duty_counts[w_outcome] * 120) + wait_mins + 60
if prev_m.court_id == m.court_id:
score -= 30
if score < best_score:
best_score = score
best_outcome = (w_outcome, prev_m)
if best_outcome:
outcome_tuple, prev_m = best_outcome
duty_counts[outcome_tuple] += 1
active_refs.append((outcome_tuple, m_start, m_end))
role = "Winner" if outcome_tuple[0] == "W" else "Loser"
m.ref_label = f"{role} of #{prev_m.match_number}"
else:
m.ref_label = "Staff / Volunteers"
for future_m in all_matches:
if future_m.start_time and future_m.start_time >= m_end:
assigned = False
for t_id in [future_m.p1_team_id, future_m.p2_team_id]:
if t_id:
team_identifier = f"TEAM_{t_id}"
is_team_busy = any(
r_outcome == team_identifier and not (r_end <= m_start or r_start >= m_end)
for r_outcome, r_start, r_end in active_refs
)
if not is_team_busy:
m.ref_team_id = t_id
m.ref_label = None
active_refs.append((team_identifier, m_start, m_end))
assigned = True
break
if assigned:
break
# ----------------------------------
db.commit() db.commit()
@@ -182,6 +282,14 @@ def advance_winner(db: Session, match: models.Match, winner_id: int):
loser_id = match.p1_team_id if match.p1_team_id != winner_id else match.p2_team_id loser_id = match.p1_team_id if match.p1_team_id != winner_id else match.p2_team_id
for m in match.tournament.matches:
if m.ref_label == f"Loser of #{match.match_number}":
m.ref_team_id = loser_id
db.add(m)
elif m.ref_label == f"Winner of #{match.match_number}":
m.ref_team_id = winner_id
db.add(m)
if ( if (
match.bracket_type == BracketTypes.FINALS match.bracket_type == BracketTypes.FINALS
and match.winner_next_match and match.winner_next_match
@@ -246,6 +354,11 @@ def undo_advancement(db: Session, match: models.Match):
if not match.winner_team_id: if not match.winner_team_id:
return return
for m in match.tournament.matches:
if m.ref_label == f"Loser of #{match.match_number}" or m.ref_label == f"Winner of #{match.match_number}":
m.ref_team_id = None
db.add(m)
winner_id = match.winner_team_id winner_id = match.winner_team_id
loser_id = match.p1_team_id if match.p1_team_id != winner_id else match.p2_team_id loser_id = match.p1_team_id if match.p1_team_id != winner_id else match.p2_team_id
@@ -272,3 +385,5 @@ def undo_advancement(db: Session, match: models.Match):
clear_from_next(match.winner_next_match, match.winner_next_match_slot) clear_from_next(match.winner_next_match, match.winner_next_match_slot)
if match.loser_next_match and loser_id: if match.loser_next_match and loser_id:
clear_from_next(match.loser_next_match, match.loser_next_match_slot) clear_from_next(match.loser_next_match, match.loser_next_match_slot)
db.commit()
+17 -36
View File
@@ -70,51 +70,32 @@ class Match(Base):
bracket_type: Mapped[BracketTypes] = mapped_column(SqlEnum(BracketTypes)) bracket_type: Mapped[BracketTypes] = mapped_column(SqlEnum(BracketTypes))
court_id: Mapped[Optional[int]] = mapped_column( court_id: Mapped[Optional[int]] = mapped_column(ForeignKey("courts.id"), nullable=True)
ForeignKey("courts.id"), nullable=True
)
start_time: Mapped[Optional[datetime]] = mapped_column(DateTime, nullable=True) start_time: Mapped[Optional[datetime]] = mapped_column(DateTime, nullable=True)
status: Mapped[MatchStatus] = mapped_column( status: Mapped[MatchStatus] = mapped_column(SqlEnum(MatchStatus), default=MatchStatus.SCHEDULED)
SqlEnum(MatchStatus), default=MatchStatus.SCHEDULED
) p1_team_id: Mapped[Optional[int]] = mapped_column(ForeignKey("teams.id"), nullable=True)
p2_team_id: Mapped[Optional[int]] = mapped_column(ForeignKey("teams.id"), nullable=True)
ref_team_id: Mapped[Optional[int]] = mapped_column(ForeignKey("teams.id"), nullable=True)
ref_label: Mapped[Optional[str]] = mapped_column(String, nullable=True)
p1_team_id: Mapped[Optional[int]] = mapped_column(
ForeignKey("teams.id"), nullable=True
)
p2_team_id: Mapped[Optional[int]] = mapped_column(
ForeignKey("teams.id"), nullable=True
)
sets: Mapped[list[dict]] = mapped_column(JSON, default=list) sets: Mapped[list[dict]] = mapped_column(JSON, default=list)
winner_team_id: Mapped[Optional[int]] = mapped_column( winner_team_id: Mapped[Optional[int]] = mapped_column(ForeignKey("teams.id"), nullable=True)
ForeignKey("teams.id"), nullable=True
)
winner_next_match_id: Mapped[Optional[str]] = mapped_column(
ForeignKey("matches.id"), nullable=True
)
loser_next_match_id: Mapped[Optional[str]] = mapped_column(
ForeignKey("matches.id"), nullable=True
)
winner_next_match_id: Mapped[Optional[str]] = mapped_column( winner_next_match_id: Mapped[Optional[str]] = mapped_column(ForeignKey("matches.id"), nullable=True)
ForeignKey("matches.id"), nullable=True winner_next_match_slot: Mapped[Optional[int]] = mapped_column(Integer, nullable=True)
)
winner_next_match_slot: Mapped[Optional[int]] = mapped_column(
Integer, nullable=True
)
loser_next_match_id: Mapped[Optional[str]] = mapped_column( loser_next_match_id: Mapped[Optional[str]] = mapped_column(ForeignKey("matches.id"), nullable=True)
ForeignKey("matches.id"), nullable=True
)
loser_next_match_slot: Mapped[Optional[int]] = mapped_column(Integer, nullable=True) loser_next_match_slot: Mapped[Optional[int]] = mapped_column(Integer, nullable=True)
tournament: Mapped["Tournament"] = relationship(back_populates="matches") tournament: Mapped["Tournament"] = relationship(back_populates="matches")
court: Mapped[Optional["Court"]] = relationship() court: Mapped[Optional["Court"]] = relationship()
p1_team: Mapped["Team"] = relationship("Team", foreign_keys=[p1_team_id]) p1_team: Mapped["Team"] = relationship("Team", foreign_keys=[p1_team_id])
p2_team: Mapped["Team"] = relationship("Team", foreign_keys=[p2_team_id]) p2_team: Mapped["Team"] = relationship("Team", foreign_keys=[p2_team_id])
winner_next_match: Mapped["Match"] = relationship(
"Match", remote_side=[id], foreign_keys=[winner_next_match_id] ref_team: Mapped[Optional["Team"]] = relationship("Team", foreign_keys=[ref_team_id])
)
loser_next_match: Mapped["Match"] = relationship( winner_next_match: Mapped["Match"] = relationship("Match", remote_side=[id], foreign_keys=[winner_next_match_id])
"Match", remote_side=[id], foreign_keys=[loser_next_match_id] loser_next_match: Mapped["Match"] = relationship("Match", remote_side=[id], foreign_keys=[loser_next_match_id])
)
+4
View File
@@ -50,6 +50,10 @@ class MatchOut(BaseModel):
p2_team_id: int | None = None p2_team_id: int | None = None
winner_team_id: int | None = None winner_team_id: int | None = None
ref_team_id: int | None = None
ref_label: str | None = None
ref_team: TeamSchema | None = None
winner_next_match_id: str | None = None winner_next_match_id: str | None = None
winner_next_match_slot: int | None = None winner_next_match_slot: int | None = None
loser_next_match_id: str | None = None loser_next_match_id: str | None = None
+981 -561
View File
File diff suppressed because it is too large Load Diff
+1
View File
@@ -39,6 +39,7 @@
"typescript-eslint": "^8.57.0", "typescript-eslint": "^8.57.0",
"vite": "^7.3.1", "vite": "^7.3.1",
"vite-plugin-pwa": "^1.2.0", "vite-plugin-pwa": "^1.2.0",
"vite-plugin-svgr": "^5.0.0",
"vitest": "^4.0.18" "vitest": "^4.0.18"
} }
} }
+3
View File
@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 512" fill="currentColor">
<path d="M400 64c8.8 0 16 7.2 16 16 0 17.7 14.3 32 32 32s32-14.3 32-32c0-8.8 7.2-16 16-16l112 0c17.7 0 32 14.3 32 32l0 70.3c0 15-10.4 28-25.1 31.2L413.8 242.2c1.4 9.7 2.2 19.6 2.2 29.8 0 114.9-93.1 208-208 208S0 386.9 0 272 93.1 64 208 64l192 0zM208 192a80 80 0 1 0 0 160 80 80 0 1 0 0-160z"/>
</svg>

After

Width:  |  Height:  |  Size: 386 B

+12 -1
View File
@@ -3,6 +3,7 @@
import { Check } from 'lucide-react'; import { Check } from 'lucide-react';
import { type MatchData } from '../../types'; import { type MatchData } from '../../types';
import { printName, stringToColor } from '../../utils/helpers'; import { printName, stringToColor } from '../../utils/helpers';
import WhistleIcon from "../../assets/whistle.svg?react"
interface MatchCardProps { interface MatchCardProps {
match: MatchData; match: MatchData;
@@ -21,6 +22,8 @@ export default function MatchCard({ match, onClick }: MatchCardProps) {
? 'cursor-pointer hover:shadow-md hover:-translate-y-0.5' ? 'cursor-pointer hover:shadow-md hover:-translate-y-0.5'
: 'cursor-default opacity-100'; : 'cursor-default opacity-100';
const refName = match.ref_team?.name || match.ref_label;
return ( return (
<div <div
id={`match-${match.id}`} id={`match-${match.id}`}
@@ -43,7 +46,7 @@ export default function MatchCard({ match, onClick }: MatchCardProps) {
)} )}
</div> </div>
<div className="p-2 space-y-1.5"> <div className="p-2 space-y-1.5 flex-1 flex flex-col justify-center">
{[ {[
{ n: match.p1, s: match.p1_sets, win: match.winner_team_id !== null && match.winner_team_id === match.p1_team_id, real: match.p1_is_real }, { n: match.p1, s: match.p1_sets, win: match.winner_team_id !== null && match.winner_team_id === match.p1_team_id, real: match.p1_is_real },
{ n: match.p2, s: match.p2_sets, win: match.winner_team_id !== null && match.winner_team_id === match.p2_team_id, real: match.p2_is_real } { n: match.p2, s: match.p2_sets, win: match.winner_team_id !== null && match.winner_team_id === match.p2_team_id, real: match.p2_is_real }
@@ -66,6 +69,14 @@ export default function MatchCard({ match, onClick }: MatchCardProps) {
</div> </div>
))} ))}
</div> </div>
{/* --- FLOATING REF BADGE --- */}
{refName && !isFinished && (
<div className="absolute -bottom-2.5 left-1/2 -translate-x-1/2 bg-zinc-100 dark:bg-zinc-800 border border-zinc-200 dark:border-zinc-700 text-zinc-500 dark:text-zinc-400 text-[8px] font-black uppercase tracking-widest px-2.5 py-0.5 rounded-full shadow-sm whitespace-nowrap z-20 flex items-center gap-1.5 print:hidden max-w-[90%]">
<WhistleIcon className="text-orange-500 shrink-0" width={12} height={12} />
<span className="truncate">{refName}</span>
</div>
)}
</div> </div>
); );
} }
@@ -4,6 +4,7 @@ import { CheckCircle, Pencil, Plus, Trophy } from 'lucide-react';
import React from 'react'; import React from 'react';
import { type MatchData } from '../../types'; import { type MatchData } from '../../types';
import { printName, stringToColor } from '../../utils/helpers'; import { printName, stringToColor } from '../../utils/helpers';
import WhistleIcon from "../../assets/whistle.svg?react"
interface ScheduleRowProps { interface ScheduleRowProps {
match: MatchData; match: MatchData;
@@ -14,9 +15,10 @@ interface ScheduleRowProps {
export default function ScheduleRow({ match: m, onMatchClick, badgeWidth }: ScheduleRowProps) { export default function ScheduleRow({ match: m, onMatchClick, badgeWidth }: ScheduleRowProps) {
const courtColor = stringToColor(m.court); const courtColor = stringToColor(m.court);
const isFinished = m.isFinished; const isFinished = m.isFinished;
const refName = m.ref_team?.name || m.ref_label;
return ( return (
<div className="transition-colors bg-white dark:bg-zinc-900 print:bg-white! p-4 print:p-3 rounded-2xl print:rounded-none border border-zinc-300 dark:border-zinc-800 print:border-b! print:border-x-0! print:border-t-0! print:border-zinc-300! shadow-sm print:shadow-none! flex items-center justify-between hover:border-orange-500/30"> <div className="relative mb-5 transition-colors bg-white dark:bg-zinc-900 print:bg-white! p-4 print:p-3 rounded-2xl print:rounded-none border border-zinc-300 dark:border-zinc-800 print:border-b! print:border-x-0! print:border-t-0! print:border-zinc-300! shadow-sm print:shadow-none! flex items-center justify-between hover:border-orange-500/30">
<div className="flex gap-4 md:gap-6 print:gap-6 flex-1 min-w-0"> <div className="flex gap-4 md:gap-6 print:gap-6 flex-1 min-w-0">
<div className="flex flex-col gap-1 items-center shrink-0" style={{ minWidth: badgeWidth }}> <div className="flex flex-col gap-1 items-center shrink-0" style={{ minWidth: badgeWidth }}>
<div className="text-lg md:text-xl print:text-xl font-black font-mono text-zinc-900 dark:text-white print:text-black!">{m.time}</div> <div className="text-lg md:text-xl print:text-xl font-black font-mono text-zinc-900 dark:text-white print:text-black!">{m.time}</div>
@@ -42,7 +44,10 @@ export default function ScheduleRow({ match: m, onMatchClick, badgeWidth }: Sche
</React.Fragment> </React.Fragment>
))} ))}
</div> </div>
<div className="transition-colors hidden md:block print:block text-tiny font-black bg-zinc-100 dark:bg-zinc-800 print:bg-transparent! text-zinc-400 print:text-zinc-500! px-2 py-1 rounded w-fit">Match #{m.number}</div>
<div className="flex flex-col md:flex-row md:items-center gap-0.5 md:gap-2 mt-0.5 md:mt-1">
<div className="transition-colors hidden md:block print:block text-tiny font-black bg-zinc-100 dark:bg-zinc-800 print:bg-transparent! text-zinc-400 print:text-zinc-500! px-2 py-1 rounded w-fit">Match #{m.number}</div>
</div>
</div> </div>
</div> </div>
@@ -54,19 +59,14 @@ export default function ScheduleRow({ match: m, onMatchClick, badgeWidth }: Sche
className="flex flex-col justify-between items-center md:justify-center md:items-end hover:bg-zinc-50 dark:hover:bg-zinc-800 p-1.5 md:p-2 rounded-xl transition group/btn min-w-8 md:min-w-20 border border-transparent hover:border-zinc-200 dark:hover:border-zinc-700" className="flex flex-col justify-between items-center md:justify-center md:items-end hover:bg-zinc-50 dark:hover:bg-zinc-800 p-1.5 md:p-2 rounded-xl transition group/btn min-w-8 md:min-w-20 border border-transparent hover:border-zinc-200 dark:hover:border-zinc-700"
title="Edit Score" title="Edit Score"
> >
{/* --- MOBILE VERTICAL STACK --- */}
<div className={`${m.winnerName === m.p1 ? 'bg-orange-100 dark:bg-orange-900/30 text-orange-700' : 'bg-zinc-100 dark:bg-zinc-800 text-zinc-900 dark:text-zinc-50'} px-2 py-0.5 rounded text-[10px] font-black font-mono border border-zinc-200 dark:border-zinc-700 md:hidden`}> <div className={`${m.winnerName === m.p1 ? 'bg-orange-100 dark:bg-orange-900/30 text-orange-700' : 'bg-zinc-100 dark:bg-zinc-800 text-zinc-900 dark:text-zinc-50'} px-2 py-0.5 rounded text-[10px] font-black font-mono border border-zinc-200 dark:border-zinc-700 md:hidden`}>
{m.p1_sets} {m.p1_sets}
</div> </div>
{/* Clean minimal vertical line for mobile */}
<div className="w-0.5 h-3 bg-zinc-200 dark:bg-zinc-700 rounded-full md:hidden my-1" /> <div className="w-0.5 h-3 bg-zinc-200 dark:bg-zinc-700 rounded-full md:hidden my-1" />
<div className={`${m.winnerName === m.p2 ? 'bg-orange-100 dark:bg-orange-900/30 text-orange-700' : 'bg-zinc-100 dark:bg-zinc-800 text-zinc-900 dark:text-zinc-50'} px-2 py-0.5 rounded text-[10px] font-black font-mono border border-zinc-200 dark:border-zinc-700 md:hidden`}> <div className={`${m.winnerName === m.p2 ? 'bg-orange-100 dark:bg-orange-900/30 text-orange-700' : 'bg-zinc-100 dark:bg-zinc-800 text-zinc-900 dark:text-zinc-50'} px-2 py-0.5 rounded text-[10px] font-black font-mono border border-zinc-200 dark:border-zinc-700 md:hidden`}>
{m.p2_sets} {m.p2_sets}
</div> </div>
{/* --- DESKTOP VIEW (Default + Hover Edit) --- */}
<div className="hidden md:flex group-hover/btn:hidden flex-col items-end"> <div className="hidden md:flex group-hover/btn:hidden flex-col items-end">
<div className="text-orange-500 font-black text-[10px] uppercase flex items-center gap-1"> <div className="text-orange-500 font-black text-[10px] uppercase flex items-center gap-1">
<CheckCircle size={12} strokeWidth={3} /> Finished <CheckCircle size={12} strokeWidth={3} /> Finished
@@ -102,6 +102,13 @@ export default function ScheduleRow({ match: m, onMatchClick, badgeWidth }: Sche
{isFinished ? m.p2_sets : ''} {isFinished ? m.p2_sets : ''}
</div> </div>
</div> </div>
{refName && !isFinished && (
<div className="absolute -bottom-3 left-1/2 -translate-x-1/2 bg-zinc-100 dark:bg-zinc-800 border border-zinc-200 dark:border-zinc-700 text-zinc-500 dark:text-zinc-300 text-[9px] md:text-[10px] font-black uppercase tracking-widest px-3 md:px-4 py-1 rounded-full shadow-md whitespace-nowrap z-20 flex items-center gap-1.5 print:hidden max-w-[90%]">
<WhistleIcon className="text-orange-500 shrink-0" width={12} height={12} />
<span className="truncate">{refName}</span>
</div>
)}
</div> </div>
); );
} }
@@ -21,7 +21,7 @@ export default function ScheduleView({ schedule, onMatchClick }: ScheduleViewPro
const badgeWidth = Math.max(80, longestCourt.length * 10); const badgeWidth = Math.max(80, longestCourt.length * 10);
const filteredAndSorted = schedule const filteredAndSorted = schedule
.filter(m => (m.p1 + m.p2 + m.number + `Match #${m.number}`).toLowerCase().includes(filter.toLowerCase())) .filter(m => (m.p1 + m.p2 + m.number + `Match #${m.number}` + m.ref_label).toLowerCase().includes(filter.toLowerCase()))
.sort((a, b) => { .sort((a, b) => {
const timeA = a.start_time || a.timestamp || a.time || ""; const timeA = a.start_time || a.timestamp || a.time || "";
const timeB = b.start_time || b.timestamp || b.time || ""; const timeB = b.start_time || b.timestamp || b.time || "";
@@ -4,7 +4,7 @@ import { type SetData } from '../../types';
import { Clock, Eraser, MapPin, Trophy } from 'lucide-react'; import { Clock, Eraser, MapPin, Trophy } from 'lucide-react';
import { useState } from 'react'; import { useState } from 'react';
import Modal from '../UI/Modal'; import Modal from '../UI/Modal';
import WhistleIcon from "../../assets/whistle.svg?react"
interface MatchData { interface MatchData {
id: string | number; id: string | number;
@@ -15,6 +15,8 @@ interface MatchData {
p1_label?: string; p1_label?: string;
p2?: string; p2?: string;
p2_label?: string; p2_label?: string;
ref_label?: string;
ref_team?: { name: string };
isFinished: boolean; isFinished: boolean;
sets?: SetData[]; sets?: SetData[];
} }
@@ -30,6 +32,7 @@ const ScoreForm = ({ match, isAuthenticated, onSubmit, onClear }: ScoreFormProps
const [sets, setSets] = useState<SetData[]>(match.sets && match.sets.length ? match.sets : [{ p1: '', p2: '' }]); const [sets, setSets] = useState<SetData[]>(match.sets && match.sets.length ? match.sets : [{ p1: '', p2: '' }]);
const [code, setCode] = useState<string>(''); const [code, setCode] = useState<string>('');
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
const refName = match.ref_team?.name || match.ref_label;
const handleSubmit = async () => { const handleSubmit = async () => {
try { try {
@@ -52,17 +55,29 @@ const ScoreForm = ({ match, isAuthenticated, onSubmit, onClear }: ScoreFormProps
return ( return (
<div className="space-y-6"> <div className="space-y-6">
{/* Top Info Bar */} <div className="flex flex-col gap-2">
<div className="flex justify-center items-center gap-4 bg-zinc-50 dark:bg-zinc-950 p-3 rounded-lg border border-gray-200 dark:border-zinc-800 shadow-sm transition-colors"> {/* Time & Court Row */}
<div className="flex items-center gap-2 text-sm font-mono text-zinc-600 dark:text-zinc-300"> <div className="flex justify-center items-center gap-4 bg-zinc-50 dark:bg-zinc-950 p-3 rounded-lg border border-gray-200 dark:border-zinc-800 shadow-sm transition-colors">
<Clock className="text-orange-500" size={18} /> <div className="flex items-center gap-2 text-sm font-mono text-zinc-600 dark:text-zinc-300">
<span>{match.time || "10:00"}</span> <Clock className="text-orange-500" size={16} />
</div> <span>{match.time || "10:00"}</span>
<div className="h-4 w-px bg-zinc-300 dark:bg-zinc-800" /> </div>
<div className="flex items-center gap-2 text-sm font-mono text-zinc-900 dark:text-white"> <div className="h-4 w-px bg-zinc-300 dark:bg-zinc-800" />
<MapPin className="text-orange-500" size={18} /> <div className="flex items-center gap-2 text-sm font-mono text-zinc-900 dark:text-white">
<span>{match.court || "TBD"}</span> <MapPin className="text-orange-500" size={16} />
<span>{match.court || "TBD"}</span>
</div>
</div> </div>
{/* Dedicated Ref Row (Always visible) */}
{refName && (
<div className="flex justify-center items-center gap-2 bg-orange-50 dark:bg-orange-900/10 p-2.5 rounded-lg border border-orange-100 dark:border-orange-900/30">
<WhistleIcon className="text-orange-500 shrink-0" width={14} height={14} />
<span className="text-xs font-bold text-orange-700 dark:text-orange-500 uppercase tracking-wide truncate">
{refName}
</span>
</div>
)}
</div> </div>
{error && ( {error && (
+2
View File
@@ -31,6 +31,8 @@ export interface MatchData {
loser_next_match_id?: string | number | null; loser_next_match_id?: string | number | null;
timestamp?: string; timestamp?: string;
start_time?: string; start_time?: string;
ref_label?: string;
ref_team?: { id: string | number; name: string };
} }
export interface SetData { export interface SetData {
+1
View File
@@ -1,3 +1,4 @@
// frontend/src/vite-env.d.ts // frontend/src/vite-env.d.ts
/// <reference types="vite/client" /> /// <reference types="vite/client" />
/// <reference types="vite-plugin-svgr/client" />
+2
View File
@@ -4,11 +4,13 @@ import tailwindcss from '@tailwindcss/vite';
import react from '@vitejs/plugin-react'; import react from '@vitejs/plugin-react';
import { defineConfig } from 'vitest/config'; import { defineConfig } from 'vitest/config';
import { VitePWA } from 'vite-plugin-pwa'; import { VitePWA } from 'vite-plugin-pwa';
import svgr from 'vite-plugin-svgr';
export default defineConfig({ export default defineConfig({
plugins: [ plugins: [
react(), react(),
tailwindcss(), tailwindcss(),
svgr(),
VitePWA({ VitePWA({
registerType: 'autoUpdate', registerType: 'autoUpdate',
includeAssets: ['favicon.svg'], includeAssets: ['favicon.svg'],