added map page files and fixed code so that when an admin starts the timer the user can move on and isnt accidentally being redirected to the page to show that the timer isn't active even though it is

This commit is contained in:
bobbert committed 2025-03-14 15:39:05 +00:00
1 parent 6b233c90e7
commit 6ad943a868
7 files changed
+965 -39

No files matched your search

+298
View File
@@ -0,0 +1,298 @@
"use client";
import { useState, useEffect } from "react";
import { useRouter } from "next/navigation";
import Link from "next/link";
export default function CluePage({ params }) {
const { clueId } = params;
const [clue, setClue] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
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();
// 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 fetchClueDetails() {
try {
// First verify the hunt is active
const huntIsActive = await checkHuntStatus();
if (!huntIsActive) return;
// Fetch the specific clue from the API with improved error handling
const response = await fetch(`/api/clues/${clueId}`, {
credentials: "include",
});
// Better error handling for non-JSON responses
if (!response.ok) {
const contentType = response.headers.get("content-type");
if (contentType && contentType.includes("application/json")) {
const errorData = await response.json();
throw new Error(errorData.error || "Failed to fetch clue details");
} else {
throw new Error(
`Failed to fetch clue details: ${response.status} ${response.statusText}`
);
}
}
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);
}
setClue(data.clue);
} catch (err) {
console.error("Error fetching clue details:", err);
setError(err.message);
} finally {
setLoading(false);
}
}
if (clueId) {
fetchClueDetails();
}
}, [clueId, router]);
if (loading) {
return (
<div className="clue-container">
<div className="loading">Loading clue details...</div>
</div>
);
}
if (error) {
return (
<div className="clue-container">
<div className="error">Error: {error}</div>
<div className="nav-buttons">
<button onClick={() => router.push("/map")} className="map-button">
Back to Map
</button>
<button onClick={() => router.push("/")} className="home-button">
Back to Home
</button>
</div>
</div>
);
}
if (!clue) {
return (
<div className="clue-container">
<div className="not-found">Clue not found</div>
<div className="nav-buttons">
<button onClick={() => router.push("/map")} className="map-button">
Back to Map
</button>
<button onClick={() => router.push("/")} className="home-button">
Back to Home
</button>
</div>
</div>
);
}
return (
<div className="clue-container">
<div className="clue-scroll">
<h1 className="clue-title">{clue.title}</h1>
<div className="clue-content">
<h2>Description</h2>
<p className="clue-description">{clue.description}</p>
{clue.location && (
<div className="clue-location">
<h2>Location Hint</h2>
<p>{clue.location}</p>
</div>
)}
</div>
<div className="nav-buttons">
<button onClick={() => router.push("/map")} className="map-button">
Back to Map
</button>
<button onClick={() => router.push("/")} className="home-button">
Back to Home
</button>
</div>
</div>
<style jsx>{`
.clue-container {
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
background-color: #f4f1ea;
padding: 20px;
}
.clue-scroll {
background-color: #f5e6ca;
border: 2px solid #9c6b30;
border-radius: 10px;
padding: 30px;
max-width: 800px;
width: 100%;
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.2);
position: relative;
}
.clue-scroll::before,
.clue-scroll::after {
content: "";
position: absolute;
height: 30px;
width: 100%;
left: 0;
}
.clue-scroll::before {
top: 0;
background: linear-gradient(
180deg,
rgba(156, 107, 48, 0.2) 0%,
rgba(156, 107, 48, 0) 100%
);
border-radius: 8px 8px 0 0;
}
.clue-scroll::after {
bottom: 0;
background: linear-gradient(
0deg,
rgba(156, 107, 48, 0.2) 0%,
rgba(156, 107, 48, 0) 100%
);
border-radius: 0 0 8px 8px;
}
.clue-title {
color: #5d4037;
font-size: 2rem;
text-align: center;
margin-bottom: 20px;
font-family: "Pirates", cursive, serif;
border-bottom: 2px dashed #9c6b30;
padding-bottom: 15px;
}
.clue-content {
padding: 20px;
background-color: #f9f3e3;
border-radius: 8px;
margin-bottom: 20px;
border: 1px dashed #9c6b30;
}
h2 {
color: #9c6b30;
font-size: 1.5rem;
margin-top: 10px;
margin-bottom: 15px;
}
.clue-description {
font-size: 1.2rem;
line-height: 1.6;
color: #5d4037;
margin-bottom: 20px;
}
.clue-location {
margin-top: 20px;
padding-top: 15px;
border-top: 1px dashed #9c6b30;
}
.nav-buttons {
display: flex;
justify-content: space-between;
margin-top: 30px;
}
.map-button,
.home-button {
padding: 10px 20px;
border: none;
border-radius: 5px;
font-size: 1rem;
cursor: pointer;
transition: background-color 0.3s, transform 0.2s;
}
.map-button {
background-color: #9c6b30;
color: white;
}
.home-button {
background-color: #5d4037;
color: white;
}
.map-button:hover,
.home-button:hover {
transform: translateY(-3px);
}
.map-button:hover {
background-color: #7d562a;
}
.home-button:hover {
background-color: #4a332e;
}
.loading,
.error,
.not-found {
padding: 50px;
text-align: center;
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>
);
}
+358
View File
@@ -0,0 +1,358 @@
"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>
);
}
+16 -17
View File
@@ -233,26 +233,25 @@ export default function TeamPage({ params }) {
onClick={async () => {
try {
setLoading(true);
if (!huntActive) {
// If hunt is not active, redirect to the "not started" page
router.push(`/hunt-not-started?teamId=${teamId}`);
} else {
// If hunt is active, try to start the hunt
const response = await fetch(
`/api/teams/${teamId}/start-hunt`,
{
method: "POST",
headers: { "Content-Type": "application/json" },
credentials: "include",
}
);
if (response.ok) {
fetchTeamData();
// Check hunt status first
const response = await fetch("/api/hunt-status", {
method: "GET",
headers: { "Content-Type": "application/json" },
credentials: "include",
});
if (response.ok) {
const data = await response.json();
if (data.active) {
// If hunt is active, navigate to map page
router.push("/map");
} else {
const error = await response.json();
throw new Error(error.message || "Failed to start hunt");
// If hunt is not active, show not started page
router.push(`/hunt-not-started?teamId=${teamId}`);
}
} else {
throw new Error("Failed to check hunt status");
}
} catch (err) {
setError(err.message);