mirror of
https://github.com/mudabbir-ahmad/peworkshop.git
synced 2026-10-07 19:50:20 +00:00
359 lines
9.0 KiB
JavaScript
359 lines
9.0 KiB
JavaScript
"use client";
|
|
|
|
import { useState, useEffect } from "react";
|
|
import { useRouter } from "next/navigation";
|
|
import Link from "next/link";
|
|
|
|
export default function MapPage() {
|
|
const [clues, setClues] = useState([]);
|
|
const [loading, setLoading] = useState(true);
|
|
const [error, setError] = useState(null);
|
|
const [huntActive, setHuntActive] = useState(false);
|
|
const router = useRouter();
|
|
|
|
useEffect(() => {
|
|
async function checkHuntStatus() {
|
|
try {
|
|
const response = await fetch("/api/hunt-status", {
|
|
credentials: "include",
|
|
});
|
|
|
|
if (response.ok) {
|
|
const data = await response.json();
|
|
setHuntActive(data.active || false);
|
|
|
|
// If hunt is not active, redirect to the home page
|
|
if (!data.active) {
|
|
router.push("/");
|
|
return false;
|
|
}
|
|
return true;
|
|
} else {
|
|
console.error("Failed to check hunt status");
|
|
return false;
|
|
}
|
|
} catch (err) {
|
|
console.error("Error checking hunt status:", err);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
async function fetchClues() {
|
|
try {
|
|
// First verify the hunt is active
|
|
const huntIsActive = await checkHuntStatus();
|
|
if (!huntIsActive) return;
|
|
|
|
// Fetch the clues from the API with proper error handling
|
|
const response = await fetch("/api/clues", {
|
|
credentials: "include",
|
|
});
|
|
|
|
// Handle non-OK responses without throwing
|
|
if (!response.ok) {
|
|
const contentType = response.headers.get("content-type");
|
|
if (contentType && contentType.includes("application/json")) {
|
|
// If it's JSON, parse it for the error message
|
|
const errorData = await response.json();
|
|
throw new Error(errorData.error || "Failed to fetch clues");
|
|
} else {
|
|
// If it's not JSON, use the status text
|
|
throw new Error(
|
|
`Failed to fetch clues: ${response.status} ${response.statusText}`
|
|
);
|
|
}
|
|
}
|
|
|
|
// Parse the JSON response
|
|
const data = await response.json();
|
|
|
|
// If we got back an error field, show it as an error
|
|
if (data.error) {
|
|
throw new Error(data.error);
|
|
}
|
|
|
|
setClues(data.clues || []);
|
|
} catch (err) {
|
|
console.error("Error fetching clues:", err);
|
|
setError(err.message);
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
}
|
|
|
|
fetchClues();
|
|
}, [router]);
|
|
|
|
// Generate random x positions and sort by descending y positions
|
|
const generatePositions = (clues) => {
|
|
const positions = [];
|
|
const maxWidth = 800;
|
|
const maxHeight = 500;
|
|
const padding = 50;
|
|
|
|
for (let i = 0; i < clues.length; i++) {
|
|
positions.push({
|
|
id: clues[i].id,
|
|
title: clues[i].title || `Clue ${i + 1}`,
|
|
x: Math.random() * (maxWidth - 2 * padding) + padding,
|
|
y: (i + 1) * (maxHeight / (clues.length + 1)),
|
|
});
|
|
}
|
|
|
|
return positions;
|
|
};
|
|
|
|
const cluePositions = clues.length > 0 ? generatePositions(clues) : [];
|
|
|
|
if (loading) {
|
|
return (
|
|
<div className="map-container">
|
|
<div className="loading">Loading clues...</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
if (error) {
|
|
return (
|
|
<div className="map-container">
|
|
<div className="error">Error: {error}</div>
|
|
<button onClick={() => router.push("/")} className="back-button">
|
|
Back to Home
|
|
</button>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
if (clues.length === 0) {
|
|
return (
|
|
<div className="map-container">
|
|
<div className="no-clues">No clues available yet.</div>
|
|
<button onClick={() => router.push("/")} className="back-button">
|
|
Back to Home
|
|
</button>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className="map-container">
|
|
<h1>Treasure Map</h1>
|
|
<div className="map-wrapper">
|
|
<svg
|
|
className="map-svg"
|
|
viewBox="0 0 800 500"
|
|
xmlns="http://www.w3.org/2000/svg"
|
|
>
|
|
{/* Map background with a treasure map style */}
|
|
<rect width="800" height="500" fill="#f5e6ca" />
|
|
<path
|
|
d="M0,0 L800,0 L800,500 L0,500 Z"
|
|
fill="none"
|
|
stroke="#9c6b30"
|
|
strokeWidth="10"
|
|
/>
|
|
|
|
{/* Decorative map elements */}
|
|
<path
|
|
d="M100,100 Q400,50 700,150"
|
|
stroke="#9c6b30"
|
|
strokeWidth="2"
|
|
fill="none"
|
|
strokeDasharray="5,5"
|
|
/>
|
|
<path
|
|
d="M150,400 Q400,450 650,350"
|
|
stroke="#9c6b30"
|
|
strokeWidth="2"
|
|
fill="none"
|
|
strokeDasharray="5,5"
|
|
/>
|
|
<path
|
|
d="M300,100 Q350,250 300,400"
|
|
stroke="#9c6b30"
|
|
strokeWidth="2"
|
|
fill="none"
|
|
strokeDasharray="5,5"
|
|
/>
|
|
<path
|
|
d="M500,100 Q450,250 500,400"
|
|
stroke="#9c6b30"
|
|
strokeWidth="2"
|
|
fill="none"
|
|
strokeDasharray="5,5"
|
|
/>
|
|
|
|
{/* Connection lines between clue points */}
|
|
{cluePositions.slice(0, -1).map((clue, index) => (
|
|
<line
|
|
key={`line-${index}`}
|
|
x1={clue.x}
|
|
y1={clue.y}
|
|
x2={cluePositions[index + 1].x}
|
|
y2={cluePositions[index + 1].y}
|
|
stroke="#9c6b30"
|
|
strokeWidth="2"
|
|
strokeDasharray="5,5"
|
|
/>
|
|
))}
|
|
|
|
{/* Clue points */}
|
|
{cluePositions.map((clue, index) => (
|
|
<g key={`clue-${index}`} className="clue-point">
|
|
<circle
|
|
cx={clue.x}
|
|
cy={clue.y}
|
|
r="15"
|
|
fill="#d4a76a"
|
|
stroke="#9c6b30"
|
|
strokeWidth="2"
|
|
onClick={() => router.push(`/clue/${clue.id}`)}
|
|
style={{ cursor: "pointer" }}
|
|
/>
|
|
<text
|
|
x={clue.x}
|
|
y={clue.y}
|
|
textAnchor="middle"
|
|
dominantBaseline="central"
|
|
fill="#5d4037"
|
|
fontSize="12"
|
|
fontWeight="bold"
|
|
>
|
|
{index + 1}
|
|
</text>
|
|
</g>
|
|
))}
|
|
</svg>
|
|
</div>
|
|
|
|
{/* Clue list */}
|
|
<div className="clue-list">
|
|
<h2>Clues</h2>
|
|
<ul>
|
|
{cluePositions.map((clue, index) => (
|
|
<li key={`clue-list-${index}`}>
|
|
<Link href={`/clue/${clue.id}`}>
|
|
{index + 1}. {clue.title}
|
|
</Link>
|
|
</li>
|
|
))}
|
|
</ul>
|
|
</div>
|
|
|
|
<button onClick={() => router.push("/")} className="back-button">
|
|
Back to Home
|
|
</button>
|
|
|
|
<style jsx>{`
|
|
.map-container {
|
|
padding: 20px;
|
|
max-width: 1000px;
|
|
margin: 0 auto;
|
|
text-align: center;
|
|
}
|
|
|
|
h1 {
|
|
color: #5d4037;
|
|
margin-bottom: 20px;
|
|
font-family: "Pirates", cursive, serif;
|
|
font-size: 2.5rem;
|
|
}
|
|
|
|
.map-wrapper {
|
|
position: relative;
|
|
width: 100%;
|
|
max-width: 800px;
|
|
margin: 0 auto;
|
|
border: 5px solid #9c6b30;
|
|
border-radius: 10px;
|
|
overflow: hidden;
|
|
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.2);
|
|
background: #f5e6ca;
|
|
}
|
|
|
|
.map-svg {
|
|
width: 100%;
|
|
height: auto;
|
|
display: block;
|
|
}
|
|
|
|
.clue-list {
|
|
margin-top: 30px;
|
|
background: #f5e6ca;
|
|
border: 2px solid #9c6b30;
|
|
border-radius: 8px;
|
|
padding: 15px;
|
|
text-align: left;
|
|
}
|
|
|
|
.clue-list h2 {
|
|
color: #5d4037;
|
|
margin-top: 0;
|
|
}
|
|
|
|
ul {
|
|
list-style-type: none;
|
|
padding: 0;
|
|
}
|
|
|
|
li {
|
|
margin: 10px 0;
|
|
padding: 8px;
|
|
border-bottom: 1px dashed #9c6b30;
|
|
}
|
|
|
|
li:last-child {
|
|
border-bottom: none;
|
|
}
|
|
|
|
a {
|
|
text-decoration: none;
|
|
color: #5d4037;
|
|
font-weight: bold;
|
|
display: block;
|
|
padding: 5px;
|
|
transition: all 0.3s ease;
|
|
}
|
|
|
|
a:hover {
|
|
background-color: rgba(156, 107, 48, 0.2);
|
|
transform: translateX(5px);
|
|
}
|
|
|
|
.back-button {
|
|
margin-top: 20px;
|
|
padding: 10px 20px;
|
|
background-color: #9c6b30;
|
|
color: white;
|
|
border: none;
|
|
border-radius: 5px;
|
|
cursor: pointer;
|
|
font-size: 1rem;
|
|
transition: background-color 0.3s;
|
|
}
|
|
|
|
.back-button:hover {
|
|
background-color: #7d562a;
|
|
}
|
|
|
|
.loading,
|
|
.error,
|
|
.no-clues {
|
|
padding: 50px;
|
|
border: 2px dashed #9c6b30;
|
|
border-radius: 8px;
|
|
margin: 30px 0;
|
|
color: #5d4037;
|
|
background: #f5e6ca;
|
|
font-size: 1.2rem;
|
|
}
|
|
|
|
.error {
|
|
border-color: #d32f2f;
|
|
color: #d32f2f;
|
|
}
|
|
`}</style>
|
|
</div>
|
|
);
|
|
}
|