diff --git a/config.php b/config.php index 1ac86c0..0cf3ce2 100644 --- a/config.php +++ b/config.php @@ -1,7 +1,7 @@ String(c.id)); // Push all active cards first. for (const card of activeCards) { try { await pushOne(card); } catch {} } + // Tell the relay to drop any rows that aren't in our active set. + // This cleans up stale entries from previous activations that + // were never explicitly deleted (old single-list era, failed + // DELETEs, migration leftovers). + try { + await hyprcrd.api('api/shopping', { method: 'DELETE', body: { keep: activeUids } }); + } catch {} // Pull: fetch all lists from relay, match by card_uid. try { const res = await hyprcrd.api('api/shopping', { method: 'GET' }); diff --git a/relay/inc/routes/shopping.php b/relay/inc/routes/shopping.php index 0974a7c..6904194 100644 --- a/relay/inc/routes/shopping.php +++ b/relay/inc/routes/shopping.php @@ -63,11 +63,30 @@ if ($route === '/api/shopping' && $method === 'POST') { } // ── DELETE /api/shopping ───────────────────────────────────── +// Two modes: +// { card_uid: "123" } → delete one specific list +// { keep: ["123", "456"] } → delete everything EXCEPT these if ($route === '/api/shopping' && $method === 'DELETE') { if (!$clientTenant) jsonResponse(['error' => 'unauthorized'], 401); $input = json_decode(file_get_contents('php://input'), true) ?: []; + $keep = is_array($input['keep'] ?? null) ? $input['keep'] : null; $uid = trim((string)($input['card_uid'] ?? '')); - if ($uid === '') jsonResponse(['error' => 'card_uid required'], 400); + + if ($keep !== null) { + // Bulk cleanup: keep only the listed card_uids, drop the rest. + if (count($keep)) { + $placeholders = implode(',', array_fill(0, count($keep), '?')); + $params = array_merge([$clientTenant['id']], array_map('strval', $keep)); + $db->prepare("DELETE FROM shopping_state WHERE tenant_id = ? AND card_uid NOT IN ($placeholders)") + ->execute($params); + } else { + $db->prepare('DELETE FROM shopping_state WHERE tenant_id = :t') + ->execute(['t' => $clientTenant['id']]); + } + jsonResponse(['ok' => true]); + } + + if ($uid === '') jsonResponse(['error' => 'card_uid or keep[] required'], 400); $db->prepare('DELETE FROM shopping_state WHERE tenant_id = :t AND card_uid = :u') ->execute(['t' => $clientTenant['id'], 'u' => $uid]); jsonResponse(['ok' => true]); diff --git a/relay/ui/app.js b/relay/ui/app.js index 4be75a5..1c1e3fd 100644 --- a/relay/ui/app.js +++ b/relay/ui/app.js @@ -112,6 +112,11 @@ function showView(name) { } else { stopShoppingPoll(); } + // Remember the active view so a page reload lands back here + // instead of defaulting to compose every time. + if (['compose', 'account', 'admin', 'chat', 'shopping'].includes(name)) { + try { localStorage.setItem('relay-last-view', name); } catch {} + } if (name === 'admin') { loadUsers(); } else if (name === 'account') { @@ -279,7 +284,8 @@ function routePostLogin(status) { showView('account'); toast('set your client secret to start using the relay'); } else { - showView('compose'); + const last = localStorage.getItem('relay-last-view') || 'compose'; + showView(['compose', 'account', 'admin', 'chat', 'shopping'].includes(last) ? last : 'compose'); } } @@ -786,7 +792,7 @@ async function loadShopping(quiet = false) { list.innerHTML = '
  • no active shopping lists
  • '; return; } - if (title) title.textContent = lists.length === 1 ? (lists[0].title || 'Shopping') : 'Shopping'; + if (title) title.textContent = 'Shopping'; list.innerHTML = ''; for (const entry of lists) { const nodes = shoppingParseLines(entry.body); @@ -794,12 +800,10 @@ async function loadShopping(quiet = false) { // List box wrapper const box = document.createElement('li'); box.className = 'shopping-list-box'; - if (lists.length > 1 || entry.title) { - const hdr = document.createElement('div'); - hdr.className = 'shopping-list-header'; - hdr.textContent = entry.title || '(untitled)'; - box.appendChild(hdr); - } + const hdr = document.createElement('div'); + hdr.className = 'shopping-list-header'; + hdr.textContent = entry.title || '(untitled)'; + box.appendChild(hdr); if (!hasItems) { const done = document.createElement('div'); done.className = 'shopping-list-done'; diff --git a/relay/ui/layout.php b/relay/ui/layout.php index a1fa2ca..418779a 100644 --- a/relay/ui/layout.php +++ b/relay/ui/layout.php @@ -8,7 +8,7 @@ if (!defined('RELAY_BOOTSTRAPPED')) { http_response_code(404); exit; } hyprcrd relay - + @@ -263,6 +263,6 @@ if (!defined('RELAY_BOOTSTRAPPED')) { http_response_code(404); exit; }
    - + diff --git a/relay/ui/style.css b/relay/ui/style.css index b44f8e9..5f6fbb6 100644 --- a/relay/ui/style.css +++ b/relay/ui/style.css @@ -271,18 +271,14 @@ label.checkbox input[type="checkbox"] { accent-color:var(--accent); cursor:point .shopping-items .empty { list-style: none; text-align: center; padding: 40px 16px; color: var(--dim); background: transparent; border: none; cursor: default; } .shopping-items .empty:active { transform: none; } -/* Multi-list stacked boxes */ +/* Multi-list stacked boxes — no own border/background so items + don't look doubled. The header provides section separation. */ .shopping-list-box { list-style: none; - background: var(--surface); - border: 1px solid var(--border); - border-radius: var(--radius-lg); - padding: 10px; display: flex; flex-direction: column; gap: 6px; } -.shopping-list-box:active { transform: none; } .shopping-list-header { font-weight: 600; font-size: 13px;