aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorPinapelz <yukais@pinapelz.com>2025-07-07 01:07:34 -0700
committerPinapelz <yukais@pinapelz.com>2025-07-07 01:07:41 -0700
commit7fe146f97ddd3f5a8d0c1a996a73cb296c28b9cc (patch)
tree916d2645c6332fe582ca62572a497724e1c8474a
parent152894146b72830e48e800721ea6160228a9bdc1 (diff)
implement score deletion
-rw-r--r--backend/schema.prisma4
-rw-r--r--backend/src/index.ts3
-rw-r--r--backend/src/routes/score.ts30
-rw-r--r--frontend/src/components/displays/DancerushScoreDisplay.tsx25
-rw-r--r--frontend/src/components/displays/GenericScoreDisplay.tsx25
-rw-r--r--frontend/src/pages/Game.tsx0
-rw-r--r--frontend/src/pages/Score.tsx32
-rw-r--r--frontend/src/types/constants.ts2
8 files changed, 113 insertions, 8 deletions
diff --git a/backend/schema.prisma b/backend/schema.prisma
index a613628..f51093b 100644
--- a/backend/schema.prisma
+++ b/backend/schema.prisma
@@ -34,9 +34,9 @@ model Game {
}
model Score {
- id Int @id @default(autoincrement())
+ id Int @id @default(autoincrement()) // This is the numerical score number (global)
gameInternalName String
- chartId String
+ chartId String // Refers to the unqiue chart identifier
userId Int
timestamp BigInt // in UNIX milliseconds
data Json
diff --git a/backend/src/index.ts b/backend/src/index.ts
index ec38ee6..114dfac 100644
--- a/backend/src/index.ts
+++ b/backend/src/index.ts
@@ -54,7 +54,8 @@ app.get('/api/session', userRoutes.handleGetCurrentSession);
app.get('/api/supportedGames', gameRoutes.handleGetSupportedGames);
app.post('/api/uploadScore', requireAuth, scoreRoutes.handleScoreUpload);
-app.get('/api/scores', scoreRoutes.handleGetScores);
+app.get('/api/scores', requireAuth, scoreRoutes.handleGetScores);
+app.delete('/api/scores', requireAuth, scoreRoutes.handleScoreDeletion);
app.listen(port, () => {
console.log(`Server listening on port ${port}`);
diff --git a/backend/src/routes/score.ts b/backend/src/routes/score.ts
index d0fdc56..54e4784 100644
--- a/backend/src/routes/score.ts
+++ b/backend/src/routes/score.ts
@@ -110,6 +110,36 @@ export const handleScoreUpload = async (
}
};
+export const handleScoreDeletion = async (
+ req: express.Request,
+ res: express.Response,
+) => {
+ try {
+ const { userId, internalGameName, scoreId } = req.query;
+ if (!userId || !internalGameName || !scoreId) {
+ return res.status(400).json({ error: "Missing required parameters" });
+ }
+
+ const userIdNumber = parseInt(userId as string);
+ const scoreIdNumber = parseInt(scoreId as string);
+
+ await prisma.score.deleteMany({
+ where: {
+ userId: userIdNumber,
+ gameInternalName: internalGameName as string,
+ id: scoreIdNumber,
+ },
+ });
+
+ res.status(200).json({ message: "Scores deleted successfully" });
+ } catch (error) {
+ console.error("Score deletion endpoint error:", error);
+ res
+ .status(500)
+ .json({ error: "Internal server error. Unable to delete scores" });
+ }
+};
+
export const handleGetScores = async (
req: express.Request,
res: express.Response,
diff --git a/frontend/src/components/displays/DancerushScoreDisplay.tsx b/frontend/src/components/displays/DancerushScoreDisplay.tsx
index e99f6e9..b030db7 100644
--- a/frontend/src/components/displays/DancerushScoreDisplay.tsx
+++ b/frontend/src/components/displays/DancerushScoreDisplay.tsx
@@ -15,6 +15,7 @@ interface ScoreDisplayProps {
sortField: string;
sortDirection: "asc" | "desc";
onSort: (field: string) => void;
+ onDelete?: (scoreId: number) => void;
}
const DancerushScoreDisplay: React.FC<ScoreDisplayProps> = ({
@@ -23,6 +24,7 @@ const DancerushScoreDisplay: React.FC<ScoreDisplayProps> = ({
sortField,
sortDirection,
onSort,
+ onDelete,
}) => {
// Key mappings for better display names. Hit or miss
const keyDisplayNames: Record<string, string> = {
@@ -242,6 +244,9 @@ const DancerushScoreDisplay: React.FC<ScoreDisplayProps> = ({
"timestamp",
].filter((key) => allKeys.includes(key));
+ // Add actions column if delete function is provided
+ const showActions = onDelete && viewMode === "table";
+
if (scores.length === 0) {
return (
<div className="text-center py-16">
@@ -374,13 +379,18 @@ const DancerushScoreDisplay: React.FC<ScoreDisplayProps> = ({
)}
</th>
))}
+ {showActions && (
+ <th className="px-4 py-3 text-left text-slate-300 font-medium w-16">
+ Actions
+ </th>
+ )}
</tr>
</thead>
<tbody className="divide-y divide-slate-800/50">
{sortedScores.map((score, index) => (
<tr
key={score.id || index}
- className="hover:bg-slate-800/30 transition-colors"
+ className="hover:bg-slate-800/30 transition-colors group"
>
{tableKeys.map((key) => (
<td key={key} className="px-4 py-3">
@@ -411,6 +421,19 @@ const DancerushScoreDisplay: React.FC<ScoreDisplayProps> = ({
)}
</td>
))}
+ {showActions && (
+ <td className="px-4 py-3">
+ <button
+ onClick={() => onDelete(score.id)}
+ className="text-red-400 hover:text-red-300 opacity-100 transition-opacity duration-200 p-1 rounded bg-red-500/10"
+ title="Delete score"
+ >
+ <svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
+ </svg>
+ </button>
+ </td>
+ )}
</tr>
))}
</tbody>
diff --git a/frontend/src/components/displays/GenericScoreDisplay.tsx b/frontend/src/components/displays/GenericScoreDisplay.tsx
index c30e475..45bf1ca 100644
--- a/frontend/src/components/displays/GenericScoreDisplay.tsx
+++ b/frontend/src/components/displays/GenericScoreDisplay.tsx
@@ -13,6 +13,7 @@ interface ScoreDisplayProps {
sortField: string;
sortDirection: "asc" | "desc";
onSort: (field: string) => void;
+ onDelete?: (scoreId: number) => void;
}
const ScoreDisplay: React.FC<ScoreDisplayProps> = ({
@@ -21,6 +22,7 @@ const ScoreDisplay: React.FC<ScoreDisplayProps> = ({
sortField,
sortDirection,
onSort,
+ onDelete,
}) => {
// Key mappings for better display names. Hit or miss
const keyDisplayNames: Record<string, string> = {
@@ -238,6 +240,9 @@ const ScoreDisplay: React.FC<ScoreDisplayProps> = ({
"timestamp",
].filter((key) => allKeys.includes(key));
+ // Add actions column if delete function is provided
+ const showActions = onDelete && viewMode === "table";
+
if (scores.length === 0) {
return (
<div className="text-center py-16">
@@ -369,13 +374,18 @@ const ScoreDisplay: React.FC<ScoreDisplayProps> = ({
)}
</th>
))}
+ {showActions && (
+ <th className="px-4 py-3 text-left text-slate-300 font-medium w-16">
+ Actions
+ </th>
+ )}
</tr>
</thead>
<tbody className="divide-y divide-slate-800/50">
{sortedScores.map((score, index) => (
<tr
key={score.id || index}
- className="hover:bg-slate-800/30 transition-colors"
+ className="hover:bg-slate-800/30 transition-colors group"
>
{tableKeys.map((key) => (
<td key={key} className="px-4 py-3">
@@ -406,6 +416,19 @@ const ScoreDisplay: React.FC<ScoreDisplayProps> = ({
)}
</td>
))}
+ {showActions && (
+ <td className="px-4 py-3">
+ <button
+ onClick={() => onDelete(score.id)}
+ className="text-red-400 hover:text-red-300 opacity-100 transition-opacity duration-200 p-1 rounded bg-red-500/10"
+ title="Delete score"
+ >
+ <svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
+ </svg>
+ </button>
+ </td>
+ )}
</tr>
))}
</tbody>
diff --git a/frontend/src/pages/Game.tsx b/frontend/src/pages/Game.tsx
deleted file mode 100644
index e69de29..0000000
--- a/frontend/src/pages/Game.tsx
+++ /dev/null
diff --git a/frontend/src/pages/Score.tsx b/frontend/src/pages/Score.tsx
index e32f001..0d18d7a 100644
--- a/frontend/src/pages/Score.tsx
+++ b/frontend/src/pages/Score.tsx
@@ -5,7 +5,6 @@ import { NavBar } from "../components/NavBar";
import SessionExpiredPopup from "../components/SessionExpiredPopup";
import ScoreDisplay from "../components/displays/GenericScoreDisplay";
import DancerushScoreDisplay from "../components/displays/DancerushScoreDisplay";
-// TODO: selector for PB/Recent
type SortField = string;
type SortDirection = "asc" | "desc";
@@ -79,7 +78,7 @@ const Score = () => {
url.searchParams.append("sortKey", requestOrder);
url.searchParams.append("direction", "asc");
- const response = await fetch(url.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);
@@ -96,6 +95,33 @@ const Score = () => {
[user, gameName, requestOrder],
);
+ const handleDeleteScore = async (scoreId: number) => {
+ if (!user) return;
+
+ if (!confirm("Are you sure you want to delete this score? This action cannot be undone.")) {
+ return;
+ }
+
+ try {
+ const url = new URL(import.meta.env.VITE_API_URL + "/scores");
+ url.searchParams.append("userId", user.id);
+ url.searchParams.append("internalGameName", gameName);
+ url.searchParams.append("scoreId", scoreId.toString());
+
+ const response = await fetch(url.toString(), {
+ method: "DELETE",
+ credentials: "include",
+ });
+
+ if (!response.ok) throw new Error("Failed to delete score");
+
+ await fetchScores(currentPage);
+ } catch (error) {
+ console.error("Failed to delete score:", error);
+ alert("Failed to delete score. Please try again.");
+ }
+ };
+
useEffect(() => {
if (user) fetchScores(1);
}, [user, fetchScores]);
@@ -175,6 +201,7 @@ const Score = () => {
sortField={sortField}
sortDirection={sortDirection}
onSort={handleSort}
+ onDelete={handleDeleteScore}
/>
);
default:
@@ -185,6 +212,7 @@ const Score = () => {
sortField={sortField}
sortDirection={sortDirection}
onSort={handleSort}
+ onDelete={handleDeleteScore}
/>
);
}
diff --git a/frontend/src/types/constants.ts b/frontend/src/types/constants.ts
index b1309e9..028545d 100644
--- a/frontend/src/types/constants.ts
+++ b/frontend/src/types/constants.ts
@@ -17,7 +17,7 @@ export function getFilterOptions(game: string): { value: string; label: string }
switch (game) {
case "dancerush":
return [
- { value: "timestamp", label: "Recent" },
+ { value: "timestamp", label: "Recently Played" },
{ value: "score", label: "Score" },
{ value: "lamp", label: "Rank" },
{ value: "lamp_diff", label: "Difficulty"}
send patches to the email below
yukais@pinapelz.com
include the subject [PATCH repo_name]
pinapelz.com
homepage