diff --git a/thoughts-frontend/app/users/[username]/followers/page.tsx b/thoughts-frontend/app/users/[username]/followers/page.tsx new file mode 100644 index 0000000..4f8e41d --- /dev/null +++ b/thoughts-frontend/app/users/[username]/followers/page.tsx @@ -0,0 +1,33 @@ +import { cookies } from "next/headers"; +import { notFound } from "next/navigation"; +import { getFollowersList } from "@/lib/api"; +import { UserListCard } from "@/components/user-list-card"; + +interface FollowersPageProps { + params: { username: string }; +} + +export default async function FollowersPage({ params }: FollowersPageProps) { + const { username } = params; + const token = (await cookies()).get("auth_token")?.value ?? null; + + const followersData = await getFollowersList(username, token).catch( + () => null + ); + + if (!followersData) { + notFound(); + } + + return ( +
+
+

Followers

+

Users following @{username}.

+
+
+ +
+
+ ); +} diff --git a/thoughts-frontend/app/users/[username]/following/page.tsx b/thoughts-frontend/app/users/[username]/following/page.tsx new file mode 100644 index 0000000..a12af39 --- /dev/null +++ b/thoughts-frontend/app/users/[username]/following/page.tsx @@ -0,0 +1,33 @@ +import { cookies } from "next/headers"; +import { notFound } from "next/navigation"; +import { getFollowingList } from "@/lib/api"; +import { UserListCard } from "@/components/user-list-card"; + +interface FollowingPageProps { + params: { username: string }; +} + +export default async function FollowingPage({ params }: FollowingPageProps) { + const { username } = params; + const token = (await cookies()).get("auth_token")?.value ?? null; + + const followingData = await getFollowingList(username, token).catch( + () => null + ); + + if (!followingData) { + notFound(); + } + + return ( +
+
+

Following

+

Users that @{username} follows.

+
+
+ +
+
+ ); +} diff --git a/thoughts-frontend/app/users/[username]/page.tsx b/thoughts-frontend/app/users/[username]/page.tsx index 69a1f2b..d9fdf91 100644 --- a/thoughts-frontend/app/users/[username]/page.tsx +++ b/thoughts-frontend/app/users/[username]/page.tsx @@ -1,4 +1,11 @@ -import { getMe, getUserProfile, getUserThoughts, Me } from "@/lib/api"; +import { + getFollowersList, + getFollowingList, + getMe, + getUserProfile, + getUserThoughts, + Me, +} from "@/lib/api"; import { UserAvatar } from "@/components/user-avatar"; import { Calendar, Settings } from "lucide-react"; import { Card } from "@/components/ui/card"; @@ -22,11 +29,21 @@ export default async function ProfilePage({ params }: ProfilePageProps) { const userProfilePromise = getUserProfile(username, token); const thoughtsPromise = getUserThoughts(username, token); const mePromise = token ? getMe(token) : Promise.resolve(null); + const followersPromise = getFollowersList(username, token); + const followingPromise = getFollowingList(username, token); - const [userResult, thoughtsResult, meResult] = await Promise.allSettled([ + const [ + userResult, + thoughtsResult, + meResult, + followersResult, + followingResult, + ] = await Promise.allSettled([ userProfilePromise, thoughtsPromise, mePromise, + followersPromise, + followingPromise, ]); if (userResult.status === "rejected") { @@ -40,6 +57,15 @@ export default async function ProfilePage({ params }: ProfilePageProps) { thoughtsResult.status === "fulfilled" ? thoughtsResult.value.thoughts : []; const { topLevelThoughts, repliesByParentId } = buildThoughtThreads(thoughts); + const followersCount = + followersResult.status === "fulfilled" + ? followersResult.value.users.length + : 0; + const followingCount = + followingResult.status === "fulfilled" + ? followingResult.value.users.length + : 0; + const isOwnProfile = me?.username === user.username; const isFollowing = me?.following?.some( @@ -101,6 +127,29 @@ export default async function ProfilePage({ params }: ProfilePageProps) {

{user.bio}

+ {isOwnProfile && ( +
+ + {followingCount} + + Following + + + + {followersCount} + + Followers + + +
+ )} +
@@ -113,7 +162,6 @@ export default async function ProfilePage({ params }: ProfilePageProps) {
- {/* Main Content (Thoughts Feed) */}
{topLevelThoughts.map((thought) => ( {thought.content}

- - - + {token && ( + + + + )} {isReplyOpen && (
diff --git a/thoughts-frontend/components/user-list-card.tsx b/thoughts-frontend/components/user-list-card.tsx new file mode 100644 index 0000000..e86d2e6 --- /dev/null +++ b/thoughts-frontend/components/user-list-card.tsx @@ -0,0 +1,38 @@ +import Link from "next/link"; +import { User } from "@/lib/api"; +import { UserAvatar } from "./user-avatar"; +import { Card, CardContent } from "./ui/card"; + +interface UserListCardProps { + users: User[]; +} + +export function UserListCard({ users }: UserListCardProps) { + if (users.length === 0) { + return ( +

+ No users to display. +

+ ); + } + + return ( + + + {users.map((user) => ( + + +
+

{user.displayName || user.username}

+

@{user.username}

+
+ + ))} +
+
+ ); +} diff --git a/thoughts-frontend/lib/api.ts b/thoughts-frontend/lib/api.ts index 43bd55a..7cee691 100644 --- a/thoughts-frontend/lib/api.ts +++ b/thoughts-frontend/lib/api.ts @@ -208,4 +208,20 @@ export const getThoughtById = (thoughtId: string, token: string | null) => {}, ThoughtSchema, // Expect a single thought object token + ); + + export const getFollowingList = (username: string, token: string | null) => + apiFetch( + `/users/${username}/following`, + {}, + z.object({ users: z.array(UserSchema) }), + token + ); + +export const getFollowersList = (username: string, token: string | null) => + apiFetch( + `/users/${username}/followers`, + {}, + z.object({ users: z.array(UserSchema) }), + token ); \ No newline at end of file