User Profile Pages
Every username in Bulletin links to a profile page showing the user’s post history. The profile page reuses the same fetch-on-mount pattern from the post detail page, applied to the GET /users/:id endpoint.
The getUser API call
Section titled “The getUser API call”import apiClient from './apiClient'import { Post } from './postsApi'
export interface UserProfile { id: number username: string createdAt: string posts: Post[]}
export async function getUser(id: number): Promise<UserProfile> { const res = await apiClient.get<UserProfile>(`/users/${id}`) return res.data}The Express endpoint returns the user’s public fields plus their post history — no password, no email.
The UserProfile page
Section titled “The UserProfile page”import { useEffect, useState } from 'react'import { useParams, Link } from 'react-router-dom'import { getUser, UserProfile as UserProfileType } from '../api/usersApi'
export default function UserProfile() { const { id } = useParams<{ id: string }>() const [profile, setProfile] = useState<UserProfileType | null>(null) const [loading, setLoading] = useState(true) const [error, setError] = useState<string | null>(null)
useEffect(() => { if (!id) return getUser(Number(id)) .then(setProfile) .catch(() => setError('User not found.')) .finally(() => setLoading(false)) }, [id])
if (loading) return <p>Loading...</p> if (error || !profile) return <p>{error ?? 'User not found.'}</p>
return ( <main> <h1>{profile.username}</h1> <p>Member since {new Date(profile.createdAt).toLocaleDateString()}</p>
<section> <h2>Posts ({profile.posts.length})</h2> {profile.posts.length === 0 && <p>No posts yet.</p>} <ul> {profile.posts.map(post => ( <li key={post.id}> <Link to={`/posts/${post.id}`}>{post.title}</Link> <span> · {post.upvotes} votes · {post.commentCount} comments</span> </li> ))} </ul> </section> </main> )}Linking usernames to profiles
Section titled “Linking usernames to profiles”Anywhere a username appears — the post feed, post detail, comments — link it to the user’s profile:
// In PostFeed, PostDetail, CommentForm — replace plain text with:<Link to={`/users/${post.userId}`}>{post.username}</Link>For comments:
<Link to={`/users/${comment.userId}`}>{comment.username}</Link>Register the route
Section titled “Register the route”// src/App.tsx (routes excerpt)<Route path="/users/:id" element={<UserProfile />} />This route is public — anyone can view profiles.
Formatting dates
Section titled “Formatting dates”new Date(profile.createdAt).toLocaleDateString()// → "6/3/2026" (format varies by browser locale)toLocaleDateString() produces a readable date in the user’s locale. For consistent formatting across locales, pass options:
new Date(profile.createdAt).toLocaleDateString('en-US', { year: 'numeric', month: 'long', day: 'numeric',})// → "June 3, 2026"Exercise
Section titled “Exercise”Open src/pages/UserProfile.tsx in your Bulletin project.
- Create
src/api/usersApi.tswithgetUserandUserProfileinterface. - Create
UserProfile.tsxusing the three-state fetch pattern with[id]dependency. - Register the
/users/:idroute inApp.tsx. - Update
PostFeed.tsxto link each post’susernameto/users/${post.userId}. - Update
PostDetail.tsxto link the author’s username and each comment’susernameto their profile pages. - Click a username in the feed and confirm the profile page loads with their post history.
getUserfetchesGET /users/:idand returns public user info plus post history.- The same
useParams+useEffect([id])pattern asPostDetail— these pages are structurally identical. - Link every username across the app to
/users/:idfor a connected experience. toLocaleDateString()formats ISO timestamps into readable dates.