Dateien nach "frontend/src/tools" hochladen

This commit is contained in:
2026-05-26 15:54:43 +02:00
parent ec290bb5a4
commit 1cdc7613e6

View File

@@ -1,289 +1,370 @@
import { useState, useEffect, useRef } 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, XIcon, PlusIcon } from '../icons.jsx'; import { UploadIcon, DownloadIcon, TrashIcon, SearchIcon, PlusIcon, ChevronIcon, DatabaseIcon } 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' });
}
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 extIcon = name => {
const ext = name.split('.').pop().toLowerCase(); const e = name.split('.').pop().toLowerCase();
if (['jpg','jpeg','png','gif','webp'].includes(ext)) return '🖼'; if (['jpg','jpeg','png','gif','webp'].includes(e)) return '🖼';
if (['pdf'].includes(ext)) return '📄'; if (e==='pdf') return '📄'; if (['zip','rar','7z'].includes(e)) return '📦';
if (['zip','rar','7z'].includes(ext)) return '📦'; if (['mp4','mov','avi'].includes(e)) return '🎬'; if (['mp3','wav'].includes(e)) return '🎵';
if (['mp4','mov','avi'].includes(ext)) return '🎬'; if (['stl','3mf'].includes(e)) return '🖨'; if (['txt','md'].includes(e)) return '📝';
if (['mp3','wav'].includes(ext)) return '🎵'; if (['xlsx','csv'].includes(e)) return '📊'; if (['docx','doc'].includes(e)) 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 '📃';
return '📎'; return '📎';
}; };
function ShareModal({ file, onClose, toast }) { // ── Share Modal ───────────────────────────────────────────────────────────────
function ShareModal({ item, type, onClose, toast }) {
const [username, setUsername] = useState(''); const [username, setUsername] = useState('');
const [shares, setShares] = useState([]); const [shares, setShares] = useState([]);
const [busy, setBusy] = useState(false); const base = type==='folder' ? `/tools/dateien/folders/${item.id}` : `/tools/dateien/${item.id}`;
useEffect(() => { api(`${base}/shares`).then(setShares).catch(()=>{}); }, [base]);
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 share = async () => { const share = async () => {
if (!username.trim()) return; if (!username.trim()) return;
setBusy(true); try { await api(`${base}/share`,{body:{username:username.trim()}}); toast(`Geteilt ✓`); setUsername(''); api(`${base}/shares`).then(setShares); }
try { catch(e) { toast(e.message,'error'); }
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); }
}; };
const unshare = async uid => {
const unshare = async userId => { try { await api(`${base}/share/${uid}`,{method:'DELETE'}); setShares(p=>p.filter(s=>s.id!==uid)); toast('Freigabe entfernt'); }
try { catch(e) { toast(e.message,'error'); }
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 isMobile = window.innerWidth<768;
const isMobile = window.innerWidth < 768;
return ( return (
<div style={{ position:'fixed', inset:0, background:'rgba(0,0,0,0.8)', zIndex:6000, <div style={{position:'fixed',inset:0,background:'rgba(0,0,0,0.8)',zIndex:6000,
display:'flex', alignItems: isMobile?'flex-end':'center', justifyContent:'center', display:'flex',alignItems:isMobile?'flex-end':'center',justifyContent:'center',padding:isMobile?0:24}}
padding: isMobile?0:24 }} onClick={e=>e.target===e.currentTarget&&onClose()}>
onClick={e => e.target===e.currentTarget&&onClose()}> <div style={{background:'#1a1a1e',borderRadius:isMobile?'16px 16px 0 0':16,
<div style={{ background:'#1a1a1e', borderRadius: isMobile?'16px 16px 0 0':16, width:'100%',maxWidth:440,padding:'20px 20px 28px',border:'1px solid rgba(255,255,255,0.12)'}}>
width:'100%', maxWidth:460, padding:'20px 20px 28px', {isMobile&&<div style={{width:36,height:4,background:'rgba(255,255,255,0.15)',borderRadius:2,margin:'0 auto 14px'}}/>}
border:'1px solid rgba(255,255,255,0.12)' }}> <div style={{display:'flex',justifyContent:'space-between',alignItems:'center',marginBottom:14}}>
{isMobile && <div style={{ width:36,height:4,background:'rgba(255,255,255,0.15)',borderRadius:2,margin:'0 auto 16px' }}/>} <div style={{color:'#fff',fontFamily:"'Space Mono',monospace",fontSize:13,fontWeight:700}}>
<div style={{ display:'flex', justifyContent:'space-between', alignItems:'center', marginBottom:14 }}> {type==='folder'?'📁':'📎'} {item.name||item.originalname} teilen
<div style={{ color:'#fff', fontFamily:"'Space Mono',monospace", fontSize:13, fontWeight:700 }}>
Teilen: {file.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}}></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)} <input value={username} onChange={e=>setUsername(e.target.value)} onKeyDown={e=>e.key==='Enter'&&share()}
onKeyDown={e=>e.key==='Enter'&&share()} placeholder="Benutzername" autoCapitalize="none" style={{...S.inp,flex:1,fontSize:15}}/>
placeholder="Benutzername" autoCapitalize="none" <button onClick={share} style={S.btn('#4ecdc4')}>Teilen</button>
style={{ ...S.inp, flex:1, fontSize:15 }}/>
<button onClick={share} disabled={busy} style={{ ...S.btn('#4ecdc4'), padding:'0 14px' }}>
{busy?'…':'Teilen'}
</button>
</div> </div>
{shares.length > 0 && ( {shares.length>0&&(<div>
<div> <div style={{...S.head,marginBottom:6}}>GETEILT MIT</div>
<div style={{ ...S.head, marginBottom:6 }}>GETEILT MIT</div> {shares.map(s=>(
{shares.map(s => ( <div key={s.id} style={{display:'flex',justifyContent:'space-between',alignItems:'center',
<div key={s.id} style={{ display:'flex', justifyContent:'space-between', alignItems:'center', padding:'7px 0',borderBottom:'1px solid rgba(255,255,255,0.05)'}}>
padding:'7px 0', borderBottom:'1px solid rgba(255,255,255,0.05)' }}> <span style={{color:'rgba(255,255,255,0.75)',fontFamily:'monospace',fontSize:13}}>{s.username}</span>
<span style={{ color:'rgba(255,255,255,0.75)', fontFamily:'monospace', fontSize:13 }}>{s.username}</span> <button onClick={()=>unshare(s.id)} style={S.btn('#ff6b9d',true)}></button>
<button onClick={()=>unshare(s.id)} style={S.btn('#ff6b9d', true)}> Entfernen</button> </div>
</div> ))}
))} </div>)}
</div>
)}
</div> </div>
</div> </div>
); );
} }
// ── Datei-Manager ─────────────────────────────────────────────────────────────
export default function Dateien({ toast, mobile }) { export default function Dateien({ toast, mobile }) {
const [data, setData] = useState({ own:[], shared:[] }); const [data, setData] = useState({own:[],shared:[]});
const [loading, setLoading] = useState(true); const [folders, setFolders] = useState({own:[],shared:[]});
const [search, setSearch] = useState(''); const [loading, setLoading] = useState(true);
const [tab, setTab] = useState('own'); const [currentFolderId, setCurrentFolderId] = useState(null);
const [shareFile, setShareFile] = useState(null); const [folderPath, setFolderPath] = useState([]); // [{id, name}]
const [uploading, setUploading] = useState(false); 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 fileRef = useRef(null);
const { confirm, ConfirmDialog } = useConfirm(); const { confirm, ConfirmDialog } = useConfirm();
const load = () => { const load = useCallback(() => {
setLoading(true); setLoading(true);
api('/tools/dateien').then(setData).catch(e=>toast(e.message,'error')).finally(()=>setLoading(false)); const fq = currentFolderId ? `?folder_id=${currentFolderId}` : '';
}; const pq = currentFolderId ? `?parent_id=${currentFolderId}` : '';
useEffect(load, []); 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 => { useEffect(() => { load(); }, [load]);
const file = e.target.files?.[0];
const doUpload = async file => {
if (!file) return; if (!file) return;
e.target.value = '';
setUploading(true); setUploading(true);
try { try {
const form = new FormData(); const form = new FormData();
form.append('file', file); form.append('file', file);
await api('/tools/dateien', { method:'POST', body:form, isFile:true }); if (currentFolderId) form.append('folder_id', currentFolderId);
toast(`${file.name} hochgeladen ✓`); await api('/tools/dateien', {method:'POST', body:form, isFile:true});
load(); toast(`${file.name} hochgeladen ✓`); load();
} catch(err) { toast(err.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 onDrop = e => {
e.preventDefault(); setDragOver(false);
const file = e.dataTransfer.files?.[0];
if (file) doUpload(file);
}; };
const download = async 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 { try {
const res = await fetch(`/api/tools/dateien/${file.id}/download`, { 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 blob = await res.blob();
const url = URL.createObjectURL(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); URL.revokeObjectURL(url);
} catch(e) { toast(e.message, 'error'); } } catch(e) { toast(e.message,'error'); }
}; };
const del = async file => { const delFile = async file => {
if (!await confirm(`"${file.originalname}" wirklich 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 => {
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 sq = search.toLowerCase();
const list = tab === 'own' const currentFolders = tab==='own' ? folders.own : folders.shared;
? data.own.filter(f => !sq || f.originalname.toLowerCase().includes(sq)) const currentFiles = tab==='own' ? data.own : data.shared;
: data.shared.filter(f => !sq || f.originalname.toLowerCase().includes(sq)); 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 ( return (
<div style={{ padding: mobile?'14px 14px 90px':'36px 44px', maxWidth:900 }}> <div style={{padding:mobile?'14px 14px 90px':'36px 44px',maxWidth:900}}>
<ConfirmDialog/> <ConfirmDialog/>
{shareFile && <ShareModal file={shareFile} onClose={()=>{setShareFile(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'}}/>
<div style={{ display:'flex', justifyContent:'space-between', alignItems:'center', marginBottom:14 }}> {/* Header */}
<div style={{display:'flex',justifyContent:'space-between',alignItems:'flex-start',marginBottom:14}}>
<div> <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> </div>
<div style={{ display:'flex', gap:8 }}> <div style={{display:'flex',gap:6,flexWrap:'wrap',justifyContent:'flex-end'}}>
<input ref={fileRef} type="file" onChange={upload} style={{ display:'none' }}/> {tab==='own' && !inSharedFolder && (
<button onClick={()=>setNewFolder(true)} style={{...S.btn('rgba(255,255,255,0.5)',true),display:'flex',alignItems:'center',gap:4}}>
📁 Ordner
</button>
)}
<button onClick={()=>fileRef.current?.click()} disabled={uploading} <button onClick={()=>fileRef.current?.click()} disabled={uploading}
style={{ ...S.btn('#4ecdc4'), display:'flex', alignItems:'center', gap:6 }}> style={{...S.btn('#4ecdc4'),display:'flex',alignItems:'center',gap:6}}>
<UploadIcon size={14} color="#4ecdc4"/> <UploadIcon size={14} color="#4ecdc4"/>
{uploading ? 'Lädt…' : 'Hochladen'} {uploading?'Lädt…':'Hochladen'}
</button> </button>
</div> </div>
</div> </div>
{/* Tabs */} {/* Tabs */}
<div style={{ display:'flex', gap:6, marginBottom:12 }}> {!currentFolderId && (
{[['own','Meine Dateien'],['shared','Geteilt mit mir']].map(([k,l]) => ( <div style={{display:'flex',gap:6,marginBottom:12}}>
<button key={k} onClick={()=>setTab(k)} style={{ {[['own','Meine Dateien'],['shared','Geteilt mit mir']].map(([k,l])=>(
padding:'5px 14px', borderRadius:20, fontFamily:'monospace', fontSize:11, cursor:'pointer', <button key={k} onClick={()=>{setTab(k);setSearch('');}} style={{
border:`1px solid ${tab===k?'rgba(78,205,196,0.5)':'rgba(255,255,255,0.1)'}`, padding:'5px 14px',borderRadius:20,fontFamily:'monospace',fontSize:11,cursor:'pointer',
background: tab===k?'rgba(78,205,196,0.12)':'transparent', border:`1px solid ${tab===k?'rgba(78,205,196,0.5)':'rgba(255,255,255,0.1)'}`,
color: tab===k?'#4ecdc4':'rgba(255,255,255,0.5)', background:tab===k?'rgba(78,205,196,0.12)':'transparent',
}}>{l} {k==='shared'&&data.shared.length>0?`(${data.shared.length})`:''}</button> color:tab===k?'#4ecdc4':'rgba(255,255,255,0.5)',
))} }}>{l}</button>
</div> ))}
{/* Suche */}
<div style={{ position:'relative', marginBottom:12 }}>
<span style={{ position:'absolute', left:12, top:'50%', transform:'translateY(-50%)' }}>
<SearchIcon size={14} color="rgba(255,255,255,0.3)"/>
</span>
<input value={search} onChange={e=>setSearch(e.target.value)} placeholder="Datei suchen…"
style={{ ...S.inp, paddingLeft:34, fontSize:15 }}/>
{search && <button onClick={()=>setSearch('')} style={{ position:'absolute', right:10, top:'50%',
transform:'translateY(-50%)', background:'transparent', border:'none',
color:'rgba(255,255,255,0.3)', cursor:'pointer', fontSize:16 }}></button>}
</div>
{loading && <div style={{ color:'rgba(255,255,255,0.4)', fontFamily:'monospace' }}>Lädt</div>}
{!loading && list.length === 0 && (
<div style={{ ...S.card, textAlign:'center', padding:40 }}>
<div style={{ fontSize:28, marginBottom:10 }}>{tab==='own'?'📂':'🤝'}</div>
<div style={{ color:'rgba(255,255,255,0.35)', fontFamily:'monospace', fontSize:12 }}>
{tab==='own' ? 'Noch keine Dateien. Klicke auf "Hochladen".' : 'Noch keine Dateien geteilt.'}
</div>
</div> </div>
)} )}
{list.map(file => ( {/* Breadcrumb */}
<div key={file.id} style={{ ...S.card, marginBottom:8, padding:'12px 14px', {folderPath.length > 0 && (
display:'flex', alignItems:'center', gap:12 }}> <div style={{display:'flex',alignItems:'center',gap:4,marginBottom:12,flexWrap:'wrap'}}>
<span style={{ fontSize:24, flexShrink:0 }}>{extIcon(file.originalname)}</span> <button onClick={()=>navigateTo(-1)} style={{background:'transparent',border:'none',
<div style={{ flex:1, minWidth:0 }}> color:'rgba(255,255,255,0.5)',cursor:'pointer',fontFamily:'monospace',fontSize:11,padding:'2px 6px'}}>
<div style={{ color:'#fff', fontFamily:'monospace', fontSize:13, fontWeight:700, Dateien
overflow:'hidden', textOverflow:'ellipsis', whiteSpace:'nowrap' }}>{file.originalname}</div> </button>
<div style={{ color:'rgba(255,255,255,0.45)', fontFamily:'monospace', fontSize:10, marginTop:2 }}> {folderPath.map((p,i)=>(
<span key={p.id} style={{display:'flex',alignItems:'center',gap:4}}>
<ChevronIcon size={12} color="rgba(255,255,255,0.25)" dir="right"/>
<button onClick={()=>navigateTo(i)} style={{
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',
borderRadius:6,color:i===folderPath.length-1?'#4ecdc4':'rgba(255,255,255,0.5)',
cursor:'pointer',fontFamily:'monospace',fontSize:11,padding:'2px 8px'}}>
{p.name}
</button>
</span>
))}
</div>
)}
{/* Neuer Ordner */}
{newFolder && (
<div style={{...S.card,marginBottom:10,display:'flex',gap:8,alignItems:'center'}}>
<input value={folderName} onChange={e=>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}}/>
<button onClick={createFolder} style={S.btn('#4ecdc4')}></button>
<button onClick={()=>{setNewFolder(false);setFolderName('');}} style={S.btn('#ff6b9d',true)}></button>
</div>
)}
{/* Suche */}
<div style={{position:'relative',marginBottom:12}}>
<span style={{position:'absolute',left:12,top:'50%',transform:'translateY(-50%)'}}>
<SearchIcon size={14} color="rgba(255,255,255,0.3)"/>
</span>
<input value={search} onChange={e=>setSearch(e.target.value)} placeholder="Suchen…"
style={{...S.inp,paddingLeft:34,fontSize:15}}/>
{search&&<button onClick={()=>setSearch('')} style={{position:'absolute',right:10,top:'50%',
transform:'translateY(-50%)',background:'transparent',border:'none',
color:'rgba(255,255,255,0.3)',cursor:'pointer',fontSize:16}}></button>}
</div>
{/* Drop Zone */}
<div
onDragOver={e=>{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()}>
<div style={{color:dragOver?'#4ecdc4':'rgba(255,255,255,0.3)',fontFamily:'monospace',fontSize:11}}>
{dragOver?'Loslassen zum Hochladen':'Dateien hierher ziehen oder klicken zum Auswählen'}
</div>
</div>
{loading&&<div style={{color:'rgba(255,255,255,0.4)',fontFamily:'monospace'}}>Lädt</div>}
{/* Ordner */}
{filtFolders.map(folder=>(
<div key={folder.id} style={{...S.card,marginBottom:6,padding:'10px 14px',
display:'flex',alignItems:'center',gap:12,cursor:'pointer'}}
onClick={()=>enterFolder(folder, folder.owner!==undefined)}>
<span style={{fontSize:22,flexShrink:0}}>📁</span>
<div style={{flex:1,minWidth:0}}>
<div style={{color:'#fff',fontFamily:'monospace',fontSize:13,fontWeight:700}}>{folder.name}</div>
{folder.owner&&<div style={{color:'rgba(255,255,255,0.4)',fontFamily:'monospace',fontSize:10}}>von {folder.owner}</div>}
</div>
{!folder.owner && (
<div style={{display:'flex',gap:5}} onClick={e=>e.stopPropagation()}>
<button onClick={()=>setShareItem({item:folder,type:'folder'})} style={S.btn('#ffe66d',true)} title="Teilen">🤝</button>
<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 */}
{filtFiles.map(file=>(
<div key={file.id} style={{...S.card,marginBottom:6,padding:'10px 14px',
display:'flex',alignItems:'center',gap:12}}>
<span style={{fontSize:22,flexShrink:0}}>{extIcon(file.originalname)}</span>
<div style={{flex:1,minWidth:0}}>
<div style={{color:'#fff',fontFamily:'monospace',fontSize:12,fontWeight:700,
overflow:'hidden',textOverflow:'ellipsis',whiteSpace:'nowrap'}}>{file.originalname}</div>
<div style={{color:'rgba(255,255,255,0.45)',fontFamily:'monospace',fontSize:10,marginTop:2}}>
{fmtSize(file.size)} · {fmtDate(file.created_at)} {fmtSize(file.size)} · {fmtDate(file.created_at)}
{tab==='shared' && ` · von ${file.owner}`} {file.owner&&file.owner!==undefined&&tab==='shared'?` · von ${file.owner}`:''}
{tab==='own' && file.shared_count > 0 && ` · geteilt mit ${file.shared_count}`}
</div> </div>
</div> </div>
<div style={{ display:'flex', gap:6, flexShrink:0 }}> <div style={{display:'flex',gap:5,flexShrink:0}}>
<button onClick={()=>download(file)} style={S.btn('#4ecdc4', true)} title="Herunterladen"> <button onClick={()=>download(file)} style={S.btn('#4ecdc4',true)} title="Download">
<DownloadIcon size={14} color="#4ecdc4"/> <DownloadIcon size={13} color="#4ecdc4"/>
</button> </button>
{tab==='own' && <> {tab==='own'&&!inSharedFolder&&<>
<button onClick={()=>setShareFile(file)} style={S.btn('#ffe66d', true)} title="Teilen"> <button onClick={()=>setShareItem({item:file,type:'file'})} style={S.btn('#ffe66d',true)} title="Teilen">🤝</button>
🤝 <button onClick={()=>delFile(file)} style={S.btn('#ff6b9d',true)} title="Löschen">
</button> <TrashIcon size={13} color="#ff6b9d"/>
<button onClick={()=>del(file)} style={S.btn('#ff6b9d', true)} title="Löschen">
<TrashIcon size={14} color="#ff6b9d"/>
</button> </button>
</>} </>}
</div> </div>
</div> </div>
))} ))}
{!loading&&filtFolders.length===0&&filtFiles.length===0&&(
<div style={{...S.card,textAlign:'center',padding:36}}>
<div style={{fontSize:26,marginBottom:8}}>📂</div>
<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.'}
</div>
</div>
)}
</div> </div>
); );
} }
// ── Datei-Einstellungen (Admin) ─────────────────────────────────────────────── // ── Datei-Einstellungen (Admin) ───────────────────────────────────────────────
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>
<div style={{ marginBottom:12 }}> <div style={{marginBottom:10}}>
<label style={{ ...S.head, display:'block', marginBottom:4 }}>MAX. DATEIGRÖSSE (MB)</label> <label style={{...S.head,display:'block',marginBottom:4}}>MAX. DATEIGRÖSSE (MB)</label>
<input type="number" value={s.file_max_size_mb} onChange={e=>setS(p=>({...p,file_max_size_mb:e.target.value}))} <input type="number" value={s.file_max_size_mb} onChange={e=>setS(p=>({...p,file_max_size_mb:e.target.value}))} style={{...S.inp,fontSize:15}} min="1"/>
style={{ ...S.inp, fontSize:15 }} min="1" max="500"/>
</div> </div>
<div style={{ marginBottom:12 }}> <div style={{marginBottom:10}}>
<label style={{ ...S.head, display:'block', marginBottom:4 }}>MAX. ANZAHL DATEIEN PRO BENUTZER</label> <label style={{...S.head,display:'block',marginBottom:4}}>MAX. DATEIEN PRO BENUTZER</label>
<input type="number" value={s.file_max_count} onChange={e=>setS(p=>({...p,file_max_count:e.target.value}))} <input type="number" value={s.file_max_count} onChange={e=>setS(p=>({...p,file_max_count:e.target.value}))} style={{...S.inp,fontSize:15}} min="1"/>
style={{ ...S.inp, fontSize:15 }} min="1"/>
</div> </div>
<div style={{ marginBottom:14 }}> <div style={{marginBottom:14}}>
<label style={{ ...S.head, display:'block', marginBottom:4 }}>ERLAUBTE ENDUNGEN (kommagetrennt)</label> <label style={{...S.head,display:'block',marginBottom:4}}>ERLAUBTE ENDUNGEN (kommagetrennt)</label>
<input value={s.file_allowed_ext} onChange={e=>setS(p=>({...p,file_allowed_ext:e.target.value}))} <input value={s.file_allowed_ext} onChange={e=>setS(p=>({...p,file_allowed_ext:e.target.value}))} autoCapitalize="none" style={{...S.inp,fontSize:13}}/>
placeholder=".pdf,.jpg,.png" autoCapitalize="none"
style={{ ...S.inp, fontSize:13 }}/>
</div> </div>
<button onClick={save} disabled={busy} style={{ ...S.btn('#4ecdc4'), width:'100%', textAlign:'center', padding:'10px 0' }}> <button onClick={save} disabled={busy} style={{...S.btn('#4ecdc4'),width:'100%',textAlign:'center',padding:'10px 0'}}>
{busy?'…':'✓ Speichern'} {busy?'…':'✓ Speichern'}
</button> </button>
</div> </div>