rottg's picture
Update code
999a034 verified
<!DOCTYPE html>
<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()">&#9776;</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' : ''}>&laquo;</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' : ''}>&raquo;</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>