Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
4 changes: 4 additions & 0 deletions frontend/src/api/Game.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -82,8 +83,11 @@ export type Submission = {
export type SpectateGame = {
user: User,
problem: Problem,
problemIndex: 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.filter((element) => !element).length === 0}>{`${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;
}
`;
15 changes: 14 additions & 1 deletion frontend/src/components/game/Editor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,19 @@ function ResizableMonacoEditor(props: EditorProps) {
});
};

const handleCodeChange = () => {
if (onCodeChange) {
onCodeChange(codeEditor?.getValue() || '');
}
};

// When spectating, clear any extraneous selections that occur when code changes
useEffect(() => {
if (codeEditor) {
codeEditor.setSelection(new monaco.Selection(0, 0, 0, 0));
}
}, [codeEditor, liveCode]);

const handleLanguageChange = (language: Language) => {
// Save the code for this language
if (codeMap != null && codeEditor != null) {
Expand Down Expand Up @@ -199,7 +212,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}
Expand Down
110 changes: 70 additions & 40 deletions frontend/src/components/game/PlayerGameView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -114,25 +114,29 @@ type StateRefType = {
currentUser: User | null,
currentCode: string,
currentLanguage: string,
currentProblemIndex: number,
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 @@ -142,8 +146,8 @@ function PlayerGameView(props: PlayerGameViewProps) {
const [problems, setProblems] = useState<Problem[]>([]);
const [languageList, setLanguageList] = useState<Language[]>([Language.Java]);
const [codeList, setCodeList] = useState<string[]>(['']);
const [currentProblemIndex, setCurrentProblemIndex] = useState<number>(0);
let currentSubmission = useGetSubmission(currentProblemIndex, submissions);
const [currentProblemIndex, setCurrentProblemIndex] = useState<number>(defaultIndex || 0);
const [currentSubmission, setCurrentSubmission] = useState<Submission | null>(null);

const [loading, setLoading] = useState<boolean>(false);
const [error, setError] = useState<string>(gameError);
Expand All @@ -153,9 +157,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 @@ -170,33 +186,23 @@ 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;
}));
};

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

const setDefaultCodeFromProblems = useCallback((problemsParam: Problem[],
playerSubmissions: Submission[]) => {
Expand Down Expand Up @@ -246,14 +252,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,
});
problemIndex: 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 @@ -265,7 +279,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 @@ -275,7 +289,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?.currentProblemIndex);
stateRef.current?.currentIndex, stateRef.current?.codeList,
stateRef.current?.languageList, true);
}
};

Expand Down Expand Up @@ -314,7 +329,7 @@ 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;

// If this user refreshed and has already submitted code, load and save their latest code
Expand All @@ -334,6 +349,18 @@ function PlayerGameView(props: PlayerGameViewProps) {
}, [game, currentUser, defaultCodeList, setDefaultCodeFromProblems,
subscribePlayer, playerSocket, getSpectatedPlayer]);

// When spectate game code changes, update the corresponding problem with that code
useEffect(() => {
if (spectateGame?.codeList && spectateGame.languageList) {
setCodeList(spectateGame.codeList);
setLanguageList(spectateGame.languageList);
} else if (spectateGame?.code && spectateGame.language
&& spectateGame.problemIndex !== undefined) {
setOneCurrentCode(spectateGame.code, spectateGame.problemIndex);
setOneCurrentLanguage(spectateGame.language as Language, spectateGame.problemIndex);
}
}, [spectateGame, setOneCurrentCode, setOneCurrentLanguage]);

// Creates Event when splitter bar is dragged
const onSecondaryPanelSizeChange = () => {
const event = new Event('secondaryPanelSizeChange');
Expand All @@ -359,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; // note: this seems a bit improper (fine as long as it works ig)
setCurrentSubmission(res);
})
.catch((err) => {
setLoading(false);
Expand All @@ -386,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);
Expand All @@ -398,6 +426,7 @@ function PlayerGameView(props: PlayerGameViewProps) {

if (problems && next < problems.length) {
setCurrentProblemIndex(next);
setCurrentSubmission(getSubmission(next, submissions));
}
};

Expand All @@ -406,6 +435,7 @@ function PlayerGameView(props: PlayerGameViewProps) {

if (prev >= 0) {
setCurrentProblemIndex(prev);
setCurrentSubmission(getSubmission(prev, submissions));
}
};

Expand Down Expand Up @@ -439,6 +469,7 @@ function PlayerGameView(props: PlayerGameViewProps) {
Spectating:
{' '}
<b>{spectateGame?.user.nickname}</b>
{currentProblemIndex === spectateGame?.problemIndex ? ' (live)' : null}
</GameHeaderText>
</GameHeaderContainerChild>
<GameHeaderContainerChild>
Expand All @@ -457,7 +488,7 @@ function PlayerGameView(props: PlayerGameViewProps) {
<NoMarginDefaultText>
<b>Submissions:</b>
{' '}
{getSubmissionCount(spectatedPlayer)}
{getSubmissionCount(spectatedPlayer, stateRef.current?.currentIndex)}
</NoMarginDefaultText>
</GameHeaderStatsSubContainer>
</GameHeaderStatsContainer>
Expand All @@ -477,8 +508,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 @@ -517,11 +547,11 @@ function PlayerGameView(props: PlayerGameViewProps) {
<Editor
onLanguageChange={null}
onCodeChange={null}
getCurrentLanguage={() => spectateGame?.language as Language || Language.Java}
getCurrentLanguage={getCurrentLanguage}
defaultCodeMap={null}
currentProblem={currentProblemIndex}
defaultCode={spectateGame?.code}
liveCode={spectateGame?.code}
defaultCode={null}
liveCode={codeList[currentProblemIndex]}
/>
</NoPaddingPanel>
)
Expand Down
Loading