'use client';
import { Document, Page, pdfjs } from 'react-pdf';
// COMPONENTS
import { useModalControl } from '@components/modals/document-modal/hooks/useModalControl';
import { usePdfViewer } from '@components/modals/document-modal/hooks/usePdfViewer';
import { PdfControls } from './components/PdfControls';
// STYLES
import 'react-pdf/dist/Page/AnnotationLayer.css';
import 'react-pdf/dist/Page/TextLayer.css';
import style from './DocumentModal.module.scss';
// Установите worker
pdfjs.GlobalWorkerOptions.workerSrc = new URL('pdfjs-dist/build/pdf.worker.min.mjs', import.meta.url).toString();
interface DocumentModalProps {
isOpen: boolean;
onClose: () => void;
documentUrl: string;
}
export const DocumentModal = ({ isOpen, onClose, documentUrl }: DocumentModalProps) => {
const dialogRef = useModalControl(isOpen);
const { scale, isLoading, error, numPages, handleZoomIn, handleZoomOut, handleLoadSuccess, handleLoadError } =
usePdfViewer();
const handleDownload = () => {
const link = document.createElement('a');
link.href = documentUrl;
link.download = documentUrl.split('/').pop() || 'document.pdf';
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
};
if (!isOpen) return null;
const handleLinkClick = (href: string) => {
try {
const isAbsolute = /^https?:\/\//i.test(href);
const absoluteUrl = isAbsolute ? href : new URL(href, window.location.origin).toString();
if (/Android|iPhone|iPad/i.test(navigator.userAgent)) {
const newWindow = window.open(absoluteUrl, '_blank', 'noopener,noreferrer');
if (!newWindow || newWindow.closed) {
window.location.href = absoluteUrl;
}
} else {
const a = document.createElement('a');
a.href = absoluteUrl;
a.target = '_blank';
a.rel = 'noopener noreferrer';
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
}
} catch (error) {
console.error('Error opening link:', error);
}
};
return (
<dialog ref={dialogRef} className={style['document-modal']} onClose={onClose}>
<div
className={style['document-modal__overlay']}
onClick={e => {
if (e.target === e.currentTarget) {
onClose();
}
}}
>
<div className={style['document-modal__wrapper']}>
<PdfControls
scale={scale}
onZoomIn={handleZoomIn}
onZoomOut={handleZoomOut}
onDownload={handleDownload}
onClose={onClose}
/>
<div className={style['document-modal__content']}>
{isLoading && <div className={style['document-modal__loader']}>Загрузка документа...</div>}
{error && <div className={style['document-modal__error']}>{error}</div>}
<Document
file={documentUrl}
onLoadSuccess={handleLoadSuccess}
onLoadError={handleLoadError}
loading={null}
noData={null}
externalLinkTarget='_blank'
>
{Array.from(new Array(numPages), (el, index) => (
<Page
key={`page_${index + 1}`}
pageNumber={index + 1}
renderTextLayer={true}
renderAnnotationLayer={true}
scale={scale}
className={style['document-modal__page']}
loading={null}
onClick={e => {
if (e.target instanceof HTMLAnchorElement) {
e.preventDefault();
handleLinkClick(e.target.href);
}
}}
onTouchEnd={e => {
const touch = e.changedTouches[0];
const element = document.elementFromPoint(touch.clientX, touch.clientY);
if (element && element.tagName === 'A') {
e.preventDefault();
handleLinkClick((element as HTMLAnchorElement).href);
}
}}
/>
))}
</Document>
</div>
</div>
</div>
</dialog>
);
};
.document-modal {
padding: 0;
border: none;
width: 100vw;
height: 100vh;
max-width: 100vw !important;
max-height: 100vh !important;
margin: 0;
background: transparent;
position: fixed;
inset: 0;
&::backdrop {
background: rgba(0, 0, 0, 0.5);
backdrop-filter: blur(3px);
}
&__overlay {
display: flex;
justify-content: center;
align-items: center;
width: 100%;
height: 100%;
padding: 16px;
}
&__wrapper {
position: relative;
display: flex;
flex-direction: column;
overflow: hidden;
background: white;
border-radius: 8px;
width: 100%;
height: 100%;
}
&__header {
display: grid;
grid-template-columns: 1fr auto 1fr;
align-items: center;
padding: 15px 20px;
border-bottom: 1px solid #eee;
font-size: clamp(12px, 1.5vw, 16px);
}
&__controls-left {
justify-self: start;
button {
padding: 5px clamp(8px, 1.5vw, 15px);
border-radius: 4px;
border: 1px solid #ddd;
background: white;
cursor: pointer;
&:hover {
background: #f0f0f0;
}
}
}
&__controls-center {
display: flex;
gap: clamp(5px, 1vw, 10px);
align-items: center;
justify-content: center;
button {
padding: 5px 15px;
border-radius: 4px;
border: 1px solid #ddd;
background: white;
cursor: pointer;
&:hover {
background: #f0f0f0;
}
}
span {
text-align: center;
font-weight: 500;
}
}
&__close {
justify-self: end;
width: 24px;
height: 24px;
border: none;
background: transparent;
cursor: pointer;
padding: 0;
position: relative;
&::before,
&::after {
content: '';
position: absolute;
width: 2px;
height: 16px;
background-color: #666;
top: 50%;
left: 50%;
}
&::before {
transform: translate(-50%, -50%) rotate(45deg);
}
&::after {
transform: translate(-50%, -50%) rotate(-45deg);
}
&:hover::before,
&:hover::after {
background-color: #333;
}
}
&__content {
flex: 1;
overflow-y: auto;
padding: 20px;
display: flex;
flex-direction: column;
align-items: center;
gap: 20px;
}
&__page {
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
background: white;
&:not(:last-child) {
margin-bottom: 20px;
}
canvas {
max-width: 100%;
height: auto !important;
}
}
&__loader,
&__error {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: white;
padding: 20px;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
text-align: center;
}
&__error {
color: #dc3545;
}
}
.document-modal__page-container {
position: relative;
}
.react-pdf__Page__annotations.annotationLayer {
a {
cursor: pointer;
touch-action: manipulation;
pointer-events: auto !important;
}
}
.annotationLayer {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
'use client';
import { useRef } from 'react';
import { Document, Page, pdfjs } from 'react-pdf';
// COMPONENTS
import { useModalControl } from '@components/modals/document-modal/hooks/useModalControl';
import { usePdfViewer } from '@components/modals/document-modal/hooks/usePdfViewer';
import { PdfControls } from './components/PdfControls';
// STYLES
import 'react-pdf/dist/Page/AnnotationLayer.css';
import style from './DocumentModal.module.scss';
// Установите worker
pdfjs.GlobalWorkerOptions.workerSrc = new URL('pdfjs-dist/build/pdf.worker.min.mjs', import.meta.url).toString();
interface DocumentModalProps {
isOpen: boolean;
onClose: () => void;
documentUrl: string;
}
export const DocumentModal = ({ isOpen, onClose, documentUrl }: DocumentModalProps) => {
const dialogRef = useModalControl(isOpen);
const documentWrapperRef = useRef<HTMLDivElement>(null);
const {
scale,
isLoading,
error,
numPages,
handleZoomIn,
handleZoomOut,
handleLoadSuccess,
handleLoadError,
handleDownload,
} = usePdfViewer();
if (!isOpen) return null;
return (
<dialog ref={dialogRef} className={style['document-modal']} onClose={onClose}>
<div
className={style['document-modal__overlay']}
onClick={e => {
if (e.target === e.currentTarget) {
onClose();
}
}}
>
<div className={style['document-modal__wrapper']}>
<PdfControls
scale={scale}
onZoomIn={handleZoomIn}
onZoomOut={handleZoomOut}
onDownload={() => handleDownload(documentUrl)}
onClose={onClose}
/>
<div className={style['document-modal__content']} ref={documentWrapperRef}>
{isLoading && <div className={style['document-modal__loader']}>Загрузка документа...</div>}
{error && <div className={style['document-modal__error']}>{error}</div>}
<Document
file={documentUrl}
onLoadSuccess={handleLoadSuccess}
onLoadError={handleLoadError}
loading={null}
noData={null}
externalLinkTarget='_blank'
>
{Array.from(new Array(numPages), (el, index) => (
<Page
key={`page_${index + 1}`}
pageNumber={index + 1}
renderAnnotationLayer={true}
scale={scale}
className={style['document-modal__page']}
loading={null}
width={documentWrapperRef.current?.getBoundingClientRect().width || undefined}
/>
))}
</Document>
</div>
</div>
</div>
</dialog>
);
};