Files
peworkshop/app/(main)/clue/[clueId]/page.js
T
bobbert 8d0bca1d87 fixed a bunch of styling and component usage
made sure almost all viewer pages use the navbar component properly instead of using their hardcoded version, started to match the styling of the design for a lot of pages
2025-03-23 14:33:53 +00:00

1211 lines
33 KiB
JavaScript

"use client";
import { useState, useEffect, useRef } from "react";
import { useRouter } from "next/navigation";
import { useSession } from "next-auth/react";
import {
formatElapsedTime,
getHuntTimer,
syncTimerWithServer,
} from "@/utils/timerService";
import Link from "next/link";
import dynamic from "next/dynamic";
import { use } from "react";
// Add missing import for NavBar component
import NavBar from "@/components/NavBar";
// Dynamically import the QR scanner component with no SSR
const EnhancedQRScanner = dynamic(
() => import("@/components/EnhancedQRScanner"),
{
ssr: false,
loading: () => <div className="scanner-loading">Loading scanner...</div>,
}
);
export default function CluePage({ params }) {
// Unwrap params properly using use() from React
const unwrappedParams = use(params);
const clueId = unwrappedParams.clueId;
const [clue, setClue] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
const [elapsedTime, setElapsedTime] = useState("00:00:00");
const [scanMessage, setScanMessage] = useState(null);
const [lastRefresh, setLastRefresh] = useState(0);
const [clueFound, setClueFound] = useState(false);
const [clueSkipped, setClueSkipped] = useState(false); // Add state for skipped status
const [allClues, setAllClues] = useState([]);
const [nextClueId, setNextClueId] = useState(null);
const [nextClueAvailable, setNextClueAvailable] = useState(false);
const [huntActive, setHuntActive] = useState(true);
const [previousHuntState, setPreviousHuntState] = useState(true);
const [showSuccessNotification, setShowSuccessNotification] = useState(false);
const [showNavErrorNotification, setShowNavErrorNotification] =
useState(false);
const [teamData, setTeamData] = useState(null);
const router = useRouter();
const { data: session, status } = useSession();
const notificationTimeoutRef = useRef(null);
const navErrorTimeoutRef = useRef(null);
const timerIdRef = useRef(null);
const timerInitializedRef = useRef(false); // Add this ref to track if timer is already initialized
// Check if user is admin
const isAdmin = session?.user?.role === "admin";
// This effect handles authentication and initial clue data fetch
useEffect(() => {
let isMounted = true;
const now = Date.now();
const minRefreshInterval = isAdmin ? 30000 : 10000;
// Skip if we refreshed too recently and this isn't the first load
if (loading === false && now - lastRefresh < minRefreshInterval) {
return;
}
if (status === "unauthenticated") {
router.push("/login");
return;
}
if (status === "loading") {
return;
}
// Fetch all clues to get navigation context
const fetchAllClues = async () => {
try {
const response = await fetch("/api/clues");
if (!response.ok) return [];
const data = await response.json();
if (isMounted) {
setAllClues(data);
// Find next clue for navigation
const currentIndex = data.findIndex((c) => c.id === parseInt(clueId));
if (currentIndex !== -1 && currentIndex < data.length - 1) {
const nextClue = data[currentIndex + 1];
setNextClueId(nextClue.id);
// Set if the next clue is available (already found or is the next unfound clue)
setNextClueAvailable(nextClue.found || nextClue.available);
}
}
return data;
} catch (err) {
console.error("Error fetching all clues:", err);
return [];
}
};
// Fetch current clue details
async function fetchClue() {
try {
setLoading(true);
await fetchAllClues();
// Add timestamp to avoid browser caching
const timestamp = Date.now();
const response = await fetch(`/api/clues/${clueId}?t=${timestamp}`, {
method: "GET",
headers: {
"Content-Type": "application/json",
"Cache-Control": "no-cache, no-store, must-revalidate",
},
credentials: "include",
});
if (!response.ok) {
if (response.status === 404) {
throw new Error("Clue not found");
} else {
const data = await response.json();
throw new Error(data.error || "Failed to fetch clue");
}
}
const data = await response.json();
if (isMounted) {
setClue(data);
setClueFound(data.found);
setClueSkipped(data.skipped); // Set skipped status from API response
setLoading(false);
setLastRefresh(Date.now());
}
} catch (err) {
console.error("Error fetching clue:", err);
if (isMounted) {
setError(err.message);
setLoading(false);
}
}
}
fetchClue();
return () => {
isMounted = false;
};
}, [clueId, router, status, lastRefresh, loading, isAdmin]);
// Add a new effect to check hunt status periodically
useEffect(() => {
// Check hunt status every 5 seconds
const checkHuntStatus = async () => {
try {
// First check locally if timer indicates hunt has ended
const timerData = getHuntTimer();
let isActive = timerData?.isActive ?? true;
// Only make API call if local data suggests the hunt is active
// This reduces unnecessary API calls
if (isActive) {
// Also sync with server occasionally
const result = await syncTimerWithServer();
if (result.success) {
isActive = result.data?.active ?? false;
}
}
// Update hunt active state
setHuntActive(isActive);
// If hunt was active before but is not active now, it just ended
if (previousHuntState && !isActive) {
console.log("Hunt has ended! Redirecting to leaderboard...");
router.push("/leaderboard?huntEnded=true");
}
// Update previous state for next comparison
setPreviousHuntState(isActive);
} catch (err) {
console.error("Error checking hunt status:", err);
}
};
// Check immediately on mount and after any hunt state change
checkHuntStatus();
// Also set up interval for periodic checks
const intervalId = setInterval(checkHuntStatus, 5000);
return () => {
clearInterval(intervalId);
};
}, [router, previousHuntState]);
// Function to ensure we have timer data from the server - only if needed
async function fetchTeamHuntStartTime() {
try {
// First try to get any existing timer data
const timerData = getHuntTimer();
if (timerData && timerData.startTime) {
return true;
}
// If no timer data exists, sync with server
console.log("No timer data found, syncing with server...");
const result = await syncTimerWithServer();
return result.success;
} catch (err) {
console.error("Error fetching team hunt start time:", err);
return false;
}
}
// Updated timer effect with protection against multiple initializations
useEffect(() => {
// Prevent multiple initializations of the timer
if (timerInitializedRef.current) {
return;
}
// Initialize timer
async function initializeTimer() {
// First ensure we have timer data
await fetchTeamHuntStartTime();
// Set up timer updates if not already set
if (!timerIdRef.current) {
const updateTimer = () => {
const timerData = getHuntTimer();
if (timerData && timerData.startTime) {
setElapsedTime(formatElapsedTime(timerData.startTime));
// Check if timer has ended (if there's an endTime)
if (timerData.endTime) {
const now = Date.now();
const endTime = new Date(timerData.endTime).getTime();
// If current time is past the end time, hunt has ended
if (now >= endTime) {
setHuntActive(false);
clearInterval(timerIdRef.current);
timerIdRef.current = null;
// Redirect to leaderboard with huntEnded flag
console.log(
"Hunt end time reached! Redirecting to leaderboard..."
);
router.push("/leaderboard?huntEnded=true");
}
}
}
};
// Initial update
updateTimer();
// Update every second
timerIdRef.current = setInterval(updateTimer, 1000);
timerInitializedRef.current = true;
}
}
initializeTimer();
// Clean up interval on unmount
return () => {
if (timerIdRef.current) {
clearInterval(timerIdRef.current);
timerIdRef.current = null;
timerInitializedRef.current = false;
}
};
}, [router]); // Only depend on router
// Make sure hunt status check doesn't interfere with the timer
useEffect(() => {
// This function should NOT reset the timer or recreate it
async function checkHuntStatus() {
try {
const response = await fetch("/api/hunt-status", {
method: "GET",
headers: {
"Content-Type": "application/json",
"Cache-Control": "no-cache, no-store, must-revalidate",
},
credentials: "include",
});
if (response.ok) {
const data = await response.json();
// Only update hunt state, don't touch the timer
const newHuntState = !!data.active;
// If hunt was active but is now inactive, redirect to leaderboard
if (previousHuntState === true && newHuntState === false) {
console.log("Hunt has ended! Redirecting to leaderboard...");
router.push("/leaderboard?huntEnded=true");
return;
}
// Update the previous hunt state for next comparison
setPreviousHuntState(newHuntState);
}
} catch (err) {
console.error("Failed to check hunt status:", err);
}
}
const intervalId = setInterval(checkHuntStatus, 5000);
return () => {
clearInterval(intervalId);
};
}, [router, previousHuntState]);
// Fetch team data on component mount
useEffect(() => {
if (status === "authenticated") {
fetchTeamData();
}
}, [status]);
// Add function to fetch team data
const fetchTeamData = async () => {
if (!session) return;
try {
const response = await fetch("/api/user/team", {
headers: {
"Content-Type": "application/json",
"Cache-Control": "no-cache, no-store, must-revalidate",
},
credentials: "include",
});
if (response.ok) {
const data = await response.json();
setTeamData(data);
}
} catch (err) {
console.error("Failed to fetch team data:", err);
}
};
// Handle QR scan result
const handleScan = (data) => {
// Only update message if we have data
if (data) {
setScanMessage(`QR Code scanned: ${data}`);
}
try {
if (!data || !clue) return;
// Normalize the scanned QR code value
const normalizedInput = data.trim().toUpperCase();
// Try to use the QR code field, but fall back to a generated one if it's missing or empty
let expectedQrCode = (clue.qr_code || "").trim().toUpperCase();
// If QR code is missing or empty, generate a fallback based on the clue ID
if (!expectedQrCode) {
expectedQrCode = `QR${clue.id}`.toUpperCase();
}
// First try a direct match with the QR code
if (normalizedInput === expectedQrCode) {
// Exact match with the clue's QR code
setScanMessage("✓ Correct QR code found!");
markClueAsFound(clueId);
return;
}
// If not a direct match, try to extract the numeric value
const qrPattern = /^QR[^0-9]*(\d+)$/i;
const cluePattern = /^clue[^0-9]*(\d+)$/i;
const numberPattern = /^(\d+)$/;
// Extract the number from the scanned input
const inputMatch =
normalizedInput.match(qrPattern) ||
normalizedInput.match(cluePattern) ||
normalizedInput.match(numberPattern);
// Extract the number from the expected QR code
const expectedMatch = expectedQrCode.match(qrPattern);
if (inputMatch && expectedMatch && inputMatch[1] === expectedMatch[1]) {
// The numeric parts match
setScanMessage("✓ Correct QR code found!");
markClueAsFound(clueId);
} else if (inputMatch) {
// Found a number in the input, but it doesn't match
setScanMessage(
`QR code "${data}" scanned, but you need to scan "${expectedQrCode}". Please scan the correct code.`
);
} else {
setScanMessage(
"Couldn't recognize this QR code. Please scan the correct code for this clue."
);
}
} catch (err) {
console.error("Error processing scan result:", err);
setScanMessage("Invalid QR code format. Please try again.");
}
};
// Function to mark clue as found via API
const markClueAsFound = async (currentClueId) => {
try {
// Only proceed if the clue isn't already found
if (clueFound) {
setScanMessage("You've already found this clue!");
return;
}
// Mark the clue as found via API
const response = await fetch("/api/mark-clue", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ clueId: currentClueId }),
});
if (response.ok) {
// Update local state to show clue is found
setClueFound(true);
setScanMessage(
"✅ Clue verified! You can now proceed to the next clue."
);
// Make next clue available immediately
setNextClueAvailable(true);
// Force a refresh of the clue data to update availability
setLastRefresh(0);
// Show success notification
setShowSuccessNotification(true);
// Clear previous timeout if it exists
if (notificationTimeoutRef.current) {
clearTimeout(notificationTimeoutRef.current);
}
// Auto-hide the notification after 5 seconds
notificationTimeoutRef.current = setTimeout(() => {
setShowSuccessNotification(false);
}, 5000);
} else {
setScanMessage("Error verifying clue. Please try again.");
}
} catch (err) {
console.error("Error marking clue as found:", err);
setScanMessage("Error verifying clue. Please try again.");
}
};
// Clear notification timeouts on component unmount
useEffect(() => {
return () => {
if (notificationTimeoutRef.current) {
clearTimeout(notificationTimeoutRef.current);
}
if (navErrorTimeoutRef.current) {
clearTimeout(navErrorTimeoutRef.current);
}
};
}, []);
// Replace the previous clue function with skip clue functionality
const handleSkipClue = async () => {
// Show confirmation dialog
if (
!confirm(
"Are you sure you want to skip this clue? You can come back to it later."
)
) {
return;
}
try {
// Make an API call to mark this clue as skipped
const response = await fetch("/api/mark-clue", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
clueId: parseInt(clueId),
skipped: true,
}),
});
// Get the response data to handle errors better
const data = await response.json();
if (response.ok) {
setScanMessage("Clue skipped. You can return to it later.");
// Make next clue available immediately
setNextClueAvailable(true);
// Navigate to the next clue if available, otherwise go to map
if (nextClueId) {
router.push(`/clue/${nextClueId}`);
} else {
router.push("/map");
}
} else {
console.error(
"Failed to skip clue:",
data.error || response.statusText
);
setScanMessage(`Error: ${data.error || "Could not skip clue"}`);
}
} catch (err) {
console.error("Error skipping clue:", err);
setScanMessage("Error: Could not connect to server");
}
};
// Navigate to the next clue
const handleNextClue = () => {
if (nextClueId) {
// Always allow navigation if current clue is found
if (clueFound || nextClueAvailable || isAdmin) {
router.push(`/clue/${nextClueId}`);
} else {
// Show notification that the next clue is locked
setShowNavErrorNotification(true);
// Clear previous timeout if it exists
if (navErrorTimeoutRef.current) {
clearTimeout(navErrorTimeoutRef.current);
}
// Auto-hide the notification after 3 seconds
navErrorTimeoutRef.current = setTimeout(() => {
setShowNavErrorNotification(false);
}, 3000);
}
} else {
router.push("/map");
}
};
// Function for manual refresh - for admins to test without waiting
const handleManualRefresh = () => {
setLastRefresh(0);
};
// Rendering states
if (loading) {
return (
<div className="clue-container">
<div className="loading">
<div className="loading-spinner"></div>
<p>Loading clue...</p>
</div>
</div>
);
}
if (!huntActive) {
// Display temporary message before redirecting
return (
<div className="clue-container">
<div className="hunt-ended">
<div className="hunt-ended-icon">🏁</div>
<h2>Hunt Has Ended!</h2>
<p>The treasure hunt has concluded. Redirecting to results page...</p>
<div className="loading-spinner"></div>
</div>
</div>
);
}
if (error) {
return (
<div className="clue-container">
<div className="error">Error: {error}</div>
<div className="nav-buttons">
<button onClick={() => router.push("/map")} className="map-button">
Back to Map
</button>
<button onClick={() => router.push("/")} className="home-button">
Back to Home
</button>
</div>
</div>
);
}
if (!clue) {
return (
<div className="clue-container">
<div className="not-found">Clue not found</div>
<div className="nav-buttons">
<button onClick={() => router.push("/map")} className="map-button">
Back to Map
</button>
<button onClick={() => router.push("/")} className="home-button">
Back to Home
</button>
</div>
</div>
);
}
return (
<div className="clue-container">
{/* Map Banner with team info and timer */}
<div className="map-banner">
<div className="team-name">
<span className="team-label">Team Name: </span>
{teamData?.team?.name || "Loading..."}
</div>
<div className="elapsed-time">
<span className="time-value">{elapsedTime}</span>
</div>
<div className="team-code">{teamData?.team?.code || ""}</div>
</div>
{showSuccessNotification && (
<div className="success-notification">
<div className="success-notification-content">
<div className="success-icon">🎉</div>
<div className="success-text">
<h3>Clue Found!</h3>
<p>Great work! You've found this clue.</p>
</div>
</div>
</div>
)}
{showNavErrorNotification && (
<div className="nav-error-notification">
<div className="nav-error-notification-content">
<div className="nav-error-icon">🔒</div>
<div className="nav-error-text">
<h3>Clue Locked</h3>
<p>
You need to find this clue first before moving to the next one.
</p>
</div>
</div>
</div>
)}
<div className="clue-content-wrapper">
{/* Show notification based on found OR skipped status */}
{clueFound && (
<div className="clue-found-notification">
<div className="clue-found-icon">✓</div>
<div className="clue-found-text">
You've already found this clue!
</div>
</div>
)}
{clueSkipped && !clueFound && (
<div className="clue-skipped-notification">
<div className="clue-skipped-icon">↷</div>
<div className="clue-skipped-text">
You skipped this clue earlier. Try to solve it now!
</div>
</div>
)}
{/* Admin tools section - only visible to admins */}
{isAdmin && (
<div className="admin-tools">
<div className="admin-badge">Admin View</div>
<button
onClick={handleManualRefresh}
className="admin-refresh-button"
>
Refresh Clue Data
</button>
</div>
)}
{/* QR Scanner Section */}
<div className="scanner-section">
<h2>Scan QR Code</h2>
<EnhancedQRScanner
onScan={handleScan}
width={280}
height={280}
disableTorch={true} // Disable torch functionality
/>
{scanMessage && (
<div className={`scan-message ${clueFound ? "success" : ""}`}>
{scanMessage}
</div>
)}
{/* QR Code helper removed as requested */}
</div>
{/* Clue Information Section */}
<div className="clue-scroll">
<h1 className="clue-title">{clue?.title}</h1>
<div className="clue-content">
<h2>Description</h2>
<p className="clue-description">{clue?.description}</p>
{clue?.location && (
<div className="clue-location">
<h2>Location Hint</h2>
<p>{clue.location}</p>
</div>
)}
</div>
</div>
{/* Navigation Buttons */}
<div className="navigation-buttons">
<button onClick={handleSkipClue} className="nav-button prev-button">
Skip Clue
</button>
<button
onClick={() => router.push("/map")}
className="nav-button map-button"
>
Back to Map
</button>
<button onClick={handleNextClue} className="nav-button next-button">
Next Clue
</button>
</div>
</div>
{/* Replace hardcoded navbar with NavBar component */}
<NavBar activeLink="clue" teamData={teamData?.team} />
<style jsx>{`
.clue-container {
display: flex;
flex-direction: column;
min-height: 100vh;
background-color: #f4f1ea;
padding-bottom: 70px; /* Space for bottom navbar */
position: relative;
}
.success-notification {
position: fixed;
top: 0;
left: 0;
width: 100%;
padding: 20px;
background-color: rgba(0, 0, 0, 0.7);
z-index: 1000;
display: flex;
justify-content: center;
animation: slide-down 0.5s ease-out;
}
.success-notification-content {
display: flex;
align-items: center;
background-color: #4caf50;
color: white;
padding: 15px 25px;
border-radius: 10px;
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2);
max-width: 400px;
}
.success-icon {
font-size: 2rem;
margin-right: 15px;
}
.success-text h3 {
margin: 0 0 5px 0;
font-size: 1.2rem;
}
.success-text p {
margin: 0;
}
.nav-error-notification {
position: fixed;
top: 0;
left: 0;
width: 100%;
padding: 20px;
background-color: rgba(0, 0, 0, 0.7);
z-index: 1000;
display: flex;
justify-content: center;
animation: slide-down 0.5s ease-out;
}
.nav-error-notification-content {
display: flex;
align-items: center;
background-color: #f44336;
color: white;
padding: 15px 25px;
border-radius: 10px;
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2);
max-width: 400px;
}
.nav-error-icon {
font-size: 2rem;
margin-right: 15px;
}
.nav-error-text h3 {
margin: 0 0 5px 0;
font-size: 1.2rem;
}
.nav-error-text p {
margin: 0;
}
.clue-found-notification {
display: flex;
align-items: center;
background-color: #e8f5e9;
border-radius: 8px;
padding: 12px 16px;
margin-bottom: 15px;
width: 100%;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
}
.clue-found-icon {
font-size: 1.5rem;
color: #2e7d32;
margin-right: 12px;
}
.clue-found-text {
color: #2e7d32;
font-weight: bold;
}
.clue-skipped-notification {
display: flex;
align-items: center;
background-color: #e3f2fd;
border-radius: 8px;
padding: 12px 16px;
margin-bottom: 15px;
width: 100%;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
}
.clue-skipped-icon {
font-size: 1.5rem;
color: #1976d2;
margin-right: 12px;
}
.clue-skipped-text {
color: #1976d2;
font-weight: bold;
}
@keyframes slide-down {
0% {
transform: translateY(-100%);
}
100% {
transform: translateY(0);
}
}
.hunt-ended {
text-align: center;
padding: 30px;
background-color: white;
border-radius: 10px;
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1);
margin: auto;
max-width: 500px;
}
.hunt-ended-icon {
font-size: 48px;
margin-bottom: 20px;
}
.hunt-ended h2 {
color: #68543c;
margin-bottom: 15px;
}
.hunt-ended p {
margin-bottom: 20px;
}
.clue-content-wrapper {
padding: 0 20px;
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
width: 100%;
max-width: 600px;
margin: 0 auto;
}
.scanner-section {
width: 100%;
display: flex;
flex-direction: column;
align-items: center;
margin-bottom: 20px;
background-color: white;
border-radius: 10px;
padding: 15px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.scanner-section h2 {
margin-top: 0;
margin-bottom: 15px;
color: #68543c;
}
.scan-message {
margin-top: 10px;
padding: 8px;
background-color: #e8f5e9;
border-radius: 4px;
text-align: center;
width: 100%;
}
.scan-message.success {
background-color: #c8e6c9;
color: #2e7d32;
font-weight: bold;
}
.clue-scroll {
background-color: #f8f3e3;
border-radius: 10px;
padding: 20px;
width: 100%;
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1);
position: relative;
margin-bottom: 20px;
border: 1px dashed #9c6b30;
}
h2 {
color: #9c6b30;
font-size: 1.5rem;
margin-top: 10px;
margin-bottom: 15px;
}
.clue-description {
font-size: 1.1rem;
line-height: 1.5;
color: #5d4037;
margin-bottom: 20px;
}
.clue-location {
margin-top: 20px;
padding-top: 15px;
border-top: 1px dashed #9c6b30;
}
.navigation-buttons {
display: flex;
justify-content: space-between;
width: 100%;
margin-top: 20px;
margin-bottom: 20px;
flex-wrap: wrap; /* Allow buttons to wrap on small screens */
gap: 10px;
}
.nav-button {
padding: 12px 15px;
border: none;
border-radius: 5px;
cursor: pointer;
font-weight: bold;
transition: all 0.2s;
font-size: 14px;
flex: 1;
min-width: 120px;
}
.prev-button {
background-color: #a98467;
color: white;
}
.map-button {
background-color: #607d8b;
color: white;
}
.next-button {
background-color: #68543c;
color: white;
}
.nav-button:hover {
opacity: 0.9;
transform: translateY(-2px);
}
.loading,
.error,
.not-found {
text-align: center;
padding: 30px;
}
.loading-spinner {
border: 4px solid #f3f3f3;
border-top: 4px solid #9c6b30;
border-radius: 50%;
width: 40px;
height: 40px;
animation: spin 1s linear infinite;
margin: 0 auto 20px;
}
@keyframes spin {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
.error {
color: #d32f2f;
}
.clue-title {
color: #68543c;
font-size: 1.8rem;
text-align: center;
margin-bottom: 20px;
border-bottom: 2px solid #9c6b30;
padding-bottom: 10px;
}
.admin-tools {
background-color: #f8d7da;
border: 2px dashed #d9534f;
border-radius: 8px;
padding: 12px;
margin-bottom: 15px;
text-align: center;
width: 100%;
}
.admin-badge {
font-weight: bold;
margin-bottom: 8px;
color: #d9534f;
}
.admin-refresh-button {
background-color: #d9534f;
color: white;
border: none;
border-radius: 4px;
padding: 8px 16px;
cursor: pointer;
font-size: 14px;
}
.admin-refresh-button:hover {
background-color: #c9302c;
}
/* Map Banner Styles */
.map-banner {
background-image: url("/images/Bottom-Bar.svg");
background-repeat: no-repeat;
background-size: cover;
background-position: center;
color: white;
padding: 15px;
display: flex;
align-items: center;
justify-content: space-between;
border-bottom: 2px solid #4a3c2a;
z-index: 10;
flex-shrink: 0;
margin-bottom: 20px;
}
.team-name {
font-size: 18px;
font-weight: bold;
flex: 1;
text-align: left;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
padding-right: 5px;
}
.team-label {
font-weight: normal;
opacity: 0.9;
}
.elapsed-time {
text-align: center;
padding: 5px 10px;
background-color: rgba(0, 0, 0, 0.2);
border-radius: 8px;
flex: 0 0 auto;
margin: 0 10px;
}
.time-value {
font-size: 18px;
font-weight: bold;
}
.team-code {
font-size: 18px; /* Increased from 16px to 18px */
font-weight: bold;
flex: 1;
text-align: right;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
padding-left: 5px;
}
.clue-timer {
display: flex;
align-items: center;
justify-content: center;
margin: 10px 0;
padding: 5px 10px;
background-color: rgba(104, 84, 60, 0.1);
border-radius: 5px;
}
.timer-label {
font-weight: bold;
margin-right: 8px;
color: #68543c;
}
.timer-value {
font-family: monospace;
font-size: 1.1em;
}
.clue-page {
display: flex;
flex-direction: column;
min-height: 100vh;
background-color: #f4f4f4;
padding-bottom: 70px; /* Space for navbar */
}
.clue-banner {
background-image: url("/images/Bottom-Bar.svg");
background-repeat: no-repeat;
background-size: cover;
background-position: center;
color: white;
padding: 20px;
position: relative;
display: flex;
align-items: center;
}
.team-info {
flex: 1;
display: flex;
justify-content: space-between;
align-items: center;
}
.team-name {
margin: 0;
font-size: 24px;
font-weight: bold;
color: white;
}
.team-code {
font-size: 24px;
font-weight: bold;
margin-left: auto;
color: white;
}
`}</style>
</div>
);
}