diff --git a/thoughts-frontend/components/remote-user-card.tsx b/thoughts-frontend/components/remote-user-card.tsx index ec0b1de..df7cc93 100644 --- a/thoughts-frontend/components/remote-user-card.tsx +++ b/thoughts-frontend/components/remote-user-card.tsx @@ -3,14 +3,19 @@ import { useState } from "react"; import { useAuth } from "@/hooks/use-auth"; import Link from "next/link"; -import { followUser, RemoteActor } from "@/lib/api"; +import { followUser } from "@/lib/api"; import { Button } from "@/components/ui/button"; import { UserAvatar } from "@/components/user-avatar"; import { toast } from "sonner"; import { UserPlus } from "lucide-react"; interface RemoteUserCardProps { - actor: RemoteActor; + actor: { + handle: string; + displayName: string | null; + avatarUrl: string | null; + url: string; + }; } export function RemoteUserCard({ actor }: RemoteUserCardProps) { diff --git a/thoughts-frontend/components/remote-user-profile.tsx b/thoughts-frontend/components/remote-user-profile.tsx index b46f998..1a48e7b 100644 --- a/thoughts-frontend/components/remote-user-profile.tsx +++ b/thoughts-frontend/components/remote-user-profile.tsx @@ -7,7 +7,9 @@ import { ThoughtList } from "@/components/thought-list"; import { Card } from "@/components/ui/card"; import { Button } from "@/components/ui/button"; import { ExternalLink, UserPlus, UserMinus } from "lucide-react"; -import { followUser, unfollowUser, RemoteActor, Thought, Me } from "@/lib/api"; +import { followUser, unfollowUser, RemoteActor, Thought, Me, getActorFollowers, getActorFollowing, ActorConnection } from "@/lib/api"; +import { RemoteUserCard } from "@/components/remote-user-card"; +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { toast } from "sonner"; import { useAuth } from "@/hooks/use-auth"; @@ -26,6 +28,17 @@ export function RemoteUserProfile({ const [loading, setLoading] = useState(false); const { token } = useAuth(); + type ConnectionTab = "posts" | "followers" | "following"; + const [activeTab, setActiveTab] = useState("posts"); + const [followers, setFollowers] = useState([]); + const [following, setFollowing] = useState([]); + const [followersPage, setFollowersPage] = useState(1); + const [followingPage, setFollowingPage] = useState(1); + const [followersHasMore, setFollowersHasMore] = useState(false); + const [followingHasMore, setFollowingHasMore] = useState(false); + const [followersLoaded, setFollowersLoaded] = useState(false); + const [followingLoaded, setFollowingLoaded] = useState(false); + const handleFollow = async () => { if (!token) { toast.error("You must be logged in to follow users."); @@ -50,6 +63,30 @@ export function RemoteUserProfile({ } }; + const loadFollowers = async (page: number) => { + const result = await getActorFollowers(actor.handle, page, token).catch(() => null); + if (!result) return; + setFollowers((prev) => (page === 1 ? result.items : [...prev, ...result.items])); + setFollowersHasMore(result.hasMore); + setFollowersLoaded(true); + setFollowersPage(page); + }; + + const loadFollowing = async (page: number) => { + const result = await getActorFollowing(actor.handle, page, token).catch(() => null); + if (!result) return; + setFollowing((prev) => (page === 1 ? result.items : [...prev, ...result.items])); + setFollowingHasMore(result.hasMore); + setFollowingLoaded(true); + setFollowingPage(page); + }; + + const handleTabChange = (tab: string) => { + setActiveTab(tab as ConnectionTab); + if (tab === "followers" && !followersLoaded) loadFollowers(1); + if (tab === "following" && !followingLoaded) loadFollowing(1); + }; + const isOwnProfile = me?.username === actor.handle; const authorDetails = new Map(); @@ -133,31 +170,6 @@ export function RemoteUserProfile({ - {(actor.followersUrl || actor.followingUrl) && ( -
- {actor.followersUrl && ( - - Followers - - )} - {actor.followingUrl && ( - - Following - - )} -
- )} - {actor.alsoKnownAs && (

Also known as:{" "} @@ -194,20 +206,86 @@ export function RemoteUserProfile({ -

- {initialPosts.length > 0 ? ( - - ) : ( - -

- Posts are being fetched — check back soon. -

-
- )} +
+ + + Posts + Followers + Following + + + + {initialPosts.length > 0 ? ( + + ) : ( + +

+ Posts are being fetched — check back soon. +

+
+ )} +
+ + + {!followersLoaded ? ( + +

Loading followers…

+
+ ) : followers.length === 0 ? ( + +

+ No followers cached yet — check back soon. +

+
+ ) : ( +
+ {followers.map((f) => ( + + ))} + {followersHasMore && ( + + )} +
+ )} +
+ + + {!followingLoaded ? ( + +

Loading following…

+
+ ) : following.length === 0 ? ( + +

+ No following cached yet — check back soon. +

+
+ ) : ( +
+ {following.map((f) => ( + + ))} + {followingHasMore && ( + + )} +
+ )} +
+
diff --git a/thoughts-frontend/lib/api.ts b/thoughts-frontend/lib/api.ts index b2d471a..c211dfd 100644 --- a/thoughts-frontend/lib/api.ts +++ b/thoughts-frontend/lib/api.ts @@ -270,6 +270,44 @@ export const getRemoteActorPosts = ( token ); +export const ActorConnectionSchema = z.object({ + handle: z.string(), + displayName: z.string().nullable(), + avatarUrl: z.string().nullable(), + url: z.string(), +}); +export type ActorConnection = z.infer; + +const ActorConnectionPageSchema = z.object({ + items: z.array(ActorConnectionSchema), + page: z.number(), + hasMore: z.boolean(), +}); + +export const getActorFollowers = ( + handle: string, + page: number, + token: string | null +) => + apiFetch( + `/federation/actors/${encodeURIComponent(handle)}/followers-list?page=${page}`, + {}, + ActorConnectionPageSchema, + token + ); + +export const getActorFollowing = ( + handle: string, + page: number, + token: string | null +) => + apiFetch( + `/federation/actors/${encodeURIComponent(handle)}/following-list?page=${page}`, + {}, + ActorConnectionPageSchema, + token + ); + export const getAllUsers = (page: number = 1, pageSize: number = 20) => apiFetch( `/users?page=${page}&per_page=${pageSize}`,