'use client'; import { useState, useEffect } from 'react'; import Link from 'next/link'; import { SearchIcon, TrendingIcon, UsersIcon } from '@/components/Icons'; import { PostCard } from '@/components/PostCard'; import { Post } from '@/lib/types'; import { formatFullHandle } from '@/lib/utils/handle'; import { Bot, Network, Server, EyeOff } from 'lucide-react'; import { useAuth } from '@/lib/contexts/AuthContext'; interface User { id: string; handle: string; displayName: string; avatarUrl?: string; bio?: string; profileUrl?: string | null; isRemote?: boolean; isBot?: boolean; } function UserCard({ user }: { user: User }) { return (
{user.avatarUrl ? ( {user.displayName} ) : ( user.displayName?.charAt(0).toUpperCase() || user.handle.charAt(0).toUpperCase() )}
{user.displayName || user.handle} {user.isBot && ( AI Account )}
{formatFullHandle(user.handle)}
{user.bio &&
{user.bio}
}
); } interface SwarmPost { id: string; content: string; createdAt: string; author: { handle: string; displayName: string; avatarUrl?: string; }; nodeDomain: string; likeCount: number; repostCount: number; replyCount: number; media?: { url: string; mimeType?: string; altText?: string }[]; linkPreviewUrl?: string; linkPreviewTitle?: string; linkPreviewDescription?: string; linkPreviewImage?: string; } export default function ExplorePage() { const { user } = useAuth(); const [query, setQuery] = useState(''); const [activeTab, setActiveTab] = useState<'node' | 'swarm' | 'users' | 'search'>('node'); const [nodePosts, setNodePosts] = useState([]); const [swarmPosts, setSwarmPosts] = useState([]); const [swarmSources, setSwarmSources] = useState<{ domain: string; postCount: number }[]>([]); const [users, setUsers] = useState([]); const [searchResults, setSearchResults] = useState<{ posts: Post[]; users: User[] }>({ posts: [], users: [] }); const [loading, setLoading] = useState(true); const [searching, setSearching] = useState(false); const [nodeCursor, setNodeCursor] = useState(null); const [swarmCursor, setSwarmCursor] = useState(null); const [loadingMore, setLoadingMore] = useState(false); const [hasMoreNode, setHasMoreNode] = useState(true); const [hasMoreSwarm, setHasMoreSwarm] = useState(true); const [isNsfwNode, setIsNsfwNode] = useState(false); // Fetch node info to check if NSFW useEffect(() => { fetch('/api/node') .then(res => res.json()) .then(data => { setIsNsfwNode(data.isNsfw || false); }) .catch(() => { }); }, []); useEffect(() => { // Load node posts (local only) const loadNodePosts = async () => { setLoading(true); try { const res = await fetch('/api/posts?type=local&limit=20'); const data = await res.json(); setNodePosts(data.posts || []); setNodeCursor(data.nextCursor || null); setHasMoreNode(!!data.nextCursor); } catch { setNodePosts([]); } finally { setLoading(false); } }; if (activeTab === 'node' && nodePosts.length === 0) { loadNodePosts(); } }, [activeTab, nodePosts.length]); useEffect(() => { // Load swarm posts when tab changes if (activeTab === 'swarm' && swarmPosts.length === 0) { const loadSwarm = async () => { setLoading(true); try { const res = await fetch('/api/posts/swarm'); const data = await res.json(); setSwarmPosts(data.posts || []); setSwarmSources(data.sources || []); // Set cursor from the last post if available if (data.posts && data.posts.length > 0) { const lastPost = data.posts[data.posts.length - 1]; setSwarmCursor(lastPost.createdAt); // Use timestamp as cursor setHasMoreSwarm(true); } else { setHasMoreSwarm(false); } } catch { setSwarmPosts([]); } finally { setLoading(false); } }; loadSwarm(); } }, [activeTab, swarmPosts.length]); // Load more node posts const loadMoreNode = async () => { if (!nodeCursor || loadingMore || !hasMoreNode) return; setLoadingMore(true); try { const res = await fetch(`/api/posts?type=local&limit=20&cursor=${nodeCursor}`); const data = await res.json(); if (data.posts && data.posts.length > 0) { setNodePosts(prev => [...prev, ...data.posts]); setNodeCursor(data.nextCursor || null); setHasMoreNode(!!data.nextCursor); } else { setHasMoreNode(false); } } catch { // Error loading more } finally { setLoadingMore(false); } }; // Load more swarm posts const loadMoreSwarm = async () => { if (!swarmCursor || loadingMore || !hasMoreSwarm) return; setLoadingMore(true); try { // Use timestamp of last post as cursor const res = await fetch(`/api/posts/swarm?limit=20&cursor=${encodeURIComponent(swarmCursor)}`); const data = await res.json(); if (data.posts && data.posts.length > 0) { setSwarmPosts(prev => [...prev, ...data.posts]); const lastPost = data.posts[data.posts.length - 1]; setSwarmCursor(lastPost.createdAt); setHasMoreSwarm(true); } else { setHasMoreSwarm(false); } } catch { // Error loading more } finally { setLoadingMore(false); } }; // Intersection Observer for Infinite Scroll useEffect(() => { const observer = new IntersectionObserver( (entries) => { if (entries[0].isIntersecting) { if (activeTab === 'node') { loadMoreNode(); } else if (activeTab === 'swarm') { loadMoreSwarm(); } } }, { threshold: 0.5 } ); const sentinel = document.getElementById('scroll-sentinel'); if (sentinel) { observer.observe(sentinel); } return () => observer.disconnect(); }, [activeTab, nodeCursor, swarmCursor, loadingMore, hasMoreNode, hasMoreSwarm]); useEffect(() => { // Load users when tab changes to users if (activeTab === 'users' && users.length === 0) { const loadUsers = async () => { setLoading(true); try { const res = await fetch('/api/users?limit=20'); const data = await res.json(); setUsers(data.users || []); } catch { setUsers([]); } finally { setLoading(false); } }; loadUsers(); } }, [activeTab, users.length]); const handleSearch = async (e: React.FormEvent) => { e.preventDefault(); if (!query.trim()) return; setSearching(true); setActiveTab('search'); try { const res = await fetch(`/api/search?q=${encodeURIComponent(query)}`); const data = await res.json(); setSearchResults({ posts: data.posts || [], users: data.users || [], }); } catch { setSearchResults({ posts: [], users: [] }); } finally { setSearching(false); } }; 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 handleDelete = (postId: string) => { setNodePosts(prev => prev.filter(p => p.id !== postId)); setSwarmPosts(prev => prev.filter(p => p.id !== postId)); setSearchResults(prev => ({ ...prev, posts: prev.posts.filter(p => p.id !== postId) })); }; return (

Explore

setQuery(e.target.value)} />
{searchResults.posts.length > 0 || searchResults.users.length > 0 ? ( ) : null}
{activeTab === 'node' && ( !user && isNsfwNode ? (

Adult Content

This node contains adult or sensitive content. You must be 18 or older and signed in to view posts.

) : loading ? (
Loading posts...
) : nodePosts.length === 0 ? (

No posts on this node yet

) : ( <>
Node feed
A chronological feed of all posts from users on this node. See what the local community is sharing.
{nodePosts.map((post) => ( ))} {/* Sentinel for Infinite Scroll */} {hasMoreNode && (
{loadingMore ? 'Loading more...' : ''}
)}
) )} {activeTab === 'swarm' && ( loading ? (
Loading swarm posts...
) : swarmPosts.length === 0 ? (

No swarm posts yet

Posts from other Synapsis nodes will appear here

) : ( <>
Swarm feed
Posts from across the Synapsis network. Currently showing posts from {swarmSources.filter(s => s.postCount > 0).length} node{swarmSources.filter(s => s.postCount > 0).length !== 1 ? 's' : ''}.
{swarmPosts.map((post) => { // Transform swarm post to Post format for PostCard const transformedPost: Post = { id: `swarm:${post.nodeDomain}:${post.id}`, originalPostId: post.id, content: post.content, createdAt: post.createdAt, likesCount: post.likeCount, repostsCount: post.repostCount, repliesCount: post.replyCount, isSwarm: true, nodeDomain: post.nodeDomain, author: { id: `swarm:${post.nodeDomain}:${post.author.handle}`, handle: post.author.handle, displayName: post.author.displayName, avatarUrl: post.author.avatarUrl, }, media: post.media?.map((m, idx) => ({ id: `swarm:${post.nodeDomain}:${post.id}:media:${idx}`, url: m.url, altText: m.altText || null, mimeType: m.mimeType || null, })) || [], linkPreviewUrl: post.linkPreviewUrl || null, linkPreviewTitle: post.linkPreviewTitle || null, linkPreviewDescription: post.linkPreviewDescription || null, linkPreviewImage: post.linkPreviewImage || null, }; return ( ); })} {/* Sentinel for Infinite Scroll */} {hasMoreSwarm && (
{loadingMore ? 'Loading more...' : ''}
)}
) )} {activeTab === 'users' && ( loading ? (
Loading users...
) : users.length === 0 ? (

No users found

) : (
{users.map((user) => ( ))}
) )} {activeTab === 'search' && ( searching ? (
Searching...
) : (
{searchResults.users.length > 0 && (

Users

{searchResults.users.map((user) => ( ))}
)} {searchResults.posts.length > 0 && (

Posts

{searchResults.posts.map((post) => ( ))}
)} {searchResults.users.length === 0 && searchResults.posts.length === 0 && (

No results found for “{query}”

)}
) )}
); }