import React from 'react' import { Head, usePage } from '@inertiajs/react' import Checkbox from '../../components/ui/Checkbox' import DateTimePicker from '../../components/ui/DateTimePicker' import NovaSelect from '../../components/ui/NovaSelect' const FILTER_OPTIONS = [ { value: 'all', label: 'All rows' }, { value: 'winner', label: 'Winner' }, { value: 'active', label: 'Active' }, { value: 'eligible', label: 'Eligible' }, { value: 'attention', label: 'Needs attention' }, { value: 'ineligible', label: 'Ineligible' }, { value: 'expired', label: 'Expired' }, { value: 'inactive', label: 'Inactive' }, ] const SORT_OPTIONS = [ { value: 'priority', label: 'Priority' }, { value: 'featured_at', label: 'Featured Since' }, { value: 'expires_at', label: 'Expires' }, { value: 'score_30d', label: 'Medal Score (30d)' }, ] const PRIORITY_PRESETS = [60, 100, 180, 260, 340] const PAGE_SIZE = 24 function cn(...values) { return values.filter(Boolean).join(' ') } function getCsrfToken() { if (typeof document === 'undefined') return '' return document.querySelector('meta[name="csrf-token"]')?.getAttribute('content') || '' } async function requestJson(url, { method = 'POST', body } = {}) { const response = await fetch(url, { method, credentials: 'same-origin', headers: { Accept: 'application/json', 'Content-Type': 'application/json', 'X-CSRF-TOKEN': getCsrfToken(), 'X-Requested-With': 'XMLHttpRequest', }, body: body ? JSON.stringify(body) : undefined, }) const payload = await response.json().catch(() => ({})) if (!response.ok) { throw new Error(payload?.message || payload?.errors?.artwork_id?.[0] || payload?.errors?.is_active?.[0] || payload?.errors?.force_hero?.[0] || 'Request failed.') } return payload } function isoToLocalInput(value) { if (!value) return '' const date = new Date(value) if (Number.isNaN(date.getTime())) return '' const local = new Date(date.getTime() - (date.getTimezoneOffset() * 60000)) return local.toISOString().slice(0, 16) } function localInputToIso(value) { if (!value) return null const date = new Date(value) if (Number.isNaN(date.getTime())) return null return date.toISOString() } function formatDateTime(value) { if (!value) return '—' const date = new Date(value) if (Number.isNaN(date.getTime())) return '—' return new Intl.DateTimeFormat('en', { dateStyle: 'medium', timeStyle: 'short', }).format(date) } function formatShortDate(value) { if (!value) return 'No expiry' const date = new Date(value) if (Number.isNaN(date.getTime())) return 'No expiry' return new Intl.DateTimeFormat('en', { month: 'short', day: 'numeric', }).format(date) } function formatRelativeExpiry(value) { if (!value) return 'No expiry' const date = new Date(value) if (Number.isNaN(date.getTime())) return 'No expiry' const now = new Date() const diffInDays = Math.ceil((date.getTime() - now.getTime()) / 86400000) if (diffInDays < 0) return `${Math.abs(diffInDays)}d overdue` if (diffInDays === 0) return 'Due today' if (diffInDays === 1) return 'Due tomorrow' return `${diffInDays}d left` } function formatOwner(entry) { if (!entry?.artwork?.owner) return 'Unknown artist' if (entry.artwork.owner.type === 'group') return `${entry.artwork.owner.display_name || 'Unknown'} · Group publisher` return `${entry.artwork.owner.display_name || 'Unknown'} · @${entry.artwork.owner.username || ''}` } function compareWinnerOrder(left, right) { if (Boolean(left?.is_force_hero) !== Boolean(right?.is_force_hero)) { return Boolean(right?.is_force_hero) - Boolean(left?.is_force_hero) } const priorityDiff = Number(right?.priority || 0) - Number(left?.priority || 0) if (priorityDiff !== 0) return priorityDiff const scoreDiff = Number(right?.medals?.score_30d || 0) - Number(left?.medals?.score_30d || 0) if (scoreDiff !== 0) return scoreDiff const featuredDiff = (new Date(right?.featured_at || 0).getTime() || 0) - (new Date(left?.featured_at || 0).getTime() || 0) if (featuredDiff !== 0) return featuredDiff const publishedDiff = (new Date(right?.artwork?.published_at || 0).getTime() || 0) - (new Date(left?.artwork?.published_at || 0).getTime() || 0) if (publishedDiff !== 0) return publishedDiff return Number(right?.id || 0) - Number(left?.id || 0) } function compareEntries(left, right, sortKey, direction) { const dir = direction === 'asc' ? 1 : -1 const value = (entry) => { switch (sortKey) { case 'featured_at': return new Date(entry.featured_at || 0).getTime() || 0 case 'expires_at': return new Date(entry.expires_at || 0).getTime() || 0 case 'score_30d': return Number(entry.medals?.score_30d || 0) default: return Number(entry.priority || 0) } } const leftValue = value(left) const rightValue = value(right) if (leftValue !== rightValue) { return (leftValue > rightValue ? 1 : -1) * dir } return compareWinnerOrder(left, right) } function buildEntrySummary(entry) { if (entry?.is_force_hero) { return 'Force Hero is enabled, so this row overrides the normal homepage winner order until editors switch it off.' } if (entry?.is_winner && entry?.winner_reason) { return entry.winner_reason } if (!entry?.is_active) { return 'Inactive rows stay visible for planning and editing, but they cannot win the homepage slot.' } if (entry?.is_expired) { return 'This featured slot has expired and is excluded from the live homepage rotation.' } if (!entry?.eligibility?.is_eligible) { const reasons = Array.isArray(entry?.eligibility?.reasons) && entry.eligibility.reasons.length > 0 ? entry.eligibility.reasons.join(', ') : 'Eligibility checks failed.' return `Currently blocked from winning: ${reasons}` } return 'Eligible and in the active selection pool. Priority leads first, then medal score, featured time, and publish date.' } function Badge({ label, tone = 'slate' }) { const toneClasses = { slate: 'border-white/10 bg-white/[0.07] text-slate-100', sky: 'border-sky-300/20 bg-sky-400/15 text-sky-100', emerald: 'border-emerald-300/20 bg-emerald-400/15 text-emerald-100', amber: 'border-amber-300/20 bg-amber-400/15 text-amber-100', rose: 'border-rose-300/20 bg-rose-400/15 text-rose-100', } return ( {label} ) } function Field({ label, help, children }) { return ( ) } function NoticeBanner({ notice }) { if (!notice?.message) return null const toneClasses = { success: 'border-emerald-300/20 bg-emerald-400/10 text-emerald-50', error: 'border-rose-300/20 bg-rose-400/10 text-rose-50', info: 'border-sky-300/20 bg-sky-400/10 text-sky-50', } return (
{notice.message}
) } function StatCard({ label, value, detail, tone = 'sky' }) { const toneClasses = { sky: 'border-sky-300/15 bg-sky-400/10 text-sky-100', amber: 'border-amber-300/15 bg-amber-400/10 text-amber-100', emerald: 'border-emerald-300/15 bg-emerald-400/10 text-emerald-100', rose: 'border-rose-300/15 bg-rose-400/10 text-rose-100', } return (
{label}
{value}
{detail ?
{detail}
: null}
) } function MetricTile({ label, value, hint }) { return (
{label}
{value}
{hint ?
{hint}
: null}
) } function DockButton({ label, detail, tone = 'sky', onClick }) { const toneClasses = { sky: 'border-sky-300/25 bg-sky-400/12 text-sky-50', amber: 'border-amber-300/25 bg-amber-400/12 text-amber-50', slate: 'border-white/10 bg-white/[0.05] text-slate-100', } return ( ) } function FilterChip({ label, value, active, onClick, count }) { return ( ) } function emptyForm() { return { artwork_id: '', priority: 100, featured_at: isoToLocalInput(new Date().toISOString()), expires_at: '', is_active: true, } } function mapEntryToCandidate(entry) { if (!entry) return null return { ...entry.artwork, medals: entry.medals, eligibility: entry.eligibility, existing_feature_count: entry.duplicate_count, already_featured: entry.duplicate_count > 0, } } export default function FeaturedArtworksAdmin() { const { props } = usePage() const composerRef = React.useRef(null) const rosterRef = React.useRef(null) const loadMoreRef = React.useRef(null) const endpoints = props.endpoints || {} const capabilities = props.capabilities || {} const seo = props.seo || {} const [entries, setEntries] = React.useState(Array.isArray(props.entries) ? props.entries : []) const [winner, setWinner] = React.useState(props.winner || null) const [stats, setStats] = React.useState(props.stats || {}) const [notice, setNotice] = React.useState(null) const [busy, setBusy] = React.useState('') const [filter, setFilter] = React.useState('all') const [sortKey, setSortKey] = React.useState('priority') const [sortDirection, setSortDirection] = React.useState('desc') const [listQuery, setListQuery] = React.useState('') const deferredListQuery = React.useDeferredValue(listQuery) const [searchQuery, setSearchQuery] = React.useState('') const [searchResults, setSearchResults] = React.useState([]) const [selectedArtwork, setSelectedArtwork] = React.useState(null) const [editingId, setEditingId] = React.useState(null) const [form, setForm] = React.useState(emptyForm()) const [visibleCount, setVisibleCount] = React.useState(PAGE_SIZE) React.useEffect(() => { setEntries(Array.isArray(props.entries) ? props.entries : []) setWinner(props.winner || null) setStats(props.stats || {}) }, [props.entries, props.stats, props.winner]) React.useEffect(() => { setVisibleCount(PAGE_SIZE) }, [deferredListQuery, filter, sortDirection, sortKey]) function scrollToSection(ref) { ref.current?.scrollIntoView({ behavior: 'smooth', block: 'start' }) } function scrollToTop() { if (typeof window !== 'undefined') { window.scrollTo({ top: 0, behavior: 'smooth' }) } } function syncPayload(payload) { setEntries(Array.isArray(payload.entries) ? payload.entries : []) setWinner(payload.winner || null) setStats(payload.stats || {}) if (payload.message) { setNotice({ tone: 'success', message: payload.message }) } } function resetEditor() { setEditingId(null) setSelectedArtwork(null) setSearchResults([]) setSearchQuery('') setForm(emptyForm()) } async function handleArtworkSearch(event) { event.preventDefault() if (!searchQuery.trim()) { setSearchResults([]) return } setBusy('search') setNotice(null) try { const url = `${endpoints.search}?q=${encodeURIComponent(searchQuery.trim())}` const payload = await requestJson(url, { method: 'GET' }) const results = Array.isArray(payload.results) ? payload.results : [] setSearchResults(results) if (results.length === 0) { setNotice({ tone: 'info', message: 'No artworks matched that search.' }) } } catch (error) { setNotice({ tone: 'error', message: error.message || 'Artwork search failed.' }) } finally { setBusy('') } } function chooseArtwork(artwork) { setSelectedArtwork(artwork) setForm((current) => ({ ...current, artwork_id: artwork.id, })) } function editEntry(entry) { setEditingId(entry.id) setSelectedArtwork(mapEntryToCandidate(entry)) setSearchResults([]) setSearchQuery('') setForm({ artwork_id: entry.artwork_id, priority: entry.priority, featured_at: isoToLocalInput(entry.featured_at), expires_at: isoToLocalInput(entry.expires_at), is_active: Boolean(entry.is_active), }) scrollToSection(composerRef) } async function handleSubmit(event) { event.preventDefault() if (!editingId && !form.artwork_id) { setNotice({ tone: 'error', message: 'Select an artwork first.' }) return } setBusy('submit') setNotice(null) try { const payload = await requestJson( editingId ? endpoints.updatePattern.replace('__FEATURE__', String(editingId)) : endpoints.store, { method: editingId ? 'PATCH' : 'POST', body: { artwork_id: Number(form.artwork_id), priority: Number(form.priority || 0), featured_at: localInputToIso(form.featured_at), expires_at: localInputToIso(form.expires_at), is_active: Boolean(form.is_active), }, }, ) syncPayload(payload) resetEditor() } catch (error) { setNotice({ tone: 'error', message: error.message || 'Failed to save this featured entry.' }) } finally { setBusy('') } } async function handleToggle(entry) { setBusy(`toggle-${entry.id}`) setNotice(null) try { const payload = await requestJson(endpoints.togglePattern.replace('__FEATURE__', String(entry.id)), { method: 'PATCH', }) syncPayload(payload) } catch (error) { setNotice({ tone: 'error', message: error.message || 'Failed to change active state.' }) } finally { setBusy('') } } async function handleDelete(entry) { if (typeof window !== 'undefined' && !window.confirm(`Delete featured entry #${entry.id}?`)) { return } setBusy(`delete-${entry.id}`) setNotice(null) try { const payload = await requestJson(endpoints.destroyPattern.replace('__FEATURE__', String(entry.id)), { method: 'DELETE', }) syncPayload(payload) if (editingId === entry.id) { resetEditor() } } catch (error) { setNotice({ tone: 'error', message: error.message || 'Failed to delete this featured entry.' }) } finally { setBusy('') } } async function handleForceHero(entry) { setBusy(`force-${entry.id}`) setNotice(null) try { const payload = await requestJson(endpoints.forceHeroPattern.replace('__FEATURE__', String(entry.id)), { method: 'PATCH', }) syncPayload(payload) } catch (error) { setNotice({ tone: 'error', message: error.message || 'Failed to change force hero state.' }) } finally { setBusy('') } } const duplicateSelection = !editingId && selectedArtwork?.already_featured const suggestedPriority = winner ? Number(winner.priority || 0) + 20 : 120 const filterCounts = React.useMemo(() => ({ all: entries.length, winner: entries.filter((entry) => Boolean(entry.is_winner)).length, active: entries.filter((entry) => Boolean(entry.is_active)).length, eligible: entries.filter((entry) => Boolean(entry.eligibility?.is_eligible)).length, attention: entries.filter((entry) => Boolean(entry.is_active) && (Boolean(entry.is_expired) || !entry.eligibility?.is_eligible)).length, ineligible: entries.filter((entry) => !entry.eligibility?.is_eligible).length, expired: entries.filter((entry) => Boolean(entry.is_expired)).length, inactive: entries.filter((entry) => !entry.is_active).length, }), [entries]) const activeForceHeroEntry = React.useMemo(() => entries.find((entry) => Boolean(entry.is_force_hero)) || null, [entries]) const soonExpiringEntries = React.useMemo(() => { const now = Date.now() const cutoff = now + (7 * 86400000) return entries .filter((entry) => { if (!entry.expires_at || entry.is_expired) return false const expiresAt = new Date(entry.expires_at).getTime() return !Number.isNaN(expiresAt) && expiresAt >= now && expiresAt <= cutoff }) .sort((left, right) => (new Date(left.expires_at).getTime() || 0) - (new Date(right.expires_at).getTime() || 0)) }, [entries]) const naturalFallback = React.useMemo(() => { return [...entries] .filter((entry) => Boolean(entry.is_active) && !Boolean(entry.is_expired) && Boolean(entry.eligibility?.is_eligible) && !Boolean(entry.is_winner)) .sort(compareWinnerOrder)[0] || null }, [entries]) const attentionEntries = React.useMemo(() => { return [...entries] .filter((entry) => Boolean(entry.is_active) && (Boolean(entry.is_expired) || !entry.eligibility?.is_eligible)) .sort(compareWinnerOrder) .slice(0, 3) }, [entries]) const selectedArtworkSignals = React.useMemo(() => { if (!selectedArtwork) return [] const signals = [] const eligibilityReasons = Array.isArray(selectedArtwork.eligibility?.reasons) ? selectedArtwork.eligibility.reasons : [] signals.push({ label: selectedArtwork.eligibility?.is_eligible ? 'Ready for rotation now' : 'Needs editorial attention', tone: selectedArtwork.eligibility?.is_eligible ? 'emerald' : 'rose', detail: selectedArtwork.eligibility?.is_eligible ? 'Current eligibility checks pass.' : eligibilityReasons.join(', ') || 'Eligibility checks are failing.', }) if (winner) { const priorityGap = Number(form.priority || 0) - Number(winner.priority || 0) signals.push({ label: priorityGap >= 0 ? 'Priority matches or exceeds current winner' : 'Lower priority than current winner', tone: priorityGap >= 0 ? 'sky' : 'amber', detail: `Current winner priority: ${winner.priority}. Proposed priority: ${Number(form.priority || 0)}.`, }) } if (selectedArtwork.already_featured) { signals.push({ label: 'Existing featured row found', tone: 'amber', detail: `This artwork already appears in ${selectedArtwork.existing_feature_count || 1} featured row${selectedArtwork.existing_feature_count === 1 ? '' : 's'}.`, }) } return signals }, [form.priority, selectedArtwork, winner]) const filteredEntries = React.useMemo(() => { const query = deferredListQuery.trim().toLowerCase() return [...entries] .filter((entry) => { if (filter === 'active') return Boolean(entry.is_active) if (filter === 'inactive') return !entry.is_active if (filter === 'expired') return Boolean(entry.is_expired) if (filter === 'winner') return Boolean(entry.is_winner) if (filter === 'eligible') return Boolean(entry.eligibility?.is_eligible) if (filter === 'ineligible') return !entry.eligibility?.is_eligible if (filter === 'attention') return Boolean(entry.is_active) && (Boolean(entry.is_expired) || !entry.eligibility?.is_eligible) return true }) .filter((entry) => { if (!query) return true const haystack = [ entry.artwork?.title, entry.artwork?.owner?.display_name, entry.artwork?.owner?.username, entry.artwork?.id, entry.id, ].join(' ').toLowerCase() return haystack.includes(query) }) .sort((left, right) => compareEntries(left, right, sortKey, sortDirection)) }, [deferredListQuery, entries, filter, sortDirection, sortKey]) const visibleEntries = React.useMemo(() => filteredEntries.slice(0, visibleCount), [filteredEntries, visibleCount]) const hasMoreEntries = visibleEntries.length < filteredEntries.length React.useEffect(() => { if (!hasMoreEntries || typeof IntersectionObserver === 'undefined' || !loadMoreRef.current) { return undefined } const observer = new IntersectionObserver((observerEntries) => { if (observerEntries.some((observerEntry) => observerEntry.isIntersecting)) { setVisibleCount((current) => Math.min(current + PAGE_SIZE, filteredEntries.length)) } }, { rootMargin: '320px 0px' }) observer.observe(loadMoreRef.current) return () => observer.disconnect() }, [filteredEntries.length, hasMoreEntries, visibleCount]) function loadMoreEntries() { setVisibleCount((current) => Math.min(current + PAGE_SIZE, filteredEntries.length)) } return ( <> {seo.title || 'Featured Artworks'} {seo.description ? : null} {seo.robots ? : null} {seo.canonical ? : null}
Featured Artworks

Curate the homepage with live winner context, not guesswork.

This workspace now behaves like an editorial command center: current winner visibility, natural fallback awareness, faster filters, and a cleaner composer for new or existing featured rows.

{winner?.artwork?.canonical_url ? ( Open current winner ) : null}
0 ? 'rose' : 'sky'} />
Current Homepage Hero

{winner ? winner.artwork?.title : 'No eligible featured artwork'}

{winner?.selection_reason || 'There is no active, non-expired, eligible featured artwork right now.'}

{winner ? : } {winner?.is_force_hero ? : }
{winner?.is_force_hero ? (
Force Hero is active. This row bypasses the normal ranking until editors disable the override from the roster below.
) : null}
Editorial Radar

Know what happens next before you touch a row.

Natural fallback
{naturalFallback?.artwork?.title || 'No fallback candidate'}
{naturalFallback ? `${formatOwner(naturalFallback)} · Priority ${naturalFallback.priority} · Medal ${naturalFallback.medals?.score_30d || 0}` : 'All remaining rows are either inactive, expired, or ineligible.'}
Needs attention
{attentionEntries.length === 0 ? (
No active rows are currently blocked by expiry or eligibility rules.
) : (
{attentionEntries.map((entry) => ( ))}
)}
Next expiry checkpoint
{soonExpiringEntries[0]?.artwork?.title || 'Nothing expiring soon'}
{soonExpiringEntries[0] ? `${formatRelativeExpiry(soonExpiringEntries[0].expires_at)} · ${formatDateTime(soonExpiringEntries[0].expires_at)}` : 'No featured rows expire in the next 7 days.'}
Featured Pool

Every featured row, with enough context to act quickly.

Showing {visibleEntries.length} of {filteredEntries.length} matching rows. {entries.length} rows in the full pool.

setListQuery(event.target.value)} placeholder="Filter by title, artist, row ID, or artwork ID" className="rounded-2xl border border-white/10 bg-[#08111d] px-4 py-3 text-sm text-white outline-none transition focus:border-sky-300/40" /> setSortKey(value)} searchable={false} options={SORT_OPTIONS} />
{FILTER_OPTIONS.map((option) => ( React.startTransition(() => setFilter(nextValue))} count={filterCounts[option.value] ?? 0} /> ))}
{filteredEntries.length === 0 ? (
No featured entries match the current search and filter combination.
) : visibleEntries.map((entry) => (
{entry.artwork?.title

{entry.artwork?.title || 'Missing artwork'}

Artwork #{entry.artwork?.id || entry.artwork_id} Row #{entry.id}
{formatOwner(entry)}

{buildEntrySummary(entry)}

{(entry.status_badges || []).map((badge, index) => ( ))}
Visibility: {entry.artwork?.visibility || '—'} Published: {entry.artwork?.published_at ? 'Yes' : 'No'}
Open artwork {capabilities.forceHeroEnabled ? ( ) : null}
))} {hasMoreEntries ? (
Loading more rows as you reach the bottom.
) : filteredEntries.length > PAGE_SIZE ? (
All matching rows loaded
) : null}
Editorial Composer

{editingId ? `Edit featured row #${editingId}` : 'Build a new featured row'}

Search, inspect readiness, choose timing, and ship the change without leaving the page context.

{editingId ? ( ) : null}
{!editingId ? (
setSearchQuery(event.target.value)} className="w-full rounded-2xl border border-white/10 bg-[#08111d] px-4 py-3 text-sm text-white outline-none transition focus:border-sky-300/40" placeholder="Try an artwork ID, title, or creator" />
{searchResults.length > 0 ? (
Search results
{searchResults.map((artwork) => ( ))}
) : null}
) : null} {selectedArtwork ? (
{selectedArtwork.title
Selected Artwork
{selectedArtwork.title}
#{selectedArtwork.id} · {selectedArtwork.owner?.display_name || 'Unknown'} · Medal Score (30d): {selectedArtwork.medals?.score_30d || 0}
{(selectedArtwork.eligibility?.is_eligible ? [{ label: 'Currently eligible', tone: 'emerald' }] : [{ label: 'Currently ineligible', tone: 'rose' }]).concat( (selectedArtwork.eligibility?.reasons || []).map((reason) => ({ label: reason, tone: reason === 'Missing preview' ? 'rose' : 'slate', })) ).map((badge) => ( ))}
{selectedArtwork.canonical_url ? ( Open artwork ) : null} {selectedArtwork.already_featured ? ( ) : null}
{selectedArtworkSignals.length > 0 ? (
{selectedArtworkSignals.map((signal) => (
{signal.detail}
))}
) : null}
) : null} {duplicateSelection ? (
This artwork already has a featured entry. Edit the existing row instead of creating a duplicate.
) : null}
setForm((current) => ({ ...current, priority: event.target.value }))} className="w-full rounded-2xl border border-white/10 bg-[#08111d] px-4 py-3 text-sm text-white outline-none transition focus:border-sky-300/40" />
{PRIORITY_PRESETS.map((preset) => ( ))} {winner ? ( ) : null}
setForm((current) => ({ ...current, featured_at: nextValue }))} placeholder="Featured since" clearable className="bg-[#08111d]" /> setForm((current) => ({ ...current, expires_at: nextValue }))} placeholder="Expiry date" clearable className="bg-[#08111d]" />
{editingId ? ( ) : null}
{ React.startTransition(() => setFilter('attention')) scrollToSection(rosterRef) }} /> scrollToSection(composerRef)} />
) }