[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:
@@ -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>
|
||||
)
|
||||
}
|
||||
79
studio-v2/app/korrektur/archiv/_components/DokumentCard.tsx
Normal file
79
studio-v2/app/korrektur/archiv/_components/DokumentCard.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
49
studio-v2/app/korrektur/archiv/_components/GlassCard.tsx
Normal file
49
studio-v2/app/korrektur/archiv/_components/GlassCard.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
164
studio-v2/app/korrektur/archiv/_components/PreviewModal.tsx
Normal file
164
studio-v2/app/korrektur/archiv/_components/PreviewModal.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
'use client'
|
||||
|
||||
import { useState, useEffect, useCallback, useMemo } from 'react'
|
||||
import { useState, useEffect, useMemo } from 'react'
|
||||
import { useRouter } from 'next/navigation'
|
||||
import { useTheme } from '@/lib/ThemeContext'
|
||||
import { useLanguage } from '@/lib/LanguageContext'
|
||||
@@ -8,507 +8,33 @@ import { Sidebar } from '@/components/Sidebar'
|
||||
import { ThemeToggle } from '@/components/ThemeToggle'
|
||||
import { LanguageDropdown } from '@/components/LanguageDropdown'
|
||||
import { korrekturApi } from '@/lib/korrektur/api'
|
||||
|
||||
// =============================================================================
|
||||
// TYPES
|
||||
// =============================================================================
|
||||
|
||||
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 ThemaSuggestion {
|
||||
label: string
|
||||
count: number
|
||||
aufgabentyp: string
|
||||
kategorie?: string
|
||||
}
|
||||
import { GlassCard } from './_components/GlassCard'
|
||||
import { FilterDropdown } from './_components/FilterDropdown'
|
||||
import { DokumentCard, type AbiturDokument } from './_components/DokumentCard'
|
||||
import { PreviewModal } from './_components/PreviewModal'
|
||||
import { CreateKlausurFromTemplateModal } from './_components/CreateKlausurFromTemplateModal'
|
||||
|
||||
// =============================================================================
|
||||
// CONSTANTS
|
||||
// =============================================================================
|
||||
|
||||
// Default filter options (will be updated from API)
|
||||
const DEFAULT_FAECHER = ['Alle', 'Deutsch', 'Englisch', 'Mathematik', 'Geschichte', 'Politik']
|
||||
const DEFAULT_JAHRE = ['Alle', '2025', '2024', '2023', '2022', '2021']
|
||||
const BUNDESLAENDER = ['Alle', 'Niedersachsen', 'NRW', 'Bayern', 'Baden-Wuerttemberg', 'Hessen']
|
||||
const DEFAULT_NIVEAUS = ['Alle', 'eA', 'gA']
|
||||
const DEFAULT_DOKUMENTTYPEN = ['Alle', 'Aufgabe', 'Erwartungshorizont', 'Loesungshinweise']
|
||||
|
||||
const POPULAR_THEMES = [
|
||||
'Textanalyse',
|
||||
'Gedichtanalyse',
|
||||
'Eroerterung',
|
||||
'Dramenanalyse',
|
||||
'Sprachreflexion',
|
||||
'Romantik',
|
||||
'Expressionismus',
|
||||
'Textanalyse', 'Gedichtanalyse', 'Eroerterung', 'Dramenanalyse',
|
||||
'Sprachreflexion', 'Romantik', 'Expressionismus',
|
||||
]
|
||||
|
||||
// =============================================================================
|
||||
// GLASS CARD COMPONENT
|
||||
// =============================================================================
|
||||
const SAMPLE_PDF = 'https://mozilla.github.io/pdf.js/web/compressed.tracemonkey-pldi-09.pdf'
|
||||
|
||||
interface GlassCardProps {
|
||||
children: React.ReactNode
|
||||
className?: string
|
||||
onClick?: () => void
|
||||
size?: 'sm' | 'md' | 'lg'
|
||||
delay?: number
|
||||
isDark?: boolean
|
||||
}
|
||||
|
||||
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>
|
||||
)
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// FILTER DROPDOWN
|
||||
// =============================================================================
|
||||
|
||||
interface FilterDropdownProps {
|
||||
label: string
|
||||
value: string
|
||||
options: string[]
|
||||
onChange: (value: string) => void
|
||||
isDark: boolean
|
||||
}
|
||||
|
||||
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>
|
||||
)
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// DOCUMENT CARD
|
||||
// =============================================================================
|
||||
|
||||
interface DokumentCardProps {
|
||||
dokument: AbiturDokument
|
||||
onPreview: () => void
|
||||
onUseAsTemplate: () => void
|
||||
delay?: number
|
||||
isDark: boolean
|
||||
}
|
||||
|
||||
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>
|
||||
)
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// PREVIEW MODAL
|
||||
// =============================================================================
|
||||
|
||||
interface PreviewModalProps {
|
||||
dokument: AbiturDokument | null
|
||||
onClose: () => void
|
||||
onUseAsTemplate: () => void
|
||||
isDark: boolean
|
||||
}
|
||||
|
||||
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">
|
||||
<div>
|
||||
<span className={`text-xs ${isDark ? 'text-white/50' : 'text-slate-500'}`}>Fach</span>
|
||||
<p className={isDark ? 'text-white' : 'text-slate-900'}>{dokument.fach}</p>
|
||||
</div>
|
||||
<div>
|
||||
<span className={`text-xs ${isDark ? 'text-white/50' : 'text-slate-500'}`}>Jahr</span>
|
||||
<p className={isDark ? 'text-white' : 'text-slate-900'}>{dokument.jahr}</p>
|
||||
</div>
|
||||
<div>
|
||||
<span className={`text-xs ${isDark ? 'text-white/50' : 'text-slate-500'}`}>Bundesland</span>
|
||||
<p className={isDark ? 'text-white' : 'text-slate-900'}>{dokument.bundesland}</p>
|
||||
</div>
|
||||
<div>
|
||||
<span className={`text-xs ${isDark ? 'text-white/50' : 'text-slate-500'}`}>Niveau</span>
|
||||
<p className={isDark ? 'text-white' : 'text-slate-900'}>{dokument.niveau}</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>
|
||||
)
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// CREATE KLAUSUR FROM TEMPLATE MODAL
|
||||
// =============================================================================
|
||||
|
||||
interface CreateKlausurFromTemplateModalProps {
|
||||
template: AbiturDokument
|
||||
onClose: () => void
|
||||
onCreate: (title: string) => void
|
||||
onFallback: () => void
|
||||
isLoading: boolean
|
||||
error: string | null
|
||||
isDark: boolean
|
||||
}
|
||||
|
||||
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">
|
||||
<div>
|
||||
<span className={isDark ? 'text-white/50' : 'text-slate-500'}>Fach:</span>
|
||||
<span className={`ml-2 ${isDark ? 'text-white' : 'text-slate-900'}`}>{template.fach}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className={isDark ? 'text-white/50' : 'text-slate-500'}>Jahr:</span>
|
||||
<span className={`ml-2 ${isDark ? 'text-white' : 'text-slate-900'}`}>{template.jahr}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className={isDark ? 'text-white/50' : 'text-slate-500'}>Niveau:</span>
|
||||
<span className={`ml-2 ${isDark ? 'text-white' : 'text-slate-900'}`}>{template.niveau}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className={isDark ? 'text-white/50' : 'text-slate-500'}>Typ:</span>
|
||||
<span className={`ml-2 ${isDark ? 'text-white' : 'text-slate-900'}`}>{template.aufgabentyp}</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>
|
||||
)
|
||||
}
|
||||
const MOCK_DOKUMENTE: AbiturDokument[] = [
|
||||
{ id: '1', dateiname: 'Deutsch_eA_2024_Aufgabe1.pdf', fach: 'Deutsch', jahr: 2024, bundesland: 'Niedersachsen', niveau: 'eA', dokumenttyp: 'Aufgabe', aufgabentyp: 'Textanalyse', thema: 'Textanalyse: "Der Prozess" - Kafka', download_url: SAMPLE_PDF, preview_url: SAMPLE_PDF, page_count: 4 },
|
||||
{ id: '2', dateiname: 'Deutsch_eA_2024_EH1.pdf', fach: 'Deutsch', jahr: 2024, bundesland: 'Niedersachsen', niveau: 'eA', dokumenttyp: 'Erwartungshorizont', aufgabentyp: 'Textanalyse', thema: 'EH zu Kafka-Analyse', download_url: SAMPLE_PDF, preview_url: SAMPLE_PDF, page_count: 8 },
|
||||
{ id: '3', dateiname: 'Deutsch_gA_2024_Aufgabe2.pdf', fach: 'Deutsch', jahr: 2024, bundesland: 'Niedersachsen', niveau: 'gA', dokumenttyp: 'Aufgabe', aufgabentyp: 'Gedichtanalyse', thema: 'Gedichtvergleich Romantik', download_url: SAMPLE_PDF, preview_url: SAMPLE_PDF, page_count: 3 },
|
||||
{ id: '4', dateiname: 'Deutsch_eA_2023_Aufgabe1.pdf', fach: 'Deutsch', jahr: 2023, bundesland: 'Niedersachsen', niveau: 'eA', dokumenttyp: 'Aufgabe', aufgabentyp: 'Eroerterung', thema: 'Materialgestuetzte Eroerterung: Digitalisierung', download_url: SAMPLE_PDF, preview_url: SAMPLE_PDF, page_count: 5 },
|
||||
{ id: '5', dateiname: 'Deutsch_eA_2023_EH1.pdf', fach: 'Deutsch', jahr: 2023, bundesland: 'Niedersachsen', niveau: 'eA', dokumenttyp: 'Erwartungshorizont', aufgabentyp: 'Eroerterung', thema: 'EH zu Digitalisierungs-Eroerterung', download_url: SAMPLE_PDF, preview_url: SAMPLE_PDF, page_count: 10 },
|
||||
{ id: '6', dateiname: 'Deutsch_gA_2023_Aufgabe3.pdf', fach: 'Deutsch', jahr: 2023, bundesland: 'Niedersachsen', niveau: 'gA', dokumenttyp: 'Aufgabe', aufgabentyp: 'Dramenanalyse', thema: 'Szenenanalyse: "Faust I"', download_url: SAMPLE_PDF, preview_url: SAMPLE_PDF, page_count: 4 },
|
||||
]
|
||||
|
||||
// =============================================================================
|
||||
// MAIN PAGE
|
||||
@@ -521,8 +47,6 @@ export default function ArchivPage() {
|
||||
|
||||
// State
|
||||
const [searchQuery, setSearchQuery] = useState('')
|
||||
const [suggestions, setSuggestions] = useState<ThemaSuggestion[]>([])
|
||||
const [showSuggestions, setShowSuggestions] = useState(false)
|
||||
const [dokumente, setDokumente] = useState<AbiturDokument[]>([])
|
||||
const [isLoading, setIsLoading] = useState(true)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
@@ -543,15 +67,9 @@ export default function ArchivPage() {
|
||||
const [isCreating, setIsCreating] = useState(false)
|
||||
const [createError, setCreateError] = useState<string | null>(null)
|
||||
|
||||
// Example PDF URL (Mozilla public sample) - fallback when API fails
|
||||
const SAMPLE_PDF = 'https://mozilla.github.io/pdf.js/web/compressed.tracemonkey-pldi-09.pdf'
|
||||
|
||||
// Available filter options (updated from API response)
|
||||
const [availableFilters, setAvailableFilters] = useState<{
|
||||
subjects: string[]
|
||||
years: number[]
|
||||
niveaus: string[]
|
||||
doc_types: string[]
|
||||
subjects: string[]; years: number[]; niveaus: string[]; doc_types: string[]
|
||||
}>({
|
||||
subjects: ['Deutsch', 'Englisch', 'Mathematik'],
|
||||
years: [2025, 2024, 2023, 2022, 2021],
|
||||
@@ -564,49 +82,25 @@ export default function ArchivPage() {
|
||||
const loadDocuments = async () => {
|
||||
setIsLoading(true)
|
||||
setError(null)
|
||||
|
||||
try {
|
||||
// Build filter params
|
||||
const filters: {
|
||||
subject?: string
|
||||
year?: number
|
||||
niveau?: string
|
||||
doc_type?: string
|
||||
search?: string
|
||||
} = {}
|
||||
|
||||
const filters: { subject?: string; year?: number; niveau?: string; doc_type?: string; search?: string } = {}
|
||||
if (fach !== 'Alle') filters.subject = fach
|
||||
if (jahr !== 'Alle') filters.year = parseInt(jahr)
|
||||
if (niveau !== 'Alle') filters.niveau = niveau
|
||||
if (dokumenttyp !== 'Alle') {
|
||||
// Map frontend names to API names
|
||||
const docTypeMap: Record<string, string> = {
|
||||
'Erwartungshorizont': 'EWH',
|
||||
'Aufgabe': 'Aufgabe',
|
||||
'Loesungshinweise': 'Material'
|
||||
}
|
||||
const docTypeMap: Record<string, string> = { 'Erwartungshorizont': 'EWH', 'Aufgabe': 'Aufgabe', 'Loesungshinweise': 'Material' }
|
||||
filters.doc_type = docTypeMap[dokumenttyp] || dokumenttyp
|
||||
}
|
||||
if (searchQuery) filters.search = searchQuery
|
||||
|
||||
const response = await korrekturApi.getArchivDocuments(filters)
|
||||
|
||||
// Map API response to frontend interface
|
||||
const mappedDokumente: AbiturDokument[] = response.documents.map((doc) => ({
|
||||
id: doc.id,
|
||||
dateiname: `${doc.subject}_${doc.niveau}_${doc.year}_${doc.doc_type}.pdf`,
|
||||
fach: doc.subject,
|
||||
jahr: doc.year,
|
||||
bundesland: doc.bundesland === 'NI' ? 'Niedersachsen' : doc.bundesland,
|
||||
niveau: doc.niveau,
|
||||
dokumenttyp: doc.doc_type === 'EWH' ? 'Erwartungshorizont' : doc.doc_type,
|
||||
aufgabentyp: doc.doc_type,
|
||||
thema: doc.title,
|
||||
download_url: doc.preview_url || SAMPLE_PDF,
|
||||
preview_url: doc.preview_url || SAMPLE_PDF,
|
||||
id: doc.id, dateiname: `${doc.subject}_${doc.niveau}_${doc.year}_${doc.doc_type}.pdf`,
|
||||
fach: doc.subject, jahr: doc.year, bundesland: doc.bundesland === 'NI' ? 'Niedersachsen' : doc.bundesland,
|
||||
niveau: doc.niveau, dokumenttyp: doc.doc_type === 'EWH' ? 'Erwartungshorizont' : doc.doc_type,
|
||||
aufgabentyp: doc.doc_type, thema: doc.title, download_url: doc.preview_url || SAMPLE_PDF, preview_url: doc.preview_url || SAMPLE_PDF,
|
||||
}))
|
||||
|
||||
// Update available filters from API
|
||||
if (response.filters) {
|
||||
setAvailableFilters({
|
||||
subjects: response.filters.subjects || ['Deutsch', 'Englisch', 'Mathematik'],
|
||||
@@ -616,163 +110,50 @@ export default function ArchivPage() {
|
||||
})
|
||||
}
|
||||
|
||||
// If API returns documents, use them
|
||||
if (mappedDokumente.length > 0) {
|
||||
setDokumente(mappedDokumente)
|
||||
} else {
|
||||
// API returned empty, use fallback mock data
|
||||
console.warn('API returned empty documents, using fallback data')
|
||||
throw new Error('Empty response')
|
||||
}
|
||||
|
||||
} catch (err) {
|
||||
console.warn('Failed to load from API, using fallback data:', err)
|
||||
// Fallback to mock data
|
||||
const mockDokumente: AbiturDokument[] = [
|
||||
{
|
||||
id: '1',
|
||||
dateiname: 'Deutsch_eA_2024_Aufgabe1.pdf',
|
||||
fach: 'Deutsch',
|
||||
jahr: 2024,
|
||||
bundesland: 'Niedersachsen',
|
||||
niveau: 'eA',
|
||||
dokumenttyp: 'Aufgabe',
|
||||
aufgabentyp: 'Textanalyse',
|
||||
thema: 'Textanalyse: "Der Prozess" - Kafka',
|
||||
download_url: SAMPLE_PDF,
|
||||
preview_url: SAMPLE_PDF,
|
||||
page_count: 4,
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
dateiname: 'Deutsch_eA_2024_EH1.pdf',
|
||||
fach: 'Deutsch',
|
||||
jahr: 2024,
|
||||
bundesland: 'Niedersachsen',
|
||||
niveau: 'eA',
|
||||
dokumenttyp: 'Erwartungshorizont',
|
||||
aufgabentyp: 'Textanalyse',
|
||||
thema: 'EH zu Kafka-Analyse',
|
||||
download_url: SAMPLE_PDF,
|
||||
preview_url: SAMPLE_PDF,
|
||||
page_count: 8,
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
dateiname: 'Deutsch_gA_2024_Aufgabe2.pdf',
|
||||
fach: 'Deutsch',
|
||||
jahr: 2024,
|
||||
bundesland: 'Niedersachsen',
|
||||
niveau: 'gA',
|
||||
dokumenttyp: 'Aufgabe',
|
||||
aufgabentyp: 'Gedichtanalyse',
|
||||
thema: 'Gedichtvergleich Romantik',
|
||||
download_url: SAMPLE_PDF,
|
||||
preview_url: SAMPLE_PDF,
|
||||
page_count: 3,
|
||||
},
|
||||
{
|
||||
id: '4',
|
||||
dateiname: 'Deutsch_eA_2023_Aufgabe1.pdf',
|
||||
fach: 'Deutsch',
|
||||
jahr: 2023,
|
||||
bundesland: 'Niedersachsen',
|
||||
niveau: 'eA',
|
||||
dokumenttyp: 'Aufgabe',
|
||||
aufgabentyp: 'Eroerterung',
|
||||
thema: 'Materialgestuetzte Eroerterung: Digitalisierung',
|
||||
download_url: SAMPLE_PDF,
|
||||
preview_url: SAMPLE_PDF,
|
||||
page_count: 5,
|
||||
},
|
||||
{
|
||||
id: '5',
|
||||
dateiname: 'Deutsch_eA_2023_EH1.pdf',
|
||||
fach: 'Deutsch',
|
||||
jahr: 2023,
|
||||
bundesland: 'Niedersachsen',
|
||||
niveau: 'eA',
|
||||
dokumenttyp: 'Erwartungshorizont',
|
||||
aufgabentyp: 'Eroerterung',
|
||||
thema: 'EH zu Digitalisierungs-Eroerterung',
|
||||
download_url: SAMPLE_PDF,
|
||||
preview_url: SAMPLE_PDF,
|
||||
page_count: 10,
|
||||
},
|
||||
{
|
||||
id: '6',
|
||||
dateiname: 'Deutsch_gA_2023_Aufgabe3.pdf',
|
||||
fach: 'Deutsch',
|
||||
jahr: 2023,
|
||||
bundesland: 'Niedersachsen',
|
||||
niveau: 'gA',
|
||||
dokumenttyp: 'Aufgabe',
|
||||
aufgabentyp: 'Dramenanalyse',
|
||||
thema: 'Szenenanalyse: "Faust I"',
|
||||
download_url: SAMPLE_PDF,
|
||||
preview_url: SAMPLE_PDF,
|
||||
page_count: 4,
|
||||
},
|
||||
]
|
||||
setDokumente(mockDokumente)
|
||||
setDokumente(MOCK_DOKUMENTE)
|
||||
} finally {
|
||||
setIsLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
loadDocuments()
|
||||
}, [fach, jahr, bundesland, niveau, dokumenttyp, searchQuery])
|
||||
|
||||
// Documents are now filtered by API, but keep local filtering for bundesland (API only filters NI)
|
||||
// and for fallback mock data, also apply search filter locally for mock data
|
||||
const filteredDokumente = useMemo(() => {
|
||||
return dokumente.filter((dok) => {
|
||||
// Bundesland filter (not yet supported by API)
|
||||
if (bundesland !== 'Alle' && dok.bundesland !== bundesland) return false
|
||||
// Search filter (for local mock data - API handles this server-side)
|
||||
if (searchQuery) {
|
||||
const query = searchQuery.toLowerCase()
|
||||
const matchesSearch =
|
||||
dok.thema?.toLowerCase().includes(query) ||
|
||||
dok.fach.toLowerCase().includes(query) ||
|
||||
dok.aufgabentyp?.toLowerCase().includes(query) ||
|
||||
dok.dateiname.toLowerCase().includes(query)
|
||||
const matchesSearch = dok.thema?.toLowerCase().includes(query) || dok.fach.toLowerCase().includes(query) ||
|
||||
dok.aufgabentyp?.toLowerCase().includes(query) || dok.dateiname.toLowerCase().includes(query)
|
||||
if (!matchesSearch) return false
|
||||
}
|
||||
return true
|
||||
})
|
||||
}, [dokumente, bundesland, searchQuery])
|
||||
|
||||
// Handle theme search
|
||||
const handleThemeClick = (theme: string) => {
|
||||
setSearchQuery(theme)
|
||||
setShowSuggestions(false)
|
||||
}
|
||||
const handleThemeClick = (theme: string) => { setSearchQuery(theme) }
|
||||
|
||||
// Handle use as template
|
||||
const handleUseAsTemplate = (dokument: AbiturDokument) => {
|
||||
setSelectedTemplate(dokument)
|
||||
setShowCreateModal(true)
|
||||
setCreateError(null)
|
||||
setSelectedTemplate(dokument); setShowCreateModal(true); setCreateError(null)
|
||||
}
|
||||
|
||||
// Create Klausur with template
|
||||
const handleCreateKlausur = async (title: string) => {
|
||||
if (!selectedTemplate) return
|
||||
setIsCreating(true)
|
||||
setCreateError(null)
|
||||
|
||||
setIsCreating(true); setCreateError(null)
|
||||
try {
|
||||
const newKlausur = await korrekturApi.createKlausur({
|
||||
title: title || `${selectedTemplate.fach} ${selectedTemplate.aufgabentyp || ''} ${selectedTemplate.jahr}`,
|
||||
subject: selectedTemplate.fach,
|
||||
year: selectedTemplate.jahr,
|
||||
semester: 'Abitur',
|
||||
modus: 'landes_abitur',
|
||||
subject: selectedTemplate.fach, year: selectedTemplate.jahr, semester: 'Abitur', modus: 'landes_abitur',
|
||||
})
|
||||
|
||||
setShowCreateModal(false)
|
||||
setSelectedTemplate(null)
|
||||
setShowCreateModal(false); setSelectedTemplate(null)
|
||||
router.push(`/korrektur/${newKlausur.id}`)
|
||||
} catch (err) {
|
||||
console.error('Failed to create klausur:', err)
|
||||
@@ -787,109 +168,54 @@ export default function ArchivPage() {
|
||||
}
|
||||
}
|
||||
|
||||
// Navigate to korrektur page without creating (fallback)
|
||||
const handleGoToKorrektur = () => {
|
||||
setShowCreateModal(false)
|
||||
setSelectedTemplate(null)
|
||||
router.push('/korrektur')
|
||||
}
|
||||
const handleGoToKorrektur = () => { setShowCreateModal(false); setSelectedTemplate(null); router.push('/korrektur') }
|
||||
|
||||
const activeFilters = [fach, jahr, bundesland, niveau, dokumenttyp].filter(f => f !== 'Alle').length
|
||||
|
||||
// Computed filter options with "Alle" prefix
|
||||
const FAECHER = useMemo(() => ['Alle', ...availableFilters.subjects], [availableFilters.subjects])
|
||||
const JAHRE = useMemo(() => ['Alle', ...availableFilters.years.map(String)], [availableFilters.years])
|
||||
const NIVEAUS = useMemo(() => ['Alle', ...availableFilters.niveaus], [availableFilters.niveaus])
|
||||
const DOKUMENTTYPEN = useMemo(() => ['Alle', ...availableFilters.doc_types.map(t => t === 'EWH' ? 'Erwartungshorizont' : t)], [availableFilters.doc_types])
|
||||
|
||||
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'
|
||||
}`}>
|
||||
<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 */}
|
||||
<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>
|
||||
<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-6">
|
||||
<div>
|
||||
<div className="flex items-center gap-3 mb-2">
|
||||
<button
|
||||
onClick={() => router.push('/korrektur')}
|
||||
className={`p-2 rounded-xl transition-colors ${isDark ? 'hover:bg-white/10' : 'hover:bg-slate-200'}`}
|
||||
>
|
||||
<svg className={`w-5 h-5 ${isDark ? 'text-white/60' : 'text-slate-600'}`} 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 onClick={() => router.push('/korrektur')} className={`p-2 rounded-xl transition-colors ${isDark ? 'hover:bg-white/10' : 'hover:bg-slate-200'}`}>
|
||||
<svg className={`w-5 h-5 ${isDark ? 'text-white/60' : 'text-slate-600'}`} 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>
|
||||
<h1 className={`text-3xl font-bold ${isDark ? 'text-white' : 'text-slate-900'}`}>Abitur-Archiv</h1>
|
||||
</div>
|
||||
<p className={isDark ? 'text-white/50' : 'text-slate-500'}>
|
||||
Zentralabitur-Materialien 2021-2025 durchsuchen
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<ThemeToggle />
|
||||
<LanguageDropdown />
|
||||
<p className={isDark ? 'text-white/50' : 'text-slate-500'}>Zentralabitur-Materialien 2021-2025 durchsuchen</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-3"><ThemeToggle /><LanguageDropdown /></div>
|
||||
</div>
|
||||
|
||||
{/* Search Bar */}
|
||||
<GlassCard className="mb-6" size="md" delay={100} isDark={isDark}>
|
||||
<div className="relative">
|
||||
<div className="flex items-center gap-3">
|
||||
<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="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
|
||||
</svg>
|
||||
<input
|
||||
type="text"
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
onFocus={() => setShowSuggestions(true)}
|
||||
onBlur={() => setTimeout(() => setShowSuggestions(false), 200)}
|
||||
placeholder="Thema suchen... z.B. Gedichtanalyse, Romantik, Kafka"
|
||||
className={`flex-1 bg-transparent border-none outline-none text-lg ${
|
||||
isDark ? 'text-white placeholder-white/40' : 'text-slate-900 placeholder-slate-400'
|
||||
}`}
|
||||
/>
|
||||
<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="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" /></svg>
|
||||
<input type="text" value={searchQuery} onChange={(e) => setSearchQuery(e.target.value)} placeholder="Thema suchen... z.B. Gedichtanalyse, Romantik, Kafka" className={`flex-1 bg-transparent border-none outline-none text-lg ${isDark ? 'text-white placeholder-white/40' : 'text-slate-900 placeholder-slate-400'}`} />
|
||||
{searchQuery && (
|
||||
<button
|
||||
onClick={() => setSearchQuery('')}
|
||||
className={`p-1 rounded-lg ${isDark ? 'hover:bg-white/10' : 'hover:bg-slate-200'}`}
|
||||
>
|
||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
<button onClick={() => setSearchQuery('')} className={`p-1 rounded-lg ${isDark ? 'hover:bg-white/10' : 'hover:bg-slate-200'}`}>
|
||||
<svg className="w-4 h-4" 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>
|
||||
|
||||
{/* Popular Themes */}
|
||||
<div className="flex flex-wrap gap-2 mt-4">
|
||||
{POPULAR_THEMES.map((theme) => (
|
||||
<button
|
||||
key={theme}
|
||||
onClick={() => handleThemeClick(theme)}
|
||||
className={`px-3 py-1.5 rounded-full text-sm transition-colors ${
|
||||
searchQuery === theme
|
||||
? 'bg-purple-500 text-white'
|
||||
: isDark
|
||||
? 'bg-white/10 text-white/70 hover:bg-white/20'
|
||||
: 'bg-slate-200 text-slate-600 hover:bg-slate-300'
|
||||
}`}
|
||||
>
|
||||
{theme}
|
||||
</button>
|
||||
<button key={theme} onClick={() => handleThemeClick(theme)} className={`px-3 py-1.5 rounded-full text-sm transition-colors ${searchQuery === theme ? 'bg-purple-500 text-white' : isDark ? 'bg-white/10 text-white/70 hover:bg-white/20' : 'bg-slate-200 text-slate-600 hover:bg-slate-300'}`}>{theme}</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
@@ -903,27 +229,12 @@ export default function ArchivPage() {
|
||||
<FilterDropdown label="Bundesland" value={bundesland} options={BUNDESLAENDER} onChange={setBundesland} isDark={isDark} />
|
||||
<FilterDropdown label="Niveau" value={niveau} options={NIVEAUS} onChange={setNiveau} isDark={isDark} />
|
||||
<FilterDropdown label="Typ" value={dokumenttyp} options={DOKUMENTTYPEN} onChange={setDokumenttyp} isDark={isDark} />
|
||||
|
||||
{activeFilters > 0 && (
|
||||
<button
|
||||
onClick={() => {
|
||||
setFach('Alle')
|
||||
setJahr('Alle')
|
||||
setBundesland('Alle')
|
||||
setNiveau('Alle')
|
||||
setDokumenttyp('Alle')
|
||||
}}
|
||||
className={`px-3 py-2 rounded-xl text-sm transition-colors ${
|
||||
isDark ? 'text-purple-400 hover:bg-purple-500/20' : 'text-purple-600 hover:bg-purple-100'
|
||||
}`}
|
||||
>
|
||||
<button onClick={() => { setFach('Alle'); setJahr('Alle'); setBundesland('Alle'); setNiveau('Alle'); setDokumenttyp('Alle') }} className={`px-3 py-2 rounded-xl text-sm transition-colors ${isDark ? 'text-purple-400 hover:bg-purple-500/20' : 'text-purple-600 hover:bg-purple-100'}`}>
|
||||
Filter zuruecksetzen ({activeFilters})
|
||||
</button>
|
||||
)}
|
||||
|
||||
<div className={`ml-auto text-sm ${isDark ? 'text-white/50' : 'text-slate-500'}`}>
|
||||
{filteredDokumente.length} Dokumente
|
||||
</div>
|
||||
<div className={`ml-auto text-sm ${isDark ? 'text-white/50' : 'text-slate-500'}`}>{filteredDokumente.length} Dokumente</div>
|
||||
</div>
|
||||
</GlassCard>
|
||||
|
||||
@@ -938,9 +249,7 @@ export default function ArchivPage() {
|
||||
{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>
|
||||
<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>
|
||||
</div>
|
||||
</GlassCard>
|
||||
@@ -950,21 +259,11 @@ export default function ArchivPage() {
|
||||
{!isLoading && (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
{filteredDokumente.map((dok, index) => (
|
||||
<DokumentCard
|
||||
key={dok.id}
|
||||
dokument={dok}
|
||||
onPreview={() => setPreviewDokument(dok)}
|
||||
onUseAsTemplate={() => handleUseAsTemplate(dok)}
|
||||
delay={200 + index * 50}
|
||||
isDark={isDark}
|
||||
/>
|
||||
<DokumentCard key={dok.id} dokument={dok} onPreview={() => setPreviewDokument(dok)} onUseAsTemplate={() => handleUseAsTemplate(dok)} delay={200 + index * 50} isDark={isDark} />
|
||||
))}
|
||||
|
||||
{filteredDokumente.length === 0 && !isLoading && (
|
||||
<div className={`col-span-full text-center py-12 ${isDark ? 'text-white/50' : 'text-slate-500'}`}>
|
||||
<svg className="w-16 h-16 mx-auto mb-4 opacity-50" 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>
|
||||
<svg className="w-16 h-16 mx-auto mb-4 opacity-50" 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 className="text-lg">Keine Dokumente gefunden</p>
|
||||
<p className="text-sm mt-1">Versuchen Sie andere Filtereinstellungen</p>
|
||||
</div>
|
||||
@@ -973,28 +272,10 @@ export default function ArchivPage() {
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Preview Modal */}
|
||||
<PreviewModal
|
||||
dokument={previewDokument}
|
||||
onClose={() => setPreviewDokument(null)}
|
||||
onUseAsTemplate={() => {
|
||||
if (previewDokument) handleUseAsTemplate(previewDokument)
|
||||
setPreviewDokument(null)
|
||||
}}
|
||||
isDark={isDark}
|
||||
/>
|
||||
<PreviewModal dokument={previewDokument} onClose={() => setPreviewDokument(null)} onUseAsTemplate={() => { if (previewDokument) handleUseAsTemplate(previewDokument); setPreviewDokument(null) }} isDark={isDark} />
|
||||
|
||||
{/* Create Klausur Modal */}
|
||||
{showCreateModal && selectedTemplate && (
|
||||
<CreateKlausurFromTemplateModal
|
||||
template={selectedTemplate}
|
||||
onClose={() => { setShowCreateModal(false); setSelectedTemplate(null); setCreateError(null) }}
|
||||
onCreate={handleCreateKlausur}
|
||||
onFallback={handleGoToKorrektur}
|
||||
isLoading={isCreating}
|
||||
error={createError}
|
||||
isDark={isDark}
|
||||
/>
|
||||
<CreateKlausurFromTemplateModal template={selectedTemplate} onClose={() => { setShowCreateModal(false); setSelectedTemplate(null); setCreateError(null) }} onCreate={handleCreateKlausur} onFallback={handleGoToKorrektur} isLoading={isCreating} error={createError} isDark={isDark} />
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user