3e4cfcf683
- Create detailed event tracking system (views, watch time, retention, traffic) - Build channel analytics with daily aggregation - Implement real-time stats caching - Add traffic source detection and tracking - Create audience demographics and retention tracking - Build creator dashboard with Chart.js visualizations - Implement automatic video analytics tracker JavaScript - Add cron script for daily stats aggregation - Document complete analytics system Features: - Channel overview (views, watch time, subscribers, engagement) - Time series charts with multiple metrics - Traffic sources breakdown (search, social, direct, etc.) - Audience demographics (countries, devices, age ranges) - Top videos by performance metric - Audience retention graphs (minute-by-minute) - Real-time viewer statistics - Automatic tracking (no manual instrumentation) - Privacy-compliant (hashed IPs, GDPR-ready) - Production-ready with proper indexing
390 lines
14 KiB
PHP
390 lines
14 KiB
PHP
<?php
|
|
if (!defined('_ISVALID')) define('_ISVALID', true);
|
|
include_once '../f_core/config.core.php';
|
|
|
|
// Require authentication
|
|
session_start();
|
|
if (empty($_SESSION['user_id'])) {
|
|
header('Location: /login.php');
|
|
exit;
|
|
}
|
|
|
|
$usr_id = $_SESSION['user_id'];
|
|
?>
|
|
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
<title>Creator Dashboard - EasyStream</title>
|
|
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.min.js"></script>
|
|
<style>
|
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; background: #f8f9fa; }
|
|
|
|
.header { background: white; border-bottom: 1px solid #e9ecef; padding: 16px 24px; display: flex; justify-content: space-between; align-items: center; }
|
|
.header h1 { font-size: 24px; font-weight: 600; }
|
|
.header nav a { margin-left: 20px; color: #666; text-decoration: none; }
|
|
.header nav a:hover { color: #007bff; }
|
|
|
|
.container { max-width: 1400px; margin: 0 auto; padding: 24px; }
|
|
|
|
.stats-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); gap: 20px; margin-bottom: 30px; }
|
|
.stat-card { background: white; border-radius: 12px; padding: 20px; box-shadow: 0 2px 8px rgba(0,0,0,0.1); }
|
|
.stat-label { color: #666; font-size: 14px; margin-bottom: 8px; }
|
|
.stat-value { font-size: 32px; font-weight: 700; color: #212529; }
|
|
.stat-change { font-size: 14px; margin-top: 8px; }
|
|
.stat-change.positive { color: #28a745; }
|
|
.stat-change.negative { color: #dc3545; }
|
|
|
|
.chart-section { background: white; border-radius: 12px; padding: 24px; box-shadow: 0 2px 8px rgba(0,0,0,0.1); margin-bottom: 30px; }
|
|
.chart-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; }
|
|
.chart-header h2 { font-size: 20px; font-weight: 600; }
|
|
.chart-controls { display: flex; gap: 12px; }
|
|
.chart-controls select, .chart-controls button { padding: 8px 16px; border: 1px solid #ddd; border-radius: 6px; background: white; cursor: pointer; }
|
|
.chart-controls button.active { background: #007bff; color: white; border-color: #007bff; }
|
|
|
|
canvas { max-height: 400px; }
|
|
|
|
.two-col { display: grid; grid-template-columns: repeat(auto-fit, minmax(500px, 1fr)); gap: 30px; margin-bottom: 30px; }
|
|
|
|
.top-videos { background: white; border-radius: 12px; padding: 24px; box-shadow: 0 2px 8px rgba(0,0,0,0.1); }
|
|
.top-videos h2 { font-size: 20px; font-weight: 600; margin-bottom: 20px; }
|
|
.video-item { display: flex; gap: 12px; padding: 12px; border-radius: 8px; margin-bottom: 8px; }
|
|
.video-item:hover { background: #f8f9fa; }
|
|
.video-thumb { width: 120px; height: 68px; border-radius: 8px; overflow: hidden; flex-shrink: 0; }
|
|
.video-thumb img { width: 100%; height: 100%; object-fit: cover; }
|
|
.video-info { flex: 1; }
|
|
.video-title { font-weight: 500; margin-bottom: 4px; }
|
|
.video-stats { font-size: 14px; color: #666; }
|
|
|
|
.loading { text-align: center; padding: 60px 20px; color: #666; }
|
|
|
|
@media (max-width: 768px) {
|
|
.stats-grid { grid-template-columns: 1fr; }
|
|
.two-col { grid-template-columns: 1fr; }
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="header">
|
|
<h1>📊 Creator Dashboard</h1>
|
|
<nav>
|
|
<a href="/upload.php">Upload Video</a>
|
|
<a href="/channel/<?php echo $usr_id; ?>">My Channel</a>
|
|
<a href="/settings.php">Settings</a>
|
|
</nav>
|
|
</div>
|
|
|
|
<div class="container">
|
|
<!-- Key Metrics -->
|
|
<div class="stats-grid" id="statsGrid">
|
|
<div class="stat-card">
|
|
<div class="stat-label">Views (Last 30 Days)</div>
|
|
<div class="stat-value" id="statViews">-</div>
|
|
<div class="stat-change" id="changeViews">Loading...</div>
|
|
</div>
|
|
|
|
<div class="stat-card">
|
|
<div class="stat-label">Watch Time</div>
|
|
<div class="stat-value" id="statWatchTime">-</div>
|
|
<div class="stat-change" id="changeWatchTime">Loading...</div>
|
|
</div>
|
|
|
|
<div class="stat-card">
|
|
<div class="stat-label">Subscribers</div>
|
|
<div class="stat-value" id="statSubscribers">-</div>
|
|
<div class="stat-change" id="changeSubscribers">Loading...</div>
|
|
</div>
|
|
|
|
<div class="stat-card">
|
|
<div class="stat-label">Engagement Rate</div>
|
|
<div class="stat-value" id="statEngagement">-</div>
|
|
<div class="stat-change" id="changeEngagement">Loading...</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Views Chart -->
|
|
<div class="chart-section">
|
|
<div class="chart-header">
|
|
<h2>Views Over Time</h2>
|
|
<div class="chart-controls">
|
|
<button class="period-btn active" data-days="7">7 Days</button>
|
|
<button class="period-btn" data-days="30">30 Days</button>
|
|
<button class="period-btn" data-days="90">90 Days</button>
|
|
<select id="metricSelect">
|
|
<option value="views">Views</option>
|
|
<option value="watch_time_minutes">Watch Time</option>
|
|
<option value="unique_viewers">Unique Viewers</option>
|
|
<option value="likes">Likes</option>
|
|
<option value="comments">Comments</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
<canvas id="chartTimeSeries"></canvas>
|
|
</div>
|
|
|
|
<!-- Two Column Layout -->
|
|
<div class="two-col">
|
|
<!-- Traffic Sources -->
|
|
<div class="chart-section">
|
|
<div class="chart-header">
|
|
<h2>Traffic Sources</h2>
|
|
</div>
|
|
<canvas id="chartTrafficSources"></canvas>
|
|
</div>
|
|
|
|
<!-- Demographics -->
|
|
<div class="chart-section">
|
|
<div class="chart-header">
|
|
<h2>Audience Demographics</h2>
|
|
</div>
|
|
<canvas id="chartDemographics"></canvas>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Top Videos -->
|
|
<div class="top-videos">
|
|
<h2>Top Performing Videos</h2>
|
|
<div id="topVideosList">
|
|
<div class="loading">Loading...</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
let currentDays = 30;
|
|
let currentMetric = 'views';
|
|
let charts = {};
|
|
|
|
// Load dashboard data
|
|
async function loadDashboard() {
|
|
await Promise.all([
|
|
loadOverview(),
|
|
loadTimeSeries(),
|
|
loadTrafficSources(),
|
|
loadDemographics(),
|
|
loadTopVideos()
|
|
]);
|
|
}
|
|
|
|
async function loadOverview() {
|
|
try {
|
|
const res = await fetch(`/api/analytics.php?action=overview&days=${currentDays}`);
|
|
const data = await res.json();
|
|
|
|
if (data.success) {
|
|
const d = data.data;
|
|
|
|
document.getElementById('statViews').textContent = formatNumber(d.total_views || 0);
|
|
document.getElementById('statWatchTime').textContent = formatWatchTime(d.total_watch_time || 0);
|
|
document.getElementById('statSubscribers').textContent = formatNumber(d.subscribers_gained || 0) + ' new';
|
|
|
|
const engagementRate = d.total_views ? ((d.total_likes + d.total_comments) / d.total_views * 100).toFixed(1) : 0;
|
|
document.getElementById('statEngagement').textContent = engagementRate + '%';
|
|
}
|
|
} catch (err) {
|
|
console.error('Failed to load overview:', err);
|
|
}
|
|
}
|
|
|
|
async function loadTimeSeries() {
|
|
try {
|
|
const res = await fetch(`/api/analytics.php?action=timeseries&metric=${currentMetric}&days=${currentDays}`);
|
|
const data = await res.json();
|
|
|
|
if (data.success) {
|
|
renderTimeSeriesChart(data.data);
|
|
}
|
|
} catch (err) {
|
|
console.error('Failed to load time series:', err);
|
|
}
|
|
}
|
|
|
|
async function loadTrafficSources() {
|
|
try {
|
|
const res = await fetch(`/api/analytics.php?action=traffic_sources&days=${currentDays}`);
|
|
const data = await res.json();
|
|
|
|
if (data.success) {
|
|
renderTrafficSourcesChart(data.data);
|
|
}
|
|
} catch (err) {
|
|
console.error('Failed to load traffic sources:', err);
|
|
}
|
|
}
|
|
|
|
async function loadDemographics() {
|
|
try {
|
|
const res = await fetch(`/api/analytics.php?action=demographics&days=${currentDays}`);
|
|
const data = await res.json();
|
|
|
|
if (data.success) {
|
|
renderDemographicsChart(data.data);
|
|
}
|
|
} catch (err) {
|
|
console.error('Failed to load demographics:', err);
|
|
}
|
|
}
|
|
|
|
async function loadTopVideos() {
|
|
try {
|
|
const res = await fetch(`/api/analytics.php?action=top_videos&metric=views&limit=10&days=${currentDays}`);
|
|
const data = await res.json();
|
|
|
|
if (data.success) {
|
|
renderTopVideos(data.data);
|
|
}
|
|
} catch (err) {
|
|
console.error('Failed to load top videos:', err);
|
|
}
|
|
}
|
|
|
|
function renderTimeSeriesChart(data) {
|
|
const ctx = document.getElementById('chartTimeSeries');
|
|
|
|
if (charts.timeSeries) {
|
|
charts.timeSeries.destroy();
|
|
}
|
|
|
|
charts.timeSeries = new Chart(ctx, {
|
|
type: 'line',
|
|
data: {
|
|
labels: data.map(d => d.date),
|
|
datasets: [{
|
|
label: currentMetric.replace('_', ' ').toUpperCase(),
|
|
data: data.map(d => d.value),
|
|
borderColor: '#007bff',
|
|
backgroundColor: 'rgba(0, 123, 255, 0.1)',
|
|
tension: 0.3,
|
|
fill: true
|
|
}]
|
|
},
|
|
options: {
|
|
responsive: true,
|
|
maintainAspectRatio: true,
|
|
plugins: {
|
|
legend: { display: false }
|
|
},
|
|
scales: {
|
|
y: { beginAtZero: true }
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
function renderTrafficSourcesChart(data) {
|
|
const ctx = document.getElementById('chartTrafficSources');
|
|
|
|
if (charts.trafficSources) {
|
|
charts.trafficSources.destroy();
|
|
}
|
|
|
|
// Group by source_type
|
|
const grouped = data.reduce((acc, item) => {
|
|
acc[item.source_type] = (acc[item.source_type] || 0) + parseInt(item.views);
|
|
return acc;
|
|
}, {});
|
|
|
|
charts.trafficSources = new Chart(ctx, {
|
|
type: 'doughnut',
|
|
data: {
|
|
labels: Object.keys(grouped),
|
|
datasets: [{
|
|
data: Object.values(grouped),
|
|
backgroundColor: ['#007bff', '#28a745', '#ffc107', '#dc3545', '#6c757d', '#17a2b8']
|
|
}]
|
|
},
|
|
options: {
|
|
responsive: true,
|
|
maintainAspectRatio: true
|
|
}
|
|
});
|
|
}
|
|
|
|
function renderDemographicsChart(data) {
|
|
const ctx = document.getElementById('chartDemographics');
|
|
|
|
if (charts.demographics) {
|
|
charts.demographics.destroy();
|
|
}
|
|
|
|
charts.demographics = new Chart(ctx, {
|
|
type: 'bar',
|
|
data: {
|
|
labels: data.devices.map(d => d.device_type),
|
|
datasets: [{
|
|
label: 'Views by Device',
|
|
data: data.devices.map(d => d.views),
|
|
backgroundColor: '#007bff'
|
|
}]
|
|
},
|
|
options: {
|
|
responsive: true,
|
|
maintainAspectRatio: true,
|
|
plugins: {
|
|
legend: { display: false }
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
function renderTopVideos(videos) {
|
|
const container = document.getElementById('topVideosList');
|
|
|
|
if (videos.length === 0) {
|
|
container.innerHTML = '<div class="loading">No videos yet</div>';
|
|
return;
|
|
}
|
|
|
|
container.innerHTML = videos.map((v, i) => `
|
|
<div class="video-item">
|
|
<div class="video-thumb">
|
|
<img src="${v.thumbnail}" alt="${v.title}">
|
|
</div>
|
|
<div class="video-info">
|
|
<div class="video-title">${i + 1}. ${v.title}</div>
|
|
<div class="video-stats">${formatNumber(v.metric_value)} views</div>
|
|
</div>
|
|
</div>
|
|
`).join('');
|
|
}
|
|
|
|
// Event listeners
|
|
document.querySelectorAll('.period-btn').forEach(btn => {
|
|
btn.addEventListener('click', function() {
|
|
document.querySelectorAll('.period-btn').forEach(b => b.classList.remove('active'));
|
|
this.classList.add('active');
|
|
currentDays = parseInt(this.dataset.days);
|
|
loadDashboard();
|
|
});
|
|
});
|
|
|
|
document.getElementById('metricSelect').addEventListener('change', function() {
|
|
currentMetric = this.value;
|
|
loadTimeSeries();
|
|
});
|
|
|
|
// Utility functions
|
|
function formatNumber(num) {
|
|
if (num >= 1000000) return (num / 1000000).toFixed(1) + 'M';
|
|
if (num >= 1000) return (num / 1000).toFixed(1) + 'K';
|
|
return num.toString();
|
|
}
|
|
|
|
function formatWatchTime(minutes) {
|
|
if (minutes >= 60) {
|
|
return Math.floor(minutes / 60) + 'h ' + (minutes % 60) + 'm';
|
|
}
|
|
return minutes + 'm';
|
|
}
|
|
|
|
// Initialize
|
|
loadDashboard();
|
|
|
|
// Refresh every 5 minutes
|
|
setInterval(() => loadOverview(), 300000);
|
|
</script>
|
|
</body>
|
|
</html>
|