From a0babc2cfa0e08b9ba72459405fbf63828e4dca1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?m=C3=BCde?= Date: Tue, 19 May 2026 18:22:49 +0200 Subject: [PATCH] admin: drag-and-drop drink reordering per bar --- .claude/settings.local.json | 5 +++ client/src/admin/Admin.tsx | 78 +++++++++++++++++++++++-------------- client/src/styles.css | 18 +++++++++ 3 files changed, 71 insertions(+), 30 deletions(-) create mode 100644 .claude/settings.local.json diff --git a/.claude/settings.local.json b/.claude/settings.local.json new file mode 100644 index 0000000..0cbf4db --- /dev/null +++ b/.claude/settings.local.json @@ -0,0 +1,5 @@ +{ + "enabledPlugins": { + "typescript-lsp@claude-plugins-official": true + } +} diff --git a/client/src/admin/Admin.tsx b/client/src/admin/Admin.tsx index 20cc6b9..77be805 100644 --- a/client/src/admin/Admin.tsx +++ b/client/src/admin/Admin.tsx @@ -1,4 +1,4 @@ -import { useEffect, useState } from 'preact/hooks'; +import { useEffect, useRef, useState } from 'preact/hooks'; import { formatCents } from '../api'; async function j(res: Response): Promise { @@ -207,44 +207,62 @@ function BarDrinkEditor({ }); const available = allDrinks.filter(d => !d.archived && !selected.includes(d.id)); - function move(idx: number, dir: -1 | 1) { - const j = idx + dir; - if (j < 0 || j >= selected.length) return; - const next = selected.slice(); - [next[idx], next[j]] = [next[j]!, next[idx]!]; - onChange(next); - } + const dragFrom = useRef(null); + const [dragOver, setDragOver] = useState(null); + function remove(idx: number) { onChange(selected.filter((_, i) => i !== idx)); } function add(id: number) { onChange([...selected, id]); } + function reorder(from: number, to: number) { + if (from === to || from < 0 || to < 0 || from >= selected.length || to >= selected.length) return; + const next = selected.slice(); + const [moved] = next.splice(from, 1); + next.splice(to, 0, moved!); + onChange(next); + } return ( <> -
Aktive Getränke (Reihenfolge)
- - - {selected.length === 0 && ( - - )} - {selected.map((id, idx) => { - const d = byId.get(id); - if (!d) return null; - return ( - - - - - ); - })} - -
Keine
{idx + 1}. {d.name} - - - -
+
Aktive Getränke (zum Sortieren ziehen)
+
    + {selected.length === 0 &&
  • Keine
  • } + {selected.map((id, idx) => { + const d = byId.get(id); + if (!d) return null; + return ( +
  • { + dragFrom.current = idx; + e.dataTransfer.effectAllowed = 'move'; + }} + onDragOver={(e: any) => { + e.preventDefault(); + e.dataTransfer.dropEffect = 'move'; + if (dragOver !== idx) setDragOver(idx); + }} + onDragLeave={() => { if (dragOver === idx) setDragOver(null); }} + onDrop={(e: any) => { + e.preventDefault(); + const from = dragFrom.current; + dragFrom.current = null; + setDragOver(null); + if (from != null) reorder(from, idx); + }} + onDragEnd={() => { dragFrom.current = null; setDragOver(null); }} + > + + {idx + 1}. {d.name} + +
  • + ); + })} +
{available.length > 0 && ( <>
Hinzufügen
diff --git a/client/src/styles.css b/client/src/styles.css index d716865..05707c6 100644 --- a/client/src/styles.css +++ b/client/src/styles.css @@ -159,3 +159,21 @@ button:disabled { opacity: 0.4; } .admin .row { display: flex; gap: 8px; align-items: center; margin-bottom: 8px; flex-wrap: wrap; } .admin .muted { opacity: 0.6; } .admin .login { max-width: 320px; margin: 80px auto; display: flex; flex-direction: column; gap: 12px; } + +.dnd-list { list-style: none; padding: 0; margin: 0 0 8px; } +.dnd-item { + display: flex; + align-items: center; + gap: 8px; + padding: 6px 8px; + margin-bottom: 4px; + background: #1a1a1a; + border: 1px solid #333; + border-radius: 6px; + cursor: grab; +} +.dnd-item:active { cursor: grabbing; } +.dnd-item.drop-target { border-color: #6aa; background: #1a2630; } +.dnd-item .grip { opacity: 0.4; cursor: grab; user-select: none; } +.dnd-item .dnd-name { flex: 1; } +.dnd-list li.muted { padding: 6px 8px; }