frontend_nyla/src/components/ContentPreview/renderers/PdfJsRenderer.tsx
2026-04-11 19:44:52 +02:00

245 lines
7.5 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { useEffect, useRef, useState } from 'react';
// @ts-ignore
import * as pdfjsLib from 'pdfjs-dist';
import styles from '../ContentPreview.module.css';
import { useLanguage } from '../../../providers/language/LanguageContext';
// 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 { t } = useLanguage();
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 : t('PDF konnte nicht geladen werden.'));
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 : t('PDF-Seite konnte nicht gerendert werden.'));
}
}
};
renderPage(currentPage);
return () => {
isMounted = false;
};
}, [previewUrl, currentPage, scale, isLoading, error]);
if (error) {
return (
<div className={styles.errorContainer}>
<div className={styles.errorIcon}></div>
<p>
{t('Fehler beim Laden der PDF:')} {error}
</p>
</div>
);
}
if (isLoading) {
return (
<div className={styles.loadingContainer}>
<div className={styles.spinner}></div>
<p>{t('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>
);
}