winx_prinx-api / client /src /components /ModeratorTab.jsx
Sasha
refactor: modularize client App.jsx tab components and update Netlify CSP
fdc7871
Raw
History Blame
14.6 kB
import React from 'react';
import { Lock, LogIn, ShieldAlert, UserCheck } from 'lucide-react';
import { Radar } from 'react-chartjs-2';
export default function ModeratorTab({
twitchConfigured,
auth,
handleLogin,
TWITCH_CHANNEL,
modProfiles,
selectedMod,
setSelectedMod
}) {
return (
<div className="tab-content">
{twitchConfigured && !auth.loggedIn ? (
<div className="login-overlay">
<div className="login-card">
<Lock size={48} style={{ color: 'var(--color-brand)', marginBottom: '1.5rem' }} />
<h2 className="login-title">Доступ Ограничен</h2>
<p className="login-description">
Лог действий модераторов и статистика банов/удалений доступны только стримеру и официальным модераторам канала **winx_prinx**.
Пожалуйста, авторизуйтесь через Twitch для проверки прав.
</p>
<button className="btn" onClick={handleLogin}>
<LogIn size={16} /> Войти через Twitch
</button>
</div>
</div>
) : twitchConfigured && (auth.user?.role !== 'streamer' && auth.user?.role !== 'moderator' && auth.user?.role !== 'admin') ? (
<div className="status-msg">
<ShieldAlert size={48} className="status-msg-icon" />
<h2>Недостаточно прав</h2>
<p>Вы успешно вошли как <strong>{auth.user?.displayName}</strong>, но вы не являетесь модератором канала {TWITCH_CHANNEL}.</p>
</div>
) : (
<div>
{modProfiles.length <= 2 && (
<div style={{ background: 'rgba(145, 70, 255, 0.1)', border: '1px solid rgba(145, 70, 255, 0.3)', borderRadius: '8px', padding: '1rem', marginBottom: '1.5rem', display: 'flex', gap: '1rem', alignItems: 'center' }}>
<ShieldAlert size={24} style={{ color: 'var(--color-brand)' }} />
<div>
<h4 style={{ margin: '0 0 0.25rem 0', color: 'var(--color-text-main)', fontSize: '0.95rem' }}>Данные накапливаются</h4>
<p style={{ margin: 0, fontSize: '0.85rem', color: 'var(--color-text-muted)' }}>Информация о модераторах появляется по мере их активности в выбранном стриме.</p>
</div>
</div>
)}
{/* TOP 3 PODIUM */}
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(240px, 1fr))', gap: '1.5rem', marginBottom: '2rem' }}>
{modProfiles.slice(0, 3).map((mod, idx) => {
const trophyColor = idx === 0 ? '#FFD700' : idx === 1 ? '#C0C0C0' : '#CD7F32';
const glowStyle = {
border: `1px solid ${trophyColor}40`,
boxShadow: `0 0 15px ${trophyColor}12`,
position: 'relative',
overflow: 'hidden'
};
return (
<div key={idx} className="panel" style={glowStyle}>
<div style={{ position: 'absolute', top: '-10px', right: '-10px', fontSize: '5rem', opacity: 0.08, color: trophyColor, fontWeight: 900 }}>
#{idx + 1}
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: '1rem' }}>
<div style={{
width: '44px',
height: '44px',
borderRadius: '50%',
background: 'var(--color-bg-base)',
border: `2px solid ${trophyColor}`,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
fontWeight: 700,
fontSize: '1.1rem',
color: trophyColor
}}>
{idx === 0 ? '👑' : idx === 1 ? '🥈' : '🥉'}
</div>
<div>
<h4 style={{ margin: 0, fontSize: '1rem', fontWeight: 700 }}>{mod.moderator}</h4>
<span style={{ fontSize: '0.7rem', color: 'var(--color-text-muted)', textTransform: 'uppercase', letterSpacing: '0.05em' }}>
{idx === 0 ? 'Глава патруля' : idx === 1 ? 'Старший мод' : 'Защитник чата'}
</span>
</div>
</div>
<div style={{ display: 'flex', justifyContent: 'space-between', marginTop: '1.25rem', fontSize: '0.8rem' }}>
<div>
<div style={{ color: 'var(--color-text-muted)' }}>Действий</div>
<div style={{ fontSize: '1.1rem', fontWeight: 700, color: 'var(--color-text-accent)' }}>{mod.total_actions}</div>
</div>
<div>
<div style={{ color: 'var(--color-text-muted)' }}>Ср. Реакция</div>
<div style={{ fontSize: '1.1rem', fontWeight: 700, color: '#00F5D4' }}>
{mod.reaction_time_avg ? `${mod.reaction_time_avg}с` : '—'}
</div>
</div>
<div>
<div style={{ color: 'var(--color-text-muted)' }}>КПД Активности</div>
<div style={{ fontSize: '1.1rem', fontWeight: 700, color: '#A370F7' }}>{mod.scores.activity}%</div>
</div>
</div>
</div>
);
})}
</div>
<div className="grid-2col" style={{ gridTemplateColumns: 'repeat(auto-fit, minmax(320px, 1fr))', gap: '1.5rem', alignItems: 'start' }}>
{/* Left: Mod List */}
<div className="panel">
<div className="panel-header">
<h3 className="panel-title"><ShieldAlert size={20} /> Рейтинг модераторов</h3>
</div>
{modProfiles.length === 0 ? (
<div className="status-msg">
<ShieldAlert size={40} className="status-msg-icon" />
<p>Модераторы еще не совершали действий в этом периоде</p>
</div>
) : (
<div className="table-wrapper">
<table className="data-table">
<thead>
<tr>
<th>Ранг</th>
<th>Никнейм</th>
<th>Действий</th>
<th>Ср. Реакция</th>
</tr>
</thead>
<tbody>
{modProfiles.map((mod, idx) => (
<tr
key={idx}
onClick={() => setSelectedMod(mod)}
style={{
cursor: 'pointer',
background: selectedMod && selectedMod.moderator === mod.moderator ? 'rgba(163, 112, 247, 0.08)' : 'transparent',
borderLeft: selectedMod && selectedMod.moderator === mod.moderator ? '3px solid var(--color-brand)' : 'none'
}}
>
<td>#{idx + 1}</td>
<td style={{ fontWeight: 600 }}>{mod.moderator}</td>
<td>{mod.total_actions}</td>
<td style={{ color: '#00F5D4', fontWeight: 600 }}>{mod.reaction_time_avg ? `${mod.reaction_time_avg}с` : '—'}</td>
</tr>
))}
</tbody>
</table>
</div>
)}
</div>
{/* Right: Mod Dossier */}
<div className="panel">
<div className="panel-header">
<h3 className="panel-title"><UserCheck size={20} /> Личное досье модератора</h3>
</div>
{!selectedMod ? (
<div className="status-msg">
<UserCheck size={40} className="status-msg-icon" />
<p>Выберите модератора слева для просмотра досье</p>
</div>
) : (
<div>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '1.25rem' }}>
<div>
<h3 style={{ margin: 0, fontSize: '1.25rem', fontWeight: 700, color: 'var(--color-brand)' }}>{selectedMod.moderator}</h3>
<p style={{ margin: 0, fontSize: '0.75rem', color: 'var(--color-text-muted)' }}>Анализ стиля модерирования и характеристик</p>
</div>
</div>
{/* Radar chart */}
<div style={{ height: '280px', display: 'flex', justifyContent: 'center', marginBottom: '1.25rem' }}>
<Radar
data={{
labels: ['Скорость', 'Активность', 'Жесткость', 'Внимательность'],
datasets: [{
data: [
selectedMod.scores.speed,
selectedMod.scores.activity,
selectedMod.scores.harshness,
selectedMod.scores.watchfulness
],
backgroundColor: 'rgba(163, 112, 247, 0.2)',
borderColor: '#A370F7',
borderWidth: 2,
pointBackgroundColor: '#A370F7',
pointBorderColor: '#FFF',
pointHoverBackgroundColor: '#FFF',
pointHoverBorderColor: '#A370F7'
}]
}}
options={{
plugins: { legend: { display: false } },
scales: {
r: {
angleLines: { color: '#2F2F35' },
grid: { color: '#2F2F35' },
pointLabels: { color: '#ADADB8', font: { size: 10, weight: 'bold' } },
ticks: { display: false },
min: 0,
max: 100
}
}
}}
/>
</div>
{/* Actions distribution */}
<div style={{ marginBottom: '1rem' }}>
<h4 style={{ margin: '0 0 0.5rem 0', fontSize: '0.85rem', fontWeight: 600 }}>Распределение наказаний:</h4>
<div style={{ height: '16px', display: 'flex', borderRadius: '4px', overflow: 'hidden', background: 'var(--color-bg-base)' }}>
{selectedMod.bans_count > 0 && (
<div
style={{ width: `${(selectedMod.bans_count / selectedMod.total_actions) * 100}%`, background: '#FF0055', height: '100%' }}
title={`Баны: ${selectedMod.bans_count}`}
/>
)}
{selectedMod.timeouts_count > 0 && (
<div
style={{ width: `${(selectedMod.timeouts_count / selectedMod.total_actions) * 100}%`, background: '#FFAA00', height: '100%' }}
title={`Муты/Таймауты: ${selectedMod.timeouts_count}`}
/>
)}
{selectedMod.deletions_count > 0 && (
<div
style={{ width: `${(selectedMod.deletions_count / selectedMod.total_actions) * 100}%`, background: '#00AAFF', height: '100%' }}
title={`Удаления сообщений: ${selectedMod.deletions_count}`}
/>
)}
{selectedMod.unbans_count > 0 && (
<div
style={{ width: `${(selectedMod.unbans_count / selectedMod.total_actions) * 100}%`, background: '#00FF88', height: '100%' }}
title={`Разбаны: ${selectedMod.unbans_count}`}
/>
)}
</div>
{/* Legend */}
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '0.75rem 1.25rem', marginTop: '0.5rem', fontSize: '0.7rem', color: 'var(--color-text-muted)' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: '0.25rem' }}>
<span style={{ display: 'inline-block', width: '6px', height: '6px', borderRadius: '50%', background: '#FF0055' }}></span>
Баны ({selectedMod.bans_count})
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: '0.25rem' }}>
<span style={{ display: 'inline-block', width: '6px', height: '6px', borderRadius: '50%', background: '#FFAA00' }}></span>
Муты ({selectedMod.timeouts_count})
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: '0.25rem' }}>
<span style={{ display: 'inline-block', width: '6px', height: '6px', borderRadius: '50%', background: '#00AAFF' }}></span>
Удаления ({selectedMod.deletions_count})
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: '0.25rem' }}>
<span style={{ display: 'inline-block', width: '6px', height: '6px', borderRadius: '50%', background: '#00FF88' }}></span>
Разбаны ({selectedMod.unbans_count})
</div>
</div>
</div>
</div>
)}
</div>
</div>
</div>
)}
</div>
);
}