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

@@ -1,4 +1,4 @@
import React, { useState, useCallback, useEffect, useMemo } from 'react'
import React, { useState, useCallback, useEffect } from 'react'
import { createRoot } from 'react-dom/client'
import axios from 'axios'
import ArtworkHero from '../components/artwork/ArtworkHero'
@@ -7,6 +7,7 @@ import ArtworkMeta from '../components/artwork/ArtworkMeta'
import ArtworkAwards from '../components/artwork/ArtworkAwards'
import ArtworkTags from '../components/artwork/ArtworkTags'
import ArtworkDescription from '../components/artwork/ArtworkDescription'
import ArtworkEvolutionPanel from '../components/artwork/ArtworkEvolutionPanel'
import ArtworkComments from '../components/artwork/ArtworkComments'
import ArtworkActionBar from '../components/artwork/ArtworkActionBar'
import ArtworkDetailsPanel from '../components/artwork/ArtworkDetailsPanel'
@@ -42,6 +43,7 @@ function publisherToGroupSummary(publisher) {
function ArtworkPage({ artwork: initialArtwork, related: initialRelated, presentMd: initialMd, presentLg: initialLg, presentXl: initialXl, presentSq: initialSq, canonicalUrl: initialCanonical, isAuthenticated = false, comments: initialComments = [], groupSummary: initialGroupSummary = null }) {
const [viewerOpen, setViewerOpen] = useState(false)
const [showMatureArtwork, setShowMatureArtwork] = useState(false)
const openViewer = useCallback(() => setViewerOpen(true), [])
const closeViewer = useCallback(() => setViewerOpen(false), [])
@@ -98,47 +100,77 @@ function ArtworkPage({ artwork: initialArtwork, related: initialRelated, present
setGroupSummary(data.group_summary ?? publisherToGroupSummary(data.publisher))
setSelectedMediaId('cover')
setViewerOpen(false) // close viewer when navigating away
setShowMatureArtwork(false)
}, [])
if (!artwork) return null
const mediaItems = useMemo(() => {
const coverItem = {
id: 'cover',
label: 'Cover art',
thumbUrl: presentSq?.url || presentMd?.url || presentLg?.url || artwork?.thumbs?.sq?.url || artwork?.thumbs?.md?.url || null,
mdUrl: presentMd?.url || artwork?.thumbs?.md?.url || null,
lgUrl: presentLg?.url || artwork?.thumbs?.lg?.url || null,
xlUrl: presentXl?.url || artwork?.thumbs?.xl?.url || null,
width: Number(artwork?.dimensions?.width || artwork?.width || 0) || null,
height: Number(artwork?.dimensions?.height || artwork?.height || 0) || null,
}
const requiresInterstitial = Boolean(artwork?.maturity?.requires_interstitial) && !showMatureArtwork
const screenshotItems = Array.isArray(artwork?.screenshots)
? artwork.screenshots.map((item, index) => ({
id: item.id || `shot-${index + 1}`,
label: item.label || `Screenshot ${index + 1}`,
thumbUrl: item.thumb_url || item.url || null,
mdUrl: item.url || item.thumb_url || null,
lgUrl: item.url || item.thumb_url || null,
xlUrl: item.url || item.thumb_url || null,
width: null,
height: null,
}))
: []
if (requiresInterstitial) {
return (
<main className="pb-24 pt-8 lg:pb-12 lg:pt-10">
<div className="mx-auto w-full max-w-3xl px-4 sm:px-6 lg:px-8">
<section className="rounded-[32px] border border-amber-300/20 bg-[radial-gradient(circle_at_top_left,rgba(251,191,36,0.18),transparent_32%),linear-gradient(180deg,rgba(15,23,42,0.96),rgba(2,6,23,0.92))] p-6 shadow-[0_24px_70px_rgba(2,6,23,0.34)] md:p-8">
<p className="text-[11px] font-semibold uppercase tracking-[0.28em] text-amber-200/80">Content warning</p>
<h1 className="mt-3 text-3xl font-semibold tracking-[-0.04em] text-white">{artwork?.maturity?.warning_title || 'Mature content warning'}</h1>
<p className="mt-3 text-sm leading-relaxed text-slate-200/90">{artwork?.maturity?.warning_message || 'This artwork may contain mature material. Continue only if you want to view it.'}</p>
<div className="mt-5 rounded-2xl border border-white/10 bg-white/[0.04] px-4 py-4 text-sm text-slate-300">
<div className="font-semibold text-white">{artwork.title}</div>
<div className="mt-1">by {artwork?.publisher?.name || artwork?.user?.name || 'Artist'}</div>
</div>
<div className="mt-6 flex flex-wrap gap-3">
<button
type="button"
onClick={() => setShowMatureArtwork(true)}
className="inline-flex items-center gap-2 rounded-2xl border border-amber-300/25 bg-amber-400/12 px-5 py-3 text-sm font-semibold text-amber-100 transition hover:bg-amber-400/18"
>
<i className="fa-solid fa-eye" />
Show artwork
</button>
<a
href={artwork?.publisher?.profile_url || '/discover/trending'}
className="inline-flex items-center gap-2 rounded-2xl border border-white/10 bg-white/[0.04] px-5 py-3 text-sm font-semibold text-white transition hover:bg-white/[0.08]"
>
<i className="fa-solid fa-arrow-left" />
Leave this page
</a>
</div>
</section>
</div>
</main>
)
}
return [coverItem, ...screenshotItems].filter((item) => Boolean(item.thumbUrl || item.lgUrl || item.xlUrl))
}, [artwork, presentMd, presentLg, presentXl, presentSq])
const coverItem = {
id: 'cover',
label: 'Cover art',
thumbUrl: presentSq?.url || presentMd?.url || presentLg?.url || artwork?.thumbs?.sq?.url || artwork?.thumbs?.md?.url || null,
mdUrl: presentMd?.url || artwork?.thumbs?.md?.url || null,
lgUrl: presentLg?.url || artwork?.thumbs?.lg?.url || null,
xlUrl: presentXl?.url || artwork?.thumbs?.xl?.url || null,
width: Number(artwork?.dimensions?.width || artwork?.width || 0) || null,
height: Number(artwork?.dimensions?.height || artwork?.height || 0) || null,
}
const screenshotItems = Array.isArray(artwork?.screenshots)
? artwork.screenshots.map((item, index) => ({
id: item.id || `shot-${index + 1}`,
label: item.label || `Screenshot ${index + 1}`,
thumbUrl: item.thumb_url || item.url || null,
mdUrl: item.url || item.thumb_url || null,
lgUrl: item.url || item.thumb_url || null,
xlUrl: item.url || item.thumb_url || null,
width: null,
height: null,
}))
: []
const mediaItems = [coverItem, ...screenshotItems].filter((item) => Boolean(item.thumbUrl || item.lgUrl || item.xlUrl))
const selectedMedia = mediaItems.find((item) => item.id === selectedMediaId) || mediaItems[0] || null
useEffect(() => {
if (!selectedMedia && mediaItems.length > 0) {
setSelectedMediaId(mediaItems[0].id)
}
}, [mediaItems, selectedMedia])
const initialAwards = artwork?.awards ?? null
const initialAwards = artwork?.medals ?? artwork?.awards ?? null
return (
<>
@@ -188,6 +220,9 @@ function ArtworkPage({ artwork: initialArtwork, related: initialRelated, present
{/* Description */}
<ArtworkDescription artwork={artwork} />
{/* Artwork evolution */}
<ArtworkEvolutionPanel evolution={artwork?.evolution} />
{/* Artwork reactions */}
{reactionTotals !== null && (
<section className="relative z-20 overflow-visible rounded-[28px] border border-white/[0.08] bg-[radial-gradient(circle_at_top_left,rgba(245,158,11,0.14),transparent_42%),linear-gradient(180deg,rgba(255,255,255,0.06),rgba(255,255,255,0.02))] px-5 py-5 shadow-[0_22px_55px_rgba(0,0,0,0.26)] backdrop-blur-xl sm:px-6">
@@ -232,7 +267,7 @@ function ArtworkPage({ artwork: initialArtwork, related: initialRelated, present
{/* Details (collapsible) */}
<ArtworkDetailsPanel artwork={artwork} stats={liveStats} />
{/* Awards */}
{/* Medals */}
<ArtworkAwards artwork={artwork} initialAwards={initialAwards} isAuthenticated={isAuthenticated} />
</aside>
</div>