Add public curator notes and U-shaped hall wall hang.
Paintings get editable curator notes with brass plates in the 3D hall, and visit order now uses the far/end wall between left and right. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
co-authored by
Cursor
parent
5ddc3fd7f0
commit
bc8369e373
@@ -21,6 +21,7 @@ interface Props {
|
||||
onCatalogNavigate: (paintingId: number) => void;
|
||||
onArtistBio: () => void;
|
||||
onInfluenceArtistClick?: (artistId: number) => void;
|
||||
isCurator?: boolean;
|
||||
debugMode?: boolean;
|
||||
debugShowMore?: boolean;
|
||||
onPaintingImageFixed?: (
|
||||
@@ -32,6 +33,7 @@ interface Props {
|
||||
flags: { checked: boolean; fixed: boolean }
|
||||
) => void | Promise<void>;
|
||||
onPaintingRemoved?: (paintingId: number, artistId: number) => void | Promise<void>;
|
||||
onCuratorNotesUpdated?: (paintingId: number, curatorNotes: string) => void;
|
||||
}
|
||||
|
||||
function influenceKey(inf: InfluenceLink, index: number): string {
|
||||
@@ -203,15 +205,22 @@ export default function PaintingDetailView({
|
||||
onCatalogNavigate,
|
||||
onArtistBio,
|
||||
onInfluenceArtistClick,
|
||||
isCurator = false,
|
||||
debugMode = false,
|
||||
debugShowMore = false,
|
||||
onPaintingImageFixed,
|
||||
onPaintingCheckupFlagsUpdated,
|
||||
onPaintingRemoved,
|
||||
onCuratorNotesUpdated,
|
||||
}: Props) {
|
||||
const { t } = useTranslation('painting');
|
||||
const { painting, influencedBy, influenced, annotations = [] } = data;
|
||||
const inTour = tourText != null;
|
||||
const [curatorNotes, setCuratorNotes] = useState(painting.curator_notes ?? '');
|
||||
const [editingNotes, setEditingNotes] = useState(false);
|
||||
const [notesDraft, setNotesDraft] = useState(painting.curator_notes ?? '');
|
||||
const [savingNotes, setSavingNotes] = useState(false);
|
||||
const [notesError, setNotesError] = useState<string | null>(null);
|
||||
const [fullscreen, setFullscreen] = useState(false);
|
||||
const [imageVersion, setImageVersion] = useState(0);
|
||||
const [debugSearch, setDebugSearch] = useState<DebugImageSearchResult | null>(null);
|
||||
@@ -257,7 +266,13 @@ export default function PaintingDetailView({
|
||||
setMarkingChecked(false);
|
||||
setApplyingUrl(null);
|
||||
setMoreLoading(false);
|
||||
}, [painting.id]);
|
||||
const notes = painting.curator_notes ?? '';
|
||||
setCuratorNotes(notes);
|
||||
setNotesDraft(notes);
|
||||
setEditingNotes(false);
|
||||
setSavingNotes(false);
|
||||
setNotesError(null);
|
||||
}, [painting.id, painting.curator_notes]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!debugMode) {
|
||||
@@ -291,6 +306,34 @@ export default function PaintingDetailView({
|
||||
};
|
||||
}, [debugMode, uploading, painting.id, painting.title, painting.artist_name]);
|
||||
|
||||
const startEditingNotes = () => {
|
||||
setNotesDraft(curatorNotes);
|
||||
setNotesError(null);
|
||||
setEditingNotes(true);
|
||||
};
|
||||
|
||||
const cancelEditingNotes = () => {
|
||||
setNotesDraft(curatorNotes);
|
||||
setNotesError(null);
|
||||
setEditingNotes(false);
|
||||
};
|
||||
|
||||
const saveCuratorNotes = async () => {
|
||||
setSavingNotes(true);
|
||||
setNotesError(null);
|
||||
try {
|
||||
const result = await api.updatePaintingCuratorNotes(painting.id, notesDraft);
|
||||
setCuratorNotes(result.curatorNotes);
|
||||
setNotesDraft(result.curatorNotes);
|
||||
setEditingNotes(false);
|
||||
onCuratorNotesUpdated?.(painting.id, result.curatorNotes);
|
||||
} catch {
|
||||
setNotesError(t('curatorNotesSaveFailed'));
|
||||
} finally {
|
||||
setSavingNotes(false);
|
||||
}
|
||||
};
|
||||
|
||||
const applyImageUpdate = async (fixResult: FixPaintingImageResult) => {
|
||||
setImageVersion((v) => v + 1);
|
||||
if (onPaintingImageFixed) {
|
||||
@@ -586,6 +629,57 @@ export default function PaintingDetailView({
|
||||
)}
|
||||
</aside>
|
||||
)}
|
||||
{(isCurator || curatorNotes.trim()) && (
|
||||
<aside className="curator-notes-panel" aria-label={t('curatorNotes')}>
|
||||
<div className="curator-notes-header">
|
||||
<h3>{t('curatorNotes')}</h3>
|
||||
{isCurator && !editingNotes && (
|
||||
<button
|
||||
type="button"
|
||||
className="curator-notes-edit-btn"
|
||||
onClick={startEditingNotes}
|
||||
>
|
||||
{t('curatorNotesEdit')}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
{editingNotes ? (
|
||||
<div className="curator-notes-editor">
|
||||
<textarea
|
||||
className="curator-notes-textarea"
|
||||
value={notesDraft}
|
||||
onChange={(e) => setNotesDraft(e.target.value)}
|
||||
rows={6}
|
||||
disabled={savingNotes}
|
||||
aria-label={t('curatorNotes')}
|
||||
/>
|
||||
{notesError && <p className="curator-notes-error">{notesError}</p>}
|
||||
<div className="curator-notes-actions">
|
||||
<button
|
||||
type="button"
|
||||
className="curator-notes-save-btn"
|
||||
onClick={saveCuratorNotes}
|
||||
disabled={savingNotes}
|
||||
>
|
||||
{savingNotes ? t('curatorNotesSaving') : t('curatorNotesSave')}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="curator-notes-cancel-btn"
|
||||
onClick={cancelEditingNotes}
|
||||
disabled={savingNotes}
|
||||
>
|
||||
{t('curatorNotesCancel')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
) : curatorNotes.trim() ? (
|
||||
<p className="curator-notes-body">{curatorNotes}</p>
|
||||
) : (
|
||||
<p className="curator-notes-empty">{t('curatorNotesEmpty')}</p>
|
||||
)}
|
||||
</aside>
|
||||
)}
|
||||
{painting.description && (
|
||||
<div className="painting-description">
|
||||
<p>{painting.description}</p>
|
||||
|
||||
Reference in New Issue
Block a user