Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions src/components/contests/ContestListingClient.tsx
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -222,12 +222,14 @@ export default function ContestListingClient({
}
};

const handleCloseCreateModal = useCallback(() => setShowCreateModal(false), []);

return (
<div className={styles.page}>
{showCreateModal && (
<CreateRoomModal
isOpen={true}
onClose={() => setShowCreateModal(false)}
onClose={handleCloseCreateModal}
isAdmin={isAdmin}
presets={presets}
deadlineMinutes={deadlineMinutes}
Expand Down
149 changes: 86 additions & 63 deletions src/components/contests/CreateRoomModal.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"use client";

import { useState, useEffect } from "react";
import { useState, useEffect, memo } from "react";
import {
GripVertical,
Lock,
Expand All @@ -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,
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -347,6 +385,7 @@ export default function CreateRoomModal({
try {
const res = await createBracketContest({
...formData,
presetId: finalPresetId,
deadline: start.toISOString(),
registrationStartTime: regStartIso,
registeredUsers: finalRegisteredUsers,
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -1047,42 +1066,43 @@ export default function CreateRoomModal({
)}
</div>

<div
className={`${styles.field} ${
topPresetId ? styles.locked : ""
}`}
>
<label className={styles.label} htmlFor="preset-id">
Match Preset
</label>
<select
id="preset-id"
value={formData.presetId}
onChange={(e) =>
setFormData({ ...formData, presetId: e.target.value })
}
disabled={!!topPresetId}
className={`${styles.formInput} ${styles.formSelect}`}
{isAdmin && (
<div
className={`${styles.field} ${
topPresetId ? styles.locked : ""
}`}
>
<option value="" disabled>
Select a preset...
</option>
<option value="custom">
Custom (Manual Configuration)
</option>
{presets.map((p) => (
<option key={p._id} value={p._id}>
{p.name}
<label className={styles.label} htmlFor="preset-id">
Match Preset
</label>
<select
id="preset-id"
value={formData.presetId}
onChange={(e) =>
setFormData({ ...formData, presetId: e.target.value })
}
disabled={!!topPresetId}
className={`${styles.formInput} ${styles.formSelect}`}
>
<option value="" disabled>
Select a preset...
</option>
))}
</select>
<span className={styles.hint}>
Bracket tournaments use presets to define the problem
criteria for all rounds.
</span>
{(() => {
<option value="custom">
Custom (Manual Configuration)
</option>
{presets.map((p) => (
<option key={p._id} value={p._id}>
{p.name}
</option>
))}
</select>
<span className={styles.hint}>
Bracket tournaments use presets to define the problem
criteria for all rounds.
</span>
{(() => {
const selectedMatchPreset = presets.find(
(p) => p._id === formData.presetId,
(p) => p._id === effectivePresetId,
);
return selectedMatchPreset ? (
<div className={styles.presetInfo}>
Expand Down Expand Up @@ -1111,9 +1131,10 @@ export default function CreateRoomModal({
</div>
) : null;
})()}
</div>
</div>
)}

{formData.presetId === "custom" && renderProblemConfiguration()}
{effectivePresetId === "custom" && renderProblemConfiguration()}

<div className={styles.grid2}>
<div className={styles.field}>
Expand Down Expand Up @@ -1232,10 +1253,10 @@ export default function CreateRoomModal({
<div className={styles.timeChipRow}>
<button
type="button"
onClick={() => handleRegTimeAdd(3)}
onClick={() => handleRegTimeAdd(4)}
className={styles.timeChip}
>
+3m
+4m
</button>
<button
type="button"
Expand Down Expand Up @@ -1810,7 +1831,7 @@ export default function CreateRoomModal({
className={`${styles.formInput} ${styles.dateInput}`}
/>
<div className={styles.timeAddRow}>
{[3, 5, 10, 15].map((mins) => (
{[4, 5, 10, 15].map((mins) => (
<button
key={mins}
type="button"
Expand Down Expand Up @@ -1851,4 +1872,6 @@ export default function CreateRoomModal({
</div>
</div>
);
}
});

export default CreateRoomModal;
27 changes: 23 additions & 4 deletions src/lib/actions/contests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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(),
Expand Down Expand Up @@ -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<string, string> = {};
users.forEach((u: any) => {
if (u.image) imageMap[u._id.toString()] = u.image;
});

const cpUserMap: Record<string, string> = {};
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,
}),
);

Expand Down Expand Up @@ -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(),
Expand All @@ -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(),
Expand Down
4 changes: 2 additions & 2 deletions src/lib/bracket.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, number>();
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) => {
Expand Down
10 changes: 5 additions & 5 deletions src/lib/workers/reconciliationWorker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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++;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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<string>(
bracketRefreshedUsers.flatMap((u) =>
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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<string>(
refreshedUsers.flatMap((u) =>
(u.solvedProblems || []).map((sp: any) => sp.problemId),
Expand Down