From 79d6eb35d25c74d6b3fef0a776a4b0312be70d32 Mon Sep 17 00:00:00 2001 From: Alan Bi Date: Thu, 24 Jun 2021 21:24:35 -0700 Subject: [PATCH 01/19] Extract out ProblemNavButton for reusability --- frontend/src/components/core/Button.tsx | 25 +++++++++++++++++ frontend/src/components/game/ProblemPanel.tsx | 27 +------------------ 2 files changed, 26 insertions(+), 26 deletions(-) diff --git a/frontend/src/components/core/Button.tsx b/frontend/src/components/core/Button.tsx index c40d18c4..d91f4eac 100644 --- a/frontend/src/components/core/Button.tsx +++ b/frontend/src/components/core/Button.tsx @@ -220,3 +220,28 @@ export const InvertedSmallButton = styled(SmallButton)` color: ${({ theme }) => theme.colors.text}; background: ${({ theme }) => theme.colors.white}; `; + +type ProblemNavButtonProps = { + disabled: boolean, +}; + +export const ProblemNavButton = styled(DefaultButton)` + font-size: ${({ theme }) => theme.fontSize.default}; + color: ${({ theme }) => theme.colors.gray}; + background-color: ${({ theme, disabled }) => (disabled ? theme.colors.background : theme.colors.white)}; + border-radius: 5px; + width: 35px; + height: 35px; + margin: 5px; + + box-shadow: 0 1px 6px rgba(0, 0, 0, 0.16); + + &:hover { + box-shadow: ${({ disabled }) => (disabled ? '0 1px 6px rgba(0, 0, 0, 0.16)' : '0 1px 6px rgba(0, 0, 0, 0.20)')}; + cursor: ${({ disabled }) => (disabled ? 'default' : 'pointer')}; + } + + i { + line-height: 35px; + } +`; diff --git a/frontend/src/components/game/ProblemPanel.tsx b/frontend/src/components/game/ProblemPanel.tsx index c148059b..38926653 100644 --- a/frontend/src/components/game/ProblemPanel.tsx +++ b/frontend/src/components/game/ProblemPanel.tsx @@ -2,7 +2,7 @@ import React from 'react'; import styled from 'styled-components'; import MarkdownEditor from 'rich-markdown-editor'; import { BottomFooterText, ProblemHeaderText, SmallText } from '../core/Text'; -import { DefaultButton, getDifficultyDisplayButton } from '../core/Button'; +import { getDifficultyDisplayButton, ProblemNavButton } from '../core/Button'; import { Copyable } from '../special/CopyIndicator'; import { CenteredContainer, @@ -40,31 +40,6 @@ const ProblemCountText = styled(SmallText)` color: gray; `; -type ProblemNavButtonProps = { - disabled: boolean, -}; - -const ProblemNavButton = styled(DefaultButton)` - font-size: ${({ theme }) => theme.fontSize.default}; - color: ${({ theme }) => theme.colors.gray}; - background-color: ${({ theme, disabled }) => (disabled ? theme.colors.background : theme.colors.white)}; - border-radius: 5px; - width: 35px; - height: 35px; - margin: 5px; - - box-shadow: 0 1px 6px rgba(0, 0, 0, 0.16); - - &:hover { - box-shadow: ${({ disabled }) => (disabled ? '0 1px 6px rgba(0, 0, 0, 0.16)' : '0 1px 6px rgba(0, 0, 0, 0.20)')}; - cursor: ${({ disabled }) => (disabled ? 'default' : 'pointer')}; - } - - i { - line-height: 35px; - } -`; - type ProblemPanelProps = { problems: Problem[], index: number, From f0393991548d4818f0a844f0b02ddddef4842559 Mon Sep 17 00:00:00 2001 From: Alan Bi Date: Thu, 24 Jun 2021 21:36:50 -0700 Subject: [PATCH 02/19] Add problem nav buttons to results page --- .../src/components/results/ResultsTable.tsx | 84 +++++++++++++------ 1 file changed, 60 insertions(+), 24 deletions(-) diff --git a/frontend/src/components/results/ResultsTable.tsx b/frontend/src/components/results/ResultsTable.tsx index 71413150..d9fda1f7 100644 --- a/frontend/src/components/results/ResultsTable.tsx +++ b/frontend/src/components/results/ResultsTable.tsx @@ -1,13 +1,22 @@ /* eslint-disable jsx-a11y/control-has-associated-label */ -import React from 'react'; +import React, { useState } from 'react'; import styled from 'styled-components'; import { Player } from '../../api/Game'; import PlayerResultsItem from './PlayerResultsItem'; import { User } from '../../api/User'; +import { NextIcon, PrevIcon } from '../core/Icon'; +import { ProblemNavButton } from '../core/Button'; +import { Problem } from '../../api/Problem'; -const Content = styled.table` - text-align: center; +const Content = styled.div` width: 65%; + text-align: left; + margin: 0 auto; +`; + +const TableContent = styled.table` + text-align: center; + width: 100%; min-width: 600px; margin: 0 auto; @@ -47,29 +56,56 @@ function ResultsTable(props: ResultsTableProps) { players, currentUser, gameStartTime, numProblems, viewPlayerCode, spectatePlayer, } = props; + const [problemIndex, setProblemIndex] = useState(0); + const problems: string[] = ['t', 'af', 'af']; // TODO: temp - replace with prop + + const nextProblem = () => { + const next = problemIndex + 1; + + if (problems && next < problems.length) { + setProblemIndex(next); + } + }; + + const previousProblem = () => { + const prev = problemIndex - 1; + + if (prev >= 0) { + setProblemIndex(prev); + } + }; + return ( - - - Player - Score - Time - Submissions - {!spectatePlayer ? Code : null} - {spectatePlayer ? Spectate Live : null} - - {players?.map((player, index) => ( - viewPlayerCode(index)) : null} - onSpectateLive={spectatePlayer ? (() => spectatePlayer(index)) : null} - /> - ))} + + + + = problems.length}> + + + + + + Player + Score + Time + Submissions + {!spectatePlayer ? Code : null} + {spectatePlayer ? Spectate Live : null} + + {players?.map((player, index) => ( + viewPlayerCode(index)) : null} + onSpectateLive={spectatePlayer ? (() => spectatePlayer(index)) : null} + /> + ))} + ); } From 1c6670ca8e3fd1863e2d3aa926209fd70844849c Mon Sep 17 00:00:00 2001 From: Alan Bi Date: Thu, 24 Jun 2021 21:58:30 -0700 Subject: [PATCH 03/19] Updating problem nav styling and results table props --- .../src/components/game/SpectatorGameView.tsx | 2 +- .../src/components/results/ResultsTable.tsx | 38 ++++++++++++++----- frontend/src/views/Results.tsx | 2 +- 3 files changed, 30 insertions(+), 12 deletions(-) diff --git a/frontend/src/components/game/SpectatorGameView.tsx b/frontend/src/components/game/SpectatorGameView.tsx index 7e3f8f7c..31c55ce5 100644 --- a/frontend/src/components/game/SpectatorGameView.tsx +++ b/frontend/src/components/game/SpectatorGameView.tsx @@ -83,7 +83,7 @@ function SpectatorGameView() { subscribePlayer(game.room.roomId, game.players[index].user.userId!); } }} - numProblems={game?.problems.length || 1} + problems={game?.problems || []} /> {error ? : null} {loading ? : null} diff --git a/frontend/src/components/results/ResultsTable.tsx b/frontend/src/components/results/ResultsTable.tsx index d9fda1f7..ec71fdb5 100644 --- a/frontend/src/components/results/ResultsTable.tsx +++ b/frontend/src/components/results/ResultsTable.tsx @@ -7,6 +7,7 @@ import { User } from '../../api/User'; import { NextIcon, PrevIcon } from '../core/Icon'; import { ProblemNavButton } from '../core/Button'; import { Problem } from '../../api/Problem'; +import { NoMarginSubtitleText } from '../core/Text'; const Content = styled.div` width: 65%; @@ -34,6 +35,18 @@ const TableContent = styled.table` } `; +const TopContent = styled.div` + display: flex; + justify-content: center; + align-items: center; + height: 70px; +`; + +const ProblemText = styled(NoMarginSubtitleText)` + display: inline; + margin: 0 12px; +`; + const PrimaryTableHeader = styled.th` text-align: left; `; @@ -46,18 +59,17 @@ type ResultsTableProps = { players: Player[], currentUser: User | null, gameStartTime: string, - numProblems: number, + problems: Problem[], viewPlayerCode: ((index: number) => void) | null, spectatePlayer: ((index: number) => void) | null, }; function ResultsTable(props: ResultsTableProps) { const { - players, currentUser, gameStartTime, numProblems, viewPlayerCode, spectatePlayer, + players, currentUser, gameStartTime, problems, viewPlayerCode, spectatePlayer, } = props; const [problemIndex, setProblemIndex] = useState(0); - const problems: string[] = ['t', 'af', 'af']; // TODO: temp - replace with prop const nextProblem = () => { const next = problemIndex + 1; @@ -77,12 +89,18 @@ function ResultsTable(props: ResultsTableProps) { return ( - - - - = problems.length}> - - + + + + + + {`Problem ${problemIndex + 1} of ${problems.length}. `} + {problems[problemIndex]?.name || ''} + + = problems.length}> + + + @@ -100,7 +118,7 @@ function ResultsTable(props: ResultsTableProps) { isCurrentPlayer={currentUser?.userId === player.user.userId} gameStartTime={gameStartTime} color={player.color} - numProblems={numProblems} + numProblems={problems.length} onViewCode={viewPlayerCode ? (() => viewPlayerCode(index)) : null} onSpectateLive={spectatePlayer ? (() => spectatePlayer(index)) : null} /> diff --git a/frontend/src/views/Results.tsx b/frontend/src/views/Results.tsx index b7b03fbb..a2d1b241 100644 --- a/frontend/src/views/Results.tsx +++ b/frontend/src/views/Results.tsx @@ -343,7 +343,7 @@ function GameResultsPage() { players={players} currentUser={currentUser} gameStartTime={startTime} - numProblems={game?.problems.length || 1} + problems={game?.problems || []} viewPlayerCode={(index: number) => setCodeModal(index)} spectatePlayer={null} /> From f626c7144daf8b96c6eccb577264b244f5636fff Mon Sep 17 00:00:00 2001 From: Alan Bi Date: Sat, 26 Jun 2021 20:10:55 -0700 Subject: [PATCH 04/19] Update hooks in prep for problem specific results --- .../components/results/PlayerResultsItem.tsx | 38 ++++--------------- .../src/components/results/ResultsTable.tsx | 2 +- frontend/src/util/Hook.tsx | 9 ++++- frontend/src/util/Utility.ts | 8 +++- 4 files changed, 22 insertions(+), 35 deletions(-) diff --git a/frontend/src/components/results/PlayerResultsItem.tsx b/frontend/src/components/results/PlayerResultsItem.tsx index 4d48a163..ca391556 100644 --- a/frontend/src/components/results/PlayerResultsItem.tsx +++ b/frontend/src/components/results/PlayerResultsItem.tsx @@ -6,7 +6,7 @@ import { Color } from '../../api/Color'; import { useBestSubmission, useGetScore, useGetSubmissionTime } from '../../util/Hook'; import Language, { displayNameFromLanguage } from '../../api/Language'; import { TextButton } from '../core/Button'; -// import { getScore, getSubmissionCount, getSubmissionTime } from '../../util/Utility'; +import { getScore, getSubmissionCount, getSubmissionTime } from '../../util/Utility'; const Content = styled.tr` border-radius: 5px; @@ -83,47 +83,23 @@ type PlayerResultsCardProps = { isCurrentPlayer: boolean, gameStartTime: string, color: Color, - numProblems: number, + problemIndex: number, onViewCode: (() => void) | null, onSpectateLive: (() => void) | null, }; function PlayerResultsItem(props: PlayerResultsCardProps) { const { - player, place, isCurrentPlayer, color, gameStartTime, numProblems, onViewCode, onSpectateLive, + player, place, isCurrentPlayer, color, gameStartTime, problemIndex, onViewCode, onSpectateLive, } = props; - const score = useGetScore(player); - const time = useGetSubmissionTime(player); - const bestSubmission : Submission | null = useBestSubmission(player); + const bestSubmission: Submission | null = useBestSubmission(player, problemIndex); const getDisplayNickname = () => { const { nickname } = player.user; return `${nickname} ${isCurrentPlayer ? '(you)' : ''}`; }; - const getScore = () => { - if (!score) { - return '0'; - } - - const percent = Math.round((score / numProblems) * 100); - return `${percent}%`; - }; - - const getSubmissionTime = () => { - if (!time) { - return 'N/A'; - } - - const startTime = new Date(gameStartTime).getTime(); - const diffMilliseconds = new Date(time).getTime() - startTime; - const diffMinutes = Math.floor(diffMilliseconds / (60 * 1000)); - return `${diffMinutes}m ago`; - }; - - const getSubmissionCount = () => player.submissions.length || '0'; - const getSubmissionLanguage = () => { if (!bestSubmission) { return 'N/A'; @@ -153,13 +129,13 @@ function PlayerResultsItem(props: PlayerResultsCardProps) { - {getScore()} + {getScore(bestSubmission)} - {getSubmissionTime()} + {getSubmissionTime(bestSubmission, gameStartTime)} - {getSubmissionCount()} + {getSubmissionCount(player, problemIndex)} {!onSpectateLive ? ( {getSubmissionLanguage()} diff --git a/frontend/src/components/results/ResultsTable.tsx b/frontend/src/components/results/ResultsTable.tsx index ec71fdb5..3e0a86de 100644 --- a/frontend/src/components/results/ResultsTable.tsx +++ b/frontend/src/components/results/ResultsTable.tsx @@ -118,7 +118,7 @@ function ResultsTable(props: ResultsTableProps) { isCurrentPlayer={currentUser?.userId === player.user.userId} gameStartTime={gameStartTime} color={player.color} - numProblems={problems.length} + problemIndex={problemIndex} onViewCode={viewPlayerCode ? (() => viewPlayerCode(index)) : null} onSpectateLive={spectatePlayer ? (() => spectatePlayer(index)) : null} /> diff --git a/frontend/src/util/Hook.tsx b/frontend/src/util/Hook.tsx index 5954c341..0391a620 100644 --- a/frontend/src/util/Hook.tsx +++ b/frontend/src/util/Hook.tsx @@ -9,15 +9,20 @@ import { Problem } from '../api/Problem'; import { Coordinate } from '../components/special/FloatingCircle'; import app from '../api/Firebase'; -export const useBestSubmission = (player?: Player | null) => { +// todo: verify correct usages of this function +export const useBestSubmission = (player?: Player | null, problemIndex?: number) => { const [bestSubmission, setBestSubmission] = useState(null); useEffect(() => { if (player) { let newBestSubmission: Submission | null = null; + // If problemIndex is specified, find best submission only for that problem + const submissions = (problemIndex !== undefined) + ? player.submissions.filter((s) => s.problemIndex === problemIndex) : player.submissions; + // Find best submission - player.submissions.forEach((submission) => { + submissions.forEach((submission) => { if (!newBestSubmission || submission.numCorrect > newBestSubmission.numCorrect) { newBestSubmission = submission; } diff --git a/frontend/src/util/Utility.ts b/frontend/src/util/Utility.ts index 6a8e8270..7ad936d8 100644 --- a/frontend/src/util/Utility.ts +++ b/frontend/src/util/Utility.ts @@ -110,6 +110,7 @@ export const problemMatchesFilterText = (problem: Problem | SelectableProblem, return true; }; +// todo: verify correct usages of these functions export const getScore = (bestSubmission: Submission | null) => { if (!bestSubmission) { return '0'; @@ -133,4 +134,9 @@ export const getSubmissionTime = (bestSubmission: Submission | null, return ` ${diffMinutes} min`; }; -export const getSubmissionCount = (player: Player | null) => player?.submissions.length || '0'; +export const getSubmissionCount = (player: Player | null, problemIndex?: number) => { + const submissions = (problemIndex !== undefined) + ? player?.submissions.filter((s) => s.problemIndex === problemIndex) : player?.submissions; + + return submissions?.length || '0'; +}; From 7d5f9570ed6d2564625cacc933ef724223a46bda Mon Sep 17 00:00:00 2001 From: Alan Bi Date: Sat, 26 Jun 2021 20:32:54 -0700 Subject: [PATCH 05/19] Update hooks/util methods and add comments --- .../src/components/game/PlayerGameView.tsx | 24 +++++++++---------- .../components/results/PlayerResultsItem.tsx | 2 +- .../src/components/results/ResultsTable.tsx | 3 +-- frontend/src/util/Hook.tsx | 6 +++-- frontend/src/util/Utility.ts | 4 +++- frontend/src/views/ContactUs.tsx | 2 +- 6 files changed, 22 insertions(+), 19 deletions(-) diff --git a/frontend/src/components/game/PlayerGameView.tsx b/frontend/src/components/game/PlayerGameView.tsx index 1c165a8a..ecfcf2a9 100644 --- a/frontend/src/components/game/PlayerGameView.tsx +++ b/frontend/src/components/game/PlayerGameView.tsx @@ -151,9 +151,19 @@ function PlayerGameView(props: PlayerGameViewProps) { // Variable to hold whether the user is subscribed to their own player socket. const [playerSocket, setPlayerSocket] = useState(null); + // References necessary for the spectator subscription callback. + const stateRef = useRef(); + stateRef.current = { + game, + currentUser, + currentCode: codeList[currentProblemIndex], + currentLanguage: languageList[currentProblemIndex], + currentIndex: currentProblemIndex, + }; + // Variables to hold the player stats when spectating. const [spectatedPlayer, setSpectatedPlayer] = useState(null); - const bestSubmission = useBestSubmission(spectatedPlayer); + const bestSubmission = useBestSubmission(spectatedPlayer, stateRef.current.currentIndex); useEffect(() => setProblems(game?.problems || []), [game]); @@ -197,16 +207,6 @@ function PlayerGameView(props: PlayerGameViewProps) { return null; }; - // References necessary for the spectator subscription callback. - const stateRef = useRef(); - stateRef.current = { - game, - currentUser, - currentCode: codeList[currentProblemIndex], - currentLanguage: languageList[currentProblemIndex], - currentIndex: currentProblemIndex, - }; - const setDefaultCodeFromProblems = useCallback((problemsParam: Problem[], playerSubmissions: Submission[]) => { setSubmissions(playerSubmissions); @@ -469,7 +469,7 @@ function PlayerGameView(props: PlayerGameViewProps) { Submissions: {' '} - {getSubmissionCount(spectatedPlayer)} + {getSubmissionCount(spectatedPlayer, stateRef.current.currentIndex)} diff --git a/frontend/src/components/results/PlayerResultsItem.tsx b/frontend/src/components/results/PlayerResultsItem.tsx index ca391556..d1a22078 100644 --- a/frontend/src/components/results/PlayerResultsItem.tsx +++ b/frontend/src/components/results/PlayerResultsItem.tsx @@ -3,7 +3,7 @@ import styled from 'styled-components'; import { Player, Submission } from '../../api/Game'; import { LowMarginText, Text } from '../core/Text'; import { Color } from '../../api/Color'; -import { useBestSubmission, useGetScore, useGetSubmissionTime } from '../../util/Hook'; +import { useBestSubmission } from '../../util/Hook'; import Language, { displayNameFromLanguage } from '../../api/Language'; import { TextButton } from '../core/Button'; import { getScore, getSubmissionCount, getSubmissionTime } from '../../util/Utility'; diff --git a/frontend/src/components/results/ResultsTable.tsx b/frontend/src/components/results/ResultsTable.tsx index 3e0a86de..a0692b16 100644 --- a/frontend/src/components/results/ResultsTable.tsx +++ b/frontend/src/components/results/ResultsTable.tsx @@ -108,8 +108,7 @@ function ResultsTable(props: ResultsTableProps) { Score Time Submissions - {!spectatePlayer ? Code : null} - {spectatePlayer ? Spectate Live : null} + {!spectatePlayer ? Code : Spectate Live} {players?.map((player, index) => ( { const [bestSubmission, setBestSubmission] = useState(null); @@ -30,11 +30,12 @@ export const useBestSubmission = (player?: Player | null, problemIndex?: number) setBestSubmission(newBestSubmission); } - }, [player, setBestSubmission]); + }, [player, setBestSubmission, problemIndex]); return bestSubmission; }; +// Calculates the overall score of a player (number solved) export const useGetScore = (player?: Player) => { const counted = new Set(); const [score, setScore] = useState(0); @@ -58,6 +59,7 @@ export const useGetScore = (player?: Player) => { return score; }; +// Calculates the time taken for the latest 100% correct solution for a problem export const useGetSubmissionTime = (player?: Player) => { const counted = new Set(); const [time, setTime] = useState(); diff --git a/frontend/src/util/Utility.ts b/frontend/src/util/Utility.ts index 7ad936d8..507b9ef6 100644 --- a/frontend/src/util/Utility.ts +++ b/frontend/src/util/Utility.ts @@ -110,7 +110,7 @@ export const problemMatchesFilterText = (problem: Problem | SelectableProblem, return true; }; -// todo: verify correct usages of these functions +// Displays the percentage correct of a specific submission export const getScore = (bestSubmission: Submission | null) => { if (!bestSubmission) { return '0'; @@ -120,6 +120,7 @@ export const getScore = (bestSubmission: Submission | null) => { return `${percent}%`; }; +// Displays the time taken for a specific submission export const getSubmissionTime = (bestSubmission: Submission | null, gameStartTime: string | null) => { if (!bestSubmission || !gameStartTime) { @@ -134,6 +135,7 @@ export const getSubmissionTime = (bestSubmission: Submission | null, return ` ${diffMinutes} min`; }; +// Gets the number of submissions for a specific player and problem export const getSubmissionCount = (player: Player | null, problemIndex?: number) => { const submissions = (problemIndex !== undefined) ? player?.submissions.filter((s) => s.problemIndex === problemIndex) : player?.submissions; diff --git a/frontend/src/views/ContactUs.tsx b/frontend/src/views/ContactUs.tsx index 76c72975..ef6b2bf5 100644 --- a/frontend/src/views/ContactUs.tsx +++ b/frontend/src/views/ContactUs.tsx @@ -1,4 +1,4 @@ -import React, { useState } from 'react'; +import React from 'react'; import { DynamicWidthContainer } from '../components/core/Container'; import { InlineExternalLink } from '../components/core/Link'; import { ContactHeaderTitle, ContactHeaderText } from '../components/core/Text'; From a709fa4092fe082e8b9548285e22212c592fa2f3 Mon Sep 17 00:00:00 2001 From: Alan Bi Date: Tue, 29 Jun 2021 23:41:45 -0700 Subject: [PATCH 06/19] Update game leaderboard to use fractions and correct solved --- frontend/src/api/Game.ts | 2 +- .../src/components/card/LeaderboardCard.tsx | 18 ++++-------------- .../src/components/results/ResultsTable.tsx | 1 + frontend/src/util/Hook.tsx | 2 +- 4 files changed, 7 insertions(+), 16 deletions(-) diff --git a/frontend/src/api/Game.ts b/frontend/src/api/Game.ts index 696bea04..96aa5ea4 100644 --- a/frontend/src/api/Game.ts +++ b/frontend/src/api/Game.ts @@ -9,7 +9,7 @@ import { Color } from './Color'; export type Player = { user: User, submissions: Submission[], - solved: boolean, + solved: boolean[], color: Color, }; diff --git a/frontend/src/components/card/LeaderboardCard.tsx b/frontend/src/components/card/LeaderboardCard.tsx index b045486a..5278d93a 100644 --- a/frontend/src/components/card/LeaderboardCard.tsx +++ b/frontend/src/components/card/LeaderboardCard.tsx @@ -79,19 +79,9 @@ function LeaderboardCard(props: LeaderboardCardProps) { const score = useGetScore(player); const time = useGetSubmissionTime(player); - const getScoreDisplay = () => { - if (!score) { - return 0; - } - return score; - }; + const getScoreDisplay = () => `${score || 0}/${numProblems}`; - const getScorePercentage = () => { - if (!score) { - return ''; - } - return ` ${Math.round((score / numProblems) * 100)}%`; - }; + const getAllSolved = () => player.solved.every((solved: boolean) => solved); const getSubmissionTime = () => { if (!time) { @@ -115,7 +105,7 @@ function LeaderboardCard(props: LeaderboardCardProps) { nickname={player.user.nickname} active={Boolean(player.user.sessionId)} /> - {`${place}.${getScorePercentage()}`} + {`${place}. ${getScoreDisplay()}`} {showHover ? ( @@ -123,7 +113,7 @@ function LeaderboardCard(props: LeaderboardCardProps) { {player.user.nickname} - {`Score: ${getScoreDisplay()}`} + {`Solved: ${getScoreDisplay()}`} {`Last: ${getSubmissionTime()}`} diff --git a/frontend/src/components/results/ResultsTable.tsx b/frontend/src/components/results/ResultsTable.tsx index a0692b16..10b3ac06 100644 --- a/frontend/src/components/results/ResultsTable.tsx +++ b/frontend/src/components/results/ResultsTable.tsx @@ -69,6 +69,7 @@ function ResultsTable(props: ResultsTableProps) { players, currentUser, gameStartTime, problems, viewPlayerCode, spectatePlayer, } = props; + // todo: use value -1 to represent overview mode const [problemIndex, setProblemIndex] = useState(0); const nextProblem = () => { diff --git a/frontend/src/util/Hook.tsx b/frontend/src/util/Hook.tsx index 069276b9..aad44a5e 100644 --- a/frontend/src/util/Hook.tsx +++ b/frontend/src/util/Hook.tsx @@ -36,7 +36,7 @@ export const useBestSubmission = (player?: Player | null, problemIndex?: number) }; // Calculates the overall score of a player (number solved) -export const useGetScore = (player?: Player) => { +export const useGetScore = (player?: Player): number | null => { const counted = new Set(); const [score, setScore] = useState(0); From d14ed4aa70764f433ec53361c4248cc97710647d Mon Sep 17 00:00:00 2001 From: Alan Bi Date: Wed, 30 Jun 2021 20:07:31 -0700 Subject: [PATCH 07/19] Change results item to support an overview mode --- frontend/src/components/game/ProblemPanel.tsx | 2 +- .../src/components/results/PlayerResultsItem.tsx | 13 ++++++++++++- frontend/src/util/Hook.tsx | 12 ++++++++++-- frontend/src/util/Utility.ts | 2 +- 4 files changed, 24 insertions(+), 5 deletions(-) diff --git a/frontend/src/components/game/ProblemPanel.tsx b/frontend/src/components/game/ProblemPanel.tsx index cdee8464..789dcfb1 100644 --- a/frontend/src/components/game/ProblemPanel.tsx +++ b/frontend/src/components/game/ProblemPanel.tsx @@ -50,7 +50,7 @@ const ProblemNavContainer = styled.div` `; const ProblemCountText = styled(SmallText)` - color: gray; + color: ${({ theme }) => theme.colors.gray}; `; type ProblemPanelProps = { diff --git a/frontend/src/components/results/PlayerResultsItem.tsx b/frontend/src/components/results/PlayerResultsItem.tsx index d1a22078..d6057963 100644 --- a/frontend/src/components/results/PlayerResultsItem.tsx +++ b/frontend/src/components/results/PlayerResultsItem.tsx @@ -115,6 +115,17 @@ function PlayerResultsItem(props: PlayerResultsCardProps) { ); }; + const getScoreToDisplay = () => { + // Show score of specific problem (in percent) + if (problemIndex !== -1) { + return getScore(bestSubmission); + } + + // If in overview mode, show overall number of problems solved + const { solved } = player; + return `${solved.filter((s) => s).length}/${solved.length}`; + }; + return ( @@ -129,7 +140,7 @@ function PlayerResultsItem(props: PlayerResultsCardProps) { - {getScore(bestSubmission)} + {getScoreToDisplay()} {getSubmissionTime(bestSubmission, gameStartTime)} diff --git a/frontend/src/util/Hook.tsx b/frontend/src/util/Hook.tsx index aad44a5e..236a5472 100644 --- a/frontend/src/util/Hook.tsx +++ b/frontend/src/util/Hook.tsx @@ -9,7 +9,15 @@ import { Problem } from '../api/Problem'; import { Coordinate } from '../components/special/FloatingCircle'; import app from '../api/Firebase'; -// Finds the first best submission by a player (for a specific problem, if specified) +/** + * Finds the best submission by a player (the first one, if there's a tie). + * If an index is specified that's not -1, then this will find the best submission + * for that problem specifically. If problemIndex is -1 (overview mode), then it + * will resort back to finding the best overall submission as usual. + * + * @param player The player in question + * @param problemIndex Optional index to specify a problem + */ export const useBestSubmission = (player?: Player | null, problemIndex?: number) => { const [bestSubmission, setBestSubmission] = useState(null); @@ -18,7 +26,7 @@ export const useBestSubmission = (player?: Player | null, problemIndex?: number) let newBestSubmission: Submission | null = null; // If problemIndex is specified, find best submission only for that problem - const submissions = (problemIndex !== undefined) + const submissions = (problemIndex !== undefined && problemIndex !== -1) ? player.submissions.filter((s) => s.problemIndex === problemIndex) : player.submissions; // Find best submission diff --git a/frontend/src/util/Utility.ts b/frontend/src/util/Utility.ts index 39c62ea8..ece1853a 100644 --- a/frontend/src/util/Utility.ts +++ b/frontend/src/util/Utility.ts @@ -145,7 +145,7 @@ export const getSubmissionTime = (bestSubmission: Submission | null, // Gets the number of submissions for a specific player and problem export const getSubmissionCount = (player: Player | null, problemIndex?: number): number => { - const submissions = (problemIndex !== undefined) + const submissions = (problemIndex !== undefined && problemIndex !== -1) ? player?.submissions.filter((s) => s.problemIndex === problemIndex) : player?.submissions; return submissions?.length || 0; From 8ad2e29264dbb7f7456f231e311ad98ae8fca288 Mon Sep 17 00:00:00 2001 From: Alan Bi Date: Wed, 30 Jun 2021 21:59:18 -0700 Subject: [PATCH 08/19] Fix issue and implement previewing correct problem on results --- .../components/results/PreviewCodeContent.tsx | 18 ++++++++---------- .../src/components/results/ResultsTable.tsx | 14 +++++++------- frontend/src/util/Hook.tsx | 18 ++---------------- frontend/src/util/Utility.ts | 19 +++++++++++++++++++ frontend/src/views/Results.tsx | 13 ++++++++++++- 5 files changed, 48 insertions(+), 34 deletions(-) diff --git a/frontend/src/components/results/PreviewCodeContent.tsx b/frontend/src/components/results/PreviewCodeContent.tsx index f823d190..8dba0846 100644 --- a/frontend/src/components/results/PreviewCodeContent.tsx +++ b/frontend/src/components/results/PreviewCodeContent.tsx @@ -1,9 +1,11 @@ -import React from 'react'; +import React, { useEffect, useState } from 'react'; import styled from 'styled-components'; -import { Player, Submission } from '../../api/Game'; +import { Player } from '../../api/Game'; import Language from '../../api/Language'; import { SecondaryHeaderText } from '../core/Text'; import ResizableMonacoEditor from '../game/Editor'; +import { useBestSubmission } from '../../util/Hook'; +import { getBestSubmission } from '../../util/Utility'; const CodePreview = styled.div` position: relative; @@ -20,22 +22,18 @@ const CodePreview = styled.div` type PreviewCodeContentProps = { player: Player | undefined, + problemIndex: number, } function PreviewCodeContent(props: PreviewCodeContentProps) { - const { player } = props; + const { player, problemIndex } = props; + + const bestSubmission = getBestSubmission(player, problemIndex); if (player === undefined || !player || !player.submissions.length) { return null; } - let bestSubmission: Submission | undefined; - player.submissions.forEach((submission) => { - if (!bestSubmission || submission.numCorrect > bestSubmission.numCorrect) { - bestSubmission = submission; - } - }); - return (
diff --git a/frontend/src/components/results/ResultsTable.tsx b/frontend/src/components/results/ResultsTable.tsx index 10b3ac06..fe3c3694 100644 --- a/frontend/src/components/results/ResultsTable.tsx +++ b/frontend/src/components/results/ResultsTable.tsx @@ -60,7 +60,7 @@ type ResultsTableProps = { currentUser: User | null, gameStartTime: string, problems: Problem[], - viewPlayerCode: ((index: number) => void) | null, + viewPlayerCode: ((playerIndex: number, probIndex: number) => void) | null, spectatePlayer: ((index: number) => void) | null, }; @@ -70,7 +70,7 @@ function ResultsTable(props: ResultsTableProps) { } = props; // todo: use value -1 to represent overview mode - const [problemIndex, setProblemIndex] = useState(0); + const [problemIndex, setProblemIndex] = useState(-1); const nextProblem = () => { const next = problemIndex + 1; @@ -83,7 +83,7 @@ function ResultsTable(props: ResultsTableProps) { const previousProblem = () => { const prev = problemIndex - 1; - if (prev >= 0) { + if (prev >= -1) { setProblemIndex(prev); } }; @@ -91,12 +91,12 @@ function ResultsTable(props: ResultsTableProps) { return ( - + - {`Problem ${problemIndex + 1} of ${problems.length}. `} - {problems[problemIndex]?.name || ''} + {problemIndex !== -1 ? `Problem ${problemIndex + 1} of ${problems.length}. ` : null} + {problemIndex !== -1 ? problems[problemIndex]?.name || '' : 'Overview'} = problems.length}> @@ -119,7 +119,7 @@ function ResultsTable(props: ResultsTableProps) { gameStartTime={gameStartTime} color={player.color} problemIndex={problemIndex} - onViewCode={viewPlayerCode ? (() => viewPlayerCode(index)) : null} + onViewCode={viewPlayerCode ? (() => viewPlayerCode(index, problemIndex)) : null} onSpectateLive={spectatePlayer ? (() => spectatePlayer(index)) : null} /> ))} diff --git a/frontend/src/util/Hook.tsx b/frontend/src/util/Hook.tsx index 236a5472..b8e24e45 100644 --- a/frontend/src/util/Hook.tsx +++ b/frontend/src/util/Hook.tsx @@ -8,6 +8,7 @@ import { FirebaseUserType } from '../redux/Account'; import { Problem } from '../api/Problem'; import { Coordinate } from '../components/special/FloatingCircle'; import app from '../api/Firebase'; +import { getBestSubmission } from './Utility'; /** * Finds the best submission by a player (the first one, if there's a tie). @@ -22,22 +23,7 @@ export const useBestSubmission = (player?: Player | null, problemIndex?: number) const [bestSubmission, setBestSubmission] = useState(null); useEffect(() => { - if (player) { - let newBestSubmission: Submission | null = null; - - // If problemIndex is specified, find best submission only for that problem - const submissions = (problemIndex !== undefined && problemIndex !== -1) - ? player.submissions.filter((s) => s.problemIndex === problemIndex) : player.submissions; - - // Find best submission - submissions.forEach((submission) => { - if (!newBestSubmission || submission.numCorrect > newBestSubmission.numCorrect) { - newBestSubmission = submission; - } - }); - - setBestSubmission(newBestSubmission); - } + setBestSubmission(getBestSubmission(player, problemIndex)); }, [player, setBestSubmission, problemIndex]); return bestSubmission; diff --git a/frontend/src/util/Utility.ts b/frontend/src/util/Utility.ts index ece1853a..1f63b9ad 100644 --- a/frontend/src/util/Utility.ts +++ b/frontend/src/util/Utility.ts @@ -128,6 +128,25 @@ export const getScore = (bestSubmission: Submission | null) => { return `${percent}%`; }; +// Find and return the best submission. A non-hook variant of useBestSubmission +export const getBestSubmission = (player?: Player | null, problemIndex?: number) => { + let newBestSubmission = null as Submission | null; + + if (player) { + // If problemIndex is specified, find best submission only for that problem + const submissions = (problemIndex !== undefined && problemIndex !== -1) + ? player.submissions.filter((s) => s.problemIndex === problemIndex) : player.submissions; + + submissions.forEach((submission) => { + if (!newBestSubmission || submission.numCorrect > newBestSubmission.numCorrect) { + newBestSubmission = submission; + } + }); + } + + return newBestSubmission; +}; + // Displays the time taken for a specific submission export const getSubmissionTime = (bestSubmission: Submission | null, gameStartTime: string | null) => { diff --git a/frontend/src/views/Results.tsx b/frontend/src/views/Results.tsx index 977ecc46..9a5276aa 100644 --- a/frontend/src/views/Results.tsx +++ b/frontend/src/views/Results.tsx @@ -98,7 +98,12 @@ function GameResultsPage() { const [hoverVisible, setHoverVisible] = useState(false); const [showFeedbackModal, setShowFeedbackModal] = useState(false); const [showFeedbackPrompt, setShowFeedbackPrompt] = useState(false); + + // If not -1, codeModal represents the index of the player whose code should show in the modal const [codeModal, setCodeModal] = useState(-1); + const [problemIndex, setProblemIndex] = useState(0); + + // If not -1, placeModal is set to the index of the player whose place should show in the modal const [placeModal, setPlaceModal] = useState(-1); const [displayPlaceModal, setDisplayPlaceModal] = useState(true); @@ -211,6 +216,11 @@ function GameResultsPage() { return 'th'; }; + const onViewPlayerCode = (playerIndex: number, probIndex: number) => { + setProblemIndex(probIndex); + setCodeModal(playerIndex); + }; + // Reset hover status on host changes useEffect(() => { setHoverVisible(false); @@ -248,6 +258,7 @@ function GameResultsPage() { setCodeModal(-1)} fullScreen> @@ -343,7 +354,7 @@ function GameResultsPage() { currentUser={currentUser} gameStartTime={startTime} problems={game?.problems || []} - viewPlayerCode={(index: number) => setCodeModal(index)} + viewPlayerCode={onViewPlayerCode} spectatePlayer={null} /> ) : null} From 83673720bb6b33ffc91060f78972732623a59de1 Mon Sep 17 00:00:00 2001 From: Alan Bi Date: Wed, 30 Jun 2021 22:17:06 -0700 Subject: [PATCH 09/19] Refactor time and final column behavior --- .../components/results/PlayerResultsItem.tsx | 19 ++++++++++++++++--- frontend/src/components/results/Podium.tsx | 7 ++----- .../components/results/PreviewCodeContent.tsx | 3 +-- .../src/components/results/ResultsTable.tsx | 14 +++++++++++--- frontend/src/util/Utility.ts | 14 ++++++++------ 5 files changed, 38 insertions(+), 19 deletions(-) diff --git a/frontend/src/components/results/PlayerResultsItem.tsx b/frontend/src/components/results/PlayerResultsItem.tsx index d6057963..72906c10 100644 --- a/frontend/src/components/results/PlayerResultsItem.tsx +++ b/frontend/src/components/results/PlayerResultsItem.tsx @@ -3,10 +3,10 @@ import styled from 'styled-components'; import { Player, Submission } from '../../api/Game'; import { LowMarginText, Text } from '../core/Text'; import { Color } from '../../api/Color'; -import { useBestSubmission } from '../../util/Hook'; +import { useBestSubmission, useGetSubmissionTime } from '../../util/Hook'; import Language, { displayNameFromLanguage } from '../../api/Language'; import { TextButton } from '../core/Button'; -import { getScore, getSubmissionCount, getSubmissionTime } from '../../util/Utility'; +import { getScore, getSubmissionCount, getSubmissionTime, getTimeBetween } from '../../util/Utility'; const Content = styled.tr` border-radius: 5px; @@ -94,6 +94,7 @@ function PlayerResultsItem(props: PlayerResultsCardProps) { } = props; const bestSubmission: Submission | null = useBestSubmission(player, problemIndex); + const finalSubmissionTime = useGetSubmissionTime(player); const getDisplayNickname = () => { const { nickname } = player.user; @@ -126,6 +127,18 @@ function PlayerResultsItem(props: PlayerResultsCardProps) { return `${solved.filter((s) => s).length}/${solved.length}`; }; + const getTimeToDisplay = () => { + if (problemIndex !== -1) { + return getSubmissionTime(bestSubmission, gameStartTime); + } + + if (!finalSubmissionTime) { + return 'N/A'; + } + + return `${getTimeBetween(gameStartTime, finalSubmissionTime)} min`; + }; + return ( @@ -143,7 +156,7 @@ function PlayerResultsItem(props: PlayerResultsCardProps) { {getScoreToDisplay()} - {getSubmissionTime(bestSubmission, gameStartTime)} + {getTimeToDisplay()} {getSubmissionCount(player, problemIndex)} diff --git a/frontend/src/components/results/Podium.tsx b/frontend/src/components/results/Podium.tsx index 3aab8cdc..fdcbae43 100644 --- a/frontend/src/components/results/Podium.tsx +++ b/frontend/src/components/results/Podium.tsx @@ -4,6 +4,7 @@ import { Player } from '../../api/Game'; import { Text, MediumText } from '../core/Text'; import Language, { displayNameFromLanguage } from '../../api/Language'; import { useBestSubmission, useGetScore, useGetSubmissionTime } from '../../util/Hook'; +import { getTimeBetween } from '../../util/Utility'; type PodiumProps = { place: number, @@ -147,11 +148,7 @@ function Podium(props: PodiumProps) { return ; } - // Calculate time from start of game till best submission - const startTime = new Date(gameStartTime).getTime(); - const diffMilliseconds = new Date(time).getTime() - startTime; - const diffMinutes = Math.floor(diffMilliseconds / (60 * 1000)); - + const diffMinutes = getTimeBetween(gameStartTime, time); return ( in diff --git a/frontend/src/components/results/PreviewCodeContent.tsx b/frontend/src/components/results/PreviewCodeContent.tsx index 8dba0846..cdf353a0 100644 --- a/frontend/src/components/results/PreviewCodeContent.tsx +++ b/frontend/src/components/results/PreviewCodeContent.tsx @@ -1,10 +1,9 @@ -import React, { useEffect, useState } from 'react'; +import React from 'react'; import styled from 'styled-components'; import { Player } from '../../api/Game'; import Language from '../../api/Language'; import { SecondaryHeaderText } from '../core/Text'; import ResizableMonacoEditor from '../game/Editor'; -import { useBestSubmission } from '../../util/Hook'; import { getBestSubmission } from '../../util/Utility'; const CodePreview = styled.div` diff --git a/frontend/src/components/results/ResultsTable.tsx b/frontend/src/components/results/ResultsTable.tsx index fe3c3694..da6fbbbb 100644 --- a/frontend/src/components/results/ResultsTable.tsx +++ b/frontend/src/components/results/ResultsTable.tsx @@ -88,6 +88,14 @@ function ResultsTable(props: ResultsTableProps) { } }; + const getFinalColumn = () => { + if (problemIndex === -1) { + return null; + } + + return !spectatePlayer ? Code : Spectate Live + }; + return ( @@ -106,10 +114,10 @@ function ResultsTable(props: ResultsTableProps) { Player - Score + {problemIndex === -1 ? 'Problems Solved' : 'Tests Passed'} Time - Submissions - {!spectatePlayer ? Code : Spectate Live} + {problemIndex === -1 ? 'Submissions' : 'Attempts'} + {getFinalColumn()} {players?.map((player, index) => ( { + // Calculate time from start of game till best submission + const startTime = new Date(start).getTime(); + const diffMilliseconds = new Date(end).getTime() - startTime; + return Math.floor(diffMilliseconds / (60 * 1000)); +}; + // Displays the time taken for a specific submission export const getSubmissionTime = (bestSubmission: Submission | null, gameStartTime: string | null) => { @@ -154,12 +161,7 @@ export const getSubmissionTime = (bestSubmission: Submission | null, return 'N/A'; } - // Calculate time from start of game till best submission - const startTime = new Date(gameStartTime).getTime(); - const diffMilliseconds = new Date(bestSubmission.startTime).getTime() - startTime; - const diffMinutes = Math.floor(diffMilliseconds / (60 * 1000)); - - return ` ${diffMinutes} min`; + return ` ${getTimeBetween(gameStartTime, bestSubmission.startTime)} min`; }; // Gets the number of submissions for a specific player and problem From a358de719bdf8eaca35c0708fc39d33709b1ac13 Mon Sep 17 00:00:00 2001 From: Alan Bi Date: Wed, 30 Jun 2021 22:39:49 -0700 Subject: [PATCH 10/19] Implement sorting by problem and update final column --- .../components/results/PlayerResultsItem.tsx | 35 ++++++++++++------- frontend/src/components/results/Podium.tsx | 6 ++-- .../src/components/results/ResultsTable.tsx | 34 +++++++++++++++--- 3 files changed, 54 insertions(+), 21 deletions(-) diff --git a/frontend/src/components/results/PlayerResultsItem.tsx b/frontend/src/components/results/PlayerResultsItem.tsx index 72906c10..058e2fff 100644 --- a/frontend/src/components/results/PlayerResultsItem.tsx +++ b/frontend/src/components/results/PlayerResultsItem.tsx @@ -139,6 +139,27 @@ function PlayerResultsItem(props: PlayerResultsCardProps) { return `${getTimeBetween(gameStartTime, finalSubmissionTime)} min`; }; + const getFinalColumn = () => { + if (problemIndex === -1) { + return null; + } + + if (!onSpectateLive) { + return {getSubmissionLanguage()}; + } + + return ( + + + + Launch + launch + + + + ); + }; + return ( @@ -161,19 +182,7 @@ function PlayerResultsItem(props: PlayerResultsCardProps) { {getSubmissionCount(player, problemIndex)} - {!onSpectateLive ? ( - {getSubmissionLanguage()} - ) : null} - {onSpectateLive ? ( - - - - Launch - launch - - - - ) : null} + {getFinalColumn()} ); } diff --git a/frontend/src/components/results/Podium.tsx b/frontend/src/components/results/Podium.tsx index fdcbae43..1a88d989 100644 --- a/frontend/src/components/results/Podium.tsx +++ b/frontend/src/components/results/Podium.tsx @@ -134,11 +134,11 @@ function Podium(props: PodiumProps) { ); } - const percent = Math.round((score / numProblems) * 100); + const { solved } = player; return ( - Scored - {` ${percent}%`} + Solved + {` ${solved.filter((s) => s).length}/${solved.length}`} ); }; diff --git a/frontend/src/components/results/ResultsTable.tsx b/frontend/src/components/results/ResultsTable.tsx index da6fbbbb..f3197acf 100644 --- a/frontend/src/components/results/ResultsTable.tsx +++ b/frontend/src/components/results/ResultsTable.tsx @@ -1,5 +1,5 @@ /* eslint-disable jsx-a11y/control-has-associated-label */ -import React, { useState } from 'react'; +import React, { useEffect, useState } from 'react'; import styled from 'styled-components'; import { Player } from '../../api/Game'; import PlayerResultsItem from './PlayerResultsItem'; @@ -8,6 +8,7 @@ import { NextIcon, PrevIcon } from '../core/Icon'; import { ProblemNavButton } from '../core/Button'; import { Problem } from '../../api/Problem'; import { NoMarginSubtitleText } from '../core/Text'; +import { getBestSubmission } from '../../util/Utility'; const Content = styled.div` width: 65%; @@ -69,8 +70,31 @@ function ResultsTable(props: ResultsTableProps) { players, currentUser, gameStartTime, problems, viewPlayerCode, spectatePlayer, } = props; - // todo: use value -1 to represent overview mode + // A value of -1 represents the Game Overview state const [problemIndex, setProblemIndex] = useState(-1); + const [sortedPlayers, setSortedPlayers] = useState(players); + + useEffect(() => { + // By default, already sorted by overall num problems solved + if (problemIndex === -1) { + setSortedPlayers(players); + return; + } + + setSortedPlayers(players.sort((p1, p2) => { + const p1Best = getBestSubmission(p1, problemIndex); + const p2Best = getBestSubmission(p2, problemIndex); + + if ((p1Best?.numCorrect || 0) > (p2Best?.numCorrect || 0)) { + return -1; + } + if ((p1Best?.numCorrect || 0) < (p2Best?.numCorrect || 0)) { + return 1; + } + + return (p1Best?.startTime || '') < (p2Best?.startTime || '') ? -1 : 1; + })); + }, [players, problemIndex]); const nextProblem = () => { const next = problemIndex + 1; @@ -93,7 +117,7 @@ function ResultsTable(props: ResultsTableProps) { return null; } - return !spectatePlayer ? Code : Spectate Live + return !spectatePlayer ? Code : Spectate Live; }; return ( @@ -104,7 +128,7 @@ function ResultsTable(props: ResultsTableProps) { {problemIndex !== -1 ? `Problem ${problemIndex + 1} of ${problems.length}. ` : null} - {problemIndex !== -1 ? problems[problemIndex]?.name || '' : 'Overview'} + {problemIndex !== -1 ? problems[problemIndex]?.name || '' : 'Game Overview'} = problems.length}> @@ -119,7 +143,7 @@ function ResultsTable(props: ResultsTableProps) { {problemIndex === -1 ? 'Submissions' : 'Attempts'} {getFinalColumn()} - {players?.map((player, index) => ( + {sortedPlayers?.map((player, index) => ( Date: Thu, 1 Jul 2021 21:22:35 -0700 Subject: [PATCH 11/19] Remove useGetScore and replace with filter --- .../src/components/card/LeaderboardCard.tsx | 4 ++-- frontend/src/components/results/Podium.tsx | 4 ++-- .../src/components/results/ResultsTable.tsx | 2 +- frontend/src/util/Hook.tsx | 24 ------------------- 4 files changed, 5 insertions(+), 29 deletions(-) diff --git a/frontend/src/components/card/LeaderboardCard.tsx b/frontend/src/components/card/LeaderboardCard.tsx index 5278d93a..66da5151 100644 --- a/frontend/src/components/card/LeaderboardCard.tsx +++ b/frontend/src/components/card/LeaderboardCard.tsx @@ -4,7 +4,7 @@ import { Player } from '../../api/Game'; import { LowMarginText, SmallText } from '../core/Text'; import PlayerIcon from './PlayerIcon'; import { Color } from '../../api/Color'; -import { useGetScore, useGetSubmissionTime } from '../../util/Hook'; +import { useGetSubmissionTime } from '../../util/Hook'; type ContentStyleType = { isCurrentPlayer: boolean, @@ -76,7 +76,7 @@ function LeaderboardCard(props: LeaderboardCardProps) { } = props; const [showHover, setShowHover] = useState(false); - const score = useGetScore(player); + const score = player.solved.filter((s) => s).length; const time = useGetSubmissionTime(player); const getScoreDisplay = () => `${score || 0}/${numProblems}`; diff --git a/frontend/src/components/results/Podium.tsx b/frontend/src/components/results/Podium.tsx index 1a88d989..552adc97 100644 --- a/frontend/src/components/results/Podium.tsx +++ b/frontend/src/components/results/Podium.tsx @@ -3,7 +3,7 @@ import styled from 'styled-components'; import { Player } from '../../api/Game'; import { Text, MediumText } from '../core/Text'; import Language, { displayNameFromLanguage } from '../../api/Language'; -import { useBestSubmission, useGetScore, useGetSubmissionTime } from '../../util/Hook'; +import { useBestSubmission, useGetSubmissionTime } from '../../util/Hook'; import { getTimeBetween } from '../../util/Utility'; type PodiumProps = { @@ -90,7 +90,7 @@ function Podium(props: PodiumProps) { place, player, gameStartTime, loading, inviteContent, isCurrentPlayer, numProblems, } = props; - const score = useGetScore(player); + const score = (player?.solved || []).filter((s) => s).length; const bestSubmission = useBestSubmission(player); const time = useGetSubmissionTime(player); diff --git a/frontend/src/components/results/ResultsTable.tsx b/frontend/src/components/results/ResultsTable.tsx index f3197acf..0876f4aa 100644 --- a/frontend/src/components/results/ResultsTable.tsx +++ b/frontend/src/components/results/ResultsTable.tsx @@ -81,7 +81,7 @@ function ResultsTable(props: ResultsTableProps) { return; } - setSortedPlayers(players.sort((p1, p2) => { + setSortedPlayers([...players].sort((p1, p2) => { const p1Best = getBestSubmission(p1, problemIndex); const p2Best = getBestSubmission(p2, problemIndex); diff --git a/frontend/src/util/Hook.tsx b/frontend/src/util/Hook.tsx index b8e24e45..da20e93e 100644 --- a/frontend/src/util/Hook.tsx +++ b/frontend/src/util/Hook.tsx @@ -29,30 +29,6 @@ export const useBestSubmission = (player?: Player | null, problemIndex?: number) return bestSubmission; }; -// Calculates the overall score of a player (number solved) -export const useGetScore = (player?: Player): number | null => { - const counted = new Set(); - const [score, setScore] = useState(0); - - useEffect(() => { - if (player) { - for (let i = 0; i < player.submissions.length; i += 1) { - if (player.submissions[i].numCorrect === player.submissions[i].numTestCases - && !counted.has(player.submissions[i].problemIndex)) { - counted.add(player.submissions[i].problemIndex); - } - } - - setScore(counted.size); - } - }, [player, setScore, counted]); - - if (player == null || player.submissions.length === 0) { - return null; - } - return score; -}; - // Calculates the time taken for the latest 100% correct solution for a problem export const useGetSubmissionTime = (player?: Player) => { const counted = new Set(); From 1153713eff54b5cd0be6e276e317e7ea56ef2aac Mon Sep 17 00:00:00 2001 From: Alan Bi Date: Thu, 1 Jul 2021 22:30:28 -0700 Subject: [PATCH 12/19] Attempt at spectating any problem --- frontend/src/api/Game.ts | 1 + .../src/components/game/PlayerGameView.tsx | 40 ++++++++++++++----- .../src/components/results/ResultsTable.tsx | 4 +- 3 files changed, 32 insertions(+), 13 deletions(-) diff --git a/frontend/src/api/Game.ts b/frontend/src/api/Game.ts index 96aa5ea4..75081438 100644 --- a/frontend/src/api/Game.ts +++ b/frontend/src/api/Game.ts @@ -82,6 +82,7 @@ export type Submission = { export type SpectateGame = { user: User, problem: Problem, + index: number, code: string, language: string, }; diff --git a/frontend/src/components/game/PlayerGameView.tsx b/frontend/src/components/game/PlayerGameView.tsx index ecfcf2a9..ae5ec1dd 100644 --- a/frontend/src/components/game/PlayerGameView.tsx +++ b/frontend/src/components/game/PlayerGameView.tsx @@ -38,6 +38,7 @@ import { routes, send, subscribe } from '../../api/Socket'; import { User } from '../../api/User'; import { getScore, getSubmissionCount, getSubmissionTime } from '../../util/Utility'; import ProblemPanel from './ProblemPanel'; +import const NoPaddingPanel = styled(Panel)` padding: 0; @@ -178,18 +179,18 @@ function PlayerGameView(props: PlayerGameViewProps) { const getCurrentLanguage = useCallback(() => languageList[currentProblemIndex], [languageList, currentProblemIndex]); - const setOneCurrentLanguage = (newLanguage: Language) => { + const setOneCurrentLanguage = (newLanguage: Language, specifiedIndex?: number) => { setLanguageList(languageList.map((current, index) => { - if (index === currentProblemIndex) { + if (index === (specifiedIndex || currentProblemIndex)) { return newLanguage; } return current; })); }; - const setOneCurrentCode = (newCode: string) => { + const setOneCurrentCode = (newCode: string, specifiedIndex?: number) => { setCodeList(codeList.map((current, index) => { - if (index === currentProblemIndex) { + if (index === (specifiedIndex || currentProblemIndex)) { return newCode; } return current; @@ -257,12 +258,15 @@ function PlayerGameView(props: PlayerGameViewProps) { currentLanguageParam: string | undefined, currentIndexParam: number | undefined) => { if (gameParam && currentUserParam) { - const spectatorViewBody: string = JSON.stringify({ + const body: SpectateGame = { user: currentUserParam, problem: gameParam.problems[currentIndexParam || 0], // must satisfy problems.length > 0 - code: currentCodeParam, - language: currentLanguageParam, - }); + index: currentIndexParam || 0, + code: currentCodeParam || '', + language: currentLanguageParam || Language.Java, + }; + const spectatorViewBody: string = JSON.stringify(body); + send( routes(gameParam.room.roomId, currentUserParam.userId).subscribe_player, {}, @@ -343,6 +347,21 @@ function PlayerGameView(props: PlayerGameViewProps) { }, [game, currentUser, defaultCodeList, setDefaultCodeFromProblems, subscribePlayer, playerSocket, getSpectatedPlayer]); + // If spectator and first time on page, load list of problems + useEffect(() => { + if (spectatedPlayer && !defaultCodeList.length && game?.problems) { + setDefaultCodeFromProblems(game.problems, spectatedPlayer.submissions); + } + }, [game, spectatedPlayer, defaultCodeList, setDefaultCodeFromProblems]); + + // When spectate game code changes, update the corresponding problem with that code + useEffect(() => { + if (spectateGame?.code && spectateGame.index && spectateGame.language) { + setOneCurrentCode(spectateGame.code, spectateGame.index); + setOneCurrentLanguage(spectateGame.language as Language, spectateGame.index); + } + }, [spectateGame]); + // Creates Event when splitter bar is dragged const onSecondaryPanelSizeChange = () => { const event = new Event('secondaryPanelSizeChange'); @@ -489,8 +508,7 @@ function PlayerGameView(props: PlayerGameViewProps) { > p.problemId === spectateGame.problem.problemId) || 0} + index={currentProblemIndex} onNext={currentProblemIndex < problems.length - 1 ? nextProblem : null} onPrev={currentProblemIndex > 0 ? previousProblem : null} /> @@ -535,7 +553,7 @@ function PlayerGameView(props: PlayerGameViewProps) { defaultCodeMap={null} currentProblem={currentProblemIndex} defaultCode={spectateGame?.code} - liveCode={spectateGame?.code} + liveCode={null} // todo: check if needed /> ) diff --git a/frontend/src/components/results/ResultsTable.tsx b/frontend/src/components/results/ResultsTable.tsx index 0876f4aa..607b0e15 100644 --- a/frontend/src/components/results/ResultsTable.tsx +++ b/frontend/src/components/results/ResultsTable.tsx @@ -62,7 +62,7 @@ type ResultsTableProps = { gameStartTime: string, problems: Problem[], viewPlayerCode: ((playerIndex: number, probIndex: number) => void) | null, - spectatePlayer: ((index: number) => void) | null, + spectatePlayer: ((playerIndex: number, probIndex: number) => void) | null, }; function ResultsTable(props: ResultsTableProps) { @@ -152,7 +152,7 @@ function ResultsTable(props: ResultsTableProps) { color={player.color} problemIndex={problemIndex} onViewCode={viewPlayerCode ? (() => viewPlayerCode(index, problemIndex)) : null} - onSpectateLive={spectatePlayer ? (() => spectatePlayer(index)) : null} + onSpectateLive={spectatePlayer ? (() => spectatePlayer(index, problemIndex)) : null} /> ))} From ffae85698c69b5e81cfeaf40c1398e4d503336ca Mon Sep 17 00:00:00 2001 From: Alan Bi Date: Sat, 3 Jul 2021 22:24:55 -0700 Subject: [PATCH 13/19] Fix bugs to get multi-problem spectate working --- .../src/components/game/PlayerGameView.tsx | 28 +++++++++++-------- .../src/components/game/SpectatorGameView.tsx | 7 +++-- frontend/src/views/Game.tsx | 1 + 3 files changed, 22 insertions(+), 14 deletions(-) diff --git a/frontend/src/components/game/PlayerGameView.tsx b/frontend/src/components/game/PlayerGameView.tsx index ae5ec1dd..017b4ef5 100644 --- a/frontend/src/components/game/PlayerGameView.tsx +++ b/frontend/src/components/game/PlayerGameView.tsx @@ -38,7 +38,6 @@ import { routes, send, subscribe } from '../../api/Socket'; import { User } from '../../api/User'; import { getScore, getSubmissionCount, getSubmissionTime } from '../../util/Utility'; import ProblemPanel from './ProblemPanel'; -import const NoPaddingPanel = styled(Panel)` padding: 0; @@ -121,17 +120,19 @@ type StateRefType = { * the game page is used for the spectator view. spectateGame is the live data, * primarily the player code, of the player being spectated. * spectatorUnsubscribePlayer unsubscribes the spectator from the player socket - * and brings them back to the main spectator page. + * and brings them back to the main spectator page. defaultIndex is an optional + * parameter to specify which problem to open on when loading this component. */ type PlayerGameViewProps = { gameError: string, spectateGame: SpectateGame | null, spectatorUnsubscribePlayer: (() => void) | null, + defaultIndex: number | null, }; function PlayerGameView(props: PlayerGameViewProps) { const { - gameError, spectateGame, spectatorUnsubscribePlayer, + gameError, spectateGame, spectatorUnsubscribePlayer, defaultIndex, } = props; const { currentUser, game } = useAppSelector((state) => state); @@ -142,7 +143,7 @@ function PlayerGameView(props: PlayerGameViewProps) { const [languageList, setLanguageList] = useState([Language.Java]); const [codeList, setCodeList] = useState(['']); const [currentSubmission, setCurrentSubmission] = useState(null); - const [currentProblemIndex, setCurrentProblemIndex] = useState(0); + const [currentProblemIndex, setCurrentProblemIndex] = useState(defaultIndex || 0); const [loading, setLoading] = useState(false); const [error, setError] = useState(gameError); @@ -209,7 +210,7 @@ function PlayerGameView(props: PlayerGameViewProps) { }; const setDefaultCodeFromProblems = useCallback((problemsParam: Problem[], - playerSubmissions: Submission[]) => { + playerSubmissions: Submission[], focusLatest = true) => { setSubmissions(playerSubmissions); const promises: Promise[] = []; @@ -239,7 +240,9 @@ function PlayerGameView(props: PlayerGameViewProps) { newCodeList[i] = temp.code; codeMap[i][temp.language as Language] = temp.code; newLanguageList[i] = temp.language as Language; - setCurrentProblemIndex(i); + if (focusLatest) { + setCurrentProblemIndex(i); + } } } @@ -350,13 +353,13 @@ function PlayerGameView(props: PlayerGameViewProps) { // If spectator and first time on page, load list of problems useEffect(() => { if (spectatedPlayer && !defaultCodeList.length && game?.problems) { - setDefaultCodeFromProblems(game.problems, spectatedPlayer.submissions); + setDefaultCodeFromProblems(game.problems, spectatedPlayer.submissions, false); } }, [game, spectatedPlayer, defaultCodeList, setDefaultCodeFromProblems]); // When spectate game code changes, update the corresponding problem with that code useEffect(() => { - if (spectateGame?.code && spectateGame.index && spectateGame.language) { + if (spectateGame?.code && spectateGame.language && spectateGame.index !== undefined) { setOneCurrentCode(spectateGame.code, spectateGame.index); setOneCurrentLanguage(spectateGame.language as Language, spectateGame.index); } @@ -470,6 +473,7 @@ function PlayerGameView(props: PlayerGameViewProps) { Spectating: {' '} {spectateGame?.user.nickname} + {currentProblemIndex === spectateGame?.index ? ' (live)' : null} @@ -548,12 +552,12 @@ function PlayerGameView(props: PlayerGameViewProps) { spectateGame?.language as Language} + defaultLanguage={getCurrentLanguage()} + getCurrentLanguage={getCurrentLanguage} defaultCodeMap={null} currentProblem={currentProblemIndex} - defaultCode={spectateGame?.code} - liveCode={null} // todo: check if needed + defaultCode={null} + liveCode={codeList[currentProblemIndex]} /> ) diff --git a/frontend/src/components/game/SpectatorGameView.tsx b/frontend/src/components/game/SpectatorGameView.tsx index 31c55ce5..d6e2ea02 100644 --- a/frontend/src/components/game/SpectatorGameView.tsx +++ b/frontend/src/components/game/SpectatorGameView.tsx @@ -17,6 +17,7 @@ function SpectatorGameView() { const [spectateGame, setSpectateGame] = useState(); const [error, setError] = useState(''); const [loading, setLoading] = useState(false); + const [problemIndex, setProblemIndex] = useState(0); // Unsubscribe from the player socket. const unsubscribePlayer = useCallback(() => { @@ -66,6 +67,7 @@ function SpectatorGameView() { gameError={error} spectateGame={spectateGame} spectatorUnsubscribePlayer={unsubscribePlayer} + defaultIndex={problemIndex} /> ); } @@ -78,9 +80,10 @@ function SpectatorGameView() { currentUser={currentUser} gameStartTime={game?.gameTimer.startTime || ''} viewPlayerCode={null} - spectatePlayer={(index: number) => { + spectatePlayer={(playerIndex: number, probIndex: number) => { if (game) { - subscribePlayer(game.room.roomId, game.players[index].user.userId!); + setProblemIndex(probIndex); + subscribePlayer(game.room.roomId, game.players[playerIndex].user.userId!); } }} problems={game?.problems || []} diff --git a/frontend/src/views/Game.tsx b/frontend/src/views/Game.tsx index 39b371c6..fded63b8 100644 --- a/frontend/src/views/Game.tsx +++ b/frontend/src/views/Game.tsx @@ -220,6 +220,7 @@ function GamePage() { gameError={error} spectateGame={null} spectatorUnsubscribePlayer={null} + defaultIndex={0} /> ) } From 4e00817884418205b7c6067375bcd6e9d3578ee8 Mon Sep 17 00:00:00 2001 From: Alan Bi Date: Sun, 4 Jul 2021 22:15:07 -0700 Subject: [PATCH 14/19] Improve initial spectator load logic and bug fixes --- frontend/src/api/Game.ts | 3 + .../src/components/game/PlayerGameView.tsx | 63 +++++++++++-------- .../src/components/game/SpectatorGameView.tsx | 9 ++- frontend/src/components/results/Podium.tsx | 3 +- .../src/components/results/ResultsTable.tsx | 8 +-- frontend/src/views/Lobby.tsx | 2 +- frontend/src/views/Results.tsx | 7 +-- 7 files changed, 55 insertions(+), 40 deletions(-) diff --git a/frontend/src/api/Game.ts b/frontend/src/api/Game.ts index 75081438..a0d6f137 100644 --- a/frontend/src/api/Game.ts +++ b/frontend/src/api/Game.ts @@ -5,6 +5,7 @@ import { Room } from './Room'; import { User } from './User'; import { Problem } from './Problem'; import { Color } from './Color'; +import Language from './Language'; export type Player = { user: User, @@ -85,6 +86,8 @@ export type SpectateGame = { index: number, code: string, language: string, + codeList?: string[], + languageList?: Language[], }; const basePath = '/api/v1'; diff --git a/frontend/src/components/game/PlayerGameView.tsx b/frontend/src/components/game/PlayerGameView.tsx index 017b4ef5..7dd144da 100644 --- a/frontend/src/components/game/PlayerGameView.tsx +++ b/frontend/src/components/game/PlayerGameView.tsx @@ -113,6 +113,8 @@ type StateRefType = { currentCode: string, currentLanguage: string, currentIndex: number, + codeList: string[], + languageList: Language[], } /** @@ -161,6 +163,8 @@ function PlayerGameView(props: PlayerGameViewProps) { currentCode: codeList[currentProblemIndex], currentLanguage: languageList[currentProblemIndex], currentIndex: currentProblemIndex, + codeList, + languageList, }; // Variables to hold the player stats when spectating. @@ -180,23 +184,29 @@ function PlayerGameView(props: PlayerGameViewProps) { const getCurrentLanguage = useCallback(() => languageList[currentProblemIndex], [languageList, currentProblemIndex]); - const setOneCurrentLanguage = (newLanguage: Language, specifiedIndex?: number) => { - setLanguageList(languageList.map((current, index) => { - if (index === (specifiedIndex || currentProblemIndex)) { + const setOneCurrentLanguage = useCallback((newLanguage: Language, specifiedIndex?: number) => { + setLanguageList((stateRef.current?.languageList || []).map((current, index) => { + if (index === (specifiedIndex !== undefined ? specifiedIndex : currentProblemIndex)) { return newLanguage; } return current; })); - }; + }, [currentProblemIndex]); - const setOneCurrentCode = (newCode: string, specifiedIndex?: number) => { - setCodeList(codeList.map((current, index) => { - if (index === (specifiedIndex || currentProblemIndex)) { + const setOneCurrentCode = useCallback((newCode: string, specifiedIndex?: number) => { + setCodeList((stateRef.current?.codeList || []).map((current, index) => { + if (index === (specifiedIndex !== undefined ? specifiedIndex : currentProblemIndex)) { return newCode; } return current; })); - }; + console.log((stateRef.current?.codeList || []).map((current, index) => { + if (index === (specifiedIndex !== undefined ? specifiedIndex : currentProblemIndex)) { + return newCode; + } + return current; + })); + }, [currentProblemIndex]); // Returns the most recent submission made for problem of index curr. const getSubmission = (curr: number, playerSubmissions: Submission[]) => { @@ -210,7 +220,7 @@ function PlayerGameView(props: PlayerGameViewProps) { }; const setDefaultCodeFromProblems = useCallback((problemsParam: Problem[], - playerSubmissions: Submission[], focusLatest = true) => { + playerSubmissions: Submission[]) => { setSubmissions(playerSubmissions); const promises: Promise[] = []; @@ -240,9 +250,7 @@ function PlayerGameView(props: PlayerGameViewProps) { newCodeList[i] = temp.code; codeMap[i][temp.language as Language] = temp.code; newLanguageList[i] = temp.language as Language; - if (focusLatest) { - setCurrentProblemIndex(i); - } + setCurrentProblemIndex(i); } } @@ -259,7 +267,10 @@ function PlayerGameView(props: PlayerGameViewProps) { currentUserParam: User | null | undefined, currentCodeParam: string | undefined, currentLanguageParam: string | undefined, - currentIndexParam: number | undefined) => { + currentIndexParam: number | undefined, + currentCodeList: string[] | undefined, + currentLanguageList: Language[] | undefined, + sendFullLists = false) => { if (gameParam && currentUserParam) { const body: SpectateGame = { user: currentUserParam, @@ -267,6 +278,8 @@ function PlayerGameView(props: PlayerGameViewProps) { index: currentIndexParam || 0, code: currentCodeParam || '', language: currentLanguageParam || Language.Java, + codeList: sendFullLists ? currentCodeList : undefined, + languageList: sendFullLists ? currentLanguageList : undefined, }; const spectatorViewBody: string = JSON.stringify(body); @@ -281,7 +294,7 @@ function PlayerGameView(props: PlayerGameViewProps) { // Send updates via socket to any spectators. useEffect(() => { sendViewUpdate(game, currentUser, codeList[currentProblemIndex], - languageList[currentProblemIndex], currentProblemIndex); + languageList[currentProblemIndex], currentProblemIndex, codeList, languageList); }, [game, currentUser, codeList, languageList, currentProblemIndex, sendViewUpdate]); // Re-subscribe in order to get the correct subscription callback. @@ -291,7 +304,8 @@ function PlayerGameView(props: PlayerGameViewProps) { if (JSON.parse(result.body).newSpectator) { sendViewUpdate(stateRef.current?.game, stateRef.current?.currentUser, stateRef.current?.currentCode, stateRef.current?.currentLanguage, - stateRef.current?.currentIndex); + stateRef.current?.currentIndex, stateRef.current?.codeList, + stateRef.current?.languageList, true); } }; @@ -330,8 +344,9 @@ function PlayerGameView(props: PlayerGameViewProps) { * If default code list is empty and current user (non-spectator) is * loaded, fetch the code from the backend */ - if (!defaultCodeList.length && !currentUser.spectator) { + if (!defaultCodeList.length && currentUser && !currentUser.spectator) { let matchFound = false; + console.log('in call to fetch problems'); // If this user refreshed and has already submitted code, load and save their latest code game.players.forEach((player) => { @@ -350,20 +365,18 @@ function PlayerGameView(props: PlayerGameViewProps) { }, [game, currentUser, defaultCodeList, setDefaultCodeFromProblems, subscribePlayer, playerSocket, getSpectatedPlayer]); - // If spectator and first time on page, load list of problems - useEffect(() => { - if (spectatedPlayer && !defaultCodeList.length && game?.problems) { - setDefaultCodeFromProblems(game.problems, spectatedPlayer.submissions, false); - } - }, [game, spectatedPlayer, defaultCodeList, setDefaultCodeFromProblems]); - // When spectate game code changes, update the corresponding problem with that code useEffect(() => { - if (spectateGame?.code && spectateGame.language && spectateGame.index !== undefined) { + console.log('here'); + console.log(spectateGame); + if (spectateGame?.codeList && spectateGame.languageList) { + setCodeList(spectateGame.codeList); + setLanguageList(spectateGame.languageList); + } else if (spectateGame?.code && spectateGame.language && spectateGame.index !== undefined) { setOneCurrentCode(spectateGame.code, spectateGame.index); setOneCurrentLanguage(spectateGame.language as Language, spectateGame.index); } - }, [spectateGame]); + }, [spectateGame, setOneCurrentCode, setOneCurrentLanguage]); // Creates Event when splitter bar is dragged const onSecondaryPanelSizeChange = () => { diff --git a/frontend/src/components/game/SpectatorGameView.tsx b/frontend/src/components/game/SpectatorGameView.tsx index d6e2ea02..8ff561c9 100644 --- a/frontend/src/components/game/SpectatorGameView.tsx +++ b/frontend/src/components/game/SpectatorGameView.tsx @@ -39,6 +39,8 @@ function SpectatorGameView() { const subscribePlayerCallback = (result: Message) => { if (!JSON.parse(result.body).newSpectator) { const updatedSpectateGame: SpectateGame = JSON.parse(result.body); + console.log('in spectator page'); + console.log(updatedSpectateGame); setSpectateGame(updatedSpectateGame); } }; @@ -80,10 +82,11 @@ function SpectatorGameView() { currentUser={currentUser} gameStartTime={game?.gameTimer.startTime || ''} viewPlayerCode={null} - spectatePlayer={(playerIndex: number, probIndex: number) => { - if (game) { + spectatePlayer={(playerUserId: string, probIndex: number) => { + const player = game?.players.find((p) => p.user.userId === playerUserId); + if (game && player) { setProblemIndex(probIndex); - subscribePlayer(game.room.roomId, game.players[playerIndex].user.userId!); + subscribePlayer(game.room.roomId, player.user.userId!); } }} problems={game?.problems || []} diff --git a/frontend/src/components/results/Podium.tsx b/frontend/src/components/results/Podium.tsx index 552adc97..7ce71fc8 100644 --- a/frontend/src/components/results/Podium.tsx +++ b/frontend/src/components/results/Podium.tsx @@ -13,7 +13,6 @@ type PodiumProps = { inviteContent: React.ReactNode, loading: boolean, isCurrentPlayer: boolean, - numProblems: number, }; type MedalProps = { @@ -87,7 +86,7 @@ const Medal = styled.div` function Podium(props: PodiumProps) { const { - place, player, gameStartTime, loading, inviteContent, isCurrentPlayer, numProblems, + place, player, gameStartTime, loading, inviteContent, isCurrentPlayer, } = props; const score = (player?.solved || []).filter((s) => s).length; diff --git a/frontend/src/components/results/ResultsTable.tsx b/frontend/src/components/results/ResultsTable.tsx index 607b0e15..7cb1c3df 100644 --- a/frontend/src/components/results/ResultsTable.tsx +++ b/frontend/src/components/results/ResultsTable.tsx @@ -61,8 +61,8 @@ type ResultsTableProps = { currentUser: User | null, gameStartTime: string, problems: Problem[], - viewPlayerCode: ((playerIndex: number, probIndex: number) => void) | null, - spectatePlayer: ((playerIndex: number, probIndex: number) => void) | null, + viewPlayerCode: ((playerUserId: string, probIndex: number) => void) | null, + spectatePlayer: ((playerUserId: string, probIndex: number) => void) | null, }; function ResultsTable(props: ResultsTableProps) { @@ -151,8 +151,8 @@ function ResultsTable(props: ResultsTableProps) { gameStartTime={gameStartTime} color={player.color} problemIndex={problemIndex} - onViewCode={viewPlayerCode ? (() => viewPlayerCode(index, problemIndex)) : null} - onSpectateLive={spectatePlayer ? (() => spectatePlayer(index, problemIndex)) : null} + onViewCode={viewPlayerCode ? (() => viewPlayerCode(player.user.userId || '', problemIndex)) : null} + onSpectateLive={spectatePlayer ? (() => spectatePlayer(player.user.userId || '', problemIndex)) : null} /> ))} diff --git a/frontend/src/views/Lobby.tsx b/frontend/src/views/Lobby.tsx index 9f495089..4d495226 100644 --- a/frontend/src/views/Lobby.tsx +++ b/frontend/src/views/Lobby.tsx @@ -237,7 +237,7 @@ function LobbyPage() { dispatch(setCurrentUser(user)); } }); - }, [currentUser, dispatch]); + }, [currentUser, modifiedProblems, dispatch]); // Map the room in Redux to the state variables used in this file useEffect(() => { diff --git a/frontend/src/views/Results.tsx b/frontend/src/views/Results.tsx index 9a5276aa..4661e15d 100644 --- a/frontend/src/views/Results.tsx +++ b/frontend/src/views/Results.tsx @@ -216,9 +216,9 @@ function GameResultsPage() { return 'th'; }; - const onViewPlayerCode = (playerIndex: number, probIndex: number) => { + const onViewPlayerCode = (playerUserId: string, probIndex: number) => { setProblemIndex(probIndex); - setCodeModal(playerIndex); + setCodeModal(players.findIndex((p) => p.user.userId === playerUserId)); }; // Reset hover status on host changes @@ -292,7 +292,6 @@ function GameResultsPage() { inviteContent={inviteContent()} loading={loading} isCurrentPlayer={players[1]?.user.userId === currentUser?.userId} - numProblems={game?.problems.length || 1} /> From 6f490dc6eb1dd06fc63fabe9d9cca0c9dcbe522c Mon Sep 17 00:00:00 2001 From: Alan Bi Date: Tue, 6 Jul 2021 23:06:09 -0700 Subject: [PATCH 15/19] Address nits --- frontend/src/api/Game.ts | 2 +- frontend/src/components/game/Editor.tsx | 8 +++++++- .../src/components/game/PlayerGameView.tsx | 19 +++++-------------- .../src/components/game/SpectatorGameView.tsx | 2 -- 4 files changed, 13 insertions(+), 18 deletions(-) diff --git a/frontend/src/api/Game.ts b/frontend/src/api/Game.ts index a0d6f137..f204c637 100644 --- a/frontend/src/api/Game.ts +++ b/frontend/src/api/Game.ts @@ -83,7 +83,7 @@ export type Submission = { export type SpectateGame = { user: User, problem: Problem, - index: number, + problemIndex: number, code: string, language: string, codeList?: string[], diff --git a/frontend/src/components/game/Editor.tsx b/frontend/src/components/game/Editor.tsx index fb85f7e0..f9982a6e 100644 --- a/frontend/src/components/game/Editor.tsx +++ b/frontend/src/components/game/Editor.tsx @@ -134,6 +134,12 @@ function ResizableMonacoEditor(props: EditorProps) { }); }; + const handleCodeChange = () => { + if (onCodeChange) { + onCodeChange(codeEditor?.getValue() || ''); + } + }; + const handleLanguageChange = (language: Language) => { // Save the code for this language if (codeMap != null && codeEditor != null) { @@ -199,7 +205,7 @@ function ResizableMonacoEditor(props: EditorProps) { height="100%" editorDidMount={handleEditorDidMount} editorWillMount={handleEditorWillMount} - onChange={() => onCodeChange && onCodeChange(codeEditor?.getValue() || '')} + onChange={handleCodeChange} language={languageToEditorLanguage(currentLanguage)} defaultValue={defaultCode || 'Loading...'} value={liveCode} diff --git a/frontend/src/components/game/PlayerGameView.tsx b/frontend/src/components/game/PlayerGameView.tsx index 41da724e..49169238 100644 --- a/frontend/src/components/game/PlayerGameView.tsx +++ b/frontend/src/components/game/PlayerGameView.tsx @@ -202,12 +202,6 @@ function PlayerGameView(props: PlayerGameViewProps) { } return current; })); - console.log((stateRef.current?.codeList || []).map((current, index) => { - if (index === (specifiedIndex !== undefined ? specifiedIndex : currentProblemIndex)) { - return newCode; - } - return current; - })); }, [currentProblemIndex]); const setDefaultCodeFromProblems = useCallback((problemsParam: Problem[], @@ -266,7 +260,7 @@ function PlayerGameView(props: PlayerGameViewProps) { const body: SpectateGame = { user: currentUserParam, problem: gameParam.problems[currentIndexParam || 0], // must satisfy problems.length > 0 - index: currentIndexParam || 0, + problemIndex: currentIndexParam || 0, code: currentCodeParam || '', language: currentLanguageParam || Language.Java, codeList: sendFullLists ? currentCodeList : undefined, @@ -337,7 +331,6 @@ function PlayerGameView(props: PlayerGameViewProps) { */ if (!defaultCodeList.length && currentUser && !currentUser.spectator) { let matchFound = false; - console.log('in call to fetch problems'); // If this user refreshed and has already submitted code, load and save their latest code game.players.forEach((player) => { @@ -358,14 +351,12 @@ function PlayerGameView(props: PlayerGameViewProps) { // When spectate game code changes, update the corresponding problem with that code useEffect(() => { - console.log('here'); - console.log(spectateGame); if (spectateGame?.codeList && spectateGame.languageList) { setCodeList(spectateGame.codeList); setLanguageList(spectateGame.languageList); - } else if (spectateGame?.code && spectateGame.language && spectateGame.index !== undefined) { - setOneCurrentCode(spectateGame.code, spectateGame.index); - setOneCurrentLanguage(spectateGame.language as Language, spectateGame.index); + } else if (spectateGame?.code && spectateGame.language && spectateGame.problemIndex !== undefined) { + setOneCurrentCode(spectateGame.code, spectateGame.problemIndex); + setOneCurrentLanguage(spectateGame.language as Language, spectateGame.problemIndex); } }, [spectateGame, setOneCurrentCode, setOneCurrentLanguage]); @@ -474,7 +465,7 @@ function PlayerGameView(props: PlayerGameViewProps) { Spectating: {' '} {spectateGame?.user.nickname} - {currentProblemIndex === spectateGame?.index ? ' (live)' : null} + {currentProblemIndex === spectateGame?.problemIndex ? ' (live)' : null} diff --git a/frontend/src/components/game/SpectatorGameView.tsx b/frontend/src/components/game/SpectatorGameView.tsx index 8ff561c9..b838a183 100644 --- a/frontend/src/components/game/SpectatorGameView.tsx +++ b/frontend/src/components/game/SpectatorGameView.tsx @@ -39,8 +39,6 @@ function SpectatorGameView() { const subscribePlayerCallback = (result: Message) => { if (!JSON.parse(result.body).newSpectator) { const updatedSpectateGame: SpectateGame = JSON.parse(result.body); - console.log('in spectator page'); - console.log(updatedSpectateGame); setSpectateGame(updatedSpectateGame); } }; From 61553375ce25806afb09fbc3c9bc94ad6b1d0bbb Mon Sep 17 00:00:00 2001 From: Alan Bi Date: Wed, 7 Jul 2021 20:56:42 -0700 Subject: [PATCH 16/19] Fix spectate selection issue --- frontend/src/components/game/Editor.tsx | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/frontend/src/components/game/Editor.tsx b/frontend/src/components/game/Editor.tsx index f9982a6e..abce38e5 100644 --- a/frontend/src/components/game/Editor.tsx +++ b/frontend/src/components/game/Editor.tsx @@ -140,6 +140,13 @@ function ResizableMonacoEditor(props: EditorProps) { } }; + // When spectating, clear any extraneous selections that occur when code changes + useEffect(() => { + if (codeEditor) { + codeEditor.setSelection(new monaco.Selection(0, 0, 0, 0)); + } + }, [liveCode]); + const handleLanguageChange = (language: Language) => { // Save the code for this language if (codeMap != null && codeEditor != null) { From b1201c3115482c0d06ec093ef490a94eedf75bd0 Mon Sep 17 00:00:00 2001 From: Alan Bi Date: Thu, 8 Jul 2021 20:34:46 -0700 Subject: [PATCH 17/19] Add TODO for potential bug --- frontend/src/components/game/PlayerGameView.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/components/game/PlayerGameView.tsx b/frontend/src/components/game/PlayerGameView.tsx index 49169238..d86f544b 100644 --- a/frontend/src/components/game/PlayerGameView.tsx +++ b/frontend/src/components/game/PlayerGameView.tsx @@ -385,7 +385,7 @@ function PlayerGameView(props: PlayerGameViewProps) { // Set the 'test' submission type to correctly display result. // eslint-disable-next-line no-param-reassign res.submissionType = SubmissionType.Test; - currentSubmission = res; // note: this seems a bit improper (fine as long as it works ig) + currentSubmission = res; // TODO: this may be improper and cause potential bugs }) .catch((err) => { setLoading(false); From 057806437b84cf72c37672acfc40e98c00b02a8e Mon Sep 17 00:00:00 2001 From: Alan Bi Date: Thu, 8 Jul 2021 21:02:22 -0700 Subject: [PATCH 18/19] Fix dependency error --- frontend/src/components/game/Editor.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/components/game/Editor.tsx b/frontend/src/components/game/Editor.tsx index abce38e5..a6e6de82 100644 --- a/frontend/src/components/game/Editor.tsx +++ b/frontend/src/components/game/Editor.tsx @@ -145,7 +145,7 @@ function ResizableMonacoEditor(props: EditorProps) { if (codeEditor) { codeEditor.setSelection(new monaco.Selection(0, 0, 0, 0)); } - }, [liveCode]); + }, [codeEditor, liveCode]); const handleLanguageChange = (language: Language) => { // Save the code for this language From c4fc0fd46733c2213ca3193ca8e4787dd61cacda Mon Sep 17 00:00:00 2001 From: Alan Bi Date: Thu, 8 Jul 2021 22:18:07 -0700 Subject: [PATCH 19/19] Fix run code console bug --- frontend/src/components/game/PlayerGameView.tsx | 12 ++++++++---- frontend/src/util/Hook.tsx | 16 ---------------- 2 files changed, 8 insertions(+), 20 deletions(-) diff --git a/frontend/src/components/game/PlayerGameView.tsx b/frontend/src/components/game/PlayerGameView.tsx index d86f544b..0fe40b7a 100644 --- a/frontend/src/components/game/PlayerGameView.tsx +++ b/frontend/src/components/game/PlayerGameView.tsx @@ -36,7 +36,7 @@ import Language from '../../api/Language'; import { routes, send, subscribe } from '../../api/Socket'; import { User } from '../../api/User'; import ProblemPanel from './ProblemPanel'; -import { useAppSelector, useBestSubmission, useGetSubmission } from '../../util/Hook'; +import { useAppSelector, useBestSubmission } from '../../util/Hook'; import { getScore, getSubmissionCount, getSubmissionTime, getSubmission, } from '../../util/Utility'; @@ -147,7 +147,7 @@ function PlayerGameView(props: PlayerGameViewProps) { const [languageList, setLanguageList] = useState([Language.Java]); const [codeList, setCodeList] = useState(['']); const [currentProblemIndex, setCurrentProblemIndex] = useState(defaultIndex || 0); - let currentSubmission = useGetSubmission(currentProblemIndex, submissions); + const [currentSubmission, setCurrentSubmission] = useState(null); const [loading, setLoading] = useState(false); const [error, setError] = useState(gameError); @@ -354,7 +354,8 @@ function PlayerGameView(props: PlayerGameViewProps) { if (spectateGame?.codeList && spectateGame.languageList) { setCodeList(spectateGame.codeList); setLanguageList(spectateGame.languageList); - } else if (spectateGame?.code && spectateGame.language && spectateGame.problemIndex !== undefined) { + } else if (spectateGame?.code && spectateGame.language + && spectateGame.problemIndex !== undefined) { setOneCurrentCode(spectateGame.code, spectateGame.problemIndex); setOneCurrentLanguage(spectateGame.language as Language, spectateGame.problemIndex); } @@ -385,7 +386,7 @@ function PlayerGameView(props: PlayerGameViewProps) { // Set the 'test' submission type to correctly display result. // eslint-disable-next-line no-param-reassign res.submissionType = SubmissionType.Test; - currentSubmission = res; // TODO: this may be improper and cause potential bugs + setCurrentSubmission(res); }) .catch((err) => { setLoading(false); @@ -412,6 +413,7 @@ function PlayerGameView(props: PlayerGameViewProps) { // eslint-disable-next-line no-param-reassign res.submissionType = SubmissionType.Submit; setSubmissions(submissions.concat([res])); + setCurrentSubmission(res); }) .catch((err) => { setLoading(false); @@ -424,6 +426,7 @@ function PlayerGameView(props: PlayerGameViewProps) { if (problems && next < problems.length) { setCurrentProblemIndex(next); + setCurrentSubmission(getSubmission(next, submissions)); } }; @@ -432,6 +435,7 @@ function PlayerGameView(props: PlayerGameViewProps) { if (prev >= 0) { setCurrentProblemIndex(prev); + setCurrentSubmission(getSubmission(prev, submissions)); } }; diff --git a/frontend/src/util/Hook.tsx b/frontend/src/util/Hook.tsx index 4134a5a1..da20e93e 100644 --- a/frontend/src/util/Hook.tsx +++ b/frontend/src/util/Hook.tsx @@ -53,22 +53,6 @@ export const useGetSubmissionTime = (player?: Player) => { return time; }; -// Returns the most recent submission made for problem of index curr. -export const useGetSubmission = (curr: number, playerSubmissions: Submission[]) => { - const [submission, setSubmission] = useState(null); - - useEffect(() => { - for (let i = playerSubmissions.length - 1; i >= 0; i -= 1) { - if (playerSubmissions[i].problemIndex === curr) { - setSubmission(playerSubmissions[i]); - i = -1; - } - } - }, [curr, playerSubmissions]); - - return submission; -}; - export const useProblemEditable = (user: FirebaseUserType | null, problem: Problem | null) => { const [editable, setEditable] = useState(false);