feat: add followers and following pages with API integration, enhance profile page with follower/following counts
This commit is contained in:
33
thoughts-frontend/app/users/[username]/followers/page.tsx
Normal file
33
thoughts-frontend/app/users/[username]/followers/page.tsx
Normal 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>
|
||||
);
|
||||
}
|
33
thoughts-frontend/app/users/[username]/following/page.tsx
Normal file
33
thoughts-frontend/app/users/[username]/following/page.tsx
Normal 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>
|
||||
);
|
||||
}
|
@@ -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) {
|
||||
|
||||
<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">
|
||||
<Calendar className="h-4 w-4" />
|
||||
<span>
|
||||
@@ -113,7 +162,6 @@ export default async function ProfilePage({ params }: ProfilePageProps) {
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
{/* Main Content (Thoughts Feed) */}
|
||||
<div className="col-span-1 lg:col-span-3 space-y-4">
|
||||
{topLevelThoughts.map((thought) => (
|
||||
<ThoughtThread
|
||||
|
@@ -135,16 +135,18 @@ export function ThoughtCard({
|
||||
<p className="whitespace-pre-wrap break-words">{thought.content}</p>
|
||||
</CardContent>
|
||||
|
||||
<CardFooter className="border-t px-4 pt-2 pb-2">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => setIsReplyOpen(!isReplyOpen)}
|
||||
>
|
||||
<MessageSquare className="mr-2 h-4 w-4" />
|
||||
Reply
|
||||
</Button>
|
||||
</CardFooter>
|
||||
{token && (
|
||||
<CardFooter className="border-t px-4 pt-2 pb-2">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => setIsReplyOpen(!isReplyOpen)}
|
||||
>
|
||||
<MessageSquare className="mr-2 h-4 w-4" />
|
||||
Reply
|
||||
</Button>
|
||||
</CardFooter>
|
||||
)}
|
||||
|
||||
{isReplyOpen && (
|
||||
<div className="border-t p-4">
|
||||
|
38
thoughts-frontend/components/user-list-card.tsx
Normal file
38
thoughts-frontend/components/user-list-card.tsx
Normal 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>
|
||||
);
|
||||
}
|
@@ -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
|
||||
);
|
Reference in New Issue
Block a user