From a3cbaaa2e741ce80dc8e9d3ba747d92bb75daa9f Mon Sep 17 00:00:00 2001 From: smanylov Date: Fri, 1 May 2026 15:12:00 +0700 Subject: [PATCH] add document viewer --- package-lock.json | 267 +++++++++++++++++- package.json | 1 + src/app/[locale]/watch-doc/layout.tsx | 9 + src/app/[locale]/watch-doc/page.tsx | 135 +++------ src/app/actions/trackingActions.ts | 90 ++++++ src/app/components/PdfViewer.tsx | 248 ++++++++++++++++ .../document-viewer/CopyLinkButton.tsx | 30 ++ .../document-viewer/DownloadButton.tsx | 13 + src/app/contexts/PdfContext.tsx | 55 ++++ src/app/styles/watch-doc.scss | 58 ++++ 10 files changed, 814 insertions(+), 92 deletions(-) create mode 100644 src/app/[locale]/watch-doc/layout.tsx create mode 100644 src/app/actions/trackingActions.ts create mode 100644 src/app/components/PdfViewer.tsx create mode 100644 src/app/components/document-viewer/CopyLinkButton.tsx create mode 100644 src/app/components/document-viewer/DownloadButton.tsx create mode 100644 src/app/contexts/PdfContext.tsx 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 Name
-
+
Защита подтверждена
-
Документ Название документа зарегистрирован и защищён платформой NoCopy.
+
+ Документ {documentTitle} зарегистрирован и защищён платформой NoCopy. +
-
+
-
-
- Правообладатель -
- -
+
+
Правообладатель
+
-
N
+
Н
-
Сидоров Станислав Андреевич
-
ООО "ПРОКОНТРОЛ"
+
{ownerName}
+ {/*
{companyName}
*/}
Зарегистрирован в NoCopy
-
-
- Скачать документ -
- -
+ {/*
+
Скачать документ
+
+
- Вы вошли как Сидоров Станислав Андреевич. + Вы вошли как {ownerName}.
Скачивание будет зарегистрировано.
- -
-
+
*/}
- - - О NoCopy - + + + О NoCopy
-
-
- Предосмотр -
- -
-
- документ -
-
-
- - 1 / 11 - -
- -
+
+
Предпросмотр
+
+
-
- NoCopy - Условия - Конфиденциальность +
+ NoCopy + Условия + Конфиденциальность
- ) + ); } \ 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 ( +
+
{error}
+
+ ); + } + + 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 {