A previous `git pull --rebase origin main` dropped 177 local commits,
losing 3400+ files across admin-v2, backend, studio-v2, website,
klausur-service, and many other services. The partial restore attempt
(660295e2) only recovered some files.
This commit restores all missing files from pre-rebase ref 98933f5e
while preserving post-rebase additions (night-scheduler, night-mode UI,
NightModeWidget dashboard integration).
Restored features include:
- AI Module Sidebar (FAB), OCR Labeling, OCR Compare
- GPU Dashboard, RAG Pipeline, Magic Help
- Klausur-Korrektur (8 files), Abitur-Archiv (5+ files)
- Companion, Zeugnisse-Crawler, Screen Flow
- Full backend, studio-v2, website, klausur-service
- All compliance SDKs, agent-core, voice-service
- CI/CD configs, documentation, scripts
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
947 lines
43 KiB
TypeScript
947 lines
43 KiB
TypeScript
'use client'
|
||
|
||
import { useState, useEffect, useCallback } from 'react'
|
||
import { useRouter } from 'next/navigation'
|
||
import { BPIcon } from '@/components/Logo'
|
||
import { useLanguage } from '@/lib/LanguageContext'
|
||
import { useTheme } from '@/lib/ThemeContext'
|
||
import { useAlerts, getImportanceColor, getRelativeTime } from '@/lib/AlertsContext'
|
||
import { useMessages, formatMessageTime, getContactInitials } from '@/lib/MessagesContext'
|
||
import { useActivity, formatDurationCompact } from '@/lib/ActivityContext'
|
||
import { LanguageDropdown } from '@/components/LanguageDropdown'
|
||
import { ThemeToggle } from '@/components/ThemeToggle'
|
||
import { Footer } from '@/components/Footer'
|
||
import { Sidebar } from '@/components/Sidebar'
|
||
import { OnboardingWizard, OnboardingData } from '@/components/OnboardingWizard'
|
||
import { DocumentUpload } from '@/components/DocumentUpload'
|
||
import { QRCodeUpload } from '@/components/QRCodeUpload'
|
||
import { DocumentSpace } from '@/components/DocumentSpace'
|
||
import { ChatOverlay } from '@/components/ChatOverlay'
|
||
import { AiPrompt } from '@/components/AiPrompt'
|
||
|
||
// LocalStorage Keys
|
||
const ONBOARDING_KEY = 'bp_onboarding_complete'
|
||
const USER_DATA_KEY = 'bp_user_data'
|
||
const DOCUMENTS_KEY = 'bp_documents'
|
||
const FIRST_VISIT_KEY = 'bp_first_dashboard_visit'
|
||
const SESSION_ID_KEY = 'bp_session_id'
|
||
|
||
// BreakPilot Studio v2 - Glassmorphism Design
|
||
|
||
interface StoredDocument {
|
||
id: string
|
||
name: string
|
||
type: string
|
||
size: number
|
||
uploadedAt: Date
|
||
url?: string
|
||
}
|
||
|
||
export default function HomePage() {
|
||
const router = useRouter()
|
||
const [selectedTab, setSelectedTab] = useState('dashboard')
|
||
const [showOnboarding, setShowOnboarding] = useState<boolean | null>(null)
|
||
const [userData, setUserData] = useState<OnboardingData | null>(null)
|
||
const [documents, setDocuments] = useState<StoredDocument[]>([])
|
||
const [showUploadModal, setShowUploadModal] = useState(false)
|
||
const [showQRModal, setShowQRModal] = useState(false)
|
||
const [isFirstVisit, setIsFirstVisit] = useState(false)
|
||
const [sessionId, setSessionId] = useState<string>('')
|
||
const [showAlertsDropdown, setShowAlertsDropdown] = useState(false)
|
||
const { t } = useLanguage()
|
||
const { isDark } = useTheme()
|
||
const { alerts, unreadCount, markAsRead } = useAlerts()
|
||
const { conversations, unreadCount: messagesUnreadCount, contacts, markAsRead: markMessageAsRead } = useMessages()
|
||
const { stats: activityStats } = useActivity()
|
||
|
||
// Funktion zum Laden von Uploads aus der API
|
||
const fetchUploadsFromAPI = useCallback(async (sid: string) => {
|
||
if (!sid) return
|
||
try {
|
||
const response = await fetch(`/api/uploads?sessionId=${encodeURIComponent(sid)}`)
|
||
if (response.ok) {
|
||
const data = await response.json()
|
||
if (data.uploads && data.uploads.length > 0) {
|
||
// Konvertiere API-Uploads zu StoredDocument Format
|
||
const apiDocs: StoredDocument[] = data.uploads.map((u: any) => ({
|
||
id: u.id,
|
||
name: u.name,
|
||
type: u.type,
|
||
size: u.size,
|
||
uploadedAt: new Date(u.uploadedAt),
|
||
url: u.dataUrl // Data URL direkt verwenden
|
||
}))
|
||
// Merge mit existierenden Dokumenten (ohne Duplikate)
|
||
setDocuments(prev => {
|
||
const existingIds = new Set(prev.map(d => d.id))
|
||
const newDocs = apiDocs.filter(d => !existingIds.has(d.id))
|
||
if (newDocs.length > 0) {
|
||
return [...prev, ...newDocs]
|
||
}
|
||
return prev
|
||
})
|
||
}
|
||
}
|
||
} catch (error) {
|
||
console.error('Error fetching uploads:', error)
|
||
}
|
||
}, [])
|
||
|
||
// Prüfe beim Laden, ob Onboarding abgeschlossen ist
|
||
useEffect(() => {
|
||
const onboardingComplete = localStorage.getItem(ONBOARDING_KEY)
|
||
const storedUserData = localStorage.getItem(USER_DATA_KEY)
|
||
const storedDocs = localStorage.getItem(DOCUMENTS_KEY)
|
||
const firstVisit = localStorage.getItem(FIRST_VISIT_KEY)
|
||
let storedSessionId = localStorage.getItem(SESSION_ID_KEY)
|
||
|
||
// Session ID generieren falls nicht vorhanden
|
||
if (!storedSessionId) {
|
||
storedSessionId = `session-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`
|
||
localStorage.setItem(SESSION_ID_KEY, storedSessionId)
|
||
}
|
||
setSessionId(storedSessionId)
|
||
|
||
if (onboardingComplete === 'true' && storedUserData) {
|
||
setUserData(JSON.parse(storedUserData))
|
||
setShowOnboarding(false)
|
||
|
||
// Dokumente laden
|
||
if (storedDocs) {
|
||
setDocuments(JSON.parse(storedDocs))
|
||
}
|
||
|
||
// Erster Dashboard-Besuch nach Onboarding?
|
||
if (!firstVisit) {
|
||
setIsFirstVisit(true)
|
||
localStorage.setItem(FIRST_VISIT_KEY, 'true')
|
||
}
|
||
|
||
// Initialer Fetch von der API
|
||
fetchUploadsFromAPI(storedSessionId)
|
||
} else {
|
||
setShowOnboarding(true)
|
||
}
|
||
}, [fetchUploadsFromAPI])
|
||
|
||
// Polling fuer neue Uploads von der API (alle 3 Sekunden)
|
||
useEffect(() => {
|
||
if (!sessionId || showOnboarding) return
|
||
|
||
const interval = setInterval(() => {
|
||
fetchUploadsFromAPI(sessionId)
|
||
}, 3000)
|
||
|
||
return () => clearInterval(interval)
|
||
}, [sessionId, showOnboarding, fetchUploadsFromAPI])
|
||
|
||
// Dokumente in localStorage speichern
|
||
useEffect(() => {
|
||
if (documents.length > 0) {
|
||
localStorage.setItem(DOCUMENTS_KEY, JSON.stringify(documents))
|
||
}
|
||
}, [documents])
|
||
|
||
// Handler fuer neue Uploads
|
||
const handleUploadComplete = (uploadedDocs: any[]) => {
|
||
const newDocs: StoredDocument[] = uploadedDocs.map(d => ({
|
||
id: d.id,
|
||
name: d.name,
|
||
type: d.type,
|
||
size: d.size,
|
||
uploadedAt: d.uploadedAt,
|
||
url: d.url
|
||
}))
|
||
setDocuments(prev => [...prev, ...newDocs])
|
||
setIsFirstVisit(false)
|
||
}
|
||
|
||
// Dokument loeschen (aus State und API)
|
||
const handleDeleteDocument = async (id: string) => {
|
||
setDocuments(prev => prev.filter(d => d.id !== id))
|
||
// Auch aus API loeschen
|
||
try {
|
||
await fetch(`/api/uploads?id=${encodeURIComponent(id)}`, { method: 'DELETE' })
|
||
} catch (error) {
|
||
console.error('Error deleting from API:', error)
|
||
}
|
||
}
|
||
|
||
// Dokument umbenennen
|
||
const handleRenameDocument = (id: string, newName: string) => {
|
||
setDocuments(prev => prev.map(d => d.id === id ? { ...d, name: newName } : d))
|
||
}
|
||
|
||
// Onboarding abschließen
|
||
const handleOnboardingComplete = (data: OnboardingData) => {
|
||
localStorage.setItem(ONBOARDING_KEY, 'true')
|
||
localStorage.setItem(USER_DATA_KEY, JSON.stringify(data))
|
||
setUserData(data)
|
||
setShowOnboarding(false)
|
||
}
|
||
|
||
// Zeige Ladebildschirm während der Prüfung
|
||
if (showOnboarding === null) {
|
||
return (
|
||
<div className={`min-h-screen flex items-center justify-center ${
|
||
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="flex items-center gap-4">
|
||
<BPIcon variant="cupertino" size={48} className="animate-pulse" />
|
||
<span className={`text-xl font-medium ${isDark ? 'text-white' : 'text-slate-900'}`}>
|
||
Laden...
|
||
</span>
|
||
</div>
|
||
</div>
|
||
)
|
||
}
|
||
|
||
// Zeige Onboarding falls noch nicht abgeschlossen
|
||
if (showOnboarding) {
|
||
return <OnboardingWizard onComplete={handleOnboardingComplete} />
|
||
}
|
||
|
||
// Ab hier: Dashboard (bestehender Code)
|
||
|
||
// Calculate time saved from activity tracking
|
||
const timeSaved = formatDurationCompact(activityStats.weekSavedSeconds)
|
||
const timeSavedDisplay = activityStats.weekSavedSeconds > 0
|
||
? `${timeSaved.value}${timeSaved.unit}`
|
||
: '0min'
|
||
|
||
const stats = [
|
||
{ labelKey: 'stat_open_corrections', value: '12', icon: '📋', color: 'from-blue-400 to-blue-600' },
|
||
{ labelKey: 'stat_completed_week', value: String(activityStats.activityCount), icon: '✅', color: 'from-green-400 to-green-600' },
|
||
{ labelKey: 'stat_average', value: '2.3', icon: '📈', color: 'from-purple-400 to-purple-600' },
|
||
{ labelKey: 'stat_time_saved', value: timeSavedDisplay, icon: '⏱', color: 'from-orange-400 to-orange-600' },
|
||
]
|
||
|
||
const recentKlausuren = [
|
||
{ id: 1, title: 'Deutsch LK - Textanalyse', students: 24, completed: 18, statusKey: 'status_in_progress' },
|
||
{ id: 2, title: 'Deutsch GK - Erörterung', students: 28, completed: 28, statusKey: 'status_completed' },
|
||
{ id: 3, title: 'Vorabitur - Gedichtanalyse', students: 22, completed: 10, statusKey: 'status_in_progress' },
|
||
]
|
||
|
||
return (
|
||
<div className={`min-h-screen relative overflow-hidden flex flex-col ${
|
||
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 inset-0 overflow-hidden pointer-events-none">
|
||
<div className={`absolute -top-40 -right-40 w-80 h-80 rounded-full mix-blend-multiply filter blur-3xl animate-blob ${
|
||
isDark ? 'bg-purple-500 opacity-70' : 'bg-purple-300 opacity-50'
|
||
}`} />
|
||
<div className={`absolute -bottom-40 -left-40 w-80 h-80 rounded-full mix-blend-multiply filter blur-3xl animate-blob animation-delay-2000 ${
|
||
isDark ? 'bg-blue-500 opacity-70' : 'bg-blue-300 opacity-50'
|
||
}`} />
|
||
<div className={`absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 w-80 h-80 rounded-full mix-blend-multiply filter blur-3xl animate-blob animation-delay-4000 ${
|
||
isDark ? 'bg-pink-500 opacity-70' : 'bg-pink-300 opacity-50'
|
||
}`} />
|
||
</div>
|
||
|
||
<div className="relative z-10 flex min-h-screen gap-6 p-4">
|
||
{/* Sidebar */}
|
||
<Sidebar selectedTab={selectedTab} onTabChange={setSelectedTab} />
|
||
|
||
{/* ============================================
|
||
ARBEITSFLAECHE (Main Content)
|
||
============================================ */}
|
||
<main className="flex-1">
|
||
{/* Kopfleiste (Header) */}
|
||
<div className="flex items-center justify-between mb-8">
|
||
<div>
|
||
<h1 className={`text-4xl font-bold mb-2 ${isDark ? 'text-white' : 'text-slate-900'}`}>{t('dashboard')}</h1>
|
||
<p className={isDark ? 'text-white/60' : 'text-slate-600'}>{t('dashboard_subtitle')}</p>
|
||
</div>
|
||
|
||
{/* Search, Language & Actions */}
|
||
<div className="flex items-center gap-4">
|
||
<div className="relative">
|
||
<input
|
||
type="text"
|
||
placeholder={t('search_placeholder')}
|
||
className={`backdrop-blur-xl border rounded-2xl px-5 py-3 pl-12 focus:outline-none focus:ring-2 w-64 transition-all ${
|
||
isDark
|
||
? 'bg-white/10 border-white/20 text-white placeholder-white/40 focus:ring-white/30'
|
||
: 'bg-white/70 border-black/10 text-slate-900 placeholder-slate-400 focus:ring-indigo-300'
|
||
}`}
|
||
/>
|
||
<svg className={`absolute left-4 top-1/2 -translate-y-1/2 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>
|
||
</div>
|
||
|
||
{/* Language Dropdown */}
|
||
<LanguageDropdown />
|
||
|
||
{/* Theme Toggle */}
|
||
<ThemeToggle />
|
||
|
||
{/* Notifications Bell with Glow Effect */}
|
||
<div className="relative">
|
||
<button
|
||
onClick={() => setShowAlertsDropdown(!showAlertsDropdown)}
|
||
className={`relative p-3 backdrop-blur-xl border rounded-2xl transition-all ${
|
||
unreadCount > 0
|
||
? 'animate-pulse bg-gradient-to-r from-amber-500/20 to-orange-500/20 border-amber-500/30 shadow-lg shadow-amber-500/30'
|
||
: isDark
|
||
? 'bg-white/10 border-white/20 hover:bg-white/20'
|
||
: 'bg-black/5 border-black/10 hover:bg-black/10'
|
||
} ${isDark ? 'text-white' : 'text-slate-700'}`}
|
||
>
|
||
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6.002 6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 6 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9" />
|
||
</svg>
|
||
{unreadCount > 0 && (
|
||
<span className="absolute -top-1 -right-1 w-5 h-5 bg-red-500 rounded-full text-xs text-white flex items-center justify-center font-medium">
|
||
{unreadCount > 9 ? '9+' : unreadCount}
|
||
</span>
|
||
)}
|
||
</button>
|
||
|
||
{/* Alerts Dropdown */}
|
||
{showAlertsDropdown && (
|
||
<>
|
||
<div className="fixed inset-0 z-40" onClick={() => setShowAlertsDropdown(false)} />
|
||
<div className={`absolute right-0 mt-2 w-80 rounded-2xl border shadow-xl z-50 ${
|
||
isDark
|
||
? 'bg-slate-900 border-white/20'
|
||
: 'bg-white border-slate-200'
|
||
}`}>
|
||
<div className={`p-4 border-b ${isDark ? 'border-white/10' : 'border-slate-100'}`}>
|
||
<div className="flex items-center justify-between">
|
||
<h3 className={`font-semibold ${isDark ? 'text-white' : 'text-slate-900'}`}>
|
||
Aktuelle Alerts
|
||
</h3>
|
||
{unreadCount > 0 && (
|
||
<span className="px-2 py-0.5 text-xs font-medium rounded-full bg-amber-500/20 text-amber-500">
|
||
{unreadCount} neu
|
||
</span>
|
||
)}
|
||
</div>
|
||
</div>
|
||
<div className="max-h-80 overflow-y-auto">
|
||
{alerts.slice(0, 5).map(alert => (
|
||
<button
|
||
key={alert.id}
|
||
onClick={() => {
|
||
markAsRead(alert.id)
|
||
setShowAlertsDropdown(false)
|
||
router.push('/alerts')
|
||
}}
|
||
className={`w-full text-left p-4 transition-all ${
|
||
isDark
|
||
? `hover:bg-white/5 ${!alert.isRead ? 'bg-amber-500/5 border-l-2 border-amber-500' : ''}`
|
||
: `hover:bg-slate-50 ${!alert.isRead ? 'bg-amber-50 border-l-2 border-amber-500' : ''}`
|
||
}`}
|
||
>
|
||
<div className="flex items-start gap-3">
|
||
<span className={`px-1.5 py-0.5 rounded text-xs font-medium ${getImportanceColor(alert.importance, isDark)}`}>
|
||
{alert.importance.slice(0, 4)}
|
||
</span>
|
||
<div className="flex-1 min-w-0">
|
||
<p className={`text-sm font-medium truncate ${isDark ? 'text-white' : 'text-slate-900'}`}>
|
||
{alert.title}
|
||
</p>
|
||
<p className={`text-xs ${isDark ? 'text-white/40' : 'text-slate-400'}`}>
|
||
{getRelativeTime(alert.timestamp)}
|
||
</p>
|
||
</div>
|
||
</div>
|
||
</button>
|
||
))}
|
||
{alerts.length === 0 && (
|
||
<div className={`p-8 text-center ${isDark ? 'text-white/40' : 'text-slate-400'}`}>
|
||
<span className="text-2xl block mb-2">📭</span>
|
||
<p className="text-sm">Keine Alerts</p>
|
||
</div>
|
||
)}
|
||
</div>
|
||
<div className={`p-3 border-t ${isDark ? 'border-white/10' : 'border-slate-100'}`}>
|
||
<button
|
||
onClick={() => {
|
||
setShowAlertsDropdown(false)
|
||
router.push('/alerts')
|
||
}}
|
||
className={`w-full py-2 text-sm font-medium rounded-lg transition-all ${
|
||
isDark
|
||
? 'text-amber-400 hover:bg-amber-500/10'
|
||
: 'text-amber-600 hover:bg-amber-50'
|
||
}`}
|
||
>
|
||
Alle Alerts anzeigen →
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</>
|
||
)}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Willkommensnachricht fuer ersten Besuch */}
|
||
{isFirstVisit && documents.length === 0 && (
|
||
<div className={`mb-8 p-6 rounded-3xl border backdrop-blur-xl ${
|
||
isDark
|
||
? 'bg-gradient-to-r from-purple-500/20 to-pink-500/20 border-purple-500/30'
|
||
: 'bg-gradient-to-r from-purple-50 to-pink-50 border-purple-200'
|
||
}`}>
|
||
<div className="flex items-start gap-6">
|
||
<div className={`w-16 h-16 rounded-2xl flex items-center justify-center text-3xl ${
|
||
isDark ? 'bg-white/20' : 'bg-white shadow-lg'
|
||
}`}>
|
||
🎉
|
||
</div>
|
||
<div className="flex-1">
|
||
<h2 className={`text-xl font-bold mb-2 ${isDark ? 'text-white' : 'text-slate-900'}`}>
|
||
Willkommen bei BreakPilot Studio!
|
||
</h2>
|
||
<p className={`mb-4 ${isDark ? 'text-white/70' : 'text-slate-600'}`}>
|
||
Grossartig, dass Sie hier sind! Laden Sie jetzt Ihr erstes Dokument hoch,
|
||
um die KI-gestuetzte Korrektur zu erleben. Sie koennen Dateien von Ihrem
|
||
Computer oder Mobiltelefon hochladen.
|
||
</p>
|
||
<div className="flex gap-3">
|
||
<button
|
||
onClick={() => setShowUploadModal(true)}
|
||
className="px-6 py-3 bg-gradient-to-r from-purple-500 to-pink-500 text-white rounded-xl font-medium hover:shadow-lg hover:shadow-purple-500/30 transition-all hover:scale-105"
|
||
>
|
||
Dokument hochladen
|
||
</button>
|
||
<button
|
||
onClick={() => setShowQRModal(true)}
|
||
className={`px-6 py-3 rounded-xl font-medium transition-all ${
|
||
isDark
|
||
? 'bg-white/20 text-white hover:bg-white/30'
|
||
: 'bg-white text-slate-700 hover:bg-slate-50 shadow'
|
||
}`}
|
||
>
|
||
Mit Mobiltelefon hochladen
|
||
</button>
|
||
</div>
|
||
</div>
|
||
<button
|
||
onClick={() => setIsFirstVisit(false)}
|
||
className={`p-2 rounded-lg ${isDark ? 'hover:bg-white/10' : 'hover:bg-white'}`}
|
||
>
|
||
<svg className={`w-5 h-5 ${isDark ? 'text-white/60' : 'text-slate-400'}`} 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>
|
||
</div>
|
||
)}
|
||
|
||
{/* KI-Assistent */}
|
||
<AiPrompt />
|
||
|
||
{/* Stats Kacheln */}
|
||
<div className="grid grid-cols-4 gap-6 mb-8">
|
||
{stats.map((stat, index) => (
|
||
<div
|
||
key={index}
|
||
className={`backdrop-blur-xl border rounded-3xl p-6 transition-all hover:scale-105 hover:shadow-xl cursor-pointer ${
|
||
isDark
|
||
? 'bg-white/10 border-white/20 hover:bg-white/15'
|
||
: 'bg-white/70 border-black/10 hover:bg-white/90 shadow-lg'
|
||
}`}
|
||
>
|
||
<div className="flex items-center justify-between mb-4">
|
||
<div className={`w-12 h-12 bg-gradient-to-br ${stat.color} rounded-2xl flex items-center justify-center text-2xl shadow-lg`}>
|
||
{stat.icon}
|
||
</div>
|
||
<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>
|
||
<p className={`text-sm mb-1 ${isDark ? 'text-white/60' : 'text-slate-600'}`}>{t(stat.labelKey)}</p>
|
||
<p className={`text-3xl font-bold ${isDark ? 'text-white' : 'text-slate-900'}`}>{stat.value}</p>
|
||
</div>
|
||
))}
|
||
</div>
|
||
|
||
{/* Tab-Content */}
|
||
{selectedTab === 'dokumente' ? (
|
||
/* Dokumente-Tab */
|
||
<div className="space-y-6">
|
||
{/* Upload-Optionen */}
|
||
<div className="grid grid-cols-2 gap-6">
|
||
<button
|
||
onClick={() => setShowUploadModal(true)}
|
||
className={`p-6 rounded-3xl border backdrop-blur-xl text-left transition-all hover:scale-105 ${
|
||
isDark
|
||
? 'bg-white/10 border-white/20 hover:bg-white/15'
|
||
: 'bg-white/70 border-black/10 hover:bg-white/90 shadow-lg'
|
||
}`}
|
||
>
|
||
<div className={`w-14 h-14 rounded-2xl flex items-center justify-center text-3xl mb-4 ${
|
||
isDark ? 'bg-blue-500/20' : 'bg-blue-100'
|
||
}`}>
|
||
📤
|
||
</div>
|
||
<h3 className={`text-lg font-semibold mb-2 ${isDark ? 'text-white' : 'text-slate-900'}`}>
|
||
Direkt hochladen
|
||
</h3>
|
||
<p className={`text-sm ${isDark ? 'text-white/60' : 'text-slate-500'}`}>
|
||
Ziehen Sie Dateien hierher oder klicken Sie zum Auswaehlen
|
||
</p>
|
||
</button>
|
||
|
||
<button
|
||
onClick={() => setShowQRModal(true)}
|
||
className={`p-6 rounded-3xl border backdrop-blur-xl text-left transition-all hover:scale-105 ${
|
||
isDark
|
||
? 'bg-white/10 border-white/20 hover:bg-white/15'
|
||
: 'bg-white/70 border-black/10 hover:bg-white/90 shadow-lg'
|
||
}`}
|
||
>
|
||
<div className={`w-14 h-14 rounded-2xl flex items-center justify-center text-3xl mb-4 ${
|
||
isDark ? 'bg-purple-500/20' : 'bg-purple-100'
|
||
}`}>
|
||
📱
|
||
</div>
|
||
<h3 className={`text-lg font-semibold mb-2 ${isDark ? 'text-white' : 'text-slate-900'}`}>
|
||
Mit Mobiltelefon hochladen
|
||
</h3>
|
||
<p className={`text-sm ${isDark ? 'text-white/60' : 'text-slate-500'}`}>
|
||
QR-Code scannen (nur im lokalen Netzwerk)
|
||
</p>
|
||
</button>
|
||
</div>
|
||
|
||
{/* Document Space */}
|
||
<div className={`rounded-3xl border backdrop-blur-xl p-6 ${
|
||
isDark
|
||
? 'bg-white/10 border-white/20'
|
||
: 'bg-white/70 border-black/10 shadow-lg'
|
||
}`}>
|
||
<h2 className={`text-xl font-semibold mb-6 ${isDark ? 'text-white' : 'text-slate-900'}`}>
|
||
Meine Dokumente
|
||
</h2>
|
||
<DocumentSpace
|
||
documents={documents}
|
||
onDelete={handleDeleteDocument}
|
||
onRename={handleRenameDocument}
|
||
onOpen={(doc) => doc.url && window.open(doc.url, '_blank')}
|
||
/>
|
||
</div>
|
||
</div>
|
||
) : (
|
||
/* Dashboard-Tab (Standard) */
|
||
<div className="grid grid-cols-3 gap-6">
|
||
{/* Aktuelle Klausuren Kachel */}
|
||
<div className={`col-span-2 backdrop-blur-xl border rounded-3xl p-6 ${
|
||
isDark
|
||
? 'bg-white/10 border-white/20'
|
||
: 'bg-white/70 border-black/10 shadow-lg'
|
||
}`}>
|
||
<div className="flex items-center justify-between mb-6">
|
||
<h2 className={`text-xl font-semibold ${isDark ? 'text-white' : 'text-slate-900'}`}>{t('recent_klausuren')}</h2>
|
||
<button className={`text-sm transition-colors ${
|
||
isDark ? 'text-white/60 hover:text-white' : 'text-slate-500 hover:text-slate-900'
|
||
}`}>
|
||
{t('show_all')} →
|
||
</button>
|
||
</div>
|
||
|
||
<div className="space-y-4">
|
||
{recentKlausuren.map((klausur) => (
|
||
<div
|
||
key={klausur.id}
|
||
className={`flex items-center gap-4 p-4 rounded-2xl transition-all cursor-pointer group ${
|
||
isDark
|
||
? 'bg-white/5 hover:bg-white/10'
|
||
: 'bg-slate-50 hover:bg-slate-100'
|
||
}`}
|
||
>
|
||
<div className="w-14 h-14 bg-gradient-to-br from-blue-400 to-purple-500 rounded-2xl flex items-center justify-center">
|
||
<span className="text-2xl">📝</span>
|
||
</div>
|
||
<div className="flex-1">
|
||
<h3 className={`font-medium ${isDark ? 'text-white' : 'text-slate-900'}`}>{klausur.title}</h3>
|
||
<p className={`text-sm ${isDark ? 'text-white/50' : 'text-slate-500'}`}>{klausur.students} {t('students')}</p>
|
||
</div>
|
||
<div className="text-right">
|
||
<span className={`px-3 py-1 rounded-full text-xs font-medium ${
|
||
klausur.statusKey === 'status_completed'
|
||
? isDark ? 'bg-green-500/20 text-green-300' : 'bg-green-100 text-green-700'
|
||
: isDark ? 'bg-yellow-500/20 text-yellow-300' : 'bg-yellow-100 text-yellow-700'
|
||
}`}>
|
||
{t(klausur.statusKey)}
|
||
</span>
|
||
<div className="flex items-center gap-2 mt-2">
|
||
<div className={`w-24 h-1.5 rounded-full overflow-hidden ${isDark ? 'bg-white/10' : 'bg-slate-200'}`}>
|
||
<div
|
||
className="h-full bg-gradient-to-r from-cyan-400 to-blue-500 rounded-full"
|
||
style={{ width: `${(klausur.completed / klausur.students) * 100}%` }}
|
||
/>
|
||
</div>
|
||
<span className={`text-xs ${isDark ? 'text-white/50' : 'text-slate-500'}`}>{klausur.completed}/{klausur.students}</span>
|
||
</div>
|
||
</div>
|
||
<svg className={`w-5 h-5 transition-colors ${
|
||
isDark ? 'text-white/30 group-hover:text-white/60' : 'text-slate-300 group-hover:text-slate-600'
|
||
}`} fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
|
||
</svg>
|
||
</div>
|
||
))}
|
||
</div>
|
||
</div>
|
||
|
||
{/* Schnellaktionen Kachel */}
|
||
<div className={`backdrop-blur-xl border rounded-3xl p-6 ${
|
||
isDark
|
||
? 'bg-white/10 border-white/20'
|
||
: 'bg-white/70 border-black/10 shadow-lg'
|
||
}`}>
|
||
<h2 className={`text-xl font-semibold mb-6 ${isDark ? 'text-white' : 'text-slate-900'}`}>{t('quick_actions')}</h2>
|
||
|
||
<div className="space-y-3">
|
||
<button className="w-full flex items-center gap-4 p-4 bg-gradient-to-r from-blue-500 to-purple-500 rounded-2xl text-white hover:shadow-xl hover:shadow-purple-500/30 transition-all hover:scale-105">
|
||
<span className="text-2xl">➕</span>
|
||
<span className="font-medium">{t('create_klausur')}</span>
|
||
</button>
|
||
|
||
<button
|
||
onClick={() => setShowUploadModal(true)}
|
||
className={`w-full flex items-center gap-4 p-4 rounded-2xl transition-all ${
|
||
isDark
|
||
? 'bg-white/10 text-white hover:bg-white/20'
|
||
: 'bg-slate-100 text-slate-800 hover:bg-slate-200'
|
||
}`}>
|
||
<span className="text-2xl">📤</span>
|
||
<span className="font-medium">{t('upload_work')}</span>
|
||
</button>
|
||
|
||
<button
|
||
onClick={() => setSelectedTab('dokumente')}
|
||
className={`w-full flex items-center justify-between p-4 rounded-2xl transition-all ${
|
||
isDark
|
||
? 'bg-white/10 text-white hover:bg-white/20'
|
||
: 'bg-slate-100 text-slate-800 hover:bg-slate-200'
|
||
}`}>
|
||
<div className="flex items-center gap-4">
|
||
<span className="text-2xl">📁</span>
|
||
<span className="font-medium">{t('nav_dokumente')}</span>
|
||
</div>
|
||
{documents.length > 0 && (
|
||
<span className={`px-2 py-1 text-xs font-medium rounded-full ${
|
||
isDark ? 'bg-white/20' : 'bg-slate-200'
|
||
}`}>
|
||
{documents.length}
|
||
</span>
|
||
)}
|
||
</button>
|
||
|
||
<button
|
||
onClick={() => router.push('/worksheet-editor')}
|
||
className={`w-full flex items-center gap-4 p-4 rounded-2xl transition-all ${
|
||
isDark
|
||
? 'bg-gradient-to-r from-purple-500/20 to-pink-500/20 text-white hover:from-purple-500/30 hover:to-pink-500/30 border border-purple-500/30'
|
||
: 'bg-gradient-to-r from-purple-50 to-pink-50 text-slate-800 hover:from-purple-100 hover:to-pink-100 border border-purple-200'
|
||
}`}>
|
||
<span className="text-2xl">🎨</span>
|
||
<span className="font-medium">{t('nav_worksheet_editor')}</span>
|
||
</button>
|
||
|
||
<button className={`w-full flex items-center gap-4 p-4 rounded-2xl transition-all ${
|
||
isDark
|
||
? 'bg-white/10 text-white hover:bg-white/20'
|
||
: 'bg-slate-100 text-slate-800 hover:bg-slate-200'
|
||
}`}>
|
||
<span className="text-2xl">✨</span>
|
||
<span className="font-medium">{t('magic_help')}</span>
|
||
</button>
|
||
|
||
<button className={`w-full flex items-center gap-4 p-4 rounded-2xl transition-all ${
|
||
isDark
|
||
? 'bg-white/10 text-white hover:bg-white/20'
|
||
: 'bg-slate-100 text-slate-800 hover:bg-slate-200'
|
||
}`}>
|
||
<span className="text-2xl">📊</span>
|
||
<span className="font-medium">{t('fairness_check')}</span>
|
||
</button>
|
||
</div>
|
||
|
||
{/* AI Insight mini */}
|
||
<div className={`mt-6 p-4 rounded-2xl border ${
|
||
isDark
|
||
? 'bg-gradient-to-r from-pink-500/20 to-purple-500/20 border-pink-500/30'
|
||
: 'bg-gradient-to-r from-pink-50 to-purple-50 border-pink-200'
|
||
}`}>
|
||
<div className="flex items-center gap-3 mb-2">
|
||
<span className="text-lg">🤖</span>
|
||
<span className={`text-sm font-medium ${isDark ? 'text-white/80' : 'text-slate-700'}`}>{t('ai_tip')}</span>
|
||
</div>
|
||
<p className={`text-sm ${isDark ? 'text-white/60' : 'text-slate-600'}`}>
|
||
{t('ai_tip_text')}
|
||
</p>
|
||
</div>
|
||
|
||
{/* Alerts Kachel */}
|
||
<div className={`mt-6 backdrop-blur-xl border rounded-2xl p-4 ${
|
||
isDark
|
||
? 'bg-gradient-to-r from-amber-500/10 to-orange-500/10 border-amber-500/30'
|
||
: 'bg-gradient-to-r from-amber-50 to-orange-50 border-amber-200'
|
||
}`}>
|
||
<div className="flex items-center justify-between mb-3">
|
||
<h3 className={`font-semibold flex items-center gap-2 ${isDark ? 'text-white' : 'text-slate-900'}`}>
|
||
<span>🔔</span> Aktuelle Alerts
|
||
</h3>
|
||
{unreadCount > 0 && (
|
||
<span className="text-xs px-2 py-1 rounded-full bg-amber-500/20 text-amber-500 font-medium">
|
||
{unreadCount} neu
|
||
</span>
|
||
)}
|
||
</div>
|
||
|
||
{/* Headlines Liste */}
|
||
<div className="space-y-2">
|
||
{alerts.slice(0, 3).map(alert => (
|
||
<button
|
||
key={alert.id}
|
||
onClick={() => {
|
||
markAsRead(alert.id)
|
||
router.push('/alerts')
|
||
}}
|
||
className={`w-full text-left p-2 rounded-lg transition-all text-sm ${
|
||
isDark
|
||
? `hover:bg-white/10 ${!alert.isRead ? 'bg-white/5' : ''}`
|
||
: `hover:bg-white ${!alert.isRead ? 'bg-white/50' : ''}`
|
||
}`}
|
||
>
|
||
<div className="flex items-start gap-2">
|
||
{!alert.isRead && (
|
||
<span className="w-1.5 h-1.5 rounded-full bg-amber-500 mt-1.5 flex-shrink-0" />
|
||
)}
|
||
<p className={`truncate ${isDark ? 'text-white/80' : 'text-slate-700'}`}>
|
||
{alert.title}
|
||
</p>
|
||
</div>
|
||
</button>
|
||
))}
|
||
{alerts.length === 0 && (
|
||
<p className={`text-sm ${isDark ? 'text-white/40' : 'text-slate-400'}`}>
|
||
Keine Alerts vorhanden
|
||
</p>
|
||
)}
|
||
</div>
|
||
|
||
{/* Mehr anzeigen */}
|
||
<button
|
||
onClick={() => router.push('/alerts')}
|
||
className={`w-full mt-3 text-sm font-medium ${
|
||
isDark ? 'text-amber-400 hover:text-amber-300' : 'text-amber-600 hover:text-amber-700'
|
||
}`}
|
||
>
|
||
Alle Alerts anzeigen →
|
||
</button>
|
||
</div>
|
||
|
||
{/* Nachrichten Kachel */}
|
||
<div className={`mt-6 backdrop-blur-xl border rounded-2xl p-4 ${
|
||
isDark
|
||
? 'bg-gradient-to-r from-green-500/10 to-emerald-500/10 border-green-500/30'
|
||
: 'bg-gradient-to-r from-green-50 to-emerald-50 border-green-200'
|
||
}`}>
|
||
<div className="flex items-center justify-between mb-3">
|
||
<h3 className={`font-semibold flex items-center gap-2 ${isDark ? 'text-white' : 'text-slate-900'}`}>
|
||
<span>💬</span> {t('nav_messages')}
|
||
</h3>
|
||
{messagesUnreadCount > 0 && (
|
||
<span className="text-xs px-2 py-1 rounded-full bg-green-500/20 text-green-500 font-medium">
|
||
{messagesUnreadCount} neu
|
||
</span>
|
||
)}
|
||
</div>
|
||
|
||
{/* Conversations Liste */}
|
||
<div className="space-y-2">
|
||
{conversations.slice(0, 3).map(conv => {
|
||
const contact = contacts.find(c => conv.participant_ids.includes(c.id))
|
||
return (
|
||
<button
|
||
key={conv.id}
|
||
onClick={() => {
|
||
if (conv.unread_count > 0) {
|
||
markMessageAsRead(conv.id)
|
||
}
|
||
router.push('/messages')
|
||
}}
|
||
className={`w-full text-left p-2 rounded-lg transition-all text-sm ${
|
||
isDark
|
||
? `hover:bg-white/10 ${conv.unread_count > 0 ? 'bg-white/5' : ''}`
|
||
: `hover:bg-white ${conv.unread_count > 0 ? 'bg-white/50' : ''}`
|
||
}`}
|
||
>
|
||
<div className="flex items-center gap-2">
|
||
{/* Avatar */}
|
||
<div className={`w-8 h-8 rounded-full flex items-center justify-center text-xs font-medium flex-shrink-0 ${
|
||
contact?.online
|
||
? isDark
|
||
? 'bg-green-500/30 text-green-300'
|
||
: 'bg-green-200 text-green-700'
|
||
: isDark
|
||
? 'bg-slate-600 text-slate-300'
|
||
: 'bg-slate-200 text-slate-600'
|
||
}`}>
|
||
{conv.title ? getContactInitials(conv.title) : '?'}
|
||
</div>
|
||
<div className="flex-1 min-w-0">
|
||
<div className="flex items-center gap-2">
|
||
{conv.unread_count > 0 && (
|
||
<span className="w-1.5 h-1.5 rounded-full bg-green-500 flex-shrink-0" />
|
||
)}
|
||
<span className={`font-medium truncate ${isDark ? 'text-white' : 'text-slate-900'}`}>
|
||
{conv.title || 'Unbenannt'}
|
||
</span>
|
||
</div>
|
||
{conv.last_message && (
|
||
<p className={`text-xs truncate ${isDark ? 'text-white/50' : 'text-slate-500'}`}>
|
||
{conv.last_message}
|
||
</p>
|
||
)}
|
||
</div>
|
||
{conv.last_message_time && (
|
||
<span className={`text-xs flex-shrink-0 ${isDark ? 'text-white/40' : 'text-slate-400'}`}>
|
||
{formatMessageTime(conv.last_message_time)}
|
||
</span>
|
||
)}
|
||
</div>
|
||
</button>
|
||
)
|
||
})}
|
||
{conversations.length === 0 && (
|
||
<p className={`text-sm ${isDark ? 'text-white/40' : 'text-slate-400'}`}>
|
||
Keine Nachrichten vorhanden
|
||
</p>
|
||
)}
|
||
</div>
|
||
|
||
{/* Mehr anzeigen */}
|
||
<button
|
||
onClick={() => router.push('/messages')}
|
||
className={`w-full mt-3 text-sm font-medium ${
|
||
isDark ? 'text-green-400 hover:text-green-300' : 'text-green-600 hover:text-green-700'
|
||
}`}
|
||
>
|
||
Alle Nachrichten anzeigen →
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)}
|
||
</main>
|
||
</div>
|
||
|
||
{/* Upload Modal */}
|
||
{showUploadModal && (
|
||
<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={() => setShowUploadModal(false)} />
|
||
<div className={`relative w-full max-w-2xl rounded-3xl border p-6 max-h-[90vh] overflow-y-auto ${
|
||
isDark
|
||
? 'bg-slate-900 border-white/20'
|
||
: 'bg-white border-slate-200 shadow-2xl'
|
||
}`}>
|
||
<div className="flex items-center justify-between mb-6">
|
||
<h2 className={`text-xl font-semibold ${isDark ? 'text-white' : 'text-slate-900'}`}>
|
||
Dokumente hochladen
|
||
</h2>
|
||
<button
|
||
onClick={() => setShowUploadModal(false)}
|
||
className={`p-2 rounded-lg ${isDark ? 'hover:bg-white/10' : 'hover:bg-slate-100'}`}
|
||
>
|
||
<svg className={`w-5 h-5 ${isDark ? 'text-white/60' : 'text-slate-400'}`} 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>
|
||
<DocumentUpload
|
||
onUploadComplete={(docs) => {
|
||
handleUploadComplete(docs)
|
||
}}
|
||
/>
|
||
{/* Aktions-Buttons */}
|
||
<div className={`mt-6 pt-6 border-t flex justify-between items-center ${
|
||
isDark ? 'border-white/10' : 'border-slate-200'
|
||
}`}>
|
||
<p className={`text-sm ${isDark ? 'text-white/50' : 'text-slate-500'}`}>
|
||
{documents.length > 0 ? `${documents.length} Dokument${documents.length !== 1 ? 'e' : ''} gespeichert` : 'Noch keine Dokumente'}
|
||
</p>
|
||
<div className="flex gap-3">
|
||
<button
|
||
onClick={() => setShowUploadModal(false)}
|
||
className={`px-4 py-2 rounded-xl text-sm font-medium ${
|
||
isDark ? 'text-white/60 hover:text-white' : 'text-slate-500 hover:text-slate-700'
|
||
}`}
|
||
>
|
||
Schliessen
|
||
</button>
|
||
<button
|
||
onClick={() => {
|
||
setShowUploadModal(false)
|
||
setSelectedTab('dokumente')
|
||
}}
|
||
className="px-4 py-2 bg-gradient-to-r from-purple-500 to-pink-500 text-white rounded-xl text-sm font-medium hover:shadow-lg transition-all"
|
||
>
|
||
Zu meinen Dokumenten
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{/* 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={sessionId}
|
||
onClose={() => setShowQRModal(false)}
|
||
/>
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{/* Diegetic Chat Overlay - Cinematic message notifications */}
|
||
<ChatOverlay
|
||
typewriterEnabled={true}
|
||
typewriterSpeed={25}
|
||
autoDismissMs={0}
|
||
maxQueue={5}
|
||
/>
|
||
|
||
{/* Footer */}
|
||
<Footer />
|
||
|
||
{/* Blob Animation Styles */}
|
||
<style jsx>{`
|
||
@keyframes blob {
|
||
0% { transform: translate(0px, 0px) scale(1); }
|
||
33% { transform: translate(30px, -50px) scale(1.1); }
|
||
66% { transform: translate(-20px, 20px) scale(0.9); }
|
||
100% { transform: translate(0px, 0px) scale(1); }
|
||
}
|
||
.animate-blob {
|
||
animation: blob 7s infinite;
|
||
}
|
||
.animation-delay-2000 {
|
||
animation-delay: 2s;
|
||
}
|
||
.animation-delay-4000 {
|
||
animation-delay: 4s;
|
||
}
|
||
`}</style>
|
||
</div>
|
||
)
|
||
}
|