Update PDF viewer to display user-uploaded documents as slides
Integrates the react-pdf library to render PDF documents page by page as slides, replacing the previous canvas-based rendering approach. Includes necessary setup for the PDF worker and static file serving. Replit-Commit-Author: Agent Replit-Commit-Session-Id: 9a264234-c5d7-4dcc-adf3-a954b149b30d Replit-Commit-Checkpoint-Type: intermediate_checkpoint Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/3df548ff-50ae-432f-9be4-25d34eccc983/9a264234-c5d7-4dcc-adf3-a954b149b30d/VGhYqEL
This commit is contained in:
4
.replit
4
.replit
@ -38,6 +38,10 @@ externalPort = 6000
|
|||||||
localPort = 43349
|
localPort = 43349
|
||||||
externalPort = 3000
|
externalPort = 3000
|
||||||
|
|
||||||
|
[[ports]]
|
||||||
|
localPort = 43469
|
||||||
|
externalPort = 5000
|
||||||
|
|
||||||
[[ports]]
|
[[ports]]
|
||||||
localPort = 43777
|
localPort = 43777
|
||||||
externalPort = 4200
|
externalPort = 4200
|
||||||
|
|||||||
@ -11,9 +11,9 @@ import { useState, useEffect, useRef } from "react";
|
|||||||
import type { MediaOutlet } from "@shared/schema";
|
import type { MediaOutlet } from "@shared/schema";
|
||||||
import Footer from "@/components/Footer";
|
import Footer from "@/components/Footer";
|
||||||
import SearchModal from "@/components/SearchModal";
|
import SearchModal from "@/components/SearchModal";
|
||||||
import * as pdfjsLib from 'pdfjs-dist';
|
import { Document, Page, pdfjs } from 'react-pdf';
|
||||||
|
|
||||||
pdfjsLib.GlobalWorkerOptions.workerSrc = `//cdnjs.cloudflare.com/ajax/libs/pdf.js/${pdfjsLib.version}/pdf.worker.min.js`;
|
pdfjs.GlobalWorkerOptions.workerSrc = '/pdf.worker.min.mjs';
|
||||||
|
|
||||||
const reportContent: Record<string, { htmlPath: string; pdfPath?: string; pptPath?: string; customComponent?: boolean }> = {
|
const reportContent: Record<string, { htmlPath: string; pdfPath?: string; pptPath?: string; customComponent?: boolean }> = {
|
||||||
'chayan-asli': {
|
'chayan-asli': {
|
||||||
@ -32,71 +32,32 @@ const reportContent: Record<string, { htmlPath: string; pdfPath?: string; pptPat
|
|||||||
};
|
};
|
||||||
|
|
||||||
function MohamedSalahSlides() {
|
function MohamedSalahSlides() {
|
||||||
const [currentSlide, setCurrentSlide] = useState(0);
|
const [currentSlide, setCurrentSlide] = useState(1);
|
||||||
const [numPages, setNumPages] = useState(0);
|
const [numPages, setNumPages] = useState(0);
|
||||||
const [pdfDoc, setPdfDoc] = useState<any>(null);
|
|
||||||
const [loading, setLoading] = useState(true);
|
|
||||||
const canvasRef = useRef<HTMLCanvasElement>(null);
|
|
||||||
|
|
||||||
useEffect(() => {
|
const onDocumentLoadSuccess = ({ numPages }: { numPages: number }) => {
|
||||||
const loadPdf = async () => {
|
setNumPages(numPages);
|
||||||
try {
|
setCurrentSlide(1); // Reset to first slide after load
|
||||||
const loadingTask = pdfjsLib.getDocument('/attached_assets/mohamed_salah_pdf_en_1760419721874.pdf');
|
};
|
||||||
const pdf = await loadingTask.promise;
|
|
||||||
setPdfDoc(pdf);
|
|
||||||
setNumPages(pdf.numPages);
|
|
||||||
setLoading(false);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error loading PDF:', error);
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
loadPdf();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!pdfDoc || !canvasRef.current) return;
|
|
||||||
|
|
||||||
const renderPage = async () => {
|
|
||||||
const page = await pdfDoc.getPage(currentSlide + 1);
|
|
||||||
const canvas = canvasRef.current!;
|
|
||||||
const context = canvas.getContext('2d')!;
|
|
||||||
|
|
||||||
const containerWidth = canvas.parentElement?.clientWidth || 800;
|
|
||||||
const viewport = page.getViewport({ scale: 1 });
|
|
||||||
const scale = containerWidth / viewport.width;
|
|
||||||
const scaledViewport = page.getViewport({ scale });
|
|
||||||
|
|
||||||
canvas.width = scaledViewport.width;
|
|
||||||
canvas.height = scaledViewport.height;
|
|
||||||
|
|
||||||
await page.render({
|
|
||||||
canvasContext: context,
|
|
||||||
viewport: scaledViewport
|
|
||||||
}).promise;
|
|
||||||
};
|
|
||||||
|
|
||||||
renderPage();
|
|
||||||
}, [pdfDoc, currentSlide]);
|
|
||||||
|
|
||||||
if (loading) {
|
|
||||||
return (
|
|
||||||
<div className="flex items-center justify-center h-96">
|
|
||||||
<p data-testid="text-loading-slides">Loading slides...</p>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<div className="w-full relative overflow-hidden" style={{ paddingTop: '56.25%' }} data-testid="slide-container-16-9">
|
<div className="w-full relative overflow-hidden" style={{ paddingTop: '56.25%' }} data-testid="slide-container-16-9">
|
||||||
<Card className="absolute inset-0 bg-white">
|
<Card className="absolute inset-0 bg-white">
|
||||||
<CardContent className="p-0 h-full w-full flex items-center justify-center">
|
<CardContent className="p-0 h-full w-full flex items-center justify-center">
|
||||||
<canvas
|
<Document
|
||||||
ref={canvasRef}
|
file="/attached_assets/mohamed_salah_pdf_en_1760419721874.pdf"
|
||||||
className="max-w-full max-h-full object-contain"
|
onLoadSuccess={onDocumentLoadSuccess}
|
||||||
data-testid="canvas-pdf-slide"
|
loading={<p data-testid="text-loading-slides">Loading slides...</p>}
|
||||||
/>
|
>
|
||||||
|
<Page
|
||||||
|
pageNumber={currentSlide}
|
||||||
|
width={1200}
|
||||||
|
renderTextLayer={false}
|
||||||
|
renderAnnotationLayer={false}
|
||||||
|
data-testid="canvas-pdf-slide"
|
||||||
|
/>
|
||||||
|
</Document>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
@ -104,19 +65,19 @@ function MohamedSalahSlides() {
|
|||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
onClick={() => setCurrentSlide(Math.max(0, currentSlide - 1))}
|
onClick={() => setCurrentSlide(Math.max(1, currentSlide - 1))}
|
||||||
disabled={currentSlide === 0}
|
disabled={numPages === 0 || currentSlide === 1}
|
||||||
data-testid="button-previous-slide"
|
data-testid="button-previous-slide"
|
||||||
>
|
>
|
||||||
Previous
|
Previous
|
||||||
</Button>
|
</Button>
|
||||||
<span className="text-sm text-gray-600" data-testid="text-slide-counter">
|
<span className="text-sm text-gray-600" data-testid="text-slide-counter">
|
||||||
Slide {currentSlide + 1} of {numPages}
|
Slide {currentSlide} of {numPages}
|
||||||
</span>
|
</span>
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
onClick={() => setCurrentSlide(Math.min(numPages - 1, currentSlide + 1))}
|
onClick={() => setCurrentSlide(Math.min(numPages || 1, currentSlide + 1))}
|
||||||
disabled={currentSlide === numPages - 1}
|
disabled={numPages === 0 || currentSlide === numPages}
|
||||||
data-testid="button-next-slide"
|
data-testid="button-next-slide"
|
||||||
>
|
>
|
||||||
Next
|
Next
|
||||||
|
|||||||
83
package-lock.json
generated
83
package-lock.json
generated
@ -67,6 +67,7 @@
|
|||||||
"react-dom": "^18.3.1",
|
"react-dom": "^18.3.1",
|
||||||
"react-hook-form": "^7.55.0",
|
"react-hook-form": "^7.55.0",
|
||||||
"react-icons": "^5.4.0",
|
"react-icons": "^5.4.0",
|
||||||
|
"react-pdf": "^10.2.0",
|
||||||
"react-resizable-panels": "^2.1.7",
|
"react-resizable-panels": "^2.1.7",
|
||||||
"recharts": "^2.15.2",
|
"recharts": "^2.15.2",
|
||||||
"tailwind-merge": "^2.6.0",
|
"tailwind-merge": "^2.6.0",
|
||||||
@ -4530,6 +4531,15 @@
|
|||||||
"node": ">= 0.8"
|
"node": ">= 0.8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/dequal": {
|
||||||
|
"version": "2.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz",
|
||||||
|
"integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/destroy": {
|
"node_modules/destroy": {
|
||||||
"version": "1.2.0",
|
"version": "1.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz",
|
||||||
@ -5961,6 +5971,24 @@
|
|||||||
"@jridgewell/sourcemap-codec": "^1.5.0"
|
"@jridgewell/sourcemap-codec": "^1.5.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/make-cancellable-promise": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/make-cancellable-promise/-/make-cancellable-promise-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-3SEQqTpV9oqVsIWqAcmDuaNeo7yBO3tqPtqGRcKkEo0lrzD3wqbKG9mkxO65KoOgXqj+zH2phJ2LiAsdzlogSw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/wojtekmaj/make-cancellable-promise?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/make-event-props": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/make-event-props/-/make-event-props-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-G/hncXrl4Qt7mauJEXSg3AcdYzmpkIITTNl5I+rH9sog5Yw0kK6vseJjCaPfOXqOqQuPUP89Rkhfz5kPS8ijtw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/wojtekmaj/make-event-props?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/media-typer": {
|
"node_modules/media-typer": {
|
||||||
"version": "0.3.0",
|
"version": "0.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
|
||||||
@ -6027,6 +6055,23 @@
|
|||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/merge-refs": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/merge-refs/-/merge-refs-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-3+B21mYK2IqUWnd2EivABLT7ueDhb0b8/dGK8LoFQPrU61YITeCMn14F7y7qZafWNZhUEKb24cJdiT5Wxs3prg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/wojtekmaj/merge-refs?sponsor=1"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/merge2": {
|
"node_modules/merge2": {
|
||||||
"version": "1.4.1",
|
"version": "1.4.1",
|
||||||
"resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
|
"resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
|
||||||
@ -6992,6 +7037,35 @@
|
|||||||
"integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==",
|
"integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/react-pdf": {
|
||||||
|
"version": "10.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-pdf/-/react-pdf-10.2.0.tgz",
|
||||||
|
"integrity": "sha512-zk0DIL31oCh8cuQycM0SJKfwh4Onz0/Nwi6wTOjgtEjWGUY6eM+/vuzvOP3j70qtEULn7m1JtaeGzud1w5fY2Q==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"clsx": "^2.0.0",
|
||||||
|
"dequal": "^2.0.3",
|
||||||
|
"make-cancellable-promise": "^2.0.0",
|
||||||
|
"make-event-props": "^2.0.0",
|
||||||
|
"merge-refs": "^2.0.0",
|
||||||
|
"pdfjs-dist": "5.4.296",
|
||||||
|
"tiny-invariant": "^1.0.0",
|
||||||
|
"warning": "^4.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/wojtekmaj/react-pdf?sponsor=1"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
|
||||||
|
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
|
||||||
|
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/react-refresh": {
|
"node_modules/react-refresh": {
|
||||||
"version": "0.17.0",
|
"version": "0.17.0",
|
||||||
"resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz",
|
"resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz",
|
||||||
@ -8482,6 +8556,15 @@
|
|||||||
"@esbuild/win32-x64": "0.21.5"
|
"@esbuild/win32-x64": "0.21.5"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/warning": {
|
||||||
|
"version": "4.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/warning/-/warning-4.0.3.tgz",
|
||||||
|
"integrity": "sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"loose-envify": "^1.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/which": {
|
"node_modules/which": {
|
||||||
"version": "2.0.2",
|
"version": "2.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
|
||||||
|
|||||||
@ -69,6 +69,7 @@
|
|||||||
"react-dom": "^18.3.1",
|
"react-dom": "^18.3.1",
|
||||||
"react-hook-form": "^7.55.0",
|
"react-hook-form": "^7.55.0",
|
||||||
"react-icons": "^5.4.0",
|
"react-icons": "^5.4.0",
|
||||||
|
"react-pdf": "^10.2.0",
|
||||||
"react-resizable-panels": "^2.1.7",
|
"react-resizable-panels": "^2.1.7",
|
||||||
"recharts": "^2.15.2",
|
"recharts": "^2.15.2",
|
||||||
"tailwind-merge": "^2.6.0",
|
"tailwind-merge": "^2.6.0",
|
||||||
|
|||||||
28
public/pdf.worker.min.mjs
Normal file
28
public/pdf.worker.min.mjs
Normal file
File diff suppressed because one or more lines are too long
@ -54,6 +54,9 @@ app.use((req, res, next) => {
|
|||||||
// Serve attached assets statically
|
// Serve attached assets statically
|
||||||
app.use('/attached_assets', express.static('attached_assets'));
|
app.use('/attached_assets', express.static('attached_assets'));
|
||||||
|
|
||||||
|
// Serve public folder for PDF worker and other static files
|
||||||
|
app.use(express.static('public'));
|
||||||
|
|
||||||
app.use((err: any, _req: Request, res: Response, _next: NextFunction) => {
|
app.use((err: any, _req: Request, res: Response, _next: NextFunction) => {
|
||||||
const status = err.status || err.statusCode || 500;
|
const status = err.status || err.statusCode || 500;
|
||||||
const message = err.message || "Internal Server Error";
|
const message = err.message || "Internal Server Error";
|
||||||
|
|||||||
Reference in New Issue
Block a user