Files
brackets/frontend/src/components/Forms/TournamentForm.tsx
T

262 lines
11 KiB
TypeScript

// frontend/src/components/Forms/TournamentForm.tsx
import { Loader2 } from 'lucide-react';
import React, { useEffect, useState } from 'react';
import api from '../../services/api';
interface TournamentSettings {
id: string | number;
name: string;
code: string;
type: string;
timestamp: string;
duration: number;
courts: { name?: string }[];
teams: { name?: string }[];
}
interface TournamentFormProps {
tournamentId?: string | number | null;
onSuccess: () => void;
onDelete?: (id: string | number) => void;
}
export default function TournamentForm({ tournamentId, onSuccess, onDelete }: TournamentFormProps) {
const [initialData, setInitialData] = useState<TournamentSettings | null>(null);
const [isLoading, setIsLoading] = useState<boolean>(!!tournamentId);
const [error, setError] = useState<string | null>(null);
const [isSubmitting, setIsSubmitting] = useState<boolean>(false);
useEffect(() => {
if (!tournamentId) {
setInitialData(null);
setIsLoading(false);
return;
}
const fetchSettings = async () => {
try {
const data = await api.get<TournamentSettings>(`/tournaments/${tournamentId}/settings`);
setInitialData(data);
} catch (err) {
console.error(err);
setError("Failed to load tournament settings.");
} finally {
setIsLoading(false);
}
};
fetchSettings();
}, [tournamentId]);
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
setIsSubmitting(true);
setError(null);
const formData = new FormData(e.currentTarget);
const rawTeams = formData.get('teams') as string;
const rawCourts = formData.get('courts') as string;
const date = formData.get('date') as string;
const startTime = formData.get('start_time') as string;
const typeRaw = formData.get('type') as string;
const duration = formData.get('duration') as string;
const name = formData.get('name') as string;
const code = formData.get('code') as string;
const teams = rawTeams.split('\n').map(t => t.trim()).filter(t => t.length > 0);
const courts = rawCourts.split(',').map(c => c.trim()).filter(c => c.length > 0);
if (teams.length < 2) {
setError("At least 2 teams required.");
setIsSubmitting(false);
return;
}
const timestamp = `${date}T${startTime}:00`;
const formattedType = typeRaw.charAt(0).toUpperCase() + typeRaw.slice(1);
const parsedDuration = parseInt(duration, 10);
try {
if (tournamentId) {
const basePayload = {
name,
code,
type: formattedType,
timestamp,
duration: parsedDuration
};
await Promise.all([
api.patch(`/tournaments/${tournamentId}`, basePayload),
api.patch(`/tournaments/${tournamentId}/teams`, teams),
api.patch(`/tournaments/${tournamentId}/courts`, courts)
]);
} else {
const fullPayload = {
name,
code,
type: formattedType,
timestamp,
duration: parsedDuration,
teams,
courts
};
await api.post('/tournaments', fullPayload);
}
onSuccess();
} catch (err: unknown) {
console.error(err);
const error = err as { detail?: string | Array<{ loc: string[]; msg: string }> };
if (Array.isArray(error.detail)) {
setError(error.detail.map((e) => `${e.loc.join('.')}: ${e.msg}`).join(', '));
} else {
setError(typeof error.detail === 'string' ? error.detail : "Error saving tournament");
}
} finally {
setIsSubmitting(false);
}
};
if (isLoading) {
return (
<div className="flex justify-center items-center p-12">
<Loader2 className="animate-spin text-orange-600" size={32} />
</div>
);
}
let defaultDate = new Date().toISOString().split('T')[0];
let defaultTime = "09:00";
if (initialData?.timestamp) {
const [d, t] = initialData.timestamp.split('T');
defaultDate = d;
defaultTime = t ? t.substring(0, 5) : "09:00";
}
return (
<form key={initialData?.id || 'new'} onSubmit={handleSubmit} className="space-y-4">
{error && (
<div className="text-xs text-red-500 dark:text-red-400 text-center mb-4 bg-red-50 dark:bg-red-900/10 p-2 rounded border border-red-200 dark:border-red-900/30">
{error}
</div>
)}
<div className="space-y-4">
<div>
<label className="text-xs font-bold text-zinc-500 uppercase">Name</label>
<input
name="name"
defaultValue={initialData?.name}
required
placeholder="My Awesome Tournament"
autoFocus
className="w-full bg-zinc-50 dark:bg-zinc-950 border border-zinc-300 dark:border-zinc-600 rounded p-2 focus:border-orange-500 outline-none transition text-zinc-900 dark:text-white"
/>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<label className="font-bold text-zinc-500 text-xs uppercase">Code</label>
<input
name="code"
defaultValue={initialData?.code}
required
placeholder="••••"
autoComplete="off"
className="w-full h-10 bg-zinc-50 dark:bg-zinc-950 border border-zinc-300 dark:border-zinc-600 rounded p-2 text-center font-mono focus:border-orange-500 outline-none transition text-zinc-900 dark:text-white"
/>
</div>
<div>
<label className="font-bold text-zinc-500 text-xs uppercase">Type</label>
<select
name="type"
defaultValue={initialData?.type?.toLowerCase() || "double"}
className="w-full h-10 bg-zinc-50 dark:bg-zinc-950 border border-zinc-300 dark:border-zinc-600 rounded p-2 focus:border-orange-500 outline-none transition text-zinc-900 dark:text-white"
>
<option value="double">Double Elimination</option>
<option value="single">Single Elimination</option>
</select>
</div>
</div>
<div className="grid grid-cols-7 gap-4">
<div className="col-span-2">
<label className="text-xs font-bold text-zinc-500 uppercase mb-1 block">Duration</label>
<input
type="number"
name="duration"
defaultValue={initialData?.duration || 30}
min="0"
className="w-full bg-zinc-50 dark:bg-zinc-950 border border-zinc-300 dark:border-zinc-600 rounded p-2 h-10 text-base appearance-none focus:border-orange-500 outline-none transition text-zinc-900 dark:text-white"
/>
</div>
<div className="col-span-2">
<label className="text-xs font-bold text-zinc-500 uppercase mb-1 block">Start Time</label>
<input
type="time"
name="start_time"
defaultValue={defaultTime}
className="w-full bg-zinc-50 dark:bg-zinc-950 border border-zinc-300 dark:border-zinc-600 rounded p-2 h-10 text-base appearance-none focus:border-orange-500 outline-none transition text-zinc-900 dark:text-white"
/>
</div>
<div className="col-span-3">
<label className="text-xs font-bold text-zinc-500 uppercase mb-1 block">Date</label>
<input
type="date"
name="date"
defaultValue={defaultDate}
className="w-full bg-zinc-50 dark:bg-zinc-950 border border-zinc-300 dark:border-zinc-800 rounded p-2 h-10 text-base appearance-none focus:border-orange-500 outline-none transition text-zinc-900 dark:text-white"
/>
</div>
</div>
<div>
<label className="text-xs font-bold text-zinc-500 uppercase">Courts</label>
<input
name="courts"
placeholder="Center Court, Court 1"
defaultValue={initialData?.courts?.map(c => c.name || c).join(', ')}
required
className="w-full bg-zinc-50 dark:bg-zinc-950 border border-zinc-300 dark:border-zinc-600 rounded p-2 focus:border-orange-500 outline-none transition text-zinc-900 dark:text-white"
/>
</div>
<div>
<label className="text-xs font-bold text-zinc-500 uppercase">Teams</label>
<textarea
name="teams"
placeholder="One team per line..."
defaultValue={initialData?.teams?.map(t => t.name || t).join('\n')}
rows={5}
required
className="w-full bg-zinc-50 dark:bg-zinc-950 border border-zinc-300 dark:border-zinc-600 rounded p-2 font-mono text-sm focus:border-orange-500 outline-none transition text-zinc-900 dark:text-white resize-none"
/>
</div>
</div>
<div className="flex justify-between mt-4 pt-4 border-t border-zinc-200 dark:border-zinc-800 flex-wrap gap-y-4">
{tournamentId && onDelete && (
<button
type="button"
onClick={() => onDelete(tournamentId)}
className="text-red-500 text-sm hover:underline h-5 self-end"
>
Delete Tournament
</button>
)}
{!tournamentId && <div className="hidden"></div>}
<button
disabled={isSubmitting}
type="submit"
className="bg-orange-600 hover:bg-orange-500 text-white px-6 py-2 rounded font-bold shadow-lg shadow-orange-900/20 ml-auto transition active:scale-95 disabled:opacity-50"
>
{isSubmitting ? 'Saving...' : (tournamentId ? 'Save Changes' : 'Create')}
</button>
</div>
</form>
);
}