diff options
| author | Pinapelz <yukais@pinapelz.com> | 2025-11-04 18:35:54 -0800 |
|---|---|---|
| committer | Pinapelz <yukais@pinapelz.com> | 2025-11-04 18:35:54 -0800 |
| commit | c71ababd5bbc7ab31f9f74eb2794e4cd04ba8d08 (patch) | |
| tree | 7caba0aab623b8e552c5841639e2fb3b4d365be2 | |
| parent | f3f8eaca7a7340cf268405c1f95ea38806ad23ad (diff) | |
add recently played games to profile page
| -rw-r--r-- | backend/src/routes/score.ts | 11 | ||||
| -rw-r--r-- | backend/src/routes/user.ts | 27 | ||||
| -rw-r--r-- | frontend/src/pages/Profile.tsx | 28 | ||||
| -rw-r--r-- | frontend/src/pages/Score.tsx | 19 |
4 files changed, 76 insertions, 9 deletions
diff --git a/backend/src/routes/score.ts b/backend/src/routes/score.ts index a652b93..168629a 100644 --- a/backend/src/routes/score.ts +++ b/backend/src/routes/score.ts @@ -181,6 +181,7 @@ export const handleGetScores = async ( if (!userId || !internalGameName || !pageNum) { 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); @@ -200,6 +201,15 @@ export const handleGetScores = async ( let scores; let totalScores; + const user = await prisma.user.findUnique({ + where: { id: userIdNumber }, + select: { username: true }, + }); + + if (!user) { + return res.status(404).json({ error: "User not found" }); + } + if (pbOnlyFlag) { // For pbOnly, we need to get the best score for each chart if (sortKeyString === "timestamp") { @@ -359,6 +369,7 @@ export const handleGetScores = async ( res.status(200).json({ scores: safeScores, num_pages, + user: user.username }); } catch (error) { console.error("Failed to fetch scores:", error); diff --git a/backend/src/routes/user.ts b/backend/src/routes/user.ts index 497d6da..b99964b 100644 --- a/backend/src/routes/user.ts +++ b/backend/src/routes/user.ts @@ -2,6 +2,11 @@ import express from 'express'; import { prisma } from '../config/db'; +interface recentPlayedGame { + gameInternalName: string; + timestamp: BigInt; +} + export const handleMeRoute = async (req: express.Request, res: express.Response) => { try { const { userId } = req.query; @@ -12,13 +17,31 @@ export const handleMeRoute = async (req: express.Request, res: express.Response) where: { id: parseInt(userId as string) }, select: { id: true, username: true, isAdmin: true, bio: true } }); + const recentPlayedGames: recentPlayedGame[] = await prisma.$queryRaw` + SELECT DISTINCT ON (s."gameInternalName") + g."formattedName", + s."gameInternalName", + s."timestamp" + FROM "Score" s + INNER JOIN "Game" g ON g."internalName" = s."gameInternalName" + WHERE s."userId" = ${parseInt(userId as string)} + ORDER BY s."gameInternalName", s."timestamp" DESC; + `; + const safeGames= recentPlayedGames.map((game) => ({ + ...game, + timestamp: + typeof game.timestamp === "bigint" + ? Number(game.timestamp) + : game.timestamp, + })); const isAdmin = user.id === 1 || user.isAdmin; - res.json({user, isAdmin}); + const { isAdmin: _, ...safeUser } = user; + res.json({ user: safeUser, recentPlayedGames: safeGames, isAdmin }); } catch (error) { console.error('Me endpoint error:', error); res.status(500).json({ error: 'Internal server error' }); } -} +}; export const handleGetCurrentSession = async (req: express.Request, res: express.Response) => { try { diff --git a/frontend/src/pages/Profile.tsx b/frontend/src/pages/Profile.tsx index cc8fc99..bffc710 100644 --- a/frontend/src/pages/Profile.tsx +++ b/frontend/src/pages/Profile.tsx @@ -9,8 +9,15 @@ import Heatmap from "../components/Heatmap"; import type { HeatmapData } from "../components/Heatmap"; import type { User } from "../utils/authApi"; +interface recentPlayedGame { + gameInternalName: string; + formattedName: string; + timestamp: number; +} + interface UserData { user: User; + recentPlayedGames: recentPlayedGame[]; isAdmin: boolean; } @@ -91,14 +98,14 @@ const Profile = () => { navigate("/"); } - if (isLoading || fetchingHeatmapData || fetchingUserData) { - return <LoadingDisplay message="Loading Profile Page..." />; - } - if (!user) { return <SessionExpiredPopup />; } + if (isLoading || fetchingHeatmapData || fetchingUserData) { + return <LoadingDisplay message="Loading Profile Page..." />; + } + const handleLogout = async () => { try { await logout(); @@ -125,9 +132,20 @@ const Profile = () => { {user.username} </h1> <p className="text-sm sm:text-base text-slate-400"> - {userData?.user.bio || "I'm a fairly non-descript person"} + {userData?.user.bio ? userData.user.bio.replace(/</g, '<').replace(/>/g, '>') : "I'm a fairly non-descript person"} </p> </div> + <div className="mb-6 sm:mb-8"> + <h2 className="text-xl sm:text-2xl font-bold text-white mb-4">Recently Played Games</h2> + <ul className="list-disc list-inside space-y-2 text-white"> + {userData?.recentPlayedGames.map((game, index) => ( + <li key={index} className="flex items-center justify-between p-2 bg-slate-800 rounded-lg border border-slate-700"> + <span className="font-bold hover:underline"><a href={`/score?game=${game.gameInternalName}&userId=${userData.user.id}`}>{game.formattedName}</a> </span> + <span className="text-sm text-slate-400">{new Date(game.timestamp).toLocaleString()}</span> + </li> + ))} + </ul> + </div> {isBrowser ? ( <div className="flex flex-col items-center justify-center"> <Heatmap data={heatmapData.data} /> diff --git a/frontend/src/pages/Score.tsx b/frontend/src/pages/Score.tsx index cf2b09c..3701b1f 100644 --- a/frontend/src/pages/Score.tsx +++ b/frontend/src/pages/Score.tsx @@ -23,6 +23,8 @@ const Score = () => { const [formattedGameName, setFormattedGameName] = useState(""); // eslint-disable-next-line @typescript-eslint/no-explicit-any const [scores, setScores] = useState<any[]>([]); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const [username, setUsername] = useState<any>([]); const [loading, setLoading] = useState(true); const [currentPage, setCurrentPage] = useState(1); const [numPages, setNumPages] = useState(1); @@ -30,6 +32,7 @@ const Score = () => { const [sortField, setSortField] = useState<SortField>(""); const [sortDirection, setSortDirection] = useState<SortDirection>("asc"); const [requestOrder, setRequestOrder] = useState<string>("timestamp"); + const [viewingOwnScores, setViewingOwnScores] = useState(true); const gameName = new URLSearchParams(window.location.search).get("game") || "dancerush"; @@ -95,7 +98,18 @@ const Score = () => { setLoading(true); try { const url = new URL(import.meta.env.VITE_API_URL + "/scores"); - url.searchParams.append("userId", user.id.toString()); + const targetUserId = new URLSearchParams(window.location.search).get("userId"); + if (targetUserId) { + url.searchParams.append("userId", targetUserId); + } else { + url.searchParams.append("userId", user.id.toString()); + } + if(targetUserId !== user.id.toString()){ + setViewingOwnScores(false); + } + else{ + setViewingOwnScores(true); + } url.searchParams.append("internalGameName", gameName); url.searchParams.append("pageNum", pageNum.toString()); url.searchParams.append("sortKey", requestOrder); @@ -104,6 +118,7 @@ const Score = () => { const response = await fetch(url.toString(), {credentials: 'include'}); if (!response.ok) throw new Error("Failed to fetch scores"); const data = await response.json(); + setUsername(data.user); const flattened = data.scores.map(flattenScoreData); setScores(flattened); setNumPages(data.num_pages); @@ -175,7 +190,7 @@ const Score = () => { <div className="mb-6 sm:mb-12"> <div className="flex flex-col sm:flex-row items-start sm:items-center justify-between gap-4 mb-4"> <h1 className="text-2xl sm:text-3xl md:text-4xl font-bold bg-linear-to-r from-violet-400 to-violet-600 bg-clip-text text-transparent"> - Your Scores for {formattedGameName} + {viewingOwnScores ? "Your Scores" : `${username}'s Scores`} for {formattedGameName} </h1> <div className="flex items-center space-x-1 sm:space-x-2 bg-slate-900/50 backdrop-blur-sm rounded-xl p-1 border border-slate-800/50"> <button |
