Add debug Remove entry, Show more auto-picker, and update docs.
DELETE /api/paintings/:id removes works and image files with gallery refresh and catalog navigation; Show more opens the search modal on load; documentation updated for migrate schema and debug workflow. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
co-authored by
Cursor
parent
b2cae284ac
commit
f542c689c9
@@ -9,7 +9,7 @@ import CheckupPage from '../pages/CheckupPage';
|
||||
import { api, type FixPaintingImageResult, type FixArtistPortraitResult } from '../api/client';
|
||||
import type { TimelineData, Artist, ArtistDetail, Painting, PaintingDetail, MovementGalleryDetail } from '../types';
|
||||
import { sortArtistPaintingsChronological } from '../utils/paintingUtils';
|
||||
import { readDebugMode, writeDebugMode } from '../utils/debugMode';
|
||||
import { readDebugMode, readDebugShowMore, writeDebugMode, writeDebugShowMore } from '../utils/debugMode';
|
||||
import './HomePage.css';
|
||||
|
||||
type View =
|
||||
@@ -53,6 +53,47 @@ function patchArtistInArtistDetail(detail: ArtistDetail, patch: Partial<Artist>)
|
||||
};
|
||||
}
|
||||
|
||||
function patchReturnToAfterRemove(
|
||||
returnTo: View,
|
||||
freshArtist?: ArtistDetail,
|
||||
freshMovement?: MovementGalleryDetail
|
||||
): View {
|
||||
if (returnTo.type === 'gallery' && freshArtist && returnTo.artistId === freshArtist.artist.id) {
|
||||
return { ...returnTo, data: freshArtist };
|
||||
}
|
||||
if (
|
||||
returnTo.type === 'movement-gallery' &&
|
||||
freshMovement &&
|
||||
returnTo.movementId === freshMovement.movement.id
|
||||
) {
|
||||
return { ...returnTo, data: freshMovement };
|
||||
}
|
||||
if (returnTo.type === 'painting') {
|
||||
return { ...returnTo, returnTo: patchReturnToAfterRemove(returnTo.returnTo, freshArtist, freshMovement) };
|
||||
}
|
||||
if (returnTo.type === 'bio') {
|
||||
const data =
|
||||
freshArtist && returnTo.artistId === freshArtist.artist.id ? freshArtist : returnTo.data;
|
||||
return {
|
||||
...returnTo,
|
||||
data,
|
||||
returnTo: patchReturnToAfterRemove(returnTo.returnTo, freshArtist, freshMovement),
|
||||
};
|
||||
}
|
||||
return returnTo;
|
||||
}
|
||||
|
||||
function catalogNavigateTarget(
|
||||
sorted: Painting[],
|
||||
removedId: number
|
||||
): number | null {
|
||||
const idx = sorted.findIndex((p) => p.id === removedId);
|
||||
if (idx < 0) return null;
|
||||
const remaining = sorted.filter((p) => p.id !== removedId);
|
||||
if (remaining.length === 0) return null;
|
||||
return idx < remaining.length ? remaining[idx].id : remaining[remaining.length - 1].id;
|
||||
}
|
||||
|
||||
export default function HomePage() {
|
||||
const [view, setView] = useState<View>({ type: 'timeline' });
|
||||
const [gallerySession, setGallerySession] = useState<GallerySession | null>(null);
|
||||
@@ -67,6 +108,10 @@ export default function HomePage() {
|
||||
const [imageRevisions, setImageRevisions] = useState<Record<number, number>>({});
|
||||
const [portraitRevisions, setPortraitRevisions] = useState<Record<number, number>>({});
|
||||
const [debugMode, setDebugMode] = useState(readDebugMode);
|
||||
const [debugShowMore, setDebugShowMore] = useState(readDebugShowMore);
|
||||
const [galleryRevision, setGalleryRevision] = useState(0);
|
||||
const viewRef = useRef(view);
|
||||
viewRef.current = view;
|
||||
const [hoveredLifespan, setHoveredLifespan] = useState<{
|
||||
birthYear: number;
|
||||
deathYear: number;
|
||||
@@ -130,6 +175,11 @@ export default function HomePage() {
|
||||
});
|
||||
};
|
||||
|
||||
const setDebugShowMoreEnabled = (enabled: boolean) => {
|
||||
setDebugShowMore(enabled);
|
||||
writeDebugShowMore(enabled);
|
||||
};
|
||||
|
||||
const handlePaintingImageFixed = useCallback(async (paintingId: number, fixResult: FixPaintingImageResult) => {
|
||||
const data = await api.getPainting(paintingId);
|
||||
const patch: Partial<Painting> = {
|
||||
@@ -344,6 +394,95 @@ export default function HomePage() {
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handlePaintingRemoved = useCallback(
|
||||
async (paintingId: number, artistId: number) => {
|
||||
const currentView = viewRef.current;
|
||||
if (currentView.type !== 'painting' || currentView.paintingId !== paintingId) return;
|
||||
|
||||
const sorted = sortArtistPaintingsChronological(detailArtistPaintings);
|
||||
const nextId = catalogNavigateTarget(sorted, paintingId);
|
||||
const inMovementCatalog =
|
||||
gallerySession?.kind === 'movement' || currentView.returnTo.type === 'movement-gallery';
|
||||
|
||||
await api.deletePainting(paintingId);
|
||||
|
||||
const freshArtist = await api.getArtist(artistId);
|
||||
let freshMovement: MovementGalleryDetail | undefined;
|
||||
if (gallerySession?.kind === 'movement') {
|
||||
freshMovement = await api.getMovementGallery(gallerySession.movementId);
|
||||
} else if (currentView.returnTo.type === 'movement-gallery') {
|
||||
freshMovement = await api.getMovementGallery(currentView.returnTo.movementId);
|
||||
}
|
||||
|
||||
const freshCatalog =
|
||||
inMovementCatalog && freshMovement
|
||||
? sortArtistPaintingsChronological(freshMovement.paintings)
|
||||
: sortArtistPaintingsChronological(freshArtist.paintings);
|
||||
|
||||
const removedIdx = sorted.findIndex((p) => p.id === paintingId);
|
||||
const navigateId =
|
||||
nextId && freshCatalog.some((p) => p.id === nextId)
|
||||
? nextId
|
||||
: freshCatalog.length > 0
|
||||
? freshCatalog[Math.min(removedIdx, freshCatalog.length - 1)]?.id ??
|
||||
freshCatalog[0].id
|
||||
: null;
|
||||
|
||||
setDetailArtistPaintings(freshCatalog);
|
||||
|
||||
setGallerySession((session) => {
|
||||
if (!session) return session;
|
||||
if (session.kind === 'artist' && session.artistId === artistId) {
|
||||
return { ...session, data: freshArtist };
|
||||
}
|
||||
if (session.kind === 'movement' && freshMovement) {
|
||||
return { ...session, data: freshMovement };
|
||||
}
|
||||
return session;
|
||||
});
|
||||
|
||||
setImageRevisions((prev) => {
|
||||
const next = { ...prev };
|
||||
delete next[paintingId];
|
||||
return next;
|
||||
});
|
||||
|
||||
setGalleryRevision((v) => v + 1);
|
||||
|
||||
const patchedReturnTo = patchReturnToAfterRemove(
|
||||
currentView.returnTo,
|
||||
freshArtist,
|
||||
freshMovement
|
||||
);
|
||||
detailReturnToRef.current = patchedReturnTo;
|
||||
|
||||
setView((current) => {
|
||||
if (current.type === 'gallery' && current.artistId === artistId) {
|
||||
return { ...current, data: freshArtist };
|
||||
}
|
||||
if (current.type === 'movement-gallery' && freshMovement) {
|
||||
return { ...current, data: freshMovement };
|
||||
}
|
||||
if (current.type !== 'painting' || current.paintingId !== paintingId) {
|
||||
return current;
|
||||
}
|
||||
if (navigateId) return current;
|
||||
return patchedReturnTo;
|
||||
});
|
||||
|
||||
if (navigateId) {
|
||||
const data = await api.getPainting(navigateId);
|
||||
setView({
|
||||
type: 'painting',
|
||||
paintingId: navigateId,
|
||||
data,
|
||||
returnTo: patchedReturnTo,
|
||||
});
|
||||
}
|
||||
},
|
||||
[detailArtistPaintings, gallerySession]
|
||||
);
|
||||
|
||||
const handleBioClick = (artistData: ArtistDetail, returnTo: View) => {
|
||||
setView({ type: 'bio', artistId: artistData.artist.id, data: artistData, returnTo });
|
||||
};
|
||||
@@ -393,6 +532,7 @@ export default function HomePage() {
|
||||
<div className={galleryActive ? undefined : 'gallery-session-suspended'} aria-hidden={!galleryActive}>
|
||||
{gallerySession.kind === 'artist' ? (
|
||||
<VirtualGallery
|
||||
key={`artist-${gallerySession.artistId}-${galleryRevision}`}
|
||||
mode="artist"
|
||||
data={gallerySession.data}
|
||||
imageRevisions={imageRevisions}
|
||||
@@ -410,6 +550,7 @@ export default function HomePage() {
|
||||
/>
|
||||
) : (
|
||||
<VirtualGallery
|
||||
key={`movement-${gallerySession.movementId}-${galleryRevision}`}
|
||||
mode="movement"
|
||||
data={gallerySession.data}
|
||||
imageRevisions={imageRevisions}
|
||||
@@ -424,6 +565,7 @@ export default function HomePage() {
|
||||
{view.type === 'painting' && (
|
||||
<div className="home-overlay">
|
||||
<PaintingDetailView
|
||||
key={view.paintingId}
|
||||
data={view.data}
|
||||
artistPaintings={sortedDetailArtistPaintings}
|
||||
onBack={() => {
|
||||
@@ -460,8 +602,10 @@ export default function HomePage() {
|
||||
}}
|
||||
onInfluenceArtistClick={handleArtistClick}
|
||||
debugMode={debugMode}
|
||||
debugShowMore={debugShowMore}
|
||||
onPaintingImageFixed={handlePaintingImageFixed}
|
||||
onPaintingCheckupFlagsUpdated={handlePaintingCheckupFlagsUpdated}
|
||||
onPaintingRemoved={handlePaintingRemoved}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
@@ -471,6 +615,7 @@ export default function HomePage() {
|
||||
<ArtistBio
|
||||
artist={view.data.artist}
|
||||
debugMode={debugMode}
|
||||
debugShowMore={debugShowMore}
|
||||
portraitRevision={portraitRevisions[view.data.artist.id]}
|
||||
onBack={() => setView(view.returnTo)}
|
||||
onEnterGallery={() =>
|
||||
@@ -501,6 +646,17 @@ export default function HomePage() {
|
||||
>
|
||||
Debug mode{debugMode ? ': ON' : ''}
|
||||
</button>
|
||||
<label
|
||||
className={`debug-show-more-toggle${debugShowMore ? ' debug-show-more-toggle-active' : ''}${!debugMode ? ' debug-show-more-toggle-muted' : ''}`}
|
||||
title="When debug mode is on, open the More search results popup automatically on each painting or artist page"
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={debugShowMore}
|
||||
onChange={(e) => setDebugShowMoreEnabled(e.target.checked)}
|
||||
/>
|
||||
Show more
|
||||
</label>
|
||||
<button
|
||||
type="button"
|
||||
className="checkup-link-btn"
|
||||
|
||||
Reference in New Issue
Block a user