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) } return ( {/* ── Trigger button ──────────────────────────────────────────── */} { if (myReaction) { // Quick-toggle: remove own reaction toggle(myReaction) } else { // Quick-like with thumbs_up toggle('thumbs_up') } }} className={[ '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(' ')} aria-label={myReaction ? `You reacted with ${myReactionData?.label}. Click to remove.` : 'React to this comment'} > {myReaction ? ( {myReactionData?.emoji} ) : ( )} {myReaction ? myReactionData?.label : 'React'} {/* ── Floating picker ─────────────────────────────────────── */} {pickerOpen && ( { clearTimeout(hoverTimeout.current) }} onMouseLeave={onMouseLeave} > {REACTIONS.map((r, i) => { const isActive = totals[r.slug]?.mine return ( toggle(r.slug)} disabled={loading === r.slug} aria-label={`${r.label}${isActive ? ' (selected)' : ''}`} className={[ 'group/reaction relative flex items-center justify-center w-9 h-9 rounded-full transition-all duration-200', 'hover:bg-white/[0.08] hover:scale-125 hover:-translate-y-1', 'focus:outline-none focus-visible:ring-2 focus-visible:ring-accent/50', 'disabled:opacity-50', isActive ? 'bg-white/[0.1] scale-110' : '', ].join(' ')} style={{ animationDelay: `${i * 30}ms` }} title={r.label} > {r.emoji} {/* Tooltip */} {r.label} ) })} )} {/* ── Summary: stacked emoji + count ───────────────────────── */} {totalCount > 0 && ( setPickerOpen(v => !v)} className="inline-flex items-center gap-1.5 rounded-full px-1.5 py-0.5 transition-colors hover:bg-white/[0.06] group/summary" aria-label={`${totalCount} reaction${totalCount !== 1 ? 's' : ''}`} > {/* Stacked emoji circles (Facebook-style, max 3) */} {activeReactions.slice(0, 3).map(([slug, info], i) => ( {info.emoji} ))} {totalCount} )} ) }