import React, { useEffect, useMemo, useState } from 'react' import ArtworkCard from './ArtworkCard' function getCsrfToken() { if (typeof document === 'undefined') return '' return document.querySelector('meta[name="csrf-token"]')?.getAttribute('content') || '' } async function revokeDismissSignal(entry) { const item = entry?.item || null const kind = entry?.kind || null if (!item || !kind) { throw new Error('missing_dismiss_entry') } const endpoint = kind === 'dislike-tag' ? item.dislike_tag_endpoint || item?.negative_feedback?.dislike_tag_endpoint : item.hide_artwork_endpoint || item?.negative_feedback?.hide_artwork_endpoint if (!endpoint) { throw new Error('missing_revoke_endpoint') } const payload = kind === 'dislike-tag' ? { tag_id: item?.primary_tag?.id ? Number(item.primary_tag.id) : undefined, tag_slug: item?.primary_tag?.slug || item?.primary_tag?.name || undefined, artwork_id: Number(item.id), algo_version: item?.recommendation_algo_version || item?.algo_version || undefined, meta: { gallery_type: item?.recommendation_surface || item?.gallery_type || 'recommendation-surface', reason: item?.recommendation_reason || null, interaction_origin: 'artwork-gallery-undo-dislike-tag', }, } : { artwork_id: Number(item.id), algo_version: item?.recommendation_algo_version || item?.algo_version || undefined, meta: { gallery_type: item?.recommendation_surface || item?.gallery_type || 'recommendation-surface', reason: item?.recommendation_reason || null, interaction_origin: 'artwork-gallery-undo-hide', }, } const response = await fetch(endpoint, { method: 'DELETE', credentials: 'same-origin', headers: { 'Content-Type': 'application/json', 'X-CSRF-TOKEN': getCsrfToken(), 'X-Requested-With': 'XMLHttpRequest', }, body: JSON.stringify(payload), }) if (!response.ok) { throw new Error('revoke_request_failed') } return response.json().catch(() => null) } function cx(...parts) { return parts.filter(Boolean).join(' ') } function getArtworkKey(item, index) { if (item?.id) return item.id if (item?.title || item?.name || item?.author) { return `${item.title || item.name || 'artwork'}-${item.author || item.author_name || item.uname || 'artist'}-${index}` } return `artwork-${index}` } function DismissNotice({ notice, onUndo, onClose }) { if (!notice) return null return (

Discovery Feedback

{notice.message}

) } export default function ArtworkGallery({ items, layout = 'grid', compact = false, showStats = true, showAuthor = true, className = '', cardClassName = '', limit, containerProps = {}, resolveCardProps, children, }) { if (!Array.isArray(items) || items.length === 0) return null const visibleItems = typeof limit === 'number' ? items.slice(0, limit) : items const [dismissedEntries, setDismissedEntries] = useState([]) const [dismissNotice, setDismissNotice] = useState(null) const visibleArtworkItems = useMemo( () => visibleItems.filter((item) => !dismissedEntries.some((entry) => entry.item?.id === item?.id)), [dismissedEntries, visibleItems] ) const baseClassName = layout === 'masonry' ? 'grid gap-6' : 'grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 2xl:grid-cols-5' useEffect(() => { if (!dismissNotice) { return undefined } const timeoutId = window.setTimeout(() => { setDismissNotice(null) }, 3200) return () => { window.clearTimeout(timeoutId) } }, [dismissNotice]) function handleDismissed(item, kind) { if (!item?.id) return setDismissedEntries((current) => { const next = current.filter((entry) => entry.item?.id !== item.id) next.push({ item, kind }) return next }) setDismissNotice({ itemId: item.id, busy: false, message: kind === 'dislike-tag' ? `We will show less content like #${item?.primary_tag?.slug || item?.primary_tag?.name || 'this tag'}.` : 'Artwork hidden from this recommendation view.', }) } async function handleUndoDismiss() { if (!dismissNotice?.itemId) { setDismissNotice(null) return } const entry = dismissedEntries.find((current) => current.item?.id === dismissNotice.itemId) if (!entry) { setDismissNotice(null) return } setDismissNotice((current) => current ? { ...current, busy: true } : current) try { await revokeDismissSignal(entry) } catch { setDismissNotice({ itemId: entry.item.id, busy: false, message: 'Undo failed. The feedback signal is still active.', }) return } setDismissedEntries((current) => current.filter((entry) => entry.item?.id !== dismissNotice.itemId)) setDismissNotice(null) } return ( <>
{visibleArtworkItems.map((item, index) => { const cardProps = resolveCardProps?.(item, index) || {} const { className: resolvedClassName = '', ...restCardProps } = cardProps return ( ) })} {children}
setDismissNotice(null)} /> ) }