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
13 changes: 13 additions & 0 deletions client/src/components/PlayerStatusBar.css
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,19 @@
display: block;
}

.session-wins-badge {
display: inline-block;
font-size: 0.7rem;
font-weight: 700;
color: #FFD700;
background: rgba(245, 128, 37, 0.15);
border: 1px solid rgba(245, 128, 37, 0.3);
border-radius: 10px;
padding: 0.05rem 0.45rem;
line-height: 1.3;
white-space: nowrap;
}

.player-name {
font-weight: 600;
color: var(--text-color);
Expand Down
8 changes: 7 additions & 1 deletion client/src/components/PlayerStatusBar.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@ function PlayerStatusBar({
countdownActive = false,
waitingForMinimumPlayers = false,
readinessSummary = null,
readinessDetail = null
readinessDetail = null,
sessionWins = null
}) {
const [enlargedAvatar, setEnlargedAvatar] = useState(null);
const { authenticated, user } = useAuth();
Expand Down Expand Up @@ -232,6 +233,11 @@ function PlayerStatusBar({
/>
</div>
<div className="player-text">
{sessionWins && sessionWins[player.netid] > 0 && (
<span className="session-wins-badge">
{sessionWins[player.netid]} {sessionWins[player.netid] === 1 ? 'win' : 'wins'}
</span>
)}
<span className="player-name">{player.netid}</span>
{/* Determine the title to display */}
{(() => {
Expand Down
13 changes: 13 additions & 0 deletions client/src/components/Results.css
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,19 @@
align-items: flex-start;
}

.results-wins {
display: inline-block;
font-size: 0.75rem;
font-weight: 700;
color: #FFD700;
background: rgba(245, 128, 37, 0.15);
border: 1px solid rgba(245, 128, 37, 0.3);
border-radius: 10px;
padding: 0.1rem 0.55rem;
margin-bottom: 0.35rem;
white-space: nowrap;
}

.winner-header {
display: flex;
align-items: center;
Expand Down
11 changes: 11 additions & 0 deletions client/src/components/Results.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import ProfileModal from './ProfileModal.jsx';
function Results({ onShowLeaderboard }) {
const navigate = useNavigate();
const { raceState, typingState, resetRace, joinPublicRace, playAgain } = useRace();
const sessionWins = raceState.sessionWins || {};
const { isRunning, endTutorial } = useTutorial();
const { user } = useAuth();
// State for profile modal
Expand Down Expand Up @@ -251,6 +252,11 @@ function Results({ onShowLeaderboard }) {
/>
</div>
<div className="winner-details">
{raceState.type === 'private' && sessionWins[winner.netid] > 0 && (
<div className="session-wins-badge results-wins">
{sessionWins[winner.netid]} {sessionWins[winner.netid] === 1 ? 'win' : 'wins'}
</div>
)}
<div className="winner-header">
<div className="winner-trophy"><i className="bi bi-trophy"></i></div>
<div className="winner-netid">{winner.netid}</div>
Expand Down Expand Up @@ -302,6 +308,11 @@ function Results({ onShowLeaderboard }) {
/>
</div>
<div className="result-text">
{raceState.type === 'private' && sessionWins[result.netid] > 0 && (
<span className="session-wins-badge results-wins">
{sessionWins[result.netid]} {sessionWins[result.netid] === 1 ? 'win' : 'wins'}
</span>
)}
<div className="result-netid">{result.netid}</div>
{(() => {
const titlesList = resultTitlesMap[result.netid] || [];
Expand Down
38 changes: 31 additions & 7 deletions client/src/context/RaceContext.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,8 @@ export const RaceProvider = ({ children }) => {
testDuration: 15,
// Add other potential settings here
},
countdown: null // Track countdown seconds
countdown: null, // Track countdown seconds
sessionWins: {} // Win tally per player across play-again sessions { netid: count }
});

// Explicit state for TestConfigurator to avoid passing setRaceState
Expand Down Expand Up @@ -196,6 +197,14 @@ export const RaceProvider = ({ children }) => {
redirectToHome: false
});

const clearInactivityWarning = useCallback(() => {
setInactivityState(prev => (
prev.warning
? { ...prev, warning: false, warningMessage: '' }
: prev
));
}, []);

// Update session storage when inactivity state changes
useEffect(() => {
saveInactivityState(inactivityState);
Expand Down Expand Up @@ -286,11 +295,20 @@ export const RaceProvider = ({ children }) => {
hostNetId: data.hostNetId || null, // Explicitly store hostNetId
snippet: data.snippet ? { ...data.snippet, text: sanitizeSnippetText(data.snippet.text) } : null,
settings: data.settings || prev.settings, // Store settings from server
players: data.players || []
players: data.players || [],
sessionWins: data.sessionWins || {}
}));
};

const handlePlayersUpdate = (data) => {
const currentUserReady = Boolean(
user?.netid && data.players?.some(player => player.netid === user.netid && player.ready)
);

if (currentUserReady) {
clearInactivityWarning();
}

setRaceState(prev => {
// For quick-match public races, if the race is already in progress, we want to keep
// any players previously marked as disconnected even if they are no longer in the
Expand Down Expand Up @@ -358,6 +376,8 @@ export const RaceProvider = ({ children }) => {
});

if (shouldResetTyping) {
clearInactivityWarning();

// Reset typing state
setTypingState({
input: '',
Expand Down Expand Up @@ -419,7 +439,8 @@ export const RaceProvider = ({ children }) => {
return {
...prev,
inProgress: false,
completed: true
completed: true,
sessionWins: data.sessionWins || {}
};
});
};
Expand Down Expand Up @@ -600,7 +621,8 @@ export const RaceProvider = ({ children }) => {
},
snippetFilters: data.settings?.snippetFilters || { difficulty: 'all', type: 'all', department: 'all' },
settings: data.settings || { testMode: 'snippet', testDuration: 15 },
countdown: null
countdown: null,
sessionWins: data.sessionWins || {}
});
};

Expand Down Expand Up @@ -653,7 +675,7 @@ export const RaceProvider = ({ children }) => {
socket.off('snippetNotFound', handleSnippetNotFound); // Cleanup snippet not found listener
};
// Add raceState.snippet?.id to dependency array to reset typing state on snippet change
}, [socket, connected, raceState.type, raceState.manuallyStarted, raceState.snippet?.id, resetAnticheatState]);
}, [socket, connected, raceState.type, raceState.manuallyStarted, raceState.snippet?.id, resetAnticheatState, clearInactivityWarning, user?.netid]);

// Methods for race actions
const joinPracticeMode = () => {
Expand Down Expand Up @@ -696,6 +718,7 @@ export const RaceProvider = ({ children }) => {

const setPlayerReady = () => {
if (!socket || !connected) return;
clearInactivityWarning();
// console.log('Setting player ready...');
socket.emit('player:ready');
};
Expand Down Expand Up @@ -1075,7 +1098,8 @@ export const RaceProvider = ({ children }) => {
testMode: 'snippet',
testDuration: 15,
},
countdown: null
countdown: null,
sessionWins: {}
});

setTypingState({
Expand All @@ -1088,7 +1112,7 @@ export const RaceProvider = ({ children }) => {
accuracy: 0,
lockedPosition: 0
});

// Clear race state from session storage
sessionStorage.removeItem('raceState');
};
Expand Down
13 changes: 13 additions & 0 deletions client/src/pages/Lobby.css
Original file line number Diff line number Diff line change
Expand Up @@ -443,6 +443,19 @@
gap: 1rem;
}

.lobby-wins {
display: inline-block;
font-size: 0.7rem;
font-weight: 700;
color: #FFD700;
background: rgba(245, 128, 37, 0.15);
border: 1px solid rgba(245, 128, 37, 0.3);
border-radius: 10px;
padding: 0.1rem 0.5rem;
margin: 0.4rem auto 0;
white-space: nowrap;
}

.lobby-page .player-card {
background: linear-gradient(135deg, var(--container-color) 0%, rgba(245, 128, 37, 0.05) 100%);
border-radius: 10px;
Expand Down
5 changes: 5 additions & 0 deletions client/src/pages/Lobby.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -310,6 +310,11 @@ function Lobby() {
<div className="player-grid">
{raceState.players?.map(player => (
<div key={player.netid} className="player-card">
{raceState.sessionWins?.[player.netid] > 0 && (
<span className="session-wins-badge lobby-wins">
{raceState.sessionWins[player.netid]} {raceState.sessionWins[player.netid] === 1 ? 'win' : 'wins'}
</span>
)}
<ProfileWidget
// Pass user object including avg_wpm fetched from server
user={{ netid: player.netid, avatar_url: player.avatar_url, avg_wpm: player.avg_wpm }}
Expand Down
1 change: 1 addition & 0 deletions client/src/pages/Race.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,7 @@ function Race() {
players={players}
isRaceInProgress={raceState.inProgress}
currentUser={window.user}
sessionWins={raceState.type === 'private' ? raceState.sessionWins : null}
onReadyClick={setPlayerReady}
countdownActive={countdownActive}
waitingForMinimumPlayers={shouldShowLobbyStatus && waitingForMinimumPlayers}
Expand Down
Loading
Loading