Files
SkinbaseNova/resources/js/Pages/World/WorldShow.jsx

307 lines
16 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import React, { useEffect, useRef } from 'react'
import { usePage } from '@inertiajs/react'
import SeoHead from '../../components/seo/SeoHead'
import WorldArchiveNotice from '../../components/worlds/WorldArchiveNotice'
import WorldChallengeEntriesRail from '../../components/worlds/WorldChallengeEntriesRail'
import WorldChallengeFinalistsGrid from '../../components/worlds/WorldChallengeFinalistsGrid'
import WorldChallengePanel from '../../components/worlds/WorldChallengePanel'
import WorldChallengeWinnersPanel from '../../components/worlds/WorldChallengeWinnersPanel'
import WorldHero from '../../components/worlds/WorldHero'
import WorldCommunitySubmissionsSection from '../../components/worlds/WorldCommunitySubmissionsSection'
import WorldRecapArticleCard from '../../components/worlds/WorldRecapArticleCard'
import WorldRecapCommunityHighlights from '../../components/worlds/WorldRecapCommunityHighlights'
import WorldRecapCreatorsPanel from '../../components/worlds/WorldRecapCreatorsPanel'
import WorldRecapFeaturedArtworks from '../../components/worlds/WorldRecapFeaturedArtworks'
import WorldRecapHero from '../../components/worlds/WorldRecapHero'
import WorldRecapStatsGrid from '../../components/worlds/WorldRecapStatsGrid'
import WorldRecapSummaryCard from '../../components/worlds/WorldRecapSummaryCard'
import WorldSection from '../../components/worlds/WorldSection'
import WorldCard from '../../components/worlds/WorldCard'
import WorldFamilyCard from '../../components/worlds/WorldFamilyCard'
import { resolveWorldLandingSource, trackWorldAnalytics, trackWorldSourceClick, withWorldSource } from '../../lib/worldAnalytics'
function SupportingRail({ title, description, items, sourceDetail = 'navigation_rail' }) {
if (!Array.isArray(items) || items.length === 0) {
return null
}
return (
<section className="mt-10">
<div className="mb-5 flex items-end justify-between gap-4">
<div>
<h2 className="text-2xl font-semibold tracking-[-0.03em] text-white">{title}</h2>
{description ? <p className="mt-2 max-w-3xl text-sm leading-6 text-slate-400">{description}</p> : null}
</div>
</div>
<div className="grid gap-4 xl:grid-cols-3">
{items.map((item) => <WorldCard key={item.id} world={item} compact sourceSurface="navigation" sourceDetail={sourceDetail} />)}
</div>
</section>
)
}
function EditionNavigation({ previousEdition, nextEdition }) {
if (!previousEdition && !nextEdition) {
return null
}
return (
<section className="mt-10 rounded-[28px] border border-white/10 bg-white/[0.03] p-5">
<div className="mb-5">
<h2 className="text-2xl font-semibold tracking-[-0.03em] text-white">Edition Navigation</h2>
<p className="mt-2 max-w-3xl text-sm leading-6 text-slate-400">Move through adjacent editions in this recurring world family without losing the archive context.</p>
</div>
<div className="grid gap-4 md:grid-cols-2">
<a href={previousEdition ? withWorldSource(previousEdition.public_url, 'navigation', 'previous_edition') : '#'} onClick={() => previousEdition && trackWorldSourceClick({ worldId: previousEdition.id, worldTitle: previousEdition.title, sourceSurface: 'navigation', sourceDetail: 'previous_edition' })} className={`rounded-[24px] border border-white/10 bg-black/20 p-4 ${previousEdition ? 'text-white hover:bg-white/[0.06]' : 'pointer-events-none text-slate-500'}`}>
<div className="text-[11px] font-semibold uppercase tracking-[0.16em] text-slate-500">Previous edition</div>
<div className="mt-2 text-lg font-semibold">{previousEdition?.title || 'No earlier edition'}</div>
{previousEdition?.edition_year ? <div className="mt-1 text-sm text-slate-300">{previousEdition.edition_year}</div> : null}
</a>
<a href={nextEdition ? withWorldSource(nextEdition.public_url, 'navigation', 'next_edition') : '#'} onClick={() => nextEdition && trackWorldSourceClick({ worldId: nextEdition.id, worldTitle: nextEdition.title, sourceSurface: 'navigation', sourceDetail: 'next_edition' })} className={`rounded-[24px] border border-white/10 bg-black/20 p-4 ${nextEdition ? 'text-white hover:bg-white/[0.06]' : 'pointer-events-none text-slate-500'}`}>
<div className="text-[11px] font-semibold uppercase tracking-[0.16em] text-slate-500">Next edition</div>
<div className="mt-2 text-lg font-semibold">{nextEdition?.title || 'No newer archive edition'}</div>
{nextEdition?.edition_year ? <div className="mt-1 text-sm text-slate-300">{nextEdition.edition_year}</div> : null}
</a>
</div>
</section>
)
}
function RewardedContributors({ section, world }) {
const items = Array.isArray(section?.items) ? section.items : []
const counts = section?.counts || {}
const summaryChips = [
section?.creator_count ? `${section.creator_count} rewarded creators` : null,
world?.live_submission_count ? `${world.live_submission_count} live submissions` : null,
world?.featured_submission_count ? `${world.featured_submission_count} featured artworks` : null,
].filter(Boolean)
const rewardTypeChips = [
counts.winner ? `${counts.winner} winner${counts.winner === 1 ? '' : 's'}` : null,
counts.finalist ? `${counts.finalist} finalist${counts.finalist === 1 ? '' : 's'}` : null,
counts.spotlight ? `${counts.spotlight} spotlight` : null,
counts.featured ? `${counts.featured} featured` : null,
counts.participant ? `${counts.participant} participant` : null,
].filter(Boolean)
if (items.length === 0) {
return null
}
return (
<section className="mt-10 rounded-[28px] border border-white/10 bg-white/[0.03] p-5">
<div className="mb-5">
<h2 className="text-2xl font-semibold tracking-[-0.03em] text-white">Rewarded Contributors</h2>
<p className="mt-2 max-w-3xl text-sm leading-6 text-slate-400">Creators who earned visible recognition in this edition. Live participation builds history here, while featured and editorial selections raise the level of recognition.</p>
</div>
{summaryChips.length > 0 || rewardTypeChips.length > 0 || world?.cta_url ? (
<div className="mb-5 rounded-[24px] border border-white/10 bg-black/20 p-4">
{summaryChips.length > 0 ? (
<div className="flex flex-wrap gap-2">
{summaryChips.map((item) => (
<span key={item} className="rounded-full border border-white/10 bg-white/[0.04] px-3 py-1 text-[11px] font-semibold uppercase tracking-[0.14em] text-slate-200">{item}</span>
))}
</div>
) : null}
{rewardTypeChips.length > 0 ? (
<div className="mt-3 flex flex-wrap gap-2">
{rewardTypeChips.map((item) => (
<span key={item} className="rounded-full border border-sky-300/20 bg-sky-400/10 px-3 py-1 text-[11px] font-semibold uppercase tracking-[0.14em] text-sky-100">{item}</span>
))}
</div>
) : null}
{world?.cta_url ? (
<div className="mt-4">
<a href={world.cta_url} data-world-event="world_cta_clicked" data-world-section-key="rewards" data-world-cta-key="rewards_join_world" className="inline-flex items-center gap-2 rounded-full border border-white/12 bg-white/[0.06] px-4 py-2 text-xs font-semibold uppercase tracking-[0.16em] text-white transition hover:bg-white/[0.1]">
{world.cta_label || 'Join this world'}
<i className="fa-solid fa-arrow-right" />
</a>
</div>
) : null}
</div>
) : null}
<div className="mb-5">
<p className="max-w-3xl text-sm leading-6 text-slate-400">This editions rewards are edition-aware, so recognition here remains part of each creators recurring world history.</p>
</div>
<div className="grid gap-4 md:grid-cols-2 xl:grid-cols-3">
{items.map((item) => (
<a key={item.id} href={item.creator?.profile_url || item.world?.url || '#'} data-world-event="world_entity_clicked" data-world-section-key="rewards" data-world-entity-type="creator" data-world-entity-id={item.creator?.id || 0} data-world-entity-title={item.creator?.name || item.creator?.username || 'Creator'} className="rounded-[24px] border border-white/10 bg-black/20 p-4 transition hover:border-white/15 hover:bg-white/[0.06]">
<div className="flex items-center gap-3">
{item.creator?.avatar_url ? <img src={item.creator.avatar_url} alt={item.creator.username || item.creator.name} className="h-12 w-12 rounded-2xl object-cover ring-1 ring-white/10" /> : <div className="flex h-12 w-12 items-center justify-center rounded-2xl border border-white/10 bg-white/[0.04] text-slate-500"><i className="fa-solid fa-user" /></div>}
<div className="min-w-0 flex-1">
<div className="truncate text-sm font-semibold text-white">{item.creator?.name || item.creator?.username || 'Creator'}</div>
<div className="truncate text-xs uppercase tracking-[0.16em] text-slate-500">{item.badge_label}</div>
</div>
</div>
{item.artwork?.title ? <div className="mt-3 text-sm text-slate-300">{item.artwork.title}</div> : null}
</a>
))}
</div>
</section>
)
}
export default function WorldShow() {
const { props } = usePage()
const world = props.world
const recap = props.recap || null
const sections = Array.isArray(props.sections) ? props.sections : []
const linkedChallenge = props.linkedChallenge || null
const linkedChallengeEntries = props.linkedChallengeEntries || null
const linkedChallengeWinners = props.linkedChallengeWinners || null
const linkedChallengeFinalists = props.linkedChallengeFinalists || null
const communitySubmissions = props.communitySubmissions || null
const rewardedContributors = props.rewardedContributors || null
const previewMode = Boolean(props.previewMode)
const archiveNotice = props.archiveNotice || null
const familySummary = props.familySummary || null
const currentEdition = props.currentEdition || null
const previousEdition = props.previousEdition || null
const nextEdition = props.nextEdition || null
const archiveTitle = currentEdition ? 'Previous Editions' : 'Archive Editions'
const archiveDescription = currentEdition
? 'Earlier editions remain public so the recurring family keeps its full history accessible.'
: 'Past iterations remain accessible so recurring worlds can build continuity over time.'
const rootRef = useRef(null)
useEffect(() => {
if (previewMode || !world?.id) {
return
}
const landing = resolveWorldLandingSource()
trackWorldAnalytics('world_viewed', {
world_id: world.id,
source_surface: landing.sourceSurface,
source_detail: landing.sourceDetail,
})
}, [previewMode, world?.id])
useEffect(() => {
if (previewMode || !world?.id || !rootRef.current) {
return
}
const landing = resolveWorldLandingSource()
const container = rootRef.current
const clickHandler = (event) => {
const target = event.target.closest('[data-world-event], [data-world-section-key]')
if (!target || !container.contains(target)) {
return
}
const sectionKey = target.dataset.worldSectionKey || ''
const challengeId = Number(target.dataset.worldChallengeId || 0)
const entityId = Number(target.dataset.worldEntityId || 0)
const payload = {
world_id: world.id,
source_surface: landing.sourceSurface,
source_detail: landing.sourceDetail,
...(sectionKey ? { section_key: sectionKey } : {}),
...(target.dataset.worldCtaKey ? { cta_key: target.dataset.worldCtaKey } : {}),
...(target.dataset.worldEntityType ? { entity_type: target.dataset.worldEntityType } : {}),
...(entityId > 0 ? { entity_id: entityId } : {}),
...(target.dataset.worldEntityTitle ? { entity_title: target.dataset.worldEntityTitle } : {}),
...(challengeId > 0 ? { challenge_id: challengeId } : {}),
}
if (sectionKey) {
trackWorldAnalytics('world_section_clicked', payload)
}
if (target.dataset.worldEvent) {
trackWorldAnalytics(target.dataset.worldEvent, payload)
}
}
container.addEventListener('click', clickHandler)
return () => {
container.removeEventListener('click', clickHandler)
}
}, [previewMode, world?.id])
return (
<main ref={rootRef} className="min-h-screen bg-[radial-gradient(circle_at_top,_rgba(56,189,248,0.12),_transparent_28%),linear-gradient(180deg,_#020617_0%,_#02040a_100%)] px-4 py-10 sm:px-6 lg:px-8">
<SeoHead title={props.seo?.title || `${world?.title || 'World'} - Skinbase`} description={props.seo?.description || world?.summary} image={props.seo?.image} />
<div className="mx-auto max-w-7xl">
{previewMode ? (
<section className="mb-6 rounded-[28px] border border-amber-300/20 bg-amber-400/10 px-5 py-4 text-sm text-amber-50">
<div className="flex flex-wrap items-center justify-between gap-3">
<div>
<div className="text-[11px] font-semibold uppercase tracking-[0.18em] text-amber-100/75">Studio preview</div>
<div className="mt-1 font-semibold text-white">You are viewing the editorial preview version of this world before or alongside public release.</div>
</div>
{world?.public_url ? <a href={world.public_url} className="inline-flex items-center gap-2 rounded-full border border-white/15 bg-white/10 px-4 py-2 text-xs font-semibold uppercase tracking-[0.16em] text-white">Open canonical page <i className="fa-solid fa-up-right-from-square" /></a> : null}
</div>
</section>
) : null}
<WorldArchiveNotice notice={archiveNotice} />
{recap ? <WorldRecapHero world={world} recap={recap} previewMode={previewMode} /> : <WorldHero world={world} previewMode={previewMode} />}
{recap ? (
<>
<WorldRecapSummaryCard recap={recap} />
<WorldRecapStatsGrid stats={recap.stats} />
<WorldRecapArticleCard article={recap.article} />
<WorldRecapFeaturedArtworks section={recap.featured_artworks} />
<WorldChallengePanel section={linkedChallenge} />
</>
) : <WorldChallengePanel section={linkedChallenge} />}
{familySummary ? (
<section className="mt-10">
<div className="mb-5">
<h2 className="text-2xl font-semibold tracking-[-0.03em] text-white">Recurring Family</h2>
<p className="mt-2 max-w-3xl text-sm leading-6 text-slate-400">Each edition stays public, but the family route always resolves to the canonical current or latest edition.</p>
</div>
<WorldFamilyCard family={familySummary} sourceSurface="navigation" sourceDetail="family_summary" />
</section>
) : null}
{sections.length > 0 ? sections.map((section) => <WorldSection key={section.key} section={section} />) : null}
<WorldChallengeEntriesRail section={linkedChallengeEntries} challengeId={linkedChallenge?.id || null} />
<WorldChallengeWinnersPanel section={linkedChallengeWinners} challengeId={linkedChallenge?.id || null} />
<WorldChallengeFinalistsGrid panel={linkedChallenge} section={linkedChallengeFinalists} />
{recap ? <WorldRecapCommunityHighlights section={recap.community_highlights} /> : <RewardedContributors section={rewardedContributors} world={world} />}
{recap ? <WorldRecapCreatorsPanel section={recap.creators} /> : <WorldCommunitySubmissionsSection section={communitySubmissions} />}
{currentEdition ? (
<SupportingRail
title="Current Edition"
description="This recurring family has a newer public edition. Use the family route to follow the current canonical page."
items={[currentEdition]}
sourceDetail="current_edition"
/>
) : null}
<EditionNavigation previousEdition={previousEdition} nextEdition={nextEdition} />
<SupportingRail
title={archiveTitle}
description={archiveDescription}
items={props.archiveEditions}
sourceDetail="archive_editions"
/>
<SupportingRail
title="Related Worlds"
description="Other worlds with adjacent themes, related editorial mood, or connected programming context."
items={props.relatedWorlds}
sourceDetail="related_worlds"
/>
</div>
</main>
)
}