/** * Telegram Analytics Dashboard - JavaScript * * Handles all interactivity: * - Data fetching from API * - Chart rendering with Chart.js * - Real-time updates * - User interactions * - Export functionality */ // ========================================== // MOBILE MENU // ========================================== function toggleMobileMenu() { const sidebar = document.querySelector('.sidebar'); const overlay = document.querySelector('.sidebar-overlay'); sidebar.classList.toggle('open'); if (overlay) overlay.classList.toggle('active'); } // Close mobile menu when clicking a nav link document.addEventListener('DOMContentLoaded', function() { document.querySelectorAll('.nav-link').forEach(function(link) { link.addEventListener('click', function() { if (window.innerWidth <= 768) { const sidebar = document.querySelector('.sidebar'); const overlay = document.querySelector('.sidebar-overlay'); sidebar.classList.remove('open'); if (overlay) overlay.classList.remove('active'); } }); }); }); // ========================================== // GLOBAL STATE // ========================================== const state = { timeframe: 'month', charts: {}, autoRefresh: null, currentPage: 1, usersPerPage: 20 }; // Chart.js default configuration Chart.defaults.color = '#a0aec0'; Chart.defaults.borderColor = '#2d3748'; Chart.defaults.font.family = '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif'; // ========================================== // 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.toLocaleString(); } function formatDate(timestamp) { if (!timestamp) return '-'; return new Date(timestamp * 1000).toLocaleDateString('en-US', { year: 'numeric', month: 'short', day: 'numeric' }); } function getTimeframe() { const select = document.getElementById('timeframe'); return select ? select.value : state.timeframe; } async function fetchAPI(endpoint) { try { const timeframe = getTimeframe(); const separator = endpoint.includes('?') ? '&' : '?'; const response = await fetch(`${endpoint}${separator}timeframe=${timeframe}`); return await response.json(); } catch (error) { console.error('API Error:', error); return null; } } function showLoading(elementId) { const element = document.getElementById(elementId); if (element) { element.innerHTML = '
'; } } function showEmpty(elementId, message = 'No data available') { const element = document.getElementById(elementId); if (element) { element.innerHTML = `
📭

${message}

`; } } // ========================================== // DATA LOADING // ========================================== async function loadAllData() { state.timeframe = getTimeframe(); // Load all data in parallel await Promise.all([ loadOverviewStats(), loadMessagesChart(), loadUsersChart(), loadHourlyChart(), loadDailyChart(), loadHeatmap(), loadTopUsers(), loadTopWords(), loadTopDomains() ]); } async function loadOverviewStats() { const data = await fetchAPI('/api/overview'); if (!data) return; // Update stat cards document.getElementById('total-messages').textContent = formatNumber(data.total_messages); document.getElementById('active-users').textContent = formatNumber(data.active_users); document.getElementById('messages-per-day').textContent = formatNumber(data.messages_per_day); document.getElementById('links-count').textContent = formatNumber(data.links_count); document.getElementById('media-count').textContent = formatNumber(data.media_count); document.getElementById('replies-count').textContent = formatNumber(data.replies_count); } // ========================================== // CHARTS // ========================================== async function loadMessagesChart() { const granularitySelect = document.getElementById('messages-granularity'); const granularity = granularitySelect ? granularitySelect.value : 'day'; const data = await fetchAPI(`/api/chart/messages?granularity=${granularity}`); if (!data || data.length === 0) return; const ctx = document.getElementById('messages-chart'); if (!ctx) return; // Destroy existing chart if (state.charts.messages) { state.charts.messages.destroy(); } state.charts.messages = new Chart(ctx, { type: 'line', data: { labels: data.map(d => d.label), datasets: [{ label: 'Messages', data: data.map(d => d.value), borderColor: '#0088cc', backgroundColor: 'rgba(0, 136, 204, 0.1)', fill: true, tension: 0.4, pointRadius: 2, pointHoverRadius: 5 }] }, options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { display: false } }, scales: { x: { grid: { display: false }, ticks: { maxTicksLimit: 10 } }, y: { beginAtZero: true, grid: { color: '#2d3748' } } }, interaction: { intersect: false, mode: 'index' } } }); } async function loadUsersChart() { const data = await fetchAPI('/api/chart/users?granularity=day'); if (!data || data.length === 0) return; const ctx = document.getElementById('users-chart'); if (!ctx) return; if (state.charts.users) { state.charts.users.destroy(); } state.charts.users = new Chart(ctx, { type: 'line', data: { labels: data.map(d => d.label), datasets: [{ label: 'Active Users', data: data.map(d => d.value), borderColor: '#28a745', backgroundColor: 'rgba(40, 167, 69, 0.1)', fill: true, tension: 0.4, pointRadius: 2, pointHoverRadius: 5 }] }, options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { display: false } }, scales: { x: { grid: { display: false }, ticks: { maxTicksLimit: 10 } }, y: { beginAtZero: true, grid: { color: '#2d3748' } } } } }); } async function loadHourlyChart() { const data = await fetchAPI('/api/chart/hourly'); if (!data || data.length === 0) return; const ctx = document.getElementById('hourly-chart'); if (!ctx) return; if (state.charts.hourly) { state.charts.hourly.destroy(); } state.charts.hourly = new Chart(ctx, { type: 'bar', data: { labels: data.map(d => d.label), datasets: [{ label: 'Messages', data: data.map(d => d.value), backgroundColor: '#0088cc', borderRadius: 4 }] }, options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { display: false } }, scales: { x: { grid: { display: false }, ticks: { maxTicksLimit: 12 } }, y: { beginAtZero: true, grid: { color: '#2d3748' } } } } }); } async function loadDailyChart() { const data = await fetchAPI('/api/chart/daily'); if (!data || data.length === 0) return; const ctx = document.getElementById('daily-chart'); if (!ctx) return; if (state.charts.daily) { state.charts.daily.destroy(); } const colors = [ '#dc3545', // Sunday - red '#ffc107', // Monday - yellow '#28a745', // Tuesday - green '#17a2b8', // Wednesday - cyan '#0088cc', // Thursday - blue '#6f42c1', // Friday - purple '#fd7e14' // Saturday - orange ]; state.charts.daily = new Chart(ctx, { type: 'bar', data: { labels: data.map(d => d.label.substring(0, 3)), datasets: [{ label: 'Messages', data: data.map(d => d.value), backgroundColor: colors, borderRadius: 4 }] }, options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { display: false } }, scales: { x: { grid: { display: false } }, y: { beginAtZero: true, grid: { color: '#2d3748' } } } } }); } async function loadHeatmap() { const data = await fetchAPI('/api/chart/heatmap'); if (!data || !data.data) return; const container = document.getElementById('heatmap'); if (!container) return; // Find max value for color scaling const maxValue = Math.max(...data.data.flat()); // Generate color based on intensity function getColor(value) { if (value === 0) return 'rgba(0, 136, 204, 0.1)'; const intensity = value / maxValue; return `rgba(0, 136, 204, ${0.2 + intensity * 0.8})`; } let html = ''; // Hour headers for (let h = 0; h < 24; h++) { html += ``; } html += ''; // Day rows data.days.forEach((day, dayIndex) => { html += ``; for (let h = 0; h < 24; h++) { const value = data.data[dayIndex][h]; const color = getColor(value); html += ``; } html += ''; }); html += '
${h}
${day.substring(0, 3)}
'; container.innerHTML = html; } // ========================================== // TOP LISTS // ========================================== async function loadTopUsers() { const listElement = document.getElementById('top-users-list'); if (!listElement) return; showLoading('top-users-list'); const data = await fetchAPI('/api/users?limit=10'); if (!data || !data.users || data.users.length === 0) { showEmpty('top-users-list'); return; } let html = ''; data.users.forEach((user, index) => { const rankClass = index === 0 ? 'gold' : index === 1 ? 'silver' : index === 2 ? 'bronze' : ''; const initial = user.name.charAt(0).toUpperCase(); html += `
#${user.rank}
${initial}
${escapeHtml(user.name)}
${user.percentage}% of total
${formatNumber(user.messages)}
`; }); listElement.innerHTML = html; } async function loadTopWords() { const listElement = document.getElementById('top-words-list'); if (!listElement) return; showLoading('top-words-list'); const data = await fetchAPI('/api/top/words?limit=10'); if (!data || data.length === 0) { showEmpty('top-words-list'); return; } const maxCount = data[0].count; let html = ''; data.forEach((item, index) => { const percentage = (item.count / maxCount * 100).toFixed(0); html += `
#${index + 1}
${escapeHtml(item.word)}
${formatNumber(item.count)}
`; }); listElement.innerHTML = html; } async function loadTopDomains() { const listElement = document.getElementById('top-domains-list'); if (!listElement) return; showLoading('top-domains-list'); const data = await fetchAPI('/api/top/domains?limit=10'); if (!data || data.length === 0) { showEmpty('top-domains-list'); return; } const maxCount = data[0].count; let html = ''; data.forEach((item, index) => { const percentage = (item.count / maxCount * 100).toFixed(0); html += `
#${index + 1}
${escapeHtml(item.domain)}
${formatNumber(item.count)}
`; }); listElement.innerHTML = html; } // ========================================== // USER MODAL // ========================================== async function openUserModal(userId) { // Create modal if it doesn't exist let modal = document.getElementById('user-modal'); if (!modal) { modal = document.createElement('div'); modal.id = 'user-modal'; modal.className = 'modal-overlay'; modal.innerHTML = ` `; document.body.appendChild(modal); // Close on backdrop click modal.addEventListener('click', (e) => { if (e.target === modal) closeUserModal(); }); } modal.classList.add('active'); document.getElementById('user-modal-content').innerHTML = '
'; const data = await fetchAPI(`/api/user/${userId}`); if (!data || data.error) { document.getElementById('user-modal-content').innerHTML = '

User not found

'; return; } const initial = data.name.charAt(0).toUpperCase(); document.getElementById('user-modal-content').innerHTML = `
${initial}

${escapeHtml(data.name)}

Rank #${data.rank} • Member since ${formatDate(data.first_seen)}

${formatNumber(data.messages)}
Messages
${formatNumber(data.characters)}
Characters
${data.daily_average}
Daily Avg
${formatNumber(data.links)}
Links
${formatNumber(data.media)}
Media
${data.active_days}
Active Days

Activity by Hour

`; // Render user's hourly chart const ctx = document.getElementById('user-hourly-chart'); new Chart(ctx, { type: 'bar', data: { labels: Array.from({length: 24}, (_, i) => `${i}:00`), datasets: [{ data: data.hourly_activity, backgroundColor: '#0088cc', borderRadius: 2 }] }, options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { display: false } }, scales: { x: { grid: { display: false }, ticks: { maxTicksLimit: 12 } }, y: { beginAtZero: true, grid: { color: '#2d3748' } } } } }); } function closeUserModal() { const modal = document.getElementById('user-modal'); if (modal) modal.classList.remove('active'); } // ========================================== // EXPORT FUNCTIONS // ========================================== function exportUsers() { const timeframe = getTimeframe(); window.location.href = `/api/export/users?timeframe=${timeframe}`; } function exportMessages() { const timeframe = getTimeframe(); window.location.href = `/api/export/messages?timeframe=${timeframe}`; } // ========================================== // AUTO REFRESH // ========================================== function toggleAutoRefresh() { if (state.autoRefresh) { clearInterval(state.autoRefresh); state.autoRefresh = null; console.log('Auto-refresh disabled'); } else { state.autoRefresh = setInterval(loadAllData, 60000); // Refresh every minute console.log('Auto-refresh enabled (60s)'); } } // ========================================== // UTILITY // ========================================== function escapeHtml(text) { const div = document.createElement('div'); div.textContent = text; return div.innerHTML; } // Keyboard shortcuts document.addEventListener('keydown', (e) => { // Escape to close modal if (e.key === 'Escape') { closeUserModal(); } // R to refresh if (e.key === 'r' && !e.ctrlKey && !e.metaKey) { const activeElement = document.activeElement; if (activeElement.tagName !== 'INPUT' && activeElement.tagName !== 'TEXTAREA') { loadAllData(); } } });