Skip to content
Merged
Show file tree
Hide file tree
Changes from 15 commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion frontend/src/api/Game.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,12 @@ 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,
submissions: Submission[],
solved: boolean,
solved: boolean[],
color: Color,
};

Expand Down Expand Up @@ -82,8 +83,11 @@ export type Submission = {
export type SpectateGame = {
user: User,
problem: Problem,
index: number,
code: string,
language: string,
codeList?: string[],
languageList?: Language[],
};

const basePath = '/api/v1';
Expand Down
22 changes: 6 additions & 16 deletions frontend/src/components/card/LeaderboardCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -76,22 +76,12 @@ 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 = () => {
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) {
Expand All @@ -115,15 +105,15 @@ function LeaderboardCard(props: LeaderboardCardProps) {
nickname={player.user.nickname}
active={Boolean(player.user.sessionId)}
/>
<LowMarginText bold={player.solved}>{`${place}.${getScorePercentage()}`}</LowMarginText>
<LowMarginText bold={getAllSolved()}>{`${place}. ${getScoreDisplay()}`}</LowMarginText>

{showHover ? (
<HoverBar>
<CenteredScrollableContent>
<SmallText bold>{player.user.nickname}</SmallText>
</CenteredScrollableContent>
<CenteredScrollableContent>
<SmallText>{`Score: ${getScoreDisplay()}`}</SmallText>
<SmallText>{`Solved: ${getScoreDisplay()}`}</SmallText>
</CenteredScrollableContent>
<CenteredScrollableContent>
<SmallText>{`Last: ${getSubmissionTime()}`}</SmallText>
Expand Down
25 changes: 25 additions & 0 deletions frontend/src/components/core/Button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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)<ProblemNavButtonProps>`
font-size: ${({ theme }) => theme.fontSize.default};
color: ${({ theme, disabled }) => (disabled ? theme.colors.lightgray : theme.colors.gray)};
background-color: ${({ theme }) => 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;
}
`;
109 changes: 72 additions & 37 deletions frontend/src/components/game/PlayerGameView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -113,24 +113,28 @@ type StateRefType = {
currentCode: string,
currentLanguage: string,
currentIndex: number,
codeList: string[],
languageList: Language[],
}

/**
* The spectateGame and spectatorUnsubscribePlayer parameters are only used when
* 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);
Expand All @@ -141,7 +145,7 @@ function PlayerGameView(props: PlayerGameViewProps) {
const [languageList, setLanguageList] = useState<Language[]>([Language.Java]);
const [codeList, setCodeList] = useState<string[]>(['']);
const [currentSubmission, setCurrentSubmission] = useState<Submission | null>(null);
const [currentProblemIndex, setCurrentProblemIndex] = useState<number>(0);
const [currentProblemIndex, setCurrentProblemIndex] = useState<number>(defaultIndex || 0);

const [loading, setLoading] = useState<boolean>(false);
const [error, setError] = useState<string>(gameError);
Expand All @@ -151,9 +155,21 @@ function PlayerGameView(props: PlayerGameViewProps) {
// Variable to hold whether the user is subscribed to their own player socket.
const [playerSocket, setPlayerSocket] = useState<Subscription | null>(null);

// References necessary for the spectator subscription callback.
const stateRef = useRef<StateRefType>();
stateRef.current = {
game,
currentUser,
currentCode: codeList[currentProblemIndex],
currentLanguage: languageList[currentProblemIndex],
currentIndex: currentProblemIndex,
codeList,
languageList,
};

// Variables to hold the player stats when spectating.
const [spectatedPlayer, setSpectatedPlayer] = useState<Player | null>(null);
const bestSubmission = useBestSubmission(spectatedPlayer);
const bestSubmission = useBestSubmission(spectatedPlayer, stateRef.current.currentIndex);

useEffect(() => setProblems(game?.problems || []), [game]);

Expand All @@ -168,23 +184,29 @@ function PlayerGameView(props: PlayerGameViewProps) {
const getCurrentLanguage = useCallback(() => languageList[currentProblemIndex],
[languageList, currentProblemIndex]);

const setOneCurrentLanguage = (newLanguage: Language) => {
setLanguageList(languageList.map((current, index) => {
if (index === 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) => {
setCodeList(codeList.map((current, index) => {
if (index === 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[]) => {
Expand All @@ -197,16 +219,6 @@ function PlayerGameView(props: PlayerGameViewProps) {
return null;
};

// References necessary for the spectator subscription callback.
const stateRef = useRef<StateRefType>();
stateRef.current = {
game,
currentUser,
currentCode: codeList[currentProblemIndex],
currentLanguage: languageList[currentProblemIndex],
currentIndex: currentProblemIndex,
};

const setDefaultCodeFromProblems = useCallback((problemsParam: Problem[],
playerSubmissions: Submission[]) => {
setSubmissions(playerSubmissions);
Expand Down Expand Up @@ -255,14 +267,22 @@ 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 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,
codeList: sendFullLists ? currentCodeList : undefined,
languageList: sendFullLists ? currentLanguageList : undefined,
};
const spectatorViewBody: string = JSON.stringify(body);

send(
routes(gameParam.room.roomId, currentUserParam.userId).subscribe_player,
{},
Expand All @@ -274,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.
Expand All @@ -284,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);
}
};

Expand Down Expand Up @@ -323,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) => {
Expand All @@ -343,6 +365,19 @@ function PlayerGameView(props: PlayerGameViewProps) {
}, [game, currentUser, defaultCodeList, setDefaultCodeFromProblems,
subscribePlayer, playerSocket, getSpectatedPlayer]);

// 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);
}
}, [spectateGame, setOneCurrentCode, setOneCurrentLanguage]);

// Creates Event when splitter bar is dragged
const onSecondaryPanelSizeChange = () => {
const event = new Event('secondaryPanelSizeChange');
Expand Down Expand Up @@ -451,6 +486,7 @@ function PlayerGameView(props: PlayerGameViewProps) {
Spectating:
{' '}
<b>{spectateGame?.user.nickname}</b>
{currentProblemIndex === spectateGame?.index ? ' (live)' : null}
</GameHeaderText>
</GameHeaderContainerChild>
<GameHeaderContainerChild>
Expand All @@ -469,7 +505,7 @@ function PlayerGameView(props: PlayerGameViewProps) {
<NoMarginDefaultText>
<b>Submissions:</b>
{' '}
{getSubmissionCount(spectatedPlayer)}
{getSubmissionCount(spectatedPlayer, stateRef.current.currentIndex)}
</NoMarginDefaultText>
</GameHeaderStatsSubContainer>
</GameHeaderStatsContainer>
Expand All @@ -489,8 +525,7 @@ function PlayerGameView(props: PlayerGameViewProps) {
>
<ProblemPanel
problems={game?.problems || []}
index={!spectateGame ? currentProblemIndex : game?.problems
.findIndex((p) => p.problemId === spectateGame.problem.problemId) || 0}
index={currentProblemIndex}
onNext={currentProblemIndex < problems.length - 1 ? nextProblem : null}
onPrev={currentProblemIndex > 0 ? previousProblem : null}
/>
Expand Down Expand Up @@ -530,12 +565,12 @@ function PlayerGameView(props: PlayerGameViewProps) {
<Editor
onLanguageChange={null}
onCodeChange={null}
defaultLanguage={spectateGame?.language as Language}
getCurrentLanguage={() => spectateGame?.language as Language}
defaultLanguage={getCurrentLanguage()}
getCurrentLanguage={getCurrentLanguage}
defaultCodeMap={null}
currentProblem={currentProblemIndex}
defaultCode={spectateGame?.code}
liveCode={spectateGame?.code}
defaultCode={null}
liveCode={codeList[currentProblemIndex]}
/>
</NoPaddingPanel>
)
Expand Down
33 changes: 3 additions & 30 deletions frontend/src/components/game/ProblemPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,9 @@ 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, FlexLeft, FlexRight, Panel,
} from '../core/Container';
import { CenteredContainer, Panel } from '../core/Container';
import { Problem } from '../../api/Problem';
import { NextIcon, PrevIcon } from '../core/Icon';

Expand Down Expand Up @@ -52,32 +50,7 @@ const ProblemNavContainer = styled.div`
`;

const ProblemCountText = styled(SmallText)`
color: gray;
`;

type ProblemNavButtonProps = {
disabled: boolean,
};

const ProblemNavButton = styled(DefaultButton)<ProblemNavButtonProps>`
font-size: ${({ theme }) => theme.fontSize.default};
color: ${({ theme, disabled }) => (disabled ? theme.colors.lightgray : theme.colors.gray)};
background-color: ${({ theme }) => 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;
}
color: ${({ theme }) => theme.colors.gray};
`;

type ProblemPanelProps = {
Expand Down
Loading