diff options
| author | Pinapelz <yukais@pinapelz.com> | 2025-07-07 01:07:34 -0700 |
|---|---|---|
| committer | Pinapelz <yukais@pinapelz.com> | 2025-07-07 01:07:41 -0700 |
| commit | 7fe146f97ddd3f5a8d0c1a996a73cb296c28b9cc (patch) | |
| tree | 916d2645c6332fe582ca62572a497724e1c8474a | |
| parent | 152894146b72830e48e800721ea6160228a9bdc1 (diff) | |
implement score deletion
| -rw-r--r-- | backend/schema.prisma | 4 | ||||
| -rw-r--r-- | backend/src/index.ts | 3 | ||||
| -rw-r--r-- | backend/src/routes/score.ts | 30 | ||||
| -rw-r--r-- | frontend/src/components/displays/DancerushScoreDisplay.tsx | 25 | ||||
| -rw-r--r-- | frontend/src/components/displays/GenericScoreDisplay.tsx | 25 | ||||
| -rw-r--r-- | frontend/src/pages/Game.tsx | 0 | ||||
| -rw-r--r-- | frontend/src/pages/Score.tsx | 32 | ||||
| -rw-r--r-- | frontend/src/types/constants.ts | 2 |
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"} |
