import React, { useCallback, useEffect, useRef, useState } from 'react' import axios from 'axios' /* ── Reaction definitions ────────────────────────────────────────────────── */ const REACTIONS = [ { slug: 'thumbs_up', emoji: '👍', label: 'Like' }, { slug: 'heart', emoji: '❤️', label: 'Love' }, { slug: 'fire', emoji: '🔥', label: 'Fire' }, { slug: 'laugh', emoji: '😂', label: 'Haha' }, { slug: 'clap', emoji: '👏', label: 'Clap' }, { slug: 'wow', emoji: '😮', label: 'Wow' }, ] /* ── Small heart outline icon for the trigger ─────────────────────────────── */ function HeartOutlineIcon({ className }) { return ( ) } /** * Facebook-style reaction bar. * * - Compact trigger button (heart icon or the user's reaction) * - Floating picker that appears on hover/click with scale animation * - Summary row showing unique reaction emoji + total count * * Props: * entityType 'artwork' | 'comment' * entityId number * initialTotals Record * isLoggedIn boolean */ export default function ReactionBar({ entityType, entityId, initialTotals = {}, isLoggedIn = false }) { const [totals, setTotals] = useState(initialTotals) const [loading, setLoading] = useState(null) const [pickerOpen, setPickerOpen] = useState(false) const containerRef = useRef(null) const hoverTimeout = useRef(null) const endpoint = entityType === 'artwork' ? `/api/artworks/${entityId}/reactions` : `/api/comments/${entityId}/reactions` // Close picker when clicking outside useEffect(() => { if (!pickerOpen) return const handler = (e) => { if (containerRef.current && !containerRef.current.contains(e.target)) { setPickerOpen(false) } } document.addEventListener('mousedown', handler) return () => document.removeEventListener('mousedown', handler) }, [pickerOpen]) const toggle = useCallback( async (slug) => { if (!isLoggedIn) { window.location.href = '/login' return } if (loading) return setLoading(slug) setPickerOpen(false) // Optimistic update setTotals((prev) => { const entry = prev[slug] ?? { count: 0, mine: false, emoji: REACTIONS.find(r => r.slug === slug)?.emoji, label: REACTIONS.find(r => r.slug === slug)?.label } return { ...prev, [slug]: { ...entry, count: entry.mine ? Math.max(0, entry.count - 1) : entry.count + 1, mine: !entry.mine, }, } }) try { const { data } = await axios.post(endpoint, { reaction: slug }) setTotals(data.totals) } catch { setTotals((prev) => { const entry = prev[slug] ?? { count: 0, mine: false } return { ...prev, [slug]: { ...entry, count: entry.mine ? Math.max(0, entry.count - 1) : entry.count + 1, mine: !entry.mine, }, } }) } finally { setLoading(null) } }, [endpoint, isLoggedIn, loading], ) // Compute summary data const entries = Object.entries(totals) const activeReactions = entries.filter(([, info]) => info.count > 0) const totalCount = activeReactions.reduce((sum, [, info]) => sum + info.count, 0) const myReaction = entries.find(([, info]) => info.mine)?.[0] ?? null const myReactionData = myReaction ? REACTIONS.find(r => r.slug === myReaction) : null // Hover handlers for desktop — open on hover with a small delay const onMouseEnter = () => { clearTimeout(hoverTimeout.current) hoverTimeout.current = setTimeout(() => setPickerOpen(true), 200) } const onMouseLeave = () => { clearTimeout(hoverTimeout.current) hoverTimeout.current = setTimeout(() => setPickerOpen(false), 400) } const isArtworkVariant = entityType === 'artwork' const triggerClassName = isArtworkVariant ? [ 'inline-flex items-center gap-2.5 rounded-full border px-4 py-2.5 text-sm font-semibold transition-all duration-200', 'focus:outline-none focus-visible:ring-2 focus-visible:ring-accent/50', myReaction ? 'border-accent/35 bg-accent/12 text-accent shadow-[0_12px_30px_rgba(245,158,11,0.14)] hover:bg-accent/18' : 'border-white/[0.12] bg-white/[0.06] text-white/75 hover:border-accent/30 hover:bg-white/[0.1] hover:text-white', ].join(' ') : [ 'inline-flex items-center gap-1.5 rounded-full px-2.5 py-1 text-xs font-medium transition-all duration-200', 'focus:outline-none focus-visible:ring-2 focus-visible:ring-accent/50', myReaction ? 'text-accent' : 'text-white/40 hover:text-white/70', ].join(' ') const summaryClassName = isArtworkVariant ? 'inline-flex items-center gap-2 rounded-full border border-white/[0.1] bg-white/[0.05] px-3 py-1.5 transition-colors hover:border-white/[0.16] hover:bg-white/[0.08] group/summary' : 'inline-flex items-center gap-1.5 rounded-full px-1.5 py-0.5 transition-colors hover:bg-white/[0.06] group/summary' return (
{/* ── Trigger button ──────────────────────────────────────────── */}
{/* ── Floating picker ─────────────────────────────────────── */} {pickerOpen && (
{ clearTimeout(hoverTimeout.current) }} onMouseLeave={onMouseLeave} >
{REACTIONS.map((r, i) => { const isActive = totals[r.slug]?.mine return ( ) })}
)}
{/* ── Summary: stacked emoji + count ───────────────────────── */} {totalCount > 0 && ( )}
) }