mirror of
https://gitea.tendokyu.moe/pinapelz/Mirage.git
synced 2026-10-09 15:10:09 +03:00
add recently played games to profile page
This commit is contained in:
@@ -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);
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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} />
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user