Add SQLite-backed announcement system and admin portal
- Add SQLite database initialization and management in inc/db.php - Implement session authentication and CSRF security helpers in inc/auth.php - Implement announcement querying and saving in inc/announcements.php - Add management dashboard, login, and logout in admin/ - Protect db/ directory with Apache access controls
This commit is contained in:
1 parent
b9d3262233
commit
9b7d9ceccb
7 files changed
+761
No files matched your search
+361
@@ -0,0 +1,361 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/../inc/auth.php';
|
||||
require_once __DIR__ . '/../inc/announcements.php';
|
||||
|
||||
require_auth();
|
||||
$user = get_logged_in_user();
|
||||
$pdo = get_db();
|
||||
|
||||
$successMsg = '';
|
||||
$errorMsg = '';
|
||||
|
||||
// Handle announcement form submission
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action']) && $_POST['action'] === 'save_announcement') {
|
||||
if (!verify_csrf_token($_POST['csrf_token'] ?? '')) {
|
||||
$errorMsg = 'Session expired or invalid security token. Please try again.';
|
||||
} else {
|
||||
$annId = !empty($_POST['announcement_id']) ? (int)$_POST['announcement_id'] : null;
|
||||
save_announcement([
|
||||
'is_active' => isset($_POST['is_active']) ? 1 : 0,
|
||||
'type' => $_POST['type'] ?? 'holiday',
|
||||
'headline' => $_POST['headline'] ?? '',
|
||||
'message' => $_POST['message'] ?? '',
|
||||
'start_date' => $_POST['start_date'] ?? null,
|
||||
'end_date' => $_POST['end_date'] ?? null,
|
||||
'show_banner' => isset($_POST['show_banner']) ? 1 : 0,
|
||||
'show_in_hours' => isset($_POST['show_in_hours']) ? 1 : 0,
|
||||
], $annId);
|
||||
|
||||
$successMsg = 'Announcement updated successfully!';
|
||||
}
|
||||
}
|
||||
|
||||
// Fetch current announcement (or default empty template)
|
||||
$announcement = get_active_announcement();
|
||||
if (!$announcement) {
|
||||
$stmt = $pdo->query("SELECT * FROM announcements ORDER BY id DESC LIMIT 1");
|
||||
$announcement = $stmt->fetch() ?: [
|
||||
'id' => 0,
|
||||
'is_active' => 0,
|
||||
'type' => 'holiday',
|
||||
'headline' => 'Holiday Closure',
|
||||
'message' => 'The Old Fitz will be closed for the holiday weekend.',
|
||||
'start_date' => '',
|
||||
'end_date' => '',
|
||||
'show_banner' => 1,
|
||||
'show_in_hours' => 1
|
||||
];
|
||||
}
|
||||
?>
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Pub Management Dashboard | The Old Fitz</title>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Fraunces:ital,opsz,wght@0,9..144,400;0,9..144,600;1,9..144,400&family=Karla:wght@400;500;600;700&family=IBM+Plex+Mono:wght@400;500&display=swap" rel="stylesheet">
|
||||
<link rel="stylesheet" href="../styles.css">
|
||||
<style>
|
||||
.admin-nav {
|
||||
background: var(--ground-2);
|
||||
border-bottom: 1px solid var(--rule);
|
||||
padding: 0.85rem 0;
|
||||
}
|
||||
.admin-nav-inner {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
.admin-brand {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
text-decoration: none;
|
||||
font-family: 'Fraunces', serif;
|
||||
font-size: 1.15rem;
|
||||
color: var(--ink);
|
||||
}
|
||||
.admin-user {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1.25rem;
|
||||
font-family: 'IBM Plex Mono', monospace;
|
||||
font-size: 0.82rem;
|
||||
color: var(--ink-muted);
|
||||
}
|
||||
.admin-user a {
|
||||
color: var(--gold-bright);
|
||||
text-decoration: none;
|
||||
}
|
||||
.admin-user a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.dashboard-layout {
|
||||
padding-block: 2.5rem 4rem;
|
||||
}
|
||||
.dash-header {
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
.dash-header h1 {
|
||||
font-size: 2rem;
|
||||
color: var(--ink);
|
||||
}
|
||||
|
||||
.panel {
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--rule);
|
||||
border-radius: 0.25rem;
|
||||
padding: 2rem;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
.panel h2 {
|
||||
font-size: 1.35rem;
|
||||
color: var(--gold-bright);
|
||||
margin-bottom: 1.25rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.6rem;
|
||||
}
|
||||
|
||||
.form-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 1.5rem;
|
||||
}
|
||||
@media (max-width: 47.5em) {
|
||||
.form-grid { grid-template-columns: 1fr; }
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 1.25rem;
|
||||
}
|
||||
.form-group.full-width {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
.form-group label {
|
||||
display: block;
|
||||
font-family: 'IBM Plex Mono', monospace;
|
||||
font-size: 0.75rem;
|
||||
letter-spacing: 0.08em;
|
||||
text-transform: uppercase;
|
||||
color: var(--gold);
|
||||
margin-bottom: 0.45rem;
|
||||
}
|
||||
.form-control {
|
||||
width: 100%;
|
||||
background: var(--ground-2);
|
||||
border: 1px solid var(--rule);
|
||||
color: var(--ink);
|
||||
font-family: 'Karla', sans-serif;
|
||||
font-size: 0.95rem;
|
||||
padding: 0.7rem 0.85rem;
|
||||
border-radius: 0.125rem;
|
||||
box-sizing: border-box;
|
||||
outline: none;
|
||||
transition: border-color .15s ease;
|
||||
}
|
||||
.form-control:focus {
|
||||
border-color: var(--gold-bright);
|
||||
}
|
||||
textarea.form-control {
|
||||
resize: vertical;
|
||||
min-height: 5.5rem;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.checkbox-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.75rem;
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
.checkbox-label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.6rem;
|
||||
font-size: 0.92rem;
|
||||
color: var(--ink);
|
||||
cursor: pointer;
|
||||
}
|
||||
.checkbox-label input[type="checkbox"] {
|
||||
width: 1.15rem;
|
||||
height: 1.15rem;
|
||||
accent-color: var(--gold);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.status-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.4rem;
|
||||
font-family: 'IBM Plex Mono', monospace;
|
||||
font-size: 0.72rem;
|
||||
letter-spacing: 0.08em;
|
||||
text-transform: uppercase;
|
||||
padding: 0.25rem 0.6rem;
|
||||
border-radius: 0.125rem;
|
||||
}
|
||||
.status-badge.active {
|
||||
background: rgba(51, 80, 63, 0.4);
|
||||
border: 1px solid var(--green);
|
||||
color: #9fe2bf;
|
||||
}
|
||||
.status-badge.inactive {
|
||||
background: rgba(138, 115, 74, 0.2);
|
||||
border: 1px solid var(--rule);
|
||||
color: var(--ink-muted);
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
.alert-error {
|
||||
background: rgba(180, 50, 40, 0.15);
|
||||
border: 1px solid rgba(220, 80, 60, 0.4);
|
||||
color: #ff9d9d;
|
||||
padding: 0.9rem 1.25rem;
|
||||
border-radius: 0.125rem;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.preview-box {
|
||||
margin-top: 1.5rem;
|
||||
background: var(--ground-2);
|
||||
border: 1px dashed var(--rule);
|
||||
padding: 1.5rem;
|
||||
border-radius: 0.25rem;
|
||||
}
|
||||
.preview-box h3 {
|
||||
font-size: 0.9rem;
|
||||
font-family: 'IBM Plex Mono', monospace;
|
||||
color: var(--ink-muted);
|
||||
margin-bottom: 1rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.1em;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<nav class="admin-nav">
|
||||
<div class="container admin-nav-inner">
|
||||
<a href="index.php" class="admin-brand">
|
||||
<img src="../assets/knot.png" alt="" class="knot">
|
||||
<span>The Old Fitz Admin</span>
|
||||
</a>
|
||||
<div class="admin-user">
|
||||
<span>Logged in as <strong><?= htmlspecialchars($user['username']) ?></strong></span>
|
||||
<a href="../" target="_blank" rel="noopener">View Public Site ↗</a>
|
||||
<a href="logout.php">Sign Out</a>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<main class="dashboard-layout">
|
||||
<div class="container">
|
||||
<div class="dash-header">
|
||||
<p class="eyebrow">Content & Operations</p>
|
||||
<h1 style="margin-top: 0.4rem;">Site Announcements & Alerts</h1>
|
||||
</div>
|
||||
|
||||
<?php if (!empty($successMsg)): ?>
|
||||
<div class="alert-success" role="status"><?= htmlspecialchars($successMsg) ?></div>
|
||||
<?php endif; ?>
|
||||
<?php if (!empty($errorMsg)): ?>
|
||||
<div class="alert-error" role="alert"><?= htmlspecialchars($errorMsg) ?></div>
|
||||
<?php endif; ?>
|
||||
|
||||
<form method="POST" action="index.php">
|
||||
<input type="hidden" name="action" value="save_announcement">
|
||||
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars(get_csrf_token()) ?>">
|
||||
<input type="hidden" name="announcement_id" value="<?= (int)($announcement['id'] ?? 0) ?>">
|
||||
|
||||
<div class="panel">
|
||||
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 1.5rem;">
|
||||
<h2>
|
||||
<span>📢 Announcement Settings</span>
|
||||
</h2>
|
||||
<?php if (!empty($announcement['is_active'])): ?>
|
||||
<span class="status-badge active">● Live on site</span>
|
||||
<?php else: ?>
|
||||
<span class="status-badge inactive">○ Inactive / Off</span>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<div class="form-grid">
|
||||
<div class="form-group full-width">
|
||||
<label class="checkbox-label" style="font-size: 1.05rem; font-weight: 500;">
|
||||
<input type="checkbox" name="is_active" value="1" <?= !empty($announcement['is_active']) ? 'checked' : '' ?>>
|
||||
<strong>Enable Announcement on Site</strong>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="headline">Notice Headline</label>
|
||||
<input type="text" id="headline" name="headline" class="form-control"
|
||||
value="<?= htmlspecialchars($announcement['headline'] ?? '') ?>"
|
||||
placeholder="e.g. Labor Day Closure" required>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="type">Style / Category</label>
|
||||
<select id="type" name="type" class="form-control">
|
||||
<option value="holiday" <?= ($announcement['type'] ?? '') === 'holiday' ? 'selected' : '' ?>>Holiday / Special Schedule (Warm Brass & Green)</option>
|
||||
<option value="special" <?= ($announcement['type'] ?? '') === 'special' ? 'selected' : '' ?>>Event / Live Music Announcement</option>
|
||||
<option value="closure" <?= ($announcement['type'] ?? '') === 'closure' ? 'selected' : '' ?>>Urgent Closure / Weather Alert</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group full-width">
|
||||
<label for="message">Notice Message</label>
|
||||
<textarea id="message" name="message" class="form-control" required
|
||||
placeholder="Write details of closure, dates, and when regular hours resume..."><?= htmlspecialchars($announcement['message'] ?? '') ?></textarea>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="start_date">Start Date (Optional Auto-Schedule)</label>
|
||||
<input type="date" id="start_date" name="start_date" class="form-control"
|
||||
value="<?= htmlspecialchars($announcement['start_date'] ?? '') ?>">
|
||||
<span class="muted" style="font-size: 0.78rem;">Leave blank to show immediately while enabled.</span>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="end_date">End Date (Optional Expiration)</label>
|
||||
<input type="date" id="end_date" name="end_date" class="form-control"
|
||||
value="<?= htmlspecialchars($announcement['end_date'] ?? '') ?>">
|
||||
<span class="muted" style="font-size: 0.78rem;">Auto-hides after 11:59 PM on this date.</span>
|
||||
</div>
|
||||
|
||||
<div class="form-group full-width">
|
||||
<label>Display Placements</label>
|
||||
<div class="checkbox-group">
|
||||
<label class="checkbox-label">
|
||||
<input type="checkbox" name="show_banner" value="1" <?= !empty($announcement['show_banner']) ? 'checked' : '' ?>>
|
||||
Show as top alert banner across the site
|
||||
</label>
|
||||
<label class="checkbox-label">
|
||||
<input type="checkbox" name="show_in_hours" value="1" <?= !empty($announcement['show_in_hours']) ? 'checked' : '' ?>>
|
||||
Highlight inside the "Hours & Visit" card
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="margin-top: 1.75rem;">
|
||||
<button type="submit" class="btn btn-primary">Save & Update Site</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
Reference in new issue
Block a user