'use client'; /** * ChannelAnalytics — вкладка аналитики в ChannelView. * Props: channelId, channelName */ import { useState, useEffect, useCallback } from 'react'; import { BarChart2, TrendingUp, Clock, RefreshCw, Heart, Share2, Eye, AlertCircle, Calendar } from 'lucide-react'; const DOW_SHORT = ['Пн','Вт','Ср','Чт','Пт','Сб','Вс']; const BAR_HEIGHT = 80; // ── Мини-бар для гистограммы ────────────────────────────────────────────────── function Bar({ value, max, label, highlight }) { const pct = max > 0 ? (value / max) : 0; return (
{value > 0 ? value : ''}
0 ? 4 : 0)}px` }} />
{label}
); } // ── Карточка метрики ────────────────────────────────────────────────────────── function MetricCard({ Icon, label, value, sub, color = 'text-accent' }) { return (
{value ?? '—'}
{label}
{sub &&
{sub}
}
); } // ── Строка реакций ──────────────────────────────────────────────────────────── function ReactionBar({ reactions }) { if (!reactions || !Object.keys(reactions).length) return null; const total = Object.values(reactions).reduce((s, v) => s + v, 0); return (
{Object.entries(reactions) .sort((a, b) => b[1] - a[1]) .map(([emoji, count]) => ( {emoji} {count} ))} всего {total}
); } // ── Главный компонент ───────────────────────────────────────────────────────── export default function ChannelAnalytics({ channelId, channelName }) { const [metrics, setMetrics] = useState(null); const [bestTime, setBestTime] = useState(null); const [loading, setLoading] = useState(false); const [error, setError] = useState(''); const [days, setDays] = useState(30); const load = useCallback(async () => { setLoading(true); setError(''); try { const [m, b] = await Promise.all([ fetch(`/api/metrics/channel/${channelId}?days=${days}`).then(r => r.json()), fetch(`/api/metrics/best-time/${channelId}?days=90`).then(r => r.json()), ]); if (m.error) throw new Error(m.error); setMetrics(m); setBestTime(b); } catch (e) { setError(e.message); } finally { setLoading(false); } }, [channelId, days]); useEffect(() => { load(); }, [load]); const totals = metrics?.totals || {}; const topPosts = metrics?.top_posts || []; const reactionTotals = metrics?.reaction_totals || []; const totalReactions = reactionTotals.reduce((s, r) => s + parseInt(r.total), 0); // Лучший день недели по кол-ву публикаций const dowData = Array.from({ length: 7 }, (_, i) => { const d = bestTime?.by_dow?.find(r => parseInt(r.dow) === i + 1); return { label: DOW_SHORT[i], value: d?.count || 0 }; }); const maxDow = Math.max(...dowData.map(d => d.value), 1); const bestDow = dowData.reduce((best, d, i) => d.value > best.value ? { ...d, i } : best, { value: 0, i: -1 }); // Лучший час const hourData = Array.from({ length: 24 }, (_, h) => { const d = bestTime?.by_hour?.find(r => parseInt(r.hour) === h); return { label: `${h}`, value: d?.count || 0 }; }); const maxHour = Math.max(...hourData.map(d => d.value), 1); const bestHour = hourData.reduce((best, d, i) => d.value > best.value ? { ...d, i } : best, { value: 0, i: -1 }); return (
{/* Тулбар */}

Аналитика

{error && (
{error}
)} {/* Карточки-цифры */}
{/* Топ реакций */} {reactionTotals.length > 0 && (
Реакции за {days} дней
{reactionTotals.map(r => ( {r.emoji} {r.total} ))}
)} {/* Лучший день недели */}
Активность по дням недели
{bestDow.value > 0 && (

Чаще всего публикуешь в {DOW_SHORT[bestDow.i]}

)}
{dowData.map((d, i) => ( ))}
{/* Лучший час */}
Активность по часам (МСК, 90 дн.)
{bestHour.value > 0 && (

Пик публикаций в {bestHour.i}:00

)}
{hourData.map((d, i) => ( ))}
{/* Топ постов по реакциям */} {topPosts.length > 0 && (
Топ постов по реакциям
{topPosts.map(p => (
{p.preview}
{p.forwards > 0 && ( {p.forwards} )} {p.published_at ? new Date(p.published_at).toLocaleDateString('ru', { day:'numeric', month:'short' }) : ''} {p.tg_message_id && ( Открыть )}
))}
)} {/* Нет данных */} {!loading && metrics && topPosts.length === 0 && reactionTotals.length === 0 && (

Пока нет данных для отображения

Реакции появятся после первых публикаций через этот канал

)}
); }