Add mobile-responsive admin view and disable dark mode

Improve mobile experience for admin interface with responsive layouts
that stack on smaller screens, hamburger menu navigation, and force
light mode across the app.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
ryan
2026-01-18 14:49:03 -05:00
parent ab962725e6
commit 96716d95b6
5 changed files with 598 additions and 297 deletions

View File

@@ -0,0 +1,351 @@
/* GameAdminView Mobile Styles */
.admin-container {
padding: 1rem 2rem;
max-width: 1400px;
margin: 0 auto;
min-height: calc(100vh - 60px);
}
.admin-header {
margin-bottom: 1.5rem;
border-bottom: 2px solid #ccc;
padding-bottom: 1rem;
}
.admin-header h1 {
margin: 0 0 0.75rem 0;
font-size: 1.5rem;
}
.admin-header-controls {
display: flex;
gap: 1rem;
align-items: center;
flex-wrap: wrap;
}
.admin-main-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 2rem;
}
.question-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 1rem;
flex-wrap: wrap;
gap: 0.5rem;
}
.question-header h2 {
margin: 0;
}
.timer-controls {
display: flex;
gap: 0.5rem;
align-items: center;
flex-wrap: wrap;
}
.timer-display {
padding: 0.75rem 1rem;
border-radius: 4px;
font-weight: bold;
font-size: 1.2rem;
min-width: 120px;
text-align: center;
}
.question-card {
padding: 1.5rem;
border: 2px solid #2196F3;
border-radius: 8px;
background: #e3f2fd;
}
.question-text {
font-size: 1.3rem;
font-weight: bold;
margin-bottom: 1rem;
}
.answer-box {
padding: 1rem;
background: #4CAF50;
color: white;
border-radius: 4px;
}
.team-section h2 {
margin-top: 0;
}
.add-team-form {
display: flex;
gap: 0.5rem;
margin-bottom: 1rem;
flex-wrap: wrap;
}
.add-team-input {
padding: 0.5rem;
flex: 1;
min-width: 150px;
font-size: 1rem;
border-radius: 4px;
border: 1px solid #ccc;
}
.teams-list {
display: flex;
flex-direction: column;
gap: 1rem;
}
.team-card {
padding: 1rem;
border: 1px solid #ccc;
border-radius: 8px;
background: white;
}
.team-card-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 0.5rem;
flex-wrap: wrap;
gap: 0.5rem;
}
.team-name-section {
display: flex;
align-items: center;
gap: 0.5rem;
}
.team-name {
font-size: 1.2rem;
font-weight: bold;
}
.team-score {
font-size: 1.5rem;
font-weight: bold;
color: #2196F3;
}
.team-buttons {
display: flex;
gap: 0.5rem;
flex-wrap: wrap;
}
.team-buttons button {
padding: 0.5rem 1rem;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 0.9rem;
white-space: nowrap;
}
.btn-points {
background: #4CAF50;
}
.btn-minus {
background: #f44336;
}
.btn-lifeline-use {
background: #ff9800;
}
.btn-lifeline-use:disabled {
background: #ccc;
cursor: not-allowed;
}
.btn-lifeline-add {
background: #9C27B0;
}
.game-controls {
background: #f5f5f5;
border-radius: 8px;
padding: 1.5rem;
margin-top: 2rem;
}
.game-controls h2 {
margin-top: 0;
margin-bottom: 1rem;
}
.controls-header {
display: flex;
gap: 1rem;
margin-bottom: 1rem;
justify-content: space-between;
align-items: center;
flex-wrap: wrap;
}
.question-indicator {
padding: 0.75rem 1rem;
background: white;
border-radius: 4px;
font-weight: bold;
}
.controls-button-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 0.75rem;
}
.controls-button-grid button {
padding: 0.75rem 1.5rem;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 1rem;
}
/* Mobile styles */
@media (max-width: 768px) {
.admin-container {
padding: 0.75rem 1rem;
}
.admin-header h1 {
font-size: 1.2rem;
}
.admin-main-grid {
grid-template-columns: 1fr;
gap: 1.5rem;
}
.question-header {
flex-direction: column;
align-items: flex-start;
}
.timer-controls {
width: 100%;
justify-content: flex-start;
}
.timer-display {
font-size: 1rem;
padding: 0.5rem 0.75rem;
min-width: auto;
flex: 1;
}
.timer-controls button {
padding: 0.5rem 0.75rem;
font-size: 0.85rem;
}
.question-card {
padding: 1rem;
}
.question-text {
font-size: 1.1rem;
}
.team-card {
padding: 0.75rem;
}
.team-card-header {
flex-direction: column;
align-items: flex-start;
}
.team-score {
font-size: 1.2rem;
}
.team-buttons {
width: 100%;
}
.team-buttons button {
padding: 0.4rem 0.6rem;
font-size: 0.85rem;
flex: 1;
min-width: 0;
}
.game-controls {
padding: 1rem;
margin-top: 1.5rem;
}
.controls-header {
flex-direction: column;
align-items: stretch;
gap: 0.75rem;
}
.controls-button-grid {
grid-template-columns: 1fr;
}
.controls-button-grid button {
padding: 0.75rem 1rem;
}
}
/* Very small screens */
@media (max-width: 480px) {
.admin-container {
padding: 0.5rem;
}
.admin-header h1 {
font-size: 1rem;
}
.timer-display {
font-size: 0.9rem;
}
.question-text {
font-size: 1rem;
}
.team-name {
font-size: 1rem;
}
.team-buttons {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 0.4rem;
}
.team-buttons button {
padding: 0.5rem 0.3rem;
font-size: 0.8rem;
}
.add-team-form {
flex-direction: column;
}
.add-team-input {
width: 100%;
}
}

View File

@@ -4,6 +4,7 @@ import { useSocket } from "../../hooks/useSocket";
import { adminAPI, gamesAPI } from "../../services/api";
import AdminNavbar from "../common/AdminNavbar";
import AudioPlayer from "../audio/AudioPlayer";
import "./GameAdminView.css";
export default function GameAdminView() {
const { gameId } = useParams();
@@ -261,26 +262,11 @@ export default function GameAdminView() {
return (
<>
<AdminNavbar />
<div
style={{
padding: "1rem 2rem",
maxWidth: "1400px",
margin: "0 auto",
minHeight: "calc(100vh - 60px)",
}}
>
<div className="admin-container">
{/* Header */}
<div
style={{
marginBottom: "1.5rem",
borderBottom: "2px solid #ccc",
paddingBottom: "1rem",
}}
>
<h1 style={{ margin: "0 0 0.75rem 0" }}>
Game Admin - {gameState?.game_name}
</h1>
<div style={{ display: "flex", gap: "1rem", alignItems: "center" }}>
<div className="admin-header">
<h1>Game Admin - {gameState?.game_name}</h1>
<div className="admin-header-controls">
<span>{isConnected ? "● Connected" : "○ Disconnected"}</span>
<button
onClick={() => window.open(contestantViewUrl, "_blank")}
@@ -291,45 +277,25 @@ export default function GameAdminView() {
</div>
</div>
<div
style={{
display: "grid",
gridTemplateColumns: "1fr 1fr",
gap: "2rem",
}}
>
<div className="admin-main-grid">
{/* Current Question with Answer */}
<div>
<div
style={{
display: "flex",
justifyContent: "space-between",
alignItems: "center",
marginBottom: "1rem",
}}
>
<h2 style={{ margin: 0 }}>Current Question</h2>
<div
style={{ display: "flex", gap: "0.5rem", alignItems: "center" }}
>
<div className="question-header">
<h2>Current Question</h2>
<div className="timer-controls">
<div
className="timer-display"
style={{
padding: "0.75rem 1rem",
background: timerExpired
? "#ffebee"
: timerSeconds <= 10
? "#fff3e0"
: "#e8f5e9",
borderRadius: "4px",
fontWeight: "bold",
fontSize: "1.2rem",
color: timerExpired
? "#c62828"
: timerSeconds <= 10
? "#e65100"
: "#2e7d32",
minWidth: "120px",
textAlign: "center",
}}
>
{String(Math.floor(timerSeconds / 60)).padStart(2, "0")}:
@@ -385,14 +351,7 @@ export default function GameAdminView() {
</div>
</div>
{currentQuestion ? (
<div
style={{
padding: "1.5rem",
border: "2px solid #2196F3",
borderRadius: "8px",
background: "#e3f2fd",
}}
>
<div className="question-card">
{currentQuestion.type === "image" &&
currentQuestion.image_path && (
<img
@@ -414,23 +373,10 @@ export default function GameAdminView() {
gameId={gameId}
/>
)}
<p
style={{
fontSize: "1.3rem",
fontWeight: "bold",
marginBottom: "1rem",
}}
>
<p className="question-text">
{currentQuestion.question_content}
</p>
<div
style={{
padding: "1rem",
background: "#4CAF50",
color: "white",
borderRadius: "4px",
}}
>
<div className="answer-box">
<strong>Answer:</strong> {currentQuestion.answer}
</div>
</div>
@@ -440,24 +386,16 @@ export default function GameAdminView() {
</div>
{/* Team Scoring */}
<div>
<div className="team-section">
<h2>Team Scoring</h2>
<div
style={{ display: "flex", gap: "0.5rem", marginBottom: "1rem" }}
>
<div className="add-team-form">
<input
type="text"
value={newTeamName}
onChange={(e) => setNewTeamName(e.target.value)}
onKeyPress={(e) => e.key === "Enter" && handleAddTeam()}
placeholder="Enter team name"
style={{
padding: "0.5rem",
flex: 1,
fontSize: "1rem",
borderRadius: "4px",
border: "1px solid #ccc",
}}
className="add-team-input"
/>
<button
onClick={handleAddTeam}
@@ -477,42 +415,13 @@ export default function GameAdminView() {
{teams.length === 0 ? (
<p>No teams in this game</p>
) : (
<div
style={{
display: "flex",
flexDirection: "column",
gap: "1rem",
}}
>
<div className="teams-list">
{teams.map((team) => (
<div
key={team.id}
style={{
padding: "1rem",
border: "1px solid #ccc",
borderRadius: "8px",
background: "white",
}}
>
<div
style={{
display: "flex",
justifyContent: "space-between",
alignItems: "center",
marginBottom: "0.5rem",
}}
>
<div
style={{
display: "flex",
alignItems: "center",
gap: "0.5rem",
}}
>
<strong style={{ fontSize: "1.2rem" }}>
{team.name}
</strong>
<span style={{ fontSize: "1rem" }}>
<div key={team.id} className="team-card">
<div className="team-card-header">
<div className="team-name-section">
<strong className="team-name">{team.name}</strong>
<span>
{Array.from({
length: team.phone_a_friend_count || 0,
}).map((_, i) => (
@@ -520,82 +429,46 @@ export default function GameAdminView() {
))}
</span>
</div>
<span
style={{
fontSize: "1.5rem",
fontWeight: "bold",
color: "#2196F3",
}}
>
<span className="team-score">
{team.total_score} pts
</span>
</div>
<div
style={{
display: "flex",
gap: "0.5rem",
flexWrap: "wrap",
}}
>
<div className="team-buttons">
{[1, 2, 3, 5, 10].map((points) => (
<button
key={points}
onClick={() => handleAwardPoints(team.id, points)}
style={{
padding: "0.5rem 1rem",
background: "#4CAF50",
color: "white",
border: "none",
borderRadius: "4px",
cursor: "pointer",
}}
className="btn-points"
>
+{points}
</button>
))}
<button
onClick={() => handleAwardPoints(team.id, -1)}
style={{
padding: "0.5rem 1rem",
background: "#f44336",
color: "white",
border: "none",
borderRadius: "4px",
cursor: "pointer",
}}
className="btn-minus"
>
-1
</button>
<button
onClick={() => handleUseLifeline(team.id)}
disabled={team.phone_a_friend_count <= 0}
className="btn-lifeline-use"
style={{
padding: "0.5rem 1rem",
background:
team.phone_a_friend_count <= 0 ? "#ccc" : "#ff9800",
color: "white",
border: "none",
borderRadius: "4px",
cursor:
team.phone_a_friend_count <= 0
? "not-allowed"
: "pointer",
}}
>
📞 Use Lifeline
📞 Use
</button>
<button
onClick={() => handleAddLifeline(team.id)}
style={{
padding: "0.5rem 1rem",
background: "#9C27B0",
color: "white",
border: "none",
borderRadius: "4px",
cursor: "pointer",
}}
className="btn-lifeline-add"
>
📞 Add Lifeline
📞 Add
</button>
</div>
</div>
@@ -606,57 +479,28 @@ export default function GameAdminView() {
</div>
{/* Game Controls */}
<div
style={{
background: "#f5f5f5",
borderRadius: "8px",
padding: "1.5rem",
marginTop: "2rem",
}}
>
<h2 style={{ marginTop: 0, marginBottom: "1rem" }}>Game Controls</h2>
<div className="game-controls">
<h2>Game Controls</h2>
{/* Question indicator and timer */}
<div
style={{
display: "flex",
gap: "1rem",
marginBottom: "1rem",
justifyContent: "space-between",
alignItems: "center",
}}
>
<div
style={{
padding: "0.75rem 1rem",
background: "white",
borderRadius: "4px",
fontWeight: "bold",
}}
>
<div className="controls-header">
<div className="question-indicator">
Question {questionIndex + 1} of {totalQuestions}
</div>
<div
style={{ display: "flex", gap: "0.5rem", alignItems: "center" }}
>
<div className="timer-controls">
<div
className="timer-display"
style={{
padding: "0.75rem 1rem",
background: timerExpired
? "#ffebee"
: timerSeconds <= 10
? "#fff3e0"
: "#e8f5e9",
borderRadius: "4px",
fontWeight: "bold",
fontSize: "1.2rem",
color: timerExpired
? "#c62828"
: timerSeconds <= 10
? "#e65100"
: "#2e7d32",
minWidth: "120px",
textAlign: "center",
}}
>
{String(Math.floor(timerSeconds / 60)).padStart(2, "0")}:
@@ -692,14 +536,8 @@ export default function GameAdminView() {
</div>
</div>
{/* Button grid - 2 columns */}
<div
style={{
display: "grid",
gridTemplateColumns: "1fr 1fr",
gap: "0.75rem",
}}
>
{/* Button grid - 2 columns on desktop, 1 on mobile */}
<div className="controls-button-grid">
{currentQuestion && (
<button
onClick={handleToggleAnswer}

View File

@@ -0,0 +1,169 @@
/* AdminNavbar Styles */
.admin-navbar {
background: black;
padding: 1rem 2rem;
position: sticky;
top: 0;
z-index: 1000;
}
.navbar-container {
display: flex;
align-items: center;
justify-content: space-between;
}
.navbar-brand {
font-size: 1.5rem;
font-weight: bold;
color: white;
}
.navbar-menu {
display: flex;
gap: 1rem;
align-items: center;
}
.navbar-links {
display: flex;
gap: 1rem;
align-items: center;
}
.navbar-link {
text-decoration: none;
padding: 0.5rem 1rem;
border-radius: 8px;
background: transparent;
color: white;
font-weight: normal;
transition: all 0.2s ease;
}
.navbar-link:hover {
background: #333;
}
.navbar-link.active {
background: white;
color: black;
font-weight: bold;
}
.navbar-link.active:hover {
background: white;
}
.navbar-user {
display: flex;
gap: 0.75rem;
align-items: center;
margin-left: 1rem;
padding-left: 1rem;
border-left: 1px solid #444;
}
.navbar-user-name {
color: white;
font-size: 0.9rem;
}
.navbar-logout {
padding: 0.5rem 1rem;
background: #e74c3c;
color: white;
border: none;
border-radius: 6px;
cursor: pointer;
font-size: 0.9rem;
font-weight: 500;
transition: all 0.2s ease;
}
.navbar-logout:hover {
background: #c0392b;
}
/* Mobile menu button - hidden by default */
.navbar-mobile-toggle {
display: none;
background: transparent;
border: none;
color: white;
font-size: 1.5rem;
cursor: pointer;
padding: 0.5rem;
}
/* Mobile styles */
@media (max-width: 768px) {
.admin-navbar {
padding: 0.75rem 1rem;
}
.navbar-brand {
font-size: 1.2rem;
}
.navbar-mobile-toggle {
display: block;
}
.navbar-menu {
display: none;
position: absolute;
top: 100%;
left: 0;
right: 0;
background: black;
flex-direction: column;
padding: 1rem;
gap: 0.5rem;
border-top: 1px solid #333;
}
.navbar-menu.open {
display: flex;
}
.navbar-links {
flex-direction: column;
width: 100%;
gap: 0.5rem;
}
.navbar-link {
width: 100%;
text-align: center;
padding: 0.75rem 1rem;
}
.navbar-user {
flex-direction: column;
margin-left: 0;
padding-left: 0;
padding-top: 0.75rem;
border-left: none;
border-top: 1px solid #444;
width: 100%;
gap: 0.5rem;
}
.navbar-user-name {
text-align: center;
}
.navbar-logout {
width: 100%;
padding: 0.75rem 1rem;
}
}
/* Very small screens */
@media (max-width: 480px) {
.navbar-brand {
font-size: 1rem;
}
}

View File

@@ -1,9 +1,12 @@
import { useState } from "react";
import { Link, useLocation } from "react-router-dom";
import { useAuth } from "../../contexts/AuthContext";
import "./AdminNavbar.css";
export default function AdminNavbar() {
const location = useLocation();
const { user, logout } = useAuth();
const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
const navItems = [
{ path: "/", label: "Home" },
@@ -20,92 +23,45 @@ export default function AdminNavbar() {
return location.pathname.startsWith(path);
};
const handleLinkClick = () => {
setMobileMenuOpen(false);
};
return (
<nav
style={{
background: "black",
padding: "1rem 2rem",
position: "sticky",
top: 0,
zIndex: 1000,
}}
>
<div
style={{
display: "flex",
alignItems: "center",
justifyContent: "space-between",
}}
>
<nav className="admin-navbar">
<div className="navbar-container">
<div style={{ display: "flex", alignItems: "center", gap: "0.5rem" }}>
<span
style={{ fontSize: "1.5rem", fontWeight: "bold", color: "white" }}
>
🎮 Trivia Admin
</span>
<span className="navbar-brand">Trivia Admin</span>
</div>
<div style={{ display: "flex", gap: "1rem", alignItems: "center" }}>
{navItems.map((item) => (
<Link
key={item.path}
to={item.path}
style={{
textDecoration: "none",
padding: "0.5rem 1rem",
borderRadius: "8px",
background: isActive(item.path) ? "white" : "transparent",
color: isActive(item.path) ? "black" : "white",
fontWeight: isActive(item.path) ? "bold" : "normal",
transition: "all 0.2s ease",
}}
onMouseEnter={(e) => {
if (!isActive(item.path)) {
e.target.style.background = "#333";
}
}}
onMouseLeave={(e) => {
if (!isActive(item.path)) {
e.target.style.background = "transparent";
}
}}
>
{item.label}
</Link>
))}
{/* Mobile menu toggle */}
<button
className="navbar-mobile-toggle"
onClick={() => setMobileMenuOpen(!mobileMenuOpen)}
aria-label="Toggle menu"
>
{mobileMenuOpen ? "✕" : "☰"}
</button>
<div className={`navbar-menu ${mobileMenuOpen ? "open" : ""}`}>
<div className="navbar-links">
{navItems.map((item) => (
<Link
key={item.path}
to={item.path}
className={`navbar-link ${isActive(item.path) ? "active" : ""}`}
onClick={handleLinkClick}
>
{item.label}
</Link>
))}
</div>
{/* User info and logout */}
<div
style={{
display: "flex",
gap: "0.75rem",
alignItems: "center",
marginLeft: "1rem",
paddingLeft: "1rem",
borderLeft: "1px solid #444",
}}
>
<span style={{ color: "white", fontSize: "0.9rem" }}>
<div className="navbar-user">
<span className="navbar-user-name">
{user?.profile?.name || user?.profile?.email}
</span>
<button
onClick={logout}
style={{
padding: "0.5rem 1rem",
background: "#e74c3c",
color: "white",
border: "none",
borderRadius: "6px",
cursor: "pointer",
fontSize: "0.9rem",
fontWeight: "500",
transition: "all 0.2s ease",
}}
onMouseEnter={(e) => {
e.target.style.background = "#c0392b";
}}
onMouseLeave={(e) => {
e.target.style.background = "#e74c3c";
}}
>
<button onClick={logout} className="navbar-logout">
Logout
</button>
</div>

View File

@@ -3,9 +3,9 @@
line-height: 1.5;
font-weight: 400;
color-scheme: light dark;
color: rgba(255, 255, 255, 0.87);
background-color: #242424;
color-scheme: light;
color: #213547;
background-color: #ffffff;
font-synthesis: none;
text-rendering: optimizeLegibility;
@@ -43,7 +43,7 @@ button {
font-size: 1em;
font-weight: 500;
font-family: inherit;
background-color: #1a1a1a;
background-color: #f9f9f9;
cursor: pointer;
transition: border-color 0.25s;
}
@@ -54,16 +54,3 @@ button:focus,
button:focus-visible {
outline: 4px auto -webkit-focus-ring-color;
}
@media (prefers-color-scheme: light) {
:root {
color: #213547;
background-color: #ffffff;
}
a:hover {
color: #747bff;
}
button {
background-color: #f9f9f9;
}
}