import { useState, useEffect, useCallback, useMemo, useRef } from 'react'; import Timeline from '../components/Timeline'; 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 { api, type FixPaintingImageResult } from '../api/client'; import type { TimelineData, Artist, ArtistDetail, Painting, PaintingDetail } from '../types'; import { sortArtistPaintingsChronological } from '../utils/paintingUtils'; import { readDebugMode, writeDebugMode } from '../utils/debugMode'; import './HomePage.css'; type View = | { type: 'timeline' } | { type: 'checkup' } | { type: 'gallery'; artistId: number; data: ArtistDetail } | { type: 'painting'; paintingId: number; data: PaintingDetail; returnTo: View } | { type: 'bio'; artistId: number; data: ArtistDetail; returnTo: View }; function patchPaintingInArtistDetail( detail: ArtistDetail, paintingId: number, patch: Partial ): ArtistDetail { return { ...detail, paintings: detail.paintings.map((p) => (p.id === paintingId ? { ...p, ...patch } : p)), }; } export default function HomePage() { const [view, setView] = useState({ type: 'timeline' }); const [gallerySession, setGallerySession] = useState<{ artistId: number; data: ArtistDetail } | null>( 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 [error, setError] = useState(null); const [detailArtistPaintings, setDetailArtistPaintings] = useState([]); const [imageRevisions, setImageRevisions] = useState>({}); const [debugMode, setDebugMode] = useState(readDebugMode); const detailReturnToRef = useRef({ type: 'timeline' }); useEffect(() => { api.getBounds() .then((b) => { const min = b.min_year ?? -800; const max = b.max_year ?? 2025; setBounds({ min, max }); setViewStart(min); setViewEnd(max); }) .catch(() => {}); }, []); useEffect(() => { if (view.type === 'gallery') { setGallerySession({ artistId: view.artistId, data: view.data }); } else if (view.type === 'timeline') { setGallerySession(null); } }, [view]); const loadTimelineData = useCallback(async (start: number, end: number) => { try { setLoading(true); const [timeline, artistList] = await Promise.all([ api.getTimeline(start, end), api.getArtists(start, end), ]); setTimelineData(timeline); setArtists(artistList); setError(null); } catch { setError('Could not load gallery data. Is the server running?'); } finally { setLoading(false); } }, []); useEffect(() => { loadTimelineData(viewStart, viewEnd); }, [viewStart, viewEnd, loadTimelineData]); const handleViewChange = (start: number, end: number) => { setViewStart(start); setViewEnd(end); }; const toggleDebugMode = () => { setDebugMode((prev) => { const next = !prev; writeDebugMode(next); return next; }); }; const handlePaintingImageFixed = useCallback(async (paintingId: number, fixResult: FixPaintingImageResult) => { const data = await api.getPainting(paintingId); const patch: Partial = { image_path: fixResult.imagePath ?? data.painting.image_path, thumbnail_path: fixResult.thumbnailPath ?? data.painting.thumbnail_path, 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), }; } return { ...current, data: updatedData, returnTo }; }); setDetailArtistPaintings((list) => list.map((p) => (p.id === paintingId ? { ...p, ...patch } : p)) ); setGallerySession((session) => session && session.artistId === data.painting.artist_id ? { ...session, data: patchPaintingInArtistDetail(session.data, paintingId, patch) } : 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), }; } return { ...current, data: updatedData, returnTo }; }); setDetailArtistPaintings((list) => list.map((p) => (p.id === paintingId ? { ...p, ...patch } : p)) ); setGallerySession((session) => session && session.artistId === data.painting.artist_id ? { ...session, data: patchPaintingInArtistDetail(session.data, paintingId, patch) } : session ); }, [] ); const handleArtistClick = async (artistId: number) => { try { const data = await api.getArtist(artistId); setGallerySession({ artistId, data }); setView({ type: 'gallery', artistId, data }); } catch { setError('Failed to load artist gallery.'); } }; 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 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?.artistId === artistId) { setDetailArtistPaintings(gallerySession.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( () => sortArtistPaintingsChronological(detailArtistPaintings), [detailArtistPaintings] ); const galleryActive = view.type === 'gallery'; return ( <> {gallerySession && (
setView({ type: 'timeline' })} onBioClick={() => handleBioClick(gallerySession.data, { type: 'gallery', ...gallerySession })} />
)} {view.type === 'painting' && (
{ const returnTo = view.returnTo; if (returnTo.type === 'gallery' && gallerySession?.artistId === returnTo.artistId) { setView({ type: 'gallery', artistId: gallerySession.artistId, data: gallerySession.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={debugMode} onPaintingImageFixed={handlePaintingImageFixed} onPaintingCheckupFlagsUpdated={handlePaintingCheckupFlagsUpdated} />
)} {view.type === 'bio' && (
setView(view.returnTo)} onEnterGallery={() => setView({ type: 'gallery', artistId: view.artistId, data: view.data }) } />
)} {view.type === 'checkup' && ( setView({ type: 'timeline' })} onOpenPainting={handlePaintingClick} /> )} {view.type === 'timeline' && (

Virtual Art Gallery

Watch art movements branch forward through time — each flowing from what came before

{error &&
{error}
} {loading ? (
Loading art history...
) : (
)}
)} ); }