import React, { useCallback, useEffect, useRef, useState } from 'react' import axios from 'axios' import CommentForm from '../comments/CommentForm' import ReactionBar from '../comments/ReactionBar' import LevelBadge from '../xp/LevelBadge' import { isFlood } from '../../utils/emojiFlood' // ── Helpers ─────────────────────────────────────────────────────────────────── function timeAgo(dateStr) { if (!dateStr) return '' const date = new Date(dateStr) const seconds = Math.floor((Date.now() - date.getTime()) / 1000) if (seconds < 60) return 'just now' const minutes = Math.floor(seconds / 60) if (minutes < 60) return `${minutes}m ago` const hours = Math.floor(minutes / 60) if (hours < 24) return `${hours}h ago` const days = Math.floor(hours / 24) if (days < 365) return `${days}d ago` return date.toLocaleDateString(undefined, { year: 'numeric', month: 'short', day: 'numeric' }) } /* ── Icons ─────────────────────────────────────────────────────────────────── */ function ReplyIcon() { return ( ) } function ChatBubbleIcon() { return ( ) } function ChevronDownIcon({ className }) { return ( ) } /* ── Avatar ─────────────────────────────────────────────────────────────────── */ function Avatar({ user, size = 36 }) { if (user?.avatar_url) { return ( {user.name { e.currentTarget.onerror = null e.currentTarget.src = 'https://files.skinbase.org/default/avatar_default.webp' }} /> ) } const initials = (user?.name || user?.username || '?').slice(0, 1).toUpperCase() return ( {initials} ) } // ── Reply item (nested under a parent) ──────────────────────────────────────── function ReplyItem({ reply, parentId, artworkId, isLoggedIn, onReplyPosted, depth = 1 }) { const user = reply.user const html = reply.rendered_content ?? null const plain = reply.content ?? reply.raw_content ?? '' const profileLabel = user?.display || user?.username || user?.name || 'Member' const replies = reply.replies || [] const [showReplyForm, setShowReplyForm] = useState(false) const [showAllReplies, setShowAllReplies] = useState(false) const [reactionTotals, setReactionTotals] = useState(reply.reactions ?? {}) useEffect(() => { if (reply.reactions || !reply.id) return axios .get(`/api/comments/${reply.id}/reactions`) .then(({ data }) => setReactionTotals(data.totals ?? {})) .catch(() => {}) }, [reply.id, reply.reactions]) const handleReplyPosted = useCallback((newReply) => { // Reply posts under THIS reply's id as parent onReplyPosted?.(reply.id, newReply) setShowReplyForm(false) setShowAllReplies(true) }, [reply.id, onReplyPosted]) // Show first 2 nested replies, expand to show all const visibleReplies = showAllReplies ? replies : replies.slice(0, 2) const hiddenReplyCount = replies.length - 2 // Shrink avatar at deeper levels const avatarSize = depth >= 3 ? 22 : 28 return (
  • {user?.profile_url ? ( ) : ( )}
    {user?.profile_url ? ( {profileLabel} ) : ( {profileLabel} )}
    {html ? (
    ) : (

    {plain}

    )} {/* Actions — Reply + React inline */}
    {isLoggedIn && ( )}
    {/* Inline reply form */} {showReplyForm && (
    setShowReplyForm(false)} onPosted={handleReplyPosted} isLoggedIn={isLoggedIn} compact />
    )} {/* Nested replies (tree) */} {replies.length > 0 && (
      = 3 ? 'border-white/[0.03]' : 'border-white/[0.05]'}`}> {visibleReplies.map((child) => ( ))}
    {!showAllReplies && hiddenReplyCount > 0 && ( )}
    )}
  • ) } // ── Single comment (top-level) ──────────────────────────────────────────────── function CommentItem({ comment, isLoggedIn, artworkId, onReplyPosted }) { const user = comment.user const html = comment.rendered_content ?? null const plain = comment.content ?? comment.raw_content ?? '' const profileLabel = user?.display || user?.username || user?.name || 'Member' const replies = comment.replies || [] const flood = isFlood(plain) const [expanded, setExpanded] = useState(!flood) const [showReplyForm, setShowReplyForm] = useState(false) const [showAllReplies, setShowAllReplies] = useState(false) const [reactionTotals, setReactionTotals] = useState(comment.reactions ?? {}) useEffect(() => { if (comment.reactions || !comment.id) return axios .get(`/api/comments/${comment.id}/reactions`) .then(({ data }) => setReactionTotals(data.totals ?? {})) .catch(() => {}) }, [comment.id, comment.reactions]) const handleReplyPosted = useCallback((newReply) => { onReplyPosted?.(comment.id, newReply) setShowReplyForm(false) setShowAllReplies(true) }, [comment.id, onReplyPosted]) // Show first 2 replies by default, expand to show all const visibleReplies = showAllReplies ? replies : replies.slice(0, 2) const hiddenReplyCount = replies.length - 2 return (
  • {/* Avatar */} {user?.profile_url ? ( ) : ( )} {/* Content */}
    {/* Header */}
    {user?.profile_url ? ( {profileLabel} ) : ( {profileLabel} )}
    {/* Body */}
    {html ? (
    ) : (

    {plain}

    )} {flood && !expanded && ( {flood && ( )} {/* Actions */}
    {isLoggedIn && ( )}
    {/* ── Replies thread ───────────────────────────────────────────────── */} {(replies.length > 0 || showReplyForm) && (
    {replies.length > 0 && ( <>
      {visibleReplies.map((reply) => ( ))}
    {!showAllReplies && hiddenReplyCount > 0 && ( )} )} {/* Inline reply form */} {showReplyForm && (
    0 ? 'mt-3 pl-4 border-l-2 border-accent/20' : ''}> setShowReplyForm(false)} onPosted={handleReplyPosted} isLoggedIn={isLoggedIn} compact />
    )}
    )}
  • ) } // ── Skeleton ────────────────────────────────────────────────────────────────── function Skeleton() { return (
    {Array.from({ length: 3 }).map((_, i) => (
    ))}
    ) } // ── Main export ─────────────────────────────────────────────────────────────── export default function ArtworkComments({ artworkId, comments: initialComments = [], isLoggedIn = false, loginUrl = '/login', }) { const [comments, setComments] = useState(initialComments) const [loading, setLoading] = useState(false) const [page, setPage] = useState(1) const [lastPage, setLastPage] = useState(1) const [total, setTotal] = useState(initialComments.length) const initialized = useRef(false) const loadComments = useCallback( async (p = 1) => { if (!artworkId) return setLoading(true) try { const { data } = await axios.get(`/api/artworks/${artworkId}/comments?page=${p}`) if (p === 1) { setComments(data.data ?? []) } else { setComments((prev) => [...prev, ...(data.data ?? [])]) } setPage(data.meta?.current_page ?? p) setLastPage(data.meta?.last_page ?? 1) setTotal(data.meta?.total ?? 0) } catch { // keep existing } finally { setLoading(false) } }, [artworkId], ) useEffect(() => { if (initialized.current) return initialized.current = true if (artworkId && initialComments.length === 0) { loadComments(1) } else { setTotal(initialComments.length) } }, [artworkId, initialComments.length, loadComments]) // New top-level comment posted const handlePosted = useCallback((newComment) => { // Ensure it has a replies array const comment = { ...newComment, replies: newComment.replies || [] } setComments((prev) => [comment, ...prev]) setTotal((t) => t + 1) }, []) // Reply posted under a parent comment (works at any nesting depth) const handleReplyPosted = useCallback((parentId, newReply) => { // Recursively find the parent node and append the reply const insertReply = (nodes) => nodes.map((c) => { if (c.id === parentId) { return { ...c, replies: [...(c.replies || []), { ...newReply, replies: [] }] } } if (c.replies?.length) { return { ...c, replies: insertReply(c.replies) } } return c }) setComments((prev) => insertReply(prev)) setTotal((t) => t + 1) }, []) return (
    {/* Section header */}

    Comments

    {total > 0 && ( {total} )}
    {/* Comment list */} {loading && comments.length === 0 ? ( ) : comments.length === 0 ? (

    No comments yet

    Be the first to share your thoughts.

    ) : ( <>
      {comments.map((comment) => ( ))}
    {page < lastPage && (
    )} )} {/* Comment form — after all comments */} {artworkId && ( )}
    ) }