'use client'; import { useState, useEffect, useRef, useCallback } from 'react'; import Link from 'next/link'; import { useParams, useRouter } from 'next/navigation'; import { ArrowLeftIcon, CalendarIcon } from '@/components/Icons'; import { PostCard } from '@/components/PostCard'; import { User, Post } from '@/lib/types'; import AutoTextarea from '@/components/AutoTextarea'; import { Rocket, MoreHorizontal } from 'lucide-react'; import { formatFullHandle } from '@/lib/utils/handle'; import { Bot } from 'lucide-react'; interface BotOwner { id: string; handle: string; displayName?: string | null; avatarUrl?: string | null; } interface UserSummary { id: string; handle: string; displayName?: string | null; bio?: string | null; avatarUrl?: string | null; isBot?: boolean; } // Strip HTML tags from a string const stripHtml = (html: string | null | undefined): string | null => { if (!html) return null; return html.replace(/<[^>]*>/g, '').trim() || null; }; function UserRow({ user }: { user: UserSummary }) { return (
{user.avatarUrl ? ( {user.displayName ) : ( (user.displayName || user.handle).charAt(0).toUpperCase() )}
{user.displayName || user.handle} {user.isBot && ( AI Account )}
{formatFullHandle(user.handle)}
{user.bio && stripHtml(user.bio) && (
{stripHtml(user.bio)}
)}
); } export default function ProfilePage() { const params = useParams(); const router = useRouter(); const handle = (params.handle as string)?.replace(/^@/, '') || ''; const [user, setUser] = useState(null); const [posts, setPosts] = useState([]); const [likedPosts, setLikedPosts] = useState([]); const [currentUser, setCurrentUser] = useState<{ id: string; handle: string } | null>(null); const [isFollowing, setIsFollowing] = useState(false); const [loading, setLoading] = useState(true); const [activeTab, setActiveTab] = useState<'posts' | 'replies' | 'likes' | 'followers' | 'following'>('posts'); const [followers, setFollowers] = useState([]); const [following, setFollowing] = useState([]); const [repliesPosts, setRepliesPosts] = useState([]); const [postsLoading, setPostsLoading] = useState(true); const [likesLoading, setLikesLoading] = useState(false); const [repliesLoading, setRepliesLoading] = useState(false); const [followersLoading, setFollowersLoading] = useState(false); const [followingLoading, setFollowingLoading] = useState(false); const [postsLoadingMore, setPostsLoadingMore] = useState(false); const [repliesLoadingMore, setRepliesLoadingMore] = useState(false); const [postsCursor, setPostsCursor] = useState(null); const [repliesCursor, setRepliesCursor] = useState(null); const [isEditing, setIsEditing] = useState(false); const [profileForm, setProfileForm] = useState({ displayName: '', bio: '', avatarUrl: '', headerUrl: '', website: '', }); const [saveError, setSaveError] = useState(null); const [isSaving, setIsSaving] = useState(false); const [isBlocked, setIsBlocked] = useState(false); const [showMenu, setShowMenu] = useState(false); useEffect(() => { setIsEditing(false); setSaveError(null); setFollowers([]); setFollowing([]); setLikedPosts([]); setRepliesPosts([]); // Get current user fetch('/api/auth/me') .then(res => res.json()) .then(data => setCurrentUser(data.user)) .catch(() => { }); // Get profile fetch(`/api/users/${handle}`) .then(res => res.json()) .then(data => { setUser(data.user); setLoading(false); }) .catch(() => setLoading(false)); setPostsLoading(true); setPostsCursor(null); setRepliesCursor(null); fetch(`/api/users/${handle}/posts`) .then(res => res.json()) .then(data => { setPosts(data.posts || []); setPostsCursor(data.nextCursor || null); }) .catch(() => { }) .finally(() => setPostsLoading(false)); }, [handle]); // Infinite scroll ref const loadMoreRef = useRef(null); // Load more posts const loadMorePosts = useCallback(async () => { if (!postsCursor || postsLoadingMore) return; setPostsLoadingMore(true); try { const res = await fetch(`/api/users/${handle}/posts?cursor=${postsCursor}`); const data = await res.json(); setPosts(prev => [...prev, ...(data.posts || [])]); setPostsCursor(data.nextCursor || null); } catch { // ignore } finally { setPostsLoadingMore(false); } }, [handle, postsCursor, postsLoadingMore]); // Load more replies const loadMoreReplies = useCallback(async () => { if (!repliesCursor || repliesLoadingMore || !user) return; setRepliesLoadingMore(true); try { const res = await fetch(`/api/posts?type=replies&userId=${user.id}&cursor=${repliesCursor}`); const data = await res.json(); setRepliesPosts(prev => [...prev, ...(data.posts || [])]); setRepliesCursor(data.nextCursor || null); } catch { // ignore } finally { setRepliesLoadingMore(false); } }, [user, repliesCursor, repliesLoadingMore]); // Infinite scroll observer useEffect(() => { if (!loadMoreRef.current) return; const observer = new IntersectionObserver( (entries) => { if (entries[0].isIntersecting) { if (activeTab === 'posts' && postsCursor && !postsLoadingMore) { loadMorePosts(); } else if (activeTab === 'replies' && repliesCursor && !repliesLoadingMore) { loadMoreReplies(); } } }, { threshold: 0.1 } ); observer.observe(loadMoreRef.current); return () => observer.disconnect(); }, [activeTab, postsCursor, repliesCursor, postsLoadingMore, repliesLoadingMore, loadMorePosts, loadMoreReplies]); const handleLike = async (postId: string, currentLiked: boolean) => { const method = currentLiked ? 'DELETE' : 'POST'; await fetch(`/api/posts/${postId}/like`, { method }); }; const handleRepost = async (postId: string, currentReposted: boolean) => { const method = currentReposted ? 'DELETE' : 'POST'; await fetch(`/api/posts/${postId}/repost`, { method }); }; const handleComment = (post: Post) => { // Navigation is handled by the PostCard overlay, // but we can also use router.push if they explicitly click the comment button. router.push(`/u/${post.author.handle}/posts/${post.id}`); }; const handleDelete = (postId: string) => { setPosts(prev => prev.filter(p => p.id !== postId)); if (user && isOwnProfile) { setUser({ ...user, postsCount: (user.postsCount || 0) - 1 }); } }; useEffect(() => { if (user && currentUser?.handle === user.handle) { setProfileForm({ displayName: user.displayName || '', bio: user.bio || '', avatarUrl: user.avatarUrl || '', headerUrl: user.headerUrl || '', website: user.website || '', }); } }, [user, currentUser]); useEffect(() => { if (!currentUser || !user || currentUser.handle === user.handle) { setIsFollowing(false); setIsBlocked(false); return; } fetch(`/api/users/${handle}/follow`) .then(res => res.json()) .then(data => setIsFollowing(!!data.following)) .catch(() => setIsFollowing(false)); fetch(`/api/users/${handle}/block`) .then(res => res.json()) .then(data => setIsBlocked(!!data.blocked)) .catch(() => setIsBlocked(false)); }, [currentUser, user, handle]); useEffect(() => { if (activeTab === 'followers') { setFollowersLoading(true); fetch(`/api/users/${handle}/followers`) .then(res => res.json()) .then(data => setFollowers(data.followers || [])) .catch(() => setFollowers([])) .finally(() => setFollowersLoading(false)); } if (activeTab === 'following') { setFollowingLoading(true); fetch(`/api/users/${handle}/following`) .then(res => res.json()) .then(data => setFollowing(data.following || [])) .catch(() => setFollowing([])) .finally(() => setFollowingLoading(false)); } if (activeTab === 'likes') { setLikesLoading(true); fetch(`/api/users/${handle}/likes`) .then(res => res.json()) .then(data => setLikedPosts(data.posts || [])) .catch(() => setLikedPosts([])) .finally(() => setLikesLoading(false)); } if (activeTab === 'replies' && user) { setRepliesLoading(true); setRepliesCursor(null); fetch(`/api/posts?type=replies&userId=${user.id}`) .then(res => res.json()) .then(data => { setRepliesPosts(data.posts || []); setRepliesCursor(data.nextCursor || null); }) .catch(() => setRepliesPosts([])) .finally(() => setRepliesLoading(false)); } }, [activeTab, handle, user]); const handleFollow = async () => { if (!currentUser) return; const method = isFollowing ? 'DELETE' : 'POST'; const res = await fetch(`/api/users/${handle}/follow`, { method }); if (res.ok && user) { setIsFollowing(!isFollowing); setUser({ ...user, followersCount: isFollowing ? (user.followersCount || 0) - 1 : (user.followersCount || 0) + 1, }); } }; const handleBlock = async () => { if (!currentUser) return; const method = isBlocked ? 'DELETE' : 'POST'; const res = await fetch(`/api/users/${handle}/block`, { method }); if (res.ok) { setIsBlocked(!isBlocked); if (!isBlocked) { // If blocking, also unfollow setIsFollowing(false); } setShowMenu(false); } }; const handleSaveProfile = async () => { if (!isOwnProfile) return; setIsSaving(true); setSaveError(null); try { const res = await fetch('/api/auth/me', { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(profileForm), }); const data = await res.json(); if (!res.ok) { throw new Error(data.error || 'Failed to update profile'); } setUser(data.user); setIsEditing(false); } catch (error) { console.error('Profile update failed', error); setSaveError('Unable to update profile. Please try again.'); } finally { setIsSaving(false); } }; const formatDate = (dateStr: string) => { return new Date(dateStr).toLocaleDateString('en-US', { month: 'long', year: 'numeric', }); }; if (loading) { return (
Loading...
); } if (!user) { return (

User not found

Go home
); } const isOwnProfile = currentUser?.handle === user.handle; return (
{/* Header */}

{user.displayName || user.handle}

{user.postsCount} posts

{/* Account Moved Banner */} {user.movedTo && (
This account has moved
)} {/* Profile Header */}
{/* Banner */}
{/* Avatar & Actions */}
{user.avatarUrl ? ( {user.displayName ) : ( (user.displayName || user.handle).charAt(0).toUpperCase() )}
{!isOwnProfile && currentUser && ( <> {!isBlocked && ( )}
{showMenu && ( <>
setShowMenu(false)} />
)}
)} {isOwnProfile && ( )}
{/* User Info */}

{user.displayName || user.handle}

{formatFullHandle(user.handle)}

{user.bio && (

{user.bio}

)}
Joined {formatDate(user.createdAt || new Date().toISOString())}
{user.website && (
{user.website.replace(/^https?:\/\/(www\.)?/, '').replace(/\/$/, '')}
)} {/* Bot indicator and owner info */} {user.isBot && (
Automated account {(user as any).botOwner && ( <> {' ยท Managed by '} @{(user as any).botOwner.handle} )}
)}
{isOwnProfile && isEditing && (
Edit profile
setProfileForm({ ...profileForm, displayName: e.target.value })} maxLength={50} />
setProfileForm({ ...profileForm, bio: e.target.value })} maxLength={160} style={{ minHeight: '80px', resize: 'vertical' }} />
setProfileForm({ ...profileForm, website: e.target.value })} maxLength={100} />
{profileForm.avatarUrl && (
Preview
)}
{profileForm.headerUrl && (
Preview
)}
{saveError && (
{saveError}
)}
)} {/* Tabs */}
{(user?.isBot ? ['posts', 'replies', 'followers', 'following'] as const : ['posts', 'replies', 'likes', 'followers', 'following'] as const ).map(tab => ( ))}
{/* Content */} {activeTab === 'posts' && ( postsLoading ? (

Loading...

) : posts.length === 0 ? (

No posts yet

) : ( <> {posts.map((post, index) => ( ))}
{postsLoadingMore && ( Loading more... )}
) )} {activeTab === 'replies' && ( repliesLoading ? (

Loading...

) : repliesPosts.length === 0 ? (

No replies yet

) : ( <> {repliesPosts.map((post, index) => ( ))}
{repliesLoadingMore && ( Loading more... )}
) )} {activeTab === 'likes' && ( likesLoading ? (

Loading...

) : likedPosts.length === 0 ? (

No liked posts yet

) : ( likedPosts.map((post, index) => ( )) ) )} {activeTab === 'followers' && ( followersLoading ? (
Loading followers...
) : followers.length === 0 ? (

No followers yet

) : (
{followers.map(follower => ( ))}
) )} {activeTab === 'following' && ( followingLoading ? (
Loading following...
) : following.length === 0 ? (

Not following anyone yet

) : (
{following.map(userItem => ( ))}
) )}
); }