diff --git a/backend/src/routes/dashboard.js b/backend/src/routes/dashboard.js index a206579..36c1e4d 100644 --- a/backend/src/routes/dashboard.js +++ b/backend/src/routes/dashboard.js @@ -30,6 +30,14 @@ router.delete('/links/:id', authenticate, (req, res) => { res.json({ success: true }); }); +router.put('/links-order', authenticate, (req, res) => { + const { ids } = req.body; + if (!Array.isArray(ids)) return res.status(400).json({ error: 'ids erforderlich' }); + const update = db.prepare('UPDATE quick_links SET sort_order=? WHERE id=? AND user_id=?'); + db.transaction(() => { ids.forEach((id, i) => update.run(i, id, uid(req))); })(); + res.json({ ok: true }); +}); + // ── Todos ───────────────────────────────────────────────────────────────────── router.get('/todos', authenticate, (req, res) => { res.json(db.prepare('SELECT * FROM todos WHERE user_id=? ORDER BY done,sort_order,id').all(uid(req))); diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index 0430544..64e74ae 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -1408,6 +1408,8 @@ function QuickLinksSettings({ toast }) { const [showForm, setForm] = useState(false); const [form, setF] = useState({ title:'', url:'', icon:'🔗' }); const [editId, setEditId] = useState(null); + const dragRef = useRef(null); + const touchRef = useRef({}); const { confirm, ConfirmDialog } = useConfirm(); useEffect(() => { api('/dashboard/links').then(setLinks).catch(() => {}); }, []); @@ -1441,6 +1443,52 @@ function QuickLinksSettings({ toast }) { catch(e) { toast(e.message, 'error'); } }; + const reorder = async (newLinks) => { + setLinks(newLinks); + try { await api('/dashboard/links-order', { method:'PUT', body:{ ids: newLinks.map(l=>l.id) } }); } + catch(e) { toast(e.message,'error'); } + }; + + // Desktop drag handlers + const onDragStart = (e, id) => { dragRef.current = id; e.dataTransfer.effectAllowed = 'move'; }; + const onDragOver = (e, id) => { + e.preventDefault(); + if (dragRef.current === id) return; + const from = links.findIndex(l => l.id === dragRef.current); + const to = links.findIndex(l => l.id === id); + if (from < 0 || to < 0) return; + const next = [...links]; next.splice(to, 0, next.splice(from, 1)[0]); + setLinks(next); + }; + const onDrop = () => reorder(links); + + // Touch handlers (long press + drag) + const onTouchStart = (e, id) => { + touchRef.current = { id, startY: e.touches[0].clientY, timer: setTimeout(() => { + touchRef.current.active = true; + }, 300) }; + }; + const onTouchMove = (e) => { + if (!touchRef.current.active) return; + e.preventDefault(); + const y = e.touches[0].clientY; + const els = document.elementsFromPoint(e.touches[0].clientX, y); + const target = els.find(el => el.dataset?.linkId); + if (!target) return; + const targetId = parseInt(target.dataset.linkId); + if (targetId === touchRef.current.id) return; + const from = links.findIndex(l => l.id === touchRef.current.id); + const to = links.findIndex(l => l.id === targetId); + if (from < 0 || to < 0) return; + const next = [...links]; next.splice(to, 0, next.splice(from, 1)[0]); + setLinks(next); + }; + const onTouchEnd = () => { + clearTimeout(touchRef.current.timer); + if (touchRef.current.active) reorder(links); + touchRef.current = {}; + }; + return (
@@ -1448,8 +1496,19 @@ function QuickLinksSettings({ toast }) { {links.length > 0 && (
{links.map(l => ( -
+
onDragStart(e, l.id)} + onDragOver={e => onDragOver(e, l.id)} + onDrop={onDrop} + onTouchStart={e => onTouchStart(e, l.id)} + onTouchMove={onTouchMove} + onTouchEnd={onTouchEnd} + style={{ display:'flex', alignItems:'center', gap:10, padding:'8px 0', + borderBottom:'1px solid rgba(255,255,255,0.05)', + cursor:'grab', userSelect:'none' }}> + {l.icon && l.icon.startsWith('http') ? e.target.style.display='none'}/>