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
d675b228c6
commit
c5f2c0a98a
4 files changed
+189
-230
No files matched your search
+9
-51
@@ -10,7 +10,11 @@ import {
|
|||||||
clearHuntTimer,
|
clearHuntTimer,
|
||||||
formatElapsedTime,
|
formatElapsedTime,
|
||||||
} from "@/utils/timerService";
|
} from "@/utils/timerService";
|
||||||
import { populateMap, updateCluePointStatus } from "@/utils/map-populate";
|
import {
|
||||||
|
populateMap,
|
||||||
|
updateCluePointStatus,
|
||||||
|
attachClueClickHandlers,
|
||||||
|
} from "@/utils/map-populate";
|
||||||
|
|
||||||
export default function MapPage() {
|
export default function MapPage() {
|
||||||
const { data: session, status } = useSession();
|
const { data: session, status } = useSession();
|
||||||
@@ -188,59 +192,13 @@ export default function MapPage() {
|
|||||||
.then((success) => {
|
.then((success) => {
|
||||||
if (success) {
|
if (success) {
|
||||||
console.log("Map populated successfully");
|
console.log("Map populated successfully");
|
||||||
|
|
||||||
// After populating the map, update the status of each clue point
|
// After populating the map, update the status of each clue point
|
||||||
updateCluePointStatus(svgElement, clues);
|
updateCluePointStatus(svgElement, clues);
|
||||||
|
|
||||||
// Add click handlers to each clue point
|
// Attach click handlers for navigation
|
||||||
const cluePoints = svgElement.querySelectorAll(".clue-point");
|
attachClueClickHandlers(svgElement, clues, (clueId) => {
|
||||||
cluePoints.forEach((circle, index) => {
|
router.push(`/clue/${clueId}`);
|
||||||
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);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
console.error("Failed to populate map");
|
console.error("Failed to populate map");
|
||||||
|
|||||||
@@ -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 });
|
||||||
|
}
|
||||||
|
}
|
||||||
Binary file not shown.
+140
-179
@@ -1,174 +1,71 @@
|
|||||||
/**
|
/**
|
||||||
* Utility to dynamically populate the map SVG with clue points
|
* Utility module for processing and populating the treasure map SVG.
|
||||||
* based on the total number of clues in the database
|
* Handles clue points, their status, and interaction.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fetches the total number of clues from the API
|
* Populates the SVG map with clue points
|
||||||
* @returns {Promise<number>} The total number of clues
|
* @param {SVGElement} svgElement - The SVG DOM element to process
|
||||||
|
* @returns {Promise<boolean>} - Success flag
|
||||||
*/
|
*/
|
||||||
async function fetchTotalClues() {
|
export async function populateMap(svgElement) {
|
||||||
try {
|
try {
|
||||||
const response = await fetch("/api/clues", {
|
if (!svgElement) {
|
||||||
method: "GET",
|
console.error("No SVG element provided");
|
||||||
headers: {
|
return false;
|
||||||
"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}`);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const clues = await response.json();
|
console.log("Starting map population process");
|
||||||
return clues.length;
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error fetching total clues:", error);
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
// First, find all existing circle elements in the SVG
|
||||||
* Shows the specified number of clue points on the map
|
const existingCircles = Array.from(svgElement.querySelectorAll("circle"));
|
||||||
* @param {SVGElement} svgElement - The SVG DOM element of the map
|
console.log(`Found ${existingCircles.length} existing circles in SVG`);
|
||||||
* @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;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
// Hide all existing circles initially
|
||||||
// Find all clue points in the SVG - improved selector for better compatibility
|
existingCircles.forEach((circle) => {
|
||||||
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) => {
|
|
||||||
circle.style.display = "none";
|
circle.style.display = "none";
|
||||||
});
|
});
|
||||||
|
|
||||||
// Show only the required number of clue points
|
// Query the number of clues via API
|
||||||
for (let i = 0; i < Math.min(numClues, cluePoints.length); i++) {
|
const clueCountResponse = await fetch("/api/clues/count", {
|
||||||
const circle = cluePoints[i];
|
credentials: "include",
|
||||||
circle.style.display = "";
|
cache: "no-cache",
|
||||||
// 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);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log(`Created ${numClues} clue points dynamically`);
|
if (!clueCountResponse.ok) {
|
||||||
return true;
|
throw new Error(
|
||||||
} catch (error) {
|
`Failed to fetch clue count: ${clueCountResponse.status}`
|
||||||
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<boolean>} 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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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) {
|
} catch (error) {
|
||||||
console.error("Error populating map:", error);
|
console.error("Error populating map:", error);
|
||||||
return false;
|
return false;
|
||||||
@@ -176,41 +73,105 @@ export async function populateMap(svgElement) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Updates the styling of clue points based on their status
|
* Updates the status of each clue point based on clues data
|
||||||
* @param {SVGElement} svgElement - The SVG DOM element of the map
|
* @param {SVGElement} svgElement - The SVG DOM element
|
||||||
* @param {Array} clues - Array of clue objects with status information
|
* @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) {
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
clues.forEach((clue, index) => {
|
const cluePoints = svgElement.querySelectorAll(".clue-point");
|
||||||
// Find the corresponding circle in the SVG
|
|
||||||
const circle = svgElement.querySelector(
|
|
||||||
`.clue-point[data-clue-id="${clue.id}"]`
|
|
||||||
);
|
|
||||||
|
|
||||||
if (circle) {
|
cluePoints.forEach((circle, index) => {
|
||||||
// Apply appropriate styling based on clue status
|
if (index >= clues.length) return;
|
||||||
if (clue.found) {
|
|
||||||
// Found clues are green
|
const clue = clues[index];
|
||||||
circle.style.fill = "#4caf50";
|
const isAvailable = circle.getAttribute("data-available") === "true";
|
||||||
circle.style.stroke = "#2e7d32";
|
|
||||||
} else if (clue.available) {
|
// Set cursor style based on availability
|
||||||
// Available clues are orange
|
circle.style.cursor = isAvailable ? "pointer" : "not-allowed";
|
||||||
circle.style.fill = "#ff9800";
|
|
||||||
circle.style.stroke = "#d87f00";
|
// Add click handler
|
||||||
|
circle.onclick = (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
|
||||||
|
if (isAvailable) {
|
||||||
|
console.log(`Clicked on clue ${clue.id}`);
|
||||||
|
navigationHandler(clue.id);
|
||||||
} else {
|
} else {
|
||||||
// Unavailable clues remain grey
|
// Alert or other feedback if clue is not accessible
|
||||||
circle.style.fill = "#9e9e9e";
|
console.log("Clue not available yet");
|
||||||
circle.style.stroke = "#757575";
|
// Could show a tooltip or message here
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error updating clue point status:", error);
|
console.error("Error attaching click handlers:", error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Reference in new issue
Block a user