242 lines
9.6 KiB
React
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;
|
|
|