mirror of
https://github.com/mudabbir-ahmad/peworkshop.git
synced 2026-10-07 19:50:20 +00:00
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.
This commit is contained in:
1 parent
16319f1333
commit
30d88407a0
8 files changed
+550
-122
No files matched your search
@@ -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 (
|
||||
<div className="clue-container">
|
||||
@@ -123,6 +223,17 @@ export default function CluePage({ params }) {
|
||||
|
||||
return (
|
||||
<div className="clue-container">
|
||||
<div className="timer-display">{elapsedTime}</div>
|
||||
|
||||
<div className="clue-content-wrapper">
|
||||
{/* QR Scanner Section */}
|
||||
<div className="scanner-section">
|
||||
<h2>Scan QR Code</h2>
|
||||
<QRScanner onScan={handleScan} width={280} height={280} />
|
||||
{scanMessage && <div className="scan-message">{scanMessage}</div>}
|
||||
</div>
|
||||
|
||||
{/* Clue Information Section */}
|
||||
<div className="clue-scroll">
|
||||
{foundNow && (
|
||||
<div className="new-discovery">
|
||||
@@ -131,8 +242,6 @@ export default function CluePage({ params }) {
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="timer-display">{elapsedTime}</div>
|
||||
|
||||
<h1 className="clue-title">{clue.title}</h1>
|
||||
|
||||
<div className="clue-content">
|
||||
@@ -146,33 +255,105 @@ export default function CluePage({ params }) {
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="nav-buttons">
|
||||
<button onClick={() => router.push("/map")} className="map-button">
|
||||
Back to Map
|
||||
{/* Action Buttons */}
|
||||
<div className="action-buttons">
|
||||
<button onClick={handleSkip} className="skip-button">
|
||||
Skip
|
||||
</button>
|
||||
<button onClick={() => router.push("/")} className="home-button">
|
||||
Back to Home
|
||||
<button onClick={() => setShowScanner(true)} className="scan-button">
|
||||
Scan Clue
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Bottom Navigation */}
|
||||
<div className="nav-bar">
|
||||
<Link href="/map" className="nav-item">
|
||||
<div className="nav-icon">🗺️</div>
|
||||
<div className="nav-label">Map</div>
|
||||
</Link>
|
||||
<Link href="/leaderboard" className="nav-item">
|
||||
<div className="nav-icon">📊</div>
|
||||
<div className="nav-label">Stats</div>
|
||||
</Link>
|
||||
<Link href="/team-selection" className="nav-item">
|
||||
<div className="nav-icon">👥</div>
|
||||
<div className="nav-label">Team</div>
|
||||
</Link>
|
||||
<Link href="/" className="nav-item">
|
||||
<div className="nav-icon">🏠</div>
|
||||
<div className="nav-label">Home</div>
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<style jsx>{`
|
||||
.clue-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
min-height: 100vh;
|
||||
background-color: #f4f1ea;
|
||||
padding: 20px;
|
||||
padding-bottom: 70px; /* Space for bottom navbar */
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.clue-content-wrapper {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
max-width: 600px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.timer-display {
|
||||
position: absolute;
|
||||
top: 15px;
|
||||
right: 15px;
|
||||
background-color: rgba(0, 0, 0, 0.1);
|
||||
padding: 5px 10px;
|
||||
border-radius: 5px;
|
||||
font-family: monospace;
|
||||
font-weight: bold;
|
||||
font-size: 1rem;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.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%;
|
||||
}
|
||||
|
||||
.clue-scroll {
|
||||
background-color: #f8f3e3;
|
||||
border-radius: 10px;
|
||||
padding: 30px;
|
||||
padding: 20px;
|
||||
width: 100%;
|
||||
max-width: 600px;
|
||||
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1);
|
||||
position: relative;
|
||||
margin-bottom: 20px;
|
||||
@@ -187,8 +368,8 @@ export default function CluePage({ params }) {
|
||||
}
|
||||
|
||||
.clue-description {
|
||||
font-size: 1.2rem;
|
||||
line-height: 1.6;
|
||||
font-size: 1.1rem;
|
||||
line-height: 1.5;
|
||||
color: #5d4037;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
@@ -199,35 +380,39 @@ export default function CluePage({ params }) {
|
||||
border-top: 1px dashed #9c6b30;
|
||||
}
|
||||
|
||||
.nav-buttons {
|
||||
.action-buttons {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-top: 30px;
|
||||
width: 100%;
|
||||
margin-top: 10px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.map-button,
|
||||
.home-button {
|
||||
padding: 10px 20px;
|
||||
.skip-button,
|
||||
.scan-button {
|
||||
padding: 12px 0;
|
||||
border: none;
|
||||
border-radius: 5px;
|
||||
cursor: pointer;
|
||||
font-weight: bold;
|
||||
transition: all 0.2s;
|
||||
width: 48%;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.map-button {
|
||||
background-color: #68543c;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.home-button {
|
||||
.skip-button {
|
||||
background-color: #a98467;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.map-button:hover,
|
||||
.home-button:hover {
|
||||
transform: scale(1.05);
|
||||
.scan-button {
|
||||
background-color: #68543c;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.skip-button:hover,
|
||||
.scan-button:hover {
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.loading,
|
||||
@@ -262,25 +447,13 @@ export default function CluePage({ params }) {
|
||||
|
||||
.clue-title {
|
||||
color: #68543c;
|
||||
font-size: 2rem;
|
||||
font-size: 1.8rem;
|
||||
text-align: center;
|
||||
margin-bottom: 20px;
|
||||
border-bottom: 2px solid #9c6b30;
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
|
||||
.timer-display {
|
||||
position: absolute;
|
||||
top: 15px;
|
||||
right: 15px;
|
||||
background-color: rgba(0, 0, 0, 0.1);
|
||||
padding: 5px 10px;
|
||||
border-radius: 5px;
|
||||
font-family: monospace;
|
||||
font-weight: bold;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.new-discovery {
|
||||
background-color: #feefb3;
|
||||
border: 1px solid #9f6000;
|
||||
@@ -313,6 +486,37 @@ export default function CluePage({ params }) {
|
||||
.discovery-text {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.nav-bar {
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
background-color: white;
|
||||
border-top: 1px solid #ddd;
|
||||
padding: 10px 0;
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.nav-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
text-decoration: none;
|
||||
color: #666;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.nav-icon {
|
||||
font-size: 24px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.nav-label {
|
||||
font-size: 12px;
|
||||
}
|
||||
`}</style>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -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 */
|
||||
|
||||
+94
-39
@@ -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.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 = () => {
|
||||
// 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,47 +403,57 @@ 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
|
||||
// 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
|
||||
if (!timerData) {
|
||||
fetchTeamHuntStartTime().then((startTimeStr) => {
|
||||
// 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();
|
||||
startTimer(timerData);
|
||||
});
|
||||
} else {
|
||||
startTimer(timerData);
|
||||
}
|
||||
}
|
||||
|
||||
function startTimer(timerData) {
|
||||
const timer = setInterval(() => {
|
||||
// Set up interval for timer updates
|
||||
const timerInterval = setInterval(() => {
|
||||
if (!isMounted.current) {
|
||||
clearInterval(timer);
|
||||
clearInterval(timerInterval);
|
||||
return;
|
||||
}
|
||||
|
||||
// Refresh timer data on each interval to catch any updates
|
||||
const currentTimerData = getHuntTimer();
|
||||
if (!currentTimerData || !currentTimerData.isActive) {
|
||||
if (currentTimerData && currentTimerData.startTime) {
|
||||
const formattedTime = formatElapsedTime(currentTimerData.startTime);
|
||||
console.log("Updated timer:", formattedTime);
|
||||
setElapsedTime(formattedTime);
|
||||
} else {
|
||||
setElapsedTime("00:00:00");
|
||||
return;
|
||||
}
|
||||
|
||||
setElapsedTime(formatElapsedTime(currentTimerData.startTime));
|
||||
}, 1000);
|
||||
|
||||
return () => clearInterval(timer);
|
||||
// Initial timer update
|
||||
if (timerData && timerData.startTime) {
|
||||
setElapsedTime(formatElapsedTime(timerData.startTime));
|
||||
}
|
||||
|
||||
// Cleanup function
|
||||
return () => {
|
||||
isMounted.current = false;
|
||||
clearInterval(timerInterval);
|
||||
};
|
||||
}, [teamData, fetchTeamHuntStartTime]);
|
||||
} 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(() => {
|
||||
@@ -456,6 +510,7 @@ export default function MapPage() {
|
||||
// Check immediately and every second
|
||||
checkHuntStatus();
|
||||
const intervalId = setInterval(checkHuntStatus, 1000);
|
||||
|
||||
return () => clearInterval(intervalId);
|
||||
}, [softRedirect]);
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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 = ?', [
|
||||
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,
|
||||
teamId,
|
||||
]);
|
||||
|
||||
return Response.json({ message: 'Clue marked as found' });
|
||||
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 }
|
||||
);
|
||||
}
|
||||
}
|
||||
Binary file not shown.
+93
-21
@@ -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."
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Cleanup function
|
||||
return () => {
|
||||
if (scannerRef.current) {
|
||||
try {
|
||||
scannerRef.current.clear();
|
||||
} catch (err) {
|
||||
console.error("Failed to clear QR scanner:", err);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleError = (err) => {
|
||||
console.error(err);
|
||||
};
|
||||
}, [onScan, width, height, scanning]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<QrReader
|
||||
delay={300}
|
||||
onError={handleError}
|
||||
onScan={handleScan}
|
||||
style={{ width: '100%' }}
|
||||
/>
|
||||
<p>{result}</p>
|
||||
<div className="qr-scanner">
|
||||
{error && <div className="qr-error">{error}</div>}
|
||||
<div id="qr-reader" style={{ width, height }}></div>
|
||||
|
||||
<style jsx>{`
|
||||
.qr-scanner {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.qr-error {
|
||||
color: red;
|
||||
margin-bottom: 10px;
|
||||
text-align: center;
|
||||
padding: 8px;
|
||||
background-color: #ffebee;
|
||||
border-radius: 4px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
#qr-reader {
|
||||
border: 2px solid #ddd;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
|
||||
#qr-reader video {
|
||||
object-fit: cover;
|
||||
border-radius: 6px;
|
||||
}
|
||||
`}</style>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
+29
-3
@@ -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;
|
||||
}
|
||||
};
|
||||
|
||||
Reference in new issue
Block a user