Extends the debug panel on painting detail and artist bio with a 20-result search picker, local image upload, and clear-to-empty-frame workflow, plus API routes, artist checkup migration, and documentation. Co-authored-by: Cursor <cursoragent@cursor.com>
102 lines
3.3 KiB
TypeScript
102 lines
3.3 KiB
TypeScript
import { useEffect } from 'react';
|
||
import { debugImageProxyUrl, type DebugImageSearchManyResult, type DebugImageSearchResultItem } from '../api/client';
|
||
import './DebugSearchResultsModal.css';
|
||
|
||
interface Props {
|
||
open: boolean;
|
||
title: string;
|
||
data: DebugImageSearchManyResult | null;
|
||
loading: boolean;
|
||
error: string | null;
|
||
applyingUrl: string | null;
|
||
onClose: () => void;
|
||
onSelect: (item: DebugImageSearchResultItem) => void;
|
||
}
|
||
|
||
export default function DebugSearchResultsModal({
|
||
open,
|
||
title,
|
||
data,
|
||
loading,
|
||
error,
|
||
applyingUrl,
|
||
onClose,
|
||
onSelect,
|
||
}: Props) {
|
||
useEffect(() => {
|
||
if (!open) return;
|
||
|
||
const onKeyDown = (e: KeyboardEvent) => {
|
||
if (e.key === 'Escape') onClose();
|
||
};
|
||
window.addEventListener('keydown', onKeyDown);
|
||
return () => window.removeEventListener('keydown', onKeyDown);
|
||
}, [open, onClose]);
|
||
|
||
if (!open) return null;
|
||
|
||
return (
|
||
<div
|
||
className="debug-search-modal-overlay"
|
||
role="dialog"
|
||
aria-modal="true"
|
||
aria-label={title}
|
||
onClick={onClose}
|
||
>
|
||
<div className="debug-search-modal" onClick={(e) => e.stopPropagation()}>
|
||
<header className="debug-search-modal-header">
|
||
<div>
|
||
<h3>{title}</h3>
|
||
{data?.query && <p className="debug-search-modal-query">{data.query}</p>}
|
||
</div>
|
||
<button type="button" className="debug-search-modal-close" onClick={onClose} aria-label="Close">
|
||
×
|
||
</button>
|
||
</header>
|
||
|
||
{loading && <p className="debug-search-modal-status">Loading results…</p>}
|
||
{error && <p className="debug-search-modal-error">{error}</p>}
|
||
|
||
{!loading && !error && data && data.results.length === 0 && (
|
||
<p className="debug-search-modal-status">No images found.</p>
|
||
)}
|
||
|
||
{!loading && data && data.results.length > 0 && (
|
||
<>
|
||
<p className="debug-search-modal-hint">Click an image to replace the current one.</p>
|
||
<div className="debug-search-modal-grid">
|
||
{data.results.map((item, index) => {
|
||
const busy = applyingUrl === item.imageUrl;
|
||
return (
|
||
<button
|
||
key={`${item.imageUrl}-${index}`}
|
||
type="button"
|
||
className={`debug-search-modal-item${busy ? ' debug-search-modal-item-busy' : ''}`}
|
||
disabled={!!applyingUrl}
|
||
onClick={() => onSelect(item)}
|
||
title="Use this image"
|
||
>
|
||
<img
|
||
src={debugImageProxyUrl(item.thumbUrl || item.imageUrl, {
|
||
searchUrl: data.searchUrl,
|
||
source: item.source,
|
||
})}
|
||
alt={`Result ${index + 1}`}
|
||
loading="lazy"
|
||
onError={(e) => {
|
||
(e.target as HTMLImageElement).src = '/placeholder-art.svg';
|
||
}}
|
||
/>
|
||
<span className="debug-search-modal-item-index">{index + 1}</span>
|
||
{busy && <span className="debug-search-modal-item-busy-label">Saving…</span>}
|
||
</button>
|
||
);
|
||
})}
|
||
</div>
|
||
</>
|
||
)}
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|