import React, { useState, useEffect, useCallback } from 'react'; import { useParams, useNavigate } from 'react-router-dom'; import Layout from '@/components/Layout'; import { supabase } from '@/lib/supabase'; import { useAuth } from '@/contexts/AuthContext'; import { Button } from '@/components/ui/button'; import { Textarea } from '@/components/ui/textarea'; import { Card, CardContent } from '@/components/ui/card'; import { Badge } from '@/components/ui/badge'; import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogFooter, } from '@/components/ui/dialog'; import { ArrowLeft, MessageSquare, Eye, Pin, Lock, Clock, ThumbsUp, Loader2, Users, Flag, Share2, Reply, ChevronUp, ChevronDown, LogIn, Send, MoreHorizontal, Bookmark, AlertTriangle, } from 'lucide-react'; import { formatDistanceToNow, format } from 'date-fns'; import { useTranslation } from 'react-i18next'; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, } from '@/components/ui/dropdown-menu'; interface Category { id: string; name: string; icon: string; color: string; } interface Discussion { id: string; category_id: string; title: string; content: string; author_id: string; author_name: string; author_address: string; is_pinned: boolean; is_locked: boolean; views_count: number; replies_count: number; tags: string[]; created_at: string; last_activity_at: string; category?: Category; upvotes: number; } interface Reply { id: string; discussion_id: string; content: string; author_id: string; author_name: string; author_address: string; parent_reply_id: string | null; upvotes: number; downvotes: number; created_at: string; is_hidden: boolean; replies?: Reply[]; } const ForumTopic: React.FC = () => { const { id } = useParams<{ id: string }>(); const navigate = useNavigate(); const { user } = useAuth(); const { t } = useTranslation(); const [discussion, setDiscussion] = useState(null); const [replies, setReplies] = useState([]); const [isLoading, setIsLoading] = useState(true); const [isSubmitting, setIsSubmitting] = useState(false); const [replyContent, setReplyContent] = useState(''); const [replyingTo, setReplyingTo] = useState(null); const [nestedReplyContent, setNestedReplyContent] = useState(''); const [showLoginPrompt, setShowLoginPrompt] = useState(false); const [userVotes, setUserVotes] = useState>({}); const [isBookmarked, setIsBookmarked] = useState(false); const [showReportModal, setShowReportModal] = useState(false); const [reportingId, setReportingId] = useState<{ type: 'discussion' | 'reply'; id: string } | null>(null); // Auth-gated action wrapper const requireAuth = (action: () => void) => { if (!user) { setShowLoginPrompt(true); return; } action(); }; // Fetch discussion const fetchDiscussion = useCallback(async () => { if (!id) return; const { data, error } = await supabase .from('forum_discussions') .select(` *, category:forum_categories(id, name, icon, color) `) .eq('id', id) .single(); if (!error && data) { setDiscussion(data); // Increment view count await supabase .from('forum_discussions') .update({ views_count: (data.views_count || 0) + 1 }) .eq('id', id); } }, [id]); // Fetch replies const fetchReplies = useCallback(async () => { if (!id) return; const { data, error } = await supabase .from('forum_replies') .select('*') .eq('discussion_id', id) .eq('is_hidden', false) .order('created_at', { ascending: true }); if (!error && data) { // Organize into nested structure const replyMap = new Map(); const rootReplies: Reply[] = []; data.forEach(reply => { replyMap.set(reply.id, { ...reply, replies: [] }); }); data.forEach(reply => { const replyWithChildren = replyMap.get(reply.id)!; if (reply.parent_reply_id) { const parent = replyMap.get(reply.parent_reply_id); if (parent) { parent.replies = parent.replies || []; parent.replies.push(replyWithChildren); } } else { rootReplies.push(replyWithChildren); } }); setReplies(rootReplies); } }, [id]); // Fetch user's votes const fetchUserVotes = useCallback(async () => { if (!user || !id) return; const { data } = await supabase .from('forum_votes') .select('target_id, vote_type') .eq('user_id', user.id) .eq('discussion_id', id); if (data) { const votes: Record = {}; data.forEach(v => { votes[v.target_id] = v.vote_type as 'up' | 'down'; }); setUserVotes(votes); } }, [user, id]); // Check if bookmarked const checkBookmark = useCallback(async () => { if (!user || !id) return; const { data } = await supabase .from('forum_bookmarks') .select('id') .eq('user_id', user.id) .eq('discussion_id', id) .single(); setIsBookmarked(!!data); }, [user, id]); // Initial load useEffect(() => { const loadData = async () => { setIsLoading(true); await Promise.all([ fetchDiscussion(), fetchReplies(), fetchUserVotes(), checkBookmark(), ]); setIsLoading(false); }; loadData(); }, [fetchDiscussion, fetchReplies, fetchUserVotes, checkBookmark]); // Submit reply const handleSubmitReply = async (parentId: string | null = null) => { const content = parentId ? nestedReplyContent : replyContent; if (!user || !content.trim() || !id) return; setIsSubmitting(true); const { error } = await supabase .from('forum_replies') .insert({ discussion_id: id, content: content.trim(), author_id: user.id, author_name: user.user_metadata?.name || user.email?.split('@')[0] || 'Anonymous', author_address: user.user_metadata?.wallet_address || null, parent_reply_id: parentId, }); if (!error) { // Update reply count await supabase .from('forum_discussions') .update({ replies_count: (discussion?.replies_count || 0) + 1, last_activity_at: new Date().toISOString(), }) .eq('id', id); if (parentId) { setNestedReplyContent(''); setReplyingTo(null); } else { setReplyContent(''); } fetchReplies(); fetchDiscussion(); } setIsSubmitting(false); }; // Vote on reply const handleVote = async (replyId: string, voteType: 'up' | 'down') => { if (!user || !id) return; const currentVote = userVotes[replyId]; if (currentVote === voteType) { // Remove vote await supabase .from('forum_votes') .delete() .eq('user_id', user.id) .eq('target_id', replyId); setUserVotes(prev => ({ ...prev, [replyId]: null })); // Update reply vote count const field = voteType === 'up' ? 'upvotes' : 'downvotes'; await supabase.rpc('decrement_vote', { reply_id: replyId, vote_field: field }); } else { // Upsert vote await supabase .from('forum_votes') .upsert({ user_id: user.id, discussion_id: id, target_id: replyId, target_type: 'reply', vote_type: voteType, }, { onConflict: 'user_id,target_id' }); setUserVotes(prev => ({ ...prev, [replyId]: voteType })); // Update vote counts if (currentVote) { const oldField = currentVote === 'up' ? 'upvotes' : 'downvotes'; await supabase.rpc('decrement_vote', { reply_id: replyId, vote_field: oldField }); } const newField = voteType === 'up' ? 'upvotes' : 'downvotes'; await supabase.rpc('increment_vote', { reply_id: replyId, vote_field: newField }); } fetchReplies(); }; // Toggle bookmark const handleBookmark = async () => { if (!user || !id) return; if (isBookmarked) { await supabase .from('forum_bookmarks') .delete() .eq('user_id', user.id) .eq('discussion_id', id); } else { await supabase .from('forum_bookmarks') .insert({ user_id: user.id, discussion_id: id, }); } setIsBookmarked(!isBookmarked); }; // Copy share link const handleShare = () => { navigator.clipboard.writeText(window.location.href); // You could add a toast notification here }; // Report content const handleReport = async (reason: string) => { if (!user || !reportingId) return; await supabase .from('forum_reports') .insert({ user_id: user.id, target_type: reportingId.type, target_id: reportingId.id, reason, }); setShowReportModal(false); setReportingId(null); }; // Render a single reply const renderReply = (reply: Reply, depth = 0) => { const vote = userVotes[reply.id]; const netVotes = (reply.upvotes || 0) - (reply.downvotes || 0); return (
0 ? 'ml-8 border-l-2 border-gray-800 pl-4' : ''}`}>
{/* Vote buttons */}
0 ? 'text-green-400' : netVotes < 0 ? 'text-red-400' : 'text-gray-400' }`}> {netVotes}
{/* Content */}
{reply.author_name.charAt(0).toUpperCase()}
{reply.author_name} {formatDistanceToNow(new Date(reply.created_at), { addSuffix: true })}

{reply.content}

{ setReportingId({ type: 'reply', id: reply.id }); setShowReportModal(true); }} className="text-gray-300 hover:bg-gray-800" > {t('forumTopic.report')}
{/* Nested reply form */} {replyingTo === reply.id && (