mirror of
https://github.com/mudabbir-ahmad/peworkshop.git
synced 2026-10-07 19:50:20 +00:00
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:
1 parent
6b233c90e7
commit
6ad943a868
7 files changed
+965
-39
No files matched your search
@@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -233,26 +233,25 @@ export default function TeamPage({ params }) {
|
|||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
try {
|
try {
|
||||||
setLoading(true);
|
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) {
|
// Check hunt status first
|
||||||
fetchTeamData();
|
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 {
|
} else {
|
||||||
const error = await response.json();
|
// If hunt is not active, show not started page
|
||||||
throw new Error(error.message || "Failed to start hunt");
|
router.push(`/hunt-not-started?teamId=${teamId}`);
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
throw new Error("Failed to check hunt status");
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError(err.message);
|
setError(err.message);
|
||||||
|
|||||||
@@ -0,0 +1,65 @@
|
|||||||
|
import { queryOne, run } from "@/lib/db";
|
||||||
|
import { cookies } from "next/headers";
|
||||||
|
import { verifyToken } from "@/app/api/auth/[...nextauth]/route";
|
||||||
|
|
||||||
|
export const dynamic = "force-dynamic"; // Disable caching
|
||||||
|
|
||||||
|
// GET endpoint to retrieve a specific clue
|
||||||
|
export async function GET(request, { params }) {
|
||||||
|
const { clueId } = params;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Verify the user is authenticated
|
||||||
|
const cookieStore = await cookies();
|
||||||
|
const sessionToken = cookieStore.get("next-auth.session-token")?.value;
|
||||||
|
|
||||||
|
if (!sessionToken) {
|
||||||
|
return Response.json(
|
||||||
|
{ error: "Unauthorized", clue: null },
|
||||||
|
{ status: 401 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const userData = verifyToken(sessionToken);
|
||||||
|
if (!userData) {
|
||||||
|
return Response.json(
|
||||||
|
{ error: "Invalid session", clue: null },
|
||||||
|
{ status: 401 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the hunt is active
|
||||||
|
const huntActiveSetting = await queryOne(
|
||||||
|
"SELECT value FROM settings WHERE key = 'hunt_active'"
|
||||||
|
);
|
||||||
|
|
||||||
|
const huntActive = huntActiveSetting && huntActiveSetting.value === "true";
|
||||||
|
|
||||||
|
if (!huntActive) {
|
||||||
|
// Return a valid JSON response with clue set to null
|
||||||
|
return Response.json({
|
||||||
|
error: "Hunt is not active",
|
||||||
|
clue: null,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch the specific clue
|
||||||
|
const clue = await queryOne("SELECT * FROM clues WHERE id = ?", [clueId]);
|
||||||
|
|
||||||
|
if (!clue) {
|
||||||
|
return Response.json(
|
||||||
|
{ error: "Clue not found", clue: null },
|
||||||
|
{ status: 404 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Response.json({ clue });
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error fetching clue details:", error);
|
||||||
|
// Always return a valid JSON response even on error
|
||||||
|
return Response.json(
|
||||||
|
{ error: "Failed to fetch clue details: " + error.message, clue: null },
|
||||||
|
{ status: 500 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,104 @@
|
|||||||
|
import { query, queryOne, run } from "@/lib/db";
|
||||||
|
import { cookies } from "next/headers";
|
||||||
|
import { verifyToken } from "@/app/api/auth/[...nextauth]/route";
|
||||||
|
|
||||||
|
export const dynamic = "force-dynamic"; // Disable caching
|
||||||
|
|
||||||
|
// Helper function to ensure the clues table exists (if not already created elsewhere)
|
||||||
|
async function ensureCluesTableExists() {
|
||||||
|
try {
|
||||||
|
// Create the clues table if it doesn't exist
|
||||||
|
await run(`
|
||||||
|
CREATE TABLE IF NOT EXISTS clues (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
title TEXT NOT NULL,
|
||||||
|
description TEXT NOT NULL,
|
||||||
|
location TEXT,
|
||||||
|
order_number INTEGER,
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||||
|
)
|
||||||
|
`);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error ensuring clues table exists:", error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GET endpoint to retrieve all clues
|
||||||
|
export async function GET(request) {
|
||||||
|
try {
|
||||||
|
// Verify the user is authenticated
|
||||||
|
const cookieStore = await cookies();
|
||||||
|
const sessionToken = cookieStore.get("next-auth.session-token")?.value;
|
||||||
|
|
||||||
|
if (!sessionToken) {
|
||||||
|
return Response.json(
|
||||||
|
{ error: "Unauthorized", clues: [] },
|
||||||
|
{ status: 401 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const userData = verifyToken(sessionToken);
|
||||||
|
if (!userData) {
|
||||||
|
return Response.json(
|
||||||
|
{ error: "Invalid session", clues: [] },
|
||||||
|
{ status: 401 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure the clues table exists
|
||||||
|
await ensureCluesTableExists();
|
||||||
|
|
||||||
|
// Check if the hunt is active
|
||||||
|
const huntActiveSetting = await queryOne(
|
||||||
|
"SELECT value FROM settings WHERE key = 'hunt_active'"
|
||||||
|
);
|
||||||
|
|
||||||
|
const huntActive = huntActiveSetting && huntActiveSetting.value === "true";
|
||||||
|
|
||||||
|
if (!huntActive) {
|
||||||
|
// Return empty clues array instead of error status to avoid redirect issues
|
||||||
|
return Response.json({
|
||||||
|
error: "Hunt is not active",
|
||||||
|
clues: [],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch all clues
|
||||||
|
const clues = await query("SELECT * FROM clues ORDER BY order_number ASC");
|
||||||
|
|
||||||
|
// If no clues are found, insert sample clues (for testing purposes)
|
||||||
|
if (!clues || clues.length === 0) {
|
||||||
|
// Insert sample clues
|
||||||
|
await run(
|
||||||
|
"INSERT INTO clues (title, description, location, order_number) VALUES (?, ?, ?, ?)",
|
||||||
|
["First Clue", "This is the first clue description", "Location 1", 1]
|
||||||
|
);
|
||||||
|
await run(
|
||||||
|
"INSERT INTO clues (title, description, location, order_number) VALUES (?, ?, ?, ?)",
|
||||||
|
["Second Clue", "This is the second clue description", "Location 2", 2]
|
||||||
|
);
|
||||||
|
await run(
|
||||||
|
"INSERT INTO clues (title, description, location, order_number) VALUES (?, ?, ?, ?)",
|
||||||
|
["Final Clue", "This is the final clue description", "Location 3", 3]
|
||||||
|
);
|
||||||
|
|
||||||
|
// Fetch the inserted clues
|
||||||
|
const sampleClues = await query(
|
||||||
|
"SELECT * FROM clues ORDER BY order_number ASC"
|
||||||
|
);
|
||||||
|
return Response.json({ clues: sampleClues });
|
||||||
|
}
|
||||||
|
|
||||||
|
return Response.json({ clues });
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error fetching clues:", error);
|
||||||
|
// Always return a valid JSON response even on error
|
||||||
|
return Response.json(
|
||||||
|
{ error: "Failed to fetch clues: " + error.message, clues: [] },
|
||||||
|
{ status: 500 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
+124
-22
@@ -7,18 +7,42 @@ export const dynamic = "force-dynamic"; // Disable caching
|
|||||||
// Helper function to ensure the hunt_timer table exists
|
// Helper function to ensure the hunt_timer table exists
|
||||||
async function ensureTableExists() {
|
async function ensureTableExists() {
|
||||||
try {
|
try {
|
||||||
// Check if the table exists by querying it
|
// Directly try to create the table (IF NOT EXISTS prevents errors if it already exists)
|
||||||
await queryOne("SELECT name FROM sqlite_master WHERE type='table' AND name='hunt_timer'");
|
await run(`
|
||||||
|
CREATE TABLE IF NOT EXISTS hunt_timer (
|
||||||
// If no error was thrown, the table exists
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
active INTEGER NOT NULL DEFAULT 0,
|
||||||
|
start_time TEXT NOT NULL,
|
||||||
|
end_time TEXT,
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||||
|
)
|
||||||
|
`);
|
||||||
|
|
||||||
|
// Verify the table exists by inserting a dummy record and then deleting it
|
||||||
|
// This is a more reliable way to test if the table is usable
|
||||||
|
const tempTimestamp = new Date().toISOString();
|
||||||
|
const tempId = await run(
|
||||||
|
"INSERT INTO hunt_timer (active, start_time) VALUES (0, ?)",
|
||||||
|
[tempTimestamp]
|
||||||
|
);
|
||||||
|
|
||||||
|
if (tempId) {
|
||||||
|
// Delete the test record
|
||||||
|
await run("DELETE FROM hunt_timer WHERE id = ?", [tempId]);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("hunt_timer table verified successfully");
|
||||||
return true;
|
return true;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error checking for hunt_timer table:", error);
|
console.error("Error ensuring hunt_timer table:", error);
|
||||||
|
|
||||||
// Try to create the table
|
// Try with an alternative approach if the first method failed
|
||||||
try {
|
try {
|
||||||
|
console.log("Attempting alternative table creation method...");
|
||||||
|
// Execute the CREATE TABLE statement directly
|
||||||
await run(`
|
await run(`
|
||||||
CREATE TABLE IF NOT EXISTS hunt_timer (
|
DROP TABLE IF EXISTS hunt_timer;
|
||||||
|
CREATE TABLE hunt_timer (
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
active INTEGER NOT NULL DEFAULT 0,
|
active INTEGER NOT NULL DEFAULT 0,
|
||||||
start_time TEXT NOT NULL,
|
start_time TEXT NOT NULL,
|
||||||
@@ -26,7 +50,7 @@ async function ensureTableExists() {
|
|||||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||||
)
|
)
|
||||||
`);
|
`);
|
||||||
console.log("hunt_timer table created successfully");
|
console.log("hunt_timer table created with alternative method");
|
||||||
return true;
|
return true;
|
||||||
} catch (createError) {
|
} catch (createError) {
|
||||||
console.error("Failed to create hunt_timer table:", createError);
|
console.error("Failed to create hunt_timer table:", createError);
|
||||||
@@ -35,11 +59,60 @@ async function ensureTableExists() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Helper function to ensure settings table exists and manage hunt active status
|
||||||
|
async function ensureSettingsTableExists() {
|
||||||
|
try {
|
||||||
|
// Create settings table if it doesn't exist
|
||||||
|
await run(`
|
||||||
|
CREATE TABLE IF NOT EXISTS settings (
|
||||||
|
key TEXT PRIMARY KEY,
|
||||||
|
value TEXT NOT NULL
|
||||||
|
)
|
||||||
|
`);
|
||||||
|
|
||||||
|
// Check if hunt_active setting exists
|
||||||
|
const huntActiveSetting = await queryOne(
|
||||||
|
"SELECT value FROM settings WHERE key = 'hunt_active'"
|
||||||
|
);
|
||||||
|
|
||||||
|
// If not, create it with default value "false"
|
||||||
|
if (!huntActiveSetting) {
|
||||||
|
await run("INSERT OR IGNORE INTO settings (key, value) VALUES (?, ?)", [
|
||||||
|
"hunt_active",
|
||||||
|
"false",
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error ensuring settings table:", error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper function to update hunt active status
|
||||||
|
async function updateHuntActiveStatus(isActive) {
|
||||||
|
try {
|
||||||
|
await ensureSettingsTableExists();
|
||||||
|
|
||||||
|
await run("INSERT OR REPLACE INTO settings (key, value) VALUES (?, ?)", [
|
||||||
|
"hunt_active",
|
||||||
|
isActive ? "true" : "false",
|
||||||
|
]);
|
||||||
|
|
||||||
|
console.log("Hunt active status updated:", isActive);
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to update hunt active status:", error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// GET endpoint to get current timer status
|
// GET endpoint to get current timer status
|
||||||
export async function GET(request) {
|
export async function GET(request) {
|
||||||
try {
|
try {
|
||||||
// Verify the user is authenticated
|
// Verify the user is authenticated
|
||||||
const cookieStore = cookies();
|
const cookieStore = await cookies(); // Add await here
|
||||||
const sessionToken = cookieStore.get("next-auth.session-token")?.value;
|
const sessionToken = cookieStore.get("next-auth.session-token")?.value;
|
||||||
|
|
||||||
if (!sessionToken) {
|
if (!sessionToken) {
|
||||||
@@ -51,8 +124,10 @@ export async function GET(request) {
|
|||||||
return Response.json({ error: "Invalid session" }, { status: 401 });
|
return Response.json({ error: "Invalid session" }, { status: 401 });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ensure the table exists
|
// Ensure both tables exist
|
||||||
const tableExists = await ensureTableExists();
|
const tableExists = await ensureTableExists();
|
||||||
|
await ensureSettingsTableExists();
|
||||||
|
|
||||||
if (!tableExists) {
|
if (!tableExists) {
|
||||||
return Response.json(
|
return Response.json(
|
||||||
{ error: "Database table not found" },
|
{ error: "Database table not found" },
|
||||||
@@ -65,16 +140,26 @@ export async function GET(request) {
|
|||||||
"SELECT active, start_time, end_time FROM hunt_timer ORDER BY id DESC LIMIT 1"
|
"SELECT active, start_time, end_time FROM hunt_timer ORDER BY id DESC LIMIT 1"
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Get the hunt active status from settings
|
||||||
|
const huntActiveSetting = await queryOne(
|
||||||
|
"SELECT value FROM settings WHERE key = 'hunt_active'"
|
||||||
|
);
|
||||||
|
|
||||||
|
const huntActive = huntActiveSetting
|
||||||
|
? huntActiveSetting.value === "true"
|
||||||
|
: false;
|
||||||
|
|
||||||
if (!timerStatus) {
|
if (!timerStatus) {
|
||||||
return Response.json({
|
return Response.json({
|
||||||
active: false,
|
active: huntActive, // Use settings value if timer not found
|
||||||
startTime: null,
|
startTime: null,
|
||||||
endTime: null,
|
endTime: null,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return Response.json({
|
return Response.json({
|
||||||
active: !!timerStatus.active,
|
active: huntActive, // Return settings value for consistency
|
||||||
|
timerActive: !!timerStatus.active, // Also return timer status for admin panel
|
||||||
startTime: timerStatus.start_time,
|
startTime: timerStatus.start_time,
|
||||||
endTime: timerStatus.end_time,
|
endTime: timerStatus.end_time,
|
||||||
});
|
});
|
||||||
@@ -91,7 +176,7 @@ export async function GET(request) {
|
|||||||
export async function POST(request) {
|
export async function POST(request) {
|
||||||
try {
|
try {
|
||||||
// Verify the user is authenticated and is an admin
|
// Verify the user is authenticated and is an admin
|
||||||
const cookieStore = cookies();
|
const cookieStore = await cookies(); // Add await here
|
||||||
const sessionToken = cookieStore.get("next-auth.session-token")?.value;
|
const sessionToken = cookieStore.get("next-auth.session-token")?.value;
|
||||||
|
|
||||||
if (!sessionToken) {
|
if (!sessionToken) {
|
||||||
@@ -102,22 +187,30 @@ export async function POST(request) {
|
|||||||
if (!userData) {
|
if (!userData) {
|
||||||
return Response.json({ error: "Invalid session" }, { status: 401 });
|
return Response.json({ error: "Invalid session" }, { status: 401 });
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log("User role:", userData.role);
|
console.log("User role:", userData.role);
|
||||||
|
|
||||||
// Check if user is admin (more lenient check)
|
// Check if user is admin (more lenient check)
|
||||||
if (!(userData.role === "admin" || userData.isAdmin === true || userData.is_admin === 1)) {
|
if (
|
||||||
|
!(
|
||||||
|
userData.role === "admin" ||
|
||||||
|
userData.isAdmin === true ||
|
||||||
|
userData.is_admin === 1
|
||||||
|
)
|
||||||
|
) {
|
||||||
return Response.json(
|
return Response.json(
|
||||||
{ error: "Unauthorized: Admin access required" },
|
{ error: "Unauthorized: Admin access required" },
|
||||||
{ status: 403 }
|
{ status: 403 }
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ensure the table exists
|
// Create required tables if they don't exist
|
||||||
const tableExists = await ensureTableExists();
|
const tableExists = await ensureTableExists();
|
||||||
|
await ensureSettingsTableExists();
|
||||||
|
|
||||||
if (!tableExists) {
|
if (!tableExists) {
|
||||||
return Response.json(
|
return Response.json(
|
||||||
{ error: "Database table not found" },
|
{ error: "Failed to create hunt_timer database table" },
|
||||||
{ status: 500 }
|
{ status: 500 }
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -141,6 +234,9 @@ export async function POST(request) {
|
|||||||
[1, startTime, endTime || null]
|
[1, startTime, endTime || null]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Update hunt active status to true
|
||||||
|
await updateHuntActiveStatus(true);
|
||||||
|
|
||||||
return Response.json({
|
return Response.json({
|
||||||
success: true,
|
success: true,
|
||||||
message: "Timer started successfully",
|
message: "Timer started successfully",
|
||||||
@@ -162,6 +258,9 @@ export async function POST(request) {
|
|||||||
[new Date().toISOString()]
|
[new Date().toISOString()]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Update hunt active status to false
|
||||||
|
await updateHuntActiveStatus(false);
|
||||||
|
|
||||||
return Response.json({
|
return Response.json({
|
||||||
success: true,
|
success: true,
|
||||||
message: "Timer stopped successfully",
|
message: "Timer stopped successfully",
|
||||||
@@ -181,8 +280,11 @@ export async function POST(request) {
|
|||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error managing timer:", error);
|
console.error("Error managing timer:", error);
|
||||||
return Response.json({
|
return Response.json(
|
||||||
error: "Failed to manage timer: " + error.message
|
{
|
||||||
}, { status: 500 });
|
error: "Failed to manage timer: " + error.message,
|
||||||
|
},
|
||||||
|
{ status: 500 }
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Binary file not shown.
Reference in new issue
Block a user