diff --git a/frontend/src/tools/dateien.jsx b/frontend/src/tools/dateien.jsx index 961ed35..db5d8b5 100644 --- a/frontend/src/tools/dateien.jsx +++ b/frontend/src/tools/dateien.jsx @@ -1,289 +1,370 @@ -import { useState, useEffect, useRef } from 'react'; +import { useState, useEffect, useRef, useCallback } from 'react'; import { api, S } from '../lib.js'; import { useConfirm } from '../confirm.jsx'; -import { UploadIcon, DownloadIcon, TrashIcon, SearchIcon, XIcon, PlusIcon } from '../icons.jsx'; - -function fmtSize(bytes) { - if (bytes < 1024) return `${bytes} B`; - if (bytes < 1024*1024) return `${(bytes/1024).toFixed(1)} KB`; - return `${(bytes/(1024*1024)).toFixed(1)} MB`; -} - -function fmtDate(s) { - return new Date(s).toLocaleDateString('de-DE', { day:'2-digit', month:'2-digit', year:'numeric' }); -} +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 ext = name.split('.').pop().toLowerCase(); - if (['jpg','jpeg','png','gif','webp'].includes(ext)) return 'πŸ–Ό'; - if (['pdf'].includes(ext)) return 'πŸ“„'; - if (['zip','rar','7z'].includes(ext)) return 'πŸ“¦'; - if (['mp4','mov','avi'].includes(ext)) return '🎬'; - if (['mp3','wav'].includes(ext)) return '🎡'; - if (['stl','3mf'].includes(ext)) return 'πŸ–¨'; - if (['txt','md'].includes(ext)) return 'πŸ“'; - if (['xlsx','csv'].includes(ext)) return 'πŸ“Š'; - if (['docx','doc'].includes(ext)) return 'πŸ“ƒ'; + 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 'πŸ“Ž'; }; -function ShareModal({ file, onClose, toast }) { +// ── Share Modal ─────────────────────────────────────────────────────────────── +function ShareModal({ item, type, onClose, toast }) { const [username, setUsername] = useState(''); const [shares, setShares] = useState([]); - const [busy, setBusy] = useState(false); - - useEffect(() => { - api('/tools/dateien').then(d => { - // find current shares for this file - const allShares = d.own.find(f => f.id === file.id); - // get shares via separate query approach - just reload full list - }).catch(() => {}); - // Load shares from API - api(`/tools/dateien/${file.id}/shares`).then(setShares).catch(() => {}); - }, [file.id]); - + 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; - setBusy(true); - try { - await api(`/tools/dateien/${file.id}/share`, { body: { username: username.trim() } }); - toast(`Mit ${username} geteilt βœ“`); - setUsername(''); - api(`/tools/dateien/${file.id}/shares`).then(setShares).catch(() => {}); - } catch(e) { toast(e.message, 'error'); } finally { setBusy(false); } + 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 userId => { - try { - await api(`/tools/dateien/${file.id}/share/${userId}`, { method: 'DELETE' }); - setShares(p => p.filter(s => s.id !== userId)); - toast('Freigabe entfernt'); - } 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; + const isMobile = window.innerWidth<768; return ( -
e.target===e.currentTarget&&onClose()}> -
- {isMobile &&
} -
-
- Teilen: {file.originalname} +
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 }}/> - +
+ 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} - -
- ))} -
- )} + {shares.length>0&&(
+
GETEILT MIT
+ {shares.map(s=>( +
+ {s.username} + +
+ ))} +
)}
); } +// ── Datei-Manager ───────────────────────────────────────────────────────────── export default function Dateien({ toast, mobile }) { - const [data, setData] = useState({ own:[], shared:[] }); - const [loading, setLoading] = useState(true); - const [search, setSearch] = useState(''); - const [tab, setTab] = useState('own'); - const [shareFile, setShareFile] = useState(null); - const [uploading, setUploading] = useState(false); + const [data, setData] = useState({own:[],shared:[]}); + const [folders, setFolders] = useState({own:[],shared:[]}); + 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 = () => { + const load = useCallback(() => { setLoading(true); - api('/tools/dateien').then(setData).catch(e=>toast(e.message,'error')).finally(()=>setLoading(false)); - }; - useEffect(load, []); + 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(d); setFolders(f); }) + .catch(e => toast(e.message,'error')) + .finally(() => setLoading(false)); + }, [currentFolderId]); - const upload = async e => { - const file = e.target.files?.[0]; + useEffect(() => { load(); }, [load]); + + const doUpload = async file => { if (!file) return; - e.target.value = ''; setUploading(true); try { const form = new FormData(); form.append('file', file); - await api('/tools/dateien', { method:'POST', body:form, isFile:true }); - toast(`${file.name} hochgeladen βœ“`); - load(); - } catch(err) { toast(err.message, 'error'); } finally { setUploading(false); } + 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 => { - const token = localStorage.getItem('sk_token'); - const a = document.createElement('a'); - a.href = `/api/tools/dateien/${file.id}/download?token=${token}`; - // Use auth header approach via fetch blob try { const res = await fetch(`/api/tools/dateien/${file.id}/download`, { - headers: { Authorization: `Bearer ${token}` } + headers: { Authorization: `Bearer ${localStorage.getItem('sk_token')}` } }); - if (!res.ok) { toast('Download fehlgeschlagen', 'error'); return; } + if (!res.ok) { toast('Download fehlgeschlagen','error'); return; } const blob = await res.blob(); const url = URL.createObjectURL(blob); - Object.assign(document.createElement('a'), { href:url, download:file.originalname }).click(); + Object.assign(document.createElement('a'),{href:url,download:file.originalname}).click(); URL.revokeObjectURL(url); - } catch(e) { toast(e.message, 'error'); } + } catch(e) { toast(e.message,'error'); } }; - const del = async file => { - if (!await confirm(`"${file.originalname}" wirklich lΓΆschen?`)) return; - try { await api(`/tools/dateien/${file.id}`, { method:'DELETE' }); toast('GelΓΆscht'); load(); } - 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 { + await api('/tools/dateien/folders',{body:{name:folderName.trim(),parent_id:currentFolderId}}); + 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 list = tab === 'own' - ? data.own.filter(f => !sq || f.originalname.toLowerCase().includes(sq)) - : data.shared.filter(f => !sq || f.originalname.toLowerCase().includes(sq)); + const currentFolders = tab==='own' ? folders.own : folders.shared; + const currentFiles = tab==='own' ? data.own : data.shared; + const filtFolders = sq ? currentFolders.filter(f=>f.name.toLowerCase().includes(sq)) : currentFolders; + const filtFiles = sq ? currentFiles.filter(f=>f.originalname.toLowerCase().includes(sq)) : currentFiles; - const totalSize = data.own.reduce((s,f) => s+f.size, 0); + const totalSize = data.own.reduce((s,f)=>s+f.size,0); + const inSharedFolder = folderPath.some(p=>p.isShared); return ( -
+
- {shareFile && {setShareFile(null);load();}} toast={toast}/>} + {shareItem && {setShareItem(null);load();}} toast={toast}/>} + -
+ {/* Header */} +
-

Dateien

-
+

Dateien

+
{data.own.length} Dateien Β· {fmtSize(totalSize)}
-
- +
+ {tab==='own' && !inSharedFolder && ( + + )}
{/* Tabs */} -
- {[['own','Meine Dateien'],['shared','Geteilt mit mir']].map(([k,l]) => ( - - ))} -
- - {/* Suche */} -
- - - - setSearch(e.target.value)} placeholder="Datei suchen…" - style={{ ...S.inp, paddingLeft:34, fontSize:15 }}/> - {search && } -
- - {loading &&
LΓ€dt…
} - - {!loading && list.length === 0 && ( -
-
{tab==='own'?'πŸ“‚':'🀝'}
-
- {tab==='own' ? 'Noch keine Dateien. Klicke auf "Hochladen".' : 'Noch keine Dateien geteilt.'} -
+ {!currentFolderId && ( +
+ {[['own','Meine Dateien'],['shared','Geteilt mit mir']].map(([k,l])=>( + + ))}
)} - {list.map(file => ( -
- {extIcon(file.originalname)} -
-
{file.originalname}
-
+ {/* Breadcrumb */} + {folderPath.length > 0 && ( +
+ + {folderPath.map((p,i)=>( + + + + + ))} +
+ )} + + {/* Neuer Ordner */} + {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()}> +
+ {dragOver?'Loslassen zum Hochladen':'Dateien hierher ziehen oder klicken zum AuswΓ€hlen'} +
+
+ + {loading&&
LΓ€dt…
} + + {/* Ordner */} + {filtFolders.map(folder=>( +
enterFolder(folder, folder.owner!==undefined)}> + πŸ“ +
+
{folder.name}
+ {folder.owner&&
von {folder.owner}
} +
+ {!folder.owner && ( +
e.stopPropagation()}> + + +
+ )} + +
+ ))} + + {/* Dateien */} + {filtFiles.map(file=>( +
+ {extIcon(file.originalname)} +
+
{file.originalname}
+
{fmtSize(file.size)} Β· {fmtDate(file.created_at)} - {tab==='shared' && ` Β· von ${file.owner}`} - {tab==='own' && file.shared_count > 0 && ` Β· geteilt mit ${file.shared_count}`} + {file.owner&&file.owner!==undefined&&tab==='shared'?` Β· von ${file.owner}`:''}
-
- - {tab==='own' && <> - - + }
))} + + {!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 [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 () => { + 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 βœ“'); } + 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" max="500"/> +
+ + 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_max_count:e.target.value}))} style={{...S.inp,fontSize:15}} min="1"/>
-
- - setS(p=>({...p,file_allowed_ext:e.target.value}))} - placeholder=".pdf,.jpg,.png" autoCapitalize="none" - style={{ ...S.inp, fontSize:13 }}/> +
+ + setS(p=>({...p,file_allowed_ext:e.target.value}))} autoCapitalize="none" style={{...S.inp,fontSize:13}}/>
-