admin: drag-and-drop drink reordering per bar

This commit is contained in:
müde 2026-05-19 18:22:49 +02:00
parent 4904ff0032
commit a0babc2cfa
3 changed files with 71 additions and 30 deletions

View file

@ -0,0 +1,5 @@
{
"enabledPlugins": {
"typescript-lsp@claude-plugins-official": true
}
}

View file

@ -1,4 +1,4 @@
import { useEffect, useState } from 'preact/hooks';
import { useEffect, useRef, useState } from 'preact/hooks';
import { formatCents } from '../api';
async function j<T = any>(res: Response): Promise<T> {
@ -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<number | null>(null);
const [dragOver, setDragOver] = useState<number | null>(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 (
<>
<div style="margin-top:8px; font-size:14px; opacity:0.7">Aktive Getränke (Reihenfolge)</div>
<table style="margin-bottom:8px">
<tbody>
{selected.length === 0 && (
<tr><td class="muted" colSpan={2}>Keine</td></tr>
)}
<div style="margin-top:8px; font-size:14px; opacity:0.7">Aktive Getränke (zum Sortieren ziehen)</div>
<ul class="dnd-list">
{selected.length === 0 && <li class="muted">Keine</li>}
{selected.map((id, idx) => {
const d = byId.get(id);
if (!d) return null;
return (
<tr key={id}>
<td style="width:60%">{idx + 1}. {d.name}</td>
<td>
<button onClick={() => move(idx, -1)} disabled={idx === 0}></button>
<button onClick={() => move(idx, 1)} disabled={idx === selected.length - 1}></button>
<li
key={id}
class={`dnd-item ${dragOver === idx ? 'drop-target' : ''}`}
draggable
onDragStart={(e: any) => {
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); }}
>
<span class="grip" aria-hidden="true"></span>
<span class="dnd-name">{idx + 1}. {d.name}</span>
<button onClick={() => remove(idx)}>×</button>
</td>
</tr>
</li>
);
})}
</tbody>
</table>
</ul>
{available.length > 0 && (
<>
<div style="font-size:14px; opacity:0.7">Hinzufügen</div>

View file

@ -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; }