Files
peworkshop/app/(main)/map/page.js
T

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>
);
}