/**
* InlineSectionComments - Displays comments for a specific bill section inline
*
* Features:
* - Section-specific comment display
* - Collapsible/expandable (default: collapsed)
* - Comment count badge
* - Inline comment form (auth-gated)
* - Reddit-style threaded display (max 7 levels)
* - Real-time updates via props
*/
'use client';
import React, { useState, useMemo } from 'react';
import { useAuth } from '@/contexts/AuthContext';
import { deletePost } from '@/actions/forum';
import { reportPost } from '@/actions/moderation';
import { PostThread } from '@/components/forum';
import { InlineCommentForm } from './InlineCommentForm';
import type { ForumPost } from '@/types/forum';
import { MessageSquare, ChevronDown, ChevronRight } from 'lucide-react';
interface InlineSectionCommentsProps {
billNumber: string;
session: string;
sectionRef: string | null; // null = general discussion
sectionLabel: string; // e.g., "Section 2.1" or "General Discussion"
locale: string;
billTitle?: string;
comments: ForumPost[]; // Flat list of comments for this section
defaultExpanded?: boolean;
onCommentCreated?: () => void;
}
/**
* Build threaded tree from flat list of posts
* Returns array of top-level posts with nested replies
*/
function buildThreadTree(posts: ForumPost[]): ForumPost[] {
// Create a map for quick lookup
const postMap = new Map<string, ForumPost & { replies: ForumPost[] }>();
// First pass: Initialize all posts with empty replies array
posts.forEach((post) => {
postMap.set(post.id, { ...post, replies: [] });
});
// Second pass: Build parent-child relationships
const topLevelPosts: ForumPost[] = [];
posts.forEach((post) => {
const postWithReplies = postMap.get(post.id)!;
if (post.parent_post_id && postMap.has(post.parent_post_id)) {
// This is a reply - add to parent's replies
const parent = postMap.get(post.parent_post_id)!;
parent.replies.push(postWithReplies);
} else {
// This is a top-level post (or orphaned reply)
topLevelPosts.push(postWithReplies);
}
});
// Sort top-level by creation date (newest first)
topLevelPosts.sort((a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime());
// Sort replies within each thread (oldest first for natural conversation flow)
const sortReplies = (post: any) => {
if (post.replies && Array.isArray(post.replies) && post.replies.length > 0) {
post.replies.sort((a: ForumPost, b: ForumPost) =>
new Date(a.created_at).getTime() - new Date(b.created_at).getTime()
);
post.replies.forEach(sortReplies);
}
};
topLevelPosts.forEach(sortReplies);
return topLevelPosts;
}
/**
* Count total comments (including all nested replies)
*/
function countComments(posts: ForumPost[]): number {
return posts.reduce((total, post) => {
return total + 1 + (post.replies ? countComments(post.replies) : 0);
}, 0);
}
export function InlineSectionComments({
billNumber,
session,
sectionRef,
sectionLabel,
locale,
billTitle,
comments,
defaultExpanded = false,
onCommentCreated,
}: InlineSectionCommentsProps) {
const { user } = useAuth();
const [isExpanded, setIsExpanded] = useState(defaultExpanded);
const [showCommentForm, setShowCommentForm] = useState(false);
// Build threaded tree from flat comments
const threadedComments = useMemo(() => buildThreadTree(comments), [comments]);
// Total comment count
const commentCount = countComments(comments);
// Handle delete
const handleDelete = async (postId: string) => {
const result = await deletePost(postId);
if (result.success) {
// Trigger refresh to update the comments list
onCommentCreated?.();
} else {
alert(result.error || (locale === 'fr' ? 'Échec de la suppression' : 'Failed to delete'));
}
};
// Handle report - simple one-click report
const handleReport = async (postId: string) => {
if (!user) {
alert(locale === 'fr' ? 'Connectez-vous pour signaler' : 'Please sign in to report');
return;
}
const result = await reportPost({ post_id: postId, reason: 'other' });
if (result.success) {
alert(locale === 'fr' ? 'Merci pour votre signalement' : 'Thank you for reporting this post. Our moderation team will review it.');
} else {
alert(result.error || (locale === 'fr' ? 'Échec du signalement' : 'Failed to report'));
}
};
// Handle successful comment creation
const handleCommentSuccess = () => {
setShowCommentForm(false);
setIsExpanded(true); // Auto-expand when new comment is added
onCommentCreated?.();
};
return (
<div className="my-6 bg-gray-50 dark:bg-gray-800/50 rounded-lg overflow-hidden">
{/* Header - clickable to expand/collapse */}
<button
onClick={() => setIsExpanded(!isExpanded)}
className="w-full flex items-center justify-between px-4 py-3 hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors"
>
<div className="flex items-center gap-3">
<MessageSquare className="h-5 w-5 text-blue-600 dark:text-blue-400" />
<span className="font-medium text-gray-700 dark:text-white">
{commentCount === 0 ? (
locale === 'fr' ? 'Aucun commentaire' : 'No comments'
) : commentCount === 1 ? (
locale === 'fr' ? '1 commentaire' : '1 comment'
) : (
`${commentCount} ${locale === 'fr' ? 'commentaires' : 'comments'}`
)}{sectionLabel && (
<span className="text-gray-700 dark:text-gray-200">
{' '}{locale === 'fr' ? 'sur' : 'on'} {sectionLabel}
</span>
)}
</span>
</div>
{/* Expand/collapse icon */}
{isExpanded ? (
<ChevronDown className="h-5 w-5 text-text-tertiary" />
) : (
<ChevronRight className="h-5 w-5 text-text-tertiary" />
)}
</button>
{/* Content - shown when expanded */}
{isExpanded && (
<div className="px-4 pb-4 space-y-4">
{/* Add comment form - show button or form */}
{user ? (
showCommentForm ? (
<InlineCommentForm
billNumber={billNumber}
session={session}
sectionRef={sectionRef}
billTitle={billTitle}
locale={locale}
onSuccess={handleCommentSuccess}
onCancel={() => setShowCommentForm(false)}
autoFocus={true}
/>
) : (
<button
onClick={() => setShowCommentForm(true)}
className="w-full px-4 py-3 bg-bg-elevated border border-border-subtle rounded-lg text-left text-text-tertiary hover:text-text-primary hover:border-accent-red/50 transition-colors"
>
{locale === 'fr' ? 'Ajouter un commentaire...' : 'Add a comment...'}
</button>
)
) : (
<div className="py-3 px-4 bg-bg-elevated border border-border-subtle rounded-lg text-center">
<p className="text-sm text-text-secondary mb-2">
{locale === 'fr'
? 'Connectez-vous pour participer à la discussion'
: 'Sign in to join the discussion'}
</p>
<a
href="/auth/signin"
className="inline-flex items-center gap-2 px-4 py-2 bg-accent-red hover:bg-red-600 text-white text-sm font-medium rounded-lg transition-colors"
>
{locale === 'fr' ? 'Se connecter' : 'Sign In'}
</a>
</div>
)}
{/* Empty state */}
{commentCount === 0 && (
<div className="text-center py-8 text-text-tertiary">
<MessageSquare className="h-12 w-12 mx-auto mb-3 opacity-30" />
<p className="text-sm">
{locale === 'fr'
? 'Soyez le premier à commenter cette section'
: 'Be the first to comment on this section'}
</p>
</div>
)}
{/* Comment threads */}
{commentCount > 0 && (
<div className="space-y-4">
{threadedComments.map((post) => (
<PostThread
key={post.id}
post={post}
onDelete={handleDelete}
onReport={handleReport}
onReplySuccess={onCommentCreated}
maxDepth={7} // Reddit-style: limit to 7 levels
showCollapseButton={true}
/>
))}
</div>
)}
</div>
)}
</div>
);
}
export default InlineSectionComments;