import React, { useState, useEffect, useCallback, useRef, Suspense } from 'react'; import { Box, Card, CardContent, Typography, Grid, Chip, LinearProgress, Alert, CircularProgress, IconButton, FormControl, InputLabel, Select, MenuItem, List, ListItem, ListItemText, ListItemIcon, Tooltip, Paper, Dialog, DialogTitle, DialogContent, DialogActions, TextField, Stack, } from '@mui/material'; import Visibility from '@mui/icons-material/Visibility'; import MouseOutlined from '@mui/icons-material/MouseOutlined'; import Search from '@mui/icons-material/Search'; import Web from '@mui/icons-material/Web'; import Refresh from '@mui/icons-material/Refresh'; import Info from '@mui/icons-material/Info'; import CheckCircle from '@mui/icons-material/CheckCircle'; import ErrorIcon from '@mui/icons-material/Error'; import Warning from '@mui/icons-material/Warning'; import TrendingUp from '@mui/icons-material/TrendingUp'; import { Button } from '@mui/material'; import { PlatformAnalytics as PlatformAnalyticsType, AnalyticsSummary, PlatformConnectionStatus } from '../../api/analytics'; import { cachedAnalyticsAPI } from '../../api/cachedAnalytics'; import BingInsightsCard from './BingInsightsCard'; import BackgroundJobManager from './BackgroundJobManager'; import TopPagesInsightsPanel from './TopPagesInsightsPanel'; import GscSuggestionsPanel from './GscSuggestionsPanel'; import RefreshQueuePanel from './RefreshQueuePanel'; import ChipLegend from './ChipLegend'; import { apiClient } from '../../api/client'; import { LazyBarChart, LazyLineChart, XAxis, YAxis, CartesianGrid, Tooltip as RechartsTooltip, ResponsiveContainer, Bar, Line, ChartLoadingFallback, } from '../../utils/lazyRecharts'; interface CannibalizationPage { page: string; clicks: number; impressions: number; ctr: number; } interface CannibalizationAlert { query: string; total_clicks: number; recommended_target_page?: string; pages?: CannibalizationPage[]; } interface CannibalizationAlertsPanelProps { alerts: CannibalizationAlert[]; formatNumber: (n: number) => string; isValidHttpUrl: (url: string) => boolean; onOpenBrief: (page: string, query: string, totalClicks: number) => void; } const CannibalizationAlertsPanel: React.FC = ({ alerts, formatNumber, isValidHttpUrl, onOpenBrief, }) => { return ( Cannibalization Alerts Queries competing across pages “No cannibalization” is normal for tightly targeted sites or low‑traffic windows. For demos we can relax sensitivity. , tooltip: 'Each chip is a page that shares the same query. Text shows URL • clicks • impressions • CTR.', sx: { backgroundImage: 'linear-gradient(135deg, #e2e8f0 0%, #f8fafc 100%)', color: '#0f172a', border: '1px solid #cbd5e1', boxShadow: '0 1px 2px rgba(0,0,0,0.05)', fontWeight: 700, }, }, { label: 'Higher CTR', tooltip: 'Greener backgrounds mean this page converts searchers relatively well.', sx: { backgroundImage: 'linear-gradient(135deg, #d1fae5 0%, #ecfdf5 100%)', color: '#065f46', border: '1px solid #86efac', boxShadow: '0 1px 2px rgba(0,0,0,0.05)', fontWeight: 700, }, }, { label: 'Weaker CTR', tooltip: 'Redder backgrounds flag pages that may need consolidation or updates.', sx: { backgroundImage: 'linear-gradient(135deg, #fee2e2 0%, #fff1f2 100%)', color: '#7f1d1d', border: '1px solid #fecdd3', boxShadow: '0 1px 2px rgba(0,0,0,0.05)', fontWeight: 700, }, }, ]} /> {(!alerts || alerts.length === 0) ? ( No cannibalization detected for this window. ) : ( {alerts.slice(0, 10).map((a, idx) => ( Total clicks: {formatNumber(a.total_clicks || 0)} • Target: {a.recommended_target_page || 'N/A'} {(a.pages || []).map((p, i) => { const clicks = Number(p.clicks || 0); const impressions = Number(p.impressions || 0); const ctr = Number(p.ctr || 0); const ctrColor = ctr >= 3 ? '#065f46' : ctr >= 1 ? '#92400e' : '#7f1d1d'; const ctrBg = ctr >= 3 ? 'linear-gradient(135deg, #d1fae5 0%, #ecfdf5 100%)' : ctr >= 1 ? 'linear-gradient(135deg, #fef3c7 0%, #fffbeb 100%)' : 'linear-gradient(135deg, #fee2e2 0%, #fff1f2 100%)'; const label = `${String(p.page || '').replace(/^https?:\/\//, '').slice(0, 40)} • ${formatNumber(clicks)}c/${formatNumber(impressions)}i • ${ctr.toFixed(1)}%`; return ( ); })} } primaryTypographyProps={{ variant: 'body2' }} /> ))} )} ); }; interface PlatformAnalyticsComponentProps { platforms?: string[]; showSummary?: boolean; refreshInterval?: number; // in milliseconds, 0 = no auto-refresh onDataLoaded?: (data: any) => void; onRefreshReady?: (refreshFn: () => Promise) => void; // Expose refresh function to parent onReconnect?: (platform: string) => void; // Reconnect handler for individual platforms showBackgroundJobs?: boolean; // Only render background jobs when user triggers } const PlatformAnalytics: React.FC = ({ platforms, showSummary = true, refreshInterval = 0, onDataLoaded, onRefreshReady, onReconnect, showBackgroundJobs = false, }) => { const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [analyticsData, setAnalyticsData] = useState>({}); const [summary, setSummary] = useState(null); const [platformStatus, setPlatformStatus] = useState>({}); const [lastUpdated, setLastUpdated] = useState(null); const [priorityPlatform, setPriorityPlatform] = useState<'auto' | 'gsc' | 'bing'>('auto'); const [rangeDays, setRangeDays] = useState(30); const [suggestions, setSuggestions] = useState>([]); const [refreshQueue, setRefreshQueue] = useState<{ risingQueries: Array<{ query: string; deltaClicks: number; deltaImpressions: number }>; decliningQueries: Array<{ query: string; deltaClicks: number; deltaImpressions: number }>; }>({ risingQueries: [], decliningQueries: [] }); const [loadingQueue, setLoadingQueue] = useState(false); const [briefOpen, setBriefOpen] = useState(false); const [briefData, setBriefData] = useState<{ page: string; queries: Array<{ query: string; clicks: number; impressions: number; ctr: number }> } | null>(null); const [aiLoading, setAiLoading] = useState(false); const [aiError, setAiError] = useState(null); const [aiInsights, setAiInsights] = useState(null); const [resyncAttempted, setResyncAttempted] = useState(false); const [bingCollecting, setBingCollecting] = useState(false); const [bingCollectMsg, setBingCollectMsg] = useState(null); const [bingSiteUrl, setBingSiteUrl] = useState(''); const [showLegend, setShowLegend] = useState(false); const onDataLoadedRef = useRef(); const onRefreshReadyRef = useRef(); useEffect(() => { onDataLoadedRef.current = onDataLoaded; }, [onDataLoaded]); useEffect(() => { onRefreshReadyRef.current = onRefreshReady; }, [onRefreshReady]); const loadData = useCallback(async () => { try { setLoading(true); setError(null); // Load platform connection status const statusResponse = await cachedAnalyticsAPI.getPlatformStatus(); setPlatformStatus(statusResponse.platforms); const bingSitesResp: any[] = (statusResponse.platforms?.['bing']?.sites || []); // Load analytics data const end = new Date(); const start = new Date(end); start.setDate(end.getDate() - (rangeDays - 1)); const fmt = (d: Date) => d.toISOString().slice(0, 10); const analyticsResponse = await cachedAnalyticsAPI.getAnalyticsData(platforms, false, { start_date: fmt(start), end_date: fmt(end), }); console.log('PlatformAnalytics: analyticsResponse', analyticsResponse); setAnalyticsData(analyticsResponse.data as Record); setSummary(analyticsResponse.summary); setLastUpdated(new Date()); // Initialize Bing site URL preference with safe fallbacks (avoid backend lookup on failure) let initialSite = ''; if (bingSitesResp && bingSitesResp.length > 0) { const preferred = bingSitesResp.find(s => typeof s?.Url === 'string')?.Url || bingSitesResp.find(s => typeof s?.url === 'string')?.url || ''; initialSite = preferred; } if (!initialSite) { const ls = (typeof window !== 'undefined') ? (localStorage.getItem('website_url') || sessionStorage.getItem('website_url') || '') : ''; initialSite = ls || ''; } if (initialSite) { setBingSiteUrl(initialSite); } const dataCallback = onDataLoadedRef.current; if (dataCallback) { dataCallback({ analytics: analyticsResponse.data, summary: analyticsResponse.summary, status: statusResponse.platforms, }); } const gsc = (analyticsResponse.data as any)['gsc'] as PlatformAnalyticsType | undefined; if (gsc && gsc.status === 'success') { const tq = (gsc.metrics as any)?.top_queries || []; const impThreshold = rangeDays <= 7 ? 100 : rangeDays <= 30 ? 500 : 1500; const ctrThreshold = 2.5; let filtered = tq .filter((row: any) => { const impressions = Number(row.impressions || 0); const ctr = Number(row.ctr || 0); return impressions >= impThreshold && ctr > 0 && ctr <= ctrThreshold; }) .map((row: any) => ({ query: String(row.query || ''), impressions: Number(row.impressions || 0), ctr: Number(row.ctr || 0), position: Number(row.position || 0), })); if (filtered.length === 0 && Array.isArray(tq) && tq.length > 0) { // Fallback: show lowest-CTR queries with decent impressions const fallback = [...tq] .filter((row: any) => Number(row.impressions || 0) >= Math.max(20, Math.floor(impThreshold / 2))) .sort((a: any, b: any) => Number(a.ctr || 0) - Number(b.ctr || 0)) .slice(0, 5) .map((row: any) => ({ query: String(row.query || ''), impressions: Number(row.impressions || 0), ctr: Number(row.ctr || 0), position: Number(row.position || 0), })); filtered = fallback; } setSuggestions(filtered.slice(0, 10)); } else { setSuggestions([]); } } catch (err: unknown) { console.error('Error loading analytics data:', err); let errorMessage = 'Failed to load analytics data'; if (err instanceof Error) { errorMessage = (err as Error).message; } else if (typeof err === 'string') { errorMessage = err; } setError(errorMessage); } finally { setLoading(false); } }, [platforms, rangeDays]); // Method to force refresh (bypass cache) const forceRefresh = useCallback(async () => { setLoading(true); setError(null); try { // Clear cache and force fresh data const end = new Date(); const start = new Date(end); start.setDate(end.getDate() - (rangeDays - 1)); const fmt = (d: Date) => d.toISOString().slice(0, 10); await cachedAnalyticsAPI.forceRefreshAnalyticsData(platforms, { start_date: fmt(start), end_date: fmt(end), }); // Reload data await loadData(); } catch (err) { console.error('PlatformAnalytics: Force refresh failed:', err); setError(err instanceof Error ? err.message : 'Failed to refresh data'); } finally { setLoading(false); } }, [platforms, loadData, rangeDays]); // Auto-resync when Bing status shows connected but analytics returns token errors (post-OAuth page reload) useEffect(() => { if (resyncAttempted) return; const status = platformStatus?.['bing']; const bing = analyticsData?.['bing']; const connected = !!status?.connected; const hasTokenError = !!(bing && bing.status === 'error' && /token|expired|not connected|oauth/i.test(bing.error_message || '')); if (connected && hasTokenError) { setResyncAttempted(true); (async () => { try { await cachedAnalyticsAPI.invalidatePlatformStatus(); await cachedAnalyticsAPI.forceRefreshAnalyticsData(['bing']); await loadData(); } catch (e) { // swallow; user can force refresh } })(); } }, [platformStatus, analyticsData, resyncAttempted, loadData]); const computeRefreshQueue = useCallback(async () => { try { setLoadingQueue(true); const end = new Date(); const start = new Date(end); start.setDate(end.getDate() - (rangeDays - 1)); const prevEnd = new Date(start); prevEnd.setDate(start.getDate() - 1); const prevStart = new Date(prevEnd); prevStart.setDate(prevEnd.getDate() - (rangeDays - 1)); const fmt = (d: Date) => d.toISOString().slice(0, 10); let currentGSC = (analyticsData['gsc'] as PlatformAnalyticsType | undefined); if (!currentGSC) { const currentResp = await cachedAnalyticsAPI.getAnalyticsData(['gsc'], false, { start_date: fmt(start), end_date: fmt(end), }); currentGSC = (currentResp.data as any)['gsc'] as PlatformAnalyticsType | undefined; } const prevResp = await cachedAnalyticsAPI.getAnalyticsData(['gsc'], false, { start_date: fmt(prevStart), end_date: fmt(prevEnd), }); const prevGSC = (prevResp.data as any)['gsc'] as PlatformAnalyticsType | undefined; const currQueries = (currentGSC?.metrics as any)?.top_queries || []; const prevQueries = (prevGSC?.metrics as any)?.top_queries || []; const prevMap: Record = {}; prevQueries.forEach((q: any) => { const key = String(q.query || '').toLowerCase(); prevMap[key] = { clicks: Number(q.clicks || 0), impressions: Number(q.impressions || 0) }; }); const rising: Array<{ query: string; deltaClicks: number; deltaImpressions: number }> = []; const declining: Array<{ query: string; deltaClicks: number; deltaImpressions: number }> = []; const riseClicksThresh = rangeDays <= 7 ? 5 : rangeDays <= 30 ? 20 : 40; const riseImprThresh = rangeDays <= 7 ? 50 : rangeDays <= 30 ? 200 : 500; const dropClicksThresh = -riseClicksThresh; const dropImprThresh = -riseImprThresh; currQueries.forEach((q: any) => { const key = String(q.query || '').toLowerCase(); const prev = prevMap[key] || { clicks: 0, impressions: 0 }; const deltaClicks = Number(q.clicks || 0) - prev.clicks; const deltaImpressions = Number(q.impressions || 0) - prev.impressions; if (deltaClicks > 0 && deltaImpressions > 0 && (deltaClicks >= riseClicksThresh || deltaImpressions >= riseImprThresh)) { rising.push({ query: String(q.query || ''), deltaClicks, deltaImpressions }); } if (deltaClicks < 0 && deltaImpressions <= 0 && (deltaClicks <= dropClicksThresh || deltaImpressions <= dropImprThresh)) { declining.push({ query: String(q.query || ''), deltaClicks, deltaImpressions }); } }); rising.sort((a, b) => (b.deltaClicks + b.deltaImpressions) - (a.deltaClicks + a.deltaImpressions)); declining.sort((a, b) => (a.deltaClicks + a.deltaImpressions) - (b.deltaClicks + b.deltaImpressions)); // Fallback: if none meet thresholds, show the most changed queries by absolute delta if (rising.length === 0 && declining.length === 0) { const deltas: Array<{ query: string; deltaClicks: number; deltaImpressions: number; score: number }> = []; currQueries.forEach((q: any) => { const key = String(q.query || '').toLowerCase(); const prev = prevMap[key] || { clicks: 0, impressions: 0 }; const dC = Number(q.clicks || 0) - prev.clicks; const dI = Number(q.impressions || 0) - prev.impressions; const score = Math.abs(dC) + Math.abs(dI); if (score > 0) { deltas.push({ query: String(q.query || ''), deltaClicks: dC, deltaImpressions: dI, score }); } }); deltas.sort((a, b) => b.score - a.score); const top = deltas.slice(0, 10); if (top.length === 0 && Array.isArray(currQueries) && currQueries.length > 0) { const topByClicks = [...currQueries] .sort((a: any, b: any) => Number(b.clicks || 0) - Number(a.clicks || 0)) .slice(0, 10); setRefreshQueue({ risingQueries: topByClicks.map((q: any) => ({ query: String(q.query || ''), deltaClicks: Number(q.clicks || 0), deltaImpressions: Number(q.impressions || 0), })), decliningQueries: [], }); } else { setRefreshQueue({ risingQueries: top.filter(d => d.deltaClicks > 0 || d.deltaImpressions > 0).map(({ score, ...rest }) => rest), decliningQueries: top.filter(d => d.deltaClicks < 0 || d.deltaImpressions < 0).map(({ score, ...rest }) => rest), }); } } else { setRefreshQueue({ risingQueries: rising.slice(0, 10), decliningQueries: declining.slice(0, 10) }); } } catch (e) { console.error('Error computing refresh queue:', e); setRefreshQueue({ risingQueries: [], decliningQueries: [] }); } finally { setLoadingQueue(false); } }, [rangeDays, analyticsData]); // One-run guard to prevent duplicate calls in StrictMode const dataLoadedRef = useRef(false); useEffect(() => { if (dataLoadedRef.current) return; dataLoadedRef.current = true; loadData(); // Listen for Bing OAuth success/error to invalidate caches and refresh const handleMessage = (event: MessageEvent) => { const data: any = event?.data; if (!data || typeof data !== 'object') return; if (data.type === 'BING_OAUTH_SUCCESS') { try { cachedAnalyticsAPI.invalidatePlatformStatus(); cachedAnalyticsAPI.invalidateAnalyticsData(); } catch {} forceRefresh(); } if (data.type === 'BING_OAUTH_ERROR') { try { cachedAnalyticsAPI.invalidatePlatformStatus(); } catch {} } }; window.addEventListener('message', handleMessage); // Set up auto-refresh if interval is specified let interval: NodeJS.Timeout | null = null; if (refreshInterval > 0) { interval = setInterval(loadData, refreshInterval); } return () => { if (interval) { clearInterval(interval); } window.removeEventListener('message', handleMessage); }; }, [refreshInterval, loadData, forceRefresh]); // Reload data when the date range changes after initial mount useEffect(() => { if (!dataLoadedRef.current) return; loadData(); }, [rangeDays]); // Auto-compute refresh queue only when background jobs/advanced insights are enabled useEffect(() => { if (!dataLoadedRef.current) return; if (!lastUpdated) return; if (!showBackgroundJobs) return; computeRefreshQueue(); }, [rangeDays, lastUpdated, computeRefreshQueue, showBackgroundJobs]); // Expose refresh function to parent component useEffect(() => { const cb = onRefreshReadyRef.current; if (cb) { cb(forceRefresh); } }, [forceRefresh]); const getPlatformIcon = (platform: string) => { switch (platform.toLowerCase()) { case 'gsc': return ; case 'wix': return ; case 'wordpress': return ; case 'bing': return ; default: return ; } }; const getStatusColor = (status: string) => { switch (status) { case 'success': return 'success'; case 'error': return 'error'; case 'partial': return 'warning'; default: return 'default'; } }; const getStatusIcon = (status: string) => { switch (status) { case 'success': return ; case 'error': return ; case 'partial': return ; default: return ; } }; const isValidHttpUrl = (value: string) => { try { const u = new URL(value); return u.protocol === 'http:' || u.protocol === 'https:'; } catch { return false; } }; const formatNumber = (num: number) => { if (num >= 1000000) { return (num / 1000000).toFixed(1) + 'M'; } else if (num >= 1000) { return (num / 1000).toFixed(1) + 'K'; } return num.toString(); }; // Compute summary display based on priority and available platform data const computedSummary = React.useMemo(() => { const gsc = analyticsData['gsc']; const bing = analyticsData['bing']; const isGscOk = gsc && (gsc.status === 'success' || gsc.status === 'partial'); const isBingOk = bing && (bing.status === 'success' || bing.status === 'partial'); const sumFromTopPages = (metrics?: any) => { const pages = Array.isArray(metrics?.top_pages) ? metrics.top_pages : []; if (!pages.length) { return { clicks: 0, impressions: 0 }; } let clicks = 0; let impressions = 0; for (const row of pages) { clicks += Number(row?.clicks || 0); impressions += Number(row?.impressions || 0); } return { clicks, impressions }; }; const pick = (m?: any) => ({ clicks: Number(m?.total_clicks || 0), impressions: Number(m?.total_impressions || 0), }); if (priorityPlatform === 'auto') { if (isGscOk) { let g = pick(gsc.metrics); if (g.clicks === 0) { const fromPages = sumFromTopPages(gsc.metrics); if (fromPages.clicks > 0) { g = { clicks: fromPages.clicks, impressions: g.impressions || fromPages.impressions, }; } } return { clicks: g.clicks, impressions: g.impressions, label: 'GSC (Auto)', na: g.clicks === 0 && g.impressions === 0, }; } if (summary) { const clicks = Number(summary.total_clicks || 0); const impressions = Number(summary.total_impressions || 0); return { clicks, impressions, label: 'Combined', na: clicks === 0 && impressions === 0, }; } return { clicks: 0, impressions: 0, label: 'Combined', na: true as const }; } if (priorityPlatform === 'gsc') { if (isGscOk) { let g = pick(gsc.metrics); if (g.clicks === 0) { const fromPages = sumFromTopPages(gsc.metrics); if (fromPages.clicks > 0) { g = { clicks: fromPages.clicks, impressions: g.impressions || fromPages.impressions, }; } } return { ...g, label: 'GSC' }; } return { clicks: 0, impressions: 0, label: 'GSC', na: true }; } if (priorityPlatform === 'bing') { if (isBingOk) return { ...pick(bing.metrics), label: 'Bing' }; return { clicks: 0, impressions: 0, label: 'Bing', na: true }; } return { clicks: 0, impressions: 0, label: 'N/A', na: true }; }, [analyticsData, priorityPlatform, summary]); useEffect(() => { console.log('PlatformAnalytics: debug summary/computedSummary', { priorityPlatform, summary, computedSummary, analyticsData, platformStatus, }); }, [summary, computedSummary, analyticsData, priorityPlatform, platformStatus]); const renderMetricsCard = (platform: string, data: PlatformAnalyticsType) => { const metrics = data.metrics; return ( {getPlatformIcon(platform)} {platform.toUpperCase()} {getStatusIcon(data.status)} {platform === 'bing' && ( <> setBingSiteUrl(e.target.value)} sx={{ minWidth: 280 }} label="Bing Site URL" /> )} {data.status === 'success' && ( <> {metrics.total_clicks !== undefined && ( {formatNumber(metrics.total_clicks)} Clicks )} {metrics.total_impressions !== undefined && ( {formatNumber(metrics.total_impressions)} Impressions )} {metrics.avg_ctr !== undefined && ( CTR {metrics.avg_ctr}% )} {metrics.avg_position !== undefined && ( Avg Position {metrics.avg_position.toFixed(1)} )} {metrics.top_queries && metrics.top_queries.length > 0 && ( Top Queries {metrics.top_queries.slice(0, 3).map((q: any, index: number) => { const clicks = Number(q.clicks || 0); const impressions = Number(q.impressions || 0); const ctr = Number(q.ctr || 0); const ctrColor = ctr >= 3 ? '#065f46' : ctr >= 1 ? '#92400e' : '#7f1d1d'; const ctrBg = ctr >= 3 ? 'linear-gradient(135deg, #d1fae5 0%, #ecfdf5 100%)' : ctr >= 1 ? 'linear-gradient(135deg, #fef3c7 0%, #fffbeb 100%)' : 'linear-gradient(135deg, #fee2e2 0%, #fff1f2 100%)'; const risingSet = new Set((refreshQueue?.risingQueries || []).map(r => String(r.query || '').toLowerCase())); const isTrending = risingSet.has(String(q.query || '').toLowerCase()); return ( {index + 1} {q.query} {isTrending && ( } label="Trending" size="small" sx={{ mt: 0.5, backgroundImage: 'linear-gradient(135deg, #ecfdf5 0%, #ffffff 100%)', color: '#065f46', border: '1px solid #a7f3d0', boxShadow: '0 1px 2px rgba(0,0,0,0.04)', fontWeight: 700 }} /> )} } label={`${formatNumber(clicks)}`} size="small" sx={{ backgroundImage: 'linear-gradient(135deg, #dbeafe 0%, #eef2ff 100%)', color: '#1e3a8a', border: '1px solid #c7d2fe', boxShadow: '0 1px 2px rgba(0,0,0,0.05)', fontWeight: 700 }} /> } label={`${formatNumber(impressions)}`} size="small" sx={{ backgroundImage: 'linear-gradient(135deg, #e2e8f0 0%, #f8fafc 100%)', color: '#0f172a', border: '1px solid #cbd5e1', boxShadow: '0 1px 2px rgba(0,0,0,0.05)', fontWeight: 700 }} /> ); })} )} )} {data.status === 'error' && ( {data.error_message || 'Failed to load analytics data'} {platform === 'bing' && bingCollectMsg && ( {bingCollectMsg} )} {onReconnect && ( )} )} {data.status === 'partial' && ( {data.error_message || 'Limited analytics data available'} )} Last updated: {data.last_updated ? new Date(data.last_updated).toLocaleString() : 'Never'} ); }; const renderSummaryCard = () => { if (!summary) return null; const totalClicks = computedSummary.clicks || 0; const totalImpressions = computedSummary.impressions || 0; const connectedCount = Object.values(platformStatus).filter(s => s.connected).length; const ctrDisplay = totalImpressions > 0 ? ((totalClicks / totalImpressions) * 100).toFixed(2) : 'N/A'; const bingStatus = platformStatus['bing']; const bingConnected = !!bingStatus?.connected; const bingLastSync = (analyticsData['bing']?.last_updated) ? new Date(analyticsData['bing']!.last_updated).toLocaleString() : (bingStatus as any)?.last_sync || null; const gscMetrics: any = (analyticsData['gsc'] as any)?.metrics || {}; const topPagesRaw: any[] = Array.isArray(gscMetrics.top_pages) ? gscMetrics.top_pages : []; const topPagesChart = topPagesRaw .slice() .sort((a, b) => Number(b?.clicks || 0) - Number(a?.clicks || 0)) .slice(0, 5) .map((p) => ({ label: String(p?.page || '') .replace(/^https?:\/\//, '') .replace(/^www\./, '') .slice(0, 26), clicks: Number(p?.clicks || 0), impressions: Number(p?.impressions || 0), ctr: Number(p?.ctr || 0), fullUrl: String(p?.page || ''), })); const topQueriesRaw: any[] = Array.isArray(gscMetrics.top_queries) ? gscMetrics.top_queries : []; const ctrPositionData = topQueriesRaw .filter((q) => typeof q?.position !== 'undefined' && typeof q?.ctr !== 'undefined') .slice(0, 40) .map((q) => ({ query: String(q?.query || ''), position: Number(q?.position || 0), ctr: Number(q?.ctr || 0), })); return ( Analytics Summary Platform Health {bingLastSync ? `Last sync: ${bingLastSync}` : 'Last sync: N/A'} {lastUpdated && ( Last refreshed: {lastUpdated.toLocaleString()} )} Platform View Date Range {connectedCount} Connected Platforms {computedSummary.na ? 'N/A' : formatNumber(totalClicks)} Total Clicks {computedSummary.na ? 'N/A' : formatNumber(totalImpressions)} Total Impressions {typeof ctrDisplay === 'string' ? ctrDisplay : `${ctrDisplay}%`} Overall CTR {(totalClicks === 0 && totalImpressions === 0) && ( {computedSummary.na ? 'Failed to fetch analytics for selected view.' : 'No recent search traffic detected.'} )} {(topPagesChart.length > 0 || ctrPositionData.length > 0) && ( {topPagesChart.length > 0 && ( Top pages impact Where most of your clicks are concentrated in this window. }> { if (name === 'clicks') { return [formatNumber(Number(value || 0)), 'Clicks']; } if (name === 'impressions') { return [formatNumber(Number(value || 0)), 'Impressions']; } if (name === 'ctr') { return [`${Number(value || 0).toFixed(2)}%`, 'CTR']; } return [value, name]; }} labelFormatter={(label: any, payload: any) => { const full = payload && payload[0] && (payload[0].payload as any)?.fullUrl; return full || String(label || ''); }} /> )} {ctrPositionData.length > 0 && ( CTR vs average position How click‑through rate changes as your queries move up and down. }> `${v}%`} tickLine={false} /> { if (name === 'ctr') { return [`${Number(value || 0).toFixed(2)}%`, 'CTR']; } return [value, name]; }} labelFormatter={(label: any, payload: any) => { const q = payload && payload[0] && (payload[0].payload as any)?.query; return `Position ${label}${q ? ` • ${q}` : ''}`; }} /> )} )} {showLegend && ( Metric legend How to read the chips across this step , tooltip: 'Total visits from Google for this item in the selected date range.', sx: { backgroundImage: 'linear-gradient(135deg, #dbeafe 0%, #eef2ff 100%)', color: '#1e3a8a', border: '1px solid #c7d2fe', boxShadow: '0 1px 2px rgba(0,0,0,0.05)', fontWeight: 700 }, }, { label: 'Impressions', icon: , tooltip: 'How often your result was shown in search. Higher means more visibility.', sx: { backgroundImage: 'linear-gradient(135deg, #e2e8f0 0%, #f8fafc 100%)', color: '#0f172a', border: '1px solid #cbd5e1', boxShadow: '0 1px 2px rgba(0,0,0,0.05)', fontWeight: 700 }, }, { label: 'CTR', tooltip: 'Click‑through rate: clicks ÷ impressions. Higher is better.', sx: { backgroundImage: 'linear-gradient(135deg, #d1fae5 0%, #ecfdf5 100%)', color: '#065f46', border: '1px solid #86efac', boxShadow: '0 1px 2px rgba(0,0,0,0.05)', fontWeight: 700 }, }, ]} /> , tooltip: 'Query is rising versus the previous window. Great candidate to double‑down on.', sx: { backgroundImage: 'linear-gradient(135deg, #ecfdf5 0%, #ffffff 100%)', color: '#065f46', border: '1px solid #a7f3d0', boxShadow: '0 1px 2px rgba(0,0,0,0.05)', fontWeight: 700 }, }, { label: 'Δ Clicks / Δ Impr', icon: , tooltip: 'Change in clicks or impressions versus the previous date window.', sx: { backgroundImage: 'linear-gradient(135deg, #ede9fe 0%, #eff6ff 100%)', color: '#4c1d95', border: '1px solid #ddd6fe', boxShadow: '0 1px 2px rgba(0,0,0,0.05)', fontWeight: 700 }, }, ]} /> )} {(aiError || aiInsights) && ( AI Insights {aiError && {aiError}} {aiInsights && ( {aiInsights.quick_summary} {Array.isArray(aiInsights.prioritized_findings) && aiInsights.prioritized_findings.length > 0 && ( {aiInsights.prioritized_findings.slice(0, 3).map((f: any, i: number) => ( {f.evidence} {(f.actions || []).slice(0, 2).map((a: string, idx: number) => ( ))} } primaryTypographyProps={{ variant: 'body2' }} /> ))} )} )} )} ); }; if (loading) { return ( Loading analytics data... ); } if (error) { return ( {error} ); } return ( {showSummary && renderSummaryCard()} { const queries = [{ query, clicks: totalClicks, impressions: 0, ctr: 0 }]; setBriefData({ page, queries }); setBriefOpen(true); }} /> {(() => { const gsc = analyticsData['gsc']; const pages = (gsc?.metrics as any)?.top_pages || []; return ( { if (url && isValidHttpUrl(String(url))) window.open(String(url), '_blank'); }} onCreateBrief={(page, queries) => { setBriefData({ page: String(page || ''), queries: Array.isArray(queries) ? queries : [] }); setBriefOpen(true); }} formatNumber={formatNumber} /> ); })()} setBriefOpen(false)} fullWidth maxWidth="sm"> Create Content Brief Recent queries pointing to this page {(briefData?.queries || []).slice(0, 10).map((q, i) => ( ))} {(briefData?.queries || []).length === 0 && ( No query mappings available for this window. )} {showBackgroundJobs && ( )} {Object.entries(analyticsData) .filter(([platform]) => platform.toLowerCase() !== 'wordpress') // Exclude WordPress analytics .map(([platform, data]) => ( {renderMetricsCard(platform, data)} ))} {/* Background Job Manager - render only when explicitly enabled */} {showBackgroundJobs && ( { console.log('🎉 Background job completed:', job); // Refresh analytics data when job completes forceRefresh(); }} /> )} {/* Debug section removed */} {/* Bing Insights Card - Show when Bing is connected */} {analyticsData.bing && ( {/* Debug text removed */} {analyticsData.bing.metrics?.connection_status === 'connected' && ( { console.log('Bing insights loaded:', insights); }} /> )} )} {Object.keys(analyticsData).length === 0 && ( No analytics data available. Connect your platforms to see analytics insights. )} ); }; export default PlatformAnalytics;