aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--backend/src/index.ts2
-rw-r--r--backend/src/routes/user.ts43
-rw-r--r--frontend/package.json1
-rw-r--r--frontend/pnpm-lock.yaml21
-rw-r--r--frontend/src/components/Heatmap.tsx36
-rw-r--r--frontend/src/pages/Profile.tsx88
6 files changed, 183 insertions, 8 deletions
diff --git a/backend/src/index.ts b/backend/src/index.ts
index ed9c45a..c0089c2 100644
--- a/backend/src/index.ts
+++ b/backend/src/index.ts
@@ -51,6 +51,8 @@ app.post('/api/logout', requireAuth, authRoutes.handleLogout);
app.get('/api/me', userRoutes.handleMeRoute);
app.get('/api/session', userRoutes.handleGetCurrentSession);
+app.get('/api/heatmap', requireAuth, userRoutes.handleGetScoresHeatmap);
+
app.get('/api/supportedGames', gameRoutes.handleGetSupportedGames);
diff --git a/backend/src/routes/user.ts b/backend/src/routes/user.ts
index 7db25db..fc03e68 100644
--- a/backend/src/routes/user.ts
+++ b/backend/src/routes/user.ts
@@ -46,3 +46,46 @@ export const handleGetCurrentSession = async (req: express.Request, res: expres
res.status(500).json({ error: 'Internal server error' });
}
}
+
+export const handleGetScoresHeatmap = async (req: express.Request, res: express.Response) => {
+ const { userId, gameInternalName } = req.query;
+ if (!userId) {
+ return res.status(400).json({ error: "Must specify userId to lookup parameters" });
+ }
+ try {
+ const user = await prisma.user.findUnique({
+ where: { id: parseInt(userId as string) },
+ select: { id: true, username: true, isAdmin: true }
+ });
+ if (!user) {
+ return res.status(404).json({ error: "User not found" });
+ }
+
+ const oneYearAndOneDay = new Date(Date.now() - 365 * 24 * 60 * 60 * 1000);
+ const unixMs = Math.floor(oneYearAndOneDay.getTime() / 1000);
+
+ const scores = await prisma.score.findMany({
+ where: {
+ userId: parseInt(userId as string),
+ timestamp: { gte: unixMs },
+ ...(gameInternalName && { gameInternalName: gameInternalName as string })
+ },
+ orderBy: { timestamp: 'desc' },
+ select: {
+ timestamp: true
+ }
+ }).then(scores => scores.map(score => ({
+ ...score,
+ timestamp: Number(score.timestamp)
+ })))
+
+ res.json({
+ "username": user.username,
+ "isAdmin": user.isAdmin,
+ scores
+ });
+ } catch (error) {
+ console.error('Session check error:', error);
+ res.status(500).json({ error: 'Internal server error' });
+ }
+}
diff --git a/frontend/package.json b/frontend/package.json
index af7efd0..ef68bfd 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -14,6 +14,7 @@
"@uiw/react-heat-map": "^2.3.3",
"crypto-js": "^4.2.0",
"react": "^19.1.1",
+ "react-device-detect": "^2.2.3",
"react-dom": "^19.1.1",
"react-router": "^7.9.1",
"tailwindcss": "^4.1.13"
diff --git a/frontend/pnpm-lock.yaml b/frontend/pnpm-lock.yaml
index 80bdfb1..ac95a81 100644
--- a/frontend/pnpm-lock.yaml
+++ b/frontend/pnpm-lock.yaml
@@ -20,6 +20,9 @@ importers:
react:
specifier: ^19.1.1
version: 19.1.1
+ react-device-detect:
+ specifier: ^2.2.3
+ version: 2.2.3(react-dom@19.1.1(react@19.1.1))(react@19.1.1)
react-dom:
specifier: ^19.1.1
version: 19.1.1(react@19.1.1)
@@ -1186,6 +1189,12 @@ packages:
queue-microtask@1.2.3:
resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==}
+ react-device-detect@2.2.3:
+ resolution: {integrity: sha512-buYY3qrCnQVlIFHrC5UcUoAj7iANs/+srdkwsnNjI7anr3Tt7UY6MqNxtMLlr0tMBied0O49UZVK8XKs3ZIiPw==}
+ peerDependencies:
+ react: '>= 0.14.0'
+ react-dom: '>= 0.14.0'
+
react-dom@19.1.1:
resolution: {integrity: sha512-Dlq/5LAZgF0Gaz6yiqZCf6VCcZs1ghAJyrsu84Q/GT0gV+mCxbfmKNoGRKBYMJ8IEdGPqu49YWXD02GCknEDkw==}
peerDependencies:
@@ -1301,6 +1310,10 @@ packages:
engines: {node: '>=14.17'}
hasBin: true
+ ua-parser-js@1.0.41:
+ resolution: {integrity: sha512-LbBDqdIC5s8iROCUjMbW1f5dJQTEFB1+KO9ogbvlb3nm9n4YHa5p4KTvFPWvh2Hs8gZMBuiB1/8+pdfe/tDPug==}
+ hasBin: true
+
update-browserslist-db@1.1.3:
resolution: {integrity: sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==}
hasBin: true
@@ -2385,6 +2398,12 @@ snapshots:
queue-microtask@1.2.3: {}
+ react-device-detect@2.2.3(react-dom@19.1.1(react@19.1.1))(react@19.1.1):
+ dependencies:
+ react: 19.1.1
+ react-dom: 19.1.1(react@19.1.1)
+ ua-parser-js: 1.0.41
+
react-dom@19.1.1(react@19.1.1):
dependencies:
react: 19.1.1
@@ -2502,6 +2521,8 @@ snapshots:
typescript@5.9.2: {}
+ ua-parser-js@1.0.41: {}
+
update-browserslist-db@1.1.3(browserslist@4.26.2):
dependencies:
browserslist: 4.26.2
diff --git a/frontend/src/components/Heatmap.tsx b/frontend/src/components/Heatmap.tsx
new file mode 100644
index 0000000..6720481
--- /dev/null
+++ b/frontend/src/components/Heatmap.tsx
@@ -0,0 +1,36 @@
+import HeatMap from '@uiw/react-heat-map';
+
+export type HeatmapObject = {
+ date: string,
+ count: number
+}
+
+export type HeatmapData = {
+ data: HeatmapObject[]
+}
+
+const Heatmap = (data : HeatmapData) => {
+ const oneYearAgo = new Date();
+ oneYearAgo.setFullYear(oneYearAgo.getFullYear() - 1);
+ console.log(data.data);
+
+ return (
+ <HeatMap
+ value={data.data}
+ width={800}
+ startDate={oneYearAgo}
+ style={{
+ color: '#ffffff'
+ }}
+ panelColors={{
+ 0: '#161b22',
+ 2: '#0e4429',
+ 4: '#006d32',
+ 10: '#26a641',
+ 20: '#39d353'
+ }}
+ />
+ )
+
+}
+export default Heatmap;
diff --git a/frontend/src/pages/Profile.tsx b/frontend/src/pages/Profile.tsx
index 184681d..4ab5995 100644
--- a/frontend/src/pages/Profile.tsx
+++ b/frontend/src/pages/Profile.tsx
@@ -1,19 +1,74 @@
-import { useNavigate } from 'react-router';
-import LoadingDisplay from "../components/LoadingDisplay";
+import { useState, useEffect } from "react";
+import { useNavigate } from "react-router";
+import { isBrowser } from "react-device-detect";
+import LoadingDisplay from "../components/LoadingDisplay";
import SessionExpiredPopup from "../components/SessionExpiredPopup";
-import { NavBar } from '../components/NavBar';
+import { NavBar } from "../components/NavBar";
import { useAuth } from "../contexts/AuthContext";
-
-
+import Heatmap from "../components/Heatmap";
+import type { HeatmapData } from "../components/Heatmap";
const Profile = () => {
const { user, isLoading, logout } = useAuth();
+ const targetUser =
+ new URLSearchParams(window.location.search).get("userId") || ""; // looking at profile of this user
const navigate = useNavigate();
+ const [fetchingHeatmapData, setFetchingHeatmapData] = useState(false);
+ const [heatmapData, setHeatmapData] = useState<HeatmapData>({ data: [] });
+
+ useEffect(() => {
+ if (targetUser) {
+ setFetchingHeatmapData(true);
+ const fetchHeatmapData = async () => {
+ try {
+ const response = await fetch(
+ new URL(
+ import.meta.env.VITE_API_URL + "/heatmap?userId=" + targetUser,
+ ),
+ { credentials: "include" },
+ );
+ const data = await response.json();
+ return data;
+ } catch (error) {
+ setFetchingHeatmapData(false);
+ console.error("Failed to fetch heatmap data:", error);
+ throw error;
+ }
+ };
+ fetchHeatmapData().then((data) => {
+ const heatmapDates: { [key: string]: number } = {};
+ for (let i = 0; i < data.scores.length; i++) {
+ const date = new Date(data.scores[i].timestamp);
+ const dateString = date.toDateString();
+ if (!heatmapDates[dateString]) {
+ heatmapDates[dateString] = 1;
+ } else {
+ heatmapDates[dateString] += 1;
+ }
+ }
+ setHeatmapData({
+ data: Object.entries(heatmapDates).map(([date, count]) => ({
+ date,
+ count,
+ })),
+ });
+ setFetchingHeatmapData(false);
+ });
+ }
+ }, [targetUser]);
+
+ if (!targetUser) {
+ navigate("/");
+ }
- if (isLoading) {
+ if (isLoading || fetchingHeatmapData) {
return <LoadingDisplay message="Loading Profile Page..." />;
}
+ if (!user) {
+ return <SessionExpiredPopup />;
+ }
+
const handleLogout = async () => {
try {
await logout();
@@ -30,8 +85,25 @@ const Profile = () => {
return (
<div className="min-h-screen bg-slate-950">
- <NavBar user={user} handleLogout={handleLogout} currentPage="import"/>
- <h1>Profile</h1>
+ <NavBar user={user} handleLogout={handleLogout} currentPage="" />
+
+ {/* Main Content */}
+ <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-4 sm:py-8">
+ {/* Header */}
+ <div className="mb-6 sm:mb-8">
+ <h1 className="text-2xl sm:text-3xl font-bold text-white mb-2">
+ {user.username}
+ </h1>
+ <p className="text-sm sm:text-base text-slate-400">
+ This is a profile page for {user.username}
+ </p>
+ </div>
+ {isBrowser ? (
+ <div className="flex flex-col items-center justify-center">
+ <Heatmap data={heatmapData.data} />
+ </div>
+ ) : null}
+ </div>
</div>
);
};
send patches to the email below
yukais@pinapelz.com
include the subject [PATCH repo_name]
pinapelz.com
homepage