Add online menu management, modernized and hardened

Adds a public menu page (menu.php) and an admin CRUD interface
(admin/menu.php + admin/menu.js) backed by new menu_categories and
menu_items tables, replacing the "menu lives on Facebook" placeholder
on the homepage.

The admin JS/PHP came out of an AI-assisted pass that used outdated
JS (var, .then() chains, string-built HTML) and had some rough edges
security-wise, so this also:

- Rewrites the admin JS to modern ES (const/let, async/await,
  template literals, dataset API) and splits it out of admin/menu.php
  into admin/menu.js for proper separation of concerns
- Adds declare(strict_types=1) and PHP 8 match-based action dispatch
  in place of an if-chain
- Validates category_id/item_id/category_id server-side before use,
  so a bad ID returns a clean JSON error instead of an uncaught
  PDOException or a null item breaking the client
- Rejects non-string POST fields instead of crashing trim() on them
- Replaces the category-save handler's full-page location.reload()
  with in-place DOM updates (panel insert/update, resort by
  sort_order, keep the item modal's category dropdown in sync), so
  every admin action is now AJAX with no reload
- Sets data-name/data-desc via dataset assignment rather than string-
  interpolating them into an HTML attribute, closing a stored-XSS
  path through the category name/description fields

Removes seed-menu.php, a leftover stub with no working code left in
it after the AI's one-time seeding run.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
David DyerandClaude Sonnet 5 committed 2026-08-30 21:35:22 -04:00
1 parent 21ad6c31cf
commit f1bbf59548
8 files changed
+1382 -27

No files matched your search

+22 -14
View File
@@ -1,4 +1,6 @@
<?php
declare(strict_types=1);
/**
* Database connection & schema management for SQLite
*/
@@ -55,6 +57,26 @@ function init_db_schema(PDO $pdo): void {
show_in_hours INTEGER NOT NULL DEFAULT 1,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS menu_categories (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
description TEXT NULL,
sort_order INTEGER NOT NULL DEFAULT 0,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS menu_items (
id INTEGER PRIMARY KEY AUTOINCREMENT,
category_id INTEGER NOT NULL REFERENCES menu_categories(id) ON DELETE CASCADE,
name TEXT NOT NULL,
description TEXT NULL,
price TEXT NOT NULL,
badge TEXT NULL,
is_available INTEGER NOT NULL DEFAULT 1,
sort_order INTEGER NOT NULL DEFAULT 0,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
");
// Seed default admin user if table is empty (username: admin / password: password)
@@ -65,18 +87,4 @@ function init_db_schema(PDO $pdo): void {
$insert = $pdo->prepare("INSERT INTO users (username, password_hash, role) VALUES (?, ?, 'admin')");
$insert->execute(['admin', $defaultPassHash]);
}
// Seed default Labor Day announcement row if table is empty
$stmtAnn = $pdo->query("SELECT COUNT(*) as count FROM announcements");
$annCount = $stmtAnn->fetchColumn();
if ($annCount == 0) {
$insertAnn = $pdo->prepare("
INSERT INTO announcements (
is_active, type, headline, message, start_date, end_date, show_banner, show_in_hours
) VALUES (
1, 'holiday', 'Labor Day Closure', 'The Old Fitz will be closed Thursday, Sept 3 through Saturday, Sept 5 for the holiday weekend. Doors reopen Thursday, Sept 10 at 4:00 PM!', NULL, NULL, 1, 1
)
");
$insertAnn->execute();
}
}
+135
View File
@@ -0,0 +1,135 @@
<?php
declare(strict_types=1);
/**
* Menu helper functions
*/
require_once __DIR__ . '/db.php';
function get_full_menu(bool $availableOnly = false): array {
$pdo = get_db();
$catStmt = $pdo->query("SELECT * FROM menu_categories ORDER BY sort_order ASC, id ASC");
$categories = $catStmt->fetchAll() ?: [];
$itemSql = "SELECT * FROM menu_items";
if ($availableOnly) {
$itemSql .= " WHERE is_available = 1";
}
$itemSql .= " ORDER BY sort_order ASC, id ASC";
$itemStmt = $pdo->query($itemSql);
$items = $itemStmt->fetchAll() ?: [];
// Group items by category_id
$grouped = [];
foreach ($categories as $cat) {
$cat['items'] = [];
$grouped[$cat['id']] = $cat;
}
foreach ($items as $item) {
if (isset($grouped[$item['category_id']])) {
$grouped[$item['category_id']]['items'][] = $item;
}
}
return array_values($grouped);
}
function get_all_categories(): array {
$pdo = get_db();
$stmt = $pdo->query("SELECT * FROM menu_categories ORDER BY sort_order ASC, id ASC");
return $stmt->fetchAll() ?: [];
}
function get_category_by_id(int $id): ?array {
$pdo = get_db();
$stmt = $pdo->prepare("SELECT * FROM menu_categories WHERE id = ?");
$stmt->execute([$id]);
$res = $stmt->fetch();
return $res ?: null;
}
function category_exists(int $id): bool {
if ($id <= 0) {
return false;
}
$pdo = get_db();
$stmt = $pdo->prepare("SELECT 1 FROM menu_categories WHERE id = ?");
$stmt->execute([$id]);
return (bool)$stmt->fetchColumn();
}
function save_category(array $data, ?int $id = null): int {
$pdo = get_db();
$name = trim($data['name'] ?? '');
$description = trim($data['description'] ?? '');
$sortOrder = (int)($data['sort_order'] ?? 0);
if ($id && $id > 0) {
$stmt = $pdo->prepare("UPDATE menu_categories SET name = ?, description = ?, sort_order = ? WHERE id = ?");
$stmt->execute([$name, $description, $sortOrder, $id]);
return $id;
} else {
$stmt = $pdo->prepare("INSERT INTO menu_categories (name, description, sort_order) VALUES (?, ?, ?)");
$stmt->execute([$name, $description, $sortOrder]);
return (int)$pdo->lastInsertId();
}
}
function delete_category(int $id): bool {
$pdo = get_db();
$stmt = $pdo->prepare("DELETE FROM menu_categories WHERE id = ?");
return $stmt->execute([$id]);
}
function get_menu_item_by_id(int $id): ?array {
$pdo = get_db();
$stmt = $pdo->prepare("SELECT * FROM menu_items WHERE id = ?");
$stmt->execute([$id]);
$res = $stmt->fetch();
return $res ?: null;
}
function save_menu_item(array $data, ?int $id = null): int {
$pdo = get_db();
$categoryId = (int)($data['category_id'] ?? 0);
$name = trim($data['name'] ?? '');
$description = trim($data['description'] ?? '');
$price = trim($data['price'] ?? '');
$badge = trim($data['badge'] ?? '');
$isAvailable = !empty($data['is_available']) ? 1 : 0;
$sortOrder = (int)($data['sort_order'] ?? 0);
if ($id && $id > 0) {
$stmt = $pdo->prepare("
UPDATE menu_items SET
category_id = ?, name = ?, description = ?, price = ?,
badge = ?, is_available = ?, sort_order = ?, updated_at = CURRENT_TIMESTAMP
WHERE id = ?
");
$stmt->execute([$categoryId, $name, $description, $price, $badge, $isAvailable, $sortOrder, $id]);
return $id;
} else {
$stmt = $pdo->prepare("
INSERT INTO menu_items (category_id, name, description, price, badge, is_available, sort_order)
VALUES (?, ?, ?, ?, ?, ?, ?)
");
$stmt->execute([$categoryId, $name, $description, $price, $badge, $isAvailable, $sortOrder]);
return (int)$pdo->lastInsertId();
}
}
function toggle_menu_item_availability(int $id): bool {
$pdo = get_db();
$stmt = $pdo->prepare("UPDATE menu_items SET is_available = CASE WHEN is_available = 1 THEN 0 ELSE 1 END, updated_at = CURRENT_TIMESTAMP WHERE id = ?");
return $stmt->execute([$id]);
}
function delete_menu_item(int $id): bool {
$pdo = get_db();
$stmt = $pdo->prepare("DELETE FROM menu_items WHERE id = ?");
return $stmt->execute([$id]);
}