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 { formatElapsedTime, getHuntTimer } from "@/utils/timerService";
import Link from "next/link"; import Link from "next/link";
import dynamic from "next/dynamic"; 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 // Dynamically import the new QR scanner component with no SSR
const QRScanner = dynamic(() => import("@/components/QRScanner"), { const EnhancedQRScanner = dynamic(
ssr: false, () => import("@/components/EnhancedQRScanner"),
}); {
ssr: false,
loading: () => <div className="scanner-loading">Loading scanner...</div>,
}
);
export default function CluePage({ params }) { 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 [clue, setClue] = useState(null);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [error, setError] = useState(null); const [error, setError] = useState(null);
const [foundNow, setFoundNow] = useState(false);
const [elapsedTime, setElapsedTime] = useState("00:00:00"); const [elapsedTime, setElapsedTime] = useState("00:00:00");
const [showScanner, setShowScanner] = useState(false);
const [scanResult, setScanResult] = useState(null);
const [scanMessage, setScanMessage] = 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 router = useRouter();
const { data: session, status } = useSession(); // Get session to check if user is admin const { data: session, status } = useSession();
const { clueId } = params;
// Check if user is admin // Check if user is admin
const isAdmin = session?.user?.role === "admin"; const isAdmin = session?.user?.role === "admin";
// This effect handles authentication and initial clue data fetch // This effect handles authentication and initial clue data fetch
useEffect(() => { useEffect(() => {
let isMounted = true; // Flag to prevent state updates after unmount let isMounted = true;
const now = Date.now(); const now = Date.now();
const minRefreshInterval = isAdmin ? 30000 : 10000;
// Only allow refreshes every 10 seconds (more for admins)
const minRefreshInterval = isAdmin ? 30000 : 10000; // 30 seconds for admins, 10 seconds for regular users
// Skip if we refreshed too recently and this isn't the first load // Skip if we refreshed too recently and this isn't the first load
if (loading === false && now - lastRefresh < minRefreshInterval) { if (loading === false && now - lastRefresh < minRefreshInterval) {
@@ -50,10 +57,34 @@ export default function CluePage({ params }) {
return; 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() { async function fetchClue() {
try { try {
setLoading(true); setLoading(true);
await fetchAllClues();
// Add timestamp to avoid browser caching // Add timestamp to avoid browser caching
const timestamp = Date.now(); const timestamp = Date.now();
@@ -76,16 +107,11 @@ export default function CluePage({ params }) {
} }
const data = await response.json(); const data = await response.json();
// Only update state if the component is still mounted
if (isMounted) { if (isMounted) {
setClue(data); setClue(data);
// Only set foundNow on initial load or if it's actually newly found setClueFound(data.found);
if (loading === true) {
setFoundNow(data.foundNow);
}
setLoading(false); setLoading(false);
setLastRefresh(Date.now()); // Update last refresh timestamp setLastRefresh(Date.now());
} }
} catch (err) { } catch (err) {
console.error("Error fetching clue:", err); console.error("Error fetching clue:", err);
@@ -98,17 +124,14 @@ export default function CluePage({ params }) {
fetchClue(); fetchClue();
// Cleanup function to prevent state updates after unmount
return () => { return () => {
isMounted = false; 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(() => { useEffect(() => {
let timerId; let timerId;
// Set up timer display updates
const timerData = getHuntTimer(); const timerData = getHuntTimer();
if (!timerData || !timerData.isActive) { if (!timerData || !timerData.isActive) {
return; return;
@@ -125,42 +148,60 @@ export default function CluePage({ params }) {
return () => { return () => {
if (timerId) clearInterval(timerId); if (timerId) clearInterval(timerId);
}; };
}, []); // Empty dependency array so it only runs once }, []);
// Handle QR scan result // Handle QR scan result
const handleScan = (data) => { const handleScan = (data) => {
setScanResult(data);
setScanMessage(`QR Code scanned: ${data}`); setScanMessage(`QR Code scanned: ${data}`);
try { try {
// Handle both "QR123" format and just "123" format // Handle multiple QR code formats: "QR123", "123", or even "Clue 123"
let scannedClueId; 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) { if (match) {
// Extract the number from QR[number]
scannedClueId = parseInt(match[1], 10); scannedClueId = parseInt(match[1], 10);
} else { } else {
// Try to parse the raw value // Try direct parsing as a last resort
scannedClueId = parseInt(data, 10); scannedClueId = parseInt(data, 10);
} }
if (!isNaN(scannedClueId) && scannedClueId === parseInt(clueId)) { 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); markClueAsFound(clueId);
} else if (!isNaN(scannedClueId)) {
setScanMessage(
`QR code for clue ${scannedClueId} scanned, but you need clue ${clueId}. Please scan the correct code.`
);
} else { } else {
setScanMessage( 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) { } catch (err) {
console.error("Error processing scan result:", err);
setScanMessage("Invalid QR code format. Please try again."); 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) => { const markClueAsFound = async (currentClueId) => {
try { 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 // Mark the clue as found via API
const response = await fetch("/api/mark-clue", { const response = await fetch("/api/mark-clue", {
method: "POST", method: "POST",
@@ -169,29 +210,11 @@ export default function CluePage({ params }) {
}); });
if (response.ok) { if (response.ok) {
// Success message // Update local state to show clue is found
setScanMessage("Clue verified! Moving to the next clue..."); setClueFound(true);
setScanMessage(
// Fetch all clues to find the next available one "✅ Clue verified! You can now proceed to the next clue."
const allCluesResponse = await fetch("/api/clues");
const allClues = await allCluesResponse.json();
// Find the current clue index
const currentIndex = allClues.findIndex(
(c) => c.id === parseInt(currentClueId)
); );
if (currentIndex !== -1 && currentIndex < allClues.length - 1) {
// Navigate to the next clue after a brief delay
setTimeout(() => {
router.push(`/clue/${allClues[currentIndex + 1].id}`);
}, 1500);
} else {
// This was the last clue
setTimeout(() => {
router.push("/map");
}, 1500);
}
} else { } else {
setScanMessage("Error verifying clue. Please try again."); setScanMessage("Error verifying clue. Please try again.");
} }
@@ -201,35 +224,35 @@ export default function CluePage({ params }) {
} }
}; };
// Function to handle skip button // Navigate to the next clue
const handleSkip = async () => { const handleNextClue = () => {
// Fetch all clues to find the next one if (nextClueId) {
try { router.push(`/clue/${nextClueId}`);
const response = await fetch("/api/clues"); } else {
const allClues = await response.json(); 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)); const currentIndex = allClues.findIndex((c) => c.id === parseInt(clueId));
if (currentIndex > 0) {
if (currentIndex !== -1 && currentIndex < allClues.length - 1) { router.push(`/clue/${allClues[currentIndex - 1].id}`);
// Navigate to the next clue
router.push(`/clue/${allClues[currentIndex + 1].id}`);
} else { } else {
// This was the last clue
router.push("/map"); router.push("/map");
} }
} catch (err) { } else {
console.error("Error skipping to next clue:", err); router.push("/map");
setError("Failed to skip to next clue");
} }
}; };
// Function for manual refresh - for admins to test without waiting // Function for manual refresh - for admins to test without waiting
const handleManualRefresh = () => { const handleManualRefresh = () => {
// Force a refresh by setting lastRefresh to 0
setLastRefresh(0); setLastRefresh(0);
}; };
// Rendering states
if (loading) { if (loading) {
return ( return (
<div className="clue-container"> <div className="clue-container">
@@ -276,7 +299,6 @@ export default function CluePage({ params }) {
return ( return (
<div className="clue-container"> <div className="clue-container">
<div className="timer-display">{elapsedTime}</div> <div className="timer-display">{elapsedTime}</div>
<div className="clue-content-wrapper"> <div className="clue-content-wrapper">
{/* Admin tools section - only visible to admins */} {/* Admin tools section - only visible to admins */}
{isAdmin && ( {isAdmin && (
@@ -291,29 +313,26 @@ export default function CluePage({ params }) {
</div> </div>
)} )}
{/* QR Scanner Section */} {/* QR Scanner Section - Removed manual entry button */}
<div className="scanner-section"> <div className="scanner-section">
<h2>Scan QR Code</h2> <h2>Scan QR Code</h2>
<QRScanner onScan={handleScan} width={280} height={280} /> <EnhancedQRScanner onScan={handleScan} width={280} height={280} />
{scanMessage && <div className="scan-message">{scanMessage}</div>} {scanMessage && (
<div className={`scan-message ${clueFound ? "success" : ""}`}>
{scanMessage}
</div>
)}
{/* Add the QR Code helper component */}
<QRCodeHelper clueId={clueId} />
</div> </div>
{/* Clue Information Section */} {/* Clue Information Section */}
<div className="clue-scroll"> <div className="clue-scroll">
{foundNow && ( <h1 className="clue-title">{clue?.title}</h1>
<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>
<div className="clue-content"> <div className="clue-content">
<h2>Description</h2> <h2>Description</h2>
<p className="clue-description">{clue.description}</p> <p className="clue-description">{clue?.description}</p>
{clue?.location && (
{clue.location && (
<div className="clue-location"> <div className="clue-location">
<h2>Location Hint</h2> <h2>Location Hint</h2>
<p>{clue.location}</p> <p>{clue.location}</p>
@@ -322,13 +341,19 @@ export default function CluePage({ params }) {
</div> </div>
</div> </div>
{/* Action Buttons */} {/* Navigation Buttons */}
<div className="action-buttons"> <div className="navigation-buttons">
<button onClick={handleSkip} className="skip-button"> <button onClick={handlePrevClue} className="nav-button prev-button">
Skip Previous Clue
</button> </button>
<button onClick={() => setShowScanner(true)} className="scan-button"> <button
Scan Clue onClick={() => router.push("/map")}
className="nav-button map-button"
>
Back to Map
</button>
<button onClick={handleNextClue} className="nav-button next-button">
Next Clue
</button> </button>
</div> </div>
</div> </div>
@@ -414,6 +439,12 @@ export default function CluePage({ params }) {
width: 100%; width: 100%;
} }
.scan-message.success {
background-color: #c8e6c9;
color: #2e7d32;
font-weight: bold;
}
.clue-scroll { .clue-scroll {
background-color: #f8f3e3; background-color: #f8f3e3;
border-radius: 10px; border-radius: 10px;
@@ -445,39 +476,46 @@ export default function CluePage({ params }) {
border-top: 1px dashed #9c6b30; border-top: 1px dashed #9c6b30;
} }
.action-buttons { .navigation-buttons {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
width: 100%; width: 100%;
margin-top: 10px; margin-top: 20px;
margin-bottom: 20px; margin-bottom: 20px;
flex-wrap: wrap; /* Allow buttons to wrap on small screens */
gap: 10px;
} }
.skip-button, .nav-button {
.scan-button { padding: 12px 15px;
padding: 12px 0;
border: none; border: none;
border-radius: 5px; border-radius: 5px;
cursor: pointer; cursor: pointer;
font-weight: bold; font-weight: bold;
transition: all 0.2s; transition: all 0.2s;
width: 48%; font-size: 14px;
font-size: 16px; flex: 1;
min-width: 120px;
} }
.skip-button { .prev-button {
background-color: #a98467; background-color: #a98467;
color: white; color: white;
} }
.scan-button { .map-button {
background-color: #607d8b;
color: white;
}
.next-button {
background-color: #68543c; background-color: #68543c;
color: white; color: white;
} }
.skip-button:hover, .nav-button:hover {
.scan-button:hover {
opacity: 0.9; opacity: 0.9;
transform: translateY(-2px);
} }
.loading, .loading,
@@ -519,39 +557,6 @@ export default function CluePage({ params }) {
padding-bottom: 10px; 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 { .nav-bar {
display: flex; display: flex;
justify-content: space-around; justify-content: space-around;
@@ -583,7 +588,6 @@ export default function CluePage({ params }) {
font-size: 12px; font-size: 12px;
} }
/* Admin tools styling */
.admin-tools { .admin-tools {
background-color: #f8d7da; background-color: #f8d7da;
border: 2px dashed #d9534f; border: 2px dashed #d9534f;
+81 -138
View File
@@ -64,162 +64,45 @@
height: 100%; height: 100%;
background-color: #69b5a8; /* Background color */ background-color: #69b5a8; /* Background color */
overflow: auto; /* Enable scrolling */ overflow: auto; /* Enable scrolling */
display: block; /* Change from flex to block for proper scrolling */ display: block; /* Use block for proper scrolling */
} }
.treasure-map { .treasure-map {
position: relative; 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 */ min-width: 100%; /* Ensure it's at least full width */
height: fit-content; height: fit-content;
min-height: 100%; min-height: 100%;
display: flex; /* Keep flex for alignment */ display: flex;
justify-content: flex-end; /* Align to the right */ justify-content: flex-end; /* Align to the right */
align-items: flex-start; /* Align to the top */ align-items: flex-start; /* Align to the top */
} }
/* SVG container for better scrolling and zooming */ /* Map image container for positioning */
.svg-container { .map-image-container {
position: relative; position: relative;
width: fit-content; /* Allow natural width */ width: fit-content;
height: fit-content; /* Allow natural height */ height: fit-content;
display: block; /* Change to block for proper scrolling */
overflow: visible; /* Show content beyond boundaries */
z-index: 5; /* Ensure it's above the background */
} }
.svg-container svg { /* Map image styling */
max-width: none; /* Remove max-width limitation */ .map-image {
display: block;
max-width: none; /* Don't constrain the image */
width: auto; width: auto;
height: auto; height: auto;
display: block;
object-fit: none; /* Don't constrain the SVG */
position: relative;
} }
/* Force SVG elements to show properly with z-index */ /* Map overlay positioned absolutely over the map image */
.svg-container svg * { .map-overlay {
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 {
position: absolute; position: absolute;
top: 0; top: 0;
right: 0; /* Align to the right */ left: 0;
width: 100vw; /* Full viewport width */ width: 100%;
height: auto; height: 100%;
background-image: url("/images/map.svg"); pointer-events: auto; /* Allow interaction with buttons */
background-size: auto; /* Don't resize the background */ z-index: 20; /* Ensure overlay is above map */
background-position: top right; /* Position at top right */ box-sizing: border-box;
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;
} }
.loading-container { .loading-container {
@@ -274,8 +157,8 @@
box-sizing: border-box; box-sizing: border-box;
z-index: 100; /* Ensure it stays on top */ z-index: 100; /* Ensure it stays on top */
flex-shrink: 0; /* Prevent navbar from shrinking */ flex-shrink: 0; /* Prevent navbar from shrinking */
margin: 0; /* Added margin property */ margin: 0;
height: auto; /* Added height property */ height: auto;
} }
.nav-item { .nav-item {
@@ -343,3 +226,63 @@
font-size: 11px; 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, setHuntTimer,
clearHuntTimer, clearHuntTimer,
formatElapsedTime, formatElapsedTime,
syncTimerWithServer,
} from "@/utils/timerService"; } from "@/utils/timerService";
import {
populateMap,
updateCluePointStatus,
attachClueClickHandlers,
} from "@/utils/map-populate";
import { addDebugOverlay, fixCluePointsRendering } from "@/utils/map-debug";
export default function MapPage() { export default function MapPage() {
const { data: session, status } = useSession(); const { data: session, status } = useSession();
@@ -24,7 +19,6 @@ export default function MapPage() {
const [error, setError] = useState(null); const [error, setError] = useState(null);
const [teamData, setTeamData] = useState(null); const [teamData, setTeamData] = useState(null);
const [elapsedTime, setElapsedTime] = useState("00:00:00"); const [elapsedTime, setElapsedTime] = useState("00:00:00");
const [mapSvg, setMapSvg] = useState(null);
const [clues, setClues] = useState([]); const [clues, setClues] = useState([]);
const mapContainerRef = useRef(null); const mapContainerRef = useRef(null);
const router = useRouter(); const router = useRouter();
@@ -120,6 +114,7 @@ export default function MapPage() {
}); });
setClues(processedClues); setClues(processedClues);
setLoading(false); // Set loading to false after clues are fetched
return processedClues; return processedClues;
} catch (err) { } catch (err) {
console.error("Failed to fetch clues:", 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 // Handle clue button click
const enhanceSvgMap = useCallback(() => { const handleClueClick = useCallback(
if (!mapContainerRef.current) return; (clueId, isAvailable, isFound) => {
if (isFound || isAvailable) {
// Find the SVG element inside the map container router.push(`/clue/${clueId}`);
const svgElement = mapContainerRef.current.querySelector("svg"); } else {
if (!svgElement) { alert("Complete previous clues to unlock this one!");
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;
} }
},
[router]
);
const svgText = await response.text(); // Size and position the overlay to match the map image
console.log(`SVG loaded, length: ${svgText.length} characters`); const setupOverlay = useCallback(() => {
const overlay = document.getElementById("map-overlay-container");
const mapImg = document.getElementById("treasure-map-img");
// Ensure SVG has correct namespaces and viewBox if (overlay && mapImg) {
let fixedSvg = svgText.replace( // Wait for the image to load to get its dimensions
/<svg/, mapImg.onload = () => {
'<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"' 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 // Handle resize events
if (!fixedSvg.includes('viewBox="')) { const resizeOverlay = () => {
const widthMatch = fixedSvg.match(/width="([^"]+)"/); const mapRect = mapImg.getBoundingClientRect();
const heightMatch = fixedSvg.match(/height="([^"]+)"/); overlay.style.width = `${mapRect.width}px`;
if (widthMatch && heightMatch) { overlay.style.height = `${mapRect.height}px`;
const width = widthMatch[1]; };
const height = heightMatch[1];
fixedSvg = fixedSvg.replace(
/<svg/,
`<svg viewBox="0 0 ${width} ${height}"`
);
}
}
setMapSvg(fixedSvg); // Initial resize and listen for window resize
setLoading(false); setTimeout(resizeOverlay, 300);
} catch (error) { window.addEventListener("resize", resizeOverlay);
console.error("Failed to load map SVG:", error);
setMapError(true); return () => {
setLoading(false); 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 // Function to check hunt status and timer
async function checkHuntAndTimer() { async function checkHuntAndTimer() {
if (!isMounted.current) return;
try { try {
const response = await fetch("/api/hunt-status", { const response = await fetch("/api/hunt-status", {
method: "GET", method: "GET",
headers: { headers: {
"Content-Type": "application/json", "Content-Type": "application/json",
"Cache-Control": "no-cache, no-store, must-revalidate", "Cache-Control": "no-cache, no-store, must-revalidate",
Pragma: "no-cache",
}, },
credentials: "include", credentials: "include",
}); });
if (response.ok) { if (response.ok) {
const data = await response.json(); 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 hunt is active, ensure we have timer data
if (!data.active) { if (data.active && data.startTime) {
console.log("Hunt not active, checking if it ended..."); setHuntTimer(data.startTime);
// 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);
}
} }
} }
} catch (err) { } catch (err) {
@@ -343,298 +217,65 @@ export default function MapPage() {
// Add this function before it's used in initializeTimer // Add this function before it's used in initializeTimer
async function fetchTeamHuntStartTime() { async function fetchTeamHuntStartTime() {
try { try {
// First get the user's team ID // First try to get any existing timer data
const userResponse = await fetch("/api/user", { const timerData = getHuntTimer();
method: "GET", if (timerData && timerData.startTime) {
headers: { return true;
"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;
} }
const userData = await userResponse.json(); // If no timer data exists, sync with server
const teamId = userData.teamId; const result = await syncTimerWithServer();
return result.success;
if (!teamId) { } catch (err) {
console.error("User does not belong to a team"); console.error("Error fetching team hunt start time:", err);
return null; return false;
}
// 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;
} }
} }
// Use team hunt start time for elapsed time calculation // Use team hunt start time for elapsed time calculation
useEffect(() => { useEffect(() => {
// Initialize the timer without waiting for teamData let timerId;
// This ensures timer starts as soon as possible
const initializeTimer = async () => {
try {
// First try to get timer data from the centralized service
let timerData = getHuntTimer();
// If no timer data yet, fetch it from API // Initialize timer
if (!timerData || !timerData.startTime) { async function initializeTimer() {
console.log("No timer data found, fetching from API..."); // First ensure we have timer data
const startTimeStr = await fetchTeamHuntStartTime(); await fetchTeamHuntStartTime();
console.log("Got start time:", startTimeStr);
if (startTimeStr) { // Set up timer updates
setHuntTimer(startTimeStr); const updateTimer = () => {
timerData = getHuntTimer(); const 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
if (timerData && timerData.startTime) { if (timerData && timerData.startTime) {
setElapsedTime(formatElapsedTime(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(); // Initial update
} else if (status === "unauthenticated") { updateTimer();
router.push("/login");
}
}, [status, fetchClues, loadAndEnhanceMap, router]);
// 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(() => { useEffect(() => {
// Only try to enhance the map if both the SVG and clues are loaded // Initial check
if (!mapSvg || !clues.length) return; 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; return () => {
const maxAttempts = 3; clearInterval(huntStatusInterval);
};
function attemptMapEnhancement() { }, [softRedirect, hardRedirect]);
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]);
// Add useEffect to load team data // Add useEffect to load team data
useEffect(() => { useEffect(() => {
@@ -680,11 +321,22 @@ export default function MapPage() {
<div className={styles["map-error"]}> <div className={styles["map-error"]}>
<h2>Error Loading Map</h2> <h2>Error Loading Map</h2>
<p>We couldn't load the treasure map. Please try again.</p> <p>We couldn't load the treasure map. Please try again.</p>
<button onClick={loadAndEnhanceMap}>Retry</button> <button onClick={fetchClues}>Retry</button>
</div> </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 ( return (
<div className={styles["map-page"]}> <div className={styles["map-page"]}>
{/* Map Banner with team info and timer */} {/* Map Banner with team info and timer */}
@@ -701,36 +353,41 @@ export default function MapPage() {
{/* Main map content area */} {/* Main map content area */}
<div className={styles["map-container"]} ref={mapContainerRef}> <div className={styles["map-container"]} ref={mapContainerRef}>
<div className={styles["treasure-map"]}> <div className={styles["treasure-map"]}>
{mapSvg ? ( {/* Simpler map implementation using an img element */}
<div <div className={styles["map-image-container"]}>
className={styles["svg-container"]} <img
dangerouslySetInnerHTML={{ __html: mapSvg }} id="treasure-map-img"
src="/images/map.svg"
alt="Treasure Map"
className={styles["map-image"]}
/> />
) : (
<div className={styles["loading-container"]}> {/* Overlay div with the clue button - positioned exactly over the map */}
<div className={styles["loading-spinner"]}></div> <div id="map-overlay-container" className={styles["map-overlay"]}>
<div>Loading map...</div> {/* 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> </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 */} {/* Navigation bar */}
<nav className={styles["nav-bar"]}> <nav className={styles["nav-bar"]}>
<Link <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; const { clueId } = context.params;
// Validate clueId is a number // Validate clueId is a number
@@ -31,7 +32,7 @@ export async function GET(request, context) {
return Response.json({ error: "Invalid clue ID" }, { status: 400 }); return Response.json({ error: "Invalid clue ID" }, { status: 400 });
} }
// Get the specific clue - removed coordinates // Get the specific clue
const clue = await queryOne( const clue = await queryOne(
"SELECT id, title, description, location FROM clues WHERE id = ?", "SELECT id, title, description, location FROM clues WHERE id = ?",
[clueId] [clueId]
@@ -41,54 +42,12 @@ export async function GET(request, context) {
return Response.json({ error: "Clue not found" }, { status: 404 }); 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 // Check if this team has found this clue before
const found = await queryOne( const found = await queryOne(
"SELECT * FROM team_clues WHERE team_id = ? AND clue_id = ?", "SELECT * FROM team_clues WHERE team_id = ? AND clue_id = ?",
[token.teamId, clueId] [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 // Add different cache headers for admins vs regular users
const cacheHeaders = const cacheHeaders =
token.role === "admin" token.role === "admin"
@@ -106,8 +65,7 @@ export async function GET(request, context) {
return Response.json( return Response.json(
{ {
...clue, ...clue,
found: true, // Always return as found since the user is viewing it found: !!found, // Only mark as found if it was previously found
foundNow, // Only true during initial discovery for non-admins
isAdmin: token.role === "admin", // Add flag so client knows user is admin isAdmin: token.role === "admin", // Add flag so client knows user is admin
}, },
{ headers: cacheHeaders } { headers: cacheHeaders }
+29 -22
View File
@@ -1,5 +1,5 @@
import { query, queryOne } from "@/lib/db";
import { getToken } from "next-auth/jwt"; import { getToken } from "next-auth/jwt";
import { queryOne, run } from "@/lib/db";
export const dynamic = "force-dynamic"; // Disable caching export const dynamic = "force-dynamic"; // Disable caching
@@ -24,15 +24,36 @@ export async function POST(request) {
); );
} }
// Parse request body
const { clueId } = await request.json(); const { clueId } = await request.json();
// Validate clueId is a number // Validate clueId is a number
if (!clueId || isNaN(parseInt(clueId))) { if (isNaN(parseInt(clueId))) {
return Response.json({ error: "Invalid clue ID" }, { status: 400 }); return Response.json({ error: "Invalid clue ID" }, { status: 400 });
} }
// Ensure the team_clues table exists // Check if clue exists
await run(` 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 ( CREATE TABLE IF NOT EXISTS team_clues (
team_id INTEGER NOT NULL, team_id INTEGER NOT NULL,
clue_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 // 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, token.teamId,
clueId, clueId,
]); ]);
console.log(`Clue ${clueId} marked as found for team ${token.teamId}`);
// Return successful response
return Response.json({ return Response.json({
success: true, success: true,
message: "Clue marked as found successfully", message: "Clue marked as found successfully",
clueId: clueId,
teamId: token.teamId,
}); });
} catch (error) { } catch (error) {
console.error("Error marking clue as found:", 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"; "use client";
import { useState, useRef, useEffect } from "react"; import { useState, useEffect } from "react";
import { Html5QrcodeScanner } from "html5-qrcode"; import { Scanner } from "@yudiel/react-qr-scanner";
export default function QRScanner({ onScan, width = 300, height = 300 }) { export default function QRScanner({ onScan, width = 300, height = 300 }) {
const [scanning, setScanning] = useState(false);
const [error, setError] = useState(null); 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(() => { useEffect(() => {
// Initialize scanner // Check if we're in a secure context (https or localhost)
if (!scanning && typeof window !== "undefined") { const secure = window.isSecureContext;
setIsSecureContext(secure);
// Check if camera can be accessed
async function checkCameraAccess() {
try { try {
const qrScanner = new Html5QrcodeScanner( const devices = await navigator.mediaDevices.enumerateDevices();
"qr-reader", const videoDevices = devices.filter(
{ (device) => device.kind === "videoinput"
fps: 10,
qrbox: { width: width * 0.7, height: height * 0.7 },
aspectRatio: 1,
disableFlip: false,
formatsToSupport: ["QR_CODE"],
},
false // don't start scanning immediately
); );
// Store the scanner instance if (videoDevices.length === 0) {
scannerRef.current = qrScanner; setError(
"No camera detected. Please make sure your device has a camera."
// If onScan is provided, set up the success callback
if (onScan) {
qrScanner.render(
(decodedText) => {
if (onScan) {
onScan(decodedText);
}
setScanning(false);
},
(errorMessage) => {
// No need to show decode errors to user as they're expected during scanning
console.log(errorMessage);
}
); );
setScanning(true);
} }
setInitialized(true);
} catch (err) { } catch (err) {
console.error("QR Scanner initialization error:", err); console.error("Error checking camera devices:", err);
setError( setError(`Camera initialization error: ${err.message}`);
"Could not initialize QR scanner. Please make sure you have camera access." setInitialized(true);
);
} }
} }
// Cleanup function checkCameraAccess();
return () => { }, []);
if (scannerRef.current) {
try { const handleDecode = (result) => {
scannerRef.current.clear(); if (onScan && result) {
} catch (err) { onScan(result);
console.error("Failed to clear QR scanner:", err); }
} };
}
}; const handleError = (error) => {
}, [onScan, width, height, scanning]); 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 ( return (
<div className="qr-scanner"> <div className="qr-scanner">
{error && <div className="qr-error">{error}</div>} {error && (
<div id="qr-reader" style={{ width, height }}></div> <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>{` <style jsx>{`
.qr-scanner { .qr-scanner {
@@ -85,16 +152,24 @@ export default function QRScanner({ onScan, width = 300, height = 300 }) {
width: 100%; width: 100%;
} }
#qr-reader { .scanner-loading {
border: 2px solid #ddd; text-align: center;
padding: 12px;
background-color: #e8f5e9;
border-radius: 8px; border-radius: 8px;
overflow: hidden; width: 100%;
background-color: #f5f5f5; margin-bottom: 15px;
} }
#qr-reader video { .scanner-container {
object-fit: cover; position: relative;
border-radius: 6px; width: 100%;
max-width: ${width}px;
height: ${height}px;
border-radius: 8px;
overflow: hidden;
border: 2px solid #ddd;
background-color: #f5f5f5;
} }
`}</style> `}</style>
</div> </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", "name": "clue-hunt-app",
"version": "0.1.0", "version": "0.1.0",
"dependencies": { "dependencies": {
"@panva/hkdf": "^1.1.1",
"@yudiel/react-qr-scanner": "^1.2.4",
"bcryptjs": "^3.0.2", "bcryptjs": "^3.0.2",
"better-sqlite3": "^11.8.1", "better-sqlite3": "^11.8.1",
"clue-hunt-app": "file:",
"html5-qrcode": "^2.3.8",
"jose": "^4.14.4",
"jsonwebtoken": "^9.0.2", "jsonwebtoken": "^9.0.2",
"next": "15.2.2", "next": "15.2.2",
"next-auth": "^4.24.11", "next-auth": "^4.24.11",
"qrcode.react": "^4.2.0", "qrcode.react": "^4.2.0",
"react": "^19.0.0", "react": "^19.0.0",
"react-dom": "^19.0.0", "react-dom": "^19.0.0",
"react-qr-reader": "^3.0.0-beta-1",
"sqlite": "^5.1.1", "sqlite": "^5.1.1",
"sqlite3": "^5.1.7" "sqlite3": "^5.1.7"
}, },
@@ -1429,25 +1433,26 @@
"url": "https://opencollective.com/typescript-eslint" "url": "https://opencollective.com/typescript-eslint"
} }
}, },
"node_modules/@zxing/browser": { "node_modules/@yudiel/react-qr-scanner": {
"version": "0.0.7", "version": "1.2.10",
"resolved": "https://registry.npmjs.org/@zxing/browser/-/browser-0.0.7.tgz", "resolved": "https://registry.npmjs.org/@yudiel/react-qr-scanner/-/react-qr-scanner-1.2.10.tgz",
"integrity": "sha512-AepzMgDnD6EjxewqmXpHJsi4S3Gw9ilZJLIbTf6fWuWySEcHBodnGu3p7FWlgq1Sd5QyfPhTum5z3CBkkhMVng==", "integrity": "sha512-I94AyrEu/2oZxXT6TEOK9+K2IfjqfPmic/GUah7alMIxfgXP9rs9+IZE56JhQr4pZ7Iz2RUC4JFfeaysFYNEIA==",
"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==",
"license": "MIT", "license": "MIT",
"dependencies": { "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": { "engines": {
"node": ">= 10.4.0" "node": ">= 10.4.0"
@@ -2109,6 +2114,10 @@
"integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==", "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/clue-hunt-app": {
"resolved": "",
"link": true
},
"node_modules/color": { "node_modules/color": {
"version": "4.2.3", "version": "4.2.3",
"resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz", "resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz",
@@ -3274,20 +3283,6 @@
"license": "ISC", "license": "ISC",
"optional": true "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": { "node_modules/function-bind": {
"version": "1.1.2", "version": "1.1.2",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
@@ -3619,6 +3614,12 @@
"node": ">= 0.4" "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": { "node_modules/http-cache-semantics": {
"version": "4.1.1", "version": "4.1.1",
"resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz", "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz",
@@ -5781,21 +5782,6 @@
"dev": true, "dev": true,
"license": "MIT" "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": { "node_modules/readable-stream": {
"version": "3.6.2", "version": "3.6.2",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
@@ -5939,21 +5925,6 @@
"url": "https://github.com/sponsors/isaacs" "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": { "node_modules/run-parallel": {
"version": "1.2.0", "version": "1.2.0",
"resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", "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" "init-db": "node lib/init-db.js"
}, },
"dependencies": { "dependencies": {
"@panva/hkdf": "^1.1.1",
"@yudiel/react-qr-scanner": "^1.2.4",
"bcryptjs": "^3.0.2", "bcryptjs": "^3.0.2",
"better-sqlite3": "^11.8.1", "better-sqlite3": "^11.8.1",
"clue-hunt-app": "file:",
"html5-qrcode": "^2.3.8",
"jose": "^4.14.4",
"jsonwebtoken": "^9.0.2", "jsonwebtoken": "^9.0.2",
"next": "15.2.2", "next": "15.2.2",
"next-auth": "^4.24.11", "next-auth": "^4.24.11",
"qrcode.react": "^4.2.0", "qrcode.react": "^4.2.0",
"react": "^19.0.0", "react": "^19.0.0",
"react-dom": "^19.0.0", "react-dom": "^19.0.0",
"react-qr-reader": "^3.0.0-beta-1",
"sqlite": "^5.1.1", "sqlite": "^5.1.1",
"sqlite3": "^5.1.7" "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="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)"/> <rect x="373" y="1512" width="88" height="88" fill="url(#pattern11_37_192)"/>
<g filter="url(#filter0_d_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"/> <circle cx="230" cy="211" r="25" stroke="#757575" stroke-width="2"/>
</g> </g>
<g filter="url(#filter1_d_37_192)"> <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"/> <circle cx="320" cy="284" r="25" stroke="#757575" stroke-width="2"/>
</g> </g>
<g filter="url(#filter2_d_37_192)"> <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"/> <circle cx="264" cy="397" r="25" stroke="#757575" stroke-width="2"/>
</g> </g>
<g filter="url(#filter3_d_37_192)"> <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"/> <circle cx="342" cy="517" r="25" stroke="#757575" stroke-width="2"/>
</g> </g>
<g filter="url(#filter4_d_37_192)"> <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"/> <circle cx="320" cy="645" r="25" stroke="#757575" stroke-width="2"/>
</g> </g>
<g filter="url(#filter5_d_37_192)"> <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"/> <circle cx="295" cy="773" r="25" stroke="#757575" stroke-width="2"/>
</g> </g>
<g filter="url(#filter6_d_37_192)"> <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"/> <circle cx="239" cy="880" r="25" stroke="#757575" stroke-width="2"/>
</g> </g>
<g filter="url(#filter7_d_37_192)"> <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"/> <circle cx="354" cy="959" r="25" stroke="#757575" stroke-width="2"/>
</g> </g>
<g filter="url(#filter8_d_37_192)"> <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"/> <circle cx="270" cy="1036" r="25" stroke="#757575" stroke-width="2"/>
</g> </g>
<g filter="url(#filter9_d_37_192)"> <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"/> <circle cx="279" cy="1174" r="25" stroke="#757575" stroke-width="2"/>
</g> </g>
<g filter="url(#filter10_d_37_192)"> <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"/> <circle cx="337" cy="1302" r="25" stroke="#757575" stroke-width="2"/>
</g> </g>
<g filter="url(#filter11_d_37_192)"> <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"/> <circle cx="250" cy="1388" r="25" stroke="#757575" stroke-width="2"/>
</g> </g>
<g filter="url(#filter12_d_37_192)"> <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"/> <circle cx="162" cy="1487" r="25" stroke="#757575" stroke-width="2"/>
</g> </g>
<g filter="url(#filter13_d_37_192)"> <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"/> <circle cx="306" cy="1531" r="25" stroke="#757575" stroke-width="2"/>
</g> </g>
<g filter="url(#filter14_d_37_192)"> <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"/> <circle cx="342" cy="1649" r="25" stroke="#757575" stroke-width="2"/>
</g> </g>
<g filter="url(#filter15_d_37_192)"> <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"/> <circle cx="225" cy="1724" r="25" stroke="#757575" stroke-width="2"/>
</g> </g>
<g filter="url(#filter16_d_37_192)"> <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"/> <circle cx="216" cy="1822" r="25" stroke="#757575" stroke-width="2"/>
</g> </g>
<g filter="url(#filter17_d_37_192)"> <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"/> <circle cx="275" cy="1883" r="25" stroke="#757575" stroke-width="2"/>
</g> </g>
<g filter="url(#filter18_d_37_192)"> <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"/> <circle cx="362" cy="1917" r="25" stroke="#757575" stroke-width="2"/>
</g> </g>
<g filter="url(#filter19_d_37_192)"> <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"/> <circle cx="300" cy="2037" r="25" stroke="#757575" stroke-width="2"/>
</g> </g>
</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 // Ensure all clue points are in the overlay group and have proper styles
cluePoints.forEach((point, i) => { 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 // Clone the point to remove any existing event handlers and styling issues
const newPoint = point.cloneNode(true); 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 // Apply explicit styles to ensure visibility
newPoint.setAttribute( newPoint.setAttribute(
"style", "style",
@@ -114,6 +128,12 @@ export function fixCluePointsRendering(svgElement) {
// Add to overlay group // Add to overlay group
overlayGroup.appendChild(newPoint); 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 // 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"); 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`); console.log(`Found ${allCircles.length} circles in SVG`);
if (allCircles.length === 0) { if (allCircles.length === 0) {
console.error("No circles found in SVG. Creating fallback circles."); console.error(
"No circles found in SVG. Attempting to load from original map."
// Create some fallback circles at predefined positions
const defs = document.createElementNS(
"http://www.w3.org/2000/svg",
"defs"
); );
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) => { // If no circles in current SVG, try to fetch circles from the original map SVG
const circle = document.createElementNS( try {
"http://www.w3.org/2000/svg", const mapResponse = await fetch("/images/map.svg");
"circle" if (!mapResponse.ok) {
); throw new Error("Failed to load original map SVG");
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 const mapText = await mapResponse.text();
svgElement.appendChild(defs); const parser = new DOMParser();
const mapDoc = parser.parseFromString(mapText, "image/svg+xml");
// Now find all circles again // Find circles in the original map
const fallbackCircles = Array.from(defs.querySelectorAll("circle")); const originalCircles = Array.from(mapDoc.querySelectorAll("circle"));
if (fallbackCircles.length > 0) { console.log(`Found ${originalCircles.length} circles in original map`);
console.log(`Created ${fallbackCircles.length} fallback circles`);
return await processCircles(svgElement, fallbackCircles); if (originalCircles.length > 0) {
} else { return await processCircles(svgElement, originalCircles, true);
return false; } 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 { } else {
return await processCircles(svgElement, allCircles); return await processCircles(svgElement, allCircles, false);
} }
} catch (error) { } catch (error) {
console.error("Error populating map:", 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 * Process circles to create clue points
* @param {SVGElement} svgElement - The SVG DOM element * @param {SVGElement} svgElement - The SVG DOM element
* @param {Array} circles - Array of circle elements * @param {Array} circles - Array of circle elements
* @param {boolean} isFromOriginalMap - Whether circles are from the original map
* @returns {Promise<boolean>} - Success flag * @returns {Promise<boolean>} - Success flag
*/ */
async function processCircles(svgElement, circles) { async function processCircles(svgElement, circles, isFromOriginalMap) {
try { try {
// Hide original circles // Don't hide original circles if they are from the original map and not present in current SVG
circles.forEach((circle) => (circle.style.display = "none")); 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 // Fetch the number of clues from the API
const response = await fetch("/api/clues/count", { const response = await fetch("/api/clues/count", {
@@ -198,7 +234,7 @@ async function processCircles(svgElement, circles) {
} }
const data = await response.json(); 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}`); console.log(`API returned clue count: ${clueCount}`);
if (clueCount <= 0) { if (clueCount <= 0) {
@@ -206,21 +242,17 @@ async function processCircles(svgElement, circles) {
clueCount = 5; // Fallback to 5 clues if API returns invalid data clueCount = 5; // Fallback to 5 clues if API returns invalid data
} }
// Sort circles by vertical position (top to bottom) // Use the circles exactly where they are - preserve original positions
const sortedCircles = circles.sort((a, b) => { // Just limit how many we use based on clue count
const aY = parseFloat(a.getAttribute("cy") || "0"); const circlesToShow = Math.min(circles.length, clueCount);
const bY = parseFloat(b.getAttribute("cy") || "0"); console.log(
return aY - bY; `Will create ${circlesToShow} clue points from available circles`
}); );
// Determine how many circles to show
const circlesToShow = Math.min(sortedCircles.length, clueCount);
console.log(`Will create ${circlesToShow} clue points from sorted circles`);
// Create the overlay element that will go on top of everything // Create the overlay element that will go on top of everything
const svgNamespace = "http://www.w3.org/2000/svg"; 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"); const oldOverlay = svgElement.querySelector(".clue-points-overlay");
if (oldOverlay && oldOverlay.parentNode) { if (oldOverlay && oldOverlay.parentNode) {
oldOverlay.parentNode.removeChild(oldOverlay); 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 // Add the overlay group as the LAST element in the SVG to ensure proper rendering order
svgElement.appendChild(overlayGroup); svgElement.appendChild(overlayGroup);
// Create interactive clue points // Create interactive clue points based on the original circle positions
for (let i = 0; i < circlesToShow; i++) { for (let i = 0; i < circlesToShow; i++) {
const originalCircle = sortedCircles[i]; const originalCircle = circles[i];
// Get original circle attributes // Get original circle attributes
const cx = originalCircle.getAttribute("cx") || "0"; 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 // This ensures it's the last child of the SVG
svgElement.appendChild(overlayGroup); 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; return true;
} catch (error) { } catch (error) {
console.error("Error processing circles:", error); console.error("Error processing circles:", error);
+139 -135
View File
@@ -16,7 +16,7 @@
"@emnapi/runtime@^1.2.0": "@emnapi/runtime@^1.2.0":
version "1.3.1" 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== integrity sha512-kEBmG8KyqtxJZv+ygbEim+KCGtIq1fC22Ms3S4ziXmYKm8uyoLX0MHONVKwp+9opg390VaKRNt4a7A9NwmpNhw==
dependencies: dependencies:
tslib "^2.4.0" tslib "^2.4.0"
@@ -122,110 +122,110 @@
"@img/sharp-darwin-arm64@0.33.5": "@img/sharp-darwin-arm64@0.33.5":
version "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== integrity sha512-UT4p+iz/2H4twwAoLCqfA9UH5pI6DggwKEGuaPy7nCVQ8ZsiY5PIcrRvD1DzuY3qYL07NtIQcWnBSY/heikIFQ==
optionalDependencies: optionalDependencies:
"@img/sharp-libvips-darwin-arm64" "1.0.4" "@img/sharp-libvips-darwin-arm64" "1.0.4"
"@img/sharp-darwin-x64@0.33.5": "@img/sharp-darwin-x64@0.33.5":
version "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== integrity sha512-fyHac4jIc1ANYGRDxtiqelIbdWkIuQaI84Mv45KvGRRxSAa7o7d1ZKAOBaYbnepLC1WqxfpimdeWfvqqSGwR2Q==
optionalDependencies: optionalDependencies:
"@img/sharp-libvips-darwin-x64" "1.0.4" "@img/sharp-libvips-darwin-x64" "1.0.4"
"@img/sharp-libvips-darwin-arm64@1.0.4": "@img/sharp-libvips-darwin-arm64@1.0.4":
version "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== integrity sha512-XblONe153h0O2zuFfTAbQYAX2JhYmDHeWikp1LM9Hul9gVPjFY427k6dFEcOL72O01QxQsWi761svJ/ev9xEDg==
"@img/sharp-libvips-darwin-x64@1.0.4": "@img/sharp-libvips-darwin-x64@1.0.4":
version "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== 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": "@img/sharp-libvips-linux-arm@1.0.5":
version "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== 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": "@img/sharp-libvips-linux-s390x@1.0.4":
version "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== integrity sha512-u7Wz6ntiSSgGSGcjZ55im6uvTrOxSIS8/dgoVMoiGE9I6JAfU50yH5BoDlYA1tcuGS7g/QNtetJnxA6QEsCVTA==
"@img/sharp-libvips-linux-x64@1.0.4": "@img/sharp-libvips-linux-x64@1.0.4":
version "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== integrity sha512-MmWmQ3iPFZr0Iev+BAgVMb3ZyC4KeFc3jFxnNbEPas60e1cIfevbtuyf9nDGIzOaW9PdnDciJm+wFFaTlj5xYw==
"@img/sharp-libvips-linuxmusl-arm64@1.0.4": "@img/sharp-libvips-linuxmusl-arm64@1.0.4":
version "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== integrity sha512-9Ti+BbTYDcsbp4wfYib8Ctm1ilkugkA/uscUn6UXK1ldpC1JjiXbLfFZtRlBhjPZ5o1NCLiDbg8fhUPKStHoTA==
"@img/sharp-libvips-linuxmusl-x64@1.0.4": "@img/sharp-libvips-linuxmusl-x64@1.0.4":
version "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== 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": "@img/sharp-linux-arm@0.33.5":
version "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== integrity sha512-JTS1eldqZbJxjvKaAkxhZmBqPRGmxgu+qFKSInv8moZ2AmT5Yib3EQ1c6gp493HvrvV8QgdOXdyaIBrhvFhBMQ==
optionalDependencies: optionalDependencies:
"@img/sharp-libvips-linux-arm" "1.0.5" "@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": "@img/sharp-linux-s390x@0.33.5":
version "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== integrity sha512-y/5PCd+mP4CA/sPDKl2961b+C9d+vPAveS33s6Z3zfASk2j5upL6fXVPZi7ztePZ5CuH+1kW8JtvxgbuXHRa4Q==
optionalDependencies: optionalDependencies:
"@img/sharp-libvips-linux-s390x" "1.0.4" "@img/sharp-libvips-linux-s390x" "1.0.4"
"@img/sharp-linux-x64@0.33.5": "@img/sharp-linux-x64@0.33.5":
version "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== integrity sha512-opC+Ok5pRNAzuvq1AG0ar+1owsu842/Ab+4qvU879ippJBHvyY5n2mxF1izXqkPYlGuP/M556uh53jRLJmzTWA==
optionalDependencies: optionalDependencies:
"@img/sharp-libvips-linux-x64" "1.0.4" "@img/sharp-libvips-linux-x64" "1.0.4"
"@img/sharp-linuxmusl-arm64@0.33.5": "@img/sharp-linuxmusl-arm64@0.33.5":
version "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== integrity sha512-XrHMZwGQGvJg2V/oRSUfSAfjfPxO+4DkiRh6p2AFjLQztWUuY/o8Mq0eMQVIY7HJ1CDQUJlxGGZRw1a5bqmd1g==
optionalDependencies: optionalDependencies:
"@img/sharp-libvips-linuxmusl-arm64" "1.0.4" "@img/sharp-libvips-linuxmusl-arm64" "1.0.4"
"@img/sharp-linuxmusl-x64@0.33.5": "@img/sharp-linuxmusl-x64@0.33.5":
version "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== integrity sha512-WT+d/cgqKkkKySYmqoZ8y3pxx7lx9vVejxW/W4DOFMYVSkErR+w7mf2u8m/y4+xHe7yY9DAXQMWQhpnMuFfScw==
optionalDependencies: optionalDependencies:
"@img/sharp-libvips-linuxmusl-x64" "1.0.4" "@img/sharp-libvips-linuxmusl-x64" "1.0.4"
"@img/sharp-wasm32@0.33.5": "@img/sharp-wasm32@0.33.5":
version "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== integrity sha512-ykUW4LVGaMcU9lu9thv85CbRMAwfeadCJHRsg2GmeRa/cJxsVY9Rbd57JcMxBkKHag5U/x7TSBpScF4U8ElVzg==
dependencies: dependencies:
"@emnapi/runtime" "^1.2.0" "@emnapi/runtime" "^1.2.0"
"@img/sharp-win32-ia32@0.33.5": "@img/sharp-win32-ia32@0.33.5":
version "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== integrity sha512-T36PblLaTwuVJ/zw/LaH0PdZkRz5rd3SmMHX8GSmR7vtNSP5Z6bQkExdSK7xGWyxLw4sUknBuugTelgw2faBbQ==
"@img/sharp-win32-x64@0.33.5": "@img/sharp-win32-x64@0.33.5":
@@ -247,37 +247,37 @@
"@next/swc-darwin-arm64@15.2.2": "@next/swc-darwin-arm64@15.2.2":
version "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== integrity sha512-HNBRnz+bkZ+KfyOExpUxTMR0Ow8nkkcE6IlsdEa9W/rI7gefud19+Sn1xYKwB9pdCdxIP1lPru/ZfjfA+iT8pw==
"@next/swc-darwin-x64@15.2.2": "@next/swc-darwin-x64@15.2.2":
version "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== integrity sha512-mJOUwp7al63tDpLpEFpKwwg5jwvtL1lhRW2fI1Aog0nYCPAhxbJsaZKdoVyPZCy8MYf/iQVNDuk/+i29iLCzIA==
"@next/swc-linux-arm64-gnu@15.2.2": "@next/swc-linux-arm64-gnu@15.2.2":
version "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== integrity sha512-5ZZ0Zwy3SgMr7MfWtRE7cQWVssfOvxYfD9O7XHM7KM4nrf5EOeqwq67ZXDgo86LVmffgsu5tPO57EeFKRnrfSQ==
"@next/swc-linux-arm64-musl@15.2.2": "@next/swc-linux-arm64-musl@15.2.2":
version "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== integrity sha512-cgKWBuFMLlJ4TWcFHl1KOaVVUAF8vy4qEvX5KsNd0Yj5mhu989QFCq1WjuaEbv/tO1ZpsQI6h/0YR8bLwEi+nA==
"@next/swc-linux-x64-gnu@15.2.2": "@next/swc-linux-x64-gnu@15.2.2":
version "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== integrity sha512-c3kWSOSsVL8rcNBBfOq1+/j2PKs2nsMwJUV4icUxRgGBwUOfppeh7YhN5s79enBQFU+8xRgVatFkhHU1QW7yUA==
"@next/swc-linux-x64-musl@15.2.2": "@next/swc-linux-x64-musl@15.2.2":
version "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== integrity sha512-PXTW9PLTxdNlVYgPJ0equojcq1kNu5NtwcNjRjHAB+/sdoKZ+X8FBu70fdJFadkxFIGekQTyRvPMFF+SOJaQjw==
"@next/swc-win32-arm64-msvc@15.2.2": "@next/swc-win32-arm64-msvc@15.2.2":
version "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== integrity sha512-nG644Es5llSGEcTaXhnGWR/aThM/hIaz0jx4MDg4gWC8GfTCp8eDBWZ77CVuv2ha/uL9Ce+nPTfYkSLG67/sHg==
"@next/swc-win32-x64-msvc@15.2.2": "@next/swc-win32-x64-msvc@15.2.2":
@@ -293,7 +293,7 @@
"@nodelib/fs.stat" "2.0.5" "@nodelib/fs.stat" "2.0.5"
run-parallel "^1.1.9" 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" version "2.0.5"
resolved "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz" resolved "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz"
integrity sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A== integrity sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==
@@ -327,7 +327,7 @@
mkdirp "^1.0.4" mkdirp "^1.0.4"
rimraf "^3.0.2" rimraf "^3.0.2"
"@panva/hkdf@^1.0.2": "@panva/hkdf@^1.0.2", "@panva/hkdf@^1.1.1":
version "1.2.1" version "1.2.1"
resolved "https://registry.npmjs.org/@panva/hkdf/-/hkdf-1.2.1.tgz" resolved "https://registry.npmjs.org/@panva/hkdf/-/hkdf-1.2.1.tgz"
integrity sha512-6oclG6Y3PiDFcoyk8srjLfVKyMfVCKJ27JwNPViuXziFpmdz+MZnZN/aKY0JGXgYuO/VghU0jcOAZgWXZ1Dmrw== integrity sha512-6oclG6Y3PiDFcoyk8srjLfVKyMfVCKJ27JwNPViuXziFpmdz+MZnZN/aKY0JGXgYuO/VghU0jcOAZgWXZ1Dmrw==
@@ -365,52 +365,52 @@
"@tailwindcss/oxide-android-arm64@4.0.12": "@tailwindcss/oxide-android-arm64@4.0.12":
version "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== integrity sha512-dAXCaemu3mHLXcA5GwGlQynX8n7tTdvn5i1zAxRvZ5iC9fWLl5bGnjZnzrQqT7ttxCvRwdVf3IHUnMVdDBO/kQ==
"@tailwindcss/oxide-darwin-arm64@4.0.12": "@tailwindcss/oxide-darwin-arm64@4.0.12":
version "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== integrity sha512-vPNI+TpJQ7sizselDXIJdYkx9Cu6JBdtmRWujw9pVIxW8uz3O2PjgGGzL/7A0sXI8XDjSyRChrUnEW9rQygmJQ==
"@tailwindcss/oxide-darwin-x64@4.0.12": "@tailwindcss/oxide-darwin-x64@4.0.12":
version "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== integrity sha512-RL/9jM41Fdq4Efr35C5wgLx98BirnrfwuD+zgMFK6Ir68HeOSqBhW9jsEeC7Y/JcGyPd3MEoJVIU4fAb7YLg7A==
"@tailwindcss/oxide-freebsd-x64@4.0.12": "@tailwindcss/oxide-freebsd-x64@4.0.12":
version "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== integrity sha512-7WzWiax+LguJcMEimY0Q4sBLlFXu1tYxVka3+G2M9KmU/3m84J3jAIV4KZWnockbHsbb2XgrEjtlJKVwHQCoRA==
"@tailwindcss/oxide-linux-arm-gnueabihf@4.0.12": "@tailwindcss/oxide-linux-arm-gnueabihf@4.0.12":
version "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== integrity sha512-X9LRC7jjE1QlfIaBbXjY0PGeQP87lz5mEfLSVs2J1yRc9PSg1tEPS9NBqY4BU9v5toZgJgzKeaNltORyTs22TQ==
"@tailwindcss/oxide-linux-arm64-gnu@4.0.12": "@tailwindcss/oxide-linux-arm64-gnu@4.0.12":
version "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== integrity sha512-i24IFNq2402zfDdoWKypXz0ZNS2G4NKaA82tgBlE2OhHIE+4mg2JDb5wVfyP6R+MCm5grgXvurcIcKWvo44QiQ==
"@tailwindcss/oxide-linux-arm64-musl@4.0.12": "@tailwindcss/oxide-linux-arm64-musl@4.0.12":
version "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== integrity sha512-LmOdshJBfAGIBG0DdBWhI0n5LTMurnGGJCHcsm9F//ISfsHtCnnYIKgYQui5oOz1SUCkqsMGfkAzWyNKZqbGNw==
"@tailwindcss/oxide-linux-x64-gnu@4.0.12": "@tailwindcss/oxide-linux-x64-gnu@4.0.12":
version "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== integrity sha512-OSK667qZRH30ep8RiHbZDQfqkXjnzKxdn0oRwWzgCO8CoTxV+MvIkd0BWdQbYtYuM1wrakARV/Hwp0eA/qzdbw==
"@tailwindcss/oxide-linux-x64-musl@4.0.12": "@tailwindcss/oxide-linux-x64-musl@4.0.12":
version "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== integrity sha512-uylhWq6OWQ8krV8Jk+v0H/3AZKJW6xYMgNMyNnUbbYXWi7hIVdxRKNUB5UvrlC3RxtgsK5EAV2i1CWTRsNcAnA==
"@tailwindcss/oxide-win32-arm64-msvc@4.0.12": "@tailwindcss/oxide-win32-arm64-msvc@4.0.12":
version "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== integrity sha512-XDLnhMoXZEEOir1LK43/gHHwK84V1GlV8+pAncUAIN2wloeD+nNciI9WRIY/BeFTqES22DhTIGoilSO39xDb2g==
"@tailwindcss/oxide-win32-x64-msvc@4.0.12": "@tailwindcss/oxide-win32-x64-msvc@4.0.12":
@@ -567,23 +567,23 @@
"@typescript-eslint/types" "8.26.1" "@typescript-eslint/types" "8.26.1"
eslint-visitor-keys "^4.2.0" eslint-visitor-keys "^4.2.0"
"@zxing/browser@0.0.7": "@yudiel/react-qr-scanner@^1.2.4":
version "0.0.7" version "1.2.10"
resolved "https://registry.npmjs.org/@zxing/browser/-/browser-0.0.7.tgz" resolved "https://registry.npmjs.org/@yudiel/react-qr-scanner/-/react-qr-scanner-1.2.10.tgz"
integrity sha512-AepzMgDnD6EjxewqmXpHJsi4S3Gw9ilZJLIbTf6fWuWySEcHBodnGu3p7FWlgq1Sd5QyfPhTum5z3CBkkhMVng== integrity sha512-I94AyrEu/2oZxXT6TEOK9+K2IfjqfPmic/GUah7alMIxfgXP9rs9+IZE56JhQr4pZ7Iz2RUC4JFfeaysFYNEIA==
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==
dependencies: 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: 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": "@zxing/text-encoding@~0.9.0":
version "0.9.0" version "0.9.0"
resolved "https://registry.npmjs.org/@zxing/text-encoding/-/text-encoding-0.9.0.tgz" resolved "https://registry.npmjs.org/@zxing/text-encoding/-/text-encoding-0.9.0.tgz"
integrity sha512-U/4aVJ2mxI0aDNI8Uq0wEhMgY+u4CNtEb0om3+y3+niDAsoTCOB33UF0sxpzqzdqXLqmvc+vZyAt4O8pPdfkwA== 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" resolved "https://registry.npmjs.org/acorn/-/acorn-8.14.1.tgz"
integrity sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg== 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" version "6.0.2"
resolved "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz" resolved "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz"
integrity sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ== 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" resolved "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz"
integrity sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA== 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: color-convert@^2.0.1:
version "2.0.1" version "2.0.1"
resolved "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz" 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" es-errors "^1.3.0"
is-data-view "^1.0.1" 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: debug@^3.2.7:
version "3.2.7" version "3.2.7"
resolved "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz" resolved "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz"
@@ -1062,6 +1075,13 @@ debug@^3.2.7:
dependencies: dependencies:
ms "^2.1.1" 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: decompress-response@^6.0.0:
version "6.0.0" version "6.0.0"
resolved "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz" 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" resolved "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz"
integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== 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: fast-glob@^3.3.2:
version "3.3.3" version "3.3.3"
resolved "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz" 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" merge2 "^1.3.0"
micromatch "^4.0.8" 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: fast-json-stable-stringify@^2.0.0:
version "2.1.0" version "2.1.0"
resolved "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz" 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" resolved "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz"
integrity sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw== 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: function-bind@^1.1.2:
version "1.1.2" version "1.1.2"
resolved "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz" resolved "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz"
@@ -1817,6 +1832,11 @@ hasown@^2.0.2:
dependencies: dependencies:
function-bind "^1.1.2" 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: http-cache-semantics@^4.1.0:
version "4.1.1" version "4.1.1"
resolved "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz" 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" once "^1.3.0"
wrappy "1" 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" version "2.0.4"
resolved "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz" resolved "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz"
integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== 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" resolved "https://registry.npmjs.org/jiti/-/jiti-2.4.2.tgz"
integrity sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A== 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" version "4.15.9"
resolved "https://registry.npmjs.org/jose/-/jose-4.15.9.tgz" resolved "https://registry.npmjs.org/jose/-/jose-4.15.9.tgz"
integrity sha512-1vUQX+IdDMVPj4k8kOxgUqlcK518yluMuGZwqlr44FS1ppZB/5GWh4rZG89erpOBOJjU/OBsnCVFfapsRz6nEA== integrity sha512-1vUQX+IdDMVPj4k8kOxgUqlcK518yluMuGZwqlr44FS1ppZB/5GWh4rZG89erpOBOJjU/OBsnCVFfapsRz6nEA==
@@ -2263,47 +2283,47 @@ levn@^0.4.1:
lightningcss-darwin-arm64@1.29.2: lightningcss-darwin-arm64@1.29.2:
version "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== integrity sha512-cK/eMabSViKn/PG8U/a7aCorpeKLMlK0bQeNHmdb7qUnBkNPnL+oV5DjJUo0kqWsJUapZsM4jCfYItbqBDvlcA==
lightningcss-darwin-x64@1.29.2: lightningcss-darwin-x64@1.29.2:
version "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== integrity sha512-j5qYxamyQw4kDXX5hnnCKMf3mLlHvG44f24Qyi2965/Ycz829MYqjrVg2H8BidybHBp9kom4D7DR5VqCKDXS0w==
lightningcss-freebsd-x64@1.29.2: lightningcss-freebsd-x64@1.29.2:
version "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== integrity sha512-wDk7M2tM78Ii8ek9YjnY8MjV5f5JN2qNVO+/0BAGZRvXKtQrBC4/cn4ssQIpKIPP44YXw6gFdpUF+Ps+RGsCwg==
lightningcss-linux-arm-gnueabihf@1.29.2: lightningcss-linux-arm-gnueabihf@1.29.2:
version "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== integrity sha512-IRUrOrAF2Z+KExdExe3Rz7NSTuuJ2HvCGlMKoquK5pjvo2JY4Rybr+NrKnq0U0hZnx5AnGsuFHjGnNT14w26sg==
lightningcss-linux-arm64-gnu@1.29.2: lightningcss-linux-arm64-gnu@1.29.2:
version "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== integrity sha512-KKCpOlmhdjvUTX/mBuaKemp0oeDIBBLFiU5Fnqxh1/DZ4JPZi4evEH7TKoSBFOSOV3J7iEmmBaw/8dpiUvRKlQ==
lightningcss-linux-arm64-musl@1.29.2: lightningcss-linux-arm64-musl@1.29.2:
version "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== integrity sha512-Q64eM1bPlOOUgxFmoPUefqzY1yV3ctFPE6d/Vt7WzLW4rKTv7MyYNky+FWxRpLkNASTnKQUaiMJ87zNODIrrKQ==
lightningcss-linux-x64-gnu@1.29.2: lightningcss-linux-x64-gnu@1.29.2:
version "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== integrity sha512-0v6idDCPG6epLXtBH/RPkHvYx74CVziHo6TMYga8O2EiQApnUPZsbR9nFNrg2cgBzk1AYqEd95TlrsL7nYABQg==
lightningcss-linux-x64-musl@1.29.2: lightningcss-linux-x64-musl@1.29.2:
version "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== integrity sha512-rMpz2yawkgGT8RULc5S4WiZopVMOFWjiItBT7aSfDX4NQav6M44rhn5hjtkKzB+wMTRlLLqxkeYEtQ3dd9696w==
lightningcss-win32-arm64-msvc@1.29.2: lightningcss-win32-arm64-msvc@1.29.2:
version "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== integrity sha512-nL7zRW6evGQqYVu/bKGK+zShyz8OVzsCotFgc7judbt6wnB2KbiKKJwBE4SGoDBQ1O94RjW4asrCjQL4i8Fhbw==
lightningcss-win32-x64-msvc@1.29.2: 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" resolved "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz"
integrity sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg== 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: postcss@^8.4.41:
version "8.5.3" version "8.5.3"
resolved "https://registry.npmjs.org/postcss/-/postcss-8.5.3.tgz" resolved "https://registry.npmjs.org/postcss/-/postcss-8.5.3.tgz"
@@ -2835,6 +2846,15 @@ postcss@^8.4.41:
picocolors "^1.1.1" picocolors "^1.1.1"
source-map-js "^1.2.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: preact-render-to-string@^5.1.19:
version "5.2.6" version "5.2.6"
resolved "https://registry.npmjs.org/preact-render-to-string/-/preact-render-to-string-5.2.6.tgz" 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" resolved "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz"
integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ== 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: react@^19.0.0:
version "19.0.0" version "19.0.0"
resolved "https://registry.npmjs.org/react/-/react-19.0.0.tgz" resolved "https://registry.npmjs.org/react/-/react-19.0.0.tgz"
@@ -3041,13 +3052,6 @@ rimraf@^3.0.2:
dependencies: dependencies:
glob "^7.1.3" 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: run-parallel@^1.1.9:
version "1.2.0" version "1.2.0"
resolved "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz" 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" resolved "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz"
integrity sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA== 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: sqlite3@^5.1.7:
version "5.1.7" version "5.1.7"
resolved "https://registry.npmjs.org/sqlite3/-/sqlite3-5.1.7.tgz" resolved "https://registry.npmjs.org/sqlite3/-/sqlite3-5.1.7.tgz"
@@ -3295,11 +3304,6 @@ sqlite3@^5.1.7:
optionalDependencies: optionalDependencies:
node-gyp "8.x" 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: ssri@^8.0.0, ssri@^8.0.1:
version "8.0.1" version "8.0.1"
resolved "https://registry.npmjs.org/ssri/-/ssri-8.0.1.tgz" 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" resolved "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz"
integrity sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg== 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: "string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.2.3:
version "4.2.3" version "4.2.3"
resolved "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz" 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" define-properties "^1.2.1"
es-object-atoms "^1.0.0" 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: strip-ansi@^6.0.1:
version "6.0.1" version "6.0.1"
resolved "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz" 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" resolved "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz"
integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w== integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==
tailwindcss@4.0.12, tailwindcss@^4: tailwindcss@^4, tailwindcss@4.0.12:
version "4.0.12" version "4.0.12"
resolved "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.0.12.tgz" resolved "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.0.12.tgz"
integrity sha512-bT0hJo91FtncsAMSsMzUkoo/iEU0Xs5xgFgVC9XmdM9bw5MhZuQFjPNl6wxAE0SiQF/YTZJa+PndGWYSDtuxAg== 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" resolved "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.0.1.tgz"
integrity sha512-dnlgjFSVetynI8nzgJ+qF62efpglpWRk8isUEWZGWlJYySCTD6aKvbUDu+zbPeDakk3bg5H4XpitHukgfL1m9w== integrity sha512-dnlgjFSVetynI8nzgJ+qF62efpglpWRk8isUEWZGWlJYySCTD6aKvbUDu+zbPeDakk3bg5H4XpitHukgfL1m9w==
ts-custom-error@^3.0.0: ts-custom-error@^3.2.1:
version "3.3.1" version "3.3.1"
resolved "https://registry.npmjs.org/ts-custom-error/-/ts-custom-error-3.3.1.tgz" resolved "https://registry.npmjs.org/ts-custom-error/-/ts-custom-error-3.3.1.tgz"
integrity sha512-5OX1tzOjxWEgsr/YEUWSuPrQ00deKLh6D7OTWcvNHm12/7QPyRh8SYpyWvA4IZv8H/+GQWQEh/kwo95Q9OVW1A== integrity sha512-5OX1tzOjxWEgsr/YEUWSuPrQ00deKLh6D7OTWcvNHm12/7QPyRh8SYpyWvA4IZv8H/+GQWQEh/kwo95Q9OVW1A==