This commit is contained in:
bobbert committed 2025-03-16 21:40:28 +00:00
1 parent c5f2c0a98a
commit 25e877cc3a
6 files changed
+490 -154

No files matched your search

+329 -101
View File
@@ -1,8 +1,79 @@
/**
* Utility module for processing and populating the treasure map SVG.
* Handles clue points, their status, and interaction.
*/
// Global variable to track if we've added our custom styles
let stylesAdded = false;
/**
* Adds custom styles to the document head for clue points
*/
function addCustomStyles() {
if (stylesAdded) return;
const style = document.createElement("style");
style.id = "map-custom-styles";
style.textContent = `
@keyframes cluePointPulse {
0% { stroke-width: 3px; transform: scale(1); filter: drop-shadow(0px 0px 3px rgba(255, 152, 0, 0.9)); }
50% { stroke-width: 6px; transform: scale(1.2); filter: drop-shadow(0px 0px 10px rgba(255, 152, 0, 1)); }
100% { stroke-width: 3px; transform: scale(1); filter: drop-shadow(0px 0px 3px rgba(255, 152, 0, 0.9)); }
}
/* Ensure SVG elements use CSS z-index correctly */
svg * {
transform-box: fill-box;
stroke-width: 2px;
pointer-events: all !important;
transition: fill 0.2s ease, stroke-width 0.2s ease, transform 0.2s ease !important;
z-index: 100;
position: relative;
}
.clue-point:hover {
fill: #ffb74d !important;
stroke: #ff6d00 !important;
stroke-width: 4px !important;
transform: scale(1.2) !important;
z-index: 200 !important;
filter: drop-shadow(0px 0px 4px rgba(0, 0, 0, 0.5)) !important;
}
.clue-point-found {
fill: #2e7d32 !important;
stroke: #1b5e20 !important;
}
.clue-point-current {
fill: #ff9800 !important;
stroke: #e65100 !important;
animation: cluePointPulse 2s infinite !important;
}
.clue-point-skipped {
fill: #1976d2 !important;
stroke: #0d47a1 !important;
}
.clue-point-locked {
fill: #9e9e9e !important;
stroke: #757575 !important;
}
.clue-click-area {
opacity: 0;
fill: transparent;
stroke: transparent;
pointer-events: all !important;
cursor: pointer !important;
}
`;
document.head.appendChild(style);
stylesAdded = true;
console.log("Custom map styles added to document");
}
/**
* Populates the SVG map with clue points
* @param {SVGElement} svgElement - The SVG DOM element to process
@@ -10,62 +81,139 @@
*/
export async function populateMap(svgElement) {
try {
console.log("Starting map population process");
// Add custom styles to document if not already added
addCustomStyles();
if (!svgElement) {
console.error("No SVG element provided");
return false;
}
console.log("Starting map population process");
// Clear any existing clue points and click areas to avoid duplicates
const existingCluePoints = svgElement.querySelectorAll(
".clue-point, .clue-click-area"
);
existingCluePoints.forEach((element) => {
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;
}
}
View File
Whitespace-only changes.