239 lines
7.3 KiB
TypeScript
239 lines
7.3 KiB
TypeScript
import { useEffect, useRef, useState } from 'react';
|
||
// @ts-ignore
|
||
import * as pdfjsLib from 'pdfjs-dist';
|
||
import styles from '../ContentPreview.module.css';
|
||
|
||
// Set worker source for PDF.js
|
||
if (typeof window !== 'undefined') {
|
||
// Try to use local worker first, fallback to CDN
|
||
try {
|
||
pdfjsLib.GlobalWorkerOptions.workerSrc = new URL(
|
||
'../../../../node_modules/pdfjs-dist/build/pdf.worker.min.js',
|
||
import.meta.url
|
||
).toString();
|
||
} catch (e) {
|
||
// Fallback to CDN if local worker not available
|
||
pdfjsLib.GlobalWorkerOptions.workerSrc = `//cdnjs.cloudflare.com/ajax/libs/pdf.js/${pdfjsLib.version}/pdf.worker.min.js`;
|
||
}
|
||
}
|
||
|
||
interface PdfJsRendererProps {
|
||
previewUrl: string;
|
||
fileName: string;
|
||
onError: () => void;
|
||
onLoad?: () => void;
|
||
}
|
||
|
||
export function PdfJsRenderer({ previewUrl, fileName: _fileName, onError, onLoad }: PdfJsRendererProps) {
|
||
const canvasRef = useRef<HTMLCanvasElement>(null);
|
||
const containerRef = useRef<HTMLDivElement>(null);
|
||
const [isLoading, setIsLoading] = useState(true);
|
||
const [error, setError] = useState<string | null>(null);
|
||
const [numPages, setNumPages] = useState(0);
|
||
const [currentPage, setCurrentPage] = useState(1);
|
||
const [scale, setScale] = useState(1.5);
|
||
|
||
useEffect(() => {
|
||
let pdfDoc: pdfjsLib.PDFDocumentProxy | null = null;
|
||
let isMounted = true;
|
||
|
||
const loadPdf = async () => {
|
||
try {
|
||
setIsLoading(true);
|
||
setError(null);
|
||
|
||
// Load PDF using fetch (like download)
|
||
const response = await fetch(previewUrl);
|
||
if (!response.ok) {
|
||
throw new Error(`Failed to fetch PDF: ${response.statusText}`);
|
||
}
|
||
|
||
const arrayBuffer = await response.arrayBuffer();
|
||
const loadingTask = pdfjsLib.getDocument({ data: arrayBuffer });
|
||
pdfDoc = await loadingTask.promise;
|
||
|
||
if (!isMounted) return;
|
||
|
||
setNumPages(pdfDoc.numPages);
|
||
setIsLoading(false);
|
||
if (onLoad) {
|
||
onLoad();
|
||
}
|
||
} catch (err) {
|
||
console.error('Error loading PDF with PDF.js:', err);
|
||
if (isMounted) {
|
||
setError(err instanceof Error ? err.message : 'Failed to load PDF');
|
||
setIsLoading(false);
|
||
onError();
|
||
}
|
||
}
|
||
};
|
||
|
||
loadPdf();
|
||
|
||
return () => {
|
||
isMounted = false;
|
||
};
|
||
}, [previewUrl, onLoad, onError]);
|
||
|
||
useEffect(() => {
|
||
if (!canvasRef.current || isLoading || error) return;
|
||
|
||
let isMounted = true;
|
||
|
||
const renderPage = async (pageNum: number) => {
|
||
try {
|
||
// Load PDF again for rendering (could be optimized with caching)
|
||
const response = await fetch(previewUrl);
|
||
const arrayBuffer = await response.arrayBuffer();
|
||
const loadingTask = pdfjsLib.getDocument({ data: arrayBuffer });
|
||
const pdfDoc = await loadingTask.promise;
|
||
|
||
if (!isMounted) return;
|
||
|
||
const page = await pdfDoc.getPage(pageNum);
|
||
const viewport = page.getViewport({ scale });
|
||
|
||
const canvas = canvasRef.current;
|
||
if (!canvas) return;
|
||
|
||
canvas.height = viewport.height;
|
||
canvas.width = viewport.width;
|
||
|
||
const context = canvas.getContext('2d');
|
||
if (!context) return;
|
||
|
||
const renderContext = {
|
||
canvasContext: context,
|
||
viewport: viewport,
|
||
};
|
||
|
||
await page.render(renderContext).promise;
|
||
} catch (err) {
|
||
console.error('Error rendering PDF page:', err);
|
||
if (isMounted) {
|
||
setError(err instanceof Error ? err.message : 'Failed to render PDF page');
|
||
}
|
||
}
|
||
};
|
||
|
||
renderPage(currentPage);
|
||
|
||
return () => {
|
||
isMounted = false;
|
||
};
|
||
}, [previewUrl, currentPage, scale, isLoading, error]);
|
||
|
||
if (error) {
|
||
return (
|
||
<div className={styles.errorContainer}>
|
||
<div className={styles.errorIcon}>⚠️</div>
|
||
<p>Fehler beim Laden der PDF: {error}</p>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
if (isLoading) {
|
||
return (
|
||
<div className={styles.loadingContainer}>
|
||
<div className={styles.spinner}></div>
|
||
<p>PDF wird geladen...</p>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
return (
|
||
<div ref={containerRef} style={{ width: '100%', height: '100%', overflow: 'auto', display: 'flex', flexDirection: 'column' }}>
|
||
{/* Navigation Controls */}
|
||
{numPages > 1 && (
|
||
<div style={{
|
||
padding: '0.75rem 1rem',
|
||
background: 'var(--color-background-secondary, #f3f4f6)',
|
||
borderBottom: '1px solid var(--color-border, #e5e7eb)',
|
||
display: 'flex',
|
||
alignItems: 'center',
|
||
gap: '1rem',
|
||
flexWrap: 'wrap'
|
||
}}>
|
||
<button
|
||
onClick={() => setCurrentPage(prev => Math.max(1, prev - 1))}
|
||
disabled={currentPage === 1}
|
||
style={{
|
||
padding: '0.5rem 1rem',
|
||
background: 'var(--color-primary, #3b82f6)',
|
||
color: 'white',
|
||
border: 'none',
|
||
borderRadius: '4px',
|
||
cursor: currentPage === 1 ? 'not-allowed' : 'pointer',
|
||
opacity: currentPage === 1 ? 0.5 : 1
|
||
}}
|
||
>
|
||
Zurück
|
||
</button>
|
||
<span style={{ fontSize: '0.875rem', color: 'var(--color-text, #1f2937)' }}>
|
||
Seite {currentPage} von {numPages}
|
||
</span>
|
||
<button
|
||
onClick={() => setCurrentPage(prev => Math.min(numPages, prev + 1))}
|
||
disabled={currentPage === numPages}
|
||
style={{
|
||
padding: '0.5rem 1rem',
|
||
background: 'var(--color-primary, #3b82f6)',
|
||
color: 'white',
|
||
border: 'none',
|
||
borderRadius: '4px',
|
||
cursor: currentPage === numPages ? 'not-allowed' : 'pointer',
|
||
opacity: currentPage === numPages ? 0.5 : 1
|
||
}}
|
||
>
|
||
Weiter
|
||
</button>
|
||
<div style={{ marginLeft: 'auto', display: 'flex', gap: '0.5rem', alignItems: 'center' }}>
|
||
<button
|
||
onClick={() => setScale(prev => Math.max(0.5, prev - 0.25))}
|
||
style={{
|
||
padding: '0.25rem 0.75rem',
|
||
background: 'var(--color-background, #ffffff)',
|
||
border: '1px solid var(--color-border, #e5e7eb)',
|
||
borderRadius: '4px',
|
||
cursor: 'pointer',
|
||
fontSize: '0.75rem'
|
||
}}
|
||
>
|
||
-
|
||
</button>
|
||
<span style={{ fontSize: '0.75rem', minWidth: '3rem', textAlign: 'center' }}>
|
||
{Math.round(scale * 100)}%
|
||
</span>
|
||
<button
|
||
onClick={() => setScale(prev => Math.min(3, prev + 0.25))}
|
||
style={{
|
||
padding: '0.25rem 0.75rem',
|
||
background: 'var(--color-background, #ffffff)',
|
||
border: '1px solid var(--color-border, #e5e7eb)',
|
||
borderRadius: '4px',
|
||
cursor: 'pointer',
|
||
fontSize: '0.75rem'
|
||
}}
|
||
>
|
||
+
|
||
</button>
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{/* PDF Canvas */}
|
||
<div style={{ flex: 1, display: 'flex', justifyContent: 'center', alignItems: 'flex-start', padding: '1rem', overflow: 'auto' }}>
|
||
<canvas
|
||
ref={canvasRef}
|
||
style={{
|
||
maxWidth: '100%',
|
||
height: 'auto',
|
||
boxShadow: '0 2px 8px rgba(0,0,0,0.1)',
|
||
background: 'white'
|
||
}}
|
||
/>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|