aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--backend/src/config/constants.ts2
-rw-r--r--backend/src/routes/score.ts136
-rw-r--r--frontend/src/pages/Score.tsx54
3 files changed, 133 insertions, 59 deletions
diff --git a/backend/src/config/constants.ts b/backend/src/config/constants.ts
index 5ade0a7..8ab5cc7 100644
--- a/backend/src/config/constants.ts
+++ b/backend/src/config/constants.ts
@@ -1 +1 @@
-export const PAGE_SIZE = 30;
+export const PAGE_SIZE = 100;
diff --git a/backend/src/routes/score.ts b/backend/src/routes/score.ts
index 6e4dc19..e0f2281 100644
--- a/backend/src/routes/score.ts
+++ b/backend/src/routes/score.ts
@@ -1,29 +1,39 @@
-import express from 'express';
-import { prisma } from '../config/db';
-import { PAGE_SIZE } from '../config/constants';
+import express from "express";
+import { prisma } from "../config/db";
+import { PAGE_SIZE } from "../config/constants";
-export const handleScoreUpload = async (req: express.Request, res: express.Response) => {
+export const handleScoreUpload = async (
+ req: express.Request,
+ res: express.Response,
+) => {
try {
const { meta, scores } = req.body;
const userId = req.session.userId;
if (!userId) {
- return res.status(401).json({ error: 'Unauthorized. Please log in to upload scores.' });
+ return res
+ .status(401)
+ .json({ error: "Unauthorized. Please log in to upload scores." });
}
// Basic universal validation
if (!meta || !meta.game || !meta.service || !scores) {
- return res.status(400).json({ error: 'Invalid request format. Expected meta with game/service and scores array' });
+ return res.status(400).json({
+ error:
+ "Invalid request format. Expected meta with game/service and scores array",
+ });
}
let game = await prisma.game.findUnique({
- where: { internalName: meta.game }
+ where: { internalName: meta.game },
});
if (!game) {
game = await prisma.game.findFirst({
- where: { formattedName: meta.game }
+ where: { formattedName: meta.game },
});
}
if (!game) {
- return res.status(400).json({ error: `Game '${meta.game}' is not supported. Ensure that you are using the case-sensitive version of either the internal name or formatted name` });
+ return res.status(400).json({
+ error: `Game '${meta.game}' is not supported. Ensure that you are using the case-sensitive version of either the internal name or formatted name`,
+ });
}
const internalGameName = game.internalName;
const scoresArray = Array.isArray(scores) ? scores : [scores];
@@ -38,9 +48,9 @@ export const handleScoreUpload = async (req: express.Request, res: express.Respo
gameInternalName: internalGameName,
userId: userId,
data: {
- equals: scoreData
- }
- }
+ equals: scoreData,
+ },
+ },
});
if (existingScore) {
@@ -50,92 +60,114 @@ export const handleScoreUpload = async (req: express.Request, res: express.Respo
gameInternalName: internalGameName,
userId: userId,
timestamp: scoreData.timestamp,
- data: scoreData
+ data: scoreData,
});
}
}
- const createdScores = scoresToCreate.length > 0
- ? await prisma.score.createMany({
- data: scoresToCreate
- })
- : { count: 0 };
+ const createdScores =
+ scoresToCreate.length > 0
+ ? await prisma.score.createMany({
+ data: scoresToCreate,
+ })
+ : { count: 0 };
res.status(200).json({
- message: 'Score upload processed successfully',
+ message: "Score upload processed successfully",
game: meta.game,
service: meta.service,
scoreCount: createdScores.count,
skippedCount: skippedCount,
- totalProcessed: scoresArray.length
+ totalProcessed: scoresArray.length,
});
-
} catch (error) {
- console.error('Score upload endpoint error:', error);
- res.status(500).json({ error: 'Internal server error. Unable to process score upload' });
+ console.error("Score upload endpoint error:", error);
+ res
+ .status(500)
+ .json({ error: "Internal server error. Unable to process score upload" });
}
-}
+};
-export const handleGetScores = async (req: express.Request, res: express.Response) => {
+export const handleGetScores = async (
+ req: express.Request,
+ res: express.Response,
+) => {
try {
const { userId, internalGameName, pageNum, sortKey, direction } = req.query;
if (!userId || !internalGameName || !pageNum) {
- return res.status(400).json({ error: 'Missing required parameters' });
+ return res.status(400).json({ error: "Missing required parameters" });
}
const pageNumber = parseInt(pageNum as string);
const gameInternalName = internalGameName as string;
const userIdNumber = parseInt(userId as string);
- const sortKeyString = (sortKey as string) || 'timestamp';
- const directionString = ((direction as string)?.toLowerCase() === 'asc') ? 'asc' : 'desc';
+ const sortKeyString = (sortKey as string) || "timestamp";
+ const directionString =
+ (direction as string)?.toLowerCase() === "asc" ? "asc" : "desc";
- const num_pages = Math.ceil(await prisma.score.count({
- where: {
- gameInternalName,
- userId: userIdNumber
- }
- }) / PAGE_SIZE);
+ const num_pages = Math.ceil(
+ (await prisma.score.count({
+ where: {
+ gameInternalName,
+ userId: userIdNumber,
+ },
+ })) / PAGE_SIZE,
+ );
let scores;
- if (sortKeyString === 'timestamp') {
+ if (sortKeyString === "timestamp") {
scores = await prisma.score.findMany({
where: {
gameInternalName,
- userId: userIdNumber
+ userId: userIdNumber,
},
orderBy: {
- timestamp: directionString
+ timestamp: directionString,
},
skip: (pageNumber - 1) * PAGE_SIZE,
- take: PAGE_SIZE
+ take: PAGE_SIZE,
});
- } else if (sortKeyString === 'score') {
- // raw SQL for JSON field ordering
- scores = await prisma.$queryRawUnsafe<any[]>(`
+ } else {
+ // everything else attempt to rawsql it
+ scores = await prisma.$queryRawUnsafe<any[]>(
+ `
SELECT * FROM "Score"
WHERE "gameInternalName" = $1 AND "userId" = $2
- ORDER BY (data->>'score')::numeric ${directionString.toUpperCase()}
+ ORDER BY (data->>'${sortKeyString}')::numeric ${directionString.toUpperCase()}
OFFSET $3
LIMIT $4
- `, gameInternalName, userIdNumber, (pageNumber - 1) * PAGE_SIZE, PAGE_SIZE);
- } else {
- return res.status(400).json({ error: 'Invalid sort key' });
+ `,
+ gameInternalName,
+ userIdNumber,
+ (pageNumber - 1) * PAGE_SIZE,
+ PAGE_SIZE,
+ );
+ }
+ if (!scores) {
+ return res.status(404).json({
+ error:
+ "No scores found. Either no scores exist or the sortKey provided is invalid for the game, sortKey: " +
+ sortKeyString,
+ });
}
- const safeScores = scores.map(score => ({
+ const safeScores = scores.map((score) => ({
...score,
- timestamp: typeof score.timestamp === 'bigint'
- ? Number(score.timestamp)
- : score.timestamp
+ timestamp:
+ typeof score.timestamp === "bigint"
+ ? Number(score.timestamp)
+ : score.timestamp,
}));
res.status(200).json({
scores: safeScores,
- num_pages
+ num_pages,
});
} catch (error) {
- console.error('Failed to fetch scores:', error);
- res.status(500).json({ error: 'Internal server error. Unable to fetch scores' });
+ console.error("Failed to fetch scores:", error);
+ res
+ .status(500)
+ .json({ error: "Internal server error. Unable to fetch scores" });
}
};
diff --git a/frontend/src/pages/Score.tsx b/frontend/src/pages/Score.tsx
index 8e16a86..2bf1a2b 100644
--- a/frontend/src/pages/Score.tsx
+++ b/frontend/src/pages/Score.tsx
@@ -20,6 +20,7 @@ const Score = () => {
const [viewMode, setViewMode] = useState<"cards" | "table">("cards");
const [sortField, setSortField] = useState<SortField>("");
const [sortDirection, setSortDirection] = useState<SortDirection>("asc");
+ const [requestOrder, setRequestOrder] = useState<string>("timestamp");
const gameName =
new URLSearchParams(window.location.search).get("game") || "dancerush";
@@ -33,6 +34,37 @@ const Score = () => {
alert("Network error during logout. Please try again.");
}
};
+
+ const renderRequestFilterMenu = () => {
+ if (gameName === "dancerush") {
+ const filterOptions = [
+ { value: "timestamp", label: "Recent" },
+ { value: "score", label: "Score" },
+ { value: "lamp", label: "Rank" },
+ { value: "lamo_diff", label: "Difficulty" },
+ ];
+
+ return (
+ <div className="flex items-center space-x-2 bg-slate-900/50 backdrop-blur-sm rounded-xl p-1 border border-slate-800/50">
+ {filterOptions.map((option) => (
+ <button
+ key={option.value}
+ onClick={() => setRequestOrder(option.value)}
+ className={`px-4 py-2 rounded-lg text-sm font-medium transition-all duration-200 ${
+ requestOrder === option.value
+ ? "bg-violet-600 text-white shadow-lg shadow-violet-500/25"
+ : "text-slate-300 hover:text-white hover:bg-slate-800/50"
+ }`}
+ >
+ {option.label}
+ </button>
+ ))}
+ </div>
+ );
+ }
+ return null;
+ };
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const flattenScoreData = (score: any) => {
const flat = { ...score, ...score.data };
@@ -51,7 +83,7 @@ const Score = () => {
url.searchParams.append("userId", user.id);
url.searchParams.append("internalGameName", gameName);
url.searchParams.append("pageNum", pageNum.toString());
- url.searchParams.append("sortKey", 'timestamp');
+ url.searchParams.append("sortKey", requestOrder);
url.searchParams.append("direction", "asc");
const response = await fetch(url.toString());
@@ -68,7 +100,7 @@ const Score = () => {
setLoading(false);
}
},
- [user],
+ [user, gameName, requestOrder],
);
useEffect(() => {
@@ -101,7 +133,7 @@ const Score = () => {
return (
<div className="min-h-screen bg-gradient-to-br from-slate-950 via-slate-900 to-slate-950 text-white">
- <NavBar user={user} handleLogout={handleLogout} currentPage="score"/>
+ <NavBar user={user} handleLogout={handleLogout} currentPage="score" />
<main className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
<div className="mb-12">
<div className="flex items-center justify-between mb-4">
@@ -131,9 +163,16 @@ const Score = () => {
</button>
</div>
</div>
- <p className="text-slate-400 text-lg">
- Displaying {scores.length} scores • Page {currentPage} of {numPages}
- </p>
+
+ {/* Filter Menu */}
+ <div className="flex items-center justify-between mb-6">
+ <div className="flex items-center space-x-4">
+ <span className="text-slate-300 text-sm font-medium">
+ Sort by:
+ </span>
+ {renderRequestFilterMenu()}
+ </div>
+ </div>
</div>
{(() => {
@@ -180,6 +219,9 @@ const Score = () => {
</div>
</div>
)}
+ <p className="text-slate-400 mt-4 text-lg">
+ Displaying {scores.length} scores • Page {currentPage} of {numPages}
+ </p>
</main>
</div>
);
send patches to the email below
yukais@pinapelz.com
include the subject [PATCH repo_name]
pinapelz.com
homepage