diff --git a/.gitignore b/.gitignore index 4757342..df980ab 100644 --- a/.gitignore +++ b/.gitignore @@ -15,3 +15,4 @@ db/fitz.sqlite-shm .hintrc dev.sh +.claude/ diff --git a/admin/hours.js b/admin/hours.js new file mode 100644 index 0000000..7030aba --- /dev/null +++ b/admin/hours.js @@ -0,0 +1,18 @@ +// Hours admin page: purely a progressive enhancement over the plain form +// POST that actually saves β€” greys out (and stops submitting) a day's time +// fields while its "Open" box is unchecked, so a closed day can't look like +// it still has stale hours set. + +document.querySelectorAll('.hours-day-row').forEach((row) => { + const toggle = row.querySelector('input[type="checkbox"]'); + const timeFields = row.querySelectorAll('.day-time-field'); + + function applyState() { + timeFields.forEach((field) => { + field.disabled = !toggle.checked; + }); + } + + toggle.addEventListener('change', applyState); + applyState(); +}); diff --git a/admin/hours.php b/admin/hours.php new file mode 100644 index 0000000..582800e --- /dev/null +++ b/admin/hours.php @@ -0,0 +1,205 @@ + !empty($dayData['is_open']) ? 1 : 0, + 'open_time' => normalize_time_value($dayData['open_time'] ?? null), + 'close_time' => normalize_time_value($dayData['close_time'] ?? null), + 'kitchen_close_time' => normalize_time_value($dayData['kitchen_close_time'] ?? null), + ]; +} + +/** + * A day marked "Open" needs both ends of its range set β€” everything else + * (closed days, the optional kitchen closing time) is fine left blank. + */ +function validate_hours_input(array $daysInput): ?string { + for ($d = 0; $d <= 6; $d++) { + $day = hours_day_from_post(is_array($daysInput[$d] ?? null) ? $daysInput[$d] : []); + if ($day['is_open'] && (!$day['open_time'] || !$day['close_time'])) { + return HOURS_DAY_NAMES[$d] . ' is marked open but is missing an opening or closing time.'; + } + } + return null; +} + +$submittedHours = null; + +if ($_SERVER['REQUEST_METHOD'] === 'POST') { + if (!verify_csrf_token($_POST['csrf_token'] ?? '')) { + $errorMsg = 'Session expired or invalid security token. Please try again.'; + } elseif (($_POST['action'] ?? '') === 'save_hours') { + $daysInput = is_array($_POST['day'] ?? null) ? $_POST['day'] : []; + + if ($error = validate_hours_input($daysInput)) { + $errorMsg = $error; + $submittedHours = []; + for ($d = 0; $d <= 6; $d++) { + $submittedHours[$d] = hours_day_from_post(is_array($daysInput[$d] ?? null) ? $daysInput[$d] : []); + } + } else { + save_all_hours($daysInput); + header('Location: hours.php?saved=1'); + exit; + } + } +} + +$hoursByDay = $submittedHours ?? get_all_hours(); +$justSaved = !$_POST && isset($_GET['saved']); +?> + + + + + + + Hours | The Old Fitz Admin + + + + + + + + + + +
+
+
+

Operations

+

Pub Hours

+
+ + + + +
Hours saved β€” the public site is now showing the updated schedule.
+ + +
+
+

+ πŸ•’ Weekly Schedule +

+
+ +
+ + + +
+ +
+ + +
+
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +

Leave "Kitchen Closes" blank on a day if the kitchen doesn't have a separate cutoff from the bar. Special closures (holidays, weather) are handled on the Announcements page instead of here.

+ +
+ +
+
+
+ +
+
+

+ πŸ‘€ Public Site Preview +

+
+

This is how the schedule above will read on the site once saved.

+ +
+
+
+ +
+
+
+
+ +
+ +

+ β€” bar snacks stay out later. Holiday hours may vary; check our Facebook page for the latest. +

+
+ +
+
+
+
+ + + + + + diff --git a/admin/index.js b/admin/index.js new file mode 100644 index 0000000..62b5ee7 --- /dev/null +++ b/admin/index.js @@ -0,0 +1,34 @@ +// Announcements admin page: intercepts the status badge's toggle form so +// clicking it updates in place via fetch instead of reloading the whole +// page β€” same interaction as the menu item's status toggle. Everything +// else here (create/edit/delete) is still a plain form POST + reload. + +document.addEventListener('submit', async (e) => { + const form = e.target; + if (!form.classList.contains('status-toggle-form')) return; + + e.preventDefault(); + const btn = form.querySelector('button'); + btn.disabled = true; + + try { + const response = await fetch('index.php', { + method: 'POST', + body: new FormData(form), + headers: { 'X-Requested-With': 'XMLHttpRequest' } + }); + const data = await response.json(); + + if (data.success) { + btn.className = `status-badge ${data.status_class}`; + btn.textContent = data.status_label; + btn.title = `Click to turn ${data.is_active ? 'off' : 'on'}`; + } else { + alert(data.error || 'Failed to update status.'); + } + } catch { + alert('Network error. Please try again.'); + } finally { + btn.disabled = false; + } +}); diff --git a/admin/index.php b/admin/index.php index c884cf2..016627a 100644 --- a/admin/index.php +++ b/admin/index.php @@ -5,12 +5,40 @@ require_once __DIR__ . '/../inc/announcements.php'; require_auth(); $user = get_logged_in_user(); -$successMsg = ''; $errorMsg = ''; +$today = date('Y-m-d'); -// Handle Actions (Save, Delete, Toggle) +/** + * Whether an announcement is actually showing right now β€” active flag on + * AND within its optional date window. Matches the currently-live check + * used when rendering the list below. + */ +function announcement_is_live(array $ann, string $today): bool +{ + return (bool) $ann['is_active'] + && (empty($ann['start_date']) || $ann['start_date'] <= $today) + && (empty($ann['end_date']) || $ann['end_date'] >= $today); +} + +// Handle Actions (Save, Delete, Toggle). Success isn't announced with a +// banner β€” the list itself is the feedback (the row appears/updates/ +// disappears, the status badge flips) β€” same as the menu admin, and it +// avoids a banner shoving the whole page down every time someone clicks +// the status badge. +// +// The status toggle specifically is also fetched via AJAX (see index.js) +// so clicking it doesn't reload the page, matching the menu item's status +// toggle β€” everything else here (save/delete) is still a plain form POST. if ($_SERVER['REQUEST_METHOD'] === 'POST') { + $isAjax = ($_SERVER['HTTP_X_REQUESTED_WITH'] ?? '') === 'XMLHttpRequest'; + if (!verify_csrf_token($_POST['csrf_token'] ?? '')) { + if ($isAjax) { + header('Content-Type: application/json; charset=utf-8'); + http_response_code(403); + echo json_encode(['success' => false, 'error' => 'Session expired or invalid security token. Please refresh.']); + exit; + } $errorMsg = 'Session expired or invalid security token. Please try again.'; } else { $action = $_POST['action'] ?? ''; @@ -27,19 +55,34 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') { 'show_banner' => isset($_POST['show_banner']) ? 1 : 0, 'show_in_hours' => isset($_POST['show_in_hours']) ? 1 : 0, ], $annId); - - $successMsg = $annId ? 'Announcement updated successfully!' : 'New announcement created successfully!'; } elseif ($action === 'delete_announcement') { $annId = (int)($_POST['announcement_id'] ?? 0); if ($annId > 0) { delete_announcement($annId); - $successMsg = 'Announcement deleted successfully.'; } } elseif ($action === 'toggle_active') { $annId = (int)($_POST['announcement_id'] ?? 0); if ($annId > 0) { toggle_announcement_active($annId); - $successMsg = 'Status updated.'; + } + + if ($isAjax) { + header('Content-Type: application/json; charset=utf-8'); + $ann = $annId > 0 ? get_announcement_by_id($annId) : null; + if (!$ann) { + http_response_code(404); + echo json_encode(['success' => false, 'error' => 'Announcement not found.']); + exit; + } + + $isLive = announcement_is_live($ann, $today); + echo json_encode([ + 'success' => true, + 'is_active' => (bool) $ann['is_active'], + 'status_class' => $isLive ? 'active' : 'inactive', + 'status_label' => $isLive ? '● Live' : ($ann['is_active'] ? '⏳ Scheduled' : 'β—‹ Off'), + ]); + exit; } } } @@ -55,7 +98,6 @@ if ($editingId) { } $announcements = get_all_announcements(); -$today = date('Y-m-d'); ?> @@ -80,6 +122,7 @@ $today = date('Y-m-d'); Announcements Menu Users + Hours
Logged in as @@ -96,193 +139,183 @@ $today = date('Y-m-d');

Site Announcements & Alerts

- -
- - - -
- - - +
+ + Create Announcement +
-
- - -
-
- -
- -
- - -
- -
- - -
- -
- - -
- -
- - - Leave blank to show immediately while enabled. -
- -
- - - Auto-hides after 11:59 PM on this date. -
- -
- -
- - -
-
-
- -
- - Cancel -
-
-
- - -
-
-

- πŸ“’ All Announcements () -

- + Create Announcement -
- - -
-

No announcements created yet.

- + Create Your First Announcement -
- -
- - - - - - - - - - - - - - = $today); - ?> - - - - - - - - - - -
StatusHeadlineTypeSchedulePlacementsActions
- - ● Live - - ⏳ Scheduled - - β—‹ Off - - - -
- -
-
- - - - β†’ - - Always (while on) - - - - -
- Edit - -
- - - - -
- -
- - - - -
-
-
-
- + +
+
+

+ πŸ“’ All Announcements () +

- + + +
+

No announcements created yet.

+ + Create Your First Announcement +
+ +
+ +
+ + +
+
+
+ + + + + + + + + + +
+
+
+ +
+ +
+
+
+ +
+
+ + β†’ + + Always (while on) + +
+
+ +
+
+
+ Edit + +
+ + + + +
+
+
+
+ +
+
+ +
+ + + + + diff --git a/admin/menu.js b/admin/menu.js index 3e9047e..e3fdcd9 100644 --- a/admin/menu.js +++ b/admin/menu.js @@ -352,7 +352,7 @@ function itemRowHtml(item, isAvail) {
${escapeHtml(item.name)}${badgeHtml}
${escapeHtml(item.description || '')}
${escapeHtml(item.price)}
-
+
@@ -432,7 +432,7 @@ function categoryPanelHtml(category) { ${escapeHtml(category.name)}

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

-
+
diff --git a/admin/menu.php b/admin/menu.php index 6900229..ee73eb7 100644 --- a/admin/menu.php +++ b/admin/menu.php @@ -193,6 +193,7 @@ $menuGrouped = get_full_menu(false); Announcements Menu Users + Hours
Logged in as @@ -231,7 +232,7 @@ $menuGrouped = get_full_menu(false);

-
+
@@ -284,7 +285,7 @@ $menuGrouped = get_full_menu(false);
-
+
diff --git a/admin/users.js b/admin/users.js index 7600de4..40f3044 100644 --- a/admin/users.js +++ b/admin/users.js @@ -152,8 +152,8 @@ document.getElementById('form-password').addEventListener('submit', async (e) => } }); -// Delegate table actions: Change Password, Delete -document.getElementById('users-tbody').addEventListener('click', async (e) => { +// Delegate row actions: Change Password, Delete +document.getElementById('users-rows').addEventListener('click', async (e) => { const { target } = e; if (target.classList.contains('btn-change-password')) { @@ -163,7 +163,7 @@ document.getElementById('users-tbody').addEventListener('click', async (e) => { if (target.classList.contains('btn-delete-user')) { if (!confirm('Are you sure you want to delete this user? This cannot be undone.')) return; - const row = target.closest('tr'); + const row = target.closest('.user-row'); const fd = new FormData(); fd.append('action', 'delete_user'); @@ -187,25 +187,26 @@ document.getElementById('users-tbody').addEventListener('click', async (e) => { }); function insertUserRow(user) { - const tbody = document.getElementById('users-tbody'); + const rowsContainer = document.getElementById('users-rows'); - const tr = document.createElement('tr'); - tr.id = `user-row-${user.id}`; - tr.dataset.userId = user.id; + const row = document.createElement('div'); + row.className = 'user-row'; + row.id = `user-row-${user.id}`; + row.dataset.userId = user.id; // 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. - tr.innerHTML = ` - ${escapeHtml(user.username)} - ${escapeHtml(user.role.charAt(0).toUpperCase() + user.role.slice(1))} - ${escapeHtml(user.created_at)} -
+ row.innerHTML = ` +
${escapeHtml(user.username)}
+
${escapeHtml(user.role.charAt(0).toUpperCase() + user.role.slice(1))}
+
${escapeHtml(user.created_at)}
+
-
+
`; - tr.querySelector('.btn-change-password').dataset.username = user.username; + row.querySelector('.btn-change-password').dataset.username = user.username; - tbody.appendChild(tr); + rowsContainer.appendChild(row); } diff --git a/admin/users.php b/admin/users.php index af848c6..3ca0452 100644 --- a/admin/users.php +++ b/admin/users.php @@ -161,6 +161,7 @@ $users = get_all_users(); Announcements Menu Users + Hours
Logged in as @@ -182,45 +183,47 @@ $users = get_all_users();
-
- - - - - - - - - - - - - - - - - - - -
UsernameRoleCreatedActions
- - - (you) - - -
- + + - - - -
-
+ +
+
+
+ +
diff --git a/inc/db.php b/inc/db.php index 28fc9e0..2b327a4 100644 --- a/inc/db.php +++ b/inc/db.php @@ -77,6 +77,15 @@ function init_db_schema(PDO $pdo): void { sort_order INTEGER NOT NULL DEFAULT 0, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ); + + CREATE TABLE IF NOT EXISTS pub_hours ( + day_of_week INTEGER PRIMARY KEY, + is_open INTEGER NOT NULL DEFAULT 0, + open_time TEXT NULL, + close_time TEXT NULL, + kitchen_close_time TEXT NULL, + updated_at DATETIME DEFAULT CURRENT_TIMESTAMP + ); "); // Seed default admin user if table is empty (username: admin / password: password) @@ -87,4 +96,33 @@ 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 weekly hours if the table is empty: closed Sun–Wed, + // 4–10pm Thu–Sat with the kitchen closing at 9 β€” matches the hours the + // site originally shipped with as hardcoded copy. + $stmt = $pdo->query("SELECT COUNT(*) as count FROM pub_hours"); + $hoursCount = $stmt->fetchColumn(); + if ($hoursCount == 0) { + $insert = $pdo->prepare(" + INSERT INTO pub_hours (day_of_week, is_open, open_time, close_time, kitchen_close_time) + VALUES (?, ?, ?, ?, ?) + "); + // 0=Sunday ... 6=Saturday + $defaults = [ + 0 => null, // Sunday β€” closed + 1 => null, // Monday β€” closed + 2 => null, // Tuesday β€” closed + 3 => null, // Wednesday β€” closed + 4 => ['16:00', '22:00', '21:00'], // Thursday + 5 => ['16:00', '22:00', '21:00'], // Friday + 6 => ['16:00', '22:00', '21:00'], // Saturday + ]; + foreach ($defaults as $day => $times) { + if ($times === null) { + $insert->execute([$day, 0, null, null, null]); + } else { + $insert->execute([$day, 1, $times[0], $times[1], $times[2]]); + } + } + } } diff --git a/inc/hours.php b/inc/hours.php new file mode 100644 index 0000000..1560915 --- /dev/null +++ b/inc/hours.php @@ -0,0 +1,294 @@ + 'Sunday', 1 => 'Monday', 2 => 'Tuesday', 3 => 'Wednesday', + 4 => 'Thursday', 5 => 'Friday', 6 => 'Saturday', +]; + +const HOURS_DAY_ABBR = [ + 0 => 'Sun', 1 => 'Mon', 2 => 'Tue', 3 => 'Wed', + 4 => 'Thu', 5 => 'Fri', 6 => 'Sat', +]; + +/** + * Returns all 7 days, 0..6, always fully populated (a day missing from the + * table β€” shouldn't happen once seeded, but this keeps every caller below + * from having to guard against a gap) defaults to closed. + */ +function get_all_hours(): array { + $pdo = get_db(); + $rows = $pdo->query("SELECT * FROM pub_hours ORDER BY day_of_week ASC")->fetchAll() ?: []; + + $byDay = []; + foreach ($rows as $row) { + $byDay[(int) $row['day_of_week']] = $row; + } + + $result = []; + for ($d = 0; $d <= 6; $d++) { + $result[$d] = $byDay[$d] ?? [ + 'day_of_week' => $d, + 'is_open' => 0, + 'open_time' => null, + 'close_time' => null, + 'kitchen_close_time' => null, + ]; + } + return $result; +} + +/** + * Normalizes a submitted time value down to "HH:MM", or null if it's blank + * or not something an would ever actually send. + */ +function normalize_time_value(?string $value): ?string { + if ($value === null) { + return null; + } + $value = trim($value); + if ($value === '') { + return null; + } + if (!preg_match('/^([01]?\d|2[0-3]):([0-5]\d)/', $value, $m)) { + return null; + } + return sprintf('%02d:%s', (int) $m[1], $m[2]); +} + +/** + * Persists all 7 days in one go from the admin form's day[N][...] fields. + * A day marked closed has its times forced to null regardless of whatever + * stale values its (disabled) time inputs may have carried in the POST. + */ +function save_all_hours(array $daysInput): void { + $pdo = get_db(); + $stmt = $pdo->prepare(" + INSERT INTO pub_hours (day_of_week, is_open, open_time, close_time, kitchen_close_time, updated_at) + VALUES (:day, :is_open, :open_time, :close_time, :kitchen_close_time, CURRENT_TIMESTAMP) + ON CONFLICT(day_of_week) DO UPDATE SET + is_open = excluded.is_open, + open_time = excluded.open_time, + close_time = excluded.close_time, + kitchen_close_time = excluded.kitchen_close_time, + updated_at = CURRENT_TIMESTAMP + "); + + for ($d = 0; $d <= 6; $d++) { + $dayData = is_array($daysInput[$d] ?? null) ? $daysInput[$d] : []; + $isOpen = !empty($dayData['is_open']) ? 1 : 0; + $openTime = normalize_time_value($dayData['open_time'] ?? null); + $closeTime = normalize_time_value($dayData['close_time'] ?? null); + $kitchenClose = normalize_time_value($dayData['kitchen_close_time'] ?? null); + + if (!$isOpen) { + $openTime = null; + $closeTime = null; + $kitchenClose = null; + } + + $stmt->execute([ + ':day' => $d, + ':is_open' => $isOpen, + ':open_time' => $openTime, + ':close_time' => $closeTime, + ':kitchen_close_time' => $kitchenClose, + ]); + } +} + +/** + * "4:00 PM" / "9 AM" β€” 12-hour, minutes dropped when they're :00 and + * $dropZeroMinutes is set (used for one-off mentions like the kitchen note; + * ranges below always keep :00 so both ends line up visually). + */ +function format_time_12h(?string $hhmm, bool $dropZeroMinutes = false): string { + if (!$hhmm) { + return ''; + } + [$h, $m] = array_map('intval', explode(':', $hhmm)); + $period = $h >= 12 ? 'PM' : 'AM'; + $hour12 = $h % 12; + if ($hour12 === 0) { + $hour12 = 12; + } + if ($dropZeroMinutes && $m === 0) { + return "{$hour12} {$period}"; + } + return sprintf('%d:%02d %s', $hour12, $m, $period); +} + +/** + * "4:00 – 10:00 PM" (or "11:00 AM – 2:00 PM" when the two ends straddle + * noon) β€” a single trailing meridiem when both ends share one, otherwise + * both spelled out. $dash lets callers pick the tight "–" the footer's + * condensed line uses vs the spaced " – " the hours card uses. + */ +function format_time_range(string $openTime, string $closeTime, string $dash = ' – '): string { + $openHour = (int) explode(':', $openTime)[0]; + $closeHour = (int) explode(':', $closeTime)[0]; + $openPeriod = $openHour >= 12 ? 'PM' : 'AM'; + $closePeriod = $closeHour >= 12 ? 'PM' : 'AM'; + + $openLabel = format_time_12h($openTime); + $closeLabel = format_time_12h($closeTime); + + if ($openPeriod === $closePeriod) { + $openLabel = trim(str_replace($openPeriod, '', $openLabel)); + } + + return "{$openLabel}{$dash}{$closeLabel}"; +} + +/** + * Groups the week (in Sun..Sat order) into consecutive runs sharing the + * same open/closed status and, if open, the same start/end time. Doesn't + * wrap around from Saturday back to Sunday β€” a closed Sat + Sun would come + * back as two separate single-day groups rather than one "Sat – Sun" β€” but + * that's an edge case this pub's schedule doesn't hit, and not worth the + * extra complexity of circular grouping for. + */ +function group_hours_into_ranges(array $hoursByDay): array { + $groups = []; + $current = null; + + for ($d = 0; $d <= 6; $d++) { + $day = $hoursByDay[$d]; + $isOpen = !empty($day['is_open']) && $day['open_time'] && $day['close_time']; + $key = $isOpen ? 'open|' . $day['open_time'] . '|' . $day['close_time'] : 'closed'; + + if ($current !== null && $current['key'] === $key) { + $current['days'][] = $d; + } else { + if ($current !== null) { + $groups[] = $current; + } + $current = ['key' => $key, 'is_open' => $isOpen, 'days' => [$d], 'data' => $day]; + } + } + if ($current !== null) { + $groups[] = $current; + } + + return $groups; +} + +/** + * Rows for the "Hours & Visit" card: every open day gets its own line (even + * if it's identical to its neighbor's), any run of closed days collapses + * into one summary row β€” matches the level of detail the site originally + * hardcoded (Thursday / Friday / Saturday individually, "Sun – Wed" combined). + */ +function hours_card_rows(array $hoursByDay): array { + // Open days are collected separately from closed runs, and always + // rendered first β€” matching the original hardcoded card, which led + // with the days you *can* visit and tucked the closed stretch at the + // bottom as a footnote, rather than the plain chronological (Sun..Sat) + // order that would otherwise put a Sun–Wed closed run up top. + $openRows = []; + $closedGroups = []; + $closedRun = []; + + for ($d = 0; $d <= 6; $d++) { + $day = $hoursByDay[$d]; + $isOpen = !empty($day['is_open']) && $day['open_time'] && $day['close_time']; + + if ($isOpen) { + if (!empty($closedRun)) { + $closedGroups[] = $closedRun; + $closedRun = []; + } + $openRows[] = ['label' => HOURS_DAY_NAMES[$d], 'text' => format_time_range($day['open_time'], $day['close_time'])]; + } else { + $closedRun[] = $d; + } + } + if (!empty($closedRun)) { + $closedGroups[] = $closedRun; + } + + $rows = $openRows; + foreach ($closedGroups as $group) { + $rows[] = ['label' => day_abbr_range($group), 'text' => 'Closed']; + } + + return $rows; +} + +/** + * Condensed lines for the footer's compact hours summary: every group + * (open or closed) collapses to one line, open groups listed first β€” the + * order the site's footer originally used β€” with the kitchen note (if any) + * inserted between the open and closed lines. + */ +function hours_footer_lines(array $hoursByDay): array { + $groups = group_hours_into_ranges($hoursByDay); + $openLines = []; + $closedLines = []; + + foreach ($groups as $group) { + $label = day_abbr_range($group['days'], ' β€” '); + if ($group['is_open']) { + $openLines[] = $label . ' Β· ' . format_time_range($group['data']['open_time'], $group['data']['close_time'], '–'); + } else { + $closedLines[] = $label . ' Β· Closed'; + } + } + + $lines = $openLines; + $kitchenNote = hours_kitchen_note($hoursByDay); + if ($kitchenNote) { + $lines[] = $kitchenNote; + } + return array_merge($lines, $closedLines); +} + +/** + * "Sun" for a single day, "Sun – Wed" (or with a custom $dash) for a run. + */ +function day_abbr_range(array $days, string $dash = ' – '): string { + if (count($days) === 1) { + return HOURS_DAY_ABBR[$days[0]]; + } + return HOURS_DAY_ABBR[$days[0]] . $dash . HOURS_DAY_ABBR[end($days)]; +} + +/** + * "Kitchen closes at 9 PM" β€” or, if open days don't all share one kitchen + * closing time, one clause per distinct time naming which days it applies + * to. Returns null when no open day has a kitchen closing time set, so + * callers can omit the note entirely rather than print an empty sentence. + */ +function hours_kitchen_note(array $hoursByDay, bool $dropZeroMinutes = true): ?string { + $byTime = []; + for ($d = 0; $d <= 6; $d++) { + $day = $hoursByDay[$d]; + if (!empty($day['is_open']) && !empty($day['kitchen_close_time'])) { + $byTime[$day['kitchen_close_time']][] = $d; + } + } + if (empty($byTime)) { + return null; + } + + if (count($byTime) === 1) { + $time = array_key_first($byTime); + return 'Kitchen closes at ' . format_time_12h($time, $dropZeroMinutes); + } + + $parts = []; + foreach ($byTime as $time => $days) { + $parts[] = format_time_12h($time, $dropZeroMinutes) . ' (' . day_abbr_range($days, '–') . ')'; + } + return 'Kitchen closes at ' . implode(', ', $parts); +} diff --git a/index.php b/index.php index 4126224..b0a6d10 100644 --- a/index.php +++ b/index.php @@ -2,8 +2,10 @@ require __DIR__ . '/config.php'; require __DIR__ . '/inc/facebook-stats.php'; require __DIR__ . '/inc/announcements.php'; +require __DIR__ . '/inc/hours.php'; $fbStats = fritz_facebook_stats(); $activeAnnouncements = get_active_announcements(); +$hoursByDay = get_all_hours(); $siteUrl = rtrim(SITE_URL, '/'); ?> @@ -157,24 +159,15 @@ $siteUrl = rtrim(SITE_URL, '/');
-
-
Thursday
-
4:00 – 10:00 PM
-
-
-
Friday
-
4:00 – 10:00 PM
-
-
-
Saturday
-
4:00 – 10:00 PM
-
-
-
Sun – Wed
-
Closed
-
+ +
+
+
+
+
-

Kitchen closes at 9 β€” bar snacks stay out later. Holiday hours may vary; check our Facebook page for the latest.

+ +

β€” bar snacks stay out later. Holiday hours may vary; check our Facebook page for the latest.

Hours

- Thu β€” Sat Β· 4:00–10:00 PM - Kitchen closes at 9 PM - Sun β€” Wed Β· Closed + + +
diff --git a/menu.php b/menu.php index b133228..122606d 100644 --- a/menu.php +++ b/menu.php @@ -4,9 +4,11 @@ declare(strict_types=1); require __DIR__ . '/config.php'; require __DIR__ . '/inc/announcements.php'; require __DIR__ . '/inc/menu.php'; +require __DIR__ . '/inc/hours.php'; $activeAnnouncements = get_active_announcements(); $menuCategories = get_full_menu(true); // available items only +$hoursByDay = get_all_hours(); $siteUrl = rtrim(SITE_URL, '/'); ?> @@ -177,9 +179,9 @@ $siteUrl = rtrim(SITE_URL, '/');

Hours

- Thu β€” Sat Β· 4:00–10:00 PM - Kitchen closes at 9 PM - Sun β€” Wed Β· Closed + + +
diff --git a/styles.css b/styles.css index 52b9dd5..8c449e2 100644 --- a/styles.css +++ b/styles.css @@ -87,18 +87,6 @@ color: var(--ink-muted); } - .visually-hidden { - position: absolute; - width: 1px; - height: 1px; - overflow: hidden; - clip: rect(0 0 0 0); - white-space: nowrap; - border: 0; - padding: 0; - margin: -1px; - } - .skip-link { position: absolute; left: 1rem; @@ -170,10 +158,6 @@ background: linear-gradient(90deg, transparent, var(--gold-dim), transparent); } - .rule-row.left-align { - margin-inline: 0; - } - /* ---------- announcement banner ---------- */ .announcement-banner { background: var(--ground-2); @@ -1123,15 +1107,6 @@ margin-bottom: 1.25rem; } - .alert-success { - background: rgba(51, 80, 63, 0.25); - border: 1px solid var(--green); - color: #bbf7d0; - padding: 0.9rem 1.25rem; - border-radius: 0.125rem; - margin-bottom: 1.5rem; - } - .login-btn { width: 100%; justify-content: center; @@ -1320,20 +1295,19 @@ color: var(--ink-muted); } - /* The menu admin's status badge doubles as its own availability toggle - (see .btn-toggle-item) β€” a plain