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,149 @@
|
||||
import type { PaintingDetail } from '../types';
|
||||
import { paintingImageUrl } from '../api/client';
|
||||
import './PaintingDetail.css';
|
||||
|
||||
interface Props {
|
||||
data: PaintingDetail;
|
||||
onBack: () => void;
|
||||
onPaintingClick: (paintingId: number) => void;
|
||||
onArtistBio: () => void;
|
||||
}
|
||||
|
||||
function InfluenceCard({
|
||||
inf,
|
||||
onPaintingClick,
|
||||
}: {
|
||||
inf: PaintingDetail['influencedBy'][0];
|
||||
onPaintingClick: (id: number) => void;
|
||||
}) {
|
||||
const aspects = inf.aspects
|
||||
? inf.aspects.split(',').map((a) => a.trim()).filter(Boolean)
|
||||
: [];
|
||||
|
||||
return (
|
||||
<article className="influence-card-expanded">
|
||||
<button
|
||||
type="button"
|
||||
className="influence-image-btn"
|
||||
onClick={() => onPaintingClick(inf.id)}
|
||||
title={`View ${inf.title}`}
|
||||
>
|
||||
<img
|
||||
src={paintingImageUrl({ id: inf.id, image_path: inf.image_path })}
|
||||
alt={inf.title}
|
||||
onError={(e) => {
|
||||
(e.target as HTMLImageElement).src = '/placeholder-art.svg';
|
||||
}}
|
||||
/>
|
||||
</button>
|
||||
|
||||
<div className="influence-body">
|
||||
<button type="button" className="influence-title-btn" onClick={() => onPaintingClick(inf.id)}>
|
||||
<strong>{inf.title}</strong>
|
||||
<span>{inf.artist_name}{inf.year ? `, ${inf.year}` : ''}</span>
|
||||
</button>
|
||||
|
||||
{aspects.length > 0 && (
|
||||
<div className="influence-aspects">
|
||||
{aspects.map((aspect) => (
|
||||
<span key={aspect} className="aspect-tag">{aspect}</span>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{inf.notes && <p className="influence-notes">{inf.notes}</p>}
|
||||
|
||||
{inf.quote && (
|
||||
<blockquote className="influence-quote">
|
||||
<p>“{inf.quote}”</p>
|
||||
{(inf.source_author || inf.source) && (
|
||||
<footer>
|
||||
— {inf.source_author}
|
||||
{inf.source && <cite>, {inf.source}</cite>}
|
||||
</footer>
|
||||
)}
|
||||
</blockquote>
|
||||
)}
|
||||
|
||||
{inf.source_url && (
|
||||
<a
|
||||
className="influence-source-link"
|
||||
href={inf.source_url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
Read source: {inf.source_author || inf.source || 'Reference'}
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
</article>
|
||||
);
|
||||
}
|
||||
|
||||
export default function PaintingDetailView({ data, onBack, onPaintingClick, onArtistBio }: Props) {
|
||||
const { painting, influencedBy, influenced } = data;
|
||||
|
||||
return (
|
||||
<div className="painting-detail">
|
||||
<header className="painting-header">
|
||||
<button className="back-btn" onClick={onBack}>← Back to Gallery</button>
|
||||
<div className="painting-title-block">
|
||||
<h1>{painting.title}</h1>
|
||||
<p className="painting-meta">
|
||||
{painting.artist_name}
|
||||
{painting.year && ` · ${painting.year}`}
|
||||
</p>
|
||||
</div>
|
||||
<button className="bio-btn" onClick={onArtistBio}>
|
||||
About {painting.artist_name}
|
||||
</button>
|
||||
</header>
|
||||
|
||||
<div className="painting-layout">
|
||||
<aside className="influence-panel influence-left">
|
||||
<h3>Influenced By</h3>
|
||||
{influencedBy.length === 0 ? (
|
||||
<p className="no-influences">No documented influences for this work.</p>
|
||||
) : (
|
||||
<div className="influence-list">
|
||||
{influencedBy.map((inf) => (
|
||||
<InfluenceCard key={inf.id} inf={inf} onPaintingClick={onPaintingClick} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</aside>
|
||||
|
||||
<main className="painting-center">
|
||||
<div className="painting-frame-large">
|
||||
<img
|
||||
src={paintingImageUrl(painting)}
|
||||
alt={painting.title}
|
||||
onError={(e) => {
|
||||
(e.target as HTMLImageElement).src = '/placeholder-art.svg';
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{painting.description && (
|
||||
<div className="painting-description">
|
||||
<p>{painting.description}</p>
|
||||
</div>
|
||||
)}
|
||||
</main>
|
||||
|
||||
<aside className="influence-panel influence-right">
|
||||
<h3>Influenced</h3>
|
||||
{influenced.length === 0 ? (
|
||||
<p className="no-influences">No documented works influenced by this painting yet.</p>
|
||||
) : (
|
||||
<div className="influence-list">
|
||||
{influenced.map((inf) => (
|
||||
<InfluenceCard key={inf.id} inf={inf} onPaintingClick={onPaintingClick} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</aside>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user