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>
|
||||||
<div data-label="Actions">
|
<div data-label="Actions">
|
||||||
<div class="row-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?');">
|
<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="action" value="delete_announcement">
|
||||||
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars(get_csrf_token()) ?>">
|
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars(get_csrf_token()) ?>">
|
||||||
<input type="hidden" name="announcement_id" value="<?= (int)$ann['id'] ?>">
|
<input type="hidden" name="announcement_id" value="<?= (int)$ann['id'] ?>">
|
||||||
<button type="submit" class="btn btn-danger btn-sm">Delete</button>
|
<button type="submit" class="btn btn-danger btn-sm">Delete</button>
|
||||||
</form>
|
</form>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -86,9 +86,51 @@ async function postForm(formData) {
|
|||||||
return response.json();
|
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
|
// Modal listeners
|
||||||
document.getElementById('btn-new-item').addEventListener('click', () => openItemModal());
|
document.getElementById('btn-new-item').addEventListener('click', () => openItemModal());
|
||||||
document.getElementById('btn-new-cat').addEventListener('click', () => openCategoryModal());
|
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-create-first-cat')?.addEventListener('click', () => openCategoryModal());
|
||||||
document.getElementById('btn-close-item-modal').addEventListener('click', closeItemModal);
|
document.getElementById('btn-close-item-modal').addEventListener('click', closeItemModal);
|
||||||
document.getElementById('btn-cancel-item').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];
|
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
|
function handle_menu_action(string $action, array $post): array
|
||||||
{
|
{
|
||||||
return match ($action) {
|
return match ($action) {
|
||||||
@@ -145,6 +151,7 @@ function handle_menu_action(string $action, array $post): array
|
|||||||
'save_category' => handle_save_category($post),
|
'save_category' => handle_save_category($post),
|
||||||
'move_category' => handle_move_category($post),
|
'move_category' => handle_move_category($post),
|
||||||
'delete_category' => handle_delete_category($post),
|
'delete_category' => handle_delete_category($post),
|
||||||
|
'toggle_menu_enabled' => handle_toggle_menu_enabled(),
|
||||||
default => ['success' => false, 'error' => 'Unknown action'],
|
default => ['success' => false, 'error' => 'Unknown action'],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -167,6 +174,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
|||||||
|
|
||||||
$categories = get_all_categories();
|
$categories = get_all_categories();
|
||||||
$menuGrouped = get_full_menu(false);
|
$menuGrouped = get_full_menu(false);
|
||||||
|
$menuEnabled = is_menu_enabled();
|
||||||
?>
|
?>
|
||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
@@ -213,6 +221,13 @@ $menuGrouped = get_full_menu(false);
|
|||||||
<div class="admin-toolbar">
|
<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-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-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>
|
||||||
|
|
||||||
<div id="categories-container">
|
<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) => {
|
document.getElementById('users-rows').addEventListener('click', async (e) => {
|
||||||
const { target } = 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')) {
|
if (target.classList.contains('btn-change-password')) {
|
||||||
openPasswordModal(target.dataset.id, target.dataset.username);
|
openPasswordModal(target.dataset.id, target.dataset.username);
|
||||||
return;
|
return;
|
||||||
@@ -197,14 +225,20 @@ function insertUserRow(user) {
|
|||||||
// data-username is set via dataset below, never interpolated into the
|
// data-username is set via dataset below, never interpolated into the
|
||||||
// HTML string — escapeHtml() only escapes for text-content positions,
|
// HTML string — escapeHtml() only escapes for text-content positions,
|
||||||
// not attribute values, so it can't safely build an attribute string.
|
// 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 = `
|
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="Username"><strong>${escapeHtml(user.username)}</strong></div>
|
||||||
<div data-label="Role">${escapeHtml(user.role.charAt(0).toUpperCase() + user.role.slice(1))}</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 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-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>
|
<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;
|
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];
|
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 {
|
function handle_delete_user(array $post, array $currentUser): array {
|
||||||
$targetId = (int) ($post['user_id'] ?? 0);
|
$targetId = (int) ($post['user_id'] ?? 0);
|
||||||
if ($targetId <= 0 || !get_user_by_id($targetId)) {
|
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) {
|
return match ($action) {
|
||||||
'create_user' => handle_create_user($post, $currentUser),
|
'create_user' => handle_create_user($post, $currentUser),
|
||||||
'change_password' => handle_change_password($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),
|
'delete_user' => handle_delete_user($post, $currentUser),
|
||||||
default => ['success' => false, 'error' => 'Unknown action'],
|
default => ['success' => false, 'error' => 'Unknown action'],
|
||||||
};
|
};
|
||||||
@@ -191,6 +218,7 @@ $users = get_all_users();
|
|||||||
|
|
||||||
<div class="user-grid-wrap">
|
<div class="user-grid-wrap">
|
||||||
<div class="user-grid-head" aria-hidden="true">
|
<div class="user-grid-head" aria-hidden="true">
|
||||||
|
<span class="col-status">Status</span>
|
||||||
<span class="col-username">Username</span>
|
<span class="col-username">Username</span>
|
||||||
<span class="col-role">Role</span>
|
<span class="col-role">Role</span>
|
||||||
<span class="col-created">Created</span>
|
<span class="col-created">Created</span>
|
||||||
@@ -198,10 +226,23 @@ $users = get_all_users();
|
|||||||
</div>
|
</div>
|
||||||
<div class="user-rows" id="users-rows">
|
<div class="user-rows" id="users-rows">
|
||||||
<?php foreach ($users as $row): ?>
|
<?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 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">
|
<div data-label="Username">
|
||||||
<strong><?= htmlspecialchars($row['username']) ?></strong>
|
<strong><?= htmlspecialchars($row['username']) ?></strong>
|
||||||
<?php if ((int) $row['id'] === (int) $user['id']): ?>
|
<?php if ($isSelf): ?>
|
||||||
<span class="muted"> (you)</span>
|
<span class="muted"> (you)</span>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
</div>
|
</div>
|
||||||
@@ -209,16 +250,18 @@ $users = get_all_users();
|
|||||||
<div class="font-mono timestamp-cell" data-label="Created"><?= htmlspecialchars($row['created_at']) ?></div>
|
<div class="font-mono timestamp-cell" data-label="Created"><?= htmlspecialchars($row['created_at']) ?></div>
|
||||||
<div data-label="Actions">
|
<div data-label="Actions">
|
||||||
<div class="row-actions">
|
<div class="row-actions">
|
||||||
<button type="button" class="btn btn-outline btn-sm btn-change-password"
|
<div class="action-stack">
|
||||||
data-id="<?= (int) $row['id'] ?>"
|
<button type="button" class="btn btn-outline btn-sm btn-change-password"
|
||||||
data-username="<?= htmlspecialchars($row['username']) ?>">
|
data-id="<?= (int) $row['id'] ?>"
|
||||||
Change Password
|
data-username="<?= htmlspecialchars($row['username']) ?>">
|
||||||
</button>
|
Change Password
|
||||||
<?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
|
|
||||||
</button>
|
</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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+14
-1
@@ -25,6 +25,19 @@ function require_auth(): void {
|
|||||||
header('Location: login.php');
|
header('Location: login.php');
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// A user disabled mid-session should lose access immediately rather
|
||||||
|
// than keep it until their session expires — re-check against the DB
|
||||||
|
// instead of trusting the session's now-possibly-stale copy.
|
||||||
|
$pdo = get_db();
|
||||||
|
$stmt = $pdo->prepare("SELECT is_enabled FROM users WHERE id = ?");
|
||||||
|
$stmt->execute([$_SESSION['admin_user_id']]);
|
||||||
|
$stillEnabled = $stmt->fetchColumn();
|
||||||
|
if ($stillEnabled === false || (int) $stillEnabled !== 1) {
|
||||||
|
logout_user();
|
||||||
|
header('Location: login.php');
|
||||||
|
exit;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function get_logged_in_user(): ?array {
|
function get_logged_in_user(): ?array {
|
||||||
@@ -44,7 +57,7 @@ function authenticate_user(string $username, string $password): bool {
|
|||||||
$stmt->execute([trim($username)]);
|
$stmt->execute([trim($username)]);
|
||||||
$user = $stmt->fetch();
|
$user = $stmt->fetch();
|
||||||
|
|
||||||
if ($user && password_verify($password, $user['password_hash'])) {
|
if ($user && (int) $user['is_enabled'] === 1 && password_verify($password, $user['password_hash'])) {
|
||||||
session_regenerate_id(true);
|
session_regenerate_id(true);
|
||||||
$_SESSION['admin_user_id'] = $user['id'];
|
$_SESSION['admin_user_id'] = $user['id'];
|
||||||
$_SESSION['admin_username'] = $user['username'];
|
$_SESSION['admin_username'] = $user['username'];
|
||||||
|
|||||||
+31
@@ -42,6 +42,7 @@ function init_db_schema(PDO $pdo): void {
|
|||||||
username TEXT UNIQUE NOT NULL,
|
username TEXT UNIQUE NOT NULL,
|
||||||
password_hash TEXT NOT NULL,
|
password_hash TEXT NOT NULL,
|
||||||
role TEXT NOT NULL DEFAULT 'admin',
|
role TEXT NOT NULL DEFAULT 'admin',
|
||||||
|
is_enabled INTEGER NOT NULL DEFAULT 1,
|
||||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -86,8 +87,31 @@ function init_db_schema(PDO $pdo): void {
|
|||||||
kitchen_close_time TEXT NULL,
|
kitchen_close_time TEXT NULL,
|
||||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||||
);
|
);
|
||||||
|
|
||||||
|
-- Single-row table (id is pinned to 1) for site-wide toggles that
|
||||||
|
-- don't belong to any one record — starting with whether the
|
||||||
|
-- public menu page shows its items at all.
|
||||||
|
CREATE TABLE IF NOT EXISTS site_settings (
|
||||||
|
id INTEGER PRIMARY KEY CHECK (id = 1),
|
||||||
|
menu_enabled INTEGER NOT NULL DEFAULT 1,
|
||||||
|
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||||
|
);
|
||||||
");
|
");
|
||||||
|
|
||||||
|
// users.is_enabled shipped after the initial release — CREATE TABLE IF
|
||||||
|
// NOT EXISTS above is a no-op against an existing table, so an install
|
||||||
|
// from before this needs the column added explicitly, once.
|
||||||
|
$hasIsEnabled = false;
|
||||||
|
foreach ($pdo->query("PRAGMA table_info(users)") as $col) {
|
||||||
|
if ($col['name'] === 'is_enabled') {
|
||||||
|
$hasIsEnabled = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!$hasIsEnabled) {
|
||||||
|
$pdo->exec("ALTER TABLE users ADD COLUMN is_enabled INTEGER NOT NULL DEFAULT 1");
|
||||||
|
}
|
||||||
|
|
||||||
// Seed default admin user if table is empty (username: admin / password: password)
|
// Seed default admin user if table is empty (username: admin / password: password)
|
||||||
$stmt = $pdo->query("SELECT COUNT(*) as count FROM users");
|
$stmt = $pdo->query("SELECT COUNT(*) as count FROM users");
|
||||||
$userCount = $stmt->fetchColumn();
|
$userCount = $stmt->fetchColumn();
|
||||||
@@ -125,4 +149,11 @@ function init_db_schema(PDO $pdo): void {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Seed the singleton settings row (menu on by default) if it doesn't exist yet.
|
||||||
|
$stmt = $pdo->query("SELECT COUNT(*) as count FROM site_settings");
|
||||||
|
$settingsCount = $stmt->fetchColumn();
|
||||||
|
if ($settingsCount == 0) {
|
||||||
|
$pdo->exec("INSERT INTO site_settings (id, menu_enabled) VALUES (1, 1)");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -7,6 +7,22 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
require_once __DIR__ . '/db.php';
|
require_once __DIR__ . '/db.php';
|
||||||
|
|
||||||
|
// Whether the public menu page should show its items at all — separate
|
||||||
|
// from any individual item's is_available, this is the admin-facing "take
|
||||||
|
// the whole menu down for maintenance" switch. Defaults to enabled if the
|
||||||
|
// singleton settings row is somehow missing.
|
||||||
|
function is_menu_enabled(): bool {
|
||||||
|
$pdo = get_db();
|
||||||
|
$value = $pdo->query("SELECT menu_enabled FROM site_settings WHERE id = 1")->fetchColumn();
|
||||||
|
return $value === false ? true : (bool) $value;
|
||||||
|
}
|
||||||
|
|
||||||
|
function set_menu_enabled(bool $enabled): void {
|
||||||
|
$pdo = get_db();
|
||||||
|
$stmt = $pdo->prepare("UPDATE site_settings SET menu_enabled = ?, updated_at = CURRENT_TIMESTAMP WHERE id = 1");
|
||||||
|
$stmt->execute([$enabled ? 1 : 0]);
|
||||||
|
}
|
||||||
|
|
||||||
function get_full_menu(bool $availableOnly = false): array {
|
function get_full_menu(bool $availableOnly = false): array {
|
||||||
$pdo = get_db();
|
$pdo = get_db();
|
||||||
|
|
||||||
|
|||||||
+13
-2
@@ -13,13 +13,13 @@ require_once __DIR__ . '/db.php';
|
|||||||
|
|
||||||
function get_all_users(): array {
|
function get_all_users(): array {
|
||||||
$pdo = get_db();
|
$pdo = get_db();
|
||||||
$stmt = $pdo->query("SELECT id, username, role, created_at FROM users ORDER BY username ASC");
|
$stmt = $pdo->query("SELECT id, username, role, is_enabled, created_at FROM users ORDER BY username ASC");
|
||||||
return $stmt->fetchAll() ?: [];
|
return $stmt->fetchAll() ?: [];
|
||||||
}
|
}
|
||||||
|
|
||||||
function get_user_by_id(int $id): ?array {
|
function get_user_by_id(int $id): ?array {
|
||||||
$pdo = get_db();
|
$pdo = get_db();
|
||||||
$stmt = $pdo->prepare("SELECT id, username, role, created_at FROM users WHERE id = ?");
|
$stmt = $pdo->prepare("SELECT id, username, role, is_enabled, created_at FROM users WHERE id = ?");
|
||||||
$stmt->execute([$id]);
|
$stmt->execute([$id]);
|
||||||
$res = $stmt->fetch();
|
$res = $stmt->fetch();
|
||||||
return $res ?: null;
|
return $res ?: null;
|
||||||
@@ -30,6 +30,17 @@ function count_users(): int {
|
|||||||
return (int) $pdo->query("SELECT COUNT(*) FROM users")->fetchColumn();
|
return (int) $pdo->query("SELECT COUNT(*) FROM users")->fetchColumn();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function count_enabled_users(): int {
|
||||||
|
$pdo = get_db();
|
||||||
|
return (int) $pdo->query("SELECT COUNT(*) FROM users WHERE is_enabled = 1")->fetchColumn();
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggle_user_enabled(int $id): bool {
|
||||||
|
$pdo = get_db();
|
||||||
|
$stmt = $pdo->prepare("UPDATE users SET is_enabled = CASE WHEN is_enabled = 1 THEN 0 ELSE 1 END WHERE id = ?");
|
||||||
|
return $stmt->execute([$id]);
|
||||||
|
}
|
||||||
|
|
||||||
function username_exists(string $username, ?int $excludeId = null): bool {
|
function username_exists(string $username, ?int $excludeId = null): bool {
|
||||||
$pdo = get_db();
|
$pdo = get_db();
|
||||||
if ($excludeId !== null) {
|
if ($excludeId !== null) {
|
||||||
|
|||||||
@@ -84,7 +84,7 @@ $siteUrl = rtrim(SITE_URL, '/');
|
|||||||
<div class="announcement-banner banner-<?= htmlspecialchars($banner['type']) ?>" role="region" aria-label="Important Announcement">
|
<div class="announcement-banner banner-<?= htmlspecialchars($banner['type']) ?>" role="region" aria-label="Important Announcement">
|
||||||
<div class="container announcement-inner">
|
<div class="container announcement-inner">
|
||||||
<span class="announcement-tag"><?= htmlspecialchars($banner['headline']) ?></span>
|
<span class="announcement-tag"><?= htmlspecialchars($banner['headline']) ?></span>
|
||||||
<span class="announcement-sep" aria-hidden="true">·</span>
|
<span class="announcement-sep wrap-sep" aria-hidden="true">·</span>
|
||||||
<span class="announcement-text"><?= htmlspecialchars($banner['message']) ?></span>
|
<span class="announcement-text"><?= htmlspecialchars($banner['message']) ?></span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -105,7 +105,7 @@ $siteUrl = rtrim(SITE_URL, '/');
|
|||||||
<!-- <h1>The Old Fitz</h1> -->
|
<!-- <h1>The Old Fitz</h1> -->
|
||||||
<!-- <p class="sub">Public House</p> -->
|
<!-- <p class="sub">Public House</p> -->
|
||||||
<div class="tagline-row">
|
<div class="tagline-row">
|
||||||
<span>Traditional Irish Pub</span><span>Family Friendly</span><span>No Reservations</span>
|
<span>Traditional Irish Pub</span><span class="tag-sep wrap-sep" aria-hidden="true">·</span><span>Family Friendly</span><span class="tag-sep wrap-sep" aria-hidden="true">·</span><span>No Reservations</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="hero-ctas">
|
<div class="hero-ctas">
|
||||||
<a class="btn btn-primary" href="https://www.google.com/maps/search/?api=1&query=100+N+Washington+St+Marion+IN" target="_blank" rel="noopener">Get Directions</a>
|
<a class="btn btn-primary" href="https://www.google.com/maps/search/?api=1&query=100+N+Washington+St+Marion+IN" target="_blank" rel="noopener">Get Directions</a>
|
||||||
|
|||||||
@@ -64,3 +64,56 @@
|
|||||||
closeNav();
|
closeNav();
|
||||||
});
|
});
|
||||||
})();
|
})();
|
||||||
|
|
||||||
|
// Wrap-aware separator dots (announcement banner, hero tagline row).
|
||||||
|
// Hides a "·" only when its two flanking elements wrap onto different
|
||||||
|
// visual lines. There's no CSS way to detect "same line as sibling," and
|
||||||
|
// a fixed viewport breakpoint can't substitute for it either, since
|
||||||
|
// wrapping depends on the announcement's own text length, not just
|
||||||
|
// screen width.
|
||||||
|
(() => {
|
||||||
|
const seps = document.querySelectorAll('.wrap-sep');
|
||||||
|
if (!seps.length) return;
|
||||||
|
|
||||||
|
function sameLine(a, b) {
|
||||||
|
const ra = a.getBoundingClientRect();
|
||||||
|
const rb = b.getBoundingClientRect();
|
||||||
|
return ra.top < rb.bottom && rb.top < ra.bottom;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hiding a separator removes it from flow (see .wrap-sep-hidden in
|
||||||
|
// styles.css — needs to, so a hidden dot never skews centering by still
|
||||||
|
// reserving its space), which means one separator's hide/show can shift
|
||||||
|
// where a *later* one's neighbors land — e.g. the tagline row's second
|
||||||
|
// dot needs to see the first dot's decision already applied, or it may
|
||||||
|
// judge its own neighbors' line-sharing from a layout that's since
|
||||||
|
// changed underneath it. So this deliberately processes separators one
|
||||||
|
// at a time, in DOM order, applying each decision before measuring the
|
||||||
|
// next: reset just this one to visible (so its own contribution is
|
||||||
|
// never argued away by whatever its own current hidden state already
|
||||||
|
// happens to be — otherwise "hidden" would be self-confirming, since a
|
||||||
|
// hidden dot's neighbors always look like they share a line), measure,
|
||||||
|
// decide, apply, then move on with that change already in effect.
|
||||||
|
function sync() {
|
||||||
|
seps.forEach((sep) => {
|
||||||
|
const prev = sep.previousElementSibling;
|
||||||
|
const next = sep.nextElementSibling;
|
||||||
|
if (!prev || !next) return;
|
||||||
|
|
||||||
|
sep.classList.remove('wrap-sep-hidden');
|
||||||
|
sep.classList.toggle('wrap-sep-hidden', !sameLine(prev, next));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
sync();
|
||||||
|
|
||||||
|
let resizeTimer;
|
||||||
|
window.addEventListener('resize', () => {
|
||||||
|
clearTimeout(resizeTimer);
|
||||||
|
resizeTimer = setTimeout(sync, 100);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (document.fonts && document.fonts.ready) {
|
||||||
|
document.fonts.ready.then(sync);
|
||||||
|
}
|
||||||
|
})();
|
||||||
@@ -7,7 +7,8 @@ require __DIR__ . '/inc/menu.php';
|
|||||||
require __DIR__ . '/inc/hours.php';
|
require __DIR__ . '/inc/hours.php';
|
||||||
|
|
||||||
$activeAnnouncements = get_active_announcements();
|
$activeAnnouncements = get_active_announcements();
|
||||||
$menuCategories = get_full_menu(true); // available items only
|
$menuEnabled = is_menu_enabled();
|
||||||
|
$menuCategories = $menuEnabled ? get_full_menu(true) : []; // available items only, or none while the menu is disabled
|
||||||
$hoursByDay = get_all_hours();
|
$hoursByDay = get_all_hours();
|
||||||
$siteUrl = rtrim(SITE_URL, '/');
|
$siteUrl = rtrim(SITE_URL, '/');
|
||||||
?>
|
?>
|
||||||
@@ -83,7 +84,7 @@ $siteUrl = rtrim(SITE_URL, '/');
|
|||||||
<div class="announcement-banner banner-<?= htmlspecialchars($banner['type']) ?>" role="region" aria-label="Important Announcement">
|
<div class="announcement-banner banner-<?= htmlspecialchars($banner['type']) ?>" role="region" aria-label="Important Announcement">
|
||||||
<div class="container announcement-inner">
|
<div class="container announcement-inner">
|
||||||
<span class="announcement-tag"><?= htmlspecialchars($banner['headline']) ?></span>
|
<span class="announcement-tag"><?= htmlspecialchars($banner['headline']) ?></span>
|
||||||
<span class="announcement-sep" aria-hidden="true">·</span>
|
<span class="announcement-sep wrap-sep" aria-hidden="true">·</span>
|
||||||
<span class="announcement-text"><?= htmlspecialchars($banner['message']) ?></span>
|
<span class="announcement-text"><?= htmlspecialchars($banner['message']) ?></span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -109,7 +110,7 @@ $siteUrl = rtrim(SITE_URL, '/');
|
|||||||
|
|
||||||
<section class="menu-page-content" style="padding-top: 0;">
|
<section class="menu-page-content" style="padding-top: 0;">
|
||||||
<div class="container" style="max-width: 62rem;">
|
<div class="container" style="max-width: 62rem;">
|
||||||
<?php if (!empty($menuCategories)): ?>
|
<?php if ($menuEnabled && !empty($menuCategories)): ?>
|
||||||
<div class="menu-grid" style="margin-top: 0;">
|
<div class="menu-grid" style="margin-top: 0;">
|
||||||
<?php foreach ($menuCategories as $category): ?>
|
<?php foreach ($menuCategories as $category): ?>
|
||||||
<?php if (!empty($category['items'])): ?>
|
<?php if (!empty($category['items'])): ?>
|
||||||
|
|||||||
+116
-22
@@ -34,6 +34,26 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ---------- wrap-aware separators (JS-toggled, see main.js) ----------
|
||||||
|
Shared hook used by the announcement banner and hero tagline row: a
|
||||||
|
"·" between two pieces of text that can independently wrap onto
|
||||||
|
separate lines. No CSS selector exists for "am I on the same line as
|
||||||
|
my sibling," so main.js measures and toggles this class instead of a
|
||||||
|
fixed breakpoint. Visible by default so a no-JS visitor always sees
|
||||||
|
the dot (today's behavior), never hidden by default.
|
||||||
|
|
||||||
|
display, not visibility: a hidden separator must take no space, or
|
||||||
|
it still gets counted when justify-content: center balances its flex
|
||||||
|
line — a lone badge sharing its line with a space-reserving invisible
|
||||||
|
dot renders off-center. Safe from the "text merges together" bug that
|
||||||
|
originally motivated reserving space: both wrap-sep contexts space
|
||||||
|
their siblings with a real flex `gap` (see .announcement-inner,
|
||||||
|
.tagline-row) rather than the separator's own margin, so removing it
|
||||||
|
from flow still leaves normal spacing behind, never a collision. */
|
||||||
|
.wrap-sep-hidden {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
background: var(--ground);
|
background: var(--ground);
|
||||||
@@ -478,17 +498,28 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
gap: 0.6rem 0;
|
/* column-gap carries the spacing now (was 0 — the dot's own margin
|
||||||
|
used to be the only thing separating the phrases). Needs to be a
|
||||||
|
real flex gap, not a margin on the dot, so removing a hidden dot
|
||||||
|
from flow (see .wrap-sep-hidden) still leaves normal spacing
|
||||||
|
between the two phrases instead of running them together. */
|
||||||
|
gap: 0.6rem 0.7rem;
|
||||||
font-family: var(--font-mono);
|
font-family: var(--font-mono);
|
||||||
font-size: 0.82rem;
|
font-size: 0.82rem;
|
||||||
letter-spacing: 0.04em;
|
letter-spacing: 0.04em;
|
||||||
color: var(--ink-muted);
|
color: var(--ink-muted);
|
||||||
}
|
}
|
||||||
|
|
||||||
.tagline-row span:not(:last-child)::after {
|
/* Real element instead of a ::after pseudo-element — needs to be a
|
||||||
content: "·";
|
measurable, independently-toggleable DOM node for the wrap-aware
|
||||||
|
script in main.js to hide it when its neighbors wrap apart. No margin
|
||||||
|
of its own: the column-gap above already provides 0.7rem on each
|
||||||
|
side of it (matching the old margin-inline's total spacing exactly),
|
||||||
|
and still applies directly between the two phrases whenever the dot
|
||||||
|
itself is removed from flow. */
|
||||||
|
.tag-sep {
|
||||||
color: var(--gold-dim);
|
color: var(--gold-dim);
|
||||||
margin-inline: 0.7rem;
|
user-select: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.hero-ctas {
|
.hero-ctas {
|
||||||
@@ -502,6 +533,11 @@
|
|||||||
.btn {
|
.btn {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
/* Centers label text horizontally too — matters once a button is
|
||||||
|
stretched wider than its content (e.g. action-stack children filling
|
||||||
|
a fixed-width column) or wraps to multiple lines; a no-op otherwise,
|
||||||
|
so every button gets this instead of one-off overrides per case. */
|
||||||
|
justify-content: center;
|
||||||
gap: 0.5rem;
|
gap: 0.5rem;
|
||||||
padding: 0.8rem 1.5rem;
|
padding: 0.8rem 1.5rem;
|
||||||
font-family: var(--font-mono);
|
font-family: var(--font-mono);
|
||||||
@@ -1330,6 +1366,14 @@
|
|||||||
width: 7.25rem;
|
width: 7.25rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* "○ Disabled" is the longer of the two user labels. Not scoped to
|
||||||
|
button.status-badge like the two above — your own row's badge is a
|
||||||
|
plain non-interactive span (see users.php), and it still needs to line
|
||||||
|
up in the same column. */
|
||||||
|
.user-row .status-badge {
|
||||||
|
width: 7rem;
|
||||||
|
}
|
||||||
|
|
||||||
/* Admin row actions: the button-group styling shared by every admin
|
/* Admin row actions: the button-group styling shared by every admin
|
||||||
list (menu items/categories, announcements, users). The actual
|
list (menu items/categories, announcements, users). The actual
|
||||||
row/column layout for each list is its own CSS Grid, defined where
|
row/column layout for each list is its own CSS Grid, defined where
|
||||||
@@ -1576,7 +1620,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.user-grid-wrap {
|
.user-grid-wrap {
|
||||||
grid-template-columns: minmax(0, 1fr) auto auto auto;
|
grid-template-columns: auto minmax(0, 1fr) auto auto auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.item-grid-wrap,
|
.item-grid-wrap,
|
||||||
@@ -1640,12 +1684,13 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.user-grid-head, .user-row {
|
.user-grid-head, .user-row {
|
||||||
grid-template-areas: "username role created actions";
|
grid-template-areas: "status username role created actions";
|
||||||
}
|
}
|
||||||
|
|
||||||
.col-status,
|
.col-status,
|
||||||
.item-row [data-label="Status"],
|
.item-row [data-label="Status"],
|
||||||
.ann-row [data-label="Status"] {
|
.ann-row [data-label="Status"],
|
||||||
|
.user-row [data-label="Status"] {
|
||||||
grid-area: status;
|
grid-area: status;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1726,10 +1771,37 @@
|
|||||||
columns: subgrid` from the desktop rule, so this one change is all
|
columns: subgrid` from the desktop rule, so this one change is all
|
||||||
it takes to reflow the whole list (see grid-template-areas per row
|
it takes to reflow the whole list (see grid-template-areas per row
|
||||||
further down for how each field then stacks within that column). */
|
further down for how each field then stacks within that column). */
|
||||||
|
/* Every list's cards get a second column on mobile so Actions can sit
|
||||||
|
in the top-right corner alongside that list's leading field, instead
|
||||||
|
of stacking at the very bottom of the card — overrides the shared
|
||||||
|
single-column rule that would otherwise apply here too.
|
||||||
|
minmax(0, auto), not plain auto, so this column can still shrink
|
||||||
|
below its content's natural width under pressure instead of forcing
|
||||||
|
the whole row to overflow the card. */
|
||||||
.item-grid-wrap,
|
.item-grid-wrap,
|
||||||
.ann-grid-wrap,
|
.ann-grid-wrap,
|
||||||
.user-grid-wrap {
|
.user-grid-wrap {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr minmax(0, auto);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Users' actions column gets a fixed cap instead of auto: "Change
|
||||||
|
Password" is long enough that its max-content size (what auto would
|
||||||
|
size the column to) is wider than this corner has room for, which
|
||||||
|
pushed the button past the card edge. A firm cap plus the
|
||||||
|
wrap/stretch rules below keep it predictable instead. */
|
||||||
|
.user-grid-wrap {
|
||||||
|
grid-template-columns: minmax(0, 1fr) 6rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Every row's Actions cell holds one or two .action-stacks (move
|
||||||
|
up/down, edit/delete, etc. — each stacked vertically, see
|
||||||
|
.action-stack itself further up) side by side. Keeps those stacks
|
||||||
|
from wrapping onto their own lines in this narrow corner, which is
|
||||||
|
what the shared .row-actions wrap rule below would otherwise do. */
|
||||||
|
.item-row .row-actions,
|
||||||
|
.ann-row .row-actions,
|
||||||
|
.user-row .row-actions {
|
||||||
|
flex-wrap: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.item-grid-head,
|
.item-grid-head,
|
||||||
@@ -1770,28 +1842,50 @@
|
|||||||
|
|
||||||
.item-row {
|
.item-row {
|
||||||
grid-template-areas:
|
grid-template-areas:
|
||||||
"status"
|
"status actions"
|
||||||
"item"
|
"item item"
|
||||||
"price"
|
"price price";
|
||||||
"actions";
|
/* Overrides the shared align-items: center above — on desktop that
|
||||||
|
vertically centers Status against the taller Item cell in one wide
|
||||||
|
row, which is fine, but here it also centers Status against the
|
||||||
|
taller Actions block above, pushing its badge down instead of
|
||||||
|
keeping both flush with the card's top edge. */
|
||||||
|
align-items: start;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ann-row {
|
.ann-row {
|
||||||
grid-template-areas:
|
grid-template-areas:
|
||||||
"status"
|
"status actions"
|
||||||
"headline"
|
"headline headline"
|
||||||
"type"
|
"type type"
|
||||||
"schedule"
|
"schedule schedule"
|
||||||
"placements"
|
"placements placements";
|
||||||
"actions";
|
align-items: start;
|
||||||
}
|
}
|
||||||
|
|
||||||
.user-row {
|
.user-row {
|
||||||
grid-template-areas:
|
grid-template-areas:
|
||||||
"username"
|
"status actions"
|
||||||
"role"
|
"username username"
|
||||||
"created"
|
"role role"
|
||||||
"actions";
|
"created created";
|
||||||
|
align-items: start;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Stretches Actions' whole flex chain out to the fixed 6rem column
|
||||||
|
above (each is shrink-wrapped by default), then lets "Change
|
||||||
|
Password" — long enough on its own to blow out this corner next to a
|
||||||
|
username — wrap onto two lines within that width instead of forcing
|
||||||
|
the column wider. Every other action-stack button here (Edit,
|
||||||
|
Delete, ↑/↓) is short enough to stay on one line regardless. */
|
||||||
|
.user-row .row-actions,
|
||||||
|
.user-row .action-stack,
|
||||||
|
.user-row .row-actions .btn {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-row .btn-change-password {
|
||||||
|
white-space: normal;
|
||||||
}
|
}
|
||||||
|
|
||||||
.category-panel .panel-head {
|
.category-panel .panel-head {
|
||||||
|
|||||||
Reference in new issue
Block a user