diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx
index 8213b8d..2c23f07 100644
--- a/frontend/src/App.jsx
+++ b/frontend/src/App.jsx
@@ -2862,6 +2862,7 @@ function PublicUpload({ token }) {
const [done, setDone] = useState([]);
const [msg, setMsg] = useState(null);
const [attLeft, setAttLeft] = useState(null);
+ const [dragging, setDragging] = useState(false);
const fileRef = useRef(null);
useEffect(() => {
@@ -2892,8 +2893,8 @@ function PublicUpload({ token }) {
} catch(e) { setMsg({text:'Netzwerkfehler: '+e.message,type:'error'}); }
};
- const doUpload = async () => {
- const files = fileRef.current?.files;
+ const doUpload = async (droppedFiles) => {
+ const files = droppedFiles || fileRef.current?.files;
if (!files?.length) { fileRef.current?.click(); return; }
setUploading(true); setMsg(null);
const newDone = [];
@@ -2919,6 +2920,22 @@ function PublicUpload({ token }) {
setUploading(false);
};
+ const onDragOver = e => { e.preventDefault(); setDragging(true); };
+ const onDragLeave = e => { if (!e.currentTarget.contains(e.relatedTarget)) setDragging(false); };
+ const onDrop = e => {
+ e.preventDefault(); setDragging(false);
+ if (!fileRef.current) return;
+ // DataTransfer.files is read-only – assign via input to trigger React update
+ const dt = e.dataTransfer;
+ if (!dt.files.length) return;
+ // Assign dropped files to the file input
+ Object.defineProperty(fileRef.current, 'files', { value: dt.files, writable: true });
+ fileRef.current.files = dt.files;
+ // Trigger display update by reading back
+ setMsg({text:`${dt.files.length} Datei${dt.files.length!==1?'en':''} ausgewählt`,type:'ok'});
+ doUpload(dt.files);
+ };
+
const fmtDate = d => new Date(d).toLocaleString('de-DE',{timeZone:'Europe/Berlin',day:'2-digit',month:'2-digit',year:'numeric',hour:'2-digit',minute:'2-digit'});
const C = {
@@ -2928,6 +2945,7 @@ function PublicUpload({ token }) {
infoBox: {background:'rgba(78,205,196,0.08)',border:'1px solid rgba(78,205,196,0.2)',borderRadius:8,padding:'10px 12px',fontSize:12,color:'rgba(255,255,255,0.6)',marginBottom:14},
btn: {width:'100%',background:'#4ecdc4',border:'none',borderRadius:8,color:'#0d0d0f',cursor:'pointer',fontFamily:"'Courier New',monospace",fontSize:14,fontWeight:700,padding:12,marginTop:12},
msg: t=>({borderRadius:8,padding:'10px 12px',fontSize:13,marginTop:10,...(t==='ok'?{background:'rgba(78,205,196,0.12)',border:'1px solid rgba(78,205,196,0.3)',color:'#4ecdc4'}:t==='warn'?{background:'rgba(255,230,109,0.12)',border:'1px solid rgba(255,230,109,0.3)',color:'#ffe66d'}:{background:'rgba(255,107,157,0.12)',border:'1px solid rgba(255,107,157,0.3)',color:'#ff6b9d'})}),
+ dropZone: active=>({border:`2px dashed ${active?'#4ecdc4':'rgba(255,255,255,0.18)'}`,borderRadius:12,padding:'28px 16px',textAlign:'center',cursor:'pointer',transition:'border-color 0.15s, background 0.15s',background:active?'rgba(78,205,196,0.07)':'rgba(255,255,255,0.02)',marginBottom:12}),
};
return (
@@ -2952,8 +2970,21 @@ function PublicUpload({ token }) {
{phase==='upload' && info && <>
✓ Zugang OK · max {info.max_size_mb} MB pro Datei · bis {fmtDate(info.expires_at)}
-
-