mirror of
https://gitea.tendokyu.moe/pinapelz/Mirage.git
synced 2026-10-10 08:28:13 +03:00
implement score deletion
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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}`);
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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"}
|
||||
|
||||
Reference in New Issue
Block a user