Build world campaigns rewards and recaps
This commit is contained in:
@@ -1,12 +1,26 @@
|
||||
import React from 'react'
|
||||
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 }) {
|
||||
function SupportingRail({ title, description, items, sourceDetail = 'navigation_rail' }) {
|
||||
if (!Array.isArray(items) || items.length === 0) {
|
||||
return null
|
||||
}
|
||||
@@ -20,7 +34,110 @@ function SupportingRail({ title, description, items }) {
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid gap-4 xl:grid-cols-3">
|
||||
{items.map((item) => <WorldCard key={item.id} world={item} compact />)}
|
||||
{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 edition’s rewards are edition-aware, so recognition here remains part of each creator’s 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>
|
||||
)
|
||||
@@ -29,12 +146,87 @@ function SupportingRail({ title, description, items }) {
|
||||
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 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">
|
||||
<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 Nova`} description={props.seo?.description || world?.summary} image={props.seo?.image} />
|
||||
<div className="mx-auto max-w-7xl">
|
||||
{previewMode ? (
|
||||
@@ -49,26 +241,65 @@ export default function WorldShow() {
|
||||
</section>
|
||||
) : null}
|
||||
|
||||
<WorldHero world={world} previewMode={previewMode} />
|
||||
<WorldArchiveNotice notice={archiveNotice} />
|
||||
|
||||
{sections.length > 0 ? sections.map((section) => <WorldSection key={section.key} section={section} />) : (
|
||||
<section className="mt-10 rounded-[28px] border border-dashed border-white/10 bg-white/[0.02] p-6 text-sm text-slate-400">
|
||||
This world has been themed and published, but no curated sections have been attached yet.
|
||||
{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}
|
||||
|
||||
<WorldCommunitySubmissionsSection section={communitySubmissions} />
|
||||
{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="Archive Editions"
|
||||
description="Past iterations remain accessible so recurring worlds can build continuity over time."
|
||||
title={archiveTitle}
|
||||
description={archiveDescription}
|
||||
items={props.archiveEditions}
|
||||
sourceDetail="archive_editions"
|
||||
/>
|
||||
|
||||
<SupportingRail
|
||||
title="Related Worlds"
|
||||
description="Other worlds sharing the same recurrence, theme, or editorial lineage."
|
||||
description="Other worlds with adjacent themes, related editorial mood, or connected programming context."
|
||||
items={props.relatedWorlds}
|
||||
sourceDetail="related_worlds"
|
||||
/>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
Reference in New Issue
Block a user