import { useState, useEffect, useCallback, useMemo, useRef } from 'react'; import { useTranslation } from 'react-i18next'; import Timeline from '../components/Timeline'; import TimelineEventGuides from '../components/TimelineEventGuides'; import MovementBands from '../components/MovementBands'; import VirtualGallery from '../components/VirtualGallery'; import PaintingDetailView from '../components/PaintingDetail'; import ArtistBio from '../components/ArtistBio'; import CheckupPage from '../pages/CheckupPage'; import TranslationsPage from '../pages/TranslationsPage'; import InfluencesPage from '../pages/InfluencesPage'; import ToursPage from '../pages/ToursPage'; import CuratorLoginModal from '../components/CuratorLoginModal'; import ToursPopup from '../components/ToursPopup'; import CatalogSearchBar from '../components/CatalogSearchBar'; import LocaleSwitcher from '../components/LocaleSwitcher'; import GalleryLoadingMarker from '../components/GalleryLoadingMarker'; import '../components/CatalogSearchBar.css'; import '../components/CuratorLoginModal.css'; import '../components/ToursPopup.css'; import '../components/LocaleSwitcher.css'; import '../pages/TranslationsPage.css'; import '../pages/InfluencesPage.css'; import '../pages/ToursPage.css'; import { useAuth } from '../context/AuthContext'; import { api, setApiLocale, type FixPaintingImageResult, type FixArtistPortraitResult } from '../api/client'; import type { TimelineData, Artist, ArtistDetail, Painting, PaintingDetail, MovementGalleryDetail, TourGalleryDetail, } from '../types'; import { createViewChangeScheduler } from '../utils/timelineView'; import { sortArtistPaintingsChronological } from '../utils/paintingUtils'; import { readDebugMode, readDebugShowMore, writeDebugMode, writeDebugShowMore } from '../utils/debugMode'; import './HomePage.css'; type View = | { type: 'timeline' } | { type: 'checkup' } | { type: 'translations' } | { type: 'influences' } | { type: 'tours' } | { type: 'gallery'; artistId: number; data: ArtistDetail } | { type: 'movement-gallery'; movementId: number; data: MovementGalleryDetail } | { type: 'tour-gallery'; tourId: number; data: TourGalleryDetail } | { type: 'painting'; paintingId: number; data: PaintingDetail; returnTo: View } | { type: 'bio'; artistId: number; data: ArtistDetail; returnTo: View }; type GallerySession = | { kind: 'artist'; artistId: number; data: ArtistDetail } | { kind: 'movement'; movementId: number; data: MovementGalleryDetail } | { kind: 'tour'; tourId: number; data: TourGalleryDetail }; type CuratorLoginRedirect = 'checkup' | 'translations' | 'influences' | 'tours' | null; function patchPaintingInMovementDetail( detail: MovementGalleryDetail, paintingId: number, patch: Partial ): MovementGalleryDetail { return { ...detail, paintings: detail.paintings.map((p) => (p.id === paintingId ? { ...p, ...patch } : p)), }; } function patchPaintingInTourDetail( detail: TourGalleryDetail, paintingId: number, patch: Partial ): TourGalleryDetail { return { ...detail, paintings: detail.paintings.map((p) => (p.id === paintingId ? { ...p, ...patch } : p)), }; } function patchPaintingInArtistDetail( detail: ArtistDetail, paintingId: number, patch: Partial ): ArtistDetail { return { ...detail, paintings: detail.paintings.map((p) => (p.id === paintingId ? { ...p, ...patch } : p)), }; } function patchArtistInArtistDetail(detail: ArtistDetail, patch: Partial): ArtistDetail { return { ...detail, artist: { ...detail.artist, ...patch }, }; } function patchReturnToAfterRemove( returnTo: View, freshArtist?: ArtistDetail, freshMovement?: MovementGalleryDetail, freshTour?: TourGalleryDetail ): View { if (returnTo.type === 'gallery' && freshArtist && returnTo.artistId === freshArtist.artist.id) { return { ...returnTo, data: freshArtist }; } if ( returnTo.type === 'movement-gallery' && freshMovement && returnTo.movementId === freshMovement.movement.id ) { return { ...returnTo, data: freshMovement }; } if (returnTo.type === 'tour-gallery' && freshTour && returnTo.tourId === freshTour.tour.id) { return { ...returnTo, data: freshTour }; } if (returnTo.type === 'painting') { return { ...returnTo, returnTo: patchReturnToAfterRemove(returnTo.returnTo, freshArtist, freshMovement, freshTour), }; } if (returnTo.type === 'bio') { const data = freshArtist && returnTo.artistId === freshArtist.artist.id ? freshArtist : returnTo.data; return { ...returnTo, data, returnTo: patchReturnToAfterRemove(returnTo.returnTo, freshArtist, freshMovement, freshTour), }; } return returnTo; } function catalogNavigateTarget( sorted: Painting[], removedId: number ): number | null { const idx = sorted.findIndex((p) => p.id === removedId); if (idx < 0) return null; const remaining = sorted.filter((p) => p.id !== removedId); if (remaining.length === 0) return null; return idx < remaining.length ? remaining[idx].id : remaining[remaining.length - 1].id; } export default function HomePage() { const { t } = useTranslation('home'); const { isCurator, username, login, logout } = useAuth(); const [view, setView] = useState({ type: 'timeline' }); const [gallerySession, setGallerySession] = useState(null); const [bounds, setBounds] = useState({ min: -800, max: 2025 }); const [viewStart, setViewStart] = useState(-800); const [viewEnd, setViewEnd] = useState(2025); const [timelineData, setTimelineData] = useState({ eras: [], movements: [] }); const [artists, setArtists] = useState([]); const [loading, setLoading] = useState(true); const [portraitsLoading, setPortraitsLoading] = useState(false); const [galleryEntryLoading, setGalleryEntryLoading] = useState(null); const [error, setError] = useState(null); const [detailArtistPaintings, setDetailArtistPaintings] = useState([]); const [imageRevisions, setImageRevisions] = useState>({}); const [portraitRevisions, setPortraitRevisions] = useState>({}); const [localeVersion, setLocaleVersion] = useState(0); const [debugMode, setDebugMode] = useState(readDebugMode); const [debugShowMore, setDebugShowMore] = useState(readDebugShowMore); const [loginOpen, setLoginOpen] = useState(false); const [loginRedirect, setLoginRedirect] = useState(null); const [toursPopupOpen, setToursPopupOpen] = useState(false); const effectiveDebugMode = debugMode && isCurator; const [galleryRevision, setGalleryRevision] = useState(0); const viewRef = useRef(view); viewRef.current = view; const [hoveredLifespan, setHoveredLifespan] = useState<{ birthYear: number; deathYear: number; color: string; } | null>(null); const detailReturnToRef = useRef({ type: 'timeline' }); useEffect(() => { if (view.type === 'gallery') { setGallerySession({ kind: 'artist', artistId: view.artistId, data: view.data }); } else if (view.type === 'movement-gallery') { setGallerySession({ kind: 'movement', movementId: view.movementId, data: view.data }); } else if (view.type === 'tour-gallery') { setGallerySession({ kind: 'tour', tourId: view.tourId, data: view.data }); } else if (view.type === 'timeline') { setGallerySession(null); } }, [view]); // Load full catalog once — pan/zoom filters client-side (MovementBands, Timeline). useEffect(() => { let cancelled = false; (async () => { try { setLoading(true); const catalog = await api.getCatalogBootstrap(); if (cancelled) return; const min = catalog.bounds.min_year ?? -800; const max = catalog.bounds.max_year ?? 2025; setBounds({ min, max }); setViewStart(min); setViewEnd(max); setTimelineData({ eras: catalog.eras, movements: catalog.movements }); setArtists(catalog.artists); setError(null); } catch { if (!cancelled) setError('Could not load gallery data. Is the server running?'); } finally { if (!cancelled) setLoading(false); } })(); return () => { cancelled = true; }; }, [localeVersion]); const handleLocaleChange = useCallback((locale: 'en' | 'ru') => { setApiLocale(locale); setLocaleVersion((v) => v + 1); }, []); const viewChangeScheduler = useRef( createViewChangeScheduler((start, end) => { setViewStart(start); setViewEnd(end); }) ); useEffect(() => () => viewChangeScheduler.current.cancel(), []); const handleViewChange = useCallback((start: number, end: number) => { viewChangeScheduler.current.schedule(start, end); }, []); const goToTimelineHome = useCallback(() => { detailReturnToRef.current = { type: 'timeline' }; setGallerySession(null); setHoveredLifespan(null); setGalleryEntryLoading(null); setViewStart(bounds.min); setViewEnd(bounds.max); setGalleryRevision((revision) => revision + 1); setView({ type: 'timeline' }); }, [bounds.min, bounds.max]); const toggleDebugMode = () => { setDebugMode((prev) => { const next = !prev; writeDebugMode(next); return next; }); }; const setDebugShowMoreEnabled = (enabled: boolean) => { setDebugShowMore(enabled); writeDebugShowMore(enabled); }; const openCuratorLogin = (redirect: CuratorLoginRedirect = null) => { setLoginRedirect(redirect); setLoginOpen(true); }; const handleCuratorLogin = async (user: string, password: string) => { await login(user, password); setLoginOpen(false); if (loginRedirect === 'checkup') { setView({ type: 'checkup' }); } else if (loginRedirect === 'translations') { setView({ type: 'translations' }); } else if (loginRedirect === 'influences') { setView({ type: 'influences' }); } else if (loginRedirect === 'tours') { setView({ type: 'tours' }); } setLoginRedirect(null); }; const handleCuratorLogout = async () => { await logout(); writeDebugMode(false); setDebugMode(false); if ( view.type === 'checkup' || view.type === 'translations' || view.type === 'influences' || view.type === 'tours' ) { goToTimelineHome(); } }; const openCheckup = () => { if (!isCurator) { openCuratorLogin('checkup'); return; } setView({ type: 'checkup' }); }; const openTranslations = () => { if (!isCurator) { openCuratorLogin('translations'); return; } setView({ type: 'translations' }); }; const openInfluences = () => { if (!isCurator) { openCuratorLogin('influences'); return; } setView({ type: 'influences' }); }; const openToursEditor = () => { if (!isCurator) { openCuratorLogin('tours'); return; } setView({ type: 'tours' }); }; const handlePaintingImageFixed = useCallback(async (paintingId: number, fixResult: FixPaintingImageResult) => { const data = await api.getPainting(paintingId); const patch: Partial = { image_path: fixResult.imagePath, thumbnail_path: fixResult.thumbnailPath, image_cache_key: fixResult.image_cache_key ?? null, thumbnail_cache_key: fixResult.thumbnail_cache_key ?? null, checkup_checked: fixResult.checked ?? true, checkup_fixed: fixResult.fixed ?? true, }; const updatedData: PaintingDetail = { ...data, painting: { ...data.painting, ...patch }, }; setImageRevisions((prev) => ({ ...prev, [paintingId]: (prev[paintingId] ?? 0) + 1 })); setView((current) => { if (current.type !== 'painting' || current.paintingId !== paintingId) return current; let returnTo = current.returnTo; if (returnTo.type === 'gallery' && returnTo.artistId === data.painting.artist_id) { returnTo = { ...returnTo, data: patchPaintingInArtistDetail(returnTo.data, paintingId, patch), }; } if (returnTo.type === 'movement-gallery') { returnTo = { ...returnTo, data: patchPaintingInMovementDetail(returnTo.data, paintingId, patch), }; } if (returnTo.type === 'tour-gallery') { returnTo = { ...returnTo, data: patchPaintingInTourDetail(returnTo.data, paintingId, patch), }; } return { ...current, data: updatedData, returnTo }; }); setDetailArtistPaintings((list) => list.map((p) => (p.id === paintingId ? { ...p, ...patch } : p)) ); setGallerySession((session) => { if (session?.kind === 'artist' && session.artistId === data.painting.artist_id) { return { ...session, data: patchPaintingInArtistDetail(session.data, paintingId, patch) }; } if (session?.kind === 'movement') { return { ...session, data: patchPaintingInMovementDetail(session.data, paintingId, patch) }; } if (session?.kind === 'tour') { return { ...session, data: patchPaintingInTourDetail(session.data, paintingId, patch) }; } return session; }); }, []); const handlePaintingCheckupFlagsUpdated = useCallback( async (paintingId: number, flags: { checked: boolean; fixed: boolean }) => { const data = await api.getPainting(paintingId); const patch: Partial = { checkup_checked: flags.checked, checkup_fixed: flags.fixed, }; const updatedData: PaintingDetail = { ...data, painting: { ...data.painting, ...patch }, }; setView((current) => { if (current.type !== 'painting' || current.paintingId !== paintingId) return current; let returnTo = current.returnTo; if (returnTo.type === 'gallery' && returnTo.artistId === data.painting.artist_id) { returnTo = { ...returnTo, data: patchPaintingInArtistDetail(returnTo.data, paintingId, patch), }; } if (returnTo.type === 'movement-gallery') { returnTo = { ...returnTo, data: patchPaintingInMovementDetail(returnTo.data, paintingId, patch), }; } if (returnTo.type === 'tour-gallery') { returnTo = { ...returnTo, data: patchPaintingInTourDetail(returnTo.data, paintingId, patch), }; } return { ...current, data: updatedData, returnTo }; }); setDetailArtistPaintings((list) => list.map((p) => (p.id === paintingId ? { ...p, ...patch } : p)) ); setGallerySession((session) => { if (session?.kind === 'artist' && session.artistId === data.painting.artist_id) { return { ...session, data: patchPaintingInArtistDetail(session.data, paintingId, patch) }; } if (session?.kind === 'movement') { return { ...session, data: patchPaintingInMovementDetail(session.data, paintingId, patch) }; } if (session?.kind === 'tour') { return { ...session, data: patchPaintingInTourDetail(session.data, paintingId, patch) }; } return session; }); }, [] ); const applyArtistPatch = useCallback((artistId: number, patch: Partial) => { setArtists((list) => list.map((a) => (a.id === artistId ? { ...a, ...patch } : a))); setView((current) => { if (current.type === 'bio' && current.artistId === artistId) { return { ...current, data: patchArtistInArtistDetail(current.data, patch) }; } if (current.type === 'gallery' && current.artistId === artistId) { return { ...current, data: patchArtistInArtistDetail(current.data, patch) }; } if (current.type === 'painting' && current.data.painting.artist_id === artistId) { return { ...current, data: { ...current.data, painting: { ...current.data.painting, artist_portrait: patch.portrait_path ?? current.data.painting.artist_portrait, }, }, }; } return current; }); setGallerySession((session) => session?.kind === 'artist' && session.artistId === artistId ? { ...session, data: patchArtistInArtistDetail(session.data, patch) } : session ); }, []); const handleArtistPortraitFixed = useCallback( async (artistId: number, fixResult: FixArtistPortraitResult) => { const data = await api.getArtist(artistId); const patch: Partial = { portrait_path: fixResult.portraitPath, portrait_thumb_path: fixResult.portraitThumbPath ?? null, portrait_cache_key: fixResult.portrait_cache_key ?? null, portrait_thumb_cache_key: fixResult.portrait_thumb_cache_key ?? null, checkup_checked: fixResult.checked ?? true, checkup_fixed: fixResult.fixed ?? true, }; setPortraitRevisions((prev) => ({ ...prev, [artistId]: (prev[artistId] ?? 0) + 1 })); applyArtistPatch(artistId, patch); setView((current) => current.type === 'bio' && current.artistId === artistId ? { ...current, data: { ...data, artist: { ...data.artist, ...patch } } } : current ); }, [applyArtistPatch] ); const handleArtistCheckupFlagsUpdated = useCallback( async (artistId: number, flags: { checked: boolean; fixed: boolean }) => { const patch: Partial = { checkup_checked: flags.checked, checkup_fixed: flags.fixed, }; applyArtistPatch(artistId, patch); setView((current) => current.type === 'bio' && current.artistId === artistId ? { ...current, data: patchArtistInArtistDetail(current.data, patch) } : current ); }, [applyArtistPatch] ); const openArtistGallery = useCallback((artistId: number, data: ArtistDetail) => { const session: GallerySession = { kind: 'artist', artistId, data }; setGallerySession(session); setView({ type: 'gallery', artistId, data }); }, []); const openMovementGallery = useCallback((movementId: number, data: MovementGalleryDetail) => { const session: GallerySession = { kind: 'movement', movementId, data }; setGallerySession(session); setView({ type: 'movement-gallery', movementId, data }); }, []); const openTourGallery = useCallback((tourId: number, data: TourGalleryDetail) => { const session: GallerySession = { kind: 'tour', tourId, data }; setGallerySession(session); setView({ type: 'tour-gallery', tourId, data }); }, []); const handleSelectPublishedTour = useCallback( async (tourId: number) => { setToursPopupOpen(false); setGalleryEntryLoading(t('openingTourGallery')); try { const data = await api.getTour(tourId); if (!data.paintings.length) { setError(t('tourEmpty')); return; } openTourGallery(tourId, data); } catch { setError(t('tourLoadFailed')); } finally { setGalleryEntryLoading(null); } }, [openTourGallery, t] ); const handleArtistClick = async (artistId: number) => { setGalleryEntryLoading('Opening artist gallery…'); try { await api.preloadArtistImages(artistId).catch(() => undefined); const data = await api.getArtist(artistId); openArtistGallery(artistId, data); } catch { setError('Failed to load artist gallery.'); } finally { setGalleryEntryLoading(null); } }; const handleMovementClick = async (movementId: number) => { setGalleryEntryLoading('Opening movement gallery…'); try { const data = await api.getMovementGallery(movementId); openMovementGallery(movementId, data); } catch { setError('Failed to load movement gallery.'); } finally { setGalleryEntryLoading(null); } }; const handlePaintingClick = async (paintingId: number) => { try { const data = await api.getPainting(paintingId); setView((prev) => { detailReturnToRef.current = prev; return { type: 'painting', paintingId, data, returnTo: prev, }; }); } catch { setError('Failed to load painting details.'); } }; const handleCatalogNavigate = useCallback(async (paintingId: number) => { const returnTo = detailReturnToRef.current; try { const data = await api.getPainting(paintingId); setView({ type: 'painting', paintingId, data, returnTo, }); } catch { setError('Failed to load painting details.'); } }, []); const handlePaintingRemoved = useCallback( async (paintingId: number, artistId: number) => { const currentView = viewRef.current; if (currentView.type !== 'painting' || currentView.paintingId !== paintingId) return; const sorted = gallerySession?.kind === 'tour' || currentView.returnTo.type === 'tour-gallery' ? detailArtistPaintings : sortArtistPaintingsChronological(detailArtistPaintings); const nextId = catalogNavigateTarget(sorted, paintingId); const inMovementCatalog = gallerySession?.kind === 'movement' || currentView.returnTo.type === 'movement-gallery'; const inTourCatalog = gallerySession?.kind === 'tour' || currentView.returnTo.type === 'tour-gallery'; await api.deletePainting(paintingId); const freshArtist = await api.getArtist(artistId); let freshMovement: MovementGalleryDetail | undefined; let freshTour: TourGalleryDetail | undefined; if (gallerySession?.kind === 'movement') { freshMovement = await api.getMovementGallery(gallerySession.movementId); } else if (currentView.returnTo.type === 'movement-gallery') { freshMovement = await api.getMovementGallery(currentView.returnTo.movementId); } if (gallerySession?.kind === 'tour') { freshTour = await api.getTour(gallerySession.tourId); } else if (currentView.returnTo.type === 'tour-gallery') { freshTour = await api.getTour(currentView.returnTo.tourId); } const freshCatalog = inTourCatalog && freshTour ? freshTour.paintings : inMovementCatalog && freshMovement ? sortArtistPaintingsChronological(freshMovement.paintings) : sortArtistPaintingsChronological(freshArtist.paintings); const removedIdx = sorted.findIndex((p) => p.id === paintingId); const navigateId = nextId && freshCatalog.some((p) => p.id === nextId) ? nextId : freshCatalog.length > 0 ? freshCatalog[Math.min(removedIdx, freshCatalog.length - 1)]?.id ?? freshCatalog[0].id : null; setDetailArtistPaintings(freshCatalog); setGallerySession((session) => { if (!session) return session; if (session.kind === 'artist' && session.artistId === artistId) { return { ...session, data: freshArtist }; } if (session.kind === 'movement' && freshMovement) { return { ...session, data: freshMovement }; } if (session.kind === 'tour' && freshTour) { return { ...session, data: freshTour }; } return session; }); setImageRevisions((prev) => { const next = { ...prev }; delete next[paintingId]; return next; }); setGalleryRevision((v) => v + 1); const patchedReturnTo = patchReturnToAfterRemove( currentView.returnTo, freshArtist, freshMovement, freshTour ); detailReturnToRef.current = patchedReturnTo; setView((current) => { if (current.type === 'gallery' && current.artistId === artistId) { return { ...current, data: freshArtist }; } if (current.type === 'movement-gallery' && freshMovement) { return { ...current, data: freshMovement }; } if (current.type === 'tour-gallery' && freshTour) { return { ...current, data: freshTour }; } if (current.type !== 'painting' || current.paintingId !== paintingId) { return current; } if (navigateId) return current; return patchedReturnTo; }); if (navigateId) { const data = await api.getPainting(navigateId); setView({ type: 'painting', paintingId: navigateId, data, returnTo: patchedReturnTo, }); } }, [detailArtistPaintings, gallerySession] ); const handleBioClick = (artistData: ArtistDetail, returnTo: View) => { setView({ type: 'bio', artistId: artistData.artist.id, data: artistData, returnTo }); }; useEffect(() => { if (view.type !== 'painting') { setDetailArtistPaintings([]); return; } const artistId = view.data.painting.artist_id; if (gallerySession?.kind === 'tour') { setDetailArtistPaintings(gallerySession.data.paintings); return; } if (gallerySession?.kind === 'artist' && gallerySession.artistId === artistId) { setDetailArtistPaintings(gallerySession.data.paintings); return; } const returnTo = detailReturnToRef.current; if (returnTo.type === 'tour-gallery') { setDetailArtistPaintings(returnTo.data.paintings); return; } if (returnTo.type === 'movement-gallery') { setDetailArtistPaintings(sortArtistPaintingsChronological(returnTo.data.paintings)); return; } let cancelled = false; api.getArtist(artistId) .then((data) => { if (!cancelled) setDetailArtistPaintings(data.paintings); }) .catch(() => { if (!cancelled) setDetailArtistPaintings([]); }); return () => { cancelled = true; }; }, [view, gallerySession]); const sortedDetailArtistPaintings = useMemo(() => { const fromTourSession = gallerySession?.kind === 'tour'; const fromTourReturn = view.type === 'painting' && view.returnTo.type === 'tour-gallery'; if (fromTourSession || fromTourReturn) { return detailArtistPaintings; } return sortArtistPaintingsChronological(detailArtistPaintings); }, [detailArtistPaintings, gallerySession, view]); const tourOverlay = view.type === 'painting' && gallerySession?.kind === 'tour' ? { title: gallerySession.data.tour.title, text: gallerySession.data.stopBodies[view.paintingId] ?? '', } : view.type === 'painting' && view.returnTo.type === 'tour-gallery' ? { title: view.returnTo.data.tour.title, text: view.returnTo.data.stopBodies[view.paintingId] ?? '', } : null; const galleryActive = view.type === 'gallery' || view.type === 'movement-gallery' || view.type === 'tour-gallery'; const displayGallery = useMemo((): GallerySession | null => { if (view.type === 'gallery') { return { kind: 'artist', artistId: view.artistId, data: view.data }; } if (view.type === 'movement-gallery') { return { kind: 'movement', movementId: view.movementId, data: view.data }; } if (view.type === 'tour-gallery') { return { kind: 'tour', tourId: view.tourId, data: view.data }; } return gallerySession; }, [view, gallerySession]); return ( <> {displayGallery && (
{displayGallery.kind === 'artist' ? ( handleBioClick(displayGallery.data, { type: 'gallery', artistId: displayGallery.artistId, data: displayGallery.data, }) } /> ) : displayGallery.kind === 'movement' ? ( ) : ( )}
)} {view.type === 'painting' && (
{ if (view.returnTo.type === 'timeline') { goToTimelineHome(); return; } const returnTo = view.returnTo; if ( returnTo.type === 'gallery' && gallerySession?.kind === 'artist' && gallerySession.artistId === returnTo.artistId ) { openArtistGallery(gallerySession.artistId, gallerySession.data); } else if ( returnTo.type === 'movement-gallery' && gallerySession?.kind === 'movement' && gallerySession.movementId === returnTo.movementId ) { openMovementGallery(gallerySession.movementId, gallerySession.data); } else if ( returnTo.type === 'tour-gallery' && gallerySession?.kind === 'tour' && gallerySession.tourId === returnTo.tourId ) { openTourGallery(gallerySession.tourId, gallerySession.data); } else if (returnTo.type === 'gallery') { openArtistGallery(returnTo.artistId, returnTo.data); } else if (returnTo.type === 'movement-gallery') { openMovementGallery(returnTo.movementId, returnTo.data); } else if (returnTo.type === 'tour-gallery') { openTourGallery(returnTo.tourId, returnTo.data); } else { setView(returnTo); } }} onPaintingClick={handlePaintingClick} onCatalogNavigate={handleCatalogNavigate} onArtistBio={async () => { const artistData = await api.getArtist(view.data.painting.artist_id); setView({ type: 'bio', artistId: artistData.artist.id, data: artistData, returnTo: view }); }} onInfluenceArtistClick={handleArtistClick} debugMode={effectiveDebugMode} debugShowMore={debugShowMore && isCurator} onPaintingImageFixed={handlePaintingImageFixed} onPaintingCheckupFlagsUpdated={handlePaintingCheckupFlagsUpdated} onPaintingRemoved={handlePaintingRemoved} />
)} {view.type === 'bio' && (
setView(view.returnTo)} onEnterGallery={() => openArtistGallery(view.artistId, view.data) } onArtistPortraitFixed={handleArtistPortraitFixed} onArtistCheckupFlagsUpdated={handleArtistCheckupFlagsUpdated} />
)} {view.type === 'influences' && ( isCurator ? ( ) : (

{t('curatorRequiredTitle')}

{t('curatorRequiredBody')}

) )} {view.type === 'tours' && ( isCurator ? ( ) : (

{t('curatorRequiredTitle')}

{t('curatorRequiredBody')}

) )} {view.type === 'translations' && ( isCurator ? ( ) : (

{t('curatorRequiredTitle')}

{t('curatorRequiredBody')}

) )} {view.type === 'checkup' && ( isCurator ? ( ) : (

Curator access required

The painting checkup table is available to logged-in curators only.

) )} { setLoginOpen(false); setLoginRedirect(null); }} onLogin={handleCuratorLogin} /> setToursPopupOpen(false)} onSelectTour={(tourId) => void handleSelectPublishedTour(tourId)} /> {view.type === 'timeline' && (
{isCurator ? ( <> {username} ) : ( )}

{t('title')}

{t('subtitle')}

{loading && ( )} {galleryEntryLoading && ( )} {portraitsLoading && !loading && !galleryEntryLoading && ( )} {error &&
{error}
} {!loading && ( <>
)}
)} ); }