Schnellzugriff: Reihenfolge per Drag & Drop änderbar
This commit is contained in:
@@ -30,6 +30,14 @@ router.delete('/links/:id', authenticate, (req, res) => {
|
|||||||
res.json({ success: true });
|
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 ─────────────────────────────────────────────────────────────────────
|
// ── Todos ─────────────────────────────────────────────────────────────────────
|
||||||
router.get('/todos', authenticate, (req, res) => {
|
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)));
|
res.json(db.prepare('SELECT * FROM todos WHERE user_id=? ORDER BY done,sort_order,id').all(uid(req)));
|
||||||
|
|||||||
@@ -1408,6 +1408,8 @@ function QuickLinksSettings({ toast }) {
|
|||||||
const [showForm, setForm] = useState(false);
|
const [showForm, setForm] = useState(false);
|
||||||
const [form, setF] = useState({ title:'', url:'', icon:'🔗' });
|
const [form, setF] = useState({ title:'', url:'', icon:'🔗' });
|
||||||
const [editId, setEditId] = useState(null);
|
const [editId, setEditId] = useState(null);
|
||||||
|
const dragRef = useRef(null);
|
||||||
|
const touchRef = useRef({});
|
||||||
const { confirm, ConfirmDialog } = useConfirm();
|
const { confirm, ConfirmDialog } = useConfirm();
|
||||||
|
|
||||||
useEffect(() => { api('/dashboard/links').then(setLinks).catch(() => {}); }, []);
|
useEffect(() => { api('/dashboard/links').then(setLinks).catch(() => {}); }, []);
|
||||||
@@ -1441,6 +1443,52 @@ function QuickLinksSettings({ toast }) {
|
|||||||
catch(e) { toast(e.message, 'error'); }
|
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 (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<ConfirmDialog/>
|
<ConfirmDialog/>
|
||||||
@@ -1448,8 +1496,19 @@ function QuickLinksSettings({ toast }) {
|
|||||||
{links.length > 0 && (
|
{links.length > 0 && (
|
||||||
<div style={{ marginBottom:12 }}>
|
<div style={{ marginBottom:12 }}>
|
||||||
{links.map(l => (
|
{links.map(l => (
|
||||||
<div key={l.id} style={{ display:'flex', alignItems:'center', gap:10, padding:'8px 0',
|
<div key={l.id}
|
||||||
borderBottom:'1px solid rgba(255,255,255,0.05)' }}>
|
data-link-id={l.id}
|
||||||
|
draggable
|
||||||
|
onDragStart={e => 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' }}>
|
||||||
|
<span style={{ color:'rgba(255,255,255,0.2)', fontSize:14, flexShrink:0, cursor:'grab' }}>⠿</span>
|
||||||
<span style={{ fontSize:18, width:24, textAlign:'center', flexShrink:0 }}>
|
<span style={{ fontSize:18, width:24, textAlign:'center', flexShrink:0 }}>
|
||||||
{l.icon && l.icon.startsWith('http')
|
{l.icon && l.icon.startsWith('http')
|
||||||
? <img src={l.icon} alt="" style={{width:18,height:18,objectFit:'contain'}} onError={e=>e.target.style.display='none'}/>
|
? <img src={l.icon} alt="" style={{width:18,height:18,objectFit:'contain'}} onError={e=>e.target.style.display='none'}/>
|
||||||
|
|||||||
Reference in New Issue
Block a user