first commit
This commit is contained in:
105
apps/frontend/components/documents/document-photo-gallery.tsx
Normal file
105
apps/frontend/components/documents/document-photo-gallery.tsx
Normal file
@@ -0,0 +1,105 @@
|
||||
'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 { 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: 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}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user