"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"; import NavBar from "@/components/NavBar"; // Dynamically import the QR scanner component with no SSR const EnhancedQRScanner = dynamic( () => import("@/components/EnhancedQRScanner"), { ssr: false, loading: () =>
Loading scanner...
, } ); export default function CluePage({ params }) { 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); 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); const isAdmin = session?.user?.role === "admin"; useEffect(() => { let isMounted = true; const now = Date.now(); const minRefreshInterval = isAdmin ? 30000 : 10000; if (loading === false && now - lastRefresh < minRefreshInterval) { return; } if (status === "unauthenticated") { router.push("/login"); return; } if (status === "loading") { return; } const fetchAllClues = async () => { try { const response = await fetch("/api/clues"); if (!response.ok) return []; const data = await response.json(); if (isMounted) { setAllClues(data); const currentIndex = data.findIndex((c) => c.id === parseInt(clueId)); if (currentIndex !== -1 && currentIndex < data.length - 1) { const nextClue = data[currentIndex + 1]; setNextClueId(nextClue.id); setNextClueAvailable(nextClue.found || nextClue.available); } } return data; } catch (err) { console.error("Error fetching all clues:", err); return []; } }; async function fetchClue() { try { setLoading(true); await fetchAllClues(); 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); 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]); useEffect(() => { const checkHuntStatus = async () => { try { const timerData = getHuntTimer(); let isActive = timerData?.isActive ?? true; if (isActive) { const result = await syncTimerWithServer(); if (result.success) { isActive = result.data?.active ?? false; } } setHuntActive(isActive); if (previousHuntState && !isActive) { console.log("Hunt has ended! Redirecting to leaderboard..."); router.push("/leaderboard?huntEnded=true"); } setPreviousHuntState(isActive); } catch (err) { console.error("Error checking hunt status:", err); } }; checkHuntStatus(); const intervalId = setInterval(checkHuntStatus, 5000); return () => { clearInterval(intervalId); }; }, [router, previousHuntState]); async function fetchTeamHuntStartTime() { try { const timerData = getHuntTimer(); if (timerData && timerData.startTime) { return true; } 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; } } useEffect(() => { if (timerInitializedRef.current) { return; } async function initializeTimer() { await fetchTeamHuntStartTime(); if (!timerIdRef.current) { const updateTimer = () => { const timerData = getHuntTimer(); if (timerData && timerData.startTime) { setElapsedTime(formatElapsedTime(timerData.startTime)); if (timerData.endTime) { const now = Date.now(); const endTime = new Date(timerData.endTime).getTime(); if (now >= endTime) { setHuntActive(false); clearInterval(timerIdRef.current); timerIdRef.current = null; console.log( "Hunt end time reached! Redirecting to leaderboard..." ); router.push("/leaderboard?huntEnded=true"); } } } }; updateTimer(); timerIdRef.current = setInterval(updateTimer, 1000); timerInitializedRef.current = true; } } initializeTimer(); return () => { if (timerIdRef.current) { clearInterval(timerIdRef.current); timerIdRef.current = null; timerInitializedRef.current = false; } }; }, [router]); useEffect(() => { 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 (

Loading clue...

); } if (!huntActive) { // Display temporary message before redirecting return (
🏁

Hunt Has Ended!

The treasure hunt has concluded. Redirecting to results page...

); } if (error) { return (
Error: {error}
); } if (!clue) { return (
Clue not found
); } return (
{/* Map Banner with team info and timer */}
Team Name: {teamData?.team?.name || "Loading..."}
{elapsedTime}
{teamData?.team?.code || ""}
{showSuccessNotification && (
🎉

Clue Found!

Great work! You've found this clue.

)} {showNavErrorNotification && (
🔒

Clue Locked

You need to find this clue first before moving to the next one.

)}
{/* Show notification based on found OR skipped status */} {clueFound && (
✓
You've already found this clue!
)} {clueSkipped && !clueFound && (
↷
You skipped this clue earlier. Try to solve it now!
)} {/* Admin tools section - only visible to admins */} {isAdmin && (
Admin View
)} {/* QR Scanner Section */}

Scan QR Code

{scanMessage && (
{scanMessage}
)} {/* QR Code helper removed as requested */}
{/* Clue Information Section */}

{clue?.title}

Description

{clue?.description}

{clue?.location && (

Location Hint

{clue.location}

)}
{/* Navigation Buttons */}
{/* Replace hardcoded navbar with NavBar component */}
); }