fixed skip clue implimentation

This commit is contained in:
bobbert committed 2025-03-18 22:25:07 +00:00
1 parent fccc2a4edc
commit 2f9a6c732c
18 files changed
+524 -151

No files matched your search

+1 -1
View File
@@ -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();
+88 -19
View File
@@ -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;
+7
View File
@@ -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
View File
@@ -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
View File
@@ -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>
); );
} }
+2
View File
@@ -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 {
+5 -4
View File
@@ -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
View File
@@ -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
View File
@@ -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 }
); );
} }
+18 -4
View File
@@ -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);
+22 -22
View File
@@ -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;
}
View File
Whitespace-only changes.
BIN
View File
Binary file not shown.
+56
View File
@@ -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>
);
}
+109
View File
@@ -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
View File
@@ -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

View File
Whitespace-only changes.
+20 -7
View File
@@ -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