Excludes analytics/data, vendor, support, .env, ACME tokens. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
474 lines
19 KiB
PHP
474 lines
19 KiB
PHP
<?php
|
|
/* Version: 1.0.0 | Created: 2026-04-08 | Updated: 2026-04-08 */
|
|
/* HELP4BIS Analytics Dashboard — Self-hosted admin panel */
|
|
|
|
session_start();
|
|
|
|
// ── Authentication ──────────────────────────────────────────────────────────
|
|
$ADMIN_USER = 'admin';
|
|
$ADMIN_HASH = '$2y$10$PLACEHOLDER'; // Set on first run
|
|
|
|
$configFile = __DIR__ . '/data/.auth_config';
|
|
|
|
// First-run setup: create password hash
|
|
if (!file_exists($configFile)) {
|
|
if (isset($_POST['setup_password'])) {
|
|
$pass = $_POST['setup_password'];
|
|
if (strlen($pass) < 8) {
|
|
$setupError = 'Password must be at least 8 characters.';
|
|
} else {
|
|
$hash = password_hash($pass, PASSWORD_BCRYPT);
|
|
$dataDir = __DIR__ . '/data';
|
|
if (!is_dir($dataDir)) mkdir($dataDir, 0750, true);
|
|
file_put_contents($configFile, json_encode([
|
|
'user' => $ADMIN_USER,
|
|
'hash' => $hash,
|
|
'created' => date('Y-m-d H:i:s')
|
|
]));
|
|
chmod($configFile, 0600);
|
|
$_SESSION['analytics_auth'] = true;
|
|
$_SESSION['analytics_user'] = $ADMIN_USER;
|
|
header('Location: /analytics/');
|
|
exit;
|
|
}
|
|
}
|
|
// Show setup form
|
|
?><!DOCTYPE html>
|
|
<html lang="en-AU"><head>
|
|
<meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1">
|
|
<title>HELP4BIS Analytics — Setup</title>
|
|
<style>
|
|
*{margin:0;padding:0;box-sizing:border-box}
|
|
body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;background:#0f172a;color:#e2e8f0;display:flex;align-items:center;justify-content:center;min-height:100vh}
|
|
.setup{background:#1e293b;padding:2.5rem;border-radius:12px;width:100%;max-width:400px;box-shadow:0 25px 50px -12px rgba(0,0,0,.5)}
|
|
h1{font-size:1.25rem;margin-bottom:.5rem;color:#f8fafc}
|
|
p{font-size:.875rem;color:#94a3b8;margin-bottom:1.5rem}
|
|
label{display:block;font-size:.75rem;font-weight:600;text-transform:uppercase;letter-spacing:.05em;color:#64748b;margin-bottom:.5rem}
|
|
input{width:100%;padding:.75rem 1rem;border:1px solid #334155;border-radius:8px;background:#0f172a;color:#f8fafc;font-size:1rem;margin-bottom:1rem}
|
|
input:focus{outline:none;border-color:#3b82f6;box-shadow:0 0 0 3px rgba(59,130,246,.2)}
|
|
button{width:100%;padding:.75rem;background:#3b82f6;color:#fff;border:none;border-radius:8px;font-size:1rem;font-weight:600;cursor:pointer;transition:background .2s}
|
|
button:hover{background:#2563eb}
|
|
.error{background:#7f1d1d;color:#fca5a5;padding:.75rem;border-radius:8px;margin-bottom:1rem;font-size:.875rem}
|
|
.brand{color:#3b82f6;font-weight:700}
|
|
</style></head><body>
|
|
<div class="setup">
|
|
<h1><span class="brand">HELP4BIS</span> Analytics</h1>
|
|
<p>First-time setup. Create your admin password.</p>
|
|
<?php if (isset($setupError)): ?><div class="error"><?= htmlspecialchars($setupError) ?></div><?php endif; ?>
|
|
<form method="post">
|
|
<label>Admin Username</label>
|
|
<input type="text" value="admin" disabled>
|
|
<label>Password</label>
|
|
<input type="password" name="setup_password" required minlength="8" autofocus placeholder="Minimum 8 characters">
|
|
<button type="submit">Create Account</button>
|
|
</form></div></body></html><?php
|
|
exit;
|
|
}
|
|
|
|
// Load auth config
|
|
$auth = json_decode(file_get_contents($configFile), true);
|
|
|
|
// Login
|
|
if (!isset($_SESSION['analytics_auth'])) {
|
|
if (isset($_POST['password'])) {
|
|
if (password_verify($_POST['password'], $auth['hash'])) {
|
|
$_SESSION['analytics_auth'] = true;
|
|
$_SESSION['analytics_user'] = $auth['user'];
|
|
header('Location: /analytics/');
|
|
exit;
|
|
}
|
|
$loginError = 'Incorrect password.';
|
|
}
|
|
?><!DOCTYPE html>
|
|
<html lang="en-AU"><head>
|
|
<meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1">
|
|
<title>HELP4BIS Analytics — Login</title>
|
|
<style>
|
|
*{margin:0;padding:0;box-sizing:border-box}
|
|
body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;background:#0f172a;color:#e2e8f0;display:flex;align-items:center;justify-content:center;min-height:100vh}
|
|
.login{background:#1e293b;padding:2.5rem;border-radius:12px;width:100%;max-width:400px;box-shadow:0 25px 50px -12px rgba(0,0,0,.5)}
|
|
h1{font-size:1.25rem;margin-bottom:1.5rem;color:#f8fafc}
|
|
label{display:block;font-size:.75rem;font-weight:600;text-transform:uppercase;letter-spacing:.05em;color:#64748b;margin-bottom:.5rem}
|
|
input{width:100%;padding:.75rem 1rem;border:1px solid #334155;border-radius:8px;background:#0f172a;color:#f8fafc;font-size:1rem;margin-bottom:1rem}
|
|
input:focus{outline:none;border-color:#3b82f6;box-shadow:0 0 0 3px rgba(59,130,246,.2)}
|
|
button{width:100%;padding:.75rem;background:#3b82f6;color:#fff;border:none;border-radius:8px;font-size:1rem;font-weight:600;cursor:pointer;transition:background .2s}
|
|
button:hover{background:#2563eb}
|
|
.error{background:#7f1d1d;color:#fca5a5;padding:.75rem;border-radius:8px;margin-bottom:1rem;font-size:.875rem}
|
|
.brand{color:#3b82f6;font-weight:700}
|
|
</style></head><body>
|
|
<div class="login">
|
|
<h1><span class="brand">HELP4BIS</span> Analytics</h1>
|
|
<?php if (isset($loginError)): ?><div class="error"><?= htmlspecialchars($loginError) ?></div><?php endif; ?>
|
|
<form method="post">
|
|
<label>Password</label>
|
|
<input type="password" name="password" required autofocus placeholder="Enter admin password">
|
|
<button type="submit">Log In</button>
|
|
</form></div></body></html><?php
|
|
exit;
|
|
}
|
|
|
|
// Logout
|
|
if (isset($_GET['logout'])) {
|
|
session_destroy();
|
|
header('Location: /analytics/');
|
|
exit;
|
|
}
|
|
|
|
// ── Dashboard ───────────────────────────────────────────────────────────────
|
|
$dbPath = __DIR__ . '/data/analytics.db';
|
|
$hasData = file_exists($dbPath);
|
|
$db = null;
|
|
|
|
if ($hasData) {
|
|
$db = new SQLite3($dbPath);
|
|
$db->busyTimeout(5000);
|
|
}
|
|
|
|
// Date range
|
|
$range = $_GET['range'] ?? '7d';
|
|
$rangeMap = ['24h' => 1, '7d' => 7, '30d' => 30, '90d' => 90];
|
|
$days = $rangeMap[$range] ?? 7;
|
|
$since = date('Y-m-d', strtotime("-{$days} days"));
|
|
|
|
// Fetch metrics
|
|
$metrics = ['pageviews' => 0, 'unique_visitors' => 0, 'avg_duration' => 0, 'cta_clicks' => 0, 'form_submits' => 0, 'bounce_rate' => 0];
|
|
$topPages = [];
|
|
$topReferrers = [];
|
|
$topCTAs = [];
|
|
$dailyChart = [];
|
|
$scrollData = [];
|
|
|
|
if ($db) {
|
|
// Total pageviews
|
|
$r = $db->querySingle("SELECT COUNT(*) FROM events WHERE event='pageview' AND created_at >= '$since'");
|
|
$metrics['pageviews'] = (int)$r;
|
|
|
|
// Unique visitors
|
|
$r = $db->querySingle("SELECT COUNT(DISTINCT session_id) FROM events WHERE event='pageview' AND created_at >= '$since'");
|
|
$metrics['unique_visitors'] = (int)$r;
|
|
|
|
// Avg duration
|
|
$r = $db->querySingle("SELECT AVG(CAST(json_extract(extra,'$.seconds') AS REAL)) FROM events WHERE event='duration' AND created_at >= '$since'");
|
|
$metrics['avg_duration'] = round((float)$r, 1);
|
|
|
|
// CTA clicks
|
|
$r = $db->querySingle("SELECT COUNT(*) FROM events WHERE event='cta_click' AND created_at >= '$since'");
|
|
$metrics['cta_clicks'] = (int)$r;
|
|
|
|
// Form submits
|
|
$r = $db->querySingle("SELECT COUNT(*) FROM events WHERE event='form_submit' AND created_at >= '$since'");
|
|
$metrics['form_submits'] = (int)$r;
|
|
|
|
// Bounce rate (sessions with only 1 pageview)
|
|
$totalSessions = $db->querySingle("SELECT COUNT(DISTINCT session_id) FROM events WHERE event='pageview' AND created_at >= '$since'");
|
|
$singlePageSessions = $db->querySingle("SELECT COUNT(*) FROM (SELECT session_id, COUNT(*) as cnt FROM events WHERE event='pageview' AND created_at >= '$since' GROUP BY session_id HAVING cnt = 1)");
|
|
$metrics['bounce_rate'] = $totalSessions > 0 ? round(($singlePageSessions / $totalSessions) * 100, 1) : 0;
|
|
|
|
// Top pages
|
|
$result = $db->query("SELECT page, COUNT(*) as views, COUNT(DISTINCT session_id) as visitors
|
|
FROM events WHERE event='pageview' AND created_at >= '$since'
|
|
GROUP BY page ORDER BY views DESC LIMIT 15");
|
|
while ($row = $result->fetchArray(SQLITE3_ASSOC)) {
|
|
$topPages[] = $row;
|
|
}
|
|
|
|
// Top referrers
|
|
$result = $db->query("SELECT referrer, COUNT(*) as hits
|
|
FROM events WHERE event='pageview' AND referrer != '' AND created_at >= '$since'
|
|
GROUP BY referrer ORDER BY hits DESC LIMIT 10");
|
|
while ($row = $result->fetchArray(SQLITE3_ASSOC)) {
|
|
$topReferrers[] = $row;
|
|
}
|
|
|
|
// Top CTA clicks
|
|
$result = $db->query("SELECT json_extract(extra,'$.text') as label, json_extract(extra,'$.href') as href, COUNT(*) as clicks
|
|
FROM events WHERE event='cta_click' AND created_at >= '$since'
|
|
GROUP BY label, href ORDER BY clicks DESC LIMIT 10");
|
|
while ($row = $result->fetchArray(SQLITE3_ASSOC)) {
|
|
$topCTAs[] = $row;
|
|
}
|
|
|
|
// Daily chart data
|
|
$result = $db->query("SELECT date(created_at) as day, COUNT(*) as views, COUNT(DISTINCT session_id) as visitors
|
|
FROM events WHERE event='pageview' AND created_at >= '$since'
|
|
GROUP BY day ORDER BY day ASC");
|
|
while ($row = $result->fetchArray(SQLITE3_ASSOC)) {
|
|
$dailyChart[] = $row;
|
|
}
|
|
|
|
// Scroll depth
|
|
$result = $db->query("SELECT json_extract(extra,'$.depth') as depth, COUNT(*) as cnt
|
|
FROM events WHERE event='scroll' AND created_at >= '$since'
|
|
GROUP BY depth ORDER BY depth ASC");
|
|
while ($row = $result->fetchArray(SQLITE3_ASSOC)) {
|
|
$scrollData[] = $row;
|
|
}
|
|
|
|
$db->close();
|
|
}
|
|
|
|
$chartLabels = json_encode(array_column($dailyChart, 'day'));
|
|
$chartViews = json_encode(array_map('intval', array_column($dailyChart, 'views')));
|
|
$chartVisitors = json_encode(array_map('intval', array_column($dailyChart, 'visitors')));
|
|
?>
|
|
<!DOCTYPE html>
|
|
<html lang="en-AU">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width,initial-scale=1">
|
|
<meta name="robots" content="noindex,nofollow">
|
|
<title>HELP4BIS Analytics Dashboard</title>
|
|
<script src="https://cdn.jsdelivr.net/npm/chart.js@4/dist/chart.umd.min.js"></script>
|
|
<style>
|
|
*{margin:0;padding:0;box-sizing:border-box}
|
|
:root{--bg:#0f172a;--surface:#1e293b;--border:#334155;--text:#e2e8f0;--muted:#94a3b8;--accent:#3b82f6;--green:#22c55e;--red:#ef4444;--amber:#f59e0b}
|
|
body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;background:var(--bg);color:var(--text);line-height:1.5}
|
|
a{color:var(--accent);text-decoration:none}
|
|
a:hover{text-decoration:underline}
|
|
|
|
/* Layout */
|
|
.header{background:var(--surface);border-bottom:1px solid var(--border);padding:1rem 2rem;display:flex;justify-content:space-between;align-items:center}
|
|
.header h1{font-size:1.125rem;font-weight:700}
|
|
.header h1 span{color:var(--accent)}
|
|
.header nav{display:flex;gap:1rem;align-items:center}
|
|
.header nav a{font-size:.875rem;color:var(--muted)}
|
|
.container{max-width:1280px;margin:0 auto;padding:1.5rem}
|
|
|
|
/* Range tabs */
|
|
.range-tabs{display:flex;gap:.5rem;margin-bottom:1.5rem}
|
|
.range-tabs a{padding:.5rem 1rem;border-radius:8px;font-size:.875rem;font-weight:500;color:var(--muted);background:var(--surface);border:1px solid var(--border);transition:all .2s}
|
|
.range-tabs a.active,.range-tabs a:hover{color:#fff;background:var(--accent);border-color:var(--accent)}
|
|
|
|
/* Metric cards */
|
|
.metrics{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:1rem;margin-bottom:1.5rem}
|
|
.metric{background:var(--surface);border:1px solid var(--border);border-radius:12px;padding:1.25rem}
|
|
.metric .label{font-size:.75rem;font-weight:600;text-transform:uppercase;letter-spacing:.05em;color:var(--muted);margin-bottom:.25rem}
|
|
.metric .value{font-size:1.75rem;font-weight:700;color:#f8fafc}
|
|
.metric .sub{font-size:.75rem;color:var(--muted);margin-top:.25rem}
|
|
|
|
/* Chart */
|
|
.chart-card{background:var(--surface);border:1px solid var(--border);border-radius:12px;padding:1.25rem;margin-bottom:1.5rem}
|
|
.chart-card h2{font-size:.875rem;font-weight:600;color:var(--muted);text-transform:uppercase;letter-spacing:.05em;margin-bottom:1rem}
|
|
|
|
/* Tables */
|
|
.grid-2{display:grid;grid-template-columns:repeat(auto-fit,minmax(400px,1fr));gap:1.5rem;margin-bottom:1.5rem}
|
|
.table-card{background:var(--surface);border:1px solid var(--border);border-radius:12px;padding:1.25rem}
|
|
.table-card h2{font-size:.875rem;font-weight:600;color:var(--muted);text-transform:uppercase;letter-spacing:.05em;margin-bottom:1rem}
|
|
table{width:100%;border-collapse:collapse}
|
|
th{text-align:left;font-size:.75rem;font-weight:600;color:var(--muted);text-transform:uppercase;letter-spacing:.05em;padding:.5rem .75rem;border-bottom:1px solid var(--border)}
|
|
td{padding:.625rem .75rem;font-size:.875rem;border-bottom:1px solid var(--border)}
|
|
tr:last-child td{border-bottom:none}
|
|
.num{text-align:right;font-variant-numeric:tabular-nums;font-weight:600}
|
|
|
|
/* Scroll bars */
|
|
.scroll-bars{display:flex;gap:1rem;align-items:flex-end;height:100px;margin-top:1rem}
|
|
.scroll-bar{flex:1;display:flex;flex-direction:column;align-items:center;gap:.25rem}
|
|
.scroll-bar .bar{width:100%;border-radius:4px 4px 0 0;background:var(--accent);min-height:4px;transition:height .3s}
|
|
.scroll-bar .lbl{font-size:.75rem;color:var(--muted)}
|
|
.scroll-bar .val{font-size:.75rem;font-weight:600;color:#f8fafc}
|
|
|
|
/* Empty state */
|
|
.empty{text-align:center;padding:4rem 2rem}
|
|
.empty h2{font-size:1.25rem;margin-bottom:.5rem;color:#f8fafc}
|
|
.empty p{color:var(--muted);max-width:500px;margin:0 auto}
|
|
.empty code{background:var(--surface);padding:.125rem .5rem;border-radius:4px;font-size:.875rem}
|
|
|
|
@media(max-width:768px){
|
|
.header{flex-direction:column;gap:.75rem;text-align:center}
|
|
.container{padding:1rem}
|
|
.grid-2{grid-template-columns:1fr}
|
|
.metrics{grid-template-columns:repeat(2,1fr)}
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
|
|
<div class="header">
|
|
<h1><span>HELP4BIS</span> Analytics</h1>
|
|
<nav>
|
|
<a href="/">View Site</a>
|
|
<a href="?logout">Log Out</a>
|
|
</nav>
|
|
</div>
|
|
|
|
<div class="container">
|
|
|
|
<!-- Range tabs -->
|
|
<div class="range-tabs">
|
|
<?php foreach (['24h'=>'24 Hours','7d'=>'7 Days','30d'=>'30 Days','90d'=>'90 Days'] as $k=>$v): ?>
|
|
<a href="?range=<?= $k ?>" class="<?= $range===$k?'active':'' ?>"><?= $v ?></a>
|
|
<?php endforeach; ?>
|
|
</div>
|
|
|
|
<?php if (!$hasData || $metrics['pageviews'] === 0): ?>
|
|
<!-- Empty state -->
|
|
<div class="empty">
|
|
<h2>No data yet</h2>
|
|
<p>Analytics tracking is active. Data will appear here once visitors start arriving.
|
|
The tracker is loaded on all pages via <code><script src="/analytics/tracker.js"></code></p>
|
|
</div>
|
|
|
|
<?php else: ?>
|
|
<!-- Metric cards -->
|
|
<div class="metrics">
|
|
<div class="metric">
|
|
<div class="label">Pageviews</div>
|
|
<div class="value"><?= number_format($metrics['pageviews']) ?></div>
|
|
<div class="sub">Last <?= $days ?> day<?= $days>1?'s':'' ?></div>
|
|
</div>
|
|
<div class="metric">
|
|
<div class="label">Unique Visitors</div>
|
|
<div class="value"><?= number_format($metrics['unique_visitors']) ?></div>
|
|
<div class="sub">By session hash</div>
|
|
</div>
|
|
<div class="metric">
|
|
<div class="label">Avg. Duration</div>
|
|
<div class="value"><?= $metrics['avg_duration'] ?>s</div>
|
|
<div class="sub">Time on page</div>
|
|
</div>
|
|
<div class="metric">
|
|
<div class="label">Bounce Rate</div>
|
|
<div class="value"><?= $metrics['bounce_rate'] ?>%</div>
|
|
<div class="sub">Single-page sessions</div>
|
|
</div>
|
|
<div class="metric">
|
|
<div class="label">CTA Clicks</div>
|
|
<div class="value"><?= number_format($metrics['cta_clicks']) ?></div>
|
|
<div class="sub">Buttons & links</div>
|
|
</div>
|
|
<div class="metric">
|
|
<div class="label">Form Submissions</div>
|
|
<div class="value"><?= number_format($metrics['form_submits']) ?></div>
|
|
<div class="sub">Contact / brief forms</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Chart -->
|
|
<div class="chart-card">
|
|
<h2>Traffic Over Time</h2>
|
|
<canvas id="trafficChart" height="80"></canvas>
|
|
</div>
|
|
|
|
<!-- Tables row -->
|
|
<div class="grid-2">
|
|
<!-- Top pages -->
|
|
<div class="table-card">
|
|
<h2>Top Pages</h2>
|
|
<table>
|
|
<tr><th>Page</th><th class="num">Views</th><th class="num">Visitors</th></tr>
|
|
<?php foreach ($topPages as $p): ?>
|
|
<tr>
|
|
<td><?= htmlspecialchars($p['page'] === '/' ? 'Homepage' : ltrim($p['page'], '/')) ?></td>
|
|
<td class="num"><?= number_format($p['views']) ?></td>
|
|
<td class="num"><?= number_format($p['visitors']) ?></td>
|
|
</tr>
|
|
<?php endforeach; ?>
|
|
</table>
|
|
</div>
|
|
|
|
<!-- Top referrers -->
|
|
<div class="table-card">
|
|
<h2>Top Referrers</h2>
|
|
<?php if (empty($topReferrers)): ?>
|
|
<p style="color:var(--muted);font-size:.875rem;padding:.5rem 0">No external referrers yet.</p>
|
|
<?php else: ?>
|
|
<table>
|
|
<tr><th>Source</th><th class="num">Visits</th></tr>
|
|
<?php foreach ($topReferrers as $r): ?>
|
|
<tr>
|
|
<td><?= htmlspecialchars(parse_url($r['referrer'], PHP_URL_HOST) ?: $r['referrer']) ?></td>
|
|
<td class="num"><?= number_format($r['hits']) ?></td>
|
|
</tr>
|
|
<?php endforeach; ?>
|
|
</table>
|
|
<?php endif; ?>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="grid-2">
|
|
<!-- CTA clicks -->
|
|
<div class="table-card">
|
|
<h2>CTA Clicks</h2>
|
|
<?php if (empty($topCTAs)): ?>
|
|
<p style="color:var(--muted);font-size:.875rem;padding:.5rem 0">No CTA clicks recorded yet.</p>
|
|
<?php else: ?>
|
|
<table>
|
|
<tr><th>Button</th><th>Destination</th><th class="num">Clicks</th></tr>
|
|
<?php foreach ($topCTAs as $c): ?>
|
|
<tr>
|
|
<td><?= htmlspecialchars(substr($c['label'] ?? '—', 0, 40)) ?></td>
|
|
<td><?= htmlspecialchars(substr($c['href'] ?? '', 0, 40)) ?></td>
|
|
<td class="num"><?= number_format($c['clicks']) ?></td>
|
|
</tr>
|
|
<?php endforeach; ?>
|
|
</table>
|
|
<?php endif; ?>
|
|
</div>
|
|
|
|
<!-- Scroll depth -->
|
|
<div class="table-card">
|
|
<h2>Scroll Depth</h2>
|
|
<?php if (empty($scrollData)): ?>
|
|
<p style="color:var(--muted);font-size:.875rem;padding:.5rem 0">No scroll data yet.</p>
|
|
<?php else: ?>
|
|
<?php
|
|
$maxScroll = max(array_column($scrollData, 'cnt'));
|
|
?>
|
|
<div class="scroll-bars">
|
|
<?php foreach ($scrollData as $s): ?>
|
|
<div class="scroll-bar">
|
|
<div class="val"><?= number_format($s['cnt']) ?></div>
|
|
<div class="bar" style="height:<?= $maxScroll > 0 ? round(($s['cnt']/$maxScroll)*80) : 4 ?>px"></div>
|
|
<div class="lbl"><?= htmlspecialchars($s['depth']) ?>%</div>
|
|
</div>
|
|
<?php endforeach; ?>
|
|
</div>
|
|
<?php endif; ?>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
var ctx = document.getElementById('trafficChart').getContext('2d');
|
|
new Chart(ctx, {
|
|
type: 'line',
|
|
data: {
|
|
labels: <?= $chartLabels ?>,
|
|
datasets: [{
|
|
label: 'Pageviews',
|
|
data: <?= $chartViews ?>,
|
|
borderColor: '#3b82f6',
|
|
backgroundColor: 'rgba(59,130,246,.1)',
|
|
fill: true,
|
|
tension: .3,
|
|
pointRadius: 3,
|
|
pointHoverRadius: 5
|
|
}, {
|
|
label: 'Visitors',
|
|
data: <?= $chartVisitors ?>,
|
|
borderColor: '#22c55e',
|
|
backgroundColor: 'rgba(34,197,94,.1)',
|
|
fill: true,
|
|
tension: .3,
|
|
pointRadius: 3,
|
|
pointHoverRadius: 5
|
|
}]
|
|
},
|
|
options: {
|
|
responsive: true,
|
|
maintainAspectRatio: true,
|
|
plugins: {
|
|
legend: { labels: { color: '#94a3b8', font: { size: 12 } } }
|
|
},
|
|
scales: {
|
|
x: { grid: { color: '#1e293b' }, ticks: { color: '#64748b', font: { size: 11 } } },
|
|
y: { beginAtZero: true, grid: { color: '#1e293b' }, ticks: { color: '#64748b', font: { size: 11 }, precision: 0 } }
|
|
}
|
|
}
|
|
});
|
|
</script>
|
|
<?php endif; ?>
|
|
|
|
</div>
|
|
</body>
|
|
</html>
|