Better bracket logic
This commit is contained in:
@@ -1,37 +1,19 @@
|
||||
// frontend/src/components/Tournament/ScoreModal.jsx
|
||||
|
||||
import { Trash2 } from 'lucide-react';
|
||||
import { useState } from 'react';
|
||||
import api from '../../services/api';
|
||||
import React, { useState } from 'react';
|
||||
import { Eraser, Clock, MapPin } from 'lucide-react';
|
||||
import { stringToColor } from '../../utils/helpers';
|
||||
import Modal from '../UI/Modal';
|
||||
|
||||
export default function ScoreModal({ isOpen, onClose, node, tournamentId, isAdmin }) {
|
||||
const match = node.match;
|
||||
|
||||
const initialSets = match.sets?.length ? match.sets : [{ p1: '', p2: '' }];
|
||||
const [sets, setSets] = useState(initialSets);
|
||||
const ScoreForm = ({ match, isAdmin, onSubmit, onClear }) => {
|
||||
// Safe init of sets
|
||||
const [sets, setSets] = useState(match.sets && match.sets.length ? match.sets : [{ p1: '', p2: '' }]);
|
||||
const [code, setCode] = useState('');
|
||||
const [error, setError] = useState(null);
|
||||
|
||||
const handleSubmit = async () => {
|
||||
try {
|
||||
await api.post(`/tournaments/${tournamentId}/matches/${match.id}/score`, {
|
||||
code: code || undefined,
|
||||
sets: sets.map(s => ({ p1: Number(s.p1), p2: Number(s.p2) }))
|
||||
});
|
||||
onClose();
|
||||
} catch (err) {
|
||||
setError("Check code or scores");
|
||||
}
|
||||
};
|
||||
|
||||
const handleClear = async () => {
|
||||
if (!confirm("Clear match?")) return;
|
||||
try {
|
||||
await api.delete(`/tournaments/${tournamentId}/matches/${match.id}/score`, { params: { code: code || undefined } });
|
||||
onClose();
|
||||
} catch (err) { setError("Error clearing"); }
|
||||
try { await onSubmit(match.id, sets, code); }
|
||||
catch (err) { setError(typeof err.detail === 'string' ? err.detail : "Check code or scores"); }
|
||||
};
|
||||
|
||||
const removeSet = (idx) => {
|
||||
@@ -44,68 +26,132 @@ export default function ScoreModal({ isOpen, onClose, node, tournamentId, isAdmi
|
||||
setSets(n);
|
||||
};
|
||||
|
||||
const timeDisplay = match.start_time || node.planned_start_time;
|
||||
const courtName = match.court?.name || 'TBD';
|
||||
const p1Name = node.p1_team?.name || 'TBD';
|
||||
const p2Name = node.p2_team?.name || 'TBD';
|
||||
|
||||
return (
|
||||
<Modal isOpen={isOpen} onClose={onClose} title={`Match Protocol #${node.display_number}`}>
|
||||
<div className="space-y-6">
|
||||
{error && <div className="bg-red-50 text-red-600 p-3 rounded-xl text-center text-sm font-bold border border-red-100">{error}</div>}
|
||||
|
||||
<div className="flex justify-around items-center bg-zinc-50 dark:bg-zinc-950 p-5 rounded-2xl border border-zinc-200 dark:border-zinc-800 shadow-inner">
|
||||
<div className="text-center">
|
||||
<div className="text-[10px] font-black uppercase tracking-[0.2em] text-zinc-400 mb-1">Time</div>
|
||||
<div className="text-xl font-black font-mono text-zinc-900 dark:text-white">{new Date(timeDisplay).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}</div>
|
||||
</div>
|
||||
<div className="w-px h-10 bg-zinc-200 dark:bg-zinc-800" />
|
||||
<div className="text-center">
|
||||
<div className="text-[10px] font-black uppercase tracking-[0.2em] text-zinc-400 mb-1">Court</div>
|
||||
<div className="text-xl font-black uppercase text-zinc-900 dark:text-white tracking-tighter flex items-center gap-2">
|
||||
<div className="w-2.5 h-2.5 rounded-full" style={{ background: stringToColor(courtName) }} />
|
||||
{courtName}
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-6">
|
||||
{/* Top Info Bar */}
|
||||
<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">
|
||||
<div className="flex items-center gap-2 text-sm font-mono text-zinc-600 dark:text-zinc-300">
|
||||
<Clock className="text-orange-500" size={18} />
|
||||
<span>{match.time || "10:00"}</span>
|
||||
</div>
|
||||
|
||||
{!isAdmin && (
|
||||
<div className="space-y-2">
|
||||
<label className="text-[10px] font-black uppercase tracking-widest text-zinc-400">Authorization</label>
|
||||
<input type="password" value={code} onChange={e => setCode(e.target.value)} placeholder="•••••" className="w-full bg-zinc-50 dark:bg-zinc-950 border border-zinc-300 dark:border-zinc-800 p-3 rounded-xl text-center tracking-[0.5em] dark:text-white font-bold outline-none focus:border-orange-500" />
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="grid grid-cols-3 gap-2 text-center font-black text-zinc-800 dark:text-zinc-200 items-center">
|
||||
<div className="text-sm truncate uppercase tracking-tight">{p1Name}</div>
|
||||
<div className="text-[10px] bg-orange-600 text-white px-3 py-1.5 rounded-full w-fit mx-auto shadow-lg shadow-orange-600/20">VS</div>
|
||||
<div className="text-sm truncate uppercase tracking-tight">{p2Name}</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
{sets.map((s, i) => (
|
||||
<div key={i} className="animate-in slide-in-from-top-1 px-1">
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="flex-1 flex items-center gap-3">
|
||||
<input type="number" value={s.p1} onChange={e => updateSet(i, 'p1', e.target.value)} className="w-full bg-zinc-50 dark:bg-zinc-950 border border-zinc-300 dark:border-zinc-800 p-4 rounded-xl text-center dark:text-white font-black text-xl outline-none focus:border-orange-500 shadow-sm" />
|
||||
<div className="w-4 h-0.5 bg-zinc-300 dark:bg-zinc-700 rounded-full shrink-0" />
|
||||
<input type="number" value={s.p2} onChange={e => updateSet(i, 'p2', e.target.value)} className="w-full bg-zinc-50 dark:bg-zinc-950 border border-zinc-300 dark:border-zinc-800 p-4 rounded-xl text-center dark:text-white font-black text-xl outline-none focus:border-orange-500 shadow-sm" />
|
||||
</div>
|
||||
<button onClick={() => removeSet(i)} title="Remove Set" className="p-3 text-zinc-300 hover:text-red-500 hover:bg-red-50 dark:hover:bg-red-950/30 rounded-xl transition shrink-0 group">
|
||||
<Trash2 size={20} className="group-hover:scale-110 transition-transform" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<button onClick={() => setSets([...sets, { p1: '', p2: '' }])} className="w-full py-4 border-2 border-dashed border-zinc-300 dark:border-zinc-800 text-zinc-500 rounded-xl text-[10px] font-black uppercase tracking-[0.2em] hover:border-orange-500 hover:text-orange-500 transition active:bg-orange-50 dark:active:bg-orange-900/10">+ Add Set</button>
|
||||
|
||||
<div className="flex gap-3 pt-4 border-t border-zinc-100 dark:border-zinc-800">
|
||||
{match.winner_team_id && <button onClick={handleClear} className="w-1/3 bg-red-100 dark:bg-red-900/30 text-red-600 dark:text-red-400 rounded-xl font-black uppercase tracking-widest text-[10px] transition active:scale-95 border border-red-200 dark:border-red-900/50">Clear Match</button>}
|
||||
<button onClick={handleSubmit} className="flex-1 bg-orange-600 hover:bg-orange-500 text-white py-4 rounded-xl font-black uppercase tracking-widest text-sm shadow-xl shadow-orange-600/20 transition active:scale-95">Submit Result</button>
|
||||
<div className="h-4 w-px bg-zinc-300 dark:bg-zinc-800" />
|
||||
<div className="flex items-center gap-2 text-sm font-mono text-zinc-900 dark:text-white">
|
||||
<MapPin className="text-orange-500" size={18} />
|
||||
<span>{match.court || "TBD"}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<div className="text-xs text-red-500 dark:text-red-400 text-center bg-red-50 dark:bg-red-900/10 p-2 rounded border border-red-200 dark:border-red-900/30">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!isAdmin && (
|
||||
<div className="bg-zinc-50 dark:bg-zinc-950 p-4 rounded-lg border border-gray-200 dark:border-zinc-800">
|
||||
<label className="block text-xs font-bold text-orange-500 uppercase mb-2">Tournament Code</label>
|
||||
<input
|
||||
type="password"
|
||||
value={code}
|
||||
onChange={e => setCode(e.target.value)}
|
||||
placeholder="•••••"
|
||||
autoComplete="off"
|
||||
className="w-full bg-white dark:bg-zinc-900 border border-gray-300 dark:border-zinc-700 rounded p-3 text-center text-lg tracking-[0.5em] focus:ring-1 focus:ring-orange-500 outline-none transition text-zinc-900 dark:text-white shadow-sm"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="grid grid-cols-3 gap-2 text-center font-bold text-zinc-700 dark:text-zinc-200 items-center px-2">
|
||||
<div className="break-words text-sm leading-tight uppercase">{match.p1 || match.p1_label}</div>
|
||||
<div className="text-zinc-400 dark:text-zinc-600 text-[10px] font-bold uppercase bg-zinc-100 dark:bg-zinc-950 px-3 py-1 rounded-full w-fit mx-auto shadow-sm">VS</div>
|
||||
<div className="break-words text-sm leading-tight uppercase">{match.p2 || match.p2_label}</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2 max-h-48 overflow-y-auto pr-1">
|
||||
{sets.map((s, i) => (
|
||||
<div key={i} className="animate-in slide-in-from-top-1 px-1">
|
||||
<div className="flex items-center justify-center gap-2">
|
||||
<div className="grid grid-cols-3 gap-2 items-center justify-items-center w-full">
|
||||
<input
|
||||
type="number"
|
||||
min="0"
|
||||
pattern="[0-9]*"
|
||||
value={s.p1}
|
||||
onChange={(e) => updateSet(i, "p1", e.target.value)}
|
||||
className="w-full bg-gray-50 dark:bg-zinc-950 border border-gray-300 dark:border-zinc-800 p-2 rounded text-center focus:border-orange-500 outline-none text-zinc-900 dark:text-white"
|
||||
/>
|
||||
|
||||
{sets.length > 1 ? (
|
||||
<button
|
||||
onClick={() => removeSet(i)}
|
||||
title="Remove Set"
|
||||
className="mx-auto p-2 text-zinc-300 hover:text-red-500 transition shrink-0 group"
|
||||
>
|
||||
<Eraser
|
||||
size={18}
|
||||
className="group-hover:scale-110 transition-transform"
|
||||
/>
|
||||
</button>
|
||||
) : (
|
||||
<span className="text-zinc-400 dark:text-zinc-600 text-center font-bold">
|
||||
-
|
||||
</span>
|
||||
)}
|
||||
|
||||
<input
|
||||
type="number"
|
||||
min="0"
|
||||
pattern="[0-9]*"
|
||||
value={s.p2}
|
||||
onChange={(e) => updateSet(i, "p2", e.target.value)}
|
||||
className="w-full bg-gray-50 dark:bg-zinc-950 border border-gray-300 dark:border-zinc-800 p-2 rounded text-center focus:border-orange-500 outline-none text-zinc-900 dark:text-white"
|
||||
/>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<button onClick={() => setSets([...sets, { p1: '', p2: '' }])} className="w-full py-2 border border-dashed border-zinc-300 dark:border-zinc-700 text-zinc-500 dark:text-zinc-400 text-sm hover:border-orange-500 hover:text-orange-500 transition rounded">+ Add Set</button>
|
||||
|
||||
<div className="flex gap-2">
|
||||
{match.isFinished && (
|
||||
<button
|
||||
onClick={() => onClear(match.id, code)}
|
||||
className="w-1/3 bg-red-100 dark:bg-red-900/50 hover:bg-red-200 dark:hover:bg-red-900 text-red-600 dark:text-red-300 py-3 rounded-lg font-bold transition text-sm"
|
||||
>
|
||||
Clear
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
onClick={handleSubmit}
|
||||
className={`${match.isFinished ? 'w-2/3' : 'w-full'} bg-orange-600 hover:bg-orange-500 py-3 rounded-lg font-bold shadow-lg shadow-orange-900/20 transition text-white active:scale-95`}
|
||||
>
|
||||
Submit Result
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default function ScoreModal({ isOpen, onClose, match, isAdmin, onSubmit, onClear }) {
|
||||
if (!isOpen || !match) return null;
|
||||
|
||||
return (
|
||||
<Modal isOpen={isOpen} onClose={onClose} title={`Match #${match.number}`}>
|
||||
<ScoreForm
|
||||
match={match}
|
||||
isAdmin={isAdmin}
|
||||
onSubmit={async (id, sets, code) => {
|
||||
await onSubmit(id, sets, code);
|
||||
onClose();
|
||||
}}
|
||||
onClear={async (id, code) => {
|
||||
await onClear(id, code);
|
||||
onClose();
|
||||
}}
|
||||
/>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user