Save workspace changes

This commit is contained in:
2026-04-18 17:02:56 +02:00
parent f02ea9a711
commit 87d60af5a9
4220 changed files with 1388603 additions and 1554 deletions

View File

@@ -44,14 +44,31 @@ function TypeBadge({ collection }) {
return <span className="inline-flex items-center rounded-full border border-sky-300/20 bg-sky-400/10 px-3 py-1 text-[11px] font-semibold uppercase tracking-[0.18em] text-sky-100">{label}</span>
}
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 (
<a href={member?.user?.profile_url || '#'} className="flex items-center gap-3 rounded-[22px] border border-white/10 bg-white/[0.04] px-4 py-4 transition hover:bg-white/[0.07]">
<img src={member?.user?.avatar_url} alt={member?.user?.name || member?.user?.username} className="h-12 w-12 rounded-2xl object-cover ring-1 ring-white/10" />
<div className="min-w-0">
<div className="truncate text-sm font-semibold text-white">{member?.user?.name || member?.user?.username}</div>
<div className="truncate text-xs uppercase tracking-[0.16em] text-slate-400">{member?.role} {member?.status === 'pending' ? '• invited' : ''}</div>
<a href={member?.user?.profile_url || '#'} className="group flex items-center gap-4 rounded-[22px] border border-white/10 bg-white/[0.04] px-4 py-4 transition hover:border-white/20 hover:bg-white/[0.07]">
{member?.user?.avatar_url ? (
<img src={member.user.avatar_url} alt={member?.user?.name || member?.user?.username} className="h-12 w-12 rounded-2xl object-cover ring-1 ring-white/10 transition group-hover:ring-sky-400/30" />
) : (
<div className="flex h-12 w-12 items-center justify-center rounded-2xl border border-white/10 bg-white/[0.05] text-slate-400">
<i className="fa-solid fa-user" />
</div>
)}
<div className="min-w-0 flex-1">
<div className="truncate text-sm font-semibold text-white group-hover:text-sky-100">{member?.user?.name || member?.user?.username}</div>
{member?.user?.username ? <div className="text-xs text-slate-500">@{member.user.username}</div> : null}
</div>
<span className={`shrink-0 rounded-full border px-2.5 py-1 text-[10px] font-semibold uppercase tracking-[0.14em] ${roleColor}`}>
{member?.role}{member?.status === 'pending' ? ' · invited' : ''}
</span>
</a>
)
}
@@ -80,25 +97,43 @@ function SubmissionCard({ submission, onApprove, onReject, onWithdraw, onReport
)
}
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 (
<div
className="flex min-w-0 flex-col items-center rounded-2xl border border-white/10 bg-white/[0.05] px-3 py-4 text-center"
className="relative overflow-hidden flex min-w-0 flex-col items-center rounded-2xl border border-white/10 bg-white/[0.04] px-3 py-4 text-center transition-colors hover:bg-white/[0.07]"
title={title}
aria-label={title}
>
<i className={`fa-solid ${icon} text-base text-slate-300`} />
<div className="mt-3 text-xl font-semibold text-white">{value}</div>
<div className={`absolute inset-x-0 top-0 h-[2px] bg-gradient-to-r ${tone.bar} via-transparent to-transparent`} />
<div className={`flex h-9 w-9 items-center justify-center rounded-xl border ${tone.bg}`}>
<i className={`fa-solid ${icon} text-sm ${tone.icon}`} />
</div>
<div className="mt-2 text-xl font-bold tabular-nums text-white">{value}</div>
<div className="mt-0.5 text-[10px] font-semibold uppercase tracking-[0.16em] text-slate-500">{label}</div>
</div>
)
}
return (
<div className="rounded-2xl border border-white/10 bg-white/[0.05] px-4 py-3" title={title}>
<div className="flex items-center gap-2 text-[11px] font-semibold uppercase tracking-[0.18em] text-slate-400">
<div className="relative overflow-hidden rounded-2xl border border-white/10 bg-white/[0.04] px-4 py-3 transition-colors hover:bg-white/[0.07]" title={title}>
<div className={`absolute inset-x-0 top-0 h-[2px] bg-gradient-to-r ${tone.bar} via-transparent to-transparent`} />
<div className={`flex items-center gap-2 text-[11px] font-semibold uppercase tracking-[0.18em] ${tone.icon}`}>
<i className={`fa-solid ${icon} text-[10px]`} />
{label}
</div>
@@ -107,6 +142,143 @@ function MetaRow({ icon, label, value, compact = false }) {
)
}
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 (
<Component
{...componentProps}
className={`group inline-flex items-center justify-center gap-3 rounded-[20px] border px-4 ${compact ? 'py-3' : 'py-3.5'} text-sm font-semibold tracking-[-0.01em] transition duration-200 ${active ? toneClasses.active : toneClasses.idle} ${disabled ? 'cursor-not-allowed opacity-60' : ''}`}
>
<span className={`flex h-9 w-9 items-center justify-center rounded-2xl border transition ${active ? toneClasses.iconActive : toneClasses.icon}`}>
<i className={`fa-solid ${icon} text-sm`} />
</span>
<span>{label}</span>
</Component>
)
}
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 (
<div className="relative overflow-hidden rounded-[26px] border border-white/10 bg-[linear-gradient(180deg,rgba(255,255,255,0.06),rgba(15,23,42,0.32))] p-4 shadow-[inset_0_1px_0_rgba(255,255,255,0.04)]">
<div className={`absolute inset-x-0 top-0 h-[2px] bg-gradient-to-r ${style.glow} via-transparent to-transparent`} />
<div className={`absolute -right-5 top-3 h-14 w-14 rounded-full blur-2xl ${style.orb}`} />
<div className="relative z-10">
<div className={`inline-flex h-11 w-11 items-center justify-center rounded-2xl border ${style.chip}`}>
<i className={`fa-solid ${icon} text-base ${style.icon}`} />
</div>
<div className="mt-4 text-[2rem] font-black leading-none tracking-[-0.04em] text-white">{value}</div>
<div className="mt-2 text-[11px] font-semibold uppercase tracking-[0.22em] text-slate-500">{label}</div>
{helper ? <div className="mt-1 text-xs text-slate-400">{helper}</div> : null}
</div>
</div>
)
}
function HeroSignalCard({ icon, label, value, description = null, tone = 'slate' }) {
const style = HERO_METRIC_TONES[tone] ?? HERO_METRIC_TONES.slate
return (
<div className="relative overflow-hidden rounded-[24px] border border-white/10 bg-[linear-gradient(135deg,rgba(255,255,255,0.06),rgba(15,23,42,0.32))] p-4 shadow-[inset_0_1px_0_rgba(255,255,255,0.04)]">
<div className={`absolute inset-x-0 top-0 h-[2px] bg-gradient-to-r ${style.glow} via-transparent to-transparent`} />
<div className="relative z-10 flex items-start gap-3">
<div className={`mt-0.5 flex h-11 w-11 shrink-0 items-center justify-center rounded-2xl border ${style.chip}`}>
<i className={`fa-solid ${icon} text-base ${style.icon}`} />
</div>
<div className="min-w-0 flex-1">
<div className="text-[11px] font-semibold uppercase tracking-[0.2em] text-slate-500">{label}</div>
<div className="mt-1 text-2xl font-black tracking-[-0.04em] text-white">{value}</div>
{description ? <p className="mt-2 text-sm leading-relaxed text-slate-400">{description}</p> : null}
</div>
</div>
</div>
)
}
function getSpotlightClasses(style) {
switch (style) {
case 'editorial':
@@ -146,36 +318,59 @@ function OwnerCard({ owner, collectionType }) {
: 'Curator'
const body = (
<>
{owner?.avatar_url ? (
<img src={owner.avatar_url} alt={owner?.name || owner?.username} className="h-14 w-14 rounded-2xl object-cover ring-1 ring-white/10" />
) : (
<div className="flex h-14 w-14 items-center justify-center rounded-2xl border border-white/10 bg-white/[0.04] text-slate-400">
<i className="fa-solid fa-user-astronaut" />
<div className="flex items-center gap-4">
<div className="relative shrink-0">
{owner?.avatar_url ? (
<img src={owner.avatar_url} alt={owner?.name || owner?.username} className="h-14 w-14 rounded-2xl object-cover ring-2 ring-white/10" />
) : (
<div className="flex h-14 w-14 items-center justify-center rounded-2xl border border-white/10 bg-white/[0.06] text-slate-400">
<i className="fa-solid fa-user-astronaut text-xl" />
</div>
)}
<div className="absolute -bottom-1 -right-1 flex h-5 w-5 items-center justify-center rounded-full border border-white/10 bg-sky-500 text-white">
<i className="fa-solid fa-pen-nib text-[8px]" />
</div>
)}
<div>
<div className="text-[11px] font-semibold uppercase tracking-[0.18em] text-slate-400">{label}</div>
<div className="mt-1 text-lg font-semibold text-white">{owner?.name || owner?.username || 'Skinbase Curator'}</div>
</div>
<div className="min-w-0">
<div className="text-[11px] font-semibold uppercase tracking-[0.18em] text-slate-500">{label}</div>
<div className="mt-0.5 text-lg font-semibold text-white">{owner?.name || owner?.username || 'Skinbase Curator'}</div>
{owner?.username ? <div className="text-sm text-slate-400">@{owner.username}</div> : null}
</div>
</>
{owner?.profile_url ? <i className="fa-solid fa-arrow-up-right-from-square ml-auto shrink-0 text-slate-500 text-sm" /> : null}
</div>
)
if (owner?.profile_url) {
return <a href={owner.profile_url} className="mt-6 inline-flex items-center gap-4 rounded-[24px] border border-white/10 bg-white/[0.04] px-4 py-4 transition hover:bg-white/[0.07]">{body}</a>
return (
<div className="mt-7 overflow-hidden rounded-[26px] border border-white/10 bg-[linear-gradient(135deg,rgba(255,255,255,0.06),rgba(15,23,42,0.34))] shadow-[inset_0_1px_0_rgba(255,255,255,0.04)] transition hover:border-sky-400/25 hover:bg-[linear-gradient(135deg,rgba(255,255,255,0.08),rgba(15,23,42,0.4))]">
<div className="h-[2px] bg-gradient-to-r from-sky-400/55 via-sky-400/20 to-transparent" />
<a href={owner.profile_url} className="block px-5 py-4">{body}</a>
</div>
)
}
return <div className="mt-6 inline-flex items-center gap-4 rounded-[24px] border border-white/10 bg-white/[0.04] px-4 py-4">{body}</div>
return (
<div className="mt-7 overflow-hidden rounded-[26px] border border-white/10 bg-[linear-gradient(135deg,rgba(255,255,255,0.06),rgba(15,23,42,0.34))] shadow-[inset_0_1px_0_rgba(255,255,255,0.04)]">
<div className="h-[2px] bg-gradient-to-r from-sky-400/55 via-sky-400/20 to-transparent" />
<div className="px-5 py-4">{body}</div>
</div>
)
}
function PageSection({ eyebrow, title, count, children }) {
function PageSection({ eyebrow, title, count, icon, children }) {
return (
<section className="rounded-[30px] border border-white/10 bg-white/[0.04] p-6 backdrop-blur-sm">
<div className="flex 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 className="flex items-start justify-between gap-3">
<div className="flex items-start gap-3">
{icon && (
<div className="mt-0.5 flex h-10 w-10 shrink-0 items-center justify-center rounded-2xl border border-white/10 bg-white/[0.05] text-sky-300">
<i className={`fa-solid ${icon} text-sm`} />
</div>
)}
<div>
<p className="text-[11px] font-semibold uppercase tracking-[0.22em] text-sky-200/80">{eyebrow}</p>
<h2 className="mt-1 text-2xl font-semibold text-white">{title}</h2>
</div>
</div>
{count !== undefined ? <span className="rounded-full border border-white/10 bg-white/[0.04] px-3 py-1 text-xs font-semibold text-slate-300">{count}</span> : null}
</div>
@@ -268,14 +463,29 @@ function recommendationReasons(currentCollection, candidate) {
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 wrapperClassName = 'flex h-full flex-col gap-3 rounded-[24px] border border-white/10 bg-white/[0.04] p-5 transition hover:bg-white/[0.07]'
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 = (
<>
<div className={`absolute inset-x-0 top-0 h-[2px] bg-gradient-to-r ${typeStyle.accent.split(' ')[1]} via-transparent to-transparent`} />
<div className="flex items-center justify-between gap-3">
<span className="rounded-full border border-white/10 bg-white/[0.05] px-2.5 py-1 text-[10px] font-semibold uppercase tracking-[0.16em] text-slate-300">{item.meta}</span>
{item.kicker ? <span className="text-[11px] font-semibold uppercase tracking-[0.16em] text-sky-200/80">{item.kicker}</span> : null}
<div className="flex items-center gap-2">
<div className={`flex h-8 w-8 items-center justify-center rounded-xl border ${typeStyle.badge}`}>
<i className={`${typeStyle.icon} text-[11px]`} />
</div>
<span className={`rounded-full border px-2.5 py-1 text-[10px] font-semibold uppercase tracking-[0.16em] ${typeStyle.badge}`}>{item.meta}</span>
</div>
{item.kicker ? <span className={`text-[11px] font-semibold uppercase tracking-[0.16em] ${typeStyle.kicker}`}>{item.kicker}</span> : null}
</div>
<div>
<h3 className="text-lg font-semibold text-white">{item.title}</h3>
@@ -442,6 +652,73 @@ export default function CollectionShow() {
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 }) => {
@@ -609,7 +886,7 @@ export default function CollectionShow() {
if (!artworkItems.length) return null
return (
<PageSection eyebrow="Highlights" title="Featured artworks" count={Math.min(artworkItems.length, 3)}>
<PageSection icon="fa-star" eyebrow="Highlights" title="Featured artworks" count={Math.min(artworkItems.length, 3)}>
<div className="space-y-4">
<p className="text-sm leading-relaxed text-slate-300">
Start with the standout pieces from this collection before diving into the full sequence.
@@ -624,7 +901,7 @@ export default function CollectionShow() {
if (collection?.type !== 'editorial') return null
return (
<PageSection eyebrow="Editorial" title="Editorial context">
<PageSection icon="fa-pen-nib" eyebrow="Editorial" title="Editorial context">
<div className="space-y-3 text-sm leading-relaxed text-slate-300">
<div className="rounded-[24px] border border-white/10 bg-white/[0.04] px-4 py-4">
{collection?.description || 'A staff-curated collection prepared for premium discovery placement.'}
@@ -659,7 +936,7 @@ export default function CollectionShow() {
if (!collection?.allow_comments) return null
return (
<PageSection eyebrow="Discussion" title="Collection comments" count={(collection?.comments_count ?? comments.length).toLocaleString()}>
<PageSection icon="fa-comments" eyebrow="Discussion" title="Collection comments" count={(collection?.comments_count ?? comments.length).toLocaleString()}>
{canComment ? <div className="rounded-2xl border border-white/10 bg-white/[0.04] p-4"><CommentForm onSubmit={handleCommentSubmit} placeholder="Talk about the curation, mood, or standout pieces…" submitLabel="Post comment" /></div> : null}
<div className={canComment ? 'mt-5' : ''}>
<CommentList comments={comments} canReply={false} onDelete={handleDeleteComment} onReport={(comment) => handleReport('collection_comment', comment.id)} emptyMessage="No comments yet." />
@@ -672,7 +949,7 @@ export default function CollectionShow() {
if (!Array.isArray(relatedCollections) || !relatedCollections.length) return null
return (
<PageSection eyebrow="More to Explore" title="Related collections">
<PageSection icon="fa-layer-group" eyebrow="More to Explore" title="Related collections">
<div className="grid grid-cols-1 gap-4 md:grid-cols-2">
{relatedCollections.map((item) => (
<div key={item.id} className="space-y-3">
@@ -693,7 +970,7 @@ export default function CollectionShow() {
if (module.key === 'collaborators') {
return (
<PageSection eyebrow="Contributors" title="Curation team">
<PageSection icon="fa-users" eyebrow="Contributors" title="Curation team">
<div className="space-y-3">
{members.length ? members.filter((member) => member?.status === 'active').map((member) => <CollaboratorCard key={member.id} member={member} />) : <p className="text-sm text-slate-400">This collection is curated by a single owner right now.</p>}
</div>
@@ -705,7 +982,7 @@ export default function CollectionShow() {
if (!collection?.allow_submissions) return null
return (
<PageSection eyebrow="Submissions" title="Submit to this collection">
<PageSection icon="fa-paper-plane" eyebrow="Submissions" title="Submit to this collection">
{canSubmit && submissionArtworkOptions?.length ? (
<div className="space-y-3">
<select value={selectedArtworkId} onChange={(event) => setSelectedArtworkId(event.target.value)} className="w-full rounded-2xl border border-white/10 bg-[#0d1726] px-4 py-3 text-white outline-none transition focus:border-sky-300/35">
@@ -761,15 +1038,24 @@ export default function CollectionShow() {
{isOwner && historyUrl ? <a href={historyUrl} className="inline-flex items-center gap-2 rounded-full border border-white/10 bg-white/[0.04] px-4 py-2 transition hover:bg-white/[0.07] hover:text-white"><i className="fa-solid fa-timeline fa-fw text-[11px]" />History</a> : null}
</div>
<section className="mt-6 overflow-hidden rounded-[34px] border border-white/10 bg-white/[0.04] shadow-[0_30px_90px_rgba(2,6,23,0.28)] backdrop-blur-sm">
<div className="grid gap-6 p-5 md:p-7 xl:grid-cols-[minmax(0,1.2fr)_420px]">
<div className="relative overflow-hidden rounded-[28px] border border-white/10 bg-slate-950/60">
<section className="mt-6 overflow-hidden rounded-[34px] border border-white/10 bg-[linear-gradient(180deg,rgba(255,255,255,0.05),rgba(255,255,255,0.02))] shadow-[0_30px_90px_rgba(2,6,23,0.32)] backdrop-blur-xl">
{/* Per-type accent top stripe */}
<div className={`h-[3px] bg-gradient-to-r ${
collection?.type === 'editorial' ? 'from-amber-400/80 via-amber-400/30 to-transparent' :
collection?.type === 'community' ? 'from-emerald-400/80 via-emerald-400/30 to-transparent' :
collection?.mode === 'smart' ? 'from-sky-400/80 via-sky-400/30 to-transparent' :
'from-violet-400/80 via-violet-400/30 to-transparent'
}`} />
<div className="grid items-start gap-6 p-5 md:p-7 xl:grid-cols-[minmax(0,1.2fr)_420px]">
<div className="relative self-start overflow-hidden rounded-[28px] border border-white/10 bg-slate-950/60">
<CollectionCover collection={collection} />
<div className="pointer-events-none absolute inset-0 bg-[linear-gradient(to_top,rgba(2,6,23,0.8),rgba(2,6,23,0.08))]" />
</div>
<div className="flex flex-col justify-between">
<div>
<div className="relative overflow-hidden rounded-[30px] border border-white/10 bg-[radial-gradient(circle_at_top_left,rgba(56,189,248,0.12),transparent_28%),radial-gradient(circle_at_90%_8%,rgba(251,191,36,0.14),transparent_24%),linear-gradient(180deg,rgba(15,23,42,0.94),rgba(10,18,32,0.92))] px-5 py-6 shadow-[inset_0_1px_0_rgba(255,255,255,0.04)] md:px-6 md:py-7">
<div aria-hidden="true" className="pointer-events-none absolute -left-14 top-10 h-36 w-36 rounded-full bg-sky-400/10 blur-3xl" />
<div aria-hidden="true" className="pointer-events-none absolute -right-10 bottom-8 h-32 w-32 rounded-full bg-amber-300/10 blur-3xl" />
<div className="relative z-10 flex h-full flex-col justify-between">
{collection?.banner_text ? (
<div className={`mb-4 inline-flex max-w-full items-center gap-2 rounded-[22px] border px-4 py-3 text-sm font-medium shadow-[0_18px_40px_rgba(2,6,23,0.2)] ${spotlightClasses}`}>
<i className="fa-solid fa-sparkles text-[12px]" />
@@ -786,50 +1072,65 @@ export default function CollectionShow() {
{collection?.series_key ? <span className="inline-flex items-center rounded-full border border-white/10 bg-white/[0.05] px-3 py-1 text-[11px] font-semibold uppercase tracking-[0.18em] text-white">Series {collection.series_order ? `#${collection.series_order}` : ''}</span> : null}
{isOwner ? <CollectionVisibilityBadge visibility={collection?.visibility} /> : null}
</div>
<h1 className="mt-4 text-3xl font-semibold tracking-[-0.04em] text-white md:text-5xl">{collection?.title}</h1>
<h1 className="mt-4 max-w-3xl text-4xl font-black tracking-[-0.06em] text-white md:text-5xl xl:text-[4rem] xl:leading-[0.92]">{collection?.title}</h1>
{showIntroBlock ? (
<>
{collection?.subtitle ? <p className="mt-3 text-base text-slate-300">{collection.subtitle}</p> : null}
{collection?.description ? <p className="mt-4 max-w-2xl text-sm leading-relaxed text-slate-300 md:text-[15px]">{collection.description}</p> : <p className="mt-4 max-w-2xl text-sm leading-relaxed text-slate-400 md:text-[15px]">A curated selection from @{owner?.username}, assembled as a focused gallery rather than a simple archive.</p>}
{collection?.smart_summary ? <p className="mt-3 max-w-2xl text-sm leading-relaxed text-sky-100/90">{collection.smart_summary}</p> : null}
{collection?.subtitle ? <p className="mt-3 text-lg text-slate-300 md:text-xl">{collection.subtitle}</p> : null}
{collection?.summary || collection?.description ? <p className="mt-4 max-w-2xl text-sm leading-relaxed text-slate-300 md:text-[15px]">{collection?.summary || collection?.description}</p> : <p className="mt-4 max-w-2xl text-sm leading-relaxed text-slate-400 md:text-[15px]">A curated selection from @{owner?.username}, assembled as a focused gallery rather than a simple archive.</p>}
{collection?.smart_summary ? <div className="mt-4 max-w-2xl rounded-[22px] border border-sky-300/15 bg-sky-400/[0.07] px-4 py-3 text-sm leading-relaxed text-sky-100/90">{collection.smart_summary}</div> : null}
{featuringCreatorsCount > 1 ? <p className="mt-3 text-sm text-slate-300">Featuring artworks by {featuringCreatorsCount} creators.</p> : null}
</>
) : null}
<div className="mt-6 flex flex-wrap gap-3">
<button type="button" onClick={handleLike} disabled={state.busy || !engagement?.like_url} className={`inline-flex items-center gap-2 rounded-full border px-4 py-2.5 text-sm font-semibold transition ${state.liked ? 'border-rose-400/20 bg-rose-400/10 text-rose-100' : 'border-white/12 bg-white/[0.05] text-white hover:bg-white/[0.08]'}`}><i className={`fa-solid ${state.liked ? 'fa-heart' : 'fa-heart-circle-plus'} fa-fw`} />{state.liked ? 'Liked' : 'Like Collection'}</button>
<button type="button" onClick={handleFollow} disabled={state.busy || !engagement?.follow_url} className={`inline-flex items-center gap-2 rounded-full border px-4 py-2.5 text-sm font-semibold transition ${state.following ? 'border-emerald-400/20 bg-emerald-400/10 text-emerald-100' : 'border-white/12 bg-white/[0.05] text-white hover:bg-white/[0.08]'}`}><i className={`fa-solid ${state.following ? 'fa-bell' : 'fa-bell-concierge'} fa-fw`} />{state.following ? 'Following' : 'Follow Collection'}</button>
<button type="button" onClick={handleSave} disabled={state.busy || (!engagement?.save_url && !engagement?.unsave_url)} className={`inline-flex items-center gap-2 rounded-full border px-4 py-2.5 text-sm font-semibold transition ${state.saved ? 'border-violet-300/20 bg-violet-400/10 text-violet-100' : 'border-white/12 bg-white/[0.05] text-white hover:bg-white/[0.08]'}`}><i className={`fa-solid ${state.saved ? 'fa-bookmark' : 'fa-bookmark-circle'} fa-fw`} />{state.saved ? 'Saved' : 'Save Collection'}</button>
<button type="button" onClick={handleShare} className="inline-flex items-center gap-2 rounded-full border border-white/12 bg-white/[0.05] px-4 py-2.5 text-sm font-semibold text-white transition hover:bg-white/[0.08]"><i className="fa-solid fa-share-nodes fa-fw" />Share</button>
{reportEndpoint && !isOwner ? <button type="button" onClick={() => handleReport('collection', collection?.id)} className="inline-flex items-center gap-2 rounded-full border border-white/12 bg-white/[0.05] px-4 py-2.5 text-sm font-semibold text-white transition hover:bg-white/[0.08]"><i className="fa-solid fa-flag fa-fw" />Report</button> : null}
{featuredCollectionsUrl ? <a href={featuredCollectionsUrl} className="inline-flex items-center gap-2 rounded-full border border-white/12 bg-white/[0.05] px-4 py-2.5 text-sm font-semibold text-white transition hover:bg-white/[0.08]"><i className="fa-solid fa-compass fa-fw" />Featured Collections</a> : null}
<div className="mt-7 space-y-3">
<div className="flex flex-wrap gap-3">
<CollectionHeroAction onClick={handleLike} disabled={state.busy || !engagement?.like_url} icon="fa-heart" label={state.liked ? 'Liked' : 'Like'} tone="rose" active={state.liked} />
<CollectionHeroAction onClick={handleFollow} disabled={state.busy || !engagement?.follow_url} icon="fa-bell" label={state.following ? 'Following' : 'Follow'} tone="emerald" active={state.following} />
<CollectionHeroAction onClick={handleSave} disabled={state.busy || (!engagement?.save_url && !engagement?.unsave_url)} icon="fa-bookmark" label={state.saved ? 'Saved' : 'Save'} tone="violet" active={state.saved} />
</div>
<div className="flex flex-wrap gap-3">
<CollectionHeroAction onClick={handleShare} icon="fa-share-nodes" label="Share" tone="neutral" compact />
{featuredCollectionsUrl ? <CollectionHeroAction href={featuredCollectionsUrl} icon="fa-compass" label="Explore" tone="sky" compact /> : null}
{reportEndpoint && !isOwner ? <CollectionHeroAction onClick={() => handleReport('collection', collection?.id)} icon="fa-flag" label="Report" tone="amber" compact /> : null}
</div>
</div>
{state.notice ? <p className="mt-3 text-sm text-sky-100">{state.notice}</p> : null}
<div className="mt-6 grid gap-3 sm:grid-cols-3 xl:grid-cols-5">
<MetaRow compact icon="fa-images" label="Artworks" value={(collection?.artworks_count ?? 0).toLocaleString()} />
<MetaRow compact icon="fa-heart" label="Likes" value={(collection?.likes_count ?? 0).toLocaleString()} />
<MetaRow compact icon="fa-bell" label="Followers" value={(collection?.followers_count ?? 0).toLocaleString()} />
<MetaRow compact icon="fa-eye" label="Views" value={(collection?.views_count ?? 0).toLocaleString()} />
<MetaRow compact icon="fa-bookmark" label="Saves" value={(collection?.saves_count ?? 0).toLocaleString()} />
</div>
{(collection?.presentation_style && collection.presentation_style !== 'standard') || collection?.quality_score != null || collection?.ranking_score != null || collection?.campaign_key ? (
<div className="mt-6 grid gap-3 md:grid-cols-2 xl:grid-cols-4">
{collection?.presentation_style && collection.presentation_style !== 'standard' ? <MetaRow icon="fa-panorama" label="Presentation" value={String(collection.presentation_style).replace(/_/g, ' ')} /> : null}
{collection?.quality_score != null ? <MetaRow icon="fa-gauge-high" label="Quality" value={Number(collection.quality_score).toFixed(1)} /> : null}
{collection?.ranking_score != null ? <MetaRow icon="fa-ranking-star" label="Ranking" value={Number(collection.ranking_score).toFixed(1)} /> : null}
{collection?.campaign_key ? <MetaRow icon="fa-bullhorn" label="Campaign" value={collection.campaign_label || collection.campaign_key} /> : null}
</div>
) : null}
<OwnerCard owner={owner} collectionType={collection?.type} />
</div>
<OwnerCard owner={owner} collectionType={collection?.type} />
</div>
</div>
</section>
{(heroMetrics.length || heroSignals.length) ? (
<section className="mt-6 rounded-[30px] border border-white/10 bg-[linear-gradient(180deg,rgba(255,255,255,0.05),rgba(255,255,255,0.02))] p-5 shadow-[0_20px_70px_rgba(2,6,23,0.22)] backdrop-blur-xl md:p-6">
<div className="flex flex-wrap items-start justify-between gap-3">
<div>
<p className="text-[11px] font-semibold uppercase tracking-[0.22em] text-sky-200/80">Collection Snapshot</p>
<h2 className="mt-2 text-2xl font-semibold text-white">Stats and placement signals</h2>
</div>
<p className="max-w-xl text-sm leading-relaxed text-slate-400">The engagement counters and ranking signals now live outside the hero so the header can stay focused on the artwork, title, and actions.</p>
</div>
<div className="mt-6 grid gap-4 xl:grid-cols-[minmax(0,1.6fr)_minmax(320px,0.95fr)]">
<div className="grid gap-3 sm:grid-cols-2 xl:grid-cols-5">
{heroMetrics.map((item) => (
<HeroMetricCard key={item.label} icon={item.icon} label={item.label} value={item.value} helper={item.helper} tone={item.tone} />
))}
</div>
{heroSignals.length ? (
<div className="grid gap-3 md:grid-cols-2 xl:grid-cols-1">
{heroSignals.map((item) => (
<HeroSignalCard key={item.label} icon={item.icon} label={item.label} value={item.value} description={item.description} tone={item.tone} />
))}
</div>
) : null}
</div>
</section>
) : null}
{(seriesContext?.url || seriesContext?.previous || seriesContext?.next || (Array.isArray(seriesContext?.siblings) && seriesContext.siblings.length)) ? (
<section className="mt-8 rounded-[30px] border border-white/10 bg-white/[0.04] p-6 backdrop-blur-sm">
<div className="flex flex-wrap items-center justify-between gap-3">
@@ -880,9 +1181,14 @@ export default function CollectionShow() {
{contextSignals.length ? (
<section className="mt-8 rounded-[30px] border border-white/10 bg-white/[0.04] p-6 backdrop-blur-sm">
<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">Related Context</p>
<h2 className="mt-2 text-2xl font-semibold text-white">Campaign, event, and quality context</h2>
<div className="flex items-start gap-3">
<div className="flex h-10 w-10 shrink-0 items-center justify-center rounded-2xl border border-amber-300/15 bg-amber-400/10 text-amber-300">
<i className="fa-solid fa-diagram-project text-sm" />
</div>
<div>
<p className="text-[11px] font-semibold uppercase tracking-[0.22em] text-amber-200/80">Related Context</p>
<h2 className="mt-1 text-2xl font-semibold text-white">Campaign, event, and quality context</h2>
</div>
</div>
<span className="rounded-full border border-white/10 bg-white/[0.04] px-3 py-1 text-xs font-semibold text-slate-300">{contextSignals.length}</span>
</div>
@@ -898,9 +1204,14 @@ export default function CollectionShow() {
{storyLinks.length ? (
<section className="mt-8 rounded-[30px] border border-white/10 bg-white/[0.04] p-6 backdrop-blur-sm">
<div className="flex flex-wrap items-center justify-between gap-3">
<div>
<p className="text-[11px] font-semibold uppercase tracking-[0.22em] text-lime-200/80">Stories</p>
<h2 className="mt-2 text-2xl font-semibold text-white">Stories and editorial references linked to this collection</h2>
<div className="flex items-start gap-3">
<div className="flex h-10 w-10 shrink-0 items-center justify-center rounded-2xl border border-lime-300/15 bg-lime-400/10 text-lime-300">
<i className="fa-solid fa-book-open text-sm" />
</div>
<div>
<p className="text-[11px] font-semibold uppercase tracking-[0.22em] text-lime-200/80">Stories</p>
<h2 className="mt-1 text-2xl font-semibold text-white">Stories and editorial references linked to this collection</h2>
</div>
</div>
<span className="rounded-full border border-white/10 bg-white/[0.04] px-3 py-1 text-xs font-semibold text-slate-300">{storyLinks.length}</span>
</div>
@@ -916,9 +1227,14 @@ export default function CollectionShow() {
{taxonomyLinks.length ? (
<section className="mt-8 rounded-[30px] border border-white/10 bg-white/[0.04] p-6 backdrop-blur-sm">
<div className="flex flex-wrap items-center justify-between gap-3">
<div>
<p className="text-[11px] font-semibold uppercase tracking-[0.22em] text-violet-200/80">Browse The Theme</p>
<h2 className="mt-2 text-2xl font-semibold text-white">Categories and tags that anchor this collection</h2>
<div className="flex items-start gap-3">
<div className="flex h-10 w-10 shrink-0 items-center justify-center rounded-2xl border border-violet-300/15 bg-violet-400/10 text-violet-300">
<i className="fa-solid fa-tags text-sm" />
</div>
<div>
<p className="text-[11px] font-semibold uppercase tracking-[0.22em] text-violet-200/80">Browse The Theme</p>
<h2 className="mt-1 text-2xl font-semibold text-white">Categories and tags that anchor this collection</h2>
</div>
</div>
<span className="rounded-full border border-white/10 bg-white/[0.04] px-3 py-1 text-xs font-semibold text-slate-300">{taxonomyLinks.length}</span>
</div>
@@ -934,9 +1250,14 @@ export default function CollectionShow() {
{contributorLinks.length ? (
<section className="mt-8 rounded-[30px] border border-white/10 bg-white/[0.04] p-6 backdrop-blur-sm">
<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">Connected Creators</p>
<h2 className="mt-2 text-2xl font-semibold text-white">Creators and artworks that give the set its shape</h2>
<div className="flex items-start gap-3">
<div className="flex h-10 w-10 shrink-0 items-center justify-center rounded-2xl border border-sky-300/15 bg-sky-400/10 text-sky-300">
<i className="fa-solid fa-user-group text-sm" />
</div>
<div>
<p className="text-[11px] font-semibold uppercase tracking-[0.22em] text-sky-200/80">Connected Creators</p>
<h2 className="mt-1 text-2xl font-semibold text-white">Creators and artworks that give the set its shape</h2>
</div>
</div>
<span className="rounded-full border border-white/10 bg-white/[0.04] px-3 py-1 text-xs font-semibold text-slate-300">{contributorLinks.length}</span>
</div>