From 3dbc7b5e504b1eecae640d07fcd248e915306054 Mon Sep 17 00:00:00 2001 From: Ammaar Alam Date: Tue, 4 Nov 2025 11:20:18 -0500 Subject: [PATCH 1/5] updating landing page leaderboard styling and polishing mobile view --- client/src/components/Leaderboard.css | 386 ++++++++++++++++---------- client/src/components/Leaderboard.jsx | 199 +++++++------ client/src/pages/Landing.css | 18 +- 3 files changed, 365 insertions(+), 238 deletions(-) diff --git a/client/src/components/Leaderboard.css b/client/src/components/Leaderboard.css index f05e2a9b..aecef801 100644 --- a/client/src/components/Leaderboard.css +++ b/client/src/components/Leaderboard.css @@ -382,61 +382,68 @@ h2 { /* --- Styles for Landing Page Leaderboard Layout --- */ /* Applied via .leaderboard-section-landing selector in Landing.css */ + .leaderboard-landing-wrapper { - display: flex; - flex-direction: column; width: 100%; height: 100%; - gap: 1rem; } -/* New Controls Area - Horizontal Layout */ -.leaderboard-landing-controls-area { +.leaderboard-landing-panel { + position: relative; display: flex; flex-direction: column; - align-items: center; - width: 100%; gap: 1rem; - padding: 1.2rem 1rem; - background-color: rgba(30, 30, 30, 0.6); - border-radius: 10px; - box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); - border: 1px solid rgba(255, 255, 255, 0.05); - position: relative; + width: 100%; + height: 100%; + padding: 1.6rem 1.4rem 1rem; + background-color: rgba(24, 24, 24, 0.88); + border-radius: 16px; + border: 1px solid rgba(255, 255, 255, 0.08); + box-shadow: 0 10px 28px rgba(0, 0, 0, 0.24); overflow: hidden; } -.leaderboard-landing-controls-area::before { +.leaderboard-landing-panel::before { content: ''; position: absolute; top: 0; left: 0; width: 100%; height: 4px; - background: linear-gradient(90deg, rgba(245, 128, 37, 0.8) 0%, rgba(255, 155, 82, 0.8) 50%, rgba(245, 128, 37, 0.8) 100%); - box-shadow: 0 1px 8px rgba(245, 128, 37, 0.5); + background: linear-gradient(90deg, rgba(245, 128, 37, 0.95) 0%, rgba(255, 162, 88, 0.9) 50%, rgba(245, 128, 37, 0.95) 100%); + box-shadow: 0 6px 18px rgba(245, 128, 37, 0.35); } -.leaderboard-landing-controls-area h2 { - font-size: 1.6rem; - margin-bottom: 0.5rem; - margin-top: 0; +.leaderboard-landing-panel h2 { + font-size: 1.55rem; + margin: 0; color: #F58025; - text-shadow: 0 2px 4px rgba(0, 0, 0, 0.3); + text-shadow: 0 2px 8px rgba(0, 0, 0, 0.45); +} + +.leaderboard-heading-row { + display: flex; + align-items: center; + justify-content: space-between; + gap: 1rem; + padding-top: 0.2rem; +} + +.leaderboard-heading-row h2 { position: relative; - display: inline-block; + padding-bottom: 0.35rem; } -.leaderboard-landing-controls-area h2::after { +.leaderboard-heading-row h2::after { content: ''; position: absolute; - bottom: -8px; - left: 50%; - transform: translateX(-50%); - width: 80px; + bottom: 0; + left: 0; + width: 68px; height: 3px; - background-color: rgba(245, 128, 37, 0.6); border-radius: 3px; + background-color: rgba(245, 128, 37, 0.55); + box-shadow: 0 0 8px rgba(245, 128, 37, 0.35); } /* Compact mobile controls */ @@ -451,159 +458,146 @@ h2 { display: flex; flex-direction: column; gap: 0.35rem; - color: rgba(255,255,255,0.85); + color: rgba(255, 255, 255, 0.85); font-size: 0.9rem; } .leaderboard-mobile-controls select { background-color: var(--button-bg-color); color: #fff; - border: 1px solid rgba(255,255,255,0.15); + border: 1px solid rgba(255, 255, 255, 0.15); border-radius: 8px; padding: 0.5rem 0.75rem; font-size: 0.95rem; } @media (max-width: 600px) { - .leaderboard-landing-controls-area .control-group { display: none; } -} - -.leaderboard-subtitle { - color: #aaa; - font-size: 0.85rem; - text-align: center; - font-style: italic; - padding: 1vh; + .leaderboard-heading-row .control-group { display: none; } } -/* Horizontal control groups for period options (Daily/Alltime) */ -.leaderboard-landing-controls-area .control-group.horizontal { +.leaderboard-landing-panel .period-controls.horizontal { + flex: 0 0 auto; display: flex; - flex-direction: row; - justify-content: center; - gap: 0.75rem; - width: 100%; - max-width: 600px; -} - -/* Vertical control groups for duration options (15s, 30s, etc.) */ -.leaderboard-landing-controls-area .control-group.vertical { - display: flex; - flex-direction: column; - justify-content: center; - gap: 0.75rem; - width: 100%; - max-width: 120px; -} - -/* Period controls (Daily/Alltime) styling */ -.leaderboard-landing-controls-area .period-controls.horizontal { - margin-bottom: 0.5rem; - width: 100%; - display: flex; - justify-content: center; - gap: 0.75rem; - max-width: 300px; + justify-content: flex-end; + gap: 0.35rem; + padding: 0.3rem; + border-radius: 999px; + background: rgba(255, 255, 255, 0.06); + box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.04); + max-width: 320px; } -.leaderboard-landing-controls-area .period-controls.horizontal .control-button { - width: 8vh; - padding: 0.75rem 1rem; - font-size: 1rem; - letter-spacing: 0.5px; +.leaderboard-landing-panel .period-controls.horizontal .control-button { + flex: 1; + min-width: 0; + padding: 0.5rem 1.15rem; + font-size: 0.95rem; font-weight: 600; display: flex; align-items: center; justify-content: center; - border-radius: 8px; - background-color: var(--button-bg-color); - color: rgba(255, 255, 255, 0.8); - transition: all 0.3s cubic-bezier(0.2, 0.8, 0.2, 1); - box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2); - border: 1px solid rgba(255, 255, 255, 0.05); + border-radius: 999px; + background: transparent; + border: none; + color: rgba(255, 255, 255, 0.75); + transition: all 0.25s ease; } -.leaderboard-landing-controls-area .period-controls.horizontal .control-button:hover { - background-color: rgba(70, 70, 70, 0.9); - transform: translateY(-2px); - box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3); +.leaderboard-landing-panel .period-controls.horizontal .control-button:hover, +.leaderboard-landing-panel .period-controls.horizontal .control-button:focus-visible { color: white; + background: rgba(255, 255, 255, 0.12); + box-shadow: 0 6px 16px rgba(0, 0, 0, 0.25); } -.leaderboard-landing-controls-area .period-controls.horizontal .control-button.active { - background-color: #F58025; - color: white; - box-shadow: 0 4px 15px rgba(245, 128, 37, 0.4); - border: 1px solid rgba(255, 255, 255, 0.1); - transform: translateY(-2px); +.leaderboard-landing-panel .period-controls.horizontal .control-button.active { + background: linear-gradient(135deg, #F58025, #ff9b52); + color: #161616; + box-shadow: 0 10px 22px rgba(245, 128, 37, 0.45); } -/* Duration controls styling - now vertical */ -.leaderboard-landing-controls-area .duration-controls.vertical .control-button { - width: 80%; +.leaderboard-landing-panel .duration-controls.horizontal { + display: flex; + flex-wrap: wrap; + gap: 0.45rem; + width: 100%; + justify-content: flex-start; +} + +.leaderboard-landing-panel .duration-controls.horizontal .control-button { + flex: 0 0 auto; + padding: 0.45rem 1.1rem; + border-radius: 999px; + background: rgba(255, 255, 255, 0.08); + border: 1px solid transparent; + color: rgba(255, 255, 255, 0.78); font-weight: 600; - border-radius: 8px; - background-color: var(--button-bg-color); - color: rgba(255, 255, 255, 0.8); - transition: all 0.3s cubic-bezier(0.2, 0.8, 0.2, 1); - box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2); - border: 1px solid rgba(255, 255, 255, 0.05); - text-align: center; - align-self: center; + transition: all 0.2s ease; } -.leaderboard-landing-controls-area .duration-controls.vertical .control-button:hover { - background-color: rgba(70, 70, 70, 0.9); - transform: translateX(3px); - box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3); +.leaderboard-landing-panel .duration-controls.horizontal .control-button:hover, +.leaderboard-landing-panel .duration-controls.horizontal .control-button:focus-visible { color: white; + border-color: rgba(255, 255, 255, 0.2); + background: rgba(255, 255, 255, 0.15); + box-shadow: 0 6px 18px rgba(0, 0, 0, 0.25); } -.leaderboard-landing-controls-area .duration-controls.vertical .control-button.active { - background-color: #F58025; - color: white; - box-shadow: 0 4px 15px rgba(245, 128, 37, 0.4); - border: 1px solid rgba(255, 255, 255, 0.1); - transform: translateX(3px); +.leaderboard-landing-panel .duration-controls.horizontal .control-button.active { + background: linear-gradient(135deg, #F58025, #ff9b52); + color: #161616; + border-color: rgba(255, 255, 255, 0.25); + box-shadow: 0 10px 22px rgba(245, 128, 37, 0.45); } -/* List Area */ -.leaderboard-landing-list-area { - flex-grow: 1; - width: 100%; +.leaderboard-landing-panel .control-button:focus-visible { + outline: 2px solid rgba(245, 128, 37, 0.85); + outline-offset: 2px; +} + +.leaderboard-landing-content { display: flex; flex-direction: column; + flex: 1; + gap: 0.6rem; + min-height: 0; } -/* Adjust list height and scrolling */ -.leaderboard-landing-list-area .leaderboard-list { - flex-grow: 1; - max-height: 25vh; /* Reduced height to fit better in side-by-side layout */ - background-color: rgba(30, 30, 30, 0.5); +.leaderboard-landing-content .leaderboard-list { + flex: 1; + min-height: 0; + background-color: rgba(30, 30, 30, 0.6); border: 1px solid rgba(255, 255, 255, 0.08); - padding: 0.5rem; - border-radius: 10px; + border-radius: 12px; + padding: 0.35rem; + display: flex; + flex-direction: column; + gap: 0.45rem; overflow-y: auto; - box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); + overflow-x: hidden; + box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.25), 0 6px 18px rgba(0, 0, 0, 0.18); } -/* Make leaderboard items more compact for landing page */ -.leaderboard-landing-list-area .leaderboard-item { - padding: 0.7rem 1rem; - border-radius: 6px; - margin-bottom: 0.3rem; - background-color: rgba(40, 40, 40, 0.6); +.leaderboard-landing-content .leaderboard-item { + border-bottom: none; + padding: 0.85rem 1.1rem; + border-radius: 10px; + margin: 0; + background-color: rgba(38, 38, 38, 0.78); transition: all 0.2s ease; + width: 100%; + box-shadow: none; } -.leaderboard-landing-list-area .leaderboard-item:hover { - background-color: rgba(50, 50, 50, 0.8); +.leaderboard-landing-content .leaderboard-item:hover { + background-color: rgba(52, 52, 52, 0.92); transform: translateX(2px); + box-shadow: 0 10px 20px rgba(0, 0, 0, 0.28); } -.leaderboard-landing-list-area .loading-indicator, -.leaderboard-landing-list-area .error-message, -.leaderboard-landing-list-area .no-results { +.leaderboard-landing-content .loading-indicator, +.leaderboard-landing-content .error-message, +.leaderboard-landing-content .no-results { flex-grow: 1; display: flex; align-items: center; @@ -611,21 +605,125 @@ h2 { min-height: 150px; } -/* Responsive adjustments */ -@media (min-width: 768px) { - .leaderboard-landing-wrapper { - flex-direction: row; +.leaderboard-landing-content .no-results { + position: static; + width: 100%; + min-height: 100%; + padding: 0; + font-style: normal; + color: rgba(255, 255, 255, 0.65); +} + +.leaderboard-landing-panel .leaderboard-stats { + gap: 1rem; + flex-wrap: wrap; + justify-content: flex-end; +} + +.leaderboard-landing-panel .leaderboard-stats.compact { + gap: 0.3rem; + flex-wrap: nowrap; + align-items: flex-end; +} + +.leaderboard-landing-panel .leaderboard-wpm.compact { + display: flex; + align-items: center; + gap: 0.25rem; + font-size: 1rem; +} + +.leaderboard-landing-panel .leaderboard-wpm-unit { + font-size: 0.75rem; + text-transform: uppercase; + letter-spacing: 0.05em; + opacity: 0.75; +} + +.leaderboard-landing-panel .leaderboard-date.compact { + font-size: 0.78rem; + color: rgba(255, 255, 255, 0.6); +} + +.leaderboard-landing-panel .leaderboard-wpm, +.leaderboard-landing-panel .leaderboard-accuracy, +.leaderboard-landing-panel .leaderboard-date { + min-width: auto; +} + +.leaderboard-subtitle { + color: rgba(255, 255, 255, 0.55); + font-size: 0.8rem; + text-align: center; + letter-spacing: 0.4px; + padding-top: 0.65rem; + border-top: 1px solid rgba(255, 255, 255, 0.07); + margin: 0.2rem 0 0; +} + +@media (max-width: 600px) { + .leaderboard-landing-panel { + padding: 1.25rem 1rem 0.75rem; + gap: 0.75rem; + } + + .leaderboard-heading-row { + flex-direction: column; align-items: flex-start; - gap: 1.5rem; + gap: 0.6rem; + } + + .leaderboard-landing-panel .period-controls.horizontal { + width: 100%; + justify-content: center; } - - .leaderboard-landing-controls-area { - flex-basis: 200px; - flex-shrink: 0; + + .leaderboard-landing-panel .duration-controls.horizontal { + justify-content: space-between; + gap: 0.35rem; + overflow-x: auto; + padding-bottom: 0.1rem; + } + + .leaderboard-landing-panel .duration-controls.horizontal::-webkit-scrollbar { + display: none; + } + + .leaderboard-landing-content { + gap: 0.45rem; + } + + .leaderboard-landing-content .leaderboard-list { + padding: 0.3rem; + gap: 0.3rem; + } + + .leaderboard-landing-content .leaderboard-item { + padding: 0.75rem 0.85rem; + display: grid; + grid-template-columns: auto 1fr; + align-items: center; + column-gap: 0.5rem; + row-gap: 0.2rem; + } + + .leaderboard-landing-content .leaderboard-item .leaderboard-rank { + width: auto; + margin-right: 0; + } + + .leaderboard-landing-content .leaderboard-player { + gap: 0.45rem; + } + + .leaderboard-landing-content .leaderboard-player .leaderboard-netid { + font-size: 0.95rem; } - - .leaderboard-landing-list-area { - flex-grow: 1; - min-width: 0; + + .leaderboard-landing-panel .leaderboard-stats.compact { + grid-column: 1 / -1; + flex-direction: row; + justify-content: space-between; + width: 100%; } } diff --git a/client/src/components/Leaderboard.jsx b/client/src/components/Leaderboard.jsx index 94566458..e0d9f5d1 100644 --- a/client/src/components/Leaderboard.jsx +++ b/client/src/components/Leaderboard.jsx @@ -1,4 +1,4 @@ -import React, { useState, useEffect, useRef } from 'react'; +import React, { useState, useEffect, useRef, useCallback } from 'react'; import { useSocket } from '../context/SocketContext'; import { useAuth } from '../context/AuthContext'; import PropTypes from 'prop-types'; @@ -258,101 +258,126 @@ function Leaderboard({ defaultDuration = 15, defaultPeriod = 'alltime', layoutMo setShowProfileModal(true); }; + const isLandingMobile = layoutMode === 'landing' && isMobile; + const shouldShowAccuracy = !(layoutMode === 'landing' && isMobile); + + const formatDisplayDate = useCallback((timestamp) => { + if (period === 'daily') return formatRelativeTime(timestamp); + const date = new Date(timestamp); + return isLandingMobile + ? date.toLocaleDateString(undefined, { month: 'numeric', day: 'numeric' }) + : date.toLocaleDateString(); + }, [period, isLandingMobile]); + return ( <> {layoutMode === 'landing' ? (
- {/* Combined Controls Area */} -
-

Leaderboards

- {isMobile ? ( -
- - -
- ) : ( - <> - {/* Period Controls (Daily/Alltime) - Separate Row */} -
- {PERIODS.map(p => ( - - ))} -
- {/* Duration Controls (Times) */} -
- {DURATIONS.map(d => ( - - ))} -
- - )} -
-
- {(hasLoadedOnce ? showSpinner : loading) && (
Loading...

Loading Leaderboard...

)} - {error &&

Error: {error}

} - {(hasLoadedOnce ? !showSpinner : !loading) && !error && ( -
- {leaderboard.length > 0 ? ( - leaderboard.map((entry, index) => ( -
+
+

Leaderboards

+ {!isMobile && ( +
+ {PERIODS.map(p => ( + + ))} +
+ )} +
+ {isMobile ? ( +
+ + +
+ ) : ( +
+ {DURATIONS.map(d => ( + + ))} +
+ )} +
+ {(hasLoadedOnce ? showSpinner : loading) && ( +
+
+ Loading... +
+

Loading Leaderboard...

+
+ )} + {error &&

Error: {error}

} + {(hasLoadedOnce ? !showSpinner : !loading) && !error && ( +
+ {leaderboard.length > 0 ? ( + leaderboard.map((entry, index) => (
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} + key={`${entry.user_id}-${entry.created_at}`} + className={`leaderboard-item ${user && entry.netid === user.netid ? 'current-user' : ''}`} > -
- {`${entry.netid} { e.target.onerror = null; e.target.src=defaultProfileImage; }} - /> + {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} +
+
+ + {Math.round(parseFloat(entry.adjusted_wpm) || 0)} + {isLandingMobile ? WPM : ' WPM'} + + {shouldShowAccuracy && ( + {parseFloat(entry.accuracy).toFixed(1)}% + )} + + {formatDisplayDate(entry.created_at)} +
- {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.

- )}
- )} + )) + ) : ( +

No results found for this leaderboard.

+ )} +
+ )} +

Resets daily at 12:00 AM EST

diff --git a/client/src/pages/Landing.css b/client/src/pages/Landing.css index c2ba7bcd..a5c96c20 100644 --- a/client/src/pages/Landing.css +++ b/client/src/pages/Landing.css @@ -117,33 +117,37 @@ body { /* Combined Data Section */ .landing-data-section { - display: flex; + display: grid; + grid-template-columns: minmax(260px, 0.85fr) minmax(520px, 2.15fr); width: 100%; - max-width: 120vh; + max-width: min(1200px, 92vw); gap: 1.5rem; margin-top: 1rem; margin-bottom: 1rem; + align-items: stretch; } .landing-leaderboard-container { - flex: 6; /* 60% width */ background-color: var(--secondary-color, #1e1e1e); border-radius: 8px; border: 1px solid var(--hover-color, #3a3a3a); box-shadow: 0 5px 15px rgba(0, 0, 0, 0.2); - padding: 1rem; - padding-bottom: 0rem; + padding: 1.1rem; + padding-bottom: 0; + display: flex; + flex-direction: column; + height: 100%; } .landing-container { padding-top: 3vh; padding-bottom: 2rem; - max-height: fit-content; /* Added as suggested to fix extra space */ + max-height: fit-content; } /* Ensure leaderboard list within this section scrolls if needed */ .landing-leaderboard-container .leaderboard-list { - max-height: 36vh; /* PLEASE FOR THE LOVE OF GOD FIND A WAY TO MAKE THIS REPSONSIVE W/O MOVING THE DIV */ + height: 100%; } /* Right Column */ From 4b7acc27dbc24f875dcaf3e0c43c837f057edd25 Mon Sep 17 00:00:00 2001 From: Ammaar Alam Date: Tue, 4 Nov 2025 12:00:44 -0500 Subject: [PATCH 2/5] updating leaderboard pills to be rounded --- client/src/components/Leaderboard.css | 6 +++--- client/src/pages/Landing.css | 10 ++++------ 2 files changed, 7 insertions(+), 9 deletions(-) diff --git a/client/src/components/Leaderboard.css b/client/src/components/Leaderboard.css index aecef801..11f5944b 100644 --- a/client/src/components/Leaderboard.css +++ b/client/src/components/Leaderboard.css @@ -481,7 +481,7 @@ h2 { justify-content: flex-end; gap: 0.35rem; padding: 0.3rem; - border-radius: 999px; + border-radius: 12px; background: rgba(255, 255, 255, 0.06); box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.04); max-width: 320px; @@ -496,7 +496,7 @@ h2 { display: flex; align-items: center; justify-content: center; - border-radius: 999px; + border-radius: 10px; background: transparent; border: none; color: rgba(255, 255, 255, 0.75); @@ -527,7 +527,7 @@ h2 { .leaderboard-landing-panel .duration-controls.horizontal .control-button { flex: 0 0 auto; padding: 0.45rem 1.1rem; - border-radius: 999px; + border-radius: 10px; background: rgba(255, 255, 255, 0.08); border: 1px solid transparent; color: rgba(255, 255, 255, 0.78); diff --git a/client/src/pages/Landing.css b/client/src/pages/Landing.css index a5c96c20..c18cc618 100644 --- a/client/src/pages/Landing.css +++ b/client/src/pages/Landing.css @@ -128,12 +128,10 @@ body { } .landing-leaderboard-container { - background-color: var(--secondary-color, #1e1e1e); - border-radius: 8px; - border: 1px solid var(--hover-color, #3a3a3a); - box-shadow: 0 5px 15px rgba(0, 0, 0, 0.2); - padding: 1.1rem; - padding-bottom: 0; + background-color: transparent; + border: none; + box-shadow: none; + padding: 0; display: flex; flex-direction: column; height: 100%; From 21350e6477d8110db60ae9707922287b4bb193b0 Mon Sep 17 00:00:00 2001 From: Ammaar Alam Date: Tue, 4 Nov 2025 12:43:52 -0500 Subject: [PATCH 3/5] updating leaderboard pill selectors --- client/src/components/Leaderboard.css | 178 ++++++++++++++------------ client/src/components/Leaderboard.jsx | 154 +++++++++++++++------- 2 files changed, 203 insertions(+), 129 deletions(-) diff --git a/client/src/components/Leaderboard.css b/client/src/components/Leaderboard.css index 11f5944b..0e6ada38 100644 --- a/client/src/components/Leaderboard.css +++ b/client/src/components/Leaderboard.css @@ -54,6 +54,88 @@ h2 { box-shadow: 0 3px 8px rgba(245, 128, 37, 0.3); } +.segmented-toggle { + --segments: 1; + --active-index: 0; + position: relative; + display: flex; + align-items: stretch; + justify-content: stretch; + background: rgba(255, 255, 255, 0.06); + border: 1px solid rgba(255, 255, 255, 0.08); + border-radius: 12px; + padding: 0.2rem; + overflow: hidden; + gap: 0; + min-width: 0; +} + +.segmented-highlight { + position: absolute; + top: 0.2rem; + bottom: 0.2rem; + left: 0.2rem; + width: calc((100% - 0.4rem) / var(--segments)); + border-radius: 9px; + background: linear-gradient(135deg, #F58025, #ff9b52); + box-shadow: 0 12px 28px rgba(245, 128, 37, 0.42); + transform: translateX(calc(var(--active-index) * 100%)); + transition: transform 0.22s ease, width 0.22s ease; + z-index: 0; +} + +.segmented-option { + flex: 1; + position: relative; + z-index: 1; + border: none; + background: none; + background-color: transparent; + color: rgba(255, 255, 255, 0.78); + font-weight: 600; + font-size: 0.95rem; + padding: 0.45rem 0.9rem; + border-radius: 9px; + cursor: pointer; + transition: color 0.24s ease, text-shadow 0.32s ease; +} + +.segmented-option:hover, +.segmented-option:focus { + background-color: transparent; + box-shadow: none; +} + +.segmented-option .segmented-label { + position: relative; + display: inline-block; + color: inherit; + text-shadow: 0 0 0 rgba(0, 0, 0, 0); + transform: translateY(0); + transition: color 0.28s ease, text-shadow 0.36s ease, transform 0.36s ease; +} + +.segmented-option.active { + color: #161616; +} + +.segmented-option:not(.active):hover .segmented-label, +.segmented-option:not(.active):focus-visible .segmented-label { + color: #ffe0bc; + text-shadow: 0 0 6px rgba(255, 168, 92, 0.45), 0 0 14px rgba(255, 168, 92, 0.35); + transform: translateY(-1px); +} + +.segmented-option:focus-visible { + outline: none; + box-shadow: 0 0 0 2px rgba(255, 180, 98, 0.35); +} + +.segmented-option.active:focus-visible, +.segmented-option.active:hover { + color: #161616; +} + /* List Styling */ .leaderboard-list { width: 100%; @@ -472,87 +554,29 @@ h2 { } @media (max-width: 600px) { - .leaderboard-heading-row .control-group { display: none; } + .leaderboard-heading-row .segmented-toggle { display: none; } } -.leaderboard-landing-panel .period-controls.horizontal { - flex: 0 0 auto; - display: flex; - justify-content: flex-end; - gap: 0.35rem; - padding: 0.3rem; - border-radius: 12px; - background: rgba(255, 255, 255, 0.06); - box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.04); +.leaderboard-landing-panel .segmented-toggle.period-toggle { + margin-left: auto; max-width: 320px; } -.leaderboard-landing-panel .period-controls.horizontal .control-button { - flex: 1; - min-width: 0; - padding: 0.5rem 1.15rem; - font-size: 0.95rem; - font-weight: 600; - display: flex; - align-items: center; - justify-content: center; - border-radius: 10px; - background: transparent; - border: none; - color: rgba(255, 255, 255, 0.75); - transition: all 0.25s ease; -} - -.leaderboard-landing-panel .period-controls.horizontal .control-button:hover, -.leaderboard-landing-panel .period-controls.horizontal .control-button:focus-visible { - color: white; - background: rgba(255, 255, 255, 0.12); - box-shadow: 0 6px 16px rgba(0, 0, 0, 0.25); -} - -.leaderboard-landing-panel .period-controls.horizontal .control-button.active { - background: linear-gradient(135deg, #F58025, #ff9b52); - color: #161616; - box-shadow: 0 10px 22px rgba(245, 128, 37, 0.45); -} - -.leaderboard-landing-panel .duration-controls.horizontal { - display: flex; - flex-wrap: wrap; - gap: 0.45rem; +.leaderboard-landing-panel .segmented-toggle.duration-toggle { width: 100%; - justify-content: flex-start; + margin-top: 0.75rem; } -.leaderboard-landing-panel .duration-controls.horizontal .control-button { - flex: 0 0 auto; - padding: 0.45rem 1.1rem; - border-radius: 10px; - background: rgba(255, 255, 255, 0.08); - border: 1px solid transparent; - color: rgba(255, 255, 255, 0.78); - font-weight: 600; - transition: all 0.2s ease; +.leaderboard-landing-panel .segmented-toggle.duration-toggle .segmented-option { + font-size: 0.9rem; } -.leaderboard-landing-panel .duration-controls.horizontal .control-button:hover, -.leaderboard-landing-panel .duration-controls.horizontal .control-button:focus-visible { - color: white; - border-color: rgba(255, 255, 255, 0.2); - background: rgba(255, 255, 255, 0.15); - box-shadow: 0 6px 18px rgba(0, 0, 0, 0.25); +.leaderboard-controls .segmented-toggle { + min-width: 200px; } -.leaderboard-landing-panel .duration-controls.horizontal .control-button.active { - background: linear-gradient(135deg, #F58025, #ff9b52); - color: #161616; - border-color: rgba(255, 255, 255, 0.25); - box-shadow: 0 10px 22px rgba(245, 128, 37, 0.45); -} - -.leaderboard-landing-panel .control-button:focus-visible { - outline: 2px solid rgba(245, 128, 37, 0.85); - outline-offset: 2px; +.leaderboard-controls .segmented-toggle.duration-toggle { + min-width: 260px; } .leaderboard-landing-content { @@ -673,22 +697,6 @@ h2 { gap: 0.6rem; } - .leaderboard-landing-panel .period-controls.horizontal { - width: 100%; - justify-content: center; - } - - .leaderboard-landing-panel .duration-controls.horizontal { - justify-content: space-between; - gap: 0.35rem; - overflow-x: auto; - padding-bottom: 0.1rem; - } - - .leaderboard-landing-panel .duration-controls.horizontal::-webkit-scrollbar { - display: none; - } - .leaderboard-landing-content { gap: 0.45rem; } diff --git a/client/src/components/Leaderboard.jsx b/client/src/components/Leaderboard.jsx index e0d9f5d1..b1dccaba 100644 --- a/client/src/components/Leaderboard.jsx +++ b/client/src/components/Leaderboard.jsx @@ -10,6 +10,76 @@ import ProfileModal from './ProfileModal.jsx'; const DURATIONS = [15, 30, 60, 120]; const PERIODS = ['daily', 'alltime']; +function SegmentedToggle({ + options, + value, + onChange, + className = '', + ariaLabel, +}) { + const activeIndexRaw = options.findIndex(option => option.value === value); + const activeIndex = activeIndexRaw >= 0 ? activeIndexRaw : 0; + const total = options.length || 1; + const classes = ['segmented-toggle', className].filter(Boolean).join(' '); + + const handleKeyDown = (event, index) => { + if (!options.length) return; + if (event.key === 'ArrowRight' || event.key === 'ArrowDown') { + event.preventDefault(); + const next = (index + 1) % total; + onChange(options[next].value); + } else if (event.key === 'ArrowLeft' || event.key === 'ArrowUp') { + event.preventDefault(); + const prev = (index - 1 + total) % total; + onChange(options[prev].value); + } + }; + + return ( +
+ + ); +} + +SegmentedToggle.propTypes = { + options: PropTypes.arrayOf(PropTypes.shape({ + value: PropTypes.oneOfType([PropTypes.number, PropTypes.string]).isRequired, + label: PropTypes.node.isRequired, + })).isRequired, + value: PropTypes.oneOfType([PropTypes.number, PropTypes.string]).isRequired, + onChange: PropTypes.func.isRequired, + className: PropTypes.string, + ariaLabel: PropTypes.string, +}; + +SegmentedToggle.defaultProps = { + className: '', + ariaLabel: undefined, +}; + // Helper function to format relative time const formatRelativeTime = (timestamp) => { const nowUtc = Date.now(); // Current time in UTC milliseconds since epoch @@ -277,17 +347,16 @@ function Leaderboard({ defaultDuration = 15, defaultPeriod = 'alltime', layoutMo

Leaderboards

{!isMobile && ( -
- {PERIODS.map(p => ( - - ))} -
+ ({ + value: p, + label: p.charAt(0).toUpperCase() + p.slice(1), + }))} + value={period} + onChange={setPeriod} + className="period-toggle" + ariaLabel="Select leaderboard period" + /> )}
{isMobile ? ( @@ -310,17 +379,16 @@ function Leaderboard({ defaultDuration = 15, defaultPeriod = 'alltime', layoutMo
) : ( -
- {DURATIONS.map(d => ( - - ))} -
+ ({ + value: d, + label: `${d}s`, + }))} + value={duration} + onChange={setDuration} + className="duration-toggle" + ariaLabel="Select leaderboard duration" + /> )}
{(hasLoadedOnce ? showSpinner : loading) && ( @@ -385,28 +453,26 @@ function Leaderboard({ defaultDuration = 15, defaultPeriod = 'alltime', layoutMo <>

Timed Leaderboards

-
- {DURATIONS.map(d => ( - - ))} -
-
- {PERIODS.map(p => ( - - ))} -
+ ({ + value: d, + label: `${d}s`, + }))} + value={duration} + onChange={setDuration} + className="duration-toggle" + ariaLabel="Select leaderboard duration" + /> + ({ + value: p, + label: p.charAt(0).toUpperCase() + p.slice(1), + }))} + value={period} + onChange={setPeriod} + className="period-toggle" + ariaLabel="Select leaderboard period" + />
{(hasLoadedOnce ? showSpinner : loading) && (
From ef714a1d0d6db6eaef86bcc95c3d1c4229c8a78d Mon Sep 17 00:00:00 2001 From: Ammaar Alam Date: Tue, 4 Nov 2025 13:13:17 -0500 Subject: [PATCH 4/5] Update client/src/components/Leaderboard.jsx Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- client/src/components/Leaderboard.jsx | 5 ----- 1 file changed, 5 deletions(-) diff --git a/client/src/components/Leaderboard.jsx b/client/src/components/Leaderboard.jsx index b1dccaba..737dd900 100644 --- a/client/src/components/Leaderboard.jsx +++ b/client/src/components/Leaderboard.jsx @@ -75,11 +75,6 @@ SegmentedToggle.propTypes = { ariaLabel: PropTypes.string, }; -SegmentedToggle.defaultProps = { - className: '', - ariaLabel: undefined, -}; - // Helper function to format relative time const formatRelativeTime = (timestamp) => { const nowUtc = Date.now(); // Current time in UTC milliseconds since epoch From 05c1a9faee04fb58e89519506b33647104f43b70 Mon Sep 17 00:00:00 2001 From: Ammaar Alam Date: Tue, 4 Nov 2025 13:13:36 -0500 Subject: [PATCH 5/5] Update client/src/components/Leaderboard.jsx Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- client/src/components/Leaderboard.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/src/components/Leaderboard.jsx b/client/src/components/Leaderboard.jsx index 737dd900..8129e577 100644 --- a/client/src/components/Leaderboard.jsx +++ b/client/src/components/Leaderboard.jsx @@ -423,7 +423,7 @@ function Leaderboard({ defaultDuration = 15, defaultPeriod = 'alltime', layoutMo
- {Math.round(parseFloat(entry.adjusted_wpm) || 0)} + {(parseFloat(entry.adjusted_wpm) || 0).toFixed(0)} {isLandingMobile ? WPM : ' WPM'} {shouldShowAccuracy && (