diff --git a/app/(main)/clue/[clueId]/page.js b/app/(main)/clue/[clueId]/page.js
index eb9b5b8..e9f5ef2 100644
--- a/app/(main)/clue/[clueId]/page.js
+++ b/app/(main)/clue/[clueId]/page.js
@@ -1,5 +1,5 @@
"use client";
-import { useState, useEffect } from "react";
+import { useState, useEffect, useRef } from "react";
import { useRouter } from "next/navigation";
import { useSession } from "next-auth/react";
import {
@@ -10,9 +10,8 @@ import {
import Link from "next/link";
import dynamic from "next/dynamic";
import { use } from "react";
-import QRCodeHelper from "@/components/QRCodeHelper";
-// Dynamically import the new QR scanner component with no SSR
+// Dynamically import the QR scanner component with no SSR
const EnhancedQRScanner = dynamic(
() => import("@/components/EnhancedQRScanner"),
{
@@ -35,10 +34,13 @@ export default function CluePage({ params }) {
const [clueFound, setClueFound] = useState(false);
const [allClues, setAllClues] = useState([]);
const [nextClueId, setNextClueId] = useState(null);
- const [huntActive, setHuntActive] = useState(true); // Track hunt active status
- const [previousHuntState, setPreviousHuntState] = useState(true); // To detect changes
+ 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";
@@ -219,9 +221,41 @@ export default function CluePage({ params }) {
};
}, [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) => {
- setScanMessage(`QR Code scanned: ${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"
@@ -284,6 +318,19 @@ export default function CluePage({ params }) {
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.");
}
@@ -293,6 +340,15 @@ export default function CluePage({ params }) {
}
};
+ // Clear notification timeout on component unmount
+ useEffect(() => {
+ return () => {
+ if (notificationTimeoutRef.current) {
+ clearTimeout(notificationTimeoutRef.current);
+ }
+ };
+ }, []);
+
// Navigate to the next clue
const handleNextClue = () => {
if (nextClueId) {
@@ -381,7 +437,27 @@ export default function CluePage({ params }) {
return (
-
{elapsedTime}
+ {/* Map Banner with team info and timer */}
+
+
{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 && (
@@ -396,7 +472,7 @@ export default function CluePage({ params }) {
)}
- {/* QR Scanner Section - Removed manual entry button */}
+ {/* QR Scanner Section */}
Scan QR Code
@@ -405,8 +481,7 @@ export default function CluePage({ params }) {
{scanMessage}
)}
- {/* Add the QR Code helper component */}
-
+ {/* QR Code helper removed as requested */}
{/* Clue Information Section */}
@@ -455,9 +530,9 @@ export default function CluePage({ params }) {
👥
Team
-
- 🏠
- Home
+
+ 🏆
+ Leaderboard
@@ -467,11 +542,57 @@ export default function CluePage({ params }) {
flex-direction: column;
min-height: 100vh;
background-color: #f4f1ea;
- padding: 20px;
padding-bottom: 70px; /* Space for bottom navbar */
position: relative;
}
+ .success-notification {
+ position: fixed;
+ top: 0;
+ left: 0;
+ width: 100%;
+ padding: 20px;
+ background-color: rgba(0, 0, 0, 0.7);
+ z-index: 1000;
+ display: flex;
+ justify-content: center;
+ animation: slide-down 0.5s ease-out;
+ }
+
+ .success-notification-content {
+ display: flex;
+ align-items: center;
+ background-color: #4caf50;
+ color: white;
+ padding: 15px 25px;
+ border-radius: 10px;
+ box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2);
+ max-width: 400px;
+ }
+
+ .success-icon {
+ font-size: 2rem;
+ margin-right: 15px;
+ }
+
+ .success-text h3 {
+ margin: 0 0 5px 0;
+ font-size: 1.2rem;
+ }
+
+ .success-text p {
+ margin: 0;
+ }
+
+ @keyframes slide-down {
+ 0% {
+ transform: translateY(-100%);
+ }
+ 100% {
+ transform: translateY(0);
+ }
+ }
+
.hunt-ended {
text-align: center;
padding: 30px;
@@ -497,6 +618,7 @@ export default function CluePage({ params }) {
}
.clue-content-wrapper {
+ padding: 0 20px;
flex: 1;
display: flex;
flex-direction: column;
@@ -506,19 +628,6 @@ export default function CluePage({ params }) {
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;
@@ -724,6 +833,61 @@ export default function CluePage({ params }) {
.admin-refresh-button:hover {
background-color: #c9302c;
}
+
+ /* Map Banner Styles */
+ .map-banner {
+ background-color: #68543c;
+ color: white;
+ padding: 15px;
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ border-bottom: 2px solid #4a3c2a;
+ z-index: 10;
+ flex-shrink: 0;
+ margin-bottom: 20px;
+ }
+
+ .team-name {
+ font-size: 18px;
+ font-weight: bold;
+ flex: 1;
+ text-align: left;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ padding-right: 5px;
+ }
+
+ .elapsed-time {
+ text-align: center;
+ padding: 5px 10px;
+ background-color: rgba(0, 0, 0, 0.2);
+ border-radius: 8px;
+ flex: 0 0 auto;
+ margin: 0 10px;
+ }
+
+ .time-value {
+ font-size: 18px;
+ font-weight: bold;
+ }
+
+ .team-code {
+ font-size: 16px;
+ font-weight: bold;
+ flex: 1;
+ text-align: right;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ padding-left: 5px;
+ }
+
+ /* Remove the standalone timer display as it's now in the banner */
+ .timer-display {
+ display: none;
+ }
`}
);
diff --git a/app/api/clues/[clueId]/route.js b/app/api/clues/[clueId]/route.js
index 994b975..5eab2ff 100644
--- a/app/api/clues/[clueId]/route.js
+++ b/app/api/clues/[clueId]/route.js
@@ -24,8 +24,9 @@ export async function GET(request, context) {
);
}
- // Get the clueId directly from the params
- const { clueId } = context.params;
+ // Get the clueId from params - properly awaited
+ const params = await context.params;
+ const { clueId } = params;
// Validate clueId is a number
if (isNaN(parseInt(clueId))) {
diff --git a/app/api/team/[teamId]/members/route.js b/app/api/team/[teamId]/members/route.js
new file mode 100644
index 0000000..28f21cb
--- /dev/null
+++ b/app/api/team/[teamId]/members/route.js
@@ -0,0 +1,54 @@
+import { query } from "@/lib/db";
+import { cookies } from "next/headers";
+import { verifyToken } from "@/app/api/auth/[...nextauth]/route";
+
+export async function GET(request, context) {
+ try {
+ const cookieStore = cookies();
+ const token = cookieStore.get("next-auth.session-token")?.value;
+
+ if (!token) {
+ return Response.json({ error: "Unauthorized" }, { status: 401 });
+ }
+
+ const userData = verifyToken(token);
+ if (!userData) {
+ return Response.json({ error: "Invalid session" }, { status: 401 });
+ }
+
+ const { teamId } = context.params;
+ const requestedTeamId = parseInt(teamId, 10);
+ const userTeamId = parseInt(userData.teamId, 10);
+
+ console.log("Requested team:", requestedTeamId);
+ console.log("User team:", userTeamId);
+ console.log("User role:", userData.role);
+
+ // Ensure the user is requesting their own team or is an admin
+ if (userTeamId !== requestedTeamId && userData.role !== "admin") {
+ return Response.json(
+ { error: "Not authorized to view this team" },
+ { status: 403 }
+ );
+ }
+
+ const teamMembers = await query("SELECT * FROM users WHERE team_id = ?", [
+ requestedTeamId,
+ ]);
+
+ if (!teamMembers) {
+ return Response.json(
+ { error: "Team members not found" },
+ { status: 404 }
+ );
+ }
+
+ return Response.json(teamMembers);
+ } catch (error) {
+ console.error("Team members fetch error:", error);
+ return Response.json(
+ { error: "Failed to fetch team members" },
+ { status: 500 }
+ );
+ }
+}
diff --git a/clue_hunt.db b/clue_hunt.db
index 19a4a3f..f87b39e 100644
Binary files a/clue_hunt.db and b/clue_hunt.db differ
diff --git a/components/EnhancedQRScanner.js b/components/EnhancedQRScanner.js
index f6481df..1a1c502 100644
--- a/components/EnhancedQRScanner.js
+++ b/components/EnhancedQRScanner.js
@@ -1,6 +1,7 @@
"use client";
import { useState, useEffect, useRef } from "react";
-import { Scanner } from "@yudiel/react-qr-scanner";
+import { useZxing } from "react-zxing";
+import { ensureScannerCompatibility } from "./scanner-polyfill";
export default function EnhancedQRScanner({
onScan,
@@ -12,12 +13,38 @@ export default function EnhancedQRScanner({
const [initialized, setInitialized] = useState(false);
const [cameraActive, setCameraActive] = useState(false);
const [hasCameras, setHasCameras] = useState(true);
- const [retryCount, setRetryCount] = useState(0);
- const maxRetries = 3;
- const scannerRef = useRef(null);
+ const [scanSuccess, setScanSuccess] = useState(false);
+ const [manualInputValue, setManualInputValue] = useState("");
+ const [torchEnabled, setTorchEnabled] = useState(false);
+ const inputRef = useRef(null);
+ const [cameraPermissionDenied, setCameraPermissionDenied] = useState(false);
+
+ // Define video constraints explicitly
+ const videoConstraints = {
+ facingMode: "environment",
+ width: { min: 640, ideal: 1280, max: 1920 },
+ height: { min: 480, ideal: 720, max: 1080 },
+ };
+
+ // Setup the zxing scanner hook with explicit video constraints
+ const { ref, torch } = useZxing({
+ onDecodeResult(result) {
+ handleDecode(result.getText());
+ },
+ onError(error) {
+ handleError(error);
+ },
+ constraints: {
+ video: videoConstraints,
+ },
+ paused: !cameraActive,
+ });
- // Check if we're in a secure context
+ // Check if we're in a secure context and if camera is accessible
useEffect(() => {
+ // Ensure scanner compatibility
+ ensureScannerCompatibility();
+
// Check if we're in a secure context (https or localhost)
const secure = window.isSecureContext;
setIsSecureContext(secure);
@@ -25,6 +52,23 @@ export default function EnhancedQRScanner({
// Check if camera can be accessed
async function checkCameraAccess() {
try {
+ // First check if MediaDevices API is supported
+ if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) {
+ throw new Error("MediaDevices API is not supported in this browser");
+ }
+
+ // Explicitly request video permission - important to avoid the error
+ const stream = await navigator.mediaDevices.getUserMedia({
+ video: true,
+ audio: false
+ });
+
+ // If we got a stream, we have permission - release it immediately
+ if (stream) {
+ stream.getTracks().forEach(track => track.stop());
+ }
+
+ // Now enumerate devices to confirm we have video devices
const devices = await navigator.mediaDevices.enumerateDevices();
const videoDevices = devices.filter(
(device) => device.kind === "videoinput"
@@ -37,38 +81,88 @@ export default function EnhancedQRScanner({
setHasCameras(false);
} else {
setHasCameras(true);
+ setCameraActive(true); // Activate camera if we have permission
}
setInitialized(true);
} catch (err) {
- console.error("Error checking camera devices:", err);
- setError(`Camera initialization error: ${err.message}`);
+ console.error("Camera access error:", err);
+
+ // Handle permission denied specifically
+ if (err.name === "NotAllowedError" || err.name === "PermissionDeniedError") {
+ setCameraPermissionDenied(true);
+ setError("Camera permission denied. Please allow camera access in your browser settings.");
+ } else {
+ setError(`Camera error: ${err.message}`);
+ }
+
setInitialized(true);
}
}
- checkCameraAccess();
+ if (isSecureContext) {
+ checkCameraAccess();
+ } else {
+ setInitialized(true); // Still mark as initialized even if not secure
+ }
}, []);
- // Handle successful scan
- const handleDecode = (result) => {
- if (onScan && result) {
- onScan(result);
+ // Toggle torch/flashlight
+ const toggleTorch = () => {
+ if (torch) {
+ setTorchEnabled(!torchEnabled);
+ torch.toggled = !torchEnabled;
}
};
- // Try to restart the camera
- const retryCamera = () => {
- if (retryCount < maxRetries) {
+ // Handle decode result
+ const handleDecode = (text) => {
+ console.log("QR scan result:", text);
+
+ if (!onScan || !text) return;
+
+ try {
+ // Show success visual
+ setScanSuccess(true);
+ setTimeout(() => setScanSuccess(false), 1500);
+
+ // Call the onScan callback with the decoded text
+ onScan(text);
+ } catch (err) {
+ console.error("Error processing QR scan result:", err);
+ }
+ };
+
+ // Manually request camera access
+ const requestCameraAccess = async () => {
+ try {
setError(null);
- setRetryCount((prev) => prev + 1);
- // Force re-render of scanner component
- setCameraActive(false);
- setTimeout(() => setCameraActive(true), 500);
- } else {
- setError(
- "Could not access camera after multiple attempts. Please refresh the page or try another device."
- );
+
+ // Explicitly request video permission
+ const stream = await navigator.mediaDevices.getUserMedia({
+ video: true,
+ audio: false
+ });
+
+ // If we got a stream, we have permission
+ if (stream) {
+ // Release the stream since useZxing will request it again
+ stream.getTracks().forEach(track => track.stop());
+ setCameraActive(true);
+ setCameraPermissionDenied(false);
+ }
+ } catch (err) {
+ console.error("Failed to get camera permission:", err);
+ setError(`Camera error: ${err.message}`);
+ setCameraPermissionDenied(true);
+ }
+ };
+
+ // Handle manual input submission
+ const handleManualSubmit = () => {
+ if (manualInputValue.trim()) {
+ onScan(manualInputValue.trim());
+ setManualInputValue("");
}
};
@@ -89,6 +183,7 @@ export default function EnhancedQRScanner({
) {
errorMessage +=
"Camera permission denied. Please allow camera access in your browser settings.";
+ setCameraPermissionDenied(true);
} else if (
error.name === "NotFoundError" ||
error.name === "DevicesNotFoundError"
@@ -111,13 +206,6 @@ export default function EnhancedQRScanner({
setError(errorMessage);
};
- // When initialized, activate camera
- useEffect(() => {
- if (initialized && hasCameras && isSecureContext && !error) {
- setCameraActive(true);
- }
- }, [initialized, hasCameras, isSecureContext, error]);
-
// Handle secure context warning
if (!isSecureContext) {
return (
@@ -129,6 +217,7 @@ export default function EnhancedQRScanner({
network IP.
+
);
}
@@ -147,8 +236,29 @@ export default function EnhancedQRScanner({
{error && (
{error}
- {retryCount < maxRetries && (
-