Add drag & drop reordering of trips on the dashboard

- trip_members.sort_order (per-user, migrated) drives GET /api/trips order
- PATCH /api/trips/:id/order updates only the caller's membership row
- Trip cards get a drag handle reusing the generalized row-reorder helper;
  clicking the card still navigates
This commit is contained in:
2026-07-20 09:41:43 +07:00
parent d393e16ae1
commit b452430415
8 changed files with 163 additions and 27 deletions
+26 -5
View File
@@ -2,6 +2,7 @@
import { api } from '../api.js';
import { el, mount, clear, loading, errorBox, emptyState, toast } from '../dom.js';
import { formatRange, ymd, parseYMD, pluralize, normalizeCode } from '../format.js';
import { enableTripReorder } from './dragdrop.js';
export function renderTrips(container, ctx) {
mount(container, loading('Loading your trips…'));
@@ -57,27 +58,46 @@ export function renderTrips(container, ctx) {
);
} else {
const grid = el('div', { class: 'trip-grid' });
for (const trip of trips) grid.appendChild(tripCard(trip));
// Reordering only makes sense with 2+ trips (mirrors the day editor's
// entry-row reorder — see dragdrop.js).
const reorderable = trips.length > 1;
const rows = [];
for (const trip of trips) {
const { node, handle } = tripCard(trip, reorderable);
if (reorderable) rows.push({ node, handle, trip });
grid.appendChild(node);
}
page.appendChild(grid);
if (reorderable) enableTripReorder(rows, load);
}
mount(container, page);
}
function tripCard(trip) {
function tripCard(trip, reorderable) {
const today = ymd(new Date());
const future = trip.start_date > today;
const daysUntil = future
? Math.round((parseYMD(trip.start_date) - parseYMD(today)) / 86400000)
: 0;
return el(
const handle = reorderable
? el('span', {
class: 'trip-drag-handle',
title: 'Drag to reorder',
'aria-hidden': 'true',
onClick: (e) => e.preventDefault(),
}, '⋮⋮')
: null;
const node = el(
'a',
{ class: 'trip-card card', href: `#/trip/${trip.id}` },
{ class: 'trip-card card', href: `#/trip/${trip.id}`, draggable: 'false' },
el(
'div',
{ class: 'trip-card-top' },
el('h3', { class: 'trip-card-name' }, trip.name),
el('span', { class: `role-badge role-${trip.role}` }, trip.role),
el('div', { class: 'trip-card-top-right' },
handle,
el('span', { class: `role-badge role-${trip.role}` }, trip.role)),
),
el(
'div',
@@ -93,6 +113,7 @@ export function renderTrips(container, ctx) {
el('span', { class: 'trip-card-currency' }, trip.currency || 'USD'),
),
);
return { node, handle };
}
// Render `build()` into the slot, or close it if the same panel is open.