Services: Admin-Lehrer, Backend-Lehrer, Studio v2, Website, Klausur-Service, School-Service, Voice-Service, Geo-Service, BreakPilot Drive, Agent-Core Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
406 lines
16 KiB
TypeScript
406 lines
16 KiB
TypeScript
'use client'
|
|
|
|
import { useState, useMemo } from 'react'
|
|
import { useTheme } from '@/lib/ThemeContext'
|
|
|
|
interface Document {
|
|
id: string
|
|
name: string
|
|
type: string
|
|
size: number
|
|
uploadedAt: Date
|
|
category?: string
|
|
tags?: string[]
|
|
url?: string
|
|
}
|
|
|
|
interface DocumentSpaceProps {
|
|
documents: Document[]
|
|
onDelete?: (id: string) => void
|
|
onRename?: (id: string, newName: string) => void
|
|
onOpen?: (doc: Document) => void
|
|
className?: string
|
|
}
|
|
|
|
const formatFileSize = (bytes: number): string => {
|
|
if (bytes === 0) return '0 B'
|
|
const k = 1024
|
|
const sizes = ['B', 'KB', 'MB', 'GB']
|
|
const i = Math.floor(Math.log(bytes) / Math.log(k))
|
|
return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i]
|
|
}
|
|
|
|
const formatDate = (date: Date): string => {
|
|
return new Intl.DateTimeFormat('de-DE', {
|
|
day: '2-digit',
|
|
month: '2-digit',
|
|
year: 'numeric',
|
|
hour: '2-digit',
|
|
minute: '2-digit'
|
|
}).format(date)
|
|
}
|
|
|
|
export function DocumentSpace({
|
|
documents,
|
|
onDelete,
|
|
onRename,
|
|
onOpen,
|
|
className = ''
|
|
}: DocumentSpaceProps) {
|
|
const { isDark } = useTheme()
|
|
const [searchQuery, setSearchQuery] = useState('')
|
|
const [filterType, setFilterType] = useState<string>('all')
|
|
const [sortBy, setSortBy] = useState<'name' | 'date' | 'size'>('date')
|
|
const [sortOrder, setSortOrder] = useState<'asc' | 'desc'>('desc')
|
|
const [viewMode, setViewMode] = useState<'grid' | 'list'>('list')
|
|
const [editingId, setEditingId] = useState<string | null>(null)
|
|
const [editName, setEditName] = useState('')
|
|
const [previewDoc, setPreviewDoc] = useState<Document | null>(null)
|
|
|
|
// Filtertypen ermitteln
|
|
const fileTypes = useMemo(() => {
|
|
const types = new Set(documents.map(d => d.type.split('/')[1] || d.type))
|
|
return ['all', ...Array.from(types)]
|
|
}, [documents])
|
|
|
|
// Dokumente filtern und sortieren
|
|
const filteredDocuments = useMemo(() => {
|
|
let filtered = [...documents]
|
|
|
|
// Suchfilter
|
|
if (searchQuery) {
|
|
const query = searchQuery.toLowerCase()
|
|
filtered = filtered.filter(d =>
|
|
d.name.toLowerCase().includes(query) ||
|
|
d.tags?.some(t => t.toLowerCase().includes(query))
|
|
)
|
|
}
|
|
|
|
// Typfilter
|
|
if (filterType !== 'all') {
|
|
filtered = filtered.filter(d =>
|
|
d.type.includes(filterType)
|
|
)
|
|
}
|
|
|
|
// Sortieren
|
|
filtered.sort((a, b) => {
|
|
let cmp = 0
|
|
switch (sortBy) {
|
|
case 'name':
|
|
cmp = a.name.localeCompare(b.name)
|
|
break
|
|
case 'date':
|
|
cmp = new Date(a.uploadedAt).getTime() - new Date(b.uploadedAt).getTime()
|
|
break
|
|
case 'size':
|
|
cmp = a.size - b.size
|
|
break
|
|
}
|
|
return sortOrder === 'asc' ? cmp : -cmp
|
|
})
|
|
|
|
return filtered
|
|
}, [documents, searchQuery, filterType, sortBy, sortOrder])
|
|
|
|
const handleStartRename = (doc: Document) => {
|
|
setEditingId(doc.id)
|
|
setEditName(doc.name.replace(/\.[^/.]+$/, ''))
|
|
}
|
|
|
|
const handleSaveRename = (doc: Document) => {
|
|
if (editName.trim() && onRename) {
|
|
const ext = doc.name.split('.').pop()
|
|
onRename(doc.id, `${editName.trim()}.${ext}`)
|
|
}
|
|
setEditingId(null)
|
|
setEditName('')
|
|
}
|
|
|
|
const getFileIcon = (type: string) => {
|
|
if (type.includes('pdf')) return '📄'
|
|
if (type.includes('image')) return '🖼️'
|
|
if (type.includes('word') || type.includes('doc')) return '📝'
|
|
if (type.includes('sheet') || type.includes('excel')) return '📊'
|
|
return '📎'
|
|
}
|
|
|
|
if (documents.length === 0) {
|
|
return (
|
|
<div className={`${className} text-center py-12`}>
|
|
<div className={`w-16 h-16 mx-auto rounded-2xl flex items-center justify-center mb-4 ${
|
|
isDark ? 'bg-white/10' : 'bg-slate-100'
|
|
}`}>
|
|
<span className="text-3xl">📁</span>
|
|
</div>
|
|
<h3 className={`text-lg font-medium ${isDark ? 'text-white' : 'text-slate-900'}`}>
|
|
Noch keine Dokumente
|
|
</h3>
|
|
<p className={`text-sm mt-2 ${isDark ? 'text-white/50' : 'text-slate-500'}`}>
|
|
Laden Sie Ihr erstes Dokument hoch, um loszulegen.
|
|
</p>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
return (
|
|
<div className={`space-y-4 ${className}`}>
|
|
{/* Toolbar */}
|
|
<div className="flex flex-wrap items-center gap-4">
|
|
{/* Suche */}
|
|
<div className="relative flex-1 min-w-[200px]">
|
|
<input
|
|
type="text"
|
|
value={searchQuery}
|
|
onChange={(e) => setSearchQuery(e.target.value)}
|
|
placeholder="Dokumente durchsuchen..."
|
|
className={`w-full pl-10 pr-4 py-2 rounded-xl border text-sm ${
|
|
isDark
|
|
? 'bg-white/5 border-white/10 text-white placeholder-white/40'
|
|
: 'bg-white border-slate-200 text-slate-900 placeholder-slate-400'
|
|
}`}
|
|
/>
|
|
<svg className={`absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 ${
|
|
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>
|
|
|
|
{/* Filter */}
|
|
<select
|
|
value={filterType}
|
|
onChange={(e) => setFilterType(e.target.value)}
|
|
className={`px-3 py-2 rounded-xl border text-sm ${
|
|
isDark
|
|
? 'bg-white/5 border-white/10 text-white'
|
|
: 'bg-white border-slate-200 text-slate-900'
|
|
}`}
|
|
>
|
|
{fileTypes.map(type => (
|
|
<option key={type} value={type}>
|
|
{type === 'all' ? 'Alle Typen' : type.toUpperCase()}
|
|
</option>
|
|
))}
|
|
</select>
|
|
|
|
{/* Sortierung */}
|
|
<select
|
|
value={`${sortBy}-${sortOrder}`}
|
|
onChange={(e) => {
|
|
const [by, order] = e.target.value.split('-')
|
|
setSortBy(by as 'name' | 'date' | 'size')
|
|
setSortOrder(order as 'asc' | 'desc')
|
|
}}
|
|
className={`px-3 py-2 rounded-xl border text-sm ${
|
|
isDark
|
|
? 'bg-white/5 border-white/10 text-white'
|
|
: 'bg-white border-slate-200 text-slate-900'
|
|
}`}
|
|
>
|
|
<option value="date-desc">Neueste zuerst</option>
|
|
<option value="date-asc">Aelteste zuerst</option>
|
|
<option value="name-asc">Name A-Z</option>
|
|
<option value="name-desc">Name Z-A</option>
|
|
<option value="size-desc">Groesste zuerst</option>
|
|
<option value="size-asc">Kleinste zuerst</option>
|
|
</select>
|
|
|
|
{/* Ansicht */}
|
|
<div className={`flex rounded-xl border overflow-hidden ${
|
|
isDark ? 'border-white/10' : 'border-slate-200'
|
|
}`}>
|
|
<button
|
|
onClick={() => setViewMode('list')}
|
|
className={`p-2 ${
|
|
viewMode === 'list'
|
|
? isDark ? 'bg-white/20 text-white' : 'bg-slate-200 text-slate-900'
|
|
: isDark ? 'text-white/60' : 'text-slate-500'
|
|
}`}
|
|
>
|
|
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 6h16M4 12h16M4 18h16" />
|
|
</svg>
|
|
</button>
|
|
<button
|
|
onClick={() => setViewMode('grid')}
|
|
className={`p-2 ${
|
|
viewMode === 'grid'
|
|
? isDark ? 'bg-white/20 text-white' : 'bg-slate-200 text-slate-900'
|
|
: isDark ? 'text-white/60' : 'text-slate-500'
|
|
}`}
|
|
>
|
|
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 6a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2V6zM14 6a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2V6zM4 16a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2v-2zM14 16a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2v-2z" />
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Ergebnisse */}
|
|
<div className={`text-xs ${isDark ? 'text-white/40' : 'text-slate-400'}`}>
|
|
{filteredDocuments.length} Dokument{filteredDocuments.length !== 1 ? 'e' : ''} gefunden
|
|
</div>
|
|
|
|
{/* Dokumentliste */}
|
|
{viewMode === 'list' ? (
|
|
<div className={`rounded-2xl border overflow-hidden ${
|
|
isDark ? 'bg-white/5 border-white/10' : 'bg-white border-slate-200'
|
|
}`}>
|
|
<div className="divide-y divide-slate-200 dark:divide-white/10">
|
|
{filteredDocuments.map((doc) => (
|
|
<div
|
|
key={doc.id}
|
|
className={`p-4 flex items-center gap-4 cursor-pointer ${
|
|
isDark ? 'hover:bg-white/5' : 'hover:bg-slate-50'
|
|
}`}
|
|
onClick={() => setPreviewDoc(doc)}
|
|
>
|
|
<div className={`w-10 h-10 rounded-xl flex items-center justify-center text-xl ${
|
|
isDark ? 'bg-white/10' : 'bg-slate-100'
|
|
}`}>
|
|
{getFileIcon(doc.type)}
|
|
</div>
|
|
<div className="flex-1 min-w-0">
|
|
{editingId === doc.id ? (
|
|
<div className="flex items-center gap-2" onClick={(e) => e.stopPropagation()}>
|
|
<input
|
|
type="text"
|
|
value={editName}
|
|
onChange={(e) => setEditName(e.target.value)}
|
|
onKeyDown={(e) => e.key === 'Enter' && handleSaveRename(doc)}
|
|
onBlur={() => handleSaveRename(doc)}
|
|
autoFocus
|
|
className={`flex-1 px-2 py-1 rounded border text-sm ${
|
|
isDark
|
|
? 'bg-white/10 border-white/20 text-white'
|
|
: 'bg-white border-slate-300 text-slate-900'
|
|
}`}
|
|
/>
|
|
</div>
|
|
) : (
|
|
<p className={`font-medium truncate ${isDark ? 'text-white' : 'text-slate-900'}`}>
|
|
{doc.name}
|
|
</p>
|
|
)}
|
|
<p className={`text-xs ${isDark ? 'text-white/40' : 'text-slate-400'}`}>
|
|
{formatFileSize(doc.size)} · {formatDate(new Date(doc.uploadedAt))}
|
|
</p>
|
|
</div>
|
|
<div className="flex items-center gap-1" onClick={(e) => e.stopPropagation()}>
|
|
<button
|
|
onClick={() => handleStartRename(doc)}
|
|
className={`p-2 rounded-lg ${isDark ? 'hover:bg-white/10' : 'hover:bg-slate-100'}`}
|
|
title="Umbenennen"
|
|
>
|
|
<svg className={`w-4 h-4 ${isDark ? 'text-white/50' : 'text-slate-400'}`} fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" />
|
|
</svg>
|
|
</button>
|
|
<button
|
|
onClick={() => onDelete?.(doc.id)}
|
|
className={`p-2 rounded-lg ${isDark ? 'hover:bg-red-500/20' : 'hover:bg-red-100'}`}
|
|
title="Loeschen"
|
|
>
|
|
<svg className={`w-4 h-4 ${isDark ? 'text-white/50 hover:text-red-300' : 'text-slate-400 hover:text-red-600'}`} fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
) : (
|
|
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4">
|
|
{filteredDocuments.map((doc) => (
|
|
<div
|
|
key={doc.id}
|
|
className={`rounded-2xl border p-4 cursor-pointer transition-all hover:scale-105 ${
|
|
isDark
|
|
? 'bg-white/5 border-white/10 hover:bg-white/10'
|
|
: 'bg-white border-slate-200 hover:shadow-lg'
|
|
}`}
|
|
onClick={() => setPreviewDoc(doc)}
|
|
>
|
|
<div className={`w-full aspect-square rounded-xl flex items-center justify-center mb-3 ${
|
|
isDark ? 'bg-white/10' : 'bg-slate-100'
|
|
}`}>
|
|
<span className="text-4xl">{getFileIcon(doc.type)}</span>
|
|
</div>
|
|
<p className={`font-medium text-sm truncate ${isDark ? 'text-white' : 'text-slate-900'}`}>
|
|
{doc.name}
|
|
</p>
|
|
<p className={`text-xs mt-1 ${isDark ? 'text-white/40' : 'text-slate-400'}`}>
|
|
{formatFileSize(doc.size)}
|
|
</p>
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
|
|
{/* Vorschau-Modal */}
|
|
{previewDoc && previewDoc.url && (
|
|
<div className="fixed inset-0 z-50 flex items-center justify-center p-4">
|
|
<div
|
|
className="absolute inset-0 bg-black/70 backdrop-blur-sm"
|
|
onClick={() => setPreviewDoc(null)}
|
|
/>
|
|
<div className={`relative w-full max-w-4xl max-h-[90vh] rounded-3xl overflow-hidden ${
|
|
isDark ? 'bg-slate-900' : 'bg-white'
|
|
}`}>
|
|
{/* Header */}
|
|
<div className={`flex items-center justify-between p-4 border-b ${
|
|
isDark ? 'border-white/10' : 'border-slate-200'
|
|
}`}>
|
|
<div className="flex items-center gap-3">
|
|
<span className="text-2xl">{getFileIcon(previewDoc.type)}</span>
|
|
<div>
|
|
<h3 className={`font-semibold ${isDark ? 'text-white' : 'text-slate-900'}`}>
|
|
{previewDoc.name}
|
|
</h3>
|
|
<p className={`text-xs ${isDark ? 'text-white/50' : 'text-slate-500'}`}>
|
|
{formatFileSize(previewDoc.size)} · {formatDate(new Date(previewDoc.uploadedAt))}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
<button
|
|
onClick={() => setPreviewDoc(null)}
|
|
className={`p-2 rounded-lg ${isDark ? 'hover:bg-white/10' : 'hover:bg-slate-100'}`}
|
|
>
|
|
<svg className={`w-6 h-6 ${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>
|
|
|
|
{/* Vorschau-Inhalt */}
|
|
<div className={`p-4 overflow-auto max-h-[calc(90vh-120px)] ${
|
|
isDark ? 'bg-slate-800' : 'bg-slate-50'
|
|
}`}>
|
|
{previewDoc.type.includes('image') ? (
|
|
<img
|
|
src={previewDoc.url}
|
|
alt={previewDoc.name}
|
|
className="max-w-full h-auto mx-auto rounded-lg shadow-lg"
|
|
/>
|
|
) : previewDoc.type.includes('pdf') ? (
|
|
<iframe
|
|
src={previewDoc.url}
|
|
className="w-full h-[70vh] rounded-lg"
|
|
title={previewDoc.name}
|
|
/>
|
|
) : (
|
|
<div className={`text-center py-12 ${isDark ? 'text-white/50' : 'text-slate-500'}`}>
|
|
<span className="text-6xl block mb-4">{getFileIcon(previewDoc.type)}</span>
|
|
<p>Vorschau fuer diesen Dateityp nicht verfuegbar</p>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
)
|
|
}
|