Improve 3D gallery UX, placeholders, and image accuracy.

Add multi-row dynamic halls, eye-level camera, canvas covers for missing works, preserved view when returning from detail, and corrected image overrides for Kauffman and Raphael. Update documentation and add fetched painting assets.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Danila Khodjaef
2026-06-19 11:40:08 +03:00
co-authored by Cursor
parent 08f99d7a29
commit 792a0b1a77
42 changed files with 618 additions and 219 deletions
+84 -63
View File
@@ -16,6 +16,9 @@ type View =
export default function HomePage() {
const [view, setView] = useState<View>({ 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);
@@ -36,6 +39,14 @@ export default function HomePage() {
.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);
@@ -65,6 +76,7 @@ export default function HomePage() {
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.');
@@ -89,71 +101,80 @@ export default function HomePage() {
setView({ type: 'bio', artistId: artistData.artist.id, data: artistData, returnTo });
};
if (view.type === 'gallery') {
return (
<VirtualGallery
data={view.data}
onPaintingClick={handlePaintingClick}
onNavigateArtist={handleArtistClick}
onBack={() => setView({ type: 'timeline' })}
onBioClick={() => handleBioClick(view.data, view)}
/>
);
}
if (view.type === 'painting') {
return (
<PaintingDetailView
data={view.data}
onBack={() => setView(view.returnTo)}
onPaintingClick={handlePaintingClick}
onArtistBio={async () => {
const artistData = await api.getArtist(view.data.painting.artist_id);
setView({ type: 'bio', artistId: artistData.artist.id, data: artistData, returnTo: view });
}}
/>
);
}
if (view.type === 'bio') {
return (
<ArtistBio
artist={view.data.artist}
onBack={() => setView(view.returnTo)}
onEnterGallery={() => setView({ type: 'gallery', artistId: view.artistId, data: view.data })}
/>
);
}
const galleryActive = view.type === 'gallery';
return (
<div className="home-page">
<header className="site-header">
<h1>Virtual Art Gallery</h1>
<p className="site-subtitle">Explore the flowing connections of art history</p>
</header>
<Timeline
eras={timelineData.eras}
viewStart={viewStart}
viewEnd={viewEnd}
onViewChange={handleViewChange}
absoluteMin={bounds.min}
absoluteMax={bounds.max}
/>
{error && <div className="error-banner">{error}</div>}
{loading ? (
<div className="loading">Loading art history...</div>
) : (
<MovementBands
movements={timelineData.movements}
artists={artists}
viewStart={viewStart}
viewEnd={viewEnd}
onArtistClick={handleArtistClick}
/>
<>
{gallerySession && (
<div className={galleryActive ? undefined : 'gallery-session-suspended'} aria-hidden={!galleryActive}>
<VirtualGallery
data={gallerySession.data}
active={galleryActive}
onPaintingClick={handlePaintingClick}
onNavigateArtist={handleArtistClick}
onBack={() => setView({ type: 'timeline' })}
onBioClick={() => handleBioClick(gallerySession.data, { type: 'gallery', ...gallerySession })}
/>
</div>
)}
</div>
{view.type === 'painting' && (
<div className="home-overlay">
<PaintingDetailView
data={view.data}
onBack={() => setView(view.returnTo)}
onPaintingClick={handlePaintingClick}
onArtistBio={async () => {
const artistData = await api.getArtist(view.data.painting.artist_id);
setView({ type: 'bio', artistId: artistData.artist.id, data: artistData, returnTo: view });
}}
/>
</div>
)}
{view.type === 'bio' && (
<div className="home-overlay">
<ArtistBio
artist={view.data.artist}
onBack={() => setView(view.returnTo)}
onEnterGallery={() =>
setView({ type: 'gallery', artistId: view.artistId, data: view.data })
}
/>
</div>
)}
{view.type === 'timeline' && (
<div className="home-page">
<header className="site-header">
<h1>Virtual Art Gallery</h1>
<p className="site-subtitle">Explore the flowing connections of art history</p>
</header>
<Timeline
eras={timelineData.eras}
viewStart={viewStart}
viewEnd={viewEnd}
onViewChange={handleViewChange}
absoluteMin={bounds.min}
absoluteMax={bounds.max}
/>
{error && <div className="error-banner">{error}</div>}
{loading ? (
<div className="loading">Loading art history...</div>
) : (
<MovementBands
movements={timelineData.movements}
artists={artists}
viewStart={viewStart}
viewEnd={viewEnd}
onArtistClick={handleArtistClick}
/>
)}
</div>
)}
</>
);
}