diff --git a/client/src/components/Leaderboard.css b/client/src/components/Leaderboard.css index f05e2a9b..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%; @@ -382,61 +464,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,181 +540,198 @@ 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-heading-row .segmented-toggle { display: none; } } -.leaderboard-subtitle { - color: #aaa; - font-size: 0.85rem; - text-align: center; - font-style: italic; - padding: 1vh; +.leaderboard-landing-panel .segmented-toggle.period-toggle { + margin-left: auto; + max-width: 320px; } -/* Horizontal control groups for period options (Daily/Alltime) */ -.leaderboard-landing-controls-area .control-group.horizontal { - display: flex; - flex-direction: row; - justify-content: center; - gap: 0.75rem; +.leaderboard-landing-panel .segmented-toggle.duration-toggle { width: 100%; - max-width: 600px; + margin-top: 0.75rem; } -/* Vertical control groups for duration options (15s, 30s, etc.) */ -.leaderboard-landing-controls-area .control-group.vertical { +.leaderboard-landing-panel .segmented-toggle.duration-toggle .segmented-option { + font-size: 0.9rem; +} + +.leaderboard-controls .segmented-toggle { + min-width: 200px; +} + +.leaderboard-controls .segmented-toggle.duration-toggle { + min-width: 260px; +} + +.leaderboard-landing-content { display: flex; flex-direction: column; - justify-content: center; - gap: 0.75rem; - width: 100%; - max-width: 120px; + flex: 1; + gap: 0.6rem; + min-height: 0; } -/* Period controls (Daily/Alltime) styling */ -.leaderboard-landing-controls-area .period-controls.horizontal { - margin-bottom: 0.5rem; - width: 100%; +.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); + border-radius: 12px; + padding: 0.35rem; display: flex; - justify-content: center; - gap: 0.75rem; - max-width: 300px; + flex-direction: column; + gap: 0.45rem; + overflow-y: auto; + 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); } -.leaderboard-landing-controls-area .period-controls.horizontal .control-button { - width: 8vh; - padding: 0.75rem 1rem; - font-size: 1rem; - letter-spacing: 0.5px; - 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); +.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-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); - color: white; +.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-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-content .loading-indicator, +.leaderboard-landing-content .error-message, +.leaderboard-landing-content .no-results { + flex-grow: 1; + display: flex; + align-items: center; + justify-content: center; + min-height: 150px; } -/* Duration controls styling - now vertical */ -.leaderboard-landing-controls-area .duration-controls.vertical .control-button { - width: 80%; - 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; +.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-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); - color: white; +.leaderboard-landing-panel .leaderboard-stats { + gap: 1rem; + flex-wrap: wrap; + justify-content: flex-end; } -.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 .leaderboard-stats.compact { + gap: 0.3rem; + flex-wrap: nowrap; + align-items: flex-end; } -/* List Area */ -.leaderboard-landing-list-area { - flex-grow: 1; - width: 100%; +.leaderboard-landing-panel .leaderboard-wpm.compact { display: flex; - flex-direction: column; + align-items: center; + gap: 0.25rem; + font-size: 1rem; } -/* 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); - border: 1px solid rgba(255, 255, 255, 0.08); - padding: 0.5rem; - border-radius: 10px; - overflow-y: auto; - box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); +.leaderboard-landing-panel .leaderboard-wpm-unit { + font-size: 0.75rem; + text-transform: uppercase; + letter-spacing: 0.05em; + opacity: 0.75; } -/* 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); - transition: all 0.2s ease; +.leaderboard-landing-panel .leaderboard-date.compact { + font-size: 0.78rem; + color: rgba(255, 255, 255, 0.6); } -.leaderboard-landing-list-area .leaderboard-item:hover { - background-color: rgba(50, 50, 50, 0.8); - transform: translateX(2px); +.leaderboard-landing-panel .leaderboard-wpm, +.leaderboard-landing-panel .leaderboard-accuracy, +.leaderboard-landing-panel .leaderboard-date { + min-width: auto; } -.leaderboard-landing-list-area .loading-indicator, -.leaderboard-landing-list-area .error-message, -.leaderboard-landing-list-area .no-results { - flex-grow: 1; - display: flex; - align-items: center; - justify-content: center; - min-height: 150px; +.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; } -/* Responsive adjustments */ -@media (min-width: 768px) { - .leaderboard-landing-wrapper { - flex-direction: row; +@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-content { + gap: 0.45rem; + } + + .leaderboard-landing-content .leaderboard-list { + padding: 0.3rem; + gap: 0.3rem; } - - .leaderboard-landing-controls-area { - flex-basis: 200px; - flex-shrink: 0; + + .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-list-area { - flex-grow: 1; - min-width: 0; + + .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-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..8129e577 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'; @@ -10,6 +10,71 @@ 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 ( +
Loading Leaderboard...
Error: {error}
} - {(hasLoadedOnce ? !showSpinner : !loading) && !error && ( -Loading Leaderboard...
+Error: {error}
} + {(hasLoadedOnce ? !showSpinner : !loading) && !error && ( +No results found for this leaderboard.
- )}No results found for this leaderboard.
+ )} +Resets daily at 12:00 AM EST