161 lines
6.2 KiB
React
161 lines
6.2 KiB
React
import { useState, useEffect } from 'react';
|
|
import { IconTrophy, IconMapPin, IconCloud, IconHash, IconChartBar } from '@tabler/icons-react';
|
|
|
|
/**
|
|
* Компонент для отображения Top-N статистики
|
|
*/
|
|
function TopNStats({ data, loading }) {
|
|
const [topCountries, setTopCountries] = useState([]);
|
|
const [topProviders, setTopProviders] = useState([]);
|
|
const [_topCommunities, _setTopCommunities] = useState([]);
|
|
|
|
useEffect(() => {
|
|
if (!data || !Array.isArray(data.servers)) return;
|
|
|
|
// Подсчет по странам
|
|
const countriesMap = new Map();
|
|
data.servers.forEach(s => {
|
|
const country = s.country || 'Unknown';
|
|
countriesMap.set(country, (countriesMap.get(country) || 0) + 1);
|
|
});
|
|
const topC = Array.from(countriesMap.entries())
|
|
.map(([name, count]) => ({ name, count }))
|
|
.sort((a, b) => b.count - a.count)
|
|
.slice(0, 5);
|
|
setTopCountries(topC);
|
|
|
|
// Подсчет по провайдерам
|
|
const providersMap = new Map();
|
|
data.servers.forEach(s => {
|
|
const provider = s.provider || 'Unknown';
|
|
providersMap.set(provider, (providersMap.get(provider) || 0) + 1);
|
|
});
|
|
const topP = Array.from(providersMap.entries())
|
|
.map(([name, count]) => ({ name, count }))
|
|
.sort((a, b) => b.count - a.count)
|
|
.slice(0, 5);
|
|
setTopProviders(topP);
|
|
|
|
}, [data]);
|
|
|
|
if (loading) {
|
|
return (
|
|
<div className="card">
|
|
<div className="card-body text-center py-4">
|
|
<div className="spinner-border spinner-border-sm me-2" role="status"></div>
|
|
<span className="text-muted">Загрузка статистики...</span>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
const getFlagEmoji = (countryCode) => {
|
|
if (!countryCode) return '🌐';
|
|
const map = { SWE: 'SE', UK: 'GB', RU: 'RU', US: 'US', DE: 'DE', NL: 'NL', SG: 'SG' };
|
|
const cc = (map[countryCode.toUpperCase()] || countryCode).slice(0, 2);
|
|
if (cc.length !== 2) return '🌐';
|
|
return cc.replace(/./g, (ch) => String.fromCodePoint(127397 + ch.charCodeAt()));
|
|
};
|
|
|
|
return (
|
|
<div className="row g-2 g-md-3">
|
|
{/* Top Страны */}
|
|
<div className="col-12 col-md-6">
|
|
<div className="card h-100">
|
|
<div className="card-header">
|
|
<h3 className="card-title">
|
|
<IconMapPin className="me-2" size={20} />
|
|
Top-5 Стран
|
|
</h3>
|
|
</div>
|
|
<div className="card-body">
|
|
{topCountries.length === 0 ? (
|
|
<div className="text-center text-muted">Нет данных</div>
|
|
) : (
|
|
<div className="list-group list-group-flush">
|
|
{topCountries.map((item, idx) => {
|
|
const maxCount = topCountries[0]?.count || 1;
|
|
const percentage = ((item.count / maxCount) * 100).toFixed(0);
|
|
return (
|
|
<div key={item.name} className="list-group-item px-0">
|
|
<div className="d-flex align-items-center justify-content-between mb-1">
|
|
<div className="d-flex align-items-center">
|
|
<span className="me-2" style={{ fontSize: idx === 0 ? '1.5rem' : '1.2rem' }}>
|
|
{idx === 0 ? '🏆' : getFlagEmoji(item.name)}
|
|
</span>
|
|
<strong>{item.name}</strong>
|
|
</div>
|
|
<span className="badge bg-blue-lt text-blue">{item.count}</span>
|
|
</div>
|
|
<div className="progress" style={{ height: 6 }}>
|
|
<div
|
|
className="progress-bar bg-blue"
|
|
style={{ width: `${percentage}%` }}
|
|
role="progressbar"
|
|
aria-valuenow={percentage}
|
|
aria-valuemin="0"
|
|
aria-valuemax="100"
|
|
></div>
|
|
</div>
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Top Провайдеры */}
|
|
<div className="col-12 col-md-6">
|
|
<div className="card h-100">
|
|
<div className="card-header">
|
|
<h3 className="card-title">
|
|
<IconCloud className="me-2" size={20} />
|
|
Top-5 Провайдеров
|
|
</h3>
|
|
</div>
|
|
<div className="card-body">
|
|
{topProviders.length === 0 ? (
|
|
<div className="text-center text-muted">Нет данных</div>
|
|
) : (
|
|
<div className="list-group list-group-flush">
|
|
{topProviders.map((item, idx) => {
|
|
const maxCount = topProviders[0]?.count || 1;
|
|
const percentage = ((item.count / maxCount) * 100).toFixed(0);
|
|
return (
|
|
<div key={item.name} className="list-group-item px-0">
|
|
<div className="d-flex align-items-center justify-content-between mb-1">
|
|
<div className="d-flex align-items-center">
|
|
<span className="me-2" style={{ fontSize: '1.2rem' }}>
|
|
{idx === 0 ? '🏆' : '☁️'}
|
|
</span>
|
|
<strong className="text-truncate" style={{ maxWidth: 200 }}>{item.name}</strong>
|
|
</div>
|
|
<span className="badge bg-green-lt text-green">{item.count}</span>
|
|
</div>
|
|
<div className="progress" style={{ height: 6 }}>
|
|
<div
|
|
className="progress-bar bg-green"
|
|
style={{ width: `${percentage}%` }}
|
|
role="progressbar"
|
|
aria-valuenow={percentage}
|
|
aria-valuemin="0"
|
|
aria-valuemax="100"
|
|
></div>
|
|
</div>
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export default TopNStats;
|
|
|