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

@@ -65,7 +65,7 @@ export default function HomeBecauseYouLike({ items, preferences }) {
</a>
</div>
<div className="grid grid-cols-2 gap-4 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5">
{items.slice(0, Math.floor(items.length / 5) * 5 || items.length).map((item) => (
{items.map((item) => (
<ArtCard key={item.id} item={item} />
))}
</div>

View File

@@ -24,7 +24,7 @@ export default function HomeCTA({ isLoggedIn }) {
<div className="mt-6 flex flex-wrap justify-center gap-3">
<a
href={uploadHref}
className="rounded-xl bg-accent px-6 py-2.5 text-sm font-semibold text-white shadow-lg shadow-accent/20 transition hover:brightness-110 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent"
className="btn-accent-solid rounded-xl px-6 py-2.5 text-sm font-semibold"
>
Upload your artwork
</a>

View File

@@ -24,7 +24,7 @@ function CreatorCard({ creator }) {
<a href={creator.url} className="relative block">
<img
src={creator.avatar}
alt={creator.name}
alt=""
className="mx-auto h-16 w-16 rounded-full object-cover ring-4 bg-nova-800/80 ring-nova-800"
loading="lazy"
decoding="async"

View File

@@ -14,7 +14,7 @@ export default function HomeFresh({ items }) {
</div>
<ArtworkGalleryGrid
items={items.slice(0, 8)}
items={items}
showStats={false}
/>
</section>

View File

@@ -74,7 +74,7 @@ export default function HomeFromFollowing({ items }) {
</a>
</div>
<div className="grid grid-cols-2 gap-4 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5">
{items.slice(0, Math.floor(items.length / 5) * 5 || items.length).map((item) => (
{items.map((item) => (
<ArtCard key={item.id} item={item} />
))}
</div>

View File

@@ -30,7 +30,7 @@ function GroupSpotlightCard({ group }) {
{group.avatar_url ? (
<img
src={group.avatar_url}
alt={group.name}
alt=""
className="h-full w-full object-cover"
loading="lazy"
decoding="async"

View File

@@ -1,6 +1,7 @@
import React from 'react'
const FALLBACK = 'https://files.skinbase.org/default/missing_lg.webp'
const HERO_SIZES = '100vw'
export default function HomeHero({ artwork }) {
if (!artwork) {
@@ -15,7 +16,7 @@ export default function HomeHero({ artwork }) {
Discover. Create. Inspire.
</p>
<div className="mt-4 flex flex-wrap gap-3">
<a href="/discover/trending" className="rounded-xl bg-accent px-5 py-2 text-sm font-semibold text-white shadow-lg transition hover:brightness-110">Explore Trending</a>
<a href="/discover/trending" className="btn-accent-solid rounded-xl px-5 py-2 text-sm font-semibold">Explore Trending</a>
</div>
</div>
</section>
@@ -23,16 +24,20 @@ export default function HomeHero({ artwork }) {
}
const src = artwork.thumb_lg || artwork.thumb || FALLBACK
const srcSet = artwork.thumb_srcset || null
return (
<section className="group relative flex min-h-[62vh] max-h-[420px] w-full items-end overflow-hidden bg-nova-900 md:min-h-[38vh] md:max-h-[460px]">
{/* Background image */}
<img
src={src}
srcSet={srcSet || undefined}
sizes={srcSet ? HERO_SIZES : undefined}
alt={artwork.title}
className="absolute inset-0 h-full w-full object-cover transition-transform duration-700 group-hover:scale-[1.02]"
fetchPriority="high"
decoding="async"
loading="eager"
decoding="sync"
onError={(e) => { e.currentTarget.src = FALLBACK }}
/>
@@ -53,7 +58,7 @@ export default function HomeHero({ artwork }) {
<div className="mt-4 flex flex-wrap gap-3">
<a
href="/discover/trending"
className="rounded-xl bg-accent px-5 py-2 text-sm font-semibold text-white shadow-lg transition hover:brightness-110"
className="btn-accent-solid rounded-xl px-5 py-2 text-sm font-semibold"
>
Explore Trending
</a>

View File

@@ -0,0 +1,24 @@
import React from 'react'
import ArtworkGalleryGrid from '../../components/artwork/ArtworkGalleryGrid'
export default function HomeMedalHighlights({ title, href = null, items, description = '' }) {
if (!Array.isArray(items) || items.length === 0) return null
return (
<section className="mt-14 px-4 sm:px-6 lg:px-8">
<div className="mb-5 flex items-end justify-between gap-4">
<div>
<h2 className="text-xl font-bold text-white">{title}</h2>
{description ? <p className="mt-2 max-w-2xl text-sm text-slate-400">{description}</p> : null}
</div>
{href ? (
<a href={href} className="text-sm text-nova-300 transition hover:text-white">
See all
</a>
) : null}
</div>
<ArtworkGalleryGrid items={items} className="xl:grid-cols-4" />
</section>
)
}

View File

@@ -8,6 +8,7 @@ const HomeTrendingForYou = lazy(() => import('./HomeTrendingForYou'))
const HomeBecauseYouLike = lazy(() => import('./HomeBecauseYouLike'))
const HomeSuggestedCreators = lazy(() => import('./HomeSuggestedCreators'))
const HomeTrending = lazy(() => import('./HomeTrending'))
const HomeMedalHighlights = lazy(() => import('./HomeMedalHighlights'))
const HomeRising = lazy(() => import('./HomeRising'))
const HomeFresh = lazy(() => import('./HomeFresh'))
const HomeCollections = lazy(() => import('./HomeCollections'))
@@ -18,30 +19,122 @@ const HomeCreators = lazy(() => import('./HomeCreators'))
const HomeNews = lazy(() => import('./HomeNews'))
const HomeCTA = lazy(() => import('./HomeCTA'))
function SectionFallback() {
function cx(...parts) {
return parts.filter(Boolean).join(' ')
}
function SectionFallback({ variant = 'gallery' }) {
if (variant === 'welcome') {
return (
<div className="mt-10 px-4 sm:px-6 lg:px-8" aria-hidden="true">
<div className="h-20 animate-pulse rounded-[28px] border border-white/10 bg-nova-800/70" />
</div>
)
}
if (variant === 'tags') {
return (
<section className="mt-14 px-4 sm:px-6 lg:px-8" aria-hidden="true">
<div className="mb-5 h-8 w-48 animate-pulse rounded-xl bg-nova-800/70" />
<div className="flex flex-wrap gap-2">
{Array.from({ length: 12 }).map((_, index) => (
<div
key={index}
className="h-9 animate-pulse rounded-full bg-nova-800/70"
style={{ width: `${88 + (index % 4) * 16}px` }}
/>
))}
</div>
</section>
)
}
if (variant === 'cta') {
return (
<section className="mt-14 px-4 sm:px-6 lg:px-8" aria-hidden="true">
<div className="h-40 animate-pulse rounded-[28px] border border-white/10 bg-nova-800/70" />
</section>
)
}
const cardClassName = variant === 'categories'
? 'h-28 rounded-2xl'
: variant === 'news'
? 'h-24 rounded-2xl'
: variant === 'creators'
? 'h-64 rounded-2xl'
: variant === 'collections'
? 'h-80 rounded-[28px]'
: variant === 'groups'
? 'h-80 rounded-[28px]'
: 'aspect-[4/3] rounded-2xl'
const gridClassName = variant === 'creators'
? 'grid-cols-2 sm:grid-cols-3 lg:grid-cols-6'
: variant === 'news'
? 'grid-cols-1'
: variant === 'categories'
? 'grid-cols-2 lg:grid-cols-4'
: variant === 'collections'
? 'grid-cols-1 lg:grid-cols-2 xl:grid-cols-3'
: variant === 'groups'
? 'grid-cols-1 sm:grid-cols-2 xl:grid-cols-4'
: 'grid-cols-2 xl:grid-cols-4'
const cardCount = variant === 'creators' ? 6 : variant === 'news' ? 4 : 4
return (
<div className="mt-14 h-48 animate-pulse rounded-xl bg-nova-800 mx-4 sm:mx-6 lg:mx-8" />
<section className="mt-14 px-4 sm:px-6 lg:px-8" aria-hidden="true">
<div className="mb-5 flex items-center justify-between gap-4">
<div>
<div className="h-8 w-48 animate-pulse rounded-xl bg-nova-800/70" />
{(variant === 'collections' || variant === 'groups' || variant === 'news') && (
<div className="mt-3 h-4 w-80 max-w-full animate-pulse rounded bg-nova-800/60" />
)}
</div>
<div className="hidden h-5 w-24 animate-pulse rounded bg-nova-800/60 sm:block" />
</div>
<div className={cx('grid gap-4', gridClassName)}>
{Array.from({ length: cardCount }).map((_, index) => (
<div key={index} className={cx('animate-pulse bg-nova-800/70', cardClassName)} />
))}
</div>
</section>
)
}
function GuestHomePage(props) {
const { rising, trending, fresh, tags, creators, news, collections_featured, collections_trending, collections_editorial, collections_community, groups } = props
const { rising, trending, community_favorites, hall_of_fame, fresh, tags, creators, news, collections_featured, collections_trending, collections_editorial, collections_community, groups } = props
return (
<>
<Suspense fallback={<SectionFallback />}>
<Suspense fallback={<SectionFallback variant="gallery" />}>
<HomeRising items={rising} />
</Suspense>
<Suspense fallback={<SectionFallback />}>
<Suspense fallback={<SectionFallback variant="gallery" />}>
<HomeTrending items={trending} />
</Suspense>
<Suspense fallback={<SectionFallback variant="gallery" />}>
<HomeMedalHighlights
title="Community Favorites"
href="/explore/top-rated"
description="Recent medal momentum from the community. This rail highlights the strongest 30-day medal signal."
items={community_favorites}
/>
</Suspense>
<Suspense fallback={<SectionFallback variant="gallery" />}>
<HomeMedalHighlights
title="Hall of Fame"
href="/explore/best"
description="All-time medal standouts that keep being remembered long after publication."
items={hall_of_fame}
/>
</Suspense>
{/* 3. Fresh Uploads */}
<Suspense fallback={<SectionFallback />}>
<Suspense fallback={<SectionFallback variant="gallery" />}>
<HomeFresh items={fresh} />
</Suspense>
<Suspense fallback={<SectionFallback />}>
<Suspense fallback={<SectionFallback variant="collections" />}>
<HomeCollections
featured={collections_featured}
trending={collections_trending}
@@ -50,32 +143,32 @@ function GuestHomePage(props) {
/>
</Suspense>
<Suspense fallback={<SectionFallback />}>
<Suspense fallback={<SectionFallback variant="groups" />}>
<HomeGroups groups={groups} />
</Suspense>
{/* 4. Explore Categories */}
<Suspense fallback={<SectionFallback />}>
<Suspense fallback={<SectionFallback variant="categories" />}>
<HomeCategories />
</Suspense>
{/* 5. Popular Tags */}
<Suspense fallback={<SectionFallback />}>
<Suspense fallback={<SectionFallback variant="tags" />}>
<HomeTags tags={tags} />
</Suspense>
{/* 6. Top Creators */}
<Suspense fallback={<SectionFallback />}>
<Suspense fallback={<SectionFallback variant="creators" />}>
<HomeCreators creators={creators} />
</Suspense>
{/* 7. News */}
<Suspense fallback={<SectionFallback />}>
<Suspense fallback={<SectionFallback variant="news" />}>
<HomeNews items={news} />
</Suspense>
{/* 8. CTA Upload */}
<Suspense fallback={<SectionFallback />}>
<Suspense fallback={<SectionFallback variant="cta" />}>
<HomeCTA isLoggedIn={false} />
</Suspense>
</>
@@ -89,6 +182,8 @@ function AuthHomePage(props) {
from_following,
rising,
trending,
community_favorites,
hall_of_fame,
fresh,
collections_featured,
collections_recent,
@@ -107,41 +202,57 @@ function AuthHomePage(props) {
return (
<>
{/* P0. Welcome/status row — below hero so featured image sits at 0px */}
<Suspense fallback={null}>
<Suspense fallback={<SectionFallback variant="welcome" />}>
<HomeWelcomeRow user_data={user_data} />
</Suspense>
{/* P2. From Creators You Follow */}
<Suspense fallback={<SectionFallback />}>
<Suspense fallback={<SectionFallback variant="gallery" />}>
<HomeFromFollowing items={from_following} />
</Suspense>
{/* P3. Personalized For You preview */}
<Suspense fallback={<SectionFallback />}>
<Suspense fallback={<SectionFallback variant="gallery" />}>
<HomeTrendingForYou items={for_you} preferences={preferences} />
</Suspense>
{/* Rising Now */}
<Suspense fallback={<SectionFallback />}>
<Suspense fallback={<SectionFallback variant="gallery" />}>
<HomeRising items={rising} />
</Suspense>
{/* 2. Global Trending Now */}
<Suspense fallback={<SectionFallback />}>
<Suspense fallback={<SectionFallback variant="gallery" />}>
<HomeTrending items={trending} />
</Suspense>
<Suspense fallback={<SectionFallback variant="gallery" />}>
<HomeMedalHighlights
title="Community Favorites"
href="/explore/top-rated"
description="Recent medal momentum from the community. This rail highlights the strongest 30-day medal signal."
items={community_favorites}
/>
</Suspense>
<Suspense fallback={<SectionFallback variant="gallery" />}>
<HomeMedalHighlights
title="Hall of Fame"
href="/explore/best"
description="All-time medal standouts that keep being remembered long after publication."
items={hall_of_fame}
/>
</Suspense>
{/* P4. Because You Like {top tag} — uses by_categories for variety */}
<Suspense fallback={<SectionFallback />}>
<Suspense fallback={<SectionFallback variant="gallery" />}>
<HomeBecauseYouLike items={by_categories} preferences={preferences} />
</Suspense>
{/* 3. Fresh Uploads */}
<Suspense fallback={<SectionFallback />}>
<Suspense fallback={<SectionFallback variant="gallery" />}>
<HomeFresh items={fresh} />
</Suspense>
<Suspense fallback={<SectionFallback />}>
<Suspense fallback={<SectionFallback variant="collections" />}>
<HomeCollections
featured={collections_featured}
recent={collections_recent}
@@ -152,37 +263,37 @@ function AuthHomePage(props) {
/>
</Suspense>
<Suspense fallback={<SectionFallback />}>
<Suspense fallback={<SectionFallback variant="groups" />}>
<HomeGroups groups={groups} />
</Suspense>
{/* 4. Explore Categories */}
<Suspense fallback={<SectionFallback />}>
<Suspense fallback={<SectionFallback variant="categories" />}>
<HomeCategories />
</Suspense>
{/* P5. Suggested Creators */}
<Suspense fallback={<SectionFallback />}>
<Suspense fallback={<SectionFallback variant="creators" />}>
<HomeSuggestedCreators creators={suggested_creators} />
</Suspense>
{/* 5. Popular Tags */}
<Suspense fallback={<SectionFallback />}>
<Suspense fallback={<SectionFallback variant="tags" />}>
<HomeTags tags={tags} />
</Suspense>
{/* 6. Top Creators */}
<Suspense fallback={<SectionFallback />}>
<Suspense fallback={<SectionFallback variant="creators" />}>
<HomeCreators creators={creators} />
</Suspense>
{/* 7. News */}
<Suspense fallback={<SectionFallback />}>
<Suspense fallback={<SectionFallback variant="news" />}>
<HomeNews items={news} />
</Suspense>
{/* 8. CTA Upload */}
<Suspense fallback={<SectionFallback />}>
<Suspense fallback={<SectionFallback variant="cta" />}>
<HomeCTA isLoggedIn />
</Suspense>
</>

View File

@@ -76,7 +76,7 @@ export default function HomeRising({ items }) {
</div>
<div className="flex snap-x snap-mandatory gap-4 overflow-x-auto pb-3 lg:grid lg:grid-cols-5 lg:overflow-visible">
{items.slice(0, Math.floor(items.length / 5) * 5 || items.length).map((item) => (
{items.map((item) => (
<ArtCard key={item.id} item={item} />
))}
</div>

View File

@@ -16,7 +16,7 @@ export default function HomeTrending({ items }) {
</div>
<ArtworkGalleryGrid
items={items.slice(0, 8)}
items={items}
className="xl:grid-cols-4"
/>
</section>

View File

@@ -23,7 +23,7 @@ export default function HomeTrendingForYou({ items, preferences }) {
Open full feed
</a>
</div>
<ArtworkGalleryGrid items={items.slice(0, 8)} compact />
<ArtworkGalleryGrid items={items} compact />
</section>
)
}

View File

@@ -47,7 +47,7 @@ export default function HomeWelcomeRow({ user_data }) {
{notifications_unread > 0 && (
<a
href="/notifications"
href="/dashboard/notifications"
className="inline-flex items-center gap-1.5 rounded-lg bg-nova-800 px-3 py-1.5 text-xs font-medium text-white ring-1 ring-white/10 hover:bg-nova-700 transition"
>
<svg className="h-3.5 w-3.5 text-yellow-400 shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
@@ -59,7 +59,7 @@ export default function HomeWelcomeRow({ user_data }) {
<a
href="/upload"
className="inline-flex items-center gap-1.5 rounded-lg bg-accent px-3 py-1.5 text-xs font-semibold text-white shadow hover:brightness-110 transition"
className="btn-accent-solid inline-flex items-center gap-1.5 rounded-lg px-3 py-1.5 text-xs font-semibold"
>
<svg className="h-3.5 w-3.5 shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2.5}>
<path strokeLinecap="round" strokeLinejoin="round" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-8l-4-4m0 0L8 8m4-4v12" />