fix and update

This commit is contained in:
lendry
2026-07-01 23:27:48 +03:00
parent 2b88e028c6
commit bcdfbc3861
36 changed files with 1504 additions and 320 deletions

View File

@@ -2,19 +2,26 @@
import * as React from 'react';
import { createPortal } from 'react-dom';
import { ChevronLeft, ChevronRight, X, ZoomIn } from 'lucide-react';
import { ChevronLeft, ChevronRight, Download, X, ZoomIn } from 'lucide-react';
import { Button } from '@/components/ui/button';
import { MediaImageSkeleton } from '@/components/ui/media-image-skeleton';
import { isImageAttachment, isPdfAttachment } from '@/lib/document-attachments';
import { cn } from '@/lib/utils';
import type { ResolvedDocumentAttachment } from './document-photo-gallery';
interface DocumentFullscreenViewerProps {
images: { storageKey: string; url: string }[];
interface DocumentAttachmentViewerProps {
attachments: ResolvedDocumentAttachment[];
initialIndex?: number;
open: boolean;
onOpenChange: (open: boolean) => void;
}
export function DocumentFullscreenViewer({ images, initialIndex = 0, open, onOpenChange }: DocumentFullscreenViewerProps) {
export function DocumentAttachmentViewer({
attachments,
initialIndex = 0,
open,
onOpenChange
}: DocumentAttachmentViewerProps) {
const [index, setIndex] = React.useState(initialIndex);
const [mounted, setMounted] = React.useState(false);
@@ -31,11 +38,11 @@ export function DocumentFullscreenViewer({ images, initialIndex = 0, open, onOpe
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));
if (event.key === 'ArrowRight') setIndex((current) => Math.min(attachments.length - 1, current + 1));
}
window.addEventListener('keydown', onKeyDown);
return () => window.removeEventListener('keydown', onKeyDown);
}, [images.length, onOpenChange, open]);
}, [attachments.length, onOpenChange, open]);
React.useEffect(() => {
if (!open) return;
@@ -46,33 +53,48 @@ export function DocumentFullscreenViewer({ images, initialIndex = 0, open, onOpe
};
}, [open]);
if (!open || images.length === 0 || !mounted) return null;
if (!open || attachments.length === 0 || !mounted) return null;
const current = images[index];
const current = attachments[index];
const downloadUrl = `${current.url}${current.url.includes('?') ? '&' : '?'}download=1`;
return createPortal(
<div className="fixed inset-0 z-[250] flex flex-col bg-[#0f1117]/96">
<div className="flex items-center justify-between px-4 py-3 text-white">
<div className="min-w-0">
<p className="truncate text-sm font-medium">Фото документа</p>
<p className="truncate text-sm font-medium">{current.fileName}</p>
<p className="text-xs text-white/60">
{index + 1} из {images.length}
{index + 1} из {attachments.length}
</p>
</div>
<Button
type="button"
size="icon"
variant="ghost"
className="text-white hover:bg-white/10"
aria-label="Закрыть"
onClick={() => onOpenChange(false)}
>
<X className="h-5 w-5" />
</Button>
<div className="flex items-center gap-1">
<Button
type="button"
size="icon"
variant="ghost"
className="text-white hover:bg-white/10"
aria-label="Скачать"
asChild
>
<a href={downloadUrl} target="_blank" rel="noreferrer">
<Download className="h-5 w-5" />
</a>
</Button>
<Button
type="button"
size="icon"
variant="ghost"
className="text-white hover:bg-white/10"
aria-label="Закрыть"
onClick={() => onOpenChange(false)}
>
<X className="h-5 w-5" />
</Button>
</div>
</div>
<div className="relative flex flex-1 items-center justify-center px-3 pb-4">
{images.length > 1 ? (
{attachments.length > 1 ? (
<Button
type="button"
size="icon"
@@ -81,51 +103,73 @@ export function DocumentFullscreenViewer({ images, initialIndex = 0, open, onOpe
'absolute left-3 z-10 h-11 w-11 rounded-full text-white hover:bg-white/10',
index <= 0 && 'pointer-events-none opacity-30'
)}
aria-label="Предыдущее фото"
aria-label="Предыдущий файл"
onClick={() => setIndex((value) => Math.max(0, value - 1))}
>
<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-140px)] max-w-full object-contain"
/>
{isImageAttachment(current.contentType) ? (
// eslint-disable-next-line @next/next/no-img-element
<img
src={current.url}
alt={current.fileName}
className="max-h-[calc(100vh-140px)] max-w-full object-contain"
/>
) : isPdfAttachment(current.contentType) ? (
<iframe
title={current.fileName}
src={current.url}
className="h-[calc(100vh-140px)] w-full max-w-5xl rounded-2xl bg-white"
/>
) : (
<div className="max-w-md rounded-[24px] bg-white/10 p-8 text-center text-white backdrop-blur-sm">
<p className="text-lg font-medium">{current.fileName}</p>
<p className="mt-2 text-sm text-white/70">Предпросмотр недоступен для этого типа файла</p>
<Button className="mt-6 rounded-xl" asChild>
<a href={downloadUrl} target="_blank" rel="noreferrer">
Скачать файл
</a>
</Button>
</div>
)}
{images.length > 1 ? (
{attachments.length > 1 ? (
<Button
type="button"
size="icon"
variant="ghost"
className={cn(
'absolute right-3 z-10 h-11 w-11 rounded-full text-white hover:bg-white/10',
index >= images.length - 1 && 'pointer-events-none opacity-30'
index >= attachments.length - 1 && 'pointer-events-none opacity-30'
)}
aria-label="Следующее фото"
onClick={() => setIndex((value) => Math.min(images.length - 1, value + 1))}
aria-label="Следующий файл"
onClick={() => setIndex((value) => Math.min(attachments.length - 1, value + 1))}
>
<ChevronRight className="h-6 w-6" />
</Button>
) : null}
</div>
{images.length > 1 ? (
{attachments.length > 1 ? (
<div className="flex gap-2 overflow-x-auto px-4 pb-4">
{images.map((image, imageIndex) => (
{attachments.map((attachment, attachmentIndex) => (
<button
key={image.storageKey}
key={attachment.storageKey}
type="button"
className={cn(
'h-14 w-14 shrink-0 overflow-hidden rounded-lg border-2',
imageIndex === index ? 'border-[#3390ec]' : 'border-transparent opacity-70'
'h-14 min-w-[56px] shrink-0 overflow-hidden rounded-lg border-2 px-2 text-xs text-white',
attachmentIndex === index ? 'border-[#3390ec]' : 'border-transparent opacity-70'
)}
onClick={() => setIndex(imageIndex)}
onClick={() => setIndex(attachmentIndex)}
>
{/* eslint-disable-next-line @next/next/no-img-element */}
<img src={image.url} alt="" className="h-full w-full object-cover" />
{isImageAttachment(attachment.contentType) ? (
// eslint-disable-next-line @next/next/no-img-element
<img src={attachment.url} alt="" className="h-full w-full object-cover" />
) : (
<span className="flex h-full items-center justify-center">{attachment.fileName.slice(0, 8)}</span>
)}
</button>
))}
</div>
@@ -135,6 +179,27 @@ export function DocumentFullscreenViewer({ images, initialIndex = 0, open, onOpe
);
}
/** @deprecated Используйте DocumentAttachmentViewer */
export function DocumentFullscreenViewer({
images,
initialIndex = 0,
open,
onOpenChange
}: {
images: { storageKey: string; url: string }[];
initialIndex?: number;
open: boolean;
onOpenChange: (open: boolean) => void;
}) {
const attachments: ResolvedDocumentAttachment[] = images.map((image) => ({
storageKey: image.storageKey,
url: image.url,
contentType: 'image/jpeg',
fileName: 'Фото документа'
}));
return <DocumentAttachmentViewer attachments={attachments} initialIndex={initialIndex} open={open} onOpenChange={onOpenChange} />;
}
interface DocumentPhotoThumbnailProps {
url?: string;
loading?: boolean;
@@ -156,7 +221,7 @@ export function DocumentPhotoThumbnail({ url, loading, className, onClick }: Doc
{!loading && url ? (
<>
{/* eslint-disable-next-line @next/next/no-img-element */}
<img src={url} alt=ото документа" className="h-full w-full object-cover" />
<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>