diff --git a/frontend/src/Pages/TournamentHub.tsx b/frontend/src/Pages/TournamentHub.tsx index be73f1c..4a8bf78 100644 --- a/frontend/src/Pages/TournamentHub.tsx +++ b/frontend/src/Pages/TournamentHub.tsx @@ -11,6 +11,9 @@ export interface Tournament { description: string; } +const MIN_PARTICIPANTS = 4; +const MAX_PARTICIPANTS = 64; + export default function TournamentPage() { const initialTournaments: Tournament[] = [ { @@ -47,9 +50,35 @@ export default function TournamentPage() { const [name, setName] = useState(""); const [date, setDate] = useState(""); const [description, setDescription] = useState(""); + const [participantOption, setParticipantOption] = useState("8"); + const [customParticipants, setCustomParticipants] = useState(""); + const [customError, setCustomError] = useState(""); const [error, setError] = useState(""); const navigate = useNavigate(); + const isPowerOfTwo = (n: number) => n > 1 && (n & (n - 1)) === 0; + + const getMaxParticipants = (): number | null => { + if (participantOption === "custom") { + const val = Number(customParticipants); + if (!Number.isInteger(val) || val < MIN_PARTICIPANTS) { + setCustomError(`Must be at least ${MIN_PARTICIPANTS}.`); + return null; + } + if (val > MAX_PARTICIPANTS) { + setCustomError(`Must be at most ${MAX_PARTICIPANTS}.`); + return null; + } + if (!isPowerOfTwo(val)) { + setCustomError("Must be a power of 2 (e.g. 4, 8, 16, 32, 64...)."); + return null; + } + setCustomError(""); + return val; + } + return parseInt(participantOption); + }; + const handleCreate = (e: FormEvent) => { e.preventDefault(); if (!name) { @@ -57,10 +86,22 @@ export default function TournamentPage() { return; } + // Submit-time date validation — cannot be bypassed via manipulated form state + const today = new Date(); + today.setHours(0, 0, 0, 0); + const selectedDate = new Date(`${date}T00:00:00`); + if (!date || Number.isNaN(selectedDate.getTime()) || selectedDate < today) { + setError("Start date cannot be in the past."); + return; + } + + const maxParticipants = getMaxParticipants(); + if (maxParticipants === null) return; + const newTournament: Tournament = { id: Date.now().toString(), name, - maxParticipants: 8, + maxParticipants, currentParticipants: 0, date, description, @@ -70,6 +111,9 @@ export default function TournamentPage() { setName(""); setDate(""); setDescription(""); + setParticipantOption("8"); + setCustomParticipants(""); + setCustomError(""); setError(""); }; @@ -98,7 +142,11 @@ export default function TournamentPage() { const renderAvatars = (current: number, max: number) => { const avatars = []; - for (let i = 0; i < max; i++) { + const MAX_VISIBLE = 6; + const visible = Math.min(max, MAX_VISIBLE); + const overflow = max - MAX_VISIBLE; + + for (let i = 0; i < visible; i++) { const isActive = i < current; avatars.push(
); } + + if (overflow > 0) { + avatars.push( +
+
+ +{overflow} +
+
+ ); + } + return avatars; }; @@ -139,7 +202,7 @@ export default function TournamentPage() { {tournaments.map((t) => (

@@ -225,14 +288,61 @@ export default function TournamentPage() { + {/* min attribute prevents selecting past dates in the date picker */} setDate(e.target.value)} required + min={new Date().toISOString().split("T")[0]} className="mt-1 block w-full border border-input rounded-md p-3 bg-background text-foreground focus:ring-2 focus:ring-primary focus:border-transparent transition" />

+
+ + + {participantOption === "custom" && ( +
+ { + setCustomParticipants(e.target.value); + setCustomError(""); + }} + min={MIN_PARTICIPANTS} + max={MAX_PARTICIPANTS} + step={1} + placeholder="e.g. 4, 8, 16, 32" + className="block w-full border border-input rounded-md p-3 bg-background text-foreground focus:ring-2 focus:ring-primary focus:border-transparent transition" + /> + {customError ? ( +

+ {customError} +

+ ) : ( +

+ Must be a power of 2 between 4 and 64 (e.g. 4, 8, 16, 32, 64) +

+ )} +
+ )} +
); -} +} \ No newline at end of file