107 lines
3.5 KiB
TypeScript
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}
|
|
/>
|
|
</>
|
|
);
|
|
}
|