diff --git a/src/components/contests/ContestListingClient.tsx b/src/components/contests/ContestListingClient.tsx index e8706ae..54de87b 100644 --- a/src/components/contests/ContestListingClient.tsx +++ b/src/components/contests/ContestListingClient.tsx @@ -1,6 +1,6 @@ "use client"; -import { useState, useEffect } from "react"; +import { useState, useEffect, useCallback } from "react"; import { useRouter } from "next/navigation"; import { type ContestListingItem } from "@/lib/actions/contests"; import Link from "next/link"; @@ -222,12 +222,14 @@ export default function ContestListingClient({ } }; + const handleCloseCreateModal = useCallback(() => setShowCreateModal(false), []); + return (
{showCreateModal && ( setShowCreateModal(false)} + onClose={handleCloseCreateModal} isAdmin={isAdmin} presets={presets} deadlineMinutes={deadlineMinutes} diff --git a/src/components/contests/CreateRoomModal.tsx b/src/components/contests/CreateRoomModal.tsx index b39de8e..0354516 100644 --- a/src/components/contests/CreateRoomModal.tsx +++ b/src/components/contests/CreateRoomModal.tsx @@ -1,6 +1,6 @@ "use client"; -import { useState, useEffect } from "react"; +import { useState, useEffect, memo } from "react"; import { GripVertical, Lock, @@ -17,7 +17,7 @@ import { getDisplayName } from "@/lib/utils"; import { CF_CONTEST_YEAR_OPTIONS } from "@/lib/constants"; import styles from "./CreateRoomModal.module.scss"; -export default function CreateRoomModal({ +const CreateRoomModal = memo(function CreateRoomModal({ isOpen, onClose, isAdmin = false, @@ -224,8 +224,44 @@ export default function CreateRoomModal({ registeredUsers.length, ]); + useEffect(() => { + if ( + formData.problemSelectionMode === "fine-tuned" && + formData.format === "bracket" + ) { + const maxP = Math.max(2, formData.maxParticipants || 2); + const totalRounds = Math.ceil(Math.log2(maxP)); + const ppm = formData.bulkProblemCount || 3; + + const syncedRounds: { roundNumber: number; problemIds: string[] }[] = []; + for (let r = 1; r <= totalRounds; r++) { + const matchCount = Math.pow(2, totalRounds - r); + const needed = matchCount * ppm; + const existing = bracketRoundProblems.find((x) => x.roundNumber === r); + const ids = existing ? [...existing.problemIds] : []; + while (ids.length < needed) ids.push(""); + while (ids.length > needed) ids.pop(); + syncedRounds.push({ roundNumber: r, problemIds: ids }); + } + + if ( + JSON.stringify(syncedRounds) !== JSON.stringify(bracketRoundProblems) + ) { + setBracketRoundProblems(syncedRounds); + } + } + }, [ + formData.problemSelectionMode, + formData.format, + formData.maxParticipants, + formData.bulkProblemCount, + bracketRoundProblems, + ]); + if (!isOpen) return null; + const effectivePresetId = isAdmin ? formData.presetId : "custom"; + const isTeamSizeLocked = [ "1v1", "solo-tournament", @@ -313,8 +349,10 @@ export default function CreateRoomModal({ } } + const finalPresetId = isAdmin ? formData.presetId : "custom"; + if (formData.format === "bracket") { - if (!formData.presetId) { + if (!finalPresetId) { alert("Please select a match preset for the bracket."); return; } @@ -347,6 +385,7 @@ export default function CreateRoomModal({ try { const res = await createBracketContest({ ...formData, + presetId: finalPresetId, deadline: start.toISOString(), registrationStartTime: regStartIso, registeredUsers: finalRegisteredUsers, @@ -777,27 +816,7 @@ export default function CreateRoomModal({ const totalRounds = Math.ceil(Math.log2(maxP)); const ppm = formData.bulkProblemCount || 3; // problems per match - // Sync bracketRoundProblems structure with computed rounds - const syncedRounds: { roundNumber: number; problemIds: string[] }[] = - []; - for (let r = 1; r <= totalRounds; r++) { - const matchCount = Math.pow(2, totalRounds - r); - const needed = matchCount * ppm; - const existing = bracketRoundProblems.find( - (x) => x.roundNumber === r, - ); - const ids = existing ? [...existing.problemIds] : []; - while (ids.length < needed) ids.push(""); - while (ids.length > needed) ids.pop(); - syncedRounds.push({ roundNumber: r, problemIds: ids }); - } - if ( - JSON.stringify(syncedRounds) !== - JSON.stringify(bracketRoundProblems) - ) { - // Use setTimeout to avoid updating state during render - setTimeout(() => setBracketRoundProblems(syncedRounds), 0); - } + const syncedRounds = bracketRoundProblems; const getRoundLabel = (r: number) => { const matchCount = Math.pow(2, totalRounds - r); @@ -1047,42 +1066,43 @@ export default function CreateRoomModal({ )}
-
- - + setFormData({ ...formData, presetId: e.target.value }) + } + disabled={!!topPresetId} + className={`${styles.formInput} ${styles.formSelect}`} + > + - ))} - - - Bracket tournaments use presets to define the problem - criteria for all rounds. - - {(() => { + + {presets.map((p) => ( + + ))} + + + Bracket tournaments use presets to define the problem + criteria for all rounds. + + {(() => { const selectedMatchPreset = presets.find( - (p) => p._id === formData.presetId, + (p) => p._id === effectivePresetId, ); return selectedMatchPreset ? (
@@ -1111,9 +1131,10 @@ export default function CreateRoomModal({
) : null; })()} -
+ + )} - {formData.presetId === "custom" && renderProblemConfiguration()} + {effectivePresetId === "custom" && renderProblemConfiguration()}
@@ -1232,10 +1253,10 @@ export default function CreateRoomModal({
); -} +}); + +export default CreateRoomModal; diff --git a/src/lib/actions/contests.ts b/src/lib/actions/contests.ts index cfe7ae4..1e6a379 100644 --- a/src/lib/actions/contests.ts +++ b/src/lib/actions/contests.ts @@ -397,6 +397,11 @@ export async function createRoomContest( })); } + const userIds = (data.registeredUsers || []).map((u: any) => new mongoose.Types.ObjectId(u.id)); + const registeredCPUsers = await CPUser.find({ userId: { $in: userIds } }).lean(); + const cpUserIdMap = new Map(); + registeredCPUsers.forEach((cp: any) => cpUserIdMap.set(cp.userId.toString(), cp._id)); + const contest = new ContestMatch({ name: data.name, description: data.description, @@ -421,7 +426,7 @@ export async function createRoomContest( maxParticipants: maxParticipants, }, registrations: (data.registeredUsers || []).map((u: any) => ({ - userId: new mongoose.Types.ObjectId(u.id), + userId: cpUserIdMap.get(u.id) || new mongoose.Types.ObjectId(u.id), cfHandle: u.cfHandle, teamName: u.teamName, registeredAt: new Date(), @@ -491,16 +496,25 @@ export async function getContestRegistrations(contestId: string) { const User = (await import("@/models/User")).default; const userIds = (contest.registrations || []).map((r: any) => r.userId); - const users = await User.find({ _id: { $in: userIds } }, "image").lean(); + const cpUsers = await CPUser.find({ _id: { $in: userIds } }).lean(); + const actualUserIds = cpUsers.map((cp: any) => cp.userId); + const users = await User.find({ _id: { $in: actualUserIds } }, "image").lean(); const imageMap: Record = {}; users.forEach((u: any) => { if (u.image) imageMap[u._id.toString()] = u.image; }); + const cpUserMap: Record = {}; + cpUsers.forEach((cp: any) => { + if (imageMap[cp.userId.toString()]) { + cpUserMap[cp._id.toString()] = imageMap[cp.userId.toString()]; + } + }); + const populatedRegistrations = (contest.registrations || []).map( (r: any) => ({ ...r, - image: imageMap[r.userId.toString()] || null, + image: cpUserMap[r.userId.toString()] || null, }), ); @@ -749,6 +763,11 @@ export async function createBracketContest(data: any) { if (isNaN(deadlineMinutes)) throw new Error("REGISTRATION_DEADLINE_MINUTES must be a valid number."); + const userIdsBracket = (data.registeredUsers || []).map((u: any) => new mongoose.Types.ObjectId(u.id)); + const registeredCPUsersBracket = await CPUser.find({ userId: { $in: userIdsBracket } }).lean(); + const cpUserIdMapBracket = new Map(); + registeredCPUsersBracket.forEach((cp: any) => cpUserIdMapBracket.set(cp.userId.toString(), cp._id)); + const contest = await ContestMatch.create({ name: data.name.trim(), description: data.description?.trim(), @@ -766,7 +785,7 @@ export async function createBracketContest(data: any) { bulkMinContestId: bulkMinContestId || undefined, problemSlots: problemSlots, registrations: (data.registeredUsers || []).map((u: any) => ({ - userId: new mongoose.Types.ObjectId(u.id), + userId: cpUserIdMapBracket.get(u.id) || new mongoose.Types.ObjectId(u.id), cfHandle: u.cfHandle, teamName: u.teamName, registeredAt: new Date(), diff --git a/src/lib/bracket.ts b/src/lib/bracket.ts index 71fac71..9f3c9bb 100644 --- a/src/lib/bracket.ts +++ b/src/lib/bracket.ts @@ -93,11 +93,11 @@ export async function generateBracket( ); const cpUsers = await CPUser.find({ - userId: { $in: groupedTeams.flatMap((t) => t.memberIds) }, + _id: { $in: groupedTeams.flatMap((t) => t.memberIds) }, }).lean(); const ratingMap = new Map(); for (const u of cpUsers) { - ratingMap.set(toStr(u.userId), u.cfRating || 0); + ratingMap.set(toStr(u._id), u.cfRating || 0); } const seededTeams = groupedTeams.map((team) => { diff --git a/src/lib/workers/reconciliationWorker.ts b/src/lib/workers/reconciliationWorker.ts index 76f23ef..9b2756a 100644 --- a/src/lib/workers/reconciliationWorker.ts +++ b/src/lib/workers/reconciliationWorker.ts @@ -54,7 +54,7 @@ async function determineWinner( let totalRating = 0; let validMembers = 0; for (const mId of members) { - const cpUser = await CPUser.findOne({ userId: mId }); + const cpUser = await CPUser.findById(mId); if (cpUser && cpUser.cfRating) { totalRating += cpUser.cfRating; validMembers++; @@ -230,7 +230,7 @@ export const reconciliationWorker = new Worker( const { fetchCodeforcesUserStatus } = await import("../cf-api"); for (const uid of bracketUserIds) { - const cpUser = await CPUser.findOne({ userId: uid }); + const cpUser = await CPUser.findById(uid); if (!cpUser || !cpUser.cfHandle) continue; const solvedProblems: any[] = cpUser.solvedProblems || []; let latestSolvedMs = 0; @@ -311,7 +311,7 @@ export const reconciliationWorker = new Worker( // Build solved union from refreshed CPUser docs const bracketRefreshedUsers = await CPUser.find({ - userId: { $in: bracketUserIds }, + _id: { $in: bracketUserIds }, }); const bracketSolvedIds = new Set( bracketRefreshedUsers.flatMap((u) => @@ -452,7 +452,7 @@ export const reconciliationWorker = new Worker( const { fetchCodeforcesUserStatus } = await import("../cf-api"); for (const uid of allUserIds) { - const cpUser = await CPUser.findOne({ userId: uid }); + const cpUser = await CPUser.findById(uid); if (!cpUser || !cpUser.cfHandle) continue; // Find the timestamp of the most recently recorded solve @@ -509,7 +509,7 @@ export const reconciliationWorker = new Worker( } // Build union of all solved problem IDs from the (now refreshed) CPUser docs - const refreshedUsers = await CPUser.find({ userId: { $in: allUserIds } }); + const refreshedUsers = await CPUser.find({ _id: { $in: allUserIds } }); const solvedProblemIds = new Set( refreshedUsers.flatMap((u) => (u.solvedProblems || []).map((sp: any) => sp.problemId),