'use client';
import { useState, useCallback } from 'react';
import { PostCard } from './PostCard';
import type { ForumPost } from '@/types/forum';
import { ChevronDown, ChevronRight, Loader2 } from 'lucide-react';
import { getPostThread } from '@/actions/forum';
interface PostThreadProps {
post: ForumPost;
onReply?: (post: ForumPost) => void;
onEdit?: (post: ForumPost) => void;
onDelete?: (postId: string) => void;
onReport?: (postId: string) => void;
onReplySuccess?: () => void;
maxDepth?: number;
showCollapseButton?: boolean;
/** Whether to show post titles (default: true) */
showTitle?: boolean;
/** Whether replies should start collapsed (default: false for top-level, true for nested) */
defaultCollapsed?: boolean;
}
export function PostThread({
post,
onReply,
onEdit,
onDelete,
onReport,
onReplySuccess,
maxDepth = 10,
showCollapseButton = true,
showTitle = true,
defaultCollapsed,
}: PostThreadProps) {
// Default: top-level posts (depth 0) start expanded, nested replies start collapsed
const shouldStartCollapsed = defaultCollapsed ?? (post.depth > 0);
const [isCollapsed, setIsCollapsed] = useState(shouldStartCollapsed);
const [loadedReplies, setLoadedReplies] = useState<ForumPost[] | null>(null);
const [isLoadingReplies, setIsLoadingReplies] = useState(false);
const [localReplyCount, setLocalReplyCount] = useState(post.reply_count || 0);
// Use loaded replies if available, otherwise fall back to post.replies
const replies = loadedReplies ?? post.replies ?? [];
const hasRepliesLoaded = replies.length > 0;
const hasRepliesCount = localReplyCount > 0;
const needsToLoadReplies = hasRepliesCount && !hasRepliesLoaded;
const canShowReplies = hasRepliesLoaded && post.depth < maxDepth;
// Fetch replies on-demand when expanding
const handleToggleReplies = useCallback(async () => {
if (isCollapsed && needsToLoadReplies && !isLoadingReplies) {
// Need to fetch replies first
setIsLoadingReplies(true);
try {
const result = await getPostThread(post.id, maxDepth);
if (result.success && result.data && result.data.length > 0) {
// getPostThread returns a tree with the requested post at the root
// The first item should be our post with replies nested
const rootPost = result.data[0];
if (rootPost && rootPost.id === post.id) {
setLoadedReplies(rootPost.replies || []);
} else {
// Fallback: search for the post in the tree
const findPost = (posts: ForumPost[]): ForumPost | null => {
for (const p of posts) {
if (p.id === post.id) return p;
if (p.replies) {
const found = findPost(p.replies);
if (found) return found;
}
}
return null;
};
const foundPost = findPost(result.data);
setLoadedReplies(foundPost?.replies || []);
}
}
} catch (error) {
console.error('Failed to load replies:', error);
} finally {
setIsLoadingReplies(false);
}
}
setIsCollapsed(!isCollapsed);
}, [isCollapsed, needsToLoadReplies, isLoadingReplies, post.id, maxDepth]);
// Refresh replies after a new reply is created
const refreshReplies = useCallback(async () => {
try {
const result = await getPostThread(post.id, maxDepth);
if (result.success && result.data && result.data.length > 0) {
const rootPost = result.data[0];
if (rootPost && rootPost.id === post.id) {
setLoadedReplies(rootPost.replies || []);
setLocalReplyCount(rootPost.reply_count || rootPost.replies?.length || 0);
} else {
// Fallback: search for the post in the tree
const findPost = (posts: ForumPost[]): ForumPost | null => {
for (const p of posts) {
if (p.id === post.id) return p;
if (p.replies) {
const found = findPost(p.replies);
if (found) return found;
}
}
return null;
};
const foundPost = findPost(result.data);
if (foundPost) {
setLoadedReplies(foundPost.replies || []);
setLocalReplyCount(foundPost.reply_count || foundPost.replies?.length || 0);
}
}
}
// Expand to show the new reply
setIsCollapsed(false);
} catch (error) {
console.error('Failed to refresh replies:', error);
}
}, [post.id, maxDepth]);
// Handle reply success - refresh this thread's replies and call parent callback
const handleReplySuccess = useCallback(() => {
refreshReplies();
if (onReplySuccess) {
onReplySuccess();
}
}, [refreshReplies, onReplySuccess]);
// Calculate indent style based on depth (different for mobile vs desktop)
// Indentation increases with depth, but border is only on depth 1 (first reply level)
const getIndentStyle = (depth: number): React.CSSProperties => {
if (depth === 0) return {};
const level = Math.min(depth, 5);
return {
// Desktop: 24px per level, will be overridden on mobile by class
marginLeft: `${(level - 1) * 24}px`, // Reduce by 1 level since container has padding
};
};
// Class to set fixed indent on mobile (overrides inline styles)
const getIndentClass = (depth: number): string => {
if (depth === 0) return '';
// On mobile: fixed indent for reply levels
return 'max-sm:!ml-2';
};
const replyCount = localReplyCount || replies.length || 0;
return (
<div className="relative">
{/* Main post */}
<div className={getIndentClass(post.depth)} style={getIndentStyle(post.depth)}>
{/* Post card */}
<PostCard
post={post}
showReplyButton={post.depth < maxDepth}
onReply={onReply}
onEdit={onEdit}
onDelete={onDelete}
onReport={onReport}
onReplySuccess={handleReplySuccess}
variant={post.depth > 0 ? 'compact' : 'full'}
showTitle={showTitle}
/>
{/* Collapse/Expand button underneath the post */}
{showCollapseButton && (hasRepliesLoaded || hasRepliesCount) && (
<button
onClick={handleToggleReplies}
disabled={isLoadingReplies}
className="flex items-center gap-1.5 mt-2 ml-4 text-sm text-text-tertiary hover:text-accent-red transition-colors disabled:opacity-50"
>
{isLoadingReplies ? (
<>
<Loader2 size={16} className="animate-spin" />
<span>Loading replies...</span>
</>
) : isCollapsed ? (
<>
<ChevronRight size={16} />
<span>Show {replyCount} {replyCount === 1 ? 'reply' : 'replies'}</span>
</>
) : (
<>
<ChevronDown size={16} />
<span>Hide {replyCount} {replyCount === 1 ? 'reply' : 'replies'}</span>
</>
)}
</button>
)}
</div>
{/* Nested replies - only top-level (depth 0) gets the red line, deeper levels just indent */}
{!isCollapsed && canShowReplies && (
<div className={
post.depth === 0
? "mt-3 ml-4 pl-4 border-l-2 border-accent-red/20 space-y-3 max-sm:ml-2 max-sm:pl-2"
: "mt-3 ml-6 space-y-3 max-sm:ml-3"
}>
{replies.map((reply) => (
<PostThread
key={reply.id}
post={reply}
onReply={onReply}
onEdit={onEdit}
onDelete={onDelete}
onReport={onReport}
onReplySuccess={handleReplySuccess}
maxDepth={maxDepth}
showCollapseButton={showCollapseButton}
showTitle={showTitle}
defaultCollapsed={defaultCollapsed}
/>
))}
</div>
)}
{/* Max depth indicator */}
{post.depth >= maxDepth && (hasRepliesLoaded || hasRepliesCount) && (
<div className={
post.depth === 0
? "mt-3 ml-4 pl-4 border-l-2 border-accent-red/20 max-sm:ml-2 max-sm:pl-2"
: "mt-3 ml-6 max-sm:ml-3"
}>
<div className="p-3 bg-background-primary rounded border border-border-primary">
<p className="text-sm text-text-tertiary">
Thread continues...{' '}
<a
href={`/forum/posts/${replies[0]?.id || post.id}`}
className="text-accent-red hover:underline"
>
View {replyCount} more {replyCount === 1 ? 'reply' : 'replies'}
</a>
</p>
</div>
</div>
)}
</div>
);
}