From 2f9a6c732c4e4c8b1dc490a535d76c85ea603ae5 Mon Sep 17 00:00:00 2001 From: mudabbir-ahmad Date: Tue, 18 Mar 2025 22:25:07 +0000 Subject: [PATCH] fixed skip clue implimentation --- app/(main)/admin/page.js | 2 +- app/(main)/clue/[clueId]/page.js | 107 +++++++++++++++++++++++------ app/(main)/map/map.module.css | 7 ++ app/(main)/map/page.js | 37 ++++++---- app/(main)/stats/page.js | 71 +++++++++++++++---- app/(main)/stats/stats.module.css | 2 + app/api/clues/[clueId]/route.js | 9 +-- app/api/clues/route.js | 78 ++++++++++++++++++--- app/api/mark-clue/route.js | 60 ++++++++++------ app/api/team-stats/route.js | 22 ++++-- app/api/teams/create/route.js | 44 ++++++------ app/api/teams/leave/route.js | 0 clue_hunt.db | Bin 155648 -> 176128 bytes components/CreateTeamForm.js | 56 +++++++++++++++ components/TeamList.js | 109 ++++++++++++++++++++++++++++++ public/images/map.svg | 44 ++---------- styles/globals.css | 0 utils/map-populate.js | 27 ++++++-- 18 files changed, 524 insertions(+), 151 deletions(-) create mode 100644 app/api/teams/leave/route.js create mode 100644 components/CreateTeamForm.js create mode 100644 components/TeamList.js create mode 100644 styles/globals.css diff --git a/app/(main)/admin/page.js b/app/(main)/admin/page.js index 3fdf192..6a85bdb 100644 --- a/app/(main)/admin/page.js +++ b/app/(main)/admin/page.js @@ -122,7 +122,7 @@ export default function AdminDashboard() { const timerData = getHuntTimer(); if (timerData && timerData.startTime) { setTimerDisplay(formatElapsedTime(timerData.startTime)); - + // If there's an end time, check if we've reached it if (timerEndTime && Date.now() >= timerEndTime) { stopTimer(); diff --git a/app/(main)/clue/[clueId]/page.js b/app/(main)/clue/[clueId]/page.js index 61f6726..efb8ccf 100644 --- a/app/(main)/clue/[clueId]/page.js +++ b/app/(main)/clue/[clueId]/page.js @@ -32,6 +32,7 @@ export default function CluePage({ params }) { const [scanMessage, setScanMessage] = useState(null); const [lastRefresh, setLastRefresh] = useState(0); const [clueFound, setClueFound] = useState(false); + const [clueSkipped, setClueSkipped] = useState(false); // Add state for skipped status const [allClues, setAllClues] = useState([]); const [nextClueId, setNextClueId] = useState(null); const [nextClueAvailable, setNextClueAvailable] = useState(false); @@ -126,6 +127,7 @@ export default function CluePage({ params }) { if (isMounted) { setClue(data); setClueFound(data.found); + setClueSkipped(data.skipped); // Set skipped status from API response setLoading(false); 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 const handleNextClue = () => { 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 const handleManualRefresh = () => { setLastRefresh(0); @@ -541,7 +579,7 @@ export default function CluePage({ params }) { )}
- {/* Show a notification when the clue is already found */} + {/* Show notification based on found OR skipped status */} {clueFound && (
✓
@@ -550,6 +588,15 @@ export default function CluePage({ params }) {
)} + + {clueSkipped && !clueFound && ( +
+
↷
+
+ You skipped this clue earlier. Try to solve it now! +
+
+ )} {/* Admin tools section - only visible to admins */} {isAdmin && ( @@ -598,8 +645,8 @@ export default function CluePage({ params }) { {/* Navigation Buttons */}
-
+ + ); } diff --git a/app/(main)/stats/stats.module.css b/app/(main)/stats/stats.module.css index 18b5b9a..a270d03 100644 --- a/app/(main)/stats/stats.module.css +++ b/app/(main)/stats/stats.module.css @@ -16,12 +16,14 @@ .title { font-size: 1.5rem; margin: 0 0 5px 0; + color: white; } .teamName { font-size: 1.2rem; margin: 0; font-weight: normal; + color: white; } .statsCard { diff --git a/app/api/clues/[clueId]/route.js b/app/api/clues/[clueId]/route.js index 5eab2ff..689c8ce 100644 --- a/app/api/clues/[clueId]/route.js +++ b/app/api/clues/[clueId]/route.js @@ -43,9 +43,9 @@ export async function GET(request, context) { return Response.json({ error: "Clue not found" }, { status: 404 }); } - // Check if this team has found this clue before - const found = await queryOne( - "SELECT * FROM team_clues WHERE team_id = ? AND clue_id = ?", + // Check if this team has found or skipped this clue before + const clueStatus = await queryOne( + "SELECT found_at, skipped FROM team_clues WHERE team_id = ? AND clue_id = ?", [token.teamId, clueId] ); @@ -66,7 +66,8 @@ export async function GET(request, context) { return Response.json( { ...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 }, { headers: cacheHeaders } diff --git a/app/api/clues/route.js b/app/api/clues/route.js index 034058c..d2e40af 100644 --- a/app/api/clues/route.js +++ b/app/api/clues/route.js @@ -1,4 +1,4 @@ -import { query, queryOne } from "@/lib/db"; +import { query, queryOne, run } from "@/lib/db"; import { getToken } from "next-auth/jwt"; export const dynamic = "force-dynamic"; // Disable caching @@ -13,18 +13,50 @@ export async function GET(request) { }); if (!token) { + console.log("API: Unauthorized request to /api/clues - no token"); return Response.json({ error: "Unauthorized" }, { status: 401 }); } // Check if user has a team if (!token.teamId) { + console.log("API: User without team tried to access clues"); return Response.json( { error: "You must be part of a team to view clues" }, { 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(` SELECT id, title, description, location FROM clues @@ -34,21 +66,49 @@ export async function GET(request) { // For each clue, check if the team has found it const cluesWithStatus = await Promise.all( clues.map(async (clue) => { - const found = await queryOne( - "SELECT 1 FROM team_clues WHERE team_id = ? AND clue_id = ?", - [token.teamId, clue.id] - ); + let clueStatus; + try { + 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 { ...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: { "Cache-Control": "no-store, no-cache, must-revalidate", Pragma: "no-cache", diff --git a/app/api/mark-clue/route.js b/app/api/mark-clue/route.js index 9ac2b0b..b5efebf 100644 --- a/app/api/mark-clue/route.js +++ b/app/api/mark-clue/route.js @@ -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"; export const dynamic = "force-dynamic"; // Disable caching @@ -25,7 +25,7 @@ export async function POST(request) { } // Parse request body - const { clueId } = await request.json(); + const { clueId, skipped = false } = await request.json(); // Validate clueId is a number if (isNaN(parseInt(clueId))) { @@ -38,49 +38,71 @@ export async function POST(request) { return Response.json({ error: "Clue not found" }, { status: 404 }); } - // Check if clue was already found - const alreadyFound = await queryOne( + // Check if clue was already found or skipped + const alreadyProcessed = await queryOne( "SELECT * FROM team_clues WHERE team_id = ? AND clue_id = ?", [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({ success: true, - message: "This clue was already marked as found", - alreadyFound: true, + message: "This clue was already processed", + alreadyFound: !(alreadyProcessed.skipped === 1), + alreadySkipped: alreadyProcessed.skipped === 1, }); } - // Create team_clues table if it doesn't exist - await queryOne(` + // Ensure the team_clues table has a skipped column + 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, + skipped BOOLEAN DEFAULT 0, PRIMARY KEY (team_id, clue_id), FOREIGN KEY (team_id) REFERENCES teams(id), FOREIGN KEY (clue_id) REFERENCES clues(id) ) `); - // Record that the team found this clue - await queryOne("INSERT INTO team_clues (team_id, clue_id) VALUES (?, ?)", [ - token.teamId, - clueId, - ]); + // Add column if it doesn't exist (safe operation) + try { + await run("ALTER TABLE team_clues ADD COLUMN skipped BOOLEAN DEFAULT 0"); + } 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({ success: true, - message: "Clue marked as found successfully", + message: skipped ? "Clue skipped successfully" : "Clue marked as found", + skipped: skipped, }); } catch (error) { - console.error("Error marking clue as found:", error); + console.error("Error marking clue:", error); return Response.json( - { error: "Failed to mark clue as found: " + error.message }, + { error: "Failed to process clue: " + error.message }, { status: 500 } ); } diff --git a/app/api/team-stats/route.js b/app/api/team-stats/route.js index 41f5eeb..a07f67a 100644 --- a/app/api/team-stats/route.js +++ b/app/api/team-stats/route.js @@ -42,12 +42,25 @@ export async function GET(request) { const totalClues = clueCountResult?.total || 0; // Get all found clues with timestamps, ordered by found time + // Only include clues that aren't marked as skipped const foundClues = 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 = ? + 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 `, [token.teamId] @@ -69,12 +82,12 @@ export async function GET(request) { ); // If there are unfound clues, the ones after the first unfound are "skipped" - let skippedClues = 0; + let skippedCluesCount = 0; if (unfoundClueIds.length > 0) { const lowestUnfoundId = Math.min(...unfoundClueIds); // 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 @@ -107,12 +120,13 @@ export async function GET(request) { totalClues, foundClues, remainingClues: totalClues - foundClues.length, - skippedClues, + skippedCluesCount, currentRank, progress: 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); } catch (error) { console.error("Error fetching team stats:", error); diff --git a/app/api/teams/create/route.js b/app/api/teams/create/route.js index 7e50a9f..be7a613 100644 --- a/app/api/teams/create/route.js +++ b/app/api/teams/create/route.js @@ -1,10 +1,9 @@ import { getToken } from "next-auth/jwt"; -import { query, queryOne, run } from "@/lib/db"; -import { nanoid } from "nanoid"; +import { queryOne, run } from "@/lib/db"; export async function POST(request) { try { - // Get user token from session + // Verify user authentication const token = await getToken({ req: request, secret: @@ -13,30 +12,23 @@ export async function POST(request) { }); 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; - if (!userId) { - return Response.json( - { error: "Invalid user information in session" }, - { status: 401 } - ); - } - // Parse request body - const body = await request.json(); - const { name } = body; + // Get team name from request body + const { name } = await request.json(); - if (!name || name.trim() === "") { + if (!name || !name.trim()) { return Response.json({ error: "Team name is required" }, { status: 400 }); } // 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(` CREATE TABLE IF NOT EXISTS teams ( id INTEGER PRIMARY KEY AUTOINCREMENT, @@ -50,6 +42,7 @@ export async function POST(request) { const existingTeam = await queryOne("SELECT id FROM teams WHERE name = ?", [ name, ]); + if (existingTeam) { return Response.json( { 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 (?, ?)", [ name, code, @@ -71,19 +64,26 @@ export async function POST(request) { // Update the user's team_id 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({ success: true, teamId, - teamName: name, - code, message: "Team created successfully", }); } catch (error) { - console.error("Create team error:", error); + console.error("Error creating team:", error); return Response.json( { error: "Failed to create team: " + error.message }, { 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; +} diff --git a/app/api/teams/leave/route.js b/app/api/teams/leave/route.js new file mode 100644 index 0000000..e69de29 diff --git a/clue_hunt.db b/clue_hunt.db index 1848e177d4eed267220f73bed30627f7ae7d0df9..371a853bb737fc836604448c794feade926de7b8 100644 GIT binary patch delta 21614 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 W>*eSgk`KeYE}NahMUMNH`2PV|()M@& delta 416 zcmZp8z}0YobApr*`!5Ct1~njt0jG&NMojF#CMKw}@8#;?vFEX$Y$)()a|7ct4lW*h zh9q%CMaGuu$sf5SCwp?0ur$>Rvrj(Ar8ZfRd+KCQro(K!uNhqZH76(XNvM~kCg#Q` z=ai-v$LFP%#~Y&ZMS&_gK(=mf;b~x2;N`u}z<-5*6@MLnB) { + 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 ( +
+
+ + 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 + /> +
+ + +
+ ); +} diff --git a/components/TeamList.js b/components/TeamList.js new file mode 100644 index 0000000..d1bb51e --- /dev/null +++ b/components/TeamList.js @@ -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 ( +
+

Join Existing Team

+ +
+
+ + 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 + /> +
+ + + + {error &&

{error}

} +
+ + {teams && teams.length > 0 ? ( +
+

Available Teams:

+
    + {teams.map((team) => ( +
  • setJoinCode(team.code)} + > +
    {team.name}
    +
    Code: {team.code}
    +
  • + ))} +
+
+ ) : ( +

No teams available to join

+ )} +
+ ); +} diff --git a/public/images/map.svg b/public/images/map.svg index d3806eb..f433d30 100644 --- a/public/images/map.svg +++ b/public/images/map.svg @@ -23,84 +23,48 @@ - - - - - - + - - + - - + - - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/styles/globals.css b/styles/globals.css new file mode 100644 index 0000000..e69de29 diff --git a/utils/map-populate.js b/utils/map-populate.js index 1c1c57b..483307b 100644 --- a/utils/map-populate.js +++ b/utils/map-populate.js @@ -409,8 +409,10 @@ export function updateCluePointStatus(svgElement, clues) { `Found ${cluePoints.length} clue points and ${clickAreas.length} click areas to update` ); - // Find the first unfound clue (the current clue) - const currentIndex = clues.findIndex((clue) => !clue.found); + // Find the first unfound and unskipped clue + const currentIndex = clues.findIndex( + (clue) => !clue.found && !clue.skipped + ); const currentClueId = currentIndex !== -1 ? clues[currentIndex].id : null; console.log(`Current clue ID: ${currentClueId}`); @@ -439,15 +441,22 @@ export function updateCluePointStatus(svgElement, clues) { if (clue.found) { point.classList.add("clue-point-found"); 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) { point.classList.add("clue-point-current"); tooltip.textContent = `Current Clue: ${ clue.title || "Go to this location" }`; - } else if (clueId < currentClueId) { - point.classList.add("clue-point-skipped"); - tooltip.textContent = `Skipped Clue ${clueId}: ${ - clue.title || "Skipped" + } else if (clue.available) { + // Available but not current - could be previous clue or next after skipped + point.classList.add("clue-point-current"); + tooltip.textContent = `Available Clue ${clueId}: ${ + clue.title || "Available" }`; } else { point.classList.add("clue-point-locked"); @@ -455,7 +464,11 @@ export function updateCluePointStatus(svgElement, clues) { } // 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()); // Make sure tooltip is attached