{item.title}
{item.publisher} {item.category ? `• ${item.category}` : ''} {item.content_type ? `• ${item.content_type}` : ''}
import React, { useMemo, useState } from 'react' import { Head, usePage } from '@inertiajs/react' import ArtworkViewer from '../../components/viewer/ArtworkViewer' import NovaSelect from '../../components/ui/NovaSelect' function requestJson(url, { method = 'GET', body } = {}) { return fetch(url, { method, credentials: 'same-origin', headers: { Accept: 'application/json', 'Content-Type': 'application/json', 'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]')?.getAttribute('content') || '', 'X-Requested-With': 'XMLHttpRequest', }, body: body ? JSON.stringify(body) : undefined, }).then(async (response) => { const payload = await response.json().catch(() => ({})) if (!response.ok) { throw new Error(payload?.message || 'Request failed') } return payload }) } function Badge({ children, tone = 'slate' }) { const tones = { slate: 'border-white/10 bg-white/[0.05] text-slate-200', amber: 'border-amber-300/20 bg-amber-400/10 text-amber-100', rose: 'border-rose-300/20 bg-rose-400/10 text-rose-100', emerald: 'border-emerald-300/20 bg-emerald-400/10 text-emerald-100', sky: 'border-sky-300/20 bg-sky-400/10 text-sky-100', } return {children} } export default function ArtworkMaturityQueue() { const { props } = usePage() const [items, setItems] = useState(props.initialItems || []) const [stats, setStats] = useState(props.stats || {}) const [status, setStatus] = useState(props.initialFilters?.status || 'suspected') const [aiAction, setAiAction] = useState(props.initialFilters?.ai_action || 'all') const [aiStatus, setAiStatus] = useState(props.initialFilters?.ai_status || 'all') const [busyId, setBusyId] = useState(null) const [noteById, setNoteById] = useState({}) const [error, setError] = useState('') const [previewItem, setPreviewItem] = useState(null) const endpoints = props.endpoints || {} const filterOptions = props.filterOptions || {} const reviewActions = props.reviewActions || [] function queueStatusKey(key) { return key === 'mature' ? 'reviewed' : key } async function load(nextStatus, nextAiAction = aiAction, nextAiStatus = aiStatus) { setStatus(nextStatus) setAiAction(nextAiAction) setAiStatus(nextAiStatus) setError('') try { const query = new URLSearchParams({ status: nextStatus, ai_action: nextAiAction, ai_status: nextAiStatus, }) const payload = await requestJson(`${endpoints.list}?${query.toString()}`) setItems(payload.data || []) setStats(payload.meta?.stats || {}) } catch (loadError) { setError(loadError.message) } } async function review(itemId, action) { setBusyId(itemId) setError('') try { const payload = await requestJson(String(endpoints.reviewPattern || '').replace('__ARTWORK__', String(itemId)), { method: 'POST', body: { action, note: noteById[itemId] || '', }, }) setStats(payload.stats || {}) setItems((current) => current.filter((item) => item.id !== itemId).concat(status === 'reviewed' ? [payload.artwork] : [])) if (status !== 'reviewed') { setItems((current) => current.filter((item) => item.id !== itemId)) } } catch (reviewError) { setError(reviewError.message) } finally { setBusyId(null) } } const statusSummary = useMemo(() => [ { key: 'suspected', label: 'Suspected', value: Number(stats.suspected || 0) }, { key: 'audit', label: 'Audit candidates', value: Number(stats.audit || 0) }, { key: 'reviewed', label: 'Reviewed', value: Number(stats.reviewed || 0) }, { key: 'mature', label: 'Marked mature', value: Number(stats.mature || 0) }, ], [stats]) return (
Moderator surface
Review uploads where the uploader declaration and AI suspicion do not match, plus legacy artworks detected by the non-mutating thumbnail audit. Audit candidates stay read-only until a moderator confirms the final maturity state.
{item.publisher} {item.category ? `• ${item.category}` : ''} {item.content_type ? `• ${item.content_type}` : ''}