Skip to content

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.

src/api/usersApi.ts
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.

src/pages/UserProfile.tsx
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>
)
}

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>
// src/App.tsx (routes excerpt)
<Route path="/users/:id" element={<UserProfile />} />

This route is public — anyone can view profiles.

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"

Open src/pages/UserProfile.tsx in your Bulletin project.

  1. Create src/api/usersApi.ts with getUser and UserProfile interface.
  2. Create UserProfile.tsx using the three-state fetch pattern with [id] dependency.
  3. Register the /users/:id route in App.tsx.
  4. Update PostFeed.tsx to link each post’s username to /users/${post.userId}.
  5. Update PostDetail.tsx to link the author’s username and each comment’s username to their profile pages.
  6. Click a username in the feed and confirm the profile page loads with their post history.
  • getUser fetches GET /users/:id and returns public user info plus post history.
  • The same useParams + useEffect([id]) pattern as PostDetail — these pages are structurally identical.
  • Link every username across the app to /users/:id for a connected experience.
  • toLocaleDateString() formats ISO timestamps into readable dates.