diff --git a/package-lock.json b/package-lock.json
index bbbb650..be873bb 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "no-copy-frontend",
- "version": "0.58.0",
+ "version": "0.104.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "no-copy-frontend",
- "version": "0.58.0",
+ "version": "0.104.0",
"dependencies": {
"@a2seven/yoo-checkout": "^1.1.4",
"@react-input/mask": "^2.0.4",
@@ -21,6 +21,7 @@
"next": "^16.0.7",
"next-auth": "5.0.0-beta.30",
"next-intl": "^4.5.8",
+ "pdfjs-dist": "^5.7.284",
"react": "19.2.0",
"react-dom": "19.2.0",
"react-slick": "^0.31.0",
@@ -1129,6 +1130,256 @@
"@jridgewell/sourcemap-codec": "^1.4.14"
}
},
+ "node_modules/@napi-rs/canvas": {
+ "version": "0.1.100",
+ "resolved": "https://registry.npmjs.org/@napi-rs/canvas/-/canvas-0.1.100.tgz",
+ "integrity": "sha512-xglYA6q3XO5P3BNJYxVZ1IV7DLVjp1Py6nwag88YntrS+3vKHyYcMqXVS4ZztJmwz2uGvz1FWhI/4LgbR5uQDA==",
+ "license": "MIT",
+ "optional": true,
+ "workspaces": [
+ "e2e/*"
+ ],
+ "engines": {
+ "node": ">= 10"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/Brooooooklyn"
+ },
+ "optionalDependencies": {
+ "@napi-rs/canvas-android-arm64": "0.1.100",
+ "@napi-rs/canvas-darwin-arm64": "0.1.100",
+ "@napi-rs/canvas-darwin-x64": "0.1.100",
+ "@napi-rs/canvas-linux-arm-gnueabihf": "0.1.100",
+ "@napi-rs/canvas-linux-arm64-gnu": "0.1.100",
+ "@napi-rs/canvas-linux-arm64-musl": "0.1.100",
+ "@napi-rs/canvas-linux-riscv64-gnu": "0.1.100",
+ "@napi-rs/canvas-linux-x64-gnu": "0.1.100",
+ "@napi-rs/canvas-linux-x64-musl": "0.1.100",
+ "@napi-rs/canvas-win32-arm64-msvc": "0.1.100",
+ "@napi-rs/canvas-win32-x64-msvc": "0.1.100"
+ }
+ },
+ "node_modules/@napi-rs/canvas-android-arm64": {
+ "version": "0.1.100",
+ "resolved": "https://registry.npmjs.org/@napi-rs/canvas-android-arm64/-/canvas-android-arm64-0.1.100.tgz",
+ "integrity": "sha512-hjhCKhntPv9+t4ckHymdx0phYNcVW+GKQR6Lzw2zE+pOVjOplSmtx9nNNknTjbEDLcuLZqA1y8ufKg1XfgftzQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">= 10"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/Brooooooklyn"
+ }
+ },
+ "node_modules/@napi-rs/canvas-darwin-arm64": {
+ "version": "0.1.100",
+ "resolved": "https://registry.npmjs.org/@napi-rs/canvas-darwin-arm64/-/canvas-darwin-arm64-0.1.100.tgz",
+ "integrity": "sha512-2PcswRaC7Ly645DGt88///zuFDhJxJYdKAs1uU3mfk1atYkXufgcgLfBpk6Tm12nCQBaNt1wpybuPZ4qOhTo8A==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">= 10"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/Brooooooklyn"
+ }
+ },
+ "node_modules/@napi-rs/canvas-darwin-x64": {
+ "version": "0.1.100",
+ "resolved": "https://registry.npmjs.org/@napi-rs/canvas-darwin-x64/-/canvas-darwin-x64-0.1.100.tgz",
+ "integrity": "sha512-ePNZtj7pNIva/siZMg+HmbeozkIjqUIYdoymH8HaA3qK7LfzFN4WMBM8G6HQ9ZC+H3+Dnn5pqtiXpgLykaPOhw==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">= 10"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/Brooooooklyn"
+ }
+ },
+ "node_modules/@napi-rs/canvas-linux-arm-gnueabihf": {
+ "version": "0.1.100",
+ "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm-gnueabihf/-/canvas-linux-arm-gnueabihf-0.1.100.tgz",
+ "integrity": "sha512-d5cDB48oWFGU8/XPhUOFAlySgb/VAu7D+s8fi55K1Pcfg8aPplHWqMgibhVLU8ky7Pyg/fuiVLz4Nf3JrSTuUA==",
+ "cpu": [
+ "arm"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 10"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/Brooooooklyn"
+ }
+ },
+ "node_modules/@napi-rs/canvas-linux-arm64-gnu": {
+ "version": "0.1.100",
+ "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm64-gnu/-/canvas-linux-arm64-gnu-0.1.100.tgz",
+ "integrity": "sha512-rDxgxRu69RvDlX/bh9o22DxLsGr8EqsNgotL9+RwQE1S0b0cqeatqsw6aW45mukm0B42DIAaAacKaYQ8cqS1nw==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 10"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/Brooooooklyn"
+ }
+ },
+ "node_modules/@napi-rs/canvas-linux-arm64-musl": {
+ "version": "0.1.100",
+ "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm64-musl/-/canvas-linux-arm64-musl-0.1.100.tgz",
+ "integrity": "sha512-K3mDW66N+xT2/V439u1alFANiBUjdEx2gLiNYnCmUsva5jZMxWTjafBYwTzYK+EMFMHrUoabuU+T1BIP5CgbYQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 10"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/Brooooooklyn"
+ }
+ },
+ "node_modules/@napi-rs/canvas-linux-riscv64-gnu": {
+ "version": "0.1.100",
+ "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-riscv64-gnu/-/canvas-linux-riscv64-gnu-0.1.100.tgz",
+ "integrity": "sha512-mooqUBTIsccZpnoQC4NgrC1v6C1vof39etLNMnBwCY+p0gajWJvAHLGQ6g/gGyS5YrpDW+GefSN4+Cvcr08UWw==",
+ "cpu": [
+ "riscv64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 10"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/Brooooooklyn"
+ }
+ },
+ "node_modules/@napi-rs/canvas-linux-x64-gnu": {
+ "version": "0.1.100",
+ "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-x64-gnu/-/canvas-linux-x64-gnu-0.1.100.tgz",
+ "integrity": "sha512-1eCvkDCazm7FFhsT7DfGOdSaHgZVK3bt/dSBl5EWHOWmnz+I7j8tPseJqqD81NF+MH21jKUK4wQSDjN0mdhnTg==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 10"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/Brooooooklyn"
+ }
+ },
+ "node_modules/@napi-rs/canvas-linux-x64-musl": {
+ "version": "0.1.100",
+ "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-x64-musl/-/canvas-linux-x64-musl-0.1.100.tgz",
+ "integrity": "sha512-20arT6lnI19S68qNlii73TSEDbECNgzMz2EpldC1V3mZFuRkeujXkcebRk0LRJe9SEUAooYiLokfMViY8IX7yA==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 10"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/Brooooooklyn"
+ }
+ },
+ "node_modules/@napi-rs/canvas-win32-arm64-msvc": {
+ "version": "0.1.100",
+ "resolved": "https://registry.npmjs.org/@napi-rs/canvas-win32-arm64-msvc/-/canvas-win32-arm64-msvc-0.1.100.tgz",
+ "integrity": "sha512-DZFFT1wIAg37LJw37yhMRFfjATd3vTQzjZ1Yki8u2vhO6Hi5VE6BVaGQ1aaDu7xb4iMErz+9EOwjpS7xcxFeBw==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">= 10"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/Brooooooklyn"
+ }
+ },
+ "node_modules/@napi-rs/canvas-win32-x64-msvc": {
+ "version": "0.1.100",
+ "resolved": "https://registry.npmjs.org/@napi-rs/canvas-win32-x64-msvc/-/canvas-win32-x64-msvc-0.1.100.tgz",
+ "integrity": "sha512-MyT1j3mHC2+Lu4pBi9mKyMJhtP6U7k7EldY7sj/uS5gJA65gTXt8MefJQXLJo5d/vZbuWmfxzkEUNc/urV3pHA==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">= 10"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/Brooooooklyn"
+ }
+ },
"node_modules/@napi-rs/wasm-runtime": {
"version": "0.2.12",
"resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.12.tgz",
@@ -6573,6 +6824,18 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/pdfjs-dist": {
+ "version": "5.7.284",
+ "resolved": "https://registry.npmjs.org/pdfjs-dist/-/pdfjs-dist-5.7.284.tgz",
+ "integrity": "sha512-h4EdYQczmGhbOlqc3PPZwxevn7ApdWPbovAuWXOB/DjIyigSnwfy2oze7c6mRcSr9XgLp3eN3EeL4DyySTPMFw==",
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=22.13.0 || >=24"
+ },
+ "optionalDependencies": {
+ "@napi-rs/canvas": "^0.1.100"
+ }
+ },
"node_modules/picocolors": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
diff --git a/package.json b/package.json
index f16f668..14077ad 100644
--- a/package.json
+++ b/package.json
@@ -22,6 +22,7 @@
"next": "^16.0.7",
"next-auth": "5.0.0-beta.30",
"next-intl": "^4.5.8",
+ "pdfjs-dist": "^5.7.284",
"react": "19.2.0",
"react-dom": "19.2.0",
"react-slick": "^0.31.0",
diff --git a/src/app/[locale]/watch-doc/layout.tsx b/src/app/[locale]/watch-doc/layout.tsx
new file mode 100644
index 0000000..63614ba
--- /dev/null
+++ b/src/app/[locale]/watch-doc/layout.tsx
@@ -0,0 +1,9 @@
+import { PdfProvider } from '@/app/contexts/PdfContext';
+
+export default function RootLayout({ children }: { children: React.ReactNode }) {
+ return (
+
+ {children}
+
+ );
+}
\ No newline at end of file
diff --git a/src/app/[locale]/watch-doc/page.tsx b/src/app/[locale]/watch-doc/page.tsx
index 8dd22e7..6928061 100644
--- a/src/app/[locale]/watch-doc/page.tsx
+++ b/src/app/[locale]/watch-doc/page.tsx
@@ -1,135 +1,90 @@
-'use server'
+'use server';
import Link from 'next/link';
+import PdfViewer from '@/app/components/PdfViewer';
+import { DownloadButton } from '@/app/components/document-viewer/DownloadButton';
+import { CopyLinkButton } from '@/app/components/document-viewer/CopyLinkButton';
interface PageProps {
- searchParams: Promise<{ docid?: string }>;
+ searchParams: Promise<{ docid: string; title?: string }>;
}
-export default async function Page({
- searchParams
-}: PageProps) {
- const { docid } = await searchParams;
+export default async function Page({ searchParams }: PageProps) {
+ const { docid, title } = await searchParams;
+
+ const documentTitle = title || 'FileName';
+ const ownerName = "Name Name Name";
+ const companyName = "companyName";
return (
-
-
+
+
-
+
Защита подтверждена
-
Документ Название документа зарегистрирован и защищён платформой NoCopy.
+
+ Документ {documentTitle} зарегистрирован и защищён платформой NoCopy.
+
-
+
-
-
- Правообладатель
-
-
-
+
+
Правообладатель
+
-
N
+
Н
-
Сидоров Станислав Андреевич
-
ООО "ПРОКОНТРОЛ"
+
{ownerName}
+ {/*
{companyName}
*/}
Зарегистрирован в NoCopy
-
-
- Скачать документ
-
-
-
+ {/*
+
Скачать документ
+
+
- Вы вошли как Сидоров Станислав Андреевич.
+ Вы вошли как {ownerName}.
Скачивание будет зарегистрировано.
-
-
-
+
*/}
-
-
- О NoCopy
-
+
+
+ О NoCopy
-
-
- Предосмотр
-
-
-
-
- документ
-
-
-
-
- 1 / 11
-
-
-
-
+
-
- )
+ );
}
\ No newline at end of file
diff --git a/src/app/actions/trackingActions.ts b/src/app/actions/trackingActions.ts
new file mode 100644
index 0000000..7a82055
--- /dev/null
+++ b/src/app/actions/trackingActions.ts
@@ -0,0 +1,90 @@
+'use server';
+
+import { getSessionData } from '@/app/actions/session';
+import { API_BASE_URL } from '@/app/actions/definitions';
+import { headers } from 'next/headers';
+
+export async function requestFileWithTracking(fileId: string) {
+ const token = await getSessionData('token');
+ const headersList = await headers();
+ const forwardedFor = headersList.get('x-forwarded-for');
+ const realIp = headersList.get('x-real-ip');
+ const ip = forwardedFor?.split(',')[0] || realIp || 'unknown';
+ const userAgent = headersList.get('user-agent') || 'unknown';
+
+ try {
+ const response = await fetch(`${API_BASE_URL}/api/file/link`, {
+ method: 'POST',
+ body: JSON.stringify({
+ file_id: fileId,
+ token: token,
+ ip: ip,
+ user_agent: userAgent
+ }),
+ headers: {
+ 'Content-Type': 'application/json'
+ }
+ });
+
+ if (response.ok) {
+ const contentType = response.headers.get('content-type');
+
+ if (contentType?.includes('application/json')) {
+ const data = await response.json();
+ return data.message_body;
+ } else {
+ const blob = await response.blob();
+ return URL.createObjectURL(blob);
+ }
+ } else {
+ throw new Error(`${response.status}`);
+ }
+ } catch (error) {
+ return null;
+ }
+}
+
+export async function requestFileAsBuffer(fileId: string): Promise
{
+ const token = await getSessionData('token');
+ const headersList = await headers();
+ const forwardedFor = headersList.get('x-forwarded-for');
+ const realIp = headersList.get('x-real-ip');
+ const ip = forwardedFor?.split(',')[0] || realIp || 'unknown';
+ const userAgent = headersList.get('user-agent') || 'unknown';
+
+ try {
+ const response = await fetch(`${API_BASE_URL}/api/file/link`, {
+ method: 'POST',
+ body: JSON.stringify({
+ file_id: fileId,
+ token: token,
+ ip: ip,
+ user_agent: userAgent
+ }),
+ headers: {
+ 'Content-Type': 'application/json'
+ }
+ });
+
+ if (!response.ok) {
+ throw new Error(`${response.status}`);
+ }
+
+ const contentType = response.headers.get('content-type');
+ if (contentType?.includes('application/json')) {
+ const data = await response.json();
+ const fileUrl = data.message_body;
+ const fileResponse = await fetch(fileUrl);
+ if (!fileResponse.ok) {
+ throw new Error(`Failed to fetch file: ${fileResponse.status}`);
+ }
+
+ return await fileResponse.arrayBuffer();
+ }
+ else {
+ return await response.arrayBuffer();
+ }
+ } catch (error) {
+ return null;
+ }
+}
\ No newline at end of file
diff --git a/src/app/components/PdfViewer.tsx b/src/app/components/PdfViewer.tsx
new file mode 100644
index 0000000..c32d4ba
--- /dev/null
+++ b/src/app/components/PdfViewer.tsx
@@ -0,0 +1,248 @@
+'use client';
+
+import { useEffect, useRef, useState } from 'react';
+import { usePdf } from '@/app/contexts/PdfContext';
+
+interface PdfViewerProps {
+ fileId: string;
+ fileName?: string;
+}
+
+export default function PdfViewer({ fileId, fileName = 'document.pdf' }: PdfViewerProps) {
+ const canvasRef = useRef(null);
+ const [currentPage, setCurrentPage] = useState(1);
+ const [totalPages, setTotalPages] = useState(0);
+ const [loading, setLoading] = useState(true);
+ const [error, setError] = useState(null);
+ const [fullscreen, setFullscreen] = useState(false);
+ const containerRef = useRef(null);
+ const pdfDocumentRef = useRef(null);
+ const { setPdfData } = usePdf();
+
+ useEffect(() => {
+ let isMounted = true;
+ let objectUrl: string | null = null;
+
+ const loadAndDisplayPdf = async () => {
+ try {
+ setLoading(true);
+
+ // 1. Получаем данные документа как ArrayBuffer (а не blob URL)
+ const { requestFileAsBuffer } = await import('@/app/actions/trackingActions');
+ const pdfData = await requestFileAsBuffer(fileId);
+
+ if (!pdfData) {
+ throw new Error('Failed to load document');
+ }
+
+ setPdfData(pdfData, fileName);
+
+ if (!isMounted) return;
+
+ const pdfjs = await import('pdfjs-dist');
+
+ pdfjs.GlobalWorkerOptions.workerSrc = new URL(
+ 'pdfjs-dist/build/pdf.worker.mjs',
+ import.meta.url
+ ).toString();
+
+ const loadingTask = pdfjs.getDocument({
+ data: pdfData,
+ useSystemFonts: true,
+ disableFontFace: false,
+ });
+
+ const pdfDocument = await loadingTask.promise;
+
+ if (!isMounted) return;
+
+ pdfDocumentRef.current = pdfDocument;
+ setTotalPages(pdfDocument.numPages);
+ setCurrentPage(1);
+
+ await renderPage(pdfDocument, 1);
+
+ } catch (err) {
+ console.error('Error loading PDF:', err);
+ if (isMounted) {
+ setError('Не удалось загрузить документ');
+ }
+ } finally {
+ if (isMounted) {
+ setLoading(false);
+ }
+ }
+ };
+
+ loadAndDisplayPdf();
+
+ return () => {
+ isMounted = false;
+ if (pdfDocumentRef.current) {
+ pdfDocumentRef.current.destroy();
+ }
+ if (objectUrl) {
+ URL.revokeObjectURL(objectUrl);
+ }
+ };
+ }, [fileId, setPdfData]);
+
+ const renderPage = async (pdfDocument: any, pageNum: number) => {
+ if (!canvasRef.current) return;
+
+ try {
+ const page = await pdfDocument.getPage(pageNum);
+
+ const container = canvasRef.current.parentElement;
+
+ if (!container) return;
+
+ const containerWidth = container.clientWidth || 800;
+
+ // Если ширина контейнера 0, откладываем рендер
+ if (containerWidth === 0) {
+ console.log('Container width is 0, waiting...');
+ setTimeout(() => renderPage(pdfDocument, pageNum), 100);
+ return;
+ }
+
+ const originalViewport = page.getViewport({ scale: 1 });
+ const scale = containerWidth / originalViewport.width;
+ const viewport = page.getViewport({ scale: Math.min(scale, 1.5) });
+
+ const canvas = canvasRef.current;
+ const context = canvas.getContext('2d');
+
+ canvas.height = viewport.height;
+ canvas.width = viewport.width;
+
+ const renderContext = {
+ canvasContext: context!,
+ viewport: viewport,
+ };
+
+ await page.render(renderContext).promise;
+ } catch (err) {
+ console.log('Error rendering page:', err);
+ }
+ };
+
+ useEffect(() => {
+ if (!pdfDocumentRef.current || loading) return;
+
+ const checkContainer = () => {
+ const container = canvasRef.current?.parentElement;
+ if (container && container.clientWidth > 0) {
+ renderPage(pdfDocumentRef.current, currentPage);
+ } else {
+ setTimeout(checkContainer, 100);
+ }
+ };
+
+ checkContainer();
+ }, [pdfDocumentRef.current, loading]);
+
+ const goToPrevPage = async () => {
+ if (pdfDocumentRef.current && currentPage > 1) {
+ const newPage = currentPage - 1;
+ setCurrentPage(newPage);
+ await renderPage(pdfDocumentRef.current, newPage);
+ }
+ };
+
+ const goToNextPage = async () => {
+ if (pdfDocumentRef.current && currentPage < totalPages) {
+ const newPage = currentPage + 1;
+ setCurrentPage(newPage);
+ await renderPage(pdfDocumentRef.current, newPage);
+ }
+ };
+
+ const toggleFullscreen = () => {
+ if (!containerRef.current) return;
+
+ if (!document.fullscreenElement) {
+ containerRef.current.requestFullscreen();
+ setFullscreen(true);
+ } else {
+ document.exitFullscreen();
+ setFullscreen(false);
+ }
+ };
+
+ useEffect(() => {
+ const handleFullscreenChange = () => {
+ setFullscreen(!!document.fullscreenElement);
+ if (pdfDocumentRef.current && !document.fullscreenElement) {
+ setTimeout(() => renderPage(pdfDocumentRef.current, currentPage), 100);
+ }
+ };
+
+ document.addEventListener('fullscreenchange', handleFullscreenChange);
+ return () => document.removeEventListener('fullscreenchange', handleFullscreenChange);
+ }, [currentPage]);
+
+ useEffect(() => {
+ const handleResize = () => {
+ if (pdfDocumentRef.current && !loading) {
+ renderPage(pdfDocumentRef.current, currentPage);
+ }
+ };
+
+ window.addEventListener('resize', handleResize);
+ return () => window.removeEventListener('resize', handleResize);
+ }, [currentPage, loading]);
+
+ if (loading) {
+ return (
+
+
Загрузка документа...
+
+ );
+ }
+
+ if (error) {
+ return (
+
+ );
+ }
+
+ return (
+
+
+
+
+ {totalPages > 0 && (
+
+
+
+
+ {currentPage} / {totalPages}
+
+
+
+
+
+ )}
+
+ );
+}
\ No newline at end of file
diff --git a/src/app/components/document-viewer/CopyLinkButton.tsx b/src/app/components/document-viewer/CopyLinkButton.tsx
new file mode 100644
index 0000000..1dc40a6
--- /dev/null
+++ b/src/app/components/document-viewer/CopyLinkButton.tsx
@@ -0,0 +1,30 @@
+'use client';
+
+import { toast } from 'sonner';
+
+interface CopyLinkButtonProps {
+ className?: string;
+ children?: React.ReactNode;
+}
+
+export function CopyLinkButton({ className = 'btn btn-out', children = 'Скопировать ссылку' }: CopyLinkButtonProps) {
+
+ const copyCurrentPageLink = async () => {
+ try {
+ await navigator.clipboard.writeText(window.location.href);
+ toast.success('Ссылка скопирована');
+ } catch (err) {
+ toast.error('Не удалось скопировать ссылку');
+ }
+ };
+
+ return (
+
+ );
+}
\ No newline at end of file
diff --git a/src/app/components/document-viewer/DownloadButton.tsx b/src/app/components/document-viewer/DownloadButton.tsx
new file mode 100644
index 0000000..9129732
--- /dev/null
+++ b/src/app/components/document-viewer/DownloadButton.tsx
@@ -0,0 +1,13 @@
+'use client';
+
+import { usePdf } from '@/app/contexts/PdfContext';
+
+export function DownloadButton() {
+ const { downloadPdf } = usePdf();
+
+ return (
+
+ );
+}
\ No newline at end of file
diff --git a/src/app/contexts/PdfContext.tsx b/src/app/contexts/PdfContext.tsx
new file mode 100644
index 0000000..298b0b1
--- /dev/null
+++ b/src/app/contexts/PdfContext.tsx
@@ -0,0 +1,55 @@
+'use client';
+
+import { createContext, useContext, useRef, ReactNode } from 'react';
+
+interface PdfContextType {
+ downloadPdf: () => Promise;
+ setPdfData: (data: ArrayBuffer | null, fileName: string) => void;
+}
+
+const PdfContext = createContext(null);
+
+export function PdfProvider({ children }: { children: ReactNode }) {
+ const pdfDataRef = useRef(null);
+ const fileNameRef = useRef('');
+
+ const setPdfData = (data: ArrayBuffer | null, fileName: string) => {
+ pdfDataRef.current = data;
+ fileNameRef.current = fileName;
+ };
+
+ const downloadPdf = async () => {
+ if (!pdfDataRef.current) {
+ console.log('No PDF data available');
+ return;
+ }
+
+ try {
+ const blob = new Blob([pdfDataRef.current], { type: 'application/pdf' });
+ const url = URL.createObjectURL(blob);
+ const link = document.createElement('a');
+ link.href = url;
+ link.download = fileNameRef.current || 'document.pdf';
+ document.body.appendChild(link);
+ link.click();
+ document.body.removeChild(link);
+ URL.revokeObjectURL(url);
+ } catch (err) {
+ console.log('Error downloading PDF:', err);
+ }
+ };
+
+ return (
+
+ {children}
+
+ );
+}
+
+export function usePdf() {
+ const context = useContext(PdfContext);
+ if (!context) {
+ throw new Error('usePdf must be used within PdfProvider');
+ }
+ return context;
+}
\ No newline at end of file
diff --git a/src/app/styles/watch-doc.scss b/src/app/styles/watch-doc.scss
index 083d23f..a83f253 100644
--- a/src/app/styles/watch-doc.scss
+++ b/src/app/styles/watch-doc.scss
@@ -59,6 +59,7 @@
align-items: center;
justify-content: space-between;
margin-top: 8px;
+ gap: 10px;
&__nav {
display: flex;
@@ -106,6 +107,63 @@
}
}
}
+
+ .prev-wrap {
+ min-height: 400px;
+ min-width: 300px;
+ position: relative;
+ display: flex;
+ justify-content: center;
+ }
+
+ .pdf-canvas {
+ display: block;
+ max-width: 100%;
+ height: auto;
+ }
+
+ .pdf-container {
+ width: 100%;
+ overflow-x: auto;
+ }
+
+ .pdf-container:fullscreen {
+ background: #f5f5f5;
+ padding: 20px;
+ gap: 20px;
+
+ .prev-wrap {
+ flex: 1;
+ min-height: auto;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ background: #f5f5f5;
+ }
+
+ .pdf-canvas {
+ max-width: 100%;
+ max-height: calc(100vh - 120px);
+ width: auto;
+ height: auto;
+ object-fit: contain;
+ }
+
+ .document-controls {
+ position: fixed;
+ bottom: 20px;
+ left: 50%;
+ transform: translateX(-50%);
+ background: rgba(255, 255, 255, 0.95);
+ backdrop-filter: blur(10px);
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
+ border-radius: 12px;
+ padding: 0.75rem 1.5rem;
+ z-index: 1000;
+ min-width: 300px;
+ justify-content: center;
+ }
+ }
}
&-page {