Spaces:
Running
Running
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Users - Telegram Analytics</title> | |
| <link rel="stylesheet" href="/static/css/style.css"> | |
| <script src="https://cdn.jsdelivr.net/npm/chart.js"></script> | |
| </head> | |
| <body> | |
| <button class="mobile-menu-btn" onclick="toggleMobileMenu()">☰</button> | |
| <div class="sidebar-overlay" onclick="toggleMobileMenu()"></div> | |
| <!-- Sidebar --> | |
| <nav class="sidebar"> | |
| <div class="logo"> | |
| <span class="logo-icon">π</span> | |
| <span class="logo-text">TG Analytics</span> | |
| </div> | |
| <ul class="nav-menu"> | |
| <li class="nav-item"> | |
| <a href="/" class="nav-link"> | |
| <span class="icon">π</span> | |
| <span>Overview</span> | |
| </a> | |
| </li> | |
| <li class="nav-item active"> | |
| <a href="/users" class="nav-link"> | |
| <span class="icon">π₯</span> | |
| <span>Users</span> | |
| </a> | |
| </li> | |
| <li class="nav-item"> | |
| <a href="/chat" class="nav-link"> | |
| <span class="icon">π¬</span> | |
| <span>Chat</span> | |
| </a> | |
| </li> | |
| <li class="nav-item"> | |
| <a href="/search" class="nav-link"> | |
| <span class="icon">π</span> | |
| <span>Search</span> | |
| </a> | |
| </li> | |
| <li class="nav-item"> | |
| <a href="/ai-search" class="nav-link"> | |
| <span class="icon">π€</span> | |
| <span>AI Search</span> | |
| </a> | |
| </li> | |
| <li class="nav-item"> | |
| <a href="/moderation" class="nav-link"> | |
| <span class="icon">π‘οΈ</span> | |
| <span>Moderation</span> | |
| </a> | |
| </li> | |
| <li class="nav-item"> | |
| <a href="/settings" class="nav-link"> | |
| <span class="icon">βοΈ</span> | |
| <span>Settings</span> | |
| </a> | |
| </li> | |
| <li class="nav-item"> | |
| <a href="/maintenance" class="nav-link"> | |
| <span class="icon">π</span> | |
| <span>Maintenance</span> | |
| </a> | |
| </li> | |
| </ul> | |
| <div class="sidebar-footer"> | |
| <div class="export-buttons"> | |
| <button onclick="exportUsers()" class="btn btn-sm">π₯ Export Users</button> | |
| </div> | |
| </div> | |
| </nav> | |
| <!-- Main Content --> | |
| <main class="main-content"> | |
| <!-- Header --> | |
| <header class="header"> | |
| <h1>User Leaderboard</h1> | |
| <div class="header-controls"> | |
| <select id="timeframe" class="select" onchange="loadUsers()"> | |
| <option value="today">Today</option> | |
| <option value="yesterday">Yesterday</option> | |
| <option value="week">This Week</option> | |
| <option value="month" selected>This Month</option> | |
| <option value="year">This Year</option> | |
| <option value="2years">2 Years</option> | |
| <option value="all">All Time</option> | |
| </select> | |
| <button onclick="loadUsers()" class="btn btn-primary">π Refresh</button> | |
| </div> | |
| </header> | |
| <!-- User Stats Summary --> | |
| <section class="stats-grid"> | |
| <div class="stat-card"> | |
| <div class="stat-icon">π₯</div> | |
| <div class="stat-content"> | |
| <div class="stat-value" id="total-users">-</div> | |
| <div class="stat-label">Total Members</div> | |
| </div> | |
| </div> | |
| <div class="stat-card"> | |
| <div class="stat-icon">π¬</div> | |
| <div class="stat-content"> | |
| <div class="stat-value" id="total-active">-</div> | |
| <div class="stat-label">Active Users</div> | |
| </div> | |
| </div> | |
| <div class="stat-card"> | |
| <div class="stat-icon">π</div> | |
| <div class="stat-content"> | |
| <div class="stat-value" id="top-user">-</div> | |
| <div class="stat-label">Top User</div> | |
| </div> | |
| </div> | |
| <div class="stat-card"> | |
| <div class="stat-icon">π</div> | |
| <div class="stat-content"> | |
| <div class="stat-value" id="avg-messages">-</div> | |
| <div class="stat-label">Avg Messages/User</div> | |
| </div> | |
| </div> | |
| </section> | |
| <!-- Users Table --> | |
| <section class="chart-card full-width"> | |
| <div class="chart-header"> | |
| <h3>All Users</h3> | |
| <div style="display: flex; gap: 1rem; align-items: center;"> | |
| <input type="search" id="user-search" placeholder="Search users..." | |
| style="width: 200px;" onkeyup="filterUsers()"> | |
| <span id="showing-count" style="color: var(--text-muted); font-size: 0.875rem;"></span> | |
| </div> | |
| </div> | |
| <div style="overflow-x: auto;"> | |
| <table class="users-table"> | |
| <thead> | |
| <tr> | |
| <th style="width: 60px;">Rank</th> | |
| <th>User</th> | |
| <th style="width: 80px;">Role</th> | |
| <th style="width: 120px;">Messages</th> | |
| <th style="width: 100px;">Share</th> | |
| <th style="width: 100px;">Links</th> | |
| <th style="width: 100px;">Media</th> | |
| <th style="width: 100px;">Active Days</th> | |
| <th style="width: 100px;">Daily Avg</th> | |
| </tr> | |
| </thead> | |
| <tbody id="users-table-body"> | |
| <tr> | |
| <td colspan="8" class="loading"> | |
| <div class="spinner"></div> | |
| </td> | |
| </tr> | |
| </tbody> | |
| </table> | |
| </div> | |
| <!-- Pagination --> | |
| <div class="pagination" id="pagination"></div> | |
| </section> | |
| </main> | |
| <script src="/static/js/dashboard.js"></script> | |
| <script> | |
| // State | |
| let allUsers = []; | |
| let currentPage = 1; | |
| const pageSize = 20; | |
| // Initialize | |
| document.addEventListener('DOMContentLoaded', () => { | |
| loadUsers(); | |
| }); | |
| async function loadUsers() { | |
| const timeframe = document.getElementById('timeframe').value; | |
| const tbody = document.getElementById('users-table-body'); | |
| tbody.innerHTML = '<tr><td colspan="9" class="loading"><div class="spinner"></div></td></tr>'; | |
| try { | |
| const response = await fetch(`/api/users?timeframe=${timeframe}&limit=500&include_inactive=1`); | |
| const data = await response.json(); | |
| allUsers = data.users; | |
| // Update summary stats | |
| document.getElementById('total-users').textContent = formatNumber(data.total); | |
| document.getElementById('total-active').textContent = formatNumber(data.total_active); | |
| if (allUsers.length > 0) { | |
| const activeUsers = allUsers.filter(u => u.messages > 0); | |
| if (activeUsers.length > 0) { | |
| document.getElementById('top-user').textContent = activeUsers[0].name; | |
| const totalMessages = activeUsers.reduce((sum, u) => sum + u.messages, 0); | |
| document.getElementById('avg-messages').textContent = | |
| formatNumber(Math.round(totalMessages / activeUsers.length)); | |
| } | |
| } | |
| currentPage = 1; | |
| renderUsers(); | |
| } catch (error) { | |
| tbody.innerHTML = '<tr><td colspan="9" class="empty-state">Error loading users</td></tr>'; | |
| } | |
| } | |
| function filterUsers() { | |
| currentPage = 1; | |
| renderUsers(); | |
| } | |
| function renderUsers() { | |
| const search = document.getElementById('user-search').value.toLowerCase(); | |
| const filtered = allUsers.filter(u => | |
| u.name.toLowerCase().includes(search) || | |
| u.user_id.toLowerCase().includes(search) | |
| ); | |
| const start = (currentPage - 1) * pageSize; | |
| const end = start + pageSize; | |
| const pageUsers = filtered.slice(start, end); | |
| document.getElementById('showing-count').textContent = | |
| `Showing ${start + 1}-${Math.min(end, filtered.length)} of ${filtered.length}`; | |
| const tbody = document.getElementById('users-table-body'); | |
| if (pageUsers.length === 0) { | |
| tbody.innerHTML = '<tr><td colspan="9" class="empty-state">No users found</td></tr>'; | |
| return; | |
| } | |
| tbody.innerHTML = pageUsers.map((user, i) => { | |
| const rank = user.rank || '-'; | |
| const rankClass = rank === 1 ? 'gold' : rank === 2 ? 'silver' : rank === 3 ? 'bronze' : ''; | |
| const initial = user.name.charAt(0).toUpperCase(); | |
| const isInactive = user.messages === 0; | |
| const rowStyle = isInactive ? 'opacity: 0.6;' : ''; | |
| let roleBadge = ''; | |
| if (user.role === 'creator') roleBadge = '<span style="background:#ffd700;color:#1a1a2e;padding:2px 6px;border-radius:4px;font-size:0.7rem;font-weight:600;">Creator</span>'; | |
| else if (user.role === 'admin') roleBadge = '<span style="background:#28a745;color:white;padding:2px 6px;border-radius:4px;font-size:0.7rem;font-weight:600;">Admin</span>'; | |
| else if (user.role === 'bot') roleBadge = '<span style="background:#6c757d;color:white;padding:2px 6px;border-radius:4px;font-size:0.7rem;font-weight:600;">Bot</span>'; | |
| const subtitle = user.username | |
| ? `@${escapeHtml(user.username)}` | |
| : `ID: ${user.user_id}`; | |
| return ` | |
| <tr onclick="window.location.href='/user/${user.user_id}'" style="cursor: pointer; ${rowStyle}"> | |
| <td><span class="list-rank ${rankClass}">${rank !== '-' ? '#' + rank : '-'}</span></td> | |
| <td> | |
| <div class="user-cell"> | |
| <div class="user-avatar">${initial}</div> | |
| <div> | |
| <div class="list-name">${escapeHtml(user.name)}</div> | |
| <div class="list-subtitle">${subtitle}</div> | |
| </div> | |
| </div> | |
| </td> | |
| <td>${roleBadge}</td> | |
| <td> | |
| ${isInactive ? '<span style="color: var(--text-muted);">-</span>' : ` | |
| <strong>${formatNumber(user.messages)}</strong> | |
| <div class="progress-bar"> | |
| <div class="progress-fill" style="width: ${user.percentage}%"></div> | |
| </div>`} | |
| </td> | |
| <td>${isInactive ? '-' : user.percentage + '%'}</td> | |
| <td>${isInactive ? '-' : formatNumber(user.links)}</td> | |
| <td>${isInactive ? '-' : formatNumber(user.media)}</td> | |
| <td>${isInactive ? '-' : user.active_days}</td> | |
| <td>${isInactive ? '-' : user.daily_average}</td> | |
| </tr> | |
| `; | |
| }).join(''); | |
| // Render pagination | |
| const totalPages = Math.ceil(filtered.length / pageSize); | |
| renderPagination(totalPages); | |
| } | |
| function renderPagination(totalPages) { | |
| const pagination = document.getElementById('pagination'); | |
| if (totalPages <= 1) { | |
| pagination.innerHTML = ''; | |
| return; | |
| } | |
| let html = ''; | |
| // Previous button | |
| html += `<button class="page-btn" onclick="goToPage(${currentPage - 1})" | |
| ${currentPage === 1 ? 'disabled' : ''}>«</button>`; | |
| // Page numbers | |
| const maxVisible = 5; | |
| let startPage = Math.max(1, currentPage - Math.floor(maxVisible / 2)); | |
| let endPage = Math.min(totalPages, startPage + maxVisible - 1); | |
| if (endPage - startPage < maxVisible - 1) { | |
| startPage = Math.max(1, endPage - maxVisible + 1); | |
| } | |
| if (startPage > 1) { | |
| html += `<button class="page-btn" onclick="goToPage(1)">1</button>`; | |
| if (startPage > 2) html += `<span style="padding: 0 0.5rem;">...</span>`; | |
| } | |
| for (let i = startPage; i <= endPage; i++) { | |
| html += `<button class="page-btn ${i === currentPage ? 'active' : ''}" | |
| onclick="goToPage(${i})">${i}</button>`; | |
| } | |
| if (endPage < totalPages) { | |
| if (endPage < totalPages - 1) html += `<span style="padding: 0 0.5rem;">...</span>`; | |
| html += `<button class="page-btn" onclick="goToPage(${totalPages})">${totalPages}</button>`; | |
| } | |
| // Next button | |
| html += `<button class="page-btn" onclick="goToPage(${currentPage + 1})" | |
| ${currentPage === totalPages ? 'disabled' : ''}>»</button>`; | |
| pagination.innerHTML = html; | |
| } | |
| function goToPage(page) { | |
| currentPage = page; | |
| renderUsers(); | |
| window.scrollTo({ top: 0, behavior: 'smooth' }); | |
| } | |
| function openUserProfile(userId) { | |
| window.location.href = `/user/${userId}`; | |
| } | |
| // Export function | |
| function exportUsers() { | |
| const timeframe = document.getElementById('timeframe').value; | |
| window.location.href = `/api/export/users?timeframe=${timeframe}`; | |
| } | |
| // Helper 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 escapeHtml(text) { | |
| const div = document.createElement('div'); | |
| div.textContent = text; | |
| return div.innerHTML; | |
| } | |
| </script> | |
| </body> | |
| </html> | |