import { useEffect, useState, type SyntheticEvent } from 'react'; import type { InfluenceLink, Painting, PaintingDetail } from '../types'; import { api, debugImageProxyUrl, imageUrl, paintingImageUrl, type DebugImageSearchResult, type FixPaintingImageResult } from '../api/client'; import PaintingLightbox from './PaintingLightbox'; import './PaintingDetail.css'; interface Props { data: PaintingDetail; artistPaintings?: Painting[]; onBack: () => void; onPaintingClick: (paintingId: number) => void; onCatalogNavigate: (paintingId: number) => void; onArtistBio: () => void; onInfluenceArtistClick?: (artistId: number) => void; debugMode?: boolean; onPaintingImageFixed?: ( paintingId: number, fixResult: FixPaintingImageResult ) => void | Promise; onPaintingCheckupFlagsUpdated?: ( paintingId: number, flags: { checked: boolean; fixed: boolean } ) => void | Promise; } function influenceKey(inf: InfluenceLink, index: number): string { if (inf.source_type === 'movement') return `movement-${inf.movement_id ?? inf.movement_name}-${index}`; if (inf.source_type === 'artist') return `artist-${inf.source_artist_id ?? inf.source_artist_name}-${index}`; return `painting-${inf.id}-${index}`; } function periodLabel(inf: InfluenceLink): string | null { if (inf.period_note) return inf.period_note; if (inf.period_start_year != null && inf.period_end_year != null) { return `${inf.period_start_year}–${inf.period_end_year}`; } if (inf.period_start_year != null) return `from ${inf.period_start_year}`; return null; } function InfluenceCard({ inf, onPaintingClick, onInfluenceArtistClick, }: { inf: InfluenceLink; onPaintingClick: (id: number) => void; onInfluenceArtistClick?: (artistId: number) => void; }) { const aspects = inf.aspects ? inf.aspects.split(',').map((a) => a.trim()).filter(Boolean) : []; const period = periodLabel(inf); const sourceType = inf.source_type || 'painting'; const meta = ( <> {aspects.length > 0 && (
{aspects.map((aspect) => ( {aspect} ))}
)} {period &&

{period}

} {inf.notes &&

{inf.notes}

} {inf.quote && (

“{inf.quote}”

{(inf.source_author || inf.source) && (
— {inf.source_author} {inf.source && , {inf.source}}
)}
)} {inf.source_url && ( e.stopPropagation()} > Read source: {inf.source_author || inf.source || 'Reference'} )} {inf.confidence === 'discovered' && inf.discovered_via && ( Discovered via {inf.discovered_via} )} ); if (sourceType === 'movement') { return (
{inf.movement_name} Art movement
{meta}
); } if (sourceType === 'artist') { const artistId = inf.source_artist_id; const artistName = inf.source_artist_name || 'Unknown artist'; return (
{meta}
); } if (!inf.id) return null; return (
{meta}
); } export default function PaintingDetailView({ data, artistPaintings = [], onBack, onPaintingClick, onCatalogNavigate, onArtistBio, onInfluenceArtistClick, debugMode = false, onPaintingImageFixed, onPaintingCheckupFlagsUpdated, }: Props) { const { painting, influencedBy, influenced } = data; const [fullscreen, setFullscreen] = useState(false); const [imageVersion, setImageVersion] = useState(0); const [debugSearch, setDebugSearch] = useState(null); const [debugLoading, setDebugLoading] = useState(false); const [debugError, setDebugError] = useState(null); const [fixing, setFixing] = useState(false); const [markingChecked, setMarkingChecked] = useState(false); const imageSrc = `${paintingImageUrl(painting)}${paintingImageUrl(painting).includes('?') ? '&' : '?'}v=${imageVersion}`; const catalogIndex = artistPaintings.findIndex((p) => p.id === painting.id); const previousPainting = catalogIndex > 0 ? artistPaintings[catalogIndex - 1] : null; const nextPainting = catalogIndex >= 0 && catalogIndex < artistPaintings.length - 1 ? artistPaintings[catalogIndex + 1] : null; const showCatalogNav = artistPaintings.length > 1 && catalogIndex >= 0; useEffect(() => { setFullscreen(false); setImageVersion(0); }, [painting.id]); useEffect(() => { if (!debugMode) { setDebugSearch(null); setDebugError(null); return; } let cancelled = false; setDebugLoading(true); setDebugError(null); setDebugSearch(null); api.getPaintingDebugImageSearch(painting.id) .then((result) => { if (!cancelled) setDebugSearch(result); }) .catch(() => { if (!cancelled) setDebugError('Google image search failed.'); }) .finally(() => { if (!cancelled) setDebugLoading(false); }); return () => { cancelled = true; }; }, [debugMode, painting.id, painting.title, painting.artist_name]); const handleFixImage = async () => { if (!debugSearch?.imageUrl || fixing) return; setFixing(true); setDebugError(null); try { const fixResult = await api.fixPaintingImage(painting.id, debugSearch.imageUrl, { searchUrl: debugSearch.searchUrl, source: debugSearch.source, thumbUrl: debugSearch.thumbUrl, }); setImageVersion((v) => v + 1); if (onPaintingImageFixed) { await onPaintingImageFixed(painting.id, fixResult); } } catch (err) { setDebugError(err instanceof Error ? err.message : 'Could not replace image.'); } finally { setFixing(false); } }; const handleMarkChecked = async () => { if (painting.checkup_checked || markingChecked) return; setMarkingChecked(true); setDebugError(null); try { const updated = await api.updatePaintingCheckupFlags(painting.id, { checked: true }); await onPaintingCheckupFlagsUpdated?.(painting.id, updated); } catch (err) { setDebugError(err instanceof Error ? err.message : 'Could not mark as checked.'); } finally { setMarkingChecked(false); } }; useEffect(() => { if (fullscreen) return; const onKeyDown = (e: KeyboardEvent) => { if (e.key === 'ArrowLeft' && previousPainting) { e.preventDefault(); onCatalogNavigate(previousPainting.id); } else if (e.key === 'ArrowRight' && nextPainting) { e.preventDefault(); onCatalogNavigate(nextPainting.id); } }; window.addEventListener('keydown', onKeyDown); return () => window.removeEventListener('keydown', onKeyDown); }, [fullscreen, previousPainting, nextPainting, onCatalogNavigate]); const handleImageError = (e: SyntheticEvent) => { e.currentTarget.src = '/placeholder-art.svg'; }; return (

{painting.title}

{painting.artist_name} {painting.year && ` · ${painting.year}`} {showCatalogNav && ( {' · '} {catalogIndex + 1} of {artistPaintings.length} )}

{showCatalogNav && ( )}
setFullscreen(true)} onKeyDown={(e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); setFullscreen(true); } }} title="View full screen" aria-label={`View ${painting.title} full screen`} > {painting.title}
{showCatalogNav && ( )}
{painting.description && (

{painting.description}

)}
{debugMode && ( )} {fullscreen && ( setFullscreen(false)} /> )}
); }