Files
IdP/apps/frontend/components/documents/document-photo-gallery.tsx
2026-06-30 19:47:26 +03:00

107 lines
3.5 KiB
TypeScript

'use client';
import * as React from 'react';
import { Loader2, Trash2 } from 'lucide-react';
import { useAuth } from '@/components/id/auth-provider';
import { fetchDocumentPhotoUrl } from '@/lib/api';
import { resolveBrowserMediaUrl } from '@/lib/resilient-media-fetch';
import { DocumentFullscreenViewer, DocumentPhotoThumbnail } from './document-photo-viewer';
interface DocumentPhotoGalleryProps {
userId: string;
token: string | null;
storageKeys: string[];
readOnly?: boolean;
onRemove?: (storageKey: string) => void;
}
export function DocumentPhotoGallery({ userId, token, storageKeys, readOnly, onRemove }: DocumentPhotoGalleryProps) {
const { isPinLocked } = useAuth();
const [urls, setUrls] = React.useState<Record<string, string>>({});
const [loadingKeys, setLoadingKeys] = React.useState<Set<string>>(new Set());
const [viewerOpen, setViewerOpen] = React.useState(false);
const [viewerIndex, setViewerIndex] = React.useState(0);
React.useEffect(() => {
if (!token || storageKeys.length === 0 || isPinLocked) {
setUrls({});
return;
}
let cancelled = false;
setLoadingKeys(new Set(storageKeys));
void Promise.all(
storageKeys.map(async (storageKey) => {
try {
const response = await fetchDocumentPhotoUrl(userId, storageKey, token);
return { storageKey, accessUrl: resolveBrowserMediaUrl(response.accessUrl) };
} catch {
return { storageKey, accessUrl: '' };
}
})
).then((results) => {
if (cancelled) return;
const next: Record<string, string> = {};
for (const item of results) {
if (item.accessUrl) next[item.storageKey] = item.accessUrl;
}
setUrls(next);
setLoadingKeys(new Set());
});
return () => {
cancelled = true;
};
}, [isPinLocked, storageKeys.join('|'), token, userId]);
const resolvedImages = storageKeys
.map((storageKey) => ({ storageKey, url: urls[storageKey] }))
.filter((item) => Boolean(item.url));
if (storageKeys.length === 0) return null;
return (
<>
<div className="grid grid-cols-2 gap-3">
{storageKeys.map((storageKey) => (
<div key={storageKey} className="relative">
<DocumentPhotoThumbnail
url={urls[storageKey]}
loading={loadingKeys.has(storageKey)}
onClick={() => {
const resolvedIndex = resolvedImages.findIndex((item) => item.storageKey === storageKey);
if (resolvedIndex >= 0) {
setViewerIndex(resolvedIndex);
setViewerOpen(true);
}
}}
/>
{!readOnly && onRemove ? (
<button
type="button"
onClick={() => onRemove(storageKey)}
className="absolute right-2 top-2 rounded-full bg-black/60 p-1.5 text-white hover:bg-black/80"
aria-label="Удалить фото"
>
<Trash2 className="h-3.5 w-3.5" />
</button>
) : null}
{loadingKeys.has(storageKey) ? (
<div className="pointer-events-none absolute inset-0 flex items-center justify-center rounded-2xl bg-white/40">
<Loader2 className="h-5 w-5 animate-spin text-[#667085]" />
</div>
) : null}
</div>
))}
</div>
<DocumentFullscreenViewer
open={viewerOpen}
onOpenChange={setViewerOpen}
initialIndex={viewerIndex}
images={resolvedImages}
/>
</>
);
}