110 lines
4.0 KiB
TypeScript
110 lines
4.0 KiB
TypeScript
'use client';
|
||
|
||
import * as React from 'react';
|
||
import { ChevronLeft, ChevronRight, X, ZoomIn } from 'lucide-react';
|
||
import { cn } from '@/lib/utils';
|
||
|
||
interface DocumentFullscreenViewerProps {
|
||
images: { storageKey: string; url: string }[];
|
||
initialIndex?: number;
|
||
open: boolean;
|
||
onOpenChange: (open: boolean) => void;
|
||
}
|
||
|
||
export function DocumentFullscreenViewer({ images, initialIndex = 0, open, onOpenChange }: DocumentFullscreenViewerProps) {
|
||
const [index, setIndex] = React.useState(initialIndex);
|
||
|
||
React.useEffect(() => {
|
||
if (open) setIndex(initialIndex);
|
||
}, [initialIndex, open]);
|
||
|
||
React.useEffect(() => {
|
||
if (!open) return;
|
||
function onKeyDown(event: KeyboardEvent) {
|
||
if (event.key === 'Escape') onOpenChange(false);
|
||
if (event.key === 'ArrowLeft') setIndex((current) => Math.max(0, current - 1));
|
||
if (event.key === 'ArrowRight') setIndex((current) => Math.min(images.length - 1, current + 1));
|
||
}
|
||
window.addEventListener('keydown', onKeyDown);
|
||
return () => window.removeEventListener('keydown', onKeyDown);
|
||
}, [images.length, onOpenChange, open]);
|
||
|
||
if (!open || images.length === 0) return null;
|
||
|
||
const current = images[index];
|
||
|
||
return (
|
||
<div className="fixed inset-0 z-[300] flex flex-col bg-black/95">
|
||
<div className="flex items-center justify-between px-4 py-3 text-white">
|
||
<span className="text-sm text-white/80">
|
||
{index + 1} / {images.length}
|
||
</span>
|
||
<button type="button" onClick={() => onOpenChange(false)} className="rounded-full p-2 hover:bg-white/10" aria-label="Закрыть">
|
||
<X className="h-5 w-5" />
|
||
</button>
|
||
</div>
|
||
|
||
<div className="relative flex flex-1 items-center justify-center px-4 pb-6">
|
||
{images.length > 1 ? (
|
||
<button
|
||
type="button"
|
||
disabled={index === 0}
|
||
onClick={() => setIndex((value) => Math.max(0, value - 1))}
|
||
className="absolute left-4 z-10 rounded-full bg-white/10 p-2 text-white disabled:opacity-30"
|
||
aria-label="Предыдущее фото"
|
||
>
|
||
<ChevronLeft className="h-6 w-6" />
|
||
</button>
|
||
) : null}
|
||
|
||
{/* eslint-disable-next-line @next/next/no-img-element */}
|
||
<img src={current.url} alt="Фото документа" className="max-h-[calc(100vh-120px)] max-w-full object-contain" />
|
||
|
||
{images.length > 1 ? (
|
||
<button
|
||
type="button"
|
||
disabled={index === images.length - 1}
|
||
onClick={() => setIndex((value) => Math.min(images.length - 1, value + 1))}
|
||
className="absolute right-4 z-10 rounded-full bg-white/10 p-2 text-white disabled:opacity-30"
|
||
aria-label="Следующее фото"
|
||
>
|
||
<ChevronRight className="h-6 w-6" />
|
||
</button>
|
||
) : null}
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
interface DocumentPhotoThumbnailProps {
|
||
url?: string;
|
||
loading?: boolean;
|
||
className?: string;
|
||
onClick?: () => void;
|
||
}
|
||
|
||
export function DocumentPhotoThumbnail({ url, loading, className, onClick }: DocumentPhotoThumbnailProps) {
|
||
return (
|
||
<button
|
||
type="button"
|
||
onClick={onClick}
|
||
className={cn(
|
||
'group relative aspect-[4/3] overflow-hidden rounded-2xl border border-[#eceef4] bg-[#f4f5f8] transition hover:border-[#20212b]',
|
||
className
|
||
)}
|
||
>
|
||
{loading ? <div className="h-full w-full animate-pulse bg-[#eceef4]" /> : null}
|
||
{!loading && url ? (
|
||
<>
|
||
{/* eslint-disable-next-line @next/next/no-img-element */}
|
||
<img src={url} alt="Фото документа" className="h-full w-full object-cover" />
|
||
<div className="absolute inset-0 flex items-center justify-center bg-black/0 transition group-hover:bg-black/20">
|
||
<ZoomIn className="h-6 w-6 text-white opacity-0 transition group-hover:opacity-100" />
|
||
</div>
|
||
</>
|
||
) : null}
|
||
{!loading && !url ? <div className="flex h-full items-center justify-center text-xs text-[#667085]">Нет превью</div> : null}
|
||
</button>
|
||
);
|
||
}
|