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:
Danila Khodjaef
2026-06-22 22:32:23 +03:00
co-authored by Cursor
parent b2cae284ac
commit f542c689c9
31 changed files with 396 additions and 30 deletions
+157 -1
View File
@@ -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"