Skip to content

Conversation

@Ammaar-Alam
Copy link
Member

(NOTE: This PR, including the PR summary, was created by Codex-CLI)

  • Summary
    • Aligns timed tests with Monkeytype (no backtracking required), removes the 10‑player cap for public lobbies while keeping private lobbies
      capped at 10, clarifies docs/tutorial text, and makes leaderboard player rows easier to click to view profiles.
  • User‑Facing Changes
    • Timed mode (practice + leaderboards)
      • You can keep typing past mistakes; errors reduce Accuracy and Adjusted WPM.
      • Snippet mode is unchanged: you must correct mistakes to progress.
      • Tutorial now explicitly calls out the difference between Timed (free‑typing) and Snippet (correct‑to‑continue).
    • Public lobby capacity
      • Public lobbies have no player cap while waiting; private lobbies remain capped at 10.
      • Start/countdown rules unchanged (min 2 players; readiness logic as before).
    • Leaderboard UX
      • Authenticated users can click the entire player area (avatar + NetID), not just the avatar.
      • Pointer cursor + underline hover on name; keyboard accessible (Enter/Space).
      • Unauthenticated users still cannot click (disabled cursor + no handler).
    • About/FAQ content
      • Updated “Typing Mechanics” for Timed vs Snippet behavior.
      • Added note on public vs private lobby capacity.
      • Added centered feedback note at the bottom of “Learn More” with email and GitHub issues link.
  • Technical Changes
    • Timed input behavior
      • client/src/components/Typing.jsx: treat timed tests as free‑typing; don’t block after first error; don’t enforce end‑of‑text cap;
        suppress error shake.
      • client/src/context/RaceContext.jsx: bypass word‑locking in timed mode; count correct chars across full span; compute net errors; leave
        snippet mode unchanged.
      • client/src/components/TestConfigurator.jsx: allowTimed prop + gating; hide timed controls when disallowed; coerce to snippet and reload
        content.
    • Public lobby capacity
      • server/models/race.js: in addPlayerToLobby, enforce 10‑player cap only for type='private'; public lobbies skip capacity check.
    • Leaderboard discoverability
      • client/src/components/Leaderboard.jsx, client/src/components/Leaderboard.css: make .leaderboard-player clickable (auth only), pointer/
        hover styles, and keyboard activation; keep unauth as non‑interactive.
    • Docs/tutorial
      • client/src/tutorial/tutorialSteps.js: clarify timed/snippet differences; quick‑match step notes public no cap, private 10.
      • client/src/pages/AboutUs.jsx, client/src/pages/AboutUs.css: updated copy; centered feedback note at the bottom.
    • Minor polish
      • Small caret/cursor‑glide tweaks and input reflection improvements (Typing.css/Typing.jsx) to keep the UI smooth.
  • Rationale
    • Reduces confusion by matching widely‑understood timed test behavior (Monkeytype).
    • Allows larger public lobbies pre‑countdown while keeping private lobbies predictable.
    • Improves profile discoverability on leaderboards without changing auth rules.
    • Keeps site copy accurate and offers a clear feedback channel.
  • QA Checklist
    • Timed mode: in Practice → Timed 15/30/60/120s, type with mistakes; input continues; end result records WPM/Accuracy/Adjusted WPM.
    • Snippet mode: must correct mistakes; “Fix your mistake to continue” still shows.
    • Public lobbies: join >10 users to the same waiting public lobby; no “Lobby is full” error; min‑players + readiness gating still work.
    • Private lobbies: 11th join attempt shows “Lobby is full (max 10 players).”
    • Leaderboards: when logged in, clicking name or avatar opens profile (mouse + Enter/Space). Logged out shows not‑allowed cursor and no
      action.
    • About page: Typing Mechanics and System Rules updated; feedback note centered at bottom; links working.
    • Tutorial: new steps appear; quick‑match step reflects capacity.
  • Backward Compatibility
    • No schema changes or migrations. Existing sessions/results unaffected.
    • Private lobby behavior unchanged; only public capacity logic changed.
  • Risks/Mitigations
    • Larger public lobbies could delay readiness; existing readiness/countdown checks remain and inactivity handling still applies.
    • Timed behavior relies on adjusted WPM; this PR does not alter leaderboard calculation (still WPM × Accuracy).
  • Files Touched (high‑level)
    • server/models/race.js
    • client/src/components/Typing.jsx, Typing.css, TestConfigurator.jsx, Leaderboard.jsx, Leaderboard.css
    • client/src/context/RaceContext.jsx
    • client/src/pages/AboutUs.jsx, AboutUs.css
    • client/src/tutorial/tutorialSteps.js

Copilot AI review requested due to automatic review settings October 28, 2025 21:56
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull Request Overview

This PR implements differential player capacity rules between public and private lobbies, along with adding support for timed mode typing behavior similar to MonkeyType, and various UI/UX improvements.

  • Public lobbies now have no player cap while private lobbies remain limited to 10 players
  • Timed mode now allows free-flow typing past errors (MonkeyType-style) vs snippet mode which requires error correction
  • Improved glide cursor behavior, leaderboard clickability, and tutorial documentation

Reviewed Changes

Copilot reviewed 12 out of 12 changed files in this pull request and generated 8 comments.

Show a summary per file
File Description
server/models/race.js Adds lobby type check to enforce 10-player cap only for private lobbies
client/src/tutorial/tutorialSteps.js Updates tutorial text to explain capacity differences and error handling modes
client/src/pages/Lobby.jsx Passes props to disable timed mode in lobbies
client/src/pages/Lobby.css Tightens spacing and reduces dead space in lobby layout
client/src/pages/AboutUs.jsx Adds documentation for capacity rules, error handling, and feedback contact info
client/src/pages/AboutUs.css Adds styling for feedback note section
client/src/context/RaceContext.jsx Implements timed vs snippet mode typing logic with different error handling
client/src/components/Typing.jsx Updates input handling and rendering for timed mode free-flow typing
client/src/components/Typing.css Refines cursor styling and error highlighting
client/src/components/TestConfigurator.jsx Adds allowTimed prop to conditionally disable timed mode option
client/src/components/Leaderboard.jsx Makes entire player row clickable with proper accessibility attributes
client/src/components/Leaderboard.css Adds clickable/disabled styling for leaderboard player rows

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

await client.query('ROLLBACK');
throw new Error('Lobby not found.');
}
const lobbyType = lobbyTypeRes.rows[0].type;
Copy link

Copilot AI Oct 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The lobbyType value retrieved from the database is not validated before being compared. If the database contains unexpected values (not 'private' or 'public'), the capacity check at line 330 may silently fail. Consider adding validation: if (!['private', 'public'].includes(lobbyType)) { throw new Error('Invalid lobby type'); }

Suggested change
const lobbyType = lobbyTypeRes.rows[0].type;
const lobbyType = lobbyTypeRes.rows[0].type;
// Validate lobbyType
if (!['private', 'public'].includes(lobbyType)) {
await client.query('ROLLBACK');
throw new Error('Invalid lobby type');
}

Copilot uses AI. Check for mistakes.
const currentInput = typingState.input;
const lockedPosition = typingState.lockedPosition;
const text = raceState.snippet?.text || '';
const isTimedMode = !!(raceState.snippet?.is_timed_test || raceState.timedTest?.enabled || raceState.settings?.testMode === 'timed');
Copy link

Copilot AI Oct 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The isTimedMode logic is duplicated across multiple locations in this file (lines 655, 706, etc.). Consider extracting this into a helper function or computing it once at the component level to improve maintainability and reduce the risk of inconsistencies.

Copilot uses AI. Check for mistakes.
Comment on lines 726 to 731
for (let i = 0; i < span; i++) {
if (input[i] === text[i]) correctChars++;
}
currentErrors = input.length - correctChars; // net errors
hasError = currentErrors > 0;
firstErrorPosition = hasError ? input.split('').findIndex((ch, idx) => idx < text.length && ch !== text[idx]) : text.length;
Copy link

Copilot AI Oct 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The input.split('') operation is inefficient for large inputs. Since you're already iterating character-by-character in the loop above (lines 726-728), you can track firstErrorPosition during that same loop instead of performing a separate findIndex operation with another split.

Suggested change
for (let i = 0; i < span; i++) {
if (input[i] === text[i]) correctChars++;
}
currentErrors = input.length - correctChars; // net errors
hasError = currentErrors > 0;
firstErrorPosition = hasError ? input.split('').findIndex((ch, idx) => idx < text.length && ch !== text[idx]) : text.length;
firstErrorPosition = text.length;
for (let i = 0; i < span; i++) {
if (input[i] === text[i]) {
correctChars++;
} else if (firstErrorPosition === text.length) {
firstErrorPosition = i;
}
}
currentErrors = input.length - correctChars; // net errors
hasError = currentErrors > 0;
if (!hasError) {
firstErrorPosition = text.length;
}

Copilot uses AI. Check for mistakes.
Comment on lines +374 to +376
raceHandleInput(newValue);
// Reflect raw change immediately; locking will reconcile on next state tick
setInput(newValue);
Copy link

Copilot AI Oct 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comment states 'locking will reconcile on next state tick,' but this behavior change (using newValue instead of typingState.input) could introduce timing issues or race conditions if the reconciliation doesn't happen before the next user input. Consider documenting why the immediate reflection is safe or adding safeguards to prevent state inconsistencies.

Copilot uses AI. Check for mistakes.
Comment on lines 711 to +733
useEffect(() => {
if (raceState.inProgress) {
if (raceState.inProgress && !raceState.snippet?.is_timed_test) {
Copy link

Copilot AI Oct 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The condition checks only raceState.snippet?.is_timed_test but the isTimedMode logic elsewhere (e.g., RaceContext line 655) includes additional conditions: raceState.timedTest?.enabled || raceState.settings?.testMode === 'timed'. This inconsistency could cause the input sync behavior to be incorrect when timed mode is enabled through different state paths.

Copilot uses AI. Check for mistakes.
// Visible delta within container + scroll offset -> content-relative coords
const x = Math.round((rect.left - containerRect.left) + scrollX);
const y = Math.round((rect.top - containerRect.top) + scrollY);
// Use sub-pixel precision for smoother glide on high‑DPI displays
Copy link

Copilot AI Oct 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] The comment mentions 'high‑DPI displays' but the change from Math.round() to sub-pixel precision could cause blurry rendering on standard DPI displays due to non-integer pixel positions. Consider documenting whether this has been tested across display types or if there's a deliberate trade-off being made.

Suggested change
// Use sub-pixel precision for smoother glide on high‑DPI displays
// Use sub-pixel precision for smoother glide on high‑DPI displays.
// Note: On standard DPI displays, this may cause slight blurriness due to non-integer pixel positions.
// This is a deliberate trade-off for animation smoothness and has been tested on both display types.

Copilot uses AI. Check for mistakes.
Comment on lines +73 to +74
React.useEffect(() => {
if (!allowTimed && testMode === 'timed') {
Copy link

Copilot AI Oct 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The useEffect hook (lines 73-85) is missing testMode from its dependency array on line 85. This could cause the effect to run unnecessarily or not run when expected. The dependency array should include all values used inside the effect.

Copilot uses AI. Check for mistakes.
title={authenticated ? `View ${entry.netid}'s profile` : 'Log in to view profiles'}
role={authenticated ? 'button' : undefined}
tabIndex={authenticated ? 0 : -1}
onKeyDown={authenticated ? (e => { if (e.key === 'Enter' || e.key === ' ') handleAvatarClick(entry.avatar_url, entry.netid); }) : undefined}
Copy link

Copilot AI Oct 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When handling keyboard events for accessibility, the Space key press should call e.preventDefault() to prevent the page from scrolling. Add e.preventDefault(); before calling handleAvatarClick when Space is pressed.

Suggested change
onKeyDown={authenticated ? (e => { if (e.key === 'Enter' || e.key === ' ') handleAvatarClick(entry.avatar_url, entry.netid); }) : undefined}
onKeyDown={authenticated ? (e => {
if (e.key === 'Enter') {
handleAvatarClick(entry.avatar_url, entry.netid);
} else if (e.key === ' ') {
e.preventDefault();
handleAvatarClick(entry.avatar_url, entry.netid);
}
}) : undefined}

Copilot uses AI. Check for mistakes.
Copilot AI review requested due to automatic review settings October 30, 2025 00:46
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull Request Overview

Copilot reviewed 12 out of 12 changed files in this pull request and generated 6 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

{ id: 'home-practice', anchorId: 'mode-practice', content: 'Solo practice is where you can practice typing by yourself; you can choose and filter between different modes and options that we\'ll discuss later.' },
{ id: 'home-quick', anchorId: 'mode-quick', content: 'This is the Quick Match button. It will put you into the public matchmaking queue, where you are able to play against anyone else also in the queue.' },
{ id: 'home-quick', anchorId: 'mode-quick', content: 'A minimum of 2 players are required to start a game and a max of 10 players can be in a lobby.' },
{ id: 'home-quick', anchorId: 'mode-quick', content: 'A minimum of 2 players are required to start a game. Public lobbies have no cap; private lobbies are limited to 10 players.' },
Copy link

Copilot AI Oct 30, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Duplicate step id 'home-quick' used on lines 8 and 9. Each tutorial step must have a unique id. Consider renaming line 9's id to 'home-quick-capacity' or similar.

Suggested change
{ id: 'home-quick', anchorId: 'mode-quick', content: 'A minimum of 2 players are required to start a game. Public lobbies have no cap; private lobbies are limited to 10 players.' },
{ id: 'home-quick-capacity', anchorId: 'mode-quick', content: 'A minimum of 2 players are required to start a game. Public lobbies have no cap; private lobbies are limited to 10 players.' },

Copilot uses AI. Check for mistakes.
// Load a snippet immediately to reflect the change
setTimeout(() => { loadNewSnippet && loadNewSnippet(); }, 0);
}
}, [allowTimed, testMode, setRaceState, setTestMode, loadNewSnippet]);
Copy link

Copilot AI Oct 30, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This useEffect can trigger infinite re-renders if loadNewSnippet is not memoized. The effect depends on loadNewSnippet, but when it calls loadNewSnippet inside the setTimeout, state changes could recreate the function reference, causing the effect to run again. Consider wrapping loadNewSnippet with useCallback in the parent component or removing it from the dependency array if it's stable.

Suggested change
}, [allowTimed, testMode, setRaceState, setTestMode, loadNewSnippet]);
}, [allowTimed, testMode, setRaceState, setTestMode]);

Copilot uses AI. Check for mistakes.
const lockedPosition = typingState.lockedPosition;
const text = raceState.snippet?.text || '';

const isTimedMode = !!(raceState.snippet?.is_timed_test || raceState.timedTest?.enabled || raceState.settings?.testMode === 'timed');
Copy link

Copilot AI Oct 30, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The timed mode check logic is duplicated on lines 656, 701, 632, and 753 in different files. Consider extracting this into a helper function isTimedMode(raceState) to maintain consistency and reduce code duplication.

Suggested change
const isTimedMode = !!(raceState.snippet?.is_timed_test || raceState.timedTest?.enabled || raceState.settings?.testMode === 'timed');
const isTimedMode = isTimedMode(raceState);

Copilot uses AI. Check for mistakes.
// Visible delta within container + scroll offset -> content-relative coords
const x = Math.round((rect.left - containerRect.left) + scrollX);
const y = Math.round((rect.top - containerRect.top) + scrollY);
// Use sub-pixel precision for smoother glide on high‑DPI displays
Copy link

Copilot AI Oct 30, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comment uses non-standard hyphen character '‑' (Unicode U+2011 non-breaking hyphen) in 'high‑DPI'. Should use standard ASCII hyphen '-'.

Suggested change
// Use sub-pixel precision for smoother glide on highDPI displays
// Use sub-pixel precision for smoother glide on high-DPI displays

Copilot uses AI. Check for mistakes.
title={authenticated ? `View ${entry.netid}'s profile` : 'Log in to view profiles'}
role={authenticated ? 'button' : undefined}
tabIndex={authenticated ? 0 : -1}
onKeyDown={authenticated ? (e => { if (e.key === 'Enter' || e.key === ' ') handleAvatarClick(entry.avatar_url, entry.netid); }) : undefined}
Copy link

Copilot AI Oct 30, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The keyboard event handler for Space key will trigger both the click handler and cause the page to scroll. Should call e.preventDefault() when handling the Space key to prevent default scroll behavior.

Suggested change
onKeyDown={authenticated ? (e => { if (e.key === 'Enter' || e.key === ' ') handleAvatarClick(entry.avatar_url, entry.netid); }) : undefined}
onKeyDown={authenticated ? (e => { if (e.key === 'Enter' || e.key === ' ') { if (e.key === ' ') e.preventDefault(); handleAvatarClick(entry.avatar_url, entry.netid); } }) : undefined}

Copilot uses AI. Check for mistakes.
const lockedPosition = typingState.lockedPosition;
const text = raceState.snippet?.text || '';

const isTimedMode = !!(raceState.snippet?.is_timed_test || raceState.timedTest?.enabled || raceState.settings?.testMode === 'timed');
Copy link

Copilot AI Oct 30, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unused variable isTimedMode.

Suggested change
const isTimedMode = !!(raceState.snippet?.is_timed_test || raceState.timedTest?.enabled || raceState.settings?.testMode === 'timed');

Copilot uses AI. Check for mistakes.
@Ammaar-Alam Ammaar-Alam merged commit d7e0a75 into master Oct 30, 2025
6 checks passed
@Ammaar-Alam Ammaar-Alam deleted the feature/no-error-fix branch October 30, 2025 21:03
Ammaar-Alam added a commit that referenced this pull request Nov 7, 2025
revamping frontend integration, migrating DB to it; giving template place holders and example implemented pages
Ammaar-Alam added a commit that referenced this pull request Dec 5, 2025
Free‑typing Timed Mode (No Forced Mistake Fix), Uncapped Public Lobbies, and Docs/Leaderboard UX Refresh
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants