fixed the map, made it so the map is scrollable and synced the timing between the admin page and map page. They now share the same elapsed time. need to fix map css for altering the svg.

Grey = cant click
Red = current
Green = complete
blue = skipped?

Need to add Clue page Need to fix Leaderboard.
This commit is contained in:
bobbert committed 2025-03-15 16:30:04 +00:00
1 parent e7ad53643c
commit 16319f1333
14 files changed
+791 -472

No files matched your search

+3
View File
@@ -0,0 +1,3 @@
export default function ClueLayout({ children }) {
return <div className="clue-layout">{children}</div>;
}
+149 -134
View File
@@ -1,96 +1,90 @@
"use client";
import { useState, useEffect } from "react";
import { useSession } from "next-auth/react";
import { useRouter } from "next/navigation";
import { useHuntStatusCheck } from "@/hooks/useHuntStatusCheck";
import Link from "next/link";
import { use } from "react"; // Add this import for React.use()
import { useSession } from "next-auth/react";
import { formatElapsedTime, getHuntTimer } from "@/utils/timerService";
export default function CluePage({ params }) {
const { data: session, status } = useSession();
// Unwrap the params Promise using React.use()
const unwrappedParams = use(params);
const { clueId } = unwrappedParams;
const [clue, setClue] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
const [foundNow, setFoundNow] = useState(false);
const [elapsedTime, setElapsedTime] = useState("00:00:00");
const router = useRouter();
// Add this line to use the hunt status hook
const huntActive = useHuntStatusCheck();
const { status } = useSession();
const { clueId } = params;
useEffect(() => {
async function fetchClueDetails() {
if (status === "unauthenticated") {
router.push("/login");
return;
}
if (status === "loading") {
return;
}
// Fetch clue data
async function fetchClue() {
try {
// Try to check hunt status first with robust error handling
try {
const statusResponse = await fetch("/api/hunt-status", {
credentials: "include",
headers: { Accept: "application/json" },
});
// Read as text first
const statusText = await statusResponse.text();
try {
const statusData = JSON.parse(statusText);
if (!statusData.active && statusData.authenticated) {
router.push("/");
return;
}
} catch (parseError) {
console.error("Failed to parse hunt status response:", parseError);
// Continue anyway to try fetching clue details
}
} catch (statusErr) {
console.error("Error checking hunt status:", statusErr);
// Continue anyway to try fetching clue details
}
// Now fetch clue details with robust error handling
setLoading(true);
const response = await fetch(`/api/clues/${clueId}`, {
method: "GET",
headers: {
"Content-Type": "application/json",
"Cache-Control": "no-cache, no-store, must-revalidate",
},
credentials: "include",
headers: { Accept: "application/json" },
});
// Read as text first
const responseText = await response.text();
// Try to parse as JSON
let data;
try {
data = JSON.parse(responseText);
} catch (parseError) {
console.error("Failed to parse clue details response:", parseError);
console.log("Response received:", responseText.slice(0, 200));
throw new Error("Invalid response from server. Please try again.");
if (!response.ok) {
if (response.status === 404) {
throw new Error("Clue not found");
} else {
const data = await response.json();
throw new Error(data.error || "Failed to fetch clue");
}
}
// Check for error in the response
if (data.error) {
throw new Error(data.error);
}
setClue(data.clue);
const data = await response.json();
setClue(data);
setFoundNow(data.foundNow);
setLoading(false);
} catch (err) {
console.error("Error fetching clue details:", err);
console.error("Error fetching clue:", err);
setError(err.message);
} finally {
setLoading(false);
}
}
if (clueId) {
fetchClueDetails();
fetchClue();
}, [clueId, router, status]);
// Update timer display
useEffect(() => {
// Set up timer display updates
const timerData = getHuntTimer();
if (!timerData || !timerData.isActive) {
return;
}
}, [clueId, router]);
const timer = setInterval(() => {
setElapsedTime(formatElapsedTime(timerData.startTime));
}, 1000);
// Initial set
setElapsedTime(formatElapsedTime(timerData.startTime));
return () => clearInterval(timer);
}, []);
if (loading) {
return (
<div className="clue-container">
<div className="loading">Loading clue details...</div>
<div className="loading">
<div className="loading-spinner"></div>
<p>Loading clue...</p>
</div>
</div>
);
}
@@ -130,6 +124,15 @@ export default function CluePage({ params }) {
return (
<div className="clue-container">
<div className="clue-scroll">
{foundNow && (
<div className="new-discovery">
<div className="discovery-icon">🎉</div>
<div className="discovery-text">New Clue Discovered!</div>
</div>
)}
<div className="timer-display">{elapsedTime}</div>
<h1 className="clue-title">{clue.title}</h1>
<div className="clue-content">
@@ -165,59 +168,13 @@ export default function CluePage({ params }) {
}
.clue-scroll {
background-color: #f5e6ca;
border: 2px solid #9c6b30;
background-color: #f8f3e3;
border-radius: 10px;
padding: 30px;
max-width: 800px;
width: 100%;
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.2);
max-width: 600px;
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1);
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;
}
@@ -253,51 +210,109 @@ export default function CluePage({ params }) {
padding: 10px 20px;
border: none;
border-radius: 5px;
font-size: 1rem;
cursor: pointer;
transition: background-color 0.3s, transform 0.2s;
font-weight: bold;
transition: all 0.2s;
}
.map-button {
background-color: #9c6b30;
background-color: #68543c;
color: white;
}
.home-button {
background-color: #5d4037;
background-color: #a98467;
color: white;
}
.map-button:hover,
.home-button:hover {
transform: translateY(-3px);
}
.map-button:hover {
background-color: #7d562a;
}
.home-button:hover {
background-color: #4a332e;
transform: scale(1.05);
}
.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;
padding: 30px;
}
.loading-spinner {
border: 4px solid #f3f3f3;
border-top: 4px solid #9c6b30;
border-radius: 50%;
width: 40px;
height: 40px;
animation: spin 1s linear infinite;
margin: 0 auto 20px;
}
@keyframes spin {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
.error {
border-color: #d32f2f;
color: #d32f2f;
}
.clue-title {
color: #68543c;
font-size: 2rem;
text-align: center;
margin-bottom: 20px;
border-bottom: 2px solid #9c6b30;
padding-bottom: 10px;
}
.timer-display {
position: absolute;
top: 15px;
right: 15px;
background-color: rgba(0, 0, 0, 0.1);
padding: 5px 10px;
border-radius: 5px;
font-family: monospace;
font-weight: bold;
font-size: 1rem;
}
.new-discovery {
background-color: #feefb3;
border: 1px solid #9f6000;
color: #9f6000;
padding: 15px;
border-radius: 5px;
margin-bottom: 20px;
display: flex;
align-items: center;
animation: pulse 2s infinite;
}
@keyframes pulse {
0% {
opacity: 1;
}
50% {
opacity: 0.7;
}
100% {
opacity: 1;
}
}
.discovery-icon {
font-size: 1.5rem;
margin-right: 10px;
}
.discovery-text {
font-weight: bold;
}
`}</style>
</div>
);