Add movement artist filter modal and fix large-hall texture blanks

Clicking a movement opens ArtistFilterModal to choose artists before the gallery. Large halls no longer permanently blank frames when texture loads exceed the overlay deadline; fall back thumb to full to on-demand API.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Danila Khodjaef
2026-07-29 19:05:54 +03:00
co-authored by Cursor
parent ad1572b3aa
commit 0a5918c4dd
9 changed files with 589 additions and 23 deletions
+52 -1
View File
@@ -12,12 +12,14 @@ import InfluencesPage from '../pages/InfluencesPage';
import ToursPage from '../pages/ToursPage';
import UsersPage from '../pages/UsersPage';
import CuratorLoginModal from '../components/CuratorLoginModal';
import ArtistFilterModal from '../components/ArtistFilterModal';
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/ArtistFilterModal.css';
import '../components/ToursPopup.css';
import '../components/LocaleSwitcher.css';
import '../pages/TranslationsPage.css';
@@ -34,6 +36,7 @@ import type {
PaintingDetail,
MovementGalleryDetail,
TourGalleryDetail,
ArtistSummary,
} from '../types';
import { createViewChangeScheduler } from '../utils/timelineView';
import { sortArtistPaintingsChronological } from '../utils/paintingUtils';
@@ -178,6 +181,11 @@ export default function HomePage() {
const [loginOpen, setLoginOpen] = useState(false);
const [loginRedirect, setLoginRedirect] = useState<CuratorLoginRedirect>(null);
const [toursPopupOpen, setToursPopupOpen] = useState(false);
const [artistFilterModal, setArtistFilterModal] = useState<{
movementId: number;
movementName: string;
artists: ArtistSummary[];
} | null>(null);
const effectiveDebugMode = debugMode && canImages;
const [galleryRevision, setGalleryRevision] = useState(0);
const viewRef = useRef(view);
@@ -639,11 +647,43 @@ export default function HomePage() {
};
const handleMovementClick = async (movementId: number) => {
setGalleryEntryLoading('Loading artists…');
try {
const artists = await api.getMovementArtistsSummary(movementId);
if (artists.length === 0) {
setError('No artists in this movement.');
return;
}
const movement = timelineData.movements.find((m) => m.id === movementId);
setArtistFilterModal({
movementId,
movementName: movement?.name ?? 'Movement',
artists,
});
} catch {
setError('Failed to load movement artists.');
} finally {
setGalleryEntryLoading(null);
}
};
const handleArtistFilterProceed = async (selectedIds: Set<number>) => {
if (!artistFilterModal) return;
const { movementId } = artistFilterModal;
setArtistFilterModal(null);
setGalleryEntryLoading('Opening movement gallery…');
try {
await api.preloadMovementImages(movementId).catch(() => undefined);
const data = await api.getMovementGallery(movementId);
openMovementGallery(movementId, data);
const filtered: MovementGalleryDetail = {
...data,
paintings: data.paintings.filter((p) => selectedIds.has(Number(p.artist_id))),
};
if (filtered.paintings.length === 0) {
setError('No paintings for the selected artists.');
return;
}
openMovementGallery(movementId, filtered);
} catch {
setError('Failed to load movement gallery.');
} finally {
@@ -1117,6 +1157,17 @@ export default function HomePage() {
onSelectTour={(tourId) => void handleSelectPublishedTour(tourId)}
/>
{artistFilterModal && (
<ArtistFilterModal
open
movementName={artistFilterModal.movementName}
artists={artistFilterModal.artists}
portraitRevisions={portraitRevisions}
onClose={() => setArtistFilterModal(null)}
onProceed={(selectedIds) => void handleArtistFilterProceed(selectedIds)}
/>
)}
{view.type === 'timeline' && (
<div className="home-page">
<header className="site-header">