import { useState, useEffect, useRef, useCallback } from 'react'; import { api, S } from '../lib.js'; import { useConfirm } from '../confirm.jsx'; import { UploadIcon, DownloadIcon, TrashIcon, SearchIcon, PlusIcon, ChevronIcon, DatabaseIcon } from '../icons.jsx'; const fmtSize = b => b<1024?`${b} B`:b<1024*1024?`${(b/1024).toFixed(1)} KB`:`${(b/(1024*1024)).toFixed(1)} MB`; const fmtDate = s => new Date(s).toLocaleDateString('de-DE',{day:'2-digit',month:'2-digit',year:'numeric'}); const extIcon = name => { const e = name.split('.').pop().toLowerCase(); if (['jpg','jpeg','png','gif','webp'].includes(e)) return 'πŸ–Ό'; if (e==='pdf') return 'πŸ“„'; if (['zip','rar','7z'].includes(e)) return 'πŸ“¦'; if (['mp4','mov','avi'].includes(e)) return '🎬'; if (['mp3','wav'].includes(e)) return '🎡'; if (['stl','3mf'].includes(e)) return 'πŸ–¨'; if (['txt','md'].includes(e)) return 'πŸ“'; if (['xlsx','csv'].includes(e)) return 'πŸ“Š'; if (['docx','doc'].includes(e)) return 'πŸ“ƒ'; return 'πŸ“Ž'; }; // ── Share Modal ─────────────────────────────────────────────────────────────── function ShareModal({ item, type, onClose, toast }) { const [username, setUsername] = useState(''); const [shares, setShares] = useState([]); const base = type==='folder' ? `/tools/dateien/folders/${item.id}` : `/tools/dateien/${item.id}`; useEffect(() => { api(`${base}/shares`).then(setShares).catch(()=>{}); }, [base]); const share = async () => { if (!username.trim()) return; try { await api(`${base}/share`,{body:{username:username.trim()}}); toast(`Geteilt βœ“`); setUsername(''); api(`${base}/shares`).then(setShares); } catch(e) { toast(e.message,'error'); } }; const unshare = async uid => { try { await api(`${base}/share/${uid}`,{method:'DELETE'}); setShares(p=>p.filter(s=>s.id!==uid)); toast('Freigabe entfernt'); } catch(e) { toast(e.message,'error'); } }; const isMobile = window.innerWidth<768; return (
e.target===e.currentTarget&&onClose()}>
{isMobile&&
}
{type==='folder'?'πŸ“':'πŸ“Ž'} {item.name||item.originalname} teilen
setUsername(e.target.value)} onKeyDown={e=>e.key==='Enter'&&share()} placeholder="Benutzername" autoCapitalize="none" style={{...S.inp,flex:1,fontSize:15}}/>
{shares.length>0&&(
GETEILT MIT
{shares.map(s=>(
{s.username}
))}
)}
); } // ── Datei-Manager ───────────────────────────────────────────────────────────── export default function Dateien({ toast, mobile }) { const [data, setData] = useState({own:[],shared:[],sharedByMe:[]}); const [folders, setFolders] = useState({own:[],shared:[],sharedByMe:[]}); const [loading, setLoading] = useState(true); const [currentFolderId, setCurrentFolderId] = useState(null); const [folderPath, setFolderPath] = useState([]); // [{id, name}] const [tab, setTab] = useState('own'); const [search, setSearch] = useState(''); const [shareItem, setShareItem] = useState(null); // {item, type} const [uploading, setUploading] = useState(false); const [dragOver, setDragOver] = useState(false); const [newFolder, setNewFolder] = useState(false); const [folderName, setFolderName] = useState(''); const fileRef = useRef(null); const { confirm, ConfirmDialog } = useConfirm(); const load = useCallback(() => { setLoading(true); const fq = currentFolderId ? `?folder_id=${currentFolderId}` : ''; const pq = currentFolderId ? `?parent_id=${currentFolderId}` : ''; Promise.all([ api(`/tools/dateien${fq}`), api(`/tools/dateien/folders${pq}`), ]).then(([d, f]) => { setData({ own:d.own||[], shared:d.shared||[], sharedByMe:d.sharedByMe||[] }); setFolders({ own:f.own||[], shared:f.shared||[], sharedByMe:f.sharedByMe||[] }); }) .catch(e => toast(e.message,'error')) .finally(() => setLoading(false)); }, [currentFolderId]); useEffect(() => { load(); }, [load]); const doUpload = async file => { if (!file) return; setUploading(true); try { const form = new FormData(); form.append('file', file); if (currentFolderId) form.append('folder_id', currentFolderId); await api('/tools/dateien', {method:'POST', body:form, isFile:true}); toast(`${file.name} hochgeladen βœ“`); load(); } catch(e) { toast(e.message,'error'); } finally { setUploading(false); } }; const onFileInput = e => { doUpload(e.target.files?.[0]); e.target.value=''; }; const onDrop = e => { e.preventDefault(); setDragOver(false); const file = e.dataTransfer.files?.[0]; if (file) doUpload(file); }; const download = async file => { try { const res = await fetch(`/api/tools/dateien/${file.id}/download`, { headers: { Authorization: `Bearer ${localStorage.getItem('sk_token')}` } }); if (!res.ok) { const err = await res.json().catch(()=>({error:'Download fehlgeschlagen'})); toast(err.error || 'Download fehlgeschlagen', 'error'); return; } const blob = await res.blob(); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = file.originalname; document.body.appendChild(a); a.click(); document.body.removeChild(a); setTimeout(() => URL.revokeObjectURL(url), 1000); } catch(e) { toast(e.message,'error'); } }; const delFile = async file => { if (!await confirm(`"${file.originalname}" lΓΆschen?`)) return; try { await api(`/tools/dateien/${file.id}`,{method:'DELETE'}); toast('GelΓΆscht'); load(); } catch(e) { toast(e.message,'error'); } }; const delFolder = async folder => { if (!await confirm(`Ordner "${folder.name}" und alle Inhalte lΓΆschen?`)) return; try { await api(`/tools/dateien/folders/${folder.id}`,{method:'DELETE'}); toast('GelΓΆscht'); load(); } catch(e) { toast(e.message,'error'); } }; const createFolder = async () => { if (!folderName.trim()) return; try { const body = { name: folderName.trim() }; if (currentFolderId) body.parent_id = currentFolderId; await api('/tools/dateien/folders', { body }); toast('Ordner erstellt'); setFolderName(''); setNewFolder(false); load(); } catch(e) { toast(e.message,'error'); } }; const enterFolder = (folder, isShared=false) => { setCurrentFolderId(folder.id); setFolderPath(p => [...p, {id:folder.id, name:folder.name, isShared}]); setSearch(''); }; const navigateTo = (idx) => { if (idx < 0) { setCurrentFolderId(null); setFolderPath([]); } else { const target = folderPath[idx]; setCurrentFolderId(target.id); setFolderPath(p => p.slice(0, idx+1)); } setSearch(''); }; const sq = search.toLowerCase(); const currentFolders = tab==='own' ? folders.own : folders.shared; const currentFiles = tab==='own' ? data.own : data.shared; const currentFolders2 = tab==='sharedByMe' ? (folders.sharedByMe||[]) : (tab==='shared' ? (folders.shared||[]) : folders.own); const currentFiles2 = tab==='sharedByMe' ? (data.sharedByMe||[]) : (tab==='shared' ? data.shared : data.own); const filtFolders = sq ? currentFolders2.filter(f=>f.name.toLowerCase().includes(sq)) : currentFolders2; const filtFiles = sq ? currentFiles2.filter(f=>f.originalname.toLowerCase().includes(sq)) : currentFiles2; const totalSize = data.own.reduce((s,f)=>s+f.size,0); const inSharedFolder = folderPath.some(p=>p.isShared); return (
{shareItem && {setShareItem(null);load();}} toast={toast}/>} {/* Header */}

Dateien

{data.own.length} Dateien Β· {fmtSize(totalSize)}
{/* Tabs + Ordner-Button */} {!currentFolderId && (
{[ ['own','Meine Dateien', null], ['shared','Geteilt mit mir', data.shared.length + (folders.shared||[]).length], ['sharedByMe','Geteilt von mir', (data.sharedByMe||[]).length + (folders.sharedByMe||[]).length], ].map(([k,l,cnt])=>( ))} {tab==='own' && ( )}
)} {/* Breadcrumb */} {folderPath.length > 0 && (
{folderPath.map((p,i)=>( ))}
)} {/* Neuer Ordner Button (in Unterordnern) */} {tab==='own' && currentFolderId && !newFolder && !inSharedFolder && ( )} {/* Neuer Ordner Formular */} {newFolder && (
πŸ“ setFolderName(e.target.value)} onKeyDown={e=>{if(e.key==='Enter')createFolder();if(e.key==='Escape'){setNewFolder(false);setFolderName('');}}} placeholder="Ordnername…" autoFocus style={{...S.inp,flex:1,fontSize:15}}/>
)} {/* Suche */}
setSearch(e.target.value)} placeholder="Suchen…" style={{...S.inp,paddingLeft:34,fontSize:15}}/> {search&&}
{/* Drop Zone */}
{e.preventDefault();setDragOver(true);}} onDragLeave={()=>setDragOver(false)} onDrop={onDrop} style={{ border:`2px dashed ${dragOver?'#4ecdc4':'rgba(255,255,255,0.1)'}`, borderRadius:10, padding:'14px 16px', marginBottom:12, textAlign:'center', background:dragOver?'rgba(78,205,196,0.08)':'transparent', transition:'all 0.15s', cursor:'pointer', }} onClick={()=>fileRef.current?.click()}>
{uploading ? '⏳ Wird hochgeladen…' : dragOver ? '↓ Loslassen zum Hochladen' : 'πŸ“‚ Dateien hierher ziehen oder klicken'}
{loading&&
LΓ€dt…
} {/* Ordner */} {filtFolders.map(folder=>(
enterFolder(folder, folder.owner!==undefined)}> πŸ“
{folder.name}
{[ folder.owner && tab!=='sharedByMe' ? `von ${folder.owner}` : null, folder.subCount>0 ? `${folder.subCount} Ordner` : null, folder.fileCount>0 ? `${folder.fileCount} Datei${folder.fileCount!==1?'en':''}` : null, folder.fileCount>0 && folder.totalSize>0 ? fmtSize(folder.totalSize) : null, (folder.subCount===0 && folder.fileCount===0) ? 'leer' : null, tab==='sharedByMe'&&folder.shared_with_names ? `geteilt mit: ${folder.shared_with_names}` : null, ].filter(Boolean).join(' Β· ')}
{(!folder.owner || tab==='sharedByMe') && (
e.stopPropagation()}> {tab==='own'&&}
)}
))} {/* Dateien */} {filtFiles.map(file=>(
{extIcon(file.originalname)}
{file.originalname}
{fmtSize(file.size)} Β· {fmtDate(file.created_at)} {tab==='shared'?` Β· von ${file.owner}`:''} {tab==='sharedByMe'&&file.shared_with_names?` Β· mit: ${file.shared_with_names}${file.shared_at?' seit '+fmtDate(file.shared_at):''}` : ''}
{(tab==='own'&&!inSharedFolder)||tab==='sharedByMe' ? <> {tab==='own'&&} : null}
))} {!loading&&filtFolders.length===0&&filtFiles.length===0&&(
πŸ“‚
{search?'Keine Treffer.':tab==='own'?'Noch leer. Dateien hochladen oder ziehen.':'Noch nichts geteilt.'}
)}
); } // ── Datei-Einstellungen (Admin) ─────────────────────────────────────────────── export function DateiSettings({ toast }) { const [s, setS] = useState({file_max_size_mb:'50',file_max_count:'20',file_allowed_ext:'.pdf,.jpg,.jpeg,.png,.gif,.zip,.txt,.docx,.xlsx,.mp4,.stl,.3mf'}); const [busy, setBusy] = useState(false); useEffect(()=>{ api('/tools/dateien/settings').then(setS).catch(()=>{}); },[]); const save = async()=>{ setBusy(true); try { await api('/tools/dateien/settings',{method:'PUT',body:s}); toast('Gespeichert βœ“'); } catch(e) { toast(e.message,'error'); } finally { setBusy(false); } }; return (
setS(p=>({...p,file_max_size_mb:e.target.value}))} style={{...S.inp,fontSize:15}} min="1"/>
setS(p=>({...p,file_max_count:e.target.value}))} style={{...S.inp,fontSize:15}} min="1"/>
setS(p=>({...p,file_allowed_ext:e.target.value}))} autoCapitalize="none" style={{...S.inp,fontSize:13}}/>
); }