'use client'; import React, { useState, useEffect, useRef, useCallback } from 'react'; import Link from 'next/link'; import { useParams } from 'next/navigation'; import Header from '@/components/Header'; import Footer from '@/components/Footer'; import { createClient } from '@/lib/supabase/client'; interface ForumThread { id: string; title: string; body: string; author_name: string; status: string; reply_count: number; view_count: number; upvotes: number; downvotes: number; vote_score: number; created_at: string; forum_categories: { id: string; name: string; slug: string }; } interface ForumReply { id: string; body: string; author_name: string; is_ai_response: boolean; upvotes: number; downvotes: number; vote_score: number; created_at: string; } function timeAgo(dateStr: string): string { const diff = Date.now() - new Date(dateStr).getTime(); const mins = Math.floor(diff / 60000); if (mins < 2) return 'just now'; if (mins < 60) return `${mins}m ago`; const hrs = Math.floor(mins / 60); if (hrs < 24) return `${hrs}h ago`; const days = Math.floor(hrs / 24); if (days < 30) return `${days}d ago`; return new Date(dateStr).toLocaleDateString(); } function Avatar({ name, isAI }: { name: string; isAI: boolean }) { if (isAI) { return (
AI
); } const initials = name.split(' ').map(n => n[0]).join('').toUpperCase().slice(0, 2); const colors = ['#1e3a5f', '#1a3a2a', '#3a1a1a', '#2a1a3a', '#1a2a3a']; const colorIdx = name.charCodeAt(0) % colors.length; return (
{initials || '?'}
); } interface VoteButtonsProps { targetType: 'thread' | 'reply'; targetId: string; upvotes: number; downvotes: number; voteScore: number; isAI?: boolean; isAuthenticated: boolean; } function VoteButtons({ targetType, targetId, upvotes, downvotes, voteScore, isAI, isAuthenticated }: VoteButtonsProps) { const [currentVote, setCurrentVote] = useState<1 | -1 | null>(null); const [counts, setCounts] = useState({ upvotes, downvotes, vote_score: voteScore }); const [loading, setLoading] = useState(false); const [fetched, setFetched] = useState(false); useEffect(() => { if (!isAuthenticated || fetched) return; fetch(`/api/forum/votes?target_type=${targetType}&target_id=${targetId}`) .then(r => r.json()) .then(d => { setCurrentVote(d.vote ?? null); setFetched(true); }) .catch(() => setFetched(true)); }, [isAuthenticated, targetType, targetId, fetched]); const handleVote = async (value: 1 | -1) => { if (!isAuthenticated || loading || isAI) return; setLoading(true); try { const res = await fetch('/api/forum/votes', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ target_type: targetType, target_id: targetId, vote_value: value }), }); const data = await res.json(); if (!data.error) { setCurrentVote(data.vote); setCounts({ upvotes: data.upvotes, downvotes: data.downvotes, vote_score: data.vote_score }); } } catch { // silent } finally { setLoading(false); } }; const scoreColor = counts.vote_score > 0 ? 'text-[#3b82f6]' : counts.vote_score < 0 ? 'text-red-400' : 'text-[#666]'; return (
{/* Upvote */} {/* Score */} {counts.vote_score > 0 ? `+${counts.vote_score}` : counts.vote_score} {/* Downvote */} {!isAuthenticated && ( Sign in to vote )}
); } export default function ForumThreadPage() { const params = useParams(); const threadId = params?.id as string; const supabase = createClient(); const [thread, setThread] = useState(null); const [replies, setReplies] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [isAuthenticated, setIsAuthenticated] = useState(false); // Reply form const [replyBody, setReplyBody] = useState(''); const [replyAuthor, setReplyAuthor] = useState(''); const [submitting, setSubmitting] = useState(false); // AI assistant const [aiQuestion, setAiQuestion] = useState(''); const [aiLoading, setAiLoading] = useState(false); const [aiError, setAiError] = useState(null); const repliesEndRef = useRef(null); useEffect(() => { supabase.auth.getUser().then(({ data: { user } }) => { setIsAuthenticated(!!user); }); }, []); useEffect(() => { if (!threadId) return; fetch(`/api/forum/threads/${threadId}`) .then(r => r.json()) .then(d => { if (d.error) throw new Error(d.error); setThread(d.thread); setReplies(d.replies || []); setLoading(false); }) .catch(e => { setError(e.message || 'Failed to load thread.'); setLoading(false); }); }, [threadId]); const handleReply = async (e: React.FormEvent) => { e.preventDefault(); if (!replyBody.trim() || !threadId) return; setSubmitting(true); try { const res = await fetch('/api/forum/replies', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ thread_id: threadId, body: replyBody, author_name: replyAuthor || 'Anonymous', }), }); const data = await res.json(); if (data.reply) { setReplies(prev => [...prev, data.reply]); setReplyBody(''); setReplyAuthor(''); setTimeout(() => repliesEndRef.current?.scrollIntoView({ behavior: 'smooth' }), 100); } } catch { // silent } finally { setSubmitting(false); } }; const handleAiAssist = async (e: React.FormEvent) => { e.preventDefault(); if (!aiQuestion.trim() || !thread) return; setAiLoading(true); setAiError(null); try { const res = await fetch('/api/forum/ai-assist', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ thread_id: threadId, question: aiQuestion, thread_title: thread.title, thread_body: thread.body, replies: replies.slice(-5), }), }); const data = await res.json(); if (data.error) throw new Error(data.error); if (data.reply) { setReplies(prev => [...prev, data.reply]); setAiQuestion(''); setTimeout(() => repliesEndRef.current?.scrollIntoView({ behavior: 'smooth' }), 100); } } catch (err: any) { setAiError(err.message || 'AI assistant unavailable. Please try again.'); } finally { setAiLoading(false); } }; if (loading) { return ( <>