From abca372d8ef3d9ab0154c3706d88e0c3772bacc3 Mon Sep 17 00:00:00 2001 From: Pinapelz Date: Tue, 23 Sep 2025 14:48:00 -0700 Subject: add community scores API and frontend views --- frontend/src/pages/AllScores.tsx | 341 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 341 insertions(+) create mode 100644 frontend/src/pages/AllScores.tsx (limited to 'frontend/src/pages') diff --git a/frontend/src/pages/AllScores.tsx b/frontend/src/pages/AllScores.tsx new file mode 100644 index 0000000..c6fb250 --- /dev/null +++ b/frontend/src/pages/AllScores.tsx @@ -0,0 +1,341 @@ +import { useEffect, useState, useCallback } from "react"; +import { useAuth } from "../contexts/AuthContext"; +import { useNavigate } from "react-router"; +import { NavBar } from "../components/NavBar"; +import SessionExpiredPopup from "../components/SessionExpiredPopup"; +import ScoreDisplay from "../components/displays/GenericScoreDisplay"; +import DancerushScoreDisplay from "../components/displays/DancerushScoreDisplay"; +type SortField = string; +type SortDirection = "asc" | "desc"; + +import { getFilterOptions } from "../types/constants"; + +interface Game { + internalName: string; + formattedName: string; + description: string; +} + +const AllScores = () => { + const { user, isLoading, logout } = useAuth(); + const navigate = useNavigate(); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const [scores, setScores] = useState([]); + const [loading, setLoading] = useState(true); + const [currentPage, setCurrentPage] = useState(1); + const [numPages, setNumPages] = useState(1); + const [viewMode, setViewMode] = useState<"cards" | "table">("cards"); + const [sortField, setSortField] = useState(""); + const [sortDirection, setSortDirection] = useState("asc"); + const [requestOrder, setRequestOrder] = useState("timestamp"); + const [pbOnly, setPbOnly] = useState(true); + const [games, setGames] = useState([]); + const [selectedGame, setSelectedGame] = useState( + new URLSearchParams(window.location.search).get("game") || "" + ); + + const gameName = selectedGame; + + const handleLogout = async () => { + try { + await logout(); + navigate("/"); + } catch (error) { + console.error("Logout failed:", error); + alert("Network error during logout. Please try again."); + } + }; + + const renderRequestFilterMenu = () => { + const filterOptions = getFilterOptions(gameName); + return ( +
+ {filterOptions.map((option) => ( + + ))} +
+ ); + }; + + const renderPbOnlyToggle = () => { + return ( +
+ + +
+ ); + }; + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const flattenScoreData = (score: any) => { + const flat = { ...score, ...score.data }; + delete flat.data; + delete flat.gameInternalName; + return flat; + }; + + const fetchGames = useCallback(async () => { + try { + const response = await fetch(import.meta.env.VITE_API_URL + "/supportedGames"); + if (!response.ok) throw new Error("Failed to fetch games"); + const data = await response.json(); + setGames(data); + if (!selectedGame && data.length > 0) { + setSelectedGame(data[0].internalName); + } + } catch (error) { + console.error("Failed to load games:", error); + alert("Failed to load games. Please refresh the page."); + } + }, [selectedGame]); + + const fetchScores = useCallback( + async (pageNum: number) => { + if (!user || !gameName) return; + + setLoading(true); + try { + const url = new URL(import.meta.env.VITE_API_URL + "/allScores"); + url.searchParams.append("internalGameName", gameName); + url.searchParams.append("pageNum", pageNum.toString()); + url.searchParams.append("sortKey", requestOrder); + // Always sort by timestamp in desc order by default to show most recent first + // For other fields, also default to desc to show highest values first + url.searchParams.append("direction", "desc"); + url.searchParams.append("pbOnly", pbOnly.toString()); + + const response = await fetch(url.toString(), {credentials: 'include'}); + if (!response.ok) throw new Error("Failed to fetch scores"); + const data = await response.json(); + const flattened = data.scores.map(flattenScoreData); + setScores(flattened); + setNumPages(data.num_pages); + setCurrentPage(pageNum); + } catch (error) { + console.error("Failed to load scores:", error); + alert("Failed to load scores. Please refresh the page."); + } finally { + setLoading(false); + } + }, + [user, gameName, requestOrder, pbOnly], + ); + + useEffect(() => { + fetchGames(); + }, [fetchGames]); + + useEffect(() => { + if (user && gameName) { + fetchScores(1); + } + }, [user, fetchScores, gameName]); + + const handleSort = (field: SortField) => { + if (sortField === field) { + setSortDirection(sortDirection === "asc" ? "desc" : "asc"); + } else { + setSortField(field); + setSortDirection("desc"); + } + }; + + const handleGameChange = (gameInternalName: string) => { + setSelectedGame(gameInternalName); + setCurrentPage(1); + // Reset sort order to timestamp to show most recent scores first + setRequestOrder("timestamp"); + // Update URL parameter + const url = new URL(window.location.href); + url.searchParams.set("game", gameInternalName); + window.history.replaceState({}, "", url.toString()); + }; + + if (!user) { + return ; + } + + if (isLoading) { + return ( +
+
+
+

Loading games...

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

+ {gameName + ? `${games.find(g => g.internalName === gameName)?.formattedName || gameName} - Community Scores` + : "Community Scores" + } +

+
+ + +
+
+ + {/* Game Selection */} +
+ +
+ +
+ + + +
+
+
+ + {/* Filter Menu */} +
+
+ {gameName && renderRequestFilterMenu()} + {renderPbOnlyToggle()} +
+
+ +

+ {pbOnly + ? "Showing personal best scores for each chart from all players" + : "Showing all recently received scores from all players" + }{gameName ? ` for ${games.find(g => g.internalName === gameName)?.formattedName || gameName}` : ""} +

+
+ + {!gameName ? ( +
+

Please select a game to view scores

+
+ ) : loading ? ( +
+
+

Loading community scores...

+
+ ) : (() => { + switch (gameName) { + case "dancerush": + return ( + + ); + default: + return ( + + ); + } + })()} + + {numPages > 1 && ( +
+
+ {[...Array(numPages)].map((_, i) => ( + + ))} +
+
+ )} +

+ {loading ? "Loading..." : `Displaying ${scores.length} scores • Page ${currentPage} of ${numPages}`} +

+
+
+ ); +}; + +export default AllScores; -- cgit v1.2.3