mirror of
https://github.com/mudabbir-ahmad/peworkshop.git
synced 2026-10-08 03:50:21 +00:00
fixed skip clue implimentation
This commit is contained in:
1 parent
fccc2a4edc
commit
2f9a6c732c
18 files changed
+524
-151
No files matched your search
@@ -122,7 +122,7 @@ export default function AdminDashboard() {
|
|||||||
const timerData = getHuntTimer();
|
const timerData = getHuntTimer();
|
||||||
if (timerData && timerData.startTime) {
|
if (timerData && timerData.startTime) {
|
||||||
setTimerDisplay(formatElapsedTime(timerData.startTime));
|
setTimerDisplay(formatElapsedTime(timerData.startTime));
|
||||||
|
|
||||||
// If there's an end time, check if we've reached it
|
// If there's an end time, check if we've reached it
|
||||||
if (timerEndTime && Date.now() >= timerEndTime) {
|
if (timerEndTime && Date.now() >= timerEndTime) {
|
||||||
stopTimer();
|
stopTimer();
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ export default function CluePage({ params }) {
|
|||||||
const [scanMessage, setScanMessage] = useState(null);
|
const [scanMessage, setScanMessage] = useState(null);
|
||||||
const [lastRefresh, setLastRefresh] = useState(0);
|
const [lastRefresh, setLastRefresh] = useState(0);
|
||||||
const [clueFound, setClueFound] = useState(false);
|
const [clueFound, setClueFound] = useState(false);
|
||||||
|
const [clueSkipped, setClueSkipped] = useState(false); // Add state for skipped status
|
||||||
const [allClues, setAllClues] = useState([]);
|
const [allClues, setAllClues] = useState([]);
|
||||||
const [nextClueId, setNextClueId] = useState(null);
|
const [nextClueId, setNextClueId] = useState(null);
|
||||||
const [nextClueAvailable, setNextClueAvailable] = useState(false);
|
const [nextClueAvailable, setNextClueAvailable] = useState(false);
|
||||||
@@ -126,6 +127,7 @@ export default function CluePage({ params }) {
|
|||||||
if (isMounted) {
|
if (isMounted) {
|
||||||
setClue(data);
|
setClue(data);
|
||||||
setClueFound(data.found);
|
setClueFound(data.found);
|
||||||
|
setClueSkipped(data.skipped); // Set skipped status from API response
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
setLastRefresh(Date.now());
|
setLastRefresh(Date.now());
|
||||||
}
|
}
|
||||||
@@ -398,6 +400,56 @@ export default function CluePage({ params }) {
|
|||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
// Replace the previous clue function with skip clue functionality
|
||||||
|
const handleSkipClue = async () => {
|
||||||
|
// Show confirmation dialog
|
||||||
|
if (
|
||||||
|
!confirm(
|
||||||
|
"Are you sure you want to skip this clue? You can come back to it later."
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Make an API call to mark this clue as skipped
|
||||||
|
const response = await fetch("/api/mark-clue", {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({
|
||||||
|
clueId: parseInt(clueId),
|
||||||
|
skipped: true,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
// Get the response data to handle errors better
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
setScanMessage("Clue skipped. You can return to it later.");
|
||||||
|
|
||||||
|
// Make next clue available immediately
|
||||||
|
setNextClueAvailable(true);
|
||||||
|
|
||||||
|
// Navigate to the next clue if available, otherwise go to map
|
||||||
|
if (nextClueId) {
|
||||||
|
router.push(`/clue/${nextClueId}`);
|
||||||
|
} else {
|
||||||
|
router.push("/map");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.error(
|
||||||
|
"Failed to skip clue:",
|
||||||
|
data.error || response.statusText
|
||||||
|
);
|
||||||
|
setScanMessage(`Error: ${data.error || "Could not skip clue"}`);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Error skipping clue:", err);
|
||||||
|
setScanMessage("Error: Could not connect to server");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// Navigate to the next clue
|
// Navigate to the next clue
|
||||||
const handleNextClue = () => {
|
const handleNextClue = () => {
|
||||||
if (nextClueId) {
|
if (nextClueId) {
|
||||||
@@ -423,20 +475,6 @@ export default function CluePage({ params }) {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Navigate to the previous clue
|
|
||||||
const handlePrevClue = () => {
|
|
||||||
if (allClues.length > 0) {
|
|
||||||
const currentIndex = allClues.findIndex((c) => c.id === parseInt(clueId));
|
|
||||||
if (currentIndex > 0) {
|
|
||||||
router.push(`/clue/${allClues[currentIndex - 1].id}`);
|
|
||||||
} else {
|
|
||||||
router.push("/map");
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
router.push("/map");
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Function for manual refresh - for admins to test without waiting
|
// Function for manual refresh - for admins to test without waiting
|
||||||
const handleManualRefresh = () => {
|
const handleManualRefresh = () => {
|
||||||
setLastRefresh(0);
|
setLastRefresh(0);
|
||||||
@@ -541,7 +579,7 @@ export default function CluePage({ params }) {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="clue-content-wrapper">
|
<div className="clue-content-wrapper">
|
||||||
{/* Show a notification when the clue is already found */}
|
{/* Show notification based on found OR skipped status */}
|
||||||
{clueFound && (
|
{clueFound && (
|
||||||
<div className="clue-found-notification">
|
<div className="clue-found-notification">
|
||||||
<div className="clue-found-icon">✓</div>
|
<div className="clue-found-icon">✓</div>
|
||||||
@@ -550,6 +588,15 @@ export default function CluePage({ params }) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{clueSkipped && !clueFound && (
|
||||||
|
<div className="clue-skipped-notification">
|
||||||
|
<div className="clue-skipped-icon">↷</div>
|
||||||
|
<div className="clue-skipped-text">
|
||||||
|
You skipped this clue earlier. Try to solve it now!
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Admin tools section - only visible to admins */}
|
{/* Admin tools section - only visible to admins */}
|
||||||
{isAdmin && (
|
{isAdmin && (
|
||||||
@@ -598,8 +645,8 @@ export default function CluePage({ params }) {
|
|||||||
|
|
||||||
{/* Navigation Buttons */}
|
{/* Navigation Buttons */}
|
||||||
<div className="navigation-buttons">
|
<div className="navigation-buttons">
|
||||||
<button onClick={handlePrevClue} className="nav-button prev-button">
|
<button onClick={handleSkipClue} className="nav-button prev-button">
|
||||||
Previous Clue
|
Skip Clue
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => router.push("/map")}
|
onClick={() => router.push("/map")}
|
||||||
@@ -741,6 +788,28 @@ export default function CluePage({ params }) {
|
|||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.clue-skipped-notification {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
background-color: #e3f2fd;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 12px 16px;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
width: 100%;
|
||||||
|
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.clue-skipped-icon {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
color: #1976d2;
|
||||||
|
margin-right: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.clue-skipped-text {
|
||||||
|
color: #1976d2;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
@keyframes slide-down {
|
@keyframes slide-down {
|
||||||
0% {
|
0% {
|
||||||
transform: translateY(-100%);
|
transform: translateY(-100%);
|
||||||
@@ -1055,13 +1124,13 @@ export default function CluePage({ params }) {
|
|||||||
background-color: rgba(104, 84, 60, 0.1);
|
background-color: rgba(104, 84, 60, 0.1);
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.timer-label {
|
.timer-label {
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
margin-right: 8px;
|
margin-right: 8px;
|
||||||
color: #68543c;
|
color: #68543c;
|
||||||
}
|
}
|
||||||
|
|
||||||
.timer-value {
|
.timer-value {
|
||||||
font-family: monospace;
|
font-family: monospace;
|
||||||
font-size: 1.1em;
|
font-size: 1.1em;
|
||||||
|
|||||||
@@ -271,6 +271,13 @@
|
|||||||
cursor: default;
|
cursor: default;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Add this new class for skipped clues after your existing clue status styles */
|
||||||
|
.clue-skipped {
|
||||||
|
background-color: #1976d2; /* Dark blue for skipped clues */
|
||||||
|
border-color: #0d47a1;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
/* Pulse animation for available clues */
|
/* Pulse animation for available clues */
|
||||||
@keyframes pulse {
|
@keyframes pulse {
|
||||||
0% {
|
0% {
|
||||||
|
|||||||
+23
-14
@@ -1,3 +1,4 @@
|
|||||||
|
/* eslint-disable @next/next/no-img-element */
|
||||||
"use client";
|
"use client";
|
||||||
import { useState, useEffect, useRef, useCallback } from "react";
|
import { useState, useEffect, useRef, useCallback } from "react";
|
||||||
import { useSession } from "next-auth/react";
|
import { useSession } from "next-auth/react";
|
||||||
@@ -94,23 +95,30 @@ export default function MapPage() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error("Failed to fetch clues");
|
// Extract the error message from the response if possible
|
||||||
|
const errorData = await response.json().catch(() => ({}));
|
||||||
|
console.error("API response details:", {
|
||||||
|
status: response.status,
|
||||||
|
statusText: response.statusText,
|
||||||
|
errorData,
|
||||||
|
});
|
||||||
|
|
||||||
|
throw new Error(
|
||||||
|
errorData.error ||
|
||||||
|
`Failed to fetch clues: ${response.status} ${response.statusText}`
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
|
|
||||||
// Process the clues to determine which are available
|
// Process the clues to determine which are available
|
||||||
// A clue is available if it's either found already or the next in sequence
|
// A clue is available if it's either found already or the next in sequence
|
||||||
const processedClues = data.map((clue, index, arr) => {
|
const processedClues = data.map((clue) => {
|
||||||
// Find the first unfound clue's index
|
// The API now returns clues with availability already determined
|
||||||
const firstUnfoundIndex = arr.findIndex((c) => !c.found);
|
return {
|
||||||
|
...clue,
|
||||||
// A clue is available if:
|
available: clue.available || clue.found || clue.skipped,
|
||||||
// 1. It's already found, OR
|
};
|
||||||
// 2. It's the next unfound clue in the sequence
|
|
||||||
const available = clue.found || index === firstUnfoundIndex;
|
|
||||||
|
|
||||||
return { ...clue, available };
|
|
||||||
});
|
});
|
||||||
|
|
||||||
setClues(processedClues);
|
setClues(processedClues);
|
||||||
@@ -118,7 +126,8 @@ export default function MapPage() {
|
|||||||
return processedClues;
|
return processedClues;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("Failed to fetch clues:", err);
|
console.error("Failed to fetch clues:", err);
|
||||||
setError(err.message);
|
setError(err.message); // Make sure we set the error state
|
||||||
|
setLoading(false);
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
@@ -329,8 +338,8 @@ export default function MapPage() {
|
|||||||
// Function to determine clue button class based on status
|
// Function to determine clue button class based on status
|
||||||
const getClueButtonClass = (clue) => {
|
const getClueButtonClass = (clue) => {
|
||||||
if (clue.found) return `${styles["clue-button"]} ${styles["clue-found"]}`;
|
if (clue.found) return `${styles["clue-button"]} ${styles["clue-found"]}`;
|
||||||
if (clue.available)
|
if (clue.skipped) return `${styles["clue-button"]} ${styles["clue-skipped"]}`;
|
||||||
return `${styles["clue-button"]} ${styles["clue-available"]}`;
|
if (clue.available) return `${styles["clue-button"]} ${styles["clue-available"]}`;
|
||||||
return `${styles["clue-button"]} ${styles["clue-locked"]}`;
|
return `${styles["clue-button"]} ${styles["clue-locked"]}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
+59
-12
@@ -79,6 +79,54 @@ export default function StatsPage() {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const renderCluesList = () => {
|
||||||
|
if (!teamStats) return null;
|
||||||
|
|
||||||
|
const { foundClues, skippedClues } = teamStats;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="clues-section">
|
||||||
|
<h3 className="section-title">Found Clues ({foundClues?.length || 0})</h3>
|
||||||
|
<ul className="clue-list">
|
||||||
|
{foundClues?.length > 0 ? (
|
||||||
|
foundClues.map(clue => (
|
||||||
|
<li key={clue.id} className="clue-item">
|
||||||
|
<div className="clue-icon found">✓</div>
|
||||||
|
<div className="clue-details">
|
||||||
|
<div className="clue-title">{clue.title || `Clue #${clue.id}`}</div>
|
||||||
|
<div className="clue-time">
|
||||||
|
{new Date(clue.found_at).toLocaleTimeString()}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<li className="empty-state">No clues found yet</li>
|
||||||
|
)}
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<h3 className="section-title">Skipped Clues ({skippedClues?.length || 0})</h3>
|
||||||
|
<ul className="clue-list">
|
||||||
|
{skippedClues?.length > 0 ? (
|
||||||
|
skippedClues.map(clue => (
|
||||||
|
<li key={clue.id} className="clue-item">
|
||||||
|
<div className="clue-icon skipped">↷</div>
|
||||||
|
<div className="clue-details">
|
||||||
|
<div className="clue-title">{clue.title || `Clue #${clue.id}`}</div>
|
||||||
|
<div className="clue-time">
|
||||||
|
Skipped at {new Date(clue.found_at).toLocaleTimeString()}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<li className="empty-state">No clues skipped</li>
|
||||||
|
)}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return (
|
return (
|
||||||
<div className={styles.container}>
|
<div className={styles.container}>
|
||||||
@@ -167,18 +215,7 @@ export default function StatsPage() {
|
|||||||
|
|
||||||
{expanded && (
|
{expanded && (
|
||||||
<div className={styles.cluesList}>
|
<div className={styles.cluesList}>
|
||||||
{teamStats.foundClues.length === 0 ? (
|
{renderCluesList()}
|
||||||
<p className={styles.emptyMessage}>No clues found yet.</p>
|
|
||||||
) : (
|
|
||||||
teamStats.foundClues.map((clue) => (
|
|
||||||
<div key={clue.id} className={styles.clueItem}>
|
|
||||||
<span className={styles.clueTitle}>{clue.title}</span>
|
|
||||||
<span className={styles.clueTimestamp}>
|
|
||||||
{formatTimestamp(clue.found_at)}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
))
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -205,6 +242,16 @@ export default function StatsPage() {
|
|||||||
<div className={styles.navLabel}>Leaderboard</div>
|
<div className={styles.navLabel}>Leaderboard</div>
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<style jsx>{`
|
||||||
|
.clue-icon.found {
|
||||||
|
background-color: #2e7d32;
|
||||||
|
}
|
||||||
|
|
||||||
|
.clue-icon.skipped {
|
||||||
|
background-color: #1976d2;
|
||||||
|
}
|
||||||
|
`}</style>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -16,12 +16,14 @@
|
|||||||
.title {
|
.title {
|
||||||
font-size: 1.5rem;
|
font-size: 1.5rem;
|
||||||
margin: 0 0 5px 0;
|
margin: 0 0 5px 0;
|
||||||
|
color: white;
|
||||||
}
|
}
|
||||||
|
|
||||||
.teamName {
|
.teamName {
|
||||||
font-size: 1.2rem;
|
font-size: 1.2rem;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-weight: normal;
|
font-weight: normal;
|
||||||
|
color: white;
|
||||||
}
|
}
|
||||||
|
|
||||||
.statsCard {
|
.statsCard {
|
||||||
|
|||||||
@@ -43,9 +43,9 @@ export async function GET(request, context) {
|
|||||||
return Response.json({ error: "Clue not found" }, { status: 404 });
|
return Response.json({ error: "Clue not found" }, { status: 404 });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if this team has found this clue before
|
// Check if this team has found or skipped this clue before
|
||||||
const found = await queryOne(
|
const clueStatus = await queryOne(
|
||||||
"SELECT * FROM team_clues WHERE team_id = ? AND clue_id = ?",
|
"SELECT found_at, skipped FROM team_clues WHERE team_id = ? AND clue_id = ?",
|
||||||
[token.teamId, clueId]
|
[token.teamId, clueId]
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -66,7 +66,8 @@ export async function GET(request, context) {
|
|||||||
return Response.json(
|
return Response.json(
|
||||||
{
|
{
|
||||||
...clue,
|
...clue,
|
||||||
found: !!found, // Only mark as found if it was previously found
|
found: !!clueStatus && !(clueStatus.skipped === 1), // Mark as found only if it exists and is not skipped
|
||||||
|
skipped: !!clueStatus && clueStatus.skipped === 1, // Add skipped status
|
||||||
isAdmin: token.role === "admin", // Add flag so client knows user is admin
|
isAdmin: token.role === "admin", // Add flag so client knows user is admin
|
||||||
},
|
},
|
||||||
{ headers: cacheHeaders }
|
{ headers: cacheHeaders }
|
||||||
|
|||||||
+69
-9
@@ -1,4 +1,4 @@
|
|||||||
import { query, queryOne } from "@/lib/db";
|
import { query, queryOne, run } from "@/lib/db";
|
||||||
import { getToken } from "next-auth/jwt";
|
import { getToken } from "next-auth/jwt";
|
||||||
|
|
||||||
export const dynamic = "force-dynamic"; // Disable caching
|
export const dynamic = "force-dynamic"; // Disable caching
|
||||||
@@ -13,18 +13,50 @@ export async function GET(request) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (!token) {
|
if (!token) {
|
||||||
|
console.log("API: Unauthorized request to /api/clues - no token");
|
||||||
return Response.json({ error: "Unauthorized" }, { status: 401 });
|
return Response.json({ error: "Unauthorized" }, { status: 401 });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if user has a team
|
// Check if user has a team
|
||||||
if (!token.teamId) {
|
if (!token.teamId) {
|
||||||
|
console.log("API: User without team tried to access clues");
|
||||||
return Response.json(
|
return Response.json(
|
||||||
{ error: "You must be part of a team to view clues" },
|
{ error: "You must be part of a team to view clues" },
|
||||||
{ status: 403 }
|
{ status: 403 }
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get all clues with explicit ordering - removed coordinates
|
// First, ensure the team_clues table has the 'skipped' column
|
||||||
|
try {
|
||||||
|
// Check if team_clues table exists, create it if it doesn't
|
||||||
|
await run(`
|
||||||
|
CREATE TABLE IF NOT EXISTS team_clues (
|
||||||
|
team_id INTEGER NOT NULL,
|
||||||
|
clue_id INTEGER NOT NULL,
|
||||||
|
found_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
PRIMARY KEY (team_id, clue_id),
|
||||||
|
FOREIGN KEY (team_id) REFERENCES teams(id),
|
||||||
|
FOREIGN KEY (clue_id) REFERENCES clues(id)
|
||||||
|
)
|
||||||
|
`);
|
||||||
|
|
||||||
|
// Check if skipped column exists
|
||||||
|
const tableInfo = await query("PRAGMA table_info(team_clues)");
|
||||||
|
const hasSkippedColumn = tableInfo.some((col) => col.name === "skipped");
|
||||||
|
|
||||||
|
if (!hasSkippedColumn) {
|
||||||
|
console.log("Adding 'skipped' column to team_clues table");
|
||||||
|
await run(
|
||||||
|
"ALTER TABLE team_clues ADD COLUMN skipped BOOLEAN DEFAULT 0"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (schemaError) {
|
||||||
|
console.error("Error ensuring database schema:", schemaError);
|
||||||
|
// Continue execution - if this fails, the main query might still work
|
||||||
|
// with existing columns
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get all clues with explicit ordering
|
||||||
const clues = await query(`
|
const clues = await query(`
|
||||||
SELECT id, title, description, location
|
SELECT id, title, description, location
|
||||||
FROM clues
|
FROM clues
|
||||||
@@ -34,21 +66,49 @@ export async function GET(request) {
|
|||||||
// For each clue, check if the team has found it
|
// For each clue, check if the team has found it
|
||||||
const cluesWithStatus = await Promise.all(
|
const cluesWithStatus = await Promise.all(
|
||||||
clues.map(async (clue) => {
|
clues.map(async (clue) => {
|
||||||
const found = await queryOne(
|
let clueStatus;
|
||||||
"SELECT 1 FROM team_clues WHERE team_id = ? AND clue_id = ?",
|
try {
|
||||||
[token.teamId, clue.id]
|
clueStatus = await queryOne(
|
||||||
);
|
"SELECT found_at, skipped FROM team_clues WHERE team_id = ? AND clue_id = ?",
|
||||||
|
[token.teamId, clue.id]
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Error querying status for clue ${clue.id}:`, error);
|
||||||
|
// Fallback if query fails (e.g., skipped column still doesn't exist)
|
||||||
|
clueStatus = await queryOne(
|
||||||
|
"SELECT found_at FROM team_clues WHERE team_id = ? AND clue_id = ?",
|
||||||
|
[token.teamId, clue.id]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...clue,
|
...clue,
|
||||||
found: !!found,
|
found: !!clueStatus && !(clueStatus.skipped === 1),
|
||||||
|
skipped: !!clueStatus && clueStatus.skipped === 1,
|
||||||
|
available: false, // Will be determined after processing all clues
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
console.log(`Returning ${cluesWithStatus.length} clues`);
|
// Determine which clues should be available
|
||||||
|
let firstUnavailableFound = false;
|
||||||
|
const finalClues = cluesWithStatus.map((clue, index, arr) => {
|
||||||
|
if (clue.found || clue.skipped) {
|
||||||
|
return { ...clue, available: true };
|
||||||
|
}
|
||||||
|
|
||||||
return Response.json(cluesWithStatus, {
|
// If we haven't found an unavailable clue yet, this one is available
|
||||||
|
if (!firstUnavailableFound) {
|
||||||
|
firstUnavailableFound = true;
|
||||||
|
return { ...clue, available: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
return { ...clue, available: false };
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(`API: Returning ${finalClues.length} clues`);
|
||||||
|
|
||||||
|
return Response.json(finalClues, {
|
||||||
headers: {
|
headers: {
|
||||||
"Cache-Control": "no-store, no-cache, must-revalidate",
|
"Cache-Control": "no-store, no-cache, must-revalidate",
|
||||||
Pragma: "no-cache",
|
Pragma: "no-cache",
|
||||||
|
|||||||
+41
-19
@@ -1,4 +1,4 @@
|
|||||||
import { query, queryOne } from "@/lib/db";
|
import { query, queryOne, run } from "@/lib/db"; // Add 'run' to the import
|
||||||
import { getToken } from "next-auth/jwt";
|
import { getToken } from "next-auth/jwt";
|
||||||
|
|
||||||
export const dynamic = "force-dynamic"; // Disable caching
|
export const dynamic = "force-dynamic"; // Disable caching
|
||||||
@@ -25,7 +25,7 @@ export async function POST(request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Parse request body
|
// Parse request body
|
||||||
const { clueId } = await request.json();
|
const { clueId, skipped = false } = await request.json();
|
||||||
|
|
||||||
// Validate clueId is a number
|
// Validate clueId is a number
|
||||||
if (isNaN(parseInt(clueId))) {
|
if (isNaN(parseInt(clueId))) {
|
||||||
@@ -38,49 +38,71 @@ export async function POST(request) {
|
|||||||
return Response.json({ error: "Clue not found" }, { status: 404 });
|
return Response.json({ error: "Clue not found" }, { status: 404 });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if clue was already found
|
// Check if clue was already found or skipped
|
||||||
const alreadyFound = await queryOne(
|
const alreadyProcessed = await queryOne(
|
||||||
"SELECT * FROM team_clues WHERE team_id = ? AND clue_id = ?",
|
"SELECT * FROM team_clues WHERE team_id = ? AND clue_id = ?",
|
||||||
[token.teamId, clueId]
|
[token.teamId, clueId]
|
||||||
);
|
);
|
||||||
|
|
||||||
if (alreadyFound) {
|
if (alreadyProcessed) {
|
||||||
|
// If skipping is requested, update the record regardless of previous state
|
||||||
|
if (skipped) {
|
||||||
|
await run(
|
||||||
|
"UPDATE team_clues SET skipped = 1 WHERE team_id = ? AND clue_id = ?",
|
||||||
|
[token.teamId, clueId]
|
||||||
|
);
|
||||||
|
|
||||||
|
return Response.json({
|
||||||
|
success: true,
|
||||||
|
message: "Clue marked as skipped",
|
||||||
|
skipped: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// For non-skip requests on already processed clues
|
||||||
return Response.json({
|
return Response.json({
|
||||||
success: true,
|
success: true,
|
||||||
message: "This clue was already marked as found",
|
message: "This clue was already processed",
|
||||||
alreadyFound: true,
|
alreadyFound: !(alreadyProcessed.skipped === 1),
|
||||||
|
alreadySkipped: alreadyProcessed.skipped === 1,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create team_clues table if it doesn't exist
|
// Ensure the team_clues table has a skipped column
|
||||||
await queryOne(`
|
await run(`
|
||||||
CREATE TABLE IF NOT EXISTS team_clues (
|
CREATE TABLE IF NOT EXISTS team_clues (
|
||||||
team_id INTEGER NOT NULL,
|
team_id INTEGER NOT NULL,
|
||||||
clue_id INTEGER NOT NULL,
|
clue_id INTEGER NOT NULL,
|
||||||
found_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
found_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
skipped BOOLEAN DEFAULT 0,
|
||||||
PRIMARY KEY (team_id, clue_id),
|
PRIMARY KEY (team_id, clue_id),
|
||||||
FOREIGN KEY (team_id) REFERENCES teams(id),
|
FOREIGN KEY (team_id) REFERENCES teams(id),
|
||||||
FOREIGN KEY (clue_id) REFERENCES clues(id)
|
FOREIGN KEY (clue_id) REFERENCES clues(id)
|
||||||
)
|
)
|
||||||
`);
|
`);
|
||||||
|
|
||||||
// Record that the team found this clue
|
// Add column if it doesn't exist (safe operation)
|
||||||
await queryOne("INSERT INTO team_clues (team_id, clue_id) VALUES (?, ?)", [
|
try {
|
||||||
token.teamId,
|
await run("ALTER TABLE team_clues ADD COLUMN skipped BOOLEAN DEFAULT 0");
|
||||||
clueId,
|
} catch (e) {
|
||||||
]);
|
// Column likely already exists, continue
|
||||||
|
}
|
||||||
|
|
||||||
console.log(`Clue ${clueId} marked as found for team ${token.teamId}`);
|
// Record that the team found or skipped this clue
|
||||||
|
await run(
|
||||||
|
"INSERT INTO team_clues (team_id, clue_id, skipped) VALUES (?, ?, ?)",
|
||||||
|
[token.teamId, clueId, skipped ? 1 : 0]
|
||||||
|
);
|
||||||
|
|
||||||
// Return successful response
|
|
||||||
return Response.json({
|
return Response.json({
|
||||||
success: true,
|
success: true,
|
||||||
message: "Clue marked as found successfully",
|
message: skipped ? "Clue skipped successfully" : "Clue marked as found",
|
||||||
|
skipped: skipped,
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error marking clue as found:", error);
|
console.error("Error marking clue:", error);
|
||||||
return Response.json(
|
return Response.json(
|
||||||
{ error: "Failed to mark clue as found: " + error.message },
|
{ error: "Failed to process clue: " + error.message },
|
||||||
{ status: 500 }
|
{ status: 500 }
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -42,12 +42,25 @@ export async function GET(request) {
|
|||||||
const totalClues = clueCountResult?.total || 0;
|
const totalClues = clueCountResult?.total || 0;
|
||||||
|
|
||||||
// Get all found clues with timestamps, ordered by found time
|
// Get all found clues with timestamps, ordered by found time
|
||||||
|
// Only include clues that aren't marked as skipped
|
||||||
const foundClues = await query(
|
const foundClues = await query(
|
||||||
`
|
`
|
||||||
SELECT c.id, c.title, tc.found_at
|
SELECT c.id, c.title, tc.found_at
|
||||||
FROM clues c
|
FROM clues c
|
||||||
JOIN team_clues tc ON c.id = tc.clue_id
|
JOIN team_clues tc ON c.id = tc.clue_id
|
||||||
WHERE tc.team_id = ?
|
WHERE tc.team_id = ? AND (tc.skipped = 0 OR tc.skipped IS NULL)
|
||||||
|
ORDER BY tc.found_at ASC
|
||||||
|
`,
|
||||||
|
[token.teamId]
|
||||||
|
);
|
||||||
|
|
||||||
|
// Get skipped clues separately
|
||||||
|
const skippedClues = await query(
|
||||||
|
`
|
||||||
|
SELECT c.id, c.title, tc.found_at
|
||||||
|
FROM clues c
|
||||||
|
JOIN team_clues tc ON c.id = tc.clue_id
|
||||||
|
WHERE tc.team_id = ? AND tc.skipped = 1
|
||||||
ORDER BY tc.found_at ASC
|
ORDER BY tc.found_at ASC
|
||||||
`,
|
`,
|
||||||
[token.teamId]
|
[token.teamId]
|
||||||
@@ -69,12 +82,12 @@ export async function GET(request) {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// If there are unfound clues, the ones after the first unfound are "skipped"
|
// If there are unfound clues, the ones after the first unfound are "skipped"
|
||||||
let skippedClues = 0;
|
let skippedCluesCount = 0;
|
||||||
if (unfoundClueIds.length > 0) {
|
if (unfoundClueIds.length > 0) {
|
||||||
const lowestUnfoundId = Math.min(...unfoundClueIds);
|
const lowestUnfoundId = Math.min(...unfoundClueIds);
|
||||||
|
|
||||||
// Skipped clues are found clues with IDs higher than the lowest unfound
|
// Skipped clues are found clues with IDs higher than the lowest unfound
|
||||||
skippedClues = foundClues.filter((c) => c.id > lowestUnfoundId).length;
|
skippedCluesCount = foundClues.filter((c) => c.id > lowestUnfoundId).length;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get current team rank
|
// Get current team rank
|
||||||
@@ -107,12 +120,13 @@ export async function GET(request) {
|
|||||||
totalClues,
|
totalClues,
|
||||||
foundClues,
|
foundClues,
|
||||||
remainingClues: totalClues - foundClues.length,
|
remainingClues: totalClues - foundClues.length,
|
||||||
skippedClues,
|
skippedCluesCount,
|
||||||
currentRank,
|
currentRank,
|
||||||
progress:
|
progress:
|
||||||
totalClues > 0 ? Math.round((foundClues.length / totalClues) * 100) : 0,
|
totalClues > 0 ? Math.round((foundClues.length / totalClues) * 100) : 0,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Return only the teamStats object instead of trying to include undefined variables
|
||||||
return Response.json(teamStats);
|
return Response.json(teamStats);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error fetching team stats:", error);
|
console.error("Error fetching team stats:", error);
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
import { getToken } from "next-auth/jwt";
|
import { getToken } from "next-auth/jwt";
|
||||||
import { query, queryOne, run } from "@/lib/db";
|
import { queryOne, run } from "@/lib/db";
|
||||||
import { nanoid } from "nanoid";
|
|
||||||
|
|
||||||
export async function POST(request) {
|
export async function POST(request) {
|
||||||
try {
|
try {
|
||||||
// Get user token from session
|
// Verify user authentication
|
||||||
const token = await getToken({
|
const token = await getToken({
|
||||||
req: request,
|
req: request,
|
||||||
secret:
|
secret:
|
||||||
@@ -13,30 +12,23 @@ export async function POST(request) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (!token) {
|
if (!token) {
|
||||||
return Response.json({ error: "Invalid session" }, { status: 401 });
|
return Response.json({ error: "Unauthorized" }, { status: 401 });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Extract user ID from the token
|
// Get user ID from token
|
||||||
const userId = token.id;
|
const userId = token.id;
|
||||||
if (!userId) {
|
|
||||||
return Response.json(
|
|
||||||
{ error: "Invalid user information in session" },
|
|
||||||
{ status: 401 }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parse request body
|
// Get team name from request body
|
||||||
const body = await request.json();
|
const { name } = await request.json();
|
||||||
const { name } = body;
|
|
||||||
|
|
||||||
if (!name || name.trim() === "") {
|
if (!name || !name.trim()) {
|
||||||
return Response.json({ error: "Team name is required" }, { status: 400 });
|
return Response.json({ error: "Team name is required" }, { status: 400 });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generate a unique team code
|
// Generate a unique team code
|
||||||
const code = nanoid(6).toUpperCase();
|
const code = generateTeamCode();
|
||||||
|
|
||||||
// Create the team - updated schema to match actual database (no created_by column)
|
// Create teams table if it doesn't exist
|
||||||
await run(`
|
await run(`
|
||||||
CREATE TABLE IF NOT EXISTS teams (
|
CREATE TABLE IF NOT EXISTS teams (
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
@@ -50,6 +42,7 @@ export async function POST(request) {
|
|||||||
const existingTeam = await queryOne("SELECT id FROM teams WHERE name = ?", [
|
const existingTeam = await queryOne("SELECT id FROM teams WHERE name = ?", [
|
||||||
name,
|
name,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
if (existingTeam) {
|
if (existingTeam) {
|
||||||
return Response.json(
|
return Response.json(
|
||||||
{ error: "Team name already exists" },
|
{ error: "Team name already exists" },
|
||||||
@@ -57,7 +50,7 @@ export async function POST(request) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Insert team without created_by field
|
// Insert team
|
||||||
const result = await run("INSERT INTO teams (name, code) VALUES (?, ?)", [
|
const result = await run("INSERT INTO teams (name, code) VALUES (?, ?)", [
|
||||||
name,
|
name,
|
||||||
code,
|
code,
|
||||||
@@ -71,19 +64,26 @@ export async function POST(request) {
|
|||||||
// Update the user's team_id
|
// Update the user's team_id
|
||||||
await run("UPDATE users SET team_id = ? WHERE id = ?", [teamId, userId]);
|
await run("UPDATE users SET team_id = ? WHERE id = ?", [teamId, userId]);
|
||||||
|
|
||||||
// Return a more complete response with all needed data
|
// Return success with team info
|
||||||
return Response.json({
|
return Response.json({
|
||||||
success: true,
|
success: true,
|
||||||
teamId,
|
teamId,
|
||||||
teamName: name,
|
|
||||||
code,
|
|
||||||
message: "Team created successfully",
|
message: "Team created successfully",
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Create team error:", error);
|
console.error("Error creating team:", error);
|
||||||
return Response.json(
|
return Response.json(
|
||||||
{ error: "Failed to create team: " + error.message },
|
{ error: "Failed to create team: " + error.message },
|
||||||
{ status: 500 }
|
{ status: 500 }
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function generateTeamCode() {
|
||||||
|
const characters = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
|
||||||
|
let result = "";
|
||||||
|
for (let i = 0; i < 6; i++) {
|
||||||
|
result += characters.charAt(Math.floor(Math.random() * characters.length));
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
Whitespace-only changes.
Binary file not shown.
@@ -0,0 +1,56 @@
|
|||||||
|
"use client";
|
||||||
|
import { useState } from "react";
|
||||||
|
|
||||||
|
export default function CreateTeamForm({ onSubmit }) {
|
||||||
|
const [name, setName] = useState("");
|
||||||
|
const [submitting, setSubmitting] = useState(false);
|
||||||
|
|
||||||
|
const handleSubmit = async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
if (!name.trim()) {
|
||||||
|
return; // Don't submit if name is empty
|
||||||
|
}
|
||||||
|
|
||||||
|
setSubmitting(true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await onSubmit({ name });
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error submitting form:", error);
|
||||||
|
} finally {
|
||||||
|
setSubmitting(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<form onSubmit={handleSubmit} className="mb-6">
|
||||||
|
<div className="mb-4">
|
||||||
|
<label htmlFor="teamName" className="block text-gray-700 mb-2">
|
||||||
|
Team Name
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="teamName"
|
||||||
|
value={name}
|
||||||
|
onChange={(e) => setName(e.target.value)}
|
||||||
|
className="w-full px-3 py-2 border border-gray-300 rounded focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||||
|
placeholder="Enter team name"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
disabled={submitting}
|
||||||
|
className={`px-4 py-2 rounded text-white ${
|
||||||
|
submitting
|
||||||
|
? "bg-gray-400 cursor-not-allowed"
|
||||||
|
: "bg-blue-500 hover:bg-blue-600"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{submitting ? "Creating..." : "Create Team"}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,109 @@
|
|||||||
|
"use client";
|
||||||
|
import { useState } from "react";
|
||||||
|
import { useRouter } from "next/navigation";
|
||||||
|
|
||||||
|
export default function TeamList({ teams, onJoinTeam, onRefresh }) {
|
||||||
|
const [joinCode, setJoinCode] = useState("");
|
||||||
|
const [error, setError] = useState("");
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
const handleJoinTeam = async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
if (!joinCode.trim()) {
|
||||||
|
setError("Please enter a team code");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setError("");
|
||||||
|
setLoading(true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch("/api/teams/join", {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({ code: joinCode.trim() }),
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(data.error || "Failed to join team");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Call the onJoinTeam callback with the joined team
|
||||||
|
if (onJoinTeam) {
|
||||||
|
onJoinTeam(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Refresh the team list
|
||||||
|
if (onRefresh) {
|
||||||
|
onRefresh();
|
||||||
|
}
|
||||||
|
|
||||||
|
router.refresh();
|
||||||
|
} catch (err) {
|
||||||
|
setError(err.message);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="mb-8">
|
||||||
|
<h2 className="text-xl font-bold mb-4">Join Existing Team</h2>
|
||||||
|
|
||||||
|
<form onSubmit={handleJoinTeam} className="mb-6">
|
||||||
|
<div className="mb-4">
|
||||||
|
<label htmlFor="teamCode" className="block text-gray-700 mb-2">
|
||||||
|
Team Code
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="teamCode"
|
||||||
|
value={joinCode}
|
||||||
|
onChange={(e) => setJoinCode(e.target.value)}
|
||||||
|
className="w-full px-3 py-2 border border-gray-300 rounded focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||||
|
placeholder="Enter team code"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
disabled={loading}
|
||||||
|
className={`px-4 py-2 rounded text-white ${
|
||||||
|
loading
|
||||||
|
? "bg-gray-400 cursor-not-allowed"
|
||||||
|
: "bg-blue-500 hover:bg-blue-600"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{loading ? "Joining..." : "Join Team"}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{error && <p className="text-red-500 mt-2">{error}</p>}
|
||||||
|
</form>
|
||||||
|
|
||||||
|
{teams && teams.length > 0 ? (
|
||||||
|
<div>
|
||||||
|
<h3 className="text-lg font-semibold mb-2">Available Teams:</h3>
|
||||||
|
<ul className="space-y-2">
|
||||||
|
{teams.map((team) => (
|
||||||
|
<li
|
||||||
|
key={team.id}
|
||||||
|
className="border rounded p-3 hover:bg-gray-50 cursor-pointer"
|
||||||
|
onClick={() => setJoinCode(team.code)}
|
||||||
|
>
|
||||||
|
<div className="font-medium">{team.name}</div>
|
||||||
|
<div className="text-sm text-gray-500">Code: {team.code}</div>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<p className="text-gray-500">No teams available to join</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
+4
-40
@@ -23,84 +23,48 @@
|
|||||||
<rect x="181" y="1974" width="88" height="88" fill="url(#pattern10_37_192)"/>
|
<rect x="181" y="1974" width="88" height="88" fill="url(#pattern10_37_192)"/>
|
||||||
<rect x="373" y="1512" width="88" height="88" fill="url(#pattern11_37_192)"/>
|
<rect x="373" y="1512" width="88" height="88" fill="url(#pattern11_37_192)"/>
|
||||||
<g filter="url(#filter0_d_37_192)">
|
<g filter="url(#filter0_d_37_192)">
|
||||||
<circle cx="230" cy="211" r="25" fill=""/>
|
|
||||||
<circle cx="230" cy="211" r="25" stroke="#757575" stroke-width="2"/>
|
|
||||||
</g>
|
</g>
|
||||||
<g filter="url(#filter1_d_37_192)">
|
<g filter="url(#filter1_d_37_192)">
|
||||||
<circle cx="320" cy="284" r="25" fill=""/>
|
|
||||||
<circle cx="320" cy="284" r="25" stroke="#757575" stroke-width="2"/>
|
|
||||||
</g>
|
</g>
|
||||||
<g filter="url(#filter2_d_37_192)">
|
<g filter="url(#filter2_d_37_192)">
|
||||||
<circle cx="264" cy="397" r="25" fill=""/>
|
|
||||||
<circle cx="264" cy="397" r="25" stroke="#757575" stroke-width="2"/>
|
|
||||||
</g>
|
</g>
|
||||||
<g filter="url(#filter3_d_37_192)">
|
<g filter="url(#filter3_d_37_192)">
|
||||||
<circle cx="342" cy="517" r="25" fill=""/>
|
|
||||||
<circle cx="342" cy="517" r="25" stroke="#757575" stroke-width="2"/>
|
|
||||||
</g>
|
</g>
|
||||||
<g filter="url(#filter4_d_37_192)">
|
<g filter="url(#filter4_d_37_192)">
|
||||||
<circle cx="320" cy="645" r="25" fill=""/>
|
|
||||||
<circle cx="320" cy="645" r="25" stroke="#757575" stroke-width="2"/>
|
|
||||||
</g>
|
</g>
|
||||||
<g filter="url(#filter5_d_37_192)">
|
<g filter="url(#filter5_d_37_192)">
|
||||||
<circle cx="295" cy="773" r="25" fill=""/>
|
|
||||||
<circle cx="295" cy="773" r="25" stroke="#757575" stroke-width="2"/>
|
|
||||||
</g>
|
</g>
|
||||||
<g filter="url(#filter6_d_37_192)">
|
<g filter="url(#filter6_d_37_192)">
|
||||||
<circle cx="239" cy="880" r="25" fill=""/>
|
|
||||||
<circle cx="239" cy="880" r="25" stroke="#757575" stroke-width="2"/>
|
|
||||||
</g>
|
</g>
|
||||||
<g filter="url(#filter7_d_37_192)">
|
<g filter="url(#filter7_d_37_192)">
|
||||||
<circle cx="354" cy="959" r="25" fill=""/>
|
|
||||||
<circle cx="354" cy="959" r="25" stroke="#757575" stroke-width="2"/>
|
|
||||||
</g>
|
</g>
|
||||||
<g filter="url(#filter8_d_37_192)">
|
<g filter="url(#filter8_d_37_192)">
|
||||||
<circle cx="270" cy="1036" r="25" fill=""/>
|
|
||||||
<circle cx="270" cy="1036" r="25" stroke="#757575" stroke-width="2"/>
|
|
||||||
</g>
|
</g>
|
||||||
<g filter="url(#filter9_d_37_192)">
|
<g filter="url(#filter9_d_37_192)">
|
||||||
<circle cx="279" cy="1174" r="25" fill=""/>
|
|
||||||
<circle cx="279" cy="1174" r="25" stroke="#757575" stroke-width="2"/>
|
|
||||||
</g>
|
</g>
|
||||||
<g filter="url(#filter10_d_37_192)">
|
<g filter="url(#filter10_d_37_192)">
|
||||||
<circle cx="337" cy="1302" r="25" fill=""/>
|
|
||||||
<circle cx="337" cy="1302" r="25" stroke="#757575" stroke-width="2"/>
|
|
||||||
</g>
|
</g>
|
||||||
<g filter="url(#filter11_d_37_192)">
|
<g filter="url(#filter11_d_37_192)">
|
||||||
<circle cx="250" cy="1388" r="25" fill=""/>
|
|
||||||
<circle cx="250" cy="1388" r="25" stroke="#757575" stroke-width="2"/>
|
|
||||||
</g>
|
</g>
|
||||||
<g filter="url(#filter12_d_37_192)">
|
<g filter="url(#filter12_d_37_192)">
|
||||||
<circle cx="162" cy="1487" r="25" fill=""/>
|
|
||||||
<circle cx="162" cy="1487" r="25" stroke="#757575" stroke-width="2"/>
|
|
||||||
</g>
|
</g>
|
||||||
<g filter="url(#filter13_d_37_192)">
|
<g filter="url(#filter13_d_37_192)">
|
||||||
<circle cx="306" cy="1531" r="25" fill=""/>
|
|
||||||
<circle cx="306" cy="1531" r="25" stroke="#757575" stroke-width="2"/>
|
|
||||||
</g>
|
</g>
|
||||||
<g filter="url(#filter14_d_37_192)">
|
<g filter="url(#filter14_d_37_192)">
|
||||||
<circle cx="342" cy="1649" r="25" fill=""/>
|
|
||||||
<circle cx="342" cy="1649" r="25" stroke="#757575" stroke-width="2"/>
|
|
||||||
</g>
|
</g>
|
||||||
<g filter="url(#filter15_d_37_192)">
|
<g filter="url(#filter15_d_37_192)">
|
||||||
<circle cx="225" cy="1724" r="25" fill=""/>
|
|
||||||
<circle cx="225" cy="1724" r="25" stroke="#757575" stroke-width="2"/>
|
|
||||||
</g>
|
</g>
|
||||||
<g filter="url(#filter16_d_37_192)">
|
<g filter="url(#filter16_d_37_192)">
|
||||||
<circle cx="216" cy="1822" r="25" fill=""/>
|
|
||||||
<circle cx="216" cy="1822" r="25" stroke="#757575" stroke-width="2"/>
|
|
||||||
</g>
|
</g>
|
||||||
<g filter="url(#filter17_d_37_192)">
|
<g filter="url(#filter17_d_37_192)">
|
||||||
<circle cx="275" cy="1883" r="25" fill=""/>
|
|
||||||
<circle cx="275" cy="1883" r="25" stroke="#757575" stroke-width="2"/>
|
|
||||||
</g>
|
</g>
|
||||||
<g filter="url(#filter18_d_37_192)">
|
<g filter="url(#filter18_d_37_192)">
|
||||||
<circle cx="362" cy="1917" r="25" fill=""/>
|
|
||||||
<circle cx="362" cy="1917" r="25" stroke="#757575" stroke-width="2"/>
|
|
||||||
</g>
|
</g>
|
||||||
<g filter="url(#filter19_d_37_192)">
|
<g filter="url(#filter19_d_37_192)">
|
||||||
<circle cx="300" cy="2037" r="25" fill=""/>
|
|
||||||
<circle cx="300" cy="2037" r="25" stroke="#757575" stroke-width="2"/>
|
|
||||||
</g>
|
</g>
|
||||||
</g>
|
</g>
|
||||||
<defs>
|
<defs>
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 493 KiB After Width: | Height: | Size: 491 KiB |
Whitespace-only changes.
+20
-7
@@ -409,8 +409,10 @@ export function updateCluePointStatus(svgElement, clues) {
|
|||||||
`Found ${cluePoints.length} clue points and ${clickAreas.length} click areas to update`
|
`Found ${cluePoints.length} clue points and ${clickAreas.length} click areas to update`
|
||||||
);
|
);
|
||||||
|
|
||||||
// Find the first unfound clue (the current clue)
|
// Find the first unfound and unskipped clue
|
||||||
const currentIndex = clues.findIndex((clue) => !clue.found);
|
const currentIndex = clues.findIndex(
|
||||||
|
(clue) => !clue.found && !clue.skipped
|
||||||
|
);
|
||||||
const currentClueId = currentIndex !== -1 ? clues[currentIndex].id : null;
|
const currentClueId = currentIndex !== -1 ? clues[currentIndex].id : null;
|
||||||
|
|
||||||
console.log(`Current clue ID: ${currentClueId}`);
|
console.log(`Current clue ID: ${currentClueId}`);
|
||||||
@@ -439,15 +441,22 @@ export function updateCluePointStatus(svgElement, clues) {
|
|||||||
if (clue.found) {
|
if (clue.found) {
|
||||||
point.classList.add("clue-point-found");
|
point.classList.add("clue-point-found");
|
||||||
tooltip.textContent = `Clue ${clueId}: ${clue.title || "Completed"}`;
|
tooltip.textContent = `Clue ${clueId}: ${clue.title || "Completed"}`;
|
||||||
|
} else if (clue.skipped) {
|
||||||
|
// Explicitly mark skipped clues with blue color
|
||||||
|
point.classList.add("clue-point-skipped");
|
||||||
|
tooltip.textContent = `Skipped Clue ${clueId}: ${
|
||||||
|
clue.title || "Skipped"
|
||||||
|
}`;
|
||||||
} else if (clueId === currentClueId) {
|
} else if (clueId === currentClueId) {
|
||||||
point.classList.add("clue-point-current");
|
point.classList.add("clue-point-current");
|
||||||
tooltip.textContent = `Current Clue: ${
|
tooltip.textContent = `Current Clue: ${
|
||||||
clue.title || "Go to this location"
|
clue.title || "Go to this location"
|
||||||
}`;
|
}`;
|
||||||
} else if (clueId < currentClueId) {
|
} else if (clue.available) {
|
||||||
point.classList.add("clue-point-skipped");
|
// Available but not current - could be previous clue or next after skipped
|
||||||
tooltip.textContent = `Skipped Clue ${clueId}: ${
|
point.classList.add("clue-point-current");
|
||||||
clue.title || "Skipped"
|
tooltip.textContent = `Available Clue ${clueId}: ${
|
||||||
|
clue.title || "Available"
|
||||||
}`;
|
}`;
|
||||||
} else {
|
} else {
|
||||||
point.classList.add("clue-point-locked");
|
point.classList.add("clue-point-locked");
|
||||||
@@ -455,7 +464,11 @@ export function updateCluePointStatus(svgElement, clues) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Store availability data
|
// Store availability data
|
||||||
const isAvailable = clue.found || clueId === currentClueId;
|
const isAvailable =
|
||||||
|
clue.found ||
|
||||||
|
clue.skipped ||
|
||||||
|
clueId === currentClueId ||
|
||||||
|
clue.available;
|
||||||
point.setAttribute("data-available", isAvailable.toString());
|
point.setAttribute("data-available", isAvailable.toString());
|
||||||
|
|
||||||
// Make sure tooltip is attached
|
// Make sure tooltip is attached
|
||||||
|
|||||||
Reference in new issue
Block a user