163 lines
6.3 KiB
JavaScript
163 lines
6.3 KiB
JavaScript
import React, { useState, useCallback } from 'react'
|
|
import { createRoot } from 'react-dom/client'
|
|
import ArtworkHero from '../components/artwork/ArtworkHero'
|
|
import ArtworkMeta from '../components/artwork/ArtworkMeta'
|
|
import ArtworkActions from '../components/artwork/ArtworkActions'
|
|
import ArtworkAwards from '../components/artwork/ArtworkAwards'
|
|
import ArtworkStats from '../components/artwork/ArtworkStats'
|
|
import ArtworkTags from '../components/artwork/ArtworkTags'
|
|
import ArtworkAuthor from '../components/artwork/ArtworkAuthor'
|
|
import ArtworkRelated from '../components/artwork/ArtworkRelated'
|
|
import ArtworkDescription from '../components/artwork/ArtworkDescription'
|
|
import ArtworkComments from '../components/artwork/ArtworkComments'
|
|
import ArtworkReactions from '../components/artwork/ArtworkReactions'
|
|
import ArtworkNavigator from '../components/viewer/ArtworkNavigator'
|
|
import ArtworkViewer from '../components/viewer/ArtworkViewer'
|
|
|
|
function ArtworkPage({ artwork: initialArtwork, related: initialRelated, presentMd: initialMd, presentLg: initialLg, presentXl: initialXl, presentSq: initialSq, canonicalUrl: initialCanonical, isAuthenticated = false, comments: initialComments = [] }) {
|
|
const [viewerOpen, setViewerOpen] = useState(false)
|
|
const openViewer = useCallback(() => setViewerOpen(true), [])
|
|
const closeViewer = useCallback(() => setViewerOpen(false), [])
|
|
|
|
// Navigable state — updated on client-side navigation
|
|
const [artwork, setArtwork] = useState(initialArtwork)
|
|
const [liveStats, setLiveStats] = useState(initialArtwork?.stats || {})
|
|
|
|
const handleStatsChange = useCallback((delta) => {
|
|
setLiveStats(prev => {
|
|
const next = { ...prev }
|
|
Object.entries(delta).forEach(([key, val]) => {
|
|
next[key] = Math.max(0, (Number(next[key]) || 0) + val)
|
|
})
|
|
return next
|
|
})
|
|
}, [])
|
|
const [presentMd, setPresentMd] = useState(initialMd)
|
|
const [presentLg, setPresentLg] = useState(initialLg)
|
|
const [presentXl, setPresentXl] = useState(initialXl)
|
|
const [presentSq, setPresentSq] = useState(initialSq)
|
|
const [related, setRelated] = useState(initialRelated)
|
|
const [comments, setComments] = useState(initialComments)
|
|
const [canonicalUrl, setCanonicalUrl] = useState(initialCanonical)
|
|
|
|
// Nav arrow state — populated by ArtworkNavigator once neighbors resolve
|
|
const [navState, setNavState] = useState({ hasPrev: false, hasNext: false, navigatePrev: null, navigateNext: null })
|
|
|
|
/**
|
|
* Called by ArtworkNavigator after a successful no-reload navigation.
|
|
* data = ArtworkResource JSON from /api/artworks/{id}/page
|
|
*/
|
|
const handleNavigate = useCallback((data) => {
|
|
setArtwork(data)
|
|
setLiveStats(data.stats || {})
|
|
setPresentMd(data.thumbs?.md ?? null)
|
|
setPresentLg(data.thumbs?.lg ?? null)
|
|
setPresentXl(data.thumbs?.xl ?? null)
|
|
setPresentSq(data.thumbs?.sq ?? null)
|
|
setRelated([]) // cleared on navigation; user can scroll down for related
|
|
setComments([]) // cleared; per-page server data
|
|
setCanonicalUrl(data.canonical_url ?? window.location.href)
|
|
setViewerOpen(false) // close viewer when navigating away
|
|
}, [])
|
|
|
|
if (!artwork) return null
|
|
|
|
const initialAwards = artwork?.awards ?? null
|
|
|
|
return (
|
|
<>
|
|
<main className="mx-auto w-full max-w-screen-xl px-4 pb-24 pt-10 sm:px-6 lg:px-8 lg:pb-12">
|
|
<ArtworkHero
|
|
artwork={artwork}
|
|
presentMd={presentMd}
|
|
presentLg={presentLg}
|
|
presentXl={presentXl}
|
|
onOpenViewer={openViewer}
|
|
hasPrev={navState.hasPrev}
|
|
hasNext={navState.hasNext}
|
|
onPrev={navState.navigatePrev}
|
|
onNext={navState.navigateNext}
|
|
/>
|
|
|
|
<div className="mt-6 space-y-4 lg:hidden">
|
|
<ArtworkAuthor artwork={artwork} presentSq={presentSq} />
|
|
<ArtworkActions artwork={artwork} canonicalUrl={canonicalUrl} mobilePriority onStatsChange={handleStatsChange} />
|
|
<ArtworkAwards artwork={artwork} initialAwards={initialAwards} isAuthenticated={isAuthenticated} />
|
|
</div>
|
|
|
|
<div className="mt-8 grid grid-cols-1 gap-8 lg:grid-cols-3">
|
|
<div className="space-y-6 lg:col-span-2">
|
|
<ArtworkMeta artwork={artwork} />
|
|
<ArtworkStats artwork={artwork} stats={liveStats} />
|
|
<ArtworkTags artwork={artwork} />
|
|
<ArtworkDescription artwork={artwork} />
|
|
<ArtworkReactions artworkId={artwork.id} isLoggedIn={isAuthenticated} />
|
|
<ArtworkComments
|
|
artworkId={artwork.id}
|
|
comments={comments}
|
|
isLoggedIn={isAuthenticated}
|
|
loginUrl="/login"
|
|
/>
|
|
</div>
|
|
|
|
<aside className="hidden space-y-6 lg:block">
|
|
<div className="sticky top-24 space-y-4">
|
|
<ArtworkAuthor artwork={artwork} presentSq={presentSq} />
|
|
<ArtworkActions artwork={artwork} canonicalUrl={canonicalUrl} onStatsChange={handleStatsChange} />
|
|
<ArtworkAwards artwork={artwork} initialAwards={initialAwards} isAuthenticated={isAuthenticated} />
|
|
</div>
|
|
</aside>
|
|
</div>
|
|
|
|
<ArtworkRelated related={related} />
|
|
</main>
|
|
|
|
{/* Artwork navigator — prev/next arrows, keyboard, swipe, no page reload */}
|
|
<ArtworkNavigator
|
|
artworkId={artwork.id}
|
|
onNavigate={handleNavigate}
|
|
onOpenViewer={openViewer}
|
|
onReady={setNavState}
|
|
/>
|
|
|
|
{/* Fullscreen viewer modal */}
|
|
<ArtworkViewer
|
|
isOpen={viewerOpen}
|
|
onClose={closeViewer}
|
|
artwork={artwork}
|
|
presentLg={presentLg}
|
|
presentXl={presentXl}
|
|
/>
|
|
</>
|
|
)
|
|
}
|
|
|
|
// Auto-mount if the Blade view provided data attributes
|
|
const el = document.getElementById('artwork-page')
|
|
if (el) {
|
|
const parse = (key, fallback = null) => {
|
|
try {
|
|
return JSON.parse(el.dataset[key] || 'null') ?? fallback
|
|
} catch {
|
|
return fallback
|
|
}
|
|
}
|
|
|
|
const root = createRoot(el)
|
|
root.render(
|
|
<ArtworkPage
|
|
artwork={parse('artwork')}
|
|
related={parse('related', [])}
|
|
presentMd={parse('presentMd')}
|
|
presentLg={parse('presentLg')}
|
|
presentXl={parse('presentXl')}
|
|
presentSq={parse('presentSq')}
|
|
canonicalUrl={parse('canonical', '')}
|
|
isAuthenticated={parse('isAuthenticated', false)}
|
|
comments={parse('comments', [])}
|
|
/>,
|
|
)
|
|
}
|
|
|
|
export default ArtworkPage
|