diff --git a/admin.php b/admin.php
index ab5fac3..a23d3e7 100644
--- a/admin.php
+++ b/admin.php
@@ -1,53 +1,136 @@
format('M j, Y • g:i A');
+ } catch (Exception $e) {
+ return htmlspecialchars($value, ENT_QUOTES, 'UTF-8');
+ }
+}
+
+function bind_named_values(PDOStatement $stmt, array $params): void {
+ foreach ($params as $name => $value) {
+ $stmt->bindValue($name, $value, is_int($value) ? PDO::PARAM_INT : PDO::PARAM_STR);
+ }
+}
+
$message = '';
if (isset($_SESSION['message'])) {
- $message = $_SESSION['message'];
+ $message = (string) $_SESSION['message'];
unset($_SESSION['message']);
}
$pdo = db();
+$records_per_page = 15;
+$page = isset($_GET['page']) && is_numeric($_GET['page']) ? max(1, (int) $_GET['page']) : 1;
+$selected_webinar_id = isset($_GET['webinar_id']) && is_numeric($_GET['webinar_id']) ? max(0, (int) $_GET['webinar_id']) : 0;
-// Pagination settings
-$records_per_page = 10;
-$page = isset($_GET['page']) && is_numeric($_GET['page']) ? (int)$_GET['page'] : 1;
+$webinars = $pdo->query('SELECT id, title, scheduled_at FROM webinars ORDER BY scheduled_at DESC, id DESC')->fetchAll(PDO::FETCH_ASSOC);
+
+$where_parts = ['a.deleted_at IS NULL'];
+$params = [];
+if ($selected_webinar_id > 0) {
+ $where_parts[] = 'a.webinar_id = :webinar_id';
+ $params[':webinar_id'] = $selected_webinar_id;
+}
+$where_sql = 'WHERE ' . implode(' AND ', $where_parts);
+
+$count_stmt = $pdo->prepare("SELECT COUNT(*) FROM attendees a {$where_sql}");
+bind_named_values($count_stmt, $params);
+$count_stmt->execute();
+$total_records = (int) $count_stmt->fetchColumn();
+
+$total_pages = max(1, (int) ceil($total_records / $records_per_page));
+if ($page > $total_pages) {
+ $page = $total_pages;
+}
$offset = ($page - 1) * $records_per_page;
-// Get total number of records
-$total_stmt = $pdo->query("SELECT COUNT(*) FROM attendees");
-$total_records = $total_stmt->fetchColumn();
-$total_pages = ceil($total_records / $records_per_page);
+$today_stmt = $pdo->prepare("SELECT COUNT(*) FROM attendees a {$where_sql} AND DATE(a.created_at) = CURDATE()");
+bind_named_values($today_stmt, $params);
+$today_stmt->execute();
+$today_count = (int) $today_stmt->fetchColumn();
-// Get records for the current page
-$stmt = $pdo->prepare("SELECT id, first_name, last_name, email, created_at, how_did_you_hear, company FROM attendees ORDER BY first_name ASC, last_name ASC LIMIT :limit OFFSET :offset");
-$stmt->bindValue(':limit', $records_per_page, PDO::PARAM_INT);
-$stmt->bindValue(':offset', $offset, PDO::PARAM_INT);
-$stmt->execute();
-$attendees = $stmt->fetchAll(PDO::FETCH_ASSOC);
+$last7_stmt = $pdo->prepare("SELECT COUNT(*) FROM attendees a {$where_sql} AND a.created_at >= (NOW() - INTERVAL 7 DAY)");
+bind_named_values($last7_stmt, $params);
+$last7_stmt->execute();
+$last_7_days_count = (int) $last7_stmt->fetchColumn();
-// Get data for the chart
-$chart_stmt = $pdo->query("SELECT DATE(created_at) as registration_day, COUNT(*) as user_count FROM attendees GROUP BY registration_day ORDER BY registration_day");
+$latest_stmt = $pdo->prepare("SELECT MAX(a.created_at) FROM attendees a {$where_sql}");
+bind_named_values($latest_stmt, $params);
+$latest_stmt->execute();
+$latest_registration_at = $latest_stmt->fetchColumn();
+
+$company_stmt = $pdo->prepare("SELECT COUNT(DISTINCT NULLIF(TRIM(a.company), '')) FROM attendees a {$where_sql}");
+bind_named_values($company_stmt, $params);
+$company_stmt->execute();
+$unique_companies = (int) $company_stmt->fetchColumn();
+
+$attendees_sql = "SELECT
+ a.id,
+ a.webinar_id,
+ a.first_name,
+ a.last_name,
+ a.email,
+ a.company,
+ a.how_did_you_hear,
+ a.timezone,
+ a.consented,
+ a.created_at,
+ w.title AS webinar_title,
+ w.scheduled_at AS webinar_scheduled_at
+ FROM attendees a
+ LEFT JOIN webinars w ON w.id = a.webinar_id
+ {$where_sql}
+ ORDER BY a.created_at DESC, a.id DESC
+ LIMIT :limit OFFSET :offset";
+$attendees_stmt = $pdo->prepare($attendees_sql);
+bind_named_values($attendees_stmt, $params);
+$attendees_stmt->bindValue(':limit', $records_per_page, PDO::PARAM_INT);
+$attendees_stmt->bindValue(':offset', $offset, PDO::PARAM_INT);
+$attendees_stmt->execute();
+$attendees = $attendees_stmt->fetchAll(PDO::FETCH_ASSOC);
+
+$chart_stmt = $pdo->prepare("SELECT DATE(a.created_at) AS registration_day, COUNT(*) AS user_count
+ FROM attendees a
+ {$where_sql}
+ GROUP BY DATE(a.created_at)
+ ORDER BY DATE(a.created_at)");
+bind_named_values($chart_stmt, $params);
+$chart_stmt->execute();
$chart_data = $chart_stmt->fetchAll(PDO::FETCH_ASSOC);
+$selected_webinar = null;
+foreach ($webinars as $webinar) {
+ if ((int) $webinar['id'] === $selected_webinar_id) {
+ $selected_webinar = $webinar;
+ break;
+ }
+}
+
$chart_labels = json_encode(array_column($chart_data, 'registration_day'));
$chart_values = json_encode(array_column($chart_data, 'user_count'));
-
+$export_link = 'export_csv.php' . ($selected_webinar_id > 0 ? '?webinar_id=' . urlencode((string) $selected_webinar_id) : '');
?>
- Admin Dashboard
+
+ Admin Dashboard | Webinar Registrations
+
+
@@ -65,7 +148,6 @@ $chart_values = json_encode(array_column($chart_data, 'user_count'));
--text-muted: #97acd0;
--accent: #2c4bd1;
--accent-strong: #1029a0;
- --accent-soft: #bbc8fb;
--success: #18d1b3;
--danger: #ff6b81;
--font-body: "Inter", system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
@@ -82,39 +164,36 @@ $chart_values = json_encode(array_column($chart_data, 'user_count'));
color: var(--text-main);
}
.admin-shell {
- max-width: 1220px;
+ max-width: 1380px;
padding-top: 40px;
padding-bottom: 40px;
}
- h2, h3, .h5, p, label, th, td {
- color: var(--text-main);
- }
- h2, h3, .h5, th, .btn, .page-link {
+ h1, h2, h3, .h5, .btn, .page-link, th, label {
font-family: var(--font-display);
}
- h2 {
- font-size: 2rem;
+ h1 {
+ font-size: 2.2rem;
font-weight: 700;
- letter-spacing: -0.03em;
+ letter-spacing: -0.04em;
margin-bottom: 0.35rem;
}
- h3, .h5 {
- font-weight: 700;
- letter-spacing: -0.02em;
- }
- p.text-muted, .text-muted {
+ .lead-copy,
+ .text-muted {
color: var(--text-muted) !important;
}
.panel-card,
- .table-shell {
+ .table-shell,
+ .summary-card,
+ .toolbar-card {
background: var(--surface);
border: 1px solid var(--line);
border-radius: 22px;
box-shadow: 0 25px 60px rgba(3, 11, 25, 0.38);
backdrop-filter: blur(16px);
}
- .panel-card {
- margin-top: 1.5rem;
+ .panel-card,
+ .summary-card,
+ .toolbar-card {
padding: 1.5rem;
}
.table-shell {
@@ -122,153 +201,275 @@ $chart_values = json_encode(array_column($chart_data, 'user_count'));
padding: 0.35rem;
overflow: hidden;
}
- .alert-info {
- background: rgba(34, 199, 255, 0.12);
- color: var(--text-main);
- border: 1px solid rgba(34, 199, 255, 0.28);
+ .summary-grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
+ gap: 1rem;
+ margin-top: 1.5rem;
+ margin-bottom: 1.5rem;
}
- .btn,
- .page-link {
+ .summary-label {
+ color: var(--text-soft);
+ font-size: 0.9rem;
+ margin-bottom: 0.45rem;
+ }
+ .summary-value {
+ font-size: 2rem;
+ font-weight: 800;
+ line-height: 1;
+ }
+ .summary-meta {
+ margin-top: 0.6rem;
+ color: var(--text-muted);
+ font-size: 0.9rem;
+ }
+ .toolbar-card {
+ display: flex;
+ align-items: end;
+ justify-content: space-between;
+ gap: 1rem;
+ flex-wrap: wrap;
+ margin-top: 1rem;
+ }
+ .filter-form {
+ display: flex;
+ align-items: end;
+ gap: 0.75rem;
+ flex-wrap: wrap;
+ }
+ .form-select,
+ .form-control {
+ min-width: 260px;
+ background: rgba(221, 226, 253, 0.08);
+ border-color: rgba(221, 226, 253, 0.18);
+ color: var(--text-main);
+ }
+ .form-select:focus,
+ .form-control:focus {
+ background: rgba(221, 226, 253, 0.10);
+ color: var(--text-main);
+ border-color: rgba(44, 75, 209, 0.65);
+ box-shadow: 0 0 0 0.2rem rgba(187, 200, 251, 0.18);
+ }
+ .table {
+ --bs-table-bg: transparent;
+ --bs-table-color: var(--text-main);
+ --bs-table-striped-bg: rgba(255, 255, 255, 0.03);
+ --bs-table-striped-color: var(--text-main);
+ --bs-table-border-color: rgba(221, 226, 253, 0.12);
+ margin-bottom: 0;
+ }
+ th {
+ color: var(--text-soft);
+ font-size: 0.85rem;
+ letter-spacing: 0.04em;
+ text-transform: uppercase;
+ white-space: nowrap;
+ }
+ td {
+ vertical-align: middle;
+ }
+ .tag {
+ display: inline-flex;
+ align-items: center;
+ padding: 0.32rem 0.7rem;
+ border-radius: 999px;
+ background: rgba(34, 199, 255, 0.14);
+ color: var(--text-main);
+ font-size: 0.84rem;
+ border: 1px solid rgba(34, 199, 255, 0.18);
+ }
+ .attendee-name {
+ font-weight: 700;
+ }
+ .attendee-meta,
+ .webinar-meta {
+ display: block;
+ margin-top: 0.25rem;
+ color: var(--text-muted);
+ font-size: 0.86rem;
+ }
+ .btn {
border-radius: 14px;
font-weight: 700;
letter-spacing: 0.02em;
- transition: transform 0.2s ease, box-shadow 0.2s ease, background 0.2s ease;
}
- .btn:hover,
- .page-link:hover {
- transform: translateY(-1px);
- }
- .btn-outline-primary {
- color: var(--accent-soft, #bbc8fb);
- border-color: rgba(34, 199, 255, 0.35);
- background: rgba(221, 226, 253, 0.06);
- box-shadow: none;
- }
- .btn-outline-primary:hover {
- background: rgba(187, 200, 251, 0.18);
- border-color: rgba(34, 199, 255, 0.55);
- box-shadow: var(--button-shadow);
- }
- .btn-success,
.btn-primary {
background: linear-gradient(135deg, var(--accent) 0%, var(--accent-strong) 100%);
- border: 1px solid rgba(221, 226, 253, 0.12);
+ border: 1px solid rgba(221, 226, 253, 0.14);
box-shadow: var(--button-shadow);
}
- .btn-success:hover,
- .btn-primary:hover {
- background: linear-gradient(135deg, #bbc8fb 0%, #2c4bd1 100%);
- box-shadow: 0 20px 34px rgba(11, 32, 131, 0.32);
+ .btn-success {
+ background: linear-gradient(135deg, #18d1b3 0%, #0b8f7a 100%);
+ border-color: transparent;
+ box-shadow: 0 16px 30px rgba(24, 209, 179, 0.18);
}
- .btn-danger {
- background: linear-gradient(135deg, #ff7a8b 0%, #f04f74 100%);
- border: 1px solid rgba(255, 255, 255, 0.08);
- box-shadow: 0 16px 30px rgba(72, 18, 36, 0.28);
+ .btn-outline-light {
+ border-color: rgba(221, 226, 253, 0.28);
}
- .table {
- margin-bottom: 0;
- --bs-table-bg: transparent;
- --bs-table-striped-bg: rgba(221, 226, 253, 0.06);
- --bs-table-striped-color: var(--text-main);
- --bs-table-hover-bg: rgba(187, 200, 251, 0.12);
- --bs-table-hover-color: var(--text-main);
+ .pagination .page-link {
color: var(--text-main);
+ background: rgba(221, 226, 253, 0.05);
+ border-color: rgba(221, 226, 253, 0.14);
}
- .table thead th {
- border-bottom-color: rgba(221, 226, 253, 0.18);
- color: var(--accent-soft);
- font-weight: 700;
- text-transform: uppercase;
- letter-spacing: 0.08em;
- font-size: 0.78rem;
- background: rgba(255, 255, 255, 0.02);
- }
- .table td,
- .table th {
- border-color: rgba(143, 232, 255, 0.08);
- vertical-align: middle;
- }
- .page-link {
- background: rgba(255, 255, 255, 0.04);
- color: var(--text-main);
- border-color: rgba(143, 232, 255, 0.12);
- box-shadow: none;
- }
- .page-item.active .page-link {
- background: linear-gradient(135deg, var(--accent) 0%, var(--accent-strong) 100%);
+ .pagination .page-item.active .page-link {
+ background: var(--accent);
border-color: transparent;
}
- .page-link:hover {
- color: var(--text-main);
- background: rgba(34, 199, 255, 0.12);
- border-color: rgba(34, 199, 255, 0.24);
+ .alert {
+ border-radius: 16px;
+ margin-top: 1.2rem;
}
- canvas {
- filter: drop-shadow(0 10px 25px rgba(34, 199, 255, 0.08));
+ code {
+ color: var(--accent-soft, #bbc8fb);
+ }
+ @media (max-width: 991px) {
+ .admin-shell {
+ padding-top: 24px;
+ }
+ .summary-value {
+ font-size: 1.7rem;
+ }
+ .form-select,
+ .form-control {
+ min-width: 210px;
+ }
}
-
-
Admin Dashboard
-
Welcome, !
+
+
+ Webinar registrations dashboard
+ Welcome, . Review attendee data, edit registrations, export CSV, and monitor daily signup volume.
+
-
-
-
+
-
-
-
Daily Registrations
-
+
-
-
Registered Attendees
-
Download CSV
-
+
-
+
| ID |
- First Name |
- Last Name |
+ Attendee |
Email |
- Source |
Company |
- Registered At |
+ Source |
+ Timezone |
+ Webinar |
+ Registered at |
Actions |
- | No attendees found. |
+ No attendees found for this filter yet. |
- |
- |
- |
- |
- |
- |
- |
+ |
- Edit
-
+
+ Consent:
+ |
+ |
+ |
+ |
+ |
+
+
+
+ |
+ |
+
+
|
@@ -277,74 +478,74 @@ $chart_values = json_encode(array_column($chart_data, 'user_count'));
-
-
-
+ 1): ?>
+
+
+
-
\ No newline at end of file
+