Files
Synapsis/src/app/u/[handle]/posts/[id]/page.tsx
T
2026-03-08 19:22:18 -07:00

206 lines
7.2 KiB
TypeScript

'use client';
import { useState, useEffect } from 'react';
import Link from 'next/link';
import { useParams, useRouter } from 'next/navigation';
import { ArrowLeftIcon } from '@/components/Icons';
import { PostCard } from '@/components/PostCard';
import { Compose } from '@/components/Compose';
import { useAuth } from '@/lib/contexts/AuthContext';
import { Post } from '@/lib/types';
import { signedAPI } from '@/lib/api/signed-fetch';
export default function PostDetailPage() {
const params = useParams();
const router = useRouter();
const { user, did, handle: userHandle } = useAuth();
const handle = params.handle as string;
const id = params.id as string;
const [post, setPost] = useState<Post | null>(null);
const [replies, setReplies] = useState<Post[]>([]);
const [replyingTo, setReplyingTo] = useState<Post | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const fetchPostDetail = async () => {
try {
const res = await fetch(`/api/posts/${id}`);
if (!res.ok) {
throw new Error('Post not found');
}
const data = await res.json();
setPost(data.post);
setReplies(data.replies || []);
setReplyingTo(data.post);
} catch (err) {
setError(err instanceof Error ? err.message : 'Failed to load post');
} finally {
setLoading(false);
}
};
useEffect(() => {
fetchPostDetail();
}, [id]);
const handlePost = async (content: string, mediaIds: string[], linkPreview?: any, replyToId?: string, isNsfw?: boolean) => {
// Check if we're replying to a swarm post
let swarmReplyTo: { postId: string; nodeDomain: string; content?: string; author?: any } | undefined;
let localReplyToId: string | undefined = replyToId;
if (replyingTo?.isSwarm && replyingTo.nodeDomain && replyingTo.originalPostId) {
// This is a reply to a swarm post - send to the origin node
swarmReplyTo = {
postId: replyingTo.originalPostId,
nodeDomain: replyingTo.nodeDomain,
content: replyingTo.content,
author: replyingTo.author,
};
localReplyToId = undefined; // Can't use UUID foreign key for swarm posts
}
if (!did || !userHandle) return;
const res = await signedAPI.createPost(
content,
mediaIds,
linkPreview,
localReplyToId,
swarmReplyTo,
isNsfw || false,
did,
userHandle
);
if (res.ok) {
const data = await res.json();
// Add the new reply to the top of the list or re-fetch
setReplies([{ ...data.post, author: user }, ...replies]);
if (post) {
setPost({ ...post, repliesCount: post.repliesCount + 1 });
}
setReplyingTo(post);
}
};
const handleLike = async (postId: string, currentLiked: boolean) => {
if (!did || !userHandle) return;
const res = currentLiked
? await signedAPI.unlikePost(postId, did, userHandle)
: await signedAPI.likePost(postId, did, userHandle);
if (!res.ok) {
const data = await res.json().catch(() => ({}));
throw new Error(data.error || 'Failed to update like');
}
};
const handleRepost = async (postId: string, currentReposted: boolean) => {
if (!did || !userHandle) return;
const res = currentReposted
? await signedAPI.unrepostPost(postId, did, userHandle)
: await signedAPI.repostPost(postId, did, userHandle);
if (!res.ok) {
const data = await res.json().catch(() => ({}));
throw new Error(data.error || 'Failed to update repost');
}
};
const handleDelete = (postId: string) => {
if (postId === id) {
router.push(`/u/${handle}`);
} else {
setReplies(prev => prev.filter(r => r.id !== postId));
if (post) {
setPost({ ...post, repliesCount: Math.max(0, post.repliesCount - 1) });
}
}
};
if (loading) {
return (
<div style={{ padding: '48px', textAlign: 'center', color: 'var(--foreground-tertiary)' }}>
Loading...
</div>
);
}
if (error || !post) {
return (
<div style={{ padding: '48px', textAlign: 'center' }}>
<h1 style={{ fontSize: '20px', marginBottom: '16px' }}>{error || 'Post not found'}</h1>
<button className="btn btn-primary" onClick={() => router.back()}>Go Back</button>
</div>
);
}
return (
<>
<header style={{
padding: '16px',
borderBottom: '1px solid var(--border)',
position: 'sticky',
top: 0,
background: 'var(--background)',
zIndex: 10,
backdropFilter: 'blur(12px)',
display: 'flex',
alignItems: 'center',
gap: '24px',
}}>
<button
onClick={() => router.back()}
style={{ background: 'none', border: 'none', color: 'var(--foreground)', cursor: 'pointer', display: 'flex' }}
>
<ArrowLeftIcon />
</button>
<h1 style={{ fontSize: '18px', fontWeight: 600 }}>Post</h1>
</header>
<PostCard
post={post}
isDetail
onLike={handleLike}
onRepost={handleRepost}
onDelete={handleDelete}
onComment={() => {
const composer = document.querySelector('.compose-input') as HTMLTextAreaElement;
composer?.focus();
}}
/>
{user && (
<div style={{ borderBottom: '1px solid var(--border)' }}>
<Compose
onPost={handlePost}
replyingTo={replyingTo}
isReply
placeholder="Post your reply"
onCancelReply={() => setReplyingTo(post)}
/>
</div>
)}
<div className="replies-list" style={{ paddingBottom: '64px' }}>
{replies.map((reply) => (
<PostCard
key={reply.id}
post={reply}
onLike={handleLike}
onRepost={handleRepost}
onDelete={handleDelete}
parentPostAuthorId={post.author.id}
onComment={(replyTarget) => {
setReplyingTo(replyTarget);
const composer = document.querySelector('.compose-input') as HTMLTextAreaElement;
composer?.focus();
}}
/>
))}
</div>
</>
);
}