Files
Synapsis/src/app/u/[handle]/page.tsx
T
Christopher a68aee8758 Migrate user/profile URLs to /u/[handle] and remove legacy scripts
All user and post profile URLs have been updated from /[handle] to /u/[handle] for consistency. Added a redirect page for /posts/[id] to the canonical /u/[handle]/posts/[id] route. Deprecated and removed legacy scripts and documentation related to chat and federation endpoints. Push-based federation endpoints are now disabled in favor of real-time pull-based federation.
2026-01-27 01:26:41 -08:00

948 lines
43 KiB
TypeScript

'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 (
<Link href={`/u/${user.handle}`} className="user-row">
<div className="avatar">
{user.avatarUrl ? (
<img src={user.avatarUrl} alt={user.displayName || user.handle} />
) : (
(user.displayName || user.handle).charAt(0).toUpperCase()
)}
</div>
<div className="user-row-content">
<div style={{ display: 'flex', alignItems: 'center', gap: '6px' }}>
<span style={{ fontWeight: 600 }}>{user.displayName || user.handle}</span>
{user.isBot && (
<span
style={{
display: 'inline-flex',
alignItems: 'center',
gap: '3px',
fontSize: '10px',
padding: '2px 6px',
borderRadius: '4px',
background: 'var(--accent-muted)',
color: 'var(--accent)',
fontWeight: 500,
}}
>
<Bot size={12} />
AI Account
</span>
)}
</div>
<div style={{ color: 'var(--foreground-tertiary)', fontSize: '13px' }}>{formatFullHandle(user.handle)}</div>
{user.bio && stripHtml(user.bio) && (
<div className="user-row-bio">{stripHtml(user.bio)}</div>
)}
</div>
</Link>
);
}
export default function ProfilePage() {
const params = useParams();
const router = useRouter();
const handle = (params.handle as string)?.replace(/^@/, '') || '';
const [user, setUser] = useState<User | null>(null);
const [posts, setPosts] = useState<Post[]>([]);
const [likedPosts, setLikedPosts] = useState<Post[]>([]);
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<UserSummary[]>([]);
const [following, setFollowing] = useState<UserSummary[]>([]);
const [repliesPosts, setRepliesPosts] = useState<Post[]>([]);
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<string | null>(null);
const [repliesCursor, setRepliesCursor] = useState<string | null>(null);
const [isEditing, setIsEditing] = useState(false);
const [profileForm, setProfileForm] = useState({
displayName: '',
bio: '',
avatarUrl: '',
headerUrl: '',
website: '',
});
const [saveError, setSaveError] = useState<string | null>(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<HTMLDivElement>(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 (
<div style={{
minHeight: '100vh',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
color: 'var(--foreground-tertiary)',
}}>
Loading...
</div>
);
}
if (!user) {
return (
<div style={{
minHeight: '100vh',
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
gap: '16px',
}}>
<h1 style={{ fontSize: '24px', fontWeight: 600 }}>User not found</h1>
<Link href="/" className="btn btn-primary">Go home</Link>
</div>
);
}
const isOwnProfile = currentUser?.handle === user.handle;
return (
<div style={{ maxWidth: '600px', margin: '0 auto', minHeight: '100vh' }}>
{/* Header */}
<header style={{
padding: '16px',
display: 'flex',
alignItems: 'center',
gap: '24px',
borderBottom: '1px solid var(--border)',
position: 'sticky',
top: 0,
background: 'var(--background)',
zIndex: 10,
}}>
<button
onClick={() => router.back()}
style={{
color: 'var(--foreground)',
background: 'none',
border: 'none',
cursor: 'pointer',
padding: 0,
display: 'flex',
alignItems: 'center',
}}
>
<ArrowLeftIcon />
</button>
<div>
<h1 style={{ fontSize: '18px', fontWeight: 600 }}>{user.displayName || user.handle}</h1>
<p style={{ fontSize: '13px', color: 'var(--foreground-tertiary)' }}>{user.postsCount} posts</p>
</div>
</header>
{/* Account Moved Banner */}
{user.movedTo && (
<div style={{
padding: '16px',
background: 'rgba(245, 158, 11, 0.1)',
borderBottom: '1px solid rgba(245, 158, 11, 0.3)',
display: 'flex',
alignItems: 'center',
gap: '12px',
}}>
<Rocket size={24} style={{ color: 'var(--warning)' }} />
<div>
<div style={{ fontWeight: 600, color: 'var(--warning)', marginBottom: '4px' }}>
This account has moved
</div>
<div style={{ fontSize: '14px', color: 'var(--foreground-secondary)' }}>
This user has migrated to a new node:{' '}
<a
href={user.movedTo}
target="_blank"
rel="noopener noreferrer"
style={{ color: 'var(--accent)' }}
>
{user.movedTo.replace('https://', '').replace('/api/users/', '/@').replace('/users/', '/@')}
</a>
</div>
</div>
</div>
)}
{/* Profile Header */}
<div style={{ borderBottom: '1px solid var(--border)' }}>
{/* Banner */}
<div style={{
height: '150px',
background: user.headerUrl
? `url(${user.headerUrl}) center/cover`
: 'linear-gradient(135deg, var(--accent-muted) 0%, var(--background-tertiary) 100%)',
}} />
{/* Avatar & Actions */}
<div style={{ padding: '0 16px' }}>
<div style={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'flex-start',
}}>
<div className="avatar avatar-lg" style={{
width: '96px',
height: '96px',
fontSize: '36px',
border: '4px solid var(--background)',
marginTop: '-48px',
}}>
{user.avatarUrl ? (
<img src={user.avatarUrl} alt={user.displayName || user.handle} />
) : (
(user.displayName || user.handle).charAt(0).toUpperCase()
)}
</div>
<div style={{ paddingTop: '12px', display: 'flex', gap: '8px', alignItems: 'center' }}>
{!isOwnProfile && currentUser && (
<>
{!isBlocked && (
<button
className={`btn ${isFollowing ? '' : 'btn-primary'}`}
onClick={handleFollow}
>
{isFollowing ? 'Following' : 'Follow'}
</button>
)}
<div style={{ position: 'relative' }}>
<button
className="btn btn-ghost"
onClick={() => setShowMenu(!showMenu)}
style={{ padding: '8px' }}
>
<MoreHorizontal size={20} />
</button>
{showMenu && (
<>
<div
style={{
position: 'fixed',
inset: 0,
zIndex: 99,
}}
onClick={() => setShowMenu(false)}
/>
<div style={{
position: 'absolute',
right: 0,
top: '100%',
marginTop: '4px',
background: 'var(--background-secondary)',
border: '1px solid var(--border)',
borderRadius: 'var(--radius-md)',
minWidth: '160px',
zIndex: 100,
overflow: 'hidden',
}}>
<button
onClick={handleBlock}
style={{
width: '100%',
padding: '12px 16px',
background: 'none',
border: 'none',
textAlign: 'left',
cursor: 'pointer',
color: isBlocked ? 'var(--foreground)' : 'var(--error)',
fontSize: '14px',
}}
>
{isBlocked ? 'Unblock user' : 'Block user'}
</button>
</div>
</>
)}
</div>
</>
)}
{isOwnProfile && (
<button className="btn" onClick={() => setIsEditing(!isEditing)}>
{isEditing ? 'Close' : 'Edit Profile'}
</button>
)}
</div>
</div>
{/* User Info */}
<div style={{ padding: '12px 0' }}>
<h2 style={{ fontSize: '20px', fontWeight: 700 }}>{user.displayName || user.handle}</h2>
<p style={{ color: 'var(--foreground-tertiary)' }}>{formatFullHandle(user.handle)}</p>
{user.bio && (
<p style={{ marginTop: '12px', lineHeight: 1.5 }}>{user.bio}</p>
)}
<div style={{
display: 'flex',
alignItems: 'center',
gap: '8px',
marginTop: '12px',
color: 'var(--foreground-tertiary)',
fontSize: '14px',
}}>
<CalendarIcon />
<span>Joined {formatDate(user.createdAt || new Date().toISOString())}</span>
</div>
{user.website && (
<div style={{
display: 'flex',
alignItems: 'center',
gap: '8px',
marginTop: '4px',
color: 'var(--accent)',
fontSize: '14px',
}}>
<Link
href={user.website.startsWith('http') ? user.website : `https://${user.website}`}
target="_blank"
rel="noopener noreferrer"
style={{ color: 'inherit', textDecoration: 'none' }}
>
{user.website.replace(/^https?:\/\/(www\.)?/, '').replace(/\/$/, '')}
</Link>
</div>
)}
{/* Bot indicator and owner info */}
{user.isBot && (
<div style={{
display: 'flex',
alignItems: 'center',
gap: '8px',
marginTop: '12px',
padding: '8px 12px',
background: 'var(--accent-muted)',
borderRadius: 'var(--radius-md)',
fontSize: '14px',
}}>
<Bot size={16} style={{ color: 'var(--accent)' }} />
<span style={{ color: 'var(--foreground-secondary)' }}>
Automated account
{(user as any).botOwner && (
<>
{' · Managed by '}
<Link
href={`/u/${(user as any).botOwner.handle}`}
style={{ color: 'var(--accent)', fontWeight: 500 }}
>
@{(user as any).botOwner.handle}
</Link>
</>
)}
</span>
</div>
)}
<div style={{ display: 'flex', gap: '16px', marginTop: '12px' }}>
<button
onClick={() => setActiveTab('following')}
style={{
background: 'none',
border: 'none',
color: 'var(--foreground)',
cursor: 'pointer',
}}
>
<strong>{user.followingCount}</strong>{' '}
<span style={{ color: 'var(--foreground-tertiary)' }}>Following</span>
</button>
<button
onClick={() => setActiveTab('followers')}
style={{
background: 'none',
border: 'none',
color: 'var(--foreground)',
cursor: 'pointer',
}}
>
<strong>{user.followersCount}</strong>{' '}
<span style={{ color: 'var(--foreground-tertiary)' }}>Followers</span>
</button>
</div>
</div>
</div>
{isOwnProfile && isEditing && (
<div style={{ padding: '0 16px 16px' }}>
<div className="card" style={{ padding: '16px' }}>
<div style={{ fontWeight: 600, marginBottom: '12px' }}>Edit profile</div>
<div style={{ display: 'grid', gap: '12px' }}>
<div>
<label style={{ fontSize: '12px', color: 'var(--foreground-tertiary)' }}>Display name</label>
<input
className="input"
value={profileForm.displayName}
onChange={(e) => setProfileForm({ ...profileForm, displayName: e.target.value })}
maxLength={50}
/>
</div>
<div>
<label style={{ fontSize: '12px', color: 'var(--foreground-tertiary)' }}>Bio</label>
<AutoTextarea
className="input"
value={profileForm.bio}
onChange={(e) => setProfileForm({ ...profileForm, bio: e.target.value })}
maxLength={160}
style={{ minHeight: '80px', resize: 'vertical' }}
/>
</div>
<div>
<label style={{ fontSize: '12px', color: 'var(--foreground-tertiary)' }}>Website</label>
<input
className="input"
placeholder="https://example.com"
value={profileForm.website}
onChange={(e) => setProfileForm({ ...profileForm, website: e.target.value })}
maxLength={100}
/>
</div>
<div>
<label style={{ fontSize: '12px', color: 'var(--foreground-tertiary)' }}>Avatar</label>
<div style={{ display: 'flex', gap: '12px', alignItems: 'center' }}>
<label className="btn btn-ghost btn-sm" style={{ cursor: 'pointer' }}>
{isSaving ? 'Uploading...' : 'Choose File'}
<input
type="file"
accept="image/*"
onChange={async (e) => {
const file = e.target.files?.[0];
if (!file) return;
setIsSaving(true);
try {
const formData = new FormData();
formData.append('file', file);
const res = await fetch('/api/uploads', {
method: 'POST',
body: formData,
});
const data = await res.json();
if (data.url) {
setProfileForm(prev => ({ ...prev, avatarUrl: data.url }));
}
} catch (err) {
console.error(err);
setSaveError('Upload failed');
} finally {
setIsSaving(false);
}
}}
disabled={isSaving}
style={{ display: 'none' }}
/>
</label>
{profileForm.avatarUrl && (
<div style={{ width: '40px', height: '40px', borderRadius: '50%', overflow: 'hidden', border: '1px solid var(--border)' }}>
<img src={profileForm.avatarUrl} alt="Preview" style={{ width: '100%', height: '100%', objectFit: 'cover' }} />
</div>
)}
</div>
</div>
<div>
<label style={{ fontSize: '12px', color: 'var(--foreground-tertiary)' }}>Header</label>
<div style={{ display: 'flex', gap: '12px', alignItems: 'center' }}>
<label className="btn btn-ghost btn-sm" style={{ cursor: 'pointer' }}>
{isSaving ? 'Uploading...' : 'Choose File'}
<input
type="file"
accept="image/*"
onChange={async (e) => {
const file = e.target.files?.[0];
if (!file) return;
setIsSaving(true);
try {
const formData = new FormData();
formData.append('file', file);
const res = await fetch('/api/uploads', {
method: 'POST',
body: formData,
});
const data = await res.json();
if (data.url) {
setProfileForm(prev => ({ ...prev, headerUrl: data.url }));
}
} catch (err) {
console.error(err);
setSaveError('Upload failed');
} finally {
setIsSaving(false);
}
}}
disabled={isSaving}
style={{ display: 'none' }}
/>
</label>
{profileForm.headerUrl && (
<div style={{ width: '80px', height: '40px', borderRadius: '4px', overflow: 'hidden', border: '1px solid var(--border)' }}>
<img src={profileForm.headerUrl} alt="Preview" style={{ width: '100%', height: '100%', objectFit: 'cover' }} />
</div>
)}
</div>
</div>
{saveError && (
<div style={{ color: 'var(--error)', fontSize: '13px' }}>{saveError}</div>
)}
<div style={{ display: 'flex', gap: '12px', justifyContent: 'flex-end', marginTop: '8px' }}>
<button className="btn btn-ghost" onClick={() => setIsEditing(false)} disabled={isSaving}>
Cancel
</button>
<button className="btn btn-primary" onClick={handleSaveProfile} disabled={isSaving}>
{isSaving ? 'Saving...' : 'Save'}
</button>
</div>
</div>
</div>
</div>
)}
{/* Tabs */}
<div style={{ display: 'flex', borderTop: '1px solid var(--border)' }}>
{(user?.isBot
? ['posts', 'replies', 'followers', 'following'] as const
: ['posts', 'replies', 'likes', 'followers', 'following'] as const
).map(tab => (
<button
key={tab}
onClick={() => setActiveTab(tab)}
style={{
flex: 1,
padding: '16px',
background: 'none',
border: 'none',
borderBottom: activeTab === tab ? '2px solid var(--accent)' : '2px solid transparent',
color: activeTab === tab ? 'var(--foreground)' : 'var(--foreground-tertiary)',
fontWeight: activeTab === tab ? 600 : 400,
cursor: 'pointer',
textTransform: 'capitalize',
}}
>
{tab}
</button>
))}
</div>
</div>
{/* Content */}
{activeTab === 'posts' && (
postsLoading ? (
<div style={{ padding: '48px', textAlign: 'center', color: 'var(--foreground-tertiary)' }}>
<p>Loading...</p>
</div>
) : posts.length === 0 ? (
<div style={{ padding: '48px', textAlign: 'center', color: 'var(--foreground-tertiary)' }}>
<p>No posts yet</p>
</div>
) : (
<>
{posts.map((post, index) => (
<PostCard
key={`${post.id}-${index}`}
post={post}
onLike={handleLike}
onRepost={handleRepost}
onComment={handleComment}
onDelete={handleDelete}
/>
))}
<div ref={loadMoreRef} style={{ padding: '24px', textAlign: 'center' }}>
{postsLoadingMore && (
<span style={{ color: 'var(--foreground-tertiary)' }}>Loading more...</span>
)}
</div>
</>
)
)}
{activeTab === 'replies' && (
repliesLoading ? (
<div style={{ padding: '48px', textAlign: 'center', color: 'var(--foreground-tertiary)' }}>
<p>Loading...</p>
</div>
) : repliesPosts.length === 0 ? (
<div style={{ padding: '48px', textAlign: 'center', color: 'var(--foreground-tertiary)' }}>
<p>No replies yet</p>
</div>
) : (
<>
{repliesPosts.map((post, index) => (
<PostCard
key={`${post.id}-${index}`}
post={post}
onLike={handleLike}
onRepost={handleRepost}
onComment={handleComment}
onDelete={handleDelete}
/>
))}
<div ref={loadMoreRef} style={{ padding: '24px', textAlign: 'center' }}>
{repliesLoadingMore && (
<span style={{ color: 'var(--foreground-tertiary)' }}>Loading more...</span>
)}
</div>
</>
)
)}
{activeTab === 'likes' && (
likesLoading ? (
<div style={{ padding: '48px', textAlign: 'center', color: 'var(--foreground-tertiary)' }}>
<p>Loading...</p>
</div>
) : likedPosts.length === 0 ? (
<div style={{ padding: '48px', textAlign: 'center', color: 'var(--foreground-tertiary)' }}>
<p>No liked posts yet</p>
</div>
) : (
likedPosts.map((post, index) => (
<PostCard
key={`${post.id}-${index}`}
post={post}
onLike={handleLike}
onRepost={handleRepost}
onComment={handleComment}
onDelete={handleDelete}
/>
))
)
)}
{activeTab === 'followers' && (
followersLoading ? (
<div style={{ padding: '24px', textAlign: 'center', color: 'var(--foreground-tertiary)' }}>
Loading followers...
</div>
) : followers.length === 0 ? (
<div style={{ padding: '24px', textAlign: 'center', color: 'var(--foreground-tertiary)' }}>
<p>No followers yet</p>
</div>
) : (
<div>
{followers.map(follower => (
<UserRow key={follower.id} user={follower} />
))}
</div>
)
)}
{activeTab === 'following' && (
followingLoading ? (
<div style={{ padding: '24px', textAlign: 'center', color: 'var(--foreground-tertiary)' }}>
Loading following...
</div>
) : following.length === 0 ? (
<div style={{ padding: '24px', textAlign: 'center', color: 'var(--foreground-tertiary)' }}>
<p>Not following anyone yet</p>
</div>
) : (
<div>
{following.map(userItem => (
<UserRow key={userItem.id} user={userItem} />
))}
</div>
)
)}
</div>
);
}