import React, { useCallback, useEffect, useRef, useState } from 'react' import axios from 'axios' import CommentForm from '../comments/CommentForm' import ReactionBar from '../comments/ReactionBar' 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' }) } function Avatar({ user, size = 36 }) { if (user?.avatar_url) { return ( {user.name { e.currentTarget.onerror = null e.currentTarget.src = 'https://files.skinbase.org/avatars/default.webp' }} /> ) } const initials = (user?.name || user?.username || '?').slice(0, 1).toUpperCase() return ( {initials} ) } // ── Single comment ──────────────────────────────────────────────────────────── function CommentItem({ comment, isLoggedIn }) { const user = comment.user const html = comment.rendered_content ?? null const plain = comment.content ?? '' // Emoji-flood collapse: long runs of repeated emoji get a show-more toggle. const flood = isFlood(plain) const [expanded, setExpanded] = useState(!flood) // Build initial reaction totals (empty if not provided by server) const [reactionTotals, setReactionTotals] = useState(comment.reactions ?? {}) // Load reactions lazily if not provided useEffect(() => { if (comment.reactions || !comment.id) return axios .get(`/api/comments/${comment.id}/reactions`) .then(({ data }) => setReactionTotals(data.totals ?? {})) .catch(() => {}) }, [comment.id, comment.reactions]) return (
  • {/* Avatar */} {user?.profile_url ? ( ) : ( )} {/* Content */}
    {/* Header */}
    {user?.profile_url ? ( {user.display || user.username || user.name || 'Member'} ) : ( {user?.display || user?.username || user?.name || 'Member'} )}
    {/* Body — use rendered_content (safe HTML) when available, else plain text */} {/* Flood-collapse wrapper: clips height when content is a repeated-emoji flood */}
    {html ? (
    ) : (

    {plain}

    )} {/* Gradient fade at the bottom while collapsed */} {flood && !expanded && ( {/* Flood expand / collapse toggle */} {flood && ( )} {/* Reactions */} {Object.keys(reactionTotals).length > 0 && ( )}
  • ) } // ── Skeleton ────────────────────────────────────────────────────────────────── function Skeleton() { return (
    {Array.from({ length: 3 }).map((_, i) => (
    ))}
    ) } // ── Main export ─────────────────────────────────────────────────────────────── /** * ArtworkComments * * Can operate in two modes: * 1. Static: pass `comments` array from Inertia page props (legacy / SSR) * 2. Dynamic: pass `artworkId` to load + post comments via the API * * Props: * artworkId number Used for API calls * comments array SSR initial comments (optional) * isLoggedIn boolean * loginUrl string */ 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) // Load comments from API 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 data on error } finally { setLoading(false) } }, [artworkId], ) // On mount, load if artworkId provided and no SSR comments given useEffect(() => { if (initialized.current) return initialized.current = true if (artworkId && initialComments.length === 0) { loadComments(1) } else { setTotal(initialComments.length) } }, [artworkId, initialComments.length, loadComments]) const handlePosted = useCallback((newComment) => { setComments((prev) => [newComment, ...prev]) setTotal((t) => t + 1) }, []) return (

    Comments{' '} {total > 0 && ( ({total}) )}

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

    No comments yet. Be the first!

    ) : ( <>
      {comments.map((comment) => ( ))}
    {/* Load more */} {page < lastPage && (
    )} )}
    ) }