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:
351
frontend/frontend/src/components/admin/GameAdminView.css
Normal file
351
frontend/frontend/src/components/admin/GameAdminView.css
Normal 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%;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,6 +4,7 @@ import { useSocket } from "../../hooks/useSocket";
|
|||||||
import { adminAPI, gamesAPI } from "../../services/api";
|
import { adminAPI, gamesAPI } from "../../services/api";
|
||||||
import AdminNavbar from "../common/AdminNavbar";
|
import AdminNavbar from "../common/AdminNavbar";
|
||||||
import AudioPlayer from "../audio/AudioPlayer";
|
import AudioPlayer from "../audio/AudioPlayer";
|
||||||
|
import "./GameAdminView.css";
|
||||||
|
|
||||||
export default function GameAdminView() {
|
export default function GameAdminView() {
|
||||||
const { gameId } = useParams();
|
const { gameId } = useParams();
|
||||||
@@ -261,26 +262,11 @@ export default function GameAdminView() {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<AdminNavbar />
|
<AdminNavbar />
|
||||||
<div
|
<div className="admin-container">
|
||||||
style={{
|
|
||||||
padding: "1rem 2rem",
|
|
||||||
maxWidth: "1400px",
|
|
||||||
margin: "0 auto",
|
|
||||||
minHeight: "calc(100vh - 60px)",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div
|
<div className="admin-header">
|
||||||
style={{
|
<h1>Game Admin - {gameState?.game_name}</h1>
|
||||||
marginBottom: "1.5rem",
|
<div className="admin-header-controls">
|
||||||
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" }}>
|
|
||||||
<span>{isConnected ? "● Connected" : "○ Disconnected"}</span>
|
<span>{isConnected ? "● Connected" : "○ Disconnected"}</span>
|
||||||
<button
|
<button
|
||||||
onClick={() => window.open(contestantViewUrl, "_blank")}
|
onClick={() => window.open(contestantViewUrl, "_blank")}
|
||||||
@@ -291,45 +277,25 @@ export default function GameAdminView() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div className="admin-main-grid">
|
||||||
style={{
|
|
||||||
display: "grid",
|
|
||||||
gridTemplateColumns: "1fr 1fr",
|
|
||||||
gap: "2rem",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{/* Current Question with Answer */}
|
{/* Current Question with Answer */}
|
||||||
<div>
|
<div>
|
||||||
|
<div className="question-header">
|
||||||
|
<h2>Current Question</h2>
|
||||||
|
<div className="timer-controls">
|
||||||
<div
|
<div
|
||||||
|
className="timer-display"
|
||||||
style={{
|
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
|
|
||||||
style={{
|
|
||||||
padding: "0.75rem 1rem",
|
|
||||||
background: timerExpired
|
background: timerExpired
|
||||||
? "#ffebee"
|
? "#ffebee"
|
||||||
: timerSeconds <= 10
|
: timerSeconds <= 10
|
||||||
? "#fff3e0"
|
? "#fff3e0"
|
||||||
: "#e8f5e9",
|
: "#e8f5e9",
|
||||||
borderRadius: "4px",
|
|
||||||
fontWeight: "bold",
|
|
||||||
fontSize: "1.2rem",
|
|
||||||
color: timerExpired
|
color: timerExpired
|
||||||
? "#c62828"
|
? "#c62828"
|
||||||
: timerSeconds <= 10
|
: timerSeconds <= 10
|
||||||
? "#e65100"
|
? "#e65100"
|
||||||
: "#2e7d32",
|
: "#2e7d32",
|
||||||
minWidth: "120px",
|
|
||||||
textAlign: "center",
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
⏱️ {String(Math.floor(timerSeconds / 60)).padStart(2, "0")}:
|
⏱️ {String(Math.floor(timerSeconds / 60)).padStart(2, "0")}:
|
||||||
@@ -385,14 +351,7 @@ export default function GameAdminView() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{currentQuestion ? (
|
{currentQuestion ? (
|
||||||
<div
|
<div className="question-card">
|
||||||
style={{
|
|
||||||
padding: "1.5rem",
|
|
||||||
border: "2px solid #2196F3",
|
|
||||||
borderRadius: "8px",
|
|
||||||
background: "#e3f2fd",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{currentQuestion.type === "image" &&
|
{currentQuestion.type === "image" &&
|
||||||
currentQuestion.image_path && (
|
currentQuestion.image_path && (
|
||||||
<img
|
<img
|
||||||
@@ -414,23 +373,10 @@ export default function GameAdminView() {
|
|||||||
gameId={gameId}
|
gameId={gameId}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<p
|
<p className="question-text">
|
||||||
style={{
|
|
||||||
fontSize: "1.3rem",
|
|
||||||
fontWeight: "bold",
|
|
||||||
marginBottom: "1rem",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{currentQuestion.question_content}
|
{currentQuestion.question_content}
|
||||||
</p>
|
</p>
|
||||||
<div
|
<div className="answer-box">
|
||||||
style={{
|
|
||||||
padding: "1rem",
|
|
||||||
background: "#4CAF50",
|
|
||||||
color: "white",
|
|
||||||
borderRadius: "4px",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<strong>Answer:</strong> {currentQuestion.answer}
|
<strong>Answer:</strong> {currentQuestion.answer}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -440,24 +386,16 @@ export default function GameAdminView() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Team Scoring */}
|
{/* Team Scoring */}
|
||||||
<div>
|
<div className="team-section">
|
||||||
<h2>Team Scoring</h2>
|
<h2>Team Scoring</h2>
|
||||||
<div
|
<div className="add-team-form">
|
||||||
style={{ display: "flex", gap: "0.5rem", marginBottom: "1rem" }}
|
|
||||||
>
|
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={newTeamName}
|
value={newTeamName}
|
||||||
onChange={(e) => setNewTeamName(e.target.value)}
|
onChange={(e) => setNewTeamName(e.target.value)}
|
||||||
onKeyPress={(e) => e.key === "Enter" && handleAddTeam()}
|
onKeyPress={(e) => e.key === "Enter" && handleAddTeam()}
|
||||||
placeholder="Enter team name"
|
placeholder="Enter team name"
|
||||||
style={{
|
className="add-team-input"
|
||||||
padding: "0.5rem",
|
|
||||||
flex: 1,
|
|
||||||
fontSize: "1rem",
|
|
||||||
borderRadius: "4px",
|
|
||||||
border: "1px solid #ccc",
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
onClick={handleAddTeam}
|
onClick={handleAddTeam}
|
||||||
@@ -477,42 +415,13 @@ export default function GameAdminView() {
|
|||||||
{teams.length === 0 ? (
|
{teams.length === 0 ? (
|
||||||
<p>No teams in this game</p>
|
<p>No teams in this game</p>
|
||||||
) : (
|
) : (
|
||||||
<div
|
<div className="teams-list">
|
||||||
style={{
|
|
||||||
display: "flex",
|
|
||||||
flexDirection: "column",
|
|
||||||
gap: "1rem",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{teams.map((team) => (
|
{teams.map((team) => (
|
||||||
<div
|
<div key={team.id} className="team-card">
|
||||||
key={team.id}
|
<div className="team-card-header">
|
||||||
style={{
|
<div className="team-name-section">
|
||||||
padding: "1rem",
|
<strong className="team-name">{team.name}</strong>
|
||||||
border: "1px solid #ccc",
|
<span>
|
||||||
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" }}>
|
|
||||||
{Array.from({
|
{Array.from({
|
||||||
length: team.phone_a_friend_count || 0,
|
length: team.phone_a_friend_count || 0,
|
||||||
}).map((_, i) => (
|
}).map((_, i) => (
|
||||||
@@ -520,82 +429,46 @@ export default function GameAdminView() {
|
|||||||
))}
|
))}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<span
|
<span className="team-score">
|
||||||
style={{
|
|
||||||
fontSize: "1.5rem",
|
|
||||||
fontWeight: "bold",
|
|
||||||
color: "#2196F3",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{team.total_score} pts
|
{team.total_score} pts
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div className="team-buttons">
|
||||||
style={{
|
|
||||||
display: "flex",
|
|
||||||
gap: "0.5rem",
|
|
||||||
flexWrap: "wrap",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{[1, 2, 3, 5, 10].map((points) => (
|
{[1, 2, 3, 5, 10].map((points) => (
|
||||||
<button
|
<button
|
||||||
key={points}
|
key={points}
|
||||||
onClick={() => handleAwardPoints(team.id, points)}
|
onClick={() => handleAwardPoints(team.id, points)}
|
||||||
style={{
|
className="btn-points"
|
||||||
padding: "0.5rem 1rem",
|
|
||||||
background: "#4CAF50",
|
|
||||||
color: "white",
|
|
||||||
border: "none",
|
|
||||||
borderRadius: "4px",
|
|
||||||
cursor: "pointer",
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
+{points}
|
+{points}
|
||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
<button
|
<button
|
||||||
onClick={() => handleAwardPoints(team.id, -1)}
|
onClick={() => handleAwardPoints(team.id, -1)}
|
||||||
style={{
|
className="btn-minus"
|
||||||
padding: "0.5rem 1rem",
|
|
||||||
background: "#f44336",
|
|
||||||
color: "white",
|
|
||||||
border: "none",
|
|
||||||
borderRadius: "4px",
|
|
||||||
cursor: "pointer",
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
-1
|
-1
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => handleUseLifeline(team.id)}
|
onClick={() => handleUseLifeline(team.id)}
|
||||||
disabled={team.phone_a_friend_count <= 0}
|
disabled={team.phone_a_friend_count <= 0}
|
||||||
|
className="btn-lifeline-use"
|
||||||
style={{
|
style={{
|
||||||
padding: "0.5rem 1rem",
|
|
||||||
background:
|
background:
|
||||||
team.phone_a_friend_count <= 0 ? "#ccc" : "#ff9800",
|
team.phone_a_friend_count <= 0 ? "#ccc" : "#ff9800",
|
||||||
color: "white",
|
|
||||||
border: "none",
|
|
||||||
borderRadius: "4px",
|
|
||||||
cursor:
|
cursor:
|
||||||
team.phone_a_friend_count <= 0
|
team.phone_a_friend_count <= 0
|
||||||
? "not-allowed"
|
? "not-allowed"
|
||||||
: "pointer",
|
: "pointer",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
📞 Use Lifeline
|
📞 Use
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => handleAddLifeline(team.id)}
|
onClick={() => handleAddLifeline(team.id)}
|
||||||
style={{
|
className="btn-lifeline-add"
|
||||||
padding: "0.5rem 1rem",
|
|
||||||
background: "#9C27B0",
|
|
||||||
color: "white",
|
|
||||||
border: "none",
|
|
||||||
borderRadius: "4px",
|
|
||||||
cursor: "pointer",
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
📞 Add Lifeline
|
📞 Add
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -606,57 +479,28 @@ export default function GameAdminView() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Game Controls */}
|
{/* Game Controls */}
|
||||||
<div
|
<div className="game-controls">
|
||||||
style={{
|
<h2>Game Controls</h2>
|
||||||
background: "#f5f5f5",
|
|
||||||
borderRadius: "8px",
|
|
||||||
padding: "1.5rem",
|
|
||||||
marginTop: "2rem",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<h2 style={{ marginTop: 0, marginBottom: "1rem" }}>Game Controls</h2>
|
|
||||||
|
|
||||||
{/* Question indicator and timer */}
|
{/* Question indicator and timer */}
|
||||||
<div
|
<div className="controls-header">
|
||||||
style={{
|
<div className="question-indicator">
|
||||||
display: "flex",
|
|
||||||
gap: "1rem",
|
|
||||||
marginBottom: "1rem",
|
|
||||||
justifyContent: "space-between",
|
|
||||||
alignItems: "center",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
padding: "0.75rem 1rem",
|
|
||||||
background: "white",
|
|
||||||
borderRadius: "4px",
|
|
||||||
fontWeight: "bold",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Question {questionIndex + 1} of {totalQuestions}
|
Question {questionIndex + 1} of {totalQuestions}
|
||||||
</div>
|
</div>
|
||||||
|
<div className="timer-controls">
|
||||||
<div
|
<div
|
||||||
style={{ display: "flex", gap: "0.5rem", alignItems: "center" }}
|
className="timer-display"
|
||||||
>
|
|
||||||
<div
|
|
||||||
style={{
|
style={{
|
||||||
padding: "0.75rem 1rem",
|
|
||||||
background: timerExpired
|
background: timerExpired
|
||||||
? "#ffebee"
|
? "#ffebee"
|
||||||
: timerSeconds <= 10
|
: timerSeconds <= 10
|
||||||
? "#fff3e0"
|
? "#fff3e0"
|
||||||
: "#e8f5e9",
|
: "#e8f5e9",
|
||||||
borderRadius: "4px",
|
|
||||||
fontWeight: "bold",
|
|
||||||
fontSize: "1.2rem",
|
|
||||||
color: timerExpired
|
color: timerExpired
|
||||||
? "#c62828"
|
? "#c62828"
|
||||||
: timerSeconds <= 10
|
: timerSeconds <= 10
|
||||||
? "#e65100"
|
? "#e65100"
|
||||||
: "#2e7d32",
|
: "#2e7d32",
|
||||||
minWidth: "120px",
|
|
||||||
textAlign: "center",
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
⏱️ {String(Math.floor(timerSeconds / 60)).padStart(2, "0")}:
|
⏱️ {String(Math.floor(timerSeconds / 60)).padStart(2, "0")}:
|
||||||
@@ -692,14 +536,8 @@ export default function GameAdminView() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Button grid - 2 columns */}
|
{/* Button grid - 2 columns on desktop, 1 on mobile */}
|
||||||
<div
|
<div className="controls-button-grid">
|
||||||
style={{
|
|
||||||
display: "grid",
|
|
||||||
gridTemplateColumns: "1fr 1fr",
|
|
||||||
gap: "0.75rem",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{currentQuestion && (
|
{currentQuestion && (
|
||||||
<button
|
<button
|
||||||
onClick={handleToggleAnswer}
|
onClick={handleToggleAnswer}
|
||||||
|
|||||||
169
frontend/frontend/src/components/common/AdminNavbar.css
Normal file
169
frontend/frontend/src/components/common/AdminNavbar.css
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,9 +1,12 @@
|
|||||||
|
import { useState } from "react";
|
||||||
import { Link, useLocation } from "react-router-dom";
|
import { Link, useLocation } from "react-router-dom";
|
||||||
import { useAuth } from "../../contexts/AuthContext";
|
import { useAuth } from "../../contexts/AuthContext";
|
||||||
|
import "./AdminNavbar.css";
|
||||||
|
|
||||||
export default function AdminNavbar() {
|
export default function AdminNavbar() {
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const { user, logout } = useAuth();
|
const { user, logout } = useAuth();
|
||||||
|
const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
|
||||||
|
|
||||||
const navItems = [
|
const navItems = [
|
||||||
{ path: "/", label: "Home" },
|
{ path: "/", label: "Home" },
|
||||||
@@ -20,92 +23,45 @@ export default function AdminNavbar() {
|
|||||||
return location.pathname.startsWith(path);
|
return location.pathname.startsWith(path);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleLinkClick = () => {
|
||||||
|
setMobileMenuOpen(false);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<nav
|
<nav className="admin-navbar">
|
||||||
style={{
|
<div className="navbar-container">
|
||||||
background: "black",
|
|
||||||
padding: "1rem 2rem",
|
|
||||||
position: "sticky",
|
|
||||||
top: 0,
|
|
||||||
zIndex: 1000,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
display: "flex",
|
|
||||||
alignItems: "center",
|
|
||||||
justifyContent: "space-between",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div style={{ display: "flex", alignItems: "center", gap: "0.5rem" }}>
|
<div style={{ display: "flex", alignItems: "center", gap: "0.5rem" }}>
|
||||||
<span
|
<span className="navbar-brand">Trivia Admin</span>
|
||||||
style={{ fontSize: "1.5rem", fontWeight: "bold", color: "white" }}
|
|
||||||
>
|
|
||||||
🎮 Trivia Admin
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
<div style={{ display: "flex", gap: "1rem", alignItems: "center" }}>
|
|
||||||
|
{/* 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) => (
|
{navItems.map((item) => (
|
||||||
<Link
|
<Link
|
||||||
key={item.path}
|
key={item.path}
|
||||||
to={item.path}
|
to={item.path}
|
||||||
style={{
|
className={`navbar-link ${isActive(item.path) ? "active" : ""}`}
|
||||||
textDecoration: "none",
|
onClick={handleLinkClick}
|
||||||
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}
|
{item.label}
|
||||||
</Link>
|
</Link>
|
||||||
))}
|
))}
|
||||||
|
</div>
|
||||||
{/* User info and logout */}
|
{/* User info and logout */}
|
||||||
<div
|
<div className="navbar-user">
|
||||||
style={{
|
<span className="navbar-user-name">
|
||||||
display: "flex",
|
|
||||||
gap: "0.75rem",
|
|
||||||
alignItems: "center",
|
|
||||||
marginLeft: "1rem",
|
|
||||||
paddingLeft: "1rem",
|
|
||||||
borderLeft: "1px solid #444",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<span style={{ color: "white", fontSize: "0.9rem" }}>
|
|
||||||
{user?.profile?.name || user?.profile?.email}
|
{user?.profile?.name || user?.profile?.email}
|
||||||
</span>
|
</span>
|
||||||
<button
|
<button onClick={logout} className="navbar-logout">
|
||||||
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";
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Logout
|
Logout
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -3,9 +3,9 @@
|
|||||||
line-height: 1.5;
|
line-height: 1.5;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
|
|
||||||
color-scheme: light dark;
|
color-scheme: light;
|
||||||
color: rgba(255, 255, 255, 0.87);
|
color: #213547;
|
||||||
background-color: #242424;
|
background-color: #ffffff;
|
||||||
|
|
||||||
font-synthesis: none;
|
font-synthesis: none;
|
||||||
text-rendering: optimizeLegibility;
|
text-rendering: optimizeLegibility;
|
||||||
@@ -43,7 +43,7 @@ button {
|
|||||||
font-size: 1em;
|
font-size: 1em;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
font-family: inherit;
|
font-family: inherit;
|
||||||
background-color: #1a1a1a;
|
background-color: #f9f9f9;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: border-color 0.25s;
|
transition: border-color 0.25s;
|
||||||
}
|
}
|
||||||
@@ -54,16 +54,3 @@ button:focus,
|
|||||||
button:focus-visible {
|
button:focus-visible {
|
||||||
outline: 4px auto -webkit-focus-ring-color;
|
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
Reference in New Issue
Block a user