diff --git a/public/api.js b/public/api.js index 1e7ac4a..68deaa9 100644 --- a/public/api.js +++ b/public/api.js @@ -63,6 +63,7 @@ function promptForToken() { export const api = { get: (p) => call('GET', p), post: (p, body) => call('POST', p, body ?? {}), + put: (p, body) => call('PUT', p, body ?? {}), patch: (p, body) => call('PATCH', p, body ?? {}), del: (p) => call('DELETE', p), setToken: (v) => localStorage.setItem(TOKEN_KEY, v), diff --git a/public/components/sv_reorder.js b/public/components/sv_reorder.js new file mode 100644 index 0000000..75118ce --- /dev/null +++ b/public/components/sv_reorder.js @@ -0,0 +1,28 @@ +export function moveId(order, dragId, beforeId) { + if (dragId === beforeId) return [...order]; + const out = order.filter(id => id !== dragId); + if (beforeId == null) { out.push(dragId); return out; } + const i = out.indexOf(beforeId); + if (i < 0) { out.push(dragId); return out; } + out.splice(i, 0, dragId); + return out; +} + +// Wires HTML5 drag on .sv-card elements in `grid`. onReorder(newOrderIds) fires +// after a drop. Drag handle = the whole card (cursor:grab on the title via CSS). +export function attachReorder(grid, onReorder) { + let dragId = null; + grid.querySelectorAll('.sv-card').forEach(card => { + card.draggable = true; + card.addEventListener('dragstart', () => { dragId = card.dataset.cardId; card.classList.add('dragging'); }); + card.addEventListener('dragend', () => { card.classList.remove('dragging'); dragId = null; }); + card.addEventListener('dragover', e => { e.preventDefault(); card.classList.add('drag-over'); }); + card.addEventListener('dragleave', () => card.classList.remove('drag-over')); + card.addEventListener('drop', e => { + e.preventDefault(); card.classList.remove('drag-over'); + if (!dragId || dragId === card.dataset.cardId) return; + const ids = [...grid.querySelectorAll('.sv-card')].map(c => c.dataset.cardId); + onReorder(moveId(ids, dragId, card.dataset.cardId)); + }); + }); +} diff --git a/public/views/sacred_valley.js b/public/views/sacred_valley.js index 4459f82..bee6261 100644 --- a/public/views/sacred_valley.js +++ b/public/views/sacred_valley.js @@ -1,6 +1,7 @@ import { el, mount } from '../dom.js'; import { api } from '../api.js'; import { svCard } from '../components/sv_card.js'; +import { attachReorder } from '../components/sv_reorder.js'; import { orderCards } from './cards/registry.js'; import clock from './cards/clock.js'; import weather from './cards/weather.js'; @@ -30,5 +31,13 @@ export async function render(main) { try { def.mount(body); def.start && def.start(); active.push(def); } catch (e) { body.appendChild(el('span', { class: 'muted' }, 'card failed')); console.error(def.id, e); } } - // health band + drag wiring arrive in Tasks 22 and 10. + attachReorder(grid, async (newOrder) => { + // reflect immediately + const frag = document.createDocumentFragment(); + newOrder.forEach(id => { const n = grid.querySelector(`.sv-card[data-card-id="${id}"]`); if (n) frag.appendChild(n); }); + grid.appendChild(frag); + try { await api.put('/api/dashboard/layout', { ...layout, card_order: newOrder }); layout.card_order = newOrder; } + catch (e) { console.error('save layout', e); } + }); + // health band wiring arrives in Task 22. } diff --git a/tests/frontend/reorder.test.js b/tests/frontend/reorder.test.js new file mode 100644 index 0000000..0c358f9 --- /dev/null +++ b/tests/frontend/reorder.test.js @@ -0,0 +1,14 @@ +import { describe, it, expect } from 'vitest'; +import { moveId } from '../../public/components/sv_reorder.js'; + +describe('moveId', () => { + it('moves an id before a target', () => { + expect(moveId(['a', 'b', 'c'], 'c', 'a')).toEqual(['c', 'a', 'b']); + }); + it('moving onto itself is a no-op', () => { + expect(moveId(['a', 'b', 'c'], 'b', 'b')).toEqual(['a', 'b', 'c']); + }); + it('moving to end when target is null appends', () => { + expect(moveId(['a', 'b', 'c'], 'a', null)).toEqual(['b', 'c', 'a']); + }); +});