mirror of
https://github.com/mudabbir-ahmad/peworkshop.git
synced 2026-10-07 19:50:20 +00:00
map WIP
This commit is contained in:
1 parent
c5f2c0a98a
commit
25e877cc3a
6 files changed
+480
-144
No files matched your search
@@ -85,6 +85,7 @@
|
|||||||
height: fit-content; /* Allow natural height */
|
height: fit-content; /* Allow natural height */
|
||||||
display: block; /* Change to block for proper scrolling */
|
display: block; /* Change to block for proper scrolling */
|
||||||
overflow: visible; /* Show content beyond boundaries */
|
overflow: visible; /* Show content beyond boundaries */
|
||||||
|
z-index: 5; /* Ensure it's above the background */
|
||||||
}
|
}
|
||||||
|
|
||||||
.svg-container svg {
|
.svg-container svg {
|
||||||
@@ -95,7 +96,12 @@
|
|||||||
object-fit: none; /* Don't constrain the SVG */
|
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 {
|
.map-background {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0;
|
top: 0;
|
||||||
@@ -106,7 +112,7 @@
|
|||||||
background-size: auto; /* Don't resize the background */
|
background-size: auto; /* Don't resize the background */
|
||||||
background-position: top right; /* Position at top right */
|
background-position: top right; /* Position at top right */
|
||||||
background-repeat: no-repeat;
|
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 */
|
/* 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 */
|
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 */
|
/* Remaining styles unchanged */
|
||||||
.map-svg {
|
.map-svg {
|
||||||
width: 100% !important; /* Override Next.js Image default width */
|
width: 100% !important; /* Override Next.js Image default width */
|
||||||
@@ -133,8 +203,13 @@
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
height: 100vh;
|
height: 100%;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
z-index: 50;
|
||||||
|
background-color: rgba(105, 181, 168, 0.8);
|
||||||
}
|
}
|
||||||
|
|
||||||
.loading-spinner {
|
.loading-spinner {
|
||||||
|
|||||||
+79
-49
@@ -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
|
// Function to load and enhance the SVG map
|
||||||
const loadAndEnhanceMap = useCallback(async () => {
|
const loadAndEnhanceMap = useCallback(async () => {
|
||||||
try {
|
try {
|
||||||
@@ -143,7 +211,7 @@ export default function MapPage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const svgText = await response.text();
|
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
|
// Ensure SVG has correct namespaces and viewBox
|
||||||
let fixedSvg = svgText.replace(
|
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
|
// Function to check hunt status and timer
|
||||||
async function checkHuntAndTimer() {
|
async function checkHuntAndTimer() {
|
||||||
if (!isMounted.current) return;
|
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
|
// Only try to enhance the map if both the SVG and clues are loaded
|
||||||
if (!mapSvg || !clues.length) return;
|
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(() => {
|
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(() => {
|
setTimeout(() => {
|
||||||
if (
|
if (
|
||||||
mapContainerRef.current &&
|
mapContainerRef.current &&
|
||||||
mapContainerRef.current.querySelector("svg")
|
mapContainerRef.current.querySelector("svg")
|
||||||
) {
|
) {
|
||||||
console.log("SVG found in DOM, enhancing map");
|
console.log("SVG found in DOM, enhancing map now");
|
||||||
enhanceSvgMap();
|
enhanceSvgMap();
|
||||||
} else {
|
} else {
|
||||||
console.error("SVG not found in DOM after rendering");
|
console.error("SVG not found in DOM after rendering timeout");
|
||||||
setMapError(true);
|
setMapError(true);
|
||||||
}
|
}
|
||||||
}, 100);
|
}, 500); // Increased timeout for better reliability on slower devices
|
||||||
});
|
});
|
||||||
}, [mapSvg, clues, enhanceSvgMap]);
|
}, [mapSvg, clues, enhanceSvgMap]);
|
||||||
|
|
||||||
@@ -627,14 +659,12 @@ export default function MapPage() {
|
|||||||
<div
|
<div
|
||||||
className={styles["svg-container"]}
|
className={styles["svg-container"]}
|
||||||
dangerouslySetInnerHTML={{ __html: mapSvg }}
|
dangerouslySetInnerHTML={{ __html: mapSvg }}
|
||||||
onLoad={() => console.log("SVG container loaded")}
|
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<div
|
<div className={styles["loading-container"]}>
|
||||||
className={styles["map-background"]}
|
<div className={styles["loading-spinner"]}></div>
|
||||||
role="img"
|
<div>Loading map...</div>
|
||||||
aria-label="Treasure Map"
|
</div>
|
||||||
></div>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -17,13 +17,16 @@ export async function GET(request) {
|
|||||||
return Response.json({ error: "Unauthorized access" }, { status: 401 });
|
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");
|
const result = await queryOne("SELECT COUNT(*) as count FROM clues");
|
||||||
|
|
||||||
if (!result) {
|
if (!result) {
|
||||||
return Response.json({ error: "Failed to count clues" }, { status: 500 });
|
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(
|
return Response.json(
|
||||||
{ count: result.count },
|
{ count: result.count },
|
||||||
{
|
{
|
||||||
|
|||||||
Binary file not shown.
+319
-91
@@ -1,8 +1,79 @@
|
|||||||
/**
|
/**
|
||||||
* Utility module for processing and populating the treasure map SVG.
|
* 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
|
* Populates the SVG map with clue points
|
||||||
* @param {SVGElement} svgElement - The SVG DOM element to process
|
* @param {SVGElement} svgElement - The SVG DOM element to process
|
||||||
@@ -10,62 +81,139 @@
|
|||||||
*/
|
*/
|
||||||
export async function populateMap(svgElement) {
|
export async function populateMap(svgElement) {
|
||||||
try {
|
try {
|
||||||
|
console.log("Starting map population process");
|
||||||
|
|
||||||
|
// Add custom styles to document if not already added
|
||||||
|
addCustomStyles();
|
||||||
|
|
||||||
if (!svgElement) {
|
if (!svgElement) {
|
||||||
console.error("No SVG element provided");
|
console.error("No SVG element provided");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log("Starting map population process");
|
// Clear any existing clue points and click areas to avoid duplicates
|
||||||
|
const existingCluePoints = svgElement.querySelectorAll(
|
||||||
// First, find all existing circle elements in the SVG
|
".clue-point, .clue-click-area"
|
||||||
const existingCircles = Array.from(svgElement.querySelectorAll("circle"));
|
|
||||||
console.log(`Found ${existingCircles.length} existing circles in SVG`);
|
|
||||||
|
|
||||||
// Hide all existing circles initially
|
|
||||||
existingCircles.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",
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!clueCountResponse.ok) {
|
|
||||||
throw new Error(
|
|
||||||
`Failed to fetch clue count: ${clueCountResponse.status}`
|
|
||||||
);
|
);
|
||||||
|
existingCluePoints.forEach((element) => {
|
||||||
|
if (element.parentNode) {
|
||||||
|
element.parentNode.removeChild(element);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { count: clueCount } = await clueCountResponse.json();
|
// Hide all existing circles initially
|
||||||
console.log(`Total clues: ${clueCount}`);
|
allCircles.forEach((circle) => (circle.style.display = "none"));
|
||||||
|
|
||||||
// Determine how many circles to display (min of existing circles and clue count)
|
try {
|
||||||
const circlesToShow = Math.min(existingCircles.length, clueCount);
|
// Fetch the number of clues from the API
|
||||||
console.log(`Will display ${circlesToShow} circles`);
|
const response = await fetch("/api/clues/count", {
|
||||||
|
method: "GET",
|
||||||
|
credentials: "include",
|
||||||
|
headers: {
|
||||||
|
"Cache-Control": "no-cache, no-store, must-revalidate",
|
||||||
|
Pragma: "no-cache",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
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)
|
// Sort circles by vertical position (top to bottom)
|
||||||
const sortedCircles = existingCircles.sort((a, b) => {
|
const sortedCircles = allCircles.sort((a, b) => {
|
||||||
const aY = parseFloat(a.getAttribute("cy"));
|
const aY = parseFloat(a.getAttribute("cy") || "0");
|
||||||
const bY = parseFloat(b.getAttribute("cy"));
|
const bY = parseFloat(b.getAttribute("cy") || "0");
|
||||||
return aY - bY;
|
return aY - bY;
|
||||||
});
|
});
|
||||||
|
|
||||||
// Display only the needed circles and add clue-point class
|
// Determine how many circles to show
|
||||||
for (let i = 0; i < circlesToShow; i++) {
|
const circlesToShow = Math.min(sortedCircles.length, clueCount);
|
||||||
const circle = sortedCircles[i];
|
console.log(
|
||||||
circle.style.display = "";
|
`Will create ${circlesToShow} clue points from sorted circles`
|
||||||
circle.classList.add("clue-point");
|
);
|
||||||
circle.setAttribute("data-clue-id", i + 1);
|
|
||||||
|
|
||||||
// Set default appearance
|
const svgNamespace = "http://www.w3.org/2000/svg";
|
||||||
circle.style.fill = "#9e9e9e"; // Default grey color
|
|
||||||
circle.style.stroke = "#757575";
|
// Create a dedicated overlay group for clue points to ensure they're on top
|
||||||
circle.style.strokeWidth = "2";
|
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;
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error fetching clue count:", error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error populating map:", error);
|
console.error("Error populating map:", error);
|
||||||
return false;
|
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 {SVGElement} svgElement - The SVG DOM element
|
||||||
* @param {Array} clues - Array of clue objects with their status
|
* @param {Array} clues - Array of clue objects with their status
|
||||||
*/
|
*/
|
||||||
export function updateCluePointStatus(svgElement, clues) {
|
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 {
|
try {
|
||||||
// Get all clue points
|
if (!svgElement || !clues || !Array.isArray(clues) || clues.length === 0) {
|
||||||
const cluePoints = svgElement.querySelectorAll(".clue-point");
|
console.error("Invalid parameters for updateCluePointStatus");
|
||||||
|
return false;
|
||||||
if (!cluePoints.length) {
|
|
||||||
console.error("No clue points found in SVG");
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Find the first unfound clue (the current one)
|
console.log(`Updating status for ${clues.length} clues`);
|
||||||
const currentClueIndex = clues.findIndex((clue) => !clue.found);
|
|
||||||
const currentClueId =
|
|
||||||
currentClueIndex !== -1 ? clues[currentClueIndex].id : null;
|
|
||||||
|
|
||||||
// Process each clue point
|
// Find all clue points
|
||||||
cluePoints.forEach((circle, index) => {
|
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;
|
if (index >= clues.length) return;
|
||||||
|
|
||||||
const clue = clues[index];
|
const clue = clues[index];
|
||||||
const clueId = clue.id;
|
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) {
|
if (clue.found) {
|
||||||
// Found/completed clue - Dark Green
|
point.classList.add("clue-point-found");
|
||||||
circle.style.fill = "#2e7d32"; // Dark green
|
tooltip.textContent = `Clue ${clueId}: ${clue.title || "Completed"}`;
|
||||||
} else if (clueId === currentClueId) {
|
} else if (clueId === currentClueId) {
|
||||||
// Current clue - Orange
|
point.classList.add("clue-point-current");
|
||||||
circle.style.fill = "#ff9800"; // Orange
|
tooltip.textContent = `Current Clue: ${
|
||||||
|
clue.title || "Go to this location"
|
||||||
|
}`;
|
||||||
} else if (clueId < currentClueId) {
|
} else if (clueId < currentClueId) {
|
||||||
// Skipped clue - Blue
|
point.classList.add("clue-point-skipped");
|
||||||
circle.style.fill = "#1976d2"; // Blue
|
tooltip.textContent = `Skipped Clue ${clueId}: ${
|
||||||
|
clue.title || "Skipped"
|
||||||
|
}`;
|
||||||
} else {
|
} else {
|
||||||
// Not accessible yet - Grey
|
point.classList.add("clue-point-locked");
|
||||||
circle.style.fill = "#9e9e9e"; // Grey
|
tooltip.textContent = `Locked: Complete previous clues first`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Mark whether this clue point is available
|
// Store availability data
|
||||||
const isAvailable = clue.found || clueId === currentClueId;
|
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) {
|
} 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
|
* @param {Function} navigationHandler - Function to handle navigation
|
||||||
*/
|
*/
|
||||||
export function attachClueClickHandlers(svgElement, clues, navigationHandler) {
|
export function attachClueClickHandlers(svgElement, clues, navigationHandler) {
|
||||||
|
try {
|
||||||
if (!svgElement || !clues || !navigationHandler) {
|
if (!svgElement || !clues || !navigationHandler) {
|
||||||
console.error("Missing required parameters for attaching click handlers");
|
console.error("Invalid parameters for attachClueClickHandlers");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
|
||||||
const cluePoints = svgElement.querySelectorAll(".clue-point");
|
|
||||||
|
|
||||||
cluePoints.forEach((circle, index) => {
|
|
||||||
if (index >= clues.length) return;
|
|
||||||
|
|
||||||
const clue = clues[index];
|
const clue = clues[index];
|
||||||
const isAvailable = circle.getAttribute("data-available") === "true";
|
const isAvailable = clue.found || index === currentIndex;
|
||||||
|
|
||||||
// Set cursor style based on availability
|
console.log(`Clue ${clue.id} clicked, available: ${isAvailable}`);
|
||||||
circle.style.cursor = isAvailable ? "pointer" : "not-allowed";
|
|
||||||
|
|
||||||
// Add click handler
|
|
||||||
circle.onclick = (e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
e.stopPropagation();
|
|
||||||
|
|
||||||
if (isAvailable) {
|
if (isAvailable) {
|
||||||
console.log(`Clicked on clue ${clue.id}`);
|
// 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);
|
navigationHandler(clue.id);
|
||||||
|
}, 100);
|
||||||
} else {
|
} else {
|
||||||
// Alert or other feedback if clue is not accessible
|
navigationHandler(clue.id);
|
||||||
console.log("Clue not available yet");
|
}
|
||||||
// Could show a tooltip or message here
|
} 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();
|
||||||
|
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) {
|
} catch (error) {
|
||||||
console.error("Error attaching click handlers:", error);
|
console.error("Error attaching click handlers:", error);
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Whitespace-only changes.
Reference in new issue
Block a user