Dateien nach "frontend/src/tools" hochladen

This commit is contained in:
2026-05-27 14:56:41 +02:00
parent 0ce2a0673a
commit 3b7f2b60a2

View File

@@ -1,7 +1,7 @@
import { useState, useEffect, useRef, useCallback } from 'react'; import { useState, useEffect, useRef, useCallback } from 'react';
import { api, S } from '../lib.js'; import { api, S } from '../lib.js';
import { useConfirm } from '../confirm.jsx'; 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 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 fmtDate = s => new Date(s).toLocaleDateString('de-DE',{day:'2-digit',month:'2-digit',year:'numeric'});
@@ -15,6 +15,17 @@ const extIcon = name => {
return '📎'; return '📎';
}; };
const IconBtn = ({ onClick, color, title, children, stop }) => (
<button onClick={stop ? e=>{e.stopPropagation();onClick(e);} : onClick}
title={title}
style={{ background:`${color}14`, border:`1px solid ${color}35`,
borderRadius:7, padding:'6px 8px', cursor:'pointer',
display:'flex', alignItems:'center', justifyContent:'center', gap:4,
color, fontFamily:'monospace', fontSize:11, flexShrink:0 }}>
{children}
</button>
);
// ── Share Modal ─────────────────────────────────────────────────────────────── // ── Share Modal ───────────────────────────────────────────────────────────────
function ShareModal({ item, type, onClose, toast }) { function ShareModal({ item, type, onClose, toast }) {
const [username, setUsername] = useState(''); const [username, setUsername] = useState('');
@@ -23,7 +34,7 @@ function ShareModal({ item, type, onClose, toast }) {
useEffect(() => { api(`${base}/shares`).then(setShares).catch(()=>{}); }, [base]); useEffect(() => { api(`${base}/shares`).then(setShares).catch(()=>{}); }, [base]);
const share = async () => { const share = async () => {
if (!username.trim()) return; 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'); } catch(e) { toast(e.message,'error'); }
}; };
const unshare = async uid => { const unshare = async uid => {
@@ -40,40 +51,46 @@ function ShareModal({ item, type, onClose, toast }) {
{isMobile&&<div style={{width:36,height:4,background:'rgba(255,255,255,0.15)',borderRadius:2,margin:'0 auto 14px'}}/>} {isMobile&&<div style={{width:36,height:4,background:'rgba(255,255,255,0.15)',borderRadius:2,margin:'0 auto 14px'}}/>}
<div style={{display:'flex',justifyContent:'space-between',alignItems:'center',marginBottom:14}}> <div style={{display:'flex',justifyContent:'space-between',alignItems:'center',marginBottom:14}}>
<div style={{color:'#fff',fontFamily:"'Space Mono',monospace",fontSize:13,fontWeight:700}}> <div style={{color:'#fff',fontFamily:"'Space Mono',monospace",fontSize:13,fontWeight:700}}>
{type==='folder'?'📁':'📎'} {item.name||item.originalname} teilen {type==='folder'?'📁':'📎'} {item.name||item.originalname}
</div> </div>
<button onClick={onClose} style={{background:'transparent',border:'none',color:'rgba(255,255,255,0.4)',cursor:'pointer',fontSize:18}}></button> <button onClick={onClose} style={{background:'transparent',border:'none',color:'rgba(255,255,255,0.4)',cursor:'pointer',fontSize:18,padding:'0 4px'}}></button>
</div> </div>
<div style={{display:'flex',gap:8,marginBottom:14}}> <div style={{display:'flex',gap:8,marginBottom:14}}>
<input value={username} onChange={e=>setUsername(e.target.value)} onKeyDown={e=>e.key==='Enter'&&share()} <input value={username} onChange={e=>setUsername(e.target.value)} onKeyDown={e=>e.key==='Enter'&&share()}
placeholder="Benutzername" autoCapitalize="none" style={{...S.inp,flex:1,fontSize:15}}/> placeholder="Benutzername" autoCapitalize="none" style={{...S.inp,flex:1,fontSize:15}}/>
<button onClick={share} style={S.btn('#4ecdc4')}>Teilen</button> <button onClick={share} style={S.btn('#4ecdc4')}>Teilen</button>
</div> </div>
{shares.length>0&&(<div> {shares.length>0 && (
<div style={{...S.head,marginBottom:6}}>GETEILT MIT</div> <div>
{shares.map(s=>( <div style={{...S.head,marginBottom:0,marginTop:0}}>GETEILT MIT</div>
<div key={s.id} style={{display:'flex',justifyContent:'space-between',alignItems:'center', {shares.map(s=>(
padding:'7px 0',borderBottom:'1px solid rgba(255,255,255,0.05)'}}> <div key={s.id} style={{display:'flex',justifyContent:'space-between',alignItems:'center',
<span style={{color:'rgba(255,255,255,0.75)',fontFamily:'monospace',fontSize:13}}>{s.username}</span> padding:'8px 0',borderBottom:'1px solid rgba(255,255,255,0.05)'}}>
<button onClick={()=>unshare(s.id)} style={S.btn('#ff6b9d',true)}></button> <div>
</div> <div style={{color:'rgba(255,255,255,0.8)',fontFamily:'monospace',fontSize:13}}>{s.username}</div>
))} {s.shared_at && <div style={{color:'rgba(255,255,255,0.35)',fontFamily:'monospace',fontSize:10}}>seit {fmtDate(s.shared_at)}</div>}
</div>)} </div>
<button onClick={()=>unshare(s.id)} style={S.btn('#ff6b9d',true)}> Entfernen</button>
</div>
))}
</div>
)}
{shares.length===0 && <div style={{color:'rgba(255,255,255,0.3)',fontFamily:'monospace',fontSize:11}}>Noch mit niemandem geteilt.</div>}
</div> </div>
</div> </div>
); );
} }
// ── Datei-Manager ───────────────────────────────────────────────────────────── // ── Haupt-Komponente ──────────────────────────────────────────────────────────
export default function Dateien({ toast, mobile }) { export default function Dateien({ toast, mobile }) {
const [data, setData] = useState({own:[],shared:[],sharedByMe:[]}); const [data, setData] = useState({own:[],shared:[],sharedByMe:[]});
const [folders, setFolders] = useState({own:[],shared:[],sharedByMe:[]}); const [folders, setFolders] = useState({own:[],shared:[],sharedByMe:[]});
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [currentFolderId, setCurrentFolderId] = useState(null); const [currentFolderId, setCurrentFolderId] = useState(null);
const [folderPath, setFolderPath] = useState([]); // [{id, name}] const [folderPath, setFolderPath] = useState([]);
const [tab, setTab] = useState('own'); const [tab, setTab] = useState('own');
const [search, setSearch] = useState(''); const [search, setSearch] = useState('');
const [shareItem, setShareItem] = useState(null); // {item, type} const [shareItem, setShareItem] = useState(null);
const [uploading, setUploading] = useState(false); const [uploading, setUploading] = useState(false);
const [dragOver, setDragOver] = useState(false); const [dragOver, setDragOver] = useState(false);
const [newFolder, setNewFolder] = useState(false); const [newFolder, setNewFolder] = useState(false);
@@ -85,18 +102,16 @@ export default function Dateien({ toast, mobile }) {
setLoading(true); setLoading(true);
const fq = currentFolderId ? `?folder_id=${currentFolderId}` : ''; const fq = currentFolderId ? `?folder_id=${currentFolderId}` : '';
const pq = currentFolderId ? `?parent_id=${currentFolderId}` : ''; const pq = currentFolderId ? `?parent_id=${currentFolderId}` : '';
Promise.all([ Promise.all([api(`/tools/dateien${fq}`), api(`/tools/dateien/folders${pq}`)])
api(`/tools/dateien${fq}`), .then(([d,f]) => {
api(`/tools/dateien/folders${pq}`), setData({own:d.own||[],shared:d.shared||[],sharedByMe:d.sharedByMe||[]});
]).then(([d, f]) => { setFolders({own:f.own||[],shared:f.shared||[],sharedByMe:f.sharedByMe||[]});
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));
.catch(e => toast(e.message,'error'))
.finally(() => setLoading(false));
}, [currentFolderId]); }, [currentFolderId]);
useEffect(() => { load(); }, [load]); useEffect(()=>{load();},[load]);
const doUpload = async file => { const doUpload = async file => {
if (!file) return; if (!file) return;
@@ -105,135 +120,115 @@ export default function Dateien({ toast, mobile }) {
const form = new FormData(); const form = new FormData();
form.append('file', file); form.append('file', file);
if (currentFolderId) form.append('folder_id', currentFolderId); 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(); toast(`${file.name} hochgeladen ✓`); load();
} catch(e) { toast(e.message,'error'); } finally { setUploading(false); } } catch(e) { toast(e.message,'error'); } finally { setUploading(false); }
}; };
const onFileInput = e => { doUpload(e.target.files?.[0]); e.target.value=''; }; const onFileInput = e => { doUpload(e.target.files?.[0]); e.target.value=''; };
const onDrop = e => { const onDrop = e => { e.preventDefault(); setDragOver(false); doUpload(e.dataTransfer.files?.[0]); };
e.preventDefault(); setDragOver(false);
const file = e.dataTransfer.files?.[0];
if (file) doUpload(file);
};
const download = async file => { const download = async file => {
try { try {
const res = await fetch(`/api/tools/dateien/${file.id}/download`, { const res = await fetch(`/api/tools/dateien/${file.id}/download`,{
headers: { Authorization: `Bearer ${localStorage.getItem('sk_token')}` } headers:{Authorization:`Bearer ${localStorage.getItem('sk_token')}`}
}); });
if (!res.ok) { if (!res.ok){const err=await res.json().catch(()=>({error:'Fehler'}));toast(err.error,'error');return;}
const err = await res.json().catch(()=>({error:'Download fehlgeschlagen'}));
toast(err.error || 'Download fehlgeschlagen', 'error'); return;
}
const blob = await res.blob(); const blob = await res.blob();
const url = URL.createObjectURL(blob); const url = URL.createObjectURL(blob);
const a = document.createElement('a'); const a = document.createElement('a'); a.href=url; a.download=file.originalname;
a.href = url; document.body.appendChild(a); a.click(); document.body.removeChild(a);
a.download = file.originalname; setTimeout(()=>URL.revokeObjectURL(url),1000);
document.body.appendChild(a); } catch(e){toast(e.message,'error');}
a.click();
document.body.removeChild(a);
setTimeout(() => URL.revokeObjectURL(url), 1000);
} catch(e) { toast(e.message,'error'); }
}; };
const delFile = async file => { const delFile = async file => {
if (!await confirm(`"${file.originalname}" löschen?`)) return; if (!await confirm(`"${file.originalname}" löschen?`)) return;
try { await api(`/tools/dateien/${file.id}`,{method:'DELETE'}); toast('Gelöscht'); load(); } try{await api(`/tools/dateien/${file.id}`,{method:'DELETE'});toast('Gelöscht');load();}
catch(e) { toast(e.message,'error'); } catch(e){toast(e.message,'error');}
}; };
const delFolder = async folder => { const delFolder = async folder => {
if (!await confirm(`Ordner "${folder.name}" und alle Inhalte löschen?`)) return; 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(); } try{await api(`/tools/dateien/folders/${folder.id}`,{method:'DELETE'});toast('Gelöscht');load();}
catch(e) { toast(e.message,'error'); } catch(e){toast(e.message,'error');}
}; };
const createFolder = async () => { const createFolder = async () => {
if (!folderName.trim()) return; if (!folderName.trim()) return;
try { try {
const body = { name: folderName.trim() }; const body={name:folderName.trim()};
if (currentFolderId) body.parent_id = currentFolderId; if (currentFolderId) body.parent_id=currentFolderId;
await api('/tools/dateien/folders', { body }); await api('/tools/dateien/folders',{body});
toast('Ordner erstellt'); setFolderName(''); setNewFolder(false); load(); toast('Ordner erstellt'); setFolderName(''); setNewFolder(false); load();
} catch(e) { toast(e.message,'error'); } } catch(e){toast(e.message,'error');}
}; };
const enterFolder = (folder, isShared=false) => { const enterFolder = (folder, isShared=false) => {
setCurrentFolderId(folder.id); setCurrentFolderId(folder.id);
setFolderPath(p => [...p, {id:folder.id, name:folder.name, isShared}]); setFolderPath(p=>[...p,{id:folder.id,name:folder.name,isShared}]);
setSearch(''); setSearch('');
}; };
const navigateTo = (idx) => { const navigateTo = idx => {
if (idx < 0) { setCurrentFolderId(null); setFolderPath([]); } if (idx<0){setCurrentFolderId(null);setFolderPath([]);}
else { else{const t=folderPath[idx];setCurrentFolderId(t.id);setFolderPath(p=>p.slice(0,idx+1));}
const target = folderPath[idx];
setCurrentFolderId(target.id);
setFolderPath(p => p.slice(0, idx+1));
}
setSearch(''); setSearch('');
}; };
const sq = search.toLowerCase(); const sq = search.toLowerCase();
const currentFolders = tab==='own' ? folders.own : folders.shared; const getFolders = () => tab==='sharedByMe'?(folders.sharedByMe||[]):tab==='shared'?(folders.shared||[]):folders.own;
const currentFiles = tab==='own' ? data.own : data.shared; const getFiles = () => tab==='sharedByMe'?(data.sharedByMe||[]):tab==='shared'?data.shared:data.own;
const currentFolders2 = tab==='sharedByMe' ? (folders.sharedByMe||[]) : (tab==='shared' ? (folders.shared||[]) : folders.own); const filtFolders = sq ? getFolders().filter(f=>f.name.toLowerCase().includes(sq)) : getFolders();
const currentFiles2 = tab==='sharedByMe' ? (data.sharedByMe||[]) : (tab==='shared' ? data.shared : data.own); const filtFiles = sq ? getFiles().filter(f=>f.originalname.toLowerCase().includes(sq)) : getFiles();
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 totalSize = data.own.reduce((s,f)=>s+f.size,0);
const inSharedFolder = folderPath.some(p=>p.isShared); 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 ( return (
<div style={{padding:mobile?'14px 14px 90px':'36px 44px',maxWidth:900}}> <div style={{padding:mobile?'14px 14px 90px':'36px 44px',maxWidth:960}}>
<ConfirmDialog/> <ConfirmDialog/>
{shareItem && <ShareModal item={shareItem.item} type={shareItem.type} onClose={()=>{setShareItem(null);load();}} toast={toast}/>} {shareItem && <ShareModal item={shareItem.item} type={shareItem.type} onClose={()=>{setShareItem(null);load();}} toast={toast}/>}
<input ref={fileRef} type="file" onChange={onFileInput} style={{display:'none'}}/> <input ref={fileRef} type="file" onChange={onFileInput} style={{display:'none'}}/>
{/* Header */} {/* ── Seitentitel ──────────────────────────────────────────────────── */}
<div style={{display:'flex',justifyContent:'space-between',alignItems:'flex-start',marginBottom:14}}> <div style={{marginBottom:16}}>
<div> <h1 style={{color:'#fff',fontFamily:"'Space Mono',monospace",fontSize:mobile?18:22,margin:0}}>Dateien</h1>
<h1 style={{color:'#fff',fontFamily:"'Space Mono',monospace",fontSize:mobile?18:22,margin:0}}>Dateien</h1> <div style={{color:'rgba(255,255,255,0.45)',fontFamily:'monospace',fontSize:10,marginTop:3}}>
<div style={{color:'rgba(255,255,255,0.45)',fontFamily:'monospace',fontSize:10,marginTop:3}}> {data.own.length} Dateien · {fmtSize(totalSize)}
{data.own.length} Dateien · {fmtSize(totalSize)}
</div>
</div>
<div style={{display:'flex',gap:6}}>
</div> </div>
</div> </div>
{/* Tabs + Ordner-Button */} {/* ── Tabs + Neu-Ordner (überall gleich) ───────────────────────────── */}
{!currentFolderId && ( <div style={{display:'flex',gap:6,marginBottom:12,alignItems:'center',flexWrap:'wrap'}}>
<div style={{display:'flex',gap:6,marginBottom:12,flexWrap:'wrap',alignItems:'center'}}> {[
{[ ['own','Meine Dateien', null],
['own','Meine Dateien', null], ['shared','Geteilt mit mir', sharedCnt||null],
['shared','Geteilt mit mir', data.shared.length + (folders.shared||[]).length], ['sharedByMe','Geteilt von mir', byMeCnt||null],
['sharedByMe','Geteilt von mir', (data.sharedByMe||[]).length + (folders.sharedByMe||[]).length], ].map(([k,l,cnt])=>(
].map(([k,l,cnt])=>( <button key={k} onClick={()=>{setTab(k);setSearch('');setCurrentFolderId(null);setFolderPath([]);}} style={{
<button key={k} onClick={()=>{setTab(k);setSearch('');}} style={{ padding:'6px 14px',borderRadius:20,fontFamily:'monospace',fontSize:11,cursor:'pointer',
padding:'5px 14px',borderRadius:20,fontFamily:'monospace',fontSize:11,cursor:'pointer', border:`1px solid ${tab===k?'rgba(78,205,196,0.5)':'rgba(255,255,255,0.12)'}`,
border:`1px solid ${tab===k?'rgba(78,205,196,0.5)':'rgba(255,255,255,0.1)'}`, background:tab===k?'rgba(78,205,196,0.12)':'transparent',
background:tab===k?'rgba(78,205,196,0.12)':'transparent', color:tab===k?'#4ecdc4':'rgba(255,255,255,0.55)',
color:tab===k?'#4ecdc4':'rgba(255,255,255,0.5)', }}>{l}{cnt?` (${cnt})`:''}</button>
}}>{l}{cnt>0?` (${cnt})`:''}</button> ))}
))} {/* Ordner-Button immer am Ende der Tab-Reihe wenn own-Tab aktiv */}
{tab==='own' && ( {isOwn && !newFolder && (
<button onClick={()=>setNewFolder(true)} style={{ <button onClick={()=>setNewFolder(true)} style={{
marginLeft:'auto', padding:'5px 12px', borderRadius:20, fontFamily:'monospace', fontSize:11, marginLeft:'auto',padding:'6px 12px',borderRadius:20,fontFamily:'monospace',fontSize:11,
cursor:'pointer', border:'1px solid rgba(255,255,255,0.15)', cursor:'pointer',border:'1px solid rgba(255,255,255,0.15)',
background:'rgba(255,255,255,0.05)', color:'rgba(255,255,255,0.6)', background:'rgba(255,255,255,0.05)',color:'rgba(255,255,255,0.65)',
display:'flex', alignItems:'center', gap:5, display:'flex',alignItems:'center',gap:5,
}}>📁 + Ordner</button> }}>📁 + Ordner</button>
)} )}
</div> </div>
)}
{/* Breadcrumb */} {/* ── Breadcrumb ────────────────────────────────────────────────────── */}
{folderPath.length > 0 && ( {folderPath.length>0 && (
<div style={{display:'flex',alignItems:'center',gap:4,marginBottom:12,flexWrap:'wrap'}}> <div style={{display:'flex',alignItems:'center',gap:4,marginBottom:12,flexWrap:'wrap'}}>
<button onClick={()=>navigateTo(-1)} style={{background:'transparent',border:'none', <button onClick={()=>navigateTo(-1)} style={{background:'transparent',border:'none',
color:'rgba(255,255,255,0.5)',cursor:'pointer',fontFamily:'monospace',fontSize:11,padding:'2px 6px'}}> color:'rgba(255,255,255,0.5)',cursor:'pointer',fontFamily:'monospace',fontSize:11,padding:'2px 6px'}}>
@@ -241,7 +236,7 @@ export default function Dateien({ toast, mobile }) {
</button> </button>
{folderPath.map((p,i)=>( {folderPath.map((p,i)=>(
<span key={p.id} style={{display:'flex',alignItems:'center',gap:4}}> <span key={p.id} style={{display:'flex',alignItems:'center',gap:4}}>
<ChevronIcon size={12} color="rgba(255,255,255,0.25)" dir="right"/> <ChevronIcon size={11} color="rgba(255,255,255,0.25)" dir="right"/>
<button onClick={()=>navigateTo(i)} style={{ <button onClick={()=>navigateTo(i)} style={{
background:i===folderPath.length-1?'rgba(78,205,196,0.1)':'transparent', background:i===folderPath.length-1?'rgba(78,205,196,0.1)':'transparent',
border:i===folderPath.length-1?'1px solid rgba(78,205,196,0.2)':'none', border:i===folderPath.length-1?'1px solid rgba(78,205,196,0.2)':'none',
@@ -254,124 +249,130 @@ export default function Dateien({ toast, mobile }) {
</div> </div>
)} )}
{/* Neuer Ordner Button (in Unterordnern) */} {/* ── Neuer Ordner Formular ─────────────────────────────────────────── */}
{tab==='own' && currentFolderId && !newFolder && !inSharedFolder && (
<button onClick={()=>setNewFolder(true)} style={{
display:'flex', alignItems:'center', gap:8, marginBottom:8,
background:'rgba(255,255,255,0.03)', border:'1px dashed rgba(255,255,255,0.15)',
borderRadius:8, padding:'8px 14px', cursor:'pointer',
color:'rgba(255,255,255,0.55)', fontFamily:'monospace', fontSize:11, width:'100%',
}}>📁 + Neuen Unterordner erstellen</button>
)}
{/* Neuer Ordner Formular */}
{newFolder && ( {newFolder && (
<div style={{...S.card,marginBottom:10,display:'flex',gap:8,alignItems:'center'}}> <div style={{...S.card,marginBottom:10,display:'flex',gap:8,alignItems:'center'}}>
<span style={{fontSize:18}}>📁</span> <span style={{fontSize:18}}>📁</span>
<input value={folderName} onChange={e=>setFolderName(e.target.value)} <input value={folderName} onChange={e=>setFolderName(e.target.value)}
onKeyDown={e=>{if(e.key==='Enter')createFolder();if(e.key==='Escape'){setNewFolder(false);setFolderName('');}}} onKeyDown={e=>{if(e.key==='Enter')createFolder();if(e.key==='Escape'){setNewFolder(false);setFolderName('');}}}
placeholder="Ordnername…" autoFocus placeholder="Ordnername…" autoFocus style={{...S.inp,flex:1,fontSize:15}}/>
style={{...S.inp,flex:1,fontSize:15}}/>
<button onClick={createFolder} style={S.btn('#4ecdc4')}></button> <button onClick={createFolder} style={S.btn('#4ecdc4')}></button>
<button onClick={()=>{setNewFolder(false);setFolderName('');}} style={S.btn('#ff6b9d',true)}></button> <button onClick={()=>{setNewFolder(false);setFolderName('');}} style={S.btn('#ff6b9d',true)}></button>
</div> </div>
)} )}
{/* Suche */} {/* ── Suche + Upload-Zone ───────────────────────────────────────────── */}
<div style={{position:'relative',marginBottom:12}}> <div style={{display:mobile?'block':'flex',gap:8,marginBottom:12}}>
<span style={{position:'absolute',left:12,top:'50%',transform:'translateY(-50%)'}}> <div style={{position:'relative',flex:1,marginBottom:mobile?8:0}}>
<SearchIcon size={14} color="rgba(255,255,255,0.3)"/> <span style={{position:'absolute',left:12,top:'50%',transform:'translateY(-50%)'}}>
</span> <SearchIcon size={14} color="rgba(255,255,255,0.3)"/>
<input value={search} onChange={e=>setSearch(e.target.value)} placeholder="Suchen…" </span>
style={{...S.inp,paddingLeft:34,fontSize:15}}/> <input value={search} onChange={e=>setSearch(e.target.value)} placeholder="Suchen…"
{search&&<button onClick={()=>setSearch('')} style={{position:'absolute',right:10,top:'50%', style={{...S.inp,paddingLeft:34,fontSize:15}}/>
transform:'translateY(-50%)',background:'transparent',border:'none', {search&&<button onClick={()=>setSearch('')} style={{position:'absolute',right:10,top:'50%',
color:'rgba(255,255,255,0.3)',cursor:'pointer',fontSize:16}}></button>} transform:'translateY(-50%)',background:'transparent',border:'none',
</div> color:'rgba(255,255,255,0.3)',cursor:'pointer',fontSize:16}}></button>}
</div>
{/* Drop Zone */} {/* Drop-Zone */}
<div <div onDragOver={e=>{e.preventDefault();setDragOver(true);}} onDragLeave={()=>setDragOver(false)}
onDragOver={e=>{e.preventDefault();setDragOver(true);}} onDrop={onDrop} onClick={()=>fileRef.current?.click()}
onDragLeave={()=>setDragOver(false)} style={{
onDrop={onDrop} display:'flex',alignItems:'center',justifyContent:'center',gap:8,
style={{ padding:'0 18px',height:44,borderRadius:10,cursor:'pointer',
border:`2px dashed ${dragOver?'#4ecdc4':'rgba(255,255,255,0.1)'}`, border:`2px dashed ${dragOver?'#4ecdc4':'rgba(255,255,255,0.15)'}`,
borderRadius:10, padding:'14px 16px', marginBottom:12, textAlign:'center', background:dragOver?'rgba(78,205,196,0.08)':'transparent',
background:dragOver?'rgba(78,205,196,0.08)':'transparent', transition:'all 0.15s',flexShrink:0,
transition:'all 0.15s', cursor:'pointer', minWidth:mobile?'100%':200,
}} }}>
onClick={()=>fileRef.current?.click()}> <UploadIcon size={14} color={dragOver?'#4ecdc4':'rgba(255,255,255,0.4)'}/>
<div style={{color:dragOver?'#4ecdc4':'rgba(255,255,255,0.45)',fontFamily:'monospace',fontSize:11}}> <span style={{color:dragOver?'#4ecdc4':'rgba(255,255,255,0.45)',fontFamily:'monospace',fontSize:11}}>
{uploading ? '⏳ Wird hochgeladen…' : dragOver ? 'Loslassen zum Hochladen' : '📂 Dateien hierher ziehen oder klicken'} {uploading?'Hochladen…':dragOver?'Loslassen':mobile?'Datei hochladen':'Hier ablegen oder klicken'}
</span>
</div> </div>
</div> </div>
{loading&&<div style={{color:'rgba(255,255,255,0.4)',fontFamily:'monospace'}}>Lädt</div>} {loading&&<div style={{color:'rgba(255,255,255,0.4)',fontFamily:'monospace',padding:'8px 0'}}>Lädt</div>}
{/* Ordner */} {/* ── Ordner ────────────────────────────────────────────────────────── */}
{filtFolders.map(folder=>( {filtFolders.map(folder => {
<div key={folder.id} style={{...S.card,marginBottom:6,padding:'10px 14px', const canManage = !folder.owner || tab==='sharedByMe';
display:'flex',alignItems:'center',gap:12,cursor:'pointer'}} return (
onClick={()=>enterFolder(folder, folder.owner!==undefined)}> <div key={folder.id} style={{...S.card,marginBottom:6,padding:'10px 14px',
<span style={{fontSize:22,flexShrink:0}}>📁</span> display:'flex',alignItems:'center',gap:10}}>
<div style={{flex:1,minWidth:0}}> <span style={{fontSize:22,flexShrink:0}}>📁</span>
<div style={{color:'#fff',fontFamily:'monospace',fontSize:13,fontWeight:700}}>{folder.name}</div> {/* Info klickbar zum Navigieren */}
<div style={{color:'rgba(255,255,255,0.45)',fontFamily:'monospace',fontSize:10,marginTop:2}}> <div style={{flex:1,minWidth:0,cursor:'pointer'}} onClick={()=>enterFolder(folder,!!folder.owner)}>
{[ <div style={{color:'#fff',fontFamily:'monospace',fontSize:13,fontWeight:700}}>{folder.name}</div>
folder.owner && tab!=='sharedByMe' ? `von ${folder.owner}` : null, <div style={{color:'rgba(255,255,255,0.45)',fontFamily:'monospace',fontSize:10,marginTop:2}}>
folder.subCount>0 ? `${folder.subCount} Ordner` : null, {[
folder.fileCount>0 ? `${folder.fileCount} Datei${folder.fileCount!==1?'en':''}` : null, folder.owner&&tab!=='sharedByMe'?`von ${folder.owner}`:null,
folder.fileCount>0 && folder.totalSize>0 ? fmtSize(folder.totalSize) : null, folder.subCount>0?`${folder.subCount} Ordner`:null,
(folder.subCount===0 && folder.fileCount===0) ? 'leer' : null, folder.fileCount>0?`${folder.fileCount} Datei${folder.fileCount!==1?'en':''}`:null,
tab==='sharedByMe'&&folder.shared_with_names ? `geteilt mit: ${folder.shared_with_names}` : null, folder.fileCount>0&&folder.totalSize>0?fmtSize(folder.totalSize):null,
].filter(Boolean).join(' · ')} folder.subCount===0&&folder.fileCount===0?'leer':null,
tab==='sharedByMe'&&folder.shared_with_names?`${folder.shared_with_names}`:null,
].filter(Boolean).join(' · ')}
</div>
</div>
{/* Aktionen feste Breite, symmetrisch */}
<div style={{display:'flex',gap:5,flexShrink:0,alignItems:'center'}}>
{canManage && (
<IconBtn onClick={()=>setShareItem({item:folder,type:'folder'})} color="#ffe66d" title="Teilen" stop>🤝</IconBtn>
)}
{canManage&&tab==='own' && (
<IconBtn onClick={()=>delFolder(folder)} color="#ff6b9d" title="Löschen" stop>
<TrashIcon size={13} color="#ff6b9d"/>
</IconBtn>
)}
<div style={{width:24,display:'flex',alignItems:'center',justifyContent:'center'}}>
<ChevronIcon size={14} color="rgba(255,255,255,0.3)" dir="right"/>
</div>
</div> </div>
</div> </div>
{(!folder.owner || tab==='sharedByMe') && ( );
<div style={{display:'flex',gap:5}} onClick={e=>e.stopPropagation()}> })}
<button onClick={()=>setShareItem({item:folder,type:'folder'})} style={S.btn('#ffe66d',true)} title="Teilen verwalten">🤝</button>
{tab==='own'&&<button onClick={()=>delFolder(folder)} style={S.btn('#ff6b9d',true)} title="Löschen">
<TrashIcon size={13} color="#ff6b9d"/>
</button>}
</div>
)}
<ChevronIcon size={14} color="rgba(255,255,255,0.3)" dir="right"/>
</div>
))}
{/* Dateien */} {/* ── Dateien ───────────────────────────────────────────────────────── */}
{filtFiles.map(file=>( {filtFiles.map(file => {
<div key={file.id} style={{...S.card,marginBottom:6,padding:'10px 14px', const canManage = isOwn || tab==='sharedByMe';
display:'flex',alignItems:'center',gap:12}}> return (
<span style={{fontSize:22,flexShrink:0}}>{extIcon(file.originalname)}</span> <div key={file.id} style={{...S.card,marginBottom:6,padding:'10px 14px',
<div style={{flex:1,minWidth:0}}> display:'flex',alignItems:'center',gap:10}}>
<div style={{color:'#fff',fontFamily:'monospace',fontSize:12,fontWeight:700, <span style={{fontSize:22,flexShrink:0}}>{extIcon(file.originalname)}</span>
overflow:'hidden',textOverflow:'ellipsis',whiteSpace:'nowrap'}}>{file.originalname}</div> <div style={{flex:1,minWidth:0}}>
<div style={{color:'rgba(255,255,255,0.45)',fontFamily:'monospace',fontSize:10,marginTop:2}}> <div style={{color:'#fff',fontFamily:'monospace',fontSize:13,fontWeight:700,
{fmtSize(file.size)} · {fmtDate(file.created_at)} overflow:'hidden',textOverflow:'ellipsis',whiteSpace:'nowrap'}}>{file.originalname}</div>
{tab==='shared'?` · von ${file.owner}`:''} <div style={{color:'rgba(255,255,255,0.45)',fontFamily:'monospace',fontSize:10,marginTop:2}}>
{tab==='sharedByMe'&&file.shared_with_names?` · mit: ${file.shared_with_names}${file.shared_at?' seit '+fmtDate(file.shared_at):''}` : ''} {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):''}`:'' }
</div>
</div>
{/* Aktionen gleiche Breite wie bei Ordnern */}
<div style={{display:'flex',gap:5,flexShrink:0,alignItems:'center'}}>
<IconBtn onClick={()=>download(file)} color="#4ecdc4" title="Herunterladen">
<DownloadIcon size={13} color="#4ecdc4"/>
</IconBtn>
{canManage && (
<IconBtn onClick={()=>setShareItem({item:file,type:'file'})} color="#ffe66d" title="Teilen">🤝</IconBtn>
)}
{isOwn && (
<IconBtn onClick={()=>delFile(file)} color="#ff6b9d" title="Löschen">
<TrashIcon size={13} color="#ff6b9d"/>
</IconBtn>
)}
{/* Platzhalter damit Dateien-Zeile gleich breit wie Ordner-Zeile */}
<div style={{width:24}}/>
</div> </div>
</div> </div>
<div style={{display:'flex',gap:5,flexShrink:0}}> );
<button onClick={()=>download(file)} style={S.btn('#4ecdc4',true)} title="Download"> })}
<DownloadIcon size={13} color="#4ecdc4"/>
</button>
{(tab==='own'&&!inSharedFolder)||tab==='sharedByMe' ? <>
<button onClick={()=>setShareItem({item:file,type:'file'})} style={S.btn('#ffe66d',true)} title="Teilen verwalten">🤝</button>
{tab==='own'&&<button onClick={()=>delFile(file)} style={S.btn('#ff6b9d',true)} title="Löschen">
<TrashIcon size={13} color="#ff6b9d"/>
</button>}
</> : null}
</div>
</div>
))}
{!loading&&filtFolders.length===0&&filtFiles.length===0&&( {!loading&&filtFolders.length===0&&filtFiles.length===0&&(
<div style={{...S.card,textAlign:'center',padding:36}}> <div style={{...S.card,textAlign:'center',padding:40}}>
<div style={{fontSize:26,marginBottom:8}}>📂</div> <div style={{fontSize:28,marginBottom:10}}>📂</div>
<div style={{color:'rgba(255,255,255,0.35)',fontFamily:'monospace',fontSize:12}}> <div style={{color:'rgba(255,255,255,0.35)',fontFamily:'monospace',fontSize:12}}>
{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.'}
</div> </div>
</div> </div>
)} )}
@@ -383,11 +384,11 @@ export default function Dateien({ toast, mobile }) {
export function DateiSettings({ toast }) { 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); const [busy, setBusy] = useState(false);
useEffect(()=>{ api('/tools/dateien/settings').then(setS).catch(()=>{}); },[]); useEffect(()=>{api('/tools/dateien/settings').then(setS).catch(()=>{});},[]);
const save = async()=>{ const save = async()=>{
setBusy(true); 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); } catch(e){toast(e.message,'error');}finally{setBusy(false);}
}; };
return ( return (
<div> <div>