import React, { useState, useCallback, useEffect, useMemo } from 'react' import { createRoot } from 'react-dom/client' import axios from 'axios' import ArtworkHero from '../components/artwork/ArtworkHero' import ArtworkMediaStrip from '../components/artwork/ArtworkMediaStrip' import ArtworkMeta from '../components/artwork/ArtworkMeta' import ArtworkAwards from '../components/artwork/ArtworkAwards' import ArtworkTags from '../components/artwork/ArtworkTags' import ArtworkDescription from '../components/artwork/ArtworkDescription' import ArtworkComments from '../components/artwork/ArtworkComments' import ArtworkActionBar from '../components/artwork/ArtworkActionBar' import ArtworkDetailsPanel from '../components/artwork/ArtworkDetailsPanel' import CreatorSpotlight from '../components/artwork/CreatorSpotlight' import ArtworkRecommendationsRails from '../components/artwork/ArtworkRecommendationsRails' import ArtworkNavigator from '../components/viewer/ArtworkNavigator' import ArtworkViewer from '../components/viewer/ArtworkViewer' import ReactionBar from '../components/comments/ReactionBar' 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) const [selectedMediaId, setSelectedMediaId] = useState('cover') // Nav arrow state — populated by ArtworkNavigator once neighbors resolve const [navState, setNavState] = useState({ hasPrev: false, hasNext: false, navigatePrev: null, navigateNext: null }) // Artwork-level reactions const [reactionTotals, setReactionTotals] = useState(null) useEffect(() => { if (!artwork?.id) return axios .get(`/api/artworks/${artwork.id}/reactions`) .then(({ data }) => setReactionTotals(data.totals ?? {})) .catch(() => setReactionTotals({})) }, [artwork?.id]) /** * 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) setSelectedMediaId('cover') setViewerOpen(false) // close viewer when navigating away }, []) if (!artwork) return null const mediaItems = useMemo(() => { const coverItem = { id: 'cover', label: 'Cover art', thumbUrl: presentSq?.url || presentMd?.url || presentLg?.url || artwork?.thumbs?.sq?.url || artwork?.thumbs?.md?.url || null, mdUrl: presentMd?.url || artwork?.thumbs?.md?.url || null, lgUrl: presentLg?.url || artwork?.thumbs?.lg?.url || null, xlUrl: presentXl?.url || artwork?.thumbs?.xl?.url || null, width: Number(artwork?.dimensions?.width || artwork?.width || 0) || null, height: Number(artwork?.dimensions?.height || artwork?.height || 0) || null, } const screenshotItems = Array.isArray(artwork?.screenshots) ? artwork.screenshots.map((item, index) => ({ id: item.id || `shot-${index + 1}`, label: item.label || `Screenshot ${index + 1}`, thumbUrl: item.thumb_url || item.url || null, mdUrl: item.url || item.thumb_url || null, lgUrl: item.url || item.thumb_url || null, xlUrl: item.url || item.thumb_url || null, width: null, height: null, })) : [] return [coverItem, ...screenshotItems].filter((item) => Boolean(item.thumbUrl || item.lgUrl || item.xlUrl)) }, [artwork, presentMd, presentLg, presentXl, presentSq]) const selectedMedia = mediaItems.find((item) => item.id === selectedMediaId) || mediaItems[0] || null useEffect(() => { if (!selectedMedia && mediaItems.length > 0) { setSelectedMediaId(mediaItems[0].id) } }, [mediaItems, selectedMedia]) const initialAwards = artwork?.awards ?? null return ( <>
{/* ── Hero ────────────────────────────────────────────────────── */}
{/* ── Centered action bar with stat counts ────────────────────── */}
{/* ── Two-column content ──────────────────────────────────────── */}
{/* LEFT COLUMN — main content */}
{/* Title + author + breadcrumbs */} {/* Description */} {/* Artwork reactions */} {reactionTotals !== null && (
Artwork Reactions

Make this artwork feel alive

Drop a reaction so other people instantly see whether this piece hits with love, fire, wow, or a quick clap.

)} {/* Tags & categories */} {/* Comments */}
{/* RIGHT COLUMN — sidebar */}
{/* ── Full-width recommendation rails ─────────────────────────── */}
{/* Artwork navigator — prev/next arrows, keyboard, swipe, no page reload */} {/* Fullscreen viewer modal */} ) } // 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( , ) } export default ArtworkPage