admin: drag-and-drop drink reordering per bar
This commit is contained in:
parent
4904ff0032
commit
a0babc2cfa
3 changed files with 71 additions and 30 deletions
5
.claude/settings.local.json
Normal file
5
.claude/settings.local.json
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
{
|
||||||
|
"enabledPlugins": {
|
||||||
|
"typescript-lsp@claude-plugins-official": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { useEffect, useState } from 'preact/hooks';
|
import { useEffect, useRef, useState } from 'preact/hooks';
|
||||||
import { formatCents } from '../api';
|
import { formatCents } from '../api';
|
||||||
|
|
||||||
async function j<T = any>(res: Response): Promise<T> {
|
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));
|
const available = allDrinks.filter(d => !d.archived && !selected.includes(d.id));
|
||||||
|
|
||||||
function move(idx: number, dir: -1 | 1) {
|
const dragFrom = useRef<number | null>(null);
|
||||||
const j = idx + dir;
|
const [dragOver, setDragOver] = useState<number | null>(null);
|
||||||
if (j < 0 || j >= selected.length) return;
|
|
||||||
const next = selected.slice();
|
|
||||||
[next[idx], next[j]] = [next[j]!, next[idx]!];
|
|
||||||
onChange(next);
|
|
||||||
}
|
|
||||||
function remove(idx: number) {
|
function remove(idx: number) {
|
||||||
onChange(selected.filter((_, i) => i !== idx));
|
onChange(selected.filter((_, i) => i !== idx));
|
||||||
}
|
}
|
||||||
function add(id: number) {
|
function add(id: number) {
|
||||||
onChange([...selected, id]);
|
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 (
|
return (
|
||||||
<>
|
<>
|
||||||
<div style="margin-top:8px; font-size:14px; opacity:0.7">Aktive Getränke (Reihenfolge)</div>
|
<div style="margin-top:8px; font-size:14px; opacity:0.7">Aktive Getränke (zum Sortieren ziehen)</div>
|
||||||
<table style="margin-bottom:8px">
|
<ul class="dnd-list">
|
||||||
<tbody>
|
{selected.length === 0 && <li class="muted">Keine</li>}
|
||||||
{selected.length === 0 && (
|
{selected.map((id, idx) => {
|
||||||
<tr><td class="muted" colSpan={2}>Keine</td></tr>
|
const d = byId.get(id);
|
||||||
)}
|
if (!d) return null;
|
||||||
{selected.map((id, idx) => {
|
return (
|
||||||
const d = byId.get(id);
|
<li
|
||||||
if (!d) return null;
|
key={id}
|
||||||
return (
|
class={`dnd-item ${dragOver === idx ? 'drop-target' : ''}`}
|
||||||
<tr key={id}>
|
draggable
|
||||||
<td style="width:60%">{idx + 1}. {d.name}</td>
|
onDragStart={(e: any) => {
|
||||||
<td>
|
dragFrom.current = idx;
|
||||||
<button onClick={() => move(idx, -1)} disabled={idx === 0}>↑</button>
|
e.dataTransfer.effectAllowed = 'move';
|
||||||
<button onClick={() => move(idx, 1)} disabled={idx === selected.length - 1}>↓</button>
|
}}
|
||||||
<button onClick={() => remove(idx)}>×</button>
|
onDragOver={(e: any) => {
|
||||||
</td>
|
e.preventDefault();
|
||||||
</tr>
|
e.dataTransfer.dropEffect = 'move';
|
||||||
);
|
if (dragOver !== idx) setDragOver(idx);
|
||||||
})}
|
}}
|
||||||
</tbody>
|
onDragLeave={() => { if (dragOver === idx) setDragOver(null); }}
|
||||||
</table>
|
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>
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</ul>
|
||||||
{available.length > 0 && (
|
{available.length > 0 && (
|
||||||
<>
|
<>
|
||||||
<div style="font-size:14px; opacity:0.7">Hinzufügen</div>
|
<div style="font-size:14px; opacity:0.7">Hinzufügen</div>
|
||||||
|
|
|
||||||
|
|
@ -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 .row { display: flex; gap: 8px; align-items: center; margin-bottom: 8px; flex-wrap: wrap; }
|
||||||
.admin .muted { opacity: 0.6; }
|
.admin .muted { opacity: 0.6; }
|
||||||
.admin .login { max-width: 320px; margin: 80px auto; display: flex; flex-direction: column; gap: 12px; }
|
.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; }
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue