Files
SkinbaseNova/resources/js/Pages/Collection/CollectionDashboard.jsx
2026-03-28 19:15:39 +01:00

675 lines
34 KiB
JavaScript

import React from 'react'
import { Head, usePage } from '@inertiajs/react'
import CollectionCard from '../../components/profile/collections/CollectionCard'
const DEFAULT_SEARCH_FILTERS = {
q: '',
type: '',
visibility: '',
lifecycle_state: '',
workflow_state: '',
health_state: '',
placement_eligibility: '',
}
const DEFAULT_BULK_FORM = {
action: 'archive',
campaign_key: '',
campaign_label: '',
lifecycle_state: 'archived',
}
function getCsrfToken() {
if (typeof document === 'undefined') return ''
return document.querySelector('meta[name="csrf-token"]')?.getAttribute('content') || ''
}
function titleize(value) {
return String(value || '')
.split('_')
.filter(Boolean)
.map((part) => part.charAt(0).toUpperCase() + part.slice(1))
.join(' ')
}
function buildSearchUrl(baseUrl, filters) {
const url = new URL(baseUrl, window.location.origin)
Object.entries(filters || {}).forEach(([key, value]) => {
if (value === '' || value === null || value === undefined) {
return
}
url.searchParams.set(key, String(value))
})
return url.toString()
}
async function fetchSearchResults(baseUrl, filters) {
const response = await fetch(buildSearchUrl(baseUrl, filters), {
method: 'GET',
credentials: 'same-origin',
headers: {
Accept: 'application/json',
'X-Requested-With': 'XMLHttpRequest',
},
})
const payload = await response.json().catch(() => ({}))
if (!response.ok) {
throw new Error(payload?.message || 'Search failed.')
}
return payload
}
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 || 'Request failed.')
}
return payload
}
function SummaryCard({ label, value, icon, 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',
rose: 'border-rose-300/15 bg-rose-400/10 text-rose-100',
emerald: 'border-emerald-300/15 bg-emerald-400/10 text-emerald-100',
}
return (
<div className="rounded-[28px] border border-white/10 bg-white/[0.04] p-5 backdrop-blur-sm">
<div className={`inline-flex h-12 w-12 items-center justify-center rounded-2xl border ${toneClasses[tone] || toneClasses.sky}`}>
<i className={`fa-solid ${icon}`} />
</div>
<div className="mt-4 text-[11px] font-semibold uppercase tracking-[0.2em] text-slate-400">{label}</div>
<div className="mt-2 text-3xl font-semibold tracking-[-0.04em] text-white">{value}</div>
</div>
)
}
function CollectionStrip({ title, eyebrow, collections, emptyLabel, endpoints }) {
function resolve(pattern, collectionId) {
return pattern ? pattern.replace('__COLLECTION__', String(collectionId)) : null
}
return (
<section className="rounded-[32px] border border-white/10 bg-white/[0.04] p-6 shadow-[0_24px_80px_rgba(2,6,23,0.24)] backdrop-blur-sm md:p-7">
<div className="flex flex-wrap items-center justify-between gap-3">
<div>
<p className="text-[11px] font-semibold uppercase tracking-[0.22em] text-sky-200/80">{eyebrow}</p>
<h2 className="mt-2 text-2xl font-semibold text-white">{title}</h2>
</div>
<span className="rounded-full border border-white/10 bg-white/[0.04] px-3 py-1 text-xs font-semibold text-slate-300">{collections.length}</span>
</div>
{collections.length ? (
<div className="mt-6 grid gap-5 xl:grid-cols-2">
{collections.map((collection) => (
<div key={collection.id} className="space-y-3">
<CollectionCard collection={collection} isOwner />
<div className="flex flex-wrap gap-2">
{resolve(endpoints?.managePattern, collection.id) ? (
<a href={resolve(endpoints.managePattern, collection.id)} className="inline-flex items-center gap-2 rounded-full border border-white/10 bg-white/[0.04] px-4 py-2 text-xs font-semibold text-white transition hover:bg-white/[0.07]">
<i className="fa-solid fa-pen-to-square fa-fw text-[10px]" />Manage
</a>
) : null}
{resolve(endpoints?.analyticsPattern, collection.id) ? (
<a href={resolve(endpoints.analyticsPattern, collection.id)} className="inline-flex items-center gap-2 rounded-full border border-sky-300/20 bg-sky-400/10 px-4 py-2 text-xs font-semibold text-sky-100 transition hover:bg-sky-400/15">
<i className="fa-solid fa-chart-column fa-fw text-[10px]" />Analytics
</a>
) : null}
{resolve(endpoints?.historyPattern, collection.id) ? (
<a href={resolve(endpoints.historyPattern, collection.id)} className="inline-flex items-center gap-2 rounded-full border border-white/10 bg-white/[0.04] px-4 py-2 text-xs font-semibold text-white transition hover:bg-white/[0.07]">
<i className="fa-solid fa-timeline fa-fw text-[10px]" />History
</a>
) : null}
</div>
</div>
))}
</div>
) : (
<div className="mt-6 rounded-[26px] border border-dashed border-white/12 bg-white/[0.03] px-6 py-12 text-sm text-slate-300">{emptyLabel}</div>
)}
</section>
)
}
function WarningList({ warnings, endpoints }) {
function resolve(pattern, collectionId) {
return pattern ? pattern.replace('__COLLECTION__', String(collectionId)) : null
}
if (!warnings.length) {
return null
}
return (
<section className="rounded-[32px] border border-white/10 bg-white/[0.04] p-6 shadow-[0_24px_80px_rgba(2,6,23,0.24)] backdrop-blur-sm md:p-7">
<div className="flex flex-wrap items-center justify-between gap-3">
<div>
<p className="text-[11px] font-semibold uppercase tracking-[0.22em] text-amber-200/80">Health</p>
<h2 className="mt-2 text-2xl font-semibold text-white">Warnings and blockers</h2>
</div>
<span className="rounded-full border border-white/10 bg-white/[0.04] px-3 py-1 text-xs font-semibold text-slate-300">{warnings.length}</span>
</div>
<div className="mt-6 grid gap-4 xl:grid-cols-2">
{warnings.map((warning) => (
<div key={warning.collection_id} className="rounded-[24px] border border-white/10 bg-[#0d1726] p-5">
<div className="flex items-start justify-between gap-3">
<div>
<p className="text-lg font-semibold text-white">{warning.title}</p>
<p className="mt-2 text-sm text-slate-300">State: {warning.health?.health_state || 'unknown'}</p>
</div>
<span className="rounded-full border border-amber-300/20 bg-amber-400/10 px-3 py-1 text-xs font-semibold text-amber-100">
{warning.health?.health_score ?? 'n/a'}
</span>
</div>
{Array.isArray(warning.health?.flags) && warning.health.flags.length ? (
<div className="mt-3 flex flex-wrap gap-2">
{warning.health.flags.map((flag) => (
<span key={flag} className="rounded-full border border-white/10 bg-white/[0.04] px-3 py-1 text-[11px] font-semibold uppercase tracking-[0.12em] text-slate-300">{flag}</span>
))}
</div>
) : null}
<div className="mt-4 flex flex-wrap gap-2">
{resolve(endpoints?.managePattern, warning.collection_id) ? <a href={resolve(endpoints.managePattern, warning.collection_id)} className="inline-flex items-center gap-2 rounded-full border border-white/10 bg-white/[0.04] px-4 py-2 text-xs font-semibold text-white transition hover:bg-white/[0.07]"><i className="fa-solid fa-pen-to-square fa-fw text-[10px]" />Manage</a> : null}
{resolve(endpoints?.healthPattern, warning.collection_id) ? <a href={resolve(endpoints.healthPattern, warning.collection_id)} className="inline-flex items-center gap-2 rounded-full border border-amber-300/20 bg-amber-400/10 px-4 py-2 text-xs font-semibold text-amber-100 transition hover:bg-amber-400/15"><i className="fa-solid fa-shield-heart fa-fw text-[10px]" />Health JSON</a> : null}
</div>
</div>
))}
</div>
</section>
)
}
function SearchField({ label, value, onChange, children }) {
return (
<label className="block space-y-2">
<span className="text-[11px] font-semibold uppercase tracking-[0.18em] text-slate-400">{label}</span>
{children || (
<input
value={value}
onChange={onChange}
className="w-full rounded-2xl border border-white/10 bg-[#09111d] px-4 py-3 text-sm text-white outline-none transition placeholder:text-slate-500 focus:border-sky-300/40"
/>
)}
</label>
)
}
function BulkActionsPanel({
selectedCount,
totalCount,
form,
onFormChange,
onApply,
onClear,
onToggleAll,
busy,
error,
notice,
}) {
if (!selectedCount && !notice && !error) {
return null
}
return (
<div className="mt-6 rounded-[26px] border border-white/10 bg-[#0d1726] p-5">
<div className="flex flex-wrap items-center justify-between gap-3">
<div>
<p className="text-[11px] font-semibold uppercase tracking-[0.18em] text-slate-400">Bulk actions</p>
<h3 className="mt-1 text-lg font-semibold text-white">Apply safe actions to selected collections</h3>
</div>
<div className="flex flex-wrap gap-2 text-xs font-semibold">
<button type="button" onClick={onToggleAll} className="rounded-full border border-white/10 bg-white/[0.04] px-3 py-1 text-slate-200 transition hover:bg-white/[0.07]">
{selectedCount === totalCount && totalCount > 0 ? 'Clear visible' : 'Select visible'}
</button>
{selectedCount ? (
<button type="button" onClick={onClear} className="rounded-full border border-white/10 bg-white/[0.04] px-3 py-1 text-slate-200 transition hover:bg-white/[0.07]">
Clear selection
</button>
) : null}
</div>
</div>
<div className="mt-4 flex flex-wrap gap-2 text-xs font-semibold uppercase tracking-[0.12em] text-slate-400">
<span className="rounded-full border border-sky-300/20 bg-sky-400/10 px-3 py-1 text-sky-100">{selectedCount} selected</span>
</div>
{(notice || error) ? (
<div className={`mt-4 rounded-2xl px-4 py-3 text-sm ${error ? 'border border-rose-300/20 bg-rose-400/10 text-rose-100' : 'border border-emerald-300/20 bg-emerald-400/10 text-emerald-100'}`}>
{error || notice}
</div>
) : null}
<div className="mt-4 grid gap-4 lg:grid-cols-4">
<SearchField label="Action">
<select value={form.action} onChange={(event) => onFormChange('action', event.target.value)} className="w-full rounded-2xl border border-white/10 bg-[#09111d] px-4 py-3 text-sm text-white outline-none transition focus:border-sky-300/40">
<option value="archive">Archive</option>
<option value="assign_campaign">Assign campaign</option>
<option value="update_lifecycle">Update lifecycle</option>
<option value="request_ai_review">Request AI review</option>
<option value="mark_editorial_review">Mark editorial review</option>
</select>
</SearchField>
{form.action === 'assign_campaign' ? (
<>
<SearchField label="Campaign key">
<input value={form.campaign_key} onChange={(event) => onFormChange('campaign_key', event.target.value)} placeholder="spring-launch" className="w-full rounded-2xl border border-white/10 bg-[#09111d] px-4 py-3 text-sm text-white outline-none transition placeholder:text-slate-500 focus:border-sky-300/40" />
</SearchField>
<SearchField label="Campaign label">
<input value={form.campaign_label} onChange={(event) => onFormChange('campaign_label', event.target.value)} placeholder="Spring Launch" className="w-full rounded-2xl border border-white/10 bg-[#09111d] px-4 py-3 text-sm text-white outline-none transition placeholder:text-slate-500 focus:border-sky-300/40" />
</SearchField>
</>
) : null}
{form.action === 'update_lifecycle' ? (
<SearchField label="Lifecycle state">
<select value={form.lifecycle_state} onChange={(event) => onFormChange('lifecycle_state', event.target.value)} className="w-full rounded-2xl border border-white/10 bg-[#09111d] px-4 py-3 text-sm text-white outline-none transition focus:border-sky-300/40">
<option value="draft">Draft</option>
<option value="published">Published</option>
<option value="archived">Archived</option>
</select>
</SearchField>
) : null}
<div className="flex items-end">
<button type="button" onClick={onApply} disabled={busy || !selectedCount} className="inline-flex w-full items-center justify-center gap-2 rounded-2xl border border-sky-300/20 bg-sky-400/10 px-4 py-3 text-sm font-semibold text-sky-100 transition hover:bg-sky-400/15 disabled:cursor-not-allowed disabled:opacity-60">
<i className="fa-solid fa-wand-magic-sparkles fa-fw text-[12px]" />Apply action
</button>
</div>
</div>
</div>
)
}
function SearchResults({ state, endpoints, selectedIds, onToggleSelected }) {
function resolve(pattern, collectionId) {
return pattern ? pattern.replace('__COLLECTION__', String(collectionId)) : null
}
if (!state.hasSearched) {
return (
<div className="mt-6 rounded-[26px] border border-dashed border-white/12 bg-white/[0.03] px-6 py-12 text-sm text-slate-300">
Run a search to slice your collection library by workflow, health, visibility, and placement readiness.
</div>
)
}
if (state.error) {
return <div className="mt-6 rounded-[26px] border border-rose-300/20 bg-rose-400/10 px-6 py-4 text-sm text-rose-100">{state.error}</div>
}
if (!state.collections.length) {
return (
<div className="mt-6 rounded-[26px] border border-dashed border-white/12 bg-white/[0.03] px-6 py-12 text-sm text-slate-300">
No collections matched the current filters.
</div>
)
}
return (
<div className="mt-6 space-y-4">
<div className="flex flex-wrap items-center justify-between gap-3">
<div className="flex flex-wrap gap-2">
{Object.entries(state.filters || {}).filter(([, value]) => value !== '' && value !== null && value !== undefined).map(([key, value]) => (
<span key={key} className="rounded-full border border-white/10 bg-white/[0.04] px-3 py-1 text-[11px] font-semibold uppercase tracking-[0.12em] text-slate-300">
{titleize(key)}: {value === '1' ? 'Eligible' : value === '0' ? 'Blocked' : titleize(value)}
</span>
))}
</div>
{state.meta ? <span className="text-xs font-semibold uppercase tracking-[0.18em] text-slate-400">{state.meta.total || state.collections.length} results</span> : null}
</div>
<div className="grid gap-5 xl:grid-cols-2">
{state.collections.map((collection) => (
<div key={collection.id} className="space-y-3 rounded-[28px] border border-white/10 bg-[#0d1726] p-5">
<div className="flex items-center justify-between gap-3">
<label className="inline-flex items-center gap-2 text-xs font-semibold uppercase tracking-[0.12em] text-slate-400">
<input
type="checkbox"
checked={selectedIds.includes(collection.id)}
onChange={() => onToggleSelected(collection.id)}
className="h-4 w-4 rounded border-white/20 bg-[#09111d] text-sky-400 focus:ring-sky-300/30"
/>
Select
</label>
</div>
<CollectionCard collection={collection} isOwner />
<div className="flex flex-wrap gap-2 text-[11px] font-semibold uppercase tracking-[0.12em] text-slate-400">
{collection.workflow_state ? <span className="rounded-full border border-white/10 bg-white/[0.04] px-3 py-1">Workflow: {titleize(collection.workflow_state)}</span> : null}
{collection.health_state ? <span className="rounded-full border border-white/10 bg-white/[0.04] px-3 py-1">Health: {titleize(collection.health_state)}</span> : null}
{collection.placement_eligibility === true ? <span className="rounded-full border border-emerald-300/20 bg-emerald-400/10 px-3 py-1 text-emerald-100">Placement Eligible</span> : null}
{collection.placement_eligibility === false ? <span className="rounded-full border border-rose-300/20 bg-rose-400/10 px-3 py-1 text-rose-100">Placement Blocked</span> : null}
</div>
<div className="flex flex-wrap gap-2">
{resolve(endpoints?.managePattern, collection.id) ? (
<a href={resolve(endpoints.managePattern, collection.id)} className="inline-flex items-center gap-2 rounded-full border border-white/10 bg-white/[0.04] px-4 py-2 text-xs font-semibold text-white transition hover:bg-white/[0.07]">
<i className="fa-solid fa-pen-to-square fa-fw text-[10px]" />Manage
</a>
) : null}
{resolve(endpoints?.analyticsPattern, collection.id) ? (
<a href={resolve(endpoints.analyticsPattern, collection.id)} className="inline-flex items-center gap-2 rounded-full border border-sky-300/20 bg-sky-400/10 px-4 py-2 text-xs font-semibold text-sky-100 transition hover:bg-sky-400/15">
<i className="fa-solid fa-chart-column fa-fw text-[10px]" />Analytics
</a>
) : null}
{resolve(endpoints?.historyPattern, collection.id) ? (
<a href={resolve(endpoints.historyPattern, collection.id)} className="inline-flex items-center gap-2 rounded-full border border-white/10 bg-white/[0.04] px-4 py-2 text-xs font-semibold text-white transition hover:bg-white/[0.07]">
<i className="fa-solid fa-timeline fa-fw text-[10px]" />History
</a>
) : null}
{resolve(endpoints?.healthPattern, collection.id) ? (
<a href={resolve(endpoints.healthPattern, collection.id)} className="inline-flex items-center gap-2 rounded-full border border-amber-300/20 bg-amber-400/10 px-4 py-2 text-xs font-semibold text-amber-100 transition hover:bg-amber-400/15">
<i className="fa-solid fa-shield-heart fa-fw text-[10px]" />Health
</a>
) : null}
</div>
</div>
))}
</div>
</div>
)
}
export default function CollectionDashboard() {
const { props } = usePage()
const [summary, setSummary] = React.useState(props.summary || {})
const topPerforming = Array.isArray(props.topPerforming) ? props.topPerforming : []
const needsAttention = Array.isArray(props.needsAttention) ? props.needsAttention : []
const expiringCampaigns = Array.isArray(props.expiringCampaigns) ? props.expiringCampaigns : []
const healthWarnings = Array.isArray(props.healthWarnings) ? props.healthWarnings : []
const filterOptions = props.filterOptions || {}
const endpoints = props.endpoints || {}
const seo = props.seo || {}
const [searchFilters, setSearchFilters] = React.useState(DEFAULT_SEARCH_FILTERS)
const [searchState, setSearchState] = React.useState({
busy: false,
error: '',
collections: [],
meta: null,
filters: null,
hasSearched: false,
})
const [selectedIds, setSelectedIds] = React.useState([])
const [bulkForm, setBulkForm] = React.useState(DEFAULT_BULK_FORM)
const [bulkState, setBulkState] = React.useState({ busy: false, error: '', notice: '' })
React.useEffect(() => {
setSummary(props.summary || {})
}, [props.summary])
React.useEffect(() => {
const visibleIds = new Set((searchState.collections || []).map((collection) => Number(collection.id)))
setSelectedIds((current) => current.filter((id) => visibleIds.has(Number(id))))
}, [searchState.collections])
function updateFilter(key, value) {
setSearchFilters((current) => ({
...current,
[key]: value,
}))
}
async function handleSearch(event) {
event.preventDefault()
if (!endpoints.search) {
setSearchState({ busy: false, error: 'Search endpoint is unavailable.', collections: [], meta: null, filters: null, hasSearched: true })
return
}
setSearchState((current) => ({ ...current, busy: true, error: '', hasSearched: true }))
try {
const payload = await fetchSearchResults(endpoints.search, searchFilters)
setSearchState({
busy: false,
error: '',
collections: Array.isArray(payload.collections) ? payload.collections : [],
meta: payload.meta || null,
filters: payload.filters || { ...searchFilters },
hasSearched: true,
})
} catch (error) {
setSearchState({ busy: false, error: error.message || 'Search failed.', collections: [], meta: null, filters: null, hasSearched: true })
}
}
function resetSearch() {
setSearchFilters(DEFAULT_SEARCH_FILTERS)
setSearchState({ busy: false, error: '', collections: [], meta: null, filters: null, hasSearched: false })
setSelectedIds([])
}
function updateBulkForm(key, value) {
setBulkForm((current) => ({
...current,
[key]: value,
}))
}
function toggleSelected(collectionId) {
setSelectedIds((current) => (
current.includes(collectionId)
? current.filter((id) => id !== collectionId)
: [...current, collectionId]
))
}
function toggleSelectAllVisible() {
const visibleIds = (searchState.collections || []).map((collection) => Number(collection.id))
if (!visibleIds.length) {
return
}
setSelectedIds((current) => (
current.length === visibleIds.length && visibleIds.every((id) => current.includes(id))
? []
: visibleIds
))
}
async function applyBulkAction() {
if (!selectedIds.length) {
return
}
if (!endpoints.bulkActions) {
setBulkState({ busy: false, error: 'Bulk action endpoint is unavailable.', notice: '' })
return
}
if (bulkForm.action === 'archive' && !window.confirm(`Archive ${selectedIds.length} selected collection${selectedIds.length === 1 ? '' : 's'}?`)) {
return
}
const payload = {
action: bulkForm.action,
collection_ids: selectedIds,
}
if (bulkForm.action === 'assign_campaign') {
payload.campaign_key = bulkForm.campaign_key
payload.campaign_label = bulkForm.campaign_label
}
if (bulkForm.action === 'update_lifecycle') {
payload.lifecycle_state = bulkForm.lifecycle_state
}
setBulkState({ busy: true, error: '', notice: '' })
try {
const response = await requestJson(endpoints.bulkActions, { method: 'POST', body: payload })
const updates = new Map((Array.isArray(response.collections) ? response.collections : []).map((collection) => [Number(collection.id), collection]))
setSearchState((current) => ({
...current,
collections: (current.collections || []).map((collection) => updates.get(Number(collection.id)) || collection),
}))
setSummary(response.summary || summary)
setSelectedIds([])
setBulkState({ busy: false, error: '', notice: response.message || 'Bulk action applied.' })
} catch (error) {
setBulkState({ busy: false, error: error.message || 'Bulk action failed.', notice: '' })
}
}
return (
<>
<Head>
<title>{seo.title || 'Collections Dashboard — Skinbase Nova'}</title>
<meta name="description" content={seo.description || 'Collection lifecycle and performance dashboard.'} />
{seo.canonical ? <link rel="canonical" href={seo.canonical} /> : null}
<meta name="robots" content={seo.robots || 'noindex,follow'} />
</Head>
<div className="relative min-h-screen overflow-hidden pb-16">
<div aria-hidden="true" className="pointer-events-none absolute inset-x-0 top-0 -z-10 h-[34rem] opacity-95" style={{ background: 'radial-gradient(circle at 12% 15%, rgba(56,189,248,0.18), transparent 28%), radial-gradient(circle at 84% 14%, rgba(245,158,11,0.16), transparent 26%), linear-gradient(180deg, #07101d 0%, #0a1220 42%, #08111f 100%)' }} />
<div aria-hidden="true" className="pointer-events-none absolute inset-0 -z-10 opacity-[0.05]" style={{ backgroundImage: 'url(/gfx/noise.png)', backgroundSize: '180px' }} />
<div className="mx-auto max-w-7xl px-4 pt-8 md:px-6">
<section className="overflow-hidden rounded-[34px] border border-white/10 bg-white/[0.04] p-6 shadow-[0_30px_90px_rgba(2,6,23,0.28)] backdrop-blur-sm md:p-8">
<p className="text-[11px] font-semibold uppercase tracking-[0.22em] text-sky-200/80">Operations</p>
<h1 className="mt-3 text-4xl font-semibold tracking-[-0.05em] text-white md:text-5xl">Collections dashboard</h1>
<p className="mt-4 max-w-3xl text-sm leading-relaxed text-slate-300 md:text-[15px]">
A working view of collection health across lifecycle, submissions, quality, and campaign timing. Use it to decide what to publish, repair, archive, or promote next.
</p>
</section>
<section className="mt-8 grid gap-5 md:grid-cols-2 xl:grid-cols-3">
<SummaryCard label="Total Collections" value={summary.total ?? 0} icon="fa-layer-group" tone="sky" />
<SummaryCard label="Drafts" value={summary.drafts ?? 0} icon="fa-file" tone="amber" />
<SummaryCard label="Scheduled" value={summary.scheduled ?? 0} icon="fa-calendar-days" tone="emerald" />
<SummaryCard label="Published" value={summary.published ?? 0} icon="fa-globe" tone="sky" />
<SummaryCard label="Archived" value={summary.archived ?? 0} icon="fa-box-archive" tone="rose" />
<SummaryCard label="Pending Submissions" value={summary.pending_submissions ?? 0} icon="fa-inbox" tone="amber" />
<SummaryCard label="Needs Review" value={summary.needs_review ?? 0} icon="fa-triangle-exclamation" tone="amber" />
<SummaryCard label="Duplicate Risk" value={summary.duplicate_risk ?? 0} icon="fa-clone" tone="rose" />
<SummaryCard label="Placement Blocked" value={summary.placement_blocked ?? 0} icon="fa-ban" tone="rose" />
</section>
<div className="mt-8 space-y-6">
<section className="rounded-[32px] border border-white/10 bg-white/[0.04] p-6 shadow-[0_24px_80px_rgba(2,6,23,0.24)] backdrop-blur-sm md:p-7">
<div className="flex flex-wrap items-center justify-between gap-3">
<div>
<p className="text-[11px] font-semibold uppercase tracking-[0.22em] text-sky-200/80">Search</p>
<h2 className="mt-2 text-2xl font-semibold text-white">Find the exact collections that need action</h2>
</div>
{searchState.busy ? <span className="rounded-full border border-sky-300/20 bg-sky-400/10 px-3 py-1 text-xs font-semibold text-sky-100">Searching...</span> : null}
</div>
<form onSubmit={handleSearch} className="mt-6 grid gap-4 lg:grid-cols-2 xl:grid-cols-4">
<SearchField label="Query" value={searchFilters.q} onChange={(event) => updateFilter('q', event.target.value)}>
<input value={searchFilters.q} onChange={(event) => updateFilter('q', event.target.value)} placeholder="Title, slug, or campaign" className="w-full rounded-2xl border border-white/10 bg-[#09111d] px-4 py-3 text-sm text-white outline-none transition placeholder:text-slate-500 focus:border-sky-300/40" />
</SearchField>
<SearchField label="Type">
<select value={searchFilters.type} onChange={(event) => updateFilter('type', event.target.value)} className="w-full rounded-2xl border border-white/10 bg-[#09111d] px-4 py-3 text-sm text-white outline-none transition focus:border-sky-300/40">
<option value="">Any type</option>
{(Array.isArray(filterOptions.types) ? filterOptions.types : []).map((option) => (
<option key={option} value={option}>{titleize(option)}</option>
))}
</select>
</SearchField>
<SearchField label="Visibility">
<select value={searchFilters.visibility} onChange={(event) => updateFilter('visibility', event.target.value)} className="w-full rounded-2xl border border-white/10 bg-[#09111d] px-4 py-3 text-sm text-white outline-none transition focus:border-sky-300/40">
<option value="">Any visibility</option>
{(Array.isArray(filterOptions.visibilities) ? filterOptions.visibilities : []).map((option) => (
<option key={option} value={option}>{titleize(option)}</option>
))}
</select>
</SearchField>
<SearchField label="Lifecycle">
<select value={searchFilters.lifecycle_state} onChange={(event) => updateFilter('lifecycle_state', event.target.value)} className="w-full rounded-2xl border border-white/10 bg-[#09111d] px-4 py-3 text-sm text-white outline-none transition focus:border-sky-300/40">
<option value="">Any lifecycle</option>
{(Array.isArray(filterOptions.lifecycleStates) ? filterOptions.lifecycleStates : []).map((option) => (
<option key={option} value={option}>{titleize(option)}</option>
))}
</select>
</SearchField>
<SearchField label="Workflow">
<select value={searchFilters.workflow_state} onChange={(event) => updateFilter('workflow_state', event.target.value)} className="w-full rounded-2xl border border-white/10 bg-[#09111d] px-4 py-3 text-sm text-white outline-none transition focus:border-sky-300/40">
<option value="">Any workflow</option>
{(Array.isArray(filterOptions.workflowStates) ? filterOptions.workflowStates : []).map((option) => (
<option key={option} value={option}>{titleize(option)}</option>
))}
</select>
</SearchField>
<SearchField label="Health">
<select value={searchFilters.health_state} onChange={(event) => updateFilter('health_state', event.target.value)} className="w-full rounded-2xl border border-white/10 bg-[#09111d] px-4 py-3 text-sm text-white outline-none transition focus:border-sky-300/40">
<option value="">Any health state</option>
{(Array.isArray(filterOptions.healthStates) ? filterOptions.healthStates : []).map((option) => (
<option key={option} value={option}>{titleize(option)}</option>
))}
</select>
</SearchField>
<SearchField label="Placement">
<select value={searchFilters.placement_eligibility} onChange={(event) => updateFilter('placement_eligibility', event.target.value)} className="w-full rounded-2xl border border-white/10 bg-[#09111d] px-4 py-3 text-sm text-white outline-none transition focus:border-sky-300/40">
<option value="">Any placement state</option>
<option value="1">Eligible</option>
<option value="0">Blocked</option>
</select>
</SearchField>
<div className="flex items-end gap-3 xl:col-span-1">
<button type="submit" disabled={searchState.busy} className="inline-flex flex-1 items-center justify-center gap-2 rounded-2xl border border-sky-300/20 bg-sky-400/10 px-4 py-3 text-sm font-semibold text-sky-100 transition hover:bg-sky-400/15 disabled:cursor-not-allowed disabled:opacity-60">
<i className="fa-solid fa-magnifying-glass fa-fw text-[12px]" />Search
</button>
<button type="button" onClick={resetSearch} disabled={searchState.busy} className="inline-flex items-center justify-center gap-2 rounded-2xl border border-white/10 bg-white/[0.04] px-4 py-3 text-sm font-semibold text-white transition hover:bg-white/[0.07] disabled:cursor-not-allowed disabled:opacity-60">
Reset
</button>
</div>
</form>
<BulkActionsPanel
selectedCount={selectedIds.length}
totalCount={(searchState.collections || []).length}
form={bulkForm}
onFormChange={updateBulkForm}
onApply={applyBulkAction}
onClear={() => setSelectedIds([])}
onToggleAll={toggleSelectAllVisible}
busy={bulkState.busy}
error={bulkState.error}
notice={bulkState.notice}
/>
<SearchResults state={searchState} endpoints={endpoints} selectedIds={selectedIds} onToggleSelected={toggleSelected} />
</section>
<WarningList warnings={healthWarnings} endpoints={endpoints} />
<CollectionStrip title="Top Performing" eyebrow="Momentum" collections={topPerforming} emptyLabel="No collections have enough activity yet to rank here." endpoints={endpoints} />
<CollectionStrip title="Needs Attention" eyebrow="Quality" collections={needsAttention} emptyLabel="No collections currently need manual intervention." endpoints={endpoints} />
<CollectionStrip title="Expiring Campaigns" eyebrow="Timing" collections={expiringCampaigns} emptyLabel="No campaigns are approaching their sunset window." endpoints={endpoints} />
</div>
</div>
</div>
</>
)
}