diff --git a/client/src/components/Leaderboard.css b/client/src/components/Leaderboard.css index 616f6480..d1a66fe6 100644 --- a/client/src/components/Leaderboard.css +++ b/client/src/components/Leaderboard.css @@ -12,7 +12,7 @@ h2 { display: flex; justify-content: center; align-items: center; - gap: 1.5rem; /* Spacing between groups */ + gap: 1.5rem; margin-bottom: 2rem; padding: 0.75rem; background-color: rgba(20, 20, 20, 0.3); /* Darker background */ @@ -107,7 +107,12 @@ h2 { min-width: 150px; /* Ensure player name area has space */ } -/* Make more of the entry clickable when authenticated */ +.leaderboard-player-text { + display: flex; + flex-direction: column; + gap: 0.25rem; +} + .leaderboard-player.clickable { cursor: pointer; } .leaderboard-player.clickable:hover .leaderboard-netid { color: #F58025; @@ -153,9 +158,26 @@ h2 { transition: color 0.2s ease, text-shadow 0.2s ease; } +/* Title badges for leaderboard entries */ +.leaderboard-titles { + display: flex; + align-items: center; + margin-top: 0.25rem; +} + +.leaderboard-title-badge { + font-size: 0.75rem; + font-weight: 500; + color: #F58025; + border-left: 4px solid #F58025; + padding-left: 6px; + background-color: rgba(245, 128, 37, 0.1); + border-radius: 4px; +} + .leaderboard-stats { display: flex; - gap: 1.5rem; /* Increased gap */ + gap: 1.5rem; margin-left: auto; align-items: baseline; flex-shrink: 0; /* Prevent stats from shrinking too much */ @@ -201,7 +223,7 @@ h2 { } .spinner-border.text-orange { - color: #F58025 !important; /* Ensure orange color */ + color: #F58025 !important; width: 3rem; height: 3rem; } diff --git a/client/src/components/Leaderboard.jsx b/client/src/components/Leaderboard.jsx index 01f88bee..56286fef 100644 --- a/client/src/components/Leaderboard.jsx +++ b/client/src/components/Leaderboard.jsx @@ -2,6 +2,7 @@ import React, { useState, useEffect, useRef } from 'react'; import { useSocket } from '../context/SocketContext'; import { useAuth } from '../context/AuthContext'; import PropTypes from 'prop-types'; +import axios from 'axios'; import './Leaderboard.css'; import defaultProfileImage from '../assets/icons/default-profile.svg'; import ProfileModal from './ProfileModal.jsx'; @@ -29,6 +30,22 @@ const formatRelativeTime = (timestamp) => { return createdAt.toLocaleTimeString([], { hour: 'numeric', minute: '2-digit' }); }; +// Shallow comparison for arrays of title objects; assumes stable ordering +const titlesAreEqual = (a, b) => { + if (a === b) return true; + if (!Array.isArray(a) || !Array.isArray(b)) return false; + if (a.length !== b.length) return false; + for (let i = 0; i < a.length; i += 1) { + const left = a[i]; + const right = b[i]; + if (!left || !right) return false; + if (left.id !== right.id || left.name !== right.name || left.is_equipped !== right.is_equipped) { + return false; + } + } + return true; +}; + function Leaderboard({ defaultDuration = 15, defaultPeriod = 'alltime', layoutMode = 'modal' }) { const { socket } = useSocket(); // Destructure authenticated flag to check CAS login status @@ -46,6 +63,10 @@ function Leaderboard({ defaultDuration = 15, defaultPeriod = 'alltime', layoutMo const [selectedProfileNetid, setSelectedProfileNetid] = useState(null); const [showProfileModal, setShowProfileModal] = useState(false); const [isMobile, setIsMobile] = useState(false); + // State for storing fetched titles per leaderboard entry + const [leaderboardTitlesMap, setLeaderboardTitlesMap] = useState({}); + // Ref to track which netids we've initiated fetches for (to avoid duplicate requests) + const fetchingRef = useRef(new Set()); // Track viewport to switch to compact controls on small screens useEffect(() => { @@ -138,6 +159,85 @@ function Leaderboard({ defaultDuration = 15, defaultPeriod = 'alltime', layoutMo }, [socket, duration, period]); + // Fetch titles for each leaderboard entry + useEffect(() => { + if (!authenticated || leaderboard.length === 0) { + // Clear titles map and fetching ref when not authenticated or leaderboard is empty + setLeaderboardTitlesMap(prev => { + if (Object.keys(prev).length === 0) return prev; + return {}; + }); + fetchingRef.current.clear(); + return; + } + + // Get current netids in leaderboard + const currentNetids = new Set(leaderboard.map(entry => entry.netid)); + + // Clean up titles and fetching ref for users no longer in leaderboard + setLeaderboardTitlesMap(prev => { + let didRemoveEntry = false; + const cleanedEntries = Object.entries(prev).filter(([netid]) => { + const shouldKeep = currentNetids.has(netid); + if (!shouldKeep) { + didRemoveEntry = true; + } + return shouldKeep; + }); + + // Clean up fetching ref regardless of whether map entries changed + const filteredFetching = new Set(); + fetchingRef.current.forEach(netid => { + if (currentNetids.has(netid)) { + filteredFetching.add(netid); + } + }); + fetchingRef.current = filteredFetching; + + if (!didRemoveEntry) { + return prev; + } + + return Object.fromEntries(cleanedEntries); + }); + + leaderboard.forEach(entry => { + const netid = entry.netid; + + // Sync context titles for current user + if (netid === user?.netid && user?.titles) { + setLeaderboardTitlesMap(prev => { + const existingTitles = prev[netid]; + if (titlesAreEqual(existingTitles, user.titles)) return prev; // No change needed + return { ...prev, [netid]: user.titles }; + }); + } + // Fetch other users' titles if not already fetched or currently fetching + if (netid !== user?.netid && !(netid in leaderboardTitlesMap) && !fetchingRef.current.has(netid)) { + fetchingRef.current.add(netid); + axios.get(`/api/user/${netid}/titles`) + .then(res => { + const nextTitles = res.data || []; + setLeaderboardTitlesMap(prev => { + const existingTitles = prev[netid]; + if (titlesAreEqual(existingTitles, nextTitles)) return prev; + return { ...prev, [netid]: nextTitles }; + }); + fetchingRef.current.delete(netid); + }) + .catch(err => { + console.error(`Error fetching titles for ${netid}:`, err); + setLeaderboardTitlesMap(prev => { + const existingTitles = prev[netid]; + if (Array.isArray(existingTitles) && existingTitles.length === 0) return prev; + return { ...prev, [netid]: [] }; + }); + fetchingRef.current.delete(netid); + }); + } + }); + }, [leaderboard, user, authenticated, leaderboardTitlesMap]); + const handleAvatarClick = (_avatarUrl, netid) => { // Only proceed if user is authenticated via CAS if (!authenticated) return; @@ -205,40 +305,7 @@ 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) : 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.

- )} + {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} {authenticated && (() => { const titles = leaderboardTitlesMap[entry.netid]; const titleToShow = titles?.find(t => t.is_equipped); return titleToShow ? (
{titleToShow.name}
) : null; })()}
{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

@@ -290,22 +357,33 @@ 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'} - role={authenticated ? 'button' : undefined} - tabIndex={authenticated ? 0 : -1} - onKeyDown={authenticated ? (e => { if (e.key === 'Enter' || e.key === ' ') handleAvatarClick(entry.avatar_url, entry.netid); }) : undefined} - > -
+
+
handleAvatarClick(entry.avatar_url, entry.netid) : undefined} + title={authenticated ? `View ${entry.netid}\'s profile` : 'Log in to view profiles'} + > {`${entry.netid} { e.target.onerror = null; e.target.src=defaultProfileImage; }} />
- {entry.netid} +
+ {entry.netid} + {authenticated && (() => { + const titles = leaderboardTitlesMap[entry.netid]; + // Only show title if user has one equipped - no fallback to first unlocked title + const titleToShow = titles?.find(t => t.is_equipped); + return titleToShow ? ( +
+ + {titleToShow.name} + +
+ ) : null; + })()} +
{parseFloat(entry.adjusted_wpm).toFixed(0)} WPM diff --git a/client/src/components/ProfileModal.css b/client/src/components/ProfileModal.css index c1385499..a794ee43 100644 --- a/client/src/components/ProfileModal.css +++ b/client/src/components/ProfileModal.css @@ -912,6 +912,20 @@ align-items: center; } +.no-matches { + font-style: italic; + color: var(--text-color-secondary); + font-size: 1.1rem; + padding: 10px 0; + width: 100%; + text-align: center; + margin-left: 0; + min-height: 50px; + display: flex; + align-items: center; + justify-content: center; +} + .title-display.static-title { background-color: rgba(255, 255, 255, 0.05); border: 1px solid var(--hover-color); diff --git a/client/src/components/TestConfigurator.css b/client/src/components/TestConfigurator.css index d938971b..ef8cb1b3 100644 --- a/client/src/components/TestConfigurator.css +++ b/client/src/components/TestConfigurator.css @@ -259,16 +259,17 @@ max-width: 0; opacity: 0; overflow: hidden; - transition: max-width 0.3s ease-out, opacity 0.3s ease-out, margin-left 0.3s ease-out, transform 0.3s ease-out; + transition: max-width 0.4s cubic-bezier(0.4, 0, 0.2, 1), opacity 0.4s cubic-bezier(0.4, 0, 0.2, 1), margin-left 0.4s cubic-bezier(0.4, 0, 0.2, 1), transform 0.4s cubic-bezier(0.4, 0, 0.2, 1); vertical-align: middle; margin-left: 0; pointer-events: none; - transform: translateX(-20px); + transform: translateX(-10px); + will-change: max-width, opacity, transform; } .subject-filter.visible { - max-width: 200px; + max-width: 250px; opacity: 1; - margin-left: 5px; + margin-left: 0; pointer-events: auto; transform: translateX(0); } diff --git a/client/src/pages/Lobby.css b/client/src/pages/Lobby.css index 4df0e9c8..ca5fed61 100644 --- a/client/src/pages/Lobby.css +++ b/client/src/pages/Lobby.css @@ -1,19 +1,28 @@ /* Lobby Page Styles */ .lobby-page { - padding-top: 5vh; - max-width: 1200px; + padding-top: 3vh; + max-width: 1400px; margin: 0 auto; position: relative; + padding-left: 1rem; + padding-right: 1rem; } .lobby-container { background-color: var(--container-color); padding: 2rem; - border-radius: 8px; - box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2); + border-radius: 16px; + box-shadow: 0 8px 24px rgba(0, 0, 0, 0.3); + border: 2px solid rgba(245, 128, 37, 0.2); display: flex; flex-direction: column; - gap: 1rem; + gap: 1.5rem; + backdrop-filter: blur(10px); + transition: box-shadow 0.3s ease; +} + +.lobby-container:hover { + box-shadow: 0 12px 32px rgba(0, 0, 0, 0.4); } /* ──────────────────────────────────────────────────────────────── */ @@ -23,51 +32,71 @@ display: flex; justify-content: space-between; align-items: center; - border-bottom: 1px solid var(--border-color); + border-bottom: 2px solid rgba(245, 128, 37, 0.3); padding-bottom: 1rem; + margin-bottom: 0.5rem; } .lobby-header h1 { color: var(--princeton-orange); margin: 0; - font-size: 1.8rem; - font-weight: 600; + font-size: 1.75rem; + font-weight: 700; + text-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); + letter-spacing: -0.5px; } .lobby-code-display { display: flex; align-items: center; - gap: 0.5rem; - background-color: var(--type-container-color); - padding: 0.5rem 1rem; - border-radius: 4px; - font-size: 0.9rem; - border: 1px solid var(--border-color); + gap: 0.75rem; + background: linear-gradient(135deg, var(--type-container-color) 0%, rgba(245, 128, 37, 0.08) 100%); + padding: 0.65rem 1.25rem; + border-radius: 10px; + font-size: 0.95rem; + border: 1px solid rgba(245, 128, 37, 0.3); + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); + transition: all 0.3s ease; +} + +.lobby-code-display:hover { + transform: translateY(-1px); + box-shadow: 0 4px 12px rgba(245, 128, 37, 0.15); + border-color: rgba(245, 128, 37, 0.5); } .lobby-code-display span { color: var(--subtle-text-color); + font-weight: 500; } .lobby-code-display strong { - font-family: monospace; - font-size: 1.1rem; + font-family: 'Courier New', monospace; + font-size: 1.2rem; color: var(--princeton-orange); - letter-spacing: 1px; + letter-spacing: 2px; + font-weight: 700; + text-shadow: 0 1px 2px rgba(0, 0, 0, 0.1); } .lobby-code-display button { - background: none; - border: none; - color: var(--subtle-text-color); + background: rgba(245, 128, 37, 0.1); + border: 1px solid rgba(245, 128, 37, 0.3); + border-radius: 6px; + color: var(--princeton-orange); cursor: pointer; - font-size: 1.1rem; - padding: 0 0.3rem; - transition: color 0.2s; + font-size: 1rem; + padding: 0.4rem 0.5rem; + transition: all 0.2s ease; + display: flex; + align-items: center; + justify-content: center; } .lobby-code-display button:hover { - color: var(--princeton-orange); + background: rgba(245, 128, 37, 0.2); + border-color: var(--princeton-orange); + transform: scale(1.05); } .copied-message { @@ -85,33 +114,56 @@ } .leave-lobby-button { - background-color: var(--danger-bg-color); + background: linear-gradient(135deg, var(--danger-bg-color) 0%, #c82333 100%); color: white; - border: none; - padding: 0.6rem 1.2rem; - border-radius: 4px; + border: 1px solid rgba(220, 53, 69, 0.5); + padding: 0.65rem 1.35rem; + border-radius: 8px; cursor: pointer; - font-weight: 500; - transition: background-color 0.2s; + font-weight: 600; + font-size: 0.95rem; + transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); + box-shadow: 0 2px 8px rgba(220, 53, 69, 0.3); } .leave-lobby-button:hover { - background-color: var(--danger-color); + background: linear-gradient(135deg, var(--danger-color) 0%, #b21f2d 100%); + transform: translateY(-2px); + box-shadow: 0 4px 12px rgba(220, 53, 69, 0.4); +} + +.leave-lobby-button:active { + transform: translateY(0); } /* Countdown Display */ .lobby-countdown { text-align: center; - font-size: 2rem; - font-weight: bold; + font-size: 2.5rem; + font-weight: 800; color: var(--princeton-orange); - padding: 2rem 0; - animation: pulse 1s infinite; + padding: 3rem 0; + animation: pulse 1s infinite, glow 1.5s ease-in-out infinite; + text-shadow: 0 0 10px rgba(245, 128, 37, 0.3); + letter-spacing: 1px; } .lobby-countdown .countdown-value { - font-size: 3rem; - margin-left: 0.5rem; + font-size: 4rem; + margin-left: 0.75rem; + background: linear-gradient(135deg, var(--princeton-orange) 0%, #e67321 100%); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; +} + +@keyframes glow { + 0%, 100% { + text-shadow: 0 0 10px rgba(245, 128, 37, 0.3), 0 0 20px rgba(245, 128, 37, 0.2); + } + 50% { + text-shadow: 0 0 20px rgba(245, 128, 37, 0.5), 0 0 30px rgba(245, 128, 37, 0.3); + } } /* ──────────────────────────────────────────────────────────────── */ @@ -119,7 +171,7 @@ /* ──────────────────────────────────────────────────────────────── */ .lobby-main-content { display: flex; - gap: 1rem; + gap: 1.5rem; align-items: flex-start; } @@ -127,14 +179,19 @@ /* Settings Column */ /* ──────────────────────────────────────────────────────────────── */ .lobby-left-column { - flex: 1; - min-width: 300px; - padding: 1.2rem; - background-color: var(--type-container-color); - border-radius: 6px; - border: 1px solid var(--border-color); + flex: 0 0 340px; + min-width: 280px; + padding: 1.5rem; + background: linear-gradient(135deg, var(--type-container-color) 0%, rgba(245, 128, 37, 0.03) 100%); + border-radius: 12px; + border: 1px solid rgba(245, 128, 37, 0.2); position: relative; - min-height: 0; /* reduce dead space */ + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); + transition: all 0.3s ease; +} + +.lobby-left-column:hover { + box-shadow: 0 6px 16px rgba(0, 0, 0, 0.2); } .lobby-settings { @@ -146,11 +203,13 @@ } .lobby-settings h2 { - margin: 0 0 0.8rem 0; + margin: 0 0 1rem 0; color: var(--mode-text-color); - font-size: 1.3rem; - border-bottom: 1px solid var(--border-color); - padding-bottom: 0.4rem; + font-size: 1.35rem; + font-weight: 700; + border-bottom: 2px solid rgba(245, 128, 37, 0.3); + padding-bottom: 0.75rem; + letter-spacing: -0.3px; } /* --- TestConfigurator overrides for vertical lobby layout --- */ @@ -162,7 +221,7 @@ width: 100%; display: flex; flex-direction: column; - gap: 10px; /* tighten spacing */ + gap: 14px; overflow: visible; } @@ -191,7 +250,7 @@ opacity: 0; overflow: hidden; height: auto; - transition: max-height 0.35s ease, opacity 0.35s ease; + transition: max-height 0.4s cubic-bezier(0.4, 0, 0.2, 1), opacity 0.4s cubic-bezier(0.4, 0, 0.2, 1); pointer-events: none; visibility: hidden; margin-top: 0; @@ -199,7 +258,7 @@ /* Visible state when React adds .visible */ .lobby-settings .test-configurator .options-wrapper.visible { - margin-top: 12px; + margin-top: 0; max-height: 600px; opacity: 1; pointer-events: auto; @@ -212,8 +271,8 @@ display: flex; flex-direction: column; align-items: stretch; - gap: 12px; - padding: 10px 0 0 0; + gap: 14px; + padding: 0; width: 100%; } @@ -237,16 +296,21 @@ padding: 4px 10px; } -/* Department filter specific styling for lobby */ -.lobby-settings .test-configurator .department-filter { +/* Subject/Department filter specific styling for lobby vertical layout */ +.lobby-settings .test-configurator .subject-filter { display: flex; width: 100%; + max-width: 100%; margin-left: 0; + transform: translateY(-10px); + transition: max-height 0.4s cubic-bezier(0.4, 0, 0.2, 1), opacity 0.4s cubic-bezier(0.4, 0, 0.2, 1), transform 0.4s cubic-bezier(0.4, 0, 0.2, 1); + max-height: 0; } -.lobby-settings .test-configurator .department-filter.visible { +.lobby-settings .test-configurator .subject-filter.visible { max-width: 100%; - transform: none; + max-height: 100px; + transform: translateY(0); margin-left: 0; } @@ -275,18 +339,62 @@ display: none; } +/* Read-only settings for non-host players */ +.read-only-settings { + display: flex; + flex-direction: column; + gap: 0.75rem; + padding: 1.25rem; + background: linear-gradient(135deg, rgba(245, 128, 37, 0.05) 0%, rgba(245, 128, 37, 0.02) 100%); + border-radius: 10px; + border: 1px solid rgba(245, 128, 37, 0.25); +} + +.read-only-settings p { + margin: 0; + padding: 0.6rem 0.9rem; + background: var(--container-color); + border-radius: 8px; + border-left: 3px solid var(--princeton-orange); + font-size: 0.95rem; + display: flex; + align-items: center; + gap: 0.5rem; + transition: all 0.2s ease; +} + +.read-only-settings p:hover { + transform: translateX(2px); + box-shadow: 0 2px 8px rgba(245, 128, 37, 0.15); +} + +.read-only-settings p i { + font-style: italic; + color: var(--subtle-text-color); + font-size: 0.9rem; + border-left: none; + padding: 0; + background: transparent; +} + /* ──────────────────────────────────────────────────────────────── */ /* Players / Controls Column */ /* ──────────────────────────────────────────────────────────────── */ .lobby-right-column { - flex: 2; + flex: 1; display: flex; flex-direction: column; - gap: 1.5rem; + gap: 1.25rem; padding: 1.5rem; - background-color: var(--type-container-color); - border-radius: 6px; - border: 1px solid var(--border-color); + background: linear-gradient(135deg, var(--type-container-color) 0%, rgba(245, 128, 37, 0.03) 100%); + border-radius: 12px; + border: 1px solid rgba(245, 128, 37, 0.2); + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); + transition: all 0.3s ease; +} + +.lobby-right-column:hover { + box-shadow: 0 6px 16px rgba(0, 0, 0, 0.2); } /* Start warning inline toast below Start button */ @@ -299,13 +407,15 @@ .start-warning { font-size: 0.9rem; + font-weight: 500; color: var(--danger-color); - background: rgba(220, 53, 69, 0.08); - border: 1px solid rgba(220, 53, 69, 0.35); - padding: 6px 10px; - border-radius: 4px; + background: linear-gradient(135deg, rgba(220, 53, 69, 0.12) 0%, rgba(220, 53, 69, 0.08) 100%); + border: 1px solid rgba(220, 53, 69, 0.4); + padding: 8px 14px; + border-radius: 8px; width: fit-content; animation: fadeInOut 2.2s forwards; + box-shadow: 0 2px 8px rgba(220, 53, 69, 0.15); } /* Player Grid Area */ @@ -318,38 +428,56 @@ .lobby-players h2 { margin-top: 0; - margin-bottom: 1.5rem; + margin-bottom: 1rem; color: var(--mode-text-color); font-size: 1.3rem; - border-bottom: 1px solid var(--border-color); - padding-bottom: 0.5rem; + font-weight: 700; + border-bottom: 2px solid rgba(245, 128, 37, 0.3); + padding-bottom: 0.6rem; + letter-spacing: -0.3px; } .player-grid { display: grid; - grid-template-columns: repeat(2, 1fr); + grid-template-columns: repeat(auto-fill, minmax(160px, 1fr)); gap: 1rem; } .lobby-page .player-card { - background-color: var(--container-color); - border-radius: 6px; + background: linear-gradient(135deg, var(--container-color) 0%, rgba(245, 128, 37, 0.05) 100%); + border-radius: 10px; display: flex; flex-direction: column; - align-items: center; + align-items: stretch; text-align: center; - border: 1px solid var(--border-color); - box-shadow: none; - min-height: 120px; - justify-content: center; - transition: transform 0.2s ease, box-shadow 0.2s ease; + border: 1px solid rgba(245, 128, 37, 0.2); + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); + min-height: 105px; + transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); position: relative; overflow: hidden; } +.lobby-page .player-card::before { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + height: 3px; + background: linear-gradient(90deg, transparent, var(--princeton-orange), transparent); + opacity: 0; + transition: opacity 0.3s ease; +} + .lobby-page .player-card:hover:not(.empty-slot) { - transform: translateY(-3px); - box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15); + transform: translateY(-4px); + box-shadow: 0 8px 20px rgba(245, 128, 37, 0.15); + border-color: rgba(245, 128, 37, 0.4); +} + +.lobby-page .player-card:hover:not(.empty-slot)::before { + opacity: 1; } /* Make profile widget clickable */ @@ -359,15 +487,22 @@ } .lobby-page .player-card.empty-slot { - border: 2px dashed var(--border-color); - background-color: transparent; + border: 2px dashed rgba(245, 128, 37, 0.3); + background: rgba(0, 0, 0, 0.1); color: var(--subtle-text-color); display: flex; justify-content: center; align-items: center; font-style: italic; box-shadow: none; - min-height: 120px; + min-height: 105px; + font-size: 0.9rem; + transition: all 0.3s ease; +} + +.lobby-page .player-card.empty-slot:hover { + border-color: rgba(245, 128, 37, 0.5); + background: rgba(245, 128, 37, 0.05); } .lobby-page .player-card .profile-widget-clickable { @@ -378,146 +513,244 @@ .lobby-page .player-card .profile-widget { text-decoration: none; color: inherit; - padding: 1rem; + padding: 0.75rem 0.75rem 0.5rem; width: 100%; - height: 100%; box-sizing: border-box; - flex-grow: 1; + flex: 1; background-color: var(--container-color); display: flex; flex-direction: column; align-items: center; justify-content: center; + min-height: 0; + overflow: hidden; } .lobby-page .player-card .profile-widget:hover { text-decoration: none; } +/* Make profile widgets compact in lobby */ +.lobby-page .player-card .profile-widget { + flex-direction: column; + padding: 0.75rem 0.65rem 0.5rem; + height: auto; +} + +.lobby-page .player-card .profile-widget .profile-image { + width: 42px; + height: 42px; + margin-right: 0; + margin-bottom: 0.4rem; + flex-shrink: 0; +} + +.lobby-page .player-card .profile-widget .profile-info { + min-width: 0; + width: 100%; + flex: 1; + overflow: hidden; + text-align: center; +} + +.lobby-page .player-card .profile-widget .profile-name { + font-size: 0.9rem; + font-weight: 600; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + max-width: 100%; + margin-bottom: 0.15rem; +} + +.lobby-page .player-card .profile-widget .profile-details { + font-size: 0.8rem; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + justify-content: center; + flex-wrap: nowrap; +} + +.lobby-page .player-card .profile-widget .profile-title-inline { + font-size: 0.75rem; + max-width: 100%; + overflow: hidden; + text-overflow: ellipsis; +} + .lobby-page .player-status { display: flex; gap: 0.5rem; - font-size: 0.8rem; + font-size: 0.75rem; font-weight: 500; - padding: 0.5rem 1rem; + padding: 0.5rem 0.8rem; justify-content: center; + align-items: center; width: 100%; box-sizing: border-box; position: relative; z-index: 2; + background: rgba(0, 0, 0, 0.05); + border-top: 1px solid rgba(245, 128, 37, 0.1); + margin-top: auto; } .lobby-page .host-tag, .lobby-page .ready-tag, .lobby-page .not-ready-tag { - padding: 0.2rem 0.5rem; - border-radius: 10px; + padding: 0.25rem 0.55rem; + border-radius: 6px; text-transform: uppercase; - letter-spacing: 0.5px; + letter-spacing: 0.6px; + font-weight: 600; + font-size: 0.7rem; + transition: all 0.2s ease; } .lobby-page .host-tag { - background-color: rgba(245, 128, 37, 0.2); + background: linear-gradient(135deg, rgba(245, 128, 37, 0.25) 0%, rgba(245, 128, 37, 0.15) 100%); color: var(--princeton-orange); - border: 1px solid rgba(245, 128, 37, 0.4); + border: 1px solid rgba(245, 128, 37, 0.5); + box-shadow: 0 2px 4px rgba(245, 128, 37, 0.2); } .lobby-page .ready-tag { - background-color: rgba(40, 167, 69, 0.2); + background: linear-gradient(135deg, rgba(40, 167, 69, 0.25) 0%, rgba(40, 167, 69, 0.15) 100%); color: var(--success-color); - border: 1px solid rgba(40, 167, 69, 0.4); + border: 1px solid rgba(40, 167, 69, 0.5); + box-shadow: 0 2px 4px rgba(40, 167, 69, 0.2); } .lobby-page .not-ready-tag { - background-color: rgba(108, 117, 125, 0.2); + background: linear-gradient(135deg, rgba(108, 117, 125, 0.2) 0%, rgba(108, 117, 125, 0.1) 100%); color: var(--subtle-text-color); border: 1px solid rgba(108, 117, 125, 0.4); } .kick-button { position: absolute; - top: 5px; - right: 5px; - background-color: rgba(220, 53, 69, 0.7); + top: 8px; + right: 8px; + background: linear-gradient(135deg, rgba(220, 53, 69, 0.9) 0%, rgba(200, 35, 51, 0.95) 100%); color: white; - border: none; - border-radius: 50%; - width: 24px; - height: 24px; - font-size: 0.7rem; - font-weight: bold; + border: 1px solid rgba(255, 255, 255, 0.3); + border-radius: 8px; + padding: 0.35rem 0.65rem; + font-size: 0.75rem; + font-weight: 600; cursor: pointer; display: flex; justify-content: center; align-items: center; + gap: 0.25rem; + line-height: 1; + transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); + opacity: 0; + transform: translateY(-5px); + box-shadow: 0 2px 8px rgba(220, 53, 69, 0.4); + z-index: 10; +} + +.kick-button::before { + content: '×'; + font-size: 1.1rem; + font-weight: 700; line-height: 1; - transition: background-color 0.2s, transform 0.2s; - padding: 0; - opacity: 0.6; } .player-card:hover .kick-button { opacity: 1; + transform: translateY(0); } .kick-button:hover { - background-color: var(--danger-color); - transform: scale(1.1); + background: linear-gradient(135deg, var(--danger-color) 0%, #b21f2d 100%); + transform: scale(1.05) translateY(0); + box-shadow: 0 4px 12px rgba(220, 53, 69, 0.6); + border-color: rgba(255, 255, 255, 0.5); +} + +.kick-button:active { + transform: scale(0.95); } /* Controls Area */ .lobby-controls { display: flex; justify-content: center; - padding-top: 1rem; + padding-top: 0.75rem; border-top: 1px solid var(--border-color); margin-top: auto; } .start-race-button, .lobby-page .ready-button { - padding: 0.8rem 2rem; - font-size: 1.1rem; - font-weight: 600; - border-radius: 5px; + padding: 0.9rem 2.5rem; + font-size: 1.15rem; + font-weight: 700; + border-radius: 12px; cursor: pointer; - transition: all 0.2s ease; + transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); border: none; + letter-spacing: 0.3px; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); } .start-race-button { - background-color: var(--princeton-orange); + background: linear-gradient(135deg, var(--princeton-orange) 0%, #e67321 100%); color: white; + border: 1px solid rgba(255, 255, 255, 0.2); + text-shadow: 0 1px 2px rgba(0, 0, 0, 0.2); } .start-race-button:hover:not(:disabled) { - background-color: #e0701b; /* Darker orange */ + background: linear-gradient(135deg, #e67321 0%, #d96415 100%); + transform: translateY(-3px); + box-shadow: 0 6px 20px rgba(245, 128, 37, 0.4); + border-color: rgba(255, 255, 255, 0.3); +} + +.start-race-button:active:not(:disabled) { transform: translateY(-1px); - box-shadow: 0 3px 6px rgba(0, 0, 0, 0.2); + box-shadow: 0 4px 12px rgba(245, 128, 37, 0.3); } .start-race-button:disabled { - background-color: #aaa; + background: linear-gradient(135deg, #888 0%, #666 100%); cursor: not-allowed; - opacity: 0.7; + opacity: 0.6; + box-shadow: none; } .lobby-page .ready-button { - background-color: var(--subtle-bg-color); + background: linear-gradient(135deg, var(--subtle-bg-color) 0%, rgba(245, 128, 37, 0.05) 100%); color: var(--mode-text-color); - border: 1px solid var(--border-color); + border: 2px solid rgba(245, 128, 37, 0.3); } .lobby-page .ready-button:hover { - background-color: var(--hover-bg-color); + background: linear-gradient(135deg, var(--hover-bg-color) 0%, rgba(245, 128, 37, 0.1) 100%); border-color: var(--princeton-orange); + transform: translateY(-3px); + box-shadow: 0 6px 20px rgba(245, 128, 37, 0.25); +} + +.lobby-page .ready-button:active { + transform: translateY(-1px); } .lobby-page .ready-button.is-ready { - background-color: var(--success-bg-color); + background: linear-gradient(135deg, var(--success-bg-color) 0%, rgba(40, 167, 69, 0.3) 100%); color: var(--success-color); - border-color: var(--success-color); - font-weight: bold; + border: 2px solid var(--success-color); + font-weight: 700; + box-shadow: 0 4px 12px rgba(40, 167, 69, 0.3); +} + +.lobby-page .ready-button.is-ready:hover { + transform: translateY(-3px); + box-shadow: 0 6px 20px rgba(40, 167, 69, 0.4); } /* ──────────────────────────────────────────────────────────────── */ @@ -530,36 +763,47 @@ .lobby-race-active, .lobby-results { padding: 1.5rem; - background-color: var(--type-container-color); - border-radius: 6px; - border: 1px solid var(--border-color); + background: linear-gradient(135deg, var(--type-container-color) 0%, rgba(245, 128, 37, 0.03) 100%); + border-radius: 12px; + border: 1px solid rgba(245, 128, 37, 0.2); + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); } .lobby-results h2 { text-align: center; color: var(--princeton-orange); margin-bottom: 1.5rem; + font-weight: 700; + font-size: 1.5rem; + text-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); } .lobby-results button { margin-top: 1rem; - padding: 0.7rem 1.5rem; - font-size: 1rem; - font-weight: 500; - border-radius: 5px; + padding: 0.8rem 1.75rem; + font-size: 1.05rem; + font-weight: 600; + border-radius: 10px; cursor: pointer; - transition: all 0.2s ease; - border: 1px solid var(--border-color); - background-color: var(--subtle-bg-color); + transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); + border: 2px solid rgba(245, 128, 37, 0.3); + background: linear-gradient(135deg, var(--subtle-bg-color) 0%, rgba(245, 128, 37, 0.05) 100%); color: var(--mode-text-color); display: block; margin-left: auto; margin-right: auto; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); } .lobby-results button:hover { - background-color: var(--hover-bg-color); + background: linear-gradient(135deg, var(--hover-bg-color) 0%, rgba(245, 128, 37, 0.1) 100%); border-color: var(--princeton-orange); + transform: translateY(-2px); + box-shadow: 0 4px 12px rgba(245, 128, 37, 0.25); +} + +.lobby-results button:active { + transform: translateY(0); } /* ──────────────────────────────────────────────────────────────── */ @@ -572,22 +816,32 @@ .error-page h2 { color: var(--danger-color); + font-weight: 700; + font-size: 1.5rem; } .error-page button { - background-color: var(--princeton-orange); + background: linear-gradient(135deg, var(--princeton-orange) 0%, #e67321 100%); color: white; - border: none; - padding: 0.6rem 1.2rem; - border-radius: 4px; + border: 1px solid rgba(255, 255, 255, 0.2); + padding: 0.8rem 1.5rem; + border-radius: 10px; cursor: pointer; - font-weight: 500; - transition: background-color 0.2s; + font-weight: 600; + font-size: 1rem; + transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); margin-top: 1rem; + box-shadow: 0 4px 12px rgba(245, 128, 37, 0.3); } .error-page button:hover { - background-color: #e0701b; + background: linear-gradient(135deg, #e67321 0%, #d96415 100%); + transform: translateY(-2px); + box-shadow: 0 6px 20px rgba(245, 128, 37, 0.4); +} + +.error-page button:active { + transform: translateY(0); } /* ──────────────────────────────────────────────────────────────── */ @@ -622,9 +876,10 @@ min-width: 100%; flex: none; padding: 1.5rem; - background-color: var(--type-container-color); - border-radius: 6px; - border: 1px solid var(--border-color); + background: linear-gradient(135deg, var(--type-container-color) 0%, rgba(245, 128, 37, 0.03) 100%); + border-radius: 12px; + border: 1px solid rgba(245, 128, 37, 0.2); + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); } .lobby-right-column { @@ -632,7 +887,7 @@ } .player-grid { - grid-template-columns: repeat(auto-fit, minmax(130px, 1fr)); + grid-template-columns: repeat(auto-fill, minmax(160px, 1fr)); } /* Convert TestConfigurator back to horizontal row on tablets */ @@ -660,6 +915,7 @@ @media (max-width: 768px) { .lobby-container { padding: 1.5rem; + border-radius: 12px; } .lobby-header { @@ -678,18 +934,17 @@ } .player-grid { - grid-template-columns: repeat(2, minmax(120px, 1fr)); - gap: 0.8rem; + grid-template-columns: repeat(2, minmax(145px, 1fr)); + gap: 0.9rem; } - .player-card { - padding: 0.8rem; - min-height: 110px; + .lobby-page .player-card { + min-height: 100px; } .lobby-left-column, .lobby-right-column { - padding: 1rem; + padding: 1.25rem; } .lobby-settings .test-configurator { @@ -703,4 +958,15 @@ .lobby-settings .test-configurator .config-select { min-width: 0; } + + .start-race-button, + .lobby-page .ready-button { + padding: 0.8rem 2rem; + font-size: 1rem; + } + + .kick-button { + font-size: 0.7rem; + padding: 0.3rem 0.55rem; + } }