diff --git a/app/(main)/admin/clues/page.js b/app/(main)/admin/clues/page.js index fd4384c..a549718 100644 --- a/app/(main)/admin/clues/page.js +++ b/app/(main)/admin/clues/page.js @@ -29,6 +29,11 @@ export default function AdminCluesPage() { qr_code: "", // Add QR code field }); + const [isReorderMode, setIsReorderMode] = useState(false); + const [reorderingClue, setReorderingClue] = useState(null); + const [newPosition, setNewPosition] = useState(1); + const [reorderMessage, setReorderMessage] = useState(""); + // Check if user is admin useEffect(() => { if (status === "authenticated") { @@ -91,17 +96,21 @@ export default function AdminCluesPage() { try { setLoading(true); - // Add timestamp parameter to bust cache + + // Make sure to get all clues with a high limit to avoid pagination issues const timestamp = Date.now(); - const response = await fetch(`/api/admin/clues?t=${timestamp}`, { - method: "GET", - headers: { - "Content-Type": "application/json", - "Cache-Control": "no-cache, no-store, must-revalidate", - Pragma: "no-cache", - }, - credentials: "include", - }); + const response = await fetch( + `/api/admin/clues?t=${timestamp}&limit=100`, + { + method: "GET", + headers: { + "Content-Type": "application/json", + "Cache-Control": "no-cache, no-store, must-revalidate", + Pragma: "no-cache", + }, + credentials: "include", + } + ); if (!response.ok) { const errorData = await response.json().catch(() => ({})); @@ -117,8 +126,11 @@ export default function AdminCluesPage() { throw new Error("Invalid response format from server"); } - console.log("Fetched clues:", data); // Debug log to see what's coming from API - setClues(data); + // Sort the clues by ID to ensure correct ordering + const sortedClues = [...data].sort((a, b) => a.id - b.id); + + console.log("Fetched clues:", sortedClues); + setClues(sortedClues); setError(""); // Update lastRefresh timestamp setLastRefresh(Date.now()); @@ -311,6 +323,138 @@ export default function AdminCluesPage() { } }; + // Start reordering a clue + const handleReorderStart = (clue) => { + // Disable auto-refresh when starting to reorder + setAutoRefreshEnabled(false); + setReorderingClue(clue); + setNewPosition(clue.id); // Default to current position + setIsReorderMode(true); + }; + + // Handle position selection change + const handlePositionChange = (e) => { + setNewPosition(parseInt(e.target.value)); + }; + + // Cancel reordering + const handleCancelReorder = () => { + setIsReorderMode(false); + setReorderingClue(null); + // Re-enable auto-refresh + setAutoRefreshEnabled(true); + }; + + // Submit reorder request + const handleReorderSubmit = async (e) => { + e.preventDefault(); + + if (!reorderingClue || newPosition < 1 || newPosition > clues.length) { + setError("Invalid position selected"); + return; + } + + try { + setLoading(true); + setReorderMessage(""); + + // Store reordering info for success message + const clueTitle = reorderingClue.title; + const targetPosition = newPosition; + + const response = await fetch("/api/admin/clues/reorder", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + clueId: reorderingClue.id, + newPosition: newPosition, + }), + credentials: "include", + }); + + const data = await response.json(); + + if (!response.ok) { + throw new Error( + data.error || `Failed to reorder clue (Status: ${response.status})` + ); + } + + // Success message + setReorderMessage( + `Clue "${clueTitle}" moved to position ${targetPosition} successfully` + ); + + // Reset all state to ensure fresh data + setClues([]); + setIsReorderMode(false); + setReorderingClue(null); + setError(""); + + // Force a complete refresh with delay to allow database to complete operations + setTimeout(async () => { + try { + await fetchClues(); + console.log("Clues refreshed after reordering"); + } catch (refreshError) { + console.error("Error refreshing clues after reorder:", refreshError); + setError( + "Reordering succeeded but failed to refresh the list. Please reload the page." + ); + } + }, 800); + } catch (err) { + console.error("Error reordering clue:", err); + setError(err.message); + setLoading(false); + } + }; + + // Add a function to force a hard refresh that bypasses cache + const forceHardRefresh = async () => { + try { + setLoading(true); + setClues([]); + + // Clear browser cache for this specific request + const timestamp = Date.now(); + const response = await fetch( + `/api/admin/clues?t=${timestamp}&forceRefresh=true`, + { + method: "GET", + headers: { + "Content-Type": "application/json", + "Cache-Control": "no-cache, no-store, must-revalidate", + Pragma: "no-cache", + Expires: "0", + }, + credentials: "include", + } + ); + + if (!response.ok) { + throw new Error(`Failed to refresh clues (Status: ${response.status})`); + } + + const data = await response.json(); + + // Sort the clues by ID to ensure correct ordering + const sortedClues = [...data].sort((a, b) => a.id - b.id); + + console.log("Force refreshed clues:", sortedClues); + setClues(sortedClues); + setLastRefresh(Date.now()); + setNextRefresh(10); + } catch (err) { + console.error("Error during hard refresh:", err); + setError(`Refresh failed: ${err.message}. Try reloading the page.`); + } finally { + setLoading(false); + } + }; + if (status === "loading" || loading) { return
Loading...
; } @@ -350,199 +494,254 @@ export default function AdminCluesPage() { {error &&

{error}

} {fixMessage &&

{fixMessage}

} - {/* Add New Clue Form */} - {!isEditMode ? ( + {/* Reordering Modal */} + {isReorderMode ? (
-

Add New Clue

-
+

Change Clue Position

+
- - + Move clue "{reorderingClue?.title}" to new position: + + -
- -
- - -
- - {/* Add QR code field with new label */} -
- - + > + {clues.map((_, index) => ( + + ))} +
- QR codes should follow the format QR[number] (e.g., QR1, QR2). - Leave blank to auto-generate. -
-
- - -
-
- ) : ( -
-

Edit Clue

-
-
- - -
- -
- - -
- -
- - -
- -
- - -
- QR codes should follow the format QR[number] (e.g., QR1, QR2). - The QR code value determines what users scan to find this - clue. + This will change the sequence of this clue and update all clue + positions accordingly. QR codes will also be reorganized to + maintain the same sequence.
- )} + ) : ( + <> + {/* Add New Clue Form */} + {!isEditMode ? ( +
+

Add New Clue

+
+
+ + +
- {/* Existing Clues List */} -
-

Existing Clues

-

- QR codes are the values that users scan to find each clue. You can - customize these values, but they should usually follow the format - QR[number]. -

-
- Last updated: {new Date(lastRefresh).toLocaleTimeString()} - {autoRefreshEnabled && !isEditMode && ( - (next refresh in {nextRefresh}s) - )} -
- {clues.length === 0 ? ( -

No clues found.

- ) : ( -
- {clues.map((clue) => ( -
-
-

{clue.title}

-
- - +
+ + +
+ +
+ + +
+ + {/* Add QR code field with new label */} +
+ + +
+ QR codes should follow the format QR[number] (e.g., QR1, + QR2). Leave blank to auto-generate.
-

{clue.description}

- {clue.location && ( -

- Location: {clue.location} -

- )} -

- Position: Clue #{clue.id} in sequence -

-

- QR Code: {clue.qr_code || `QR${clue.id}`} -

+ + + +
+ ) : ( +
+

Edit Clue

+
+
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ QR codes should follow the format QR[number] (e.g., QR1, + QR2). The QR code value determines what users scan to find + this clue. +
+
+ +
+ + +
+
+
+ )} + + {/* Existing Clues List */} +
+

Existing Clues

+ {reorderMessage && ( +

{reorderMessage}

+ )} +

+ QR codes are the values that users scan to find each clue. You + can customize these values, but they should usually follow the + format QR[number]. +

+
+ Last updated: {new Date(lastRefresh).toLocaleTimeString()} + {autoRefreshEnabled && !isEditMode && ( + (next refresh in {nextRefresh}s) + )} +
+ {clues.length === 0 ? ( +

No clues found.

+ ) : ( +
+ {clues.map((clue) => ( +
+
+

{clue.title}

+
+ + + +
+
+

{clue.description}

+ {clue.location && ( +

+ Location: {clue.location} +

+ )} +

+ QR Code:{" "} + {clue.qr_code || `QR${clue.id}`} +

+
+ ))}
- ))} + )}
- )} -
+ + )}
{/* Admin Navigation Bar */} @@ -898,6 +1097,21 @@ export default function AdminCluesPage() { text-align: right; font-style: italic; } + + .reorder-button { + background-color: #9c27b0; + color: white; + border: none; + border-radius: 4px; + padding: 0.4rem 0.6rem; + cursor: pointer; + font-size: 0.9rem; + margin-right: 8px; + } + + .reorder-button:hover { + background-color: #7b1fa2; + } `}
); diff --git a/app/(main)/admin/leaderboard/page.js b/app/(main)/admin/leaderboard/page.js index d9011de..72d31fd 100644 --- a/app/(main)/admin/leaderboard/page.js +++ b/app/(main)/admin/leaderboard/page.js @@ -9,6 +9,10 @@ export default function AdminLeaderboardPage() { const [loading, setLoading] = useState(true); const [error, setError] = useState(""); const router = useRouter(); + // Add state for expanded team details + const [expandedTeam, setExpandedTeam] = useState(null); + const [teamDetails, setTeamDetails] = useState({}); + const [loadingDetails, setLoadingDetails] = useState(false); // Check if user is admin useEffect(() => { @@ -100,6 +104,45 @@ export default function AdminLeaderboardPage() { } }, [session]); + // New function to toggle team details dropdown and fetch data + const toggleTeamDetails = async (teamId) => { + if (expandedTeam === teamId) { + // Close dropdown if already open + setExpandedTeam(null); + } else { + // Open dropdown for this team + setExpandedTeam(teamId); + + // Only fetch if we don't already have data for this team + if (!teamDetails[teamId]) { + setLoadingDetails(true); + try { + const response = await fetch(`/api/admin/teams/${teamId}/stats`, { + headers: { + "Content-Type": "application/json", + "Cache-Control": "no-cache, no-store, must-revalidate", + }, + credentials: "include", + }); + + if (!response.ok) { + throw new Error("Failed to fetch team details"); + } + + const data = await response.json(); + setTeamDetails((prev) => ({ + ...prev, + [teamId]: data, + })); + } catch (err) { + console.error("Error fetching team details:", err); + } finally { + setLoadingDetails(false); + } + } + } + }; + if (status === "loading" || loading) { return
Loading...
; } @@ -146,40 +189,152 @@ export default function AdminLeaderboardPage() { {teams.map((team, index) => ( - - {index + 1} - - {team.name} ({team.code}) - - {team.memberCount} - -
-
- - {team.cluesFound} / {team.totalClues} ( - {team.progress}%) - -
- - - {team.lastActivity - ? new Date(team.lastActivity).toLocaleString() - : "Never"} - - - + +
+ + + {/* Team details dropdown */} + {expandedTeam === team.id && ( + - Reset Progress - - - + +
+ {loadingDetails && !teamDetails[team.id] ? ( +
+ Loading team details... +
+ ) : teamDetails[team.id] ? ( + <> +

Team Stats for {team.name}

+ + {/* Found clues section */} +
+

+ Found Clues ( + {teamDetails[team.id].foundClues.length}) +

+ {teamDetails[team.id].foundClues.length > + 0 ? ( + + + + + + + + + {teamDetails[team.id].foundClues.map( + (clue) => ( + + + + + ) + )} + +
TitleFound At
{clue.title} + {new Date( + clue.found_at + ).toLocaleString()} +
+ ) : ( +

+ No clues found yet +

+ )} +
+ + {/* Skipped clues section */} +
+

+ Skipped Clues ( + {teamDetails[team.id].skippedClues.length} + ) +

+ {teamDetails[team.id].skippedClues.length > + 0 ? ( + + + + + + + + + {teamDetails[ + team.id + ].skippedClues.map((clue) => ( + + + + + ))} + +
TitleSkipped At
{clue.title} + {new Date( + clue.found_at + ).toLocaleString()} +
+ ) : ( +

+ No clues skipped +

+ )} +
+ + ) : ( +
+ Error loading team details +
+ )} +
+ + + )} + ))} @@ -401,6 +556,97 @@ export default function AdminLeaderboardPage() { font-size: 0.8rem; text-shadow: 0 0 2px rgba(255, 255, 255, 0.7); } + + .action-buttons { + display: flex; + gap: 0.5rem; + } + + .details-button { + background-color: #3f51b5; + color: white; + border: none; + border-radius: 4px; + padding: 0.4rem 0.6rem; + cursor: pointer; + font-size: 0.9rem; + } + + .details-button:hover { + background-color: #303f9f; + } + + .team-details-row { + background-color: #f9f9f9; + } + + .team-details-container { + padding: 1.5rem; + margin: 0.5rem 0; + border: 1px solid #e0e0e0; + border-radius: 4px; + background-color: white; + } + + .team-details-container h3 { + color: #3f51b5; + margin-top: 0; + margin-bottom: 1rem; + border-bottom: 1px solid #e0e0e0; + padding-bottom: 0.5rem; + } + + .details-section { + margin-bottom: 1.5rem; + } + + .details-section h4 { + color: #68543c; + margin-bottom: 0.75rem; + } + + .details-table { + width: 100%; + border-collapse: collapse; + margin-bottom: 1rem; + font-size: 0.9rem; + } + + .details-table th { + background-color: #f5f5f5; + font-weight: 600; + text-align: left; + padding: 0.6rem; + border-bottom: 1px solid #ddd; + } + + .details-table td { + padding: 0.6rem; + border-bottom: 1px solid #eee; + } + + .details-table tr:nth-child(even) { + background-color: #fafafa; + } + + .details-loading { + padding: 1rem; + text-align: center; + color: #666; + } + + .details-error { + padding: 1rem; + text-align: center; + color: #d32f2f; + background-color: #ffebee; + border-radius: 4px; + } + + .no-data-message { + font-style: italic; + color: #666; + } `}
); diff --git a/app/(main)/clue/[clueId]/page.js b/app/(main)/clue/[clueId]/page.js index 5d06b08..3bfa499 100644 --- a/app/(main)/clue/[clueId]/page.js +++ b/app/(main)/clue/[clueId]/page.js @@ -354,32 +354,49 @@ export default function CluePage({ params }) { } try { - // Handle multiple QR code formats: "QR123", "123", or even "Clue 123" - let scannedClueId; - // Try regex patterns + if (!data || !clue) return; + + // Normalize the scanned QR code value + const normalizedInput = data.trim().toUpperCase(); + + // Try to use the QR code field, but fall back to a generated one if it's missing or empty + let expectedQrCode = (clue.qr_code || "").trim().toUpperCase(); + + // If QR code is missing or empty, generate a fallback based on the clue ID + if (!expectedQrCode) { + expectedQrCode = `QR${clue.id}`.toUpperCase(); + } + + // First try a direct match with the QR code + if (normalizedInput === expectedQrCode) { + // Exact match with the clue's QR code + setScanMessage("✓ Correct QR code found!"); + markClueAsFound(clueId); + return; + } + + // If not a direct match, try to extract the numeric value const qrPattern = /^QR[^0-9]*(\d+)$/i; const cluePattern = /^clue[^0-9]*(\d+)$/i; const numberPattern = /^(\d+)$/; - let match = - data.match(qrPattern) || - data.match(cluePattern) || - data.match(numberPattern); + // Extract the number from the scanned input + const inputMatch = + normalizedInput.match(qrPattern) || + normalizedInput.match(cluePattern) || + normalizedInput.match(numberPattern); - if (match) { - scannedClueId = parseInt(match[1], 10); - } else { - // Try direct parsing as a last resort - scannedClueId = parseInt(data, 10); - } + // Extract the number from the expected QR code + const expectedMatch = expectedQrCode.match(qrPattern); - if (!isNaN(scannedClueId) && scannedClueId === parseInt(clueId)) { - // Correct QR code scanned, mark as found + if (inputMatch && expectedMatch && inputMatch[1] === expectedMatch[1]) { + // The numeric parts match setScanMessage("✓ Correct QR code found!"); markClueAsFound(clueId); - } else if (!isNaN(scannedClueId)) { + } else if (inputMatch) { + // Found a number in the input, but it doesn't match setScanMessage( - `QR code for clue ${scannedClueId} scanned, but you need clue ${clueId}. Please scan the correct code.` + `QR code "${data}" scanned, but you need to scan "${expectedQrCode}". Please scan the correct code.` ); } else { setScanMessage( diff --git a/app/api/admin/clues/reorder/route.js b/app/api/admin/clues/reorder/route.js new file mode 100644 index 0000000..1ac7251 --- /dev/null +++ b/app/api/admin/clues/reorder/route.js @@ -0,0 +1,238 @@ +import { getToken } from "next-auth/jwt"; +import { query, queryOne, run } from "@/lib/db"; + +export const dynamic = "force-dynamic"; // Disable caching + +// Ensure the user is an admin +async function verifyAdmin(request) { + const token = await getToken({ + req: request, + secret: + process.env.NEXTAUTH_SECRET || + "your-fallback-secret-should-be-at-least-32-chars", + }); + + if (!token) { + return { authorized: false, error: "Unauthorized", status: 401 }; + } + + if (token.role !== "admin") { + return { authorized: false, error: "Admin access required", status: 403 }; + } + + return { authorized: true }; +} + +// POST - Reorder a clue to a new position +export async function POST(request) { + try { + // Verify admin access + const { authorized, error, status } = await verifyAdmin(request); + if (!authorized) { + return Response.json({ error }, { status }); + } + + // Parse the request body + const { clueId, newPosition } = await request.json(); + + // Validate required parameters + if (!clueId || !newPosition) { + return Response.json( + { error: "Clue ID and new position are required" }, + { status: 400 } + ); + } + + // Validate parameters are numbers + if (isNaN(parseInt(clueId)) || isNaN(parseInt(newPosition))) { + return Response.json( + { error: "Clue ID and new position must be numbers" }, + { status: 400 } + ); + } + + // Get all clues to validate new position is in range + const allClues = await query( + "SELECT id, qr_code FROM clues ORDER BY id ASC" + ); + + console.log("All clues before reordering:", allClues); + + if (parseInt(newPosition) < 1 || parseInt(newPosition) > allClues.length) { + return Response.json( + { error: `New position must be between 1 and ${allClues.length}` }, + { status: 400 } + ); + } + + // Check if clue exists + const clue = await queryOne("SELECT id, title FROM clues WHERE id = ?", [ + clueId, + ]); + if (!clue) { + return Response.json({ error: "Clue not found" }, { status: 404 }); + } + + // Start a transaction for the reordering operation + await run("BEGIN TRANSACTION"); + + try { + // Get current position (1-based index) + const currentPosition = + allClues.findIndex((c) => c.id === parseInt(clueId)) + 1; + console.log( + `Reordering clue ${clueId} from position ${currentPosition} to ${newPosition}` + ); + + if (currentPosition === parseInt(newPosition)) { + // No change needed, clue is already in the desired position + await run("ROLLBACK"); + return Response.json({ + success: true, + message: "Clue is already in the requested position", + }); + } + + // Create a temporary table with all clue data + await run(` + CREATE TEMPORARY TABLE temp_clues AS + SELECT id, title, description, location, qr_code, created_at + FROM clues + ORDER BY id ASC + `); + + // Create a new array with the reordered clues + const reorderedClues = [...allClues]; + const [clueToMove] = reorderedClues.splice(currentPosition - 1, 1); + reorderedClues.splice(parseInt(newPosition) - 1, 0, clueToMove); + + console.log( + "Reordered clues array:", + reorderedClues.map((c) => c.id) + ); + + // Create ID mapping before deleting anything + const idMapping = {}; + for (let i = 0; i < reorderedClues.length; i++) { + idMapping[reorderedClues[i].id] = i + 1; // New sequential IDs + } + + console.log("ID Mapping:", idMapping); + + // Get all team_clues entries before deleting + const teamClues = await query( + "SELECT team_id, clue_id, found_at, skipped FROM team_clues" + ); + console.log(`Found ${teamClues.length} team_clues records to update`); + + // Reset SQLite sequence before deletion to avoid autoincrement issues + await run("DELETE FROM sqlite_sequence WHERE name='clues'"); + + // Clear the clues table + await run("DELETE FROM clues"); + console.log("Deleted all clues for reordering"); + + // Insert the clues back in their new order with new IDs + for (let i = 0; i < reorderedClues.length; i++) { + const originalClue = reorderedClues[i]; + + // Get the full clue data from the temp table + const clueData = await queryOne( + ` + SELECT title, description, location, created_at FROM temp_clues WHERE id = ? + `, + [originalClue.id] + ); + + if (!clueData) { + throw new Error( + `Failed to retrieve data for clue ID ${originalClue.id} from temp table` + ); + } + + // Determine the new QR code based on new position + const newQrCode = `QR${i + 1}`; + const newId = i + 1; + + // Insert with the new ID (position) + const insertResult = await run( + ` + INSERT INTO clues (id, title, description, location, qr_code, created_at) + VALUES (?, ?, ?, ?, ?, ?) + `, + [ + newId, + clueData.title, + clueData.description, + clueData.location || "", + newQrCode, + clueData.created_at, + ] + ); + + console.log( + `Inserted clue with new ID ${newId}, was previously ${originalClue.id}, result:`, + insertResult + ); + } + + // Delete all team_clues entries + await run("DELETE FROM team_clues"); + console.log("Deleted all team_clues records for updating"); + + // Reinsert with updated clue_id values + let updatedCount = 0; + for (const tc of teamClues) { + if (idMapping[tc.clue_id]) { + await run( + ` + INSERT INTO team_clues (team_id, clue_id, found_at, skipped) + VALUES (?, ?, ?, ?) + `, + [tc.team_id, idMapping[tc.clue_id], tc.found_at, tc.skipped] + ); + updatedCount++; + } + } + console.log( + `Reinserted ${updatedCount} team_clues records with updated IDs` + ); + + // Drop the temporary table + await run("DROP TABLE temp_clues"); + + // Update SQLite sequence to match our new highest ID + await run(`INSERT INTO sqlite_sequence (name, seq) VALUES ('clues', ${reorderedClues.length}) + ON CONFLICT(name) DO UPDATE SET seq = ${reorderedClues.length}`); + + // Force SQLite to update all internal references + await run("VACUUM"); + + // Commit transaction + await run("COMMIT"); + console.log("Transaction committed successfully"); + + // Verify the changes + const verifyClues = await query( + "SELECT id, title FROM clues ORDER BY id ASC" + ); + console.log("Clues after reordering:", verifyClues); + + return Response.json({ + success: true, + message: `Clue successfully moved from position ${currentPosition} to position ${newPosition}`, + }); + } catch (error) { + // Rollback transaction on error + await run("ROLLBACK"); + console.error("Error during reordering transaction:", error); + throw error; + } + } catch (error) { + console.error("Error reordering clue:", error); + return Response.json( + { error: "Failed to reorder clue: " + error.message }, + { status: 500 } + ); + } +} diff --git a/app/api/admin/teams/[teamId]/stats/route.js b/app/api/admin/teams/[teamId]/stats/route.js new file mode 100644 index 0000000..879cd5b --- /dev/null +++ b/app/api/admin/teams/[teamId]/stats/route.js @@ -0,0 +1,98 @@ +import { getToken } from "next-auth/jwt"; +import { queryOne, query, run } from "@/lib/db"; + +export const dynamic = "force-dynamic"; // Disable caching + +// Ensure the user is an admin +async function verifyAdmin(request) { + const token = await getToken({ + req: request, + secret: + process.env.NEXTAUTH_SECRET || + "your-fallback-secret-should-be-at-least-32-chars", + }); + + if (!token) { + return { authorized: false, error: "Unauthorized", status: 401 }; + } + + if (token.role !== "admin") { + return { authorized: false, error: "Admin access required", status: 403 }; + } + + return { authorized: true }; +} + +// GET - Fetch detailed stats for a specific team +export async function GET(request, context) { + try { + // Verify admin access + const { authorized, error, status } = await verifyAdmin(request); + if (!authorized) { + return Response.json({ error }, { status }); + } + + // Get teamId parameter + const { teamId } = await context.params; + + // Validate teamId is a number + if (isNaN(parseInt(teamId))) { + return Response.json({ error: "Invalid team ID" }, { status: 400 }); + } + + // Check if team exists + const team = await queryOne("SELECT id, name FROM teams WHERE id = ?", [ + teamId, + ]); + if (!team) { + return Response.json({ error: "Team not found" }, { status: 404 }); + } + + // Get all found clues (not skipped) + const foundClues = await query( + ` + SELECT c.id, c.title, tc.found_at + FROM team_clues tc + JOIN clues c ON tc.clue_id = c.id + WHERE tc.team_id = ? AND (tc.skipped = 0 OR tc.skipped IS NULL) + ORDER BY tc.found_at ASC + `, + [teamId] + ); + + // Get all skipped clues + const skippedClues = await query( + ` + SELECT c.id, c.title, tc.found_at + FROM team_clues tc + JOIN clues c ON tc.clue_id = c.id + WHERE tc.team_id = ? AND tc.skipped = 1 + ORDER BY tc.found_at ASC + `, + [teamId] + ); + + // Return the data + return Response.json( + { + teamId: parseInt(teamId), + teamName: team.name, + foundClues, + skippedClues, + }, + { + headers: { + "Cache-Control": "no-store, no-cache, must-revalidate", + Pragma: "no-cache", + Expires: "0", + }, + } + ); + } catch (error) { + console.error("Error fetching team stats:", error); + return Response.json( + { error: "Failed to fetch team stats: " + error.message }, + { status: 500 } + ); + } +} diff --git a/app/api/clues/[clueId]/route.js b/app/api/clues/[clueId]/route.js index 689c8ce..f6f05e1 100644 --- a/app/api/clues/[clueId]/route.js +++ b/app/api/clues/[clueId]/route.js @@ -33,9 +33,9 @@ export async function GET(request, context) { return Response.json({ error: "Invalid clue ID" }, { status: 400 }); } - // Get the specific clue + // Get the specific clue - UPDATED to include qr_code field const clue = await queryOne( - "SELECT id, title, description, location FROM clues WHERE id = ?", + "SELECT id, title, description, location, qr_code FROM clues WHERE id = ?", [clueId] ); diff --git a/clue_hunt.db b/clue_hunt.db index 725003a..a986545 100644 Binary files a/clue_hunt.db and b/clue_hunt.db differ