'use client'; import { useState, useEffect } from 'react'; import Link from 'next/link'; import { useAuth } from '@/lib/contexts/AuthContext'; import AutoTextarea from '@/components/AutoTextarea'; interface User { id: string; handle: string; displayName: string; avatarUrl?: string; } interface MediaItem { id: string; url: string; altText?: string | null; } interface FeedMeta { score: number; reasons: string[]; engagement: { likes: number; reposts: number; replies: number; }; } interface Post { id: string; content: string; createdAt: string; likesCount: number; repostsCount: number; repliesCount: number; author: User; media?: MediaItem[]; feedMeta?: FeedMeta; linkPreviewUrl?: string | null; linkPreviewTitle?: string | null; linkPreviewDescription?: string | null; linkPreviewImage?: string | null; } // Icons as simple SVG components const HeartIcon = ({ filled }: { filled?: boolean }) => ( ); const RepeatIcon = () => ( ); const MessageIcon = () => ( ); const FlagIcon = () => ( ); function PostCard({ post, onLike, onRepost }: { post: Post; onLike: (id: string) => void; onRepost: (id: string) => void }) { const [liked, setLiked] = useState(false); const [reposted, setReposted] = useState(false); const [reporting, setReporting] = useState(false); const formatTime = (dateStr: string) => { const date = new Date(dateStr); const now = new Date(); const diff = now.getTime() - date.getTime(); const minutes = Math.floor(diff / 60000); const hours = Math.floor(minutes / 60); const days = Math.floor(hours / 24); if (minutes < 1) return 'now'; if (minutes < 60) return `${minutes}m`; if (hours < 24) return `${hours}h`; if (days < 7) return `${days}d`; return date.toLocaleDateString(); }; const handleLike = () => { setLiked(!liked); onLike(post.id); }; const handleRepost = () => { setReposted(!reposted); onRepost(post.id); }; const handleReport = async () => { if (reporting) return; const reason = window.prompt('Why are you reporting this post?'); if (!reason) return; setReporting(true); try { const res = await fetch('/api/reports', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ targetType: 'post', targetId: post.id, reason }), }); if (!res.ok) { if (res.status === 401) { alert('Please log in to report.'); } else { alert('Report failed. Please try again.'); } } else { alert('Report submitted. Thank you.'); } } catch { alert('Report failed. Please try again.'); } finally { setReporting(false); } }; return (
{post.author.avatarUrl ? ( {post.author.displayName} ) : ( post.author.displayName?.charAt(0).toUpperCase() || post.author.handle.charAt(0).toUpperCase() )}
{post.author.displayName || post.author.handle} @{post.author.handle} ยท {formatTime(post.createdAt)}
{post.content}
{post.feedMeta?.reasons?.length ? (
{post.feedMeta.reasons.map((reason, index) => ( {reason} ))}
) : null} {post.media && post.media.length > 0 && (
{post.media.map((item) => (
{item.altText
))}
)} {post.linkPreviewUrl && ( {post.linkPreviewImage && (
{post.linkPreviewTitle
)}
{post.linkPreviewTitle}
{post.linkPreviewDescription && (
{post.linkPreviewDescription}
)}
{new URL(post.linkPreviewUrl.startsWith('http') ? post.linkPreviewUrl : `https://${post.linkPreviewUrl}`).hostname}
)}
); } type Attachment = { id: string; url: string; altText?: string | null; }; function Compose({ onPost }: { onPost: (content: string, mediaIds: string[], linkPreview?: any) => void }) { const [content, setContent] = useState(''); const [isPosting, setIsPosting] = useState(false); const [attachments, setAttachments] = useState([]); const [isUploading, setIsUploading] = useState(false); const [uploadError, setUploadError] = useState(null); const [linkPreview, setLinkPreview] = useState(null); const [fetchingPreview, setFetchingPreview] = useState(false); const [lastDetectedUrl, setLastDetectedUrl] = useState(null); const maxLength = 400; const remaining = maxLength - content.length; // Detect URLs in content useEffect(() => { const urlRegex = /(?:https?:\/\/)?((?:[a-zA-Z0-9-]+\.)+[a-z]{2,63})\b([-a-zA-Z0-9@:%_\+.~#?&//=]*)/gi; const matches = content.match(urlRegex); if (matches && matches[0]) { const url = matches[0]; if (url !== lastDetectedUrl) { setLastDetectedUrl(url); fetchPreview(url); } } else if (!content.trim()) { setLinkPreview(null); setLastDetectedUrl(null); } }, [content]); const fetchPreview = async (url: string) => { setFetchingPreview(true); try { const res = await fetch(`/api/media/preview?url=${encodeURIComponent(url)}`); if (res.ok) { const data = await res.json(); setLinkPreview(data); } } catch (err) { console.error('Preview error', err); } finally { setFetchingPreview(false); } }; const handleSubmit = async () => { if (!content.trim() || isPosting || isUploading) return; setIsPosting(true); await onPost(content, attachments.map((item) => item.id).filter(Boolean), linkPreview); setContent(''); setAttachments([]); setLinkPreview(null); setLastDetectedUrl(null); setIsPosting(false); }; const handleMediaSelect = async (event: React.ChangeEvent) => { const files = Array.from(event.target.files || []); event.target.value = ''; if (files.length === 0) return; const remainingSlots = Math.max(0, 4 - attachments.length); const selectedFiles = files.slice(0, remainingSlots); if (selectedFiles.length === 0) return; setUploadError(null); setIsUploading(true); const uploaded: Attachment[] = []; for (const file of selectedFiles) { try { const formData = new FormData(); formData.append('file', file); const res = await fetch('/api/media/upload', { method: 'POST', body: formData, }); const data = await res.json(); if (!res.ok || !data.media?.id) { throw new Error(data.error || 'Upload failed'); } uploaded.push({ id: data.media.id, url: data.media.url || data.url, altText: data.media.altText ?? null, }); } catch (error) { console.error('Upload failed', error); setUploadError('One or more uploads failed. Try again.'); } } setAttachments((prev) => [...prev, ...uploaded].slice(0, 4)); setIsUploading(false); }; const handleRemoveAttachment = (id: string) => { setAttachments((prev) => prev.filter((item) => item.id !== id)); }; return (
setContent(e.target.value)} maxLength={maxLength + 50} // Allow some overflow for better UX /> {attachments.length > 0 && (
{attachments.map((item) => (
{item.altText
))}
)} {linkPreview && (
{linkPreview.image && (
)}
{linkPreview.title}
{new URL(linkPreview.url.startsWith('http') ? linkPreview.url : `https://${linkPreview.url}`).hostname}
)} {uploadError && (
{uploadError}
)}
{remaining}
); } export default function Home() { const { user } = useAuth(); const [posts, setPosts] = useState([]); const [loading, setLoading] = useState(true); const [feedType, setFeedType] = useState<'latest' | 'curated'>('latest'); const [feedMeta, setFeedMeta] = useState<{ algorithm: string; windowHours: number; seedLimit: number; weights: { engagement: number; recency: number; followBoost: number; selfBoost: number; }; } | null>(null); const loadFeed = async (type: 'latest' | 'curated') => { setLoading(true); try { const endpoint = type === 'curated' ? '/api/posts?type=curated' : '/api/posts?type=home'; const res = await fetch(endpoint); const data = await res.json(); setPosts(data.posts || []); setFeedMeta(data.meta || null); } catch { setPosts([]); setFeedMeta(null); } finally { setLoading(false); } }; useEffect(() => { loadFeed(feedType); }, [feedType]); const handlePost = async (content: string, mediaIds: string[], linkPreview?: any) => { const res = await fetch('/api/posts', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ content, mediaIds, linkPreview }), }); if (res.ok) { const data = await res.json(); if (feedType === 'curated') { loadFeed('curated'); } else { setPosts([{ ...data.post, author: user }, ...posts]); } } }; const handleLike = async (postId: string) => { await fetch(`/api/posts/${postId}/like`, { method: 'POST' }); }; const handleRepost = async (postId: string) => { await fetch(`/api/posts/${postId}/repost`, { method: 'POST' }); }; return ( <>

Home

{user && } {!user && (

Join Synapsis to post and interact

Login or Register
)} {feedType === 'curated' && feedMeta && (
Curated feed
We rank posts using recency and engagement. Following gets a boost, and your own posts stay visible.
Weights: engagement {feedMeta.weights.engagement}, recency {feedMeta.weights.recency}, follow boost {feedMeta.weights.followBoost}.
Window: {feedMeta.windowHours} hours. Seed: {feedMeta.seedLimit} posts.
)} {loading ? (
Loading...
) : posts.length === 0 ? (

No posts yet

Be the first to post something!

) : ( posts.map(post => ( )) )} ); }