[split-required] Split remaining 500-680 LOC files (final batch)
website (17 pages + 3 components): - multiplayer/wizard, middleware/wizard+test-wizard, communication - builds/wizard, staff-search, voice, sbom/wizard - foerderantrag, mail/tasks, tools/communication, sbom - compliance/evidence, uni-crawler, brandbook (already done) - CollectionsTab, IngestionTab, RiskHeatmap backend-lehrer (5 files): - letters_api (641 → 2), certificates_api (636 → 2) - alerts_agent/db/models (636 → 3) - llm_gateway/communication_service (614 → 2) - game/database already done in prior batch klausur-service (2 files): - hybrid_vocab_extractor (664 → 2) - klausur-service/frontend: api.ts (620 → 3), EHUploadWizard (591 → 2) voice-service (3 files): - bqas/rag_judge (618 → 3), runner (529 → 2) - enhanced_task_orchestrator (519 → 2) studio-v2 (6 files): - korrektur/[klausurId] (578 → 4), fairness (569 → 2) - AlertsWizard (552 → 2), OnboardingWizard (513 → 2) - korrektur/api.ts (506 → 3), geo-lernwelt (501 → 2) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,60 @@
|
||||
'use client'
|
||||
|
||||
import { useState, useEffect } from 'react'
|
||||
|
||||
interface GlassCardProps {
|
||||
children: React.ReactNode
|
||||
className?: string
|
||||
onClick?: () => void
|
||||
size?: 'sm' | 'md' | 'lg'
|
||||
delay?: number
|
||||
isDark?: boolean
|
||||
}
|
||||
|
||||
export function GlassCard({ children, className = '', onClick, size = 'md', delay = 0, isDark = true }: GlassCardProps) {
|
||||
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>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
'use client'
|
||||
|
||||
import type { StudentWork } from '../../types'
|
||||
import { STATUS_COLORS, STATUS_LABELS, getGradeLabel } from '../../types'
|
||||
import { GlassCard } from './GlassCard'
|
||||
|
||||
interface StudentCardProps {
|
||||
student: StudentWork
|
||||
index: number
|
||||
onClick: () => void
|
||||
delay?: number
|
||||
isDark?: boolean
|
||||
}
|
||||
|
||||
export 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>
|
||||
)
|
||||
}
|
||||
152
studio-v2/app/korrektur/[klausurId]/_components/UploadModal.tsx
Normal file
152
studio-v2/app/korrektur/[klausurId]/_components/UploadModal.tsx
Normal file
@@ -0,0 +1,152 @@
|
||||
'use client'
|
||||
|
||||
import { useState, useRef } from 'react'
|
||||
import { GlassCard } from './GlassCard'
|
||||
|
||||
interface UploadModalProps {
|
||||
isOpen: boolean
|
||||
onClose: () => void
|
||||
onUpload: (files: File[], anonymIds: string[]) => void
|
||||
isUploading: boolean
|
||||
}
|
||||
|
||||
export 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])
|
||||
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/') ? '\uD83D\uDDBC\uFE0F' : '\uD83D\uDCC4'}</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>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user