From 293f7f5c08980402a240c6913e54ec29855d39fb Mon Sep 17 00:00:00 2001 From: Jiheng Li <136323923+JihengLi@users.noreply.github.com> Date: Mon, 16 Sep 2024 20:03:24 -0500 Subject: [PATCH] Judge dashboard change and debug (#501) * change judging and organizer logic and ui * remove all groups from dropdown in judging page * remove time constraint of judging * debug the judging dashboard, change the get judgingsession method, add have judged property to judging session dto * typo * let organizers to set time for one judge to score one team, changed the corresponding backend and database * fix type error * fix the bug: we should clear all the elements in judging session whenever we update the settings * fix bugs in hacker page --------- Co-authored-by: Isaac Liu <69013063+xnscdev@users.noreply.github.com> --- .../Organizer/ScheduleTab/ScheduleTab.tsx | 49 ++++++------- .../SettingsTab/HackathonSettings.tsx | 33 +++++++-- components/hacker/HackerDash.tsx | 2 +- components/hacker/JudgingSchedule.tsx | 8 +-- components/judges/JudgeDash.tsx | 52 +------------- components/judges/TeamSelect.tsx | 7 -- components/judges/schedule.tsx | 71 +++++++------------ models/hackathon.ts | 2 + pages/api/hackathon-settings.ts | 14 +++- pages/api/judging-sessions.ts | 14 +++- pages/api/leaderboard.ts | 5 +- types/database.ts | 3 + 12 files changed, 119 insertions(+), 141 deletions(-) diff --git a/components/Organizer/ScheduleTab/ScheduleTab.tsx b/components/Organizer/ScheduleTab/ScheduleTab.tsx index e54de269..01bb5ea8 100644 --- a/components/Organizer/ScheduleTab/ScheduleTab.tsx +++ b/components/Organizer/ScheduleTab/ScheduleTab.tsx @@ -13,9 +13,26 @@ const ScheduleTab = () => { const [timesJudged, setTimesJudged] = useState(1); const [maxTimesJudged, setMaxTimesJudged] = useState(0); const [potentialSchedule, setPotentialSchedule] = useState(undefined); + const [hackathonSettings, setHackathonSettings] = useState(undefined); const { baseTheme } = useContext(ThemeContext); + // Get hackathon settings + useEffect(() => { + const fetchHackathonSettings = async () => { + const res = await fetch('/api/hackathon-settings'); + if (res.ok) { + const settings = await res.json(); + setHackathonSettings(settings as HackathonSettingsData); + } + }; + fetchHackathonSettings(); + }, []); + + const TimeForJudgeToScoreOneTeam = parseInt(hackathonSettings?.JUDGING_TIME_PER_TEAM as string, 10); + const JudgingDuration = hackathonSettings?.JUDGING_DURATION as number; + const MaximumNumberOfTeamsOneJudgeCanFinishInJudgingTime = JudgingDuration / TimeForJudgeToScoreOneTeam; + // Get judging sessions const { data: judgingSessions, error: judgingSessionsError } = useCustomSWR({ url: '/api/judging-sessions', @@ -37,13 +54,6 @@ const ScheduleTab = () => { errorMessage: 'Failed to get list of teams.', }); - // Get hackathon settings - const { data: hackathonSettings, error: hackathonError } = useCustomSWR({ - url: '/api/hackathon-settings', - method: RequestType.GET, - errorMessage: 'Failed to get hackathon times.', - }); - // Confirm potential schedule const handleConfirmPotentialSchedules = (potentialSchedule: JudgingSessionData[] | undefined) => { // Exit early if we don't have data yet @@ -67,33 +77,23 @@ const ScheduleTab = () => { let judgingTimes = generateTimes( new Date(hackathonSettings?.JUDGING_START as string), new Date(hackathonSettings?.JUDGING_END as string), - 10 + TimeForJudgeToScoreOneTeam ); setPotentialSchedule(matchTeams(teams, judges, judgingTimes, timesJudged)); }; useEffect(() => { if (!teamsData || !judgesData) return; - setMaxTimesJudged(Math.floor((judgesData?.length * 12) / teamsData?.length)); - }, [teamsData, judgesData]); + setMaxTimesJudged( + Math.floor((judgesData?.length * MaximumNumberOfTeamsOneJudgeCanFinishInJudgingTime) / teamsData?.length) + ); + }, [teamsData, judgesData, hackathonSettings]); useEffect(() => { // Exit early if we don't have data yet if (!judgingSessions) return; - - // Sort judging sessions by time - const time = new Date('2022-10-23T11:00:00').getTime(); - - // Set the data after filtering it by time - setPotentialSchedule( - judgingSessions.filter(judgingSession => { - let time = new Date(judgingSession.time as string); - return ( - new Date(hackathonSettings?.JUDGING_START as string) <= time && - time <= new Date(hackathonSettings?.JUDGING_END as string) - ); - }) - ); + // Set the data + setPotentialSchedule(judgingSessions); }, [judgingSessions, hackathonSettings]); // Combine all the loading, null, and error states @@ -162,6 +162,7 @@ const ScheduleTab = () => { handleChange={function (value: SetStateAction): void { throw new Error('Function not implemented.'); }} + TimeForJudgeToScoreOneTeam={TimeForJudgeToScoreOneTeam} sessionTimeStart={new Date(hackathonSettings?.JUDGING_START as string)} sessionTimeEnd={new Date(hackathonSettings?.JUDGING_END as string)} /> diff --git a/components/Organizer/SettingsTab/HackathonSettings.tsx b/components/Organizer/SettingsTab/HackathonSettings.tsx index 590d146b..91c396ea 100644 --- a/components/Organizer/SettingsTab/HackathonSettings.tsx +++ b/components/Organizer/SettingsTab/HackathonSettings.tsx @@ -1,4 +1,4 @@ -import { Button, DatePicker, Select, Space } from 'antd'; +import { Button, DatePicker, Input, Select, Space } from 'antd'; import dayjs from 'dayjs'; import React, { useContext, useEffect, useState } from 'react'; import { handleSubmitFailure, handleSubmitSuccess } from '../../../lib/helpers'; @@ -89,6 +89,7 @@ const HackathonSettings = () => { setHackathonSetting(settings as HackathonSettingsData); setStatusMessage('Successfully saved to database!'); handleSubmitSuccess('Successfully saved to database!'); + window.location.reload(); } else { setStatusMessage('Failed to save to database!'); handleSubmitFailure('Failed to save to database!'); @@ -114,14 +115,16 @@ const HackathonSettings = () => { }; const handleJudgingChange = (_: any, dateStrings: [string, string]) => { - const newJudgingStart = dayjs(dateStrings[0], { utc: true }).format('MM/DD/YYYY hh:mm A'); - const newJudgingEnd = dayjs(dateStrings[1], { utc: true }).format('MM/DD/YYYY hh:mm A'); + const newJudgingStart = dayjs(dateStrings[0], { utc: true }); + const newJudgingEnd = dayjs(dateStrings[1], { utc: true }); + const judgingDuration = newJudgingEnd.diff(newJudgingStart, 'minute'); setHackathonSetting(prev => { if (prev) return { ...prev, - JUDGING_START: newJudgingStart, - JUDGING_END: newJudgingEnd, + JUDGING_START: newJudgingStart.format('MM/DD/YYYY hh:mm A'), + JUDGING_END: newJudgingEnd.format('MM/DD/YYYY hh:mm A'), + JUDGING_DURATION: judgingDuration, }; return undefined; }); @@ -161,6 +164,26 @@ const HackathonSettings = () => { defaultValue={[dayjs(hackathonSetting?.JUDGING_START), dayjs(hackathonSetting?.JUDGING_END)]} /> +
+ +
Time for Judge to Score One Team (minutes):
+ { + setHackathonSetting(prev => { + if (prev) { + return { + ...prev, + JUDGING_TIME_PER_TEAM: e.target.value, + }; + } + return prev; + }); + }} + placeholder="Enter time in minutes" + style={{ width: 138 }} + /> +

diff --git a/components/hacker/HackerDash.tsx b/components/hacker/HackerDash.tsx index 4471c65d..b7482b8a 100644 --- a/components/hacker/HackerDash.tsx +++ b/components/hacker/HackerDash.tsx @@ -769,7 +769,7 @@ export default function HackerDash({ userApplicationStatus, setUserApplicationSt
- + {/* */} diff --git a/components/hacker/JudgingSchedule.tsx b/components/hacker/JudgingSchedule.tsx index 7b660e74..b0d0887c 100644 --- a/components/hacker/JudgingSchedule.tsx +++ b/components/hacker/JudgingSchedule.tsx @@ -34,9 +34,7 @@ const JudgingSchedule = ({ judgingSessionData }: JudgingScheduleProps) => { return (
Judging Schedule -
- You will be assigned a table and judge for judging. Please be at your table at the time indicated below. -
+
You will be assigned a table and judge for judging.
{judgingSessionData?.length === 0 ? (
Schedule will show up here when hacking ends!
) : ( @@ -44,7 +42,7 @@ const JudgingSchedule = ({ judgingSessionData }: JudgingScheduleProps) => { - + {/* */} @@ -52,7 +50,7 @@ const JudgingSchedule = ({ judgingSessionData }: JudgingScheduleProps) => { {judgingSessionData?.map(entry => ( - + {/* */} diff --git a/components/judges/JudgeDash.tsx b/components/judges/JudgeDash.tsx index 423df3fb..ac8009ff 100644 --- a/components/judges/JudgeDash.tsx +++ b/components/judges/JudgeDash.tsx @@ -45,6 +45,7 @@ async function handleSubmit( mutate('/api/teams'); mutate('/api/judging-form'); handleSubmitSuccess(`Successfully ${isNewForm ? 'submitted' : 'updated'}!`); + if (isNewForm) window.location.reload(); setIsNewForm(false); } else { handleSubmitFailure(await res.text()); @@ -54,8 +55,6 @@ async function handleSubmit( export default function JudgeDash() { const { data: session, status } = useSession(); const [teamID, setTeamID] = useState(''); - const [currentScheduleItem, setCurrentScheduleItem] = useState(undefined); - const [nextScheduleItem, setNextScheduleItem] = useState(undefined); const [isNewForm, setIsNewForm] = useState(false); const [nextIndex, setNextIndex] = useState(-1); const { mutate } = useSWRConfig(); @@ -132,51 +131,6 @@ export default function JudgeDash() { return (await res.json()) as JudgingSessionData[]; }); - // Initialize state if data was just received - useEffect(() => { - if (nextIndex === -1 && scheduleData) { - const now = Date.now(); - let index = scheduleData.findIndex(el => now < new Date(el.time as string).getTime()); - if (index === -1) index = scheduleData.length; - let currentlyGoingTime = new Date(scheduleData[index - 1]?.time as string).getTime() + judgingLength; - setNextScheduleItem(scheduleData[index]); - setCurrentScheduleItem(now < currentlyGoingTime ? scheduleData[index - 1] : undefined); - setNextIndex(index); - } - }, [scheduleData, nextIndex, judgingLength]); - - // Loop to manage current schedule state - useEffect(() => { - const interval = setInterval(() => { - const now = Date.now(); - if (scheduleData && nextIndex > -1) { - // Data has been received and state is initialized - let time2 = new Date(scheduleData[scheduleData.length - 1]?.time as string).getTime() + judgingLength; - if (now <= time2) { - // Not yet done judging - let time3 = new Date((currentScheduleItem?.time as string) || 0).getTime() + judgingLength; - - if ( - nextIndex < scheduleData.length && - now >= new Date((nextScheduleItem?.time as string) || 0).getTime() - ) { - // Next event should be current - setNextScheduleItem(scheduleData[nextIndex + 1]); - setCurrentScheduleItem(scheduleData[nextIndex]); - setNextIndex(nextIndex + 1); - } else if (now > time3) { - // Next event has not yet arrived but current event is over - setCurrentScheduleItem(undefined); - } - } else { - // Done judging - setCurrentScheduleItem(undefined); - } - } - }, 1000); - return () => clearInterval(interval); - }); - const handleTeamChange: Dispatch> = e => { setTeamID(e); setTimeout(() => { @@ -204,9 +158,7 @@ export default function JudgeDash() { - {scheduleData && ( - - )} + {scheduleData && }

{teamsData && } diff --git a/components/judges/TeamSelect.tsx b/components/judges/TeamSelect.tsx index 4dca5d2a..cc662f35 100644 --- a/components/judges/TeamSelect.tsx +++ b/components/judges/TeamSelect.tsx @@ -63,13 +63,6 @@ export default function TeamSelect(props: TeamSelectProps) { ))} )} - - {teamsData.map(team => ( - - ))} - ); diff --git a/components/judges/schedule.tsx b/components/judges/schedule.tsx index a78b5e0f..66313bfc 100644 --- a/components/judges/schedule.tsx +++ b/components/judges/schedule.tsx @@ -8,7 +8,7 @@ import { ThemeContext, getAccentColor } from '../../theme/themeProvider'; interface ScheduleProps { data: JudgingSessionData[]; - cutoffIndex?: number; + TimeForJudgeToScoreOneTeam?: number; handleChange: (teamId: string) => void; sessionTimeStart?: Date; sessionTimeEnd?: Date; @@ -67,7 +67,7 @@ export function generateTimes(start: Date, end: Date, interval: number) { } export default function OrganizerSchedule(props: ScheduleProps) { - let { data, sessionTimeStart, sessionTimeEnd } = props; + let { data, TimeForJudgeToScoreOneTeam, sessionTimeStart, sessionTimeEnd } = props; const teams = useMemo( () => [...new Set(data.filter(x => x.team !== null && x.team.name !== null).map(x => x.team.name))], @@ -103,7 +103,7 @@ export default function OrganizerSchedule(props: ScheduleProps) { sessionTimeStart = sessionTimeStart || new Date(); sessionTimeEnd = sessionTimeEnd || new Date(); - const sessionTimes = generateTimes(sessionTimeStart, sessionTimeEnd, 10); + const sessionTimes = generateTimes(sessionTimeStart, sessionTimeEnd, TimeForJudgeToScoreOneTeam as number); // Reorganize data to be fed into table const tableData = useMemo(() => { @@ -182,18 +182,11 @@ export default function OrganizerSchedule(props: ScheduleProps) { ); } -export function JudgeSchedule({ data, cutoffIndex, handleChange }: ScheduleProps) { - const [showPast, setShowPast] = useState(false); +export function JudgeSchedule({ data, handleChange }: ScheduleProps) { + const [isJudged, setIsJudged] = useState(false); const { accentColor, baseTheme } = useContext(ThemeContext); const columns = [ - { - title: 'Time', - dataIndex: 'time', - key: 'time', - width: '10%', - render: (date: string) => DateTime.fromISO(date).toLocaleString(DateTime.TIME_SIMPLE), - }, { title: 'Table', dataIndex: 'table', @@ -205,7 +198,7 @@ export function JudgeSchedule({ data, cutoffIndex, handleChange }: ScheduleProps title: 'Project', dataIndex: 'project', key: 'project', - width: '25%', + width: '40%', render: ({ name, link }: { name: string; link: URL }) => ( <>
@@ -223,46 +216,27 @@ export function JudgeSchedule({ data, cutoffIndex, handleChange }: ScheduleProps title: 'Team Members', dataIndex: 'teamMembers', key: 'teamMembers', - width: '25%', + width: '40%', render: (members: User[]) => members.map(member => {member.name}), }, - { - title: 'Judge', - dataIndex: 'judge', - key: 'judge', - width: '10%', - render: (judge: User) => {judge.name}, - }, - { - title: 'Action', - dataIndex: 'teamId', - key: 'teamId', - width: '10%', - render: (teamId: any) => ( - - ), - }, { title: 'Judgement State', - dataIndex: 'scores', - key: 'scores', + dataIndex: 'haveJudged', + key: 'haveJudged', width: '10%', - render: (scores: []) => {scores.length ? 'Judged' : 'Without Judgement'}, + render: (haveJudged: []) => {haveJudged ? 'Judged' : 'Without Judgement'}, }, ]; - const dataSource = data.slice(showPast ? 0 : cutoffIndex).map(item => { - return { - time: item.time, + + const dataSource = data + .filter(item => (isJudged ? item.haveJudged : !item.haveJudged)) + .map(item => ({ table: item.team.locationNum, project: { name: item.team.name, link: new URL(item.team.devpost) }, teamMembers: item.team.members, - judge: item.judge, teamId: item.team._id, - scores: item.team.scores, - }; - }); + haveJudged: item.haveJudged, + })); const handleRowClick = (record: any) => { handleChange(record.teamId); @@ -273,7 +247,13 @@ export function JudgeSchedule({ data, cutoffIndex, handleChange }: ScheduleProps locale={{ emptyText: (
-

Stay tuned! You will see your teams that you will judge soon!

+

+ {data.length == 0 + ? 'Stay tuned! You will see your teams that you will judge soon!' + : isJudged + ? "You haven't started judging yet." + : "Hurraaaaarrgh! You're off duty!"} +

), }} @@ -286,8 +266,9 @@ export function JudgeSchedule({ data, cutoffIndex, handleChange }: ScheduleProps - setShowPast(!showPast)} /> - Show Past Sessions + diff --git a/models/hackathon.ts b/models/hackathon.ts index 7c6685fc..5110a0f9 100644 --- a/models/hackathon.ts +++ b/models/hackathon.ts @@ -5,6 +5,8 @@ const HackathonSchema = new Schema({ HACKATHON_END: { type: String, required: true }, // MM/DD/YYYY hh:mm A JUDGING_START: { type: String, required: true }, // MM/DD/YYYY hh:mm A JUDGING_END: { type: String, required: true }, // MM/DD/YYYY hh:mm A + JUDGING_DURATION: { type: Number, required: true }, + JUDGING_TIME_PER_TEAM: { type: Number, required: true }, ON_CALL_DEV: { type: String, required: true }, }); diff --git a/pages/api/hackathon-settings.ts b/pages/api/hackathon-settings.ts index ce82884d..0379bc1a 100644 --- a/pages/api/hackathon-settings.ts +++ b/pages/api/hackathon-settings.ts @@ -2,6 +2,7 @@ import type { NextApiRequest, NextApiResponse } from 'next'; import dbConnect from '../../middleware/database'; import { getSession } from 'next-auth/react'; import hackathon from '../../models/hackathon'; +import JudgingSession from '../../models/JudgingSession'; export default async function handler(req: NextApiRequest, res: NextApiResponse) { const session = await getSession({ req }); @@ -21,7 +22,15 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse< try { if (session?.userType !== 'ORGANIZER') return res.status(403).send('Forbidden'); // extract variables from body - const { HACKATHON_START, HACKATHON_END, JUDGING_START, JUDGING_END, ON_CALL_DEV } = req.body; + const { + HACKATHON_START, + HACKATHON_END, + JUDGING_START, + JUDGING_END, + JUDGING_DURATION, + JUDGING_TIME_PER_TEAM, + ON_CALL_DEV, + } = req.body; // check if hackathon settings valid (must be correct format of date) const isValid = @@ -42,6 +51,8 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse< HACKATHON_END, JUDGING_START, JUDGING_END, + JUDGING_DURATION, + JUDGING_TIME_PER_TEAM: parseInt(JUDGING_TIME_PER_TEAM, 10), ON_CALL_DEV, }, }, @@ -50,6 +61,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse< } ); + await JudgingSession.remove(); // return the updated hackathon settings return res.status(200).json(updatedHackathonSettings); } catch (err) { diff --git a/pages/api/judging-sessions.ts b/pages/api/judging-sessions.ts index 4b445b94..6f4f3463 100644 --- a/pages/api/judging-sessions.ts +++ b/pages/api/judging-sessions.ts @@ -3,6 +3,9 @@ import { getSession } from 'next-auth/react'; import JudgingSession from '../../models/JudgingSession'; import Team from '../../models/team'; import User from '../../models/user'; +import Scores from '../../models/scores'; +import { ObjectId } from 'mongodb'; +import { ScoreData } from '../../types/database'; /** * gets a judging schedule for a team @@ -37,13 +40,20 @@ async function getOrganizerSchedule(res: NextApiResponse) { * @param userID ID of the judge * @returns response containing judging schedule for a judge */ -async function getJudgeSchedule(res: NextApiResponse, userID: string) { +async function getJudgeSchedule(res: NextApiResponse, userID: ObjectId) { Team; User; // Don't remove or the import will get optimized out and the populate will fail const data = await JudgingSession.find({ judge: userID }) .populate('team judge') .populate({ path: 'team', populate: { path: 'members' } }) .lean(); + + // whether the team in the session has judged by the specific judge + const teamsJudged = await Scores.find({ judge: userID }).select('team'); + const teamsJudgedIDs = teamsJudged.map(teamItem => teamItem.team.toString()); + data.forEach(judgingSession => { + judgingSession.haveJudged = teamsJudgedIDs.includes(judgingSession.team._id.toString()); + }); return res.status(200).send(data); } @@ -62,7 +72,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse< case 'ORGANIZER': return getOrganizerSchedule(res); case 'JUDGE': - return getJudgeSchedule(res, session?.userID as string); + return getJudgeSchedule(res, session?.userID as ObjectId); default: return res.status(403).send('Forbidden'); } diff --git a/pages/api/leaderboard.ts b/pages/api/leaderboard.ts index 0b3295e3..e640403f 100644 --- a/pages/api/leaderboard.ts +++ b/pages/api/leaderboard.ts @@ -12,7 +12,10 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse< await dbConnect(); switch (req.method) { case 'GET': - const users = await User.find({ nfcPoints: { $exists: true } }) + const users = await User.find({ + userType: 'HACKER', + nfcPoints: { $exists: true }, + }) .sort({ nfcPoints: -1 }) .limit(10) .populate('team') diff --git a/types/database.ts b/types/database.ts index f2ef8e0f..88235588 100644 --- a/types/database.ts +++ b/types/database.ts @@ -123,6 +123,7 @@ export interface JudgingSessionData { _id?: mongoose.Schema.Types.ObjectId; team: TeamData; judge: UserData; + haveJudged?: Boolean; time: String; } @@ -132,5 +133,7 @@ export interface HackathonSettingsData { HACKATHON_END: string; // MM/DD/YYYY hh:mm A JUDGING_START: string; // MM/DD/YYYY hh:mm A JUDGING_END: string; // MM/DD/YYYY hh:mm A + JUDGING_DURATION: number; + JUDGING_TIME_PER_TEAM: string; ON_CALL_DEV: string; }
TimeTimeTable Judge
{renderJudgingTime(entry.time.toString())}{renderJudgingTime(entry.time.toString())}{entry.team.locationNum} {entry.judge.name}
{name}