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:
1 parent
33b8ae5a5f
commit
44092d102b
10 files changed
+1313
-23
No files matched your search
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
</>
|
||||
|
||||
Reference in new issue
Block a user