From fd928a24cf2b9ecc649f34d565441a6337e67706 Mon Sep 17 00:00:00 2001 From: David Dyer Date: Mon, 31 Aug 2026 22:54:11 -0400 Subject: [PATCH] Rebuild menu admin item/category rows on CSS Grid, refine actions UI - Replace the -based item and category-header rows with CSS Grid containers using named grid-template-areas, so desktop and mobile each get their own independent arrangement of the same fields instead of mobile being a display:block hack forced onto table markup - Make the status badge itself the availability toggle (no separate "Mark Sold Out" button), fixed-width so toggling never shifts the Status column, and shorten its label to "Sold Out" - Merge the item Tag into the name line as an inline badge instead of giving it its own column - Group the move/edit/delete controls into two stacked button columns (was 4-5 buttons wide) and right-align them on mobile Co-Authored-By: Claude Sonnet 5 --- admin/menu.js | 161 ++++++++++++++++++++--------------------- admin/menu.php | 131 ++++++++++++++++----------------- styles.css | 193 +++++++++++++++++++++++++++++++++++++++++++++++-- 3 files changed, 329 insertions(+), 156 deletions(-) diff --git a/admin/menu.js b/admin/menu.js index e04220b..3e9047e 100644 --- a/admin/menu.js +++ b/admin/menu.js @@ -147,7 +147,7 @@ document.getElementById('categories-container').addEventListener('click', async // Edit Item if (target.classList.contains('btn-edit-item')) { - const row = target.closest('tr'); + const row = target.closest('.item-row'); openItemModal(JSON.parse(row.dataset.item)); return; } @@ -165,8 +165,8 @@ document.getElementById('categories-container').addEventListener('click', async // Move Item Up/Down if (target.classList.contains('btn-move-item')) { const direction = target.dataset.direction; - const row = target.closest('tr'); - const tbody = row.parentElement; + const row = target.closest('.item-row'); + const rowsContainer = row.parentElement; const sibling = direction === 'up' ? row.previousElementSibling : row.nextElementSibling; if (!sibling) return; // already at this edge; button should be disabled anyway @@ -185,14 +185,14 @@ document.getElementById('categories-container').addEventListener('click', async return; } if (direction === 'up') { - tbody.insertBefore(row, sibling); + rowsContainer.insertBefore(row, sibling); } else { - tbody.insertBefore(sibling, row); + rowsContainer.insertBefore(sibling, row); } // Re-derives disabled state from DOM position rather than re-enabling // `target` unconditionally, since the swap may have just moved it // onto an edge (e.g. into the first/last slot) where it should stay disabled. - refreshMoveButtonStates(tbody); + refreshMoveButtonStates(rowsContainer); } catch { alert('Network error. Please try again.'); target.disabled = false; @@ -235,10 +235,10 @@ document.getElementById('categories-container').addEventListener('click', async return; } - // Toggle Availability (Mark Sold Out / Mark Available) + // Toggle Availability — the status badge itself is the button now, so + // `target` IS the badge; no separate "Mark Sold Out" control to sync. if (target.classList.contains('btn-toggle-item')) { - const row = target.closest('tr'); - const statusBadge = row.querySelector('.status-badge'); + const row = target.closest('.item-row'); target.disabled = true; const fd = new FormData(); @@ -249,9 +249,9 @@ document.getElementById('categories-container').addEventListener('click', async try { const data = await postForm(fd); if (data.success) { - statusBadge.className = `status-badge ${data.status_class}`; - statusBadge.textContent = data.status_badge; - target.textContent = data.btn_text; + target.className = `status-badge btn-toggle-item ${data.status_class}`; + target.textContent = data.status_badge; + target.title = `Click to mark as ${data.is_available ? 'sold out' : 'available'}`; const currentData = JSON.parse(row.dataset.item); currentData.is_available = data.is_available ? 1 : 0; @@ -268,8 +268,8 @@ document.getElementById('categories-container').addEventListener('click', async // Delete Item if (target.classList.contains('btn-delete-item')) { if (!confirm('Are you sure you want to delete this menu item?')) return; - const row = target.closest('tr'); - const tbody = row?.parentElement; + const row = target.closest('.item-row'); + const rowsContainer = row?.parentElement; const fd = new FormData(); fd.append('action', 'delete_item'); @@ -284,7 +284,7 @@ document.getElementById('categories-container').addEventListener('click', async row.style.transform = 'translateX(-10px)'; setTimeout(() => { row.remove(); - refreshMoveButtonStates(tbody); + refreshMoveButtonStates(rowsContainer); }, 200); } } catch { @@ -323,10 +323,10 @@ document.getElementById('categories-container').addEventListener('click', async // Sets the up/down move buttons' disabled state from each row's actual DOM // position, rather than tracking indices by hand — first row can't move up, // last can't move down, everything in between can do either. Call this -// after any insert/remove/reorder in a category's tbody. -function refreshMoveButtonStates(tbody) { - if (!tbody) return; - const rows = [...tbody.querySelectorAll('tr[data-item-id]')]; +// after any insert/remove/reorder in a category's item-rows container. +function refreshMoveButtonStates(rowsContainer) { + if (!rowsContainer) return; + const rows = [...rowsContainer.querySelectorAll('.item-row[data-item-id]')]; rows.forEach((row, index) => { const upBtn = row.querySelector('.btn-move-item[data-direction="up"]'); const downBtn = row.querySelector('.btn-move-item[data-direction="down"]'); @@ -347,18 +347,21 @@ function refreshCategoryMoveButtonStates() { } function itemRowHtml(item, isAvail) { + const badgeHtml = item.badge ? `${escapeHtml(item.badge)}` : ''; return ` - - - - - +
+
${escapeHtml(item.name)}${badgeHtml}
${escapeHtml(item.description || '')}
+
${escapeHtml(item.price)}
+
+
+ + +
+
+ + +
+
`; } @@ -367,15 +370,15 @@ function updateOrInsertItemRow(item, isNew) { let existingRow = document.getElementById(`item-row-${item.id}`); // An edit can reassign an item to a different category — when that - // happens the row needs to move tables, not just update in place, so + // happens the row needs to move grids, not just update in place, so // detach it here and let it fall through to the insert path below. - if (existingRow && existingRow.closest('table')?.id !== `cat-table-${item.category_id}`) { - const oldTbody = existingRow.parentElement; + if (existingRow && existingRow.closest('.item-rows')?.id !== `item-rows-${item.category_id}`) { + const oldRowsContainer = existingRow.parentElement; existingRow.remove(); existingRow = null; - refreshMoveButtonStates(oldTbody); - if (oldTbody && !oldTbody.querySelector('tr[data-item-id]')) { - oldTbody.innerHTML = ''; + refreshMoveButtonStates(oldRowsContainer); + if (oldRowsContainer && !oldRowsContainer.querySelector('.item-row[data-item-id]')) { + oldRowsContainer.innerHTML = '

No items in this category yet.

'; } } @@ -383,38 +386,39 @@ function updateOrInsertItemRow(item, isNew) { // Update existing row existingRow.dataset.item = JSON.stringify(item); const statusBadge = existingRow.querySelector('.status-badge'); - statusBadge.className = `status-badge ${isAvail ? 'active' : 'inactive'}`; - statusBadge.textContent = isAvail ? '● Available' : '○ Sold Out / Hidden'; + statusBadge.className = `status-badge btn-toggle-item ${isAvail ? 'active' : 'inactive'}`; + statusBadge.textContent = isAvail ? '● Available' : '○ Sold Out'; + statusBadge.title = `Click to mark as ${isAvail ? 'sold out' : 'available'}`; existingRow.querySelector('.item-name-cell').textContent = item.name; existingRow.querySelector('.item-desc-cell').textContent = item.description || ''; existingRow.querySelector('.item-price-cell').textContent = item.price; - const badgeCell = existingRow.querySelector('.item-badge-cell'); - badgeCell.innerHTML = item.badge - ? `${escapeHtml(item.badge)}` - : '—'; + const nameRow = existingRow.querySelector('.item-name-row'); + nameRow.querySelector('.menu-item-badge')?.remove(); + if (item.badge) { + nameRow.insertAdjacentHTML('beforeend', `${escapeHtml(item.badge)}`); + } - existingRow.querySelector('.btn-toggle-item').textContent = isAvail ? 'Mark Sold Out' : 'Mark Available'; return; } // Item was created or moved to this category — it always joins at the // end (see next_item_sort_order() server-side), so just append it. - const catTable = document.getElementById(`cat-table-${item.category_id}`); - if (!catTable) return; + const rowsContainer = document.getElementById(`item-rows-${item.category_id}`); + if (!rowsContainer) return; - const tbody = catTable.querySelector('tbody'); - tbody.querySelector('.no-items-row')?.remove(); + rowsContainer.querySelector('.item-grid-empty')?.remove(); - const tr = document.createElement('tr'); - tr.id = `item-row-${item.id}`; - tr.dataset.itemId = item.id; - tr.dataset.item = JSON.stringify(item); - tr.innerHTML = itemRowHtml(item, isAvail); + const row = document.createElement('div'); + row.className = 'item-row'; + row.id = `item-row-${item.id}`; + row.dataset.itemId = item.id; + row.dataset.item = JSON.stringify(item); + row.innerHTML = itemRowHtml(item, isAvail); - tbody.appendChild(tr); - refreshMoveButtonStates(tbody); + rowsContainer.appendChild(row); + refreshMoveButtonStates(rowsContainer); } // Static shell for a category panel. Only text-content positions use @@ -424,37 +428,32 @@ function updateOrInsertItemRow(item, isNew) { function categoryPanelHtml(category) { return `
-
-

- ${escapeHtml(category.name)} -

-

${escapeHtml(category.description || '')}

-
+

+ ${escapeHtml(category.name)} +

+

${escapeHtml(category.description || '')}

- - - - +
+ + +
+
+ + +
-
-
${isAvail ? '● Available' : '○ Sold Out / Hidden'}${escapeHtml(item.name)}
${escapeHtml(item.description || '')}
${escapeHtml(item.price)}${item.badge ? `${escapeHtml(item.badge)}` : '—'}
- - - - - -
No items in this category yet.
- - - - - - - - - - - - - - -
StatusItemPriceTagActions
No items in this category yet.
+
+ +
+

No items in this category yet.

+
`; } diff --git a/admin/menu.php b/admin/menu.php index fa35d47..6900229 100644 --- a/admin/menu.php +++ b/admin/menu.php @@ -60,9 +60,8 @@ function handle_toggle_item(array $post): array return [ 'success' => true, 'is_available' => (bool) $item['is_available'], - 'status_badge' => $item['is_available'] ? '● Available' : '○ Sold Out / Hidden', + 'status_badge' => $item['is_available'] ? '● Available' : '○ Sold Out', 'status_class' => $item['is_available'] ? 'active' : 'inactive', - 'btn_text' => $item['is_available'] ? 'Mark Sold Out' : 'Mark Available', ]; } @@ -228,88 +227,80 @@ $menuGrouped = get_full_menu(false); $category): ?>
-
-

- -

-

-
+

+ +

+

- - - - +
+ + +
+
+ + +
-
- - - - - - - - - - - - - - - - - - $item): ?> - '> - - - - - - - - - -
StatusItemPriceTagActions
No items in this category yet.
- - ● Available - - ○ Sold Out / Hidden - - +
+ +
+ +

No items in this category yet.

+ + + $item): ?> +
'> +
+ +
+
+ -
- -
-
- - - - - — + - -
+ +
+ +
+
+
+ +
+
+
+
+
+
-
-
+
+
+ + + + diff --git a/styles.css b/styles.css index b76a8a5..52b9dd5 100644 --- a/styles.css +++ b/styles.css @@ -1320,6 +1320,32 @@ color: var(--ink-muted); } + /* The menu admin's status badge doubles as its own availability toggle + (see .btn-toggle-item) — a plain