Files
Benjamin Boenisch 5a31f52310 Initial commit: breakpilot-lehrer - Lehrer KI Platform
Services: Admin-Lehrer, Backend-Lehrer, Studio v2, Website,
Klausur-Service, School-Service, Voice-Service, Geo-Service,
BreakPilot Drive, Agent-Core

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-11 23:47:26 +01:00

222 lines
7.8 KiB
TypeScript

'use client'
import { useState, useRef, useCallback, useEffect } from 'react'
interface DocumentViewerProps {
fileUrl: string
fileType: 'pdf' | 'image'
currentPage: number
totalPages: number
onPageChange: (page: number) => void
children?: React.ReactNode // For annotation overlay
className?: string
}
export function DocumentViewer({
fileUrl,
fileType,
currentPage,
totalPages,
onPageChange,
children,
className = '',
}: DocumentViewerProps) {
const [zoom, setZoom] = useState(1)
const [isDragging, setIsDragging] = useState(false)
const [position, setPosition] = useState({ x: 0, y: 0 })
const [startPos, setStartPos] = useState({ x: 0, y: 0 })
const containerRef = useRef<HTMLDivElement>(null)
const handleZoomIn = () => setZoom((prev) => Math.min(prev + 0.25, 3))
const handleZoomOut = () => setZoom((prev) => Math.max(prev - 0.25, 0.5))
const handleFit = () => {
setZoom(1)
setPosition({ x: 0, y: 0 })
}
const handleMouseDown = (e: React.MouseEvent) => {
if (e.button !== 1 && !e.ctrlKey) return // Middle click or Ctrl+click for pan
setIsDragging(true)
setStartPos({ x: e.clientX - position.x, y: e.clientY - position.y })
}
const handleMouseMove = useCallback(
(e: MouseEvent) => {
if (!isDragging) return
setPosition({
x: e.clientX - startPos.x,
y: e.clientY - startPos.y,
})
},
[isDragging, startPos]
)
const handleMouseUp = useCallback(() => {
setIsDragging(false)
}, [])
useEffect(() => {
if (isDragging) {
window.addEventListener('mousemove', handleMouseMove)
window.addEventListener('mouseup', handleMouseUp)
}
return () => {
window.removeEventListener('mousemove', handleMouseMove)
window.removeEventListener('mouseup', handleMouseUp)
}
}, [isDragging, handleMouseMove, handleMouseUp])
// Keyboard shortcuts
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
if (e.key === '+' || e.key === '=') {
e.preventDefault()
handleZoomIn()
} else if (e.key === '-') {
e.preventDefault()
handleZoomOut()
} else if (e.key === '0') {
e.preventDefault()
handleFit()
} else if (e.key === 'ArrowLeft' && currentPage > 1) {
e.preventDefault()
onPageChange(currentPage - 1)
} else if (e.key === 'ArrowRight' && currentPage < totalPages) {
e.preventDefault()
onPageChange(currentPage + 1)
}
}
window.addEventListener('keydown', handleKeyDown)
return () => window.removeEventListener('keydown', handleKeyDown)
}, [currentPage, totalPages, onPageChange])
return (
<div className={`flex flex-col h-full ${className}`}>
{/* Toolbar */}
<div className="flex items-center justify-between px-4 py-2 bg-white/5 border-b border-white/10">
<div className="flex items-center gap-2">
<button
onClick={handleZoomOut}
className="p-2 rounded-lg hover:bg-white/10 text-white/60 hover:text-white transition-colors"
title="Verkleinern (-)"
>
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0zM13 10H7" />
</svg>
</button>
<span className="text-white/60 text-sm min-w-[60px] text-center">
{Math.round(zoom * 100)}%
</span>
<button
onClick={handleZoomIn}
className="p-2 rounded-lg hover:bg-white/10 text-white/60 hover:text-white transition-colors"
title="Vergroessern (+)"
>
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0zM10 7v3m0 0v3m0-3h3m-3 0H7" />
</svg>
</button>
<button
onClick={handleFit}
className="p-2 rounded-lg hover:bg-white/10 text-white/60 hover:text-white transition-colors"
title="Einpassen (0)"
>
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 8V4m0 0h4M4 4l5 5m11-1V4m0 0h-4m4 0l-5 5M4 16v4m0 0h4m-4 0l5-5m11 5l-5-5m5 5v-4m0 4h-4" />
</svg>
</button>
</div>
{/* Page Navigation */}
{totalPages > 1 && (
<div className="flex items-center gap-2">
<button
onClick={() => onPageChange(currentPage - 1)}
disabled={currentPage <= 1}
className="p-2 rounded-lg hover:bg-white/10 text-white/60 hover:text-white transition-colors disabled:opacity-30"
>
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 19l-7-7 7-7" />
</svg>
</button>
<span className="text-white/60 text-sm">
{currentPage} / {totalPages}
</span>
<button
onClick={() => onPageChange(currentPage + 1)}
disabled={currentPage >= totalPages}
className="p-2 rounded-lg hover:bg-white/10 text-white/60 hover:text-white transition-colors disabled:opacity-30"
>
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
</svg>
</button>
</div>
)}
</div>
{/* Document Area */}
<div
ref={containerRef}
className="flex-1 overflow-hidden bg-slate-800/50 relative"
onMouseDown={handleMouseDown}
style={{ cursor: isDragging ? 'grabbing' : 'default' }}
>
<div
className="absolute inset-0 flex items-center justify-center"
style={{
transform: `translate(${position.x}px, ${position.y}px) scale(${zoom})`,
transformOrigin: 'center center',
transition: isDragging ? 'none' : 'transform 0.2s ease-out',
}}
>
{/* Document Image/PDF */}
<div className="relative">
{fileType === 'image' ? (
<img
src={fileUrl}
alt="Schuelerarbeit"
className="max-w-full max-h-full object-contain shadow-2xl"
draggable={false}
/>
) : (
<iframe
src={`${fileUrl}#page=${currentPage}`}
className="w-[800px] h-[1000px] bg-white shadow-2xl"
title="PDF Dokument"
/>
)}
{/* Annotation Overlay */}
{children && (
<div className="absolute inset-0 pointer-events-none">
{children}
</div>
)}
</div>
</div>
</div>
{/* Page Thumbnails (for multi-page) */}
{totalPages > 1 && (
<div className="flex gap-2 p-3 bg-white/5 border-t border-white/10 overflow-x-auto">
{Array.from({ length: totalPages }, (_, i) => i + 1).map((page) => (
<button
key={page}
onClick={() => onPageChange(page)}
className={`flex-shrink-0 w-12 h-16 rounded-lg border-2 flex items-center justify-center text-sm font-medium transition-all ${
page === currentPage
? 'border-purple-500 bg-purple-500/20 text-white'
: 'border-white/10 bg-white/5 text-white/60 hover:border-white/30'
}`}
>
{page}
</button>
))}
</div>
)}
</div>
)
}