aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorPinapelz <yukais@pinapelz.com>2025-12-07 18:03:59 -0800
committerPinapelz <yukais@pinapelz.com>2025-12-07 18:03:59 -0800
commitf4be4722d127e2394bc63e5443592f97ec5d978c (patch)
treece7df750de86b90b8faa3bf25cc325b55a2035ff
parent4195eb5f8fad4fe7923b8c77757061a414029564 (diff)
add score export on personal score view pages
-rw-r--r--backend/src/routes/score.ts6
-rw-r--r--frontend/src/pages/Score.tsx53
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 →
send patches to the email below
yukais@pinapelz.com
include the subject [PATCH repo_name]
pinapelz.com
homepage