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 (
)
}
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}
)
}
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)}
className="h-4 w-4 rounded border-white/20 bg-[#09111d] text-sky-400 focus:ring-sky-300/30"
/>
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 Nova'}
{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}
/>
>
)
}