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

@@ -187,6 +187,10 @@ function operatorOptionsForField(field) {
return [{ value: 'equals', label: 'Is' }]
}
if (field === 'tags') {
return [{ value: 'equals', label: 'Has tag' }]
}
return [
{ value: 'contains', label: 'Contains' },
{ value: 'equals', label: 'Equals' },
@@ -304,6 +308,11 @@ function buildRuleSummary(rule, smartRuleOptions) {
return rule.value ? 'Mature artworks only' : 'Artworks not marked mature'
}
if (rule.field === 'tags') {
const tag = String(rule.value || '').trim()
return tag ? `Has tag: ${tag}` : 'Tag — no value set'
}
const label = humanizeField(rule.field, smartRuleOptions)
const value = String(rule.value || '').trim() || 'Any value'
return `${label} ${rule.operator} ${value}`
@@ -581,6 +590,33 @@ function StudioTabButton({ active, label, icon, onClick }) {
)
}
function AdvancedSection({ title, icon, children, defaultOpen = false }) {
const [open, setOpen] = useState(defaultOpen)
return (
<div className="overflow-hidden rounded-[24px] border border-white/10">
<button
type="button"
onClick={() => setOpen((prev) => !prev)}
className="flex w-full items-center justify-between px-5 py-4 text-left transition hover:bg-white/[0.03]"
>
<div className="flex items-center gap-3">
<span className="flex h-7 w-7 items-center justify-center rounded-xl border border-white/10 bg-white/[0.04]">
<i className={`fa-solid ${icon} text-[11px] text-slate-400`} />
</span>
<span className="text-sm font-semibold tracking-[-0.01em] text-white">{title}</span>
</div>
<i className={`fa-solid ${open ? 'fa-chevron-up' : 'fa-chevron-down'} text-[10px] text-slate-500 transition-transform`} />
</button>
{open ? (
<div className="space-y-5 border-t border-white/10 px-5 pb-6 pt-5">
{children}
</div>
) : null}
</div>
)
}
function SmartRuleRow({
rule,
index,
@@ -670,6 +706,16 @@ function SmartRuleRow({
)}
</select>
</Field>
) : rule.field === 'tags' ? (
<Field label="Value" help="Type a tag name exactly as it appears on your artworks.">
<input
type="text"
value={rule.value}
onChange={(event) => onValueChange(event.target.value)}
className="w-full rounded-2xl border border-white/10 bg-white/[0.04] px-4 py-3 text-white outline-none transition focus:border-sky-300/35 focus:bg-white/[0.06]"
placeholder="e.g. dark-fantasy"
/>
</Field>
) : valueOptions.length ? (
<Field label="Value">
<select
@@ -1803,10 +1849,12 @@ export default function CollectionManage() {
<div>
<p className="text-[11px] font-semibold uppercase tracking-[0.22em] text-sky-200/80">Collection Studio</p>
<h1 className="mt-2 text-3xl font-semibold tracking-[-0.04em] text-white md:text-4xl">
{mode === 'create' ? 'Create a v4 collection' : collectionState?.title || 'Manage collection'}
{mode === 'create' ? 'New Collection' : collectionState?.title || 'Manage Collection'}
</h1>
<p className="mt-3 max-w-2xl text-sm leading-relaxed text-slate-300">
Collections now carry lifecycle, presentation, campaign, and series metadata alongside the artwork curation itself. Use manual mode for exact storytelling or smart rules for creator-first automation.
{mode === 'create'
? 'Give your collection a title and choose who can see it — that is all you need to get started. Expand the sections below to add descriptions, presentation options, scheduling, and more.'
: 'Manage your collection details, artworks, members, and settings from the tabs below.'}
</p>
</div>
<div className="flex flex-wrap items-center gap-2">
@@ -1843,19 +1891,20 @@ export default function CollectionManage() {
<ModeButton
active={!isSmartMode}
title="Manual"
description="Curate artworks yourself, control the exact order, and choose a specific cover from attached pieces."
description="You choose every artwork. Control the exact order, set a cover, and tell the story your way."
icon="fa-hand-sparkles"
onClick={() => updateForm('mode', 'manual')}
/>
<ModeButton
active={isSmartMode}
title="Smart"
description="Build a rule-based collection that automatically pulls matching artworks from your own published gallery."
description="Define rules and let the collection fill automatically from your published artworks. Great for keeping series fresh."
icon="fa-wand-magic-sparkles"
onClick={() => updateForm('mode', 'smart')}
/>
</div>
{/* === Essential fields — always visible === */}
<div className="grid gap-5 md:grid-cols-2">
<Field label="Title">
<input
@@ -1867,109 +1916,285 @@ export default function CollectionManage() {
maxLength={120}
/>
</Field>
<Field label="Slug" help="Used in the collection URL.">
<input
type="text"
value={form.slug}
onChange={(event) => {
setSlugTouched(true)
updateForm('slug', slugify(event.target.value))
}}
className="w-full rounded-2xl border border-white/10 bg-white/[0.04] px-4 py-3 text-white outline-none transition focus:border-sky-300/35 focus:bg-white/[0.06]"
placeholder="dark-fantasy-series"
maxLength={140}
/>
<Field label="Visibility">
<select value={form.visibility} onChange={(event) => updateForm('visibility', 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">
<option value="public">Public visible to everyone</option>
<option value="unlisted">Unlisted accessible by link only</option>
<option value="private">Private only you can see it</option>
</select>
</Field>
</div>
<div className="grid gap-5 md:grid-cols-2">
<Field label="Subtitle" help="Optional short line that sits under the title.">
<input
type="text"
value={form.subtitle}
onChange={(event) => updateForm('subtitle', event.target.value)}
className="w-full rounded-2xl border border-white/10 bg-white/[0.04] px-4 py-3 text-white outline-none transition focus:border-sky-300/35 focus:bg-white/[0.06]"
placeholder="A moody archive of midnight environments"
maxLength={160}
/>
</Field>
<Field label="Summary" help="Optional short summary for cards and meta previews.">
<input
type="text"
value={form.summary}
onChange={(event) => updateForm('summary', event.target.value)}
className="w-full rounded-2xl border border-white/10 bg-white/[0.04] px-4 py-3 text-white outline-none transition focus:border-sky-300/35 focus:bg-white/[0.06]"
placeholder="Best performing sci-fi wallpapers from the last year"
maxLength={320}
/>
</Field>
</div>
<Field label="Description">
<textarea
value={form.description}
onChange={(event) => updateForm('description', event.target.value)}
className="min-h-[128px] w-full rounded-2xl border border-white/10 bg-white/[0.04] px-4 py-3 text-white outline-none transition focus:border-sky-300/35 focus:bg-white/[0.06]"
placeholder="Describe the mood, focus, or story behind this showcase."
maxLength={1000}
<Field label="URL Slug" help="Auto-generated from the title. Edit to customise the collection URL.">
<input
type="text"
value={form.slug}
onChange={(event) => {
setSlugTouched(true)
updateForm('slug', slugify(event.target.value))
}}
className="w-full rounded-2xl border border-white/10 bg-white/[0.04] px-4 py-3 text-white outline-none transition focus:border-sky-300/35 focus:bg-white/[0.06]"
placeholder="dark-fantasy-series"
maxLength={140}
/>
</Field>
<div className="grid gap-5 md:grid-cols-2 xl:grid-cols-4">
<Field label="Collection Type">
<select value={form.type} onChange={(event) => updateForm('type', 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">
<option value="personal">Personal</option>
<option value="community">Community</option>
<option value="editorial">Editorial</option>
</select>
</Field>
<Field label="Collaboration">
<select value={form.collaboration_mode} onChange={(event) => updateForm('collaboration_mode', 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">
<option value="closed">Closed</option>
<option value="invite_only">Invite Only</option>
<option value="open">Open Submissions</option>
</select>
</Field>
<Field label="Event Key" help="Internal campaign identifier for discovery and promotion logic.">
<input type="text" value={form.event_key} onChange={(event) => updateForm('event_key', event.target.value)} className="w-full rounded-2xl border border-white/10 bg-white/[0.04] px-4 py-3 text-white outline-none transition focus:border-sky-300/35 focus:bg-white/[0.06]" maxLength={80} />
</Field>
<Field label="Event Label" help="Optional campaign or seasonal label.">
<input type="text" value={form.event_label} onChange={(event) => updateForm('event_label', event.target.value)} className="w-full rounded-2xl border border-white/10 bg-white/[0.04] px-4 py-3 text-white outline-none transition focus:border-sky-300/35 focus:bg-white/[0.06]" maxLength={120} />
</Field>
</div>
<Field label="Summary" help="A short line shown on collection cards and in search results.">
<input
type="text"
value={form.summary}
onChange={(event) => updateForm('summary', event.target.value)}
className="w-full rounded-2xl border border-white/10 bg-white/[0.04] px-4 py-3 text-white outline-none transition focus:border-sky-300/35 focus:bg-white/[0.06]"
placeholder="Best performing sci-fi wallpapers from the last year"
maxLength={320}
/>
</Field>
<div className="grid gap-5 md:grid-cols-2 xl:grid-cols-4">
<Field label="Season Key" help="Optional seasonal key used for grouped landing surfaces.">
<input type="text" value={form.season_key} onChange={(event) => updateForm('season_key', event.target.value)} className="w-full rounded-2xl border border-white/10 bg-white/[0.04] px-4 py-3 text-white outline-none transition focus:border-sky-300/35 focus:bg-white/[0.06]" maxLength={80} />
</Field>
<Field label="Badge Label" help="Short public badge for cards and headers.">
<input type="text" value={form.badge_label} onChange={(event) => updateForm('badge_label', event.target.value)} className="w-full rounded-2xl border border-white/10 bg-white/[0.04] px-4 py-3 text-white outline-none transition focus:border-sky-300/35 focus:bg-white/[0.06]" maxLength={80} />
</Field>
<Field label="Spotlight Style" help="Choose how the public campaign banner should be framed.">
<select value={form.spotlight_style} onChange={(event) => updateForm('spotlight_style', 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">
<option value="default">Default</option>
<option value="editorial">Editorial</option>
<option value="seasonal">Seasonal</option>
<option value="challenge">Challenge</option>
<option value="community">Community</option>
</select>
</Field>
<Field label="Banner Text" help="Optional short line displayed as the collection spotlight banner.">
<input type="text" value={form.banner_text} onChange={(event) => updateForm('banner_text', event.target.value)} className="w-full rounded-2xl border border-white/10 bg-white/[0.04] px-4 py-3 text-white outline-none transition focus:border-sky-300/35 focus:bg-white/[0.06]" maxLength={200} />
</Field>
</div>
{/* === Advanced sections — collapsed on create, expanded on edit === */}
<div className="grid gap-5 md:grid-cols-2">
<Field label="Publish At" help="Leave empty to publish immediately. Future times keep the collection off public surfaces until it goes live.">
<input type="datetime-local" value={form.published_at} onChange={(event) => updateForm('published_at', event.target.value)} className="w-full rounded-2xl border border-white/10 bg-white/[0.04] px-4 py-3 text-white outline-none transition focus:border-sky-300/35 focus:bg-white/[0.06]" />
</Field>
<Field label="Unpublish At" help="Optional automatic sunset time for seasonal or editorial collections.">
<input type="datetime-local" value={form.unpublished_at} onChange={(event) => updateForm('unpublished_at', event.target.value)} className="w-full rounded-2xl border border-white/10 bg-white/[0.04] px-4 py-3 text-white outline-none transition focus:border-sky-300/35 focus:bg-white/[0.06]" />
</Field>
</div>
<AdvancedSection title="Description & Presentation" icon="fa-palette" defaultOpen={mode === 'edit'}>
<div className="grid gap-5 md:grid-cols-2">
<Field label="Subtitle" help="Optional short line that sits under the title.">
<input
type="text"
value={form.subtitle}
onChange={(event) => updateForm('subtitle', event.target.value)}
className="w-full rounded-2xl border border-white/10 bg-white/[0.04] px-4 py-3 text-white outline-none transition focus:border-sky-300/35 focus:bg-white/[0.06]"
placeholder="A moody archive of midnight environments"
maxLength={160}
/>
</Field>
<Field label="Presentation Style">
<select value={form.presentation_style} onChange={(event) => updateForm('presentation_style', 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">
<option value="standard">Standard</option>
<option value="editorial_grid">Editorial Grid</option>
<option value="hero_grid">Hero Grid</option>
<option value="masonry">Masonry</option>
</select>
</Field>
</div>
<div className="grid gap-5 md:grid-cols-2 xl:grid-cols-4">
<Field label="Lifecycle State" help="Controls whether the collection should be treated as draft, scheduled, published, or retired.">
<Field label="Description" help="Describe the mood, focus, or story behind this showcase.">
<textarea
value={form.description}
onChange={(event) => updateForm('description', event.target.value)}
className="min-h-[128px] w-full rounded-2xl border border-white/10 bg-white/[0.04] px-4 py-3 text-white outline-none transition focus:border-sky-300/35 focus:bg-white/[0.06]"
placeholder="A curated selection of pieces that share a common visual language…"
maxLength={1000}
/>
</Field>
<div className="grid gap-5 md:grid-cols-2">
<Field label="Emphasis Mode">
<select value={form.emphasis_mode} onChange={(event) => updateForm('emphasis_mode', 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">
<option value="cover_heavy">Cover Heavy</option>
<option value="balanced">Balanced</option>
<option value="artwork_first">Artwork First</option>
</select>
</Field>
<Field label="Theme">
<select value={form.theme_token} onChange={(event) => updateForm('theme_token', 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">
<option value="default">Default</option>
<option value="subtle-blue">Subtle Blue</option>
<option value="violet">Violet</option>
<option value="amber">Amber</option>
</select>
</Field>
</div>
<div className="grid gap-5 md:grid-cols-2">
{!isSmartMode ? (
<Field label="Sort Order" help="Manual keeps the display order under your direct control.">
<select value={form.sort_mode} onChange={(event) => updateForm('sort_mode', 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">
<option value="manual">Manual</option>
<option value="newest">Newest first</option>
<option value="oldest">Oldest first</option>
<option value="popular">Most popular</option>
</select>
</Field>
) : (
<Field label="Match Mode" help="All rules must match, or any one rule is enough.">
<select
value={smartRules.match}
onChange={(event) => setSmartRules((current) => ({ ...current, match: 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"
>
<option value="all">All rules</option>
<option value="any">Any rule</option>
</select>
</Field>
)}
{!isSmartMode ? (
<Field label="Cover Artwork" help={attachedCoverOptions.length ? 'Choose a cover from artworks already attached to this collection.' : 'Attach artworks first to pick a manual cover.'}>
<select
value={form.cover_artwork_id}
onChange={(event) => updateForm('cover_artwork_id', event.target.value)}
disabled={!attachedCoverOptions.length}
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 disabled:cursor-not-allowed disabled:text-slate-500"
>
<option value="">Automatic cover</option>
{attachedCoverOptions.map((artwork) => (
<option key={artwork.id} value={artwork.id}>{artwork.title}</option>
))}
</select>
</Field>
) : (
<Field label="Smart Sort" help="How matching artworks should be ordered in this collection.">
<select
value={smartRules.sort}
onChange={(event) => {
setSmartRules((current) => ({ ...current, sort: event.target.value }))
updateForm('sort_mode', 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"
>
{(smartRuleOptions?.sort_options || []).map((option) => (
<option key={option.value} value={option.value}>{option.label}</option>
))}
</select>
</Field>
)}
</div>
</AdvancedSection>
<AdvancedSection title="Collaboration & Access" icon="fa-user-group" defaultOpen={mode === 'edit'}>
<div className="grid gap-5 md:grid-cols-2">
<Field label="Collection Type">
<select value={form.type} onChange={(event) => updateForm('type', 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">
<option value="personal">Personal</option>
<option value="community">Community</option>
<option value="editorial">Editorial</option>
</select>
</Field>
<Field label="Collaboration Mode">
<select value={form.collaboration_mode} onChange={(event) => updateForm('collaboration_mode', 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">
<option value="closed">Closed curated by you only</option>
<option value="invite_only">Invite only</option>
<option value="open">Open submissions</option>
</select>
</Field>
</div>
<div className="grid gap-3 md:grid-cols-2 xl:grid-cols-4">
<label className="flex items-center gap-3 rounded-2xl border border-white/10 bg-white/[0.04] px-4 py-3 text-sm text-white">
<input type="checkbox" checked={form.allow_submissions} onChange={(event) => updateForm('allow_submissions', event.target.checked)} />
Allow submissions
</label>
<label className="flex items-center gap-3 rounded-2xl border border-white/10 bg-white/[0.04] px-4 py-3 text-sm text-white">
<input type="checkbox" checked={form.allow_comments} onChange={(event) => updateForm('allow_comments', event.target.checked)} />
Allow comments
</label>
<label className="flex items-center gap-3 rounded-2xl border border-white/10 bg-white/[0.04] px-4 py-3 text-sm text-white">
<input type="checkbox" checked={form.allow_saves} onChange={(event) => updateForm('allow_saves', event.target.checked)} />
Allow saves
</label>
<label className="flex items-center gap-3 rounded-2xl border border-white/10 bg-white/[0.04] px-4 py-3 text-sm text-white">
<input type="checkbox" checked={form.commercial_eligibility} onChange={(event) => updateForm('commercial_eligibility', event.target.checked)} />
Commercially eligible
</label>
</div>
{form.type === 'editorial' ? (
<div className="grid gap-5 md:grid-cols-3">
<Field label="Editorial Owner" help="Choose whether this editorial lives under the current curator, another staff account, or the system identity.">
<select value={form.editorial_owner_mode} onChange={(event) => updateForm('editorial_owner_mode', 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">
<option value="creator">Current curator</option>
<option value="staff_account">Staff account</option>
<option value="system">System editorial identity</option>
</select>
</Field>
{form.editorial_owner_mode === 'staff_account' ? (
<Field label="Staff Account Username" help="Must be an admin or moderator username.">
<input
type="text"
value={form.editorial_owner_username}
onChange={(event) => updateForm('editorial_owner_username', event.target.value.trimStart())}
className="w-full rounded-2xl border border-white/10 bg-white/[0.04] px-4 py-3 text-white outline-none transition focus:border-sky-300/35 focus:bg-white/[0.06]"
placeholder="skinbase-editorial"
maxLength={60}
/>
</Field>
) : null}
{form.editorial_owner_mode === 'system' ? (
<Field label="System Owner Label" help="Public-facing label for system-owned editorials.">
<input
type="text"
value={form.editorial_owner_label}
onChange={(event) => updateForm('editorial_owner_label', event.target.value)}
className="w-full rounded-2xl border border-white/10 bg-white/[0.04] px-4 py-3 text-white outline-none transition focus:border-sky-300/35 focus:bg-white/[0.06]"
placeholder="Skinbase Editorial"
maxLength={120}
/>
</Field>
) : null}
</div>
) : null}
</AdvancedSection>
<AdvancedSection title="Campaign & Events" icon="fa-bullhorn" defaultOpen={mode === 'edit'}>
<div className="grid gap-5 md:grid-cols-2 xl:grid-cols-4">
<Field label="Event Key" help="Internal identifier used by discovery and promotion logic.">
<input type="text" value={form.event_key} onChange={(event) => updateForm('event_key', event.target.value)} className="w-full rounded-2xl border border-white/10 bg-white/[0.04] px-4 py-3 text-white outline-none transition focus:border-sky-300/35 focus:bg-white/[0.06]" maxLength={80} />
</Field>
<Field label="Event Label">
<input type="text" value={form.event_label} onChange={(event) => updateForm('event_label', event.target.value)} className="w-full rounded-2xl border border-white/10 bg-white/[0.04] px-4 py-3 text-white outline-none transition focus:border-sky-300/35 focus:bg-white/[0.06]" maxLength={120} />
</Field>
<Field label="Season Key" help="Groups related collections by season on landing surfaces.">
<input type="text" value={form.season_key} onChange={(event) => updateForm('season_key', event.target.value)} className="w-full rounded-2xl border border-white/10 bg-white/[0.04] px-4 py-3 text-white outline-none transition focus:border-sky-300/35 focus:bg-white/[0.06]" maxLength={80} />
</Field>
<Field label="Badge Label" help="Short public badge on cards and headers.">
<input type="text" value={form.badge_label} onChange={(event) => updateForm('badge_label', event.target.value)} className="w-full rounded-2xl border border-white/10 bg-white/[0.04] px-4 py-3 text-white outline-none transition focus:border-sky-300/35 focus:bg-white/[0.06]" maxLength={80} />
</Field>
</div>
<div className="grid gap-5 md:grid-cols-2 xl:grid-cols-4">
<Field label="Campaign Key" help="Operational identifier for recommendation and placement logic.">
<input type="text" value={form.campaign_key} onChange={(event) => updateForm('campaign_key', event.target.value)} className="w-full rounded-2xl border border-white/10 bg-white/[0.04] px-4 py-3 text-white outline-none transition focus:border-sky-300/35 focus:bg-white/[0.06]" maxLength={80} />
</Field>
<Field label="Campaign Label" help="Public-facing campaign or promotion label.">
<input type="text" value={form.campaign_label} onChange={(event) => updateForm('campaign_label', event.target.value)} className="w-full rounded-2xl border border-white/10 bg-white/[0.04] px-4 py-3 text-white outline-none transition focus:border-sky-300/35 focus:bg-white/[0.06]" maxLength={120} />
</Field>
<Field label="Spotlight Style" help="Controls the visual frame for the public campaign banner.">
<select value={form.spotlight_style} onChange={(event) => updateForm('spotlight_style', 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">
<option value="default">Default</option>
<option value="editorial">Editorial</option>
<option value="seasonal">Seasonal</option>
<option value="challenge">Challenge</option>
<option value="community">Community</option>
</select>
</Field>
<Field label="Banner Text" help="Short line shown in the collection spotlight banner.">
<input type="text" value={form.banner_text} onChange={(event) => updateForm('banner_text', event.target.value)} className="w-full rounded-2xl border border-white/10 bg-white/[0.04] px-4 py-3 text-white outline-none transition focus:border-sky-300/35 focus:bg-white/[0.06]" maxLength={200} />
</Field>
</div>
</AdvancedSection>
<AdvancedSection title="Series" icon="fa-layer-group" defaultOpen={mode === 'edit'}>
<div className="grid gap-5 md:grid-cols-3">
<Field label="Series Key" help="Use the same key across all linked collections in a series.">
<input type="text" value={form.series_key} onChange={(event) => updateForm('series_key', event.target.value)} className="w-full rounded-2xl border border-white/10 bg-white/[0.04] px-4 py-3 text-white outline-none transition focus:border-sky-300/35 focus:bg-white/[0.06]" maxLength={80} />
</Field>
<Field label="Series Title" help="Optional public heading shown for the whole series.">
<input type="text" value={form.series_title} onChange={(event) => updateForm('series_title', event.target.value)} className="w-full rounded-2xl border border-white/10 bg-white/[0.04] px-4 py-3 text-white outline-none transition focus:border-sky-300/35 focus:bg-white/[0.06]" maxLength={160} />
</Field>
<Field label="Series Order" help="Sequence position for public next & previous navigation.">
<input type="number" min="1" max="9999" value={form.series_order} onChange={(event) => updateForm('series_order', event.target.value)} className="w-full rounded-2xl border border-white/10 bg-white/[0.04] px-4 py-3 text-white outline-none transition focus:border-sky-300/35 focus:bg-white/[0.06]" />
</Field>
</div>
<Field label="Series Description" help="Optional public intro shown on series landing pages.">
<textarea
value={form.series_description}
onChange={(event) => updateForm('series_description', event.target.value)}
className="min-h-[96px] w-full rounded-2xl border border-white/10 bg-white/[0.04] px-4 py-3 text-white outline-none transition focus:border-sky-300/35 focus:bg-white/[0.06]"
maxLength={400}
/>
</Field>
</AdvancedSection>
<AdvancedSection title="Scheduling & Lifecycle" icon="fa-calendar-days" defaultOpen={mode === 'edit'}>
<Field label="Lifecycle State" help="Draft keeps it hidden. Published makes it live. Archived retires it from active surfaces.">
<select value={form.lifecycle_state} onChange={(event) => updateForm('lifecycle_state', 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">
<option value="draft">Draft</option>
<option value="scheduled">Scheduled</option>
@@ -1979,226 +2204,72 @@ export default function CollectionManage() {
<option value="expired">Expired</option>
</select>
</Field>
<Field label="Presentation Style">
<select value={form.presentation_style} onChange={(event) => updateForm('presentation_style', 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">
<option value="standard">Standard</option>
<option value="editorial_grid">Editorial Grid</option>
<option value="hero_grid">Hero Grid</option>
<option value="masonry">Masonry</option>
</select>
</Field>
<Field label="Emphasis Mode">
<select value={form.emphasis_mode} onChange={(event) => updateForm('emphasis_mode', 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">
<option value="cover_heavy">Cover Heavy</option>
<option value="balanced">Balanced</option>
<option value="artwork_first">Artwork First</option>
</select>
</Field>
<Field label="Theme Token">
<select value={form.theme_token} onChange={(event) => updateForm('theme_token', 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">
<option value="default">Default</option>
<option value="subtle-blue">Subtle Blue</option>
<option value="violet">Violet</option>
<option value="amber">Amber</option>
</select>
</Field>
</div>
<div className="grid gap-5 md:grid-cols-2 xl:grid-cols-4">
<Field label="Series Key" help="Use the same key across linked collections in a series.">
<input type="text" value={form.series_key} onChange={(event) => updateForm('series_key', event.target.value)} className="w-full rounded-2xl border border-white/10 bg-white/[0.04] px-4 py-3 text-white outline-none transition focus:border-sky-300/35 focus:bg-white/[0.06]" maxLength={80} />
</Field>
<Field label="Series Title" help="Optional public heading for the whole series.">
<input type="text" value={form.series_title} onChange={(event) => updateForm('series_title', event.target.value)} className="w-full rounded-2xl border border-white/10 bg-white/[0.04] px-4 py-3 text-white outline-none transition focus:border-sky-300/35 focus:bg-white/[0.06]" maxLength={160} />
</Field>
<Field label="Series Order" help="Sequence within the series for public next and previous navigation.">
<input type="number" min="1" max="9999" value={form.series_order} onChange={(event) => updateForm('series_order', event.target.value)} className="w-full rounded-2xl border border-white/10 bg-white/[0.04] px-4 py-3 text-white outline-none transition focus:border-sky-300/35 focus:bg-white/[0.06]" />
</Field>
<Field label="Campaign Key" help="Operational campaign identifier for discovery, placements, and recommendations.">
<input type="text" value={form.campaign_key} onChange={(event) => updateForm('campaign_key', event.target.value)} className="w-full rounded-2xl border border-white/10 bg-white/[0.04] px-4 py-3 text-white outline-none transition focus:border-sky-300/35 focus:bg-white/[0.06]" maxLength={80} />
</Field>
<Field label="Campaign Label" help="Public-facing campaign or promotion label.">
<input type="text" value={form.campaign_label} onChange={(event) => updateForm('campaign_label', event.target.value)} className="w-full rounded-2xl border border-white/10 bg-white/[0.04] px-4 py-3 text-white outline-none transition focus:border-sky-300/35 focus:bg-white/[0.06]" maxLength={120} />
</Field>
</div>
<div className="grid gap-5 md:grid-cols-2">
<Field label="Publish At" help="Leave empty to publish immediately. A future time keeps it off public surfaces until it goes live.">
<input type="datetime-local" value={form.published_at} onChange={(event) => updateForm('published_at', event.target.value)} className="w-full rounded-2xl border border-white/10 bg-white/[0.04] px-4 py-3 text-white outline-none transition focus:border-sky-300/35 focus:bg-white/[0.06]" />
</Field>
<Field label="Unpublish At" help="Optional automatic sunset time for seasonal or editorial collections.">
<input type="datetime-local" value={form.unpublished_at} onChange={(event) => updateForm('unpublished_at', event.target.value)} className="w-full rounded-2xl border border-white/10 bg-white/[0.04] px-4 py-3 text-white outline-none transition focus:border-sky-300/35 focus:bg-white/[0.06]" />
</Field>
</div>
<Field label="Series Description" help="Optional public intro shown on series landing pages and collection series callouts.">
<textarea
value={form.series_description}
onChange={(event) => updateForm('series_description', event.target.value)}
className="min-h-[96px] w-full rounded-2xl border border-white/10 bg-white/[0.04] px-4 py-3 text-white outline-none transition focus:border-sky-300/35 focus:bg-white/[0.06]"
maxLength={400}
/>
</Field>
<div className="grid gap-5 md:grid-cols-2">
<Field label="Archive At" help="Optional timestamp for moving the collection to long-term archive workflows.">
<input type="datetime-local" value={form.archived_at} onChange={(event) => updateForm('archived_at', event.target.value)} className="w-full rounded-2xl border border-white/10 bg-white/[0.04] px-4 py-3 text-white outline-none transition focus:border-sky-300/35 focus:bg-white/[0.06]" />
</Field>
<Field label="Expire At" help="Optional hard expiry for promotional or seasonal collections.">
<input type="datetime-local" value={form.expired_at} onChange={(event) => updateForm('expired_at', event.target.value)} className="w-full rounded-2xl border border-white/10 bg-white/[0.04] px-4 py-3 text-white outline-none transition focus:border-sky-300/35 focus:bg-white/[0.06]" />
</Field>
</div>
</AdvancedSection>
<div className="grid gap-5 md:grid-cols-2 xl:grid-cols-4">
<Field label="Archive At" help="Optional timestamp for moving the collection into long-term archive workflows.">
<input type="datetime-local" value={form.archived_at} onChange={(event) => updateForm('archived_at', event.target.value)} className="w-full rounded-2xl border border-white/10 bg-white/[0.04] px-4 py-3 text-white outline-none transition focus:border-sky-300/35 focus:bg-white/[0.06]" />
</Field>
<Field label="Expire At" help="Optional hard expiry for promotional or seasonal collections.">
<input type="datetime-local" value={form.expired_at} onChange={(event) => updateForm('expired_at', event.target.value)} className="w-full rounded-2xl border border-white/10 bg-white/[0.04] px-4 py-3 text-white outline-none transition focus:border-sky-300/35 focus:bg-white/[0.06]" />
</Field>
<Field label="Promotion Tier">
<input type="text" value={form.promotion_tier} onChange={(event) => updateForm('promotion_tier', event.target.value)} className="w-full rounded-2xl border border-white/10 bg-white/[0.04] px-4 py-3 text-white outline-none transition focus:border-sky-300/35 focus:bg-white/[0.06]" maxLength={40} />
</Field>
<Field label="Monetization Status">
<input type="text" value={form.monetization_ready_status} onChange={(event) => updateForm('monetization_ready_status', event.target.value)} className="w-full rounded-2xl border border-white/10 bg-white/[0.04] px-4 py-3 text-white outline-none transition focus:border-sky-300/35 focus:bg-white/[0.06]" maxLength={40} />
</Field>
</div>
<AdvancedSection title="Commercial & Administration" icon="fa-briefcase" defaultOpen={mode === 'edit'}>
<div className="grid gap-5 md:grid-cols-2 xl:grid-cols-4">
<Field label="Promotion Tier">
<input type="text" value={form.promotion_tier} onChange={(event) => updateForm('promotion_tier', event.target.value)} className="w-full rounded-2xl border border-white/10 bg-white/[0.04] px-4 py-3 text-white outline-none transition focus:border-sky-300/35 focus:bg-white/[0.06]" maxLength={40} />
</Field>
<Field label="Monetization Status">
<input type="text" value={form.monetization_ready_status} onChange={(event) => updateForm('monetization_ready_status', event.target.value)} className="w-full rounded-2xl border border-white/10 bg-white/[0.04] px-4 py-3 text-white outline-none transition focus:border-sky-300/35 focus:bg-white/[0.06]" maxLength={40} />
</Field>
<Field label="Sponsorship Label">
<input type="text" value={form.sponsorship_label} onChange={(event) => updateForm('sponsorship_label', event.target.value)} className="w-full rounded-2xl border border-white/10 bg-white/[0.04] px-4 py-3 text-white outline-none transition focus:border-sky-300/35 focus:bg-white/[0.06]" maxLength={120} />
</Field>
<Field label="Partner Label">
<input type="text" value={form.partner_label} onChange={(event) => updateForm('partner_label', event.target.value)} className="w-full rounded-2xl border border-white/10 bg-white/[0.04] px-4 py-3 text-white outline-none transition focus:border-sky-300/35 focus:bg-white/[0.06]" maxLength={120} />
</Field>
</div>
<div className="grid gap-5 md:grid-cols-2 xl:grid-cols-4">
<Field label="Sponsorship Label">
<input type="text" value={form.sponsorship_label} onChange={(event) => updateForm('sponsorship_label', event.target.value)} className="w-full rounded-2xl border border-white/10 bg-white/[0.04] px-4 py-3 text-white outline-none transition focus:border-sky-300/35 focus:bg-white/[0.06]" maxLength={120} />
</Field>
<Field label="Partner Label">
<input type="text" value={form.partner_label} onChange={(event) => updateForm('partner_label', event.target.value)} className="w-full rounded-2xl border border-white/10 bg-white/[0.04] px-4 py-3 text-white outline-none transition focus:border-sky-300/35 focus:bg-white/[0.06]" maxLength={120} />
</Field>
<Field label="Brand Safe Status">
<input type="text" value={form.brand_safe_status} onChange={(event) => updateForm('brand_safe_status', event.target.value)} className="w-full rounded-2xl border border-white/10 bg-white/[0.04] px-4 py-3 text-white outline-none transition focus:border-sky-300/35 focus:bg-white/[0.06]" maxLength={40} />
</Field>
<label className="flex items-center gap-3 rounded-2xl border border-white/10 bg-white/[0.04] px-4 py-3 text-sm text-white">
<input type="checkbox" checked={form.analytics_enabled} onChange={(event) => updateForm('analytics_enabled', event.target.checked)} />
Analytics enabled
</label>
</div>
<div className="grid gap-5 md:grid-cols-2">
<Field label="Brand Safe Status">
<input type="text" value={form.brand_safe_status} onChange={(event) => updateForm('brand_safe_status', event.target.value)} className="w-full rounded-2xl border border-white/10 bg-white/[0.04] px-4 py-3 text-white outline-none transition focus:border-sky-300/35 focus:bg-white/[0.06]" maxLength={40} />
</Field>
<label className="flex items-center gap-3 self-end rounded-2xl border border-white/10 bg-white/[0.04] px-4 py-3 text-sm text-white">
<input type="checkbox" checked={form.analytics_enabled} onChange={(event) => updateForm('analytics_enabled', event.target.checked)} />
Analytics enabled
</label>
</div>
<Field label="Editorial Notes" help="Internal editorial context for campaign planning, curation rationale, and staff handoff.">
<textarea
value={form.editorial_notes}
onChange={(event) => updateForm('editorial_notes', event.target.value)}
className="min-h-[96px] w-full rounded-2xl border border-white/10 bg-white/[0.04] px-4 py-3 text-white outline-none transition focus:border-sky-300/35 focus:bg-white/[0.06]"
maxLength={2000}
/>
</Field>
{canModerate ? (
<Field label="Staff Commercial Notes" help="Internal admin-only notes for sponsorship readiness, partner handling, and commercial review.">
<Field label="Editorial Notes" help="Internal editorial context for campaign planning, curation rationale, and staff handoff.">
<textarea
value={form.staff_commercial_notes}
onChange={(event) => updateForm('staff_commercial_notes', event.target.value)}
className="min-h-[96px] w-full rounded-2xl border border-amber-300/15 bg-amber-400/10 px-4 py-3 text-white outline-none transition focus:border-amber-300/35 focus:bg-amber-400/15"
value={form.editorial_notes}
onChange={(event) => updateForm('editorial_notes', event.target.value)}
className="min-h-[96px] w-full rounded-2xl border border-white/10 bg-white/[0.04] px-4 py-3 text-white outline-none transition focus:border-sky-300/35 focus:bg-white/[0.06]"
maxLength={2000}
/>
</Field>
) : null}
{form.type === 'editorial' ? (
<div className="grid gap-5 md:grid-cols-3">
<Field label="Editorial Owner Mode" help="Choose whether this editorial lives under the current curator, another staff account, or the system editorial identity.">
<select value={form.editorial_owner_mode} onChange={(event) => updateForm('editorial_owner_mode', 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">
<option value="creator">Current curator</option>
<option value="staff_account">Staff account</option>
<option value="system">System editorial identity</option>
</select>
{canModerate ? (
<Field label="Staff Commercial Notes" help="Admin-only notes for sponsorship readiness, partner handling, and commercial review.">
<textarea
value={form.staff_commercial_notes}
onChange={(event) => updateForm('staff_commercial_notes', event.target.value)}
className="min-h-[96px] w-full rounded-2xl border border-amber-300/15 bg-amber-400/10 px-4 py-3 text-white outline-none transition focus:border-amber-300/35 focus:bg-amber-400/15"
maxLength={2000}
/>
</Field>
{form.editorial_owner_mode === 'staff_account' ? (
<Field label="Staff Account Username" help="Must be an admin or moderator username.">
<input
type="text"
value={form.editorial_owner_username}
onChange={(event) => updateForm('editorial_owner_username', event.target.value.trimStart())}
className="w-full rounded-2xl border border-white/10 bg-white/[0.04] px-4 py-3 text-white outline-none transition focus:border-sky-300/35 focus:bg-white/[0.06]"
placeholder="skinbase-editorial"
maxLength={60}
/>
</Field>
) : null}
{form.editorial_owner_mode === 'system' ? (
<Field label="System Owner Label" help="Public-facing label for system-owned editorials.">
<input
type="text"
value={form.editorial_owner_label}
onChange={(event) => updateForm('editorial_owner_label', event.target.value)}
className="w-full rounded-2xl border border-white/10 bg-white/[0.04] px-4 py-3 text-white outline-none transition focus:border-sky-300/35 focus:bg-white/[0.06]"
placeholder="Skinbase Editorial"
maxLength={120}
/>
</Field>
) : null}
</div>
) : null}
<div className="grid gap-3 md:grid-cols-3">
<label className="flex items-center gap-3 rounded-2xl border border-white/10 bg-white/[0.04] px-4 py-3 text-sm text-white">
<input type="checkbox" checked={form.allow_submissions} onChange={(event) => updateForm('allow_submissions', event.target.checked)} />
Allow submissions
</label>
<label className="flex items-center gap-3 rounded-2xl border border-white/10 bg-white/[0.04] px-4 py-3 text-sm text-white">
<input type="checkbox" checked={form.allow_comments} onChange={(event) => updateForm('allow_comments', event.target.checked)} />
Allow comments
</label>
<label className="flex items-center gap-3 rounded-2xl border border-white/10 bg-white/[0.04] px-4 py-3 text-sm text-white">
<input type="checkbox" checked={form.allow_saves} onChange={(event) => updateForm('allow_saves', event.target.checked)} />
Allow saves
</label>
<label className="flex items-center gap-3 rounded-2xl border border-white/10 bg-white/[0.04] px-4 py-3 text-sm text-white">
<input type="checkbox" checked={form.commercial_eligibility} onChange={(event) => updateForm('commercial_eligibility', event.target.checked)} />
Commercially eligible
</label>
</div>
<div className="grid gap-5 md:grid-cols-3">
<Field label="Visibility">
<select value={form.visibility} onChange={(event) => updateForm('visibility', 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">
<option value="public">Public</option>
<option value="unlisted">Unlisted</option>
<option value="private">Private</option>
</select>
</Field>
{!isSmartMode ? (
<Field label="Sort Mode" help="Manual keeps the display order under your control.">
<select value={form.sort_mode} onChange={(event) => updateForm('sort_mode', 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">
<option value="manual">Manual</option>
<option value="newest">Newest</option>
<option value="oldest">Oldest</option>
<option value="popular">Popular</option>
</select>
</Field>
) : (
<Field label="Match Mode" help="All means every rule must match. Any is broader.">
<select
value={smartRules.match}
onChange={(event) => setSmartRules((current) => ({ ...current, match: 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"
>
<option value="all">All rules</option>
<option value="any">Any rule</option>
</select>
</Field>
)}
{!isSmartMode ? (
<Field label="Cover Artwork" help={attachedCoverOptions.length ? 'Choose a cover from artworks already attached to this collection.' : 'Attach artworks first to pick a manual cover.'}>
<select
value={form.cover_artwork_id}
onChange={(event) => updateForm('cover_artwork_id', event.target.value)}
disabled={!attachedCoverOptions.length}
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 disabled:cursor-not-allowed disabled:text-slate-500"
>
<option value="">Automatic cover</option>
{attachedCoverOptions.map((artwork) => (
<option key={artwork.id} value={artwork.id}>{artwork.title}</option>
))}
</select>
</Field>
) : (
<Field label="Smart Sort" help="How matching artworks should be ordered.">
<select
value={smartRules.sort}
onChange={(event) => {
setSmartRules((current) => ({ ...current, sort: event.target.value }))
updateForm('sort_mode', 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"
>
{(smartRuleOptions?.sort_options || []).map((option) => (
<option key={option.value} value={option.value}>{option.label}</option>
))}
</select>
</Field>
)}
</div>
) : null}
</AdvancedSection>
<div className="flex flex-wrap items-center gap-3">
<button
@@ -2337,13 +2408,13 @@ export default function CollectionManage() {
) : null}
<section className="rounded-[32px] border border-white/10 bg-white/[0.04] p-6 backdrop-blur-sm">
<p className="text-[11px] font-semibold uppercase tracking-[0.22em] text-sky-200/80">v4 Guidance</p>
<p className="text-[11px] font-semibold uppercase tracking-[0.22em] text-sky-200/80">Quick Start</p>
<div className="mt-4 space-y-3 text-sm leading-relaxed text-slate-300">
<div className="rounded-2xl border border-white/10 bg-white/[0.04] px-4 py-3">
Manual collections work best for hand-picked sequences, premium presentation modes, and campaign landing pages.
Start with just a title everything else can be filled in later. The advanced sections are there when you need them.
</div>
<div className="rounded-2xl border border-white/10 bg-white/[0.04] px-4 py-3">
Smart collections are ideal for creator-first rulesets that keep series or editorial shelves fresh without cross-user leakage.
<span className="font-semibold text-white">Manual</span> is great for hand-picked storytelling. <span className="font-semibold text-white">Smart</span> keeps a collection up to date automatically using rules you define.
</div>
</div>
</section>

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>