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>
579 lines
24 KiB
TypeScript
579 lines
24 KiB
TypeScript
'use client'
|
|
|
|
import { useState, useEffect, useCallback, useRef } from 'react'
|
|
import { useRouter, useParams } from 'next/navigation'
|
|
import { useTheme } from '@/lib/ThemeContext'
|
|
import { Sidebar } from '@/components/Sidebar'
|
|
import { ThemeToggle } from '@/components/ThemeToggle'
|
|
import { LanguageDropdown } from '@/components/LanguageDropdown'
|
|
import { QRCodeUpload, UploadedFile } from '@/components/QRCodeUpload'
|
|
import { korrekturApi } from '@/lib/korrektur/api'
|
|
import type { Klausur, StudentWork, StudentStatus } from '../types'
|
|
import { STATUS_COLORS, STATUS_LABELS, getGradeLabel } from '../types'
|
|
|
|
// LocalStorage Key for upload session
|
|
const SESSION_ID_KEY = 'bp_korrektur_student_session'
|
|
|
|
// =============================================================================
|
|
// GLASS CARD
|
|
// =============================================================================
|
|
|
|
interface GlassCardProps {
|
|
children: React.ReactNode
|
|
className?: string
|
|
onClick?: () => void
|
|
size?: 'sm' | 'md' | 'lg'
|
|
delay?: number
|
|
}
|
|
|
|
function GlassCard({ children, className = '', onClick, size = 'md', delay = 0, isDark = true }: GlassCardProps & { isDark?: boolean }) {
|
|
const [isVisible, setIsVisible] = useState(false)
|
|
const [isHovered, setIsHovered] = useState(false)
|
|
|
|
useEffect(() => {
|
|
const timer = setTimeout(() => setIsVisible(true), delay)
|
|
return () => clearTimeout(timer)
|
|
}, [delay])
|
|
|
|
const sizeClasses = {
|
|
sm: 'p-4',
|
|
md: 'p-5',
|
|
lg: 'p-6',
|
|
}
|
|
|
|
return (
|
|
<div
|
|
className={`
|
|
rounded-3xl
|
|
${sizeClasses[size]}
|
|
${onClick ? 'cursor-pointer' : ''}
|
|
${className}
|
|
`}
|
|
style={{
|
|
background: isDark
|
|
? (isHovered ? 'rgba(255, 255, 255, 0.12)' : 'rgba(255, 255, 255, 0.08)')
|
|
: (isHovered ? 'rgba(255, 255, 255, 0.9)' : 'rgba(255, 255, 255, 0.7)'),
|
|
backdropFilter: 'blur(24px) saturate(180%)',
|
|
WebkitBackdropFilter: 'blur(24px) saturate(180%)',
|
|
border: isDark ? '1px solid rgba(255, 255, 255, 0.1)' : '1px solid rgba(0, 0, 0, 0.1)',
|
|
boxShadow: isDark
|
|
? '0 4px 24px rgba(0, 0, 0, 0.15), inset 0 1px 0 rgba(255, 255, 255, 0.05)'
|
|
: '0 4px 24px rgba(0, 0, 0, 0.08), inset 0 1px 0 rgba(255, 255, 255, 0.5)',
|
|
opacity: isVisible ? 1 : 0,
|
|
transform: isVisible
|
|
? `translateY(0) scale(${isHovered ? 1.01 : 1})`
|
|
: 'translateY(20px)',
|
|
transition: 'all 0.4s cubic-bezier(0.34, 1.56, 0.64, 1)',
|
|
}}
|
|
onMouseEnter={() => setIsHovered(true)}
|
|
onMouseLeave={() => setIsHovered(false)}
|
|
onClick={onClick}
|
|
>
|
|
{children}
|
|
</div>
|
|
)
|
|
}
|
|
|
|
// =============================================================================
|
|
// STUDENT CARD
|
|
// =============================================================================
|
|
|
|
interface StudentCardProps {
|
|
student: StudentWork
|
|
index: number
|
|
onClick: () => void
|
|
delay?: number
|
|
isDark?: boolean
|
|
}
|
|
|
|
function StudentCard({ student, index, onClick, delay = 0, isDark = true }: StudentCardProps) {
|
|
const statusColor = STATUS_COLORS[student.status] || '#6b7280'
|
|
const statusLabel = STATUS_LABELS[student.status] || student.status
|
|
|
|
const hasGrade = student.status === 'COMPLETED' || student.status === 'FIRST_EXAMINER' || student.status === 'SECOND_EXAMINER'
|
|
|
|
return (
|
|
<GlassCard onClick={onClick} delay={delay} size="sm" isDark={isDark}>
|
|
<div className="flex items-center gap-4">
|
|
{/* Index/Number */}
|
|
<div className={`w-10 h-10 rounded-xl flex items-center justify-center font-medium ${isDark ? 'bg-white/10 text-white/60' : 'bg-slate-200 text-slate-600'}`}>
|
|
{index + 1}
|
|
</div>
|
|
|
|
{/* Info */}
|
|
<div className="flex-1 min-w-0">
|
|
<p className={`font-medium truncate ${isDark ? 'text-white' : 'text-slate-900'}`}>{student.anonym_id}</p>
|
|
<div className="flex items-center gap-2 mt-1">
|
|
<span
|
|
className="px-2 py-0.5 rounded-full text-xs font-medium"
|
|
style={{ backgroundColor: `${statusColor}20`, color: statusColor }}
|
|
>
|
|
{statusLabel}
|
|
</span>
|
|
{hasGrade && student.grade_points > 0 && (
|
|
<span className={`text-sm ${isDark ? 'text-white/60' : 'text-slate-500'}`}>
|
|
{student.grade_points} P ({getGradeLabel(student.grade_points)})
|
|
</span>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Arrow */}
|
|
<svg className={`w-5 h-5 ${isDark ? 'text-white/40' : 'text-slate-400'}`} fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
|
|
</svg>
|
|
</div>
|
|
</GlassCard>
|
|
)
|
|
}
|
|
|
|
// =============================================================================
|
|
// UPLOAD MODAL
|
|
// =============================================================================
|
|
|
|
interface UploadModalProps {
|
|
isOpen: boolean
|
|
onClose: () => void
|
|
onUpload: (files: File[], anonymIds: string[]) => void
|
|
isUploading: boolean
|
|
}
|
|
|
|
function UploadModal({ isOpen, onClose, onUpload, isUploading }: UploadModalProps) {
|
|
const [files, setFiles] = useState<File[]>([])
|
|
const [anonymIds, setAnonymIds] = useState<string[]>([])
|
|
const fileInputRef = useRef<HTMLInputElement>(null)
|
|
|
|
if (!isOpen) return null
|
|
|
|
const handleFileSelect = (selectedFiles: FileList | null) => {
|
|
if (!selectedFiles) return
|
|
const newFiles = Array.from(selectedFiles)
|
|
setFiles((prev) => [...prev, ...newFiles])
|
|
// Generate default anonym IDs
|
|
setAnonymIds((prev) => [
|
|
...prev,
|
|
...newFiles.map((_, i) => `Arbeit-${prev.length + i + 1}`),
|
|
])
|
|
}
|
|
|
|
const handleDrop = (e: React.DragEvent) => {
|
|
e.preventDefault()
|
|
handleFileSelect(e.dataTransfer.files)
|
|
}
|
|
|
|
const removeFile = (index: number) => {
|
|
setFiles((prev) => prev.filter((_, i) => i !== index))
|
|
setAnonymIds((prev) => prev.filter((_, i) => i !== index))
|
|
}
|
|
|
|
const updateAnonymId = (index: number, value: string) => {
|
|
setAnonymIds((prev) => {
|
|
const updated = [...prev]
|
|
updated[index] = value
|
|
return updated
|
|
})
|
|
}
|
|
|
|
const handleSubmit = () => {
|
|
if (files.length > 0) {
|
|
onUpload(files, anonymIds)
|
|
}
|
|
}
|
|
|
|
return (
|
|
<div className="fixed inset-0 z-50 flex items-center justify-center p-4">
|
|
<div className="absolute inset-0 bg-black/50 backdrop-blur-sm" onClick={onClose} />
|
|
<GlassCard className="relative w-full max-w-2xl max-h-[80vh] overflow-hidden" size="lg" delay={0}>
|
|
<div className="flex items-center justify-between mb-6">
|
|
<h2 className="text-xl font-bold text-white">Arbeiten hochladen</h2>
|
|
<button
|
|
onClick={onClose}
|
|
className="p-2 rounded-lg hover:bg-white/10 text-white/60 transition-colors"
|
|
>
|
|
<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>
|
|
|
|
{/* Drop Zone */}
|
|
<div
|
|
className="border-2 border-dashed border-white/20 rounded-2xl p-8 text-center cursor-pointer transition-all hover:border-purple-400/50 hover:bg-white/5 mb-6"
|
|
onDrop={handleDrop}
|
|
onDragOver={(e) => e.preventDefault()}
|
|
onClick={() => fileInputRef.current?.click()}
|
|
>
|
|
<input
|
|
ref={fileInputRef}
|
|
type="file"
|
|
accept="image/*,.pdf"
|
|
multiple
|
|
onChange={(e) => handleFileSelect(e.target.files)}
|
|
className="hidden"
|
|
/>
|
|
<svg className="w-12 h-12 mx-auto mb-3 text-white/30" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M15 13l-3-3m0 0l-3 3m3-3v12" />
|
|
</svg>
|
|
<p className="text-white font-medium">Dateien hierher ziehen</p>
|
|
<p className="text-white/50 text-sm mt-1">oder klicken zum Auswaehlen</p>
|
|
</div>
|
|
|
|
{/* File List */}
|
|
{files.length > 0 && (
|
|
<div className="max-h-64 overflow-y-auto space-y-2 mb-6">
|
|
{files.map((file, index) => (
|
|
<div
|
|
key={index}
|
|
className="flex items-center gap-3 p-3 rounded-xl bg-white/5"
|
|
>
|
|
<span className="text-lg">
|
|
{file.type.startsWith('image/') ? '🖼️' : '📄'}
|
|
</span>
|
|
<div className="flex-1 min-w-0">
|
|
<p className="text-white text-sm truncate">{file.name}</p>
|
|
<input
|
|
type="text"
|
|
value={anonymIds[index] || ''}
|
|
onChange={(e) => updateAnonymId(index, e.target.value)}
|
|
placeholder="Anonym-ID"
|
|
className="mt-1 w-full px-2 py-1 rounded bg-white/10 border border-white/10 text-white text-sm placeholder-white/40 focus:outline-none focus:border-purple-500"
|
|
/>
|
|
</div>
|
|
<button
|
|
onClick={() => removeFile(index)}
|
|
className="p-2 rounded-lg hover:bg-red-500/20 text-red-400 transition-colors"
|
|
>
|
|
<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>
|
|
)}
|
|
|
|
{/* Actions */}
|
|
<div className="flex gap-3">
|
|
<button
|
|
onClick={onClose}
|
|
className="flex-1 px-4 py-3 rounded-xl bg-white/10 text-white hover:bg-white/20 transition-colors"
|
|
>
|
|
Abbrechen
|
|
</button>
|
|
<button
|
|
onClick={handleSubmit}
|
|
disabled={isUploading || files.length === 0}
|
|
className="flex-1 px-4 py-3 rounded-xl bg-gradient-to-r from-purple-500 to-pink-500 text-white font-semibold hover:shadow-lg hover:shadow-purple-500/30 transition-all disabled:opacity-50 flex items-center justify-center gap-2"
|
|
>
|
|
{isUploading ? (
|
|
<>
|
|
<div className="w-5 h-5 border-2 border-white border-t-transparent rounded-full animate-spin" />
|
|
Hochladen...
|
|
</>
|
|
) : (
|
|
<>
|
|
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-8l-4-4m0 0L8 8m4-4v12" />
|
|
</svg>
|
|
{files.length} Dateien hochladen
|
|
</>
|
|
)}
|
|
</button>
|
|
</div>
|
|
</GlassCard>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
// =============================================================================
|
|
// MAIN PAGE
|
|
// =============================================================================
|
|
|
|
export default function KlausurDetailPage() {
|
|
const { isDark } = useTheme()
|
|
const router = useRouter()
|
|
const params = useParams()
|
|
const klausurId = params.klausurId as string
|
|
|
|
// State
|
|
const [klausur, setKlausur] = useState<Klausur | null>(null)
|
|
const [students, setStudents] = useState<StudentWork[]>([])
|
|
const [isLoading, setIsLoading] = useState(true)
|
|
const [error, setError] = useState<string | null>(null)
|
|
|
|
// Modal states
|
|
const [showUploadModal, setShowUploadModal] = useState(false)
|
|
const [showQRModal, setShowQRModal] = useState(false)
|
|
const [isUploading, setIsUploading] = useState(false)
|
|
const [uploadSessionId, setUploadSessionId] = useState('')
|
|
|
|
// Initialize session ID
|
|
useEffect(() => {
|
|
let storedSessionId = localStorage.getItem(SESSION_ID_KEY)
|
|
if (!storedSessionId) {
|
|
storedSessionId = `student-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`
|
|
localStorage.setItem(SESSION_ID_KEY, storedSessionId)
|
|
}
|
|
setUploadSessionId(storedSessionId)
|
|
}, [])
|
|
|
|
// Load data
|
|
const loadData = useCallback(async () => {
|
|
if (!klausurId) return
|
|
|
|
setIsLoading(true)
|
|
setError(null)
|
|
|
|
try {
|
|
const [klausurData, studentsData] = await Promise.all([
|
|
korrekturApi.getKlausur(klausurId),
|
|
korrekturApi.getStudents(klausurId),
|
|
])
|
|
setKlausur(klausurData)
|
|
setStudents(studentsData)
|
|
} catch (err) {
|
|
console.error('Failed to load data:', err)
|
|
setError(err instanceof Error ? err.message : 'Laden fehlgeschlagen')
|
|
} finally {
|
|
setIsLoading(false)
|
|
}
|
|
}, [klausurId])
|
|
|
|
useEffect(() => {
|
|
loadData()
|
|
}, [loadData])
|
|
|
|
// Handle upload
|
|
const handleUpload = async (files: File[], anonymIds: string[]) => {
|
|
setIsUploading(true)
|
|
try {
|
|
for (let i = 0; i < files.length; i++) {
|
|
await korrekturApi.uploadStudentWork(klausurId, files[i], anonymIds[i])
|
|
}
|
|
setShowUploadModal(false)
|
|
loadData() // Refresh the list
|
|
} catch (err) {
|
|
console.error('Upload failed:', err)
|
|
setError(err instanceof Error ? err.message : 'Upload fehlgeschlagen')
|
|
} finally {
|
|
setIsUploading(false)
|
|
}
|
|
}
|
|
|
|
// Calculate progress
|
|
const completedCount = students.filter(s => s.status === 'COMPLETED').length
|
|
const progress = students.length > 0 ? Math.round((completedCount / students.length) * 100) : 0
|
|
|
|
return (
|
|
<div className={`min-h-screen flex relative overflow-hidden ${
|
|
isDark
|
|
? 'bg-gradient-to-br from-indigo-900 via-purple-900 to-pink-800'
|
|
: 'bg-gradient-to-br from-slate-100 via-blue-50 to-indigo-100'
|
|
}`}>
|
|
{/* Animated Background Blobs */}
|
|
<div className={`absolute -top-40 -right-40 w-96 h-96 rounded-full mix-blend-multiply filter blur-3xl animate-blob ${isDark ? 'bg-purple-500 opacity-30' : 'bg-purple-300 opacity-40'}`} />
|
|
<div className={`absolute top-1/2 -left-40 w-96 h-96 rounded-full mix-blend-multiply filter blur-3xl animate-blob animation-delay-2000 ${isDark ? 'bg-pink-500 opacity-30' : 'bg-pink-300 opacity-40'}`} />
|
|
<div className={`absolute -bottom-40 right-1/3 w-96 h-96 rounded-full mix-blend-multiply filter blur-3xl animate-blob animation-delay-4000 ${isDark ? 'bg-blue-500 opacity-30' : 'bg-blue-300 opacity-40'}`} />
|
|
|
|
{/* Sidebar */}
|
|
<div className="relative z-10 p-4">
|
|
<Sidebar />
|
|
</div>
|
|
|
|
{/* Main Content */}
|
|
<div className="flex-1 flex flex-col relative z-10 p-6 overflow-y-auto">
|
|
{/* Header */}
|
|
<div className="flex items-center justify-between mb-8">
|
|
<div className="flex items-center gap-4">
|
|
<button
|
|
onClick={() => router.push('/korrektur')}
|
|
className={`p-2 rounded-xl transition-colors ${isDark ? 'bg-white/10 hover:bg-white/20 text-white' : 'bg-slate-200 hover:bg-slate-300 text-slate-700'}`}
|
|
>
|
|
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M10 19l-7-7m0 0l7-7m-7 7h18" />
|
|
</svg>
|
|
</button>
|
|
<div>
|
|
<h1 className={`text-3xl font-bold mb-1 ${isDark ? 'text-white' : 'text-slate-900'}`}>
|
|
{klausur?.title || 'Klausur'}
|
|
</h1>
|
|
<p className={isDark ? 'text-white/50' : 'text-slate-500'}>
|
|
{klausur ? `${klausur.subject} ${klausur.semester} ${klausur.year}` : ''}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
<div className="flex items-center gap-3">
|
|
<ThemeToggle />
|
|
<LanguageDropdown />
|
|
</div>
|
|
</div>
|
|
|
|
{/* Stats Row */}
|
|
{!isLoading && klausur && (
|
|
<div className="grid grid-cols-1 sm:grid-cols-4 gap-4 mb-8">
|
|
<GlassCard size="sm" delay={100} isDark={isDark}>
|
|
<div className="text-center">
|
|
<p className={`text-3xl font-bold ${isDark ? 'text-white' : 'text-slate-900'}`}>{students.length}</p>
|
|
<p className={`text-sm ${isDark ? 'text-white/50' : 'text-slate-500'}`}>Arbeiten</p>
|
|
</div>
|
|
</GlassCard>
|
|
<GlassCard size="sm" delay={150} isDark={isDark}>
|
|
<div className="text-center">
|
|
<p className="text-3xl font-bold text-green-400">{completedCount}</p>
|
|
<p className={`text-sm ${isDark ? 'text-white/50' : 'text-slate-500'}`}>Abgeschlossen</p>
|
|
</div>
|
|
</GlassCard>
|
|
<GlassCard size="sm" delay={200} isDark={isDark}>
|
|
<div className="text-center">
|
|
<p className="text-3xl font-bold text-orange-400">{students.length - completedCount}</p>
|
|
<p className={`text-sm ${isDark ? 'text-white/50' : 'text-slate-500'}`}>Offen</p>
|
|
</div>
|
|
</GlassCard>
|
|
<GlassCard size="sm" delay={250} isDark={isDark}>
|
|
<div className="text-center">
|
|
<p className="text-3xl font-bold text-purple-400">{progress}%</p>
|
|
<p className={`text-sm ${isDark ? 'text-white/50' : 'text-slate-500'}`}>Fortschritt</p>
|
|
</div>
|
|
</GlassCard>
|
|
</div>
|
|
)}
|
|
|
|
{/* Progress Bar */}
|
|
{!isLoading && students.length > 0 && (
|
|
<GlassCard size="sm" className="mb-6" delay={300} isDark={isDark}>
|
|
<div className="flex items-center justify-between text-sm mb-2">
|
|
<span className={isDark ? 'text-white/50' : 'text-slate-500'}>Gesamtfortschritt</span>
|
|
<span className={isDark ? 'text-white' : 'text-slate-900'}>{completedCount}/{students.length} korrigiert</span>
|
|
</div>
|
|
<div className={`h-3 rounded-full overflow-hidden ${isDark ? 'bg-white/10' : 'bg-slate-200'}`}>
|
|
<div
|
|
className="h-full rounded-full transition-all duration-500 bg-gradient-to-r from-green-500 to-emerald-400"
|
|
style={{ width: `${progress}%` }}
|
|
/>
|
|
</div>
|
|
</GlassCard>
|
|
)}
|
|
|
|
{/* Error Display */}
|
|
{error && (
|
|
<GlassCard className="mb-6" size="sm" isDark={isDark}>
|
|
<div className="flex items-center gap-3 text-red-400">
|
|
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
|
</svg>
|
|
<span>{error}</span>
|
|
<button
|
|
onClick={loadData}
|
|
className={`ml-auto px-3 py-1 rounded-lg transition-colors text-sm ${isDark ? 'bg-white/10 hover:bg-white/20' : 'bg-slate-200 hover:bg-slate-300'}`}
|
|
>
|
|
Erneut versuchen
|
|
</button>
|
|
</div>
|
|
</GlassCard>
|
|
)}
|
|
|
|
{/* Loading */}
|
|
{isLoading && (
|
|
<div className="flex-1 flex items-center justify-center">
|
|
<div className="w-12 h-12 border-4 border-purple-500 border-t-transparent rounded-full animate-spin" />
|
|
</div>
|
|
)}
|
|
|
|
{/* Action Buttons */}
|
|
{!isLoading && (
|
|
<div className="flex gap-3 mb-6">
|
|
<button
|
|
onClick={() => setShowUploadModal(true)}
|
|
className="px-6 py-3 rounded-xl bg-gradient-to-r from-purple-500 to-pink-500 text-white font-semibold hover:shadow-lg hover:shadow-purple-500/30 transition-all flex items-center gap-2"
|
|
>
|
|
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-8l-4-4m0 0L8 8m4-4v12" />
|
|
</svg>
|
|
Arbeiten hochladen
|
|
</button>
|
|
<button
|
|
onClick={() => setShowQRModal(true)}
|
|
className={`px-6 py-3 rounded-xl transition-colors flex items-center gap-2 ${isDark ? 'bg-white/10 text-white hover:bg-white/20' : 'bg-slate-200 text-slate-700 hover:bg-slate-300'}`}
|
|
>
|
|
<span className="text-xl">📱</span>
|
|
QR Upload
|
|
</button>
|
|
{students.length > 0 && (
|
|
<button
|
|
onClick={() => router.push(`/korrektur/${klausurId}/fairness`)}
|
|
className={`px-6 py-3 rounded-xl transition-colors flex items-center gap-2 ${isDark ? 'bg-white/10 text-white hover:bg-white/20' : 'bg-slate-200 text-slate-700 hover:bg-slate-300'}`}
|
|
>
|
|
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z" />
|
|
</svg>
|
|
Fairness-Analyse
|
|
</button>
|
|
)}
|
|
</div>
|
|
)}
|
|
|
|
{/* Students List */}
|
|
{!isLoading && students.length === 0 && (
|
|
<GlassCard className="text-center py-12" delay={350} isDark={isDark}>
|
|
<div className={`w-20 h-20 mx-auto mb-4 rounded-2xl flex items-center justify-center ${isDark ? 'bg-white/10' : 'bg-slate-200'}`}>
|
|
<svg className={`w-10 h-10 ${isDark ? 'text-white/30' : 'text-slate-400'}`} fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
|
|
</svg>
|
|
</div>
|
|
<h3 className={`text-xl font-semibold mb-2 ${isDark ? 'text-white' : 'text-slate-900'}`}>Keine Arbeiten vorhanden</h3>
|
|
<p className={`mb-6 ${isDark ? 'text-white/50' : 'text-slate-500'}`}>Laden Sie Schuelerarbeiten hoch, um mit der Korrektur zu beginnen.</p>
|
|
<button
|
|
onClick={() => setShowUploadModal(true)}
|
|
className="px-6 py-3 rounded-xl bg-gradient-to-r from-purple-500 to-pink-500 text-white font-semibold hover:shadow-lg hover:shadow-purple-500/30 transition-all"
|
|
>
|
|
Arbeiten hochladen
|
|
</button>
|
|
</GlassCard>
|
|
)}
|
|
|
|
{!isLoading && students.length > 0 && (
|
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
|
{students.map((student, index) => (
|
|
<StudentCard
|
|
key={student.id}
|
|
student={student}
|
|
index={index}
|
|
onClick={() => router.push(`/korrektur/${klausurId}/${student.id}`)}
|
|
delay={350 + index * 30}
|
|
isDark={isDark}
|
|
/>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* Upload Modal */}
|
|
<UploadModal
|
|
isOpen={showUploadModal}
|
|
onClose={() => setShowUploadModal(false)}
|
|
onUpload={handleUpload}
|
|
isUploading={isUploading}
|
|
/>
|
|
|
|
{/* QR Code Modal */}
|
|
{showQRModal && (
|
|
<div className="fixed inset-0 z-50 flex items-center justify-center p-4">
|
|
<div className="absolute inset-0 bg-black/50 backdrop-blur-sm" onClick={() => setShowQRModal(false)} />
|
|
<div className={`relative w-full max-w-md rounded-3xl ${isDark ? 'bg-slate-900' : 'bg-white'}`}>
|
|
<QRCodeUpload
|
|
sessionId={uploadSessionId}
|
|
onClose={() => setShowQRModal(false)}
|
|
onFilesChanged={(files) => {
|
|
// Handle mobile uploaded files
|
|
if (files.length > 0) {
|
|
// Could auto-process the files here
|
|
}
|
|
}}
|
|
/>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
)
|
|
}
|