diff --git a/frontend/src/tools/dateien.jsx b/frontend/src/tools/dateien.jsx new file mode 100644 index 0000000..961ed35 --- /dev/null +++ b/frontend/src/tools/dateien.jsx @@ -0,0 +1,291 @@ +import { useState, useEffect, useRef } 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' }); +} + +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 'πŸ“ƒ'; + return 'πŸ“Ž'; +}; + +function ShareModal({ file, 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 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); } + }; + + 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 isMobile = window.innerWidth < 768; + return ( +
e.target===e.currentTarget&&onClose()}> +
+ {isMobile &&
} +
+
+ Teilen: {file.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} + +
+ ))} +
+ )} +
+
+ ); +} + +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 fileRef = useRef(null); + const { confirm, ConfirmDialog } = useConfirm(); + + const load = () => { + setLoading(true); + api('/tools/dateien').then(setData).catch(e=>toast(e.message,'error')).finally(()=>setLoading(false)); + }; + useEffect(load, []); + + const upload = async e => { + const file = e.target.files?.[0]; + 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); } + }; + + 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}` } + }); + 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(); + URL.revokeObjectURL(url); + } 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 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 totalSize = data.own.reduce((s,f) => s+f.size, 0); + + return ( +
+ + {shareFile && {setShareFile(null);load();}} toast={toast}/>} + +
+
+

Dateien

+
+ {data.own.length} Dateien Β· {fmtSize(totalSize)} +
+
+
+ + +
+
+ + {/* 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.'} +
+
+ )} + + {list.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}`} +
+
+
+ + {tab==='own' && <> + + + } +
+
+ ))} +
+ ); +} + +// ── Datei-Einstellungen (Admin) ─────────────────────────────────────────────── +export function DateiSettings({ toast }) { + const [s, setS] = useState({ file_max_size_mb:'50', file_max_count:'20', file_allowed_ext:'.pdf,.jpg,.jpeg,.png,.gif,.zip,.txt,.docx,.xlsx,.mp4,.stl,.3mf' }); + const [busy, setBusy] = useState(false); + + useEffect(() => { api('/tools/dateien/settings').then(setS).catch(()=>{}); }, []); + + const save = async () => { + setBusy(true); + try { await api('/tools/dateien/settings', { method:'PUT', body:s }); toast('Gespeichert βœ“'); } + catch(e) { toast(e.message,'error'); } finally { setBusy(false); } + }; + + return ( +
+
+ + setS(p=>({...p,file_max_size_mb:e.target.value}))} + style={{ ...S.inp, fontSize:15 }} min="1" max="500"/> +
+
+ + 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 }}/> +
+ +
+ ); +}