From 25e877cc3a2a706e124afa8bc15f9b63a0974cc2 Mon Sep 17 00:00:00 2001 From: mudabbir-ahmad Date: Sun, 16 Mar 2025 21:40:28 +0000 Subject: [PATCH] map WIP --- app/(main)/map/map.module.css | 81 ++++++- app/(main)/map/page.js | 128 ++++++---- app/api/clues/count/route.js | 5 +- clue_hunt.db | Bin 86016 -> 86016 bytes utils/map-populate.js | 430 ++++++++++++++++++++++++++-------- utils/map-visibility.js | 0 6 files changed, 490 insertions(+), 154 deletions(-) create mode 100644 utils/map-visibility.js diff --git a/app/(main)/map/map.module.css b/app/(main)/map/map.module.css index 60455a7..34d213c 100644 --- a/app/(main)/map/map.module.css +++ b/app/(main)/map/map.module.css @@ -85,6 +85,7 @@ 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 */ } .svg-container svg { @@ -95,7 +96,12 @@ object-fit: none; /* Don't constrain the SVG */ } -/* Map background - display as full-page map */ +/* Remove the default map background when we have the processed SVG */ +.svg-container + .map-background { + display: none; +} + +/* Map background - only shown when SVG is loading */ .map-background { position: absolute; top: 0; @@ -106,7 +112,7 @@ background-size: auto; /* Don't resize the background */ background-position: top right; /* Position at top right */ background-repeat: no-repeat; - z-index: 5; + z-index: 3; /* Lower z-index than the processed SVG */ } /* Circle elements in the SVG map - attach to a container class */ @@ -119,6 +125,70 @@ 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 */ @@ -133,8 +203,13 @@ flex-direction: column; align-items: center; justify-content: center; - height: 100vh; + height: 100%; width: 100%; + position: absolute; + top: 0; + left: 0; + z-index: 50; + background-color: rgba(105, 181, 168, 0.8); } .loading-spinner { diff --git a/app/(main)/map/page.js b/app/(main)/map/page.js index 3ea89c8..924f76e 100644 --- a/app/(main)/map/page.js +++ b/app/(main)/map/page.js @@ -127,6 +127,74 @@ 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 { @@ -143,7 +211,7 @@ export default function MapPage() { } const svgText = await response.text(); - console.log("SVG loaded, length:", svgText.length); + console.log(`SVG loaded, length: ${svgText.length} characters`); // Ensure SVG has correct namespaces and viewBox let fixedSvg = svgText.replace( @@ -174,42 +242,6 @@ 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"); - return; - } - - console.log("SVG element found, starting enhancement"); - - // Use our utility to populate the map with clue points - populateMap(svgElement) - .then((success) => { - if (success) { - console.log("Map populated successfully"); - - // After populating the map, update the status of each clue point - updateCluePointStatus(svgElement, clues); - - // Attach click handlers for navigation - attachClueClickHandlers(svgElement, clues, (clueId) => { - router.push(`/clue/${clueId}`); - }); - } else { - console.error("Failed to populate map"); - } - }) - .catch((error) => { - console.error("Error enhancing SVG map:", error); - setMapError(true); - }); - }, [clues, router]); - // Function to check hunt status and timer async function checkHuntAndTimer() { if (!isMounted.current) return; @@ -538,23 +570,23 @@ export default function MapPage() { // Only try to enhance the map if both the SVG and clues are loaded if (!mapSvg || !clues.length) return; - console.log("Both SVG and clues loaded, enhancing map"); + console.log("Both SVG and clues are loaded, preparing to enhance map"); - // Use requestAnimationFrame to ensure the DOM has been updated with the SVG + // Ensure the SVG is fully loaded in the DOM before enhancing requestAnimationFrame(() => { - // Short timeout to ensure the SVG is fully rendered in the DOM + // Longer timeout to ensure the SVG is fully rendered in the DOM setTimeout(() => { if ( mapContainerRef.current && mapContainerRef.current.querySelector("svg") ) { - console.log("SVG found in DOM, enhancing map"); + console.log("SVG found in DOM, enhancing map now"); enhanceSvgMap(); } else { - console.error("SVG not found in DOM after rendering"); + console.error("SVG not found in DOM after rendering timeout"); setMapError(true); } - }, 100); + }, 500); // Increased timeout for better reliability on slower devices }); }, [mapSvg, clues, enhanceSvgMap]); @@ -627,14 +659,12 @@ export default function MapPage() {
console.log("SVG container loaded")} /> ) : ( -
+
+
+
Loading map...
+
)}
diff --git a/app/api/clues/count/route.js b/app/api/clues/count/route.js index ae98af7..ec5efdc 100644 --- a/app/api/clues/count/route.js +++ b/app/api/clues/count/route.js @@ -17,13 +17,16 @@ export async function GET(request) { return Response.json({ error: "Unauthorized access" }, { status: 401 }); } - // Get the total count of clues + // Get the total count of clues with a direct SQL COUNT query const result = await queryOne("SELECT COUNT(*) as count FROM clues"); if (!result) { return Response.json({ error: "Failed to count clues" }, { status: 500 }); } + // Log the count to help with debugging + console.log(`API: Returning clue count: ${result.count}`); + return Response.json( { count: result.count }, { diff --git a/clue_hunt.db b/clue_hunt.db index c1577225908b0b1a8e15267d606372ce01ad3011..991e92de6808fc3f7acf1a01ed149b083284dd0f 100644 GIT binary patch delta 108 zcmZozz}m2Yb%Hcw!$cWp#)gdv&i0Iqo0r%p7%&^NicDrNNM&Lb*=#7VjK$oriII(g y! { + if (element.parentNode) { + element.parentNode.removeChild(element); + } + }); - // First, find all existing circle elements in the SVG - const existingCircles = Array.from(svgElement.querySelectorAll("circle")); - console.log(`Found ${existingCircles.length} existing circles in SVG`); + console.log("Cleared existing clue points"); + + // Find all circles in the SVG + const allCircles = Array.from(svgElement.querySelectorAll("circle")); + console.log(`Found ${allCircles.length} circles in SVG`); + + if (allCircles.length === 0) { + console.error("No circles found in SVG"); + return false; + } // Hide all existing circles initially - existingCircles.forEach((circle) => { - circle.style.display = "none"; - }); + allCircles.forEach((circle) => (circle.style.display = "none")); - // Query the number of clues via API - const clueCountResponse = await fetch("/api/clues/count", { - credentials: "include", - cache: "no-cache", - }); + try { + // Fetch the number of clues from the API + const response = await fetch("/api/clues/count", { + method: "GET", + credentials: "include", + headers: { + "Cache-Control": "no-cache, no-store, must-revalidate", + Pragma: "no-cache", + }, + }); - if (!clueCountResponse.ok) { - throw new Error( - `Failed to fetch clue count: ${clueCountResponse.status}` + if (!response.ok) { + throw new Error(`Failed to fetch clue count: ${response.status}`); + } + + const data = await response.json(); + const clueCount = data.count; + console.log(`API returned clue count: ${clueCount}`); + + if (!clueCount || clueCount <= 0) { + console.error("Invalid clue count returned from API"); + return false; + } + + // Sort circles by vertical position (top to bottom) + const sortedCircles = allCircles.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` ); + + const svgNamespace = "http://www.w3.org/2000/svg"; + + // Create a dedicated overlay group for clue points to ensure they're on top + let overlayGroup = svgElement.querySelector(".clue-points-overlay"); + if (!overlayGroup) { + overlayGroup = document.createElementNS(svgNamespace, "g"); + overlayGroup.setAttribute("class", "clue-points-overlay"); + overlayGroup.style.pointerEvents = "all"; + // Append to the end of SVG to ensure it's rendered last (on top) + svgElement.appendChild(overlayGroup); + } + + // Create interactive clue points by cloning and enhancing each original circle + for (let i = 0; i < circlesToShow; i++) { + const originalCircle = sortedCircles[i]; + + // Get original circle attributes + const cx = originalCircle.getAttribute("cx") || "0"; + const cy = originalCircle.getAttribute("cy") || "0"; + const r = Math.max( + parseFloat(originalCircle.getAttribute("r") || "5"), + 12 + ); + + // Create a click area (larger invisible circle for easier clicking) + const clickArea = document.createElementNS(svgNamespace, "circle"); + clickArea.setAttribute("cx", cx); + clickArea.setAttribute("cy", cy); + clickArea.setAttribute("r", (r * 2).toString()); + clickArea.setAttribute("class", "clue-click-area"); + clickArea.setAttribute("data-clue-id", (i + 1).toString()); + clickArea.setAttribute("data-index", i.toString()); + // Ensure click area has high z-index + clickArea.style.zIndex = "1000"; + + // Create a new visible circle for the clue point + const cluePoint = document.createElementNS(svgNamespace, "circle"); + cluePoint.setAttribute("cx", cx); + cluePoint.setAttribute("cy", cy); + cluePoint.setAttribute("r", r.toString()); + cluePoint.setAttribute("class", "clue-point clue-point-locked"); + cluePoint.setAttribute("data-clue-id", (i + 1).toString()); + cluePoint.setAttribute("data-index", i.toString()); + // Ensure clue point has high z-index + cluePoint.style.zIndex = "1001"; + + // Add a tooltip for accessibility + const tooltip = document.createElementNS(svgNamespace, "title"); + tooltip.textContent = `Clue #${i + 1}`; + cluePoint.appendChild(tooltip); + + // Add the new elements to the overlay group instead of original parent + overlayGroup.appendChild(clickArea); + overlayGroup.appendChild(cluePoint); + } + + console.log(`Successfully created ${circlesToShow} clue points`); + return true; + } catch (error) { + console.error("Error fetching clue count:", error); + return false; } - - const { count: clueCount } = await clueCountResponse.json(); - console.log(`Total clues: ${clueCount}`); - - // Determine how many circles to display (min of existing circles and clue count) - const circlesToShow = Math.min(existingCircles.length, clueCount); - console.log(`Will display ${circlesToShow} circles`); - - // Sort circles by vertical position (top to bottom) - const sortedCircles = existingCircles.sort((a, b) => { - const aY = parseFloat(a.getAttribute("cy")); - const bY = parseFloat(b.getAttribute("cy")); - return aY - bY; - }); - - // Display only the needed circles and add clue-point class - for (let i = 0; i < circlesToShow; i++) { - const circle = sortedCircles[i]; - circle.style.display = ""; - circle.classList.add("clue-point"); - circle.setAttribute("data-clue-id", i + 1); - - // Set default appearance - circle.style.fill = "#9e9e9e"; // Default grey color - circle.style.stroke = "#757575"; - circle.style.strokeWidth = "2"; - } - - return true; } catch (error) { console.error("Error populating map:", error); return false; @@ -73,62 +221,92 @@ export async function populateMap(svgElement) { } /** - * Updates the status of each clue point based on clues data + * Updates the clue points based on the latest clue data * @param {SVGElement} svgElement - The SVG DOM element * @param {Array} clues - Array of clue objects with their status */ export function updateCluePointStatus(svgElement, clues) { - if (!svgElement || !clues || !clues.length) { - console.error("Missing required parameters for updating clue status"); - return; - } - - console.log("Updating clue point status"); - try { - // Get all clue points - const cluePoints = svgElement.querySelectorAll(".clue-point"); - - if (!cluePoints.length) { - console.error("No clue points found in SVG"); - return; + if (!svgElement || !clues || !Array.isArray(clues) || clues.length === 0) { + console.error("Invalid parameters for updateCluePointStatus"); + return false; } - // Find the first unfound clue (the current one) - const currentClueIndex = clues.findIndex((clue) => !clue.found); - const currentClueId = - currentClueIndex !== -1 ? clues[currentClueIndex].id : null; + console.log(`Updating status for ${clues.length} clues`); - // Process each clue point - cluePoints.forEach((circle, index) => { + // Find all clue points + const cluePoints = svgElement.querySelectorAll(".clue-point"); + const clickAreas = svgElement.querySelectorAll(".clue-click-area"); + + if (cluePoints.length === 0) { + console.error("No clue points found to update"); + return false; + } + + console.log( + `Found ${cluePoints.length} clue points and ${clickAreas.length} click areas to update` + ); + + // Find the first unfound clue (the current clue) + const currentIndex = clues.findIndex((clue) => !clue.found); + const currentClueId = currentIndex !== -1 ? clues[currentIndex].id : null; + + console.log(`Current clue ID: ${currentClueId}`); + + // Update each clue point's status + cluePoints.forEach((point, index) => { if (index >= clues.length) return; const clue = clues[index]; const clueId = clue.id; - // Determine status and apply appropriate color + // Remove all status classes first + point.classList.remove( + "clue-point-found", + "clue-point-current", + "clue-point-skipped", + "clue-point-locked" + ); + + // Update tooltip + const tooltip = + point.querySelector("title") || + document.createElementNS("http://www.w3.org/2000/svg", "title"); + + // Determine the clue's status and apply appropriate class if (clue.found) { - // Found/completed clue - Dark Green - circle.style.fill = "#2e7d32"; // Dark green + point.classList.add("clue-point-found"); + tooltip.textContent = `Clue ${clueId}: ${clue.title || "Completed"}`; } else if (clueId === currentClueId) { - // Current clue - Orange - circle.style.fill = "#ff9800"; // Orange + point.classList.add("clue-point-current"); + tooltip.textContent = `Current Clue: ${ + clue.title || "Go to this location" + }`; } else if (clueId < currentClueId) { - // Skipped clue - Blue - circle.style.fill = "#1976d2"; // Blue + point.classList.add("clue-point-skipped"); + tooltip.textContent = `Skipped Clue ${clueId}: ${ + clue.title || "Skipped" + }`; } else { - // Not accessible yet - Grey - circle.style.fill = "#9e9e9e"; // Grey + point.classList.add("clue-point-locked"); + tooltip.textContent = `Locked: Complete previous clues first`; } - // Mark whether this clue point is available + // Store availability data const isAvailable = clue.found || clueId === currentClueId; - circle.setAttribute("data-available", isAvailable.toString()); + point.setAttribute("data-available", isAvailable.toString()); + + // Make sure tooltip is attached + if (!point.contains(tooltip)) { + point.appendChild(tooltip); + } }); - console.log("Clue points updated successfully"); + console.log("Clue point status updated successfully"); + return true; } catch (error) { - console.error("Error updating clue status:", error); + console.error("Error updating clue point status:", error); + return false; } } @@ -139,39 +317,89 @@ export function updateCluePointStatus(svgElement, clues) { * @param {Function} navigationHandler - Function to handle navigation */ export function attachClueClickHandlers(svgElement, clues, navigationHandler) { - if (!svgElement || !clues || !navigationHandler) { - console.error("Missing required parameters for attaching click handlers"); - return; - } - try { - const cluePoints = svgElement.querySelectorAll(".clue-point"); + if (!svgElement || !clues || !navigationHandler) { + console.error("Invalid parameters for attachClueClickHandlers"); + return false; + } - cluePoints.forEach((circle, index) => { - if (index >= clues.length) return; + // Get all interactive elements (clue points and their click areas) + const cluePoints = Array.from(svgElement.querySelectorAll(".clue-point")); + const clickAreas = Array.from( + svgElement.querySelectorAll(".clue-click-area") + ); + + console.log( + `Attaching click handlers to ${cluePoints.length} clue points and ${clickAreas.length} click areas` + ); + + // Find the current active clue + const currentIndex = clues.findIndex((clue) => !clue.found); + + // Helper function to process a click on a clue point + const processClueClick = (element) => { + const index = parseInt(element.getAttribute("data-index"), 10); + if (isNaN(index) || index < 0 || index >= clues.length) { + console.error(`Invalid clue index: ${index}`); + return; + } const clue = clues[index]; - const isAvailable = circle.getAttribute("data-available") === "true"; + const isAvailable = clue.found || index === currentIndex; - // Set cursor style based on availability - circle.style.cursor = isAvailable ? "pointer" : "not-allowed"; + console.log(`Clue ${clue.id} clicked, available: ${isAvailable}`); - // Add click handler - circle.onclick = (e) => { + if (isAvailable) { + // Show click feedback + const cluePoint = cluePoints[index]; + if (cluePoint) { + // Briefly change color for visual feedback + const originalClasses = [...cluePoint.classList]; + cluePoint.classList.add("click-feedback"); + + setTimeout(() => { + // Navigate to the clue page + navigationHandler(clue.id); + }, 100); + } else { + navigationHandler(clue.id); + } + } else { + // Alert about locked clue + alert("Complete previous clues to unlock this one!"); + } + }; + + // Attach new click handlers to clue points + cluePoints.forEach((point) => { + // Remove existing handlers + point.onclick = null; + + // Add new click handler + point.addEventListener("click", (e) => { e.preventDefault(); e.stopPropagation(); - - if (isAvailable) { - console.log(`Clicked on clue ${clue.id}`); - navigationHandler(clue.id); - } else { - // Alert or other feedback if clue is not accessible - console.log("Clue not available yet"); - // Could show a tooltip or message here - } - }; + processClueClick(point); + }); }); + + // Attach click handlers to click areas (larger invisible circles) + clickAreas.forEach((area) => { + // Remove existing handlers + area.onclick = null; + + // Add new click handler - these use the same handler as clue points + area.addEventListener("click", (e) => { + e.preventDefault(); + e.stopPropagation(); + processClueClick(area); + }); + }); + + console.log("Click handlers attached successfully"); + return true; } catch (error) { console.error("Error attaching click handlers:", error); + return false; } } diff --git a/utils/map-visibility.js b/utils/map-visibility.js new file mode 100644 index 0000000..e69de29