Files
breakpilot-lehrer/studio-v2/app/dashboard-experimental/_components/DashboardWidgets.tsx
Benjamin Admin b4613e26f3 [split-required] Split 500-850 LOC files (batch 2)
backend-lehrer (10 files):
- game/database.py (785 → 5), correction_api.py (683 → 4)
- classroom_engine/antizipation.py (676 → 5)
- llm_gateway schools/edu_search already done in prior batch

klausur-service (12 files):
- orientation_crop_api.py (694 → 5), pdf_export.py (677 → 4)
- zeugnis_crawler.py (676 → 5), grid_editor_api.py (671 → 5)
- eh_templates.py (658 → 5), mail/api.py (651 → 5)
- qdrant_service.py (638 → 5), training_api.py (625 → 4)

website (6 pages):
- middleware (696 → 8), mail (733 → 6), consent (628 → 8)
- compliance/risks (622 → 5), export (502 → 5), brandbook (629 → 7)

studio-v2 (3 components):
- B2BMigrationWizard (848 → 3), CleanupPanel (765 → 2)
- dashboard-experimental (739 → 2)

admin-lehrer (4 files):
- uebersetzungen (769 → 4), manager (670 → 2)
- ChunkBrowserQA (675 → 6), dsfa/page (674 → 5)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-25 08:24:01 +02:00

301 lines
14 KiB
TypeScript

'use client'
import { useState, useEffect } from 'react'
import { usePerformance } from '@/lib/spatial-ui/PerformanceContext'
// =============================================================================
// GLASS CARD - Ultra Transparent
// =============================================================================
interface GlassCardProps {
children: React.ReactNode
className?: string
onClick?: () => void
size?: 'sm' | 'md' | 'lg'
delay?: number
}
export function GlassCard({ children, className = '', onClick, size = 'md', delay = 0 }: GlassCardProps) {
const { settings } = usePerformance()
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' }
const blur = settings.enableBlur ? 24 * settings.blurIntensity : 0
return (
<div
className={`rounded-3xl ${sizeClasses[size]} ${onClick ? 'cursor-pointer' : ''} ${className}`}
style={{
background: isHovered ? 'rgba(255, 255, 255, 0.12)' : 'rgba(255, 255, 255, 0.08)',
backdropFilter: blur > 0 ? `blur(${blur}px) saturate(180%)` : 'none',
WebkitBackdropFilter: blur > 0 ? `blur(${blur}px) saturate(180%)` : 'none',
border: '1px solid rgba(255, 255, 255, 0.1)',
boxShadow: '0 4px 24px rgba(0, 0, 0, 0.15), inset 0 1px 0 rgba(255, 255, 255, 0.05)',
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>
)
}
// =============================================================================
// ANALOG CLOCK
// =============================================================================
export function AnalogClock() {
const [time, setTime] = useState(new Date())
useEffect(() => {
const timer = setInterval(() => setTime(new Date()), 1000)
return () => clearInterval(timer)
}, [])
const hours = time.getHours() % 12
const minutes = time.getMinutes()
const seconds = time.getSeconds()
const hourDeg = (hours * 30) + (minutes * 0.5)
const minuteDeg = minutes * 6
const secondDeg = seconds * 6
return (
<div className="relative w-32 h-32">
<div className="absolute inset-0 rounded-full" style={{ background: 'rgba(255, 255, 255, 0.05)', border: '2px solid rgba(255, 255, 255, 0.15)' }}>
{[...Array(12)].map((_, i) => (
<div key={i} className="absolute w-1 h-3 bg-white/40 rounded-full" style={{ left: '50%', top: '8px', transform: `translateX(-50%) rotate(${i * 30}deg)`, transformOrigin: '50% 56px' }} />
))}
<div className="absolute w-1.5 h-10 bg-white rounded-full" style={{ left: '50%', bottom: '50%', transform: `translateX(-50%) rotate(${hourDeg}deg)`, transformOrigin: 'bottom center' }} />
<div className="absolute w-1 h-14 bg-white/80 rounded-full" style={{ left: '50%', bottom: '50%', transform: `translateX(-50%) rotate(${minuteDeg}deg)`, transformOrigin: 'bottom center' }} />
<div className="absolute w-0.5 h-14 bg-orange-400 rounded-full" style={{ left: '50%', bottom: '50%', transform: `translateX(-50%) rotate(${secondDeg}deg)`, transformOrigin: 'bottom center', transition: 'transform 0.1s ease-out' }} />
<div className="absolute w-3 h-3 bg-white rounded-full left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2" />
</div>
</div>
)
}
// =============================================================================
// COMPASS
// =============================================================================
export function Compass({ direction = 225 }: { direction?: number }) {
return (
<div className="relative w-24 h-24">
<div className="absolute inset-0 rounded-full" style={{ background: 'rgba(255, 255, 255, 0.05)', border: '2px solid rgba(255, 255, 255, 0.15)' }}>
<span className="absolute top-2 left-1/2 -translate-x-1/2 text-xs font-bold text-red-400">N</span>
<span className="absolute bottom-2 left-1/2 -translate-x-1/2 text-xs font-medium text-white/50">S</span>
<span className="absolute left-2 top-1/2 -translate-y-1/2 text-xs font-medium text-white/50">W</span>
<span className="absolute right-2 top-1/2 -translate-y-1/2 text-xs font-medium text-white/50">O</span>
<div className="absolute inset-4" style={{ transform: `rotate(${direction}deg)`, transition: 'transform 0.5s ease-out' }}>
<div className="absolute w-1.5 h-8 bg-gradient-to-t from-red-500 to-red-400 rounded-full" style={{ left: '50%', bottom: '50%', transform: 'translateX(-50%)', transformOrigin: 'bottom center' }} />
<div className="absolute w-1.5 h-8 bg-gradient-to-b from-white/80 to-white/40 rounded-full" style={{ left: '50%', top: '50%', transform: 'translateX(-50%)', transformOrigin: 'top center' }} />
</div>
<div className="absolute w-3 h-3 bg-white rounded-full left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2" />
</div>
</div>
)
}
// =============================================================================
// BAR CHART
// =============================================================================
interface BarChartProps {
data: { label: string; value: number; highlight?: boolean }[]
maxValue?: number
}
export function BarChart({ data, maxValue }: BarChartProps) {
const max = maxValue || Math.max(...data.map((d) => d.value))
return (
<div className="flex items-end justify-between gap-2 h-32">
{data.map((item, index) => {
const height = (item.value / max) * 100
return (
<div key={index} className="flex flex-col items-center gap-2 flex-1">
<span className="text-xs text-white/60 font-medium">{item.value}</span>
<div className="w-full rounded-lg transition-all duration-500" style={{
height: `${height}%`, minHeight: 8,
background: item.highlight ? 'linear-gradient(to top, rgba(96, 165, 250, 0.6), rgba(167, 139, 250, 0.6))' : 'rgba(255, 255, 255, 0.2)',
boxShadow: item.highlight ? '0 0 20px rgba(139, 92, 246, 0.3)' : 'none',
}} />
<span className="text-xs text-white/40">{item.label}</span>
</div>
)
})}
</div>
)
}
// =============================================================================
// TEMPERATURE DISPLAY
// =============================================================================
export function TemperatureDisplay({ temp, condition }: { temp: number; condition: string }) {
const conditionIcons: Record<string, string> = { sunny: '☀️', cloudy: '☁️', rainy: '🌧️', snowy: '🌨️', partly_cloudy: '⛅' }
return (
<div className="text-center">
<div className="text-6xl mb-2">{conditionIcons[condition] || '☀️'}</div>
<div className="flex items-start justify-center">
<span className="text-6xl font-extralight text-white">{temp}</span>
<span className="text-2xl text-white/60 mt-2">°C</span>
</div>
<p className="text-white/50 text-sm mt-1 capitalize">{condition.replace('_', ' ')}</p>
</div>
)
}
// =============================================================================
// PROGRESS RING
// =============================================================================
interface ProgressRingProps {
progress: number; size?: number; strokeWidth?: number; label: string; value: string; color?: string
}
export function ProgressRing({ progress, size = 80, strokeWidth = 6, label, value, color = '#a78bfa' }: ProgressRingProps) {
const radius = (size - strokeWidth) / 2
const circumference = radius * 2 * Math.PI
const offset = circumference - (progress / 100) * circumference
return (
<div className="flex flex-col items-center">
<div className="relative" style={{ width: size, height: size }}>
<svg width={size} height={size} className="transform -rotate-90">
<circle cx={size / 2} cy={size / 2} r={radius} fill="none" stroke="rgba(255, 255, 255, 0.1)" strokeWidth={strokeWidth} />
<circle cx={size / 2} cy={size / 2} r={radius} fill="none" stroke={color} strokeWidth={strokeWidth} strokeLinecap="round" strokeDasharray={circumference} strokeDashoffset={offset} style={{ transition: 'stroke-dashoffset 1s ease-out' }} />
</svg>
<div className="absolute inset-0 flex items-center justify-center">
<span className="text-lg font-light text-white">{value}</span>
</div>
</div>
<p className="text-white/40 text-xs mt-2 font-medium uppercase tracking-wide">{label}</p>
</div>
)
}
// =============================================================================
// STAT DISPLAY
// =============================================================================
export function StatDisplay({ value, unit, label, icon }: { value: string; unit?: string; label: string; icon?: string }) {
return (
<div className="text-center">
{icon && <div className="text-2xl mb-2 opacity-80">{icon}</div>}
<div className="flex items-baseline justify-center gap-1">
<span className="text-4xl font-light text-white">{value}</span>
{unit && <span className="text-lg text-white/50 font-light">{unit}</span>}
</div>
<p className="text-white/40 text-xs mt-1 font-medium uppercase tracking-wide">{label}</p>
</div>
)
}
// =============================================================================
// LIST ITEM
// =============================================================================
export function ListItem({ icon, title, subtitle, value, delay = 0 }: { icon: string; title: string; subtitle?: string; value?: string; delay?: number }) {
const [isVisible, setIsVisible] = useState(false)
const [isHovered, setIsHovered] = useState(false)
useEffect(() => {
const timer = setTimeout(() => setIsVisible(true), delay)
return () => clearTimeout(timer)
}, [delay])
return (
<div
className="flex items-center gap-4 p-3 rounded-2xl cursor-pointer transition-all"
style={{ background: isHovered ? 'rgba(255, 255, 255, 0.06)' : 'transparent', opacity: isVisible ? 1 : 0, transform: isVisible ? 'translateX(0)' : 'translateX(-10px)', transition: 'all 0.3s ease-out' }}
onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
>
<div className="w-10 h-10 rounded-xl flex items-center justify-center text-xl" style={{ background: 'rgba(255,255,255,0.08)' }}>{icon}</div>
<div className="flex-1">
<p className="text-white font-medium">{title}</p>
{subtitle && <p className="text-white/40 text-sm">{subtitle}</p>}
</div>
{value && <span className="text-white/50 font-medium">{value}</span>}
<svg className="w-4 h-4 text-white/30" fill="none" stroke="currentColor" viewBox="0 0 24 24" style={{ transform: isHovered ? 'translateX(2px)' : 'translateX(0)', transition: 'transform 0.2s' }}>
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
</svg>
</div>
)
}
// =============================================================================
// ACTION BUTTON
// =============================================================================
export function ActionButton({ icon, label, primary = false, onClick, delay = 0 }: { icon: string; label: string; primary?: boolean; onClick?: () => void; delay?: number }) {
const [isVisible, setIsVisible] = useState(false)
const [isPressed, setIsPressed] = useState(false)
useEffect(() => {
const timer = setTimeout(() => setIsVisible(true), delay)
return () => clearTimeout(timer)
}, [delay])
return (
<button
className="w-full flex items-center justify-center gap-3 p-4 rounded-2xl font-medium transition-all"
style={{
background: primary ? 'linear-gradient(135deg, rgba(96, 165, 250, 0.3), rgba(167, 139, 250, 0.3))' : 'rgba(255, 255, 255, 0.06)',
border: '1px solid rgba(255, 255, 255, 0.08)', color: 'white',
opacity: isVisible ? 1 : 0, transform: isVisible ? `translateY(0) scale(${isPressed ? 0.97 : 1})` : 'translateY(10px)',
transition: 'all 0.3s cubic-bezier(0.34, 1.56, 0.64, 1)',
}}
onMouseDown={() => setIsPressed(true)} onMouseUp={() => setIsPressed(false)} onMouseLeave={() => setIsPressed(false)}
onClick={onClick}
>
<span className="text-xl">{icon}</span>
<span>{label}</span>
</button>
)
}
// =============================================================================
// QUALITY INDICATOR
// =============================================================================
export function QualityIndicator() {
const { metrics, forceQuality } = usePerformance()
const [isExpanded, setIsExpanded] = useState(false)
return (
<div className="fixed bottom-6 left-6 z-50" style={{
background: 'rgba(0, 0, 0, 0.3)', backdropFilter: 'blur(20px)', WebkitBackdropFilter: 'blur(20px)',
border: '1px solid rgba(255, 255, 255, 0.08)', borderRadius: 16,
padding: isExpanded ? 16 : 12, minWidth: isExpanded ? 200 : 'auto', transition: 'all 0.3s ease-out',
}}>
<button onClick={() => setIsExpanded(!isExpanded)} className="flex items-center gap-3 text-white/70 text-sm">
<span className={`w-2 h-2 rounded-full ${metrics.qualityLevel === 'high' ? 'bg-green-400' : metrics.qualityLevel === 'medium' ? 'bg-yellow-400' : 'bg-red-400'}`} />
<span className="font-mono">{metrics.fps} FPS</span>
<span className="text-white/30">|</span>
<span className="uppercase text-xs tracking-wide">{metrics.qualityLevel}</span>
</button>
{isExpanded && (
<div className="mt-4 pt-4 border-t border-white/10 space-y-2">
<div className="flex gap-1">
{(['high', 'medium', 'low'] as const).map((level) => (
<button key={level} onClick={() => forceQuality(level)} className={`flex-1 py-1.5 rounded-lg text-xs font-medium transition-all ${metrics.qualityLevel === level ? 'bg-white/15 text-white' : 'bg-white/5 text-white/40 hover:bg-white/10'}`}>
{level[0].toUpperCase()}
</button>
))}
</div>
</div>
)}
</div>
)
}