Redesign 3D gallery as single hall per artist with exit navigation.

Restore React client source, add hall-to-hall navigation via painting influences grouped by movement, and update documentation.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Danila Khodjaef
2026-06-19 10:10:13 +03:00
co-authored by Cursor
parent 44d3d4359a
commit 08f99d7a29
25 changed files with 2954 additions and 218 deletions
+69
View File
@@ -0,0 +1,69 @@
import type {
TimelineData,
YearBounds,
Artist,
ArtistDetail,
PaintingDetail,
ArtistNavigation,
} from '../types';
const API = '/api';
async function fetchJson<T>(url: string): Promise<T> {
const res = await fetch(url);
if (!res.ok) throw new Error(`API error: ${res.status}`);
return res.json();
}
export function imageUrl(path: string | null | undefined): string {
if (!path) return '/placeholder-art.svg';
return `/images/${path}`;
}
/** Image for 3D gallery — local cached files only (API fetch is too slow for realtime 3D) */
export function galleryImageUrl(painting: {
thumbnail_path?: string | null;
image_path?: string | null;
}): string | null {
if (painting.thumbnail_path) return `/images/${painting.thumbnail_path}`;
if (painting.image_path) return `/images/${painting.image_path}`;
return null;
}
export function paintingImageUrl(painting: {
id: number;
image_path?: string | null;
thumbnail_path?: string | null;
}): string {
if (painting.image_path) return `/images/${painting.image_path}`;
if (painting.thumbnail_path) return `/images/${painting.thumbnail_path}`;
return `/api/paintings/${painting.id}/image?size=full`;
}
export async function preloadArtistImages(artistId: number): Promise<{ fetched: number; total: number }> {
const res = await fetch(`${API}/artists/${artistId}/preload-images`, { method: 'POST' });
if (!res.ok) throw new Error('Preload failed');
return res.json();
}
export const api = {
getBounds: () => fetchJson<YearBounds>(`${API}/bounds`),
getTimeline: (start: number, end: number) =>
fetchJson<TimelineData>(`${API}/timeline?start=${start}&end=${end}`),
getArtists: (start?: number, end?: number, movementId?: number) => {
const params = new URLSearchParams();
if (start != null) params.set('start', String(start));
if (end != null) params.set('end', String(end));
if (movementId != null) params.set('movement_id', String(movementId));
return fetchJson<Artist[]>(`${API}/artists?${params}`);
},
getArtist: (id: number) => fetchJson<ArtistDetail>(`${API}/artists/${id}`),
getArtistNavigation: (id: number) =>
fetchJson<ArtistNavigation>(`${API}/artists/${id}/navigation`),
getPainting: (id: number) => fetchJson<PaintingDetail>(`${API}/paintings/${id}`),
};