"use client" import { useState, useEffect, useRef} from "react" import { useParams, useRouter, usePathname } from "next/navigation" import Link from "next/link" import { Button } from "@/components/ui/button" import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card" import { Badge } from "@/components/ui/badge" import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar" import { Textarea } from "@/components/ui/textarea" import { Separator } from "@/components/ui/separator" import { ArrowLeft, Heart, Share, Copy, Download, User, Calendar, Eye, Cpu, Monitor, MemoryStick, HardDrive, Zap, Fan, Box, Send, BatteryCharging, Server as MotherboardIcon, MoreHorizontal } from "lucide-react" import { supabase } from "@/lib/supabase" import { formatCurrency } from "@/lib/currency" import { useAuth } from "@/contexts/supabase-auth-context" import { getUpgradeRecommendations } from "@/lib/algorithm-service" import { swal } from "@/lib/sweetalert" import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu" import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, } from "@/components/ui/dialog" import { Input } from "@/components/ui/input" import { TrendingUp, Loader2 } from "lucide-react" const categoryMap = { 1: "CPU", 2: "Motherboard", 3: "RAM", 4: "Storage", 5: "GPU", 6: "PSU", 7: "Case", 8: "Cooling", }; const categoryIcons = { CPU: Cpu, Motherboard: MotherboardIcon, RAM: MemoryStick, Storage: HardDrive, GPU: Monitor, PSU: BatteryCharging, Case: Box, Cooling: Fan, } export default function BuildDetailPage() { const params = useParams() const router = useRouter() const pathname = usePathname() const { user } = useAuth() const [build, setBuild] = useState(null) const [creator, setCreator] = useState(null) const [loading, setLoading] = useState(true) const [isLiked, setIsLiked] = useState(false) const [likeCount, setLikeCount] = useState(0); const [viewCount, setViewCount] = useState(0); const [comment, setComment] = useState("") const [totalUserBuilds, setTotalUserBuilds] = useState(0) const [comments, setComments] = useState([]) // empty array initially const [isFollowing, setIsFollowing] = useState(false); const [isSubmittingComment, setIsSubmittingComment] = useState(false); const [followerCount, setFollowerCount] = useState(0); const [isTogglingFollow, setIsTogglingFollow] = useState(false); const [upgradeRecommendations, setUpgradeRecommendations] = useState([]) const [showUpgradeDialog, setShowUpgradeDialog] = useState(false) const [isLoadingUpgrades, setIsLoadingUpgrades] = useState(false) const [upgradeError, setUpgradeError] = useState(null) const [likeOwnPopupOpen, setLikeOwnPopupOpen] = useState(false) const [sharePopupOpen, setSharePopupOpen] = useState(false) const [shareUrl, setShareUrl] = useState("") const [replyTo, setReplyTo] = useState(null); const [likedCommentIds, setLikedCommentIds] = useState([]); const [openReplyIds, setOpenReplyIds] = useState([]); const [editingCommentId, setEditingCommentId] = useState(null); const editInputRef = useRef(null); useEffect(() => { if (typeof window === "undefined") return; if (!comments || comments.length === 0) return; const hash = window.location.hash; // e.g., #comment-123 if (!hash.startsWith("#comment-")) return; const commentId = hash.replace("#comment-", ""); // Wait until the comment element exists const scrollToComment = () => { const element = document.getElementById(`comment-${commentId}`); if (element) { element.scrollIntoView({ behavior: "smooth", block: "center" }); return true; } return false; }; if (!scrollToComment()) { const interval = setInterval(() => { if (scrollToComment()) clearInterval(interval); }, 50); return () => clearInterval(interval); } }, [pathname, comments]); useEffect(() => { const fetchBuild = async () => { if (!params.id) return; const { data: buildData, error: buildError } = await supabase .from("builds") .select(` *, build_components(*, components(*)) `) .eq("build_id", Number(params.id)) .single(); if (buildError || !buildData) { setBuild(null); setLoading(false); return; } // Get total likes from build_likes const { count: totalLikes, error: likesError } = await supabase .from("build_likes") .select("*", { count: "exact", head: true }) .eq("build_id", buildData.build_id); if (!likesError) setLikeCount(totalLikes || 0); // Map build_components into components object const componentsObj = (buildData.build_components || []).reduce((acc: any, comp: any) => { if (!comp.components) return acc; const categoryName = categoryMap[comp.components.category_id]; if (!categoryName) return acc; if (!acc[categoryName]) acc[categoryName] = []; acc[categoryName].push({ ...comp.components, price: comp.components.component_price ?? 0, }); return acc; }, {}); setBuild({ ...buildData, name: buildData.build_name, totalPrice: buildData.total_price ?? 0, components: componentsObj, }); // Fetch creator const { data: userData, error: userError } = await supabase .from("users") .select("*") .eq("user_id", buildData.user_id) .single(); if (!userError && userData) { setCreator(userData); const { count, error: countError } = await supabase .from("builds") .select("*", { count: "exact", head: true }) .eq("user_id", userData.user_id); if (!countError) setTotalUserBuilds(count || 0); // Fetch followers AFTER setting creator if (userData.user_id) { fetchFollowerData(userData.user_id.toString()); } } if (user && creator) { const { data: followData, error: followError } = await supabase .from("followers") .select("*") .eq("user_id", creator.user_id) .eq("follower_user_id", user.user_id) .single(); setIsFollowing(!followError && !!followData); } // check if current user already liked if (user) { const { data: likedData, error: likedError } = await supabase .from("build_likes") .select("*") .eq("build_id", buildData.build_id) .eq("user_id", user.user_id) .single(); setIsLiked(!likedError && !!likedData); } setLoading(false); }; fetchBuild(); fetchComments(); // fetch comments separately // Increase views (unique per user) const incrementViews = async () => { if (!params.id || !user) return; const buildId = Number(params.id); // Check if user already viewed this build const { data: existingView, error: viewError } = await supabase .from("build_views") .select("*") .eq("build_id", buildId) .eq("user_id", user.user_id) .single(); if (existingView) return; // already viewed // Insert new view record (Supabase will auto-generate view_id and created_at) await supabase .from("build_views") .insert({ build_id: buildId, user_id: user.user_id, }); }; incrementViews(); }, [params.id, pathname, user]); // Refetch when params.id, pathname, or user changes useEffect(() => { const fetchBuildViews = async () => { if (!params.id) return; // Count all views for this build const { count: totalViews, error } = await supabase .from("build_views") .select("*", { count: "exact", head: true }) .eq("build_id", Number(params.id)); if (!error) setViewCount(totalViews || 0); }; const incrementAndFetchViews = async () => { if (!params.id || !user) return; const buildId = Number(params.id); const { data: existingView } = await supabase .from("build_views") .select("*") .eq("build_id", buildId) .eq("user_id", user.user_id) .single(); if (!existingView) { await supabase.from("build_views").insert({ build_id: buildId, user_id: user.user_id, }); } // Fetch the updated view count after increment await fetchBuildViews(); }; incrementAndFetchViews(); }, [params.id, user]); useEffect(() => { if (editingCommentId !== null) { editInputRef.current?.focus(); } }, [editingCommentId]); const fetchComments = async () => { if (!params.id) return; try { const { data: commentsData, error: commentsError } = await supabase .from("build_comments") .select(` *, users:user_id ( user_name, avatar_url ), comment_likes(count) `) .eq("build_id", Number(params.id)) .order("created_at", { ascending: false }); // latest first if (commentsError) { console.error("Error fetching comments:", commentsError); setComments([]); return; } if (commentsData) { const mappedComments = commentsData.map((c: any) => ({ id: c.comment_id, user_id: c.user_id, user: c.users?.user_name || "Anonymous", avatar: c.users?.avatar_url ?? null, content: c.content, timestamp: new Date(c.created_at).toLocaleString("en-US", { year: "numeric", month: "short", day: "numeric", hour: "numeric", minute: "numeric", hour12: true }), likes: c.comment_likes?.[0]?.count || 0, reply_to: c.reply_to, })); setComments(mappedComments); // ✅ FETCH LIKED COMMENTS HERE if (user) { const commentIds = commentsData.map(c => c.comment_id); if (commentIds.length > 0) { const { data: likedData } = await supabase .from("comment_likes") .select("comment_id") .eq("user_id", user.user_id) .in("comment_id", commentIds); if (likedData) { setLikedCommentIds(likedData.map(l => l.comment_id)); } } } } else { console.log(`â„šī¸ No comments found for build ${params.id}`); setComments([]); } } catch (err) { console.error("Error in fetchComments:", err); setComments([]); } }; const fetchFollowerData = async (creatorId: string) => { if (!creatorId) { const { count: followersCount, error: countError } = await supabase .from("followers") .select("*", { count: "exact", head: true }) .eq("user_id", creatorId); if (!countError) setFollowerCount(followersCount || 0); setIsFollowing(false); return; } if (!user) { const { count: followersCount, error: countError } = await supabase .from("followers") .select("*", { count: "exact", head: true }) .eq("user_id", creatorId); if (!countError) setFollowerCount(followersCount || 0); setIsFollowing(false); return; } try { const { count: followersCount, error: countError } = await supabase .from("followers") .select("*", { count: "exact", head: true }) .eq("user_id", creatorId); if (!countError) setFollowerCount(followersCount || 0); const { data: followData, error: followError } = await supabase .from("followers") .select("*") .eq("user_id", creatorId) .eq("follower_user_id", user.user_id) .maybeSingle(); setIsFollowing(!!followData); } catch (err) { console.error("Error in fetchFollowerData:", err); } }; const handleAddComment = async () => { if (!comment.trim() || !user) return; setIsSubmittingComment(true); try { const { data: newComment, error } = await supabase .from("build_comments") .insert({ build_id: build.build_id, user_id: user.user_id, content: comment.trim(), reply_to: replyTo, // <-- set reply_to if replying }) .select() .single(); if (!error && newComment) { setComments(prev => [ { ...newComment, id: newComment.comment_id, user_id: user.user_id, // ✅ ADD THIS user: user.user_name, avatar: user.avatar_url, reply_to: replyTo, timestamp: new Date(newComment.created_at).toLocaleString("en-US", { year: "numeric", month: "short", day: "numeric", hour: "numeric", minute: "numeric", hour12: true }), likes: 0 }, ...prev ]); setComment(""); setReplyTo(null); // Send push notification to the build owner (non-blocking) if (build.user_id !== user.user_id) { try { const payload = { userId: build.user_id, notificationType: 'comment', title: `${user.user_name} commented on your build`, body: `${user.user_name} replied: "${comment.trim().substring(0, 50)}${comment.trim().length > 50 ? '...' : ''}"`, url: `/builds/${build.build_id}`, }; console.log('Sending push notification for comment:', payload); const res = await fetch('/api/notifications/send', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload), }); const sendResult = await res.json(); console.log('Push send result for comment:', sendResult); } catch (err) { console.error('Error sending push notification after comment:', err); } } // If this is a reply, also notify the original comment author if (replyTo) { try { // Fetch the original comment to get the author's user_id const { data: originalComment, error: fetchError } = await supabase .from("build_comments") .select("user_id") .eq("comment_id", replyTo) .single(); if (!fetchError && originalComment && originalComment.user_id !== user.user_id) { // Send notification to the person being replied to const payload = { userId: originalComment.user_id, notificationType: 'comment', title: `${user.user_name} replied to your comment`, body: `${user.user_name} replied: "${comment.trim().substring(0, 50)}${comment.trim().length > 50 ? '...' : ''}"`, url: `/builds/${build.build_id}`, }; console.log('Sending push notification for comment reply:', payload); const res = await fetch('/api/notifications/send', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload), }); const sendResult = await res.json(); console.log('Push send result for comment reply:', sendResult); } } catch (err) { console.error('Error sending push notification for reply:', err); } } } } catch (err) { console.error(err); } finally { setIsSubmittingComment(false); } }; const handleDeleteComment = async (commentId: number) => { if (!user) return; try { const { error } = await supabase .from("build_comments") .delete() .eq("comment_id", commentId) .eq("user_id", user.user_id); // security: only delete own if (!error) { setComments(prev => prev.filter(c => c.id !== commentId)); } } catch (err) { console.error(err); } }; const handleUpdateComment = async (commentId: number, newContent: string) => { if (!user) return; const { error } = await supabase .from("build_comments") .update({ content: newContent }) .eq("comment_id", commentId) .eq("user_id", user.user_id); if (!error) { setComments(prev => prev.map(c => c.id === commentId ? { ...c, content: newContent } : c ) ); setEditingCommentId(null); } }; const toggleReplies = (commentId: number) => { setOpenReplyIds(prev => prev.includes(commentId) ? prev.filter(id => id !== commentId) // close : [...prev, commentId] // open ); }; const handleLikeComment = async (commentId: number) => { if (!user) { swal.info("Login required", "Please log in to like comments."); return; } const alreadyLiked = likedCommentIds.includes(commentId); try { if (alreadyLiked) { // đŸ”Ĩ UNLIKE await supabase .from("comment_likes") .delete() .eq("comment_id", commentId) .eq("user_id", user.user_id); await supabase .from("build_comments") .update({ likes: supabase.rpc ? undefined : undefined }); setLikedCommentIds(prev => prev.filter(id => id !== commentId)); setComments(prev => prev.map(c => c.id === commentId ? { ...c, likes: Math.max(0, c.likes - 1) } : c ) ); } else { // â¤ī¸ LIKE await supabase .from("comment_likes") .insert({ comment_id: commentId, user_id: user.user_id, }); setLikedCommentIds(prev => [...prev, commentId]); setComments(prev => prev.map(c => c.id === commentId ? { ...c, likes: c.likes + 1 } : c ) ); // notification const { data: commentData } = await supabase .from("build_comments") .select("user_id") .eq("comment_id", commentId) .single(); if (commentData?.user_id !== user.user_id) { await supabase.from("notifications").insert({ user_id: commentData.user_id, actor_user_id: user.user_id, build_id: build.build_id, comment_id: commentId, type: "comment_like", }); } } } catch (err) { console.error(err); } }; const handleReply = (commentId: number, userName: string) => { if (!user) { swal.info("Login required", "Please log in to reply to comments."); return; } setReplyTo(commentId); setComment(`@${userName} `); const textarea = document.querySelector('textarea') as HTMLTextAreaElement; if (textarea) { textarea.focus(); textarea.setSelectionRange(textarea.value.length, textarea.value.length); } }; const CommentItem = ({ comment, comments, handleReply, handleLikeComment }: any) => { const replies = comments.filter(c => c.reply_to === comment.id); const profileHref = user && comment.user_id === user.user_id ? "/profile" : `/profile/${comment.user_id}`; const isCommentLiked = likedCommentIds.includes(comment.id); const [isEditing, setIsEditing] = useState(false); const [editedContent, setEditedContent] = useState(comment.content); const editInputRef = useRef(null); useEffect(() => { if (isEditing) editInputRef.current?.focus(); }, [isEditing]); return (
{comment.avatar && comment.avatar !== "/placeholder.svg" ? ( ) : ( {comment.user?.charAt(0).toUpperCase() || "?"} )}
{comment.user} {comment.timestamp}
{user && comment.user_id === user.user_id && ( { setEditingCommentId(comment.id); setEditedContent(comment.content); }} > Edit handleDeleteComment(comment.id)} className="text-red-500" > Delete )}
{editingCommentId === comment.id ? (
setEditedContent(e.target.value)} className="h-8 text-sm" />
) : (

{comment.content}

)}
{/* Toggle replies button */} {replies.length > 0 && (
)} {/* Nested replies */} {openReplyIds.includes(comment.id) && replies.length > 0 && (
{replies.map(reply => ( ))}
)}
); }; const formatDate = (isoDate: string) => { const date = new Date(isoDate); return date.toLocaleDateString("en-US", { year: "numeric", month: "short", day: "numeric", }); }; const toggleLike = async () => { if (!user) return; // ❌ Prevent liking your own build if (creator?.user_id === user.user_id) { setLikeOwnPopupOpen(true) return } if (isLiked) { const { error: deleteLikeError } = await supabase .from("build_likes") .delete() .eq("build_id", build.build_id) .eq("user_id", user.user_id); if (!deleteLikeError) { setIsLiked(false); setLikeCount((prev) => Math.max(0, prev - 1)); await supabase .from("notifications") .delete() .eq("type", "like") .eq("user_id", creator.user_id) .eq("actor_user_id", user.user_id) .eq("build_id", build.build_id); } } else { const { error: insertLikeError } = await supabase .from("build_likes") .insert({ build_id: build.build_id, user_id: user.user_id }); if (!insertLikeError) { setIsLiked(true); setLikeCount((prev) => prev + 1); await supabase.from("notifications").insert({ user_id: creator.user_id, actor_user_id: user.user_id, build_id: build.build_id, type: "like", }); // Send push notification to the build owner (non-blocking) try { const payload = { userId: creator.user_id, notificationType: 'buildLike', title: `${user.user_name} liked your build`, body: `${user.user_name} liked ${build.build_name}`, url: `/builds/${build.build_id}`, }; console.log('Sending push notification payload (build detail):', payload); fetch('/api/notifications/send', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload), }) .then(res => res.json().then(r => console.log('Push send result (build detail):', r)).catch(() => {})) .catch(err => console.error('Error sending push (build detail):', err)); } catch (err) { console.error('Error preparing push send (build detail):', err); } } } }; const toggleFollow = async () => { if (!user) { swal.info("Login required", "Please log in to follow users."); return; } if (!creator) { swal.error("Error", "Creator information not available."); return; } if (user.user_id === creator.user_id) { swal.warning("Cannot follow", "You cannot follow yourself."); return; } setIsTogglingFollow(true); try { if (isFollowing) { const { error: deleteError } = await supabase .from("followers") .delete() .eq("user_id", creator.user_id) .eq("follower_user_id", user.user_id); if (deleteError) { console.error("Error unfollowing:", deleteError); swal.error("Unfollow failed", "Please try again."); return; } setIsFollowing(false); setFollowerCount((prev) => Math.max(0, prev - 1)); // Optionally: delete the follow notification if you want await supabase.from("notifications") .delete() .eq("type", "follow") .eq("user_id", creator.user_id) .eq("actor_user_id", user.user_id); } else { // Follow: Insert the follow relationship const { error: insertError } = await supabase .from("followers") .insert({ user_id: creator.user_id, follower_user_id: user.user_id, }); if (insertError) { console.error("Error following:", insertError); if (insertError.code === "23505") { swal.info("Already following", "You are already following this user."); setIsFollowing(true); } else { swal.error("Follow failed", "Please try again."); } return; } console.log("✅ Successfully followed user"); setIsFollowing(true); setFollowerCount((prev) => prev + 1); // đŸŸĸ Insert notification for the follow const { error: notifError } = await supabase .from("notifications") .insert({ user_id: creator.user_id, // recipient of the notification actor_user_id: user.user_id, // the follower type: "follow", }); if (notifError) { console.error("Error creating follow notification:", notifError); } else { console.log("✅ Follow notification created"); // Send push notification to the followed user (non-blocking) try { const payload = { userId: creator.user_id, notificationType: 'followers', title: `${user.user_name} started following you`, body: `${user.user_name} is now following you`, url: `/profile/${user.user_id}`, }; console.log('Sending push notification for new follower:', payload); const res = await fetch('/api/notifications/send', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload), }); const sendResult = await res.json(); console.log('Push send result for follower:', sendResult); } catch (err) { console.error('Error sending push notification for follower:', err); } } } // Refresh follow status and count to ensure consistency await fetchFollowerData(creator.user_id); } catch (err) { console.error("Error in toggleFollow:", err); swal.error("Error", "An error occurred. Please try again."); } finally { setIsTogglingFollow(false); } }; const handleGetUpgradeRecommendations = async () => { if (!build || !build.components) { setUpgradeError("Build data not available") setTimeout(() => setUpgradeError(null), 5000) return } setIsLoadingUpgrades(true) setUpgradeError(null) setUpgradeRecommendations([]) try { // Convert build components to upgrade API format const currentBuild: any[] = [] Object.entries(build.components).forEach(([categoryName, components]: [string, any]) => { if (Array.isArray(components) && components.length > 0) { const comp = components[0] // Take first component of each category const componentId = Number(String(comp.component_id).replace(/\D/g, '')) || 0 currentBuild.push({ component_id: componentId, component_name: comp.component_name || comp.name || 'Unknown', component_price: comp.component_price || comp.price || 0, category_name: categoryName, }) } }) if (currentBuild.length === 0) { setUpgradeError("No components found in this build") setTimeout(() => setUpgradeError(null), 5000) return } console.log("Requesting upgrade recommendations for build:", currentBuild) const recommendations = await getUpgradeRecommendations(currentBuild) console.log("Received upgrade recommendations:", recommendations) if (!recommendations || recommendations.length === 0) { setUpgradeError("No upgrade recommendations available for this build") setTimeout(() => setUpgradeError(null), 5000) return } setUpgradeRecommendations(recommendations) setShowUpgradeDialog(true) } catch (error: any) { console.error("Error getting upgrade recommendations:", error) setUpgradeError(error.message || "Failed to get upgrade recommendations") setTimeout(() => setUpgradeError(null), 5000) } finally { setIsLoadingUpgrades(false) } }; if (loading) { return (

Loading builds...

) } if (!build) { return (

Build not found

) } const totalComponents = Object.values(build.components || {}).flat().length; const profileHref = user && creator.user_id === user.user_id ? "/profile" : `/profile/${creator.user_id}`; return (
{/* Main Content */}
{/* Build Header */}
{build.name}
{creator?.avatar_url ? ( ) : ( {creator?.user_name ? creator?.user_name?.charAt(0).toUpperCase() : "?"} )}

{creator?.user_name || "Unknown"}

{formatDate(creator?.created_at)}
{viewCount} views

{build.description}

{formatCurrency(build.totalPrice)}
{/* Tags */}
{(build.tags || []).map((tag: string) => ( {tag} ))}
{/* Upgrade Suggestions Button */}
{upgradeError && (

{upgradeError}

)}
{/* Actions */}
{/* Components List */} Components ({totalComponents}/8) Complete parts list for this build {Object.entries(build.components || {}).map(([category, comps]) => { const Icon = categoryIcons[category] || Box; return comps.map((component, idx) => (
{component.image && component.image !== '/placeholder.svg' ? ( {component.component_name { (e.target as HTMLImageElement).src = '/placeholder.svg' }} /> ) : ( )}

{category === "psu" ? "Power Supply" : category}

{component.component_brand ? `${component.component_brand} ` : ''}{component.component_name || component.name || 'Unknown Component'}

{(component.rating || component.reviews) && (
{component.rating && ★ {component.rating}} {component.reviews && {component.reviews} reviews}
)}

{formatCurrency(component.price || component.component_price || 0)}

)); })}
{/* Comments Section */} Comments ({comments.length}) {/* Add Comment */} {user && (