From 02a0088d288aab1af35c3f1534e02bae0d4536aa Mon Sep 17 00:00:00 2001
From: mudabbir-ahmad
Date: Mon, 17 Mar 2025 12:41:17 +0000
Subject: [PATCH] SCANNER WORKS!!! JUST ADD THE MORE CLUE BUTTONS !!! AND NEXT
CLUE ERROR CHECKING
---
app/(main)/clue/[clueId]/page.js | 218 +++++++++++--
app/api/clues/[clueId]/route.js | 5 +-
app/api/team/[teamId]/members/route.js | 54 ++++
clue_hunt.db | Bin 86016 -> 131072 bytes
components/EnhancedQRScanner.js | 413 ++++++++++++++++++++-----
components/QRScanner.js | 40 ++-
components/scanner-polyfill.js | 78 +++++
lib/db.js | 40 ++-
package-lock.json | 19 +-
package.json | 2 +
yarn.lock | 209 +++++++------
11 files changed, 861 insertions(+), 217 deletions(-)
create mode 100644 app/api/team/[teamId]/members/route.js
create mode 100644 components/scanner-polyfill.js
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 19a4a3f0d483ca8372fb3dae2d12d8cb21a377b7..f87b39ee4aa7d06005a8c5f42c47bb1bb1e67a21 100644
GIT binary patch
literal 131072
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
VC3T_lr(WC}o}evxtvV!O+0U#MsKf
zc(YE)4r!1ui>a|e)Z~Nm!U{m`Fu(z1aH8YQ3JDweHaiH2FiJ}@vM~VF7#kTFnd%xC
z>lzv<7@Aobm|B^dY+CS^pRsWByY+$s!iB6rGno0`gN)_i|IPoE|NUm62Y2~5zd!%g
t9_l9E?cewr`3yLi_&+i5|Kk6&T~LAX89yI83nL>VKNAxx3nM2dKLC3dLkIu>
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 && (
-