From 30d88407a04ca4c7338375c561dc41a2ffa389d8 Mon Sep 17 00:00:00 2001 From: mudabbir-ahmad Date: Sat, 15 Mar 2025 16:52:50 +0000 Subject: [PATCH] fixing map timing errors and still trying to make leaderboard and clues pages. Grey = cant click Red = current Green = complete blue = skipped? Need to add Clue page Need to fix Leaderboard. --- app/(main)/clue/[clueId]/page.js | 316 +++++++++++++++++++++++++------ app/(main)/map/map.module.css | 27 +-- app/(main)/map/page.js | 185 +++++++++++------- app/api/clues/route.js | 4 +- app/api/mark-clue/route.js | 82 +++++++- clue_hunt.db | Bin 77824 -> 77824 bytes components/QRScanner.js | 120 +++++++++--- utils/timerService.js | 32 +++- 8 files changed, 597 insertions(+), 169 deletions(-) diff --git a/app/(main)/clue/[clueId]/page.js b/app/(main)/clue/[clueId]/page.js index 45f8407..60e1b03 100644 --- a/app/(main)/clue/[clueId]/page.js +++ b/app/(main)/clue/[clueId]/page.js @@ -3,6 +3,13 @@ import { useState, useEffect } from "react"; import { useRouter } from "next/navigation"; import { useSession } from "next-auth/react"; import { formatElapsedTime, getHuntTimer } from "@/utils/timerService"; +import Link from "next/link"; +import dynamic from "next/dynamic"; + +// Dynamically import QR scanner component with no SSR to avoid hydration issues +const QRScanner = dynamic(() => import("@/components/QRScanner"), { + ssr: false, +}); export default function CluePage({ params }) { const [clue, setClue] = useState(null); @@ -10,6 +17,9 @@ export default function CluePage({ params }) { 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 router = useRouter(); const { status } = useSession(); const { clueId } = params; @@ -78,6 +88,96 @@ export default function CluePage({ params }) { return () => clearInterval(timer); }, []); + // Handle QR scan result + const handleScan = (data) => { + setScanResult(data); + setScanMessage(`QR Code scanned: ${data}`); + + // Here you would typically verify the QR code matches expected value + // and then mark the clue as found via API + + // For now, just simulate successful scan and move to the next clue + try { + const scannedClueId = parseInt(data); + if (scannedClueId === parseInt(clueId)) { + // Correct QR code scanned, mark as found and move to next clue + markClueAsFound(clueId); + } else { + setScanMessage( + "Incorrect QR code. Please scan the correct code for this clue." + ); + } + } catch (err) { + setScanMessage("Invalid QR code format. Please try again."); + } + }; + + // Function to mark clue as found and navigate to the next clue + const markClueAsFound = async (currentClueId) => { + try { + // 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) { + // 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) + ); + + 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."); + } + } catch (err) { + console.error("Error marking clue as found:", err); + setScanMessage("Error verifying clue. Please try again."); + } + }; + + // 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(); + + // Find the current clue index + 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}`); + } 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"); + } + }; + if (loading) { return (
@@ -123,56 +223,137 @@ export default function CluePage({ params }) { return (
-
- {foundNow && ( -
-
πŸŽ‰
-
New Clue Discovered!
-
- )} +
{elapsedTime}
-
{elapsedTime}
+
+ {/* QR Scanner Section */} +
+

Scan QR Code

+ + {scanMessage &&
{scanMessage}
} +
-

{clue.title}

- -
-

Description

-

{clue.description}

- - {clue.location && ( -
-

Location Hint

-

{clue.location}

+ {/* Clue Information Section */} +
+ {foundNow && ( +
+
πŸŽ‰
+
New Clue Discovered!
)} + +

{clue.title}

+ +
+

Description

+

{clue.description}

+ + {clue.location && ( +
+

Location Hint

+

{clue.location}

+
+ )} +
-
- -
+ {/* Bottom Navigation */} +
+ +
πŸ—ΊοΈ
+
Map
+ + +
πŸ“Š
+
Stats
+ + +
πŸ‘₯
+
Team
+ + +
🏠
+
Home
+ +
+
); diff --git a/app/(main)/map/map.module.css b/app/(main)/map/map.module.css index 568c217..900a479 100644 --- a/app/(main)/map/map.module.css +++ b/app/(main)/map/map.module.css @@ -64,14 +64,18 @@ height: 100%; background-color: #69b5a8; /* Background color */ overflow: auto; /* Enable scrolling */ + display: flex; /* Add flexbox to center content */ + justify-content: center; /* Center horizontally */ + align-items: center; /* Center vertically */ } .treasure-map { - position: relative; /* Changed from absolute to relative */ + position: relative; width: 100%; height: 100%; - min-width: 100%; - min-height: 100%; + display: flex; /* Add flexbox for centering */ + justify-content: center; + align-items: center; } /* SVG container for better scrolling and zooming */ @@ -82,16 +86,16 @@ display: flex; justify-content: center; align-items: center; - overflow: auto; + overflow: visible; /* Change from auto to visible */ } .svg-container svg { - min-width: 100%; - min-height: 100%; + max-width: 100%; + max-height: 90vh; /* Limit height to 90% of viewport height */ width: auto; height: auto; display: block; - transform-origin: center center; + object-fit: contain; /* Ensure the entire SVG is visible */ } /* Map background - display as full-page map */ @@ -101,10 +105,8 @@ left: 0; width: 100%; height: 100%; - min-width: 100%; - min-height: 100%; background-image: url("/images/map.svg"); - background-size: contain; /* Change back to contain for proper aspect ratio */ + background-size: contain; background-position: center; background-repeat: no-repeat; z-index: 5; @@ -113,12 +115,11 @@ /* Circle elements in the SVG map - attach to a container class */ .svg-container circle { cursor: pointer; - transition: fill 0.3s ease, transform 0.3s ease; + transition: fill 0.3s ease; /* Only transition the fill color, not size */ } .svg-container circle:hover { - fill: #ffb74d !important; - transform: scale(1.1); + fill: #ffb74d !important; /* Only change color on hover, no movement */ } /* Remaining styles unchanged */ diff --git a/app/(main)/map/page.js b/app/(main)/map/page.js index 923c62a..c48a356 100644 --- a/app/(main)/map/page.js +++ b/app/(main)/map/page.js @@ -55,10 +55,11 @@ export default function MapPage() { return cachedStartTime; } - // Otherwise fetch from the server - if (session?.user?.teamId) { + // Otherwise fetch from the server - use effectiveTeamId which is derived from session + const effectiveTeamId = session?.user?.teamId; + if (effectiveTeamId) { const response = await fetch( - `/api/teams/${session.user.teamId}/hunt-session`, + `/api/teams/${effectiveTeamId}/hunt-session`, { method: "GET", headers: { @@ -85,7 +86,7 @@ export default function MapPage() { console.error("Failed to fetch team hunt start time:", error); return new Date().toISOString(); } - }, [session?.user?.teamId]); + }, []); // No dependencies to avoid re-creation - we'll handle session internally // IMPORTANT: Move fetchTeamData up here, before it's referenced in useEffect dependencies const fetchTeamData = useCallback(async () => { @@ -188,42 +189,81 @@ export default function MapPage() { // Find all circles in the SVG const circleSvgElements = mapContainerRef.current.querySelectorAll("circle"); + console.log(`Found ${circleSvgElements.length} circles in SVG`); + + // Find the highest available clue ID (first unfound clue) + let nextAvailableClueIndex = clues.findIndex((clue) => !clue.found); + if (nextAvailableClueIndex === -1) { + // If all clues are found, allow access to all + nextAvailableClueIndex = clues.length; + } // Convert NodeList to Array for easier manipulation Array.from(circleSvgElements).forEach((circle, index) => { - // Only process if we have a clue for this circle + // Only process if we have a clue for this index if (clues[index]) { + // Check if this clue is available (either found or next in sequence) + const isAvailable = + clues[index].found || index <= nextAvailableClueIndex; + // Make circle interactive - circle.style.cursor = "pointer"; - circle.style.fill = "#ff9800"; - circle.style.stroke = "#d87f00"; + circle.style.cursor = isAvailable ? "pointer" : "not-allowed"; + + // Set appropriate colors + if (clues[index].found) { + // Found clues are green + circle.style.fill = "#4caf50"; + circle.style.stroke = "#2e7d32"; + } else if (isAvailable) { + // Next available clue is orange + circle.style.fill = "#ff9800"; + circle.style.stroke = "#d87f00"; + } else { + // Unavailable clues are grey + circle.style.fill = "#9e9e9e"; + circle.style.stroke = "#757575"; + } + circle.style.strokeWidth = "2"; - // Add click handler to navigate to the clue page - circle.onclick = () => { - router.push(`/clue/${clues[index].id}`); + // Add click handler to navigate to the clue page, but only if available + circle.onclick = (e) => { + e.preventDefault(); + e.stopPropagation(); + + if (isAvailable) { + console.log(`Navigating to clue ${clues[index].id}`); + router.push(`/clue/${clues[index].id}`); + } else { + alert("Complete previous clues first to unlock this one!"); + } }; // Add hover effects circle.onmouseenter = () => { - circle.style.fill = "#ffb74d"; - circle.style.transform = "scale(1.1)"; - circle.style.transition = "all 0.3s ease"; + if (isAvailable) { + circle.style.fill = "#ffb74d"; // Lighter orange for hover + } }; circle.onmouseleave = () => { - circle.style.fill = "#ff9800"; - circle.style.transform = "scale(1)"; + if (clues[index].found) { + circle.style.fill = "#4caf50"; // Back to green for found clues + } else if (isAvailable) { + circle.style.fill = "#ff9800"; // Back to orange for available clues + } else { + circle.style.fill = "#9e9e9e"; // Back to grey for unavailable clues + } }; - // Optional: Add label or tooltip + // Add tooltip/title const label = document.createElementNS( "http://www.w3.org/2000/svg", "title" ); label.textContent = `Clue #${index + 1}: ${ clues[index].title || "Mystery clue" - }`; + }${!isAvailable ? " (Locked)" : ""}`; circle.appendChild(label); } }); @@ -279,10 +319,14 @@ export default function MapPage() { // Add effect to enhance SVG once it's loaded and clues are fetched useEffect(() => { if (mapSvg && clues.length > 0) { + console.log("SVG and clues loaded, enhancing map..."); + // Let the DOM update first with the SVG content - setTimeout(() => { + const enhanceTimeout = setTimeout(() => { enhanceSvgMap(); - }, 100); + }, 200); + + return () => clearTimeout(enhanceTimeout); } }, [mapSvg, clues, enhanceSvgMap]); @@ -359,53 +403,63 @@ export default function MapPage() { // Use team hunt start time for elapsed time calculation useEffect(() => { - if (!teamData?.team) return; - - // Get hunt timer data from the centralized service - let timerData = getHuntTimer(); - - // If no timer data yet, fetch it - if (!timerData) { - fetchTeamHuntStartTime().then((startTimeStr) => { - setHuntTimer(startTimeStr); - timerData = getHuntTimer(); - startTimer(timerData); - }); - } else { - startTimer(timerData); - } - - function startTimer(timerData) { - const timer = setInterval(() => { - if (!isMounted.current) { - clearInterval(timer); - return; + // 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(); + + // 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); + + if (startTimeStr) { + setHuntTimer(startTimeStr); + timerData = getHuntTimer(); + } } - - // Refresh timer data on each interval to catch any updates - const currentTimerData = getHuntTimer(); - if (!currentTimerData || !currentTimerData.isActive) { - setElapsedTime("00:00:00"); - return; + + // 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 + if (timerData && timerData.startTime) { + setElapsedTime(formatElapsedTime(timerData.startTime)); } - - setElapsedTime(formatElapsedTime(currentTimerData.startTime)); - }, 1000); - - return () => clearInterval(timer); - } - - // Cleanup function - return () => { - isMounted.current = false; + + return () => { + clearInterval(timerInterval); + }; + } catch (error) { + console.error("Error initializing timer:", error); + } }; - }, [teamData, fetchTeamHuntStartTime]); + + 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}`, { @@ -417,15 +471,15 @@ export default function MapPage() { }, 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, force redirect to waiting page if (!data.active) { console.log("Hunt not active, redirecting to waiting page"); @@ -434,7 +488,7 @@ export default function MapPage() { } 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"); @@ -443,7 +497,7 @@ export default function MapPage() { } return; } - + // Otherwise, hunt is active and timer is running if (isMounted.current) { setHuntActive(true); @@ -452,10 +506,11 @@ export default function MapPage() { console.error("Failed to check hunt status:", err); } }; - + // Check immediately and every second checkHuntStatus(); const intervalId = setInterval(checkHuntStatus, 1000); + return () => clearInterval(intervalId); }, [softRedirect]); diff --git a/app/api/clues/route.js b/app/api/clues/route.js index 65bdeaa..bfbe844 100644 --- a/app/api/clues/route.js +++ b/app/api/clues/route.js @@ -24,7 +24,7 @@ export async function GET(request) { ); } - // Get all clues + // Get all clues with explicit ordering to ensure consistency const clues = await query(` SELECT id, title, description, location, x_coord, y_coord FROM clues @@ -46,6 +46,8 @@ export async function GET(request) { }) ); + console.log(`Returning ${cluesWithStatus.length} clues`); + return Response.json(cluesWithStatus, { headers: { "Cache-Control": "no-store, no-cache, must-revalidate", diff --git a/app/api/mark-clue/route.js b/app/api/mark-clue/route.js index 350a5c6..b0abb08 100644 --- a/app/api/mark-clue/route.js +++ b/app/api/mark-clue/route.js @@ -1,12 +1,80 @@ -import { query } from '../../../lib/db'; +import { getToken } from "next-auth/jwt"; +import { queryOne, run } from "@/lib/db"; + +export const dynamic = "force-dynamic"; // Disable caching export async function POST(request) { - const { clueId, teamId } = await request.json(); + try { + const token = await getToken({ + req: request, + secret: + process.env.NEXTAUTH_SECRET || + "your-fallback-secret-should-be-at-least-32-chars", + }); - await query('UPDATE clues SET found = TRUE WHERE id = ? AND team_id = ?', [ - clueId, - teamId, + if (!token) { + return Response.json({ error: "Unauthorized" }, { status: 401 }); + } + + // Check if user has a team + if (!token.teamId) { + return Response.json( + { error: "You must be part of a team to mark clues as found" }, + { status: 403 } + ); + } + + const { clueId } = await request.json(); + + // Validate clueId is a number + if (!clueId || isNaN(parseInt(clueId))) { + return Response.json({ error: "Invalid clue ID" }, { status: 400 }); + } + + // Ensure the team_clues table exists + await run(` + CREATE TABLE IF NOT EXISTS team_clues ( + team_id INTEGER NOT NULL, + clue_id INTEGER NOT NULL, + found_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (team_id, clue_id), + FOREIGN KEY (team_id) REFERENCES teams(id), + FOREIGN KEY (clue_id) REFERENCES clues(id) + ) + `); + + // Check if this clue is already found + const found = await queryOne( + "SELECT 1 FROM team_clues WHERE team_id = ? AND clue_id = ?", + [token.teamId, clueId] + ); + + // If already found, return success with a message + if (found) { + return Response.json({ + success: true, + message: "This clue was already found", + alreadyFound: true, + }); + } + + // Record that the team found this clue + await run("INSERT INTO team_clues (team_id, clue_id) VALUES (?, ?)", [ + token.teamId, + clueId, ]); - return Response.json({ message: 'Clue marked as found' }); -} \ No newline at end of file + return Response.json({ + success: true, + message: "Clue marked as found successfully", + clueId: clueId, + teamId: token.teamId, + }); + } catch (error) { + console.error("Error marking clue as found:", error); + return Response.json( + { error: "Failed to mark clue as found: " + error.message }, + { status: 500 } + ); + } +} diff --git a/clue_hunt.db b/clue_hunt.db index f7e8dacc13c9eb372966d2522c2ab37cafa5162f..003581ca012107c9485f3bec39bc90d5606793bf 100644 GIT binary patch delta 1198 zcmZ{jyG|QH6hJ)^Lf*A^*9jIlU}N$iK}s0yoyR^bAxnWGTO|b*P`JoPP()hh4;&Fe zJ^(2V%EK0Ibg0f&B9uz=Xca(H6#bm`K zqHU0-A&x_;G24)4b~M6_=oe#a8iH6U5|9-s2ACEsXCe@H4UFs>iErdd^Y$d7X|iUR zaebz$)k+}5Jn{JfZKruJ=Bh9U4rORe04vH!b6LH5NO=8TMzEin0#R1i%Q$7|T zF^ENzfP{#?Y`HWAv1DRo*H{t55-~rKKE7nxEU<}-J;IaSoav4V9>EHMn`4sqg9x(8q;g{e~;h(|xg!cqr4W9>}2;T-?5#9ve oDLfB08w%{<;V^6zV`Jd7Wtg5J!>G)uU}$Cqwqd)7EaMv?02D4M2mk;8 diff --git a/components/QRScanner.js b/components/QRScanner.js index 27c92f7..0444880 100644 --- a/components/QRScanner.js +++ b/components/QRScanner.js @@ -1,30 +1,102 @@ -'use client'; // Mark as a Client Component -import { useState } from 'react'; -import QrReader from 'react-qr-reader'; +"use client"; +import { useState, useRef, useEffect } from "react"; +import { Html5QrcodeScanner } from "html5-qrcode"; -export default function QRScanner({ onScan }) { - const [result, setResult] = useState(''); +export default function QRScanner({ onScan, width = 300, height = 300 }) { + const [scanning, setScanning] = useState(false); + const [error, setError] = useState(null); + const scannerRef = useRef(null); - const handleScan = (data) => { - if (data) { - setResult(data); - onScan(parseInt(data, 10)); + useEffect(() => { + // Initialize scanner + if (!scanning && typeof window !== "undefined") { + try { + const qrScanner = new Html5QrcodeScanner( + "qr-reader", + { + fps: 10, + qrbox: { width: width * 0.7, height: height * 0.7 }, + aspectRatio: 1, + disableFlip: false, + formatsToSupport: ["QR_CODE"], + }, + false // don't start scanning immediately + ); + + // Store the scanner instance + scannerRef.current = qrScanner; + + // If onScan is provided, set up the success callback + if (onScan) { + qrScanner.render( + (decodedText) => { + if (onScan) { + onScan(decodedText); + } + setScanning(false); + }, + (errorMessage) => { + // No need to show decode errors to user as they're expected during scanning + console.log(errorMessage); + } + ); + setScanning(true); } - }; + } catch (err) { + console.error("QR Scanner initialization error:", err); + setError( + "Could not initialize QR scanner. Please make sure you have camera access." + ); + } + } - const handleError = (err) => { - console.error(err); + // Cleanup function + return () => { + if (scannerRef.current) { + try { + scannerRef.current.clear(); + } catch (err) { + console.error("Failed to clear QR scanner:", err); + } + } }; + }, [onScan, width, height, scanning]); - return ( -
- -

{result}

-
- ); -} \ No newline at end of file + return ( +
+ {error &&
{error}
} +
+ + +
+ ); +} diff --git a/utils/timerService.js b/utils/timerService.js index e034ad3..b30721f 100644 --- a/utils/timerService.js +++ b/utils/timerService.js @@ -19,26 +19,52 @@ export const clearHuntTimer = () => { localStorage.removeItem("teamHuntStartTime"); // Remove legacy timer as well }; -// Get the current hunt timer data +// Get the current hunt timer data with improved error handling export const getHuntTimer = () => { try { + // First try to get timer data from localStorage const timerData = localStorage.getItem("huntTimerData"); + if (!timerData) { // Try to migrate from old format if exists const legacyStartTime = localStorage.getItem("teamHuntStartTime"); if (legacyStartTime) { - return { + console.log("Using legacy team hunt start time"); + const migratedData = { startTime: legacyStartTime, endTime: null, isActive: true, lastUpdated: new Date().toISOString(), }; + + // Save the migrated data for future use + localStorage.setItem("huntTimerData", JSON.stringify(migratedData)); + return migratedData; } + console.log("No hunt timer data found"); return null; } - return JSON.parse(timerData); + + // Parse the timer data + const parsedData = JSON.parse(timerData); + + // Validate the data has required fields + if (!parsedData || !parsedData.startTime) { + console.log("Invalid hunt timer data", parsedData); + return null; + } + + return parsedData; } catch (error) { console.error("Error parsing hunt timer data:", error); + + // Try to recover by clearing corrupt data + try { + localStorage.removeItem("huntTimerData"); + } catch (e) { + // Ignore errors when trying to clean up + } + return null; } };