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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 9 additions & 2 deletions client/src/components/Results.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import ProfileModal from './ProfileModal.jsx';

function Results({ onShowLeaderboard }) {
const navigate = useNavigate();
const { raceState, typingState, resetRace, joinPublicRace } = useRace();
const { raceState, typingState, resetRace, joinPublicRace, playAgain } = useRace();
const { isRunning, endTutorial } = useTutorial();
const { user } = useAuth();
// State for profile modal
Expand Down Expand Up @@ -353,13 +353,20 @@ function Results({ onShowLeaderboard }) {

{raceState.type === 'practice' ? renderPracticeResults() : renderRaceResults()}

{/* Play Again button for private match host */}
{raceState.type === 'private' && raceState.completed && user?.netid === raceState.hostNetId && (
<button className="back-btn" onClick={playAgain}>
Play Again
</button>
)}

{/* Queue Next Race button for quick matches */}
{raceState.type === 'public' && (
<button className="back-btn" onClick={handleQueueNext}>
Queue Another Race
</button>
)}

<button className="back-btn back-to-menu-btn" onClick={handleBack}>
Back to Menu
</button>
Expand Down
50 changes: 50 additions & 0 deletions client/src/context/RaceContext.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -567,6 +567,43 @@ export const RaceProvider = ({ children }) => {
resetAnticheatState();
};

// Handle play again – host created a new lobby and all players are migrated
const handleLobbyPlayAgain = (data) => {
console.log('Play again – joining new lobby:', data.code);
resetAnticheatState();
setInactivityState({
warning: false,
warningMessage: '',
kicked: false,
kickMessage: '',
redirectToHome: false
});
setTypingState({
input: '', position: 0, correctChars: 0, errors: 0,
completed: false, wpm: 0, accuracy: 0, lockedPosition: 0
});
setRaceState({
code: data.code,
type: data.type || 'private',
lobbyId: data.lobbyId,
hostNetId: data.hostNetId,
snippet: data.snippet ? { ...data.snippet, text: sanitizeSnippetText(data.snippet.text) } : null,
players: data.players || [],
startTime: null,
inProgress: false,
completed: false,
results: [],
manuallyStarted: false,
timedTest: {
enabled: data.settings?.testMode === 'timed',
duration: data.settings?.testDuration || 15
},
snippetFilters: data.settings?.snippetFilters || { difficulty: 'all', type: 'all', department: 'all' },
settings: data.settings || { testMode: 'snippet', testDuration: 15 },
countdown: null
});
};

// Register event listeners
socket.on('race:joined', handleRaceJoined);
socket.on('race:playersUpdate', handlePlayersUpdate);
Expand All @@ -588,6 +625,7 @@ export const RaceProvider = ({ children }) => {
socket.on('race:playerLeft', handlePlayerLeft);
socket.on('anticheat:lock', handleAnticheatLock);
socket.on('anticheat:reset', handleAnticheatReset);
socket.on('lobby:playAgain', handleLobbyPlayAgain);

// Clean up on unmount
return () => {
Expand All @@ -611,6 +649,7 @@ export const RaceProvider = ({ children }) => {
socket.off('race:playerLeft', handlePlayerLeft);
socket.off('anticheat:lock', handleAnticheatLock);
socket.off('anticheat:reset', handleAnticheatReset);
socket.off('lobby:playAgain', handleLobbyPlayAgain);
socket.off('snippetNotFound', handleSnippetNotFound); // Cleanup snippet not found listener
};
// Add raceState.snippet?.id to dependency array to reset typing state on snippet change
Expand Down Expand Up @@ -1080,6 +1119,16 @@ export const RaceProvider = ({ children }) => {
});
};

const playAgain = () => {
if (!socket || !connected || !raceState.code || raceState.type !== 'private') return;
socket.emit('lobby:playAgain', { code: raceState.code }, (response) => {
if (!response.success) {
console.error('Failed to play again:', response.error);
}
// State update handled by lobby:playAgain listener
});
};

// joinPrivateLobby is declared earlier with useCallback to avoid TDZ

const kickPlayer = (targetNetId) => {
Expand Down Expand Up @@ -1192,6 +1241,7 @@ export const RaceProvider = ({ children }) => {
kickPlayer,
updateLobbySettings,
startPrivateRace,
playAgain,
setPlayerReady,
handleInput,
updateProgress,
Expand Down
24 changes: 19 additions & 5 deletions client/src/pages/Lobby.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -91,12 +91,26 @@ function Lobby() {
// --- TestConfigurator State ---
// Use settings directly from raceState now that context handles it
const currentSettings = raceState.settings || { testMode: 'snippet', testDuration: 15 };
// Local state for filters not yet in raceState.settings
const [snippetDifficulty, setSnippetDifficulty] = useState('');
const [snippetCategory, setSnippetCategory] = useState('');
const [snippetSubject, setSnippetSubject] = useState('');
const currentSnippetFilters = currentSettings.snippetFilters || {
difficulty: 'all',
type: 'all',
department: 'all'
};
const [snippetDifficulty, setSnippetDifficulty] = useState(currentSnippetFilters.difficulty || 'all');
const [snippetCategory, setSnippetCategory] = useState(currentSnippetFilters.type || 'all');
const [snippetSubject, setSnippetSubject] = useState(currentSnippetFilters.department || 'all');
// --- ---

useEffect(() => {
setSnippetDifficulty(currentSnippetFilters.difficulty || 'all');
setSnippetCategory(currentSnippetFilters.type || 'all');
setSnippetSubject(currentSnippetFilters.department || 'all');
}, [
currentSnippetFilters.department,
currentSnippetFilters.difficulty,
currentSnippetFilters.type
]);

// Handler for settings changes (only host can trigger)
// Generic handler factory that maps a particular setter (identified by a string
// rather than the actual function reference) to a callback that
Expand Down Expand Up @@ -273,7 +287,7 @@ function Lobby() {
loadNewSnippet={loadNewSnippet}
snippetError={snippetError}
isLobby
allowTimed={false}
allowTimed={currentSettings.testMode === 'timed'}
onShowLeaderboard={() => {}} // Disable leaderboard button in lobby
/>
) : (
Expand Down
13 changes: 13 additions & 0 deletions client/src/pages/Race.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,19 @@ function Race() {
navigate('/home', { replace: true });
}
}, [raceState.code, navigate]);

// After play again, navigate back to the new private lobby
useEffect(() => {
if (
raceState.type === 'private' &&
raceState.code &&
!raceState.inProgress &&
!raceState.completed &&
raceState.countdown === null
) {
navigate(`/lobby/${raceState.code}`, { replace: true });
}
}, [raceState.code, raceState.type, raceState.inProgress, raceState.completed, raceState.countdown, navigate]);

// Handle back button
const handleBack = () => {
Expand Down
Loading
Loading