fix: Restore all files lost during destructive rebase
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>
This commit is contained in:
@@ -34,9 +34,9 @@ export function Breadcrumbs() {
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Check meta modules
|
||||
// Check meta modules (but skip dashboard as it's already added)
|
||||
const metaModule = metaModules.find(m => m.href === `/${pathParts[0]}`)
|
||||
if (metaModule) {
|
||||
if (metaModule && metaModule.href !== '/dashboard') {
|
||||
items.push({ label: metaModule.name, href: metaModule.href })
|
||||
}
|
||||
}
|
||||
@@ -50,7 +50,7 @@ export function Breadcrumbs() {
|
||||
return (
|
||||
<nav className="flex items-center gap-2 text-sm text-slate-500 mb-4">
|
||||
{items.map((item, index) => (
|
||||
<span key={item.href} className="flex items-center gap-2">
|
||||
<span key={`${index}-${item.href}`} className="flex items-center gap-2">
|
||||
{index > 0 && (
|
||||
<svg className="w-4 h-4 text-slate-300" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
|
||||
|
||||
217
admin-v2/components/common/SkeletonText.tsx
Normal file
217
admin-v2/components/common/SkeletonText.tsx
Normal file
@@ -0,0 +1,217 @@
|
||||
/**
|
||||
* Skeleton Loading Components
|
||||
*
|
||||
* Animated placeholder components for loading states.
|
||||
* Used throughout the app for smoother UX during async operations.
|
||||
*/
|
||||
|
||||
import { ReactNode } from 'react'
|
||||
|
||||
interface SkeletonTextProps {
|
||||
/** Number of lines to display */
|
||||
lines?: number
|
||||
/** Width variants for varied line lengths */
|
||||
variant?: 'uniform' | 'varied' | 'paragraph'
|
||||
/** Custom class names */
|
||||
className?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Animated skeleton text placeholder
|
||||
*/
|
||||
export function SkeletonText({ lines = 3, variant = 'varied', className = '' }: SkeletonTextProps) {
|
||||
const getLineWidth = (index: number) => {
|
||||
if (variant === 'uniform') return 'w-full'
|
||||
if (variant === 'paragraph') {
|
||||
// Last line is shorter for paragraph effect
|
||||
if (index === lines - 1) return 'w-3/5'
|
||||
return 'w-full'
|
||||
}
|
||||
// Varied widths
|
||||
const widths = ['w-full', 'w-4/5', 'w-3/4', 'w-5/6', 'w-2/3']
|
||||
return widths[index % widths.length]
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={`space-y-3 ${className}`}>
|
||||
{Array.from({ length: lines }).map((_, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className={`h-4 bg-slate-200 rounded animate-pulse ${getLineWidth(i)}`}
|
||||
style={{ animationDelay: `${i * 100}ms` }}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
interface SkeletonBoxProps {
|
||||
/** Width (Tailwind class or px) */
|
||||
width?: string
|
||||
/** Height (Tailwind class or px) */
|
||||
height?: string
|
||||
/** Custom class names */
|
||||
className?: string
|
||||
/** Border radius */
|
||||
rounded?: 'none' | 'sm' | 'md' | 'lg' | 'xl' | 'full'
|
||||
}
|
||||
|
||||
/**
|
||||
* Animated skeleton box for images, avatars, cards
|
||||
*/
|
||||
export function SkeletonBox({
|
||||
width = 'w-full',
|
||||
height = 'h-32',
|
||||
className = '',
|
||||
rounded = 'lg'
|
||||
}: SkeletonBoxProps) {
|
||||
const roundedClass = {
|
||||
none: '',
|
||||
sm: 'rounded-sm',
|
||||
md: 'rounded-md',
|
||||
lg: 'rounded-lg',
|
||||
xl: 'rounded-xl',
|
||||
full: 'rounded-full'
|
||||
}[rounded]
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`bg-slate-200 animate-pulse ${width} ${height} ${roundedClass} ${className}`}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
interface SkeletonCardProps {
|
||||
/** Show image placeholder */
|
||||
showImage?: boolean
|
||||
/** Number of text lines */
|
||||
lines?: number
|
||||
/** Custom class names */
|
||||
className?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Skeleton card with optional image and text lines
|
||||
*/
|
||||
export function SkeletonCard({ showImage = true, lines = 3, className = '' }: SkeletonCardProps) {
|
||||
return (
|
||||
<div className={`bg-white rounded-xl shadow-sm border p-4 ${className}`}>
|
||||
{showImage && (
|
||||
<SkeletonBox height="h-32" className="mb-4" />
|
||||
)}
|
||||
<SkeletonBox width="w-2/3" height="h-5" className="mb-3" />
|
||||
<SkeletonText lines={lines} variant="paragraph" />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
interface SkeletonOCRResultProps {
|
||||
/** Custom class names */
|
||||
className?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Skeleton specifically for OCR results display
|
||||
* Shows loading state while OCR is processing
|
||||
*/
|
||||
export function SkeletonOCRResult({ className = '' }: SkeletonOCRResultProps) {
|
||||
return (
|
||||
<div className={`bg-slate-50 rounded-lg p-4 ${className}`}>
|
||||
<div className="flex items-center gap-2 mb-4">
|
||||
<div className="w-4 h-4 rounded-full bg-purple-200 animate-pulse" />
|
||||
<SkeletonBox width="w-32" height="h-4" rounded="md" />
|
||||
</div>
|
||||
|
||||
{/* Text area skeleton */}
|
||||
<div className="bg-white border p-3 rounded space-y-2 mb-4">
|
||||
<SkeletonText lines={4} variant="paragraph" />
|
||||
</div>
|
||||
|
||||
{/* Metrics grid skeleton */}
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
||||
{Array.from({ length: 4 }).map((_, i) => (
|
||||
<div key={i} className="bg-white border rounded p-2">
|
||||
<SkeletonBox width="w-16" height="h-3" className="mb-2" rounded="sm" />
|
||||
<SkeletonBox width="w-12" height="h-5" rounded="sm" />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
interface SkeletonWrapperProps {
|
||||
/** Whether to show skeleton or children */
|
||||
loading: boolean
|
||||
/** Content to show when not loading */
|
||||
children: ReactNode
|
||||
/** Skeleton component or elements to show */
|
||||
skeleton?: ReactNode
|
||||
/** Fallback skeleton lines (if skeleton prop not provided) */
|
||||
lines?: number
|
||||
/** Custom class names */
|
||||
className?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrapper component that toggles between skeleton and content
|
||||
*/
|
||||
export function SkeletonWrapper({
|
||||
loading,
|
||||
children,
|
||||
skeleton,
|
||||
lines = 3,
|
||||
className = ''
|
||||
}: SkeletonWrapperProps) {
|
||||
if (loading) {
|
||||
return skeleton ? <>{skeleton}</> : <SkeletonText lines={lines} className={className} />
|
||||
}
|
||||
return <>{children}</>
|
||||
}
|
||||
|
||||
interface SkeletonProgressProps {
|
||||
/** Animation speed in seconds */
|
||||
speed?: number
|
||||
/** Custom class names */
|
||||
className?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Animated progress skeleton with shimmer effect
|
||||
*/
|
||||
export function SkeletonProgress({ speed = 1.5, className = '' }: SkeletonProgressProps) {
|
||||
return (
|
||||
<div className={`relative overflow-hidden bg-slate-200 rounded-full h-2 ${className}`}>
|
||||
<div
|
||||
className="absolute inset-0 bg-gradient-to-r from-slate-200 via-slate-300 to-slate-200 animate-shimmer"
|
||||
style={{
|
||||
backgroundSize: '200% 100%',
|
||||
animation: `shimmer ${speed}s infinite linear`
|
||||
}}
|
||||
/>
|
||||
<style jsx>{`
|
||||
@keyframes shimmer {
|
||||
0% { background-position: 200% 0; }
|
||||
100% { background-position: -200% 0; }
|
||||
}
|
||||
`}</style>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Pulsing dot indicator for inline loading
|
||||
*/
|
||||
export function SkeletonDots({ className = '' }: { className?: string }) {
|
||||
return (
|
||||
<span className={`inline-flex items-center gap-1 ${className}`}>
|
||||
{[0, 1, 2].map((i) => (
|
||||
<span
|
||||
key={i}
|
||||
className="w-1.5 h-1.5 rounded-full bg-purple-500 animate-pulse"
|
||||
style={{ animationDelay: `${i * 200}ms` }}
|
||||
/>
|
||||
))}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user