Files
Art-gallery/client/src/components/DebugSearchResultsModal.tsx
T
Danila KhodjaefandCursor df29848d89 Add movement gallery wings with period interiors and expand timeline features.
Movement galleries split large catalogs into chronological wings (~55 works), use era-themed 3D interiors with side-wall windows, wing navigator on the back exit, and front archways between wings. Also adds painting annotations, timeline event guides, portrait hover highlights, and documentation/API updates.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-21 16:54:19 +03:00

155 lines
4.7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useEffect, useState } from 'react';
import { debugImageProxyUrl, type DebugImageSearchManyResult, type DebugImageSearchResultItem } from '../api/client';
import './DebugSearchResultsModal.css';
function formatResolution(width?: number, height?: number): string | null {
if (!width || !height || width <= 0 || height <= 0) return null;
return `${width} × ${height}`;
}
function ResultResolution({
item,
searchUrl,
}: {
item: DebugImageSearchResultItem;
searchUrl: string;
}) {
const initial = formatResolution(item.width, item.height);
const [label, setLabel] = useState<string | null>(initial);
useEffect(() => {
if (initial) {
setLabel(initial);
return;
}
let cancelled = false;
const img = new Image();
img.onload = () => {
if (cancelled) return;
const text = formatResolution(img.naturalWidth, img.naturalHeight);
setLabel(text ?? '—');
};
img.onerror = () => {
if (!cancelled) setLabel('—');
};
img.src = debugImageProxyUrl(item.imageUrl, {
searchUrl,
source: item.source,
});
return () => {
cancelled = true;
img.onload = null;
img.onerror = null;
};
}, [item.imageUrl, item.source, item.width, item.height, searchUrl, initial]);
return (
<span className="debug-search-modal-item-resolution" aria-hidden="true">
{label ?? '…'}
</span>
);
}
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"
>
<span className="debug-search-modal-item-media">
<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>}
</span>
<ResultResolution item={item} searchUrl={data.searchUrl} />
</button>
);
})}
</div>
</>
)}
</div>
</div>
);
}