Add menu enable toggle, user status, and mobile/UI polish
- Admin menu page: mobile item cards now put Actions in the top-right
corner next to Status, matching the desktop row layout instead of
stacking at the bottom; same treatment applied to announcements and
users for consistency across all three admin lists
- Center button label text everywhere via .btn { justify-content: center }
instead of one-off overrides
- Add a Disable/Enable Menu toggle to the admin menu toolbar; the public
menu page shows its existing "menu being updated" message while disabled
- Add an Enabled/Disabled status to user accounts, with its own toggle
badge, guardrails against disabling your own account or the last
enabled account, and an immediate session kick for anyone disabled
mid-session
- Fix the announcement banner / hero tagline "·" separators to hide only
when their neighboring text actually wraps onto a different line,
instead of a fixed breakpoint or always-on dot
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
eab20899de
commit
cd4c6c1969
13 files changed
+405
-50
No files matched your search
+9
-7
@@ -215,14 +215,16 @@ $announcements = get_all_announcements();
|
||||
</div>
|
||||
<div data-label="Actions">
|
||||
<div class="row-actions">
|
||||
<a href="index.php?edit=<?= (int)$ann['id'] ?>" class="btn btn-outline btn-sm">Edit</a>
|
||||
<div class="action-stack">
|
||||
<a href="index.php?edit=<?= (int)$ann['id'] ?>" class="btn btn-outline btn-sm">Edit</a>
|
||||
|
||||
<form method="POST" action="index.php" style="display: inline;" onsubmit="return confirm('Are you sure you want to delete this announcement?');">
|
||||
<input type="hidden" name="action" value="delete_announcement">
|
||||
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars(get_csrf_token()) ?>">
|
||||
<input type="hidden" name="announcement_id" value="<?= (int)$ann['id'] ?>">
|
||||
<button type="submit" class="btn btn-danger btn-sm">Delete</button>
|
||||
</form>
|
||||
<form method="POST" action="index.php" style="display: inline;" onsubmit="return confirm('Are you sure you want to delete this announcement?');">
|
||||
<input type="hidden" name="action" value="delete_announcement">
|
||||
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars(get_csrf_token()) ?>">
|
||||
<input type="hidden" name="announcement_id" value="<?= (int)$ann['id'] ?>">
|
||||
<button type="submit" class="btn btn-danger btn-sm">Delete</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -86,9 +86,51 @@ async function postForm(formData) {
|
||||
return response.json();
|
||||
}
|
||||
|
||||
// Applies a menu_enabled state to the toggle button and its alert banner —
|
||||
// shared by the click handler below and, implicitly, the server-rendered
|
||||
// initial state (see menu.php), so the two never drift apart.
|
||||
function applyMenuEnabledState(enabled) {
|
||||
const btn = document.getElementById('btn-toggle-menu');
|
||||
btn.textContent = enabled ? 'Disable Menu' : 'Enable Menu';
|
||||
btn.classList.toggle('btn-danger', enabled);
|
||||
btn.classList.toggle('btn-outline', !enabled);
|
||||
btn.dataset.enabled = enabled ? '1' : '0';
|
||||
|
||||
document.getElementById('menu-disabled-alert').hidden = enabled;
|
||||
}
|
||||
|
||||
// Modal listeners
|
||||
document.getElementById('btn-new-item').addEventListener('click', () => openItemModal());
|
||||
document.getElementById('btn-new-cat').addEventListener('click', () => openCategoryModal());
|
||||
|
||||
document.getElementById('btn-toggle-menu').addEventListener('click', async (e) => {
|
||||
const btn = e.currentTarget;
|
||||
const isCurrentlyEnabled = btn.dataset.enabled === '1';
|
||||
|
||||
// Only the disabling direction needs a confirm — it immediately hides
|
||||
// the menu from every visitor, where re-enabling it is harmless.
|
||||
if (isCurrentlyEnabled && !confirm('Hide the menu from the public site? Visitors will see a "menu being updated" message until you re-enable it.')) {
|
||||
return;
|
||||
}
|
||||
|
||||
btn.disabled = true;
|
||||
const fd = new FormData();
|
||||
fd.append('action', 'toggle_menu_enabled');
|
||||
fd.append('csrf_token', csrfToken);
|
||||
|
||||
try {
|
||||
const data = await postForm(fd);
|
||||
if (data.success) {
|
||||
applyMenuEnabledState(data.menu_enabled);
|
||||
} else {
|
||||
alert(data.error || 'Failed to update menu visibility.');
|
||||
}
|
||||
} catch {
|
||||
alert('Network error. Please try again.');
|
||||
} finally {
|
||||
btn.disabled = false;
|
||||
}
|
||||
});
|
||||
document.getElementById('btn-create-first-cat')?.addEventListener('click', () => openCategoryModal());
|
||||
document.getElementById('btn-close-item-modal').addEventListener('click', closeItemModal);
|
||||
document.getElementById('btn-cancel-item').addEventListener('click', closeItemModal);
|
||||
|
||||
@@ -135,6 +135,12 @@ function handle_delete_category(array $post): array
|
||||
return ['success' => true];
|
||||
}
|
||||
|
||||
function handle_toggle_menu_enabled(): array
|
||||
{
|
||||
set_menu_enabled(!is_menu_enabled());
|
||||
return ['success' => true, 'menu_enabled' => is_menu_enabled()];
|
||||
}
|
||||
|
||||
function handle_menu_action(string $action, array $post): array
|
||||
{
|
||||
return match ($action) {
|
||||
@@ -145,6 +151,7 @@ function handle_menu_action(string $action, array $post): array
|
||||
'save_category' => handle_save_category($post),
|
||||
'move_category' => handle_move_category($post),
|
||||
'delete_category' => handle_delete_category($post),
|
||||
'toggle_menu_enabled' => handle_toggle_menu_enabled(),
|
||||
default => ['success' => false, 'error' => 'Unknown action'],
|
||||
};
|
||||
}
|
||||
@@ -167,6 +174,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
|
||||
$categories = get_all_categories();
|
||||
$menuGrouped = get_full_menu(false);
|
||||
$menuEnabled = is_menu_enabled();
|
||||
?>
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
@@ -213,6 +221,13 @@ $menuGrouped = get_full_menu(false);
|
||||
<div class="admin-toolbar">
|
||||
<button type="button" class="btn btn-primary btn-sm" id="btn-new-item">+ Add Menu Item</button>
|
||||
<button type="button" class="btn btn-outline btn-sm" id="btn-new-cat">+ Add Category</button>
|
||||
<button type="button" class="btn btn-sm <?= $menuEnabled ? 'btn-danger' : 'btn-outline' ?>" id="btn-toggle-menu" data-enabled="<?= $menuEnabled ? '1' : '0' ?>">
|
||||
<?= $menuEnabled ? 'Disable Menu' : 'Enable Menu' ?>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="alert-error" id="menu-disabled-alert" role="status" <?= $menuEnabled ? 'hidden' : '' ?>>
|
||||
Menu is disabled — the public menu page shows a "menu being updated" message instead of your items.
|
||||
</div>
|
||||
|
||||
<div id="categories-container">
|
||||
|
||||
+37
-3
@@ -152,10 +152,38 @@ document.getElementById('form-password').addEventListener('submit', async (e) =>
|
||||
}
|
||||
});
|
||||
|
||||
// Delegate row actions: Change Password, Delete
|
||||
// Delegate row actions: toggle Status, Change Password, Delete
|
||||
document.getElementById('users-rows').addEventListener('click', async (e) => {
|
||||
const { target } = e;
|
||||
|
||||
// The status badge itself is the toggle button — same convention as the
|
||||
// menu items and announcements lists. Self's badge renders as a plain
|
||||
// (non-button) span, so this never matches your own row.
|
||||
if (target.classList.contains('btn-toggle-user')) {
|
||||
target.disabled = true;
|
||||
|
||||
const fd = new FormData();
|
||||
fd.append('action', 'toggle_user_enabled');
|
||||
fd.append('user_id', target.dataset.id);
|
||||
fd.append('csrf_token', csrfToken);
|
||||
|
||||
try {
|
||||
const data = await postForm(fd);
|
||||
if (data.success) {
|
||||
target.className = `status-badge btn-toggle-user ${data.status_class}`;
|
||||
target.textContent = data.status_badge;
|
||||
target.title = `Click to mark as ${data.is_enabled ? 'disabled' : 'enabled'}`;
|
||||
} else {
|
||||
alert(data.error || 'Failed to update user status.');
|
||||
}
|
||||
} catch {
|
||||
alert('Network error. Please try again.');
|
||||
} finally {
|
||||
target.disabled = false;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (target.classList.contains('btn-change-password')) {
|
||||
openPasswordModal(target.dataset.id, target.dataset.username);
|
||||
return;
|
||||
@@ -197,14 +225,20 @@ function insertUserRow(user) {
|
||||
// data-username is set via dataset below, never interpolated into the
|
||||
// HTML string — escapeHtml() only escapes for text-content positions,
|
||||
// not attribute values, so it can't safely build an attribute string.
|
||||
// A user you just created is never "you" — the current admin's own row
|
||||
// already exists — so its Status badge always renders as the clickable
|
||||
// toggle, never the plain self span (see users.php for that variant).
|
||||
row.innerHTML = `
|
||||
<div data-label="Status">
|
||||
<button type="button" class="status-badge btn-toggle-user active" data-id="${user.id}" title="Click to mark as disabled">● Enabled</button>
|
||||
</div>
|
||||
<div data-label="Username"><strong>${escapeHtml(user.username)}</strong></div>
|
||||
<div data-label="Role">${escapeHtml(user.role.charAt(0).toUpperCase() + user.role.slice(1))}</div>
|
||||
<div class="font-mono timestamp-cell" data-label="Created">${escapeHtml(user.created_at)}</div>
|
||||
<div data-label="Actions"><div class="row-actions">
|
||||
<div data-label="Actions"><div class="row-actions"><div class="action-stack">
|
||||
<button type="button" class="btn btn-outline btn-sm btn-change-password" data-id="${user.id}">Change Password</button>
|
||||
<button type="button" class="btn btn-danger btn-sm btn-delete-user" data-id="${user.id}">Delete</button>
|
||||
</div></div>
|
||||
</div></div></div>
|
||||
`;
|
||||
row.querySelector('.btn-change-password').dataset.username = user.username;
|
||||
|
||||
|
||||
+53
-10
@@ -93,6 +93,32 @@ function handle_change_password(array $post, array $currentUser): array {
|
||||
return ['success' => true];
|
||||
}
|
||||
|
||||
function handle_toggle_user_enabled(array $post, array $currentUser): array {
|
||||
$targetId = (int) ($post['user_id'] ?? 0);
|
||||
$target = $targetId > 0 ? get_user_by_id($targetId) : null;
|
||||
if (!$target) {
|
||||
return ['success' => false, 'error' => 'User not found.'];
|
||||
}
|
||||
if ($targetId === (int) $currentUser['id']) {
|
||||
return ['success' => false, 'error' => "You can't disable your own account while logged in as it."];
|
||||
}
|
||||
|
||||
$currentlyEnabled = (int) $target['is_enabled'] === 1;
|
||||
if ($currentlyEnabled && count_enabled_users() <= 1) {
|
||||
return ['success' => false, 'error' => 'At least one user account must remain enabled.'];
|
||||
}
|
||||
|
||||
toggle_user_enabled($targetId);
|
||||
$isEnabled = (int) get_user_by_id($targetId)['is_enabled'] === 1;
|
||||
|
||||
return [
|
||||
'success' => true,
|
||||
'is_enabled' => $isEnabled,
|
||||
'status_badge' => $isEnabled ? '● Enabled' : '○ Disabled',
|
||||
'status_class' => $isEnabled ? 'active' : 'inactive',
|
||||
];
|
||||
}
|
||||
|
||||
function handle_delete_user(array $post, array $currentUser): array {
|
||||
$targetId = (int) ($post['user_id'] ?? 0);
|
||||
if ($targetId <= 0 || !get_user_by_id($targetId)) {
|
||||
@@ -113,6 +139,7 @@ function handle_users_action(string $action, array $post, array $currentUser): a
|
||||
return match ($action) {
|
||||
'create_user' => handle_create_user($post, $currentUser),
|
||||
'change_password' => handle_change_password($post, $currentUser),
|
||||
'toggle_user_enabled' => handle_toggle_user_enabled($post, $currentUser),
|
||||
'delete_user' => handle_delete_user($post, $currentUser),
|
||||
default => ['success' => false, 'error' => 'Unknown action'],
|
||||
};
|
||||
@@ -191,6 +218,7 @@ $users = get_all_users();
|
||||
|
||||
<div class="user-grid-wrap">
|
||||
<div class="user-grid-head" aria-hidden="true">
|
||||
<span class="col-status">Status</span>
|
||||
<span class="col-username">Username</span>
|
||||
<span class="col-role">Role</span>
|
||||
<span class="col-created">Created</span>
|
||||
@@ -198,10 +226,23 @@ $users = get_all_users();
|
||||
</div>
|
||||
<div class="user-rows" id="users-rows">
|
||||
<?php foreach ($users as $row): ?>
|
||||
<?php $isSelf = (int) $row['id'] === (int) $user['id']; ?>
|
||||
<?php $isEnabled = (int) $row['is_enabled'] === 1; ?>
|
||||
<div class="user-row" id="user-row-<?= (int) $row['id'] ?>" data-user-id="<?= (int) $row['id'] ?>">
|
||||
<div data-label="Status">
|
||||
<?php if ($isSelf): ?>
|
||||
<span class="status-badge <?= $isEnabled ? 'active' : 'inactive' ?>" title="You can't disable your own account.">
|
||||
<?= $isEnabled ? '● Enabled' : '○ Disabled' ?>
|
||||
</span>
|
||||
<?php else: ?>
|
||||
<button type="button" class="status-badge btn-toggle-user <?= $isEnabled ? 'active' : 'inactive' ?>" data-id="<?= (int) $row['id'] ?>" title="Click to mark as <?= $isEnabled ? 'disabled' : 'enabled' ?>">
|
||||
<?= $isEnabled ? '● Enabled' : '○ Disabled' ?>
|
||||
</button>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<div data-label="Username">
|
||||
<strong><?= htmlspecialchars($row['username']) ?></strong>
|
||||
<?php if ((int) $row['id'] === (int) $user['id']): ?>
|
||||
<?php if ($isSelf): ?>
|
||||
<span class="muted"> (you)</span>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
@@ -209,16 +250,18 @@ $users = get_all_users();
|
||||
<div class="font-mono timestamp-cell" data-label="Created"><?= htmlspecialchars($row['created_at']) ?></div>
|
||||
<div data-label="Actions">
|
||||
<div class="row-actions">
|
||||
<button type="button" class="btn btn-outline btn-sm btn-change-password"
|
||||
data-id="<?= (int) $row['id'] ?>"
|
||||
data-username="<?= htmlspecialchars($row['username']) ?>">
|
||||
Change Password
|
||||
</button>
|
||||
<?php if ((int) $row['id'] !== (int) $user['id']): ?>
|
||||
<button type="button" class="btn btn-danger btn-sm btn-delete-user" data-id="<?= (int) $row['id'] ?>">
|
||||
Delete
|
||||
<div class="action-stack">
|
||||
<button type="button" class="btn btn-outline btn-sm btn-change-password"
|
||||
data-id="<?= (int) $row['id'] ?>"
|
||||
data-username="<?= htmlspecialchars($row['username']) ?>">
|
||||
Change Password
|
||||
</button>
|
||||
<?php endif; ?>
|
||||
<?php if (!$isSelf): ?>
|
||||
<button type="button" class="btn btn-danger btn-sm btn-delete-user" data-id="<?= (int) $row['id'] ?>">
|
||||
Delete
|
||||
</button>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in new issue
Block a user