Add Tree of Art start page with fixed lineage geometry and shareable layout links.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Danila KhodjaefandCursor committed 2026-08-13 12:44:08 +03:00
1 parent 33b8ae5a5f
commit 44092d102b
10 files changed
+1313 -23

No files matched your search

+14
View File
@@ -44,6 +44,9 @@
top: 16px;
left: 16px;
z-index: 120;
display: flex;
gap: 8px;
align-items: center;
}
.site-layout-link {
@@ -63,6 +66,17 @@
color: #f5e6c8;
}
/* Link to the alternative tree start page — the one worth noticing. */
.site-layout-link-feature {
border-color: rgba(201, 169, 110, 0.7);
background: rgba(201, 169, 110, 0.16);
color: #f5e6c8;
}
.site-layout-link-feature:hover {
background: rgba(201, 169, 110, 0.3);
}
.gallery-session-suspended {
position: fixed;
inset: 0;
+94 -20
View File
@@ -5,6 +5,7 @@ import TimelineEventGuides from '../components/TimelineEventGuides';
import MovementBands from '../components/MovementBands';
import VerticalTimeline from '../components/VerticalTimeline';
import VerticalMovementBands from '../components/VerticalMovementBands';
import MovementTree from '../components/MovementTree';
import VirtualGallery from '../components/VirtualGallery';
import PaintingDetailView from '../components/PaintingDetail';
import ArtistBio from '../components/ArtistBio';
@@ -50,6 +51,7 @@ import './HomePage.css';
type View =
| { type: 'timeline' }
| { type: 'timeline-vertical' }
| { type: 'timeline-tree' }
| { type: 'checkup' }
| { type: 'translations' }
| { type: 'influences' }
@@ -157,6 +159,37 @@ function catalogNavigateTarget(
return idx < remaining.length ? remaining[idx].id : remaining[remaining.length - 1].id;
}
/** Shareable timeline layout via `?layout=classic|vertical|tree` (classic may omit the param). */
type TimelineLayoutId = 'classic' | 'vertical' | 'tree';
function parseTimelineLayoutParam(raw: string | null): TimelineLayoutId {
if (raw === 'vertical' || raw === 'tree') return raw;
if (raw === 'classic' || raw === 'horizontal') return 'classic';
return 'classic';
}
function timelineViewFromLayout(layout: TimelineLayoutId): View {
if (layout === 'vertical') return { type: 'timeline-vertical' };
if (layout === 'tree') return { type: 'timeline-tree' };
return { type: 'timeline' };
}
function writeTimelineLayoutParam(layout: TimelineLayoutId) {
const url = new URL(window.location.href);
if (layout === 'classic') url.searchParams.delete('layout');
else url.searchParams.set('layout', layout);
const next = `${url.pathname}${url.search}${url.hash}`;
const current = `${window.location.pathname}${window.location.search}${window.location.hash}`;
if (next !== current) window.history.replaceState(null, '', next);
}
function readInitialTimelineView(): View {
if (typeof window === 'undefined') return { type: 'timeline' };
return timelineViewFromLayout(
parseTimelineLayoutParam(new URLSearchParams(window.location.search).get('layout'))
);
}
export default function HomePage() {
const { t } = useTranslation('home');
const { isCurator, isAdmin, username, login, logout, can } = useAuth();
@@ -167,7 +200,7 @@ export default function HomePage() {
const canInfluences = can('influences');
const canTours = can('tours');
const canUsers = can('users');
const [view, setView] = useState<View>({ type: 'timeline' });
const [view, setView] = useState<View>(readInitialTimelineView);
const [gallerySession, setGallerySession] = useState<GallerySession | null>(null);
const [bounds, setBounds] = useState({ min: -800, max: 2025 });
const [viewStart, setViewStart] = useState(-800);
@@ -210,7 +243,11 @@ export default function HomePage() {
setGallerySession({ kind: 'movement', movementId: view.movementId, data: view.data });
} else if (view.type === 'tour-gallery') {
setGallerySession({ kind: 'tour', tourId: view.tourId, data: view.data });
} else if (view.type === 'timeline' || view.type === 'timeline-vertical') {
} else if (
view.type === 'timeline' ||
view.type === 'timeline-vertical' ||
view.type === 'timeline-tree'
) {
setGallerySession(null);
}
}, [view]);
@@ -271,19 +308,33 @@ export default function HomePage() {
setViewStart(bounds.min);
setViewEnd(bounds.max);
setGalleryRevision((revision) => revision + 1);
writeTimelineLayoutParam('classic');
setView({ type: 'timeline' });
}, [bounds.min, bounds.max]);
const openHorizontalTimeline = () => {
writeTimelineLayoutParam('classic');
setView({ type: 'timeline' });
};
const openVerticalTimeline = () => {
writeTimelineLayoutParam('vertical');
setView({ type: 'timeline-vertical' });
};
const isTimelineHome = view.type === 'timeline' || view.type === 'timeline-vertical';
const openTreeTimeline = () => {
writeTimelineLayoutParam('tree');
setView({ type: 'timeline-tree' });
};
const isTimelineHome =
view.type === 'timeline' ||
view.type === 'timeline-vertical' ||
view.type === 'timeline-tree';
const isVerticalTimeline = view.type === 'timeline-vertical';
const isTreeTimeline = view.type === 'timeline-tree';
/** Both alternative layouts run the year axis bottom → top beside the chart. */
const isVerticalLayout = isVerticalTimeline || isTreeTimeline;
const toggleDebugMode = () => {
setDebugMode((prev) => {
@@ -1222,16 +1273,17 @@ export default function HomePage() {
<div className="home-page">
<header className="site-header">
<div className="site-layout-switch">
{isVerticalTimeline ? (
{!isTreeTimeline && (
<button
type="button"
className="site-layout-link"
onClick={openHorizontalTimeline}
title="Switch to classic left-to-right timeline"
className="site-layout-link site-layout-link-feature"
onClick={openTreeTimeline}
title="Open the alternative start page: bottom-up timeline with movements drawn as a growing tree"
>
{t('layoutHorizontal')}
{t('layoutTree')}
</button>
) : (
)}
{!isVerticalTimeline && (
<button
type="button"
className="site-layout-link"
@@ -1241,6 +1293,16 @@ export default function HomePage() {
{t('layoutVertical')}
</button>
)}
{!(view.type === 'timeline') && (
<button
type="button"
className="site-layout-link"
onClick={openHorizontalTimeline}
title="Switch to classic left-to-right timeline"
>
{t('layoutHorizontal')}
</button>
)}
</div>
<div className="site-dev-tools">
{isCurator ? (
@@ -1369,7 +1431,7 @@ export default function HomePage() {
/>
</header>
<div className={`home-timeline-stack${isVerticalTimeline ? ' home-timeline-stack-vertical' : ''}`}>
<div className={`home-timeline-stack${isVerticalLayout ? ' home-timeline-stack-vertical' : ''}`}>
{loading && (
<GalleryLoadingMarker overlay message="Loading art history…" />
)}
@@ -1380,7 +1442,7 @@ export default function HomePage() {
<GalleryLoadingMarker banner message="Loading portraits…" />
)}
{isVerticalTimeline ? (
{isVerticalLayout ? (
<>
<VerticalTimeline
eras={timelineData.eras}
@@ -1394,15 +1456,27 @@ export default function HomePage() {
{error && <div className="error-banner">{error}</div>}
{!loading && (
<div className="home-movements-section-vertical">
<VerticalMovementBands
movements={timelineData.movements}
viewStart={viewStart}
viewEnd={viewEnd}
absoluteMin={bounds.min}
absoluteMax={bounds.max}
onViewChange={handleViewChange}
onMovementClick={handleMovementClick}
/>
{isTreeTimeline ? (
<MovementTree
movements={timelineData.movements}
viewStart={viewStart}
viewEnd={viewEnd}
absoluteMin={bounds.min}
absoluteMax={bounds.max}
onViewChange={handleViewChange}
onMovementClick={handleMovementClick}
/>
) : (
<VerticalMovementBands
movements={timelineData.movements}
viewStart={viewStart}
viewEnd={viewEnd}
absoluteMin={bounds.min}
absoluteMax={bounds.max}
onViewChange={handleViewChange}
onMovementClick={handleMovementClick}
/>
)}
</div>
)}
</>