// Analytics screen — KPIs, charts, per-post table. // Hand-rolled SVG line/bar charts (no external lib). const { useState: useStateAn, useMemo: useMemoAn } = React; function fmt(n) { if (n >= 1000000) return (n / 1000000).toFixed(1) + 'M'; if (n >= 1000) return (n / 1000).toFixed(1) + 'K'; return String(n); } function LineSpark({ data = [], color = '#0F0F0F', height = 80, width = 280 }) { if (!data.length) return null; const max = Math.max(...data, 1); const min = Math.min(...data, 0); const span = Math.max(1, max - min); const stepX = width / (data.length - 1 || 1); const pts = data.map((v, i) => [i * stepX, height - ((v - min) / span) * (height - 10) - 5]); const d = pts.map((p, i) => (i === 0 ? `M${p[0]},${p[1]}` : `L${p[0]},${p[1]}`)).join(' '); const area = d + ` L${width},${height} L0,${height} Z`; return ( {pts.map((p, i) => i === pts.length - 1 && ( ))} ); } function BarSeries({ data = [], color = '#0F0F0F', height = 80, width = 280 }) { if (!data.length) return null; const max = Math.max(...data, 1); const bw = width / data.length; const gap = Math.min(4, bw * 0.25); return ( {data.map((v, i) => { const h = (v / max) * (height - 6); return ; })} ); } function Analytics({ org, analytics, onOpenPost }) { const { Button, Card, Badge, Pill, SectionHead } = window.UI; const I = window.Icons; const [metric, setMetric] = useStateAn('reach'); // reach | impressions | clicks | comments const [range, setRange] = useStateAn('14d'); const [net, setNet] = useStateAn('all'); const METRICS = [ { id: 'reach', label: 'Охват', icon: 'Eye', tint: 'lavender' }, { id: 'impressions', label: 'Показы', icon: 'Layers', tint: 'sky' }, { id: 'clicks', label: 'Клики', icon: 'Link', tint: 'peach' }, { id: 'comments', label: 'Комментарии', icon: 'Text', tint: 'mint' }, ]; const tintBg = { lavender: 'bg-lavender-soft text-lavender-ink', sky: 'bg-sky-soft text-sky-ink', peach: 'bg-peach-soft text-peach-ink', mint: 'bg-mint-soft text-mint-ink', }; const tintCard = { lavender: 'bg-lavender-soft/40', sky: 'bg-sky-soft/40', peach: 'bg-peach-soft/45', mint: 'bg-mint-soft/40', }; const colors = { reach: 'oklch(0.52 0.22 285)', impressions: 'oklch(0.45 0.16 240)', clicks: 'oklch(0.55 0.16 50)', comments: 'oklch(0.50 0.14 165)' }; const filteredPosts = useMemoAn(() => { if (!analytics) return []; if (net === 'all') return analytics.posts; return analytics.posts.filter(p => p.network === net || p.network === 'both'); }, [analytics, net]); if (!analytics) { return (
); } return (
{['7d', '14d', '30d'].map((r) => setRange(r)}>{r === '7d' ? '7 дней' : r === '14d' ? '14 дней' : '30 дней'})}
} /> {/* KPI cards */}
{METRICS.map((m) => { const Icon = I[m.icon]; const value = analytics.total[m.id] || 0; const series = analytics.series[m.id] || []; const last = series[series.length - 1] || 0; const prev = series[series.length - 2] || last; const delta = prev ? Math.round((last - prev) / prev * 100) : 0; const active = metric === m.id; return ( ); })}
{/* Big chart */}
{METRICS.find(x => x.id === metric).label} · 14 дней
{fmt(analytics.total[metric])}
{['all', 'ig', 'tg'].map((n) => ( setNet(n)}> {n === 'all' ? 'Все' : n === 'ig' ? Instagram : Telegram} ))}
{Array.from({ length: 8 }).map((_, i) => ( {(14 - i * 2)} дн ))}
{/* Per-post table */}
По постам
Сортировка: по охвату · сегодня
{net === 'all' ? 'все сети' : net.toUpperCase()}
{/* desktop table */}
Пост Охват Показы Клики CTR Комм. Сохр.
{filteredPosts.map((p, i) => ( ))}
{/* mobile cards */}
{filteredPosts.map((p) => ( ))}
); } function NetworkChip({ net }) { const I = window.Icons; if (net === 'both') { return ( ); } if (net === 'ig') return Instagram; return Telegram; } window.Screens = window.Screens || {}; window.Screens.Analytics = Analytics; window.NetworkChip = NetworkChip;