'use client'; import { useState, useEffect, useCallback, useRef } from 'react'; import { useModalDialog } from '@/hooks/useModalDialog'; import { IconCheck, IconFolder, IconImage, IconStar } from './Icons'; import { useScrollLock } from './useScrollLock'; interface AssetInfo { id: string; originalFileName: string; fileCreatedAt: string; isFavorite: boolean; } interface Props { onSelect: (assetId: string) => void; onClose: () => void; currentAssetIds?: string[]; albumId?: string; title?: string; } type Tab = 'album' | 'favorites' | 'all'; export default function AssetPicker({ onSelect, onClose, currentAssetIds = [], albumId, title, }: Props) { useScrollLock(true); const cardRef = useModalDialog(onClose); const [tab, setTab] = useState(albumId ? 'album' : 'favorites'); const [assets, setAssets] = useState([]); const [loading, setLoading] = useState(true); const [loadingMore, setLoadingMore] = useState(false); const [page, setPage] = useState(1); const [hasMore, setHasMore] = useState(false); const [uuidInput, setUuidInput] = useState(''); const [uuidError, setUuidError] = useState(''); const listRef = useRef(null); const currentIds = new Set(currentAssetIds); const loadAssets = useCallback( async (pageNum: number, append: boolean = false) => { if (pageNum === 1) setLoading(true); else setLoadingMore(true); try { let res: Response; if (tab === 'album' && albumId) { res = await fetch(`/api/admin/albums/${albumId}/assets`); } else { const params = new URLSearchParams({ page: pageNum.toString(), favorites: tab === 'favorites' ? 'true' : 'false', }); res = await fetch(`/api/admin/assets?${params}`); } if (res.ok) { const data = await res.json(); setAssets((prev) => (append ? [...prev, ...data.assets] : data.assets)); setHasMore(data.nextPage !== null); setPage(pageNum); } } catch (err) { console.error('Failed to load assets:', err); } finally { setLoading(false); setLoadingMore(false); } }, [tab, albumId], ); useEffect(() => { setAssets([]); setPage(1); setHasMore(false); loadAssets(1); }, [tab, loadAssets]); // Infinite scroll function handleScroll() { if (!listRef.current || loadingMore || !hasMore) return; const { scrollTop, scrollHeight, clientHeight } = listRef.current; if (scrollHeight - scrollTop - clientHeight < 200) { loadAssets(page + 1, true); } } function handleUuidSubmit(e: React.FormEvent) { e.preventDefault(); const uuid = uuidInput.trim(); const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i; if (!uuidRegex.test(uuid)) { setUuidError('Invalid UUID format'); return; } setUuidError(''); onSelect(uuid); } function formatDate(dateStr: string): string { try { return new Date(dateStr).toLocaleDateString(undefined, { year: 'numeric', month: 'short', day: 'numeric', }); } catch { return ''; } } return (
e.stopPropagation()} >

{title || 'Select Hero Image'}

{/* Tabs */}
{albumId && ( )}
{/* Asset Grid */}
{loading ? (
) : assets.length === 0 ? (

{tab === 'favorites' ? 'No favorite photos found. Star photos in Immich first, or switch to "All Photos".' : 'No photos found.'}

) : ( <> {assets.map((asset) => { const isUsed = currentIds.has(asset.id); return (
!isUsed && onSelect(asset.id)} title={`${asset.originalFileName}\n${formatDate(asset.fileCreatedAt)}`} > {asset.originalFileName} {isUsed && (
)} {asset.isFavorite && !isUsed && (
)}
); })} {loadingMore && (
)} )}
{/* UUID Fallback */}
{ setUuidInput(e.target.value); setUuidError(''); }} placeholder="Or paste asset UUID directly..." className="asset-picker-uuid-input" />
{uuidError && {uuidError}}
); }