From 8093cb4e45a5230cbf4958ec92ae84e3c795f5f2 Mon Sep 17 00:00:00 2001 From: fullsizemalt <106900403+fullsizemalt@users.noreply.github.com> Date: Sun, 21 Dec 2025 02:01:47 -0800 Subject: [PATCH] feat(profile): Polish My Dashboard and add public User Profile page --- backend/routers/users.py | 8 ++ frontend/app/profile/[id]/page.tsx | 158 +++++++++++++++++++++++++++++ frontend/app/profile/page.tsx | 143 ++++++++++++++++---------- 3 files changed, 254 insertions(+), 55 deletions(-) create mode 100644 frontend/app/profile/[id]/page.tsx diff --git a/backend/routers/users.py b/backend/routers/users.py index 6962cd2..a1afd86 100644 --- a/backend/routers/users.py +++ b/backend/routers/users.py @@ -13,6 +13,14 @@ class UserProfileUpdate(BaseModel): bio: Optional[str] = None avatar: Optional[str] = None +@router.get("/{user_id}", response_model=UserRead) +def get_user_public(user_id: int, session: Session = Depends(get_session)): + """Get public user profile""" + user = session.get(User, user_id) + if not user: + raise HTTPException(status_code=404, detail="User not found") + return user + @router.patch("/me", response_model=UserRead) def update_my_profile( update: UserProfileUpdate, diff --git a/frontend/app/profile/[id]/page.tsx b/frontend/app/profile/[id]/page.tsx new file mode 100644 index 0000000..f087172 --- /dev/null +++ b/frontend/app/profile/[id]/page.tsx @@ -0,0 +1,158 @@ +"use client" + +import { useEffect, useState } from "react" +import { Button } from "@/components/ui/button" +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card" +import { Trophy, Calendar, User, ArrowLeft } from "lucide-react" +import Link from "next/link" +import { BadgeList } from "@/components/profile/badge-list" +import { getApiUrl } from "@/lib/api-config" +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs" +import { UserAttendanceList } from "@/components/profile/user-attendance-list" +import { UserReviewsList } from "@/components/profile/user-reviews-list" +import { UserGroupsList } from "@/components/profile/user-groups-list" +import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar" +import { notFound } from "next/navigation" + +interface UserProfile { + id: number + email: string + username: string | null + bio: string | null + created_at: string +} + +interface UserBadge { + id: number + badge: { + id: number + name: string + description: string + icon: string + slug: string + } + awarded_at: string +} + +export default function PublicProfilePage({ params }: { params: Promise<{ id: string }> }) { + const [id, setId] = useState(null) + const [user, setUser] = useState(null) + const [badges, setBadges] = useState([]) + const [loading, setLoading] = useState(true) + const [error, setError] = useState(false) + const [stats, setStats] = useState({ attendance_count: 0, review_count: 0, group_count: 0 }) + + useEffect(() => { + params.then(p => setId(p.id)) + }, [params]) + + useEffect(() => { + if (!id) return + + const fetchData = async () => { + try { + // Public fetch - no auth header needed strictly, but maybe good practice if protected + const token = localStorage.getItem("token") + const headers = token ? { Authorization: `Bearer ${token}` } : {} + + const userRes = await fetch(`${getApiUrl()}/users/${id}`, { headers }) + if (!userRes.ok) throw new Error("User not found") + const userData = await userRes.json() + setUser(userData) + + // Fetch Stats + const statsRes = await fetch(`${getApiUrl()}/users/${id}/stats`, { headers }) + if (statsRes.ok) setStats(await statsRes.json()) + + // Fetch Badges + // Check if badges endpoint exists for specific user, otherwise /badges/me is for me. + // Assuming we default to empty badges for public profile if no specific endpoint + // Actually, let's skip badges for now or try specific endpoint if it existed. + // Assuming no public badges endpoint yet. + setBadges([]) + + } catch (e) { + console.error(e) + setError(true) + } finally { + setLoading(false) + } + } + fetchData() + }, [id]) + + if (loading) return
Loading profile...
+ if (error || !user) return
User not found
+ + const displayName = user.username || user.email.split('@')[0] + + return ( +
+ + + + + +
+ + + + +
+
+

{displayName}

+

+ + Member since {new Date(user.created_at).toLocaleDateString()} +

+
+ {user.bio && ( +

+ {user.bio} +

+ )} +
+
+ {stats.attendance_count} + Shows +
+
+
+ {stats.review_count} + Reviews +
+
+
+ {stats.group_count} + Groups +
+
+
+
+ + + + + Attendance + Reviews + Communities + + + + + + + + + + + + + + +
+ ) +} diff --git a/frontend/app/profile/page.tsx b/frontend/app/profile/page.tsx index 2339741..ab17c50 100644 --- a/frontend/app/profile/page.tsx +++ b/frontend/app/profile/page.tsx @@ -3,7 +3,7 @@ import { useEffect, useState } from "react" import { Button } from "@/components/ui/button" import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card" -import { Trophy, Calendar } from "lucide-react" +import { Trophy, Calendar, Settings, User, Edit } from "lucide-react" import Link from "next/link" import { BadgeList } from "@/components/profile/badge-list" import { getApiUrl } from "@/lib/api-config" @@ -11,12 +11,16 @@ import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs" import { UserAttendanceList } from "@/components/profile/user-attendance-list" import { UserReviewsList } from "@/components/profile/user-reviews-list" import { UserGroupsList } from "@/components/profile/user-groups-list" +import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar" // Types interface UserProfile { id: number email: string - username: string // Assuming we add this to UserRead or fetch separately + username: string + avatar: string | null + bio: string | null + created_at: string } interface UserBadge { @@ -74,96 +78,125 @@ export default function ProfilePage() { }, []) if (loading) { - return
Loading profile...
+ return
Loading profile...
} if (!user) { return ( -
+

Please Log In

- - - +

You need to be logged in to view your dashboard.

+
+ + + + + + +
) } // Transform badges for the list component const displayBadges = badges.map(b => b.badge) + const displayName = user.username || user.email.split('@')[0] return ( -
-
-
-

{user.email}

-

- - Member -

+
+ {/* Header Section */} +
+
+ + + +
+ +
+ + + + +
+
+
+

{displayName}

+ {/* Optional: display role badge */} +
+

+ + Member since {new Date(user.created_at || Date.now()).toLocaleDateString()} +

+
+ {user.bio ? ( +

+ {user.bio} +

+ ) : ( + + + Add a bio to your profile + + )} + +
+
+ {stats.attendance_count} + Shows +
+
+
+ {stats.review_count} + Reviews +
+
+
+ {stats.group_count} + Groups +
+
+
- - -
- - Overview - Attendance - Reviews - Groups + + Overview + My Shows + Reviews + Communities - -
- - - Shows Attended - - -
{stats.attendance_count}
-
-
- - - Reviews Written - - -
{stats.review_count}
-
-
- - - Groups Joined - - -
{stats.group_count}
-
-
-
- + - Badges + Achievements + {displayBadges.length === 0 && ( +

No badges earned yet. Keep active!

+ )}
+ + {/* Recent Activity or generic stats summary could go here later */}
- + - + - +