'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 && (
)}
{/* Карточки-цифры */}
{/* Топ реакций */}
{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 && (
Пока нет данных для отображения
Реакции появятся после первых публикаций через этот канал
)}
);
}