feat: ship creator journey v2 and profile updates

This commit is contained in:
2026-04-12 21:42:07 +02:00
parent a2457f4e49
commit d5cff21ea2
335 changed files with 20147 additions and 1545 deletions

View File

@@ -0,0 +1,240 @@
import React, { useState } from 'react'
import Modal from '../ui/Modal'
function EvolutionArtworkCard({ card }) {
if (!card) return null
const shouldBlur = Boolean(card?.maturity?.should_blur)
return (
<a
href={card.url}
className="group block overflow-hidden rounded-[26px] border border-white/10 bg-[linear-gradient(180deg,rgba(255,255,255,0.06),rgba(255,255,255,0.03))] transition hover:border-white/20 hover:bg-white/[0.07]"
>
<div className="relative aspect-[1.08/1] overflow-hidden bg-slate-950">
{card.thumbnail ? (
<img
src={card.thumbnail}
alt={card.title}
className={`h-full w-full object-cover transition duration-500 group-hover:scale-[1.03] ${shouldBlur ? 'scale-[1.03] blur-xl' : ''}`}
/>
) : (
<div className="flex h-full w-full items-center justify-center text-slate-600">
<i className="fa-solid fa-image text-3xl" />
</div>
)}
<div className="absolute left-4 top-4 inline-flex items-center gap-2 rounded-full border border-white/10 bg-black/45 px-3 py-1.5 text-[10px] font-semibold uppercase tracking-[0.18em] text-white backdrop-blur-md">
<span className="h-1.5 w-1.5 rounded-full bg-sky-300" />
{card.role_label}
</div>
{shouldBlur ? (
<div className="absolute inset-x-0 bottom-0 bg-[linear-gradient(180deg,rgba(15,23,42,0),rgba(15,23,42,0.9))] px-4 py-4 text-sm text-white/85">
Mature artwork preview is softened for your current viewer settings.
</div>
) : null}
</div>
<div className="space-y-2 px-4 py-4 sm:px-5">
<div className="flex flex-wrap items-center gap-2 text-[11px] font-semibold uppercase tracking-[0.18em] text-slate-400">
{card.content_type ? <span>{card.content_type}</span> : null}
{card.category ? <span className="text-slate-500">{card.category}</span> : null}
</div>
<h3 className="text-lg font-semibold tracking-[-0.02em] text-white">{card.title}</h3>
<div className="flex flex-wrap items-center gap-2 text-sm text-slate-300">
<span>{card.publisher}</span>
{card.year ? <span className="text-slate-500">{card.year}</span> : null}
</div>
</div>
</a>
)
}
function ComparisonModal({ item, open, onClose }) {
if (!item) return null
return (
<Modal open={open} onClose={onClose} title={item.compare?.title || 'Compare versions'} size="full">
<div className="space-y-6">
<div className="flex flex-col gap-3 lg:flex-row lg:items-end lg:justify-between">
<div>
<div className="text-[11px] font-semibold uppercase tracking-[0.22em] text-sky-200/80">{item.heading}</div>
<h3 className="mt-2 text-2xl font-semibold tracking-[-0.03em] text-white">{item.relation_label}</h3>
<p className="mt-2 max-w-3xl text-sm leading-7 text-slate-300">{item.summary}</p>
</div>
{item.years_apart_label ? (
<div className="inline-flex items-center gap-2 rounded-full border border-white/10 bg-white/[0.04] px-4 py-2 text-sm text-slate-200">
<i className="fa-regular fa-clock" aria-hidden="true" />
{item.years_apart_label}
</div>
) : null}
</div>
<div className="grid gap-5 lg:grid-cols-2">
{[item.before, item.after].map((card) => (
<div key={`${item.id}-${card.id}`} className="overflow-hidden rounded-[28px] border border-white/10 bg-white/[0.03]">
<div className="flex items-center justify-between border-b border-white/10 px-5 py-4">
<div>
<div className="text-[11px] font-semibold uppercase tracking-[0.18em] text-slate-400">{card.role_label}</div>
<div className="mt-1 text-base font-semibold text-white">{card.title}</div>
</div>
<a href={card.url} className="inline-flex items-center gap-2 rounded-2xl border border-white/10 bg-white/[0.04] px-3 py-2 text-sm font-medium text-white transition hover:bg-white/[0.08]">
Open
<i className="fa-solid fa-arrow-up-right-from-square text-slate-500" aria-hidden="true" />
</a>
</div>
<div className="relative aspect-[4/3] overflow-hidden bg-slate-950">
{card.image_lg ? (
<img
src={card.image_lg}
alt={card.title}
className={`h-full w-full object-cover ${card?.maturity?.should_blur ? 'scale-[1.03] blur-xl' : ''}`}
/>
) : (
<div className="flex h-full w-full items-center justify-center text-slate-600">
<i className="fa-solid fa-image text-4xl" />
</div>
)}
</div>
<div className="flex flex-wrap items-center gap-2 px-5 py-4 text-sm text-slate-300">
<span>{card.publisher}</span>
{card.year ? <span className="text-slate-500">{card.year}</span> : null}
{card.category ? <span className="rounded-full border border-white/10 bg-white/[0.04] px-2.5 py-1 text-[10px] font-semibold uppercase tracking-[0.16em] text-slate-300">{card.category}</span> : null}
</div>
</div>
))}
</div>
{item.note ? (
<div className="rounded-[26px] border border-sky-300/20 bg-sky-300/10 px-5 py-4 text-sm leading-7 text-sky-50">
<div className="text-[11px] font-semibold uppercase tracking-[0.2em] text-sky-100/80">Creator note</div>
<p className="mt-2 whitespace-pre-wrap">{item.note}</p>
</div>
) : null}
</div>
</Modal>
)
}
function EvolutionStoryBlock({ item, onCompare }) {
if (!item) return null
return (
<section className="overflow-hidden rounded-[30px] border border-white/[0.08] bg-[radial-gradient(circle_at_top_left,rgba(56,189,248,0.16),transparent_34%),linear-gradient(180deg,rgba(255,255,255,0.06),rgba(255,255,255,0.025))] p-5 shadow-[0_22px_55px_rgba(2,6,23,0.26)] backdrop-blur-xl sm:p-6">
<div className="flex flex-col gap-4 lg:flex-row lg:items-start lg:justify-between">
<div className="max-w-2xl">
<div className="text-[11px] font-semibold uppercase tracking-[0.24em] text-sky-200/80">{item.heading}</div>
<h2 className="mt-2 text-[28px] font-semibold tracking-[-0.04em] text-white">{item.relation_label}</h2>
<p className="mt-2 text-sm leading-7 text-slate-200/90">{item.summary}</p>
</div>
<div className="flex flex-wrap items-center gap-2 lg:justify-end">
{item.years_apart_label ? (
<span className="inline-flex items-center gap-2 rounded-full border border-white/10 bg-white/[0.04] px-3 py-1.5 text-xs font-semibold uppercase tracking-[0.16em] text-slate-200">
<i className="fa-regular fa-clock" aria-hidden="true" />
{item.years_apart_label}
</span>
) : null}
{item.compare?.available ? (
<button
type="button"
onClick={() => onCompare(item)}
className="inline-flex items-center gap-2 rounded-2xl border border-sky-300/25 bg-sky-300/12 px-4 py-2.5 text-sm font-semibold text-sky-100 transition hover:bg-sky-300/18"
>
<i className="fa-solid fa-up-right-and-down-left-from-center" aria-hidden="true" />
Compare side by side
</button>
) : null}
</div>
</div>
<div className="mt-5 grid gap-4 lg:grid-cols-2">
<EvolutionArtworkCard card={item.before} />
<EvolutionArtworkCard card={item.after} />
</div>
{item.note ? (
<div className="mt-5 rounded-[24px] border border-white/10 bg-black/20 px-4 py-4 text-sm leading-7 text-slate-200/90">
<div className="text-[11px] font-semibold uppercase tracking-[0.2em] text-slate-400">Creator note</div>
<p className="mt-2 whitespace-pre-wrap">{item.note}</p>
</div>
) : null}
</section>
)
}
function EvolutionUpdates({ updates, onCompare }) {
if (!updates?.length) return null
return (
<section className="rounded-[30px] border border-white/[0.08] bg-[linear-gradient(180deg,rgba(255,255,255,0.05),rgba(255,255,255,0.02))] p-5 shadow-[0_22px_55px_rgba(0,0,0,0.18)] backdrop-blur-xl sm:p-6">
<div className="flex flex-col gap-2 sm:flex-row sm:items-end sm:justify-between">
<div>
<div className="text-[11px] font-semibold uppercase tracking-[0.24em] text-white/55">Updated Versions</div>
<h2 className="mt-2 text-2xl font-semibold tracking-[-0.03em] text-white">This piece has later evolutions</h2>
</div>
<div className="text-sm text-slate-400">Follow how the creator revisited the idea over time.</div>
</div>
<div className="mt-5 space-y-4">
{updates.map((item) => (
<article key={item.id} className="rounded-[26px] border border-white/10 bg-white/[0.03] p-4 sm:p-5">
<div className="flex flex-col gap-4 lg:flex-row lg:items-start lg:justify-between">
<div className="max-w-2xl">
<div className="text-[11px] font-semibold uppercase tracking-[0.2em] text-slate-400">{item.heading}</div>
<h3 className="mt-2 text-xl font-semibold tracking-[-0.02em] text-white">{item.after?.title}</h3>
<p className="mt-2 text-sm leading-7 text-slate-300">{item.summary}</p>
</div>
<div className="flex flex-wrap gap-2">
{item.years_apart_label ? (
<span className="inline-flex items-center gap-2 rounded-full border border-white/10 bg-white/[0.04] px-3 py-1.5 text-xs font-semibold uppercase tracking-[0.16em] text-slate-200">
<i className="fa-regular fa-clock" aria-hidden="true" />
{item.years_apart_label}
</span>
) : null}
{item.compare?.available ? (
<button
type="button"
onClick={() => onCompare(item)}
className="inline-flex items-center gap-2 rounded-2xl border border-white/10 bg-white/[0.04] px-4 py-2.5 text-sm font-semibold text-white transition hover:bg-white/[0.08]"
>
Compare
</button>
) : null}
</div>
</div>
<div className="mt-4 grid gap-4 lg:grid-cols-2">
<EvolutionArtworkCard card={item.before} />
<EvolutionArtworkCard card={item.after} />
</div>
{item.note ? <p className="mt-4 text-sm leading-7 text-slate-300">{item.note}</p> : null}
</article>
))}
</div>
</section>
)
}
export default function ArtworkEvolutionPanel({ evolution }) {
const [compareItem, setCompareItem] = useState(null)
if (!evolution?.primary && !evolution?.updates?.length) {
return null
}
return (
<>
<div className="space-y-5">
{evolution.primary ? <EvolutionStoryBlock item={evolution.primary} onCompare={setCompareItem} /> : null}
{evolution.updates?.length ? <EvolutionUpdates updates={evolution.updates} onCompare={setCompareItem} /> : null}
</div>
<ComparisonModal item={compareItem} open={Boolean(compareItem)} onClose={() => setCompareItem(null)} />
</>
)
}