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 1848e17..371a853 100644
Binary files a/clue_hunt.db and b/clue_hunt.db differ
diff --git a/components/CreateTeamForm.js b/components/CreateTeamForm.js
new file mode 100644
index 0000000..9d89151
--- /dev/null
+++ b/components/CreateTeamForm.js
@@ -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 (
+
+ );
+}
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
+
+
+
+ {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