diff --git a/app/(main)/map/page.js b/app/(main)/map/page.js index fec6339..3ea89c8 100644 --- a/app/(main)/map/page.js +++ b/app/(main)/map/page.js @@ -10,7 +10,11 @@ import { clearHuntTimer, formatElapsedTime, } from "@/utils/timerService"; -import { populateMap, updateCluePointStatus } from "@/utils/map-populate"; +import { + populateMap, + updateCluePointStatus, + attachClueClickHandlers, +} from "@/utils/map-populate"; export default function MapPage() { const { data: session, status } = useSession(); @@ -188,59 +192,13 @@ export default function MapPage() { .then((success) => { if (success) { console.log("Map populated successfully"); + // After populating the map, update the status of each clue point updateCluePointStatus(svgElement, clues); - // Add click handlers to each clue point - const cluePoints = svgElement.querySelectorAll(".clue-point"); - cluePoints.forEach((circle, index) => { - if (index < clues.length) { - const clue = clues[index]; - const isAvailable = clue.available; - - // Make circle interactive - circle.style.cursor = isAvailable ? "pointer" : "not-allowed"; - - // Add click handler - circle.onclick = (e) => { - e.preventDefault(); - e.stopPropagation(); - - if (isAvailable) { - console.log(`Navigating to clue ${clue.id}`); - router.push(`/clue/${clue.id}`); - } else { - alert("Complete previous clues first to unlock this one!"); - } - }; - - // Add hover effects - circle.onmouseenter = () => { - if (isAvailable) { - circle.style.fill = "#ffb74d"; // Lighter orange for hover - } - }; - - circle.onmouseleave = () => { - if (clue.found) { - circle.style.fill = "#4caf50"; // Back to green for found clues - } else if (isAvailable) { - circle.style.fill = "#ff9800"; // Back to orange for available clues - } else { - circle.style.fill = "#9e9e9e"; // Back to grey for unavailable clues - } - }; - - // Add tooltip with clue information - const label = document.createElementNS( - "http://www.w3.org/2000/svg", - "title" - ); - label.textContent = `Clue #${index + 1}: ${ - clue.title || "Mystery clue" - }${!isAvailable ? " (Locked)" : ""}`; - circle.appendChild(label); - } + // Attach click handlers for navigation + attachClueClickHandlers(svgElement, clues, (clueId) => { + router.push(`/clue/${clueId}`); }); } else { console.error("Failed to populate map"); diff --git a/app/api/clues/count/route.js b/app/api/clues/count/route.js new file mode 100644 index 0000000..ae98af7 --- /dev/null +++ b/app/api/clues/count/route.js @@ -0,0 +1,40 @@ +import { queryOne } from "@/lib/db"; +import { getToken } from "next-auth/jwt"; + +export const dynamic = "force-dynamic"; // No caching + +export async function GET(request) { + try { + // Verify token + const token = await getToken({ + req: request, + secret: + process.env.NEXTAUTH_SECRET || + "your-fallback-secret-should-be-at-least-32-chars", + }); + + if (!token) { + return Response.json({ error: "Unauthorized access" }, { status: 401 }); + } + + // Get the total count of clues + const result = await queryOne("SELECT COUNT(*) as count FROM clues"); + + if (!result) { + return Response.json({ error: "Failed to count clues" }, { status: 500 }); + } + + return Response.json( + { count: result.count }, + { + headers: { + "Cache-Control": "no-store, no-cache, must-revalidate", + Pragma: "no-cache", + }, + } + ); + } catch (error) { + console.error("Error counting clues:", error); + return Response.json({ error: "Server error" }, { status: 500 }); + } +} diff --git a/clue_hunt.db b/clue_hunt.db index a37b543..c157722 100644 Binary files a/clue_hunt.db and b/clue_hunt.db differ diff --git a/utils/map-populate.js b/utils/map-populate.js index 848715c..7a1315c 100644 --- a/utils/map-populate.js +++ b/utils/map-populate.js @@ -1,174 +1,71 @@ /** - * Utility to dynamically populate the map SVG with clue points - * based on the total number of clues in the database + * Utility module for processing and populating the treasure map SVG. + * Handles clue points, their status, and interaction. */ /** - * Fetches the total number of clues from the API - * @returns {Promise} The total number of clues + * Populates the SVG map with clue points + * @param {SVGElement} svgElement - The SVG DOM element to process + * @returns {Promise} - Success flag */ -async function fetchTotalClues() { +export async function populateMap(svgElement) { try { - const response = await fetch("/api/clues", { - method: "GET", - headers: { - "Content-Type": "application/json", - "Cache-Control": "no-store, no-cache, must-revalidate", - }, - credentials: "include", - }); - - if (!response.ok) { - throw new Error(`Failed to fetch clues: ${response.status}`); + if (!svgElement) { + console.error("No SVG element provided"); + return false; } - const clues = await response.json(); - return clues.length; - } catch (error) { - console.error("Error fetching total clues:", error); - return 0; - } -} + console.log("Starting map population process"); -/** - * Shows the specified number of clue points on the map - * @param {SVGElement} svgElement - The SVG DOM element of the map - * @param {number} numClues - Number of clues to display - * @returns {boolean} Success status - */ -function showCluePoints(svgElement, numClues) { - if (!svgElement || !numClues) { - console.error("Invalid SVG element or number of clues"); - return false; - } + // 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`); - try { - // Find all clue points in the SVG - improved selector for better compatibility - const cluePoints = Array.from(svgElement.querySelectorAll("circle")).filter( - (circle) => - circle.classList.contains("clue-point") || - circle.getAttribute("class")?.includes("clue-point") - ); - - console.log(`Found ${cluePoints.length} clue points in SVG`); - - // If no clue points are found, add them programmatically - if (cluePoints.length === 0) { - console.log("No clue points found, creating them dynamically"); - return createCluePoints(svgElement, numClues); - } - - // Hide all clue points first - cluePoints.forEach((circle) => { + // Hide all existing circles initially + existingCircles.forEach((circle) => { circle.style.display = "none"; }); - // Show only the required number of clue points - for (let i = 0; i < Math.min(numClues, cluePoints.length); i++) { - const circle = cluePoints[i]; - circle.style.display = ""; - // Ensure the circle has the clue-point class and a data-clue-id - circle.classList.add("clue-point"); - if (!circle.hasAttribute("data-clue-id")) { - circle.setAttribute("data-clue-id", (i + 1).toString()); - } - } - - console.log( - `Showing ${Math.min(numClues, cluePoints.length)} clue points on map` - ); - return true; - } catch (error) { - console.error("Error showing clue points:", error); - return false; - } -} - -/** - * Creates clue points dynamically if they don't exist in the SVG - * @param {SVGElement} svgElement - The SVG DOM element of the map - * @param {number} numClues - Number of clues to create - * @returns {boolean} Success status - */ -function createCluePoints(svgElement, numClues) { - try { - const svgWidth = svgElement.viewBox.baseVal.width || 800; - const svgHeight = svgElement.viewBox.baseVal.height || 600; - - // Define positions for clue points - spread across the map - const positions = []; - - // Calculate positions in a grid pattern - const cols = Math.ceil(Math.sqrt(numClues)); - const rows = Math.ceil(numClues / cols); - - const marginX = svgWidth * 0.1; - const marginY = svgHeight * 0.1; - const spacingX = (svgWidth - 2 * marginX) / (cols - 1 || 1); - const spacingY = (svgHeight - 2 * marginY) / (rows - 1 || 1); - - for (let row = 0; row < rows; row++) { - for (let col = 0; col < cols; col++) { - const index = row * cols + col; - if (index < numClues) { - positions.push({ - x: marginX + col * spacingX, - y: marginY + row * spacingY, - }); - } - } - } - - // Create circles for each clue - positions.forEach((pos, index) => { - const circle = document.createElementNS( - "http://www.w3.org/2000/svg", - "circle" - ); - circle.setAttribute("cx", pos.x); - circle.setAttribute("cy", pos.y); - circle.setAttribute("r", 10); - circle.setAttribute("fill", "#9e9e9e"); - circle.setAttribute("stroke", "#757575"); - circle.setAttribute("stroke-width", "2"); - circle.setAttribute("class", "clue-point"); - circle.setAttribute("data-clue-id", (index + 1).toString()); - - svgElement.appendChild(circle); + // Query the number of clues via API + const clueCountResponse = await fetch("/api/clues/count", { + credentials: "include", + cache: "no-cache", }); - console.log(`Created ${numClues} clue points dynamically`); - return true; - } catch (error) { - console.error("Error creating clue points:", error); - return false; - } -} - -/** - * Main function to populate map with the correct number of clue points - * @param {SVGElement} svgElement - The SVG DOM element of the map - * @returns {Promise} Success status - */ -export async function populateMap(svgElement) { - if (!svgElement) { - console.error("No SVG element provided"); - return false; - } - - try { - console.log("Starting to populate map"); - // Fetch total number of clues - const totalClues = await fetchTotalClues(); - console.log("Total clues fetched:", totalClues); - - if (totalClues > 0) { - // Show the appropriate number of clue points - return showCluePoints(svgElement, totalClues); - } else { - console.warn("No clues found in database"); - return false; + if (!clueCountResponse.ok) { + throw new Error( + `Failed to fetch clue count: ${clueCountResponse.status}` + ); } + + 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; @@ -176,41 +73,105 @@ export async function populateMap(svgElement) { } /** - * Updates the styling of clue points based on their status - * @param {SVGElement} svgElement - The SVG DOM element of the map - * @param {Array} clues - Array of clue objects with status information + * Updates the status of each clue point based on clues 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("Invalid SVG element or clues data"); + 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; + } + + // Find the first unfound clue (the current one) + const currentClueIndex = clues.findIndex((clue) => !clue.found); + const currentClueId = + currentClueIndex !== -1 ? clues[currentClueIndex].id : null; + + // Process each clue point + cluePoints.forEach((circle, index) => { + if (index >= clues.length) return; + + const clue = clues[index]; + const clueId = clue.id; + + // Determine status and apply appropriate color + if (clue.found) { + // Found/completed clue - Dark Green + circle.style.fill = "#2e7d32"; // Dark green + } else if (clueId === currentClueId) { + // Current clue - Orange + circle.style.fill = "#ff9800"; // Orange + } else if (clueId < currentClueId) { + // Skipped clue - Blue + circle.style.fill = "#1976d2"; // Blue + } else { + // Not accessible yet - Grey + circle.style.fill = "#9e9e9e"; // Grey + } + + // Mark whether this clue point is available + const isAvailable = clue.found || clueId === currentClueId; + circle.setAttribute("data-available", isAvailable.toString()); + }); + + console.log("Clue points updated successfully"); + } catch (error) { + console.error("Error updating clue status:", error); + } +} + +/** + * Attaches click handlers to clue points + * @param {SVGElement} svgElement - The SVG DOM element + * @param {Array} clues - Array of clue objects with their status + * @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 { - clues.forEach((clue, index) => { - // Find the corresponding circle in the SVG - const circle = svgElement.querySelector( - `.clue-point[data-clue-id="${clue.id}"]` - ); + const cluePoints = svgElement.querySelectorAll(".clue-point"); - if (circle) { - // Apply appropriate styling based on clue status - if (clue.found) { - // Found clues are green - circle.style.fill = "#4caf50"; - circle.style.stroke = "#2e7d32"; - } else if (clue.available) { - // Available clues are orange - circle.style.fill = "#ff9800"; - circle.style.stroke = "#d87f00"; + cluePoints.forEach((circle, index) => { + if (index >= clues.length) return; + + const clue = clues[index]; + const isAvailable = circle.getAttribute("data-available") === "true"; + + // Set cursor style based on availability + circle.style.cursor = isAvailable ? "pointer" : "not-allowed"; + + // Add click handler + circle.onclick = (e) => { + e.preventDefault(); + e.stopPropagation(); + + if (isAvailable) { + console.log(`Clicked on clue ${clue.id}`); + navigationHandler(clue.id); } else { - // Unavailable clues remain grey - circle.style.fill = "#9e9e9e"; - circle.style.stroke = "#757575"; + // Alert or other feedback if clue is not accessible + console.log("Clue not available yet"); + // Could show a tooltip or message here } - } + }; }); } catch (error) { - console.error("Error updating clue point status:", error); + console.error("Error attaching click handlers:", error); } }