Files
router-lists-ui/frontend/src/components/CommunityStats.jsx
T

242 lines
9.6 KiB
React

import { useState, useEffect } from 'react';
import api from '../lib/api.js';
import { IconChartBar, IconRefresh, IconTags } from '@tabler/icons-react';
/**
* Компонент для отображения статистики использования communities
*/
function CommunityStats({ communities = [] }) {
const [stats, setStats] = useState([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
const fetchStats = async () => {
setLoading(true);
setError(null);
try {
const res = await api.get('/communities/stats');
setStats(res.data?.stats || []);
} catch (e) {
console.error('Error loading community stats:', e);
setError('Не удалось загрузить статистику');
} finally {
setLoading(false);
}
};
useEffect(() => {
fetchStats();
}, []);
// Объединяем статистику с метаданными communities
const enrichedStats = stats.map(stat => {
const meta = communities.find(c => c.value === stat.community);
return {
...stat,
name: meta?.name || '',
description: meta?.description || '',
tags: meta?.tags || [],
color: meta?.color || '',
category: meta?.category || '',
};
});
// Группируем по категориям
const statsByCategory = enrichedStats.reduce((acc, stat) => {
const category = stat.category || 'Без категории';
if (!acc[category]) {
acc[category] = [];
}
acc[category].push(stat);
return acc;
}, {});
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>
);
}
if (error) {
return (
<div className="card">
<div className="card-body">
<div className="alert alert-warning mb-0">
{error}
<button className="btn btn-sm btn-outline-primary ms-2" onClick={fetchStats}>
<IconRefresh size={14} className="me-1" /> Повторить
</button>
</div>
</div>
</div>
);
}
const totalUsage = stats.reduce((sum, s) => sum + s.count, 0);
return (
<div className="card">
<div className="card-header">
<div className="d-flex justify-content-between align-items-center">
<h3 className="card-title">
<IconChartBar className="me-2" />
Статистика использования Communities
</h3>
<button className="btn btn-sm btn-outline-secondary" onClick={fetchStats} disabled={loading}>
<IconRefresh size={14} className="me-1" /> Обновить
</button>
</div>
</div>
<div className="card-body">
{stats.length === 0 ? (
<div className="text-center text-muted py-3">
Нет данных для отображения статистики
</div>
) : (
<>
{/* Общая статистика */}
<div className="row g-2 g-md-3 mb-4">
<div className="col-6 col-md-4">
<div className="card card-sm bg-blue-lt">
<div className="card-body">
<div className="h1 mb-0">{stats.length}</div>
<div className="text-muted">Уникальных communities</div>
</div>
</div>
</div>
<div className="col-6 col-md-4">
<div className="card card-sm bg-green-lt">
<div className="card-body">
<div className="h1 mb-0">{totalUsage}</div>
<div className="text-muted">Всего использований</div>
</div>
</div>
</div>
<div className="col-6 col-md-4">
<div className="card card-sm bg-purple-lt">
<div className="card-body">
<div className="h1 mb-0">{Object.keys(statsByCategory).length}</div>
<div className="text-muted">Категорий</div>
</div>
</div>
</div>
</div>
{/* Top-10 communities */}
<div className="mb-4">
<h4 className="mb-3">
<IconChartBar className="me-2" size={20} />
Top-10 самых используемых
</h4>
<div className="table-responsive">
<table className="table table-sm table-vcenter">
<thead>
<tr>
<th style={{ width: '10%' }}>#</th>
<th style={{ width: '25%' }}>Community</th>
<th style={{ width: '35%' }}>Название</th>
<th style={{ width: '15%' }}>Использований</th>
<th style={{ width: '15%' }}>Процент</th>
</tr>
</thead>
<tbody>
{enrichedStats.slice(0, 10).map((stat, idx) => {
const percentage = ((stat.count / totalUsage) * 100).toFixed(1);
return (
<tr key={stat.community}>
<td className="text-muted">{idx + 1}</td>
<td>
<code className="text-blue">{stat.community}</code>
</td>
<td>
<div className="d-flex align-items-center">
<span>{stat.name || '—'}</span>
{stat.tags && stat.tags.length > 0 && (
<div className="ms-2">
{stat.tags.slice(0, 2).map(tag => (
<span key={tag} className="badge bg-blue-lt text-blue me-1" style={{ fontSize: '0.7rem' }}>
<IconTags size={10} className="me-1" />
{tag}
</span>
))}
</div>
)}
</div>
</td>
<td>
<span className="badge bg-green-lt text-green">{stat.count}</span>
</td>
<td>
<div className="d-flex align-items-center">
<div className="progress flex-fill" style={{ height: 8 }}>
<div
className="progress-bar bg-blue"
style={{ width: `${percentage}%` }}
role="progressbar"
aria-valuenow={percentage}
aria-valuemin="0"
aria-valuemax="100"
></div>
</div>
<span className="ms-2 text-muted" style={{ fontSize: '0.85rem' }}>{percentage}%</span>
</div>
</td>
</tr>
);
})}
</tbody>
</table>
</div>
</div>
{/* Статистика по категориям */}
{Object.keys(statsByCategory).length > 1 && (
<div>
<h4 className="mb-3">По категориям</h4>
<div className="row g-2 g-md-3">
{Object.entries(statsByCategory).map(([category, items]) => {
const categoryTotal = items.reduce((sum, item) => sum + item.count, 0);
const categoryPercentage = ((categoryTotal / totalUsage) * 100).toFixed(1);
return (
<div key={category} className="col-12 col-md-6">
<div className="card card-sm">
<div className="card-body">
<div className="d-flex justify-content-between align-items-start mb-2">
<div>
<h5 className="mb-0">{category}</h5>
<div className="text-muted small">{items.length} communities</div>
</div>
<span className="badge bg-blue-lt text-blue">
{categoryTotal} использований
</span>
</div>
<div className="progress" style={{ height: 6 }}>
<div
className="progress-bar bg-blue"
style={{ width: `${categoryPercentage}%` }}
></div>
</div>
<div className="text-muted small mt-1">{categoryPercentage}% от общего</div>
</div>
</div>
</div>
);
})}
</div>
</div>
)}
</>
)}
</div>
</div>
);
}
export default CommunityStats;