From 726121e1051a72d4657d5a725f535e9ac61491aa Mon Sep 17 00:00:00 2001 From: mudabbir-ahmad Date: Mon, 17 Mar 2025 10:42:33 +0000 Subject: [PATCH] FIXED CLUES! all that needs to be done is manually adding circle buttons and hiding/showing feature through API.. maybe fixing QR scanner --- .npmrc | 3 + app/(main)/clue/[clueId]/page.js | 286 +++++++------- app/(main)/map/map.module.css | 219 ++++------- app/(main)/map/page.js | 635 +++++++------------------------ app/api/clues/[clueId]/route.js | 48 +-- app/api/mark-clue/route.js | 51 +-- clue_hunt.db | Bin 86016 -> 86016 bytes components/EnhancedQRScanner.js | 317 +++++++++++++++ components/QRCodeHelper.js | 79 ++++ components/QRScanner.js | 189 ++++++--- components/SimpleQRScanner.js | 164 ++++++++ package-lock.json | 95 ++--- package.json | 6 +- public/images/map.svg | 40 +- qodana.yaml | 29 -- utils/map-debug.js | 80 ++++ utils/map-populate.js | 181 ++++++--- yarn.lock | 274 ++++++------- 18 files changed, 1504 insertions(+), 1192 deletions(-) create mode 100644 .npmrc create mode 100644 components/EnhancedQRScanner.js create mode 100644 components/QRCodeHelper.js create mode 100644 components/SimpleQRScanner.js delete mode 100644 qodana.yaml diff --git a/.npmrc b/.npmrc new file mode 100644 index 0000000..9aaf598 --- /dev/null +++ b/.npmrc @@ -0,0 +1,3 @@ +legacy-peer-deps=true +fund=false +save-exact=true diff --git a/app/(main)/clue/[clueId]/page.js b/app/(main)/clue/[clueId]/page.js index bc966ba..dbb2119 100644 --- a/app/(main)/clue/[clueId]/page.js +++ b/app/(main)/clue/[clueId]/page.js @@ -5,36 +5,43 @@ import { useSession } from "next-auth/react"; import { formatElapsedTime, getHuntTimer } from "@/utils/timerService"; import Link from "next/link"; import dynamic from "next/dynamic"; +import { use } from "react"; +import QRCodeHelper from "@/components/QRCodeHelper"; -// Dynamically import QR scanner component with no SSR to avoid hydration issues -const QRScanner = dynamic(() => import("@/components/QRScanner"), { - ssr: false, -}); +// Dynamically import the new QR scanner component with no SSR +const EnhancedQRScanner = dynamic( + () => import("@/components/EnhancedQRScanner"), + { + ssr: false, + loading: () =>
Loading scanner...
, + } +); 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 [foundNow, setFoundNow] = useState(false); const [elapsedTime, setElapsedTime] = useState("00:00:00"); - const [showScanner, setShowScanner] = useState(false); - const [scanResult, setScanResult] = useState(null); const [scanMessage, setScanMessage] = useState(null); - const [lastRefresh, setLastRefresh] = useState(0); // Track last refresh time + const [lastRefresh, setLastRefresh] = useState(0); + const [clueFound, setClueFound] = useState(false); + const [allClues, setAllClues] = useState([]); + const [nextClueId, setNextClueId] = useState(null); const router = useRouter(); - const { data: session, status } = useSession(); // Get session to check if user is admin - const { clueId } = params; + const { data: session, status } = useSession(); // Check if user is admin const isAdmin = session?.user?.role === "admin"; // This effect handles authentication and initial clue data fetch useEffect(() => { - let isMounted = true; // Flag to prevent state updates after unmount + let isMounted = true; const now = Date.now(); - - // Only allow refreshes every 10 seconds (more for admins) - const minRefreshInterval = isAdmin ? 30000 : 10000; // 30 seconds for admins, 10 seconds for regular users + const minRefreshInterval = isAdmin ? 30000 : 10000; // Skip if we refreshed too recently and this isn't the first load if (loading === false && now - lastRefresh < minRefreshInterval) { @@ -50,10 +57,34 @@ export default function CluePage({ params }) { return; } - // One-time fetch clue data with debouncing + // 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) { + setNextClueId(data[currentIndex + 1].id); + } + } + 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(); @@ -76,16 +107,11 @@ export default function CluePage({ params }) { } const data = await response.json(); - - // Only update state if the component is still mounted if (isMounted) { setClue(data); - // Only set foundNow on initial load or if it's actually newly found - if (loading === true) { - setFoundNow(data.foundNow); - } + setClueFound(data.found); setLoading(false); - setLastRefresh(Date.now()); // Update last refresh timestamp + setLastRefresh(Date.now()); } } catch (err) { console.error("Error fetching clue:", err); @@ -98,17 +124,14 @@ export default function CluePage({ params }) { fetchClue(); - // Cleanup function to prevent state updates after unmount return () => { isMounted = false; }; - }, [clueId, router, status, lastRefresh, loading, isAdmin]); // Add isAdmin and lastRefresh to dependencies + }, [clueId, router, status, lastRefresh, loading, isAdmin]); - // Separate effect to handle timer updates - doesn't re-fetch data + // Separate effect to handle timer updates useEffect(() => { let timerId; - - // Set up timer display updates const timerData = getHuntTimer(); if (!timerData || !timerData.isActive) { return; @@ -125,42 +148,60 @@ export default function CluePage({ params }) { return () => { if (timerId) clearInterval(timerId); }; - }, []); // Empty dependency array so it only runs once + }, []); // Handle QR scan result const handleScan = (data) => { - setScanResult(data); setScanMessage(`QR Code scanned: ${data}`); try { - // Handle both "QR123" format and just "123" format + // Handle multiple QR code formats: "QR123", "123", or even "Clue 123" let scannedClueId; - const match = data.match(/^QR(\d+)$/i); + // Try regex patterns + const qrPattern = /^QR[^0-9]*(\d+)$/i; + const cluePattern = /^clue[^0-9]*(\d+)$/i; + const numberPattern = /^(\d+)$/; + + let match = + data.match(qrPattern) || + data.match(cluePattern) || + data.match(numberPattern); if (match) { - // Extract the number from QR[number] scannedClueId = parseInt(match[1], 10); } else { - // Try to parse the raw value + // Try direct parsing as a last resort scannedClueId = parseInt(data, 10); } if (!isNaN(scannedClueId) && scannedClueId === parseInt(clueId)) { - // Correct QR code scanned, mark as found and move to next clue + // Correct QR code scanned, mark as found + setScanMessage("✓ Correct QR code found!"); markClueAsFound(clueId); + } else if (!isNaN(scannedClueId)) { + setScanMessage( + `QR code for clue ${scannedClueId} scanned, but you need clue ${clueId}. Please scan the correct code.` + ); } else { setScanMessage( - "Incorrect QR code. Please scan the correct code for this clue." + "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 and navigate to the next clue + // 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", @@ -169,29 +210,11 @@ export default function CluePage({ params }) { }); if (response.ok) { - // Success message - setScanMessage("Clue verified! Moving to the next clue..."); - - // Fetch all clues to find the next available one - const allCluesResponse = await fetch("/api/clues"); - const allClues = await allCluesResponse.json(); - - // Find the current clue index - const currentIndex = allClues.findIndex( - (c) => c.id === parseInt(currentClueId) + // Update local state to show clue is found + setClueFound(true); + setScanMessage( + "✅ Clue verified! You can now proceed to the next clue." ); - - if (currentIndex !== -1 && currentIndex < allClues.length - 1) { - // Navigate to the next clue after a brief delay - setTimeout(() => { - router.push(`/clue/${allClues[currentIndex + 1].id}`); - }, 1500); - } else { - // This was the last clue - setTimeout(() => { - router.push("/map"); - }, 1500); - } } else { setScanMessage("Error verifying clue. Please try again."); } @@ -201,35 +224,35 @@ export default function CluePage({ params }) { } }; - // Function to handle skip button - const handleSkip = async () => { - // Fetch all clues to find the next one - try { - const response = await fetch("/api/clues"); - const allClues = await response.json(); + // Navigate to the next clue + const handleNextClue = () => { + if (nextClueId) { + router.push(`/clue/${nextClueId}`); + } else { + router.push("/map"); + } + }; - // Find the current clue index + // Navigate to the previous clue + const handlePrevClue = () => { + if (allClues.length > 0) { const currentIndex = allClues.findIndex((c) => c.id === parseInt(clueId)); - - if (currentIndex !== -1 && currentIndex < allClues.length - 1) { - // Navigate to the next clue - router.push(`/clue/${allClues[currentIndex + 1].id}`); + if (currentIndex > 0) { + router.push(`/clue/${allClues[currentIndex - 1].id}`); } else { - // This was the last clue router.push("/map"); } - } catch (err) { - console.error("Error skipping to next clue:", err); - setError("Failed to skip to next clue"); + } else { + router.push("/map"); } }; // Function for manual refresh - for admins to test without waiting const handleManualRefresh = () => { - // Force a refresh by setting lastRefresh to 0 setLastRefresh(0); }; + // Rendering states if (loading) { return (
@@ -276,7 +299,6 @@ export default function CluePage({ params }) { return (
{elapsedTime}
-
{/* Admin tools section - only visible to admins */} {isAdmin && ( @@ -291,29 +313,26 @@ export default function CluePage({ params }) {
)} - {/* QR Scanner Section */} + {/* QR Scanner Section - Removed manual entry button */}

Scan QR Code

- - {scanMessage &&
{scanMessage}
} + + {scanMessage && ( +
+ {scanMessage} +
+ )} + {/* Add the QR Code helper component */} +
{/* Clue Information Section */}
- {foundNow && ( -
-
🎉
-
New Clue Discovered!
-
- )} - -

{clue.title}

- +

{clue?.title}

Description

-

{clue.description}

- - {clue.location && ( +

{clue?.description}

+ {clue?.location && (

Location Hint

{clue.location}

@@ -322,13 +341,19 @@ export default function CluePage({ params }) {
- {/* Action Buttons */} -
- - +
@@ -414,6 +439,12 @@ export default function CluePage({ params }) { width: 100%; } + .scan-message.success { + background-color: #c8e6c9; + color: #2e7d32; + font-weight: bold; + } + .clue-scroll { background-color: #f8f3e3; border-radius: 10px; @@ -445,39 +476,46 @@ export default function CluePage({ params }) { border-top: 1px dashed #9c6b30; } - .action-buttons { + .navigation-buttons { display: flex; justify-content: space-between; width: 100%; - margin-top: 10px; + margin-top: 20px; margin-bottom: 20px; + flex-wrap: wrap; /* Allow buttons to wrap on small screens */ + gap: 10px; } - .skip-button, - .scan-button { - padding: 12px 0; + .nav-button { + padding: 12px 15px; border: none; border-radius: 5px; cursor: pointer; font-weight: bold; transition: all 0.2s; - width: 48%; - font-size: 16px; + font-size: 14px; + flex: 1; + min-width: 120px; } - .skip-button { + .prev-button { background-color: #a98467; color: white; } - .scan-button { + .map-button { + background-color: #607d8b; + color: white; + } + + .next-button { background-color: #68543c; color: white; } - .skip-button:hover, - .scan-button:hover { + .nav-button:hover { opacity: 0.9; + transform: translateY(-2px); } .loading, @@ -519,39 +557,6 @@ export default function CluePage({ params }) { padding-bottom: 10px; } - .new-discovery { - background-color: #feefb3; - border: 1px solid #9f6000; - color: #9f6000; - padding: 15px; - border-radius: 5px; - margin-bottom: 20px; - display: flex; - align-items: center; - animation: pulse 2s infinite; - } - - @keyframes pulse { - 0% { - opacity: 1; - } - 50% { - opacity: 0.7; - } - 100% { - opacity: 1; - } - } - - .discovery-icon { - font-size: 1.5rem; - margin-right: 10px; - } - - .discovery-text { - font-weight: bold; - } - .nav-bar { display: flex; justify-content: space-around; @@ -583,7 +588,6 @@ export default function CluePage({ params }) { font-size: 12px; } - /* Admin tools styling */ .admin-tools { background-color: #f8d7da; border: 2px dashed #d9534f; diff --git a/app/(main)/map/map.module.css b/app/(main)/map/map.module.css index 29189b7..b883a5b 100644 --- a/app/(main)/map/map.module.css +++ b/app/(main)/map/map.module.css @@ -64,162 +64,45 @@ height: 100%; background-color: #69b5a8; /* Background color */ overflow: auto; /* Enable scrolling */ - display: block; /* Change from flex to block for proper scrolling */ + display: block; /* Use block for proper scrolling */ } .treasure-map { position: relative; - width: fit-content; /* Change from 100% to fit-content to allow natural svg size */ + width: fit-content; /* Allow natural width */ min-width: 100%; /* Ensure it's at least full width */ height: fit-content; min-height: 100%; - display: flex; /* Keep flex for alignment */ + display: flex; justify-content: flex-end; /* Align to the right */ align-items: flex-start; /* Align to the top */ } -/* SVG container for better scrolling and zooming */ -.svg-container { +/* Map image container for positioning */ +.map-image-container { position: relative; - width: fit-content; /* Allow natural width */ - height: fit-content; /* Allow natural height */ - display: block; /* Change to block for proper scrolling */ - overflow: visible; /* Show content beyond boundaries */ - z-index: 5; /* Ensure it's above the background */ + width: fit-content; + height: fit-content; } -.svg-container svg { - max-width: none; /* Remove max-width limitation */ +/* Map image styling */ +.map-image { + display: block; + max-width: none; /* Don't constrain the image */ width: auto; height: auto; - display: block; - object-fit: none; /* Don't constrain the SVG */ - position: relative; } -/* Force SVG elements to show properly with z-index */ -.svg-container svg * { - transform-box: fill-box; -} - -/* Ensure the clue points overlay is always on top */ -.svg-container svg .clue-points-overlay { - position: relative !important; - z-index: 9999 !important; - pointer-events: all !important; -} - -/* Make clue points very visible with stronger styling */ -.svg-container .clue-point { - fill-opacity: 1 !important; - stroke-opacity: 1 !important; - pointer-events: all !important; - cursor: pointer !important; - stroke-width: 3px !important; - r: 14px !important; - filter: drop-shadow(0px 0px 5px rgba(0, 0, 0, 0.7)) !important; -} - -/* Remove the default map background when we have the processed SVG */ -.svg-container + .map-background { - display: none; -} - -/* Map background - only shown when SVG is loading */ -.map-background { +/* Map overlay positioned absolutely over the map image */ +.map-overlay { position: absolute; top: 0; - right: 0; /* Align to the right */ - width: 100vw; /* Full viewport width */ - height: auto; - background-image: url("/images/map.svg"); - background-size: auto; /* Don't resize the background */ - background-position: top right; /* Position at top right */ - background-repeat: no-repeat; - z-index: 3; /* Lower z-index than the processed SVG */ -} - -/* Circle elements in the SVG map - attach to a container class */ -.svg-container circle { - cursor: pointer; - transition: fill 0.3s ease; /* Only transition the fill color, not size */ -} - -.svg-container circle:hover { - fill: #ffb74d !important; /* Only change color on hover, no movement */ -} - -/* Circle elements in the SVG map - attach to a container class */ -.svg-container circle.clue-point { - cursor: pointer; - transition: fill 0.3s ease, stroke-width 0.3s ease; - pointer-events: all; /* Ensure clicks are detected */ - stroke-width: 2px; - r: 8; /* Set a minimum radius for all clue points */ -} - -/* Improve circle visibility and interactivity */ -.svg-container circle.clue-point { - cursor: pointer !important; - transition: all 0.3s ease; - pointer-events: all !important; - stroke-width: 2px; - r: 12px; /* Larger radius for better visibility and easier clicks */ - z-index: 10; - position: relative; -} - -/* Enhance hover effect */ -.svg-container circle.clue-point:hover { - fill: #ffb74d !important; /* Orange hover color */ - stroke: #ff6d00 !important; - stroke-width: 4px !important; - transform: scale(1.2); - z-index: 20; - filter: drop-shadow(0px 0px 4px rgba(0, 0, 0, 0.5)); -} - -/* Invisible click areas for easier interaction */ -.svg-container circle.clue-point-click-area { - pointer-events: all !important; - cursor: pointer !important; - opacity: 0.01; /* Nearly invisible but still interactive */ - z-index: 5; -} - -/* Create custom animation for the current clue */ -@keyframes clue-point-pulse { - 0% { - stroke-width: 3px; - transform: scale(1); - filter: drop-shadow(0px 0px 2px rgba(255, 152, 0, 0.7)); - } - 50% { - stroke-width: 5px; - transform: scale(1.2); - filter: drop-shadow(0px 0px 8px rgba(255, 152, 0, 0.9)); - } - 100% { - stroke-width: 3px; - transform: scale(1); - filter: drop-shadow(0px 0px 2px rgba(255, 152, 0, 0.7)); - } -} - -.svg-container circle.clue-point:hover { - fill: #ffb74d !important; /* Orange hover color */ - stroke: #ff6d00; - stroke-width: 3px; - transform: scale(1.1); -} - -/* Remaining styles unchanged */ -.map-svg { - width: 100% !important; /* Override Next.js Image default width */ - height: auto !important; /* Override Next.js Image default height */ - max-width: none; /* Allow image to grow beyond container */ - object-fit: contain; - display: block; + left: 0; + width: 100%; + height: 100%; + pointer-events: auto; /* Allow interaction with buttons */ + z-index: 20; /* Ensure overlay is above map */ + box-sizing: border-box; } .loading-container { @@ -274,8 +157,8 @@ box-sizing: border-box; z-index: 100; /* Ensure it stays on top */ flex-shrink: 0; /* Prevent navbar from shrinking */ - margin: 0; /* Added margin property */ - height: auto; /* Added height property */ + margin: 0; + height: auto; } .nav-item { @@ -343,3 +226,63 @@ font-size: 11px; } } + +/* Clue button styling */ +.clue-button { + position: absolute; + width: 60px; + height: 60px; + border-radius: 50%; + background-color: #9e9e9e; /* Default grey background */ + color: white; + font-weight: bold; + border: 3px solid #757575; + cursor: pointer; + display: flex; + justify-content: center; + align-items: center; + font-size: 14px; + box-shadow: 0 2px 5px rgba(0, 0, 0, 0.3); + transition: all 0.3s ease; + z-index: 30; + transform: translate(-50%, -50%); /* Center the button on its position */ +} + +.clue-button:hover { + transform: translate(-50%, -50%) scale(1.1); + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.4); +} + +/* Clue status styles */ +.clue-found { + background-color: #2e7d32; /* Green for found clues */ + border-color: #1b5e20; +} + +.clue-available { + background-color: #ff9800; /* Orange for available clues */ + border-color: #e65100; + animation: pulse 1.5s infinite; +} + +.clue-locked { + background-color: #9e9e9e; /* Grey for locked clues */ + border-color: #757575; + cursor: default; +} + +/* Pulse animation for available clues */ +@keyframes pulse { + 0% { + transform: translate(-50%, -50%) scale(1); + box-shadow: 0 2px 5px rgba(0, 0, 0, 0.3); + } + 50% { + transform: translate(-50%, -50%) scale(1.1); + box-shadow: 0 4px 8px rgba(255, 152, 0, 0.6); + } + 100% { + transform: translate(-50%, -50%) scale(1); + box-shadow: 0 2px 5px rgba(0, 0, 0, 0.3); + } +} diff --git a/app/(main)/map/page.js b/app/(main)/map/page.js index d9d10cb..7fdef35 100644 --- a/app/(main)/map/page.js +++ b/app/(main)/map/page.js @@ -9,13 +9,8 @@ import { setHuntTimer, clearHuntTimer, formatElapsedTime, + syncTimerWithServer, } from "@/utils/timerService"; -import { - populateMap, - updateCluePointStatus, - attachClueClickHandlers, -} from "@/utils/map-populate"; -import { addDebugOverlay, fixCluePointsRendering } from "@/utils/map-debug"; export default function MapPage() { const { data: session, status } = useSession(); @@ -24,7 +19,6 @@ export default function MapPage() { const [error, setError] = useState(null); const [teamData, setTeamData] = useState(null); const [elapsedTime, setElapsedTime] = useState("00:00:00"); - const [mapSvg, setMapSvg] = useState(null); const [clues, setClues] = useState([]); const mapContainerRef = useRef(null); const router = useRouter(); @@ -120,6 +114,7 @@ export default function MapPage() { }); setClues(processedClues); + setLoading(false); // Set loading to false after clues are fetched return processedClues; } catch (err) { console.error("Failed to fetch clues:", err); @@ -128,211 +123,90 @@ export default function MapPage() { } }, []); - // Enhanced function to populate the map with clue points - const enhanceSvgMap = useCallback(() => { - if (!mapContainerRef.current) return; - - // Find the SVG element inside the map container - const svgElement = mapContainerRef.current.querySelector("svg"); - if (!svgElement) { - console.error("No SVG element found in map container"); - setMapError(true); - return; - } - - console.log("SVG element found, starting enhancement process"); - - // Clear out any existing map-background elements to ensure we only show one map - const existingMapBgs = document.querySelectorAll( - `.${styles["map-background"]}` - ); - existingMapBgs.forEach((bg) => (bg.style.display = "none")); - - // First populate the map with basic clue points - populateMap(svgElement) - .then((success) => { - if (success) { - console.log("Map populated with clue points successfully"); - - // Process the clues to determine which are available - const processedClues = clues.map((clue, index, arr) => { - // Find the first unfound clue's index - const firstUnfoundIndex = arr.findIndex((c) => !c.found); - - // A clue is available if: - // 1. It's already found, OR - // 2. It's the next unfound clue in the sequence - const available = clue.found || index === firstUnfoundIndex; - - return { ...clue, available }; - }); - - // Debug info about clue statuses - processedClues.forEach((clue, index) => { - console.log( - `Clue ${clue.id} (index ${index}): Available: ${clue.available}, Found: ${clue.found}` - ); - }); - - // Update the clue point appearances based on their statuses - updateCluePointStatus(svgElement, processedClues); - - // Define a direct navigation function that will be used for click handling - const navigateToClue = (clueId) => { - console.log(`Navigating to clue ${clueId}`); - router.push(`/clue/${clueId}`); - }; - - // Attach click handlers to all clue points and click areas - attachClueClickHandlers(svgElement, processedClues, navigateToClue); - } else { - console.error("Failed to populate map with clue points"); - setMapError(true); - } - }) - .catch((error) => { - console.error("Error enhancing SVG map:", error); - setMapError(true); - }); - }, [clues, router, styles]); - - // Function to load and enhance the SVG map - const loadAndEnhanceMap = useCallback(async () => { - try { - // Fetch the SVG file - const response = await fetch("/images/map.svg"); - if (!response.ok) { - console.error( - "Failed to load SVG:", - response.status, - response.statusText - ); - setMapError(true); - return; + // Handle clue button click + const handleClueClick = useCallback( + (clueId, isAvailable, isFound) => { + if (isFound || isAvailable) { + router.push(`/clue/${clueId}`); + } else { + alert("Complete previous clues to unlock this one!"); } + }, + [router] + ); - const svgText = await response.text(); - console.log(`SVG loaded, length: ${svgText.length} characters`); + // Size and position the overlay to match the map image + const setupOverlay = useCallback(() => { + const overlay = document.getElementById("map-overlay-container"); + const mapImg = document.getElementById("treasure-map-img"); - // Ensure SVG has correct namespaces and viewBox - let fixedSvg = svgText.replace( - / { + const mapRect = mapImg.getBoundingClientRect(); + overlay.style.width = `${mapRect.width}px`; + overlay.style.height = `${mapRect.height}px`; + }; - // Ensure SVG has a proper viewBox attribute if missing - if (!fixedSvg.includes('viewBox="')) { - const widthMatch = fixedSvg.match(/width="([^"]+)"/); - const heightMatch = fixedSvg.match(/height="([^"]+)"/); - if (widthMatch && heightMatch) { - const width = widthMatch[1]; - const height = heightMatch[1]; - fixedSvg = fixedSvg.replace( - / { + const mapRect = mapImg.getBoundingClientRect(); + overlay.style.width = `${mapRect.width}px`; + overlay.style.height = `${mapRect.height}px`; + }; - setMapSvg(fixedSvg); - setLoading(false); - } catch (error) { - console.error("Failed to load map SVG:", error); - setMapError(true); - setLoading(false); + // Initial resize and listen for window resize + setTimeout(resizeOverlay, 300); + window.addEventListener("resize", resizeOverlay); + + return () => { + window.removeEventListener("resize", resizeOverlay); + }; } }, []); + // Add this useEffect to coordinate loading the map and clues + useEffect(() => { + if (status === "authenticated") { + fetchClues(); + fetchTeamData(); + } else if (status === "unauthenticated") { + softRedirect("/login"); + } + + // Setup cleanup function for component unmount + return () => { + isMounted.current = false; + }; + }, [status, fetchClues, fetchTeamData, softRedirect]); + + // Setup the overlay when the component is mounted + useEffect(() => { + if (!loading && clues.length > 0) { + return setupOverlay(); + } + }, [loading, clues.length, setupOverlay]); + // Function to check hunt status and timer async function checkHuntAndTimer() { - if (!isMounted.current) return; - try { const response = await fetch("/api/hunt-status", { method: "GET", headers: { "Content-Type": "application/json", "Cache-Control": "no-cache, no-store, must-revalidate", + Pragma: "no-cache", }, credentials: "include", }); if (response.ok) { const data = await response.json(); - console.log("Hunt status check from map:", data); + setHuntActive(data.active); - // If hunt is not active, check if it has ended - if (!data.active) { - console.log("Hunt not active, checking if it ended..."); - - // Check if we had an active hunt before - const timerData = getHuntTimer(); - - if (timerData && timerData.isActive) { - // Hunt was active before but not now - it has ended - console.log("Hunt has ended, redirecting to leaderboard"); - clearHuntTimer(); // Clean up all timer data - - // Show a brief message before redirecting - const huntEndedMessage = document.createElement("div"); - huntEndedMessage.innerHTML = ` -
-
🎯 Hunt Complete!
-
The treasure hunt has ended.
-
Redirecting to results...
-
- `; - document.body.appendChild(huntEndedMessage); - - // Use the appropriate redirect method after a short delay - setTimeout(() => { - if (isMounted.current) { - hardRedirect("/leaderboard?huntEnded=true"); - } - }, 2000); - return; - } - - // Otherwise just show the waiting page - if (isMounted.current) { - setHuntActive(false); - softRedirect("/waiting"); - } - return; - } - - // If timer endTime exists and has passed, redirect to leaderboard - if (data.endTime && new Date(data.endTime) <= new Date()) { - console.log("Timer expired, redirecting to leaderboard"); - clearHuntTimer(); // Clean up all timer data - - // Show a brief message before redirecting - const timerEndedMessage = document.createElement("div"); - timerEndedMessage.innerHTML = ` -
-
⏰ Time's Up!
-
The treasure hunt has ended.
-
Redirecting to results...
-
- `; - document.body.appendChild(timerEndedMessage); - - if (isMounted.current) { - setTimeout(() => { - hardRedirect("/leaderboard?huntEnded=true"); - }, 2000); - } - return; - } - - // Otherwise, hunt is active and timer is running - if (isMounted.current) { - setHuntActive(true); - - // Update hunt timer data with the server's values - if (data.startTime) { - setHuntTimer(data.startTime, data.endTime); - } + // If hunt is active, ensure we have timer data + if (data.active && data.startTime) { + setHuntTimer(data.startTime); } } } catch (err) { @@ -343,298 +217,65 @@ export default function MapPage() { // Add this function before it's used in initializeTimer async function fetchTeamHuntStartTime() { try { - // First get the user's team ID - const userResponse = await fetch("/api/user", { - method: "GET", - headers: { - "Cache-Control": "no-store, no-cache, must-revalidate", - Pragma: "no-cache", - }, - credentials: "include", - }); - - if (!userResponse.ok) { - console.error("Failed to fetch user data:", userResponse.statusText); - return null; + // First try to get any existing timer data + const timerData = getHuntTimer(); + if (timerData && timerData.startTime) { + return true; } - const userData = await userResponse.json(); - const teamId = userData.teamId; - - if (!teamId) { - console.error("User does not belong to a team"); - return null; - } - - // Now fetch the team's hunt session using the team ID - const teamHuntResponse = await fetch( - `/api/teams/${teamId}/hunt-session`, - { - method: "GET", - headers: { - "Cache-Control": "no-store, no-cache, must-revalidate", - Pragma: "no-cache", - }, - credentials: "include", - } - ); - - if (!teamHuntResponse.ok) { - console.error( - "Failed to fetch team hunt session:", - teamHuntResponse.statusText - ); - - // Try the global hunt timer as fallback - const globalResponse = await fetch("/api/hunt/timer", { - method: "GET", - headers: { - "Cache-Control": "no-store, no-cache, must-revalidate", - Pragma: "no-cache", - }, - credentials: "include", - }); - - if (globalResponse.ok) { - const globalData = await globalResponse.json(); - return globalData.startTime || null; - } - - return null; - } - - const teamHuntData = await teamHuntResponse.json(); - return teamHuntData.startTime || null; - } catch (error) { - console.error("Error fetching team hunt start time:", error); - return null; + // If no timer data exists, sync with server + const result = await syncTimerWithServer(); + return result.success; + } catch (err) { + console.error("Error fetching team hunt start time:", err); + return false; } } // Use team hunt start time for elapsed time calculation useEffect(() => { - // Initialize the timer without waiting for teamData - // This ensures timer starts as soon as possible - const initializeTimer = async () => { - try { - // First try to get timer data from the centralized service - let timerData = getHuntTimer(); + let timerId; - // If no timer data yet, fetch it from API - if (!timerData || !timerData.startTime) { - console.log("No timer data found, fetching from API..."); - const startTimeStr = await fetchTeamHuntStartTime(); - console.log("Got start time:", startTimeStr); + // Initialize timer + async function initializeTimer() { + // First ensure we have timer data + await fetchTeamHuntStartTime(); - if (startTimeStr) { - setHuntTimer(startTimeStr); - timerData = getHuntTimer(); - } - } - - // Set up interval for timer updates - const timerInterval = setInterval(() => { - if (!isMounted.current) { - clearInterval(timerInterval); - return; - } - - const currentTimerData = getHuntTimer(); - if (currentTimerData && currentTimerData.startTime) { - const formattedTime = formatElapsedTime(currentTimerData.startTime); - console.log("Updated timer:", formattedTime); - setElapsedTime(formattedTime); - } else { - setElapsedTime("00:00:00"); - } - }, 1000); - - // Initial timer update + // Set up timer updates + const updateTimer = () => { + const timerData = getHuntTimer(); if (timerData && timerData.startTime) { setElapsedTime(formatElapsedTime(timerData.startTime)); } - - return () => { - clearInterval(timerInterval); - }; - } catch (error) { - console.error("Error initializing timer:", error); - } - }; - - initializeTimer(); - }, []); // Empty dependency array since fetchTeamHuntStartTime is defined inside the component - - // Check hunt status every second to ensure immediate updates - useEffect(() => { - const checkHuntStatus = async () => { - if (!isMounted.current) return; - - try { - const timestamp = Date.now(); // Add cache-busting timestamp - const response = await fetch(`/api/hunt-status?t=${timestamp}`, { - method: "GET", - headers: { - "Content-Type": "application/json", - "Cache-Control": "no-store, no-cache, must-revalidate", - Pragma: "no-cache", - }, - credentials: "include", - }); - - if (!response.ok) { - console.error("Failed to fetch hunt status:", response.statusText); - return; - } - - const data = await response.json(); - console.log("Map page hunt status check:", data); - - // If hunt is not active, check if it was active before (hunt ended) - if (!data.active) { - // Check if we had an active hunt before - const timerData = getHuntTimer(); - - if (timerData && timerData.isActive) { - // Hunt was active before but not now - it has ended - console.log("Hunt has ended, redirecting to leaderboard"); - clearHuntTimer(); - - if (isMounted.current) { - hardRedirect("/leaderboard?huntEnded=true"); - } - return; - } - - // If hunt was never active, redirect to waiting page - console.log("Hunt not active, redirecting to waiting page"); - if (isMounted.current) { - softRedirect("/waiting"); - } - return; - } - - // If timer endTime exists and has passed, redirect to leaderboard - if (data.endTime && new Date(data.endTime) <= new Date()) { - console.log("Timer expired, redirecting to leaderboard"); - if (isMounted.current) { - clearHuntTimer(); - hardRedirect("/leaderboard?huntEnded=true"); - } - return; - } - - // Otherwise, hunt is active and timer is running - if (isMounted.current) { - setHuntActive(true); - } - } catch (err) { - console.error("Failed to check hunt status:", err); - } - }; - - // Check immediately and every second - checkHuntStatus(); - const intervalId = setInterval(checkHuntStatus, 1000); - - return () => clearInterval(intervalId); - }, [softRedirect, hardRedirect]); - - // Add this useEffect to coordinate loading the map and clues - useEffect(() => { - // Only proceed if authenticated - if (status === "authenticated") { - const initMap = async () => { - try { - // Set loading state - setLoading(true); - - // Load both map and clues in parallel - await Promise.all([fetchClues(), loadAndEnhanceMap()]); - - // Data is loaded, we'll let the next effect handle enhancement - } catch (error) { - console.error("Error initializing map data:", error); - setError("Failed to load map data"); - setLoading(false); - } }; - initMap(); - } else if (status === "unauthenticated") { - router.push("/login"); - } - }, [status, fetchClues, loadAndEnhanceMap, router]); + // Initial update + updateTimer(); - // Modified useEffect to ensure SVG elements are visible + // Update every second + timerId = setInterval(updateTimer, 1000); + } + + initializeTimer(); + + // Clean up interval on unmount + return () => { + if (timerId) clearInterval(timerId); + }; + }, []); + + // Check hunt status every 10 seconds to ensure immediate updates useEffect(() => { - // Only try to enhance the map if both the SVG and clues are loaded - if (!mapSvg || !clues.length) return; + // Initial check + checkHuntAndTimer(); - console.log("Both SVG and clues are loaded, preparing to enhance map"); + // Set up interval to check hunt status + const huntStatusInterval = setInterval(checkHuntAndTimer, 10000); - let enhancementAttempts = 0; - const maxAttempts = 3; - - function attemptMapEnhancement() { - enhancementAttempts++; - console.log( - `Map enhancement attempt ${enhancementAttempts}/${maxAttempts}` - ); - - if ( - mapContainerRef.current && - mapContainerRef.current.querySelector("svg") - ) { - const svgElement = mapContainerRef.current.querySelector("svg"); - console.log("SVG found in DOM, enhancing map now"); - - // Try to enhance the map - enhanceSvgMap(); - - // Additional check after brief delay to ensure visibility - setTimeout(() => { - const cluePoints = svgElement.querySelectorAll(".clue-point"); - console.log( - `Found ${cluePoints.length} clue points after enhancement` - ); - - if (cluePoints.length === 0 && enhancementAttempts < maxAttempts) { - console.log( - `No clue points found, retrying (attempt ${enhancementAttempts}/${maxAttempts})` - ); - attemptMapEnhancement(); - } else if (cluePoints.length === 0) { - console.error( - `Failed to create clue points after ${maxAttempts} attempts` - ); - // Fix rendering issues as a last resort - fixCluePointsRendering(svgElement); - } else { - console.log( - "Map enhancement successful, fixing any rendering issues" - ); - fixCluePointsRendering(svgElement); - } - }, 300); - } else { - console.error("SVG not found in DOM after rendering timeout"); - - if (enhancementAttempts < maxAttempts) { - // Try again after a delay - setTimeout(attemptMapEnhancement, 500); - } else { - setMapError(true); - } - } - } - - // Ensure the SVG is fully loaded in the DOM before enhancing - requestAnimationFrame(() => { - // Initial delay before first attempt - setTimeout(attemptMapEnhancement, 300); - }); - }, [mapSvg, clues, enhanceSvgMap]); + return () => { + clearInterval(huntStatusInterval); + }; + }, [softRedirect, hardRedirect]); // Add useEffect to load team data useEffect(() => { @@ -680,11 +321,22 @@ export default function MapPage() {

Error Loading Map

We couldn't load the treasure map. Please try again.

- +
); } + // Function to determine clue button class based on status + const getClueButtonClass = (clue) => { + if (clue.found) return `${styles["clue-button"]} ${styles["clue-found"]}`; + if (clue.available) + return `${styles["clue-button"]} ${styles["clue-available"]}`; + return `${styles["clue-button"]} ${styles["clue-locked"]}`; + }; + + // Get the first clue if it exists + const firstClue = clues.length > 0 ? clues[0] : null; + return (
{/* Map Banner with team info and timer */} @@ -701,36 +353,41 @@ export default function MapPage() { {/* Main map content area */}
- {mapSvg ? ( -
+ Treasure Map - ) : ( -
-
-
Loading map...
+ + {/* Overlay div with the clue button - positioned exactly over the map */} +
+ {/* Hardcoded Clue 1 button */} + {firstClue && ( + + )}
- )} +
- {/* Add a refresh button for users if they have issues with the map */} - {mapSvg && ( - - )} - {/* Navigation bar */}