/* eslint-disable @next/next/no-img-element */ 'use client' import { type Dispatch, type SetStateAction, useCallback, useEffect, useMemo, useRef, useState } from 'react' import { IconPhoto as Images } from '@tabler/icons-react' import { SearchResultImage } from '@/lib/types' import { Carousel, type CarouselApi, CarouselContent, CarouselItem, CarouselNext, CarouselPrevious } from '@/components/ui/carousel' import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogTrigger } from '@/components/ui/dialog' import { ImageCreditOverlay } from '@/components/image-credit-overlay' interface SearchResultsImageSectionProps { images: SearchResultImage[] query?: string displayMode?: 'preview' | 'full' } type NormalizedImage = { id: string; url: string; description: string } type FilterStatus = 'loading' | 'ready' | 'empty' interface FilteredImagesState { key: string status: FilterStatus images: NormalizedImage[] } const normalizeImages = (images: SearchResultImage[]): NormalizedImage[] => { if (!images || images.length === 0) { return [] } return images.map((image, index) => { if (typeof image === 'string') { return { id: `${index}-${image}`, url: image, description: '' } } const url = image.url ?? '' return { id: `${index}-${url}`, url, description: image.description ?? '' } }) } const useFilteredImages = (images: SearchResultImage[]) => { const normalizedImages = useMemo(() => normalizeImages(images), [images]) const normalizedKey = useMemo( () => normalizedImages.map(image => image.id).join('|'), [normalizedImages] ) const [removedState, setRemovedState] = useState<{ key: string ids: string[] }>({ key: '', ids: [] }) const [state, setState] = useState(() => ({ key: '', status: normalizedImages.length === 0 ? 'empty' : 'loading', images: [] })) useEffect(() => { if (normalizedImages.length === 0 || typeof window === 'undefined') { return } if (state.key === normalizedKey) { return } let cancelled = false const preloadImage = (image: NormalizedImage) => new Promise(resolve => { if (!image.url) { resolve(null) return } const img = new window.Image() img.onload = () => resolve(image) img.onerror = () => resolve(null) img.src = image.url }) Promise.all(normalizedImages.map(preloadImage)).then(results => { if (cancelled) { return } const validImages = results.filter(Boolean) as NormalizedImage[] setState({ key: normalizedKey, status: validImages.length === 0 ? 'empty' : 'ready', images: validImages }) }) return () => { cancelled = true } }, [normalizedImages, normalizedKey, state.key]) const removeImage = useCallback( (id: string) => { setRemovedState(prevState => { const ids = prevState.key === normalizedKey ? prevState.ids : [] return ids.includes(id) ? { key: normalizedKey, ids } : { key: normalizedKey, ids: [...ids, id] } }) }, [normalizedKey] ) const sourceImages = state.key === normalizedKey && state.status !== 'loading' ? state.images : normalizedImages const removedIds = removedState.key === normalizedKey ? removedState.ids : [] const visibleImages = sourceImages.filter( image => !removedIds.includes(image.id) ) const status: FilterStatus = visibleImages.length === 0 ? 'empty' : state.key === normalizedKey && state.status !== 'loading' ? state.status : normalizedImages.length === 0 ? 'empty' : 'loading' const displayImages = status === 'loading' ? normalizedImages : visibleImages return { status, filteredImages: visibleImages, displayImages, removeImage } } const useCarouselMetrics = ({ api, imageCount, selectedIndex, setSelectedIndex }: { api: CarouselApi | undefined imageCount: number selectedIndex: number setSelectedIndex: Dispatch> }) => { const [current, setCurrent] = useState(null) useEffect(() => { if (!api) { return } const handleSelect = () => { setCurrent(api.selectedScrollSnap() + 1) } handleSelect() api.on('select', handleSelect) return () => { api.off('select', handleSelect) } }, [api]) useEffect(() => { setSelectedIndex(prevIndex => { if (imageCount === 0) { return 0 } return Math.min(prevIndex, imageCount - 1) }) }, [imageCount, setSelectedIndex]) useEffect(() => { if (!api || imageCount === 0) { return } const clampedIndex = Math.min(selectedIndex, imageCount - 1) api.scrollTo(clampedIndex, false) }, [api, selectedIndex, imageCount]) const currentValue = imageCount === 0 ? 0 : (current ?? Math.min(selectedIndex + 1, imageCount)) return { current: currentValue } } const cornerClassForIndex = (actualIndex: number, isFullMode: boolean) => { if (!isFullMode) { return 'rounded-lg' } if (actualIndex === 0) return 'rounded-tl-lg' if (actualIndex === 1) return 'rounded-tr-lg' if (actualIndex === 2) return 'rounded-bl-lg' if (actualIndex === 4) return 'rounded-br-lg' return '' } export const SearchResultsImageSection: React.FC< SearchResultsImageSectionProps > = ({ images, query, displayMode = 'preview' }) => { const [api, setApi] = useState() const [selectedIndex, setSelectedIndex] = useState(0) const { status, filteredImages, displayImages, removeImage } = useFilteredImages(images) const filteredCount = filteredImages.length const isLoading = status === 'loading' const { current } = useCarouselMetrics({ api, imageCount: filteredCount, selectedIndex, setSelectedIndex }) if (status === 'empty') { return
No images found
} const handleSelect = (index: number) => { if (!isLoading) { setSelectedIndex(index) } } const renderImageGrid = ( imageSubset: NormalizedImage[], gridClasses: string, startIndex: number = 0, isFullMode: boolean = false ) => (
{imageSubset.map((image, index) => { const actualIndex = startIndex + index const cornerClasses = cornerClassForIndex(actualIndex, isFullMode) if (isLoading || !image.url) { return (
) } return (
handleSelect(actualIndex)} >
{`Image removeImage(image.id)} />
{!isFullMode && index === imageSubset.length - 1 && filteredCount > 1 && (
{filteredCount}
)}
Images {query}
1 }} className="w-full max-h-[70vh]" > {filteredImages.map((img, idx) => (
{`Image removeImage(img.id)} />
))}
{filteredCount > 1 && (
Previous Next
)}
{current} of {filteredCount}
) })}
) if (displayMode === 'full') { const firstRowImages = displayImages.slice(0, 2) const secondRowImages = displayImages.slice(2, 5) return (
{renderImageGrid(firstRowImages, 'grid grid-cols-2 gap-2', 0, true)} {secondRowImages.length > 0 && renderImageGrid(secondRowImages, 'grid grid-cols-3 gap-2', 2, true)}
) } const previewImages = displayImages.slice(0, 4) return renderImageGrid(previewImages, 'grid grid-cols-4 gap-2') }