Files
brackets/frontend/src/components/Tournament/ScoreModal.jsx
T

157 lines
7.8 KiB
React

// frontend/src/components/Tournament/ScoreModal.jsx
import React, { useState } from 'react';
import { Eraser, Clock, MapPin, Trophy } from 'lucide-react';
import { stringToColor } from '../../utils/helpers';
import Modal from '../UI/Modal';
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 onSubmit(match.id, sets, code); }
catch (err) { setError(typeof err.detail === 'string' ? err.detail : "Check code or scores"); }
};
const removeSet = (idx) => {
if (sets.length > 1) setSets(sets.filter((_, i) => i !== idx));
};
const updateSet = (idx, field, val) => {
const n = [...sets];
n[idx][field] = parseInt(val) || 0;
setSets(n);
};
return (
<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>
<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}`} icon={Trophy}>
<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>
);
}