142 lines
6.0 KiB
HTML
142 lines
6.0 KiB
HTML
{{define "content"}}
|
||
<h2>Пользователи чата</h2>
|
||
<div style="overflow-x: auto;">
|
||
<table id="users-table">
|
||
<thead>
|
||
<tr>
|
||
<th>Пользователь</th>
|
||
<th>Сообщений</th>
|
||
<th>Последняя активность</th>
|
||
<th>Модератор</th>
|
||
<th>VIP</th>
|
||
<th>Подписчик</th>
|
||
<th>Действия</th>
|
||
<th>Отмечать</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody></tbody>
|
||
</table>
|
||
</div>
|
||
|
||
<script>
|
||
let users = [];
|
||
let refreshInterval = null;
|
||
|
||
async function loadUsers() {
|
||
try {
|
||
const res = await fetch('/api/users');
|
||
users = await res.json();
|
||
renderTable();
|
||
} catch(err) {
|
||
console.error('Failed to load users', err);
|
||
}
|
||
}
|
||
|
||
function renderTable() {
|
||
const tbody = document.querySelector('#users-table tbody');
|
||
if (!users.length) {
|
||
tbody.innerHTML = '<tr><td colspan="8">Нет активных пользователей</td></tr>';
|
||
return;
|
||
}
|
||
tbody.innerHTML = users.map(user => `
|
||
<tr data-username="${escapeHtml(user.username)}">
|
||
<td>${escapeHtml(user.username)}</td>
|
||
<td>${user.message_count}</td>
|
||
<td>${formatRelativeTime(user.last_active)}</td>
|
||
<td><input type="checkbox" class="mod-checkbox" ${user.is_mod ? 'checked' : ''} data-username="${escapeHtml(user.username)}"></td>
|
||
<td><input type="checkbox" class="vip-checkbox" ${user.is_vip ? 'checked' : ''} data-username="${escapeHtml(user.username)}"></td>
|
||
<td><input type="checkbox" disabled ${user.is_subscriber ? 'checked' : ''}></td>
|
||
<td>
|
||
<button class="warn-btn" data-user="${escapeHtml(user.username)}">Предупредить</button>
|
||
<button class="timeout10s-btn" data-user="${escapeHtml(user.username)}">10 сек</button>
|
||
<button class="timeout10m-btn" data-user="${escapeHtml(user.username)}">10 мин</button>
|
||
<button class="ban-btn" data-user="${escapeHtml(user.username)}">Забанить</button>
|
||
<button class="unban-btn" data-user="${escapeHtml(user.username)}">Разбанить</button>
|
||
</td>
|
||
<td><input type="checkbox" class="mark-checkbox" ${user.is_marked ? 'checked' : ''} data-username="${escapeHtml(user.username)}"></td>
|
||
</tr>
|
||
`).join('');
|
||
|
||
document.querySelectorAll('.mod-checkbox').forEach(cb => {
|
||
cb.addEventListener('change', (e) => toggleMod(cb.dataset.username, cb.checked));
|
||
});
|
||
document.querySelectorAll('.vip-checkbox').forEach(cb => {
|
||
cb.addEventListener('change', (e) => toggleVip(cb.dataset.username, cb.checked));
|
||
});
|
||
document.querySelectorAll('.warn-btn').forEach(btn => {
|
||
btn.addEventListener('click', () => userAction(btn.dataset.user, 'warn'));
|
||
});
|
||
document.querySelectorAll('.timeout10s-btn').forEach(btn => {
|
||
btn.addEventListener('click', () => userAction(btn.dataset.user, 'timeout10sec'));
|
||
});
|
||
document.querySelectorAll('.timeout10m-btn').forEach(btn => {
|
||
btn.addEventListener('click', () => userAction(btn.dataset.user, 'timeout10min'));
|
||
});
|
||
document.querySelectorAll('.ban-btn').forEach(btn => {
|
||
btn.addEventListener('click', () => userAction(btn.dataset.user, 'ban'));
|
||
});
|
||
document.querySelectorAll('.unban-btn').forEach(btn => {
|
||
btn.addEventListener('click', () => userAction(btn.dataset.user, 'unban'));
|
||
});
|
||
document.querySelectorAll('.mark-checkbox').forEach(cb => {
|
||
cb.addEventListener('change', (e) => toggleMark(cb.dataset.username, cb.checked));
|
||
});
|
||
}
|
||
|
||
async function toggleMod(username, isMod) {
|
||
const action = isMod ? 'set_mod' : 'unset_mod';
|
||
await userAction(username, action);
|
||
}
|
||
async function toggleVip(username, isVip) {
|
||
const action = isVip ? 'set_vip' : 'unset_vip';
|
||
await userAction(username, action);
|
||
}
|
||
async function toggleMark(username, isMarked) {
|
||
await userAction(username, 'toggle_mark');
|
||
}
|
||
async function userAction(username, action) {
|
||
try {
|
||
const res = await fetch('/api/users/action', {
|
||
method: 'POST',
|
||
headers: {'Content-Type': 'application/json'},
|
||
body: JSON.stringify({ username, action, platform: 'twitch' })
|
||
});
|
||
if (!res.ok) throw new Error(await res.text());
|
||
await loadUsers();
|
||
} catch(err) {
|
||
alert('Ошибка: ' + err.message);
|
||
}
|
||
}
|
||
|
||
function escapeHtml(str) {
|
||
if (!str) return '';
|
||
return str.replace(/[&<>]/g, function(m) {
|
||
if (m === '&') return '&';
|
||
if (m === '<') return '<';
|
||
if (m === '>') return '>';
|
||
return m;
|
||
});
|
||
}
|
||
|
||
function formatRelativeTime(timestamp) {
|
||
if (!timestamp) return 'никогда';
|
||
const date = new Date(timestamp);
|
||
const now = new Date();
|
||
const seconds = Math.floor((now - date) / 1000);
|
||
if (seconds < 60) return 'только что';
|
||
const minutes = Math.floor(seconds / 60);
|
||
if (minutes < 60) return `${minutes} мин. назад`;
|
||
const hours = Math.floor(minutes / 60);
|
||
if (hours < 24) return `${hours} ч. назад`;
|
||
const days = Math.floor(hours / 24);
|
||
if (days < 30) return `${days} дн. назад`;
|
||
const months = Math.floor(days / 30);
|
||
if (months < 12) return `${months} мес. назад`;
|
||
const years = Math.floor(months / 12);
|
||
return `${years} г. назад`;
|
||
}
|
||
|
||
loadUsers();
|
||
refreshInterval = setInterval(loadUsers, 3000);
|
||
</script>
|
||
{{end}} |