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)} />
>
)
}