implement score deletion

This commit is contained in:
Pinapelz
2025-07-07 01:07:41 -07:00
parent 152894146b
commit 7fe146f97d
8 changed files with 113 additions and 8 deletions
+2 -2
View File
@@ -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
+2 -1
View File
@@ -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}`);
+30
View File
@@ -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,
@@ -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>
@@ -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>
View File
+30 -2
View File
@@ -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}
/>
);
}
+1 -1
View File
@@ -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"}