From 4fc648449d2275d34a4f94e8e2671d7d05125b1f Mon Sep 17 00:00:00 2001 From: Pinapelz Date: Mon, 7 Jul 2025 11:48:51 -0700 Subject: implement chart view by ID, allow request by pbOnly --- frontend/src/pages/Chart.tsx | 231 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 231 insertions(+) create mode 100644 frontend/src/pages/Chart.tsx (limited to 'frontend/src/pages') diff --git a/frontend/src/pages/Chart.tsx b/frontend/src/pages/Chart.tsx new file mode 100644 index 0000000..4271abe --- /dev/null +++ b/frontend/src/pages/Chart.tsx @@ -0,0 +1,231 @@ +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"; + +const Chart = () => { + 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 chartIdHash = new URLSearchParams(window.location.search).get("chartId") || ""; + if (!chartIdHash) { + navigate("/home"); + } + + const gameName = + new URLSearchParams(window.location.search).get("game") || "dancerush"; + + 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) => ( + + ))} +
+ ); + } + + // 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 fetchScores = useCallback( + async (pageNum: number) => { + if (!user) return; + + setLoading(true); + try { + const url = new URL(import.meta.env.VITE_API_URL + "/scores/" + chartIdHash); + url.searchParams.append("pageNum", pageNum.toString()); + url.searchParams.append("sortKey", requestOrder); + url.searchParams.append("direction", "asc"); + url.searchParams.append("pbOnly", "true"); + + 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, requestOrder, chartIdHash], + ); + + useEffect(() => { + if (user) fetchScores(1); + }, [user, fetchScores]); + + const handleSort = (field: SortField) => { + if (sortField === field) { + setSortDirection(sortDirection === "asc" ? "desc" : "asc"); + } else { + setSortField(field); + setSortDirection("desc"); + } + }; + + if (!user) { + return ; + } + + if (isLoading || loading) { + return ( +
+
+
+

Loading scores...

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

+ {scores.length === 0 ? "Unknown Chart" : scores[0].title} +

+

+ {scores.length === 0 ? "Unknown Artist" : scores[0].artist} +

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

+ Displaying {scores.length} scores • Page {currentPage} of {numPages} +

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