From 0269fdfe68e9ea99360542aa9f95e800f7afb369 Mon Sep 17 00:00:00 2001 From: mudabbir-ahmad Date: Sun, 16 Mar 2025 17:33:55 +0000 Subject: [PATCH] all that's left is adding slightly better error handling for map to check if a hunt is active or not and to add the clues pages. Grey = cant click Red = current Green = complete blue = skipped? --- app/(main)/[teamId]/page.js | 21 +-- app/(main)/stats/page.js | 210 ++++++++++++++++++++++++++ app/(main)/stats/stats.module.css | 243 ++++++++++++++++++++++++++++++ app/(main)/team/[teamId]/page.js | 32 ++-- app/api/team-stats/route.js | 119 +++++++++++++++ clue_hunt.db | Bin 86016 -> 86016 bytes components/NavBar.js | 0 utils/navigationGuard.js | 0 8 files changed, 602 insertions(+), 23 deletions(-) create mode 100644 app/(main)/stats/page.js create mode 100644 app/(main)/stats/stats.module.css create mode 100644 app/api/team-stats/route.js create mode 100644 components/NavBar.js create mode 100644 utils/navigationGuard.js diff --git a/app/(main)/[teamId]/page.js b/app/(main)/[teamId]/page.js index c9672ff..b6b1e7b 100644 --- a/app/(main)/[teamId]/page.js +++ b/app/(main)/[teamId]/page.js @@ -275,23 +275,26 @@ export default function TeamPage({ params }) { )} + {/* Replace the outdated navbar with the correct one */}
- {/* Update the Home link to point to the current team page */} - -
🏠
-
Home
+ +
πŸ—ΊοΈ
+
Map
- +
πŸ“Š
Stats
- +
πŸ‘₯
Team
- -
πŸ‘€
-
Profile
+ +
πŸ†
+
Leaderboard
diff --git a/app/(main)/stats/page.js b/app/(main)/stats/page.js new file mode 100644 index 0000000..a1e99a9 --- /dev/null +++ b/app/(main)/stats/page.js @@ -0,0 +1,210 @@ +"use client"; +import { useState, useEffect } from "react"; +import { useSession } from "next-auth/react"; +import { useRouter } from "next/navigation"; +import Link from "next/link"; +import styles from "./stats.module.css"; + +export default function StatsPage() { + const { data: session, status } = useSession(); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + const [teamStats, setTeamStats] = useState(null); + const [expanded, setExpanded] = useState(false); + const router = useRouter(); + + useEffect(() => { + // Redirect to login if not authenticated + if (status === "unauthenticated") { + router.push("/login"); + return; + } + + if (status === "authenticated") { + fetchTeamStats(); + } + }, [status, router]); + + const fetchTeamStats = async () => { + try { + setLoading(true); + setError(null); + const response = await fetch("/api/team-stats", { + method: "GET", + headers: { + "Content-Type": "application/json", + "Cache-Control": "no-cache, no-store, must-revalidate", + }, + credentials: "include", + }); + + if (!response.ok) { + // Handle specific error cases + const errorData = await response.json().catch(() => ({})); + + if (response.status === 401) { + throw new Error("Authentication error: Not logged in"); + } else if ( + response.status === 404 || + errorData.error?.includes("not part of a team") + ) { + throw new Error("You need to join a team before viewing stats"); + } else { + throw new Error(errorData.error || "Failed to fetch team stats"); + } + } + + const data = await response.json(); + setTeamStats(data); + } catch (err) { + console.error("Error fetching team stats:", err); + setError(err.message); + } finally { + setLoading(false); + } + }; + + const toggleExpanded = () => { + setExpanded(!expanded); + }; + + // Format timestamp to a more readable format + const formatTimestamp = (timestamp) => { + const date = new Date(timestamp); + return date.toLocaleString(undefined, { + month: "short", + day: "numeric", + hour: "numeric", + minute: "2-digit", + }); + }; + + if (loading) { + return ( +
+
+

Loading team stats...

+
+ ); + } + + if (error) { + return ( +
+
+

Error

+

{error}

+
+ + +
+
+
+ ); + } + + if (!teamStats) { + return ( +
+

No team stats available.

+
+ ); + } + + return ( +
+
+

Team Stats

+

{teamStats.team.name}

+
+ +
+
+
+ Total Clues: + {teamStats.totalClues} +
+
+ Clues Found: + + {teamStats.foundClues.length} + +
+
+ Clues Remaining: + {teamStats.remainingClues} +
+
+ Clues Skipped: + {teamStats.skippedClues} +
+
+ Current Rank: + {teamStats.currentRank} +
+
+ +
+ + + {expanded && ( +
+ {teamStats.foundClues.length === 0 ? ( +

No clues found yet.

+ ) : ( + teamStats.foundClues.map((clue) => ( +
+ {clue.title} + + {formatTimestamp(clue.found_at)} + +
+ )) + )} +
+ )} +
+
+ +
+ +
πŸ—ΊοΈ
+
Map
+ + +
πŸ“Š
+
Stats
+ + +
πŸ‘₯
+
Team
+ + +
πŸ†
+
Leaderboard
+ +
+
+ ); +} diff --git a/app/(main)/stats/stats.module.css b/app/(main)/stats/stats.module.css new file mode 100644 index 0000000..18b5b9a --- /dev/null +++ b/app/(main)/stats/stats.module.css @@ -0,0 +1,243 @@ +.container { + display: flex; + flex-direction: column; + min-height: 100vh; + background-color: #f4f4f4; + padding-bottom: 70px; /* Space for the navbar */ +} + +.header { + background-color: #68543c; + color: white; + padding: 20px; + text-align: center; +} + +.title { + font-size: 1.5rem; + margin: 0 0 5px 0; +} + +.teamName { + font-size: 1.2rem; + margin: 0; + font-weight: normal; +} + +.statsCard { + background-color: white; + border-radius: 8px; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); + margin: 20px; + overflow: hidden; +} + +.summaryStats { + padding: 20px; + border-bottom: 1px solid #eee; +} + +.statItem { + display: flex; + justify-content: space-between; + padding: 10px 0; + border-bottom: 1px solid #f0f0f0; +} + +.statItem:last-child { + border-bottom: none; +} + +.rankItem { + font-weight: bold; + color: #68543c; + margin-top: 10px; + padding-top: 10px; + border-top: 2px dashed #eee; +} + +.statLabel { + color: #555; +} + +.statValue { + font-weight: bold; +} + +.collapsibleSection { + overflow: hidden; +} + +.collapsibleButton { + width: 100%; + padding: 15px 20px; + text-align: left; + background-color: #f5f5f5; + border: none; + outline: none; + cursor: pointer; + display: flex; + justify-content: space-between; + align-items: center; + font-size: 1rem; + font-weight: 600; + color: #333; + transition: background-color 0.2s; +} + +.collapsibleButton:hover { + background-color: #e9e9e9; +} + +.collapsibleTitle { + display: flex; + align-items: center; +} + +.collapsibleIcon { + font-size: 0.8rem; + transition: transform 0.3s ease; +} + +.cluesList { + padding: 0; +} + +.clueItem { + display: flex; + justify-content: space-between; + align-items: center; + padding: 15px 20px; + border-bottom: 1px solid #f0f0f0; +} + +.clueItem:last-child { + border-bottom: none; +} + +.clueTitle { + flex: 1; + font-size: 0.95rem; +} + +.clueTimestamp { + color: #888; + font-size: 0.85rem; +} + +.emptyMessage { + padding: 20px; + text-align: center; + color: #888; + font-style: italic; +} + +/* Loading state */ +.loadingSpinner { + border: 4px solid rgba(0, 0, 0, 0.1); + border-radius: 50%; + border-top: 4px solid #68543c; + width: 40px; + height: 40px; + animation: spin 1s linear infinite; + margin: 50px auto 20px; +} + +@keyframes spin { + 0% { + transform: rotate(0deg); + } + 100% { + transform: rotate(360deg); + } +} + +.loadingText { + text-align: center; + color: #555; +} + +/* Error state */ +.errorContainer { + margin: 40px 20px; + background-color: #fff3f3; + border: 1px solid #ffcdd2; + border-radius: 8px; + padding: 20px; + text-align: center; +} + +.errorTitle { + color: #d32f2f; + margin-top: 0; +} + +.buttonContainer { + display: flex; + justify-content: center; + gap: 10px; + margin-top: 20px; +} + +.retryButton, +.teamButton { + padding: 8px 16px; + border: none; + border-radius: 4px; + cursor: pointer; + font-size: 0.9rem; +} + +.retryButton { + background-color: #68543c; + color: white; +} + +.teamButton { + background-color: #4caf50; + color: white; +} + +.noData { + text-align: center; + padding: 40px 0; + color: #888; +} + +/* Navigation Bar */ +.navBar { + display: flex; + justify-content: space-around; + background-color: white; + border-top: 1px solid #ddd; + padding: 10px 0; + position: fixed; + bottom: 0; + left: 0; + width: 100%; + box-sizing: border-box; + z-index: 100; +} + +.navItem { + display: flex; + flex-direction: column; + align-items: center; + text-decoration: none; + color: #666; + flex: 1; + padding: 8px 0; +} + +.navIcon { + font-size: 22px; + margin-bottom: 3px; +} + +.navLabel { + font-size: 12px; +} + +.navItem.active { + color: #68543c; +} diff --git a/app/(main)/team/[teamId]/page.js b/app/(main)/team/[teamId]/page.js index 44409da..265eb48 100644 --- a/app/(main)/team/[teamId]/page.js +++ b/app/(main)/team/[teamId]/page.js @@ -341,22 +341,26 @@ export default function TeamPage({ params }) { )} -
- -
🏠
-
Home
- - -
πŸ“Š
+ {/* Replace the outdated navbar with this consistent design */}av-bar"]}> +
v-item"]}> + >πŸ—ΊοΈ
+
πŸ—ΊοΈ
>Map
+
Map
+ v-item"]}> + }>πŸ“Š +
πŸ“Š
tats
Stats
- - -
πŸ‘₯
+ + }>πŸ‘₯ +
πŸ‘₯
Team
Team
- - -
πŸ‘€
-
Profile
+ v-item"]}> + }>πŸ† +
πŸ†
oard +
Leaderboard
diff --git a/app/api/team-stats/route.js b/app/api/team-stats/route.js new file mode 100644 index 0000000..3722c1d --- /dev/null +++ b/app/api/team-stats/route.js @@ -0,0 +1,119 @@ +import { query, queryOne } from "@/lib/db"; +import { getToken } from "next-auth/jwt"; + +export const dynamic = "force-dynamic"; // Disable caching + +export async function GET(request) { + try { + // Verify authentication + const token = await getToken({ + req: request, + secret: + process.env.NEXTAUTH_SECRET || + "your-fallback-secret-should-be-at-least-32-chars", + }); + + if (!token) { + return Response.json({ error: "Unauthorized" }, { status: 401 }); + } + + // Check if user has a team + if (!token.teamId) { + return Response.json( + { error: "You are not part of a team" }, + { status: 404 } + ); + } + + // Get team information + const team = await queryOne( + "SELECT id, name, code FROM teams WHERE id = ?", + [token.teamId] + ); + + if (!team) { + return Response.json({ error: "Team not found" }, { status: 404 }); + } + + // Get total number of clues + const clueCountResult = await queryOne( + "SELECT COUNT(*) as total FROM clues" + ); + const totalClues = clueCountResult?.total || 0; + + // Get all found clues with timestamps, ordered by found time + 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 = ? + ORDER BY tc.found_at ASC + `, + [token.teamId] + ); + + // We need to calculate skipped clues - these are clues that have higher IDs than the user's lowest unfound clue + // First, get all clue IDs + const allClueIds = await query("SELECT id FROM clues ORDER BY id ASC"); + + // Convert to a simple array of IDs + const allClueIdArray = allClueIds.map((c) => c.id); + + // Create a set of found clue IDs for easy lookup + const foundClueIdSet = new Set(foundClues.map((c) => c.id)); + + // Find all unfound clue IDs + const unfoundClueIds = allClueIdArray.filter( + (id) => !foundClueIdSet.has(id) + ); + + // If there are unfound clues, the ones after the first unfound are "skipped" + let skippedClues = 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; + } + + // Get current team rank + const teamRankings = await query(` + SELECT + t.id, + COUNT(DISTINCT tc.clue_id) as cluesFound + FROM + teams t + LEFT JOIN + team_clues tc ON t.id = tc.team_id + GROUP BY + t.id + ORDER BY + cluesFound DESC + `); + + // Find current team's position in the ranking + let currentRank = teamRankings.findIndex((t) => t.id === team.id) + 1; + if (currentRank === 0) currentRank = teamRankings.length + 1; // If not found (shouldn't happen) + + // Construct response object + const teamStats = { + team, + totalClues, + foundClues, + remainingClues: totalClues - foundClues.length, + skippedClues, + currentRank, + progress: + totalClues > 0 ? Math.round((foundClues.length / totalClues) * 100) : 0, + }; + + return Response.json(teamStats); + } 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/clue_hunt.db b/clue_hunt.db index d71985802d581fcf64335515db0d05ac02fe4df5..a0d1553ebd046a806a9d00a130b4e8ffed0e9c13 100644 GIT binary patch delta 263 zcmZozz}m2Yb%Hdb$3z)tMvsjN&i0H?H!rb|HDKe>U=ZiJF`2zUnTP8J_ZRL1+&$b8 z+$x(51r~8}cs3@mF>u)0PBy4fjtDU{w=y)dGB(q*G&GDdGB7gLH89pSG*d7%wK6fb zGBP!6Oa-ZCFhp+$lo#GKflYwrH3R?a?Scx75BWueGnkddGfMMH;uDig hGRsoa5_5_d|6eb_n6c@=MgPf(HImz3@iU4!003S0O9%h} delta 170 zcmV;b09F5hpap=S1&|v7Ly;Uq0YkB1Lq7rJv!p*=Fa`hs02T{alLv4k3JfR<^b5cX zj0;$^F%TpR12KVQlaG5alZ$(u12KVMlh2P9m*o!u5tABw46|N*r~?Ge01wTR4uBc6 zBY?sW2s$z~E;cbZT9d&a6S9y75CrT15A3%QAOYbI6cZ9>10xk^b#8QDVPkY@c4c&O Yb))~U5CLbhz|l{W(T^0j