"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"; // 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 }) { // 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 [allClues, setAllClues] = useState([]); const [nextClueId, setNextClueId] = useState(null); const [huntActive, setHuntActive] = useState(true); const [previousHuntState, setPreviousHuntState] = useState(true); const [showSuccessNotification, setShowSuccessNotification] = useState(false); const [teamData, setTeamData] = useState(null); const router = useRouter(); const { data: session, status } = useSession(); const notificationTimeoutRef = useRef(null); // 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) { 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(); 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); 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]); // Separate effect to handle timer updates useEffect(() => { let timerId; const timerData = getHuntTimer(); if (!timerData || !timerData.isActive) { return; } // Initial set setElapsedTime(formatElapsedTime(timerData.startTime)); // Update timer every second timerId = setInterval(() => { 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(timerId); // Redirect to leaderboard with huntEnded flag console.log("Hunt end time reached! Redirecting to leaderboard..."); router.push("/leaderboard?huntEnded=true"); } } }, 1000); return () => { if (timerId) clearInterval(timerId); }; }, [router]); // 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 { // Handle multiple QR code formats: "QR123", "123", or even "Clue 123" let scannedClueId; // 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) { scannedClueId = parseInt(match[1], 10); } else { // Try direct parsing as a last resort scannedClueId = parseInt(data, 10); } if (!isNaN(scannedClueId) && scannedClueId === parseInt(clueId)) { // 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( "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." ); // 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 timeout on component unmount useEffect(() => { return () => { if (notificationTimeoutRef.current) { clearTimeout(notificationTimeoutRef.current); } }; }, []); // Navigate to the next clue const handleNextClue = () => { if (nextClueId) { router.push(`/clue/${nextClueId}`); } else { router.push("/map"); } }; // Navigate to the previous clue const handlePrevClue = () => { if (allClues.length > 0) { const currentIndex = allClues.findIndex((c) => c.id === parseInt(clueId)); if (currentIndex > 0) { router.push(`/clue/${allClues[currentIndex - 1].id}`); } else { router.push("/map"); } } 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.

)}
{/* 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 */}
{/* Bottom Navigation */}
πŸ—ΊοΈ
Map
πŸ“Š
Stats
πŸ‘₯
Team
πŸ†
Leaderboard
); }