import React from 'react'
import { usePage } from '@inertiajs/react'
import ArtworkGallery from '../../components/artwork/ArtworkGallery'
import CollectionCard from '../../components/profile/collections/CollectionCard'
import CollectionVisibilityBadge from '../../components/profile/collections/CollectionVisibilityBadge'
import NovaSelect from '../../components/ui/NovaSelect'
import SeoHead from '../../components/seo/SeoHead'
import CommentForm from '../../components/social/CommentForm'
import CommentList from '../../components/social/CommentList'
import useWebShare from '../../hooks/useWebShare'
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 || 'Request failed.')
}
return payload
}
function TypeBadge({ collection }) {
const label = collection?.type === 'editorial'
? 'Editorial'
: collection?.type === 'community'
? 'Community'
: 'Personal'
return {label}
}
const COLLABORATOR_ROLE_COLORS = {
owner: 'border-amber-300/20 bg-amber-400/10 text-amber-200',
moderator: 'border-sky-300/20 bg-sky-400/10 text-sky-200',
contributor: 'border-emerald-300/20 bg-emerald-400/10 text-emerald-200',
curator: 'border-violet-300/20 bg-violet-400/10 text-violet-200',
}
function CollaboratorCard({ member }) {
const roleColor = COLLABORATOR_ROLE_COLORS[String(member?.role || '').toLowerCase()] ?? 'border-white/10 bg-white/[0.05] text-slate-300'
return (
{member?.user?.avatar_url ? (
) : (
)}
{member?.user?.name || member?.user?.username}
{member?.user?.username ?
@{member.user.username}
: null}
{member?.role}{member?.status === 'pending' ? ' · invited' : ''}
)
}
function SubmissionCard({ submission, onApprove, onReject, onWithdraw, onReport }) {
return (
{submission?.artwork?.thumb ?
: null}
{submission?.artwork?.title || 'Artwork submission'}
{submission?.status}
Submitted by @{submission?.user?.username}
{submission?.message ?
{submission.message}
: null}
{submission?.can_review ? onApprove?.(submission)} className="rounded-full border border-emerald-400/20 bg-emerald-400/10 px-3 py-2 text-xs font-semibold text-emerald-100">Approve : null}
{submission?.can_review ? onReject?.(submission)} className="rounded-full border border-rose-400/20 bg-rose-400/10 px-3 py-2 text-xs font-semibold text-rose-100">Reject : null}
{submission?.can_withdraw ? onWithdraw?.(submission)} className="rounded-full border border-white/12 bg-white/[0.05] px-3 py-2 text-xs font-semibold text-white">Withdraw : null}
{submission?.can_report ? onReport?.(submission)} className="rounded-full border border-white/12 bg-white/[0.05] px-3 py-2 text-xs font-semibold text-white">Report : null}
)
}
const METAROW_TONES = {
'fa-images': { icon: 'text-sky-300', bg: 'bg-sky-400/10 border-sky-300/20', bar: 'from-sky-400/60' },
'fa-heart': { icon: 'text-rose-300', bg: 'bg-rose-400/10 border-rose-300/20', bar: 'from-rose-400/60' },
'fa-bell': { icon: 'text-emerald-300', bg: 'bg-emerald-400/10 border-emerald-300/20', bar: 'from-emerald-400/60' },
'fa-eye': { icon: 'text-violet-300', bg: 'bg-violet-400/10 border-violet-300/20', bar: 'from-violet-400/60' },
'fa-bookmark': { icon: 'text-amber-300', bg: 'bg-amber-400/10 border-amber-300/20', bar: 'from-amber-400/60' },
'fa-panorama': { icon: 'text-slate-300', bg: 'bg-white/[0.05] border-white/10', bar: 'from-slate-400/40' },
'fa-gauge-high': { icon: 'text-teal-300', bg: 'bg-teal-400/10 border-teal-300/20', bar: 'from-teal-400/60' },
'fa-ranking-star': { icon: 'text-amber-300', bg: 'bg-amber-400/10 border-amber-300/20', bar: 'from-amber-400/60' },
'fa-bullhorn': { icon: 'text-orange-300', bg: 'bg-orange-400/10 border-orange-300/20', bar: 'from-orange-400/60' },
}
function MetaRow({ icon, label, value, compact = false }) {
const title = `${label}: ${value}`
const tone = METAROW_TONES[icon] ?? { icon: 'text-slate-300', bg: 'bg-white/[0.05] border-white/10', bar: 'from-slate-400/40' }
if (compact) {
return (
)
}
return (
)
}
const HERO_ACTION_TONES = {
neutral: {
idle: 'border-white/10 bg-white/[0.05] text-white hover:border-white/20 hover:bg-white/[0.09]',
active: 'border-white/15 bg-[linear-gradient(135deg,rgba(255,255,255,0.12),rgba(255,255,255,0.04))] text-white shadow-[0_18px_40px_rgba(2,6,23,0.18)]',
icon: 'border-white/10 bg-white/[0.08] text-slate-200',
iconActive: 'border-white/15 bg-white/[0.12] text-white',
},
rose: {
idle: 'border-white/10 bg-white/[0.05] text-white hover:border-rose-400/30 hover:bg-rose-400/[0.12] hover:text-rose-100',
active: 'border-rose-400/30 bg-[linear-gradient(135deg,rgba(244,63,94,0.18),rgba(255,255,255,0.06))] text-rose-50 shadow-[0_18px_40px_rgba(244,63,94,0.14)]',
icon: 'border-rose-300/15 bg-rose-400/[0.08] text-rose-200',
iconActive: 'border-rose-300/30 bg-rose-400/[0.16] text-rose-50',
},
emerald: {
idle: 'border-white/10 bg-white/[0.05] text-white hover:border-emerald-400/30 hover:bg-emerald-400/[0.12] hover:text-emerald-100',
active: 'border-emerald-400/30 bg-[linear-gradient(135deg,rgba(52,211,153,0.18),rgba(255,255,255,0.06))] text-emerald-50 shadow-[0_18px_40px_rgba(52,211,153,0.14)]',
icon: 'border-emerald-300/15 bg-emerald-400/[0.08] text-emerald-200',
iconActive: 'border-emerald-300/30 bg-emerald-400/[0.16] text-emerald-50',
},
violet: {
idle: 'border-white/10 bg-white/[0.05] text-white hover:border-violet-400/30 hover:bg-violet-400/[0.12] hover:text-violet-100',
active: 'border-violet-400/30 bg-[linear-gradient(135deg,rgba(167,139,250,0.18),rgba(255,255,255,0.06))] text-violet-50 shadow-[0_18px_40px_rgba(167,139,250,0.14)]',
icon: 'border-violet-300/15 bg-violet-400/[0.08] text-violet-200',
iconActive: 'border-violet-300/30 bg-violet-400/[0.16] text-violet-50',
},
sky: {
idle: 'border-white/10 bg-white/[0.05] text-white hover:border-sky-400/30 hover:bg-sky-400/[0.12] hover:text-sky-100',
active: 'border-sky-400/30 bg-[linear-gradient(135deg,rgba(56,189,248,0.18),rgba(255,255,255,0.06))] text-sky-50 shadow-[0_18px_40px_rgba(56,189,248,0.14)]',
icon: 'border-sky-300/15 bg-sky-400/[0.08] text-sky-200',
iconActive: 'border-sky-300/30 bg-sky-400/[0.16] text-sky-50',
},
amber: {
idle: 'border-white/10 bg-white/[0.05] text-white hover:border-amber-400/30 hover:bg-amber-400/[0.12] hover:text-amber-100',
active: 'border-amber-400/30 bg-[linear-gradient(135deg,rgba(251,191,36,0.18),rgba(255,255,255,0.06))] text-amber-50 shadow-[0_18px_40px_rgba(251,191,36,0.14)]',
icon: 'border-amber-300/15 bg-amber-400/[0.08] text-amber-200',
iconActive: 'border-amber-300/30 bg-amber-400/[0.16] text-amber-50',
},
}
function CollectionHeroAction({ href = null, onClick = null, icon, label, tone = 'neutral', active = false, disabled = false, compact = false }) {
const toneClasses = HERO_ACTION_TONES[tone] ?? HERO_ACTION_TONES.neutral
const Component = href ? 'a' : 'button'
const componentProps = href
? { href }
: { type: 'button', onClick, disabled }
return (
{label}
)
}
const HERO_METRIC_TONES = {
sky: {
icon: 'text-sky-200',
chip: 'border-sky-300/20 bg-sky-400/[0.12]',
glow: 'from-sky-400/35',
orb: 'bg-sky-400/20',
},
rose: {
icon: 'text-rose-200',
chip: 'border-rose-300/20 bg-rose-400/[0.12]',
glow: 'from-rose-400/35',
orb: 'bg-rose-400/20',
},
emerald: {
icon: 'text-emerald-200',
chip: 'border-emerald-300/20 bg-emerald-400/[0.12]',
glow: 'from-emerald-400/35',
orb: 'bg-emerald-400/20',
},
violet: {
icon: 'text-violet-200',
chip: 'border-violet-300/20 bg-violet-400/[0.12]',
glow: 'from-violet-400/35',
orb: 'bg-violet-400/20',
},
amber: {
icon: 'text-amber-200',
chip: 'border-amber-300/20 bg-amber-400/[0.12]',
glow: 'from-amber-400/35',
orb: 'bg-amber-400/20',
},
slate: {
icon: 'text-slate-200',
chip: 'border-white/10 bg-white/[0.08]',
glow: 'from-white/20',
orb: 'bg-white/[0.12]',
},
}
function HeroMetricCard({ icon, label, value, helper = null, tone = 'slate' }) {
const style = HERO_METRIC_TONES[tone] ?? HERO_METRIC_TONES.slate
return (
{value}
{label}
{helper ?
{helper}
: null}
)
}
function HeroSignalCard({ icon, label, value, description = null, tone = 'slate' }) {
const style = HERO_METRIC_TONES[tone] ?? HERO_METRIC_TONES.slate
return (
{label}
{value}
{description ?
{description}
: null}
)
}
function getSpotlightClasses(style) {
switch (style) {
case 'editorial':
return 'border-amber-300/20 bg-[linear-gradient(135deg,rgba(120,53,15,0.45),rgba(2,6,23,0.82))] text-amber-50'
case 'seasonal':
return 'border-emerald-300/20 bg-[linear-gradient(135deg,rgba(6,78,59,0.5),rgba(2,6,23,0.82))] text-emerald-50'
case 'challenge':
return 'border-fuchsia-300/20 bg-[linear-gradient(135deg,rgba(112,26,117,0.48),rgba(2,6,23,0.82))] text-fuchsia-50'
case 'community':
return 'border-sky-300/20 bg-[linear-gradient(135deg,rgba(3,105,161,0.45),rgba(2,6,23,0.82))] text-sky-50'
default:
return 'border-white/10 bg-[linear-gradient(135deg,rgba(15,23,42,0.9),rgba(30,41,59,0.72))] text-white'
}
}
function EmptyCollectionState({ isOwner, smart = false }) {
return (
This collection is still taking shape
{isOwner
? (smart ? 'Adjust the smart rules to broaden the match or publish more artworks that fit this set.' : 'Add artworks to start building the visual rhythm, cover image, and sequence for this showcase.')
: (smart ? 'This smart collection does not have visible matches right now.' : 'There are no visible artworks in this collection right now.')}
)
}
function OwnerCard({ owner, collectionType }) {
const label = owner?.is_system
? 'Editorial Owner'
: collectionType === 'editorial'
? 'Editorial Curator'
: 'Curator'
const body = (
{owner?.avatar_url ? (
) : (
)}
{label}
{owner?.name || owner?.username || 'Skinbase Curator'}
{owner?.username ?
@{owner.username}
: null}
{owner?.profile_url ?
: null}
)
if (owner?.profile_url) {
return (
)
}
return (
)
}
function PageSection({ eyebrow, title, count, icon, children }) {
return (
{count !== undefined ?
{count} : null}
{children}
)
}
function EntityLinkCard({ item }) {
return (
{item?.image_url ?
:
}
{item?.title}
{item?.meta ? {item.meta} : null}
{item?.subtitle ?
{item.subtitle}
: null}
{item?.relationship_type ?
{item.relationship_type}
: null}
{item?.description ? {item.description}
: null}
)
}
function CollectionCover({ collection }) {
const coverImage = collection?.cover_image
const coverMaturity = collection?.cover_image_maturity || null
const shouldBlur = Boolean(coverMaturity?.should_blur)
const isMature = Boolean(coverMaturity?.is_mature_effective)
if (!coverImage) {
return
}
return (
{isMature ?
Mature cover
: null}
{shouldBlur ?
Blurred by your settings
: null}
)
}
function humanizeToken(value) {
return String(value || '')
.replaceAll('_', ' ')
.replaceAll('-', ' ')
.replace(/\b\w/g, (match) => match.toUpperCase())
}
function groupEntityLinks(items) {
return (Array.isArray(items) ? items : []).reduce((groups, item) => {
const key = item?.linked_type || 'other'
if (!groups[key]) groups[key] = []
groups[key].push(item)
return groups
}, {})
}
function recommendationReasons(currentCollection, candidate) {
const reasons = []
if (candidate?.event_key && currentCollection?.event_key && candidate.event_key === currentCollection.event_key) {
reasons.push('Same event context')
}
if (candidate?.campaign_key && currentCollection?.campaign_key && candidate.campaign_key === currentCollection.campaign_key) {
reasons.push('Same campaign')
}
if (candidate?.theme_token && currentCollection?.theme_token && candidate.theme_token === currentCollection.theme_token) {
reasons.push('Shared theme')
}
if (candidate?.type && currentCollection?.type && candidate.type === currentCollection.type) {
reasons.push(`${humanizeToken(candidate.type)} collection`)
}
if (candidate?.owner?.id && currentCollection?.owner?.id && candidate.owner.id === currentCollection.owner.id) {
reasons.push('Same curator')
}
if (candidate?.trust_tier && currentCollection?.trust_tier && candidate.trust_tier === currentCollection.trust_tier) {
reasons.push(`${humanizeToken(candidate.trust_tier)} trust tier`)
}
return reasons.slice(0, 3)
}
const CONTEXT_SIGNAL_TYPES = {
Campaign: { icon: 'fa-solid fa-bullhorn', accent: 'border-orange-300/20 from-orange-400/50', badge: 'border-orange-300/20 bg-orange-400/10 text-orange-200', kicker: 'text-orange-300/80' },
Event: { icon: 'fa-solid fa-calendar-star', accent: 'border-sky-300/20 from-sky-400/50', badge: 'border-sky-300/20 bg-sky-400/10 text-sky-200', kicker: 'text-sky-300/80' },
Program: { icon: 'fa-solid fa-layer-group', accent: 'border-violet-300/20 from-violet-400/50', badge: 'border-violet-300/20 bg-violet-400/10 text-violet-200', kicker: 'text-violet-300/80' },
Theme: { icon: 'fa-solid fa-palette', accent: 'border-teal-300/20 from-teal-400/50', badge: 'border-teal-300/20 bg-teal-400/10 text-teal-200', kicker: 'text-teal-300/80' },
'Quality Tier': { icon: 'fa-solid fa-gauge-high', accent: 'border-amber-300/20 from-amber-400/50', badge: 'border-amber-300/20 bg-amber-400/10 text-amber-200', kicker: 'text-amber-300/80' },
}
function ContextSignalCard({ item }) {
const typeStyle = CONTEXT_SIGNAL_TYPES[item.meta] ?? { icon: 'fa-solid fa-circle-info', accent: 'border-white/10 from-slate-400/30', badge: 'border-white/10 bg-white/[0.05] text-slate-300', kicker: 'text-sky-200/80' }
const wrapperClassName = `relative overflow-hidden flex h-full flex-col gap-3 rounded-[24px] border ${typeStyle.accent.split(' ')[0]} bg-white/[0.04] p-5 transition hover:bg-white/[0.07]`
const body = (
<>
{item.kicker ?
{item.kicker} : null}
{item.title}
{item.subtitle ?
{item.subtitle}
: null}
{item.description ? {item.description}
: null}
>
)
if (item.url) {
return {body}
}
return {body}
}
export default function CollectionShow() {
const { props } = usePage()
const {
collection: initialCollection,
artworks,
owner,
isOwner,
manageUrl,
editUrl,
analyticsUrl,
historyUrl,
profileCollectionsUrl,
featuredCollectionsUrl,
engagement,
seo,
members: initialMembers,
comments: initialComments,
submissions: initialSubmissions,
entityLinks,
relatedCollections,
commentsEndpoint,
submitEndpoint,
submissionArtworkOptions,
seriesContext,
canSubmit,
canComment,
reportEndpoint,
} = props
const [collection, setCollection] = React.useState(initialCollection)
const [comments, setComments] = React.useState(initialComments || [])
const [submissions, setSubmissions] = React.useState(initialSubmissions || [])
const [selectedArtworkId, setSelectedArtworkId] = React.useState(submissionArtworkOptions?.[0]?.id || '')
const [state, setState] = React.useState({
liked: Boolean(engagement?.liked),
following: Boolean(engagement?.following),
saved: Boolean(engagement?.saved),
notice: '',
busy: false,
})
const artworkItems = artworks?.data ?? []
const creatorIds = Array.from(new Set(artworkItems.map((artwork) => artwork?.author?.id).filter(Boolean)))
const featuringCreatorsCount = creatorIds.length
const showArtworkAuthors = collection?.type !== 'personal' || featuringCreatorsCount > 1
const enabledModules = Array.isArray(collection?.layout_modules)
? collection.layout_modules.filter((module) => module?.enabled !== false)
: []
const enabledModuleKeys = new Set(enabledModules.map((module) => module?.key).filter(Boolean))
const showIntroBlock = enabledModuleKeys.size === 0 || enabledModuleKeys.has('intro_block')
const metaOwnerName = owner?.name || owner?.username || collection?.owner?.name || 'Skinbase Curator'
const metaTitle = seo?.title || `${collection?.title} — Skinbase`
const metaDescription = seo?.description || collection?.summary || collection?.description || ''
const collectionSchema = seo?.canonical ? {
'@context': 'https://schema.org',
'@type': 'CollectionPage',
name: collection?.title,
description: metaDescription,
url: seo.canonical,
image: seo?.og_image || collection?.cover_image || undefined,
isPartOf: {
'@type': 'WebSite',
name: 'Skinbase',
url: typeof window !== 'undefined' ? window.location.origin : undefined,
},
author: owner ? {
'@type': 'Person',
name: metaOwnerName,
url: owner.profile_url,
} : undefined,
keywords: [collection?.type, collection?.mode, collection?.badge_label].filter(Boolean).join(', ') || undefined,
mainEntity: {
'@type': 'ItemList',
numberOfItems: collection?.artworks_count || artworkItems.length || 0,
itemListElement: artworkItems.slice(0, 12).map((artwork, index) => ({
'@type': 'ListItem',
position: index + 1,
url: artwork.url,
name: artwork.title,
})),
},
} : null
const [members] = React.useState(initialMembers || [])
const spotlightClasses = getSpotlightClasses(collection?.spotlight_style)
const groupedEntityLinks = groupEntityLinks(entityLinks)
const storyLinks = groupedEntityLinks.story || []
const taxonomyLinks = [...(groupedEntityLinks.category || []), ...(groupedEntityLinks.tag || [])]
const contributorLinks = [...(groupedEntityLinks.creator || []), ...(groupedEntityLinks.artwork || [])]
const linkedContextSignals = [
...(groupedEntityLinks.campaign || []).map((item) => ({
meta: item.meta || 'Campaign',
kicker: item.relationship_type || 'Linked campaign',
title: item.title,
subtitle: item.subtitle,
description: item.description,
url: item.url,
})),
...(groupedEntityLinks.event || []).map((item) => ({
meta: item.meta || 'Event',
kicker: item.relationship_type || 'Linked event',
title: item.title,
subtitle: item.subtitle,
description: item.description,
url: item.url,
})),
]
const contextSignals = [
collection?.campaign_key ? {
meta: 'Campaign',
kicker: 'Discover surface',
title: collection.campaign_label || humanizeToken(collection.campaign_key),
subtitle: collection.campaign_key,
description: 'This collection is programmed into a campaign surface and can be explored alongside other campaign-ready sets.',
url: `/collections/campaigns/${encodeURIComponent(collection.campaign_key)}`,
} : null,
collection?.program_key ? {
meta: 'Program',
kicker: 'Partner context',
title: humanizeToken(collection.program_key),
subtitle: collection.partner_label || collection.partner_key || 'Collection program',
description: 'This collection is attached to a program or partner-ready surface, which affects how it is grouped and surfaced.',
url: `/collections/program/${encodeURIComponent(collection.program_key)}`,
} : null,
(collection?.event_label || collection?.event_key) ? {
meta: 'Event',
kicker: 'Seasonal context',
title: collection.event_label || humanizeToken(collection.event_key),
subtitle: collection.season_key ? `Season ${humanizeToken(collection.season_key)}` : 'Event-linked collection',
description: 'This collection is tied to an event or seasonal programming window, so related recommendations favor matching event context.',
url: null,
} : null,
collection?.theme_token ? {
meta: 'Theme',
kicker: 'Visual language',
title: humanizeToken(collection.theme_token),
subtitle: collection.presentation_style ? `Presentation ${humanizeToken(collection.presentation_style)}` : null,
description: 'Theme and presentation signals help similar collections cluster together in discovery and recommendation surfaces.',
url: `/collections/search?theme=${encodeURIComponent(collection.theme_token)}`,
} : null,
collection?.trust_tier ? {
meta: 'Quality Tier',
kicker: 'Placement signal',
title: humanizeToken(collection.trust_tier),
subtitle: collection?.quality_score != null ? `Quality score ${Number(collection.quality_score).toFixed(1)}` : null,
description: 'Trust tier and quality score shape how aggressively this collection can be used in premium or partner-facing placements.',
url: `/collections/search?quality_tier=${encodeURIComponent(collection.trust_tier)}`,
} : null,
...linkedContextSignals,
].filter(Boolean).filter((item, index, items) => {
const key = `${item.meta}:${item.title}:${item.subtitle || ''}`
return items.findIndex((candidate) => `${candidate.meta}:${candidate.title}:${candidate.subtitle || ''}` === key) === index
})
const heroMetrics = [
{
icon: 'fa-images',
label: 'Artworks',
value: (collection?.artworks_count ?? 0).toLocaleString(),
helper: showArtworkAuthors && featuringCreatorsCount > 1 ? `${featuringCreatorsCount} creators featured` : (collection?.mode === 'smart' ? 'Matched works' : 'Published pieces'),
tone: 'sky',
},
{
icon: 'fa-heart',
label: 'Likes',
value: (collection?.likes_count ?? 0).toLocaleString(),
helper: 'Community response',
tone: 'rose',
},
{
icon: 'fa-bell',
label: 'Followers',
value: (collection?.followers_count ?? 0).toLocaleString(),
helper: 'Watching updates',
tone: 'emerald',
},
{
icon: 'fa-eye',
label: 'Views',
value: (collection?.views_count ?? 0).toLocaleString(),
helper: 'Detail visits',
tone: 'violet',
},
{
icon: 'fa-bookmark',
label: 'Saves',
value: (collection?.saves_count ?? 0).toLocaleString(),
helper: 'Pinned for later',
tone: 'amber',
},
]
const heroSignals = [
collection?.quality_score != null ? {
icon: 'fa-gauge-high',
label: 'Quality',
value: Number(collection.quality_score).toFixed(1),
description: collection?.trust_tier ? `${humanizeToken(collection.trust_tier)} placement tier` : 'Placement quality signal',
tone: 'emerald',
} : null,
collection?.ranking_score != null ? {
icon: 'fa-ranking-star',
label: 'Ranking',
value: Number(collection.ranking_score).toFixed(1),
description: 'Current discovery momentum score',
tone: 'amber',
} : null,
collection?.presentation_style && collection.presentation_style !== 'standard' ? {
icon: 'fa-panorama',
label: 'Presentation',
value: humanizeToken(collection.presentation_style),
description: 'Visual treatment for this collection surface',
tone: 'sky',
} : null,
collection?.campaign_key ? {
icon: 'fa-bullhorn',
label: 'Campaign',
value: collection.campaign_label || humanizeToken(collection.campaign_key),
description: 'Programmed into a campaign surface',
tone: 'rose',
} : null,
].filter(Boolean)
const { share } = useWebShare({
onFallback: async ({ url }) => {
if (navigator?.clipboard?.writeText) {
await navigator.clipboard.writeText(url)
setState((current) => ({ ...current, notice: 'Collection link copied.' }))
}
},
})
async function handleLike() {
if (!engagement?.can_interact) {
if (engagement?.login_url) window.location.assign(engagement.login_url)
return
}
setState((current) => ({ ...current, busy: true, notice: '' }))
try {
const payload = await requestJson(state.liked ? engagement.unlike_url : engagement.like_url, {
method: state.liked ? 'DELETE' : 'POST',
})
setState((current) => ({ ...current, liked: Boolean(payload?.liked), busy: false }))
setCollection((current) => ({ ...current, likes_count: payload?.likes_count ?? current.likes_count }))
} catch (error) {
setState((current) => ({ ...current, busy: false, notice: error.message }))
}
}
async function handleFollow() {
if (!engagement?.can_interact) {
if (engagement?.login_url) window.location.assign(engagement.login_url)
return
}
setState((current) => ({ ...current, busy: true, notice: '' }))
try {
const payload = await requestJson(state.following ? engagement.unfollow_url : engagement.follow_url, {
method: state.following ? 'DELETE' : 'POST',
})
setState((current) => ({ ...current, following: Boolean(payload?.following), busy: false }))
setCollection((current) => ({ ...current, followers_count: payload?.followers_count ?? current.followers_count }))
} catch (error) {
setState((current) => ({ ...current, busy: false, notice: error.message }))
}
}
async function handleShare() {
try {
const payload = await requestJson(engagement?.share_url, { method: 'POST' })
setCollection((current) => ({ ...current, shares_count: payload?.shares_count ?? current.shares_count }))
await share({
title: collection?.title,
text: collection?.summary || collection?.description || `Explore ${collection?.title} on Skinbase.`,
url: collection?.public_url,
})
} catch (error) {
setState((current) => ({ ...current, notice: error.message }))
}
}
async function handleSave() {
if (!engagement?.save_url) {
if (engagement?.login_url) window.location.assign(engagement.login_url)
return
}
setState((current) => ({ ...current, busy: true, notice: '' }))
try {
const payload = await requestJson(state.saved ? engagement.unsave_url : engagement.save_url, {
method: state.saved ? 'DELETE' : 'POST',
body: state.saved ? undefined : {
context: 'collection_detail',
context_meta: {
collection_type: collection?.type || null,
collection_mode: collection?.mode || null,
},
},
})
setState((current) => ({ ...current, saved: Boolean(payload?.saved), busy: false }))
setCollection((current) => ({ ...current, saves_count: payload?.saves_count ?? current.saves_count }))
} catch (error) {
setState((current) => ({ ...current, busy: false, notice: error.message }))
}
}
async function handleCommentSubmit(body) {
const payload = await requestJson(commentsEndpoint, {
method: 'POST',
body: { body },
})
setComments(payload?.comments || [])
setCollection((current) => ({ ...current, comments_count: payload?.comments_count ?? current.comments_count }))
}
async function handleDeleteComment(commentId) {
const payload = await requestJson(`${commentsEndpoint}/${commentId}`, { method: 'DELETE' })
setComments(payload?.comments || [])
setCollection((current) => ({ ...current, comments_count: payload?.comments_count ?? current.comments_count }))
}
async function handleSubmitArtwork() {
if (!submitEndpoint || !selectedArtworkId) return
try {
const payload = await requestJson(submitEndpoint, {
method: 'POST',
body: { artwork_id: selectedArtworkId },
})
setSubmissions(payload?.submissions || [])
setState((current) => ({ ...current, notice: 'Artwork submitted for review.' }))
} catch (error) {
setState((current) => ({ ...current, notice: error.message }))
}
}
async function handleSubmissionAction(submission, action) {
const url = action === 'approve'
? `/collections/submissions/${submission.id}/approve`
: action === 'reject'
? `/collections/submissions/${submission.id}/reject`
: `/collections/submissions/${submission.id}`
const payload = await requestJson(url, {
method: action === 'withdraw' ? 'DELETE' : 'POST',
})
setSubmissions(payload?.submissions || [])
if (action === 'approve') {
setCollection((current) => ({ ...current, artworks_count: (current?.artworks_count ?? 0) + 1 }))
}
}
async function handleReport(targetType, targetId) {
if (!reportEndpoint) {
if (engagement?.login_url) window.location.assign(engagement.login_url)
return
}
const reason = window.prompt('Why are you reporting this? (required)')
if (!reason || !reason.trim()) return
try {
await requestJson(reportEndpoint, {
method: 'POST',
body: {
target_type: targetType,
target_id: targetId,
reason: reason.trim(),
},
})
setState((current) => ({ ...current, notice: 'Report submitted. Thank you.' }))
} catch (error) {
setState((current) => ({ ...current, notice: error.message }))
}
}
function renderModule(module) {
if (!module?.key) return null
if (module.key === 'intro_block') {
return null
}
if (module.key === 'featured_artworks') {
if (!artworkItems.length) return null
return (
Start with the standout pieces from this collection before diving into the full sequence.
)
}
if (module.key === 'editorial_note') {
if (collection?.type !== 'editorial') return null
return (
{collection?.description || 'A staff-curated collection prepared for premium discovery placement.'}
{(collection?.event_label || collection?.badge_label) ? (
{collection?.event_label ? {collection.event_label} : null}
{collection?.badge_label ? {collection.badge_label} : null}
) : null}
)
}
if (module.key === 'artwork_grid') {
return (
{artworkItems.length ? : }
{(artworks?.links?.prev || artworks?.links?.next) ? (
) : null}
)
}
if (module.key === 'discussion') {
if (!collection?.allow_comments) return null
return (
{canComment ?
: null}
handleReport('collection_comment', comment.id)} emptyMessage="No comments yet." />
)
}
if (module.key === 'related_collections') {
if (!Array.isArray(relatedCollections) || !relatedCollections.length) return null
return (
{relatedCollections.map((item) => (
{recommendationReasons(collection, item).map((reason) => (
{reason}
))}
))}
)
}
if (module.key === 'collaborators') {
return (
{members.length ? members.filter((member) => member?.status === 'active').map((member) =>
) :
This collection is curated by a single owner right now.
}
)
}
if (module.key === 'submissions') {
if (!collection?.allow_submissions) return null
return (
{canSubmit && submissionArtworkOptions?.length ? (
setSelectedArtworkId(val)} placeholder="Select artwork" options={submissionArtworkOptions.map((a) => ({ value: String(a.id), label: a.title }))} />
Submit artwork
) : (
Sign in with at least one artwork on your account to submit here.
)}
{submissions.length ? submissions.map((submission) => (
handleSubmissionAction(item, 'approve')}
onReject={(item) => handleSubmissionAction(item, 'reject')}
onWithdraw={(item) => handleSubmissionAction(item, 'withdraw')}
onReport={(item) => handleReport('collection_submission', item.id)}
/>
)) : No submissions yet.
}
)
}
return null
}
const renderedFullModules = enabledModules.filter((module) => module.slot === 'full').map(renderModule).filter(Boolean)
const renderedMainModules = enabledModules.filter((module) => module.slot === 'main').map(renderModule).filter(Boolean)
const renderedSidebarModules = enabledModules.filter((module) => module.slot === 'sidebar').map(renderModule).filter(Boolean)
return (
<>
{/* Per-type accent top stripe */}
{collection?.banner_text ? (
{collection.banner_text}
) : null}
{collection?.is_featured ? Featured Collection : null}
{collection?.mode === 'smart' ? Smart Collection : null}
{collection?.event_label ? {collection.event_label} : null}
{collection?.campaign_label ? {collection.campaign_label} : null}
{collection?.badge_label ? {collection.badge_label} : null}
{collection?.series_key ? Series {collection.series_order ? `#${collection.series_order}` : ''} : null}
{isOwner ? : null}
{collection?.title}
{showIntroBlock ? (
<>
{collection?.subtitle ?
{collection.subtitle}
: null}
{collection?.summary || collection?.description ?
{collection?.summary || collection?.description}
:
A curated selection from @{owner?.username}, assembled as a focused gallery rather than a simple archive.
}
{collection?.smart_summary ?
{collection.smart_summary}
: null}
{featuringCreatorsCount > 1 ?
Featuring artworks by {featuringCreatorsCount} creators.
: null}
>
) : null}
{featuredCollectionsUrl ? : null}
{reportEndpoint && !isOwner ? handleReport('collection', collection?.id)} icon="fa-flag" label="Report" tone="amber" compact /> : null}
{state.notice ?
{state.notice}
: null}
{(heroMetrics.length || heroSignals.length) ? (
Collection Snapshot
Stats and placement signals
The engagement counters and ranking signals now live outside the hero so the header can stay focused on the artwork, title, and actions.
{heroMetrics.map((item) => (
))}
{heroSignals.length ? (
{heroSignals.map((item) => (
))}
) : null}
) : null}
{(seriesContext?.url || seriesContext?.previous || seriesContext?.next || (Array.isArray(seriesContext?.siblings) && seriesContext.siblings.length)) ? (
Series
{seriesContext?.title || 'Connected collection sequence'}
{seriesContext?.description ?
{seriesContext.description}
: null}
{collection?.series_key ?
{collection.series_key} : null}
{seriesContext?.url ?
View full series : null}
{Array.isArray(seriesContext?.siblings) && seriesContext.siblings.length ? (
{seriesContext.siblings.slice(0, 2).map((item) => (
))}
) : null}
) : null}
{contextSignals.length ? (
Related Context
Campaign, event, and quality context
{contextSignals.length}
{contextSignals.map((item) => (
))}
) : null}
{storyLinks.length ? (
Stories
Stories and editorial references linked to this collection
{storyLinks.length}
{storyLinks.map((item) => (
))}
) : null}
{taxonomyLinks.length ? (
Browse The Theme
Categories and tags that anchor this collection
{taxonomyLinks.length}
{taxonomyLinks.map((item) => (
))}
) : null}
{contributorLinks.length ? (
Connected Creators
Creators and artworks that give the set its shape
{contributorLinks.length}
{contributorLinks.map((item) => (
))}
) : null}
{renderedFullModules.length ?
{renderedFullModules}
: null}
{(renderedMainModules.length || renderedSidebarModules.length) ? (
{renderedMainModules}
{renderedSidebarModules}
) : null}
>
)
}