diff --git a/thoughts-frontend/components/remote-user-profile.tsx b/thoughts-frontend/components/remote-user-profile.tsx deleted file mode 100644 index 10d609d..0000000 --- a/thoughts-frontend/components/remote-user-profile.tsx +++ /dev/null @@ -1,289 +0,0 @@ -"use client"; - -import { useState } from "react"; -import Link from "next/link"; -import { UserAvatar } from "@/components/user-avatar"; -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, 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"; - -interface RemoteUserProfileProps { - actor: RemoteActor; - initialPosts: Thought[]; - me: Me | null; - initialFollowed?: boolean; -} - -export function RemoteUserProfile({ - actor, - initialPosts, - me, - initialFollowed = false, -}: RemoteUserProfileProps) { - const [followed, setFollowed] = useState(initialFollowed); - 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."); - return; - } - setLoading(true); - try { - if (followed) { - await unfollowUser(actor.handle, token); - setFollowed(false); - } else { - await followUser(actor.handle, token); - setFollowed(true); - toast.success(`Follow request sent to ${actor.handle}`); - } - } catch { - toast.error( - followed ? "Failed to unfollow." : "Failed to send follow request.", - ); - } finally { - setLoading(false); - } - }; - - 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; - - return ( -
-
- -
- - -
- - - 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/components/remote-user-profile/connections.tsx b/thoughts-frontend/components/remote-user-profile/connections.tsx new file mode 100644 index 0000000..b9aa806 --- /dev/null +++ b/thoughts-frontend/components/remote-user-profile/connections.tsx @@ -0,0 +1,75 @@ +"use client"; + +import { useState, useEffect } from "react"; +import { ActorConnection, getActorFollowers, getActorFollowing } from "@/lib/api"; +import { Card } from "@/components/ui/card"; +import { RemoteUserCard } from "@/components/remote-user-card"; + +interface ConnectionsProps { + handle: string; + token: string | null; + type: "followers" | "following"; + /** Parent sets this to true when the tab becomes active for the first time. */ + active: boolean; +} + +export function Connections({ handle, token, type, active }: ConnectionsProps) { + const [items, setItems] = useState([]); + const [page, setPage] = useState(1); + const [hasMore, setHasMore] = useState(false); + const [loaded, setLoaded] = useState(false); + + const load = async (p: number) => { + const fetchFn = type === "followers" ? getActorFollowers : getActorFollowing; + const result = await fetchFn(handle, p, token).catch(() => null); + if (!result) return; + setItems((prev) => (p === 1 ? result.items : [...prev, ...result.items])); + setHasMore(result.hasMore); + setLoaded(true); + setPage(p); + }; + + useEffect(() => { + if (active && !loaded) { + load(1); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [active]); + + const emptyMessage = + type === "followers" + ? "No followers cached yet — check back soon." + : "No following cached yet — check back soon."; + + if (!loaded) { + return ( + +

Loading {type}…

+
+ ); + } + + if (items.length === 0) { + return ( + +

{emptyMessage}

+
+ ); + } + + return ( +
+ {items.map((f) => ( + + ))} + {hasMore && ( + + )} +
+ ); +} diff --git a/thoughts-frontend/components/remote-user-profile/index.tsx b/thoughts-frontend/components/remote-user-profile/index.tsx new file mode 100644 index 0000000..2a98a2f --- /dev/null +++ b/thoughts-frontend/components/remote-user-profile/index.tsx @@ -0,0 +1,147 @@ +"use client"; + +import { useState } from "react"; +import { UserMinus, UserPlus } from "lucide-react"; +import { followUser, unfollowUser, RemoteActor, Thought, Me } from "@/lib/api"; +import { Card } from "@/components/ui/card"; +import { Button } from "@/components/ui/button"; +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; +import { ThoughtList } from "@/components/thought-list"; +import { toast } from "sonner"; +import { useAuth } from "@/hooks/use-auth"; +import { ProfileCard } from "./profile-card"; +import { Connections } from "./connections"; + +interface RemoteUserProfileProps { + actor: RemoteActor; + initialPosts: Thought[]; + me: Me | null; + initialFollowed?: boolean; +} + +export function RemoteUserProfile({ + actor, + initialPosts, + me, + initialFollowed = false, +}: RemoteUserProfileProps) { + const [followed, setFollowed] = useState(initialFollowed); + const [followLoading, setFollowLoading] = useState(false); + const { token } = useAuth(); + + type ConnectionTab = "posts" | "followers" | "following"; + const [activeTab, setActiveTab] = useState("posts"); + const [followersActive, setFollowersActive] = useState(false); + const [followingActive, setFollowingActive] = useState(false); + + const handleFollow = async () => { + if (!token) { + toast.error("You must be logged in to follow users."); + return; + } + setFollowLoading(true); + try { + if (followed) { + await unfollowUser(actor.handle, token); + setFollowed(false); + } else { + await followUser(actor.handle, token); + setFollowed(true); + toast.success(`Follow request sent to ${actor.handle}`); + } + } catch { + toast.error(followed ? "Failed to unfollow." : "Failed to send follow request."); + } finally { + setFollowLoading(false); + } + }; + + const handleTabChange = (tab: string) => { + setActiveTab(tab as ConnectionTab); + if (tab === "followers") setFollowersActive(true); + if (tab === "following") setFollowingActive(true); + }; + + const isOwnProfile = me?.username === actor.handle; + + const followButton = + !isOwnProfile && token ? ( + + ) : undefined; + + return ( +
+
+ +
+ + +
+ + + Posts + Followers + Following + + + + {initialPosts.length > 0 ? ( + + ) : ( + +

+ Posts are being fetched — check back soon. +

+
+ )} +
+ + + + + + + + +
+
+
+
+ ); +} diff --git a/thoughts-frontend/components/remote-user-profile/profile-card.tsx b/thoughts-frontend/components/remote-user-profile/profile-card.tsx new file mode 100644 index 0000000..8c83d45 --- /dev/null +++ b/thoughts-frontend/components/remote-user-profile/profile-card.tsx @@ -0,0 +1,91 @@ +import Link from "next/link"; +import { ExternalLink } from "lucide-react"; +import { ReactNode } from "react"; +import { RemoteActor } from "@/lib/api"; +import { UserAvatar } from "@/components/user-avatar"; +import { Button } from "@/components/ui/button"; + +interface ProfileCardProps { + actor: RemoteActor; + /** Slot rendered next to the avatar (e.g. follow/unfollow button). */ + action?: ReactNode; +} + +export function ProfileCard({ actor, action }: ProfileCardProps) { + return ( + <> +
+
+ +
+ {action} +
+ +
+

+ {actor.displayName ?? actor.handle} +

+

{actor.handle}

+
+ + {actor.bio && ( +
+ )} + + + + {actor.alsoKnownAs && ( +

+ Also known as:{" "} + + {actor.alsoKnownAs} + +

+ )} + + {actor.attachment.length > 0 && ( +
+ {actor.attachment.map((field) => ( +
+ + {field.name} + + +
+ ))} +
+ )} + + ); +}