Add gallery loading indicators and recover from WebGL context loss.

- Show loading markers while the catalog, portrait thumbnails, and 3D
  halls load so users know loading is still in progress.
- Recover the 3D hall from a lost WebGL context by remounting the canvas
  with a fresh context instead of leaving a permanent dark window, and
  guard the HDR environment map behind an error boundary.
- Show movement streams whose span overlaps the view even when their
  artists lived outside the visible time range.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Danila Khodjaef
2026-07-07 12:31:56 +03:00
co-authored by Cursor
parent 99b8559607
commit 1408811948
5 changed files with 300 additions and 31 deletions
+25 -11
View File
@@ -1,12 +1,13 @@
import { useState, useEffect, useCallback, useMemo, useRef, lazy, Suspense } from 'react';
import { useState, useEffect, useCallback, useMemo, useRef } from 'react';
import Timeline from '../components/Timeline';
import TimelineEventGuides from '../components/TimelineEventGuides';
import MovementBands from '../components/MovementBands';
const VirtualGallery = lazy(() => import('../components/VirtualGallery'));
import VirtualGallery from '../components/VirtualGallery';
import PaintingDetailView from '../components/PaintingDetail';
import ArtistBio from '../components/ArtistBio';
import CheckupPage from '../pages/CheckupPage';
import CuratorLoginModal from '../components/CuratorLoginModal';
import GalleryLoadingMarker from '../components/GalleryLoadingMarker';
import '../components/CuratorLoginModal.css';
import { useAuth } from '../context/AuthContext';
import { api, type FixPaintingImageResult, type FixArtistPortraitResult } from '../api/client';
@@ -108,6 +109,8 @@ export default function HomePage() {
const [timelineData, setTimelineData] = useState<TimelineData>({ eras: [], movements: [] });
const [artists, setArtists] = useState<Artist[]>([]);
const [loading, setLoading] = useState(true);
const [portraitsLoading, setPortraitsLoading] = useState(false);
const [galleryEntryLoading, setGalleryEntryLoading] = useState<string | null>(null);
const [error, setError] = useState<string | null>(null);
const [detailArtistPaintings, setDetailArtistPaintings] = useState<Painting[]>([]);
const [imageRevisions, setImageRevisions] = useState<Record<number, number>>({});
@@ -399,21 +402,27 @@ export default function HomePage() {
}, []);
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);
}
};
@@ -599,9 +608,8 @@ export default function HomePage() {
aria-hidden={!galleryActive}
>
{displayGallery.kind === 'artist' ? (
<Suspense fallback={null}>
<VirtualGallery
key={`artist-${displayGallery.artistId}-${galleryRevision}-${galleryActive ? 'live' : 'parked'}`}
key={`artist-${displayGallery.artistId}-${galleryRevision}`}
mode="artist"
data={displayGallery.data}
imageRevisions={imageRevisions}
@@ -617,11 +625,9 @@ export default function HomePage() {
})
}
/>
</Suspense>
) : (
<Suspense fallback={null}>
<VirtualGallery
key={`movement-${displayGallery.movementId}-${galleryRevision}-${galleryActive ? 'live' : 'parked'}`}
key={`movement-${displayGallery.movementId}-${galleryRevision}`}
mode="movement"
data={displayGallery.data}
imageRevisions={imageRevisions}
@@ -629,7 +635,6 @@ export default function HomePage() {
onPaintingClick={handlePaintingClick}
onBack={() => setView({ type: 'timeline' })}
/>
</Suspense>
)}
</div>
)}
@@ -787,6 +792,16 @@ export default function HomePage() {
</header>
<div className="home-timeline-stack">
{loading && (
<GalleryLoadingMarker overlay message="Loading art history…" />
)}
{galleryEntryLoading && (
<GalleryLoadingMarker overlay message={galleryEntryLoading} />
)}
{portraitsLoading && !loading && !galleryEntryLoading && (
<GalleryLoadingMarker banner message="Loading portraits…" />
)}
<Timeline
eras={timelineData.eras}
viewStart={viewStart}
@@ -799,9 +814,7 @@ export default function HomePage() {
{error && <div className="error-banner">{error}</div>}
{loading && timelineData.movements.length === 0 ? (
<div className="loading home-movements-section">Loading art history...</div>
) : (
{!loading && (
<>
<TimelineEventGuides viewStart={viewStart} viewEnd={viewEnd} />
<div className="home-movements-section">
@@ -817,6 +830,7 @@ export default function HomePage() {
onArtistClick={handleArtistClick}
onMovementClick={handleMovementClick}
onArtistHover={setHoveredLifespan}
onPortraitsLoadingChange={setPortraitsLoading}
/>
</div>
</>