mirror of
https://github.com/mudabbir-ahmad/peworkshop.git
synced 2026-10-07 19:50:20 +00:00
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:
1 parent
2236dceac9
commit
726121e105
18 files changed
+1504
-1192
No files matched your search
+145
-141
@@ -5,36 +5,43 @@ import { useSession } from "next-auth/react";
|
||||
import { formatElapsedTime, getHuntTimer } from "@/utils/timerService";
|
||||
import Link from "next/link";
|
||||
import dynamic from "next/dynamic";
|
||||
import { use } from "react";
|
||||
import QRCodeHelper from "@/components/QRCodeHelper";
|
||||
|
||||
// Dynamically import QR scanner component with no SSR to avoid hydration issues
|
||||
const QRScanner = dynamic(() => import("@/components/QRScanner"), {
|
||||
ssr: false,
|
||||
});
|
||||
// Dynamically import the new QR scanner component with no SSR
|
||||
const EnhancedQRScanner = dynamic(
|
||||
() => import("@/components/EnhancedQRScanner"),
|
||||
{
|
||||
ssr: false,
|
||||
loading: () => <div className="scanner-loading">Loading scanner...</div>,
|
||||
}
|
||||
);
|
||||
|
||||
export default function CluePage({ params }) {
|
||||
// Unwrap params properly using use() from React
|
||||
const unwrappedParams = use(params);
|
||||
const clueId = unwrappedParams.clueId;
|
||||
|
||||
const [clue, setClue] = useState(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState(null);
|
||||
const [foundNow, setFoundNow] = useState(false);
|
||||
const [elapsedTime, setElapsedTime] = useState("00:00:00");
|
||||
const [showScanner, setShowScanner] = useState(false);
|
||||
const [scanResult, setScanResult] = useState(null);
|
||||
const [scanMessage, setScanMessage] = useState(null);
|
||||
const [lastRefresh, setLastRefresh] = useState(0); // Track last refresh time
|
||||
const [lastRefresh, setLastRefresh] = useState(0);
|
||||
const [clueFound, setClueFound] = useState(false);
|
||||
const [allClues, setAllClues] = useState([]);
|
||||
const [nextClueId, setNextClueId] = useState(null);
|
||||
const router = useRouter();
|
||||
const { data: session, status } = useSession(); // Get session to check if user is admin
|
||||
const { clueId } = params;
|
||||
const { data: session, status } = useSession();
|
||||
|
||||
// Check if user is admin
|
||||
const isAdmin = session?.user?.role === "admin";
|
||||
|
||||
// This effect handles authentication and initial clue data fetch
|
||||
useEffect(() => {
|
||||
let isMounted = true; // Flag to prevent state updates after unmount
|
||||
let isMounted = true;
|
||||
const now = Date.now();
|
||||
|
||||
// Only allow refreshes every 10 seconds (more for admins)
|
||||
const minRefreshInterval = isAdmin ? 30000 : 10000; // 30 seconds for admins, 10 seconds for regular users
|
||||
const minRefreshInterval = isAdmin ? 30000 : 10000;
|
||||
|
||||
// Skip if we refreshed too recently and this isn't the first load
|
||||
if (loading === false && now - lastRefresh < minRefreshInterval) {
|
||||
@@ -50,10 +57,34 @@ export default function CluePage({ params }) {
|
||||
return;
|
||||
}
|
||||
|
||||
// One-time fetch clue data with debouncing
|
||||
// Fetch all clues to get navigation context
|
||||
const fetchAllClues = async () => {
|
||||
try {
|
||||
const response = await fetch("/api/clues");
|
||||
if (!response.ok) return [];
|
||||
|
||||
const data = await response.json();
|
||||
if (isMounted) {
|
||||
setAllClues(data);
|
||||
|
||||
// Find next clue for navigation
|
||||
const currentIndex = data.findIndex((c) => c.id === parseInt(clueId));
|
||||
if (currentIndex !== -1 && currentIndex < data.length - 1) {
|
||||
setNextClueId(data[currentIndex + 1].id);
|
||||
}
|
||||
}
|
||||
return data;
|
||||
} catch (err) {
|
||||
console.error("Error fetching all clues:", err);
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
// Fetch current clue details
|
||||
async function fetchClue() {
|
||||
try {
|
||||
setLoading(true);
|
||||
await fetchAllClues();
|
||||
|
||||
// Add timestamp to avoid browser caching
|
||||
const timestamp = Date.now();
|
||||
@@ -76,16 +107,11 @@ export default function CluePage({ params }) {
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
// Only update state if the component is still mounted
|
||||
if (isMounted) {
|
||||
setClue(data);
|
||||
// Only set foundNow on initial load or if it's actually newly found
|
||||
if (loading === true) {
|
||||
setFoundNow(data.foundNow);
|
||||
}
|
||||
setClueFound(data.found);
|
||||
setLoading(false);
|
||||
setLastRefresh(Date.now()); // Update last refresh timestamp
|
||||
setLastRefresh(Date.now());
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Error fetching clue:", err);
|
||||
@@ -98,17 +124,14 @@ export default function CluePage({ params }) {
|
||||
|
||||
fetchClue();
|
||||
|
||||
// Cleanup function to prevent state updates after unmount
|
||||
return () => {
|
||||
isMounted = false;
|
||||
};
|
||||
}, [clueId, router, status, lastRefresh, loading, isAdmin]); // Add isAdmin and lastRefresh to dependencies
|
||||
}, [clueId, router, status, lastRefresh, loading, isAdmin]);
|
||||
|
||||
// Separate effect to handle timer updates - doesn't re-fetch data
|
||||
// Separate effect to handle timer updates
|
||||
useEffect(() => {
|
||||
let timerId;
|
||||
|
||||
// Set up timer display updates
|
||||
const timerData = getHuntTimer();
|
||||
if (!timerData || !timerData.isActive) {
|
||||
return;
|
||||
@@ -125,42 +148,60 @@ export default function CluePage({ params }) {
|
||||
return () => {
|
||||
if (timerId) clearInterval(timerId);
|
||||
};
|
||||
}, []); // Empty dependency array so it only runs once
|
||||
}, []);
|
||||
|
||||
// Handle QR scan result
|
||||
const handleScan = (data) => {
|
||||
setScanResult(data);
|
||||
setScanMessage(`QR Code scanned: ${data}`);
|
||||
|
||||
try {
|
||||
// Handle both "QR123" format and just "123" format
|
||||
// Handle multiple QR code formats: "QR123", "123", or even "Clue 123"
|
||||
let scannedClueId;
|
||||
const match = data.match(/^QR(\d+)$/i);
|
||||
// Try regex patterns
|
||||
const qrPattern = /^QR[^0-9]*(\d+)$/i;
|
||||
const cluePattern = /^clue[^0-9]*(\d+)$/i;
|
||||
const numberPattern = /^(\d+)$/;
|
||||
|
||||
let match =
|
||||
data.match(qrPattern) ||
|
||||
data.match(cluePattern) ||
|
||||
data.match(numberPattern);
|
||||
|
||||
if (match) {
|
||||
// Extract the number from QR[number]
|
||||
scannedClueId = parseInt(match[1], 10);
|
||||
} else {
|
||||
// Try to parse the raw value
|
||||
// Try direct parsing as a last resort
|
||||
scannedClueId = parseInt(data, 10);
|
||||
}
|
||||
|
||||
if (!isNaN(scannedClueId) && scannedClueId === parseInt(clueId)) {
|
||||
// Correct QR code scanned, mark as found and move to next clue
|
||||
// Correct QR code scanned, mark as found
|
||||
setScanMessage("✓ Correct QR code found!");
|
||||
markClueAsFound(clueId);
|
||||
} else if (!isNaN(scannedClueId)) {
|
||||
setScanMessage(
|
||||
`QR code for clue ${scannedClueId} scanned, but you need clue ${clueId}. Please scan the correct code.`
|
||||
);
|
||||
} else {
|
||||
setScanMessage(
|
||||
"Incorrect QR code. Please scan the correct code for this clue."
|
||||
"Couldn't recognize this QR code. Please scan the correct code for this clue."
|
||||
);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Error processing scan result:", err);
|
||||
setScanMessage("Invalid QR code format. Please try again.");
|
||||
}
|
||||
};
|
||||
|
||||
// Function to mark clue as found and navigate to the next clue
|
||||
// Function to mark clue as found via API
|
||||
const markClueAsFound = async (currentClueId) => {
|
||||
try {
|
||||
// Only proceed if the clue isn't already found
|
||||
if (clueFound) {
|
||||
setScanMessage("You've already found this clue!");
|
||||
return;
|
||||
}
|
||||
|
||||
// Mark the clue as found via API
|
||||
const response = await fetch("/api/mark-clue", {
|
||||
method: "POST",
|
||||
@@ -169,29 +210,11 @@ export default function CluePage({ params }) {
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
// Success message
|
||||
setScanMessage("Clue verified! Moving to the next clue...");
|
||||
|
||||
// Fetch all clues to find the next available one
|
||||
const allCluesResponse = await fetch("/api/clues");
|
||||
const allClues = await allCluesResponse.json();
|
||||
|
||||
// Find the current clue index
|
||||
const currentIndex = allClues.findIndex(
|
||||
(c) => c.id === parseInt(currentClueId)
|
||||
// Update local state to show clue is found
|
||||
setClueFound(true);
|
||||
setScanMessage(
|
||||
"✅ Clue verified! You can now proceed to the next clue."
|
||||
);
|
||||
|
||||
if (currentIndex !== -1 && currentIndex < allClues.length - 1) {
|
||||
// Navigate to the next clue after a brief delay
|
||||
setTimeout(() => {
|
||||
router.push(`/clue/${allClues[currentIndex + 1].id}`);
|
||||
}, 1500);
|
||||
} else {
|
||||
// This was the last clue
|
||||
setTimeout(() => {
|
||||
router.push("/map");
|
||||
}, 1500);
|
||||
}
|
||||
} else {
|
||||
setScanMessage("Error verifying clue. Please try again.");
|
||||
}
|
||||
@@ -201,35 +224,35 @@ export default function CluePage({ params }) {
|
||||
}
|
||||
};
|
||||
|
||||
// Function to handle skip button
|
||||
const handleSkip = async () => {
|
||||
// Fetch all clues to find the next one
|
||||
try {
|
||||
const response = await fetch("/api/clues");
|
||||
const allClues = await response.json();
|
||||
// Navigate to the next clue
|
||||
const handleNextClue = () => {
|
||||
if (nextClueId) {
|
||||
router.push(`/clue/${nextClueId}`);
|
||||
} else {
|
||||
router.push("/map");
|
||||
}
|
||||
};
|
||||
|
||||
// Find the current clue index
|
||||
// Navigate to the previous clue
|
||||
const handlePrevClue = () => {
|
||||
if (allClues.length > 0) {
|
||||
const currentIndex = allClues.findIndex((c) => c.id === parseInt(clueId));
|
||||
|
||||
if (currentIndex !== -1 && currentIndex < allClues.length - 1) {
|
||||
// Navigate to the next clue
|
||||
router.push(`/clue/${allClues[currentIndex + 1].id}`);
|
||||
if (currentIndex > 0) {
|
||||
router.push(`/clue/${allClues[currentIndex - 1].id}`);
|
||||
} else {
|
||||
// This was the last clue
|
||||
router.push("/map");
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Error skipping to next clue:", err);
|
||||
setError("Failed to skip to next clue");
|
||||
} else {
|
||||
router.push("/map");
|
||||
}
|
||||
};
|
||||
|
||||
// Function for manual refresh - for admins to test without waiting
|
||||
const handleManualRefresh = () => {
|
||||
// Force a refresh by setting lastRefresh to 0
|
||||
setLastRefresh(0);
|
||||
};
|
||||
|
||||
// Rendering states
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="clue-container">
|
||||
@@ -276,7 +299,6 @@ export default function CluePage({ params }) {
|
||||
return (
|
||||
<div className="clue-container">
|
||||
<div className="timer-display">{elapsedTime}</div>
|
||||
|
||||
<div className="clue-content-wrapper">
|
||||
{/* Admin tools section - only visible to admins */}
|
||||
{isAdmin && (
|
||||
@@ -291,29 +313,26 @@ export default function CluePage({ params }) {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* QR Scanner Section */}
|
||||
{/* QR Scanner Section - Removed manual entry button */}
|
||||
<div className="scanner-section">
|
||||
<h2>Scan QR Code</h2>
|
||||
<QRScanner onScan={handleScan} width={280} height={280} />
|
||||
{scanMessage && <div className="scan-message">{scanMessage}</div>}
|
||||
<EnhancedQRScanner onScan={handleScan} width={280} height={280} />
|
||||
{scanMessage && (
|
||||
<div className={`scan-message ${clueFound ? "success" : ""}`}>
|
||||
{scanMessage}
|
||||
</div>
|
||||
)}
|
||||
{/* Add the QR Code helper component */}
|
||||
<QRCodeHelper clueId={clueId} />
|
||||
</div>
|
||||
|
||||
{/* Clue Information Section */}
|
||||
<div className="clue-scroll">
|
||||
{foundNow && (
|
||||
<div className="new-discovery">
|
||||
<div className="discovery-icon">🎉</div>
|
||||
<div className="discovery-text">New Clue Discovered!</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<h1 className="clue-title">{clue.title}</h1>
|
||||
|
||||
<h1 className="clue-title">{clue?.title}</h1>
|
||||
<div className="clue-content">
|
||||
<h2>Description</h2>
|
||||
<p className="clue-description">{clue.description}</p>
|
||||
|
||||
{clue.location && (
|
||||
<p className="clue-description">{clue?.description}</p>
|
||||
{clue?.location && (
|
||||
<div className="clue-location">
|
||||
<h2>Location Hint</h2>
|
||||
<p>{clue.location}</p>
|
||||
@@ -322,13 +341,19 @@ export default function CluePage({ params }) {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Action Buttons */}
|
||||
<div className="action-buttons">
|
||||
<button onClick={handleSkip} className="skip-button">
|
||||
Skip
|
||||
{/* Navigation Buttons */}
|
||||
<div className="navigation-buttons">
|
||||
<button onClick={handlePrevClue} className="nav-button prev-button">
|
||||
Previous Clue
|
||||
</button>
|
||||
<button onClick={() => setShowScanner(true)} className="scan-button">
|
||||
Scan Clue
|
||||
<button
|
||||
onClick={() => router.push("/map")}
|
||||
className="nav-button map-button"
|
||||
>
|
||||
Back to Map
|
||||
</button>
|
||||
<button onClick={handleNextClue} className="nav-button next-button">
|
||||
Next Clue
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -414,6 +439,12 @@ export default function CluePage({ params }) {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.scan-message.success {
|
||||
background-color: #c8e6c9;
|
||||
color: #2e7d32;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.clue-scroll {
|
||||
background-color: #f8f3e3;
|
||||
border-radius: 10px;
|
||||
@@ -445,39 +476,46 @@ export default function CluePage({ params }) {
|
||||
border-top: 1px dashed #9c6b30;
|
||||
}
|
||||
|
||||
.action-buttons {
|
||||
.navigation-buttons {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
margin-top: 10px;
|
||||
margin-top: 20px;
|
||||
margin-bottom: 20px;
|
||||
flex-wrap: wrap; /* Allow buttons to wrap on small screens */
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.skip-button,
|
||||
.scan-button {
|
||||
padding: 12px 0;
|
||||
.nav-button {
|
||||
padding: 12px 15px;
|
||||
border: none;
|
||||
border-radius: 5px;
|
||||
cursor: pointer;
|
||||
font-weight: bold;
|
||||
transition: all 0.2s;
|
||||
width: 48%;
|
||||
font-size: 16px;
|
||||
font-size: 14px;
|
||||
flex: 1;
|
||||
min-width: 120px;
|
||||
}
|
||||
|
||||
.skip-button {
|
||||
.prev-button {
|
||||
background-color: #a98467;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.scan-button {
|
||||
.map-button {
|
||||
background-color: #607d8b;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.next-button {
|
||||
background-color: #68543c;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.skip-button:hover,
|
||||
.scan-button:hover {
|
||||
.nav-button:hover {
|
||||
opacity: 0.9;
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.loading,
|
||||
@@ -519,39 +557,6 @@ export default function CluePage({ params }) {
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
|
||||
.new-discovery {
|
||||
background-color: #feefb3;
|
||||
border: 1px solid #9f6000;
|
||||
color: #9f6000;
|
||||
padding: 15px;
|
||||
border-radius: 5px;
|
||||
margin-bottom: 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
animation: pulse 2s infinite;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0% {
|
||||
opacity: 1;
|
||||
}
|
||||
50% {
|
||||
opacity: 0.7;
|
||||
}
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.discovery-icon {
|
||||
font-size: 1.5rem;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.discovery-text {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.nav-bar {
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
@@ -583,7 +588,6 @@ export default function CluePage({ params }) {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
/* Admin tools styling */
|
||||
.admin-tools {
|
||||
background-color: #f8d7da;
|
||||
border: 2px dashed #d9534f;
|
||||
|
||||
+81
-138
@@ -64,162 +64,45 @@
|
||||
height: 100%;
|
||||
background-color: #69b5a8; /* Background color */
|
||||
overflow: auto; /* Enable scrolling */
|
||||
display: block; /* Change from flex to block for proper scrolling */
|
||||
display: block; /* Use block for proper scrolling */
|
||||
}
|
||||
|
||||
.treasure-map {
|
||||
position: relative;
|
||||
width: fit-content; /* Change from 100% to fit-content to allow natural svg size */
|
||||
width: fit-content; /* Allow natural width */
|
||||
min-width: 100%; /* Ensure it's at least full width */
|
||||
height: fit-content;
|
||||
min-height: 100%;
|
||||
display: flex; /* Keep flex for alignment */
|
||||
display: flex;
|
||||
justify-content: flex-end; /* Align to the right */
|
||||
align-items: flex-start; /* Align to the top */
|
||||
}
|
||||
|
||||
/* SVG container for better scrolling and zooming */
|
||||
.svg-container {
|
||||
/* Map image container for positioning */
|
||||
.map-image-container {
|
||||
position: relative;
|
||||
width: fit-content; /* Allow natural width */
|
||||
height: fit-content; /* Allow natural height */
|
||||
display: block; /* Change to block for proper scrolling */
|
||||
overflow: visible; /* Show content beyond boundaries */
|
||||
z-index: 5; /* Ensure it's above the background */
|
||||
width: fit-content;
|
||||
height: fit-content;
|
||||
}
|
||||
|
||||
.svg-container svg {
|
||||
max-width: none; /* Remove max-width limitation */
|
||||
/* Map image styling */
|
||||
.map-image {
|
||||
display: block;
|
||||
max-width: none; /* Don't constrain the image */
|
||||
width: auto;
|
||||
height: auto;
|
||||
display: block;
|
||||
object-fit: none; /* Don't constrain the SVG */
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* Force SVG elements to show properly with z-index */
|
||||
.svg-container svg * {
|
||||
transform-box: fill-box;
|
||||
}
|
||||
|
||||
/* Ensure the clue points overlay is always on top */
|
||||
.svg-container svg .clue-points-overlay {
|
||||
position: relative !important;
|
||||
z-index: 9999 !important;
|
||||
pointer-events: all !important;
|
||||
}
|
||||
|
||||
/* Make clue points very visible with stronger styling */
|
||||
.svg-container .clue-point {
|
||||
fill-opacity: 1 !important;
|
||||
stroke-opacity: 1 !important;
|
||||
pointer-events: all !important;
|
||||
cursor: pointer !important;
|
||||
stroke-width: 3px !important;
|
||||
r: 14px !important;
|
||||
filter: drop-shadow(0px 0px 5px rgba(0, 0, 0, 0.7)) !important;
|
||||
}
|
||||
|
||||
/* Remove the default map background when we have the processed SVG */
|
||||
.svg-container + .map-background {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Map background - only shown when SVG is loading */
|
||||
.map-background {
|
||||
/* Map overlay positioned absolutely over the map image */
|
||||
.map-overlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0; /* Align to the right */
|
||||
width: 100vw; /* Full viewport width */
|
||||
height: auto;
|
||||
background-image: url("/images/map.svg");
|
||||
background-size: auto; /* Don't resize the background */
|
||||
background-position: top right; /* Position at top right */
|
||||
background-repeat: no-repeat;
|
||||
z-index: 3; /* Lower z-index than the processed SVG */
|
||||
}
|
||||
|
||||
/* Circle elements in the SVG map - attach to a container class */
|
||||
.svg-container circle {
|
||||
cursor: pointer;
|
||||
transition: fill 0.3s ease; /* Only transition the fill color, not size */
|
||||
}
|
||||
|
||||
.svg-container circle:hover {
|
||||
fill: #ffb74d !important; /* Only change color on hover, no movement */
|
||||
}
|
||||
|
||||
/* Circle elements in the SVG map - attach to a container class */
|
||||
.svg-container circle.clue-point {
|
||||
cursor: pointer;
|
||||
transition: fill 0.3s ease, stroke-width 0.3s ease;
|
||||
pointer-events: all; /* Ensure clicks are detected */
|
||||
stroke-width: 2px;
|
||||
r: 8; /* Set a minimum radius for all clue points */
|
||||
}
|
||||
|
||||
/* Improve circle visibility and interactivity */
|
||||
.svg-container circle.clue-point {
|
||||
cursor: pointer !important;
|
||||
transition: all 0.3s ease;
|
||||
pointer-events: all !important;
|
||||
stroke-width: 2px;
|
||||
r: 12px; /* Larger radius for better visibility and easier clicks */
|
||||
z-index: 10;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* Enhance hover effect */
|
||||
.svg-container circle.clue-point:hover {
|
||||
fill: #ffb74d !important; /* Orange hover color */
|
||||
stroke: #ff6d00 !important;
|
||||
stroke-width: 4px !important;
|
||||
transform: scale(1.2);
|
||||
z-index: 20;
|
||||
filter: drop-shadow(0px 0px 4px rgba(0, 0, 0, 0.5));
|
||||
}
|
||||
|
||||
/* Invisible click areas for easier interaction */
|
||||
.svg-container circle.clue-point-click-area {
|
||||
pointer-events: all !important;
|
||||
cursor: pointer !important;
|
||||
opacity: 0.01; /* Nearly invisible but still interactive */
|
||||
z-index: 5;
|
||||
}
|
||||
|
||||
/* Create custom animation for the current clue */
|
||||
@keyframes clue-point-pulse {
|
||||
0% {
|
||||
stroke-width: 3px;
|
||||
transform: scale(1);
|
||||
filter: drop-shadow(0px 0px 2px rgba(255, 152, 0, 0.7));
|
||||
}
|
||||
50% {
|
||||
stroke-width: 5px;
|
||||
transform: scale(1.2);
|
||||
filter: drop-shadow(0px 0px 8px rgba(255, 152, 0, 0.9));
|
||||
}
|
||||
100% {
|
||||
stroke-width: 3px;
|
||||
transform: scale(1);
|
||||
filter: drop-shadow(0px 0px 2px rgba(255, 152, 0, 0.7));
|
||||
}
|
||||
}
|
||||
|
||||
.svg-container circle.clue-point:hover {
|
||||
fill: #ffb74d !important; /* Orange hover color */
|
||||
stroke: #ff6d00;
|
||||
stroke-width: 3px;
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
/* Remaining styles unchanged */
|
||||
.map-svg {
|
||||
width: 100% !important; /* Override Next.js Image default width */
|
||||
height: auto !important; /* Override Next.js Image default height */
|
||||
max-width: none; /* Allow image to grow beyond container */
|
||||
object-fit: contain;
|
||||
display: block;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
pointer-events: auto; /* Allow interaction with buttons */
|
||||
z-index: 20; /* Ensure overlay is above map */
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.loading-container {
|
||||
@@ -274,8 +157,8 @@
|
||||
box-sizing: border-box;
|
||||
z-index: 100; /* Ensure it stays on top */
|
||||
flex-shrink: 0; /* Prevent navbar from shrinking */
|
||||
margin: 0; /* Added margin property */
|
||||
height: auto; /* Added height property */
|
||||
margin: 0;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.nav-item {
|
||||
@@ -343,3 +226,63 @@
|
||||
font-size: 11px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Clue button styling */
|
||||
.clue-button {
|
||||
position: absolute;
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
border-radius: 50%;
|
||||
background-color: #9e9e9e; /* Default grey background */
|
||||
color: white;
|
||||
font-weight: bold;
|
||||
border: 3px solid #757575;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
font-size: 14px;
|
||||
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.3);
|
||||
transition: all 0.3s ease;
|
||||
z-index: 30;
|
||||
transform: translate(-50%, -50%); /* Center the button on its position */
|
||||
}
|
||||
|
||||
.clue-button:hover {
|
||||
transform: translate(-50%, -50%) scale(1.1);
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.4);
|
||||
}
|
||||
|
||||
/* Clue status styles */
|
||||
.clue-found {
|
||||
background-color: #2e7d32; /* Green for found clues */
|
||||
border-color: #1b5e20;
|
||||
}
|
||||
|
||||
.clue-available {
|
||||
background-color: #ff9800; /* Orange for available clues */
|
||||
border-color: #e65100;
|
||||
animation: pulse 1.5s infinite;
|
||||
}
|
||||
|
||||
.clue-locked {
|
||||
background-color: #9e9e9e; /* Grey for locked clues */
|
||||
border-color: #757575;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
/* Pulse animation for available clues */
|
||||
@keyframes pulse {
|
||||
0% {
|
||||
transform: translate(-50%, -50%) scale(1);
|
||||
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
50% {
|
||||
transform: translate(-50%, -50%) scale(1.1);
|
||||
box-shadow: 0 4px 8px rgba(255, 152, 0, 0.6);
|
||||
}
|
||||
100% {
|
||||
transform: translate(-50%, -50%) scale(1);
|
||||
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
}
|
||||
+146
-489
@@ -9,13 +9,8 @@ import {
|
||||
setHuntTimer,
|
||||
clearHuntTimer,
|
||||
formatElapsedTime,
|
||||
syncTimerWithServer,
|
||||
} from "@/utils/timerService";
|
||||
import {
|
||||
populateMap,
|
||||
updateCluePointStatus,
|
||||
attachClueClickHandlers,
|
||||
} from "@/utils/map-populate";
|
||||
import { addDebugOverlay, fixCluePointsRendering } from "@/utils/map-debug";
|
||||
|
||||
export default function MapPage() {
|
||||
const { data: session, status } = useSession();
|
||||
@@ -24,7 +19,6 @@ export default function MapPage() {
|
||||
const [error, setError] = useState(null);
|
||||
const [teamData, setTeamData] = useState(null);
|
||||
const [elapsedTime, setElapsedTime] = useState("00:00:00");
|
||||
const [mapSvg, setMapSvg] = useState(null);
|
||||
const [clues, setClues] = useState([]);
|
||||
const mapContainerRef = useRef(null);
|
||||
const router = useRouter();
|
||||
@@ -120,6 +114,7 @@ export default function MapPage() {
|
||||
});
|
||||
|
||||
setClues(processedClues);
|
||||
setLoading(false); // Set loading to false after clues are fetched
|
||||
return processedClues;
|
||||
} catch (err) {
|
||||
console.error("Failed to fetch clues:", err);
|
||||
@@ -128,211 +123,90 @@ export default function MapPage() {
|
||||
}
|
||||
}, []);
|
||||
|
||||
// Enhanced function to populate the map with clue points
|
||||
const enhanceSvgMap = useCallback(() => {
|
||||
if (!mapContainerRef.current) return;
|
||||
|
||||
// Find the SVG element inside the map container
|
||||
const svgElement = mapContainerRef.current.querySelector("svg");
|
||||
if (!svgElement) {
|
||||
console.error("No SVG element found in map container");
|
||||
setMapError(true);
|
||||
return;
|
||||
}
|
||||
|
||||
console.log("SVG element found, starting enhancement process");
|
||||
|
||||
// Clear out any existing map-background elements to ensure we only show one map
|
||||
const existingMapBgs = document.querySelectorAll(
|
||||
`.${styles["map-background"]}`
|
||||
);
|
||||
existingMapBgs.forEach((bg) => (bg.style.display = "none"));
|
||||
|
||||
// First populate the map with basic clue points
|
||||
populateMap(svgElement)
|
||||
.then((success) => {
|
||||
if (success) {
|
||||
console.log("Map populated with clue points successfully");
|
||||
|
||||
// Process the clues to determine which are available
|
||||
const processedClues = clues.map((clue, index, arr) => {
|
||||
// Find the first unfound clue's index
|
||||
const firstUnfoundIndex = arr.findIndex((c) => !c.found);
|
||||
|
||||
// A clue is available if:
|
||||
// 1. It's already found, OR
|
||||
// 2. It's the next unfound clue in the sequence
|
||||
const available = clue.found || index === firstUnfoundIndex;
|
||||
|
||||
return { ...clue, available };
|
||||
});
|
||||
|
||||
// Debug info about clue statuses
|
||||
processedClues.forEach((clue, index) => {
|
||||
console.log(
|
||||
`Clue ${clue.id} (index ${index}): Available: ${clue.available}, Found: ${clue.found}`
|
||||
);
|
||||
});
|
||||
|
||||
// Update the clue point appearances based on their statuses
|
||||
updateCluePointStatus(svgElement, processedClues);
|
||||
|
||||
// Define a direct navigation function that will be used for click handling
|
||||
const navigateToClue = (clueId) => {
|
||||
console.log(`Navigating to clue ${clueId}`);
|
||||
router.push(`/clue/${clueId}`);
|
||||
};
|
||||
|
||||
// Attach click handlers to all clue points and click areas
|
||||
attachClueClickHandlers(svgElement, processedClues, navigateToClue);
|
||||
} else {
|
||||
console.error("Failed to populate map with clue points");
|
||||
setMapError(true);
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error("Error enhancing SVG map:", error);
|
||||
setMapError(true);
|
||||
});
|
||||
}, [clues, router, styles]);
|
||||
|
||||
// Function to load and enhance the SVG map
|
||||
const loadAndEnhanceMap = useCallback(async () => {
|
||||
try {
|
||||
// Fetch the SVG file
|
||||
const response = await fetch("/images/map.svg");
|
||||
if (!response.ok) {
|
||||
console.error(
|
||||
"Failed to load SVG:",
|
||||
response.status,
|
||||
response.statusText
|
||||
);
|
||||
setMapError(true);
|
||||
return;
|
||||
// Handle clue button click
|
||||
const handleClueClick = useCallback(
|
||||
(clueId, isAvailable, isFound) => {
|
||||
if (isFound || isAvailable) {
|
||||
router.push(`/clue/${clueId}`);
|
||||
} else {
|
||||
alert("Complete previous clues to unlock this one!");
|
||||
}
|
||||
},
|
||||
[router]
|
||||
);
|
||||
|
||||
const svgText = await response.text();
|
||||
console.log(`SVG loaded, length: ${svgText.length} characters`);
|
||||
// Size and position the overlay to match the map image
|
||||
const setupOverlay = useCallback(() => {
|
||||
const overlay = document.getElementById("map-overlay-container");
|
||||
const mapImg = document.getElementById("treasure-map-img");
|
||||
|
||||
// Ensure SVG has correct namespaces and viewBox
|
||||
let fixedSvg = svgText.replace(
|
||||
/<svg/,
|
||||
'<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"'
|
||||
);
|
||||
if (overlay && mapImg) {
|
||||
// Wait for the image to load to get its dimensions
|
||||
mapImg.onload = () => {
|
||||
const mapRect = mapImg.getBoundingClientRect();
|
||||
overlay.style.width = `${mapRect.width}px`;
|
||||
overlay.style.height = `${mapRect.height}px`;
|
||||
};
|
||||
|
||||
// Ensure SVG has a proper viewBox attribute if missing
|
||||
if (!fixedSvg.includes('viewBox="')) {
|
||||
const widthMatch = fixedSvg.match(/width="([^"]+)"/);
|
||||
const heightMatch = fixedSvg.match(/height="([^"]+)"/);
|
||||
if (widthMatch && heightMatch) {
|
||||
const width = widthMatch[1];
|
||||
const height = heightMatch[1];
|
||||
fixedSvg = fixedSvg.replace(
|
||||
/<svg/,
|
||||
`<svg viewBox="0 0 ${width} ${height}"`
|
||||
);
|
||||
}
|
||||
}
|
||||
// Handle resize events
|
||||
const resizeOverlay = () => {
|
||||
const mapRect = mapImg.getBoundingClientRect();
|
||||
overlay.style.width = `${mapRect.width}px`;
|
||||
overlay.style.height = `${mapRect.height}px`;
|
||||
};
|
||||
|
||||
setMapSvg(fixedSvg);
|
||||
setLoading(false);
|
||||
} catch (error) {
|
||||
console.error("Failed to load map SVG:", error);
|
||||
setMapError(true);
|
||||
setLoading(false);
|
||||
// Initial resize and listen for window resize
|
||||
setTimeout(resizeOverlay, 300);
|
||||
window.addEventListener("resize", resizeOverlay);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener("resize", resizeOverlay);
|
||||
};
|
||||
}
|
||||
}, []);
|
||||
|
||||
// Add this useEffect to coordinate loading the map and clues
|
||||
useEffect(() => {
|
||||
if (status === "authenticated") {
|
||||
fetchClues();
|
||||
fetchTeamData();
|
||||
} else if (status === "unauthenticated") {
|
||||
softRedirect("/login");
|
||||
}
|
||||
|
||||
// Setup cleanup function for component unmount
|
||||
return () => {
|
||||
isMounted.current = false;
|
||||
};
|
||||
}, [status, fetchClues, fetchTeamData, softRedirect]);
|
||||
|
||||
// Setup the overlay when the component is mounted
|
||||
useEffect(() => {
|
||||
if (!loading && clues.length > 0) {
|
||||
return setupOverlay();
|
||||
}
|
||||
}, [loading, clues.length, setupOverlay]);
|
||||
|
||||
// Function to check hunt status and timer
|
||||
async function checkHuntAndTimer() {
|
||||
if (!isMounted.current) return;
|
||||
|
||||
try {
|
||||
const response = await fetch("/api/hunt-status", {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"Cache-Control": "no-cache, no-store, must-revalidate",
|
||||
Pragma: "no-cache",
|
||||
},
|
||||
credentials: "include",
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
console.log("Hunt status check from map:", data);
|
||||
setHuntActive(data.active);
|
||||
|
||||
// If hunt is not active, check if it has ended
|
||||
if (!data.active) {
|
||||
console.log("Hunt not active, checking if it ended...");
|
||||
|
||||
// Check if we had an active hunt before
|
||||
const timerData = getHuntTimer();
|
||||
|
||||
if (timerData && timerData.isActive) {
|
||||
// Hunt was active before but not now - it has ended
|
||||
console.log("Hunt has ended, redirecting to leaderboard");
|
||||
clearHuntTimer(); // Clean up all timer data
|
||||
|
||||
// Show a brief message before redirecting
|
||||
const huntEndedMessage = document.createElement("div");
|
||||
huntEndedMessage.innerHTML = `
|
||||
<div style="position:fixed; top:0; left:0; right:0; bottom:0; background:rgba(0,0,0,0.8); color:white; display:flex; flex-direction:column; align-items:center; justify-content:center; z-index:9999">
|
||||
<div style="font-size:32px; margin-bottom:20px">🎯 Hunt Complete!</div>
|
||||
<div style="font-size:24px; margin-bottom:20px">The treasure hunt has ended.</div>
|
||||
<div style="font-size:18px">Redirecting to results...</div>
|
||||
</div>
|
||||
`;
|
||||
document.body.appendChild(huntEndedMessage);
|
||||
|
||||
// Use the appropriate redirect method after a short delay
|
||||
setTimeout(() => {
|
||||
if (isMounted.current) {
|
||||
hardRedirect("/leaderboard?huntEnded=true");
|
||||
}
|
||||
}, 2000);
|
||||
return;
|
||||
}
|
||||
|
||||
// Otherwise just show the waiting page
|
||||
if (isMounted.current) {
|
||||
setHuntActive(false);
|
||||
softRedirect("/waiting");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// If timer endTime exists and has passed, redirect to leaderboard
|
||||
if (data.endTime && new Date(data.endTime) <= new Date()) {
|
||||
console.log("Timer expired, redirecting to leaderboard");
|
||||
clearHuntTimer(); // Clean up all timer data
|
||||
|
||||
// Show a brief message before redirecting
|
||||
const timerEndedMessage = document.createElement("div");
|
||||
timerEndedMessage.innerHTML = `
|
||||
<div style="position:fixed; top:0; left:0; right:0; bottom:0; background:rgba(0,0,0,0.8); color:white; display:flex; flex-direction:column; align-items:center; justify-content:center; z-index:9999">
|
||||
<div style="font-size:32px; margin-bottom:20px">⏰ Time's Up!</div>
|
||||
<div style="font-size:24px; margin-bottom:20px">The treasure hunt has ended.</div>
|
||||
<div style="font-size:18px">Redirecting to results...</div>
|
||||
</div>
|
||||
`;
|
||||
document.body.appendChild(timerEndedMessage);
|
||||
|
||||
if (isMounted.current) {
|
||||
setTimeout(() => {
|
||||
hardRedirect("/leaderboard?huntEnded=true");
|
||||
}, 2000);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Otherwise, hunt is active and timer is running
|
||||
if (isMounted.current) {
|
||||
setHuntActive(true);
|
||||
|
||||
// Update hunt timer data with the server's values
|
||||
if (data.startTime) {
|
||||
setHuntTimer(data.startTime, data.endTime);
|
||||
}
|
||||
// If hunt is active, ensure we have timer data
|
||||
if (data.active && data.startTime) {
|
||||
setHuntTimer(data.startTime);
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
@@ -343,298 +217,65 @@ export default function MapPage() {
|
||||
// Add this function before it's used in initializeTimer
|
||||
async function fetchTeamHuntStartTime() {
|
||||
try {
|
||||
// First get the user's team ID
|
||||
const userResponse = await fetch("/api/user", {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Cache-Control": "no-store, no-cache, must-revalidate",
|
||||
Pragma: "no-cache",
|
||||
},
|
||||
credentials: "include",
|
||||
});
|
||||
|
||||
if (!userResponse.ok) {
|
||||
console.error("Failed to fetch user data:", userResponse.statusText);
|
||||
return null;
|
||||
// First try to get any existing timer data
|
||||
const timerData = getHuntTimer();
|
||||
if (timerData && timerData.startTime) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const userData = await userResponse.json();
|
||||
const teamId = userData.teamId;
|
||||
|
||||
if (!teamId) {
|
||||
console.error("User does not belong to a team");
|
||||
return null;
|
||||
}
|
||||
|
||||
// Now fetch the team's hunt session using the team ID
|
||||
const teamHuntResponse = await fetch(
|
||||
`/api/teams/${teamId}/hunt-session`,
|
||||
{
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Cache-Control": "no-store, no-cache, must-revalidate",
|
||||
Pragma: "no-cache",
|
||||
},
|
||||
credentials: "include",
|
||||
}
|
||||
);
|
||||
|
||||
if (!teamHuntResponse.ok) {
|
||||
console.error(
|
||||
"Failed to fetch team hunt session:",
|
||||
teamHuntResponse.statusText
|
||||
);
|
||||
|
||||
// Try the global hunt timer as fallback
|
||||
const globalResponse = await fetch("/api/hunt/timer", {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Cache-Control": "no-store, no-cache, must-revalidate",
|
||||
Pragma: "no-cache",
|
||||
},
|
||||
credentials: "include",
|
||||
});
|
||||
|
||||
if (globalResponse.ok) {
|
||||
const globalData = await globalResponse.json();
|
||||
return globalData.startTime || null;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
const teamHuntData = await teamHuntResponse.json();
|
||||
return teamHuntData.startTime || null;
|
||||
} catch (error) {
|
||||
console.error("Error fetching team hunt start time:", error);
|
||||
return null;
|
||||
// If no timer data exists, sync with server
|
||||
const result = await syncTimerWithServer();
|
||||
return result.success;
|
||||
} catch (err) {
|
||||
console.error("Error fetching team hunt start time:", err);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Use team hunt start time for elapsed time calculation
|
||||
useEffect(() => {
|
||||
// Initialize the timer without waiting for teamData
|
||||
// This ensures timer starts as soon as possible
|
||||
const initializeTimer = async () => {
|
||||
try {
|
||||
// First try to get timer data from the centralized service
|
||||
let timerData = getHuntTimer();
|
||||
let timerId;
|
||||
|
||||
// If no timer data yet, fetch it from API
|
||||
if (!timerData || !timerData.startTime) {
|
||||
console.log("No timer data found, fetching from API...");
|
||||
const startTimeStr = await fetchTeamHuntStartTime();
|
||||
console.log("Got start time:", startTimeStr);
|
||||
// Initialize timer
|
||||
async function initializeTimer() {
|
||||
// First ensure we have timer data
|
||||
await fetchTeamHuntStartTime();
|
||||
|
||||
if (startTimeStr) {
|
||||
setHuntTimer(startTimeStr);
|
||||
timerData = getHuntTimer();
|
||||
}
|
||||
}
|
||||
|
||||
// Set up interval for timer updates
|
||||
const timerInterval = setInterval(() => {
|
||||
if (!isMounted.current) {
|
||||
clearInterval(timerInterval);
|
||||
return;
|
||||
}
|
||||
|
||||
const currentTimerData = getHuntTimer();
|
||||
if (currentTimerData && currentTimerData.startTime) {
|
||||
const formattedTime = formatElapsedTime(currentTimerData.startTime);
|
||||
console.log("Updated timer:", formattedTime);
|
||||
setElapsedTime(formattedTime);
|
||||
} else {
|
||||
setElapsedTime("00:00:00");
|
||||
}
|
||||
}, 1000);
|
||||
|
||||
// Initial timer update
|
||||
// Set up timer updates
|
||||
const updateTimer = () => {
|
||||
const timerData = getHuntTimer();
|
||||
if (timerData && timerData.startTime) {
|
||||
setElapsedTime(formatElapsedTime(timerData.startTime));
|
||||
}
|
||||
|
||||
return () => {
|
||||
clearInterval(timerInterval);
|
||||
};
|
||||
} catch (error) {
|
||||
console.error("Error initializing timer:", error);
|
||||
}
|
||||
};
|
||||
|
||||
initializeTimer();
|
||||
}, []); // Empty dependency array since fetchTeamHuntStartTime is defined inside the component
|
||||
|
||||
// Check hunt status every second to ensure immediate updates
|
||||
useEffect(() => {
|
||||
const checkHuntStatus = async () => {
|
||||
if (!isMounted.current) return;
|
||||
|
||||
try {
|
||||
const timestamp = Date.now(); // Add cache-busting timestamp
|
||||
const response = await fetch(`/api/hunt-status?t=${timestamp}`, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"Cache-Control": "no-store, no-cache, must-revalidate",
|
||||
Pragma: "no-cache",
|
||||
},
|
||||
credentials: "include",
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
console.error("Failed to fetch hunt status:", response.statusText);
|
||||
return;
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
console.log("Map page hunt status check:", data);
|
||||
|
||||
// If hunt is not active, check if it was active before (hunt ended)
|
||||
if (!data.active) {
|
||||
// Check if we had an active hunt before
|
||||
const timerData = getHuntTimer();
|
||||
|
||||
if (timerData && timerData.isActive) {
|
||||
// Hunt was active before but not now - it has ended
|
||||
console.log("Hunt has ended, redirecting to leaderboard");
|
||||
clearHuntTimer();
|
||||
|
||||
if (isMounted.current) {
|
||||
hardRedirect("/leaderboard?huntEnded=true");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// If hunt was never active, redirect to waiting page
|
||||
console.log("Hunt not active, redirecting to waiting page");
|
||||
if (isMounted.current) {
|
||||
softRedirect("/waiting");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// If timer endTime exists and has passed, redirect to leaderboard
|
||||
if (data.endTime && new Date(data.endTime) <= new Date()) {
|
||||
console.log("Timer expired, redirecting to leaderboard");
|
||||
if (isMounted.current) {
|
||||
clearHuntTimer();
|
||||
hardRedirect("/leaderboard?huntEnded=true");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Otherwise, hunt is active and timer is running
|
||||
if (isMounted.current) {
|
||||
setHuntActive(true);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Failed to check hunt status:", err);
|
||||
}
|
||||
};
|
||||
|
||||
// Check immediately and every second
|
||||
checkHuntStatus();
|
||||
const intervalId = setInterval(checkHuntStatus, 1000);
|
||||
|
||||
return () => clearInterval(intervalId);
|
||||
}, [softRedirect, hardRedirect]);
|
||||
|
||||
// Add this useEffect to coordinate loading the map and clues
|
||||
useEffect(() => {
|
||||
// Only proceed if authenticated
|
||||
if (status === "authenticated") {
|
||||
const initMap = async () => {
|
||||
try {
|
||||
// Set loading state
|
||||
setLoading(true);
|
||||
|
||||
// Load both map and clues in parallel
|
||||
await Promise.all([fetchClues(), loadAndEnhanceMap()]);
|
||||
|
||||
// Data is loaded, we'll let the next effect handle enhancement
|
||||
} catch (error) {
|
||||
console.error("Error initializing map data:", error);
|
||||
setError("Failed to load map data");
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
initMap();
|
||||
} else if (status === "unauthenticated") {
|
||||
router.push("/login");
|
||||
}
|
||||
}, [status, fetchClues, loadAndEnhanceMap, router]);
|
||||
// Initial update
|
||||
updateTimer();
|
||||
|
||||
// Modified useEffect to ensure SVG elements are visible
|
||||
// Update every second
|
||||
timerId = setInterval(updateTimer, 1000);
|
||||
}
|
||||
|
||||
initializeTimer();
|
||||
|
||||
// Clean up interval on unmount
|
||||
return () => {
|
||||
if (timerId) clearInterval(timerId);
|
||||
};
|
||||
}, []);
|
||||
|
||||
// Check hunt status every 10 seconds to ensure immediate updates
|
||||
useEffect(() => {
|
||||
// Only try to enhance the map if both the SVG and clues are loaded
|
||||
if (!mapSvg || !clues.length) return;
|
||||
// Initial check
|
||||
checkHuntAndTimer();
|
||||
|
||||
console.log("Both SVG and clues are loaded, preparing to enhance map");
|
||||
// Set up interval to check hunt status
|
||||
const huntStatusInterval = setInterval(checkHuntAndTimer, 10000);
|
||||
|
||||
let enhancementAttempts = 0;
|
||||
const maxAttempts = 3;
|
||||
|
||||
function attemptMapEnhancement() {
|
||||
enhancementAttempts++;
|
||||
console.log(
|
||||
`Map enhancement attempt ${enhancementAttempts}/${maxAttempts}`
|
||||
);
|
||||
|
||||
if (
|
||||
mapContainerRef.current &&
|
||||
mapContainerRef.current.querySelector("svg")
|
||||
) {
|
||||
const svgElement = mapContainerRef.current.querySelector("svg");
|
||||
console.log("SVG found in DOM, enhancing map now");
|
||||
|
||||
// Try to enhance the map
|
||||
enhanceSvgMap();
|
||||
|
||||
// Additional check after brief delay to ensure visibility
|
||||
setTimeout(() => {
|
||||
const cluePoints = svgElement.querySelectorAll(".clue-point");
|
||||
console.log(
|
||||
`Found ${cluePoints.length} clue points after enhancement`
|
||||
);
|
||||
|
||||
if (cluePoints.length === 0 && enhancementAttempts < maxAttempts) {
|
||||
console.log(
|
||||
`No clue points found, retrying (attempt ${enhancementAttempts}/${maxAttempts})`
|
||||
);
|
||||
attemptMapEnhancement();
|
||||
} else if (cluePoints.length === 0) {
|
||||
console.error(
|
||||
`Failed to create clue points after ${maxAttempts} attempts`
|
||||
);
|
||||
// Fix rendering issues as a last resort
|
||||
fixCluePointsRendering(svgElement);
|
||||
} else {
|
||||
console.log(
|
||||
"Map enhancement successful, fixing any rendering issues"
|
||||
);
|
||||
fixCluePointsRendering(svgElement);
|
||||
}
|
||||
}, 300);
|
||||
} else {
|
||||
console.error("SVG not found in DOM after rendering timeout");
|
||||
|
||||
if (enhancementAttempts < maxAttempts) {
|
||||
// Try again after a delay
|
||||
setTimeout(attemptMapEnhancement, 500);
|
||||
} else {
|
||||
setMapError(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure the SVG is fully loaded in the DOM before enhancing
|
||||
requestAnimationFrame(() => {
|
||||
// Initial delay before first attempt
|
||||
setTimeout(attemptMapEnhancement, 300);
|
||||
});
|
||||
}, [mapSvg, clues, enhanceSvgMap]);
|
||||
return () => {
|
||||
clearInterval(huntStatusInterval);
|
||||
};
|
||||
}, [softRedirect, hardRedirect]);
|
||||
|
||||
// Add useEffect to load team data
|
||||
useEffect(() => {
|
||||
@@ -680,11 +321,22 @@ export default function MapPage() {
|
||||
<div className={styles["map-error"]}>
|
||||
<h2>Error Loading Map</h2>
|
||||
<p>We couldn't load the treasure map. Please try again.</p>
|
||||
<button onClick={loadAndEnhanceMap}>Retry</button>
|
||||
<button onClick={fetchClues}>Retry</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Function to determine clue button class based on status
|
||||
const getClueButtonClass = (clue) => {
|
||||
if (clue.found) return `${styles["clue-button"]} ${styles["clue-found"]}`;
|
||||
if (clue.available)
|
||||
return `${styles["clue-button"]} ${styles["clue-available"]}`;
|
||||
return `${styles["clue-button"]} ${styles["clue-locked"]}`;
|
||||
};
|
||||
|
||||
// Get the first clue if it exists
|
||||
const firstClue = clues.length > 0 ? clues[0] : null;
|
||||
|
||||
return (
|
||||
<div className={styles["map-page"]}>
|
||||
{/* Map Banner with team info and timer */}
|
||||
@@ -701,36 +353,41 @@ export default function MapPage() {
|
||||
{/* Main map content area */}
|
||||
<div className={styles["map-container"]} ref={mapContainerRef}>
|
||||
<div className={styles["treasure-map"]}>
|
||||
{mapSvg ? (
|
||||
<div
|
||||
className={styles["svg-container"]}
|
||||
dangerouslySetInnerHTML={{ __html: mapSvg }}
|
||||
{/* Simpler map implementation using an img element */}
|
||||
<div className={styles["map-image-container"]}>
|
||||
<img
|
||||
id="treasure-map-img"
|
||||
src="/images/map.svg"
|
||||
alt="Treasure Map"
|
||||
className={styles["map-image"]}
|
||||
/>
|
||||
) : (
|
||||
<div className={styles["loading-container"]}>
|
||||
<div className={styles["loading-spinner"]}></div>
|
||||
<div>Loading map...</div>
|
||||
|
||||
{/* Overlay div with the clue button - positioned exactly over the map */}
|
||||
<div id="map-overlay-container" className={styles["map-overlay"]}>
|
||||
{/* Hardcoded Clue 1 button */}
|
||||
{firstClue && (
|
||||
<button
|
||||
className={getClueButtonClass(firstClue)}
|
||||
onClick={() =>
|
||||
handleClueClick(
|
||||
firstClue.id,
|
||||
firstClue.available,
|
||||
firstClue.found
|
||||
)
|
||||
}
|
||||
style={{
|
||||
left: "150px",
|
||||
top: "200px",
|
||||
}}
|
||||
>
|
||||
Clue {firstClue.id}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Add a refresh button for users if they have issues with the map */}
|
||||
{mapSvg && (
|
||||
<button
|
||||
className={styles["refresh-map-button"]}
|
||||
onClick={() => {
|
||||
const svgElement = mapContainerRef.current?.querySelector("svg");
|
||||
if (svgElement) {
|
||||
enhanceSvgMap();
|
||||
setTimeout(() => fixCluePointsRendering(svgElement), 300);
|
||||
}
|
||||
}}
|
||||
>
|
||||
Refresh Map
|
||||
</button>
|
||||
)}
|
||||
|
||||
{/* Navigation bar */}
|
||||
<nav className={styles["nav-bar"]}>
|
||||
<Link
|
||||
|
||||
@@ -24,6 +24,7 @@ export async function GET(request, context) {
|
||||
);
|
||||
}
|
||||
|
||||
// Get the clueId directly from the params
|
||||
const { clueId } = context.params;
|
||||
|
||||
// Validate clueId is a number
|
||||
@@ -31,7 +32,7 @@ export async function GET(request, context) {
|
||||
return Response.json({ error: "Invalid clue ID" }, { status: 400 });
|
||||
}
|
||||
|
||||
// Get the specific clue - removed coordinates
|
||||
// Get the specific clue
|
||||
const clue = await queryOne(
|
||||
"SELECT id, title, description, location FROM clues WHERE id = ?",
|
||||
[clueId]
|
||||
@@ -41,54 +42,12 @@ export async function GET(request, context) {
|
||||
return Response.json({ error: "Clue not found" }, { status: 404 });
|
||||
}
|
||||
|
||||
// Get timestamp parameter to determine if this is a refresh call
|
||||
const timestamp = request.nextUrl?.searchParams?.get("t");
|
||||
const isRefresh = timestamp && !isNaN(parseInt(timestamp));
|
||||
|
||||
// Check if this team has found this clue before
|
||||
const found = await queryOne(
|
||||
"SELECT * FROM team_clues WHERE team_id = ? AND clue_id = ?",
|
||||
[token.teamId, clueId]
|
||||
);
|
||||
|
||||
// Only mark as "foundNow" if:
|
||||
// 1. The clue was not found before
|
||||
// 2. This is not a refresh request (or we can't determine)
|
||||
// 3. The user is not an admin (admins don't need "foundNow" animations)
|
||||
let foundNow = false;
|
||||
|
||||
if (!found && (!isRefresh || token.role !== "admin")) {
|
||||
try {
|
||||
// First ensure the team_clues table exists
|
||||
await queryOne(`
|
||||
CREATE TABLE IF NOT EXISTS team_clues (
|
||||
team_id INTEGER NOT NULL,
|
||||
clue_id INTEGER NOT NULL,
|
||||
found_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (team_id, clue_id),
|
||||
FOREIGN KEY (team_id) REFERENCES teams(id),
|
||||
FOREIGN KEY (clue_id) REFERENCES clues(id)
|
||||
)
|
||||
`);
|
||||
|
||||
// Record that the team found this clue
|
||||
await queryOne(
|
||||
"INSERT INTO team_clues (team_id, clue_id) VALUES (?, ?)",
|
||||
[token.teamId, clueId]
|
||||
);
|
||||
|
||||
// Set foundNow flag for non-admin users only
|
||||
foundNow = token.role !== "admin";
|
||||
|
||||
console.log(
|
||||
`Clue ${clueId} marked as found for team ${token.teamId}, foundNow=${foundNow}`
|
||||
);
|
||||
} catch (err) {
|
||||
console.error("Error marking clue as found:", err);
|
||||
// Continue even if there's an error recording the find
|
||||
}
|
||||
}
|
||||
|
||||
// Add different cache headers for admins vs regular users
|
||||
const cacheHeaders =
|
||||
token.role === "admin"
|
||||
@@ -106,8 +65,7 @@ export async function GET(request, context) {
|
||||
return Response.json(
|
||||
{
|
||||
...clue,
|
||||
found: true, // Always return as found since the user is viewing it
|
||||
foundNow, // Only true during initial discovery for non-admins
|
||||
found: !!found, // Only mark as found if it was previously found
|
||||
isAdmin: token.role === "admin", // Add flag so client knows user is admin
|
||||
},
|
||||
{ headers: cacheHeaders }
|
||||
|
||||
+29
-22
@@ -1,5 +1,5 @@
|
||||
import { query, queryOne } from "@/lib/db";
|
||||
import { getToken } from "next-auth/jwt";
|
||||
import { queryOne, run } from "@/lib/db";
|
||||
|
||||
export const dynamic = "force-dynamic"; // Disable caching
|
||||
|
||||
@@ -24,15 +24,36 @@ export async function POST(request) {
|
||||
);
|
||||
}
|
||||
|
||||
// Parse request body
|
||||
const { clueId } = await request.json();
|
||||
|
||||
// Validate clueId is a number
|
||||
if (!clueId || isNaN(parseInt(clueId))) {
|
||||
if (isNaN(parseInt(clueId))) {
|
||||
return Response.json({ error: "Invalid clue ID" }, { status: 400 });
|
||||
}
|
||||
|
||||
// Ensure the team_clues table exists
|
||||
await run(`
|
||||
// Check if clue exists
|
||||
const clue = await queryOne("SELECT id FROM clues WHERE id = ?", [clueId]);
|
||||
if (!clue) {
|
||||
return Response.json({ error: "Clue not found" }, { status: 404 });
|
||||
}
|
||||
|
||||
// Check if clue was already found
|
||||
const alreadyFound = await queryOne(
|
||||
"SELECT * FROM team_clues WHERE team_id = ? AND clue_id = ?",
|
||||
[token.teamId, clueId]
|
||||
);
|
||||
|
||||
if (alreadyFound) {
|
||||
return Response.json({
|
||||
success: true,
|
||||
message: "This clue was already marked as found",
|
||||
alreadyFound: true,
|
||||
});
|
||||
}
|
||||
|
||||
// Create team_clues table if it doesn't exist
|
||||
await queryOne(`
|
||||
CREATE TABLE IF NOT EXISTS team_clues (
|
||||
team_id INTEGER NOT NULL,
|
||||
clue_id INTEGER NOT NULL,
|
||||
@@ -43,32 +64,18 @@ export async function POST(request) {
|
||||
)
|
||||
`);
|
||||
|
||||
// Check if this clue is already found
|
||||
const found = await queryOne(
|
||||
"SELECT 1 FROM team_clues WHERE team_id = ? AND clue_id = ?",
|
||||
[token.teamId, clueId]
|
||||
);
|
||||
|
||||
// If already found, return success with a message
|
||||
if (found) {
|
||||
return Response.json({
|
||||
success: true,
|
||||
message: "This clue was already found",
|
||||
alreadyFound: true,
|
||||
});
|
||||
}
|
||||
|
||||
// Record that the team found this clue
|
||||
await run("INSERT INTO team_clues (team_id, clue_id) VALUES (?, ?)", [
|
||||
await queryOne("INSERT INTO team_clues (team_id, clue_id) VALUES (?, ?)", [
|
||||
token.teamId,
|
||||
clueId,
|
||||
]);
|
||||
|
||||
console.log(`Clue ${clueId} marked as found for team ${token.teamId}`);
|
||||
|
||||
// Return successful response
|
||||
return Response.json({
|
||||
success: true,
|
||||
message: "Clue marked as found successfully",
|
||||
clueId: clueId,
|
||||
teamId: token.teamId,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Error marking clue as found:", error);
|
||||
|
||||
Binary file not shown.
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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
@@ -1,71 +1,138 @@
|
||||
"use client";
|
||||
import { useState, useRef, useEffect } from "react";
|
||||
import { Html5QrcodeScanner } from "html5-qrcode";
|
||||
import { useState, useEffect } from "react";
|
||||
import { Scanner } from "@yudiel/react-qr-scanner";
|
||||
|
||||
export default function QRScanner({ onScan, width = 300, height = 300 }) {
|
||||
const [scanning, setScanning] = useState(false);
|
||||
const [error, setError] = useState(null);
|
||||
const scannerRef = useRef(null);
|
||||
const [isSecureContext, setIsSecureContext] = useState(true);
|
||||
const [initialized, setInitialized] = useState(false);
|
||||
|
||||
// Check if we're in a secure context
|
||||
useEffect(() => {
|
||||
// Initialize scanner
|
||||
if (!scanning && typeof window !== "undefined") {
|
||||
// Check if we're in a secure context (https or localhost)
|
||||
const secure = window.isSecureContext;
|
||||
setIsSecureContext(secure);
|
||||
|
||||
// Check if camera can be accessed
|
||||
async function checkCameraAccess() {
|
||||
try {
|
||||
const qrScanner = new Html5QrcodeScanner(
|
||||
"qr-reader",
|
||||
{
|
||||
fps: 10,
|
||||
qrbox: { width: width * 0.7, height: height * 0.7 },
|
||||
aspectRatio: 1,
|
||||
disableFlip: false,
|
||||
formatsToSupport: ["QR_CODE"],
|
||||
},
|
||||
false // don't start scanning immediately
|
||||
const devices = await navigator.mediaDevices.enumerateDevices();
|
||||
const videoDevices = devices.filter(
|
||||
(device) => device.kind === "videoinput"
|
||||
);
|
||||
|
||||
// Store the scanner instance
|
||||
scannerRef.current = qrScanner;
|
||||
|
||||
// If onScan is provided, set up the success callback
|
||||
if (onScan) {
|
||||
qrScanner.render(
|
||||
(decodedText) => {
|
||||
if (onScan) {
|
||||
onScan(decodedText);
|
||||
}
|
||||
setScanning(false);
|
||||
},
|
||||
(errorMessage) => {
|
||||
// No need to show decode errors to user as they're expected during scanning
|
||||
console.log(errorMessage);
|
||||
}
|
||||
if (videoDevices.length === 0) {
|
||||
setError(
|
||||
"No camera detected. Please make sure your device has a camera."
|
||||
);
|
||||
setScanning(true);
|
||||
}
|
||||
|
||||
setInitialized(true);
|
||||
} catch (err) {
|
||||
console.error("QR Scanner initialization error:", err);
|
||||
setError(
|
||||
"Could not initialize QR scanner. Please make sure you have camera access."
|
||||
);
|
||||
console.error("Error checking camera devices:", err);
|
||||
setError(`Camera initialization error: ${err.message}`);
|
||||
setInitialized(true);
|
||||
}
|
||||
}
|
||||
|
||||
// Cleanup function
|
||||
return () => {
|
||||
if (scannerRef.current) {
|
||||
try {
|
||||
scannerRef.current.clear();
|
||||
} catch (err) {
|
||||
console.error("Failed to clear QR scanner:", err);
|
||||
}
|
||||
}
|
||||
};
|
||||
}, [onScan, width, height, scanning]);
|
||||
checkCameraAccess();
|
||||
}, []);
|
||||
|
||||
const handleDecode = (result) => {
|
||||
if (onScan && result) {
|
||||
onScan(result);
|
||||
}
|
||||
};
|
||||
|
||||
const handleError = (error) => {
|
||||
console.error("QR Scanner error:", error);
|
||||
|
||||
// More descriptive error messages
|
||||
let errorMessage = "Camera access error. ";
|
||||
|
||||
if (!isSecureContext) {
|
||||
errorMessage +=
|
||||
"Your browser requires HTTPS for camera access. When running locally, use http://localhost instead of your IP address.";
|
||||
} else if (
|
||||
error.name === "NotAllowedError" ||
|
||||
error.name === "PermissionDeniedError"
|
||||
) {
|
||||
errorMessage +=
|
||||
"Camera permission denied. Please allow camera access in your browser settings.";
|
||||
} else if (
|
||||
error.name === "NotFoundError" ||
|
||||
error.name === "DevicesNotFoundError"
|
||||
) {
|
||||
errorMessage +=
|
||||
"No camera found. Please ensure your device has a working camera.";
|
||||
} else if (
|
||||
error.name === "NotReadableError" ||
|
||||
error.name === "TrackStartError"
|
||||
) {
|
||||
errorMessage +=
|
||||
"Camera is already in use by another application or encountered a hardware error.";
|
||||
} else {
|
||||
errorMessage += `${
|
||||
error.message ||
|
||||
"Please check your camera permissions and refresh the page."
|
||||
}`;
|
||||
}
|
||||
|
||||
setError(errorMessage);
|
||||
};
|
||||
|
||||
if (!isSecureContext) {
|
||||
return (
|
||||
<div className="qr-scanner">
|
||||
<div className="qr-error">
|
||||
<p>Camera access requires a secure context (HTTPS or localhost).</p>
|
||||
<p>
|
||||
When running locally, use http://localhost:3000 instead of your
|
||||
network IP.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!initialized) {
|
||||
return (
|
||||
<div className="qr-scanner">
|
||||
<div className="scanner-loading">Initializing camera...</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="qr-scanner">
|
||||
{error && <div className="qr-error">{error}</div>}
|
||||
<div id="qr-reader" style={{ width, height }}></div>
|
||||
{error && (
|
||||
<div className="qr-error">
|
||||
<p>{error}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="scanner-container" style={{ width, height }}>
|
||||
<Scanner
|
||||
onResult={handleDecode}
|
||||
onError={handleError}
|
||||
constraints={{
|
||||
facingMode: "environment",
|
||||
width: { min: 640, ideal: 1280, max: 1920 },
|
||||
height: { min: 480, ideal: 720, max: 1080 },
|
||||
}}
|
||||
containerStyle={{
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
borderRadius: "8px",
|
||||
overflow: "hidden",
|
||||
}}
|
||||
videoStyle={{
|
||||
objectFit: "cover",
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<style jsx>{`
|
||||
.qr-scanner {
|
||||
@@ -85,16 +152,24 @@ export default function QRScanner({ onScan, width = 300, height = 300 }) {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
#qr-reader {
|
||||
border: 2px solid #ddd;
|
||||
.scanner-loading {
|
||||
text-align: center;
|
||||
padding: 12px;
|
||||
background-color: #e8f5e9;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
background-color: #f5f5f5;
|
||||
width: 100%;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
#qr-reader video {
|
||||
object-fit: cover;
|
||||
border-radius: 6px;
|
||||
.scanner-container {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
max-width: ${width}px;
|
||||
height: ${height}px;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
border: 2px solid #ddd;
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
`}</style>
|
||||
</div>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
Generated
+33
-62
@@ -8,15 +8,19 @@
|
||||
"name": "clue-hunt-app",
|
||||
"version": "0.1.0",
|
||||
"dependencies": {
|
||||
"@panva/hkdf": "^1.1.1",
|
||||
"@yudiel/react-qr-scanner": "^1.2.4",
|
||||
"bcryptjs": "^3.0.2",
|
||||
"better-sqlite3": "^11.8.1",
|
||||
"clue-hunt-app": "file:",
|
||||
"html5-qrcode": "^2.3.8",
|
||||
"jose": "^4.14.4",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"next": "15.2.2",
|
||||
"next-auth": "^4.24.11",
|
||||
"qrcode.react": "^4.2.0",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"react-qr-reader": "^3.0.0-beta-1",
|
||||
"sqlite": "^5.1.1",
|
||||
"sqlite3": "^5.1.7"
|
||||
},
|
||||
@@ -1429,25 +1433,26 @@
|
||||
"url": "https://opencollective.com/typescript-eslint"
|
||||
}
|
||||
},
|
||||
"node_modules/@zxing/browser": {
|
||||
"version": "0.0.7",
|
||||
"resolved": "https://registry.npmjs.org/@zxing/browser/-/browser-0.0.7.tgz",
|
||||
"integrity": "sha512-AepzMgDnD6EjxewqmXpHJsi4S3Gw9ilZJLIbTf6fWuWySEcHBodnGu3p7FWlgq1Sd5QyfPhTum5z3CBkkhMVng==",
|
||||
"license": "MIT",
|
||||
"optionalDependencies": {
|
||||
"@zxing/text-encoding": "^0.9.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@zxing/library": "^0.18.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@zxing/library": {
|
||||
"version": "0.18.6",
|
||||
"resolved": "https://registry.npmjs.org/@zxing/library/-/library-0.18.6.tgz",
|
||||
"integrity": "sha512-bulZ9JHoLFd9W36pi+7e7DnEYNJhljYjZ1UTsKPOoLMU3qtC+REHITeCRNx40zTRJZx18W5TBRXt5pq2Uopjsw==",
|
||||
"node_modules/@yudiel/react-qr-scanner": {
|
||||
"version": "1.2.10",
|
||||
"resolved": "https://registry.npmjs.org/@yudiel/react-qr-scanner/-/react-qr-scanner-1.2.10.tgz",
|
||||
"integrity": "sha512-I94AyrEu/2oZxXT6TEOK9+K2IfjqfPmic/GUah7alMIxfgXP9rs9+IZE56JhQr4pZ7Iz2RUC4JFfeaysFYNEIA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ts-custom-error": "^3.0.0"
|
||||
"@zxing/library": "^0.20.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^18.0.0",
|
||||
"react-dom": "^18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@yudiel/react-qr-scanner/node_modules/@zxing/library": {
|
||||
"version": "0.20.0",
|
||||
"resolved": "https://registry.npmjs.org/@zxing/library/-/library-0.20.0.tgz",
|
||||
"integrity": "sha512-6Ev6rcqVjMakZFIDvbUf0dtpPGeZMTfyxYg4HkVWioWeN7cRcnUWT3bU6sdohc82O1nPXcjq6WiGfXX2Pnit6A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ts-custom-error": "^3.2.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 10.4.0"
|
||||
@@ -2109,6 +2114,10 @@
|
||||
"integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/clue-hunt-app": {
|
||||
"resolved": "",
|
||||
"link": true
|
||||
},
|
||||
"node_modules/color": {
|
||||
"version": "4.2.3",
|
||||
"resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz",
|
||||
@@ -3274,20 +3283,6 @@
|
||||
"license": "ISC",
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/fsevents": {
|
||||
"version": "2.3.3",
|
||||
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
|
||||
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/function-bind": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
|
||||
@@ -3619,6 +3614,12 @@
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/html5-qrcode": {
|
||||
"version": "2.3.8",
|
||||
"resolved": "https://registry.npmjs.org/html5-qrcode/-/html5-qrcode-2.3.8.tgz",
|
||||
"integrity": "sha512-jsr4vafJhwoLVEDW3n1KvPnCCXWaQfRng0/EEYk1vNcQGcG/htAdhJX0be8YyqMoSz7+hZvOZSTAepsabiuhiQ==",
|
||||
"license": "Apache-2.0"
|
||||
},
|
||||
"node_modules/http-cache-semantics": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz",
|
||||
@@ -5781,21 +5782,6 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/react-qr-reader": {
|
||||
"version": "3.0.0-beta-1",
|
||||
"resolved": "https://registry.npmjs.org/react-qr-reader/-/react-qr-reader-3.0.0-beta-1.tgz",
|
||||
"integrity": "sha512-5HeFH9x/BlziRYQYGK2AeWS9WiKYZtGGMs9DXy3bcySTX3C9UJL9EwcPnWw8vlf7JP4FcrAlr1SnZ5nsWLQGyw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@zxing/browser": "0.0.7",
|
||||
"@zxing/library": "^0.18.3",
|
||||
"rollup": "^2.67.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^16.8.0 || ^17.0.0",
|
||||
"react-dom": "^16.8.0 || ^17.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/readable-stream": {
|
||||
"version": "3.6.2",
|
||||
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
|
||||
@@ -5939,21 +5925,6 @@
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/rollup": {
|
||||
"version": "2.79.2",
|
||||
"resolved": "https://registry.npmjs.org/rollup/-/rollup-2.79.2.tgz",
|
||||
"integrity": "sha512-fS6iqSPZDs3dr/y7Od6y5nha8dW1YnbgtsyotCVvoFGKbERG++CVRFv1meyGDE1SNItQA8BrnCw7ScdAhRJ3XQ==",
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"rollup": "dist/bin/rollup"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"fsevents": "~2.3.2"
|
||||
}
|
||||
},
|
||||
"node_modules/run-parallel": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz",
|
||||
|
||||
+5
-1
@@ -11,15 +11,19 @@
|
||||
"init-db": "node lib/init-db.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"@panva/hkdf": "^1.1.1",
|
||||
"@yudiel/react-qr-scanner": "^1.2.4",
|
||||
"bcryptjs": "^3.0.2",
|
||||
"better-sqlite3": "^11.8.1",
|
||||
"clue-hunt-app": "file:",
|
||||
"html5-qrcode": "^2.3.8",
|
||||
"jose": "^4.14.4",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"next": "15.2.2",
|
||||
"next-auth": "^4.24.11",
|
||||
"qrcode.react": "^4.2.0",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"react-qr-reader": "^3.0.0-beta-1",
|
||||
"sqlite": "^5.1.1",
|
||||
"sqlite3": "^5.1.7"
|
||||
},
|
||||
|
||||
+20
-20
@@ -23,83 +23,83 @@
|
||||
<rect x="181" y="1974" width="88" height="88" fill="url(#pattern10_37_192)"/>
|
||||
<rect x="373" y="1512" width="88" height="88" fill="url(#pattern11_37_192)"/>
|
||||
<g filter="url(#filter0_d_37_192)">
|
||||
<circle cx="230" cy="211" r="25" fill="#9e9e9e"/>
|
||||
<circle cx="230" cy="211" r="25" fill=""/>
|
||||
<circle cx="230" cy="211" r="25" stroke="#757575" stroke-width="2"/>
|
||||
</g>
|
||||
<g filter="url(#filter1_d_37_192)">
|
||||
<circle cx="320" cy="284" r="25" fill="#9e9e9e"/>
|
||||
<circle cx="320" cy="284" r="25" fill=""/>
|
||||
<circle cx="320" cy="284" r="25" stroke="#757575" stroke-width="2"/>
|
||||
</g>
|
||||
<g filter="url(#filter2_d_37_192)">
|
||||
<circle cx="264" cy="397" r="25" fill="#9e9e9e"/>
|
||||
<circle cx="264" cy="397" r="25" fill=""/>
|
||||
<circle cx="264" cy="397" r="25" stroke="#757575" stroke-width="2"/>
|
||||
</g>
|
||||
<g filter="url(#filter3_d_37_192)">
|
||||
<circle cx="342" cy="517" r="25" fill="#9e9e9e"/>
|
||||
<circle cx="342" cy="517" r="25" fill=""/>
|
||||
<circle cx="342" cy="517" r="25" stroke="#757575" stroke-width="2"/>
|
||||
</g>
|
||||
<g filter="url(#filter4_d_37_192)">
|
||||
<circle cx="320" cy="645" r="25" fill="#9e9e9e"/>
|
||||
<circle cx="320" cy="645" r="25" fill=""/>
|
||||
<circle cx="320" cy="645" r="25" stroke="#757575" stroke-width="2"/>
|
||||
</g>
|
||||
<g filter="url(#filter5_d_37_192)">
|
||||
<circle cx="295" cy="773" r="25" fill="#9e9e9e"/>
|
||||
<circle cx="295" cy="773" r="25" fill=""/>
|
||||
<circle cx="295" cy="773" r="25" stroke="#757575" stroke-width="2"/>
|
||||
</g>
|
||||
<g filter="url(#filter6_d_37_192)">
|
||||
<circle cx="239" cy="880" r="25" fill="#9e9e9e"/>
|
||||
<circle cx="239" cy="880" r="25" fill=""/>
|
||||
<circle cx="239" cy="880" r="25" stroke="#757575" stroke-width="2"/>
|
||||
</g>
|
||||
<g filter="url(#filter7_d_37_192)">
|
||||
<circle cx="354" cy="959" r="25" fill="#9e9e9e"/>
|
||||
<circle cx="354" cy="959" r="25" fill=""/>
|
||||
<circle cx="354" cy="959" r="25" stroke="#757575" stroke-width="2"/>
|
||||
</g>
|
||||
<g filter="url(#filter8_d_37_192)">
|
||||
<circle cx="270" cy="1036" r="25" fill="#9e9e9e"/>
|
||||
<circle cx="270" cy="1036" r="25" fill=""/>
|
||||
<circle cx="270" cy="1036" r="25" stroke="#757575" stroke-width="2"/>
|
||||
</g>
|
||||
<g filter="url(#filter9_d_37_192)">
|
||||
<circle cx="279" cy="1174" r="25" fill="#9e9e9e"/>
|
||||
<circle cx="279" cy="1174" r="25" fill=""/>
|
||||
<circle cx="279" cy="1174" r="25" stroke="#757575" stroke-width="2"/>
|
||||
</g>
|
||||
<g filter="url(#filter10_d_37_192)">
|
||||
<circle cx="337" cy="1302" r="25" fill="#9e9e9e"/>
|
||||
<circle cx="337" cy="1302" r="25" fill=""/>
|
||||
<circle cx="337" cy="1302" r="25" stroke="#757575" stroke-width="2"/>
|
||||
</g>
|
||||
<g filter="url(#filter11_d_37_192)">
|
||||
<circle cx="250" cy="1388" r="25" fill="#9e9e9e"/>
|
||||
<circle cx="250" cy="1388" r="25" fill=""/>
|
||||
<circle cx="250" cy="1388" r="25" stroke="#757575" stroke-width="2"/>
|
||||
</g>
|
||||
<g filter="url(#filter12_d_37_192)">
|
||||
<circle cx="162" cy="1487" r="25" fill="#9e9e9e"/>
|
||||
<circle cx="162" cy="1487" r="25" fill=""/>
|
||||
<circle cx="162" cy="1487" r="25" stroke="#757575" stroke-width="2"/>
|
||||
</g>
|
||||
<g filter="url(#filter13_d_37_192)">
|
||||
<circle cx="306" cy="1531" r="25" fill="#9e9e9e"/>
|
||||
<circle cx="306" cy="1531" r="25" fill=""/>
|
||||
<circle cx="306" cy="1531" r="25" stroke="#757575" stroke-width="2"/>
|
||||
</g>
|
||||
<g filter="url(#filter14_d_37_192)">
|
||||
<circle cx="342" cy="1649" r="25" fill="#9e9e9e"/>
|
||||
<circle cx="342" cy="1649" r="25" fill=""/>
|
||||
<circle cx="342" cy="1649" r="25" stroke="#757575" stroke-width="2"/>
|
||||
</g>
|
||||
<g filter="url(#filter15_d_37_192)">
|
||||
<circle cx="225" cy="1724" r="25" fill="#9e9e9e"/>
|
||||
<circle cx="225" cy="1724" r="25" fill=""/>
|
||||
<circle cx="225" cy="1724" r="25" stroke="#757575" stroke-width="2"/>
|
||||
</g>
|
||||
<g filter="url(#filter16_d_37_192)">
|
||||
<circle cx="216" cy="1822" r="25" fill="#9e9e9e"/>
|
||||
<circle cx="216" cy="1822" r="25" fill=""/>
|
||||
<circle cx="216" cy="1822" r="25" stroke="#757575" stroke-width="2"/>
|
||||
</g>
|
||||
<g filter="url(#filter17_d_37_192)">
|
||||
<circle cx="275" cy="1883" r="25" fill="#9e9e9e"/>
|
||||
<circle cx="275" cy="1883" r="25" fill=""/>
|
||||
<circle cx="275" cy="1883" r="25" stroke="#757575" stroke-width="2"/>
|
||||
</g>
|
||||
<g filter="url(#filter18_d_37_192)">
|
||||
<circle cx="362" cy="1917" r="25" fill="#9e9e9e"/>
|
||||
<circle cx="362" cy="1917" r="25" fill=""/>
|
||||
<circle cx="362" cy="1917" r="25" stroke="#757575" stroke-width="2"/>
|
||||
</g>
|
||||
<g filter="url(#filter19_d_37_192)">
|
||||
<circle cx="300" cy="2037" r="25" fill="#9e9e9e"/>
|
||||
<circle cx="300" cy="2037" r="25" fill=""/>
|
||||
<circle cx="300" cy="2037" r="25" stroke="#757575" stroke-width="2"/>
|
||||
</g>
|
||||
</g>
|
||||
|
||||
|
Before Width: | Height: | Size: 493 KiB After Width: | Height: | Size: 493 KiB |
-29
@@ -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
|
||||
@@ -91,9 +91,23 @@ export function fixCluePointsRendering(svgElement) {
|
||||
|
||||
// Ensure all clue points are in the overlay group and have proper styles
|
||||
cluePoints.forEach((point, i) => {
|
||||
// Get original coordinates if available
|
||||
const originalCx =
|
||||
point.getAttribute("data-original-cx") || point.getAttribute("cx");
|
||||
const originalCy =
|
||||
point.getAttribute("data-original-cy") || point.getAttribute("cy");
|
||||
|
||||
// Clone the point to remove any existing event handlers and styling issues
|
||||
const newPoint = point.cloneNode(true);
|
||||
|
||||
// Restore original coordinates if they were saved
|
||||
if (originalCx) newPoint.setAttribute("cx", originalCx);
|
||||
if (originalCy) newPoint.setAttribute("cy", originalCy);
|
||||
|
||||
// Store original coordinates for future reference
|
||||
if (originalCx) newPoint.setAttribute("data-original-cx", originalCx);
|
||||
if (originalCy) newPoint.setAttribute("data-original-cy", originalCy);
|
||||
|
||||
// Apply explicit styles to ensure visibility
|
||||
newPoint.setAttribute(
|
||||
"style",
|
||||
@@ -114,6 +128,12 @@ export function fixCluePointsRendering(svgElement) {
|
||||
// Add to overlay group
|
||||
overlayGroup.appendChild(newPoint);
|
||||
}
|
||||
|
||||
// Update the corresponding click area position too
|
||||
if (clickAreas[i]) {
|
||||
clickAreas[i].setAttribute("cx", originalCx || point.getAttribute("cx"));
|
||||
clickAreas[i].setAttribute("cy", originalCy || point.getAttribute("cy"));
|
||||
}
|
||||
});
|
||||
|
||||
// Move the overlay group to be the last child of the SVG
|
||||
@@ -121,3 +141,63 @@ export function fixCluePointsRendering(svgElement) {
|
||||
|
||||
console.log("Clue points rendering fixed");
|
||||
}
|
||||
|
||||
// New function to ensure clue circles match original map
|
||||
export function fixCluePositionsFromOriginalMap(svgElement) {
|
||||
if (!svgElement) return;
|
||||
|
||||
// Fetch the original map SVG to get circle positions
|
||||
fetch("/images/map.svg")
|
||||
.then((response) => response.text())
|
||||
.then((svgText) => {
|
||||
const parser = new DOMParser();
|
||||
const mapDoc = parser.parseFromString(svgText, "image/svg+xml");
|
||||
|
||||
// Get all circles from original map
|
||||
const originalCircles = Array.from(mapDoc.querySelectorAll("circle"));
|
||||
console.log(
|
||||
`Found ${originalCircles.length} circles in original map for position fixing`
|
||||
);
|
||||
|
||||
// Get current clue points
|
||||
const cluePoints = svgElement.querySelectorAll(".clue-point");
|
||||
console.log(`Found ${cluePoints.length} clue points to fix positions`);
|
||||
|
||||
// Only adjust positions if we have both original circles and clue points
|
||||
if (originalCircles.length > 0 && cluePoints.length > 0) {
|
||||
// Get the clue overlay group
|
||||
let overlayGroup = svgElement.querySelector(".clue-points-overlay");
|
||||
|
||||
if (!overlayGroup) return;
|
||||
|
||||
// Update each clue point to match original circle position
|
||||
cluePoints.forEach((point, i) => {
|
||||
if (i < originalCircles.length) {
|
||||
const originalCircle = originalCircles[i];
|
||||
const cx = originalCircle.getAttribute("cx") || "0";
|
||||
const cy = originalCircle.getAttribute("cy") || "0";
|
||||
|
||||
// Update position
|
||||
point.setAttribute("cx", cx);
|
||||
point.setAttribute("cy", cy);
|
||||
point.setAttribute("data-original-cx", cx);
|
||||
point.setAttribute("data-original-cy", cy);
|
||||
|
||||
// Update the corresponding click area
|
||||
const clickArea = svgElement.querySelector(
|
||||
`.clue-click-area[data-index="${i}"]`
|
||||
);
|
||||
if (clickArea) {
|
||||
clickArea.setAttribute("cx", cx);
|
||||
clickArea.setAttribute("cy", cy);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
console.log("Clue positions fixed to match original map");
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error("Error fixing clue positions from original map:", error);
|
||||
});
|
||||
}
|
||||
+128
-53
@@ -124,47 +124,39 @@ export async function populateMap(svgElement) {
|
||||
console.log(`Found ${allCircles.length} circles in SVG`);
|
||||
|
||||
if (allCircles.length === 0) {
|
||||
console.error("No circles found in SVG. Creating fallback circles.");
|
||||
|
||||
// Create some fallback circles at predefined positions
|
||||
const defs = document.createElementNS(
|
||||
"http://www.w3.org/2000/svg",
|
||||
"defs"
|
||||
console.error(
|
||||
"No circles found in SVG. Attempting to load from original map."
|
||||
);
|
||||
const positions = [
|
||||
{ x: 100, y: 100 },
|
||||
{ x: 200, y: 150 },
|
||||
{ x: 300, y: 200 },
|
||||
{ x: 400, y: 300 },
|
||||
{ x: 500, y: 400 },
|
||||
{ x: 600, y: 500 },
|
||||
];
|
||||
|
||||
positions.forEach((pos, i) => {
|
||||
const circle = document.createElementNS(
|
||||
"http://www.w3.org/2000/svg",
|
||||
"circle"
|
||||
);
|
||||
circle.setAttribute("cx", pos.x.toString());
|
||||
circle.setAttribute("cy", pos.y.toString());
|
||||
circle.setAttribute("r", "5");
|
||||
circle.setAttribute("fill", "transparent");
|
||||
defs.appendChild(circle);
|
||||
});
|
||||
// If no circles in current SVG, try to fetch circles from the original map SVG
|
||||
try {
|
||||
const mapResponse = await fetch("/images/map.svg");
|
||||
if (!mapResponse.ok) {
|
||||
throw new Error("Failed to load original map SVG");
|
||||
}
|
||||
|
||||
// Add the defs to the SVG
|
||||
svgElement.appendChild(defs);
|
||||
const mapText = await mapResponse.text();
|
||||
const parser = new DOMParser();
|
||||
const mapDoc = parser.parseFromString(mapText, "image/svg+xml");
|
||||
|
||||
// Now find all circles again
|
||||
const fallbackCircles = Array.from(defs.querySelectorAll("circle"));
|
||||
if (fallbackCircles.length > 0) {
|
||||
console.log(`Created ${fallbackCircles.length} fallback circles`);
|
||||
return await processCircles(svgElement, fallbackCircles);
|
||||
} else {
|
||||
return false;
|
||||
// Find circles in the original map
|
||||
const originalCircles = Array.from(mapDoc.querySelectorAll("circle"));
|
||||
console.log(`Found ${originalCircles.length} circles in original map`);
|
||||
|
||||
if (originalCircles.length > 0) {
|
||||
return await processCircles(svgElement, originalCircles, true);
|
||||
} else {
|
||||
console.error(
|
||||
"No circles found in original map either. Using fallback positions."
|
||||
);
|
||||
return await createFallbackCircles(svgElement);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to load circles from original map:", error);
|
||||
return await createFallbackCircles(svgElement);
|
||||
}
|
||||
} else {
|
||||
return await processCircles(svgElement, allCircles);
|
||||
return await processCircles(svgElement, allCircles, false);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error populating map:", error);
|
||||
@@ -172,16 +164,60 @@ export async function populateMap(svgElement) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create fallback circles at preset positions as a last resort
|
||||
* @param {SVGElement} svgElement - The SVG DOM element
|
||||
* @returns {Promise<boolean>} - Success flag
|
||||
*/
|
||||
async function createFallbackCircles(svgElement) {
|
||||
// Create some fallback circles at predefined positions
|
||||
const svgNamespace = "http://www.w3.org/2000/svg";
|
||||
const defs = document.createElementNS(svgNamespace, "defs");
|
||||
const positions = [
|
||||
{ x: 100, y: 100 },
|
||||
{ x: 200, y: 200 },
|
||||
{ x: 300, y: 300 },
|
||||
{ x: 400, y: 200 },
|
||||
{ x: 500, y: 100 },
|
||||
{ x: 600, y: 300 },
|
||||
];
|
||||
|
||||
positions.forEach((pos, i) => {
|
||||
const circle = document.createElementNS(svgNamespace, "circle");
|
||||
circle.setAttribute("cx", pos.x.toString());
|
||||
circle.setAttribute("cy", pos.y.toString());
|
||||
circle.setAttribute("r", "5");
|
||||
circle.setAttribute("fill", "transparent");
|
||||
defs.appendChild(circle);
|
||||
});
|
||||
|
||||
// Add the defs to the SVG
|
||||
svgElement.appendChild(defs);
|
||||
|
||||
// Now find all circles again
|
||||
const fallbackCircles = Array.from(defs.querySelectorAll("circle"));
|
||||
if (fallbackCircles.length > 0) {
|
||||
console.log(`Created ${fallbackCircles.length} fallback circles`);
|
||||
return await processCircles(svgElement, fallbackCircles, false);
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Process circles to create clue points
|
||||
* @param {SVGElement} svgElement - The SVG DOM element
|
||||
* @param {Array} circles - Array of circle elements
|
||||
* @param {boolean} isFromOriginalMap - Whether circles are from the original map
|
||||
* @returns {Promise<boolean>} - Success flag
|
||||
*/
|
||||
async function processCircles(svgElement, circles) {
|
||||
async function processCircles(svgElement, circles, isFromOriginalMap) {
|
||||
try {
|
||||
// Hide original circles
|
||||
circles.forEach((circle) => (circle.style.display = "none"));
|
||||
// Don't hide original circles if they are from the original map and not present in current SVG
|
||||
if (!isFromOriginalMap) {
|
||||
// Remove hiding originals so the new elements are clearly visible
|
||||
// circles.forEach((circle) => (circle.style.display = "none"));
|
||||
}
|
||||
|
||||
// Fetch the number of clues from the API
|
||||
const response = await fetch("/api/clues/count", {
|
||||
@@ -198,7 +234,7 @@ async function processCircles(svgElement, circles) {
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
const clueCount = parseInt(data.count, 10) || 0;
|
||||
let clueCount = parseInt(data.count, 10) || 0;
|
||||
console.log(`API returned clue count: ${clueCount}`);
|
||||
|
||||
if (clueCount <= 0) {
|
||||
@@ -206,21 +242,17 @@ async function processCircles(svgElement, circles) {
|
||||
clueCount = 5; // Fallback to 5 clues if API returns invalid data
|
||||
}
|
||||
|
||||
// Sort circles by vertical position (top to bottom)
|
||||
const sortedCircles = circles.sort((a, b) => {
|
||||
const aY = parseFloat(a.getAttribute("cy") || "0");
|
||||
const bY = parseFloat(b.getAttribute("cy") || "0");
|
||||
return aY - bY;
|
||||
});
|
||||
|
||||
// Determine how many circles to show
|
||||
const circlesToShow = Math.min(sortedCircles.length, clueCount);
|
||||
console.log(`Will create ${circlesToShow} clue points from sorted circles`);
|
||||
// Use the circles exactly where they are - preserve original positions
|
||||
// Just limit how many we use based on clue count
|
||||
const circlesToShow = Math.min(circles.length, clueCount);
|
||||
console.log(
|
||||
`Will create ${circlesToShow} clue points from available circles`
|
||||
);
|
||||
|
||||
// Create the overlay element that will go on top of everything
|
||||
const svgNamespace = "http://www.w3.org/2000/svg";
|
||||
|
||||
// Remove any old overlay group
|
||||
// Remove any old overlay group (but leave persistent button layer intact)
|
||||
const oldOverlay = svgElement.querySelector(".clue-points-overlay");
|
||||
if (oldOverlay && oldOverlay.parentNode) {
|
||||
oldOverlay.parentNode.removeChild(oldOverlay);
|
||||
@@ -238,9 +270,9 @@ async function processCircles(svgElement, circles) {
|
||||
// Add the overlay group as the LAST element in the SVG to ensure proper rendering order
|
||||
svgElement.appendChild(overlayGroup);
|
||||
|
||||
// Create interactive clue points
|
||||
// Create interactive clue points based on the original circle positions
|
||||
for (let i = 0; i < circlesToShow; i++) {
|
||||
const originalCircle = sortedCircles[i];
|
||||
const originalCircle = circles[i];
|
||||
|
||||
// Get original circle attributes
|
||||
const cx = originalCircle.getAttribute("cx") || "0";
|
||||
@@ -299,7 +331,50 @@ async function processCircles(svgElement, circles) {
|
||||
// This ensures it's the last child of the SVG
|
||||
svgElement.appendChild(overlayGroup);
|
||||
|
||||
console.log(`Successfully created ${circlesToShow} clue points`);
|
||||
// Instead of appending button layer with absolute positioning,
|
||||
// create/update it as a child of overlayGroup using normal SVG positioning.
|
||||
let buttonGroup = svgElement.querySelector(".clue-button-layer");
|
||||
if (buttonGroup) {
|
||||
buttonGroup.innerHTML = "";
|
||||
} else {
|
||||
buttonGroup = document.createElementNS(svgNamespace, "g");
|
||||
buttonGroup.setAttribute("class", "clue-button-layer");
|
||||
// Remove absolute positioning; allow buttonGroup to inherit the overlay's coordinate system.
|
||||
buttonGroup.setAttribute(
|
||||
"style",
|
||||
"pointer-events: auto; z-index: 10000;"
|
||||
);
|
||||
}
|
||||
for (let i = 0; i < circlesToShow; i++) {
|
||||
const originalCircle = circles[i];
|
||||
const cx = originalCircle.getAttribute("cx") || "0";
|
||||
const cy = originalCircle.getAttribute("cy") || "0";
|
||||
const r = Math.max(
|
||||
parseFloat(originalCircle.getAttribute("r") || "5"),
|
||||
14
|
||||
);
|
||||
|
||||
const button = document.createElementNS(svgNamespace, "circle");
|
||||
button.setAttribute("cx", cx);
|
||||
button.setAttribute("cy", cy);
|
||||
button.setAttribute("r", r.toString());
|
||||
button.setAttribute("class", "clue-button");
|
||||
button.setAttribute(
|
||||
"style",
|
||||
"fill: #4caf50; stroke: #388e3c; stroke-width: 2px; cursor: pointer; opacity: 0.9; pointer-events: auto;"
|
||||
);
|
||||
button.addEventListener("click", () => {
|
||||
console.log(`Button for clue ${i + 1} clicked`);
|
||||
// ...add navigation or other logic here...
|
||||
});
|
||||
buttonGroup.appendChild(button);
|
||||
}
|
||||
// Append or re-append the buttonGroup into the overlayGroup so they appear together.
|
||||
overlayGroup.appendChild(buttonGroup);
|
||||
|
||||
console.log(
|
||||
`Successfully created ${circlesToShow} clue points and buttons`
|
||||
);
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error("Error processing circles:", error);
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
|
||||
"@emnapi/runtime@^1.2.0":
|
||||
version "1.3.1"
|
||||
resolved "https://registry.yarnpkg.com/@emnapi/runtime/-/runtime-1.3.1.tgz#0fcaa575afc31f455fd33534c19381cfce6c6f60"
|
||||
resolved "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.3.1.tgz"
|
||||
integrity sha512-kEBmG8KyqtxJZv+ygbEim+KCGtIq1fC22Ms3S4ziXmYKm8uyoLX0MHONVKwp+9opg390VaKRNt4a7A9NwmpNhw==
|
||||
dependencies:
|
||||
tslib "^2.4.0"
|
||||
@@ -122,110 +122,110 @@
|
||||
|
||||
"@img/sharp-darwin-arm64@0.33.5":
|
||||
version "0.33.5"
|
||||
resolved "https://registry.yarnpkg.com/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.33.5.tgz#ef5b5a07862805f1e8145a377c8ba6e98813ca08"
|
||||
resolved "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.33.5.tgz"
|
||||
integrity sha512-UT4p+iz/2H4twwAoLCqfA9UH5pI6DggwKEGuaPy7nCVQ8ZsiY5PIcrRvD1DzuY3qYL07NtIQcWnBSY/heikIFQ==
|
||||
optionalDependencies:
|
||||
"@img/sharp-libvips-darwin-arm64" "1.0.4"
|
||||
|
||||
"@img/sharp-darwin-x64@0.33.5":
|
||||
version "0.33.5"
|
||||
resolved "https://registry.yarnpkg.com/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.33.5.tgz#e03d3451cd9e664faa72948cc70a403ea4063d61"
|
||||
resolved "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.33.5.tgz"
|
||||
integrity sha512-fyHac4jIc1ANYGRDxtiqelIbdWkIuQaI84Mv45KvGRRxSAa7o7d1ZKAOBaYbnepLC1WqxfpimdeWfvqqSGwR2Q==
|
||||
optionalDependencies:
|
||||
"@img/sharp-libvips-darwin-x64" "1.0.4"
|
||||
|
||||
"@img/sharp-libvips-darwin-arm64@1.0.4":
|
||||
version "1.0.4"
|
||||
resolved "https://registry.yarnpkg.com/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.0.4.tgz#447c5026700c01a993c7804eb8af5f6e9868c07f"
|
||||
resolved "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.0.4.tgz"
|
||||
integrity sha512-XblONe153h0O2zuFfTAbQYAX2JhYmDHeWikp1LM9Hul9gVPjFY427k6dFEcOL72O01QxQsWi761svJ/ev9xEDg==
|
||||
|
||||
"@img/sharp-libvips-darwin-x64@1.0.4":
|
||||
version "1.0.4"
|
||||
resolved "https://registry.yarnpkg.com/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.0.4.tgz#e0456f8f7c623f9dbfbdc77383caa72281d86062"
|
||||
resolved "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.0.4.tgz"
|
||||
integrity sha512-xnGR8YuZYfJGmWPvmlunFaWJsb9T/AO2ykoP3Fz/0X5XV2aoYBPkX6xqCQvUTKKiLddarLaxpzNe+b1hjeWHAQ==
|
||||
|
||||
"@img/sharp-libvips-linux-arm64@1.0.4":
|
||||
version "1.0.4"
|
||||
resolved "https://registry.yarnpkg.com/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.0.4.tgz#979b1c66c9a91f7ff2893556ef267f90ebe51704"
|
||||
integrity sha512-9B+taZ8DlyyqzZQnoeIvDVR/2F4EbMepXMc/NdVbkzsJbzkUjhXv/70GQJ7tdLA4YJgNP25zukcxpX2/SueNrA==
|
||||
|
||||
"@img/sharp-libvips-linux-arm@1.0.5":
|
||||
version "1.0.5"
|
||||
resolved "https://registry.yarnpkg.com/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.0.5.tgz#99f922d4e15216ec205dcb6891b721bfd2884197"
|
||||
resolved "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.0.5.tgz"
|
||||
integrity sha512-gvcC4ACAOPRNATg/ov8/MnbxFDJqf/pDePbBnuBDcjsI8PssmjoKMAz4LtLaVi+OnSb5FK/yIOamqDwGmXW32g==
|
||||
|
||||
"@img/sharp-libvips-linux-arm64@1.0.4":
|
||||
version "1.0.4"
|
||||
resolved "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.0.4.tgz"
|
||||
integrity sha512-9B+taZ8DlyyqzZQnoeIvDVR/2F4EbMepXMc/NdVbkzsJbzkUjhXv/70GQJ7tdLA4YJgNP25zukcxpX2/SueNrA==
|
||||
|
||||
"@img/sharp-libvips-linux-s390x@1.0.4":
|
||||
version "1.0.4"
|
||||
resolved "https://registry.yarnpkg.com/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.0.4.tgz#f8a5eb1f374a082f72b3f45e2fb25b8118a8a5ce"
|
||||
resolved "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.0.4.tgz"
|
||||
integrity sha512-u7Wz6ntiSSgGSGcjZ55im6uvTrOxSIS8/dgoVMoiGE9I6JAfU50yH5BoDlYA1tcuGS7g/QNtetJnxA6QEsCVTA==
|
||||
|
||||
"@img/sharp-libvips-linux-x64@1.0.4":
|
||||
version "1.0.4"
|
||||
resolved "https://registry.yarnpkg.com/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.0.4.tgz#d4c4619cdd157774906e15770ee119931c7ef5e0"
|
||||
resolved "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.0.4.tgz"
|
||||
integrity sha512-MmWmQ3iPFZr0Iev+BAgVMb3ZyC4KeFc3jFxnNbEPas60e1cIfevbtuyf9nDGIzOaW9PdnDciJm+wFFaTlj5xYw==
|
||||
|
||||
"@img/sharp-libvips-linuxmusl-arm64@1.0.4":
|
||||
version "1.0.4"
|
||||
resolved "https://registry.yarnpkg.com/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.0.4.tgz#166778da0f48dd2bded1fa3033cee6b588f0d5d5"
|
||||
resolved "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.0.4.tgz"
|
||||
integrity sha512-9Ti+BbTYDcsbp4wfYib8Ctm1ilkugkA/uscUn6UXK1ldpC1JjiXbLfFZtRlBhjPZ5o1NCLiDbg8fhUPKStHoTA==
|
||||
|
||||
"@img/sharp-libvips-linuxmusl-x64@1.0.4":
|
||||
version "1.0.4"
|
||||
resolved "https://registry.yarnpkg.com/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.0.4.tgz#93794e4d7720b077fcad3e02982f2f1c246751ff"
|
||||
resolved "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.0.4.tgz"
|
||||
integrity sha512-viYN1KX9m+/hGkJtvYYp+CCLgnJXwiQB39damAO7WMdKWlIhmYTfHjwSbQeUK/20vY154mwezd9HflVFM1wVSw==
|
||||
|
||||
"@img/sharp-linux-arm64@0.33.5":
|
||||
version "0.33.5"
|
||||
resolved "https://registry.yarnpkg.com/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.33.5.tgz#edb0697e7a8279c9fc829a60fc35644c4839bb22"
|
||||
integrity sha512-JMVv+AMRyGOHtO1RFBiJy/MBsgz0x4AWrT6QoEVVTyh1E39TrCUpTRI7mx9VksGX4awWASxqCYLCV4wBZHAYxA==
|
||||
optionalDependencies:
|
||||
"@img/sharp-libvips-linux-arm64" "1.0.4"
|
||||
|
||||
"@img/sharp-linux-arm@0.33.5":
|
||||
version "0.33.5"
|
||||
resolved "https://registry.yarnpkg.com/@img/sharp-linux-arm/-/sharp-linux-arm-0.33.5.tgz#422c1a352e7b5832842577dc51602bcd5b6f5eff"
|
||||
resolved "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.33.5.tgz"
|
||||
integrity sha512-JTS1eldqZbJxjvKaAkxhZmBqPRGmxgu+qFKSInv8moZ2AmT5Yib3EQ1c6gp493HvrvV8QgdOXdyaIBrhvFhBMQ==
|
||||
optionalDependencies:
|
||||
"@img/sharp-libvips-linux-arm" "1.0.5"
|
||||
|
||||
"@img/sharp-linux-arm64@0.33.5":
|
||||
version "0.33.5"
|
||||
resolved "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.33.5.tgz"
|
||||
integrity sha512-JMVv+AMRyGOHtO1RFBiJy/MBsgz0x4AWrT6QoEVVTyh1E39TrCUpTRI7mx9VksGX4awWASxqCYLCV4wBZHAYxA==
|
||||
optionalDependencies:
|
||||
"@img/sharp-libvips-linux-arm64" "1.0.4"
|
||||
|
||||
"@img/sharp-linux-s390x@0.33.5":
|
||||
version "0.33.5"
|
||||
resolved "https://registry.yarnpkg.com/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.33.5.tgz#f5c077926b48e97e4a04d004dfaf175972059667"
|
||||
resolved "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.33.5.tgz"
|
||||
integrity sha512-y/5PCd+mP4CA/sPDKl2961b+C9d+vPAveS33s6Z3zfASk2j5upL6fXVPZi7ztePZ5CuH+1kW8JtvxgbuXHRa4Q==
|
||||
optionalDependencies:
|
||||
"@img/sharp-libvips-linux-s390x" "1.0.4"
|
||||
|
||||
"@img/sharp-linux-x64@0.33.5":
|
||||
version "0.33.5"
|
||||
resolved "https://registry.yarnpkg.com/@img/sharp-linux-x64/-/sharp-linux-x64-0.33.5.tgz#d806e0afd71ae6775cc87f0da8f2d03a7c2209cb"
|
||||
resolved "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.33.5.tgz"
|
||||
integrity sha512-opC+Ok5pRNAzuvq1AG0ar+1owsu842/Ab+4qvU879ippJBHvyY5n2mxF1izXqkPYlGuP/M556uh53jRLJmzTWA==
|
||||
optionalDependencies:
|
||||
"@img/sharp-libvips-linux-x64" "1.0.4"
|
||||
|
||||
"@img/sharp-linuxmusl-arm64@0.33.5":
|
||||
version "0.33.5"
|
||||
resolved "https://registry.yarnpkg.com/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.33.5.tgz#252975b915894fb315af5deea174651e208d3d6b"
|
||||
resolved "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.33.5.tgz"
|
||||
integrity sha512-XrHMZwGQGvJg2V/oRSUfSAfjfPxO+4DkiRh6p2AFjLQztWUuY/o8Mq0eMQVIY7HJ1CDQUJlxGGZRw1a5bqmd1g==
|
||||
optionalDependencies:
|
||||
"@img/sharp-libvips-linuxmusl-arm64" "1.0.4"
|
||||
|
||||
"@img/sharp-linuxmusl-x64@0.33.5":
|
||||
version "0.33.5"
|
||||
resolved "https://registry.yarnpkg.com/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.33.5.tgz#3f4609ac5d8ef8ec7dadee80b560961a60fd4f48"
|
||||
resolved "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.33.5.tgz"
|
||||
integrity sha512-WT+d/cgqKkkKySYmqoZ8y3pxx7lx9vVejxW/W4DOFMYVSkErR+w7mf2u8m/y4+xHe7yY9DAXQMWQhpnMuFfScw==
|
||||
optionalDependencies:
|
||||
"@img/sharp-libvips-linuxmusl-x64" "1.0.4"
|
||||
|
||||
"@img/sharp-wasm32@0.33.5":
|
||||
version "0.33.5"
|
||||
resolved "https://registry.yarnpkg.com/@img/sharp-wasm32/-/sharp-wasm32-0.33.5.tgz#6f44f3283069d935bb5ca5813153572f3e6f61a1"
|
||||
resolved "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.33.5.tgz"
|
||||
integrity sha512-ykUW4LVGaMcU9lu9thv85CbRMAwfeadCJHRsg2GmeRa/cJxsVY9Rbd57JcMxBkKHag5U/x7TSBpScF4U8ElVzg==
|
||||
dependencies:
|
||||
"@emnapi/runtime" "^1.2.0"
|
||||
|
||||
"@img/sharp-win32-ia32@0.33.5":
|
||||
version "0.33.5"
|
||||
resolved "https://registry.yarnpkg.com/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.33.5.tgz#1a0c839a40c5351e9885628c85f2e5dfd02b52a9"
|
||||
resolved "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.33.5.tgz"
|
||||
integrity sha512-T36PblLaTwuVJ/zw/LaH0PdZkRz5rd3SmMHX8GSmR7vtNSP5Z6bQkExdSK7xGWyxLw4sUknBuugTelgw2faBbQ==
|
||||
|
||||
"@img/sharp-win32-x64@0.33.5":
|
||||
@@ -247,37 +247,37 @@
|
||||
|
||||
"@next/swc-darwin-arm64@15.2.2":
|
||||
version "15.2.2"
|
||||
resolved "https://registry.yarnpkg.com/@next/swc-darwin-arm64/-/swc-darwin-arm64-15.2.2.tgz#d3131279d4815ed7604eba7bf0063e0b8bfd2135"
|
||||
resolved "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-15.2.2.tgz"
|
||||
integrity sha512-HNBRnz+bkZ+KfyOExpUxTMR0Ow8nkkcE6IlsdEa9W/rI7gefud19+Sn1xYKwB9pdCdxIP1lPru/ZfjfA+iT8pw==
|
||||
|
||||
"@next/swc-darwin-x64@15.2.2":
|
||||
version "15.2.2"
|
||||
resolved "https://registry.yarnpkg.com/@next/swc-darwin-x64/-/swc-darwin-x64-15.2.2.tgz#8562f6b51177aa30f3a6e95a8b3e0e176a6cb04f"
|
||||
resolved "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-15.2.2.tgz"
|
||||
integrity sha512-mJOUwp7al63tDpLpEFpKwwg5jwvtL1lhRW2fI1Aog0nYCPAhxbJsaZKdoVyPZCy8MYf/iQVNDuk/+i29iLCzIA==
|
||||
|
||||
"@next/swc-linux-arm64-gnu@15.2.2":
|
||||
version "15.2.2"
|
||||
resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.2.2.tgz#c2466cffc541f39ae4d435087c81f5899d401903"
|
||||
resolved "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.2.2.tgz"
|
||||
integrity sha512-5ZZ0Zwy3SgMr7MfWtRE7cQWVssfOvxYfD9O7XHM7KM4nrf5EOeqwq67ZXDgo86LVmffgsu5tPO57EeFKRnrfSQ==
|
||||
|
||||
"@next/swc-linux-arm64-musl@15.2.2":
|
||||
version "15.2.2"
|
||||
resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.2.2.tgz#24ed87fe47325597e986bd01c427efae53d6b677"
|
||||
resolved "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.2.2.tgz"
|
||||
integrity sha512-cgKWBuFMLlJ4TWcFHl1KOaVVUAF8vy4qEvX5KsNd0Yj5mhu989QFCq1WjuaEbv/tO1ZpsQI6h/0YR8bLwEi+nA==
|
||||
|
||||
"@next/swc-linux-x64-gnu@15.2.2":
|
||||
version "15.2.2"
|
||||
resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.2.2.tgz#0240f44530ca39c1fc899fabe8f33aa35d7e77da"
|
||||
resolved "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.2.2.tgz"
|
||||
integrity sha512-c3kWSOSsVL8rcNBBfOq1+/j2PKs2nsMwJUV4icUxRgGBwUOfppeh7YhN5s79enBQFU+8xRgVatFkhHU1QW7yUA==
|
||||
|
||||
"@next/swc-linux-x64-musl@15.2.2":
|
||||
version "15.2.2"
|
||||
resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.2.2.tgz#cca6ce8e9ec89afaa618673f0f8658682c30ff56"
|
||||
resolved "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.2.2.tgz"
|
||||
integrity sha512-PXTW9PLTxdNlVYgPJ0equojcq1kNu5NtwcNjRjHAB+/sdoKZ+X8FBu70fdJFadkxFIGekQTyRvPMFF+SOJaQjw==
|
||||
|
||||
"@next/swc-win32-arm64-msvc@15.2.2":
|
||||
version "15.2.2"
|
||||
resolved "https://registry.yarnpkg.com/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.2.2.tgz#08f621602ed53fa21ddf6f656160b9186860be15"
|
||||
resolved "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.2.2.tgz"
|
||||
integrity sha512-nG644Es5llSGEcTaXhnGWR/aThM/hIaz0jx4MDg4gWC8GfTCp8eDBWZ77CVuv2ha/uL9Ce+nPTfYkSLG67/sHg==
|
||||
|
||||
"@next/swc-win32-x64-msvc@15.2.2":
|
||||
@@ -293,7 +293,7 @@
|
||||
"@nodelib/fs.stat" "2.0.5"
|
||||
run-parallel "^1.1.9"
|
||||
|
||||
"@nodelib/fs.stat@2.0.5", "@nodelib/fs.stat@^2.0.2":
|
||||
"@nodelib/fs.stat@^2.0.2", "@nodelib/fs.stat@2.0.5":
|
||||
version "2.0.5"
|
||||
resolved "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz"
|
||||
integrity sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==
|
||||
@@ -327,7 +327,7 @@
|
||||
mkdirp "^1.0.4"
|
||||
rimraf "^3.0.2"
|
||||
|
||||
"@panva/hkdf@^1.0.2":
|
||||
"@panva/hkdf@^1.0.2", "@panva/hkdf@^1.1.1":
|
||||
version "1.2.1"
|
||||
resolved "https://registry.npmjs.org/@panva/hkdf/-/hkdf-1.2.1.tgz"
|
||||
integrity sha512-6oclG6Y3PiDFcoyk8srjLfVKyMfVCKJ27JwNPViuXziFpmdz+MZnZN/aKY0JGXgYuO/VghU0jcOAZgWXZ1Dmrw==
|
||||
@@ -365,52 +365,52 @@
|
||||
|
||||
"@tailwindcss/oxide-android-arm64@4.0.12":
|
||||
version "4.0.12"
|
||||
resolved "https://registry.yarnpkg.com/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.0.12.tgz#f79cf637bc11b0ae71e75b66d7c5ba5159bd3df3"
|
||||
resolved "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.0.12.tgz"
|
||||
integrity sha512-dAXCaemu3mHLXcA5GwGlQynX8n7tTdvn5i1zAxRvZ5iC9fWLl5bGnjZnzrQqT7ttxCvRwdVf3IHUnMVdDBO/kQ==
|
||||
|
||||
"@tailwindcss/oxide-darwin-arm64@4.0.12":
|
||||
version "4.0.12"
|
||||
resolved "https://registry.yarnpkg.com/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.0.12.tgz#6b730b5d19ff01748a27af5103966f534a899f07"
|
||||
resolved "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.0.12.tgz"
|
||||
integrity sha512-vPNI+TpJQ7sizselDXIJdYkx9Cu6JBdtmRWujw9pVIxW8uz3O2PjgGGzL/7A0sXI8XDjSyRChrUnEW9rQygmJQ==
|
||||
|
||||
"@tailwindcss/oxide-darwin-x64@4.0.12":
|
||||
version "4.0.12"
|
||||
resolved "https://registry.yarnpkg.com/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.0.12.tgz#2eb33ba40cbb78922004be9d6b81542347043fe5"
|
||||
resolved "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.0.12.tgz"
|
||||
integrity sha512-RL/9jM41Fdq4Efr35C5wgLx98BirnrfwuD+zgMFK6Ir68HeOSqBhW9jsEeC7Y/JcGyPd3MEoJVIU4fAb7YLg7A==
|
||||
|
||||
"@tailwindcss/oxide-freebsd-x64@4.0.12":
|
||||
version "4.0.12"
|
||||
resolved "https://registry.yarnpkg.com/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.0.12.tgz#317b980504b0bbcaee32c9e4e961f77cdadb02a6"
|
||||
resolved "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.0.12.tgz"
|
||||
integrity sha512-7WzWiax+LguJcMEimY0Q4sBLlFXu1tYxVka3+G2M9KmU/3m84J3jAIV4KZWnockbHsbb2XgrEjtlJKVwHQCoRA==
|
||||
|
||||
"@tailwindcss/oxide-linux-arm-gnueabihf@4.0.12":
|
||||
version "4.0.12"
|
||||
resolved "https://registry.yarnpkg.com/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.0.12.tgz#0f7153dec6e200327b76775ad2d1154706cef34d"
|
||||
resolved "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.0.12.tgz"
|
||||
integrity sha512-X9LRC7jjE1QlfIaBbXjY0PGeQP87lz5mEfLSVs2J1yRc9PSg1tEPS9NBqY4BU9v5toZgJgzKeaNltORyTs22TQ==
|
||||
|
||||
"@tailwindcss/oxide-linux-arm64-gnu@4.0.12":
|
||||
version "4.0.12"
|
||||
resolved "https://registry.yarnpkg.com/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.0.12.tgz#50a3ec46600cc998f45a265ebdd5de2d7bb27ee0"
|
||||
resolved "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.0.12.tgz"
|
||||
integrity sha512-i24IFNq2402zfDdoWKypXz0ZNS2G4NKaA82tgBlE2OhHIE+4mg2JDb5wVfyP6R+MCm5grgXvurcIcKWvo44QiQ==
|
||||
|
||||
"@tailwindcss/oxide-linux-arm64-musl@4.0.12":
|
||||
version "4.0.12"
|
||||
resolved "https://registry.yarnpkg.com/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.0.12.tgz#2de10c5239766e2fb810045fe5b75db54d481fd4"
|
||||
resolved "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.0.12.tgz"
|
||||
integrity sha512-LmOdshJBfAGIBG0DdBWhI0n5LTMurnGGJCHcsm9F//ISfsHtCnnYIKgYQui5oOz1SUCkqsMGfkAzWyNKZqbGNw==
|
||||
|
||||
"@tailwindcss/oxide-linux-x64-gnu@4.0.12":
|
||||
version "4.0.12"
|
||||
resolved "https://registry.yarnpkg.com/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.0.12.tgz#f89a24ffb0f14ea23d110efcbf58baa1db783582"
|
||||
resolved "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.0.12.tgz"
|
||||
integrity sha512-OSK667qZRH30ep8RiHbZDQfqkXjnzKxdn0oRwWzgCO8CoTxV+MvIkd0BWdQbYtYuM1wrakARV/Hwp0eA/qzdbw==
|
||||
|
||||
"@tailwindcss/oxide-linux-x64-musl@4.0.12":
|
||||
version "4.0.12"
|
||||
resolved "https://registry.yarnpkg.com/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.0.12.tgz#c53a5b494e241a744f067ba3a261adca80c20af4"
|
||||
resolved "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.0.12.tgz"
|
||||
integrity sha512-uylhWq6OWQ8krV8Jk+v0H/3AZKJW6xYMgNMyNnUbbYXWi7hIVdxRKNUB5UvrlC3RxtgsK5EAV2i1CWTRsNcAnA==
|
||||
|
||||
"@tailwindcss/oxide-win32-arm64-msvc@4.0.12":
|
||||
version "4.0.12"
|
||||
resolved "https://registry.yarnpkg.com/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.0.12.tgz#63c0befad43355fb5cea6c64efd946a718acb06b"
|
||||
resolved "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.0.12.tgz"
|
||||
integrity sha512-XDLnhMoXZEEOir1LK43/gHHwK84V1GlV8+pAncUAIN2wloeD+nNciI9WRIY/BeFTqES22DhTIGoilSO39xDb2g==
|
||||
|
||||
"@tailwindcss/oxide-win32-x64-msvc@4.0.12":
|
||||
@@ -567,23 +567,23 @@
|
||||
"@typescript-eslint/types" "8.26.1"
|
||||
eslint-visitor-keys "^4.2.0"
|
||||
|
||||
"@zxing/browser@0.0.7":
|
||||
version "0.0.7"
|
||||
resolved "https://registry.npmjs.org/@zxing/browser/-/browser-0.0.7.tgz"
|
||||
integrity sha512-AepzMgDnD6EjxewqmXpHJsi4S3Gw9ilZJLIbTf6fWuWySEcHBodnGu3p7FWlgq1Sd5QyfPhTum5z3CBkkhMVng==
|
||||
optionalDependencies:
|
||||
"@zxing/text-encoding" "^0.9.0"
|
||||
|
||||
"@zxing/library@^0.18.3":
|
||||
version "0.18.6"
|
||||
resolved "https://registry.npmjs.org/@zxing/library/-/library-0.18.6.tgz"
|
||||
integrity sha512-bulZ9JHoLFd9W36pi+7e7DnEYNJhljYjZ1UTsKPOoLMU3qtC+REHITeCRNx40zTRJZx18W5TBRXt5pq2Uopjsw==
|
||||
"@yudiel/react-qr-scanner@^1.2.4":
|
||||
version "1.2.10"
|
||||
resolved "https://registry.npmjs.org/@yudiel/react-qr-scanner/-/react-qr-scanner-1.2.10.tgz"
|
||||
integrity sha512-I94AyrEu/2oZxXT6TEOK9+K2IfjqfPmic/GUah7alMIxfgXP9rs9+IZE56JhQr4pZ7Iz2RUC4JFfeaysFYNEIA==
|
||||
dependencies:
|
||||
ts-custom-error "^3.0.0"
|
||||
"@zxing/library" "^0.20.0"
|
||||
|
||||
"@zxing/library@^0.20.0":
|
||||
version "0.20.0"
|
||||
resolved "https://registry.npmjs.org/@zxing/library/-/library-0.20.0.tgz"
|
||||
integrity sha512-6Ev6rcqVjMakZFIDvbUf0dtpPGeZMTfyxYg4HkVWioWeN7cRcnUWT3bU6sdohc82O1nPXcjq6WiGfXX2Pnit6A==
|
||||
dependencies:
|
||||
ts-custom-error "^3.2.1"
|
||||
optionalDependencies:
|
||||
"@zxing/text-encoding" "~0.9.0"
|
||||
|
||||
"@zxing/text-encoding@^0.9.0", "@zxing/text-encoding@~0.9.0":
|
||||
"@zxing/text-encoding@~0.9.0":
|
||||
version "0.9.0"
|
||||
resolved "https://registry.npmjs.org/@zxing/text-encoding/-/text-encoding-0.9.0.tgz"
|
||||
integrity sha512-U/4aVJ2mxI0aDNI8Uq0wEhMgY+u4CNtEb0om3+y3+niDAsoTCOB33UF0sxpzqzdqXLqmvc+vZyAt4O8pPdfkwA==
|
||||
@@ -603,7 +603,7 @@ acorn@^8.14.0:
|
||||
resolved "https://registry.npmjs.org/acorn/-/acorn-8.14.1.tgz"
|
||||
integrity sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==
|
||||
|
||||
agent-base@6, agent-base@^6.0.2:
|
||||
agent-base@^6.0.2, agent-base@6:
|
||||
version "6.0.2"
|
||||
resolved "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz"
|
||||
integrity sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==
|
||||
@@ -954,6 +954,26 @@ client-only@0.0.1:
|
||||
resolved "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz"
|
||||
integrity sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==
|
||||
|
||||
"clue-hunt-app@file:":
|
||||
version "0.1.0"
|
||||
resolved "file:"
|
||||
dependencies:
|
||||
"@panva/hkdf" "^1.1.1"
|
||||
"@yudiel/react-qr-scanner" "^1.2.4"
|
||||
bcryptjs "^3.0.2"
|
||||
better-sqlite3 "^11.8.1"
|
||||
clue-hunt-app "file:"
|
||||
html5-qrcode "^2.3.8"
|
||||
jose "^4.14.4"
|
||||
jsonwebtoken "^9.0.2"
|
||||
next "15.2.2"
|
||||
next-auth "^4.24.11"
|
||||
qrcode.react "^4.2.0"
|
||||
react "^19.0.0"
|
||||
react-dom "^19.0.0"
|
||||
sqlite "^5.1.1"
|
||||
sqlite3 "^5.1.7"
|
||||
|
||||
color-convert@^2.0.1:
|
||||
version "2.0.1"
|
||||
resolved "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz"
|
||||
@@ -1048,13 +1068,6 @@ data-view-byte-offset@^1.0.1:
|
||||
es-errors "^1.3.0"
|
||||
is-data-view "^1.0.1"
|
||||
|
||||
debug@4, debug@^4.3.1, debug@^4.3.2, debug@^4.3.3, debug@^4.3.4, debug@^4.3.7:
|
||||
version "4.4.0"
|
||||
resolved "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz"
|
||||
integrity sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==
|
||||
dependencies:
|
||||
ms "^2.1.3"
|
||||
|
||||
debug@^3.2.7:
|
||||
version "3.2.7"
|
||||
resolved "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz"
|
||||
@@ -1062,6 +1075,13 @@ debug@^3.2.7:
|
||||
dependencies:
|
||||
ms "^2.1.1"
|
||||
|
||||
debug@^4.3.1, debug@^4.3.2, debug@^4.3.3, debug@^4.3.4, debug@^4.3.7, debug@4:
|
||||
version "4.4.0"
|
||||
resolved "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz"
|
||||
integrity sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==
|
||||
dependencies:
|
||||
ms "^2.1.3"
|
||||
|
||||
decompress-response@^6.0.0:
|
||||
version "6.0.0"
|
||||
resolved "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz"
|
||||
@@ -1521,17 +1541,6 @@ fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3:
|
||||
resolved "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz"
|
||||
integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==
|
||||
|
||||
fast-glob@3.3.1:
|
||||
version "3.3.1"
|
||||
resolved "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.1.tgz"
|
||||
integrity sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg==
|
||||
dependencies:
|
||||
"@nodelib/fs.stat" "^2.0.2"
|
||||
"@nodelib/fs.walk" "^1.2.3"
|
||||
glob-parent "^5.1.2"
|
||||
merge2 "^1.3.0"
|
||||
micromatch "^4.0.4"
|
||||
|
||||
fast-glob@^3.3.2:
|
||||
version "3.3.3"
|
||||
resolved "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz"
|
||||
@@ -1543,6 +1552,17 @@ fast-glob@^3.3.2:
|
||||
merge2 "^1.3.0"
|
||||
micromatch "^4.0.8"
|
||||
|
||||
fast-glob@3.3.1:
|
||||
version "3.3.1"
|
||||
resolved "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.1.tgz"
|
||||
integrity sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg==
|
||||
dependencies:
|
||||
"@nodelib/fs.stat" "^2.0.2"
|
||||
"@nodelib/fs.walk" "^1.2.3"
|
||||
glob-parent "^5.1.2"
|
||||
merge2 "^1.3.0"
|
||||
micromatch "^4.0.4"
|
||||
|
||||
fast-json-stable-stringify@^2.0.0:
|
||||
version "2.1.0"
|
||||
resolved "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz"
|
||||
@@ -1629,11 +1649,6 @@ fs.realpath@^1.0.0:
|
||||
resolved "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz"
|
||||
integrity sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==
|
||||
|
||||
fsevents@~2.3.2:
|
||||
version "2.3.3"
|
||||
resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.3.tgz#cac6407785d03675a2a5e1a5305c697b347d90d6"
|
||||
integrity sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==
|
||||
|
||||
function-bind@^1.1.2:
|
||||
version "1.1.2"
|
||||
resolved "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz"
|
||||
@@ -1817,6 +1832,11 @@ hasown@^2.0.2:
|
||||
dependencies:
|
||||
function-bind "^1.1.2"
|
||||
|
||||
html5-qrcode@^2.3.8:
|
||||
version "2.3.8"
|
||||
resolved "https://registry.npmjs.org/html5-qrcode/-/html5-qrcode-2.3.8.tgz"
|
||||
integrity sha512-jsr4vafJhwoLVEDW3n1KvPnCCXWaQfRng0/EEYk1vNcQGcG/htAdhJX0be8YyqMoSz7+hZvOZSTAepsabiuhiQ==
|
||||
|
||||
http-cache-semantics@^4.1.0:
|
||||
version "4.1.1"
|
||||
resolved "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz"
|
||||
@@ -1894,7 +1914,7 @@ inflight@^1.0.4:
|
||||
once "^1.3.0"
|
||||
wrappy "1"
|
||||
|
||||
inherits@2, inherits@^2.0.3, inherits@^2.0.4:
|
||||
inherits@^2.0.3, inherits@^2.0.4, inherits@2:
|
||||
version "2.0.4"
|
||||
resolved "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz"
|
||||
integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==
|
||||
@@ -2147,7 +2167,7 @@ jiti@^2.4.2:
|
||||
resolved "https://registry.npmjs.org/jiti/-/jiti-2.4.2.tgz"
|
||||
integrity sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A==
|
||||
|
||||
jose@^4.15.5, jose@^4.15.9:
|
||||
jose@^4.14.4, jose@^4.15.5, jose@^4.15.9:
|
||||
version "4.15.9"
|
||||
resolved "https://registry.npmjs.org/jose/-/jose-4.15.9.tgz"
|
||||
integrity sha512-1vUQX+IdDMVPj4k8kOxgUqlcK518yluMuGZwqlr44FS1ppZB/5GWh4rZG89erpOBOJjU/OBsnCVFfapsRz6nEA==
|
||||
@@ -2263,47 +2283,47 @@ levn@^0.4.1:
|
||||
|
||||
lightningcss-darwin-arm64@1.29.2:
|
||||
version "1.29.2"
|
||||
resolved "https://registry.yarnpkg.com/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.29.2.tgz#6ceff38b01134af48e859394e1ca21e5d49faae6"
|
||||
resolved "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.29.2.tgz"
|
||||
integrity sha512-cK/eMabSViKn/PG8U/a7aCorpeKLMlK0bQeNHmdb7qUnBkNPnL+oV5DjJUo0kqWsJUapZsM4jCfYItbqBDvlcA==
|
||||
|
||||
lightningcss-darwin-x64@1.29.2:
|
||||
version "1.29.2"
|
||||
resolved "https://registry.yarnpkg.com/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.29.2.tgz#891b6f9e57682d794223c33463ca66d3af3fb038"
|
||||
resolved "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.29.2.tgz"
|
||||
integrity sha512-j5qYxamyQw4kDXX5hnnCKMf3mLlHvG44f24Qyi2965/Ycz829MYqjrVg2H8BidybHBp9kom4D7DR5VqCKDXS0w==
|
||||
|
||||
lightningcss-freebsd-x64@1.29.2:
|
||||
version "1.29.2"
|
||||
resolved "https://registry.yarnpkg.com/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.29.2.tgz#8a95f9ab73b2b2b0beefe1599fafa8b058938495"
|
||||
resolved "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.29.2.tgz"
|
||||
integrity sha512-wDk7M2tM78Ii8ek9YjnY8MjV5f5JN2qNVO+/0BAGZRvXKtQrBC4/cn4ssQIpKIPP44YXw6gFdpUF+Ps+RGsCwg==
|
||||
|
||||
lightningcss-linux-arm-gnueabihf@1.29.2:
|
||||
version "1.29.2"
|
||||
resolved "https://registry.yarnpkg.com/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.29.2.tgz#5c60bbf92b39d7ed51e363f7b98a7111bf5914a1"
|
||||
resolved "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.29.2.tgz"
|
||||
integrity sha512-IRUrOrAF2Z+KExdExe3Rz7NSTuuJ2HvCGlMKoquK5pjvo2JY4Rybr+NrKnq0U0hZnx5AnGsuFHjGnNT14w26sg==
|
||||
|
||||
lightningcss-linux-arm64-gnu@1.29.2:
|
||||
version "1.29.2"
|
||||
resolved "https://registry.yarnpkg.com/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.29.2.tgz#e73d7608c4cce034c3654e5e8b53be74846224de"
|
||||
resolved "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.29.2.tgz"
|
||||
integrity sha512-KKCpOlmhdjvUTX/mBuaKemp0oeDIBBLFiU5Fnqxh1/DZ4JPZi4evEH7TKoSBFOSOV3J7iEmmBaw/8dpiUvRKlQ==
|
||||
|
||||
lightningcss-linux-arm64-musl@1.29.2:
|
||||
version "1.29.2"
|
||||
resolved "https://registry.yarnpkg.com/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.29.2.tgz#a95a18d5a909831c092e0a8d2de4b9ac1a8db151"
|
||||
resolved "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.29.2.tgz"
|
||||
integrity sha512-Q64eM1bPlOOUgxFmoPUefqzY1yV3ctFPE6d/Vt7WzLW4rKTv7MyYNky+FWxRpLkNASTnKQUaiMJ87zNODIrrKQ==
|
||||
|
||||
lightningcss-linux-x64-gnu@1.29.2:
|
||||
version "1.29.2"
|
||||
resolved "https://registry.yarnpkg.com/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.29.2.tgz#551ca07e565394928642edee92acc042e546cb78"
|
||||
resolved "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.29.2.tgz"
|
||||
integrity sha512-0v6idDCPG6epLXtBH/RPkHvYx74CVziHo6TMYga8O2EiQApnUPZsbR9nFNrg2cgBzk1AYqEd95TlrsL7nYABQg==
|
||||
|
||||
lightningcss-linux-x64-musl@1.29.2:
|
||||
version "1.29.2"
|
||||
resolved "https://registry.yarnpkg.com/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.29.2.tgz#2fd164554340831bce50285b57101817850dd258"
|
||||
resolved "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.29.2.tgz"
|
||||
integrity sha512-rMpz2yawkgGT8RULc5S4WiZopVMOFWjiItBT7aSfDX4NQav6M44rhn5hjtkKzB+wMTRlLLqxkeYEtQ3dd9696w==
|
||||
|
||||
lightningcss-win32-arm64-msvc@1.29.2:
|
||||
version "1.29.2"
|
||||
resolved "https://registry.yarnpkg.com/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.29.2.tgz#da43ea49fafc5d2de38e016f1a8539d5eed98318"
|
||||
resolved "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.29.2.tgz"
|
||||
integrity sha512-nL7zRW6evGQqYVu/bKGK+zShyz8OVzsCotFgc7judbt6wnB2KbiKKJwBE4SGoDBQ1O94RjW4asrCjQL4i8Fhbw==
|
||||
|
||||
lightningcss-win32-x64-msvc@1.29.2:
|
||||
@@ -2817,15 +2837,6 @@ possible-typed-array-names@^1.0.0:
|
||||
resolved "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz"
|
||||
integrity sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==
|
||||
|
||||
postcss@8.4.31:
|
||||
version "8.4.31"
|
||||
resolved "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz"
|
||||
integrity sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==
|
||||
dependencies:
|
||||
nanoid "^3.3.6"
|
||||
picocolors "^1.0.0"
|
||||
source-map-js "^1.0.2"
|
||||
|
||||
postcss@^8.4.41:
|
||||
version "8.5.3"
|
||||
resolved "https://registry.npmjs.org/postcss/-/postcss-8.5.3.tgz"
|
||||
@@ -2835,6 +2846,15 @@ postcss@^8.4.41:
|
||||
picocolors "^1.1.1"
|
||||
source-map-js "^1.2.1"
|
||||
|
||||
postcss@8.4.31:
|
||||
version "8.4.31"
|
||||
resolved "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz"
|
||||
integrity sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==
|
||||
dependencies:
|
||||
nanoid "^3.3.6"
|
||||
picocolors "^1.0.0"
|
||||
source-map-js "^1.0.2"
|
||||
|
||||
preact-render-to-string@^5.1.19:
|
||||
version "5.2.6"
|
||||
resolved "https://registry.npmjs.org/preact-render-to-string/-/preact-render-to-string-5.2.6.tgz"
|
||||
@@ -2942,15 +2962,6 @@ react-is@^16.13.1:
|
||||
resolved "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz"
|
||||
integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==
|
||||
|
||||
react-qr-reader@^3.0.0-beta-1:
|
||||
version "3.0.0-beta-1"
|
||||
resolved "https://registry.npmjs.org/react-qr-reader/-/react-qr-reader-3.0.0-beta-1.tgz"
|
||||
integrity sha512-5HeFH9x/BlziRYQYGK2AeWS9WiKYZtGGMs9DXy3bcySTX3C9UJL9EwcPnWw8vlf7JP4FcrAlr1SnZ5nsWLQGyw==
|
||||
dependencies:
|
||||
"@zxing/browser" "0.0.7"
|
||||
"@zxing/library" "^0.18.3"
|
||||
rollup "^2.67.2"
|
||||
|
||||
react@^19.0.0:
|
||||
version "19.0.0"
|
||||
resolved "https://registry.npmjs.org/react/-/react-19.0.0.tgz"
|
||||
@@ -3041,13 +3052,6 @@ rimraf@^3.0.2:
|
||||
dependencies:
|
||||
glob "^7.1.3"
|
||||
|
||||
rollup@^2.67.2:
|
||||
version "2.79.2"
|
||||
resolved "https://registry.npmjs.org/rollup/-/rollup-2.79.2.tgz"
|
||||
integrity sha512-fS6iqSPZDs3dr/y7Od6y5nha8dW1YnbgtsyotCVvoFGKbERG++CVRFv1meyGDE1SNItQA8BrnCw7ScdAhRJ3XQ==
|
||||
optionalDependencies:
|
||||
fsevents "~2.3.2"
|
||||
|
||||
run-parallel@^1.1.9:
|
||||
version "1.2.0"
|
||||
resolved "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz"
|
||||
@@ -3283,6 +3287,11 @@ sprintf-js@^1.1.3:
|
||||
resolved "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz"
|
||||
integrity sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==
|
||||
|
||||
sqlite@^5.1.1:
|
||||
version "5.1.1"
|
||||
resolved "https://registry.npmjs.org/sqlite/-/sqlite-5.1.1.tgz"
|
||||
integrity sha512-oBkezXa2hnkfuJwUo44Hl9hS3er+YFtueifoajrgidvqsJRQFpc5fKoAkAor1O5ZnLoa28GBScfHXs8j0K358Q==
|
||||
|
||||
sqlite3@^5.1.7:
|
||||
version "5.1.7"
|
||||
resolved "https://registry.npmjs.org/sqlite3/-/sqlite3-5.1.7.tgz"
|
||||
@@ -3295,11 +3304,6 @@ sqlite3@^5.1.7:
|
||||
optionalDependencies:
|
||||
node-gyp "8.x"
|
||||
|
||||
sqlite@^5.1.1:
|
||||
version "5.1.1"
|
||||
resolved "https://registry.npmjs.org/sqlite/-/sqlite-5.1.1.tgz"
|
||||
integrity sha512-oBkezXa2hnkfuJwUo44Hl9hS3er+YFtueifoajrgidvqsJRQFpc5fKoAkAor1O5ZnLoa28GBScfHXs8j0K358Q==
|
||||
|
||||
ssri@^8.0.0, ssri@^8.0.1:
|
||||
version "8.0.1"
|
||||
resolved "https://registry.npmjs.org/ssri/-/ssri-8.0.1.tgz"
|
||||
@@ -3317,6 +3321,13 @@ streamsearch@^1.1.0:
|
||||
resolved "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz"
|
||||
integrity sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==
|
||||
|
||||
string_decoder@^1.1.1:
|
||||
version "1.3.0"
|
||||
resolved "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz"
|
||||
integrity sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==
|
||||
dependencies:
|
||||
safe-buffer "~5.2.0"
|
||||
|
||||
"string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.2.3:
|
||||
version "4.2.3"
|
||||
resolved "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz"
|
||||
@@ -3394,13 +3405,6 @@ string.prototype.trimstart@^1.0.8:
|
||||
define-properties "^1.2.1"
|
||||
es-object-atoms "^1.0.0"
|
||||
|
||||
string_decoder@^1.1.1:
|
||||
version "1.3.0"
|
||||
resolved "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz"
|
||||
integrity sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==
|
||||
dependencies:
|
||||
safe-buffer "~5.2.0"
|
||||
|
||||
strip-ansi@^6.0.1:
|
||||
version "6.0.1"
|
||||
resolved "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz"
|
||||
@@ -3442,7 +3446,7 @@ supports-preserve-symlinks-flag@^1.0.0:
|
||||
resolved "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz"
|
||||
integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==
|
||||
|
||||
tailwindcss@4.0.12, tailwindcss@^4:
|
||||
tailwindcss@^4, tailwindcss@4.0.12:
|
||||
version "4.0.12"
|
||||
resolved "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.0.12.tgz"
|
||||
integrity sha512-bT0hJo91FtncsAMSsMzUkoo/iEU0Xs5xgFgVC9XmdM9bw5MhZuQFjPNl6wxAE0SiQF/YTZJa+PndGWYSDtuxAg==
|
||||
@@ -3505,7 +3509,7 @@ ts-api-utils@^2.0.1:
|
||||
resolved "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.0.1.tgz"
|
||||
integrity sha512-dnlgjFSVetynI8nzgJ+qF62efpglpWRk8isUEWZGWlJYySCTD6aKvbUDu+zbPeDakk3bg5H4XpitHukgfL1m9w==
|
||||
|
||||
ts-custom-error@^3.0.0:
|
||||
ts-custom-error@^3.2.1:
|
||||
version "3.3.1"
|
||||
resolved "https://registry.npmjs.org/ts-custom-error/-/ts-custom-error-3.3.1.tgz"
|
||||
integrity sha512-5OX1tzOjxWEgsr/YEUWSuPrQ00deKLh6D7OTWcvNHm12/7QPyRh8SYpyWvA4IZv8H/+GQWQEh/kwo95Q9OVW1A==
|
||||
|
||||
Reference in new issue
Block a user