From 3b7f2b60a2b3dbdf3ec2bae160ed402b2574302c Mon Sep 17 00:00:00 2001 From: Dicken Date: Wed, 27 May 2026 14:56:41 +0200 Subject: [PATCH] Dateien nach "frontend/src/tools" hochladen --- frontend/src/tools/dateien.jsx | 415 +++++++++++++++++---------------- 1 file changed, 208 insertions(+), 207 deletions(-) diff --git a/frontend/src/tools/dateien.jsx b/frontend/src/tools/dateien.jsx index 16e5626..0b0d192 100644 --- a/frontend/src/tools/dateien.jsx +++ b/frontend/src/tools/dateien.jsx @@ -1,7 +1,7 @@ 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'; +import { UploadIcon, DownloadIcon, TrashIcon, SearchIcon, ChevronIcon } 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'}); @@ -15,6 +15,17 @@ const extIcon = name => { return 'πŸ“Ž'; }; +const IconBtn = ({ onClick, color, title, children, stop }) => ( + +); + // ── Share Modal ─────────────────────────────────────────────────────────────── function ShareModal({ item, type, onClose, toast }) { const [username, setUsername] = useState(''); @@ -23,7 +34,7 @@ function ShareModal({ item, type, onClose, toast }) { 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); } + 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 => { @@ -40,40 +51,46 @@ function ShareModal({ item, type, onClose, toast }) { {isMobile&&
}
- {type==='folder'?'πŸ“':'πŸ“Ž'} {item.name||item.originalname} teilen + {type==='folder'?'πŸ“':'πŸ“Ž'} {item.name||item.originalname}
- +
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}
+ {s.shared_at &&
seit {fmtDate(s.shared_at)}
} +
+ +
+ ))} +
+ )} + {shares.length===0 &&
Noch mit niemandem geteilt.
}
); } -// ── Datei-Manager ───────────────────────────────────────────────────────────── +// ── Haupt-Komponente ────────────────────────────────────────────────────────── 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 [folderPath, setFolderPath] = useState([]); const [tab, setTab] = useState('own'); const [search, setSearch] = useState(''); - const [shareItem, setShareItem] = useState(null); // {item, type} + const [shareItem, setShareItem] = useState(null); const [uploading, setUploading] = useState(false); const [dragOver, setDragOver] = useState(false); const [newFolder, setNewFolder] = useState(false); @@ -85,18 +102,16 @@ export default function Dateien({ toast, mobile }) { 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)); + 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]); + useEffect(()=>{load();},[load]); const doUpload = async file => { if (!file) return; @@ -105,135 +120,115 @@ export default function Dateien({ toast, mobile }) { 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}); + 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 onDrop = e => { e.preventDefault(); setDragOver(false); doUpload(e.dataTransfer.files?.[0]); }; const download = async file => { try { - const res = await fetch(`/api/tools/dateien/${file.id}/download`, { - headers: { Authorization: `Bearer ${localStorage.getItem('sk_token')}` } + 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; - } + if (!res.ok){const err=await res.json().catch(()=>({error:'Fehler'}));toast(err.error,'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 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'); } + 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'); } + 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 }); + 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'); } + } catch(e){toast(e.message,'error');} }; const enterFolder = (folder, isShared=false) => { setCurrentFolderId(folder.id); - setFolderPath(p => [...p, {id:folder.id, name:folder.name, isShared}]); + 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)); - } + const navigateTo = idx => { + if (idx<0){setCurrentFolderId(null);setFolderPath([]);} + else{const t=folderPath[idx];setCurrentFolderId(t.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 getFolders = () => tab==='sharedByMe'?(folders.sharedByMe||[]):tab==='shared'?(folders.shared||[]):folders.own; + const getFiles = () => tab==='sharedByMe'?(data.sharedByMe||[]):tab==='shared'?data.shared:data.own; + const filtFolders = sq ? getFolders().filter(f=>f.name.toLowerCase().includes(sq)) : getFolders(); + const filtFiles = sq ? getFiles().filter(f=>f.originalname.toLowerCase().includes(sq)) : getFiles(); const totalSize = data.own.reduce((s,f)=>s+f.size,0); const inSharedFolder = folderPath.some(p=>p.isShared); + const isOwn = tab==='own' && !inSharedFolder; + const sharedCnt = data.shared.length + (folders.shared||[]).length; + const byMeCnt = (data.sharedByMe||[]).length + (folders.sharedByMe||[]).length; return ( -
+
{shareItem && {setShareItem(null);load();}} toast={toast}/>} - {/* Header */} -
-
-

Dateien

-
- {data.own.length} Dateien Β· {fmtSize(totalSize)} -
-
-
- + {/* ── Seitentitel ──────────────────────────────────────────────────── */} +
+

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' && ( - - )} -
- )} + {/* ── Tabs + Neu-Ordner (ΓΌberall gleich) ───────────────────────────── */} +
+ {[ + ['own','Meine Dateien', null], + ['shared','Geteilt mit mir', sharedCnt||null], + ['sharedByMe','Geteilt von mir', byMeCnt||null], + ].map(([k,l,cnt])=>( + + ))} + {/* Ordner-Button – immer am Ende der Tab-Reihe wenn own-Tab aktiv */} + {isOwn && !newFolder && ( + + )} +
- {/* Breadcrumb */} - {folderPath.length > 0 && ( + {/* ── Breadcrumb ────────────────────────────────────────────────────── */} + {folderPath.length>0 && (
{folderPath.map((p,i)=>( - +
)} - {/* Neuer Ordner Button (in Unterordnern) */} - {tab==='own' && currentFolderId && !newFolder && !inSharedFolder && ( - - )} - - {/* Neuer Ordner Formular */} + {/* ── 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}}/> + 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'} + {/* ── Suche + Upload-Zone ───────────────────────────────────────────── */} +
+
+ + + + 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} onClick={()=>fileRef.current?.click()} + style={{ + display:'flex',alignItems:'center',justifyContent:'center',gap:8, + padding:'0 18px',height:44,borderRadius:10,cursor:'pointer', + border:`2px dashed ${dragOver?'#4ecdc4':'rgba(255,255,255,0.15)'}`, + background:dragOver?'rgba(78,205,196,0.08)':'transparent', + transition:'all 0.15s',flexShrink:0, + minWidth:mobile?'100%':200, + }}> + + + {uploading?'Hochladen…':dragOver?'Loslassen':mobile?'Datei hochladen':'Hier ablegen oder klicken'} +
- {loading&&
LΓ€dt…
} + {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(' Β· ')} + {/* ── Ordner ────────────────────────────────────────────────────────── */} + {filtFolders.map(folder => { + const canManage = !folder.owner || tab==='sharedByMe'; + return ( +
+ πŸ“ + {/* Info – klickbar zum Navigieren */} +
enterFolder(folder,!!folder.owner)}> +
{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?`β†’ ${folder.shared_with_names}`:null, + ].filter(Boolean).join(' Β· ')} +
+
+ {/* Aktionen – feste Breite, symmetrisch */} +
+ {canManage && ( + setShareItem({item:folder,type:'folder'})} color="#ffe66d" title="Teilen" stop>🀝 + )} + {canManage&&tab==='own' && ( + delFolder(folder)} color="#ff6b9d" title="Lâschen" stop> + + + )} +
+ +
- {(!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):''}` : ''} + {/* ── Dateien ───────────────────────────────────────────────────────── */} + {filtFiles.map(file => { + const canManage = isOwn || tab==='sharedByMe'; + return ( +
+ {extIcon(file.originalname)} +
+
{file.originalname}
+
+ {fmtSize(file.size)} Β· {fmtDate(file.created_at)} + {tab==='shared'?` Β· von ${file.owner}`:''} + {tab==='sharedByMe'&&file.shared_with_names?` Β· β†’ ${file.shared_with_names}${file.shared_at?' seit '+fmtDate(file.shared_at):''}`:'' } +
+
+ {/* Aktionen – gleiche Breite wie bei Ordnern */} +
+ download(file)} color="#4ecdc4" title="Herunterladen"> + + + {canManage && ( + setShareItem({item:file,type:'file'})} color="#ffe66d" title="Teilen">🀝 + )} + {isOwn && ( + delFile(file)} color="#ff6b9d" title="Lâschen"> + + + )} + {/* Platzhalter damit Dateien-Zeile gleich breit wie Ordner-Zeile */} +
-
- - {(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.'} + {search?'Keine Treffer.':tab==='own'?'Noch leer – Datei hochladen oder Ordner anlegen.':'Noch nichts geteilt.'}
)} @@ -383,11 +384,11 @@ export default function Dateien({ toast, mobile }) { 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(()=>{}); },[]); + 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); } + try{await api('/tools/dateien/settings',{method:'PUT',body:s});toast('Gespeichert βœ“');} + catch(e){toast(e.message,'error');}finally{setBusy(false);} }; return (