diff options
| -rw-r--r-- | backend/src/routes/score.ts | 6 | ||||
| -rw-r--r-- | frontend/src/pages/Score.tsx | 53 |
2 files changed, 49 insertions, 10 deletions
diff --git a/backend/src/routes/score.ts b/backend/src/routes/score.ts index a10833d..f6d2c52 100644 --- a/backend/src/routes/score.ts +++ b/backend/src/routes/score.ts @@ -148,15 +148,17 @@ export const handleExportScoreForGame = async ( const { internalGameName, page } = req.query; const userId = req.session.userId; if (!userId || !internalGameName) { + console.log(userId); + console.log(internalGameName); return res.status(400).json({ error: "Missing required parameters" }); } - const offset = (Math.max(parseInt(page as string) || 1, 1) - 1) * 50; + const offset = (Math.max(parseInt(page as string) || 1, 1) - 1) * PAGE_SIZE; const scores: any = await prisma.$queryRaw` SELECT * FROM "Score" WHERE "userId" = ${userId} AND "gameInternalName" = ${internalGameName} ORDER BY (data->>'timestamp')::numeric desc - OFFSET ${offset} LIMIT 50 + OFFSET ${offset} LIMIT ${PAGE_SIZE} `; const safeScores = scores.map((score: any) => ({ ...score, diff --git a/frontend/src/pages/Score.tsx b/frontend/src/pages/Score.tsx index 8787137..f1d4049 100644 --- a/frontend/src/pages/Score.tsx +++ b/frontend/src/pages/Score.tsx @@ -1,9 +1,9 @@ import { useEffect, useState, useCallback } from "react"; -import LoadingDisplay from "../components/LoadingDisplay"; +import LoadingDisplay from "../components/LoadingDisplay"; import { useAuth } from "../contexts/AuthContext"; import { useNavigate } from "react-router"; import { NavBar } from "../components/NavBar"; -import type { SupportedGame} from "../types/game"; +import type { SupportedGame } from "../types/game"; import SessionExpiredPopup from "../components/SessionExpiredPopup"; import ScoreDisplay from "../components/displays/GenericScoreDisplay"; import DancerushScoreDisplay from "../components/displays/DancerushScoreDisplay"; @@ -48,6 +48,31 @@ const Score = () => { } }; + const exportScores = async () => { + try { + const response = await fetch( + import.meta.env.VITE_API_URL + + `/exportScores?internalGameName=${gameName}&page=${currentPage}`, + { + headers: { + "Content-Type": "application/json", + }, + credentials: "include", + }, + ); + const blob = await response.blob(); + const url = window.URL.createObjectURL(blob); + const a = document.createElement("a"); + a.href = url; + a.download = `scores_${gameName}_${currentPage}.json`; + a.click(); + window.URL.revokeObjectURL(url); + } catch (error) { + console.error("Export failed:", error); + alert("Network error during export. Please try again."); + } + }; + useEffect(() => { try{ fetch(import.meta.env.VITE_API_URL + `/supportedGames`) @@ -107,8 +132,7 @@ const Score = () => { } if(targetUserId && targetUserId !== user.id.toString()){ setViewingOwnScores(false); - } - else{ + } else { setViewingOwnScores(true); } url.searchParams.append("internalGameName", gameName); @@ -116,7 +140,9 @@ const Score = () => { url.searchParams.append("sortKey", requestOrder); url.searchParams.append("direction", "asc"); - const response = await fetch(url.toString(), {credentials: 'include'}); + const response = await fetch(url.toString(), { + credentials: "include", + }); if (!response.ok) throw new Error("Failed to fetch scores"); const data = await response.json(); setUsername(data.user); @@ -137,7 +163,11 @@ const Score = () => { const handleDeleteScore = async (scoreId: number) => { if (!user) return; - if (!confirm("Are you sure you want to delete this score? This action cannot be undone.")) { + if ( + !confirm( + "Are you sure you want to delete this score? This action cannot be undone.", + ) + ) { return; } @@ -180,7 +210,7 @@ const Score = () => { if (isLoading || loading) { return ( - <LoadingDisplay message={"Loading Scores for " + formattedGameName}/> + <LoadingDisplay message={"Loading Scores for " + formattedGameName} /> ); } @@ -191,7 +221,8 @@ const Score = () => { <div className="mb-6 sm:mb-12"> <div className="flex flex-col sm:flex-row items-start sm:items-center justify-between gap-4 mb-4"> <h1 className="text-2xl sm:text-3xl md:text-4xl font-bold bg-linear-to-r from-violet-400 to-violet-600 bg-clip-text text-transparent"> - {viewingOwnScores ? "Your Scores" : `${username}'s Scores`} for {formattedGameName} + {viewingOwnScores ? "Your Scores" : `${username}'s Scores`} for{" "} + {formattedGameName} </h1> <div className="flex items-center space-x-1 sm:space-x-2 bg-slate-900/50 backdrop-blur-sm rounded-xl p-1 border border-slate-800/50"> <button @@ -340,6 +371,12 @@ const Score = () => { <p className="text-slate-400 mt-4 text-sm sm:text-base md:text-lg text-center"> Displaying {scores.length} scores • Page {currentPage} of {numPages} </p> + <p + className="text-slate-400 underline mt-4 text-sm sm:text-base md:text-lg text-center cursor-pointer" + onClick={exportScores} + > + {viewingOwnScores ? "Export Scores on Page" : null} + </p> {viewMode === "table" && ( <p className="text-slate-500 mt-2 text-xs text-center md:hidden"> ← Swipe horizontally to see more → |
