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,159 @@
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import Timeline from '../components/Timeline';
|
||||
import MovementBands from '../components/MovementBands';
|
||||
import VirtualGallery from '../components/VirtualGallery';
|
||||
import PaintingDetailView from '../components/PaintingDetail';
|
||||
import ArtistBio from '../components/ArtistBio';
|
||||
import { api } from '../api/client';
|
||||
import type { TimelineData, Artist, ArtistDetail, PaintingDetail } from '../types';
|
||||
import './HomePage.css';
|
||||
|
||||
type View =
|
||||
| { type: 'timeline' }
|
||||
| { type: 'gallery'; artistId: number; data: ArtistDetail }
|
||||
| { type: 'painting'; paintingId: number; data: PaintingDetail; returnTo: View }
|
||||
| { type: 'bio'; artistId: number; data: ArtistDetail; returnTo: View };
|
||||
|
||||
export default function HomePage() {
|
||||
const [view, setView] = useState<View>({ type: 'timeline' });
|
||||
const [bounds, setBounds] = useState({ min: -800, max: 2025 });
|
||||
const [viewStart, setViewStart] = useState(-800);
|
||||
const [viewEnd, setViewEnd] = useState(2025);
|
||||
const [timelineData, setTimelineData] = useState<TimelineData>({ eras: [], movements: [] });
|
||||
const [artists, setArtists] = useState<Artist[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
api.getBounds()
|
||||
.then((b) => {
|
||||
const min = b.min_year ?? -800;
|
||||
const max = b.max_year ?? 2025;
|
||||
setBounds({ min, max });
|
||||
setViewStart(min);
|
||||
setViewEnd(max);
|
||||
})
|
||||
.catch(() => {});
|
||||
}, []);
|
||||
|
||||
const loadTimelineData = useCallback(async (start: number, end: number) => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const [timeline, artistList] = await Promise.all([
|
||||
api.getTimeline(start, end),
|
||||
api.getArtists(start, end),
|
||||
]);
|
||||
setTimelineData(timeline);
|
||||
setArtists(artistList);
|
||||
setError(null);
|
||||
} catch {
|
||||
setError('Could not load gallery data. Is the server running?');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
loadTimelineData(viewStart, viewEnd);
|
||||
}, [viewStart, viewEnd, loadTimelineData]);
|
||||
|
||||
const handleViewChange = (start: number, end: number) => {
|
||||
setViewStart(start);
|
||||
setViewEnd(end);
|
||||
};
|
||||
|
||||
const handleArtistClick = async (artistId: number) => {
|
||||
try {
|
||||
const data = await api.getArtist(artistId);
|
||||
setView({ type: 'gallery', artistId, data });
|
||||
} catch {
|
||||
setError('Failed to load artist gallery.');
|
||||
}
|
||||
};
|
||||
|
||||
const handlePaintingClick = async (paintingId: number) => {
|
||||
try {
|
||||
const data = await api.getPainting(paintingId);
|
||||
setView((prev) => ({
|
||||
type: 'painting',
|
||||
paintingId,
|
||||
data,
|
||||
returnTo: prev,
|
||||
}));
|
||||
} catch {
|
||||
setError('Failed to load painting details.');
|
||||
}
|
||||
};
|
||||
|
||||
const handleBioClick = (artistData: ArtistDetail, returnTo: View) => {
|
||||
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 })}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
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}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user