[split-required] Split website + studio-v2 monoliths (Phase 3 continued)

Website (14 monoliths split):
- compliance/page.tsx (1,519 → 9), docs/audit (1,262 → 20)
- quality (1,231 → 16), alerts (1,203 → 10), docs (1,202 → 11)
- i18n.ts (1,173 → 8 language files)
- unity-bridge (1,094 → 12), backlog (1,087 → 6)
- training (1,066 → 8), rag (1,063 → 8)
- Deleted index_original.ts (4,899 LOC dead backup)

Studio-v2 (5 monoliths split):
- meet/page.tsx (1,481 → 9), messages (1,166 → 9)
- AlertsB2BContext.tsx (1,165 → 5 modules)
- alerts-b2b/page.tsx (1,019 → 6), korrektur/archiv (1,001 → 6)

All existing imports preserved. Zero new TypeScript errors.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Benjamin Admin
2026-04-24 17:52:36 +02:00
parent b681ddb131
commit 0b37c5e692
143 changed files with 15822 additions and 15889 deletions

View File

@@ -0,0 +1,130 @@
'use client'
import { useState } from 'react'
import { GlassCard } from './GlassCard'
import type { AbiturDokument } from './DokumentCard'
interface CreateKlausurFromTemplateModalProps {
template: AbiturDokument
onClose: () => void
onCreate: (title: string) => void
onFallback: () => void
isLoading: boolean
error: string | null
isDark: boolean
}
export function CreateKlausurFromTemplateModal({
template,
onClose,
onCreate,
onFallback,
isLoading,
error,
isDark,
}: CreateKlausurFromTemplateModalProps) {
const [title, setTitle] = useState(
`${template.fach} ${template.aufgabentyp || ''} ${template.jahr}`.trim()
)
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault()
onCreate(title)
}
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-md" size="lg" delay={0} isDark={isDark}>
<h2 className={`text-xl font-bold mb-2 ${isDark ? 'text-white' : 'text-slate-900'}`}>
Klausur aus Vorlage erstellen
</h2>
<p className={`text-sm mb-6 ${isDark ? 'text-white/60' : 'text-slate-600'}`}>
Basierend auf: {template.thema || template.dateiname}
</p>
{error && (
<div className="mb-4 p-3 rounded-lg bg-red-500/20 border border-red-500/30 text-sm">
<p className="text-red-300 mb-2">{error}</p>
<button
onClick={onFallback}
className="text-purple-400 hover:text-purple-300 underline text-xs"
>
Zur Korrektur-Uebersicht (ohne Klausur erstellen)
</button>
</div>
)}
<form onSubmit={handleSubmit} className="space-y-4">
<div>
<label className={`block text-sm mb-2 ${isDark ? 'text-white/60' : 'text-slate-600'}`}>
Klausur-Titel
</label>
<input
type="text"
value={title}
onChange={(e) => setTitle(e.target.value)}
placeholder="z.B. Deutsch LK Q4 - Kafka"
className={`w-full p-3 rounded-xl border focus:ring-2 focus:ring-purple-500 focus:border-transparent ${
isDark
? 'bg-white/10 border-white/20 text-white placeholder-white/40'
: 'bg-slate-100 border-slate-300 text-slate-900 placeholder-slate-400'
}`}
required
autoFocus
/>
</div>
{/* Template Info */}
<div className={`p-4 rounded-xl ${isDark ? 'bg-white/5' : 'bg-slate-100'}`}>
<div className="grid grid-cols-2 gap-3 text-sm">
{[
{ label: 'Fach', value: template.fach },
{ label: 'Jahr', value: String(template.jahr) },
{ label: 'Niveau', value: template.niveau },
{ label: 'Typ', value: template.aufgabentyp || '-' },
].map(({ label, value }) => (
<div key={label}>
<span className={isDark ? 'text-white/50' : 'text-slate-500'}>{label}:</span>
<span className={`ml-2 ${isDark ? 'text-white' : 'text-slate-900'}`}>{value}</span>
</div>
))}
</div>
</div>
<div className="flex gap-3 pt-2">
<button
type="button"
onClick={onClose}
disabled={isLoading}
className={`flex-1 px-4 py-3 rounded-xl transition-colors ${
isDark
? 'bg-white/10 text-white hover:bg-white/20'
: 'bg-slate-200 text-slate-700 hover:bg-slate-300'
} disabled:opacity-50`}
>
Abbrechen
</button>
<button
type="submit"
disabled={isLoading || !title.trim()}
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"
>
{isLoading ? (
<span className="flex items-center justify-center gap-2">
<svg className="w-4 h-4 animate-spin" fill="none" viewBox="0 0 24 24">
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z" />
</svg>
Erstelle...
</span>
) : (
'Klausur erstellen'
)}
</button>
</div>
</form>
</GlassCard>
</div>
)
}

View File

@@ -0,0 +1,79 @@
'use client'
import { GlassCard } from './GlassCard'
export interface AbiturDokument {
id: string
dateiname: string
fach: string
jahr: number
bundesland: string
niveau: string
dokumenttyp: string
aufgabentyp?: string
thema?: string
download_url: string
preview_url?: string
file_size?: number
page_count?: number
}
interface DokumentCardProps {
dokument: AbiturDokument
onPreview: () => void
onUseAsTemplate: () => void
delay?: number
isDark: boolean
}
export function DokumentCard({ dokument, onPreview, onUseAsTemplate, delay = 0, isDark }: DokumentCardProps) {
const typeColor = dokument.dokumenttyp === 'Erwartungshorizont' ? '#22c55e' : '#3b82f6'
return (
<GlassCard delay={delay} isDark={isDark}>
<div className="flex flex-col h-full">
{/* Header */}
<div className="flex items-start justify-between mb-3">
<div className="flex-1 min-w-0">
<h3 className={`font-semibold truncate ${isDark ? 'text-white' : 'text-slate-900'}`}>
{dokument.fach} {dokument.jahr} {dokument.niveau}
</h3>
<p className={`text-sm truncate ${isDark ? 'text-white/60' : 'text-slate-600'}`}>
{dokument.thema || dokument.aufgabentyp || dokument.dateiname}
</p>
</div>
<span
className="px-2 py-1 rounded-full text-xs font-medium flex-shrink-0 ml-2"
style={{ backgroundColor: `${typeColor}20`, color: typeColor }}
>
{dokument.dokumenttyp === 'Erwartungshorizont' ? 'EH' : 'Aufgabe'}
</span>
</div>
{/* Meta */}
<div className={`flex items-center gap-3 text-xs mb-4 ${isDark ? 'text-white/40' : 'text-slate-500'}`}>
<span>{dokument.bundesland}</span>
{dokument.page_count && <span>{dokument.page_count} Seiten</span>}
</div>
{/* Actions */}
<div className="flex gap-2 mt-auto">
<button
onClick={(e) => { e.stopPropagation(); onPreview() }}
className={`flex-1 px-3 py-2 rounded-xl text-sm font-medium transition-colors ${
isDark ? 'bg-white/10 text-white hover:bg-white/20' : 'bg-slate-100 text-slate-700 hover:bg-slate-200'
}`}
>
Vorschau
</button>
<button
onClick={(e) => { e.stopPropagation(); onUseAsTemplate() }}
className="flex-1 px-3 py-2 rounded-xl text-sm font-medium bg-gradient-to-r from-purple-500 to-pink-500 text-white hover:shadow-lg hover:shadow-purple-500/30 transition-all"
>
Verwenden
</button>
</div>
</div>
</GlassCard>
)
}

View File

@@ -0,0 +1,34 @@
'use client'
interface FilterDropdownProps {
label: string
value: string
options: string[]
onChange: (value: string) => void
isDark: boolean
}
export function FilterDropdown({ label, value, options, onChange, isDark }: FilterDropdownProps) {
const inputId = `filter-${label.toLowerCase().replace(/\s+/g, '-')}`
return (
<div className="flex flex-col gap-1">
<label htmlFor={inputId} className={`text-xs ${isDark ? 'text-white/50' : 'text-slate-500'}`}>{label}</label>
<select
id={inputId}
value={value}
onChange={(e) => onChange(e.target.value)}
className={`px-3 py-2 rounded-xl text-sm transition-colors ${
isDark
? 'bg-white/10 border-white/20 text-white'
: 'bg-white border-slate-200 text-slate-900'
} border focus:ring-2 focus:ring-purple-500 focus:border-transparent`}
>
{options.map((opt) => (
<option key={opt} value={opt} className="bg-slate-800 text-white">
{opt}
</option>
))}
</select>
</div>
)
}

View File

@@ -0,0 +1,49 @@
'use client'
import { useState, useEffect, ReactNode } from 'react'
interface GlassCardProps {
children: 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>
)
}

View File

@@ -0,0 +1,164 @@
'use client'
import { useState } from 'react'
import { GlassCard } from './GlassCard'
import type { AbiturDokument } from './DokumentCard'
interface PreviewModalProps {
dokument: AbiturDokument | null
onClose: () => void
onUseAsTemplate: () => void
isDark: boolean
}
export function PreviewModal({ dokument, onClose, onUseAsTemplate, isDark }: PreviewModalProps) {
const [zoom, setZoom] = useState(100)
if (!dokument) return null
return (
<div className="fixed inset-0 z-50 flex">
{/* Backdrop */}
<div className="absolute inset-0 bg-black/70 backdrop-blur-sm" onClick={onClose} />
{/* Content */}
<div className="relative flex-1 flex m-4 gap-4">
{/* PDF Viewer */}
<div className="flex-1 flex flex-col">
<GlassCard className="flex-1 flex flex-col overflow-hidden" size="sm" isDark={isDark}>
{/* Toolbar */}
<div className={`flex items-center justify-between p-3 border-b ${isDark ? 'border-white/10' : 'border-slate-200'}`}>
<div className="flex items-center gap-2">
<button
onClick={() => setZoom(z => Math.max(50, z - 25))}
className={`p-2 rounded-lg ${isDark ? 'hover:bg-white/10' : 'hover:bg-slate-100'}`}
>
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M20 12H4" />
</svg>
</button>
<span className={`text-sm ${isDark ? 'text-white/60' : 'text-slate-600'}`}>{zoom}%</span>
<button
onClick={() => setZoom(z => Math.min(200, z + 25))}
className={`p-2 rounded-lg ${isDark ? 'hover:bg-white/10' : 'hover:bg-slate-100'}`}
>
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 4v16m8-8H4" />
</svg>
</button>
</div>
<button
onClick={onClose}
className={`p-2 rounded-lg ${isDark ? 'hover:bg-white/10' : 'hover:bg-slate-100'}`}
>
<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>
{/* PDF Content */}
<div className="flex-1 overflow-auto p-4">
<div
className="mx-auto bg-white rounded-lg shadow-xl"
style={{ width: `${zoom}%`, minHeight: '800px' }}
>
{dokument.preview_url ? (
<iframe
src={dokument.preview_url}
className="w-full h-full min-h-[800px]"
title={dokument.dateiname}
/>
) : (
<div className="flex items-center justify-center h-full text-slate-400">
<div className="text-center">
<svg className="w-16 h-16 mx-auto mb-4" 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>
<p>Vorschau nicht verfuegbar</p>
<a
href={dokument.download_url}
target="_blank"
rel="noopener noreferrer"
className="text-purple-400 hover:underline mt-2 inline-block"
>
PDF herunterladen
</a>
</div>
</div>
)}
</div>
</div>
</GlassCard>
</div>
{/* Sidebar */}
<div className="w-80 flex flex-col gap-4">
{/* Close Button - prominent */}
<button
onClick={onClose}
className={`flex items-center gap-2 px-4 py-3 rounded-2xl font-medium transition-colors ${
isDark
? 'bg-white/10 text-white hover:bg-white/20'
: 'bg-white/70 text-slate-700 hover:bg-white/90'
}`}
style={{
backdropFilter: 'blur(24px)',
border: isDark ? '1px solid rgba(255,255,255,0.1)' : '1px solid rgba(0,0,0,0.1)',
}}
>
<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>
Zurueck zum Archiv
</button>
<GlassCard size="md" isDark={isDark}>
<h3 className={`font-semibold mb-4 ${isDark ? 'text-white' : 'text-slate-900'}`}>Details</h3>
<div className="space-y-3">
{[
{ label: 'Fach', value: dokument.fach },
{ label: 'Jahr', value: String(dokument.jahr) },
{ label: 'Bundesland', value: dokument.bundesland },
{ label: 'Niveau', value: dokument.niveau },
].map(({ label, value }) => (
<div key={label}>
<span className={`text-xs ${isDark ? 'text-white/50' : 'text-slate-500'}`}>{label}</span>
<p className={isDark ? 'text-white' : 'text-slate-900'}>{value}</p>
</div>
))}
{dokument.thema && (
<div>
<span className={`text-xs ${isDark ? 'text-white/50' : 'text-slate-500'}`}>Thema</span>
<p className={isDark ? 'text-white' : 'text-slate-900'}>{dokument.thema}</p>
</div>
)}
</div>
</GlassCard>
<GlassCard size="md" isDark={isDark}>
<h3 className={`font-semibold mb-4 ${isDark ? 'text-white' : 'text-slate-900'}`}>Aktionen</h3>
<div className="space-y-2">
<button
onClick={onUseAsTemplate}
className="w-full 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"
>
Als Vorlage verwenden
</button>
<a
href={dokument.download_url}
target="_blank"
rel="noopener noreferrer"
className={`block w-full px-4 py-3 rounded-xl text-center font-medium transition-colors ${
isDark ? 'bg-white/10 text-white hover:bg-white/20' : 'bg-slate-100 text-slate-700 hover:bg-slate-200'
}`}
>
Herunterladen
</a>
</div>
</GlassCard>
</div>
</div>
</div>
)
}