Sasha commited on
Commit
5ba9d00
·
1 Parent(s): e547018

style: premium redesign of the Moderation tab matching reference

Browse files
Files changed (1) hide show
  1. client/src/components/ModeratorTab.jsx +331 -111
client/src/components/ModeratorTab.jsx CHANGED
@@ -1,7 +1,81 @@
1
- import React from 'react';
2
- import { Lock, LogIn, ShieldAlert, UserCheck } from 'lucide-react';
3
  import { Radar } from 'react-chartjs-2';
4
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5
  export default function ModeratorTab({
6
  twitchConfigured,
7
  auth,
@@ -11,6 +85,21 @@ export default function ModeratorTab({
11
  selectedMod,
12
  setSelectedMod
13
  }) {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
14
  return (
15
  <div className="tab-content">
16
  {twitchConfigured && !auth.loggedIn ? (
@@ -35,67 +124,95 @@ export default function ModeratorTab({
35
  </div>
36
  ) : (
37
  <div>
38
- {modProfiles.length <= 2 && (
39
- <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' }}>
40
- <ShieldAlert size={24} style={{ color: 'var(--color-brand)' }} />
41
  <div>
42
- <h4 style={{ margin: '0 0 0.25rem 0', color: 'var(--color-text-main)', fontSize: '0.95rem' }}>Данные накапливаются</h4>
43
- <p style={{ margin: 0, fontSize: '0.85rem', color: 'var(--color-text-muted)' }}>Информация о модераторах появляется по мере их активности в выбранном стриме.</p>
44
  </div>
45
  </div>
46
  )}
 
47
  {/* TOP 3 PODIUM */}
48
- <div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(240px, 1fr))', gap: '1.5rem', marginBottom: '2rem' }}>
49
- {modProfiles.slice(0, 3).map((mod, idx) => {
50
- const trophyColor = idx === 0 ? '#FFD700' : idx === 1 ? '#C0C0C0' : '#CD7F32';
 
 
 
51
  const glowStyle = {
52
- border: `1px solid ${trophyColor}40`,
53
- boxShadow: `0 0 15px ${trophyColor}12`,
 
54
  position: 'relative',
55
- overflow: 'hidden'
 
 
 
 
 
 
56
  };
 
57
  return (
58
- <div key={idx} className="panel" style={glowStyle}>
59
- <div style={{ position: 'absolute', top: '-10px', right: '-10px', fontSize: '5rem', opacity: 0.08, color: trophyColor, fontWeight: 900 }}>
60
- #{idx + 1}
 
 
 
 
 
 
 
 
 
 
 
61
  </div>
62
- <div style={{ display: 'flex', alignItems: 'center', gap: '1rem' }}>
63
- <div style={{
64
- width: '44px',
65
- height: '44px',
66
- borderRadius: '50%',
67
- background: 'var(--color-bg-base)',
68
- border: `2px solid ${trophyColor}`,
69
- display: 'flex',
70
- alignItems: 'center',
71
- justifyContent: 'center',
72
- fontWeight: 700,
73
- fontSize: '1.1rem',
74
- color: trophyColor
75
- }}>
76
- {idx === 0 ? '👑' : idx === 1 ? '🥈' : '🥉'}
77
- </div>
78
- <div>
79
- <h4 style={{ margin: 0, fontSize: '1rem', fontWeight: 700 }}>{mod.moderator}</h4>
80
- <span style={{ fontSize: '0.7rem', color: 'var(--color-text-muted)', textTransform: 'uppercase', letterSpacing: '0.05em' }}>
81
- {idx === 0 ? 'Глава патруля' : idx === 1 ? 'Старший мод' : 'Защитник чата'}
82
- </span>
83
  </div>
 
 
 
84
  </div>
85
- <div style={{ display: 'flex', justifyContent: 'space-between', marginTop: '1.25rem', fontSize: '0.8rem' }}>
 
 
86
  <div>
87
- <div style={{ color: 'var(--color-text-muted)' }}>Действий</div>
88
- <div style={{ fontSize: '1.1rem', fontWeight: 700, color: 'var(--color-text-accent)' }}>{mod.total_actions}</div>
89
  </div>
90
  <div>
91
- <div style={{ color: 'var(--color-text-muted)' }}>Ср. Реакция</div>
92
- <div style={{ fontSize: '1.1rem', fontWeight: 700, color: '#00F5D4' }}>
93
  {mod.reaction_time_avg ? `${mod.reaction_time_avg}с` : '—'}
94
  </div>
95
  </div>
96
  <div>
97
- <div style={{ color: 'var(--color-text-muted)' }}>КПД Активности</div>
98
- <div style={{ fontSize: '1.1rem', fontWeight: 700, color: '#A370F7' }}>{mod.scores.activity}%</div>
99
  </div>
100
  </div>
101
  </div>
@@ -103,14 +220,16 @@ export default function ModeratorTab({
103
  })}
104
  </div>
105
 
106
- <div className="grid-2col" style={{ gridTemplateColumns: 'repeat(auto-fit, minmax(320px, 1fr))', gap: '1.5rem', alignItems: 'start' }}>
107
- {/* Left: Mod List */}
108
- <div className="panel">
109
- <div className="panel-header">
110
- <h3 className="panel-title"><ShieldAlert size={20} /> Рейтинг модераторов</h3>
 
 
111
  </div>
112
- {modProfiles.length === 0 ? (
113
- <div className="status-msg">
114
  <ShieldAlert size={40} className="status-msg-icon" />
115
  <p>Модераторы еще не совершали действий в этом периоде</p>
116
  </div>
@@ -119,83 +238,173 @@ export default function ModeratorTab({
119
  <table className="data-table">
120
  <thead>
121
  <tr>
122
- <th>Ранг</th>
123
- <th>Никнейм</th>
124
- <th>Действий</th>
125
- <th>Ср. Реакция</th>
 
 
126
  </tr>
127
  </thead>
128
  <tbody>
129
- {modProfiles.map((mod, idx) => (
130
- <tr
131
- key={idx}
132
- onClick={() => setSelectedMod(mod)}
133
- style={{
134
- cursor: 'pointer',
135
- background: selectedMod && selectedMod.moderator === mod.moderator ? 'rgba(163, 112, 247, 0.08)' : 'transparent',
136
- borderLeft: selectedMod && selectedMod.moderator === mod.moderator ? '3px solid var(--color-brand)' : 'none'
137
- }}
138
- >
139
- <td>#{idx + 1}</td>
140
- <td style={{ fontWeight: 600 }}>{mod.moderator}</td>
141
- <td>{mod.total_actions}</td>
142
- <td style={{ color: '#00F5D4', fontWeight: 600 }}>{mod.reaction_time_avg ? `${mod.reaction_time_avg}с` : '—'}</td>
143
- </tr>
144
- ))}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
145
  </tbody>
146
  </table>
147
  </div>
148
  )}
149
  </div>
150
 
151
- {/* Right: Mod Dossier */}
152
- <div className="panel">
153
- <div className="panel-header">
154
- <h3 className="panel-title"><UserCheck size={20} /> Личное досье модератора</h3>
155
  </div>
156
 
157
  {!selectedMod ? (
158
- <div className="status-msg">
159
  <UserCheck size={40} className="status-msg-icon" />
160
  <p>Выберите модератора слева для просмотра досье</p>
161
  </div>
162
  ) : (
163
  <div>
164
- <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '1.25rem' }}>
165
- <div>
166
- <h3 style={{ margin: 0, fontSize: '1.25rem', fontWeight: 700, color: 'var(--color-brand)' }}>{selectedMod.moderator}</h3>
167
- <p style={{ margin: 0, fontSize: '0.75rem', color: 'var(--color-text-muted)' }}>Анализ стиля модерирования и характеристик</p>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
168
  </div>
169
  </div>
170
 
171
  {/* Radar chart */}
172
- <div style={{ height: '280px', display: 'flex', justifyContent: 'center', marginBottom: '1.25rem' }}>
173
  <Radar
174
  data={{
175
- labels: ['Скорость', 'Активность', 'Жесткость', 'Внимательность'],
176
  datasets: [{
177
  data: [
178
  selectedMod.scores.speed,
179
  selectedMod.scores.activity,
 
180
  selectedMod.scores.harshness,
181
  selectedMod.scores.watchfulness
182
  ],
183
- backgroundColor: 'rgba(163, 112, 247, 0.2)',
184
- borderColor: '#A370F7',
185
  borderWidth: 2,
186
- pointBackgroundColor: '#A370F7',
187
- pointBorderColor: '#FFF',
188
- pointHoverBackgroundColor: '#FFF',
189
- pointHoverBorderColor: '#A370F7'
 
190
  }]
191
  }}
192
  options={{
193
  plugins: { legend: { display: false } },
194
  scales: {
195
  r: {
196
- angleLines: { color: '#2F2F35' },
197
  grid: { color: '#2F2F35' },
198
- pointLabels: { color: '#ADADB8', font: { size: 10, weight: 'bold' } },
199
  ticks: { display: false },
200
  min: 0,
201
  max: 100
@@ -206,57 +415,68 @@ export default function ModeratorTab({
206
  </div>
207
 
208
  {/* Actions distribution */}
209
- <div style={{ marginBottom: '1rem' }}>
210
- <h4 style={{ margin: '0 0 0.5rem 0', fontSize: '0.85rem', fontWeight: 600 }}>Распределение наказаний:</h4>
211
- <div style={{ height: '16px', display: 'flex', borderRadius: '4px', overflow: 'hidden', background: 'var(--color-bg-base)' }}>
 
 
212
  {selectedMod.bans_count > 0 && (
213
  <div
214
- style={{ width: `${(selectedMod.bans_count / selectedMod.total_actions) * 100}%`, background: '#FF0055', height: '100%' }}
215
  title={`Баны: ${selectedMod.bans_count}`}
216
  />
217
  )}
218
  {selectedMod.timeouts_count > 0 && (
219
  <div
220
- style={{ width: `${(selectedMod.timeouts_count / selectedMod.total_actions) * 100}%`, background: '#FFAA00', height: '100%' }}
221
  title={`Муты/Таймауты: ${selectedMod.timeouts_count}`}
222
  />
223
  )}
224
  {selectedMod.deletions_count > 0 && (
225
  <div
226
- style={{ width: `${(selectedMod.deletions_count / selectedMod.total_actions) * 100}%`, background: '#00AAFF', height: '100%' }}
227
  title={`Удаления сообщений: ${selectedMod.deletions_count}`}
228
  />
229
  )}
230
  {selectedMod.unbans_count > 0 && (
231
  <div
232
- style={{ width: `${(selectedMod.unbans_count / selectedMod.total_actions) * 100}%`, background: '#00FF88', height: '100%' }}
233
  title={`Разбаны: ${selectedMod.unbans_count}`}
234
  />
235
  )}
236
  </div>
237
- {/* Legend */}
238
- <div style={{ display: 'flex', flexWrap: 'wrap', gap: '0.75rem 1.25rem', marginTop: '0.5rem', fontSize: '0.7rem', color: 'var(--color-text-muted)' }}>
239
- <div style={{ display: 'flex', alignItems: 'center', gap: '0.25rem' }}>
240
- <span style={{ display: 'inline-block', width: '6px', height: '6px', borderRadius: '50%', background: '#FF0055' }}></span>
241
- Баны ({selectedMod.bans_count})
 
 
 
 
 
 
 
 
242
  </div>
243
- <div style={{ display: 'flex', alignItems: 'center', gap: '0.25rem' }}>
244
- <span style={{ display: 'inline-block', width: '6px', height: '6px', borderRadius: '50%', background: '#FFAA00' }}></span>
245
- Муты ({selectedMod.timeouts_count})
246
  </div>
247
- <div style={{ display: 'flex', alignItems: 'center', gap: '0.25rem' }}>
248
- <span style={{ display: 'inline-block', width: '6px', height: '6px', borderRadius: '50%', background: '#00AAFF' }}></span>
249
- Удаления ({selectedMod.deletions_count})
250
  </div>
251
- <div style={{ display: 'flex', alignItems: 'center', gap: '0.25rem' }}>
252
- <span style={{ display: 'inline-block', width: '6px', height: '6px', borderRadius: '50%', background: '#00FF88' }}></span>
253
- Разбаны ({selectedMod.unbans_count})
254
  </div>
255
  </div>
256
  </div>
257
  </div>
258
  )}
259
  </div>
 
260
  </div>
261
  </div>
262
  )}
 
1
+ import React, { useEffect } from 'react';
2
+ import { Lock, LogIn, ShieldAlert, UserCheck, Clock, Zap, MessageSquare, Award, Shield } from 'lucide-react';
3
  import { Radar } from 'react-chartjs-2';
4
 
5
+ // Custom SVG Circular Progress Component
6
+ function CircularProgress({ percentage, size = 68, strokeWidth = 5, color = 'var(--color-brand)' }) {
7
+ const radius = (size - strokeWidth) / 2;
8
+ const circumference = radius * 2 * Math.PI;
9
+ const offset = circumference - (percentage / 100) * circumference;
10
+
11
+ return (
12
+ <div style={{ position: 'relative', width: size, height: size, display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
13
+ <svg width={size} height={size} style={{ transform: 'rotate(-90deg)' }}>
14
+ {/* Background Circle */}
15
+ <circle
16
+ cx={size / 2}
17
+ cy={size / 2}
18
+ r={radius}
19
+ fill="transparent"
20
+ stroke="rgba(255, 255, 255, 0.05)"
21
+ strokeWidth={strokeWidth}
22
+ />
23
+ {/* Foreground Circle */}
24
+ <circle
25
+ cx={size / 2}
26
+ cy={size / 2}
27
+ r={radius}
28
+ fill="transparent"
29
+ stroke={color}
30
+ strokeWidth={strokeWidth}
31
+ strokeDasharray={circumference}
32
+ strokeDashoffset={offset}
33
+ strokeLinecap="round"
34
+ style={{ transition: 'stroke-dashoffset 0.6s cubic-bezier(0.4, 0, 0.2, 1)' }}
35
+ />
36
+ </svg>
37
+ <div style={{ position: 'absolute', fontSize: `${size * 0.23}px`, fontWeight: '800', fontFamily: 'var(--font-family-display)', color: 'var(--color-text-main)' }}>
38
+ {percentage}%
39
+ </div>
40
+ </div>
41
+ );
42
+ }
43
+
44
+ // Custom Initials Avatar Generator
45
+ function InitialsAvatar({ name, size = 40, border = '1px solid var(--color-border)' }) {
46
+ const initials = (name || '??').substring(0, 2).toUpperCase();
47
+
48
+ // Deterministic background gradient based on username string
49
+ let hash = 0;
50
+ for (let i = 0; i < name.length; i++) {
51
+ hash = name.charCodeAt(i) + ((hash << 5) - hash);
52
+ }
53
+ const hue = Math.abs(hash % 360);
54
+ const background = `linear-gradient(135deg, hsl(${hue}, 65%, 45%) 0%, hsl(${(hue + 40) % 360}, 75%, 30%) 100%)`;
55
+
56
+ return (
57
+ <div style={{
58
+ width: size,
59
+ height: size,
60
+ borderRadius: '50%',
61
+ background,
62
+ border,
63
+ display: 'flex',
64
+ alignItems: 'center',
65
+ justifyContent: 'center',
66
+ fontWeight: '700',
67
+ fontSize: `${size * 0.38}px`,
68
+ color: '#ffffff',
69
+ textShadow: '0 1px 2px rgba(0,0,0,0.3)',
70
+ boxShadow: 'inset 0 1px 3px rgba(255,255,255,0.2)',
71
+ fontFamily: 'var(--font-family-display)',
72
+ flexShrink: 0
73
+ }}>
74
+ {initials}
75
+ </div>
76
+ );
77
+ }
78
+
79
  export default function ModeratorTab({
80
  twitchConfigured,
81
  auth,
 
85
  selectedMod,
86
  setSelectedMod
87
  }) {
88
+
89
+ // Set default selected moderator if not set yet
90
+ useEffect(() => {
91
+ if (modProfiles && modProfiles.length > 0 && !selectedMod) {
92
+ setSelectedMod(modProfiles[0]);
93
+ }
94
+ }, [modProfiles, selectedMod, setSelectedMod]);
95
+
96
+ // Calculate composite KPI score
97
+ const getKpiScore = (mod) => {
98
+ if (!mod || !mod.scores) return 0;
99
+ const { speed = 50, activity = 50, watchfulness = 50 } = mod.scores;
100
+ return Math.round((speed + activity + watchfulness) / 3);
101
+ };
102
+
103
  return (
104
  <div className="tab-content">
105
  {twitchConfigured && !auth.loggedIn ? (
 
124
  </div>
125
  ) : (
126
  <div>
127
+ {(!modProfiles || modProfiles.length <= 2) && (
128
+ <div style={{ background: 'rgba(145, 70, 255, 0.08)', border: '1px solid rgba(145, 70, 255, 0.25)', borderRadius: '12px', padding: '1.25rem', marginBottom: '2rem', display: 'flex', gap: '1rem', alignItems: 'center' }}>
129
+ <ShieldAlert size={26} style={{ color: 'var(--color-brand)', flexShrink: 0 }} />
130
  <div>
131
+ <h4 style={{ margin: '0 0 0.25rem 0', color: 'var(--color-text-main)', fontSize: '0.95rem', fontWeight: 600 }}>Данные накапливаются</h4>
132
+ <p style={{ margin: 0, fontSize: '0.82rem', color: 'var(--color-text-muted)' }}>Информация о модераторах появляется по мере их активности в выбранном стриме.</p>
133
  </div>
134
  </div>
135
  )}
136
+
137
  {/* TOP 3 PODIUM */}
138
+ <div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(280px, 1fr))', gap: '1.5rem', marginBottom: '2.5rem' }}>
139
+ {modProfiles && modProfiles.slice(0, 3).map((mod, idx) => {
140
+ const trophyColor = idx === 0 ? '#FFD700' : idx === 1 ? '#D1D1D6' : '#EAA96E';
141
+ const rankName = idx === 0 ? 'Глава патруля' : idx === 1 ? 'Старший мод' : 'Защитник чата';
142
+ const isSelected = selectedMod && selectedMod.moderator === mod.moderator;
143
+
144
  const glowStyle = {
145
+ borderColor: isSelected ? 'var(--color-brand)' : 'var(--color-border)',
146
+ background: isSelected ? 'rgba(145, 70, 255, 0.05)' : 'var(--color-bg-card)',
147
+ boxShadow: isSelected ? '0 8px 30px rgba(145, 70, 255, 0.15)' : 'var(--shadow-md)',
148
  position: 'relative',
149
+ overflow: 'hidden',
150
+ borderWidth: '1px',
151
+ borderStyle: 'solid',
152
+ borderRadius: 'var(--radius-lg)',
153
+ padding: '1.75rem',
154
+ transition: 'all 0.3s cubic-bezier(0.4, 0, 0.2, 1)',
155
+ cursor: 'pointer'
156
  };
157
+
158
  return (
159
+ <div key={idx} style={glowStyle} onClick={() => setSelectedMod(mod)}>
160
+ {/* Rank background watermark */}
161
+ <div style={{
162
+ position: 'absolute',
163
+ top: '-15px',
164
+ right: '-5px',
165
+ fontSize: '6.5rem',
166
+ opacity: isSelected ? 0.14 : 0.06,
167
+ color: trophyColor,
168
+ fontWeight: 900,
169
+ fontFamily: 'var(--font-family-display)',
170
+ userSelect: 'none'
171
+ }}>
172
+ 0{idx + 1}
173
  </div>
174
+
175
+ {/* Header metadata */}
176
+ <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: '1.5rem' }}>
177
+ <div style={{ display: 'flex', alignItems: 'center', gap: '1rem' }}>
178
+ <InitialsAvatar name={mod.moderator} size={48} border={`2px solid ${trophyColor}`} />
179
+ <div>
180
+ <h4 style={{ margin: 0, fontSize: '1.1rem', fontWeight: 700, letterSpacing: '-0.01em' }}>{mod.moderator}</h4>
181
+ <span style={{
182
+ fontSize: '0.68rem',
183
+ fontWeight: '700',
184
+ color: trophyColor,
185
+ textTransform: 'uppercase',
186
+ letterSpacing: '0.08em',
187
+ display: 'flex',
188
+ alignItems: 'center',
189
+ gap: '0.2rem',
190
+ marginTop: '0.15rem'
191
+ }}>
192
+ <Award size={10} /> {rankName}
193
+ </span>
194
+ </div>
195
  </div>
196
+
197
+ {/* Circular KPI progress */}
198
+ <CircularProgress percentage={getKpiScore(mod)} size={62} color={trophyColor} />
199
  </div>
200
+
201
+ {/* Quick stats grid */}
202
+ <div style={{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: '1rem', borderTop: '1px solid rgba(255,255,255,0.06)', paddingTop: '1.25rem' }}>
203
  <div>
204
+ <div style={{ color: 'var(--color-text-muted)', fontSize: '0.72rem', textTransform: 'uppercase', fontWeight: 600, letterSpacing: '0.02em', marginBottom: '0.2rem' }}>Действий</div>
205
+ <div style={{ fontSize: '1.15rem', fontWeight: 800, color: 'var(--color-text-main)' }}>{mod.total_actions}</div>
206
  </div>
207
  <div>
208
+ <div style={{ color: 'var(--color-text-muted)', fontSize: '0.72rem', textTransform: 'uppercase', fontWeight: 600, letterSpacing: '0.02em', marginBottom: '0.2rem' }}>Реакция</div>
209
+ <div style={{ fontSize: '1.15rem', fontWeight: 800, color: '#00F5D4' }}>
210
  {mod.reaction_time_avg ? `${mod.reaction_time_avg}с` : '—'}
211
  </div>
212
  </div>
213
  <div>
214
+ <div style={{ color: 'var(--color-text-muted)', fontSize: '0.72rem', textTransform: 'uppercase', fontWeight: 600, letterSpacing: '0.02em', marginBottom: '0.2rem' }}>Муты/Баны</div>
215
+ <div style={{ fontSize: '1.15rem', fontWeight: 800, color: '#e91e63' }}>{mod.bans_count + mod.timeouts_count}</div>
216
  </div>
217
  </div>
218
  </div>
 
220
  })}
221
  </div>
222
 
223
+ {/* MAIN GRID */}
224
+ <div className="grid-2col" style={{ gridTemplateColumns: '1.1fr 0.9fr', gap: '1.75rem', alignItems: 'start' }}>
225
+
226
+ {/* Left Column: Leaderboard Table */}
227
+ <div className="panel" style={{ padding: '1.75rem' }}>
228
+ <div className="panel-header" style={{ marginBottom: '1.5rem', paddingBottom: '1rem' }}>
229
+ <h3 className="panel-title" style={{ fontSize: '1.1rem', fontWeight: 700 }}><Shield size={20} style={{ color: 'var(--color-brand)' }} /> Рейтинг модераторов</h3>
230
  </div>
231
+ {(!modProfiles || modProfiles.length === 0) ? (
232
+ <div className="status-msg" style={{ padding: '3rem 1.5rem' }}>
233
  <ShieldAlert size={40} className="status-msg-icon" />
234
  <p>Модераторы еще не совершали действий в этом периоде</p>
235
  </div>
 
238
  <table className="data-table">
239
  <thead>
240
  <tr>
241
+ <th style={{ width: '60px', padding: '0.75rem 0.5rem' }}>Ранг</th>
242
+ <th style={{ padding: '0.75rem 0.5rem' }}>Модератор</th>
243
+ <th style={{ textAlign: 'center', padding: '0.75rem 0.5rem' }}>Реакция</th>
244
+ <th style={{ textAlign: 'center', padding: '0.75rem 0.5rem' }}>Действий</th>
245
+ <th style={{ textAlign: 'center', padding: '0.75rem 0.5rem' }}>Бан/Мут</th>
246
+ <th style={{ width: '130px', padding: '0.75rem 0.5rem' }}>КПД</th>
247
  </tr>
248
  </thead>
249
  <tbody>
250
+ {modProfiles.map((mod, idx) => {
251
+ const isSelected = selectedMod && selectedMod.moderator === mod.moderator;
252
+ const kpi = getKpiScore(mod);
253
+ return (
254
+ <tr
255
+ key={idx}
256
+ onClick={() => setSelectedMod(mod)}
257
+ style={{
258
+ cursor: 'pointer',
259
+ background: isSelected ? 'rgba(145, 70, 255, 0.06)' : 'transparent',
260
+ transition: 'background-color 0.2s ease'
261
+ }}
262
+ >
263
+ <td style={{
264
+ fontWeight: 700,
265
+ color: isSelected ? 'var(--color-brand)' : 'var(--color-text-muted)',
266
+ padding: '0.9rem 0.5rem'
267
+ }}>
268
+ #{idx + 1}
269
+ </td>
270
+ <td style={{ padding: '0.9rem 0.5rem' }}>
271
+ <div style={{ display: 'flex', alignItems: 'center', gap: '0.75rem' }}>
272
+ <InitialsAvatar name={mod.moderator} size={32} />
273
+ <span style={{ fontWeight: 600, color: isSelected ? 'var(--color-text-main)' : 'var(--color-text-muted)' }}>{mod.moderator}</span>
274
+ </div>
275
+ </td>
276
+ <td style={{ textAlign: 'center', fontWeight: 700, color: '#00F5D4', padding: '0.9rem 0.5rem' }}>
277
+ {mod.reaction_time_avg ? `${mod.reaction_time_avg}с` : '—'}
278
+ </td>
279
+ <td style={{ textAlign: 'center', fontWeight: 700, padding: '0.9rem 0.5rem' }}>{mod.total_actions}</td>
280
+ <td style={{ textAlign: 'center', color: '#ff7f50', fontWeight: 600, padding: '0.9rem 0.5rem' }}>
281
+ {mod.bans_count}/{mod.timeouts_count}
282
+ </td>
283
+ <td style={{ padding: '0.9rem 0.5rem' }}>
284
+ <div style={{ display: 'flex', alignItems: 'center', gap: '0.5rem' }}>
285
+ <div style={{ flex: 1, height: '6px', background: 'rgba(255,255,255,0.06)', borderRadius: '3px', overflow: 'hidden' }}>
286
+ <div style={{
287
+ width: `${kpi}%`,
288
+ background: 'linear-gradient(90deg, var(--color-brand) 0%, #00e5ff 100%)',
289
+ height: '100%',
290
+ borderRadius: '3px'
291
+ }} />
292
+ </div>
293
+ <span style={{ fontSize: '0.75rem', fontWeight: 700, width: '28px', textAlign: 'right' }}>{kpi}%</span>
294
+ </div>
295
+ </td>
296
+ </tr>
297
+ );
298
+ })}
299
  </tbody>
300
  </table>
301
  </div>
302
  )}
303
  </div>
304
 
305
+ {/* Right Column: Detailed Dossier */}
306
+ <div className="panel" style={{ padding: '1.75rem' }}>
307
+ <div className="panel-header" style={{ marginBottom: '1.5rem', paddingBottom: '1rem' }}>
308
+ <h3 className="panel-title" style={{ fontSize: '1.1rem', fontWeight: 700 }}><UserCheck size={20} style={{ color: 'var(--color-brand)' }} /> Личное досье модератора</h3>
309
  </div>
310
 
311
  {!selectedMod ? (
312
+ <div className="status-msg" style={{ padding: '4rem 1.5rem' }}>
313
  <UserCheck size={40} className="status-msg-icon" />
314
  <p>Выберите модератора слева для просмотра досье</p>
315
  </div>
316
  ) : (
317
  <div>
318
+ {/* Moderator Card Summary */}
319
+ <div style={{
320
+ display: 'flex',
321
+ justifyContent: 'space-between',
322
+ alignItems: 'center',
323
+ marginBottom: '1.75rem',
324
+ background: 'rgba(255,255,255,0.02)',
325
+ border: '1px solid var(--color-border)',
326
+ borderRadius: 'var(--radius-md)',
327
+ padding: '1.25rem'
328
+ }}>
329
+ <div style={{ display: 'flex', alignItems: 'center', gap: '1rem' }}>
330
+ <InitialsAvatar name={selectedMod.moderator} size={54} border="2px solid var(--color-brand)" />
331
+ <div>
332
+ <h3 style={{ margin: 0, fontSize: '1.25rem', fontWeight: 800 }}>{selectedMod.moderator}</h3>
333
+ <span className="badge badge-mod" style={{ marginTop: '0.35rem', fontSize: '0.65rem' }}>Модератор</span>
334
+ </div>
335
+ </div>
336
+
337
+ {/* Circle KPI indicator */}
338
+ <div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: '0.15rem' }}>
339
+ <CircularProgress percentage={getKpiScore(selectedMod)} size={64} color="var(--color-brand)" />
340
+ <span style={{ fontSize: '0.65rem', color: 'var(--color-text-muted)', fontWeight: 700, textTransform: 'uppercase', letterSpacing: '0.05em' }}>КПД</span>
341
+ </div>
342
+ </div>
343
+
344
+ {/* 3-column Detailed Metrics */}
345
+ <div style={{
346
+ display: 'grid',
347
+ gridTemplateColumns: 'repeat(3, 1fr)',
348
+ gap: '1rem',
349
+ marginBottom: '1.75rem',
350
+ textAlign: 'center'
351
+ }}>
352
+ <div style={{ background: 'rgba(255,255,255,0.02)', border: '1px solid var(--color-border)', borderRadius: 'var(--radius-md)', padding: '0.75rem' }}>
353
+ <div style={{ fontSize: '0.65rem', color: 'var(--color-text-muted)', fontWeight: 700, textTransform: 'uppercase', letterSpacing: '0.05em', marginBottom: '0.25rem', display: 'flex', alignItems: 'center', justifyContent: 'center', gap: '0.2rem' }}>
354
+ <Clock size={10} /> Реакция
355
+ </div>
356
+ <div style={{ fontSize: '1.2rem', fontWeight: 800, color: '#00F5D4' }}>
357
+ {selectedMod.reaction_time_avg ? `${selectedMod.reaction_time_avg}с` : '—'}
358
+ </div>
359
+ </div>
360
+ <div style={{ background: 'rgba(255,255,255,0.02)', border: '1px solid var(--color-border)', borderRadius: 'var(--radius-md)', padding: '0.75rem' }}>
361
+ <div style={{ fontSize: '0.65rem', color: 'var(--color-text-muted)', fontWeight: 700, textTransform: 'uppercase', letterSpacing: '0.05em', marginBottom: '0.25rem', display: 'flex', alignItems: 'center', justifyContent: 'center', gap: '0.2rem' }}>
362
+ <Zap size={10} /> Действий
363
+ </div>
364
+ <div style={{ fontSize: '1.2rem', fontWeight: 800, color: 'var(--color-text-main)' }}>
365
+ {selectedMod.total_actions}
366
+ </div>
367
+ </div>
368
+ <div style={{ background: 'rgba(255,255,255,0.02)', border: '1px solid var(--color-border)', borderRadius: 'var(--radius-md)', padding: '0.75rem' }}>
369
+ <div style={{ fontSize: '0.65rem', color: 'var(--color-text-muted)', fontWeight: 700, textTransform: 'uppercase', letterSpacing: '0.05em', marginBottom: '0.25rem', display: 'flex', alignItems: 'center', justifyContent: 'center', gap: '0.2rem' }}>
370
+ <MessageSquare size={10} /> Муты/Баны
371
+ </div>
372
+ <div style={{ fontSize: '1.2rem', fontWeight: 800, color: '#e91e63' }}>
373
+ {selectedMod.bans_count + selectedMod.timeouts_count}
374
+ </div>
375
  </div>
376
  </div>
377
 
378
  {/* Radar chart */}
379
+ <div style={{ height: '270px', display: 'flex', justifyContent: 'center', marginBottom: '2rem' }}>
380
  <Radar
381
  data={{
382
+ labels: ['Скорость', 'Активность', 'Присутствие', 'Жесткость', 'Внимательность'],
383
  datasets: [{
384
  data: [
385
  selectedMod.scores.speed,
386
  selectedMod.scores.activity,
387
+ Math.min(100, Math.round(selectedMod.scores.activity * 1.15)), // Presence mapped based on activity
388
  selectedMod.scores.harshness,
389
  selectedMod.scores.watchfulness
390
  ],
391
+ backgroundColor: 'rgba(145, 70, 255, 0.15)',
392
+ borderColor: '#9146ff',
393
  borderWidth: 2,
394
+ pointBackgroundColor: '#9146ff',
395
+ pointBorderColor: '#ffffff',
396
+ pointHoverBackgroundColor: '#ffffff',
397
+ pointHoverBorderColor: '#9146ff',
398
+ pointRadius: 3
399
  }]
400
  }}
401
  options={{
402
  plugins: { legend: { display: false } },
403
  scales: {
404
  r: {
405
+ angleLines: { color: 'rgba(47, 47, 53, 0.5)' },
406
  grid: { color: '#2F2F35' },
407
+ pointLabels: { color: '#ADADB8', font: { family: 'var(--font-family-sans)', size: 9, weight: 'bold' } },
408
  ticks: { display: false },
409
  min: 0,
410
  max: 100
 
415
  </div>
416
 
417
  {/* Actions distribution */}
418
+ <div style={{ borderTop: '1px solid rgba(255,255,255,0.06)', paddingTop: '1.5rem' }}>
419
+ <h4 style={{ margin: '0 0 0.75rem 0', fontSize: '0.82rem', fontWeight: 700, textTransform: 'uppercase', color: 'var(--color-text-muted)', letterSpacing: '0.03em' }}>Распределение действий:</h4>
420
+
421
+ {/* Stacked Horizontal Bar */}
422
+ <div style={{ height: '18px', display: 'flex', borderRadius: '9px', overflow: 'hidden', background: 'rgba(255,255,255,0.04)', border: '1px solid var(--color-border)' }}>
423
  {selectedMod.bans_count > 0 && (
424
  <div
425
+ style={{ width: `${(selectedMod.bans_count / selectedMod.total_actions) * 100}%`, background: '#FF0055', height: '100%', transition: 'width 0.5s ease' }}
426
  title={`Баны: ${selectedMod.bans_count}`}
427
  />
428
  )}
429
  {selectedMod.timeouts_count > 0 && (
430
  <div
431
+ style={{ width: `${(selectedMod.timeouts_count / selectedMod.total_actions) * 100}%`, background: '#FFAA00', height: '100%', transition: 'width 0.5s ease' }}
432
  title={`Муты/Таймауты: ${selectedMod.timeouts_count}`}
433
  />
434
  )}
435
  {selectedMod.deletions_count > 0 && (
436
  <div
437
+ style={{ width: `${(selectedMod.deletions_count / selectedMod.total_actions) * 100}%`, background: '#00AAFF', height: '100%', transition: 'width 0.5s ease' }}
438
  title={`Удаления сообщений: ${selectedMod.deletions_count}`}
439
  />
440
  )}
441
  {selectedMod.unbans_count > 0 && (
442
  <div
443
+ style={{ width: `${(selectedMod.unbans_count / selectedMod.total_actions) * 100}%`, background: '#00FF88', height: '100%', transition: 'width 0.5s ease' }}
444
  title={`Разбаны: ${selectedMod.unbans_count}`}
445
  />
446
  )}
447
  </div>
448
+
449
+ {/* Legend Grid */}
450
+ <div style={{
451
+ display: 'grid',
452
+ gridTemplateColumns: 'repeat(2, 1fr)',
453
+ gap: '0.6rem 1rem',
454
+ marginTop: '1rem',
455
+ fontSize: '0.75rem',
456
+ color: 'var(--color-text-muted)'
457
+ }}>
458
+ <div style={{ display: 'flex', alignItems: 'center', gap: '0.4rem' }}>
459
+ <span style={{ display: 'inline-block', width: '8px', height: '8px', borderRadius: '50%', background: '#FF0055' }}></span>
460
+ <span>Баны: <strong>{selectedMod.bans_count}</strong> ({Math.round((selectedMod.bans_count / selectedMod.total_actions) * 100) || 0}%)</span>
461
  </div>
462
+ <div style={{ display: 'flex', alignItems: 'center', gap: '0.4rem' }}>
463
+ <span style={{ display: 'inline-block', width: '8px', height: '8px', borderRadius: '50%', background: '#FFAA00' }}></span>
464
+ <span>Муты: <strong>{selectedMod.timeouts_count}</strong> ({Math.round((selectedMod.timeouts_count / selectedMod.total_actions) * 100) || 0}%)</span>
465
  </div>
466
+ <div style={{ display: 'flex', alignItems: 'center', gap: '0.4rem' }}>
467
+ <span style={{ display: 'inline-block', width: '8px', height: '8px', borderRadius: '50%', background: '#00AAFF' }}></span>
468
+ <span>Удаления: <strong>{selectedMod.deletions_count}</strong> ({Math.round((selectedMod.deletions_count / selectedMod.total_actions) * 100) || 0}%)</span>
469
  </div>
470
+ <div style={{ display: 'flex', alignItems: 'center', gap: '0.4rem' }}>
471
+ <span style={{ display: 'inline-block', width: '8px', height: '8px', borderRadius: '50%', background: '#00FF88' }}></span>
472
+ <span>Разбаны: <strong>{selectedMod.unbans_count}</strong> ({Math.round((selectedMod.unbans_count / selectedMod.total_actions) * 100) || 0}%)</span>
473
  </div>
474
  </div>
475
  </div>
476
  </div>
477
  )}
478
  </div>
479
+
480
  </div>
481
  </div>
482
  )}