From fb686b6786f209c7bf8504b8ade591aa3245e0a1 Mon Sep 17 00:00:00 2001 From: Adrian Bonpin Date: Sun, 26 Apr 2026 09:57:28 -0500 Subject: [PATCH] feat: add public and private profile pages --- app/profile/[id]/page.tsx | 99 +++++++++++++++ app/profile/[id]/profile-page-client.tsx | 50 ++++++++ app/profile/page.tsx | 150 +++++++++++++++++++++++ 3 files changed, 299 insertions(+) create mode 100644 app/profile/[id]/page.tsx create mode 100644 app/profile/[id]/profile-page-client.tsx create mode 100644 app/profile/page.tsx diff --git a/app/profile/[id]/page.tsx b/app/profile/[id]/page.tsx new file mode 100644 index 0000000..9b96c25 --- /dev/null +++ b/app/profile/[id]/page.tsx @@ -0,0 +1,99 @@ +import { notFound } from "next/navigation" +import { db } from "@/lib/db/index" +import { user, performanceEntries, games, gameVersions, hardware } from "@/lib/db/schema" +import { eq, sql, and, desc } from "drizzle-orm" +import { ProfilePageClient } from "./profile-page-client" + +export const metadata = { + title: "Profile", +} + +export default async function ProfilePage({ + params, +}: { + params: Promise<{ id: string }> +}) { + const { id } = await params + + const [profile] = await db + .select({ + id: user.id, + name: user.name, + image: user.image, + role: user.role, + createdAt: user.createdAt, + emailVerified: user.emailVerified, + }) + .from(user) + .where(eq(user.id, id)) + .limit(1) + + if (!profile) { + notFound() + } + + // Count contributions + const [{ count: contributions }] = await db + .select({ count: sql`count(*)::int` }) + .from(performanceEntries) + .where(and( + eq(performanceEntries.userId, id), + eq(performanceEntries.isRemoved, false) + )) + + // Count verified entries + const [{ count: verifiedEntries }] = await db + .select({ count: sql`count(*)::int` }) + .from(performanceEntries) + .where(and( + eq(performanceEntries.userId, id), + eq(performanceEntries.isRemoved, false), + sql`${performanceEntries.verifiedAt} IS NOT NULL` + )) + + // Fetch recent contributions (last 5) + const recentContributions = await db + .select({ + id: performanceEntries.id, + fpsAvg: performanceEntries.fpsAvg, + fpsLow: performanceEntries.fpsLow, + fpsHigh: performanceEntries.fpsHigh, + hardwareSlug: performanceEntries.hardwareSlug, + hardwareName: hardware.name, + fsrVersion: performanceEntries.fsrVersion, + frameGenMethod: performanceEntries.frameGenMethod, + verifiedAt: performanceEntries.verifiedAt, + createdAt: performanceEntries.createdAt, + gameTitle: games.title, + gameId: games.id, + gameHeaderImage: games.headerImage, + }) + .from(performanceEntries) + .innerJoin(gameVersions, eq(performanceEntries.versionId, gameVersions.id)) + .innerJoin(games, eq(gameVersions.gameId, games.id)) + .innerJoin(hardware, eq(performanceEntries.hardwareSlug, hardware.slug)) + .where(and( + eq(performanceEntries.userId, id), + eq(performanceEntries.isRemoved, false) + )) + .orderBy(desc(performanceEntries.createdAt)) + .limit(5) + + return ( + ({ + ...e, + createdAt: e.createdAt.toISOString(), + verifiedAt: e.verifiedAt?.toISOString() ?? null, + }))} + /> + ) +} diff --git a/app/profile/[id]/profile-page-client.tsx b/app/profile/[id]/profile-page-client.tsx new file mode 100644 index 0000000..22fb5bc --- /dev/null +++ b/app/profile/[id]/profile-page-client.tsx @@ -0,0 +1,50 @@ +"use client" + +import { ProfileHeader } from "@/components/profile/profile-header" +import { StatsRow } from "@/components/profile/stats-row" +import { ContributionList } from "@/components/profile/contribution-list" +import type { ContributionEntry } from "@/types/api" + +interface ProfilePageClientProps { + profile: { + id: string + name: string + image: string | null + role: string | null + createdAt: string + contributions: number + verifiedEntries: number + reputation: number + verified: boolean + } + recentContributions: ContributionEntry[] +} + +export function ProfilePageClient({ profile, recentContributions }: ProfilePageClientProps) { + return ( +
+ + + + +
+

Recent Contributions

+ +
+
+ ) +} diff --git a/app/profile/page.tsx b/app/profile/page.tsx new file mode 100644 index 0000000..acf49e6 --- /dev/null +++ b/app/profile/page.tsx @@ -0,0 +1,150 @@ +"use client" + +import { useEffect, useState } from "react" +import { useRouter } from "next/navigation" +import { useSession } from "@/lib/auth-client" +import { ProfileHeader } from "@/components/profile/profile-header" +import { StatsRow } from "@/components/profile/stats-row" +import { ContributionList } from "@/components/profile/contribution-list" +import { Bookmark, Settings, Loader2 } from "lucide-react" +import { motion } from "motion/react" +import type { ContributionEntry } from "@/types/api" + +type Tab = "overview" | "saved" | "settings" + +export default function ProfilePage() { + const router = useRouter() + const { data: session, isPending: isSessionLoading } = useSession() + const [activeTab, setActiveTab] = useState("overview") + const [profile, setProfile] = useState<{ + id: string + name: string + email: string + image: string | null + role: string | null + createdAt: string + contributions: number + verifiedEntries: number + reputation: number + verified: boolean + } | null>(null) + const [contributions, setContributions] = useState([]) + const [isLoading, setIsLoading] = useState(true) + + useEffect(() => { + if (!isSessionLoading && !session) { + router.push("/login") + } + }, [session, isSessionLoading, router]) + + useEffect(() => { + if (!session) return + + async function fetchProfile() { + try { + const [profileRes, contribRes] = await Promise.all([ + fetch("/api/user/me"), + fetch("/api/user/profile/" + session!.user.id + "/contributions?limit=10"), + ]) + + if (profileRes.ok) { + setProfile(await profileRes.json()) + } + if (contribRes.ok) { + const data = await contribRes.json() + setContributions(data.data) + } + } catch (err) { + console.error("Failed to fetch profile:", err) + } finally { + setIsLoading(false) + } + } + + fetchProfile() + }, [session]) + + if (isSessionLoading || isLoading) { + return ( +
+ +
+ ) + } + + if (!session || !profile) { + return null + } + + const tabs: { id: Tab; label: string; icon: typeof Bookmark }[] = [ + { id: "overview", label: "Overview", icon: Settings }, + { id: "saved", label: "Saved Games", icon: Bookmark }, + { id: "settings", label: "Settings", icon: Settings }, + ] + + return ( +
+ + + + + {/* Tabs */} +
+ {tabs.map((tab) => ( + + ))} +
+ + {/* Tab Content */} + + {activeTab === "overview" && ( +
+

Recent Contributions

+ +
+ )} + + {activeTab === "saved" && ( +
+ +

Saved games will appear here

+

Feature coming in the next update

+
+ )} + + {activeTab === "settings" && ( +
+ +

Account settings will appear here

+
+ )} +
+
+ ) +}