import React from 'react' import { Head, Link, usePage } from '@inertiajs/react' import NovaCardCanvasPreview from '../../components/nova-cards/NovaCardCanvasPreview' 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 renderOverrideHistoryItems(items, prefix) { return (items || []).slice(0, 3).map((entry, index) => (
{entry.moderation_status || 'unknown status'} {entry.disposition_label ? {entry.disposition_label} : null} {entry.actor_username ? @{entry.actor_username} : null}
{entry.source ? {String(entry.source).replaceAll('_', ' ')} : null} {entry.updated_at ? {new Date(entry.updated_at).toLocaleString()} : null}
{entry.note ?
{entry.note}
: null}
)) } export default function NovaCardsAdminIndex() { const { props } = usePage() const [cards, setCards] = React.useState(props.cards?.data || []) const [featuredCreators, setFeaturedCreators] = React.useState(props.featuredCreators || []) const [categories, setCategories] = React.useState(props.categories || []) const [reportStatus, setReportStatus] = React.useState('open') const [reports, setReports] = React.useState([]) const [reportsMeta, setReportsMeta] = React.useState({ total: 0 }) const [reportCounts, setReportCounts] = React.useState(props.reportingQueue?.statuses || { open: 0, reviewing: 0, closed: 0 }) const [reportNotes, setReportNotes] = React.useState({}) const [reportBusy, setReportBusy] = React.useState({}) const [reportsLoading, setReportsLoading] = React.useState(false) const [reportsError, setReportsError] = React.useState('') const [cardDispositions, setCardDispositions] = React.useState(() => Object.fromEntries((props.cards?.data || []).map((card) => [card.id, card.moderation_override?.disposition || '']))) const [reportDispositions, setReportDispositions] = React.useState({}) const [newCategory, setNewCategory] = React.useState({ slug: '', name: '', description: '', active: true, order_num: categories.length }) const endpoints = props.endpoints || {} const stats = props.stats || {} const reportingQueue = props.reportingQueue || {} const moderationDispositionOptions = props.moderationDispositionOptions || {} function dispositionOptionsForStatus(status) { return moderationDispositionOptions?.[status] || [] } function preferredDisposition(status, currentValue) { const options = dispositionOptionsForStatus(status) if (currentValue && options.some((option) => option.value === currentValue)) { return currentValue } return options[0]?.value || '' } React.useEffect(() => { let active = true async function loadReports() { if (!endpoints.reportsQueue) { return } setReportsLoading(true) setReportsError('') try { const separator = String(endpoints.reportsQueue).includes('?') ? '&' : '?' const response = await requestJson(`${endpoints.reportsQueue}${separator}status=${reportStatus}`) if (!active) return setReports(response.data || []) setReportsMeta(response.meta || { total: 0 }) setReportDispositions((current) => { const next = { ...current } ;(response.data || []).forEach((report) => { const target = report?.target?.moderation_target if (target?.card_id && !(report.id in next)) { next[report.id] = preferredDisposition(target.moderation_status, target.moderation_override?.disposition) } }) return next }) setReportNotes((current) => { const next = { ...current } ;(response.data || []).forEach((report) => { if (!(report.id in next)) { next[report.id] = report.moderator_note || '' } }) return next }) } catch (error) { if (!active) return setReportsError(error.message) setReports([]) } finally { if (active) { setReportsLoading(false) } } } loadReports() return () => { active = false } }, [endpoints.reportsQueue, reportStatus]) async function updateCard(cardId, patch) { const response = await requestJson(String(endpoints.updateCardPattern || '').replace('__CARD__', String(cardId)), { method: 'PATCH', body: patch, }) setCardDispositions((current) => ({ ...current, [cardId]: preferredDisposition(response.card?.moderation_status, response.card?.moderation_override?.disposition), })) setCards((current) => current.map((card) => (card.id === cardId ? response.card : card))) } async function updateCreator(creatorId, patch) { const response = await requestJson(String(endpoints.updateCreatorPattern || '').replace('__CREATOR__', String(creatorId)), { method: 'PATCH', body: patch, }) setFeaturedCreators((current) => current.map((creator) => (creator.id === creatorId ? response.creator : creator))) } function syncReportCounts(previousStatus, nextStatus) { if (!previousStatus || !nextStatus || previousStatus === nextStatus) { return } setReportCounts((current) => ({ ...current, [previousStatus]: Math.max(0, Number(current?.[previousStatus] || 0) - 1), [nextStatus]: Number(current?.[nextStatus] || 0) + 1, })) } function mergeUpdatedReport(updatedReport, previousStatus) { setReportNotes((current) => ({ ...current, [updatedReport.id]: updatedReport.moderator_note || '' })) setReportDispositions((current) => ({ ...current, [updatedReport.id]: preferredDisposition(updatedReport?.target?.moderation_target?.moderation_status, updatedReport?.target?.moderation_target?.moderation_override?.disposition), })) if (previousStatus && previousStatus !== updatedReport.status) { syncReportCounts(previousStatus, updatedReport.status) } setReports((current) => { if (previousStatus && previousStatus !== updatedReport.status) { return current.filter((report) => report.id !== updatedReport.id) } return current.map((report) => (report.id === updatedReport.id ? updatedReport : report)) }) } async function saveCategory(category) { const isExisting = Boolean(category.id) const url = isExisting ? String(endpoints.updateCategoryPattern || '').replace('__CATEGORY__', String(category.id)) : endpoints.storeCategory const response = await requestJson(url, { method: isExisting ? 'PATCH' : 'POST', body: category, }) setCategories((current) => { if (isExisting) { return current.map((item) => (item.id === category.id ? { ...item, ...response.category } : item)) } return [...current, { ...response.category, cards_count: 0 }] }) if (!isExisting) { setNewCategory({ slug: '', name: '', description: '', active: true, order_num: categories.length + 1 }) } } async function updateReport(reportId, patch) { const currentReport = reports.find((report) => report.id === reportId) if (!currentReport) { return } setReportBusy((current) => ({ ...current, [reportId]: true })) try { const response = await requestJson(String(endpoints.updateReportPattern || '').replace('__REPORT__', String(reportId)), { method: 'PATCH', body: patch, }) mergeUpdatedReport(response.report, currentReport.status) } finally { setReportBusy((current) => ({ ...current, [reportId]: false })) } } async function moderateReportTarget(reportId, action) { const currentReport = reports.find((report) => report.id === reportId) if (!currentReport) { return } setReportBusy((current) => ({ ...current, [reportId]: true })) try { const response = await requestJson(String(endpoints.moderateReportTargetPattern || '').replace('__REPORT__', String(reportId)), { method: 'POST', body: { action, disposition: reportDispositions[reportId] || null }, }) mergeUpdatedReport(response.report, currentReport.status) setCards((current) => current.map((card) => (card.id === response.report?.target?.moderation_target?.card_id ? { ...card, moderation_status: response.report.target.moderation_target.moderation_status, moderation_override: response.report.target.moderation_target.moderation_override, moderation_override_history: response.report.target.moderation_target.moderation_override_history, } : card))) } finally { setReportBusy((current) => ({ ...current, [reportId]: false })) } } return (

Moderation surface

Nova Cards control panel

Review pending cards, feature standout work, and keep the starter category taxonomy healthy as Nova Cards launches.

Manage templates Asset packs Challenges
{[ ['Pending', stats.pending || 0, 'fa-clock'], ['Flagged', stats.flagged || 0, 'fa-flag'], ['Featured', stats.featured || 0, 'fa-star'], ['Published', stats.published || 0, 'fa-earth-americas'], ['Remixable', stats.remixable || 0, 'fa-code-branch'], ['Challenges', stats.challenges || 0, 'fa-trophy'], ].map(([label, value, icon]) => (
{label}
{value}
))}
Reporting queue

{reportingQueue.label || 'Nova Cards report queue'}

{reportingQueue.description || 'Review reports targeting Nova Cards surfaces.'}

Pending reports
{reportCounts.open || 0}
{reportingQueue.enabled ? 'Connected to moderation pipeline' : 'Reporting disabled'}
{[ ['open', reportCounts.open || 0], ['reviewing', reportCounts.reviewing || 0], ['closed', reportCounts.closed || 0], ].map(([status, count]) => ( ))}
{reportStatus.charAt(0).toUpperCase() + reportStatus.slice(1)} reports
{reportsMeta.total || reports.length} total
{reportsLoading ?
Loading report queue…
: null} {reportsError ?
{reportsError}
: null} {!reportsLoading && !reportsError && !reports.length ?
No reports in this state.
: null}
{reports.map((report) => (
{String(report.target?.type || report.target_type).replaceAll('_', ' ')} {report.status}
{report.target?.label || `Target #${report.target_id}`}
{report.target?.subtitle || 'No target details available.'}
{report.reason}
{report.details ?

{report.details}

: null}
Reported by {report.reporter?.username ? `@${report.reporter.username}` : 'unknown'}{report.created_at ? ` • ${new Date(report.created_at).toLocaleString()}` : ''}
{report.last_moderated_by?.username || report.last_moderated_at ? (
Last touched {report.last_moderated_by?.username ? `by @${report.last_moderated_by.username}` : 'by staff'}{report.last_moderated_at ? ` • ${new Date(report.last_moderated_at).toLocaleString()}` : ''}
) : null} {report.target?.moderation_target ? (
Card moderation target
{report.target.moderation_target.title}
{report.target.moderation_target.context ?
{report.target.moderation_target.context}
: null}
Status {report.target.moderation_target.status} Moderation {report.target.moderation_target.moderation_status}
{report.target.moderation_target.moderation_reason_labels?.length ? (
Heuristic flags
{report.target.moderation_target.moderation_reason_labels.map((label) => ( {label} ))}
{report.target.moderation_target.moderation_source ?
Source {String(report.target.moderation_target.moderation_source).replaceAll('_', ' ')}
: null}
) : null} {report.target.moderation_target.moderation_override ? (
Latest staff override
Status {report.target.moderation_target.moderation_override.moderation_status} {report.target.moderation_target.moderation_override.disposition_label ? {report.target.moderation_target.moderation_override.disposition_label} : null} {report.target.moderation_target.moderation_override.actor_username ? @{report.target.moderation_target.moderation_override.actor_username} : null} {report.target.moderation_target.moderation_override.source ? {String(report.target.moderation_target.moderation_override.source).replaceAll('_', ' ')} : null}
{report.target.moderation_target.moderation_override.note ?
{report.target.moderation_target.moderation_override.note}
: null}
) : null} {report.target.moderation_target.moderation_override_history?.length > 1 ? (
Recent override history
{renderOverrideHistoryItems(report.target.moderation_target.moderation_override_history, `report-${report.id}`)}
) : null}
{(report.target.moderation_target.available_actions || []).map((actionItem) => ( ))}
) : null}
Moderator note