This repository has been archived on 2026-02-15. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
breakpilot-pwa/studio-v2/components/QRCodeUpload.tsx
Benjamin Admin bfdaf63ba9 fix: Restore all files lost during destructive rebase
A previous `git pull --rebase origin main` dropped 177 local commits,
losing 3400+ files across admin-v2, backend, studio-v2, website,
klausur-service, and many other services. The partial restore attempt
(660295e2) only recovered some files.

This commit restores all missing files from pre-rebase ref 98933f5e
while preserving post-rebase additions (night-scheduler, night-mode UI,
NightModeWidget dashboard integration).

Restored features include:
- AI Module Sidebar (FAB), OCR Labeling, OCR Compare
- GPU Dashboard, RAG Pipeline, Magic Help
- Klausur-Korrektur (8 files), Abitur-Archiv (5+ files)
- Companion, Zeugnisse-Crawler, Screen Flow
- Full backend, studio-v2, website, klausur-service
- All compliance SDKs, agent-core, voice-service
- CI/CD configs, documentation, scripts

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-09 09:51:32 +01:00

335 lines
12 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.
'use client'
import { useState, useEffect, useCallback } from 'react'
import { useTheme } from '@/lib/ThemeContext'
interface UploadedFile {
id: string
sessionId: string
name: string
type: string
size: number
uploadedAt: string
dataUrl: string
}
interface QRCodeUploadProps {
sessionId?: string
onClose?: () => void
onFileUploaded?: (file: UploadedFile) => void
onFilesChanged?: (files: UploadedFile[]) => void
className?: string
}
export function QRCodeUpload({
sessionId,
onClose,
onFileUploaded,
onFilesChanged,
className = ''
}: QRCodeUploadProps) {
const { isDark } = useTheme()
const [qrCodeUrl, setQrCodeUrl] = useState<string | null>(null)
const [uploadUrl, setUploadUrl] = useState<string>('')
const [isLoading, setIsLoading] = useState(true)
const [uploadedFiles, setUploadedFiles] = useState<UploadedFile[]>([])
const [isPolling, setIsPolling] = useState(false)
// Format file size
const formatFileSize = (bytes: number): string => {
if (bytes === 0) return '0 B'
const k = 1024
const sizes = ['B', 'KB', 'MB', 'GB']
const i = Math.floor(Math.log(bytes) / Math.log(k))
return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i]
}
// Fetch uploads for this session
const fetchUploads = useCallback(async () => {
if (!sessionId) return
try {
const response = await fetch(`/api/uploads?sessionId=${sessionId}`)
if (response.ok) {
const data = await response.json()
const newFiles = data.uploads || []
// Check if there are new files
if (newFiles.length > uploadedFiles.length) {
const newlyAdded = newFiles.slice(uploadedFiles.length)
newlyAdded.forEach((file: UploadedFile) => {
if (onFileUploaded) {
onFileUploaded(file)
}
})
}
setUploadedFiles(newFiles)
if (onFilesChanged) {
onFilesChanged(newFiles)
}
}
} catch (error) {
console.error('Failed to fetch uploads:', error)
}
}, [sessionId, uploadedFiles.length, onFileUploaded, onFilesChanged])
// Initialize QR code and start polling
useEffect(() => {
// Generate Upload-URL
let baseUrl = typeof window !== 'undefined' ? window.location.origin : ''
// Hostname to IP mapping for local network
const hostnameToIP: Record<string, string> = {
'macmini': '192.168.178.100',
'macmini.local': '192.168.178.100',
}
// Replace known hostnames with IP addresses
Object.entries(hostnameToIP).forEach(([hostname, ip]) => {
if (baseUrl.includes(hostname)) {
baseUrl = baseUrl.replace(hostname, ip)
}
})
const uploadPath = `/upload/${sessionId || 'new'}`
const fullUrl = `${baseUrl}${uploadPath}`
setUploadUrl(fullUrl)
// Generate QR code via external API
const qrApiUrl = `https://api.qrserver.com/v1/create-qr-code/?size=300x300&data=${encodeURIComponent(fullUrl)}`
setQrCodeUrl(qrApiUrl)
setIsLoading(false)
// Initial fetch
fetchUploads()
// Start polling for new uploads every 3 seconds
setIsPolling(true)
const pollInterval = setInterval(() => {
fetchUploads()
}, 3000)
return () => {
clearInterval(pollInterval)
setIsPolling(false)
}
}, [sessionId]) // Note: fetchUploads is intentionally not in deps to avoid re-creating interval
// Separate effect for fetching when uploadedFiles changes
useEffect(() => {
// This is just for the callback effect, actual polling is in the other useEffect
}, [uploadedFiles, onFilesChanged])
const copyToClipboard = async () => {
try {
await navigator.clipboard.writeText(uploadUrl)
alert('Link kopiert!')
} catch (err) {
console.error('Kopieren fehlgeschlagen:', err)
}
}
const deleteUpload = async (id: string) => {
try {
const response = await fetch(`/api/uploads?id=${id}`, { method: 'DELETE' })
if (response.ok) {
const newFiles = uploadedFiles.filter(f => f.id !== id)
setUploadedFiles(newFiles)
if (onFilesChanged) {
onFilesChanged(newFiles)
}
}
} catch (error) {
console.error('Failed to delete upload:', error)
}
}
return (
<div className={`${className}`}>
<div className={`rounded-3xl border p-6 ${
isDark ? 'bg-white/10 border-white/20' : 'bg-white border-slate-200 shadow-lg'
}`}>
{/* Header */}
<div className="flex items-center justify-between mb-6">
<div className="flex items-center gap-3">
<div className={`w-10 h-10 rounded-xl flex items-center justify-center ${
isDark ? 'bg-purple-500/20' : 'bg-purple-100'
}`}>
<span className="text-xl">📱</span>
</div>
<div>
<h3 className={`font-semibold ${isDark ? 'text-white' : 'text-slate-900'}`}>
Mit Mobiltelefon hochladen
</h3>
<p className={`text-sm ${isDark ? 'text-white/50' : 'text-slate-500'}`}>
QR-Code scannen oder Link teilen
</p>
</div>
</div>
{onClose && (
<button
onClick={onClose}
className={`p-2 rounded-lg transition-colors ${
isDark ? 'hover:bg-white/10 text-white/60' : 'hover:bg-slate-100 text-slate-400'
}`}
>
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
)}
</div>
{/* QR Code */}
<div className="flex flex-col items-center">
<div className={`p-4 rounded-2xl ${isDark ? 'bg-white' : 'bg-slate-50'}`}>
{isLoading ? (
<div className="w-[200px] h-[200px] flex items-center justify-center">
<div className="w-8 h-8 border-2 border-purple-500 border-t-transparent rounded-full animate-spin" />
</div>
) : qrCodeUrl ? (
<img
src={qrCodeUrl}
alt="QR Code zum Hochladen"
className="w-[200px] h-[200px]"
/>
) : (
<div className="w-[200px] h-[200px] flex items-center justify-center text-slate-400">
QR-Code nicht verfuegbar
</div>
)}
</div>
<p className={`mt-4 text-center text-sm ${isDark ? 'text-white/60' : 'text-slate-600'}`}>
Scannen Sie diesen Code mit Ihrem Handy,<br />
um Dokumente direkt hochzuladen.
</p>
{/* Polling indicator */}
{isPolling && (
<div className={`mt-2 flex items-center gap-2 text-xs ${isDark ? 'text-white/40' : 'text-slate-400'}`}>
<div className="w-2 h-2 bg-green-500 rounded-full animate-pulse" />
Warte auf Uploads...
</div>
)}
</div>
{/* Uploaded Files List */}
{uploadedFiles.length > 0 && (
<div className={`mt-6 p-4 rounded-xl ${
isDark ? 'bg-green-500/10 border border-green-500/20' : 'bg-green-50 border border-green-200'
}`}>
<div className="flex items-center justify-between mb-3">
<p className={`text-sm font-medium ${isDark ? 'text-green-300' : 'text-green-700'}`}>
{uploadedFiles.length} Datei{uploadedFiles.length !== 1 ? 'en' : ''} empfangen
</p>
</div>
<div className="space-y-2 max-h-40 overflow-y-auto">
{uploadedFiles.map((file) => (
<div
key={file.id}
className={`flex items-center gap-3 p-2 rounded-lg ${
isDark ? 'bg-white/5' : 'bg-white'
}`}
>
<span className="text-lg">
{file.type.startsWith('image/') ? '🖼️' : '📄'}
</span>
<div className="flex-1 min-w-0">
<p className={`text-sm font-medium truncate ${isDark ? 'text-white' : 'text-slate-900'}`}>
{file.name}
</p>
<p className={`text-xs ${isDark ? 'text-white/50' : 'text-slate-500'}`}>
{formatFileSize(file.size)}
</p>
</div>
<button
onClick={() => deleteUpload(file.id)}
className={`p-1 rounded transition-colors ${
isDark ? 'hover:bg-red-500/20 text-red-400' : 'hover:bg-red-100 text-red-500'
}`}
>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
</svg>
</button>
</div>
))}
</div>
</div>
)}
{/* Link teilen */}
<div className="mt-6">
<p className={`text-xs mb-2 ${isDark ? 'text-white/40' : 'text-slate-400'}`}>
Oder Link teilen:
</p>
<div className="flex items-center gap-2">
<input
type="text"
value={uploadUrl}
readOnly
className={`flex-1 px-3 py-2 rounded-xl text-sm border ${
isDark
? 'bg-white/5 border-white/10 text-white/80'
: 'bg-slate-50 border-slate-200 text-slate-700'
}`}
/>
<button
onClick={copyToClipboard}
className={`px-4 py-2 rounded-xl text-sm font-medium transition-colors ${
isDark
? 'bg-white/10 text-white hover:bg-white/20'
: 'bg-slate-200 text-slate-700 hover:bg-slate-300'
}`}
>
Kopieren
</button>
</div>
</div>
{/* Network hint - only show if no files uploaded yet */}
{uploadedFiles.length === 0 && (
<div className={`mt-6 p-4 rounded-xl ${
isDark ? 'bg-amber-500/10 border border-amber-500/20' : 'bg-amber-50 border border-amber-200'
}`}>
<div className="flex items-start gap-3">
<span className="text-lg"></span>
<div>
<p className={`text-sm font-medium ${isDark ? 'text-amber-300' : 'text-amber-900'}`}>
Nur im lokalen Netzwerk
</p>
<p className={`text-xs mt-1 ${isDark ? 'text-amber-300/70' : 'text-amber-700'}`}>
Ihr Mobiltelefon muss mit dem gleichen Netzwerk verbunden sein.
</p>
</div>
</div>
</div>
)}
{/* Tip */}
<div className={`mt-4 p-4 rounded-xl ${
isDark ? 'bg-blue-500/10 border border-blue-500/20' : 'bg-blue-50 border border-blue-200'
}`}>
<div className="flex items-start gap-3">
<span className="text-lg">💡</span>
<div>
<p className={`text-sm font-medium ${isDark ? 'text-blue-300' : 'text-blue-900'}`}>
Tipp: Mehrere Seiten scannen
</p>
<p className={`text-xs mt-1 ${isDark ? 'text-blue-300/70' : 'text-blue-700'}`}>
Sie koennen beliebig viele Fotos hochladen.
</p>
</div>
</div>
</div>
</div>
</div>
)
}
// Export the UploadedFile type for use in other components
export type { UploadedFile }