import React from 'react'
import { Head, usePage } from '@inertiajs/react'
import CollectionCard from '../../components/profile/collections/CollectionCard'
import Checkbox from '../../components/ui/Checkbox'
import NovaSelect from '../../components/ui/NovaSelect'
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 (
)
}
function CollectionStrip({ title, eyebrow, collections, emptyLabel, endpoints }) {
function resolve(pattern, collectionId) {
return pattern ? pattern.replace('__COLLECTION__', String(collectionId)) : null
}
return (
{collections.length ? (
{collections.map((collection) => (
{resolve(endpoints?.managePattern, collection.id) ? (
Manage
) : null}
{resolve(endpoints?.analyticsPattern, collection.id) ? (
Analytics
) : null}
{resolve(endpoints?.historyPattern, collection.id) ? (
History
) : null}
))}
) : (
{emptyLabel}
)}
)
}
function WarningList({ warnings, endpoints }) {
function resolve(pattern, collectionId) {
return pattern ? pattern.replace('__COLLECTION__', String(collectionId)) : null
}
if (!warnings.length) {
return null
}
return (
Health
Warnings and blockers
{warnings.length}
{warnings.map((warning) => (
{warning.title}
State: {warning.health?.health_state || 'unknown'}
{warning.health?.health_score ?? 'n/a'}
{Array.isArray(warning.health?.flags) && warning.health.flags.length ? (
{warning.health.flags.map((flag) => (
{flag}
))}
) : null}
{resolve(endpoints?.managePattern, warning.collection_id) ?
Manage : null}
{resolve(endpoints?.healthPattern, warning.collection_id) ?
Health JSON : null}
))}
)
}
function SearchField({ label, value, onChange, children }) {
return (
{label}
{children || (
)}
)
}
function BulkActionsPanel({
selectedCount,
totalCount,
form,
onFormChange,
onApply,
onClear,
onToggleAll,
busy,
error,
notice,
}) {
if (!selectedCount && !notice && !error) {
return null
}
return (
Bulk actions
Apply safe actions to selected collections
{selectedCount === totalCount && totalCount > 0 ? 'Clear visible' : 'Select visible'}
{selectedCount ? (
Clear selection
) : null}
{selectedCount} selected
{(notice || error) ? (
{error || notice}
) : null}
onFormChange('action', val)} searchable={false} options={[{ value: 'archive', label: 'Archive' }, { value: 'assign_campaign', label: 'Assign campaign' }, { value: 'update_lifecycle', label: 'Update lifecycle' }, { value: 'request_ai_review', label: 'Request AI review' }, { value: 'mark_editorial_review', label: 'Mark editorial review' }]} />
{form.action === 'assign_campaign' ? (
<>
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" />
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" />
>
) : null}
{form.action === 'update_lifecycle' ? (
onFormChange('lifecycle_state', val)} searchable={false} options={[{ value: 'draft', label: 'Draft' }, { value: 'published', label: 'Published' }, { value: 'archived', label: 'Archived' }]} />
) : null}
Apply action
)
}
function SearchResults({ state, endpoints, selectedIds, onToggleSelected }) {
function resolve(pattern, collectionId) {
return pattern ? pattern.replace('__COLLECTION__', String(collectionId)) : null
}
if (!state.hasSearched) {
return (
Run a search to slice your collection library by workflow, health, visibility, and placement readiness.
)
}
if (state.error) {
return {state.error}
}
if (!state.collections.length) {
return (
No collections matched the current filters.
)
}
return (
{Object.entries(state.filters || {}).filter(([, value]) => value !== '' && value !== null && value !== undefined).map(([key, value]) => (
{titleize(key)}: {value === '1' ? 'Eligible' : value === '0' ? 'Blocked' : titleize(value)}
))}
{state.meta ?
{state.meta.total || state.collections.length} results : null}
{state.collections.map((collection) => (
onToggleSelected(collection.id)}
label="Select"
/>
{collection.workflow_state ? Workflow: {titleize(collection.workflow_state)} : null}
{collection.health_state ? Health: {titleize(collection.health_state)} : null}
{collection.placement_eligibility === true ? Placement Eligible : null}
{collection.placement_eligibility === false ? Placement Blocked : null}
{resolve(endpoints?.managePattern, collection.id) ? (
Manage
) : null}
{resolve(endpoints?.analyticsPattern, collection.id) ? (
Analytics
) : null}
{resolve(endpoints?.historyPattern, collection.id) ? (
History
) : null}
{resolve(endpoints?.healthPattern, collection.id) ? (
Health
) : null}
))}
)
}
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 (
<>
{seo.title || 'Collections Dashboard — Skinbase'}
{seo.canonical ? : null}
Operations
Collections dashboard
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.
Search
Find the exact collections that need action
{searchState.busy ?
Searching... : null}
setSelectedIds([])}
onToggleAll={toggleSelectAllVisible}
busy={bulkState.busy}
error={bulkState.error}
notice={bulkState.notice}
/>
>
)
}