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:
co-authored by
Cursor
parent
44d3d4359a
commit
08f99d7a29
@@ -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}`),
|
||||
};
|
||||
Reference in New Issue
Block a user