FIXED CLUES! all that needs to be done is manually adding circle buttons and hiding/showing feature through API.. maybe fixing QR scanner

This commit is contained in:
bobbert committed 2025-03-17 10:42:33 +00:00
1 parent 2236dceac9
commit 726121e105
18 files changed
+1504 -1192

No files matched your search

+3
View File
@@ -0,0 +1,3 @@
legacy-peer-deps=true
fund=false
save-exact=true
+145 -141
View File
@@ -5,36 +5,43 @@ import { useSession } from "next-auth/react";
import { formatElapsedTime, getHuntTimer } from "@/utils/timerService";
import Link from "next/link";
import dynamic from "next/dynamic";
import { use } from "react";
import QRCodeHelper from "@/components/QRCodeHelper";
// Dynamically import QR scanner component with no SSR to avoid hydration issues
const QRScanner = dynamic(() => import("@/components/QRScanner"), {
ssr: false,
});
// Dynamically import the new QR scanner component with no SSR
const EnhancedQRScanner = dynamic(
() => import("@/components/EnhancedQRScanner"),
{
ssr: false,
loading: () => <div className="scanner-loading">Loading scanner...</div>,
}
);
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 [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 [lastRefresh, setLastRefresh] = useState(0); // Track last refresh time
const [lastRefresh, setLastRefresh] = useState(0);
const [clueFound, setClueFound] = useState(false);
const [allClues, setAllClues] = useState([]);
const [nextClueId, setNextClueId] = useState(null);
const router = useRouter();
const { data: session, status } = useSession(); // Get session to check if user is admin
const { clueId } = params;
const { data: session, status } = useSession();
// Check if user is admin
const isAdmin = session?.user?.role === "admin";
// This effect handles authentication and initial clue data fetch
useEffect(() => {
let isMounted = true; // Flag to prevent state updates after unmount
let isMounted = true;
const now = Date.now();
// Only allow refreshes every 10 seconds (more for admins)
const minRefreshInterval = isAdmin ? 30000 : 10000; // 30 seconds for admins, 10 seconds for regular users
const minRefreshInterval = isAdmin ? 30000 : 10000;
// Skip if we refreshed too recently and this isn't the first load
if (loading === false && now - lastRefresh < minRefreshInterval) {
@@ -50,10 +57,34 @@ export default function CluePage({ params }) {
return;
}
// One-time fetch clue data with debouncing
// 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();
@@ -76,16 +107,11 @@ export default function CluePage({ params }) {
}
const data = await response.json();
// Only update state if the component is still mounted
if (isMounted) {
setClue(data);
// Only set foundNow on initial load or if it's actually newly found
if (loading === true) {
setFoundNow(data.foundNow);
}
setClueFound(data.found);
setLoading(false);
setLastRefresh(Date.now()); // Update last refresh timestamp
setLastRefresh(Date.now());
}
} catch (err) {
console.error("Error fetching clue:", err);
@@ -98,17 +124,14 @@ export default function CluePage({ params }) {
fetchClue();
// Cleanup function to prevent state updates after unmount
return () => {
isMounted = false;
};
}, [clueId, router, status, lastRefresh, loading, isAdmin]); // Add isAdmin and lastRefresh to dependencies
}, [clueId, router, status, lastRefresh, loading, isAdmin]);
// Separate effect to handle timer updates - doesn't re-fetch data
// Separate effect to handle timer updates
useEffect(() => {
let timerId;
// Set up timer display updates
const timerData = getHuntTimer();
if (!timerData || !timerData.isActive) {
return;
@@ -125,42 +148,60 @@ export default function CluePage({ params }) {
return () => {
if (timerId) clearInterval(timerId);
};
}, []); // Empty dependency array so it only runs once
}, []);
// Handle QR scan result
const handleScan = (data) => {
setScanResult(data);
setScanMessage(`QR Code scanned: ${data}`);
try {
// Handle both "QR123" format and just "123" format
// Handle multiple QR code formats: "QR123", "123", or even "Clue 123"
let scannedClueId;
const match = data.match(/^QR(\d+)$/i);
// 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) {
// Extract the number from QR[number]
scannedClueId = parseInt(match[1], 10);
} else {
// Try to parse the raw value
// Try direct parsing as a last resort
scannedClueId = parseInt(data, 10);
}
if (!isNaN(scannedClueId) && scannedClueId === parseInt(clueId)) {
// Correct QR code scanned, mark as found and move to next clue
// 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(
"Incorrect QR code. Please scan the correct code for this clue."
"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 and navigate to the next clue
// 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",
@@ -169,29 +210,11 @@ export default function CluePage({ params }) {
});
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)
// Update local state to show clue is found
setClueFound(true);
setScanMessage(
"✅ Clue verified! You can now proceed to the next clue."
);
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.");
}
@@ -201,35 +224,35 @@ export default function CluePage({ params }) {
}
};
// 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();
// Navigate to the next clue
const handleNextClue = () => {
if (nextClueId) {
router.push(`/clue/${nextClueId}`);
} else {
router.push("/map");
}
};
// Find the current clue index
// Navigate to the previous clue
const handlePrevClue = () => {
if (allClues.length > 0) {
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}`);
if (currentIndex > 0) {
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");
} else {
router.push("/map");
}
};
// Function for manual refresh - for admins to test without waiting
const handleManualRefresh = () => {
// Force a refresh by setting lastRefresh to 0
setLastRefresh(0);
};
// Rendering states
if (loading) {
return (
<div className="clue-container">
@@ -276,7 +299,6 @@ export default function CluePage({ params }) {
return (
<div className="clue-container">
<div className="timer-display">{elapsedTime}</div>
<div className="clue-content-wrapper">
{/* Admin tools section - only visible to admins */}
{isAdmin && (
@@ -291,29 +313,26 @@ export default function CluePage({ params }) {
</div>
)}
{/* QR Scanner Section */}
{/* QR Scanner Section - Removed manual entry button */}
<div className="scanner-section">
<h2>Scan QR Code</h2>
<QRScanner onScan={handleScan} width={280} height={280} />
{scanMessage && <div className="scan-message">{scanMessage}</div>}
<EnhancedQRScanner onScan={handleScan} width={280} height={280} />
{scanMessage && (
<div className={`scan-message ${clueFound ? "success" : ""}`}>
{scanMessage}
</div>
)}
{/* Add the QR Code helper component */}
<QRCodeHelper clueId={clueId} />
</div>
{/* Clue Information Section */}
<div className="clue-scroll">
{foundNow && (
<div className="new-discovery">
<div className="discovery-icon">🎉</div>
<div className="discovery-text">New Clue Discovered!</div>
</div>
)}
<h1 className="clue-title">{clue.title}</h1>
<h1 className="clue-title">{clue?.title}</h1>
<div className="clue-content">
<h2>Description</h2>
<p className="clue-description">{clue.description}</p>
{clue.location && (
<p className="clue-description">{clue?.description}</p>
{clue?.location && (
<div className="clue-location">
<h2>Location Hint</h2>
<p>{clue.location}</p>
@@ -322,13 +341,19 @@ export default function CluePage({ params }) {
</div>
</div>
{/* Action Buttons */}
<div className="action-buttons">
<button onClick={handleSkip} className="skip-button">
Skip
{/* Navigation Buttons */}
<div className="navigation-buttons">
<button onClick={handlePrevClue} className="nav-button prev-button">
Previous Clue
</button>
<button onClick={() => setShowScanner(true)} className="scan-button">
Scan Clue
<button
onClick={() => router.push("/map")}
className="nav-button map-button"
>
Back to Map
</button>
<button onClick={handleNextClue} className="nav-button next-button">
Next Clue
</button>
</div>
</div>
@@ -414,6 +439,12 @@ export default function CluePage({ params }) {
width: 100%;
}
.scan-message.success {
background-color: #c8e6c9;
color: #2e7d32;
font-weight: bold;
}
.clue-scroll {
background-color: #f8f3e3;
border-radius: 10px;
@@ -445,39 +476,46 @@ export default function CluePage({ params }) {
border-top: 1px dashed #9c6b30;
}
.action-buttons {
.navigation-buttons {
display: flex;
justify-content: space-between;
width: 100%;
margin-top: 10px;
margin-top: 20px;
margin-bottom: 20px;
flex-wrap: wrap; /* Allow buttons to wrap on small screens */
gap: 10px;
}
.skip-button,
.scan-button {
padding: 12px 0;
.nav-button {
padding: 12px 15px;
border: none;
border-radius: 5px;
cursor: pointer;
font-weight: bold;
transition: all 0.2s;
width: 48%;
font-size: 16px;
font-size: 14px;
flex: 1;
min-width: 120px;
}
.skip-button {
.prev-button {
background-color: #a98467;
color: white;
}
.scan-button {
.map-button {
background-color: #607d8b;
color: white;
}
.next-button {
background-color: #68543c;
color: white;
}
.skip-button:hover,
.scan-button:hover {
.nav-button:hover {
opacity: 0.9;
transform: translateY(-2px);
}
.loading,
@@ -519,39 +557,6 @@ export default function CluePage({ params }) {
padding-bottom: 10px;
}
.new-discovery {
background-color: #feefb3;
border: 1px solid #9f6000;
color: #9f6000;
padding: 15px;
border-radius: 5px;
margin-bottom: 20px;
display: flex;
align-items: center;
animation: pulse 2s infinite;
}
@keyframes pulse {
0% {
opacity: 1;
}
50% {
opacity: 0.7;
}
100% {
opacity: 1;
}
}
.discovery-icon {
font-size: 1.5rem;
margin-right: 10px;
}
.discovery-text {
font-weight: bold;
}
.nav-bar {
display: flex;
justify-content: space-around;
@@ -583,7 +588,6 @@ export default function CluePage({ params }) {
font-size: 12px;
}
/* Admin tools styling */
.admin-tools {
background-color: #f8d7da;
border: 2px dashed #d9534f;
+81 -138
View File
@@ -64,162 +64,45 @@
height: 100%;
background-color: #69b5a8; /* Background color */
overflow: auto; /* Enable scrolling */
display: block; /* Change from flex to block for proper scrolling */
display: block; /* Use block for proper scrolling */
}
.treasure-map {
position: relative;
width: fit-content; /* Change from 100% to fit-content to allow natural svg size */
width: fit-content; /* Allow natural width */
min-width: 100%; /* Ensure it's at least full width */
height: fit-content;
min-height: 100%;
display: flex; /* Keep flex for alignment */
display: flex;
justify-content: flex-end; /* Align to the right */
align-items: flex-start; /* Align to the top */
}
/* SVG container for better scrolling and zooming */
.svg-container {
/* Map image container for positioning */
.map-image-container {
position: relative;
width: fit-content; /* Allow natural width */
height: fit-content; /* Allow natural height */
display: block; /* Change to block for proper scrolling */
overflow: visible; /* Show content beyond boundaries */
z-index: 5; /* Ensure it's above the background */
width: fit-content;
height: fit-content;
}
.svg-container svg {
max-width: none; /* Remove max-width limitation */
/* Map image styling */
.map-image {
display: block;
max-width: none; /* Don't constrain the image */
width: auto;
height: auto;
display: block;
object-fit: none; /* Don't constrain the SVG */
position: relative;
}
/* Force SVG elements to show properly with z-index */
.svg-container svg * {
transform-box: fill-box;
}
/* Ensure the clue points overlay is always on top */
.svg-container svg .clue-points-overlay {
position: relative !important;
z-index: 9999 !important;
pointer-events: all !important;
}
/* Make clue points very visible with stronger styling */
.svg-container .clue-point {
fill-opacity: 1 !important;
stroke-opacity: 1 !important;
pointer-events: all !important;
cursor: pointer !important;
stroke-width: 3px !important;
r: 14px !important;
filter: drop-shadow(0px 0px 5px rgba(0, 0, 0, 0.7)) !important;
}
/* Remove the default map background when we have the processed SVG */
.svg-container + .map-background {
display: none;
}
/* Map background - only shown when SVG is loading */
.map-background {
/* Map overlay positioned absolutely over the map image */
.map-overlay {
position: absolute;
top: 0;
right: 0; /* Align to the right */
width: 100vw; /* Full viewport width */
height: auto;
background-image: url("/images/map.svg");
background-size: auto; /* Don't resize the background */
background-position: top right; /* Position at top right */
background-repeat: no-repeat;
z-index: 3; /* Lower z-index than the processed SVG */
}
/* Circle elements in the SVG map - attach to a container class */
.svg-container circle {
cursor: pointer;
transition: fill 0.3s ease; /* Only transition the fill color, not size */
}
.svg-container circle:hover {
fill: #ffb74d !important; /* Only change color on hover, no movement */
}
/* Circle elements in the SVG map - attach to a container class */
.svg-container circle.clue-point {
cursor: pointer;
transition: fill 0.3s ease, stroke-width 0.3s ease;
pointer-events: all; /* Ensure clicks are detected */
stroke-width: 2px;
r: 8; /* Set a minimum radius for all clue points */
}
/* Improve circle visibility and interactivity */
.svg-container circle.clue-point {
cursor: pointer !important;
transition: all 0.3s ease;
pointer-events: all !important;
stroke-width: 2px;
r: 12px; /* Larger radius for better visibility and easier clicks */
z-index: 10;
position: relative;
}
/* Enhance hover effect */
.svg-container circle.clue-point:hover {
fill: #ffb74d !important; /* Orange hover color */
stroke: #ff6d00 !important;
stroke-width: 4px !important;
transform: scale(1.2);
z-index: 20;
filter: drop-shadow(0px 0px 4px rgba(0, 0, 0, 0.5));
}
/* Invisible click areas for easier interaction */
.svg-container circle.clue-point-click-area {
pointer-events: all !important;
cursor: pointer !important;
opacity: 0.01; /* Nearly invisible but still interactive */
z-index: 5;
}
/* Create custom animation for the current clue */
@keyframes clue-point-pulse {
0% {
stroke-width: 3px;
transform: scale(1);
filter: drop-shadow(0px 0px 2px rgba(255, 152, 0, 0.7));
}
50% {
stroke-width: 5px;
transform: scale(1.2);
filter: drop-shadow(0px 0px 8px rgba(255, 152, 0, 0.9));
}
100% {
stroke-width: 3px;
transform: scale(1);
filter: drop-shadow(0px 0px 2px rgba(255, 152, 0, 0.7));
}
}
.svg-container circle.clue-point:hover {
fill: #ffb74d !important; /* Orange hover color */
stroke: #ff6d00;
stroke-width: 3px;
transform: scale(1.1);
}
/* Remaining styles unchanged */
.map-svg {
width: 100% !important; /* Override Next.js Image default width */
height: auto !important; /* Override Next.js Image default height */
max-width: none; /* Allow image to grow beyond container */
object-fit: contain;
display: block;
left: 0;
width: 100%;
height: 100%;
pointer-events: auto; /* Allow interaction with buttons */
z-index: 20; /* Ensure overlay is above map */
box-sizing: border-box;
}
.loading-container {
@@ -274,8 +157,8 @@
box-sizing: border-box;
z-index: 100; /* Ensure it stays on top */
flex-shrink: 0; /* Prevent navbar from shrinking */
margin: 0; /* Added margin property */
height: auto; /* Added height property */
margin: 0;
height: auto;
}
.nav-item {
@@ -343,3 +226,63 @@
font-size: 11px;
}
}
/* Clue button styling */
.clue-button {
position: absolute;
width: 60px;
height: 60px;
border-radius: 50%;
background-color: #9e9e9e; /* Default grey background */
color: white;
font-weight: bold;
border: 3px solid #757575;
cursor: pointer;
display: flex;
justify-content: center;
align-items: center;
font-size: 14px;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.3);
transition: all 0.3s ease;
z-index: 30;
transform: translate(-50%, -50%); /* Center the button on its position */
}
.clue-button:hover {
transform: translate(-50%, -50%) scale(1.1);
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.4);
}
/* Clue status styles */
.clue-found {
background-color: #2e7d32; /* Green for found clues */
border-color: #1b5e20;
}
.clue-available {
background-color: #ff9800; /* Orange for available clues */
border-color: #e65100;
animation: pulse 1.5s infinite;
}
.clue-locked {
background-color: #9e9e9e; /* Grey for locked clues */
border-color: #757575;
cursor: default;
}
/* Pulse animation for available clues */
@keyframes pulse {
0% {
transform: translate(-50%, -50%) scale(1);
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.3);
}
50% {
transform: translate(-50%, -50%) scale(1.1);
box-shadow: 0 4px 8px rgba(255, 152, 0, 0.6);
}
100% {
transform: translate(-50%, -50%) scale(1);
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.3);
}
}
+146 -489
View File
@@ -9,13 +9,8 @@ import {
setHuntTimer,
clearHuntTimer,
formatElapsedTime,
syncTimerWithServer,
} from "@/utils/timerService";
import {
populateMap,
updateCluePointStatus,
attachClueClickHandlers,
} from "@/utils/map-populate";
import { addDebugOverlay, fixCluePointsRendering } from "@/utils/map-debug";
export default function MapPage() {
const { data: session, status } = useSession();
@@ -24,7 +19,6 @@ export default function MapPage() {
const [error, setError] = useState(null);
const [teamData, setTeamData] = useState(null);
const [elapsedTime, setElapsedTime] = useState("00:00:00");
const [mapSvg, setMapSvg] = useState(null);
const [clues, setClues] = useState([]);
const mapContainerRef = useRef(null);
const router = useRouter();
@@ -120,6 +114,7 @@ export default function MapPage() {
});
setClues(processedClues);
setLoading(false); // Set loading to false after clues are fetched
return processedClues;
} catch (err) {
console.error("Failed to fetch clues:", err);
@@ -128,211 +123,90 @@ export default function MapPage() {
}
}, []);
// Enhanced function to populate the map with clue points
const enhanceSvgMap = useCallback(() => {
if (!mapContainerRef.current) return;
// Find the SVG element inside the map container
const svgElement = mapContainerRef.current.querySelector("svg");
if (!svgElement) {
console.error("No SVG element found in map container");
setMapError(true);
return;
}
console.log("SVG element found, starting enhancement process");
// Clear out any existing map-background elements to ensure we only show one map
const existingMapBgs = document.querySelectorAll(
`.${styles["map-background"]}`
);
existingMapBgs.forEach((bg) => (bg.style.display = "none"));
// First populate the map with basic clue points
populateMap(svgElement)
.then((success) => {
if (success) {
console.log("Map populated with clue points successfully");
// Process the clues to determine which are available
const processedClues = clues.map((clue, index, arr) => {
// Find the first unfound clue's index
const firstUnfoundIndex = arr.findIndex((c) => !c.found);
// A clue is available if:
// 1. It's already found, OR
// 2. It's the next unfound clue in the sequence
const available = clue.found || index === firstUnfoundIndex;
return { ...clue, available };
});
// Debug info about clue statuses
processedClues.forEach((clue, index) => {
console.log(
`Clue ${clue.id} (index ${index}): Available: ${clue.available}, Found: ${clue.found}`
);
});
// Update the clue point appearances based on their statuses
updateCluePointStatus(svgElement, processedClues);
// Define a direct navigation function that will be used for click handling
const navigateToClue = (clueId) => {
console.log(`Navigating to clue ${clueId}`);
router.push(`/clue/${clueId}`);
};
// Attach click handlers to all clue points and click areas
attachClueClickHandlers(svgElement, processedClues, navigateToClue);
} else {
console.error("Failed to populate map with clue points");
setMapError(true);
}
})
.catch((error) => {
console.error("Error enhancing SVG map:", error);
setMapError(true);
});
}, [clues, router, styles]);
// Function to load and enhance the SVG map
const loadAndEnhanceMap = useCallback(async () => {
try {
// Fetch the SVG file
const response = await fetch("/images/map.svg");
if (!response.ok) {
console.error(
"Failed to load SVG:",
response.status,
response.statusText
);
setMapError(true);
return;
// Handle clue button click
const handleClueClick = useCallback(
(clueId, isAvailable, isFound) => {
if (isFound || isAvailable) {
router.push(`/clue/${clueId}`);
} else {
alert("Complete previous clues to unlock this one!");
}
},
[router]
);
const svgText = await response.text();
console.log(`SVG loaded, length: ${svgText.length} characters`);
// Size and position the overlay to match the map image
const setupOverlay = useCallback(() => {
const overlay = document.getElementById("map-overlay-container");
const mapImg = document.getElementById("treasure-map-img");
// Ensure SVG has correct namespaces and viewBox
let fixedSvg = svgText.replace(
/<svg/,
'<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"'
);
if (overlay && mapImg) {
// Wait for the image to load to get its dimensions
mapImg.onload = () => {
const mapRect = mapImg.getBoundingClientRect();
overlay.style.width = `${mapRect.width}px`;
overlay.style.height = `${mapRect.height}px`;
};
// Ensure SVG has a proper viewBox attribute if missing
if (!fixedSvg.includes('viewBox="')) {
const widthMatch = fixedSvg.match(/width="([^"]+)"/);
const heightMatch = fixedSvg.match(/height="([^"]+)"/);
if (widthMatch && heightMatch) {
const width = widthMatch[1];
const height = heightMatch[1];
fixedSvg = fixedSvg.replace(
/<svg/,
`<svg viewBox="0 0 ${width} ${height}"`
);
}
}
// Handle resize events
const resizeOverlay = () => {
const mapRect = mapImg.getBoundingClientRect();
overlay.style.width = `${mapRect.width}px`;
overlay.style.height = `${mapRect.height}px`;
};
setMapSvg(fixedSvg);
setLoading(false);
} catch (error) {
console.error("Failed to load map SVG:", error);
setMapError(true);
setLoading(false);
// Initial resize and listen for window resize
setTimeout(resizeOverlay, 300);
window.addEventListener("resize", resizeOverlay);
return () => {
window.removeEventListener("resize", resizeOverlay);
};
}
}, []);
// Add this useEffect to coordinate loading the map and clues
useEffect(() => {
if (status === "authenticated") {
fetchClues();
fetchTeamData();
} else if (status === "unauthenticated") {
softRedirect("/login");
}
// Setup cleanup function for component unmount
return () => {
isMounted.current = false;
};
}, [status, fetchClues, fetchTeamData, softRedirect]);
// Setup the overlay when the component is mounted
useEffect(() => {
if (!loading && clues.length > 0) {
return setupOverlay();
}
}, [loading, clues.length, setupOverlay]);
// Function to check hunt status and timer
async function checkHuntAndTimer() {
if (!isMounted.current) return;
try {
const response = await fetch("/api/hunt-status", {
method: "GET",
headers: {
"Content-Type": "application/json",
"Cache-Control": "no-cache, no-store, must-revalidate",
Pragma: "no-cache",
},
credentials: "include",
});
if (response.ok) {
const data = await response.json();
console.log("Hunt status check from map:", data);
setHuntActive(data.active);
// If hunt is not active, check if it has ended
if (!data.active) {
console.log("Hunt not active, checking if it ended...");
// Check if we had an active hunt before
const timerData = getHuntTimer();
if (timerData && timerData.isActive) {
// Hunt was active before but not now - it has ended
console.log("Hunt has ended, redirecting to leaderboard");
clearHuntTimer(); // Clean up all timer data
// Show a brief message before redirecting
const huntEndedMessage = document.createElement("div");
huntEndedMessage.innerHTML = `
<div style="position:fixed; top:0; left:0; right:0; bottom:0; background:rgba(0,0,0,0.8); color:white; display:flex; flex-direction:column; align-items:center; justify-content:center; z-index:9999">
<div style="font-size:32px; margin-bottom:20px">🎯 Hunt Complete!</div>
<div style="font-size:24px; margin-bottom:20px">The treasure hunt has ended.</div>
<div style="font-size:18px">Redirecting to results...</div>
</div>
`;
document.body.appendChild(huntEndedMessage);
// Use the appropriate redirect method after a short delay
setTimeout(() => {
if (isMounted.current) {
hardRedirect("/leaderboard?huntEnded=true");
}
}, 2000);
return;
}
// Otherwise just show the waiting page
if (isMounted.current) {
setHuntActive(false);
softRedirect("/waiting");
}
return;
}
// If timer endTime exists and has passed, redirect to leaderboard
if (data.endTime && new Date(data.endTime) <= new Date()) {
console.log("Timer expired, redirecting to leaderboard");
clearHuntTimer(); // Clean up all timer data
// Show a brief message before redirecting
const timerEndedMessage = document.createElement("div");
timerEndedMessage.innerHTML = `
<div style="position:fixed; top:0; left:0; right:0; bottom:0; background:rgba(0,0,0,0.8); color:white; display:flex; flex-direction:column; align-items:center; justify-content:center; z-index:9999">
<div style="font-size:32px; margin-bottom:20px">⏰ Time's Up!</div>
<div style="font-size:24px; margin-bottom:20px">The treasure hunt has ended.</div>
<div style="font-size:18px">Redirecting to results...</div>
</div>
`;
document.body.appendChild(timerEndedMessage);
if (isMounted.current) {
setTimeout(() => {
hardRedirect("/leaderboard?huntEnded=true");
}, 2000);
}
return;
}
// Otherwise, hunt is active and timer is running
if (isMounted.current) {
setHuntActive(true);
// Update hunt timer data with the server's values
if (data.startTime) {
setHuntTimer(data.startTime, data.endTime);
}
// If hunt is active, ensure we have timer data
if (data.active && data.startTime) {
setHuntTimer(data.startTime);
}
}
} catch (err) {
@@ -343,298 +217,65 @@ export default function MapPage() {
// Add this function before it's used in initializeTimer
async function fetchTeamHuntStartTime() {
try {
// First get the user's team ID
const userResponse = await fetch("/api/user", {
method: "GET",
headers: {
"Cache-Control": "no-store, no-cache, must-revalidate",
Pragma: "no-cache",
},
credentials: "include",
});
if (!userResponse.ok) {
console.error("Failed to fetch user data:", userResponse.statusText);
return null;
// First try to get any existing timer data
const timerData = getHuntTimer();
if (timerData && timerData.startTime) {
return true;
}
const userData = await userResponse.json();
const teamId = userData.teamId;
if (!teamId) {
console.error("User does not belong to a team");
return null;
}
// Now fetch the team's hunt session using the team ID
const teamHuntResponse = await fetch(
`/api/teams/${teamId}/hunt-session`,
{
method: "GET",
headers: {
"Cache-Control": "no-store, no-cache, must-revalidate",
Pragma: "no-cache",
},
credentials: "include",
}
);
if (!teamHuntResponse.ok) {
console.error(
"Failed to fetch team hunt session:",
teamHuntResponse.statusText
);
// Try the global hunt timer as fallback
const globalResponse = await fetch("/api/hunt/timer", {
method: "GET",
headers: {
"Cache-Control": "no-store, no-cache, must-revalidate",
Pragma: "no-cache",
},
credentials: "include",
});
if (globalResponse.ok) {
const globalData = await globalResponse.json();
return globalData.startTime || null;
}
return null;
}
const teamHuntData = await teamHuntResponse.json();
return teamHuntData.startTime || null;
} catch (error) {
console.error("Error fetching team hunt start time:", error);
return null;
// If no timer data exists, sync with server
const result = await syncTimerWithServer();
return result.success;
} catch (err) {
console.error("Error fetching team hunt start time:", err);
return false;
}
}
// Use team hunt start time for elapsed time calculation
useEffect(() => {
// 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();
let timerId;
// 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);
// Initialize timer
async function initializeTimer() {
// First ensure we have timer data
await fetchTeamHuntStartTime();
if (startTimeStr) {
setHuntTimer(startTimeStr);
timerData = getHuntTimer();
}
}
// Set up interval for timer updates
const timerInterval = setInterval(() => {
if (!isMounted.current) {
clearInterval(timerInterval);
return;
}
const currentTimerData = getHuntTimer();
if (currentTimerData && currentTimerData.startTime) {
const formattedTime = formatElapsedTime(currentTimerData.startTime);
console.log("Updated timer:", formattedTime);
setElapsedTime(formattedTime);
} else {
setElapsedTime("00:00:00");
}
}, 1000);
// Initial timer update
// Set up timer updates
const updateTimer = () => {
const timerData = getHuntTimer();
if (timerData && timerData.startTime) {
setElapsedTime(formatElapsedTime(timerData.startTime));
}
return () => {
clearInterval(timerInterval);
};
} 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(() => {
const checkHuntStatus = async () => {
if (!isMounted.current) return;
try {
const timestamp = Date.now(); // Add cache-busting timestamp
const response = await fetch(`/api/hunt-status?t=${timestamp}`, {
method: "GET",
headers: {
"Content-Type": "application/json",
"Cache-Control": "no-store, no-cache, must-revalidate",
Pragma: "no-cache",
},
credentials: "include",
});
if (!response.ok) {
console.error("Failed to fetch hunt status:", response.statusText);
return;
}
const data = await response.json();
console.log("Map page hunt status check:", data);
// If hunt is not active, check if it was active before (hunt ended)
if (!data.active) {
// Check if we had an active hunt before
const timerData = getHuntTimer();
if (timerData && timerData.isActive) {
// Hunt was active before but not now - it has ended
console.log("Hunt has ended, redirecting to leaderboard");
clearHuntTimer();
if (isMounted.current) {
hardRedirect("/leaderboard?huntEnded=true");
}
return;
}
// If hunt was never active, redirect to waiting page
console.log("Hunt not active, redirecting to waiting page");
if (isMounted.current) {
softRedirect("/waiting");
}
return;
}
// If timer endTime exists and has passed, redirect to leaderboard
if (data.endTime && new Date(data.endTime) <= new Date()) {
console.log("Timer expired, redirecting to leaderboard");
if (isMounted.current) {
clearHuntTimer();
hardRedirect("/leaderboard?huntEnded=true");
}
return;
}
// Otherwise, hunt is active and timer is running
if (isMounted.current) {
setHuntActive(true);
}
} catch (err) {
console.error("Failed to check hunt status:", err);
}
};
// Check immediately and every second
checkHuntStatus();
const intervalId = setInterval(checkHuntStatus, 1000);
return () => clearInterval(intervalId);
}, [softRedirect, hardRedirect]);
// Add this useEffect to coordinate loading the map and clues
useEffect(() => {
// Only proceed if authenticated
if (status === "authenticated") {
const initMap = async () => {
try {
// Set loading state
setLoading(true);
// Load both map and clues in parallel
await Promise.all([fetchClues(), loadAndEnhanceMap()]);
// Data is loaded, we'll let the next effect handle enhancement
} catch (error) {
console.error("Error initializing map data:", error);
setError("Failed to load map data");
setLoading(false);
}
};
initMap();
} else if (status === "unauthenticated") {
router.push("/login");
}
}, [status, fetchClues, loadAndEnhanceMap, router]);
// Initial update
updateTimer();
// Modified useEffect to ensure SVG elements are visible
// Update every second
timerId = setInterval(updateTimer, 1000);
}
initializeTimer();
// Clean up interval on unmount
return () => {
if (timerId) clearInterval(timerId);
};
}, []);
// Check hunt status every 10 seconds to ensure immediate updates
useEffect(() => {
// Only try to enhance the map if both the SVG and clues are loaded
if (!mapSvg || !clues.length) return;
// Initial check
checkHuntAndTimer();
console.log("Both SVG and clues are loaded, preparing to enhance map");
// Set up interval to check hunt status
const huntStatusInterval = setInterval(checkHuntAndTimer, 10000);
let enhancementAttempts = 0;
const maxAttempts = 3;
function attemptMapEnhancement() {
enhancementAttempts++;
console.log(
`Map enhancement attempt ${enhancementAttempts}/${maxAttempts}`
);
if (
mapContainerRef.current &&
mapContainerRef.current.querySelector("svg")
) {
const svgElement = mapContainerRef.current.querySelector("svg");
console.log("SVG found in DOM, enhancing map now");
// Try to enhance the map
enhanceSvgMap();
// Additional check after brief delay to ensure visibility
setTimeout(() => {
const cluePoints = svgElement.querySelectorAll(".clue-point");
console.log(
`Found ${cluePoints.length} clue points after enhancement`
);
if (cluePoints.length === 0 && enhancementAttempts < maxAttempts) {
console.log(
`No clue points found, retrying (attempt ${enhancementAttempts}/${maxAttempts})`
);
attemptMapEnhancement();
} else if (cluePoints.length === 0) {
console.error(
`Failed to create clue points after ${maxAttempts} attempts`
);
// Fix rendering issues as a last resort
fixCluePointsRendering(svgElement);
} else {
console.log(
"Map enhancement successful, fixing any rendering issues"
);
fixCluePointsRendering(svgElement);
}
}, 300);
} else {
console.error("SVG not found in DOM after rendering timeout");
if (enhancementAttempts < maxAttempts) {
// Try again after a delay
setTimeout(attemptMapEnhancement, 500);
} else {
setMapError(true);
}
}
}
// Ensure the SVG is fully loaded in the DOM before enhancing
requestAnimationFrame(() => {
// Initial delay before first attempt
setTimeout(attemptMapEnhancement, 300);
});
}, [mapSvg, clues, enhanceSvgMap]);
return () => {
clearInterval(huntStatusInterval);
};
}, [softRedirect, hardRedirect]);
// Add useEffect to load team data
useEffect(() => {
@@ -680,11 +321,22 @@ export default function MapPage() {
<div className={styles["map-error"]}>
<h2>Error Loading Map</h2>
<p>We couldn't load the treasure map. Please try again.</p>
<button onClick={loadAndEnhanceMap}>Retry</button>
<button onClick={fetchClues}>Retry</button>
</div>
);
}
// Function to determine clue button class based on status
const getClueButtonClass = (clue) => {
if (clue.found) return `${styles["clue-button"]} ${styles["clue-found"]}`;
if (clue.available)
return `${styles["clue-button"]} ${styles["clue-available"]}`;
return `${styles["clue-button"]} ${styles["clue-locked"]}`;
};
// Get the first clue if it exists
const firstClue = clues.length > 0 ? clues[0] : null;
return (
<div className={styles["map-page"]}>
{/* Map Banner with team info and timer */}
@@ -701,36 +353,41 @@ export default function MapPage() {
{/* Main map content area */}
<div className={styles["map-container"]} ref={mapContainerRef}>
<div className={styles["treasure-map"]}>
{mapSvg ? (
<div
className={styles["svg-container"]}
dangerouslySetInnerHTML={{ __html: mapSvg }}
{/* Simpler map implementation using an img element */}
<div className={styles["map-image-container"]}>
<img
id="treasure-map-img"
src="/images/map.svg"
alt="Treasure Map"
className={styles["map-image"]}
/>
) : (
<div className={styles["loading-container"]}>
<div className={styles["loading-spinner"]}></div>
<div>Loading map...</div>
{/* Overlay div with the clue button - positioned exactly over the map */}
<div id="map-overlay-container" className={styles["map-overlay"]}>
{/* Hardcoded Clue 1 button */}
{firstClue && (
<button
className={getClueButtonClass(firstClue)}
onClick={() =>
handleClueClick(
firstClue.id,
firstClue.available,
firstClue.found
)
}
style={{
left: "150px",
top: "200px",
}}
>
Clue {firstClue.id}
</button>
)}
</div>
)}
</div>
</div>
</div>
{/* Add a refresh button for users if they have issues with the map */}
{mapSvg && (
<button
className={styles["refresh-map-button"]}
onClick={() => {
const svgElement = mapContainerRef.current?.querySelector("svg");
if (svgElement) {
enhanceSvgMap();
setTimeout(() => fixCluePointsRendering(svgElement), 300);
}
}}
>
Refresh Map
</button>
)}
{/* Navigation bar */}
<nav className={styles["nav-bar"]}>
<Link
+3 -45
View File
@@ -24,6 +24,7 @@ export async function GET(request, context) {
);
}
// Get the clueId directly from the params
const { clueId } = context.params;
// Validate clueId is a number
@@ -31,7 +32,7 @@ export async function GET(request, context) {
return Response.json({ error: "Invalid clue ID" }, { status: 400 });
}
// Get the specific clue - removed coordinates
// Get the specific clue
const clue = await queryOne(
"SELECT id, title, description, location FROM clues WHERE id = ?",
[clueId]
@@ -41,54 +42,12 @@ export async function GET(request, context) {
return Response.json({ error: "Clue not found" }, { status: 404 });
}
// Get timestamp parameter to determine if this is a refresh call
const timestamp = request.nextUrl?.searchParams?.get("t");
const isRefresh = timestamp && !isNaN(parseInt(timestamp));
// Check if this team has found this clue before
const found = await queryOne(
"SELECT * FROM team_clues WHERE team_id = ? AND clue_id = ?",
[token.teamId, clueId]
);
// Only mark as "foundNow" if:
// 1. The clue was not found before
// 2. This is not a refresh request (or we can't determine)
// 3. The user is not an admin (admins don't need "foundNow" animations)
let foundNow = false;
if (!found && (!isRefresh || token.role !== "admin")) {
try {
// First ensure the team_clues table exists
await queryOne(`
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)
)
`);
// Record that the team found this clue
await queryOne(
"INSERT INTO team_clues (team_id, clue_id) VALUES (?, ?)",
[token.teamId, clueId]
);
// Set foundNow flag for non-admin users only
foundNow = token.role !== "admin";
console.log(
`Clue ${clueId} marked as found for team ${token.teamId}, foundNow=${foundNow}`
);
} catch (err) {
console.error("Error marking clue as found:", err);
// Continue even if there's an error recording the find
}
}
// Add different cache headers for admins vs regular users
const cacheHeaders =
token.role === "admin"
@@ -106,8 +65,7 @@ export async function GET(request, context) {
return Response.json(
{
...clue,
found: true, // Always return as found since the user is viewing it
foundNow, // Only true during initial discovery for non-admins
found: !!found, // Only mark as found if it was previously found
isAdmin: token.role === "admin", // Add flag so client knows user is admin
},
{ headers: cacheHeaders }
+29 -22
View File
@@ -1,5 +1,5 @@
import { query, queryOne } from "@/lib/db";
import { getToken } from "next-auth/jwt";
import { queryOne, run } from "@/lib/db";
export const dynamic = "force-dynamic"; // Disable caching
@@ -24,15 +24,36 @@ export async function POST(request) {
);
}
// Parse request body
const { clueId } = await request.json();
// Validate clueId is a number
if (!clueId || isNaN(parseInt(clueId))) {
if (isNaN(parseInt(clueId))) {
return Response.json({ error: "Invalid clue ID" }, { status: 400 });
}
// Ensure the team_clues table exists
await run(`
// Check if clue exists
const clue = await queryOne("SELECT id FROM clues WHERE id = ?", [clueId]);
if (!clue) {
return Response.json({ error: "Clue not found" }, { status: 404 });
}
// Check if clue was already found
const alreadyFound = await queryOne(
"SELECT * FROM team_clues WHERE team_id = ? AND clue_id = ?",
[token.teamId, clueId]
);
if (alreadyFound) {
return Response.json({
success: true,
message: "This clue was already marked as found",
alreadyFound: true,
});
}
// Create team_clues table if it doesn't exist
await queryOne(`
CREATE TABLE IF NOT EXISTS team_clues (
team_id INTEGER NOT NULL,
clue_id INTEGER NOT NULL,
@@ -43,32 +64,18 @@ export async function POST(request) {
)
`);
// 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 (?, ?)", [
await queryOne("INSERT INTO team_clues (team_id, clue_id) VALUES (?, ?)", [
token.teamId,
clueId,
]);
console.log(`Clue ${clueId} marked as found for team ${token.teamId}`);
// Return successful response
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);
BIN
View File
Binary file not shown.
+317
View File
@@ -0,0 +1,317 @@
"use client";
import { useState, useEffect, useRef } from "react";
import { Scanner } from "@yudiel/react-qr-scanner";
export default function EnhancedQRScanner({
onScan,
width = 300,
height = 300,
}) {
const [error, setError] = useState(null);
const [isSecureContext, setIsSecureContext] = useState(true);
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);
// Check if we're in a secure context
useEffect(() => {
// Check if we're in a secure context (https or localhost)
const secure = window.isSecureContext;
setIsSecureContext(secure);
// Check if camera can be accessed
async function checkCameraAccess() {
try {
const devices = await navigator.mediaDevices.enumerateDevices();
const videoDevices = devices.filter(
(device) => device.kind === "videoinput"
);
if (videoDevices.length === 0) {
setError(
"No camera detected. Please make sure your device has a camera."
);
setHasCameras(false);
} else {
setHasCameras(true);
}
setInitialized(true);
} catch (err) {
console.error("Error checking camera devices:", err);
setError(`Camera initialization error: ${err.message}`);
setInitialized(true);
}
}
checkCameraAccess();
}, []);
// Handle successful scan
const handleDecode = (result) => {
if (onScan && result) {
onScan(result);
}
};
// Try to restart the camera
const retryCamera = () => {
if (retryCount < maxRetries) {
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."
);
}
};
// Handle camera errors
const handleError = (error) => {
console.error("QR Scanner error:", error);
setCameraActive(false);
// More descriptive error messages
let errorMessage = "Camera access error. ";
if (!isSecureContext) {
errorMessage +=
"Your browser requires HTTPS for camera access. When running locally, use http://localhost instead of your IP address.";
} else if (
error.name === "NotAllowedError" ||
error.name === "PermissionDeniedError"
) {
errorMessage +=
"Camera permission denied. Please allow camera access in your browser settings.";
} else if (
error.name === "NotFoundError" ||
error.name === "DevicesNotFoundError"
) {
errorMessage +=
"No camera found. Please ensure your device has a working camera.";
} else if (
error.name === "NotReadableError" ||
error.name === "TrackStartError"
) {
errorMessage +=
"Camera is already in use by another application or encountered a hardware error.";
} else {
errorMessage += `${
error.message ||
"Please check your camera permissions and refresh the page."
}`;
}
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 (
<div className="enhanced-qr-scanner">
<div className="scanner-error">
<p>Camera access requires a secure context (HTTPS or localhost).</p>
<p>
When running locally, use http://localhost:3000 instead of your
network IP.
</p>
</div>
</div>
);
}
// Handle initialization state
if (!initialized) {
return (
<div className="enhanced-qr-scanner">
<div className="scanner-loading">Initializing camera...</div>
</div>
);
}
return (
<div className="enhanced-qr-scanner">
{error && (
<div className="scanner-error">
<p>{error}</p>
{retryCount < maxRetries && (
<button className="retry-button" onClick={retryCamera}>
Retry Camera Access
</button>
)}
</div>
)}
{cameraActive && !error && (
<div
className="scanner-container"
style={{ width, height }}
ref={scannerRef}
>
<Scanner
onResult={handleDecode}
onError={handleError}
constraints={{
facingMode: "environment",
width: { min: 640, ideal: 1280, max: 1920 },
height: { min: 480, ideal: 720, max: 1080 },
aspectRatio: { ideal: 1 },
}}
containerStyle={{
width: "100%",
height: "100%",
borderRadius: "8px",
overflow: "hidden",
}}
videoStyle={{
objectFit: "cover",
width: "100%",
height: "100%",
}}
scanDelay={500}
/>
</div>
)}
{/* Text input fallback when camera fails */}
{(!cameraActive || error) && (
<div className="manual-input-container">
<p className="fallback-text">
Having trouble with the camera? Try entering a code manually:
</p>
<div className="input-container">
<input
type="text"
placeholder="Enter QR code (e.g. QR1)"
className="manual-input"
onKeyDown={(e) => {
if (e.key === "Enter" && e.target.value) {
onScan(e.target.value);
e.target.value = "";
}
}}
/>
<button
className="submit-button"
onClick={(e) => {
const input = e.target.previousSibling;
if (input.value) {
onScan(input.value);
input.value = "";
}
}}
>
Submit
</button>
</div>
</div>
)}
<style jsx>{`
.enhanced-qr-scanner {
display: flex;
flex-direction: column;
align-items: center;
width: 100%;
}
.scanner-error {
color: #d32f2f;
margin-bottom: 15px;
text-align: center;
padding: 12px;
background-color: #ffebee;
border-radius: 8px;
width: 100%;
}
.retry-button {
margin-top: 10px;
padding: 8px 16px;
background-color: #2196f3;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-weight: bold;
}
.retry-button:hover {
background-color: #1976d2;
}
.scanner-loading {
text-align: center;
padding: 12px;
background-color: #e8f5e9;
border-radius: 8px;
width: 100%;
margin-bottom: 15px;
}
.scanner-container {
position: relative;
width: 100%;
max-width: ${width}px;
height: ${height}px;
border-radius: 8px;
overflow: hidden;
border: 2px solid #ddd;
background-color: #f5f5f5;
}
.manual-input-container {
margin-top: 15px;
width: 100%;
max-width: ${width}px;
text-align: center;
}
.fallback-text {
margin-bottom: 10px;
color: #555;
}
.input-container {
display: flex;
width: 100%;
}
.manual-input {
flex: 1;
padding: 10px;
border: 1px solid #ddd;
border-radius: 4px 0 0 4px;
font-size: 16px;
}
.submit-button {
padding: 10px 15px;
background-color: #68543c;
color: white;
border: none;
border-radius: 0 4px 4px 0;
cursor: pointer;
font-weight: bold;
}
.submit-button:hover {
background-color: #4a3c2a;
}
`}</style>
</div>
);
}
+79
View File
@@ -0,0 +1,79 @@
"use client";
import { useState, useEffect } from "react";
export default function QRCodeHelper({ clueId }) {
const [qrCodes, setQrCodes] = useState([]);
useEffect(() => {
// Generate possible QR code formats for this clue
// These are the formats that the handleScan function in the clue page can recognize
if (clueId) {
const id = parseInt(clueId);
if (!isNaN(id)) {
setQrCodes([`QR${id}`, `QR ${id}`, `${id}`, `Clue ${id}`]);
}
}
}, [clueId]);
if (!clueId || qrCodes.length === 0) {
return null;
}
return (
<div className="qr-helper">
<details>
<summary>Camera not working? Try these codes manually</summary>
<div className="code-list">
<p>
If the QR scanner doesn't work, you can use any of these text codes:
</p>
<ul>
{qrCodes.map((code, index) => (
<li key={index}>
<code>{code}</code>
</li>
))}
</ul>
</div>
</details>
<style jsx>{`
.qr-helper {
width: 100%;
margin-top: 15px;
font-size: 0.9rem;
color: #666;
}
summary {
cursor: pointer;
color: #68543c;
font-weight: bold;
}
.code-list {
margin-top: 10px;
padding: 10px;
background-color: #f5f5f5;
border-radius: 4px;
}
code {
background-color: #e0e0e0;
padding: 2px 5px;
border-radius: 3px;
font-family: monospace;
}
ul {
margin-top: 5px;
padding-left: 20px;
}
li {
margin-bottom: 5px;
}
`}</style>
</div>
);
}
+132 -57
View File
@@ -1,71 +1,138 @@
"use client";
import { useState, useRef, useEffect } from "react";
import { Html5QrcodeScanner } from "html5-qrcode";
import { useState, useEffect } from "react";
import { Scanner } from "@yudiel/react-qr-scanner";
export default function QRScanner({ onScan, width = 300, height = 300 }) {
const [scanning, setScanning] = useState(false);
const [error, setError] = useState(null);
const scannerRef = useRef(null);
const [isSecureContext, setIsSecureContext] = useState(true);
const [initialized, setInitialized] = useState(false);
// Check if we're in a secure context
useEffect(() => {
// Initialize scanner
if (!scanning && typeof window !== "undefined") {
// Check if we're in a secure context (https or localhost)
const secure = window.isSecureContext;
setIsSecureContext(secure);
// Check if camera can be accessed
async function checkCameraAccess() {
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
const devices = await navigator.mediaDevices.enumerateDevices();
const videoDevices = devices.filter(
(device) => device.kind === "videoinput"
);
// 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);
}
if (videoDevices.length === 0) {
setError(
"No camera detected. Please make sure your device has a camera."
);
setScanning(true);
}
setInitialized(true);
} catch (err) {
console.error("QR Scanner initialization error:", err);
setError(
"Could not initialize QR scanner. Please make sure you have camera access."
);
console.error("Error checking camera devices:", err);
setError(`Camera initialization error: ${err.message}`);
setInitialized(true);
}
}
// Cleanup function
return () => {
if (scannerRef.current) {
try {
scannerRef.current.clear();
} catch (err) {
console.error("Failed to clear QR scanner:", err);
}
}
};
}, [onScan, width, height, scanning]);
checkCameraAccess();
}, []);
const handleDecode = (result) => {
if (onScan && result) {
onScan(result);
}
};
const handleError = (error) => {
console.error("QR Scanner error:", error);
// More descriptive error messages
let errorMessage = "Camera access error. ";
if (!isSecureContext) {
errorMessage +=
"Your browser requires HTTPS for camera access. When running locally, use http://localhost instead of your IP address.";
} else if (
error.name === "NotAllowedError" ||
error.name === "PermissionDeniedError"
) {
errorMessage +=
"Camera permission denied. Please allow camera access in your browser settings.";
} else if (
error.name === "NotFoundError" ||
error.name === "DevicesNotFoundError"
) {
errorMessage +=
"No camera found. Please ensure your device has a working camera.";
} else if (
error.name === "NotReadableError" ||
error.name === "TrackStartError"
) {
errorMessage +=
"Camera is already in use by another application or encountered a hardware error.";
} else {
errorMessage += `${
error.message ||
"Please check your camera permissions and refresh the page."
}`;
}
setError(errorMessage);
};
if (!isSecureContext) {
return (
<div className="qr-scanner">
<div className="qr-error">
<p>Camera access requires a secure context (HTTPS or localhost).</p>
<p>
When running locally, use http://localhost:3000 instead of your
network IP.
</p>
</div>
</div>
);
}
if (!initialized) {
return (
<div className="qr-scanner">
<div className="scanner-loading">Initializing camera...</div>
</div>
);
}
return (
<div className="qr-scanner">
{error && <div className="qr-error">{error}</div>}
<div id="qr-reader" style={{ width, height }}></div>
{error && (
<div className="qr-error">
<p>{error}</p>
</div>
)}
<div className="scanner-container" style={{ width, height }}>
<Scanner
onResult={handleDecode}
onError={handleError}
constraints={{
facingMode: "environment",
width: { min: 640, ideal: 1280, max: 1920 },
height: { min: 480, ideal: 720, max: 1080 },
}}
containerStyle={{
width: "100%",
height: "100%",
borderRadius: "8px",
overflow: "hidden",
}}
videoStyle={{
objectFit: "cover",
width: "100%",
height: "100%",
}}
/>
</div>
<style jsx>{`
.qr-scanner {
@@ -85,16 +152,24 @@ export default function QRScanner({ onScan, width = 300, height = 300 }) {
width: 100%;
}
#qr-reader {
border: 2px solid #ddd;
.scanner-loading {
text-align: center;
padding: 12px;
background-color: #e8f5e9;
border-radius: 8px;
overflow: hidden;
background-color: #f5f5f5;
width: 100%;
margin-bottom: 15px;
}
#qr-reader video {
object-fit: cover;
border-radius: 6px;
.scanner-container {
position: relative;
width: 100%;
max-width: ${width}px;
height: ${height}px;
border-radius: 8px;
overflow: hidden;
border: 2px solid #ddd;
background-color: #f5f5f5;
}
`}</style>
</div>
+164
View File
@@ -0,0 +1,164 @@
"use client";
import { useState, useRef } from "react";
export default function SimpleQRScanner({ onScan }) {
const [error, setError] = useState(null);
const [isCameraActive, setIsCameraActive] = useState(false);
const videoRef = useRef(null);
const startCamera = async () => {
try {
const stream = await navigator.mediaDevices.getUserMedia({
video: { facingMode: "environment" },
});
if (videoRef.current) {
videoRef.current.srcObject = stream;
setIsCameraActive(true);
}
} catch (err) {
console.error("Error accessing camera:", err);
setError(
"Could not access camera. Please make sure you've given camera permission."
);
}
};
const stopCamera = () => {
if (videoRef.current && videoRef.current.srcObject) {
const tracks = videoRef.current.srcObject.getTracks();
tracks.forEach((track) => track.stop());
videoRef.current.srcObject = null;
setIsCameraActive(false);
}
};
const handleManualInput = () => {
const code = prompt("Enter the QR code value manually (e.g. QR123):");
if (code && onScan) {
onScan(code);
}
};
return (
<div className="simple-scanner">
{error && <div className="scanner-error">{error}</div>}
{!isCameraActive ? (
<div className="scanner-controls">
<button className="scanner-button" onClick={startCamera}>
Open Camera to Scan QR Code
</button>
<button
className="scanner-button secondary"
onClick={handleManualInput}
>
Enter Code Manually
</button>
</div>
) : (
<div className="video-container">
<video
ref={videoRef}
autoPlay
playsInline
onCanPlay={() => videoRef.current.play()}
/>
<div className="scanner-overlay">
<div className="scanner-target"></div>
</div>
<button className="close-camera-button" onClick={stopCamera}>
Close Camera
</button>
</div>
)}
<style jsx>{`
.simple-scanner {
display: flex;
flex-direction: column;
align-items: center;
width: 100%;
}
.scanner-error {
color: red;
margin-bottom: 15px;
text-align: center;
padding: 8px;
background-color: #ffebee;
border-radius: 4px;
width: 100%;
}
.scanner-controls {
display: flex;
flex-direction: column;
gap: 10px;
width: 100%;
}
.scanner-button {
padding: 12px;
background-color: #68543c;
color: white;
border: none;
border-radius: 6px;
font-weight: bold;
cursor: pointer;
}
.scanner-button.secondary {
background-color: #999;
}
.video-container {
position: relative;
width: 300px;
height: 300px;
overflow: hidden;
border-radius: 8px;
border: 2px solid #ddd;
}
video {
width: 100%;
height: 100%;
object-fit: cover;
}
.scanner-overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
display: flex;
justify-content: center;
align-items: center;
background-color: rgba(0, 0, 0, 0.2);
}
.scanner-target {
width: 200px;
height: 200px;
border: 2px solid #fff;
border-radius: 8px;
box-shadow: 0 0 0 4000px rgba(0, 0, 0, 0.3);
}
.close-camera-button {
position: absolute;
bottom: 10px;
right: 10px;
background-color: rgba(0, 0, 0, 0.5);
color: white;
border: none;
border-radius: 4px;
padding: 5px 10px;
cursor: pointer;
}
`}</style>
</div>
);
}
+33 -62
View File
@@ -8,15 +8,19 @@
"name": "clue-hunt-app",
"version": "0.1.0",
"dependencies": {
"@panva/hkdf": "^1.1.1",
"@yudiel/react-qr-scanner": "^1.2.4",
"bcryptjs": "^3.0.2",
"better-sqlite3": "^11.8.1",
"clue-hunt-app": "file:",
"html5-qrcode": "^2.3.8",
"jose": "^4.14.4",
"jsonwebtoken": "^9.0.2",
"next": "15.2.2",
"next-auth": "^4.24.11",
"qrcode.react": "^4.2.0",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"react-qr-reader": "^3.0.0-beta-1",
"sqlite": "^5.1.1",
"sqlite3": "^5.1.7"
},
@@ -1429,25 +1433,26 @@
"url": "https://opencollective.com/typescript-eslint"
}
},
"node_modules/@zxing/browser": {
"version": "0.0.7",
"resolved": "https://registry.npmjs.org/@zxing/browser/-/browser-0.0.7.tgz",
"integrity": "sha512-AepzMgDnD6EjxewqmXpHJsi4S3Gw9ilZJLIbTf6fWuWySEcHBodnGu3p7FWlgq1Sd5QyfPhTum5z3CBkkhMVng==",
"license": "MIT",
"optionalDependencies": {
"@zxing/text-encoding": "^0.9.0"
},
"peerDependencies": {
"@zxing/library": "^0.18.3"
}
},
"node_modules/@zxing/library": {
"version": "0.18.6",
"resolved": "https://registry.npmjs.org/@zxing/library/-/library-0.18.6.tgz",
"integrity": "sha512-bulZ9JHoLFd9W36pi+7e7DnEYNJhljYjZ1UTsKPOoLMU3qtC+REHITeCRNx40zTRJZx18W5TBRXt5pq2Uopjsw==",
"node_modules/@yudiel/react-qr-scanner": {
"version": "1.2.10",
"resolved": "https://registry.npmjs.org/@yudiel/react-qr-scanner/-/react-qr-scanner-1.2.10.tgz",
"integrity": "sha512-I94AyrEu/2oZxXT6TEOK9+K2IfjqfPmic/GUah7alMIxfgXP9rs9+IZE56JhQr4pZ7Iz2RUC4JFfeaysFYNEIA==",
"license": "MIT",
"dependencies": {
"ts-custom-error": "^3.0.0"
"@zxing/library": "^0.20.0"
},
"peerDependencies": {
"react": "^18.0.0",
"react-dom": "^18.0.0"
}
},
"node_modules/@yudiel/react-qr-scanner/node_modules/@zxing/library": {
"version": "0.20.0",
"resolved": "https://registry.npmjs.org/@zxing/library/-/library-0.20.0.tgz",
"integrity": "sha512-6Ev6rcqVjMakZFIDvbUf0dtpPGeZMTfyxYg4HkVWioWeN7cRcnUWT3bU6sdohc82O1nPXcjq6WiGfXX2Pnit6A==",
"license": "MIT",
"dependencies": {
"ts-custom-error": "^3.2.1"
},
"engines": {
"node": ">= 10.4.0"
@@ -2109,6 +2114,10 @@
"integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==",
"license": "MIT"
},
"node_modules/clue-hunt-app": {
"resolved": "",
"link": true
},
"node_modules/color": {
"version": "4.2.3",
"resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz",
@@ -3274,20 +3283,6 @@
"license": "ISC",
"optional": true
},
"node_modules/fsevents": {
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
"hasInstallScript": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
}
},
"node_modules/function-bind": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
@@ -3619,6 +3614,12 @@
"node": ">= 0.4"
}
},
"node_modules/html5-qrcode": {
"version": "2.3.8",
"resolved": "https://registry.npmjs.org/html5-qrcode/-/html5-qrcode-2.3.8.tgz",
"integrity": "sha512-jsr4vafJhwoLVEDW3n1KvPnCCXWaQfRng0/EEYk1vNcQGcG/htAdhJX0be8YyqMoSz7+hZvOZSTAepsabiuhiQ==",
"license": "Apache-2.0"
},
"node_modules/http-cache-semantics": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz",
@@ -5781,21 +5782,6 @@
"dev": true,
"license": "MIT"
},
"node_modules/react-qr-reader": {
"version": "3.0.0-beta-1",
"resolved": "https://registry.npmjs.org/react-qr-reader/-/react-qr-reader-3.0.0-beta-1.tgz",
"integrity": "sha512-5HeFH9x/BlziRYQYGK2AeWS9WiKYZtGGMs9DXy3bcySTX3C9UJL9EwcPnWw8vlf7JP4FcrAlr1SnZ5nsWLQGyw==",
"license": "MIT",
"dependencies": {
"@zxing/browser": "0.0.7",
"@zxing/library": "^0.18.3",
"rollup": "^2.67.2"
},
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0",
"react-dom": "^16.8.0 || ^17.0.0"
}
},
"node_modules/readable-stream": {
"version": "3.6.2",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
@@ -5939,21 +5925,6 @@
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/rollup": {
"version": "2.79.2",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-2.79.2.tgz",
"integrity": "sha512-fS6iqSPZDs3dr/y7Od6y5nha8dW1YnbgtsyotCVvoFGKbERG++CVRFv1meyGDE1SNItQA8BrnCw7ScdAhRJ3XQ==",
"license": "MIT",
"bin": {
"rollup": "dist/bin/rollup"
},
"engines": {
"node": ">=10.0.0"
},
"optionalDependencies": {
"fsevents": "~2.3.2"
}
},
"node_modules/run-parallel": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz",
+5 -1
View File
@@ -11,15 +11,19 @@
"init-db": "node lib/init-db.js"
},
"dependencies": {
"@panva/hkdf": "^1.1.1",
"@yudiel/react-qr-scanner": "^1.2.4",
"bcryptjs": "^3.0.2",
"better-sqlite3": "^11.8.1",
"clue-hunt-app": "file:",
"html5-qrcode": "^2.3.8",
"jose": "^4.14.4",
"jsonwebtoken": "^9.0.2",
"next": "15.2.2",
"next-auth": "^4.24.11",
"qrcode.react": "^4.2.0",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"react-qr-reader": "^3.0.0-beta-1",
"sqlite": "^5.1.1",
"sqlite3": "^5.1.7"
},
+20 -20
View File
@@ -23,83 +23,83 @@
<rect x="181" y="1974" width="88" height="88" fill="url(#pattern10_37_192)"/>
<rect x="373" y="1512" width="88" height="88" fill="url(#pattern11_37_192)"/>
<g filter="url(#filter0_d_37_192)">
<circle cx="230" cy="211" r="25" fill="#9e9e9e"/>
<circle cx="230" cy="211" r="25" fill=""/>
<circle cx="230" cy="211" r="25" stroke="#757575" stroke-width="2"/>
</g>
<g filter="url(#filter1_d_37_192)">
<circle cx="320" cy="284" r="25" fill="#9e9e9e"/>
<circle cx="320" cy="284" r="25" fill=""/>
<circle cx="320" cy="284" r="25" stroke="#757575" stroke-width="2"/>
</g>
<g filter="url(#filter2_d_37_192)">
<circle cx="264" cy="397" r="25" fill="#9e9e9e"/>
<circle cx="264" cy="397" r="25" fill=""/>
<circle cx="264" cy="397" r="25" stroke="#757575" stroke-width="2"/>
</g>
<g filter="url(#filter3_d_37_192)">
<circle cx="342" cy="517" r="25" fill="#9e9e9e"/>
<circle cx="342" cy="517" r="25" fill=""/>
<circle cx="342" cy="517" r="25" stroke="#757575" stroke-width="2"/>
</g>
<g filter="url(#filter4_d_37_192)">
<circle cx="320" cy="645" r="25" fill="#9e9e9e"/>
<circle cx="320" cy="645" r="25" fill=""/>
<circle cx="320" cy="645" r="25" stroke="#757575" stroke-width="2"/>
</g>
<g filter="url(#filter5_d_37_192)">
<circle cx="295" cy="773" r="25" fill="#9e9e9e"/>
<circle cx="295" cy="773" r="25" fill=""/>
<circle cx="295" cy="773" r="25" stroke="#757575" stroke-width="2"/>
</g>
<g filter="url(#filter6_d_37_192)">
<circle cx="239" cy="880" r="25" fill="#9e9e9e"/>
<circle cx="239" cy="880" r="25" fill=""/>
<circle cx="239" cy="880" r="25" stroke="#757575" stroke-width="2"/>
</g>
<g filter="url(#filter7_d_37_192)">
<circle cx="354" cy="959" r="25" fill="#9e9e9e"/>
<circle cx="354" cy="959" r="25" fill=""/>
<circle cx="354" cy="959" r="25" stroke="#757575" stroke-width="2"/>
</g>
<g filter="url(#filter8_d_37_192)">
<circle cx="270" cy="1036" r="25" fill="#9e9e9e"/>
<circle cx="270" cy="1036" r="25" fill=""/>
<circle cx="270" cy="1036" r="25" stroke="#757575" stroke-width="2"/>
</g>
<g filter="url(#filter9_d_37_192)">
<circle cx="279" cy="1174" r="25" fill="#9e9e9e"/>
<circle cx="279" cy="1174" r="25" fill=""/>
<circle cx="279" cy="1174" r="25" stroke="#757575" stroke-width="2"/>
</g>
<g filter="url(#filter10_d_37_192)">
<circle cx="337" cy="1302" r="25" fill="#9e9e9e"/>
<circle cx="337" cy="1302" r="25" fill=""/>
<circle cx="337" cy="1302" r="25" stroke="#757575" stroke-width="2"/>
</g>
<g filter="url(#filter11_d_37_192)">
<circle cx="250" cy="1388" r="25" fill="#9e9e9e"/>
<circle cx="250" cy="1388" r="25" fill=""/>
<circle cx="250" cy="1388" r="25" stroke="#757575" stroke-width="2"/>
</g>
<g filter="url(#filter12_d_37_192)">
<circle cx="162" cy="1487" r="25" fill="#9e9e9e"/>
<circle cx="162" cy="1487" r="25" fill=""/>
<circle cx="162" cy="1487" r="25" stroke="#757575" stroke-width="2"/>
</g>
<g filter="url(#filter13_d_37_192)">
<circle cx="306" cy="1531" r="25" fill="#9e9e9e"/>
<circle cx="306" cy="1531" r="25" fill=""/>
<circle cx="306" cy="1531" r="25" stroke="#757575" stroke-width="2"/>
</g>
<g filter="url(#filter14_d_37_192)">
<circle cx="342" cy="1649" r="25" fill="#9e9e9e"/>
<circle cx="342" cy="1649" r="25" fill=""/>
<circle cx="342" cy="1649" r="25" stroke="#757575" stroke-width="2"/>
</g>
<g filter="url(#filter15_d_37_192)">
<circle cx="225" cy="1724" r="25" fill="#9e9e9e"/>
<circle cx="225" cy="1724" r="25" fill=""/>
<circle cx="225" cy="1724" r="25" stroke="#757575" stroke-width="2"/>
</g>
<g filter="url(#filter16_d_37_192)">
<circle cx="216" cy="1822" r="25" fill="#9e9e9e"/>
<circle cx="216" cy="1822" r="25" fill=""/>
<circle cx="216" cy="1822" r="25" stroke="#757575" stroke-width="2"/>
</g>
<g filter="url(#filter17_d_37_192)">
<circle cx="275" cy="1883" r="25" fill="#9e9e9e"/>
<circle cx="275" cy="1883" r="25" fill=""/>
<circle cx="275" cy="1883" r="25" stroke="#757575" stroke-width="2"/>
</g>
<g filter="url(#filter18_d_37_192)">
<circle cx="362" cy="1917" r="25" fill="#9e9e9e"/>
<circle cx="362" cy="1917" r="25" fill=""/>
<circle cx="362" cy="1917" r="25" stroke="#757575" stroke-width="2"/>
</g>
<g filter="url(#filter19_d_37_192)">
<circle cx="300" cy="2037" r="25" fill="#9e9e9e"/>
<circle cx="300" cy="2037" r="25" fill=""/>
<circle cx="300" cy="2037" r="25" stroke="#757575" stroke-width="2"/>
</g>
</g>

Before

Width:  |  Height:  |  Size: 493 KiB

After

Width:  |  Height:  |  Size: 493 KiB

-29
View File
@@ -1,29 +0,0 @@
#-------------------------------------------------------------------------------#
# Qodana analysis is configured by qodana.yaml file #
# https://www.jetbrains.com/help/qodana/qodana-yaml.html #
#-------------------------------------------------------------------------------#
version: "1.0"
#Specify inspection profile for code analysis
profile:
name: qodana.starter
#Enable inspections
#include:
# - name: <SomeEnabledInspectionId>
#Disable inspections
#exclude:
# - name: <SomeDisabledInspectionId>
# paths:
# - <path/where/not/run/inspection>
#Execute shell command before Qodana execution (Applied in CI/CD pipeline)
#bootstrap: sh ./prepare-qodana.sh
#Install IDE plugins before Qodana execution (Applied in CI/CD pipeline)
#plugins:
# - id: <plugin.id> #(plugin id can be found at https://plugins.jetbrains.com)
#Specify Qodana linter for analysis (Applied in CI/CD pipeline)
linter: jetbrains/qodana-js:2024.3
+80
View File
@@ -91,9 +91,23 @@ export function fixCluePointsRendering(svgElement) {
// Ensure all clue points are in the overlay group and have proper styles
cluePoints.forEach((point, i) => {
// Get original coordinates if available
const originalCx =
point.getAttribute("data-original-cx") || point.getAttribute("cx");
const originalCy =
point.getAttribute("data-original-cy") || point.getAttribute("cy");
// Clone the point to remove any existing event handlers and styling issues
const newPoint = point.cloneNode(true);
// Restore original coordinates if they were saved
if (originalCx) newPoint.setAttribute("cx", originalCx);
if (originalCy) newPoint.setAttribute("cy", originalCy);
// Store original coordinates for future reference
if (originalCx) newPoint.setAttribute("data-original-cx", originalCx);
if (originalCy) newPoint.setAttribute("data-original-cy", originalCy);
// Apply explicit styles to ensure visibility
newPoint.setAttribute(
"style",
@@ -114,6 +128,12 @@ export function fixCluePointsRendering(svgElement) {
// Add to overlay group
overlayGroup.appendChild(newPoint);
}
// Update the corresponding click area position too
if (clickAreas[i]) {
clickAreas[i].setAttribute("cx", originalCx || point.getAttribute("cx"));
clickAreas[i].setAttribute("cy", originalCy || point.getAttribute("cy"));
}
});
// Move the overlay group to be the last child of the SVG
@@ -121,3 +141,63 @@ export function fixCluePointsRendering(svgElement) {
console.log("Clue points rendering fixed");
}
// New function to ensure clue circles match original map
export function fixCluePositionsFromOriginalMap(svgElement) {
if (!svgElement) return;
// Fetch the original map SVG to get circle positions
fetch("/images/map.svg")
.then((response) => response.text())
.then((svgText) => {
const parser = new DOMParser();
const mapDoc = parser.parseFromString(svgText, "image/svg+xml");
// Get all circles from original map
const originalCircles = Array.from(mapDoc.querySelectorAll("circle"));
console.log(
`Found ${originalCircles.length} circles in original map for position fixing`
);
// Get current clue points
const cluePoints = svgElement.querySelectorAll(".clue-point");
console.log(`Found ${cluePoints.length} clue points to fix positions`);
// Only adjust positions if we have both original circles and clue points
if (originalCircles.length > 0 && cluePoints.length > 0) {
// Get the clue overlay group
let overlayGroup = svgElement.querySelector(".clue-points-overlay");
if (!overlayGroup) return;
// Update each clue point to match original circle position
cluePoints.forEach((point, i) => {
if (i < originalCircles.length) {
const originalCircle = originalCircles[i];
const cx = originalCircle.getAttribute("cx") || "0";
const cy = originalCircle.getAttribute("cy") || "0";
// Update position
point.setAttribute("cx", cx);
point.setAttribute("cy", cy);
point.setAttribute("data-original-cx", cx);
point.setAttribute("data-original-cy", cy);
// Update the corresponding click area
const clickArea = svgElement.querySelector(
`.clue-click-area[data-index="${i}"]`
);
if (clickArea) {
clickArea.setAttribute("cx", cx);
clickArea.setAttribute("cy", cy);
}
}
});
console.log("Clue positions fixed to match original map");
}
})
.catch((error) => {
console.error("Error fixing clue positions from original map:", error);
});
}
+128 -53
View File
@@ -124,47 +124,39 @@ export async function populateMap(svgElement) {
console.log(`Found ${allCircles.length} circles in SVG`);
if (allCircles.length === 0) {
console.error("No circles found in SVG. Creating fallback circles.");
// Create some fallback circles at predefined positions
const defs = document.createElementNS(
"http://www.w3.org/2000/svg",
"defs"
console.error(
"No circles found in SVG. Attempting to load from original map."
);
const positions = [
{ x: 100, y: 100 },
{ x: 200, y: 150 },
{ x: 300, y: 200 },
{ x: 400, y: 300 },
{ x: 500, y: 400 },
{ x: 600, y: 500 },
];
positions.forEach((pos, i) => {
const circle = document.createElementNS(
"http://www.w3.org/2000/svg",
"circle"
);
circle.setAttribute("cx", pos.x.toString());
circle.setAttribute("cy", pos.y.toString());
circle.setAttribute("r", "5");
circle.setAttribute("fill", "transparent");
defs.appendChild(circle);
});
// If no circles in current SVG, try to fetch circles from the original map SVG
try {
const mapResponse = await fetch("/images/map.svg");
if (!mapResponse.ok) {
throw new Error("Failed to load original map SVG");
}
// Add the defs to the SVG
svgElement.appendChild(defs);
const mapText = await mapResponse.text();
const parser = new DOMParser();
const mapDoc = parser.parseFromString(mapText, "image/svg+xml");
// Now find all circles again
const fallbackCircles = Array.from(defs.querySelectorAll("circle"));
if (fallbackCircles.length > 0) {
console.log(`Created ${fallbackCircles.length} fallback circles`);
return await processCircles(svgElement, fallbackCircles);
} else {
return false;
// Find circles in the original map
const originalCircles = Array.from(mapDoc.querySelectorAll("circle"));
console.log(`Found ${originalCircles.length} circles in original map`);
if (originalCircles.length > 0) {
return await processCircles(svgElement, originalCircles, true);
} else {
console.error(
"No circles found in original map either. Using fallback positions."
);
return await createFallbackCircles(svgElement);
}
} catch (error) {
console.error("Failed to load circles from original map:", error);
return await createFallbackCircles(svgElement);
}
} else {
return await processCircles(svgElement, allCircles);
return await processCircles(svgElement, allCircles, false);
}
} catch (error) {
console.error("Error populating map:", error);
@@ -172,16 +164,60 @@ export async function populateMap(svgElement) {
}
}
/**
* Create fallback circles at preset positions as a last resort
* @param {SVGElement} svgElement - The SVG DOM element
* @returns {Promise<boolean>} - Success flag
*/
async function createFallbackCircles(svgElement) {
// Create some fallback circles at predefined positions
const svgNamespace = "http://www.w3.org/2000/svg";
const defs = document.createElementNS(svgNamespace, "defs");
const positions = [
{ x: 100, y: 100 },
{ x: 200, y: 200 },
{ x: 300, y: 300 },
{ x: 400, y: 200 },
{ x: 500, y: 100 },
{ x: 600, y: 300 },
];
positions.forEach((pos, i) => {
const circle = document.createElementNS(svgNamespace, "circle");
circle.setAttribute("cx", pos.x.toString());
circle.setAttribute("cy", pos.y.toString());
circle.setAttribute("r", "5");
circle.setAttribute("fill", "transparent");
defs.appendChild(circle);
});
// Add the defs to the SVG
svgElement.appendChild(defs);
// Now find all circles again
const fallbackCircles = Array.from(defs.querySelectorAll("circle"));
if (fallbackCircles.length > 0) {
console.log(`Created ${fallbackCircles.length} fallback circles`);
return await processCircles(svgElement, fallbackCircles, false);
} else {
return false;
}
}
/**
* Process circles to create clue points
* @param {SVGElement} svgElement - The SVG DOM element
* @param {Array} circles - Array of circle elements
* @param {boolean} isFromOriginalMap - Whether circles are from the original map
* @returns {Promise<boolean>} - Success flag
*/
async function processCircles(svgElement, circles) {
async function processCircles(svgElement, circles, isFromOriginalMap) {
try {
// Hide original circles
circles.forEach((circle) => (circle.style.display = "none"));
// Don't hide original circles if they are from the original map and not present in current SVG
if (!isFromOriginalMap) {
// Remove hiding originals so the new elements are clearly visible
// circles.forEach((circle) => (circle.style.display = "none"));
}
// Fetch the number of clues from the API
const response = await fetch("/api/clues/count", {
@@ -198,7 +234,7 @@ async function processCircles(svgElement, circles) {
}
const data = await response.json();
const clueCount = parseInt(data.count, 10) || 0;
let clueCount = parseInt(data.count, 10) || 0;
console.log(`API returned clue count: ${clueCount}`);
if (clueCount <= 0) {
@@ -206,21 +242,17 @@ async function processCircles(svgElement, circles) {
clueCount = 5; // Fallback to 5 clues if API returns invalid data
}
// Sort circles by vertical position (top to bottom)
const sortedCircles = circles.sort((a, b) => {
const aY = parseFloat(a.getAttribute("cy") || "0");
const bY = parseFloat(b.getAttribute("cy") || "0");
return aY - bY;
});
// Determine how many circles to show
const circlesToShow = Math.min(sortedCircles.length, clueCount);
console.log(`Will create ${circlesToShow} clue points from sorted circles`);
// Use the circles exactly where they are - preserve original positions
// Just limit how many we use based on clue count
const circlesToShow = Math.min(circles.length, clueCount);
console.log(
`Will create ${circlesToShow} clue points from available circles`
);
// Create the overlay element that will go on top of everything
const svgNamespace = "http://www.w3.org/2000/svg";
// Remove any old overlay group
// Remove any old overlay group (but leave persistent button layer intact)
const oldOverlay = svgElement.querySelector(".clue-points-overlay");
if (oldOverlay && oldOverlay.parentNode) {
oldOverlay.parentNode.removeChild(oldOverlay);
@@ -238,9 +270,9 @@ async function processCircles(svgElement, circles) {
// Add the overlay group as the LAST element in the SVG to ensure proper rendering order
svgElement.appendChild(overlayGroup);
// Create interactive clue points
// Create interactive clue points based on the original circle positions
for (let i = 0; i < circlesToShow; i++) {
const originalCircle = sortedCircles[i];
const originalCircle = circles[i];
// Get original circle attributes
const cx = originalCircle.getAttribute("cx") || "0";
@@ -299,7 +331,50 @@ async function processCircles(svgElement, circles) {
// This ensures it's the last child of the SVG
svgElement.appendChild(overlayGroup);
console.log(`Successfully created ${circlesToShow} clue points`);
// Instead of appending button layer with absolute positioning,
// create/update it as a child of overlayGroup using normal SVG positioning.
let buttonGroup = svgElement.querySelector(".clue-button-layer");
if (buttonGroup) {
buttonGroup.innerHTML = "";
} else {
buttonGroup = document.createElementNS(svgNamespace, "g");
buttonGroup.setAttribute("class", "clue-button-layer");
// Remove absolute positioning; allow buttonGroup to inherit the overlay's coordinate system.
buttonGroup.setAttribute(
"style",
"pointer-events: auto; z-index: 10000;"
);
}
for (let i = 0; i < circlesToShow; i++) {
const originalCircle = circles[i];
const cx = originalCircle.getAttribute("cx") || "0";
const cy = originalCircle.getAttribute("cy") || "0";
const r = Math.max(
parseFloat(originalCircle.getAttribute("r") || "5"),
14
);
const button = document.createElementNS(svgNamespace, "circle");
button.setAttribute("cx", cx);
button.setAttribute("cy", cy);
button.setAttribute("r", r.toString());
button.setAttribute("class", "clue-button");
button.setAttribute(
"style",
"fill: #4caf50; stroke: #388e3c; stroke-width: 2px; cursor: pointer; opacity: 0.9; pointer-events: auto;"
);
button.addEventListener("click", () => {
console.log(`Button for clue ${i + 1} clicked`);
// ...add navigation or other logic here...
});
buttonGroup.appendChild(button);
}
// Append or re-append the buttonGroup into the overlayGroup so they appear together.
overlayGroup.appendChild(buttonGroup);
console.log(
`Successfully created ${circlesToShow} clue points and buttons`
);
return true;
} catch (error) {
console.error("Error processing circles:", error);
+139 -135
View File
@@ -16,7 +16,7 @@
"@emnapi/runtime@^1.2.0":
version "1.3.1"
resolved "https://registry.yarnpkg.com/@emnapi/runtime/-/runtime-1.3.1.tgz#0fcaa575afc31f455fd33534c19381cfce6c6f60"
resolved "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.3.1.tgz"
integrity sha512-kEBmG8KyqtxJZv+ygbEim+KCGtIq1fC22Ms3S4ziXmYKm8uyoLX0MHONVKwp+9opg390VaKRNt4a7A9NwmpNhw==
dependencies:
tslib "^2.4.0"
@@ -122,110 +122,110 @@
"@img/sharp-darwin-arm64@0.33.5":
version "0.33.5"
resolved "https://registry.yarnpkg.com/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.33.5.tgz#ef5b5a07862805f1e8145a377c8ba6e98813ca08"
resolved "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.33.5.tgz"
integrity sha512-UT4p+iz/2H4twwAoLCqfA9UH5pI6DggwKEGuaPy7nCVQ8ZsiY5PIcrRvD1DzuY3qYL07NtIQcWnBSY/heikIFQ==
optionalDependencies:
"@img/sharp-libvips-darwin-arm64" "1.0.4"
"@img/sharp-darwin-x64@0.33.5":
version "0.33.5"
resolved "https://registry.yarnpkg.com/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.33.5.tgz#e03d3451cd9e664faa72948cc70a403ea4063d61"
resolved "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.33.5.tgz"
integrity sha512-fyHac4jIc1ANYGRDxtiqelIbdWkIuQaI84Mv45KvGRRxSAa7o7d1ZKAOBaYbnepLC1WqxfpimdeWfvqqSGwR2Q==
optionalDependencies:
"@img/sharp-libvips-darwin-x64" "1.0.4"
"@img/sharp-libvips-darwin-arm64@1.0.4":
version "1.0.4"
resolved "https://registry.yarnpkg.com/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.0.4.tgz#447c5026700c01a993c7804eb8af5f6e9868c07f"
resolved "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.0.4.tgz"
integrity sha512-XblONe153h0O2zuFfTAbQYAX2JhYmDHeWikp1LM9Hul9gVPjFY427k6dFEcOL72O01QxQsWi761svJ/ev9xEDg==
"@img/sharp-libvips-darwin-x64@1.0.4":
version "1.0.4"
resolved "https://registry.yarnpkg.com/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.0.4.tgz#e0456f8f7c623f9dbfbdc77383caa72281d86062"
resolved "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.0.4.tgz"
integrity sha512-xnGR8YuZYfJGmWPvmlunFaWJsb9T/AO2ykoP3Fz/0X5XV2aoYBPkX6xqCQvUTKKiLddarLaxpzNe+b1hjeWHAQ==
"@img/sharp-libvips-linux-arm64@1.0.4":
version "1.0.4"
resolved "https://registry.yarnpkg.com/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.0.4.tgz#979b1c66c9a91f7ff2893556ef267f90ebe51704"
integrity sha512-9B+taZ8DlyyqzZQnoeIvDVR/2F4EbMepXMc/NdVbkzsJbzkUjhXv/70GQJ7tdLA4YJgNP25zukcxpX2/SueNrA==
"@img/sharp-libvips-linux-arm@1.0.5":
version "1.0.5"
resolved "https://registry.yarnpkg.com/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.0.5.tgz#99f922d4e15216ec205dcb6891b721bfd2884197"
resolved "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.0.5.tgz"
integrity sha512-gvcC4ACAOPRNATg/ov8/MnbxFDJqf/pDePbBnuBDcjsI8PssmjoKMAz4LtLaVi+OnSb5FK/yIOamqDwGmXW32g==
"@img/sharp-libvips-linux-arm64@1.0.4":
version "1.0.4"
resolved "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.0.4.tgz"
integrity sha512-9B+taZ8DlyyqzZQnoeIvDVR/2F4EbMepXMc/NdVbkzsJbzkUjhXv/70GQJ7tdLA4YJgNP25zukcxpX2/SueNrA==
"@img/sharp-libvips-linux-s390x@1.0.4":
version "1.0.4"
resolved "https://registry.yarnpkg.com/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.0.4.tgz#f8a5eb1f374a082f72b3f45e2fb25b8118a8a5ce"
resolved "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.0.4.tgz"
integrity sha512-u7Wz6ntiSSgGSGcjZ55im6uvTrOxSIS8/dgoVMoiGE9I6JAfU50yH5BoDlYA1tcuGS7g/QNtetJnxA6QEsCVTA==
"@img/sharp-libvips-linux-x64@1.0.4":
version "1.0.4"
resolved "https://registry.yarnpkg.com/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.0.4.tgz#d4c4619cdd157774906e15770ee119931c7ef5e0"
resolved "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.0.4.tgz"
integrity sha512-MmWmQ3iPFZr0Iev+BAgVMb3ZyC4KeFc3jFxnNbEPas60e1cIfevbtuyf9nDGIzOaW9PdnDciJm+wFFaTlj5xYw==
"@img/sharp-libvips-linuxmusl-arm64@1.0.4":
version "1.0.4"
resolved "https://registry.yarnpkg.com/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.0.4.tgz#166778da0f48dd2bded1fa3033cee6b588f0d5d5"
resolved "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.0.4.tgz"
integrity sha512-9Ti+BbTYDcsbp4wfYib8Ctm1ilkugkA/uscUn6UXK1ldpC1JjiXbLfFZtRlBhjPZ5o1NCLiDbg8fhUPKStHoTA==
"@img/sharp-libvips-linuxmusl-x64@1.0.4":
version "1.0.4"
resolved "https://registry.yarnpkg.com/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.0.4.tgz#93794e4d7720b077fcad3e02982f2f1c246751ff"
resolved "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.0.4.tgz"
integrity sha512-viYN1KX9m+/hGkJtvYYp+CCLgnJXwiQB39damAO7WMdKWlIhmYTfHjwSbQeUK/20vY154mwezd9HflVFM1wVSw==
"@img/sharp-linux-arm64@0.33.5":
version "0.33.5"
resolved "https://registry.yarnpkg.com/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.33.5.tgz#edb0697e7a8279c9fc829a60fc35644c4839bb22"
integrity sha512-JMVv+AMRyGOHtO1RFBiJy/MBsgz0x4AWrT6QoEVVTyh1E39TrCUpTRI7mx9VksGX4awWASxqCYLCV4wBZHAYxA==
optionalDependencies:
"@img/sharp-libvips-linux-arm64" "1.0.4"
"@img/sharp-linux-arm@0.33.5":
version "0.33.5"
resolved "https://registry.yarnpkg.com/@img/sharp-linux-arm/-/sharp-linux-arm-0.33.5.tgz#422c1a352e7b5832842577dc51602bcd5b6f5eff"
resolved "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.33.5.tgz"
integrity sha512-JTS1eldqZbJxjvKaAkxhZmBqPRGmxgu+qFKSInv8moZ2AmT5Yib3EQ1c6gp493HvrvV8QgdOXdyaIBrhvFhBMQ==
optionalDependencies:
"@img/sharp-libvips-linux-arm" "1.0.5"
"@img/sharp-linux-arm64@0.33.5":
version "0.33.5"
resolved "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.33.5.tgz"
integrity sha512-JMVv+AMRyGOHtO1RFBiJy/MBsgz0x4AWrT6QoEVVTyh1E39TrCUpTRI7mx9VksGX4awWASxqCYLCV4wBZHAYxA==
optionalDependencies:
"@img/sharp-libvips-linux-arm64" "1.0.4"
"@img/sharp-linux-s390x@0.33.5":
version "0.33.5"
resolved "https://registry.yarnpkg.com/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.33.5.tgz#f5c077926b48e97e4a04d004dfaf175972059667"
resolved "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.33.5.tgz"
integrity sha512-y/5PCd+mP4CA/sPDKl2961b+C9d+vPAveS33s6Z3zfASk2j5upL6fXVPZi7ztePZ5CuH+1kW8JtvxgbuXHRa4Q==
optionalDependencies:
"@img/sharp-libvips-linux-s390x" "1.0.4"
"@img/sharp-linux-x64@0.33.5":
version "0.33.5"
resolved "https://registry.yarnpkg.com/@img/sharp-linux-x64/-/sharp-linux-x64-0.33.5.tgz#d806e0afd71ae6775cc87f0da8f2d03a7c2209cb"
resolved "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.33.5.tgz"
integrity sha512-opC+Ok5pRNAzuvq1AG0ar+1owsu842/Ab+4qvU879ippJBHvyY5n2mxF1izXqkPYlGuP/M556uh53jRLJmzTWA==
optionalDependencies:
"@img/sharp-libvips-linux-x64" "1.0.4"
"@img/sharp-linuxmusl-arm64@0.33.5":
version "0.33.5"
resolved "https://registry.yarnpkg.com/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.33.5.tgz#252975b915894fb315af5deea174651e208d3d6b"
resolved "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.33.5.tgz"
integrity sha512-XrHMZwGQGvJg2V/oRSUfSAfjfPxO+4DkiRh6p2AFjLQztWUuY/o8Mq0eMQVIY7HJ1CDQUJlxGGZRw1a5bqmd1g==
optionalDependencies:
"@img/sharp-libvips-linuxmusl-arm64" "1.0.4"
"@img/sharp-linuxmusl-x64@0.33.5":
version "0.33.5"
resolved "https://registry.yarnpkg.com/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.33.5.tgz#3f4609ac5d8ef8ec7dadee80b560961a60fd4f48"
resolved "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.33.5.tgz"
integrity sha512-WT+d/cgqKkkKySYmqoZ8y3pxx7lx9vVejxW/W4DOFMYVSkErR+w7mf2u8m/y4+xHe7yY9DAXQMWQhpnMuFfScw==
optionalDependencies:
"@img/sharp-libvips-linuxmusl-x64" "1.0.4"
"@img/sharp-wasm32@0.33.5":
version "0.33.5"
resolved "https://registry.yarnpkg.com/@img/sharp-wasm32/-/sharp-wasm32-0.33.5.tgz#6f44f3283069d935bb5ca5813153572f3e6f61a1"
resolved "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.33.5.tgz"
integrity sha512-ykUW4LVGaMcU9lu9thv85CbRMAwfeadCJHRsg2GmeRa/cJxsVY9Rbd57JcMxBkKHag5U/x7TSBpScF4U8ElVzg==
dependencies:
"@emnapi/runtime" "^1.2.0"
"@img/sharp-win32-ia32@0.33.5":
version "0.33.5"
resolved "https://registry.yarnpkg.com/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.33.5.tgz#1a0c839a40c5351e9885628c85f2e5dfd02b52a9"
resolved "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.33.5.tgz"
integrity sha512-T36PblLaTwuVJ/zw/LaH0PdZkRz5rd3SmMHX8GSmR7vtNSP5Z6bQkExdSK7xGWyxLw4sUknBuugTelgw2faBbQ==
"@img/sharp-win32-x64@0.33.5":
@@ -247,37 +247,37 @@
"@next/swc-darwin-arm64@15.2.2":
version "15.2.2"
resolved "https://registry.yarnpkg.com/@next/swc-darwin-arm64/-/swc-darwin-arm64-15.2.2.tgz#d3131279d4815ed7604eba7bf0063e0b8bfd2135"
resolved "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-15.2.2.tgz"
integrity sha512-HNBRnz+bkZ+KfyOExpUxTMR0Ow8nkkcE6IlsdEa9W/rI7gefud19+Sn1xYKwB9pdCdxIP1lPru/ZfjfA+iT8pw==
"@next/swc-darwin-x64@15.2.2":
version "15.2.2"
resolved "https://registry.yarnpkg.com/@next/swc-darwin-x64/-/swc-darwin-x64-15.2.2.tgz#8562f6b51177aa30f3a6e95a8b3e0e176a6cb04f"
resolved "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-15.2.2.tgz"
integrity sha512-mJOUwp7al63tDpLpEFpKwwg5jwvtL1lhRW2fI1Aog0nYCPAhxbJsaZKdoVyPZCy8MYf/iQVNDuk/+i29iLCzIA==
"@next/swc-linux-arm64-gnu@15.2.2":
version "15.2.2"
resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.2.2.tgz#c2466cffc541f39ae4d435087c81f5899d401903"
resolved "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.2.2.tgz"
integrity sha512-5ZZ0Zwy3SgMr7MfWtRE7cQWVssfOvxYfD9O7XHM7KM4nrf5EOeqwq67ZXDgo86LVmffgsu5tPO57EeFKRnrfSQ==
"@next/swc-linux-arm64-musl@15.2.2":
version "15.2.2"
resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.2.2.tgz#24ed87fe47325597e986bd01c427efae53d6b677"
resolved "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.2.2.tgz"
integrity sha512-cgKWBuFMLlJ4TWcFHl1KOaVVUAF8vy4qEvX5KsNd0Yj5mhu989QFCq1WjuaEbv/tO1ZpsQI6h/0YR8bLwEi+nA==
"@next/swc-linux-x64-gnu@15.2.2":
version "15.2.2"
resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.2.2.tgz#0240f44530ca39c1fc899fabe8f33aa35d7e77da"
resolved "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.2.2.tgz"
integrity sha512-c3kWSOSsVL8rcNBBfOq1+/j2PKs2nsMwJUV4icUxRgGBwUOfppeh7YhN5s79enBQFU+8xRgVatFkhHU1QW7yUA==
"@next/swc-linux-x64-musl@15.2.2":
version "15.2.2"
resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.2.2.tgz#cca6ce8e9ec89afaa618673f0f8658682c30ff56"
resolved "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.2.2.tgz"
integrity sha512-PXTW9PLTxdNlVYgPJ0equojcq1kNu5NtwcNjRjHAB+/sdoKZ+X8FBu70fdJFadkxFIGekQTyRvPMFF+SOJaQjw==
"@next/swc-win32-arm64-msvc@15.2.2":
version "15.2.2"
resolved "https://registry.yarnpkg.com/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.2.2.tgz#08f621602ed53fa21ddf6f656160b9186860be15"
resolved "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.2.2.tgz"
integrity sha512-nG644Es5llSGEcTaXhnGWR/aThM/hIaz0jx4MDg4gWC8GfTCp8eDBWZ77CVuv2ha/uL9Ce+nPTfYkSLG67/sHg==
"@next/swc-win32-x64-msvc@15.2.2":
@@ -293,7 +293,7 @@
"@nodelib/fs.stat" "2.0.5"
run-parallel "^1.1.9"
"@nodelib/fs.stat@2.0.5", "@nodelib/fs.stat@^2.0.2":
"@nodelib/fs.stat@^2.0.2", "@nodelib/fs.stat@2.0.5":
version "2.0.5"
resolved "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz"
integrity sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==
@@ -327,7 +327,7 @@
mkdirp "^1.0.4"
rimraf "^3.0.2"
"@panva/hkdf@^1.0.2":
"@panva/hkdf@^1.0.2", "@panva/hkdf@^1.1.1":
version "1.2.1"
resolved "https://registry.npmjs.org/@panva/hkdf/-/hkdf-1.2.1.tgz"
integrity sha512-6oclG6Y3PiDFcoyk8srjLfVKyMfVCKJ27JwNPViuXziFpmdz+MZnZN/aKY0JGXgYuO/VghU0jcOAZgWXZ1Dmrw==
@@ -365,52 +365,52 @@
"@tailwindcss/oxide-android-arm64@4.0.12":
version "4.0.12"
resolved "https://registry.yarnpkg.com/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.0.12.tgz#f79cf637bc11b0ae71e75b66d7c5ba5159bd3df3"
resolved "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.0.12.tgz"
integrity sha512-dAXCaemu3mHLXcA5GwGlQynX8n7tTdvn5i1zAxRvZ5iC9fWLl5bGnjZnzrQqT7ttxCvRwdVf3IHUnMVdDBO/kQ==
"@tailwindcss/oxide-darwin-arm64@4.0.12":
version "4.0.12"
resolved "https://registry.yarnpkg.com/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.0.12.tgz#6b730b5d19ff01748a27af5103966f534a899f07"
resolved "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.0.12.tgz"
integrity sha512-vPNI+TpJQ7sizselDXIJdYkx9Cu6JBdtmRWujw9pVIxW8uz3O2PjgGGzL/7A0sXI8XDjSyRChrUnEW9rQygmJQ==
"@tailwindcss/oxide-darwin-x64@4.0.12":
version "4.0.12"
resolved "https://registry.yarnpkg.com/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.0.12.tgz#2eb33ba40cbb78922004be9d6b81542347043fe5"
resolved "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.0.12.tgz"
integrity sha512-RL/9jM41Fdq4Efr35C5wgLx98BirnrfwuD+zgMFK6Ir68HeOSqBhW9jsEeC7Y/JcGyPd3MEoJVIU4fAb7YLg7A==
"@tailwindcss/oxide-freebsd-x64@4.0.12":
version "4.0.12"
resolved "https://registry.yarnpkg.com/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.0.12.tgz#317b980504b0bbcaee32c9e4e961f77cdadb02a6"
resolved "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.0.12.tgz"
integrity sha512-7WzWiax+LguJcMEimY0Q4sBLlFXu1tYxVka3+G2M9KmU/3m84J3jAIV4KZWnockbHsbb2XgrEjtlJKVwHQCoRA==
"@tailwindcss/oxide-linux-arm-gnueabihf@4.0.12":
version "4.0.12"
resolved "https://registry.yarnpkg.com/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.0.12.tgz#0f7153dec6e200327b76775ad2d1154706cef34d"
resolved "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.0.12.tgz"
integrity sha512-X9LRC7jjE1QlfIaBbXjY0PGeQP87lz5mEfLSVs2J1yRc9PSg1tEPS9NBqY4BU9v5toZgJgzKeaNltORyTs22TQ==
"@tailwindcss/oxide-linux-arm64-gnu@4.0.12":
version "4.0.12"
resolved "https://registry.yarnpkg.com/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.0.12.tgz#50a3ec46600cc998f45a265ebdd5de2d7bb27ee0"
resolved "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.0.12.tgz"
integrity sha512-i24IFNq2402zfDdoWKypXz0ZNS2G4NKaA82tgBlE2OhHIE+4mg2JDb5wVfyP6R+MCm5grgXvurcIcKWvo44QiQ==
"@tailwindcss/oxide-linux-arm64-musl@4.0.12":
version "4.0.12"
resolved "https://registry.yarnpkg.com/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.0.12.tgz#2de10c5239766e2fb810045fe5b75db54d481fd4"
resolved "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.0.12.tgz"
integrity sha512-LmOdshJBfAGIBG0DdBWhI0n5LTMurnGGJCHcsm9F//ISfsHtCnnYIKgYQui5oOz1SUCkqsMGfkAzWyNKZqbGNw==
"@tailwindcss/oxide-linux-x64-gnu@4.0.12":
version "4.0.12"
resolved "https://registry.yarnpkg.com/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.0.12.tgz#f89a24ffb0f14ea23d110efcbf58baa1db783582"
resolved "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.0.12.tgz"
integrity sha512-OSK667qZRH30ep8RiHbZDQfqkXjnzKxdn0oRwWzgCO8CoTxV+MvIkd0BWdQbYtYuM1wrakARV/Hwp0eA/qzdbw==
"@tailwindcss/oxide-linux-x64-musl@4.0.12":
version "4.0.12"
resolved "https://registry.yarnpkg.com/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.0.12.tgz#c53a5b494e241a744f067ba3a261adca80c20af4"
resolved "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.0.12.tgz"
integrity sha512-uylhWq6OWQ8krV8Jk+v0H/3AZKJW6xYMgNMyNnUbbYXWi7hIVdxRKNUB5UvrlC3RxtgsK5EAV2i1CWTRsNcAnA==
"@tailwindcss/oxide-win32-arm64-msvc@4.0.12":
version "4.0.12"
resolved "https://registry.yarnpkg.com/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.0.12.tgz#63c0befad43355fb5cea6c64efd946a718acb06b"
resolved "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.0.12.tgz"
integrity sha512-XDLnhMoXZEEOir1LK43/gHHwK84V1GlV8+pAncUAIN2wloeD+nNciI9WRIY/BeFTqES22DhTIGoilSO39xDb2g==
"@tailwindcss/oxide-win32-x64-msvc@4.0.12":
@@ -567,23 +567,23 @@
"@typescript-eslint/types" "8.26.1"
eslint-visitor-keys "^4.2.0"
"@zxing/browser@0.0.7":
version "0.0.7"
resolved "https://registry.npmjs.org/@zxing/browser/-/browser-0.0.7.tgz"
integrity sha512-AepzMgDnD6EjxewqmXpHJsi4S3Gw9ilZJLIbTf6fWuWySEcHBodnGu3p7FWlgq1Sd5QyfPhTum5z3CBkkhMVng==
optionalDependencies:
"@zxing/text-encoding" "^0.9.0"
"@zxing/library@^0.18.3":
version "0.18.6"
resolved "https://registry.npmjs.org/@zxing/library/-/library-0.18.6.tgz"
integrity sha512-bulZ9JHoLFd9W36pi+7e7DnEYNJhljYjZ1UTsKPOoLMU3qtC+REHITeCRNx40zTRJZx18W5TBRXt5pq2Uopjsw==
"@yudiel/react-qr-scanner@^1.2.4":
version "1.2.10"
resolved "https://registry.npmjs.org/@yudiel/react-qr-scanner/-/react-qr-scanner-1.2.10.tgz"
integrity sha512-I94AyrEu/2oZxXT6TEOK9+K2IfjqfPmic/GUah7alMIxfgXP9rs9+IZE56JhQr4pZ7Iz2RUC4JFfeaysFYNEIA==
dependencies:
ts-custom-error "^3.0.0"
"@zxing/library" "^0.20.0"
"@zxing/library@^0.20.0":
version "0.20.0"
resolved "https://registry.npmjs.org/@zxing/library/-/library-0.20.0.tgz"
integrity sha512-6Ev6rcqVjMakZFIDvbUf0dtpPGeZMTfyxYg4HkVWioWeN7cRcnUWT3bU6sdohc82O1nPXcjq6WiGfXX2Pnit6A==
dependencies:
ts-custom-error "^3.2.1"
optionalDependencies:
"@zxing/text-encoding" "~0.9.0"
"@zxing/text-encoding@^0.9.0", "@zxing/text-encoding@~0.9.0":
"@zxing/text-encoding@~0.9.0":
version "0.9.0"
resolved "https://registry.npmjs.org/@zxing/text-encoding/-/text-encoding-0.9.0.tgz"
integrity sha512-U/4aVJ2mxI0aDNI8Uq0wEhMgY+u4CNtEb0om3+y3+niDAsoTCOB33UF0sxpzqzdqXLqmvc+vZyAt4O8pPdfkwA==
@@ -603,7 +603,7 @@ acorn@^8.14.0:
resolved "https://registry.npmjs.org/acorn/-/acorn-8.14.1.tgz"
integrity sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==
agent-base@6, agent-base@^6.0.2:
agent-base@^6.0.2, agent-base@6:
version "6.0.2"
resolved "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz"
integrity sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==
@@ -954,6 +954,26 @@ client-only@0.0.1:
resolved "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz"
integrity sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==
"clue-hunt-app@file:":
version "0.1.0"
resolved "file:"
dependencies:
"@panva/hkdf" "^1.1.1"
"@yudiel/react-qr-scanner" "^1.2.4"
bcryptjs "^3.0.2"
better-sqlite3 "^11.8.1"
clue-hunt-app "file:"
html5-qrcode "^2.3.8"
jose "^4.14.4"
jsonwebtoken "^9.0.2"
next "15.2.2"
next-auth "^4.24.11"
qrcode.react "^4.2.0"
react "^19.0.0"
react-dom "^19.0.0"
sqlite "^5.1.1"
sqlite3 "^5.1.7"
color-convert@^2.0.1:
version "2.0.1"
resolved "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz"
@@ -1048,13 +1068,6 @@ data-view-byte-offset@^1.0.1:
es-errors "^1.3.0"
is-data-view "^1.0.1"
debug@4, debug@^4.3.1, debug@^4.3.2, debug@^4.3.3, debug@^4.3.4, debug@^4.3.7:
version "4.4.0"
resolved "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz"
integrity sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==
dependencies:
ms "^2.1.3"
debug@^3.2.7:
version "3.2.7"
resolved "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz"
@@ -1062,6 +1075,13 @@ debug@^3.2.7:
dependencies:
ms "^2.1.1"
debug@^4.3.1, debug@^4.3.2, debug@^4.3.3, debug@^4.3.4, debug@^4.3.7, debug@4:
version "4.4.0"
resolved "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz"
integrity sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==
dependencies:
ms "^2.1.3"
decompress-response@^6.0.0:
version "6.0.0"
resolved "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz"
@@ -1521,17 +1541,6 @@ fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3:
resolved "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz"
integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==
fast-glob@3.3.1:
version "3.3.1"
resolved "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.1.tgz"
integrity sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg==
dependencies:
"@nodelib/fs.stat" "^2.0.2"
"@nodelib/fs.walk" "^1.2.3"
glob-parent "^5.1.2"
merge2 "^1.3.0"
micromatch "^4.0.4"
fast-glob@^3.3.2:
version "3.3.3"
resolved "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz"
@@ -1543,6 +1552,17 @@ fast-glob@^3.3.2:
merge2 "^1.3.0"
micromatch "^4.0.8"
fast-glob@3.3.1:
version "3.3.1"
resolved "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.1.tgz"
integrity sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg==
dependencies:
"@nodelib/fs.stat" "^2.0.2"
"@nodelib/fs.walk" "^1.2.3"
glob-parent "^5.1.2"
merge2 "^1.3.0"
micromatch "^4.0.4"
fast-json-stable-stringify@^2.0.0:
version "2.1.0"
resolved "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz"
@@ -1629,11 +1649,6 @@ fs.realpath@^1.0.0:
resolved "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz"
integrity sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==
fsevents@~2.3.2:
version "2.3.3"
resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.3.tgz#cac6407785d03675a2a5e1a5305c697b347d90d6"
integrity sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==
function-bind@^1.1.2:
version "1.1.2"
resolved "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz"
@@ -1817,6 +1832,11 @@ hasown@^2.0.2:
dependencies:
function-bind "^1.1.2"
html5-qrcode@^2.3.8:
version "2.3.8"
resolved "https://registry.npmjs.org/html5-qrcode/-/html5-qrcode-2.3.8.tgz"
integrity sha512-jsr4vafJhwoLVEDW3n1KvPnCCXWaQfRng0/EEYk1vNcQGcG/htAdhJX0be8YyqMoSz7+hZvOZSTAepsabiuhiQ==
http-cache-semantics@^4.1.0:
version "4.1.1"
resolved "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz"
@@ -1894,7 +1914,7 @@ inflight@^1.0.4:
once "^1.3.0"
wrappy "1"
inherits@2, inherits@^2.0.3, inherits@^2.0.4:
inherits@^2.0.3, inherits@^2.0.4, inherits@2:
version "2.0.4"
resolved "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz"
integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==
@@ -2147,7 +2167,7 @@ jiti@^2.4.2:
resolved "https://registry.npmjs.org/jiti/-/jiti-2.4.2.tgz"
integrity sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A==
jose@^4.15.5, jose@^4.15.9:
jose@^4.14.4, jose@^4.15.5, jose@^4.15.9:
version "4.15.9"
resolved "https://registry.npmjs.org/jose/-/jose-4.15.9.tgz"
integrity sha512-1vUQX+IdDMVPj4k8kOxgUqlcK518yluMuGZwqlr44FS1ppZB/5GWh4rZG89erpOBOJjU/OBsnCVFfapsRz6nEA==
@@ -2263,47 +2283,47 @@ levn@^0.4.1:
lightningcss-darwin-arm64@1.29.2:
version "1.29.2"
resolved "https://registry.yarnpkg.com/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.29.2.tgz#6ceff38b01134af48e859394e1ca21e5d49faae6"
resolved "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.29.2.tgz"
integrity sha512-cK/eMabSViKn/PG8U/a7aCorpeKLMlK0bQeNHmdb7qUnBkNPnL+oV5DjJUo0kqWsJUapZsM4jCfYItbqBDvlcA==
lightningcss-darwin-x64@1.29.2:
version "1.29.2"
resolved "https://registry.yarnpkg.com/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.29.2.tgz#891b6f9e57682d794223c33463ca66d3af3fb038"
resolved "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.29.2.tgz"
integrity sha512-j5qYxamyQw4kDXX5hnnCKMf3mLlHvG44f24Qyi2965/Ycz829MYqjrVg2H8BidybHBp9kom4D7DR5VqCKDXS0w==
lightningcss-freebsd-x64@1.29.2:
version "1.29.2"
resolved "https://registry.yarnpkg.com/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.29.2.tgz#8a95f9ab73b2b2b0beefe1599fafa8b058938495"
resolved "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.29.2.tgz"
integrity sha512-wDk7M2tM78Ii8ek9YjnY8MjV5f5JN2qNVO+/0BAGZRvXKtQrBC4/cn4ssQIpKIPP44YXw6gFdpUF+Ps+RGsCwg==
lightningcss-linux-arm-gnueabihf@1.29.2:
version "1.29.2"
resolved "https://registry.yarnpkg.com/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.29.2.tgz#5c60bbf92b39d7ed51e363f7b98a7111bf5914a1"
resolved "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.29.2.tgz"
integrity sha512-IRUrOrAF2Z+KExdExe3Rz7NSTuuJ2HvCGlMKoquK5pjvo2JY4Rybr+NrKnq0U0hZnx5AnGsuFHjGnNT14w26sg==
lightningcss-linux-arm64-gnu@1.29.2:
version "1.29.2"
resolved "https://registry.yarnpkg.com/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.29.2.tgz#e73d7608c4cce034c3654e5e8b53be74846224de"
resolved "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.29.2.tgz"
integrity sha512-KKCpOlmhdjvUTX/mBuaKemp0oeDIBBLFiU5Fnqxh1/DZ4JPZi4evEH7TKoSBFOSOV3J7iEmmBaw/8dpiUvRKlQ==
lightningcss-linux-arm64-musl@1.29.2:
version "1.29.2"
resolved "https://registry.yarnpkg.com/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.29.2.tgz#a95a18d5a909831c092e0a8d2de4b9ac1a8db151"
resolved "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.29.2.tgz"
integrity sha512-Q64eM1bPlOOUgxFmoPUefqzY1yV3ctFPE6d/Vt7WzLW4rKTv7MyYNky+FWxRpLkNASTnKQUaiMJ87zNODIrrKQ==
lightningcss-linux-x64-gnu@1.29.2:
version "1.29.2"
resolved "https://registry.yarnpkg.com/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.29.2.tgz#551ca07e565394928642edee92acc042e546cb78"
resolved "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.29.2.tgz"
integrity sha512-0v6idDCPG6epLXtBH/RPkHvYx74CVziHo6TMYga8O2EiQApnUPZsbR9nFNrg2cgBzk1AYqEd95TlrsL7nYABQg==
lightningcss-linux-x64-musl@1.29.2:
version "1.29.2"
resolved "https://registry.yarnpkg.com/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.29.2.tgz#2fd164554340831bce50285b57101817850dd258"
resolved "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.29.2.tgz"
integrity sha512-rMpz2yawkgGT8RULc5S4WiZopVMOFWjiItBT7aSfDX4NQav6M44rhn5hjtkKzB+wMTRlLLqxkeYEtQ3dd9696w==
lightningcss-win32-arm64-msvc@1.29.2:
version "1.29.2"
resolved "https://registry.yarnpkg.com/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.29.2.tgz#da43ea49fafc5d2de38e016f1a8539d5eed98318"
resolved "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.29.2.tgz"
integrity sha512-nL7zRW6evGQqYVu/bKGK+zShyz8OVzsCotFgc7judbt6wnB2KbiKKJwBE4SGoDBQ1O94RjW4asrCjQL4i8Fhbw==
lightningcss-win32-x64-msvc@1.29.2:
@@ -2817,15 +2837,6 @@ possible-typed-array-names@^1.0.0:
resolved "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz"
integrity sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==
postcss@8.4.31:
version "8.4.31"
resolved "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz"
integrity sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==
dependencies:
nanoid "^3.3.6"
picocolors "^1.0.0"
source-map-js "^1.0.2"
postcss@^8.4.41:
version "8.5.3"
resolved "https://registry.npmjs.org/postcss/-/postcss-8.5.3.tgz"
@@ -2835,6 +2846,15 @@ postcss@^8.4.41:
picocolors "^1.1.1"
source-map-js "^1.2.1"
postcss@8.4.31:
version "8.4.31"
resolved "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz"
integrity sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==
dependencies:
nanoid "^3.3.6"
picocolors "^1.0.0"
source-map-js "^1.0.2"
preact-render-to-string@^5.1.19:
version "5.2.6"
resolved "https://registry.npmjs.org/preact-render-to-string/-/preact-render-to-string-5.2.6.tgz"
@@ -2942,15 +2962,6 @@ react-is@^16.13.1:
resolved "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz"
integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==
react-qr-reader@^3.0.0-beta-1:
version "3.0.0-beta-1"
resolved "https://registry.npmjs.org/react-qr-reader/-/react-qr-reader-3.0.0-beta-1.tgz"
integrity sha512-5HeFH9x/BlziRYQYGK2AeWS9WiKYZtGGMs9DXy3bcySTX3C9UJL9EwcPnWw8vlf7JP4FcrAlr1SnZ5nsWLQGyw==
dependencies:
"@zxing/browser" "0.0.7"
"@zxing/library" "^0.18.3"
rollup "^2.67.2"
react@^19.0.0:
version "19.0.0"
resolved "https://registry.npmjs.org/react/-/react-19.0.0.tgz"
@@ -3041,13 +3052,6 @@ rimraf@^3.0.2:
dependencies:
glob "^7.1.3"
rollup@^2.67.2:
version "2.79.2"
resolved "https://registry.npmjs.org/rollup/-/rollup-2.79.2.tgz"
integrity sha512-fS6iqSPZDs3dr/y7Od6y5nha8dW1YnbgtsyotCVvoFGKbERG++CVRFv1meyGDE1SNItQA8BrnCw7ScdAhRJ3XQ==
optionalDependencies:
fsevents "~2.3.2"
run-parallel@^1.1.9:
version "1.2.0"
resolved "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz"
@@ -3283,6 +3287,11 @@ sprintf-js@^1.1.3:
resolved "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz"
integrity sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==
sqlite@^5.1.1:
version "5.1.1"
resolved "https://registry.npmjs.org/sqlite/-/sqlite-5.1.1.tgz"
integrity sha512-oBkezXa2hnkfuJwUo44Hl9hS3er+YFtueifoajrgidvqsJRQFpc5fKoAkAor1O5ZnLoa28GBScfHXs8j0K358Q==
sqlite3@^5.1.7:
version "5.1.7"
resolved "https://registry.npmjs.org/sqlite3/-/sqlite3-5.1.7.tgz"
@@ -3295,11 +3304,6 @@ sqlite3@^5.1.7:
optionalDependencies:
node-gyp "8.x"
sqlite@^5.1.1:
version "5.1.1"
resolved "https://registry.npmjs.org/sqlite/-/sqlite-5.1.1.tgz"
integrity sha512-oBkezXa2hnkfuJwUo44Hl9hS3er+YFtueifoajrgidvqsJRQFpc5fKoAkAor1O5ZnLoa28GBScfHXs8j0K358Q==
ssri@^8.0.0, ssri@^8.0.1:
version "8.0.1"
resolved "https://registry.npmjs.org/ssri/-/ssri-8.0.1.tgz"
@@ -3317,6 +3321,13 @@ streamsearch@^1.1.0:
resolved "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz"
integrity sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==
string_decoder@^1.1.1:
version "1.3.0"
resolved "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz"
integrity sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==
dependencies:
safe-buffer "~5.2.0"
"string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.2.3:
version "4.2.3"
resolved "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz"
@@ -3394,13 +3405,6 @@ string.prototype.trimstart@^1.0.8:
define-properties "^1.2.1"
es-object-atoms "^1.0.0"
string_decoder@^1.1.1:
version "1.3.0"
resolved "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz"
integrity sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==
dependencies:
safe-buffer "~5.2.0"
strip-ansi@^6.0.1:
version "6.0.1"
resolved "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz"
@@ -3442,7 +3446,7 @@ supports-preserve-symlinks-flag@^1.0.0:
resolved "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz"
integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==
tailwindcss@4.0.12, tailwindcss@^4:
tailwindcss@^4, tailwindcss@4.0.12:
version "4.0.12"
resolved "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.0.12.tgz"
integrity sha512-bT0hJo91FtncsAMSsMzUkoo/iEU0Xs5xgFgVC9XmdM9bw5MhZuQFjPNl6wxAE0SiQF/YTZJa+PndGWYSDtuxAg==
@@ -3505,7 +3509,7 @@ ts-api-utils@^2.0.1:
resolved "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.0.1.tgz"
integrity sha512-dnlgjFSVetynI8nzgJ+qF62efpglpWRk8isUEWZGWlJYySCTD6aKvbUDu+zbPeDakk3bg5H4XpitHukgfL1m9w==
ts-custom-error@^3.0.0:
ts-custom-error@^3.2.1:
version "3.3.1"
resolved "https://registry.npmjs.org/ts-custom-error/-/ts-custom-error-3.3.1.tgz"
integrity sha512-5OX1tzOjxWEgsr/YEUWSuPrQ00deKLh6D7OTWcvNHm12/7QPyRh8SYpyWvA4IZv8H/+GQWQEh/kwo95Q9OVW1A==