feat: add followers and following pages with API integration, enhance profile page with follower/following counts

This commit is contained in:
2025-09-06 22:22:44 +02:00
parent dc92945962
commit 8ddbf45a09
6 changed files with 183 additions and 13 deletions

View File

@@ -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 (
<div className="container mx-auto max-w-2xl p-4 sm:p-6">
<header className="my-6">
<h1 className="text-3xl font-bold">Followers</h1>
<p className="text-muted-foreground">Users following @{username}.</p>
</header>
<main>
<UserListCard users={followersData.users} />
</main>
</div>
);
}

View File

@@ -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 (
<div className="container mx-auto max-w-2xl p-4 sm:p-6">
<header className="my-6">
<h1 className="text-3xl font-bold">Following</h1>
<p className="text-muted-foreground">Users that @{username} follows.</p>
</header>
<main>
<UserListCard users={followingData.users} />
</main>
</div>
);
}

View File

@@ -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 { UserAvatar } from "@/components/user-avatar";
import { Calendar, Settings } from "lucide-react"; import { Calendar, Settings } from "lucide-react";
import { Card } from "@/components/ui/card"; import { Card } from "@/components/ui/card";
@@ -22,11 +29,21 @@ export default async function ProfilePage({ params }: ProfilePageProps) {
const userProfilePromise = getUserProfile(username, token); const userProfilePromise = getUserProfile(username, token);
const thoughtsPromise = getUserThoughts(username, token); const thoughtsPromise = getUserThoughts(username, token);
const mePromise = token ? getMe(token) : Promise.resolve(null); 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, userProfilePromise,
thoughtsPromise, thoughtsPromise,
mePromise, mePromise,
followersPromise,
followingPromise,
]); ]);
if (userResult.status === "rejected") { if (userResult.status === "rejected") {
@@ -40,6 +57,15 @@ export default async function ProfilePage({ params }: ProfilePageProps) {
thoughtsResult.status === "fulfilled" ? thoughtsResult.value.thoughts : []; thoughtsResult.status === "fulfilled" ? thoughtsResult.value.thoughts : [];
const { topLevelThoughts, repliesByParentId } = buildThoughtThreads(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 isOwnProfile = me?.username === user.username;
const isFollowing = const isFollowing =
me?.following?.some( me?.following?.some(
@@ -101,6 +127,29 @@ export default async function ProfilePage({ params }: ProfilePageProps) {
<p className="mt-4 text-sm whitespace-pre-wrap">{user.bio}</p> <p className="mt-4 text-sm whitespace-pre-wrap">{user.bio}</p>
{isOwnProfile && (
<div className="flex items-center gap-4 mt-4 text-sm">
<Link
href={`/users/${user.username}/following`}
className="hover:underline"
>
<span className="font-bold">{followingCount}</span>
<span className="text-muted-foreground ml-1">
Following
</span>
</Link>
<Link
href={`/users/${user.username}/followers`}
className="hover:underline"
>
<span className="font-bold">{followersCount}</span>
<span className="text-muted-foreground ml-1">
Followers
</span>
</Link>
</div>
)}
<div className="flex items-center gap-2 mt-4 text-sm text-muted-foreground"> <div className="flex items-center gap-2 mt-4 text-sm text-muted-foreground">
<Calendar className="h-4 w-4" /> <Calendar className="h-4 w-4" />
<span> <span>
@@ -113,7 +162,6 @@ export default async function ProfilePage({ params }: ProfilePageProps) {
</div> </div>
</aside> </aside>
{/* Main Content (Thoughts Feed) */}
<div className="col-span-1 lg:col-span-3 space-y-4"> <div className="col-span-1 lg:col-span-3 space-y-4">
{topLevelThoughts.map((thought) => ( {topLevelThoughts.map((thought) => (
<ThoughtThread <ThoughtThread

View File

@@ -135,6 +135,7 @@ export function ThoughtCard({
<p className="whitespace-pre-wrap break-words">{thought.content}</p> <p className="whitespace-pre-wrap break-words">{thought.content}</p>
</CardContent> </CardContent>
{token && (
<CardFooter className="border-t px-4 pt-2 pb-2"> <CardFooter className="border-t px-4 pt-2 pb-2">
<Button <Button
variant="ghost" variant="ghost"
@@ -145,6 +146,7 @@ export function ThoughtCard({
Reply Reply
</Button> </Button>
</CardFooter> </CardFooter>
)}
{isReplyOpen && ( {isReplyOpen && (
<div className="border-t p-4"> <div className="border-t p-4">

View File

@@ -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 (
<p className="text-center text-muted-foreground pt-8">
No users to display.
</p>
);
}
return (
<Card>
<CardContent className="divide-y">
{users.map((user) => (
<Link
href={`/users/${user.username}`}
key={user.id}
className="flex items-center gap-4 p-4 -mx-6 hover:bg-accent"
>
<UserAvatar src={user.avatarUrl} alt={user.displayName} />
<div>
<p className="font-bold">{user.displayName || user.username}</p>
<p className="text-sm text-muted-foreground">@{user.username}</p>
</div>
</Link>
))}
</CardContent>
</Card>
);
}

View File

@@ -209,3 +209,19 @@ export const getThoughtById = (thoughtId: string, token: string | null) =>
ThoughtSchema, // Expect a single thought object ThoughtSchema, // Expect a single thought object
token 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
);