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 { 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
|
||||||
|
@@ -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">
|
||||||
|
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>
|
||||||
|
);
|
||||||
|
}
|
@@ -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
|
||||||
|
);
|
Reference in New Issue
Block a user