Files
SkinbaseNova/resources/js/Pages/Home/HomePage.jsx

413 lines
14 KiB
JavaScript

import React, { lazy, Suspense, useEffect, useRef, useState } from 'react'
import { createRoot } from 'react-dom/client'
import HomepageAnnouncement from '../../components/homepage/HomepageAnnouncement'
// Below-fold — lazy-loaded to keep initial bundle small
const HomeWelcomeRow = lazy(() => import('./HomeWelcomeRow'))
const HomeFromFollowing = lazy(() => import('./HomeFromFollowing'))
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'))
const HomeWorldSpotlight = lazy(() => import('./HomeWorldSpotlight'))
const HomeGroups = lazy(() => import('./HomeGroups'))
const HomeCategories = lazy(() => import('./HomeCategories'))
const HomeTags = lazy(() => import('./HomeTags'))
const HomeCreators = lazy(() => import('./HomeCreators'))
const HomeNews = lazy(() => import('./HomeNews'))
const HomeCTA = lazy(() => import('./HomeCTA'))
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 (
<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 SectionPlaceholder({ variant = 'gallery' }) {
const heightClassName = variant === 'welcome'
? 'h-20'
: variant === 'tags'
? 'h-28'
: variant === 'cta'
? 'h-40'
: variant === 'news'
? 'h-48'
: variant === 'categories'
? 'h-44'
: variant === 'creators'
? 'h-72'
: variant === 'collections'
? 'h-80'
: variant === 'groups'
? 'h-80'
: 'h-[28rem]'
return (
<section className="mt-14 px-4 sm:px-6 lg:px-8" aria-hidden="true">
<div className={`rounded-[28px] border border-white/8 bg-nova-900/40 ${heightClassName}`} />
</section>
)
}
function DeferredSection({ children, fallback, variant = 'gallery', eager = false, rootMargin = '1200px 0px' }) {
const anchorRef = useRef(null)
const [isVisible, setIsVisible] = useState(eager)
useEffect(() => {
if (eager || isVisible) {
return undefined
}
const node = anchorRef.current
if (!node) {
return undefined
}
if (typeof window === 'undefined' || typeof window.IntersectionObserver !== 'function') {
setIsVisible(true)
return undefined
}
const observer = new window.IntersectionObserver((entries) => {
entries.forEach((entry) => {
if (!entry.isIntersecting) {
return
}
setIsVisible(true)
observer.disconnect()
})
}, { rootMargin, threshold: 0.01 })
observer.observe(node)
return () => observer.disconnect()
}, [eager, isVisible, rootMargin])
return (
<div ref={anchorRef}>
{isVisible
? <Suspense fallback={fallback}><>{children}</></Suspense>
: <SectionPlaceholder variant={variant} />}
</div>
)
}
function GuestHomePage(props) {
const { rising, trending, community_favorites, hall_of_fame, fresh, tags, creators, news, collections_featured, collections_trending, collections_editorial, collections_community, groups, world_spotlight } = props
return (
<>
<DeferredSection eager fallback={<SectionFallback variant="gallery" />}>
<HomeRising items={rising} />
</DeferredSection>
<DeferredSection eager fallback={<SectionFallback variant="gallery" />}>
<HomeTrending items={trending} />
</DeferredSection>
<DeferredSection fallback={<SectionFallback variant="gallery" />}>
<HomeMedalHighlights
title="Community Favorites"
href="/explore?sort=top-rated"
description="Recent medal momentum from the community. This rail highlights the strongest 30-day medal signal."
items={community_favorites}
/>
</DeferredSection>
<DeferredSection 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}
/>
</DeferredSection>
{/* 3. Fresh Uploads */}
<DeferredSection fallback={<SectionFallback variant="gallery" />}>
<HomeFresh items={fresh} />
</DeferredSection>
<DeferredSection variant="collections" fallback={<SectionFallback variant="collections" />}>
<HomeCollections
featured={collections_featured}
trending={collections_trending}
editorial={collections_editorial}
community={collections_community}
/>
</DeferredSection>
<DeferredSection variant="collections" fallback={<SectionFallback variant="collections" />}>
<HomeWorldSpotlight world={world_spotlight} />
</DeferredSection>
<DeferredSection variant="groups" fallback={<SectionFallback variant="groups" />}>
<HomeGroups groups={groups} />
</DeferredSection>
{/* 4. Explore Categories */}
<DeferredSection variant="categories" fallback={<SectionFallback variant="categories" />}>
<HomeCategories />
</DeferredSection>
{/* 5. Popular Tags */}
<DeferredSection variant="tags" fallback={<SectionFallback variant="tags" />}>
<HomeTags tags={tags} />
</DeferredSection>
{/* 6. Top Creators */}
<DeferredSection variant="creators" fallback={<SectionFallback variant="creators" />}>
<HomeCreators creators={creators} />
</DeferredSection>
{/* 7. News */}
<DeferredSection variant="news" fallback={<SectionFallback variant="news" />}>
<HomeNews items={news} />
</DeferredSection>
{/* 8. CTA Upload */}
<DeferredSection variant="cta" fallback={<SectionFallback variant="cta" />}>
<HomeCTA isLoggedIn={false} />
</DeferredSection>
</>
)
}
function AuthHomePage(props) {
const {
user_data,
for_you,
from_following,
rising,
trending,
community_favorites,
hall_of_fame,
fresh,
collections_featured,
collections_recent,
collections_trending,
collections_editorial,
collections_community,
world_spotlight,
groups,
by_categories,
suggested_creators,
tags,
creators,
news,
preferences,
} = props
return (
<>
{/* P0. Welcome/status row — below hero so featured image sits at 0px */}
<DeferredSection eager variant="welcome" fallback={<SectionFallback variant="welcome" />}>
<HomeWelcomeRow user_data={user_data} />
</DeferredSection>
{/* P2. From Creators You Follow */}
<DeferredSection eager fallback={<SectionFallback variant="gallery" />}>
<HomeFromFollowing items={from_following} />
</DeferredSection>
{/* P3. Personalized For You preview */}
<DeferredSection eager fallback={<SectionFallback variant="gallery" />}>
<HomeTrendingForYou items={for_you} preferences={preferences} />
</DeferredSection>
{/* Rising Now */}
<DeferredSection fallback={<SectionFallback variant="gallery" />}>
<HomeRising items={rising} />
</DeferredSection>
{/* 2. Global Trending Now */}
<DeferredSection fallback={<SectionFallback variant="gallery" />}>
<HomeTrending items={trending} />
</DeferredSection>
<DeferredSection fallback={<SectionFallback variant="gallery" />}>
<HomeMedalHighlights
title="Community Favorites"
href="/explore?sort=top-rated"
description="Recent medal momentum from the community. This rail highlights the strongest 30-day medal signal."
items={community_favorites}
/>
</DeferredSection>
<DeferredSection 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}
/>
</DeferredSection>
{/* P4. Because You Like {top tag} — uses by_categories for variety */}
<DeferredSection fallback={<SectionFallback variant="gallery" />}>
<HomeBecauseYouLike items={by_categories} preferences={preferences} />
</DeferredSection>
{/* 3. Fresh Uploads */}
<DeferredSection fallback={<SectionFallback variant="gallery" />}>
<HomeFresh items={fresh} />
</DeferredSection>
<DeferredSection variant="collections" fallback={<SectionFallback variant="collections" />}>
<HomeCollections
featured={collections_featured}
recent={collections_recent}
trending={collections_trending}
editorial={collections_editorial}
community={collections_community}
isLoggedIn
/>
</DeferredSection>
<DeferredSection variant="collections" fallback={<SectionFallback variant="collections" />}>
<HomeWorldSpotlight world={world_spotlight} />
</DeferredSection>
<DeferredSection variant="groups" fallback={<SectionFallback variant="groups" />}>
<HomeGroups groups={groups} />
</DeferredSection>
{/* 4. Explore Categories */}
<DeferredSection variant="categories" fallback={<SectionFallback variant="categories" />}>
<HomeCategories />
</DeferredSection>
{/* P5. Suggested Creators */}
<DeferredSection variant="creators" fallback={<SectionFallback variant="creators" />}>
<HomeSuggestedCreators creators={suggested_creators} />
</DeferredSection>
{/* 5. Popular Tags */}
<DeferredSection variant="tags" fallback={<SectionFallback variant="tags" />}>
<HomeTags tags={tags} />
</DeferredSection>
{/* 6. Top Creators */}
<DeferredSection variant="creators" fallback={<SectionFallback variant="creators" />}>
<HomeCreators creators={creators} />
</DeferredSection>
{/* 7. News */}
<DeferredSection variant="news" fallback={<SectionFallback variant="news" />}>
<HomeNews items={news} />
</DeferredSection>
{/* 8. CTA Upload */}
<DeferredSection variant="cta" fallback={<SectionFallback variant="cta" />}>
<HomeCTA isLoggedIn />
</DeferredSection>
</>
)
}
function HomePage(props) {
return (
<div className="pb-24">
<HomepageAnnouncement announcement={props.announcement || null} />
{props.is_logged_in
? <AuthHomePage {...props} />
: <GuestHomePage {...props} />
}
</div>
)
}
// Auto-mount when the Blade view provides #homepage-root
if (typeof document !== 'undefined') {
const mountEl = document.getElementById('homepage-root')
if (mountEl) {
let props = {}
try {
const propsEl = document.getElementById('homepage-props')
props = propsEl ? JSON.parse(propsEl.textContent || '{}') : {}
} catch {
props = {}
}
createRoot(mountEl).render(<HomePage {...props} />)
}
}
export default HomePage