From af43c2567c654c9c7ca35aecd6994bf65dcfdebe Mon Sep 17 00:00:00 2001 From: Ammaar Alam Date: Tue, 28 Oct 2025 14:03:02 -0400 Subject: [PATCH 1/7] making it so timed mode lobbies don't require users to fix their errors (just like monkeytype) --- client/src/components/TestConfigurator.jsx | 33 ++++-- client/src/components/Typing.jsx | 41 +++++--- client/src/context/RaceContext.jsx | 112 ++++++++++++--------- client/src/pages/Lobby.css | 12 +-- client/src/pages/Lobby.jsx | 2 + 5 files changed, 128 insertions(+), 72 deletions(-) diff --git a/client/src/components/TestConfigurator.jsx b/client/src/components/TestConfigurator.jsx index ad97f269..a7617ecd 100644 --- a/client/src/components/TestConfigurator.jsx +++ b/client/src/components/TestConfigurator.jsx @@ -35,6 +35,7 @@ function TestConfigurator({ onShowLeaderboard, isLobby = false, snippetError = null, + allowTimed = true, }) { const [subjects, setSubjects] = React.useState(['all']); @@ -68,6 +69,21 @@ function TestConfigurator({ fetchDifficulties(); }, [snippetCategory, snippetSubject]); + // If timed mode is disallowed (e.g., private lobbies), coerce to snippet mode + React.useEffect(() => { + if (!allowTimed && testMode === 'timed') { + // Update race state first so the server/client agree on mode + setRaceState(prev => ({ + ...prev, + timedTest: { ...prev.timedTest, enabled: false }, + settings: { ...(prev.settings||{}), testMode: 'snippet' } + })); + setTestMode('snippet'); + // Load a snippet immediately to reflect the change + setTimeout(() => { loadNewSnippet && loadNewSnippet(); }, 0); + } + }, [allowTimed, testMode, setRaceState, setTestMode, loadNewSnippet]); + // Fetch subjects on mount React.useEffect(() => { const fetchSubjects = async () => { @@ -272,7 +288,7 @@ function TestConfigurator({ {/* Mode Selection Group */}
{renderButton('snippet', testMode, setTestMode, 'Snippets', QuoteIcon)} - {renderButton('timed', testMode, setTestMode, 'Timed', ClockIcon)} + {allowTimed && renderButton('timed', testMode, setTestMode, 'Timed', ClockIcon)}
{/* Separator */} @@ -372,13 +388,15 @@ function TestConfigurator({ {/* Timed Mode Duration Wrapper */} - -
-
- {DURATIONS.map(duration => renderButton(duration, testDuration, setTestDuration, `${duration}s`))} + {allowTimed && ( + +
+
+ {DURATIONS.map(duration => renderButton(duration, testDuration, setTestDuration, `${duration}s`))} +
-
- + + )}
{/* End Conditional Options Container */} @@ -409,6 +427,7 @@ TestConfigurator.propTypes = { setRaceState: PropTypes.func.isRequired, loadNewSnippet: PropTypes.func, onShowLeaderboard: PropTypes.func.isRequired, + allowTimed: PropTypes.bool, }; export default TestConfigurator; diff --git a/client/src/components/Typing.jsx b/client/src/components/Typing.jsx index d4a74d52..c82d92a4 100644 --- a/client/src/components/Typing.jsx +++ b/client/src/components/Typing.jsx @@ -608,10 +608,11 @@ function Typing({ }; }, [raceState.inProgress, raceState.startTime, raceState.completed, typingState.correctChars]); // Include typingState.correctChars - // Handle typing input with word locking + // Handle typing input with word locking (snippet) or free-typing (timed) const handleComponentInput = (e) => { const newInput = e.target.value; const text = raceState.snippet?.text || ''; + const isTimedMode = !!(raceState.snippet?.is_timed_test); // Check if new character is correct const isMovingForward = newInput.length > input.length; @@ -660,9 +661,11 @@ function Typing({ wordCount: 15 // Request 1 more words }); } - // Prevent typing past the end of the snippet - if (newInput.length >= text.length + 1) { - return; + // In snippet mode, prevent typing past the end of the text. In timed mode, + // the server will append more words, so allow typing up to one char beyond + // (we early-request extra words above). + if (!isTimedMode) { + if (newInput.length >= text.length + 1) return; } // Check if there's a typing error (improved to check all characters) @@ -677,7 +680,7 @@ function Typing({ } // Only trigger shake and error message on a new error - if (hasError && !isShaking) { + if (!isTimedMode && hasError && !isShaking) { setIsShaking(true); setShowErrorMessage(true); @@ -725,6 +728,7 @@ function Typing({ if (!raceState.snippet) return null; const text = raceState.snippet.text; + const isTimedMode = !!(raceState.snippet?.is_timed_test); // Split text by words, maintaining spaces between them const renderNonBreakingText = () => { @@ -740,11 +744,17 @@ function Typing({ const charPos = charIndex + i; if (charPos < input.length) { - if (input[charPos] === text[charPos] && !hasEncounteredError) { - wordChars.push({word[i]}); + if (isTimedMode) { + // Timed mode: per-character correctness, no cascade after first error + const cls = input[charPos] === text[charPos] ? 'correct' : 'incorrect'; + wordChars.push({word[i]}); } else { - hasEncounteredError = true; - wordChars.push({word[i]}); + if (input[charPos] === text[charPos] && !hasEncounteredError) { + wordChars.push({word[i]}); + } else { + hasEncounteredError = true; + wordChars.push({word[i]}); + } } } else if (charPos === input.length) { wordChars.push({word[i]}); @@ -765,11 +775,16 @@ function Typing({ const spacePos = charIndex + word.length; if (spacePos < input.length) { - if (input[spacePos] === ' ' && !hasEncounteredError) { - components.push( ); + if (isTimedMode) { + const cls = input[spacePos] === ' ' ? 'correct' : 'incorrect'; + components.push( ); } else { - hasEncounteredError = true; - components.push( ); + if (input[spacePos] === ' ' && !hasEncounteredError) { + components.push( ); + } else { + hasEncounteredError = true; + components.push( ); + } } } else if (spacePos === input.length) { components.push( ); diff --git a/client/src/context/RaceContext.jsx b/client/src/context/RaceContext.jsx index e0786241..5dcbd63b 100644 --- a/client/src/context/RaceContext.jsx +++ b/client/src/context/RaceContext.jsx @@ -618,7 +618,7 @@ export const RaceProvider = ({ children }) => { }); }; - // Handle text input, enforce word locking + // Handle text input, enforce word locking (snippet mode) or free-flow (timed mode) const handleInput = (newInput) => { // Disable input handling for non-practice races before countdown begins if (raceState.type !== 'practice' && !raceState.inProgress && raceState.countdown === null) { @@ -652,6 +652,14 @@ export const RaceProvider = ({ children }) => { 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'); + + // In timed mode, do not enforce word locking or special backspace preservation – + // users can continue typing past mistakes (Monkeytype-style) + if (isTimedMode) { + updateProgress(newInput); + return; + } // Find the position of the first error in the current input let firstErrorPosition = text.length; // Default to end of text (no errors) @@ -695,6 +703,7 @@ export const RaceProvider = ({ children }) => { // Calculate current position in the snippet const text = raceState.snippet?.text || ''; + const isTimedMode = !!(raceState.snippet?.is_timed_test || raceState.timedTest?.enabled || raceState.settings?.testMode === 'timed'); let correctChars = 0; let currentErrors = 0; let hasError = false; @@ -709,60 +718,71 @@ export const RaceProvider = ({ children }) => { } } - // Count only the contiguous correct characters from the start of the snippet. - // As soon as an error is encountered we stop counting further characters. - if (!hasError) { - // No error – all typed characters are correct up to input length (bounded by snippet length) - correctChars = Math.min(input.length, text.length); + if (isTimedMode) { + // Timed mode (Monkeytype-style): allow continuing past mistakes. + // Count correct characters across the whole typed span, and treat + // mismatches as current (net) errors that can be reduced by fixing. + const span = Math.min(input.length, text.length); + 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; } else { - // Error present – only count chars before the first error index - correctChars = firstErrorPosition; - } - - // Count current error characters (everything typed after first error is considered incorrect) - if (hasError) { - currentErrors = input.length - firstErrorPosition; - } - - // Get previous total errors (persist even after fixes) - let totalErrors = typingState.errors; - - // If there's a new error (wasn't there in previous input) - const previousInput = typingState.input; - let isNewError = false; - - // Check if we have a new error that wasn't in the previous input - if (hasError && (previousInput.length <= firstErrorPosition || - (previousInput.length > firstErrorPosition && previousInput[firstErrorPosition] === text[firstErrorPosition]))) { - isNewError = true; + // Snippet mode: contiguous correctness until first error + if (!hasError) { + // No error – all typed characters are correct up to input length (bounded by snippet length) + correctChars = Math.min(input.length, text.length); + } else { + // Error present – only count chars before the first error index + correctChars = firstErrorPosition; + } + // Count current error characters (everything typed after first error is considered incorrect) + if (hasError) { + currentErrors = input.length - firstErrorPosition; + } } - // Only increment error count if this is a new error - if (isNewError) { - totalErrors += 1; + // Error model differs by mode + let totalErrors; + let accuracy; + let wpm; + if (isTimedMode) { + // Net errors at the moment (can decrease if user fixes) + totalErrors = currentErrors; + const typedChars = input.length; + const minutes = elapsedSeconds > 0 ? (elapsedSeconds / 60) : 0; + const netChars = Math.max(0, typedChars - totalErrors); // equals correctChars, but keep intent explicit + wpm = minutes > 0 ? (netChars / 5) / minutes : 0; // net WPM + accuracy = typedChars > 0 ? (correctChars / typedChars) * 100 : 100; + } else { + // Snippet mode retains cumulative error count + totalErrors = typingState.errors; + // If there's a new error (wasn't there in previous input) + const previousInput = typingState.input; + let isNewError = false; + if (hasError && (previousInput.length <= firstErrorPosition || + (previousInput.length > firstErrorPosition && previousInput[firstErrorPosition] === text[firstErrorPosition]))) { + isNewError = true; + } + if (isNewError) totalErrors += 1; + // Accuracy based on contiguous correctness + cumulative errors + const totalCharsForAccuracy = Math.min(firstErrorPosition, input.length) + totalErrors; + const accuracyCorrectChars = Math.min(firstErrorPosition, input.length); + const words = correctChars / 5; // contiguous + wpm = elapsedSeconds > 0 ? (words / elapsedSeconds) * 60 : 0; + accuracy = totalCharsForAccuracy > 0 ? (accuracyCorrectChars / totalCharsForAccuracy) * 100 : 100; } - // For accuracy calculation: - // - The denominator is (all correct characters typed + all errors made) - // - The numerator is all correct characters typed - const totalCharsForAccuracy = Math.min(firstErrorPosition, input.length) + totalErrors; - const accuracyCorrectChars = Math.min(firstErrorPosition, input.length); - - // Calculate WPM using only correctly typed characters (prevents inflation) - const words = correctChars / 5; // Standard definition: 1 word = 5 correct chars - const wpm = elapsedSeconds > 0 ? (words / elapsedSeconds) * 60 : 0; - - // Calculate accuracy using only valid characters (before first error) plus cumulative errors - const accuracy = totalCharsForAccuracy > 0 ? (accuracyCorrectChars / totalCharsForAccuracy) * 100 : 100; - // Check if all characters are typed correctly for completion const isCompleted = input.length === text.length && !hasError; - // Find the last completely correct word boundary before any error + // Find the last completely correct word boundary before any error (snippet mode only) let newLockedPosition = 0; // Only process word locking if there are characters - if (input.length > 0) { + if (!isTimedMode && input.length > 0) { let wordStart = 0; // Only lock text if there are no errors, or only lock up to the last word break before first error @@ -803,10 +823,10 @@ export const RaceProvider = ({ children }) => { position: input.length, // Use actual input length instead of correct chars correctChars, errors: totalErrors, - completed: isCompleted, // Only completed when all characters match exactly + completed: isTimedMode ? false : isCompleted, // timed ends by timer wpm, accuracy, - lockedPosition: newLockedPosition + lockedPosition: isTimedMode ? 0 : newLockedPosition }); // If the race is still in progress, update progress diff --git a/client/src/pages/Lobby.css b/client/src/pages/Lobby.css index 4c8f4114..7ad3a3e1 100644 --- a/client/src/pages/Lobby.css +++ b/client/src/pages/Lobby.css @@ -129,12 +129,12 @@ .lobby-left-column { flex: 1; min-width: 300px; - padding: 1.5rem; + padding: 1.2rem; background-color: var(--type-container-color); border-radius: 6px; border: 1px solid var(--border-color); position: relative; - min-height: 23vh; + min-height: 0; /* reduce dead space */ } .lobby-settings { @@ -146,11 +146,11 @@ } .lobby-settings h2 { - margin: 0 0 1.2rem 0; + margin: 0 0 0.8rem 0; color: var(--mode-text-color); font-size: 1.3rem; border-bottom: 1px solid var(--border-color); - padding-bottom: 0.6rem; + padding-bottom: 0.4rem; } /* --- TestConfigurator overrides for vertical lobby layout --- */ @@ -162,7 +162,7 @@ width: 100%; display: flex; flex-direction: column; - gap: 14px; + gap: 10px; /* tighten spacing */ overflow: visible; } @@ -199,7 +199,7 @@ /* Visible state when React adds .visible */ .lobby-settings .test-configurator .options-wrapper.visible { - margin-top: 25px; + margin-top: 12px; /* reduce vertical gap */ max-height: 600px; opacity: 1; pointer-events: auto; diff --git a/client/src/pages/Lobby.jsx b/client/src/pages/Lobby.jsx index 36c6cffc..aa9986cf 100644 --- a/client/src/pages/Lobby.jsx +++ b/client/src/pages/Lobby.jsx @@ -272,6 +272,8 @@ function Lobby() { setRaceState={setRaceState} loadNewSnippet={loadNewSnippet} snippetError={snippetError} + isLobby + allowTimed={false} onShowLeaderboard={() => {}} // Disable leaderboard button in lobby /> ) : ( From d244ac404d02b43bc08759c2706a77bcd9a035ad Mon Sep 17 00:00:00 2001 From: Ammaar Alam Date: Tue, 28 Oct 2025 17:17:34 -0400 Subject: [PATCH 2/7] removing 10 player cap from public lobbies --- client/src/components/Typing.css | 15 ++++++--- client/src/components/Typing.jsx | 55 ++++++++++++++++++++++---------- server/models/race.js | 19 +++++++++-- 3 files changed, 65 insertions(+), 24 deletions(-) diff --git a/client/src/components/Typing.css b/client/src/components/Typing.css index c52770c9..a825a594 100644 --- a/client/src/components/Typing.css +++ b/client/src/components/Typing.css @@ -229,6 +229,12 @@ text-decoration: underline wavy rgba(255, 65, 47, 0.55); } +/* Caret cursor: prefer subtle inline color only (no red block, no underline) */ +:root[data-cursor='caret'] .snippet-display .incorrect { + background-color: transparent !important; + text-decoration: none !important; +} + .shake-animation { animation: shake 0.4s cubic-bezier(.36,.07,.19,.97) both; } @@ -415,9 +421,9 @@ pointer-events: none; z-index: 0; /* behind the text */ transition: - transform var(--cursor-glide-duration, 90ms) cubic-bezier(0.24, 0.92, 0.35, 1), - width var(--cursor-glide-duration, 90ms) cubic-bezier(0.24, 0.92, 0.35, 1), - height var(--cursor-glide-duration, 90ms) cubic-bezier(0.24, 0.92, 0.35, 1); + transform var(--cursor-glide-duration, 90ms) ease-in-out, + width var(--cursor-glide-duration, 90ms) ease-in-out, + height var(--cursor-glide-duration, 90ms) ease-in-out; opacity: calc(var(--glide-cursor-enabled, 0)); /* 0 or 1 set by Settings */ border-radius: 2px; backface-visibility: hidden; @@ -430,8 +436,7 @@ .cursor-overlay.caret { background: transparent; - border-left: var(--caret-width, 3px) solid var(--caret-color); - box-shadow: 0 0 0.001px var(--caret-color); /* sub-pixel hinting */ + border-left: var(--caret-width, 2px) solid var(--caret-color); z-index: 2; /* above text so thin caret remains visible */ animation: caretBlink 1.2s ease-in-out infinite; } diff --git a/client/src/components/Typing.jsx b/client/src/components/Typing.jsx index c82d92a4..c74cff0f 100644 --- a/client/src/components/Typing.jsx +++ b/client/src/components/Typing.jsx @@ -314,7 +314,23 @@ function Typing({ const updateFromAttr = () => { const enabled = root.getAttribute('data-glide') === '1'; setGlideEnabled(enabled); - if (!enabled) initialCursorSetRef.current = false; + // Keep CSS variable in sync so the overlay hides/shows immediately + try { + root.style.setProperty('--glide-cursor-enabled', enabled ? '1' : '0'); + } catch (_) {} + if (!enabled) { + initialCursorSetRef.current = false; + // Ensure any existing overlay is completely hidden when glide is off + const overlay = cursorRef.current; + if (overlay) { + overlay.classList.remove('typing-active'); + overlay.classList.remove('caret'); + // leave 'block' state irrelevant; force hidden + overlay.style.opacity = '0'; + // move out of view to avoid accidental paints if other rules override opacity + overlay.style.transform = 'translate3d(-9999px, -9999px, 0)'; + } + } }; updateFromAttr(); const observer = new MutationObserver(() => updateFromAttr()); @@ -354,9 +370,10 @@ function Typing({ newValue = currentInput.substring(cursorPosition); } - // Let our existing word locking handle this modified input - raceHandleInput(newValue); - setInput(typingState.input); + // Let our existing word locking handle this modified input + raceHandleInput(newValue); + // Reflect raw change immediately; locking will reconcile on next state tick + setInput(newValue); } } }; @@ -698,9 +715,10 @@ function Typing({ // Use the handleInput function from RaceContext raceHandleInput(newInput); - // Update local input state to match what's in the typing state - // This ensures the displayed input matches the processed input after word locking - setInput(typingState.input); + // Immediately reflect the user's raw input to avoid dropping characters + // during rapid multi-key presses; word-locking corrections will be + // reconciled on the next tick via typingState.input sync. + setInput(newInput); } else { // Prevent typing past the end of the snippet if (raceState.snippet && newInput.length > raceState.snippet.text.length) { @@ -710,12 +728,12 @@ function Typing({ } } - // Sync input with typingState.input to ensure locked words can't be deleted + // Sync input with typingState.input to ensure locked words can't be deleted (snippet mode) useEffect(() => { - if (raceState.inProgress) { + if (raceState.inProgress && !raceState.snippet?.is_timed_test) { setInput(typingState.input); } - }, [typingState.input, raceState.inProgress]); + }, [typingState.input, raceState.inProgress, raceState.snippet?.is_timed_test]); // Prevent paste const handlePaste = (e) => { @@ -822,8 +840,9 @@ function Typing({ const scrollY = container.scrollTop || 0; // 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 + const x = (rect.left - containerRect.left) + scrollX; + const y = (rect.top - containerRect.top) + scrollY; // Determine caret vs block based on Settings-managed CSS var const useCaret = (cursorStyleRef.current === 'caret'); @@ -844,8 +863,10 @@ function Typing({ overlay.classList.remove('typing-active'); } - // Cursor-specific duration (caret snappier) - overlay.style.setProperty('--cursor-glide-duration', useCaret ? '85ms' : '110ms'); + // Cursor-specific duration + // - Caret: smooth glide + // - Block: short glide; snaps only for big jumps to avoid trailing + overlay.style.setProperty('--cursor-glide-duration', useCaret ? '90ms' : '65ms'); // First placement should not animate from origin if (!initialCursorSetRef.current) { @@ -857,9 +878,11 @@ function Typing({ overlay.style.transition = prev || ''; initialCursorSetRef.current = true; } else { - // For large vertical jumps (e.g., auto-scroll to next line), place instantly to avoid trailing + // For large vertical jumps (e.g., line wrap / scroll), place instantly. + // Horizontal moves (including spaces) should always glide when enabled. const prevY = overlay.__prevY ?? y; - const largeJump = Math.abs(prevY - y) > height * 1.2; + const dy = Math.abs(prevY - y); + const largeJump = dy > height * 1.2; if (largeJump) { const prev = overlay.style.transition; overlay.style.transition = 'none'; diff --git a/server/models/race.js b/server/models/race.js index 258b5ac1..8da2e3f8 100644 --- a/server/models/race.js +++ b/server/models/race.js @@ -300,12 +300,25 @@ const Race = { } }, - // Add a player to a lobby, checking capacity + // Add a player to a lobby + // - Private lobbies: enforce 10-player cap + // - Public lobbies: no cap async addPlayerToLobby(lobbyId, userId, isReady = false) { const client = await db.getClient(); // Use a client for transaction try { // Start try block immediately after acquisition await client.query('BEGIN'); + // Determine lobby type for capacity rules + const lobbyTypeRes = await client.query( + `SELECT type FROM lobbies WHERE id = $1`, + [lobbyId] + ); + if (lobbyTypeRes.rowCount === 0) { + await client.query('ROLLBACK'); + throw new Error('Lobby not found.'); + } + const lobbyType = lobbyTypeRes.rows[0].type; + // Check current player count const countResult = await client.query( `SELECT COUNT(*) FROM lobby_players WHERE lobby_id = $1`, @@ -313,8 +326,8 @@ const Race = { ); const playerCount = parseInt(countResult.rows[0].count, 10); - // Check if lobby is full (max 10 players) - if (playerCount >= 10) { + // Enforce capacity only for PRIVATE lobbies (max 10 players) + if (lobbyType === 'private' && playerCount >= 10) { // Rollback before throwing ensures transaction state is clean await client.query('ROLLBACK'); throw new Error('Lobby is full.'); // Throw specific error From 07b2021c091bd1a24b54bd5560bbf158537abbaa Mon Sep 17 00:00:00 2001 From: Ammaar Alam Date: Tue, 28 Oct 2025 17:48:48 -0400 Subject: [PATCH 3/7] updating about us w/ feedback steps; making entire profile on leaderboard clickable instead of just the pfp --- client/src/components/Leaderboard.css | 8 ++++- client/src/components/Leaderboard.jsx | 50 +++++++++++++++++++++++---- client/src/pages/AboutUs.css | 17 +++++++++ client/src/pages/AboutUs.jsx | 11 ++++-- client/src/tutorial/tutorialSteps.js | 6 ++-- 5 files changed, 80 insertions(+), 12 deletions(-) diff --git a/client/src/components/Leaderboard.css b/client/src/components/Leaderboard.css index 1a801fe6..d8f86187 100644 --- a/client/src/components/Leaderboard.css +++ b/client/src/components/Leaderboard.css @@ -107,6 +107,12 @@ h2 { min-width: 150px; /* Ensure player name area has space */ } +/* Make more of the entry clickable when authenticated */ +.leaderboard-player.clickable { cursor: pointer; } +.leaderboard-player.clickable:hover .leaderboard-netid { text-decoration: underline; color: #F58025; } +.leaderboard-player.disabled { cursor: not-allowed; } +.leaderboard-player.disabled .leaderboard-avatar { cursor: not-allowed; } + .leaderboard-avatar { width: 38px; height: 38px; @@ -556,4 +562,4 @@ h2 { flex-grow: 1; min-width: 0; } -} \ No newline at end of file +} diff --git a/client/src/components/Leaderboard.jsx b/client/src/components/Leaderboard.jsx index 8011a413..01f88bee 100644 --- a/client/src/components/Leaderboard.jsx +++ b/client/src/components/Leaderboard.jsx @@ -205,7 +205,40 @@ function Leaderboard({ defaultDuration = 15, defaultPeriod = 'alltime', layoutMo {error &&

Error: {error}

} {(hasLoadedOnce ? !showSpinner : !loading) && !error && (
- {leaderboard.length > 0 ? ( leaderboard.map((entry, index) => (
{index + 1}
handleAvatarClick(entry.avatar_url, entry.netid)} title={`View ${entry.netid}\'s avatar`}> {`${entry.netid} { e.target.onerror = null; e.target.src=defaultProfileImage; }} />
{entry.netid}
{parseFloat(entry.adjusted_wpm).toFixed(0)} WPM {parseFloat(entry.accuracy).toFixed(1)}% {period === 'daily' ? formatRelativeTime(entry.created_at) : new Date(entry.created_at).toLocaleDateString()}
)) ) : (

No results found for this leaderboard.

)} + {leaderboard.length > 0 ? ( + leaderboard.map((entry, index) => ( +
+ {index + 1} +
handleAvatarClick(entry.avatar_url, entry.netid) : undefined} + 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} + > +
+ {`${entry.netid} { e.target.onerror = null; e.target.src=defaultProfileImage; }} + /> +
+ {entry.netid} +
+
+ {parseFloat(entry.adjusted_wpm).toFixed(0)} WPM + {parseFloat(entry.accuracy).toFixed(1)}% + {period === 'daily' ? formatRelativeTime(entry.created_at) : new Date(entry.created_at).toLocaleDateString()} +
+
+ )) + ) : ( +

No results found for this leaderboard.

+ )}
)}

Resets daily at 12:00 AM EST

@@ -257,12 +290,15 @@ function Leaderboard({ defaultDuration = 15, defaultPeriod = 'alltime', layoutMo className={`leaderboard-item ${user && entry.netid === user.netid ? 'current-user' : ''}`} > {index + 1} -
-
handleAvatarClick(entry.avatar_url, entry.netid) : undefined} - title={authenticated ? `View ${entry.netid}\'s profile` : 'Log in to view profiles'} - > +
handleAvatarClick(entry.avatar_url, entry.netid) : undefined} + 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} + > +
{`${entry.netid}
  • Real-time Feedback: Immediate visual cues highlight correct and incorrect keystrokes
  • -
  • Error Handling: Option to require error correction or continue typing despite mistakes
  • +
  • Error Handling: In Timed mode you do not need to go back and fix mistakes — you can keep typing; mistakes still reduce accuracy and adjusted WPM. In Snippet mode you must correct errors to continue progressing through the text.
  • Audio Feedback: Customizable keyboard sounds enhance the typing experience
  • Visible Progress: Clear indication of your position within the current text
@@ -137,6 +137,7 @@ function AboutUs() {
  • Inactivity Warning: If you are the only player who has not clicked "Ready" for 60 seconds, you will receive an inactivity warning.
  • Inactivity Kick: If you remain inactive for an additional 15 seconds after the warning, you will be automatically removed from the lobby to prevent holding up other players.
  • Anti-Cheat Measures: Pasting text is disabled during races to maintain fair competition.
  • +
  • Public Lobby Capacity: Public lobbies have no player cap. Private lobbies are limited to 10 players.
  • Stats Calculation

    @@ -190,7 +191,6 @@ function AboutUs() {
  • Difficulty Levels: Content is categorized by complexity and length, allowing progressive skill development
  • -

    Platform Statistics

    @@ -262,6 +262,13 @@ function AboutUs() { + {/* Feedback note positioned at the very bottom of Learn More */} +

    + Have suggestions or found an issue? Email us at + {' '}it.admin@tigerapps.org + {' '}or open an issue on + {' '}GitHub. +

    ); diff --git a/client/src/tutorial/tutorialSteps.js b/client/src/tutorial/tutorialSteps.js index 127760c7..85d754e2 100644 --- a/client/src/tutorial/tutorialSteps.js +++ b/client/src/tutorial/tutorialSteps.js @@ -6,7 +6,7 @@ export const tutorialSteps = { { id: 'home-start', anchorId: 'body', content: "Welcome to TigerType, a Princeton-based typing game for all students! Click Next to start the tutorial; you can also close this and come back to the tutorial at anytime by pressing the '?' button in the top left." }, { 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.' }, { id: 'home-create-private', anchorId: 'mode-create-private', content: 'You can also create private lobbies to race against just your friends. This allows you to select specific modes and options for your race (just like in solo practice).' }, { id: 'home-join-private', anchorId: 'mode-join-private', content: 'You can join a private lobby by entering the lobby code or link that your friends can share with you or entering their NetID. You can also join private lobby by directly entering the URL.' }, { id: 'home-activate-practice', anchorId: 'mode-practice', content: 'Click Solo Practice to continue the tutorial for practice mode, or click the \'X\' button to close the tutorial and come back later.', spotlightClicks: true, event: 'click', placement: 'top' } @@ -16,8 +16,10 @@ export const tutorialSteps = { { id: 'practice-configurator', anchorId: 'configurator', content: 'This is the Test Configurator where you have access to all the modes and their options.' }, { id: 'practice-mode-timed', anchorId: 'mode-timed', content: 'Let\'s check out TIMED mode.', spotlightClicks: true, event: 'click' }, { id: 'practice-mode-timed-desc', anchorId: 'mode-timed', content: 'TIMED mode is similar to MonkeyType, where you are given a random selection of the 1,000 most common english words. You have a set amount of time to type as many words as possible.' }, + { id: 'practice-mode-timed-errors', anchorId: 'mode-timed', content: 'In TIMED mode you do not need to go back and fix mistakes — you can keep typing. Mistakes still lower your accuracy and adjusted WPM.' }, { id: 'practice-timed-options', anchorId: 'timed-options', content: 'You can select any duration, which is the length of time you will be typing; each duration has its own leaderboard entry.' }, { id: 'practice-mode-snippet', anchorId: 'mode-snippet', content: 'Let\'s switch back to Snippet Mode.', spotlightClicks: true }, + { id: 'practice-mode-snippet-errors', anchorId: 'typing-input', content: 'In Snippet mode, you must correct mistakes to progress through the text. This is also the mode used in public matchmaking.' }, { id: 'practice-race-content', anchorId: 'race-content', content: 'Here is the text that you will type. As you type, you will see your stats and progress update in real time.' }, { id: 'practice-typing-start', anchorId: 'typing-input', content: 'Practice mode will automatically begin when you type the first letter.' }, { id: 'practice-typing-action', anchorId: 'typing-input', content: 'Start typing now; feel free to make a mistake to see the mistake highlighting in action. You can press TAB for a new excerpt and ESC to restart.' }, @@ -28,4 +30,4 @@ export const tutorialSteps = { further: [ { id: 'further-end', anchorId: 'body', content: 'That concludes the tutorial. Happy typing!' } ] -}; \ No newline at end of file +}; From 11d0a3b412920af93e008ab000fde16ef07f3bc7 Mon Sep 17 00:00:00 2001 From: Ammaar Alam Date: Wed, 29 Oct 2025 19:57:01 -0400 Subject: [PATCH 4/7] fixing backspace prevent for correctlctly typed words --- client/src/components/Typing.jsx | 22 +++++++++++--------- client/src/context/RaceContext.jsx | 32 +++++++++++++----------------- 2 files changed, 27 insertions(+), 27 deletions(-) diff --git a/client/src/components/Typing.jsx b/client/src/components/Typing.jsx index c74cff0f..feed73ac 100644 --- a/client/src/components/Typing.jsx +++ b/client/src/components/Typing.jsx @@ -653,13 +653,19 @@ function Typing({ // Continue processing this first character instead of ignoring it if (raceState.inProgress) { - raceHandleInput(newInput); + const processed = raceHandleInput(newInput); + setInput(processed ?? newInput); } else { // Since raceState.inProgress hasn't updated yet in this render cycle, // we need to directly set the input so the character appears setInput(newInput); - // Schedule an update after the state has changed - setTimeout(() => raceHandleInput(newInput), 0); + // Schedule an update after the state has changed so startTime is initialized + setTimeout(() => { + const processed = raceHandleInput(newInput); + if (typeof processed === 'string') { + setInput(processed); + } + }, 0); } return; } @@ -712,13 +718,11 @@ function Typing({ }, 1500); } - // Use the handleInput function from RaceContext - raceHandleInput(newInput); + // Use the handleInput function from RaceContext and capture sanitized value + const processed = raceHandleInput(newInput); - // Immediately reflect the user's raw input to avoid dropping characters - // during rapid multi-key presses; word-locking corrections will be - // reconciled on the next tick via typingState.input sync. - setInput(newInput); + // Immediately reflect the sanitized input to keep locked words intact + setInput(processed ?? newInput); } else { // Prevent typing past the end of the snippet if (raceState.snippet && newInput.length > raceState.snippet.text.length) { diff --git a/client/src/context/RaceContext.jsx b/client/src/context/RaceContext.jsx index 5dcbd63b..de13d98c 100644 --- a/client/src/context/RaceContext.jsx +++ b/client/src/context/RaceContext.jsx @@ -619,10 +619,11 @@ export const RaceProvider = ({ children }) => { }; // Handle text input, enforce word locking (snippet mode) or free-flow (timed mode) - const handleInput = (newInput) => { + const handleInput = (incomingInput) => { + let newInput = incomingInput; // Disable input handling for non-practice races before countdown begins if (raceState.type !== 'practice' && !raceState.inProgress && raceState.countdown === null) { - return; + return newInput; } // --- Start Practice Race on First Input --- @@ -645,7 +646,7 @@ export const RaceProvider = ({ children }) => { input: newInput, position: newInput.length })); - return; + return newInput; } // If we are in progress (or just started practice), proceed with full update @@ -654,13 +655,6 @@ export const RaceProvider = ({ children }) => { const text = raceState.snippet?.text || ''; const isTimedMode = !!(raceState.snippet?.is_timed_test || raceState.timedTest?.enabled || raceState.settings?.testMode === 'timed'); - // In timed mode, do not enforce word locking or special backspace preservation – - // users can continue typing past mistakes (Monkeytype-style) - if (isTimedMode) { - updateProgress(newInput); - return; - } - // Find the position of the first error in the current input let firstErrorPosition = text.length; // Default to end of text (no errors) for (let i = 0; i < Math.min(text.length, currentInput.length); i++) { @@ -669,17 +663,17 @@ export const RaceProvider = ({ children }) => { break; } } - + // If trying to delete locked text, only preserve correctly typed text before the first error if (newInput.length < currentInput.length && lockedPosition > 0) { // Only preserve text up to the last complete word before the first error const lastWordBreakBeforeError = currentInput.lastIndexOf(' ', Math.max(0, firstErrorPosition - 1)) + 1; - + // Only enforce locking if trying to delete before the locked position if (newInput.length < lastWordBreakBeforeError) { const preservedPart = currentInput.substring(0, lastWordBreakBeforeError); let newPart = ''; - + // Keep the user's input after the preserved part if (newInput.length >= preservedPart.length) { newPart = newInput.substring(preservedPart.length); @@ -687,14 +681,15 @@ export const RaceProvider = ({ children }) => { // Deletion is attempting to erase preserved text newPart = currentInput.substring(preservedPart.length); } - + // This enforces that only correctly typed words before any error cannot be deleted newInput = preservedPart + newPart; } } - + // Update progress with the potentially modified input updateProgress(newInput); + return newInput; }; const updateProgress = (input) => { @@ -728,7 +723,8 @@ export const RaceProvider = ({ children }) => { } currentErrors = input.length - correctChars; // net errors hasError = currentErrors > 0; - firstErrorPosition = hasError ? input.split('').findIndex((ch, idx) => idx < text.length && ch !== text[idx]) : text.length; + const mismatchIndex = input.split('').findIndex((ch, idx) => idx >= text.length || ch !== text[idx]); + firstErrorPosition = hasError ? (mismatchIndex === -1 ? Math.min(input.length, text.length) : mismatchIndex) : text.length; } else { // Snippet mode: contiguous correctness until first error if (!hasError) { @@ -782,7 +778,7 @@ export const RaceProvider = ({ children }) => { let newLockedPosition = 0; // Only process word locking if there are characters - if (!isTimedMode && input.length > 0) { + if (input.length > 0) { let wordStart = 0; // Only lock text if there are no errors, or only lock up to the last word break before first error @@ -826,7 +822,7 @@ export const RaceProvider = ({ children }) => { completed: isTimedMode ? false : isCompleted, // timed ends by timer wpm, accuracy, - lockedPosition: isTimedMode ? 0 : newLockedPosition + lockedPosition: newLockedPosition }); // If the race is still in progress, update progress From 3db0793979c2c943ca485fbb92207516a281efb5 Mon Sep 17 00:00:00 2001 From: Ammaar Alam Date: Wed, 29 Oct 2025 20:10:58 -0400 Subject: [PATCH 5/7] styling new components --- client/src/components/Leaderboard.css | 11 +- client/src/pages/AboutUs.css | 43 ++- client/src/pages/AboutUs.css.bak | 396 ++++++++++++++++++++++++++ client/src/pages/Lobby.css | 7 +- 4 files changed, 447 insertions(+), 10 deletions(-) create mode 100644 client/src/pages/AboutUs.css.bak diff --git a/client/src/components/Leaderboard.css b/client/src/components/Leaderboard.css index d8f86187..616f6480 100644 --- a/client/src/components/Leaderboard.css +++ b/client/src/components/Leaderboard.css @@ -109,7 +109,15 @@ h2 { /* Make more of the entry clickable when authenticated */ .leaderboard-player.clickable { cursor: pointer; } -.leaderboard-player.clickable:hover .leaderboard-netid { text-decoration: underline; color: #F58025; } +.leaderboard-player.clickable:hover .leaderboard-netid { + color: #F58025; + text-shadow: 0 0 8px rgba(245, 128, 37, 0.5); + transition: color 0.2s ease, text-shadow 0.2s ease; +} +.leaderboard-player.clickable:hover .leaderboard-avatar { + transform: scale(1.08); + border-color: #F58025; +} .leaderboard-player.disabled { cursor: not-allowed; } .leaderboard-player.disabled .leaderboard-avatar { cursor: not-allowed; } @@ -142,6 +150,7 @@ h2 { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; + transition: color 0.2s ease, text-shadow 0.2s ease; } .leaderboard-stats { diff --git a/client/src/pages/AboutUs.css b/client/src/pages/AboutUs.css index ec857f38..bc1641c3 100644 --- a/client/src/pages/AboutUs.css +++ b/client/src/pages/AboutUs.css @@ -223,19 +223,52 @@ /* Subtle feedback note under Learn More */ .feedback-note { - margin-top: 0.75rem; - font-size: 0.9rem; - color: var(--text-color-tertiary); + margin-top: 1.5rem; + padding: 1.2rem 1.5rem; + font-size: 0.95rem; + color: var(--text-color); text-align: center; + background-color: rgba(var(--primary-color-rgb), 0.08); + border-radius: 10px; + border: 1px solid rgba(var(--primary-color-rgb), 0.15); + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05); + transition: all 0.3s ease; +} + +.feedback-note:hover { + background-color: rgba(var(--primary-color-rgb), 0.12); + border-color: rgba(var(--primary-color-rgb), 0.25); + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); + transform: translateY(-2px); } .feedback-note a { - color: var(--developer-link-color); + color: var(--primary-color); text-decoration: none; + font-weight: 600; + position: relative; + transition: color 0.2s ease; + padding: 0 2px; +} + +.feedback-note a::after { + content: ''; + position: absolute; + width: 0; + height: 2px; + bottom: -1px; + left: 0; + background-color: var(--primary-color); + transition: width 0.3s ease; + border-radius: 1px; } .feedback-note a:hover { - text-decoration: underline; + color: var(--primary-color-dark); +} + +.feedback-note a:hover::after { + width: 100%; } .collapsible-section { diff --git a/client/src/pages/AboutUs.css.bak b/client/src/pages/AboutUs.css.bak new file mode 100644 index 00000000..ec857f38 --- /dev/null +++ b/client/src/pages/AboutUs.css.bak @@ -0,0 +1,396 @@ +/* [AI-DISCLAIMER: ~40% OF THIS CSS DEBUGGED WITH THE HELP OF AI] */ +.about-us-container { + max-width: 1100px; + margin: 2rem auto 4rem; + padding: 0; + color: var(--text-color); /* Use theme text color */ + font-family: var(--font-family-main); +} + +.about-us-header { + background-color: var(--background-color-secondary); + padding: 3rem 2rem; + text-align: center; + border-radius: 12px 12px 0 0; + margin-bottom: 3rem; + position: relative; + box-shadow: 0 4px 15px rgba(0, 0, 0, 0.08); + background-image: linear-gradient(to bottom right, + rgba(var(--primary-color-rgb), 0.05), + rgba(var(--primary-color-rgb), 0.1)); +} + +.about-us-header::after { + content: ''; + position: absolute; + bottom: 0; + left: 0; + right: 0; + height: 4px; + background: linear-gradient(to right, + var(--primary-color-light), + var(--primary-color), + var(--primary-color-dark)); +} + +.about-us-header h1 { + font-size: 2.8rem; + margin-bottom: 0.8rem; + color: var(--primary-color); + font-weight: 700; +} + +.about-us-subtitle { + font-size: 1.3rem; + color: var(--text-color-secondary); + margin-bottom: 0; + font-weight: 400; +} + +.about-us-container section { + background-color: var(--background-color-secondary); + border-radius: 12px; + padding: 2.5rem; + margin-bottom: 3rem; + box-shadow: 0 4px 15px rgba(0, 0, 0, 0.08); +} + +.about-us-container h2 { + color: var(--primary-color); + padding-bottom: 1rem; + margin-bottom: 2.5rem; + font-weight: 700; + font-size: 2rem; + position: relative; + text-align: center; +} + +.about-us-container h2::after { + content: ''; + position: absolute; + bottom: 0; + left: 50%; + transform: translateX(-50%); + width: 80px; + height: 3px; + background-color: var(--primary-color); + border-radius: 2px; +} + +.developers-section { + margin-bottom: 3rem; +} + +.developer-cards-container { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 2.5rem; + justify-content: center; +} + +.developer-card.leader { + grid-column: 2; + position: relative; + border: none; + z-index: 1; +} + +.developer-card { + background-color: var(--background-color); + padding: 0; + border-radius: 12px; + box-shadow: 0 6px 20px rgba(0, 0, 0, 0.1); + text-align: center; + min-width: 280px; + max-width: 100%; + transition: transform 0.3s ease, box-shadow 0.3s ease; + overflow: hidden; + display: flex; + flex-direction: column; +} + +.developer-card:hover { + transform: translateY(-8px); + box-shadow: 0 12px 30px rgba(0, 0, 0, 0.15), 0 0 15px rgba(var(--primary-color-rgb), 0.3); +} + +.leader-badge { + position: absolute; + top: 1rem; + right: 1rem; + background-color: var(--primary-color); + color: white; + padding: 0.4rem 0.8rem; + border-radius: 20px; + font-weight: 600; + font-size: 0.85rem; + box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1); + z-index: 2; +} + +.developer-image-container { + width: 100%; + padding-top: 2rem; + padding-bottom: 1rem; + background-color: rgba(var(--primary-color-rgb), 0.05); + position: relative; +} + +.developer-image { + width: 150px; + height: 150px; + margin: 0 auto; + border-radius: 50%; + overflow: hidden; + border: 5px solid white; + box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1); +} + +.developer-image img { + width: 100%; + height: 100%; + object-fit: cover; + transition: transform 0.3s ease; +} + +.developer-card:hover .developer-image img { + transform: scale(1.1); +} + +.developer-card h3 { + margin-top: 1.5rem; + margin-bottom: 0.3rem; + color: var(--text-color-highlight); + font-weight: 700; + font-size: 1.4rem; + padding: 0 1.5rem; +} + +.developer-role { + color: var(--text-color-secondary); + font-weight: 600; + margin-bottom: 0.5rem; + font-size: 1.05rem; + padding: 0 1.5rem; +} + +.developer-bio { + font-size: 0.95rem; + color: var(--text-color-tertiary); + margin-bottom: 1.5rem; + padding: 0 1.5rem; +} + +.developer-links { + margin-top: auto; + padding: 1.2rem; + border-top: 1px solid rgba(var(--border-color-rgb), 0.1); + background-color: rgba(var(--background-color-tertiary-rgb), 0.5); +} + +.developer-links a { + display: inline-block; + color: var(--developer-link-color); + text-decoration: none; + margin: 0 0.8rem; + font-weight: 600; + position: relative; + transition: color 0.2s ease; +} + +.developer-links a::after { + content: ''; + position: absolute; + width: 0; + height: 2px; + bottom: -3px; + left: 0; + background-color: var(--developer-link-hover-color); + transition: width 0.3s ease; +} + +.developer-links a:hover { + color: var(--developer-link-hover-color); +} + +.developer-links a:hover::after { + width: 100%; +} + +.info-section { + margin-top: 2rem; +} + +/* Subtle feedback note under Learn More */ +.feedback-note { + margin-top: 0.75rem; + font-size: 0.9rem; + color: var(--text-color-tertiary); + text-align: center; +} + +.feedback-note a { + color: var(--developer-link-color); + text-decoration: none; +} + +.feedback-note a:hover { + text-decoration: underline; +} + +.collapsible-section { + background-color: var(--background-color); + border-radius: 10px; + margin-bottom: 1.5rem; + overflow: hidden; + border: 1px solid var(--border-color); + transition: box-shadow 0.3s ease; +} + +.collapsible-section:hover { + box-shadow: 0 5px 15px rgba(0, 0, 0, 0.08); +} + +.collapsible-header { + background-color: transparent; + color: var(--text-color-highlight); + border: none; + padding: 1.5rem 2rem; + width: 100%; + text-align: left; + font-size: 1.25rem; + font-weight: 600; + cursor: pointer; + display: flex; + justify-content: space-between; + align-items: center; + transition: background-color 0.2s ease; +} + +.collapsible-header:hover { + background-color: rgba(var(--primary-color-rgb), 0.05); +} + +.collapsible-header .arrow { + transition: transform 0.3s ease; + font-size: 0.8em; + margin-left: 1rem; + color: var(--primary-color); +} + +.collapsible-header .arrow.open { + transform: rotate(180deg); +} + +.collapsible-content { + padding: 1.5rem 2rem 2rem; + border-top: 1px solid var(--border-color); + animation: fadeIn 0.4s ease-out; +} + +.collapsible-content h4 { + color: var(--primary-color); + font-size: 1.3rem; + font-weight: 700; + margin-top: 2rem; + margin-bottom: 1rem; + padding-bottom: 0.5rem; + border-bottom: 1px solid rgba(var(--border-color-rgb), 0.3); +} + +.collapsible-content h4:first-of-type { + margin-top: 0.5rem; +} + +.collapsible-content p { + margin-bottom: 1.2rem; + color: var(--text-color); + line-height: 1.7; + font-size: 1.05rem; +} + +.collapsible-content ul { + list-style: none; + margin-top: 0.5rem; + margin-bottom: 1.5rem; + padding-left: 1.2rem; +} + +.collapsible-content li { + position: relative; + padding-left: 1.5rem; + margin-bottom: 1rem; + line-height: 1.6; + color: var(--text-color); +} + +.collapsible-content li::before { + content: ''; + position: absolute; + left: 0; + top: 10px; + width: 8px; + height: 8px; + background-color: var(--primary-color); + border-radius: 50%; + box-shadow: 0 0 4px rgba(var(--primary-color-rgb), 0.4); +} + +.collapsible-content strong { + color: var(--primary-color); + font-weight: 700; +} + +@keyframes fadeIn { + from { opacity: 0; transform: translateY(-8px); } + to { opacity: 1; transform: translateY(0); } +} + +@media (max-width: 1100px) { + .about-us-container { + margin: 1.5rem; + } +} + +@media (max-width: 768px) { + .about-us-header h1 { + font-size: 2.2rem; + } + + .about-us-subtitle { + font-size: 1.1rem; + } + + .developer-cards-container { + grid-template-columns: 1fr; + } + + .developer-card.leader { + grid-column: 1; + transform: scale(1); + order: -1; + } +} + +@media (max-width: 480px) { + .about-us-container section { + padding: 2rem 1.5rem; + } + + .about-us-header { + padding: 2.5rem 1.5rem; + } + + .about-us-header h1 { + font-size: 2rem; + } + + .collapsible-header { + padding: 1.2rem 1.5rem; + font-size: 1.15rem; + } + + .collapsible-content { + padding: 1.2rem 1.5rem 1.8rem; + } +} diff --git a/client/src/pages/Lobby.css b/client/src/pages/Lobby.css index 7ad3a3e1..4df0e9c8 100644 --- a/client/src/pages/Lobby.css +++ b/client/src/pages/Lobby.css @@ -176,7 +176,7 @@ display: none; } -/* NEW RULE: Ensure the container for conditional options behaves correctly in vertical layout */ +/* Ensure the container for conditional options behaves correctly in vertical layout */ .lobby-settings .test-configurator .conditional-options-container { width: 100%; height: auto; @@ -199,14 +199,14 @@ /* Visible state when React adds .visible */ .lobby-settings .test-configurator .options-wrapper.visible { - margin-top: 12px; /* reduce vertical gap */ + margin-top: 12px; max-height: 600px; opacity: 1; pointer-events: auto; visibility: visible; } -/* Inner rows - now stacking vertically */ +/* Inner rows - stacking vertically */ .lobby-settings .test-configurator .snippet-filters-inner, .lobby-settings .test-configurator .duration-selection-inner { display: flex; @@ -230,7 +230,6 @@ vertical-align: middle; } -/* Fine‑tune dropdown pills */ .lobby-settings .test-configurator .config-select { min-width: 0; flex-grow: 1; From e6f5bbbc8a92fe8357911b802d685d91f9dc65c3 Mon Sep 17 00:00:00 2001 From: Ammaar Alam Date: Wed, 29 Oct 2025 20:15:33 -0400 Subject: [PATCH 6/7] making incorrect character styling uniform --- client/src/components/Typing.css | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/client/src/components/Typing.css b/client/src/components/Typing.css index a825a594..25109e0b 100644 --- a/client/src/components/Typing.css +++ b/client/src/components/Typing.css @@ -226,13 +226,26 @@ .incorrect { color: var(--incorrect-color)!important; background-color: var(--incorrect-bg-color)!important; - text-decoration: underline wavy rgba(255, 65, 47, 0.55); + position: relative; +} + +/* Visual indicator for incorrectly typed characters - solid underline */ +.snippet-display .incorrect::after { + content: ''; + position: absolute; + left: 0; + right: 0; + bottom: 0; + height: 2.5px; + background-color: var(--incorrect-color); + opacity: 0.9; + border-radius: 1px; + pointer-events: none; } -/* Caret cursor: prefer subtle inline color only (no red block, no underline) */ +/* Caret cursor: remove red background, keep solid underline */ :root[data-cursor='caret'] .snippet-display .incorrect { background-color: transparent !important; - text-decoration: none !important; } .shake-animation { From 9271b438d58c32a6713b7712c4fd0cc743fa84e1 Mon Sep 17 00:00:00 2001 From: Ammaar Alam Date: Wed, 29 Oct 2025 20:46:07 -0400 Subject: [PATCH 7/7] Delete client/src/pages/AboutUs.css.bak --- client/src/pages/AboutUs.css.bak | 396 ------------------------------- 1 file changed, 396 deletions(-) delete mode 100644 client/src/pages/AboutUs.css.bak diff --git a/client/src/pages/AboutUs.css.bak b/client/src/pages/AboutUs.css.bak deleted file mode 100644 index ec857f38..00000000 --- a/client/src/pages/AboutUs.css.bak +++ /dev/null @@ -1,396 +0,0 @@ -/* [AI-DISCLAIMER: ~40% OF THIS CSS DEBUGGED WITH THE HELP OF AI] */ -.about-us-container { - max-width: 1100px; - margin: 2rem auto 4rem; - padding: 0; - color: var(--text-color); /* Use theme text color */ - font-family: var(--font-family-main); -} - -.about-us-header { - background-color: var(--background-color-secondary); - padding: 3rem 2rem; - text-align: center; - border-radius: 12px 12px 0 0; - margin-bottom: 3rem; - position: relative; - box-shadow: 0 4px 15px rgba(0, 0, 0, 0.08); - background-image: linear-gradient(to bottom right, - rgba(var(--primary-color-rgb), 0.05), - rgba(var(--primary-color-rgb), 0.1)); -} - -.about-us-header::after { - content: ''; - position: absolute; - bottom: 0; - left: 0; - right: 0; - height: 4px; - background: linear-gradient(to right, - var(--primary-color-light), - var(--primary-color), - var(--primary-color-dark)); -} - -.about-us-header h1 { - font-size: 2.8rem; - margin-bottom: 0.8rem; - color: var(--primary-color); - font-weight: 700; -} - -.about-us-subtitle { - font-size: 1.3rem; - color: var(--text-color-secondary); - margin-bottom: 0; - font-weight: 400; -} - -.about-us-container section { - background-color: var(--background-color-secondary); - border-radius: 12px; - padding: 2.5rem; - margin-bottom: 3rem; - box-shadow: 0 4px 15px rgba(0, 0, 0, 0.08); -} - -.about-us-container h2 { - color: var(--primary-color); - padding-bottom: 1rem; - margin-bottom: 2.5rem; - font-weight: 700; - font-size: 2rem; - position: relative; - text-align: center; -} - -.about-us-container h2::after { - content: ''; - position: absolute; - bottom: 0; - left: 50%; - transform: translateX(-50%); - width: 80px; - height: 3px; - background-color: var(--primary-color); - border-radius: 2px; -} - -.developers-section { - margin-bottom: 3rem; -} - -.developer-cards-container { - display: grid; - grid-template-columns: repeat(3, 1fr); - gap: 2.5rem; - justify-content: center; -} - -.developer-card.leader { - grid-column: 2; - position: relative; - border: none; - z-index: 1; -} - -.developer-card { - background-color: var(--background-color); - padding: 0; - border-radius: 12px; - box-shadow: 0 6px 20px rgba(0, 0, 0, 0.1); - text-align: center; - min-width: 280px; - max-width: 100%; - transition: transform 0.3s ease, box-shadow 0.3s ease; - overflow: hidden; - display: flex; - flex-direction: column; -} - -.developer-card:hover { - transform: translateY(-8px); - box-shadow: 0 12px 30px rgba(0, 0, 0, 0.15), 0 0 15px rgba(var(--primary-color-rgb), 0.3); -} - -.leader-badge { - position: absolute; - top: 1rem; - right: 1rem; - background-color: var(--primary-color); - color: white; - padding: 0.4rem 0.8rem; - border-radius: 20px; - font-weight: 600; - font-size: 0.85rem; - box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1); - z-index: 2; -} - -.developer-image-container { - width: 100%; - padding-top: 2rem; - padding-bottom: 1rem; - background-color: rgba(var(--primary-color-rgb), 0.05); - position: relative; -} - -.developer-image { - width: 150px; - height: 150px; - margin: 0 auto; - border-radius: 50%; - overflow: hidden; - border: 5px solid white; - box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1); -} - -.developer-image img { - width: 100%; - height: 100%; - object-fit: cover; - transition: transform 0.3s ease; -} - -.developer-card:hover .developer-image img { - transform: scale(1.1); -} - -.developer-card h3 { - margin-top: 1.5rem; - margin-bottom: 0.3rem; - color: var(--text-color-highlight); - font-weight: 700; - font-size: 1.4rem; - padding: 0 1.5rem; -} - -.developer-role { - color: var(--text-color-secondary); - font-weight: 600; - margin-bottom: 0.5rem; - font-size: 1.05rem; - padding: 0 1.5rem; -} - -.developer-bio { - font-size: 0.95rem; - color: var(--text-color-tertiary); - margin-bottom: 1.5rem; - padding: 0 1.5rem; -} - -.developer-links { - margin-top: auto; - padding: 1.2rem; - border-top: 1px solid rgba(var(--border-color-rgb), 0.1); - background-color: rgba(var(--background-color-tertiary-rgb), 0.5); -} - -.developer-links a { - display: inline-block; - color: var(--developer-link-color); - text-decoration: none; - margin: 0 0.8rem; - font-weight: 600; - position: relative; - transition: color 0.2s ease; -} - -.developer-links a::after { - content: ''; - position: absolute; - width: 0; - height: 2px; - bottom: -3px; - left: 0; - background-color: var(--developer-link-hover-color); - transition: width 0.3s ease; -} - -.developer-links a:hover { - color: var(--developer-link-hover-color); -} - -.developer-links a:hover::after { - width: 100%; -} - -.info-section { - margin-top: 2rem; -} - -/* Subtle feedback note under Learn More */ -.feedback-note { - margin-top: 0.75rem; - font-size: 0.9rem; - color: var(--text-color-tertiary); - text-align: center; -} - -.feedback-note a { - color: var(--developer-link-color); - text-decoration: none; -} - -.feedback-note a:hover { - text-decoration: underline; -} - -.collapsible-section { - background-color: var(--background-color); - border-radius: 10px; - margin-bottom: 1.5rem; - overflow: hidden; - border: 1px solid var(--border-color); - transition: box-shadow 0.3s ease; -} - -.collapsible-section:hover { - box-shadow: 0 5px 15px rgba(0, 0, 0, 0.08); -} - -.collapsible-header { - background-color: transparent; - color: var(--text-color-highlight); - border: none; - padding: 1.5rem 2rem; - width: 100%; - text-align: left; - font-size: 1.25rem; - font-weight: 600; - cursor: pointer; - display: flex; - justify-content: space-between; - align-items: center; - transition: background-color 0.2s ease; -} - -.collapsible-header:hover { - background-color: rgba(var(--primary-color-rgb), 0.05); -} - -.collapsible-header .arrow { - transition: transform 0.3s ease; - font-size: 0.8em; - margin-left: 1rem; - color: var(--primary-color); -} - -.collapsible-header .arrow.open { - transform: rotate(180deg); -} - -.collapsible-content { - padding: 1.5rem 2rem 2rem; - border-top: 1px solid var(--border-color); - animation: fadeIn 0.4s ease-out; -} - -.collapsible-content h4 { - color: var(--primary-color); - font-size: 1.3rem; - font-weight: 700; - margin-top: 2rem; - margin-bottom: 1rem; - padding-bottom: 0.5rem; - border-bottom: 1px solid rgba(var(--border-color-rgb), 0.3); -} - -.collapsible-content h4:first-of-type { - margin-top: 0.5rem; -} - -.collapsible-content p { - margin-bottom: 1.2rem; - color: var(--text-color); - line-height: 1.7; - font-size: 1.05rem; -} - -.collapsible-content ul { - list-style: none; - margin-top: 0.5rem; - margin-bottom: 1.5rem; - padding-left: 1.2rem; -} - -.collapsible-content li { - position: relative; - padding-left: 1.5rem; - margin-bottom: 1rem; - line-height: 1.6; - color: var(--text-color); -} - -.collapsible-content li::before { - content: ''; - position: absolute; - left: 0; - top: 10px; - width: 8px; - height: 8px; - background-color: var(--primary-color); - border-radius: 50%; - box-shadow: 0 0 4px rgba(var(--primary-color-rgb), 0.4); -} - -.collapsible-content strong { - color: var(--primary-color); - font-weight: 700; -} - -@keyframes fadeIn { - from { opacity: 0; transform: translateY(-8px); } - to { opacity: 1; transform: translateY(0); } -} - -@media (max-width: 1100px) { - .about-us-container { - margin: 1.5rem; - } -} - -@media (max-width: 768px) { - .about-us-header h1 { - font-size: 2.2rem; - } - - .about-us-subtitle { - font-size: 1.1rem; - } - - .developer-cards-container { - grid-template-columns: 1fr; - } - - .developer-card.leader { - grid-column: 1; - transform: scale(1); - order: -1; - } -} - -@media (max-width: 480px) { - .about-us-container section { - padding: 2rem 1.5rem; - } - - .about-us-header { - padding: 2.5rem 1.5rem; - } - - .about-us-header h1 { - font-size: 2rem; - } - - .collapsible-header { - padding: 1.2rem 1.5rem; - font-size: 1.15rem; - } - - .collapsible-content { - padding: 1.2rem 1.5rem 1.8rem; - } -}