SEO Dashboard Fixes and content planning refactoring
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
// SEO CopilotKit Context Component
|
||||
// Provides real-time context and instructions to CopilotKit
|
||||
|
||||
import React, { useEffect, useRef } from 'react';
|
||||
import React, { useEffect, useRef, useMemo } from 'react';
|
||||
import { useCopilotReadable } from '@copilotkit/react-core';
|
||||
import { useSEOCopilotStore } from '../../stores/seoCopilotStore';
|
||||
|
||||
@@ -27,25 +27,29 @@ const SEOCopilotContext: React.FC<{ children: React.ReactNode }> = ({ children }
|
||||
}
|
||||
}, [personalizationData]);
|
||||
|
||||
// Memoize values to prevent unnecessary re-renders
|
||||
const websiteUrl = useMemo(() => analysisData?.url || '', [analysisData?.url]);
|
||||
const statusData = useMemo(() => ({
|
||||
isLoading,
|
||||
isAnalyzing,
|
||||
isGenerating,
|
||||
error
|
||||
}), [isLoading, isAnalyzing, isGenerating, error]);
|
||||
const suggestionsCount = useMemo(() => Array.isArray(suggestions) ? suggestions.length : 0, [suggestions]);
|
||||
|
||||
// Register SEO analysis data with CopilotKit
|
||||
useCopilotReadable({
|
||||
description: "Current SEO analysis data and insights",
|
||||
value: analysisData,
|
||||
categories: ["seo", "analysis"]
|
||||
});
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
console.log('[CopilotContext] Registered analysis data', !!analysisData);
|
||||
}
|
||||
|
||||
// Provide a flat, explicit website URL for the LLM
|
||||
useCopilotReadable({
|
||||
description: "Current website URL the user is working on",
|
||||
value: analysisData?.url || '',
|
||||
value: websiteUrl,
|
||||
categories: ["seo", "context"]
|
||||
});
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
console.log('[CopilotContext] Registered website URL', analysisData?.url);
|
||||
}
|
||||
|
||||
// Register personalization data with CopilotKit
|
||||
useCopilotReadable({
|
||||
@@ -53,9 +57,6 @@ const SEOCopilotContext: React.FC<{ children: React.ReactNode }> = ({ children }
|
||||
value: personalizationData,
|
||||
categories: ["user", "preferences"]
|
||||
});
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
console.log('[CopilotContext] Registered personalization', !!personalizationData);
|
||||
}
|
||||
|
||||
// Register dashboard layout with CopilotKit
|
||||
useCopilotReadable({
|
||||
@@ -63,9 +64,6 @@ const SEOCopilotContext: React.FC<{ children: React.ReactNode }> = ({ children }
|
||||
value: dashboardLayout,
|
||||
categories: ["ui", "layout"]
|
||||
});
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
console.log('[CopilotContext] Registered layout', !!dashboardLayout);
|
||||
}
|
||||
|
||||
// Register suggestions with CopilotKit
|
||||
useCopilotReadable({
|
||||
@@ -73,24 +71,25 @@ const SEOCopilotContext: React.FC<{ children: React.ReactNode }> = ({ children }
|
||||
value: suggestions,
|
||||
categories: ["actions", "suggestions"]
|
||||
});
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
console.log('[CopilotContext] Registered suggestions', Array.isArray(suggestions) ? suggestions.length : 0);
|
||||
}
|
||||
|
||||
// Register loading states with CopilotKit
|
||||
useCopilotReadable({
|
||||
description: "Current loading and processing states",
|
||||
value: {
|
||||
isLoading,
|
||||
isAnalyzing,
|
||||
isGenerating,
|
||||
error
|
||||
},
|
||||
value: statusData,
|
||||
categories: ["status", "loading"]
|
||||
});
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
console.log('[CopilotContext] Registered status', { isLoading, isAnalyzing, isGenerating, hasError: !!error });
|
||||
}
|
||||
|
||||
// Debug logging only in development and only when values actually change
|
||||
useEffect(() => {
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
console.log('[CopilotContext] Registered analysis data', !!analysisData);
|
||||
console.log('[CopilotContext] Registered website URL', websiteUrl);
|
||||
console.log('[CopilotContext] Registered personalization', !!personalizationData);
|
||||
console.log('[CopilotContext] Registered layout', !!dashboardLayout);
|
||||
console.log('[CopilotContext] Registered suggestions', suggestionsCount);
|
||||
console.log('[CopilotContext] Registered status', { isLoading, isAnalyzing, isGenerating, hasError: !!error });
|
||||
}
|
||||
}, [analysisData, websiteUrl, personalizationData, dashboardLayout, suggestionsCount, statusData]);
|
||||
|
||||
return <>{children}</>;
|
||||
};
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import {
|
||||
Box,
|
||||
Container,
|
||||
@@ -7,10 +7,28 @@ import {
|
||||
Alert,
|
||||
Skeleton,
|
||||
Chip,
|
||||
Button
|
||||
Button,
|
||||
IconButton,
|
||||
Tooltip,
|
||||
Menu,
|
||||
MenuItem,
|
||||
Divider,
|
||||
Avatar
|
||||
} from '@mui/material';
|
||||
import { motion, AnimatePresence } from 'framer-motion';
|
||||
import { useAuth, useUser, SignInButton, SignOutButton } from '@clerk/clerk-react';
|
||||
import { apiClient } from '../../api/client';
|
||||
import {
|
||||
Search as SearchIcon,
|
||||
Refresh as RefreshIcon,
|
||||
Person as PersonIcon,
|
||||
ExitToApp as ExitIcon,
|
||||
ArrowBack as ArrowBackIcon,
|
||||
MoreVert as MoreVertIcon,
|
||||
CheckCircle as CheckCircleIcon,
|
||||
Schedule as ScheduleIcon,
|
||||
Info as InfoIcon
|
||||
} from '@mui/icons-material';
|
||||
|
||||
// Shared components
|
||||
import { DashboardContainer, GlassCard } from '../shared/styled';
|
||||
@@ -28,6 +46,14 @@ import { useSEODashboardStore } from '../../stores/seoDashboardStore';
|
||||
// API
|
||||
import { userDataAPI } from '../../api/userData';
|
||||
|
||||
// Shared components
|
||||
import PlatformAnalytics from '../shared/PlatformAnalytics';
|
||||
import { cachedAnalyticsAPI } from '../../api/cachedAnalytics';
|
||||
|
||||
// OAuth hooks
|
||||
import { useBingOAuth } from '../../hooks/useBingOAuth';
|
||||
import { useGSCConnection } from '../OnboardingWizard/common/useGSCConnection';
|
||||
|
||||
// SEO Dashboard component
|
||||
const SEODashboard: React.FC = () => {
|
||||
// Clerk authentication hooks
|
||||
@@ -51,6 +77,35 @@ const SEODashboard: React.FC = () => {
|
||||
getAnalysisFreshness,
|
||||
} = useSEODashboardStore();
|
||||
|
||||
// OAuth hooks
|
||||
const { connect: connectBing } = useBingOAuth();
|
||||
const { handleGSCConnect } = useGSCConnection();
|
||||
|
||||
// Platform status state
|
||||
const [platformStatus, setPlatformStatus] = useState({
|
||||
gsc: { connected: false, sites: [], last_sync: null, status: 'disconnected' },
|
||||
bing: {
|
||||
connected: false,
|
||||
sites: [],
|
||||
last_sync: null,
|
||||
status: 'disconnected',
|
||||
has_expired_tokens: false,
|
||||
last_token_date: undefined,
|
||||
total_tokens: 0
|
||||
}
|
||||
});
|
||||
|
||||
// Menu state
|
||||
const [userMenuAnchor, setUserMenuAnchor] = useState<null | HTMLElement>(null);
|
||||
const [statusMenuAnchor, setStatusMenuAnchor] = useState<null | HTMLElement>(null);
|
||||
const [lastRefresh, setLastRefresh] = useState<Date | null>(null);
|
||||
|
||||
// Competitor analysis data from onboarding step 3
|
||||
const [competitorAnalysisData, setCompetitorAnalysisData] = useState<any>(null);
|
||||
|
||||
// PlatformAnalytics refresh handle
|
||||
const platformRefreshRef = useRef<(() => Promise<void>) | null>(null);
|
||||
|
||||
// Sync dashboard analysis to Copilot store so readables have URL/context
|
||||
const setCopilotAnalysisData = useSEOCopilotStore(state => state.setAnalysisData);
|
||||
useEffect(() => {
|
||||
@@ -62,17 +117,112 @@ const SEODashboard: React.FC = () => {
|
||||
}
|
||||
}, [analysisData, setCopilotAnalysisData]);
|
||||
|
||||
// Load competitor analysis data on component mount
|
||||
useEffect(() => {
|
||||
// Simulate fetching dashboard data
|
||||
const fetchData = async () => {
|
||||
loadCompetitorAnalysisData();
|
||||
}, []);
|
||||
|
||||
// Reconnect handlers using existing OAuth hooks
|
||||
const handleGSCReconnect = async () => {
|
||||
try {
|
||||
console.log('Initiating GSC reconnect...');
|
||||
await handleGSCConnect();
|
||||
} catch (error) {
|
||||
console.error('Error reconnecting GSC:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleBingReconnect = async () => {
|
||||
try {
|
||||
console.log('Initiating Bing reconnect...');
|
||||
// Purge expired tokens before reconnecting to avoid refresh loops
|
||||
try {
|
||||
await apiClient.post('/bing/purge-expired');
|
||||
console.log('Purged expired Bing tokens before reconnect');
|
||||
} catch (purgeError) {
|
||||
console.warn('Failed to purge expired tokens (non-critical):', purgeError);
|
||||
}
|
||||
await connectBing();
|
||||
// After successful reconnect, refresh platform status and run analysis
|
||||
try {
|
||||
// Invalidate backend analytics cache for Bing
|
||||
try {
|
||||
await apiClient.post('/api/analytics/cache/clear', null, { params: { platform: 'bing' } });
|
||||
console.log('Cleared backend analytics cache for Bing');
|
||||
} catch (cacheErr) {
|
||||
console.warn('Failed to clear backend analytics cache (non-critical):', cacheErr);
|
||||
}
|
||||
|
||||
// Invalidate frontend cached analytics
|
||||
try {
|
||||
cachedAnalyticsAPI.invalidatePlatformStatus();
|
||||
// Optional: clear all analytics cache if available
|
||||
// @ts-ignore - method may not exist in older builds
|
||||
cachedAnalyticsAPI.clearCache?.();
|
||||
console.log('Cleared frontend analytics cache');
|
||||
} catch (feCacheErr) {
|
||||
console.warn('Failed to clear frontend analytics cache (non-critical):', feCacheErr);
|
||||
}
|
||||
|
||||
await fetchPlatformStatus();
|
||||
} catch (e) {
|
||||
console.warn('Post-reconnect platform status refresh failed:', e);
|
||||
}
|
||||
try {
|
||||
await useSEODashboardStore.getState().refreshSEOAnalysis();
|
||||
} catch (e) {
|
||||
console.warn('Post-reconnect analysis refresh failed:', e);
|
||||
}
|
||||
|
||||
// Force PlatformAnalytics to refresh (bypass cache)
|
||||
try {
|
||||
await platformRefreshRef.current?.();
|
||||
} catch (e) {
|
||||
console.warn('Platform analytics forced refresh failed (non-critical):', e);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error reconnecting Bing:', error);
|
||||
}
|
||||
};
|
||||
|
||||
// One-run guard to avoid duplicate fetches under StrictMode
|
||||
const dataFetchedRef = useRef(false);
|
||||
|
||||
// Consolidated data fetching effect
|
||||
useEffect(() => {
|
||||
if (dataFetchedRef.current || !isSignedIn) return;
|
||||
dataFetchedRef.current = true;
|
||||
|
||||
const fetchAllData = async () => {
|
||||
let websiteUrl = 'https://alwrity.com'; // Default fallback
|
||||
|
||||
try {
|
||||
setLoading(true);
|
||||
|
||||
// Get user's website URL from user data
|
||||
const userData = await userDataAPI.getUserData();
|
||||
const websiteUrl = userData?.website_url || 'https://alwrity.com';
|
||||
// Fetch platform status and user data in parallel
|
||||
const [platformResponse, userData] = await Promise.all([
|
||||
apiClient.get('/api/seo-dashboard/platforms'),
|
||||
userDataAPI.getUserData()
|
||||
]);
|
||||
|
||||
// Mock data for demonstration
|
||||
console.log('Platform status response:', platformResponse.status, platformResponse.statusText);
|
||||
console.log('Platform status data:', platformResponse.data);
|
||||
setPlatformStatus(platformResponse.data);
|
||||
|
||||
websiteUrl = userData?.website_url || 'https://alwrity.com';
|
||||
|
||||
// Fetch real data from backend using authenticated API client
|
||||
console.log('Fetching SEO dashboard overview...');
|
||||
const response = await apiClient.get('/api/seo-dashboard/overview', {
|
||||
params: { site_url: websiteUrl }
|
||||
});
|
||||
|
||||
console.log('SEO overview response:', response.status, response.statusText);
|
||||
console.log('Real SEO data received:', response.data);
|
||||
setData(response.data);
|
||||
} catch (error) {
|
||||
console.error('Error fetching SEO dashboard data:', error);
|
||||
// Fallback to mock data on error
|
||||
const mockData = {
|
||||
health_score: {
|
||||
score: 84,
|
||||
@@ -118,26 +268,107 @@ const SEODashboard: React.FC = () => {
|
||||
last_updated: new Date().toISOString(),
|
||||
website_url: websiteUrl || undefined // Convert null to undefined for TypeScript
|
||||
};
|
||||
|
||||
setData(mockData);
|
||||
setLoading(false);
|
||||
} catch (err) {
|
||||
setError('Failed to load dashboard data');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
fetchData();
|
||||
}, []);
|
||||
fetchAllData();
|
||||
}, [isSignedIn, setLoading, setData]);
|
||||
|
||||
useEffect(() => {
|
||||
// Run initial SEO analysis if no data exists
|
||||
if (!loading && !error && data) {
|
||||
// Call via store to avoid changing function identity in deps
|
||||
useSEODashboardStore.getState().checkAndRunInitialAnalysis();
|
||||
// Check if we have cached analysis data first
|
||||
const store = useSEODashboardStore.getState();
|
||||
store.checkAndRunInitialAnalysis();
|
||||
|
||||
// If no cached analysis data and we have a website URL, run initial analysis
|
||||
if (!store.analysisData && data.website_url) {
|
||||
console.log('No cached analysis data found, running initial SEO analysis...');
|
||||
store.runSEOAnalysis();
|
||||
}
|
||||
}
|
||||
}, [loading, error, data]);
|
||||
|
||||
// Menu handlers
|
||||
const handleUserMenuOpen = (event: React.MouseEvent<HTMLElement>) => {
|
||||
setUserMenuAnchor(event.currentTarget);
|
||||
};
|
||||
|
||||
const handleUserMenuClose = () => {
|
||||
setUserMenuAnchor(null);
|
||||
};
|
||||
|
||||
const handleStatusMenuOpen = (event: React.MouseEvent<HTMLElement>) => {
|
||||
setStatusMenuAnchor(event.currentTarget);
|
||||
};
|
||||
|
||||
const handleStatusMenuClose = () => {
|
||||
setStatusMenuAnchor(null);
|
||||
};
|
||||
|
||||
const handleBackToDashboard = () => {
|
||||
window.location.href = '/seo-dashboard';
|
||||
};
|
||||
|
||||
const handleRefreshData = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
await refreshSEOAnalysis();
|
||||
await fetchPlatformStatus();
|
||||
setLastRefresh(new Date());
|
||||
} catch (error) {
|
||||
console.error('Error refreshing data:', error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// Background jobs visibility (user-triggered)
|
||||
const [showBackgroundJobs, setShowBackgroundJobs] = useState(false);
|
||||
|
||||
// Platform status fetching function
|
||||
const fetchPlatformStatus = async () => {
|
||||
try {
|
||||
console.log('Fetching platform status...');
|
||||
const response = await apiClient.get('/api/seo-dashboard/platforms');
|
||||
console.log('Platform status response:', response.status, response.statusText);
|
||||
console.log('Platform status data:', response.data);
|
||||
setPlatformStatus(response.data);
|
||||
} catch (error) {
|
||||
console.error('Error fetching platform status:', error);
|
||||
}
|
||||
};
|
||||
|
||||
// Load competitor analysis data from onboarding step 3
|
||||
const loadCompetitorAnalysisData = () => {
|
||||
try {
|
||||
const cachedData = localStorage.getItem('competitor_analysis_data');
|
||||
const cachedUrl = localStorage.getItem('competitor_analysis_url');
|
||||
const cachedTimestamp = localStorage.getItem('competitor_analysis_timestamp');
|
||||
|
||||
if (cachedData && cachedUrl && cachedTimestamp) {
|
||||
const analysisData = JSON.parse(cachedData);
|
||||
const timestamp = parseInt(cachedTimestamp);
|
||||
const isRecent = (Date.now() - timestamp) < (7 * 24 * 60 * 60 * 1000); // 7 days
|
||||
|
||||
if (isRecent) {
|
||||
console.log('Loading competitor analysis data from onboarding step 3:', analysisData);
|
||||
setCompetitorAnalysisData(analysisData);
|
||||
} else {
|
||||
console.log('Competitor analysis data is too old, not loading');
|
||||
}
|
||||
} else {
|
||||
console.log('No competitor analysis data found in localStorage');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading competitor analysis data:', error);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
if (loading) {
|
||||
return <Skeleton variant="rectangular" height={200} />;
|
||||
}
|
||||
@@ -202,137 +433,445 @@ const SEODashboard: React.FC = () => {
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.6 }}
|
||||
>
|
||||
{/* Header */}
|
||||
<Box sx={{ mb: 4, display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
|
||||
<Box>
|
||||
<Typography variant="h4" sx={{ color: 'white', fontWeight: 700 }}>
|
||||
🔍 SEO Dashboard
|
||||
</Typography>
|
||||
<Typography variant="subtitle1" sx={{ color: 'rgba(255, 255, 255, 0.7)' }}>
|
||||
AI-powered insights and actionable recommendations
|
||||
</Typography>
|
||||
</Box>
|
||||
{/* Professional Compact Header */}
|
||||
<Box sx={{
|
||||
mb: 4,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
py: 2,
|
||||
px: 3,
|
||||
bgcolor: 'rgba(255, 255, 255, 0.05)',
|
||||
borderRadius: 2,
|
||||
border: '1px solid rgba(255, 255, 255, 0.1)'
|
||||
}}>
|
||||
{/* Left Section - Navigation & Title */}
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2 }}>
|
||||
{/* User Info */}
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||
<Chip
|
||||
label={`Signed in as ${user?.primaryEmailAddress?.emailAddress || 'User'}`}
|
||||
size="small"
|
||||
sx={{
|
||||
bgcolor: 'rgba(76, 175, 80, 0.25)',
|
||||
border: '1px solid rgba(76, 175, 80, 0.45)',
|
||||
color: 'white',
|
||||
fontWeight: 600
|
||||
}}
|
||||
/>
|
||||
<SignOutButton>
|
||||
<Button
|
||||
variant="outlined"
|
||||
size="small"
|
||||
sx={{
|
||||
borderColor: 'rgba(255, 255, 255, 0.3)',
|
||||
color: 'white',
|
||||
'&:hover': {
|
||||
borderColor: 'rgba(255, 255, 255, 0.5)',
|
||||
bgcolor: 'rgba(255, 255, 255, 0.1)'
|
||||
}
|
||||
}}
|
||||
>
|
||||
Sign Out
|
||||
</Button>
|
||||
</SignOutButton>
|
||||
</Box>
|
||||
|
||||
{/* Freshness Indicator */}
|
||||
{(() => {
|
||||
const freshness = getAnalysisFreshness();
|
||||
const chipColor = freshness.isStale ? 'rgba(255, 193, 7, 0.25)' : 'rgba(76, 175, 80, 0.25)';
|
||||
const chipBorder = freshness.isStale ? 'rgba(255, 193, 7, 0.45)' : 'rgba(76, 175, 80, 0.45)';
|
||||
return (
|
||||
<Chip
|
||||
label={`Freshness: ${freshness.label}`}
|
||||
size="small"
|
||||
sx={{
|
||||
bgcolor: chipColor,
|
||||
border: `1px solid ${chipBorder}`,
|
||||
color: 'white',
|
||||
fontWeight: 600
|
||||
}}
|
||||
/>
|
||||
);
|
||||
})()}
|
||||
<Button
|
||||
onClick={refreshSEOAnalysis}
|
||||
disabled={analysisLoading}
|
||||
variant="outlined"
|
||||
size="small"
|
||||
sx={{
|
||||
<IconButton
|
||||
onClick={handleBackToDashboard}
|
||||
sx={{
|
||||
color: 'white',
|
||||
borderColor: 'rgba(255, 255, 255, 0.6)',
|
||||
'&:hover': { borderColor: 'rgba(255, 255, 255, 0.9)' }
|
||||
'&:hover': { bgcolor: 'rgba(255, 255, 255, 0.1)' }
|
||||
}}
|
||||
>
|
||||
{analysisLoading ? 'Refreshing…' : 'Refresh'}
|
||||
</Button>
|
||||
<ArrowBackIcon />
|
||||
</IconButton>
|
||||
|
||||
<Box>
|
||||
<Typography variant="h5" sx={{ color: 'white', fontWeight: 700, lineHeight: 1.2 }}>
|
||||
SEO Dashboard
|
||||
</Typography>
|
||||
<Typography variant="body2" sx={{ color: 'rgba(255, 255, 255, 0.7)' }}>
|
||||
AI-powered insights and recommendations
|
||||
</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
{/* Center Section - Status Overview */}
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||
<Tooltip title="Platform Connection Status">
|
||||
<IconButton
|
||||
onClick={handleStatusMenuOpen}
|
||||
sx={{
|
||||
color: 'white',
|
||||
'&:hover': { bgcolor: 'rgba(255, 255, 255, 0.1)' }
|
||||
}}
|
||||
>
|
||||
<CheckCircleIcon sx={{
|
||||
color: platformStatus.gsc.connected && platformStatus.bing.connected
|
||||
? '#4CAF50'
|
||||
: platformStatus.gsc.connected || platformStatus.bing.connected
|
||||
? '#FF9800'
|
||||
: '#f44336'
|
||||
}} />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip title="Data Freshness">
|
||||
<Chip
|
||||
icon={<ScheduleIcon />}
|
||||
label={(() => {
|
||||
const freshness = getAnalysisFreshness();
|
||||
return freshness.label;
|
||||
})()}
|
||||
size="small"
|
||||
sx={{
|
||||
bgcolor: 'rgba(255, 255, 255, 0.1)',
|
||||
color: 'white',
|
||||
border: '1px solid rgba(255, 255, 255, 0.2)',
|
||||
fontSize: '0.75rem'
|
||||
}}
|
||||
/>
|
||||
</Tooltip>
|
||||
</Box>
|
||||
|
||||
{/* Right Section - User Menu */}
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||
<Avatar sx={{ width: 32, height: 32, bgcolor: 'rgba(33, 150, 243, 0.8)' }}>
|
||||
<PersonIcon fontSize="small" />
|
||||
</Avatar>
|
||||
|
||||
<IconButton
|
||||
onClick={handleUserMenuOpen}
|
||||
sx={{
|
||||
color: 'white',
|
||||
'&:hover': { bgcolor: 'rgba(255, 255, 255, 0.1)' }
|
||||
}}
|
||||
>
|
||||
<MoreVertIcon />
|
||||
</IconButton>
|
||||
</Box>
|
||||
|
||||
{/* Status Menu */}
|
||||
<Menu
|
||||
anchorEl={statusMenuAnchor}
|
||||
open={Boolean(statusMenuAnchor)}
|
||||
onClose={handleStatusMenuClose}
|
||||
PaperProps={{
|
||||
sx: {
|
||||
bgcolor: 'rgba(30, 30, 30, 0.95)',
|
||||
border: '1px solid rgba(255, 255, 255, 0.1)',
|
||||
color: 'white',
|
||||
minWidth: 280
|
||||
}
|
||||
}}
|
||||
>
|
||||
<MenuItem disabled>
|
||||
<Typography variant="subtitle2" sx={{ color: 'rgba(255, 255, 255, 0.7)' }}>
|
||||
Platform Status
|
||||
</Typography>
|
||||
</MenuItem>
|
||||
|
||||
{/* GSC Status */}
|
||||
<MenuItem>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', width: '100%' }}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||
<CheckCircleIcon sx={{
|
||||
color: platformStatus.gsc.connected ? '#4CAF50' : '#f44336',
|
||||
fontSize: 16
|
||||
}} />
|
||||
<Typography variant="body2">
|
||||
Google Search Console: {platformStatus.gsc.connected ? 'Connected' : 'Disconnected'}
|
||||
</Typography>
|
||||
</Box>
|
||||
{!platformStatus.gsc.connected && (
|
||||
<Button
|
||||
size="small"
|
||||
variant="outlined"
|
||||
onClick={handleGSCReconnect}
|
||||
sx={{
|
||||
ml: 2,
|
||||
borderColor: 'rgba(255, 255, 255, 0.3)',
|
||||
color: 'white',
|
||||
fontSize: '0.75rem',
|
||||
'&:hover': {
|
||||
borderColor: 'rgba(255, 255, 255, 0.5)',
|
||||
bgcolor: 'rgba(255, 255, 255, 0.1)'
|
||||
}
|
||||
}}
|
||||
>
|
||||
Reconnect
|
||||
</Button>
|
||||
)}
|
||||
</Box>
|
||||
</MenuItem>
|
||||
|
||||
{/* Bing Status */}
|
||||
<MenuItem>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', width: '100%' }}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||
<CheckCircleIcon sx={{
|
||||
color: platformStatus.bing.connected ? '#4CAF50' :
|
||||
platformStatus.bing.status === 'expired' ? '#FF9800' : '#f44336',
|
||||
fontSize: 16
|
||||
}} />
|
||||
<Box>
|
||||
<Typography variant="body2">
|
||||
Bing Webmaster: {platformStatus.bing.connected ? 'Connected' :
|
||||
platformStatus.bing.status === 'expired' ? 'Expired' : 'Disconnected'}
|
||||
</Typography>
|
||||
{platformStatus.bing.status === 'expired' && platformStatus.bing.last_token_date && (
|
||||
<Typography variant="caption" sx={{ color: 'rgba(255, 255, 255, 0.5)', fontSize: '0.7rem' }}>
|
||||
Last connected: {new Date(platformStatus.bing.last_token_date).toLocaleDateString()}
|
||||
</Typography>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
{!platformStatus.bing.connected && (
|
||||
<Button
|
||||
size="small"
|
||||
variant="outlined"
|
||||
onClick={handleBingReconnect}
|
||||
sx={{
|
||||
ml: 2,
|
||||
borderColor: platformStatus.bing.status === 'expired' ? '#FF9800' : 'rgba(255, 255, 255, 0.3)',
|
||||
color: platformStatus.bing.status === 'expired' ? '#FF9800' : 'white',
|
||||
fontSize: '0.75rem',
|
||||
'&:hover': {
|
||||
borderColor: platformStatus.bing.status === 'expired' ? '#FFB74D' : 'rgba(255, 255, 255, 0.5)',
|
||||
bgcolor: platformStatus.bing.status === 'expired' ? 'rgba(255, 152, 0, 0.1)' : 'rgba(255, 255, 255, 0.1)'
|
||||
}
|
||||
}}
|
||||
>
|
||||
{platformStatus.bing.status === 'expired' ? 'Reconnect' : 'Connect'}
|
||||
</Button>
|
||||
)}
|
||||
</Box>
|
||||
</MenuItem>
|
||||
</Menu>
|
||||
|
||||
{/* User Menu */}
|
||||
<Menu
|
||||
anchorEl={userMenuAnchor}
|
||||
open={Boolean(userMenuAnchor)}
|
||||
onClose={handleUserMenuClose}
|
||||
PaperProps={{
|
||||
sx: {
|
||||
bgcolor: 'rgba(30, 30, 30, 0.95)',
|
||||
border: '1px solid rgba(255, 255, 255, 0.1)',
|
||||
color: 'white'
|
||||
}
|
||||
}}
|
||||
>
|
||||
<MenuItem disabled>
|
||||
<Typography variant="subtitle2" sx={{ color: 'rgba(255, 255, 255, 0.7)' }}>
|
||||
{user?.primaryEmailAddress?.emailAddress || 'User'}
|
||||
</Typography>
|
||||
</MenuItem>
|
||||
<Divider sx={{ bgcolor: 'rgba(255, 255, 255, 0.1)' }} />
|
||||
<MenuItem onClick={handleRefreshData}>
|
||||
<RefreshIcon sx={{ mr: 1, fontSize: 16 }} />
|
||||
<Typography variant="body2">Refresh Data</Typography>
|
||||
</MenuItem>
|
||||
<Divider sx={{ bgcolor: 'rgba(255, 255, 255, 0.1)' }} />
|
||||
<SignOutButton>
|
||||
<MenuItem>
|
||||
<ExitIcon sx={{ mr: 1, fontSize: 16 }} />
|
||||
<Typography variant="body2">Sign Out</Typography>
|
||||
</MenuItem>
|
||||
</SignOutButton>
|
||||
</Menu>
|
||||
</Box>
|
||||
|
||||
{/* GSC Connection Section */}
|
||||
<Box sx={{ mb: 3 }}>
|
||||
<GSCLoginButton />
|
||||
</Box>
|
||||
|
||||
{/* CopilotKit Test Panel removed */}
|
||||
|
||||
{/* Executive Summary */}
|
||||
{/* Search Performance Overview */}
|
||||
<Box sx={{ mb: 4 }}>
|
||||
<Typography variant="h6" sx={{ color: 'white', fontWeight: 600, mb: 2 }}>
|
||||
📊 Performance Overview
|
||||
</Typography>
|
||||
<Grid container spacing={2}>
|
||||
<Grid item xs={6} sm={3}>
|
||||
<GlassCard sx={{ p: 2 }}>
|
||||
<Typography variant="body2" sx={{ color: 'rgba(255, 255, 255, 0.7)' }}>
|
||||
Organic Traffic
|
||||
</Typography>
|
||||
<Typography variant="h5" sx={{ color: '#4CAF50' }}>
|
||||
{data.metrics.traffic.value}
|
||||
</Typography>
|
||||
</GlassCard>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, mb: 3 }}>
|
||||
<Typography variant="h6" sx={{ color: 'white', fontWeight: 600 }}>
|
||||
📊 Search Performance Overview
|
||||
</Typography>
|
||||
<Tooltip title="Real-time analytics data from connected search platforms">
|
||||
<InfoIcon sx={{ color: 'rgba(255, 255, 255, 0.5)', fontSize: 18 }} />
|
||||
</Tooltip>
|
||||
<Box sx={{ flexGrow: 1 }} />
|
||||
<Button
|
||||
variant="outlined"
|
||||
size="small"
|
||||
onClick={() => setShowBackgroundJobs((v) => !v)}
|
||||
sx={{ textTransform: 'none' }}
|
||||
>
|
||||
{showBackgroundJobs ? 'Hide Background Jobs' : 'Run Background Jobs'}
|
||||
</Button>
|
||||
</Box>
|
||||
|
||||
<PlatformAnalytics
|
||||
platforms={['gsc', 'bing']}
|
||||
showSummary={true}
|
||||
refreshInterval={0}
|
||||
onDataLoaded={(analyticsData) => {
|
||||
console.log('Real analytics data loaded:', analyticsData);
|
||||
}}
|
||||
onRefreshReady={(fn) => { platformRefreshRef.current = fn; }}
|
||||
onReconnect={(platform) => {
|
||||
if (platform === 'gsc') {
|
||||
handleGSCReconnect();
|
||||
} else if (platform === 'bing') {
|
||||
handleBingReconnect();
|
||||
}
|
||||
}}
|
||||
showBackgroundJobs={showBackgroundJobs}
|
||||
/>
|
||||
|
||||
{/* Enhanced Metrics with Tooltips */}
|
||||
<Box sx={{ mt: 3 }}>
|
||||
<Grid container spacing={2}>
|
||||
<Grid item xs={6} sm={3}>
|
||||
<Tooltip title="Number of search engine platforms (GSC, Bing) currently connected to your dashboard">
|
||||
<GlassCard sx={{ p: 2, cursor: 'help' }}>
|
||||
<Typography variant="body2" sx={{ color: 'rgba(255, 255, 255, 0.7)', mb: 1 }}>
|
||||
Connected Platforms
|
||||
</Typography>
|
||||
<Typography variant="h4" sx={{ color: '#4CAF50', fontWeight: 700 }}>
|
||||
{(platformStatus.gsc.connected ? 1 : 0) + (platformStatus.bing.connected ? 1 : 0)}
|
||||
</Typography>
|
||||
<Typography variant="caption" sx={{ color: 'rgba(255, 255, 255, 0.5)' }}>
|
||||
of 2 platforms
|
||||
</Typography>
|
||||
</GlassCard>
|
||||
</Tooltip>
|
||||
</Grid>
|
||||
|
||||
<Grid item xs={6} sm={3}>
|
||||
<Tooltip title="Total number of clicks from search results to your website within the selected time period">
|
||||
<GlassCard sx={{ p: 2, cursor: 'help' }}>
|
||||
<Typography variant="body2" sx={{ color: 'rgba(255, 255, 255, 0.7)', mb: 1 }}>
|
||||
Total Clicks
|
||||
</Typography>
|
||||
<Typography variant="h4" sx={{ color: '#2196F3', fontWeight: 700 }}>
|
||||
{data.metrics?.traffic?.value || data.summary?.clicks || 0}
|
||||
</Typography>
|
||||
<Typography variant="caption" sx={{ color: 'rgba(255, 255, 255, 0.5)' }}>
|
||||
from search results
|
||||
</Typography>
|
||||
</GlassCard>
|
||||
</Tooltip>
|
||||
</Grid>
|
||||
|
||||
<Grid item xs={6} sm={3}>
|
||||
<Tooltip title="Total number of times your website appeared in search results within the selected time period">
|
||||
<GlassCard sx={{ p: 2, cursor: 'help' }}>
|
||||
<Typography variant="body2" sx={{ color: 'rgba(255, 255, 255, 0.7)', mb: 1 }}>
|
||||
Total Impressions
|
||||
</Typography>
|
||||
<Typography variant="h4" sx={{ color: '#FF9800', fontWeight: 700 }}>
|
||||
{data.metrics?.impressions?.value || data.summary?.impressions || 0}
|
||||
</Typography>
|
||||
<Typography variant="caption" sx={{ color: 'rgba(255, 255, 255, 0.5)' }}>
|
||||
search appearances
|
||||
</Typography>
|
||||
</GlassCard>
|
||||
</Tooltip>
|
||||
</Grid>
|
||||
|
||||
<Grid item xs={6} sm={3}>
|
||||
<Tooltip title="Percentage of impressions that resulted in a click to your website (Clicks ÷ Impressions × 100)">
|
||||
<GlassCard sx={{ p: 2, cursor: 'help' }}>
|
||||
<Typography variant="body2" sx={{ color: 'rgba(255, 255, 255, 0.7)', mb: 1 }}>
|
||||
Overall CTR
|
||||
</Typography>
|
||||
<Typography variant="h4" sx={{ color: '#9C27B0', fontWeight: 700 }}>
|
||||
{data.metrics?.ctr?.value || data.summary?.ctr || 0}%
|
||||
</Typography>
|
||||
<Typography variant="caption" sx={{ color: 'rgba(255, 255, 255, 0.5)' }}>
|
||||
click-through rate
|
||||
</Typography>
|
||||
</GlassCard>
|
||||
</Tooltip>
|
||||
</Grid>
|
||||
</Grid>
|
||||
<Grid item xs={6} sm={3}>
|
||||
<GlassCard sx={{ p: 2 }}>
|
||||
<Typography variant="body2" sx={{ color: 'rgba(255, 255, 255, 0.7)' }}>
|
||||
Average Ranking
|
||||
</Typography>
|
||||
<Typography variant="h5" sx={{ color: '#2196F3' }}>
|
||||
{data.metrics.rankings.value}
|
||||
</Typography>
|
||||
</GlassCard>
|
||||
</Grid>
|
||||
<Grid item xs={6} sm={3}>
|
||||
<GlassCard sx={{ p: 2 }}>
|
||||
<Typography variant="body2" sx={{ color: 'rgba(255, 255, 255, 0.7)' }}>
|
||||
Mobile Speed
|
||||
</Typography>
|
||||
<Typography variant="h5" sx={{ color: '#FF9800' }}>
|
||||
{data.metrics.mobile.value}
|
||||
</Typography>
|
||||
</GlassCard>
|
||||
</Grid>
|
||||
<Grid item xs={6} sm={3}>
|
||||
<GlassCard sx={{ p: 2 }}>
|
||||
<Typography variant="body2" sx={{ color: 'rgba(255, 255, 255, 0.7)' }}>
|
||||
Keywords Tracked
|
||||
</Typography>
|
||||
<Typography variant="h5" sx={{ color: '#9C27B0' }}>
|
||||
{data.metrics.keywords.value}
|
||||
</Typography>
|
||||
</GlassCard>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
{/* Competitive Analysis from Onboarding Step 3 */}
|
||||
{competitorAnalysisData && (
|
||||
<Box sx={{ mb: 4 }}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, mb: 3 }}>
|
||||
<Typography variant="h6" sx={{ color: 'white', fontWeight: 600 }}>
|
||||
🎯 Competitive Analysis
|
||||
</Typography>
|
||||
<Tooltip title="Real competitor analysis data from onboarding step 3">
|
||||
<InfoIcon sx={{ color: 'rgba(255, 255, 255, 0.5)', fontSize: 18 }} />
|
||||
</Tooltip>
|
||||
</Box>
|
||||
|
||||
<Grid container spacing={2}>
|
||||
<Grid item xs={12} md={4}>
|
||||
<Tooltip title="Number of competitors discovered during onboarding analysis">
|
||||
<GlassCard sx={{ p: 2, cursor: 'help' }}>
|
||||
<Typography variant="body2" sx={{ color: 'rgba(255, 255, 255, 0.7)', mb: 1 }}>
|
||||
Competitors Found
|
||||
</Typography>
|
||||
<Typography variant="h4" sx={{ color: '#4CAF50', fontWeight: 700 }}>
|
||||
{competitorAnalysisData.competitors?.length || 0}
|
||||
</Typography>
|
||||
<Typography variant="caption" sx={{ color: 'rgba(255, 255, 255, 0.5)' }}>
|
||||
in your market
|
||||
</Typography>
|
||||
</GlassCard>
|
||||
</Tooltip>
|
||||
</Grid>
|
||||
|
||||
<Grid item xs={12} md={4}>
|
||||
<Tooltip title="Social media accounts discovered for competitors">
|
||||
<GlassCard sx={{ p: 2, cursor: 'help' }}>
|
||||
<Typography variant="body2" sx={{ color: 'rgba(255, 255, 255, 0.7)', mb: 1 }}>
|
||||
Social Media Accounts
|
||||
</Typography>
|
||||
<Typography variant="h4" sx={{ color: '#2196F3', fontWeight: 700 }}>
|
||||
{Object.keys(competitorAnalysisData.social_media_accounts || {}).length}
|
||||
</Typography>
|
||||
<Typography variant="caption" sx={{ color: 'rgba(255, 255, 255, 0.5)' }}>
|
||||
competitor accounts
|
||||
</Typography>
|
||||
</GlassCard>
|
||||
</Tooltip>
|
||||
</Grid>
|
||||
|
||||
<Grid item xs={12} md={4}>
|
||||
<Tooltip title="Social media citations and mentions found">
|
||||
<GlassCard sx={{ p: 2, cursor: 'help' }}>
|
||||
<Typography variant="body2" sx={{ color: 'rgba(255, 255, 255, 0.7)', mb: 1 }}>
|
||||
Social Citations
|
||||
</Typography>
|
||||
<Typography variant="h4" sx={{ color: '#FF9800', fontWeight: 700 }}>
|
||||
{competitorAnalysisData.social_media_citations?.length || 0}
|
||||
</Typography>
|
||||
<Typography variant="caption" sx={{ color: 'rgba(255, 255, 255, 0.5)' }}>
|
||||
mentions found
|
||||
</Typography>
|
||||
</GlassCard>
|
||||
</Tooltip>
|
||||
</Grid>
|
||||
</Grid>
|
||||
|
||||
{/* Competitor List */}
|
||||
{competitorAnalysisData.competitors && competitorAnalysisData.competitors.length > 0 && (
|
||||
<Box sx={{ mt: 3 }}>
|
||||
<Typography variant="h6" sx={{ color: 'white', fontWeight: 600, mb: 2 }}>
|
||||
Top Competitors
|
||||
</Typography>
|
||||
<Grid container spacing={2}>
|
||||
{competitorAnalysisData.competitors.slice(0, 6).map((competitor: any, index: number) => (
|
||||
<Grid item xs={12} sm={6} md={4} key={index}>
|
||||
<GlassCard sx={{ p: 2 }}>
|
||||
<Typography variant="subtitle2" sx={{ color: 'white', fontWeight: 600, mb: 1 }}>
|
||||
{competitor.name || competitor.domain || `Competitor ${index + 1}`}
|
||||
</Typography>
|
||||
<Typography variant="body2" sx={{ color: 'rgba(255, 255, 255, 0.7)', mb: 1 }}>
|
||||
{competitor.domain || competitor.url || 'No domain available'}
|
||||
</Typography>
|
||||
{competitor.description && (
|
||||
<Typography variant="caption" sx={{ color: 'rgba(255, 255, 255, 0.5)' }}>
|
||||
{competitor.description.length > 100
|
||||
? `${competitor.description.substring(0, 100)}...`
|
||||
: competitor.description}
|
||||
</Typography>
|
||||
)}
|
||||
</GlassCard>
|
||||
</Grid>
|
||||
))}
|
||||
</Grid>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{/* Research Summary */}
|
||||
{competitorAnalysisData.research_summary && (
|
||||
<Box sx={{ mt: 3 }}>
|
||||
<Typography variant="h6" sx={{ color: 'white', fontWeight: 600, mb: 2 }}>
|
||||
Research Summary
|
||||
</Typography>
|
||||
<GlassCard sx={{ p: 3 }}>
|
||||
<Typography variant="body2" sx={{ color: 'rgba(255, 255, 255, 0.9)', lineHeight: 1.6 }}>
|
||||
{competitorAnalysisData.research_summary}
|
||||
</Typography>
|
||||
</GlassCard>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{/* SEO Analyzer Panel */}
|
||||
<SEOAnalyzerPanel
|
||||
analysisData={analysisData}
|
||||
|
||||
@@ -22,6 +22,7 @@ import {
|
||||
} from '@mui/icons-material';
|
||||
import { useAuth } from '@clerk/clerk-react';
|
||||
import { gscAPI, GSCStatusResponse } from '../../../api/gsc';
|
||||
import { apiClient } from '../../../api/client';
|
||||
|
||||
interface GSCLoginButtonProps {
|
||||
onStatusChange?: (connected: boolean) => void;
|
||||
@@ -69,17 +70,28 @@ const GSCLoginButton: React.FC<GSCLoginButtonProps> = ({ onStatusChange }) => {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
|
||||
const statusResponse = await gscAPI.getStatus();
|
||||
setStatus(statusResponse);
|
||||
// Use backend API to check GSC status
|
||||
const response = await apiClient.get('/api/seo-dashboard/platforms');
|
||||
const platformData = response.data;
|
||||
|
||||
const gscStatus = {
|
||||
connected: platformData.gsc?.connected || false,
|
||||
sites: platformData.gsc?.sites || [],
|
||||
last_sync: platformData.gsc?.last_sync || undefined
|
||||
};
|
||||
|
||||
setStatus(gscStatus);
|
||||
|
||||
if (onStatusChange) {
|
||||
onStatusChange(statusResponse.connected);
|
||||
onStatusChange(gscStatus.connected);
|
||||
}
|
||||
|
||||
console.log('GSC Login Button: Status checked, connected:', statusResponse.connected);
|
||||
console.log('GSC Login Button: Status checked, connected:', gscStatus.connected);
|
||||
} catch (err) {
|
||||
console.error('GSC Login Button: Error checking status:', err);
|
||||
setError('Failed to check GSC connection status');
|
||||
// Set disconnected status on error
|
||||
setStatus({ connected: false, sites: [], last_sync: undefined });
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useState, useEffect, useCallback } from 'react';
|
||||
import React, { useState, useEffect, useCallback, useRef } from 'react';
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
@@ -206,8 +206,14 @@ const BackgroundJobManager: React.FC<BackgroundJobManagerProps> = ({
|
||||
}
|
||||
};
|
||||
|
||||
// One-run guard to prevent duplicate calls in StrictMode
|
||||
const jobsFetchedRef = useRef(false);
|
||||
|
||||
// Poll for job updates
|
||||
useEffect(() => {
|
||||
if (jobsFetchedRef.current) return;
|
||||
jobsFetchedRef.current = true;
|
||||
|
||||
fetchJobs();
|
||||
|
||||
// Only start polling if there are running jobs
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useState, useEffect, useCallback } from 'react';
|
||||
import React, { useState, useEffect, useCallback, useRef } from 'react';
|
||||
import {
|
||||
Box,
|
||||
Card,
|
||||
@@ -26,6 +26,7 @@ import {
|
||||
Error as ErrorIcon,
|
||||
Warning,
|
||||
} from '@mui/icons-material';
|
||||
import { Button } from '@mui/material';
|
||||
import { PlatformAnalytics as PlatformAnalyticsType, AnalyticsSummary, PlatformConnectionStatus } from '../../api/analytics';
|
||||
import { cachedAnalyticsAPI } from '../../api/cachedAnalytics';
|
||||
import BingInsightsCard from './BingInsightsCard';
|
||||
@@ -37,6 +38,8 @@ interface PlatformAnalyticsComponentProps {
|
||||
refreshInterval?: number; // in milliseconds, 0 = no auto-refresh
|
||||
onDataLoaded?: (data: any) => void;
|
||||
onRefreshReady?: (refreshFn: () => Promise<void>) => 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<PlatformAnalyticsComponentProps> = ({
|
||||
@@ -45,6 +48,8 @@ const PlatformAnalytics: React.FC<PlatformAnalyticsComponentProps> = ({
|
||||
refreshInterval = 0,
|
||||
onDataLoaded,
|
||||
onRefreshReady,
|
||||
onReconnect,
|
||||
showBackgroundJobs = false,
|
||||
}) => {
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
@@ -111,7 +116,13 @@ const PlatformAnalytics: React.FC<PlatformAnalyticsComponentProps> = ({
|
||||
}
|
||||
}, [platforms, loadData]);
|
||||
|
||||
// One-run guard to prevent duplicate calls in StrictMode
|
||||
const dataLoadedRef = useRef(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (dataLoadedRef.current) return;
|
||||
dataLoadedRef.current = true;
|
||||
|
||||
loadData();
|
||||
|
||||
// Set up auto-refresh if interval is specified
|
||||
@@ -300,9 +311,31 @@ const PlatformAnalytics: React.FC<PlatformAnalyticsComponentProps> = ({
|
||||
)}
|
||||
|
||||
{data.status === 'error' && (
|
||||
<Alert severity="error" sx={{ mt: 1 }}>
|
||||
{data.error_message || 'Failed to load analytics data'}
|
||||
</Alert>
|
||||
<Box sx={{ mt: 1 }}>
|
||||
<Alert severity="error" sx={{ mb: 2 }}>
|
||||
{data.error_message || 'Failed to load analytics data'}
|
||||
</Alert>
|
||||
{onReconnect && (
|
||||
<Button
|
||||
variant="outlined"
|
||||
color="error"
|
||||
size="small"
|
||||
onClick={() => onReconnect(platform)}
|
||||
sx={{
|
||||
textTransform: 'none',
|
||||
fontWeight: 600,
|
||||
borderColor: '#f44336',
|
||||
color: '#f44336',
|
||||
'&:hover': {
|
||||
borderColor: '#d32f2f',
|
||||
backgroundColor: 'rgba(244, 67, 54, 0.04)'
|
||||
}
|
||||
}}
|
||||
>
|
||||
Reconnect {platform.toUpperCase()}
|
||||
</Button>
|
||||
)}
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{data.status === 'partial' && (
|
||||
@@ -423,18 +456,20 @@ const PlatformAnalytics: React.FC<PlatformAnalyticsComponentProps> = ({
|
||||
))}
|
||||
</Grid>
|
||||
|
||||
{/* Background Job Manager */}
|
||||
<Box sx={{ mt: 3 }}>
|
||||
<BackgroundJobManager
|
||||
siteUrl="https://www.alwrity.com/"
|
||||
days={30}
|
||||
onJobCompleted={(job) => {
|
||||
console.log('🎉 Background job completed:', job);
|
||||
// Refresh analytics data when job completes
|
||||
forceRefresh();
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
{/* Background Job Manager - render only when explicitly enabled */}
|
||||
{showBackgroundJobs && (
|
||||
<Box sx={{ mt: 3 }}>
|
||||
<BackgroundJobManager
|
||||
siteUrl="https://www.alwrity.com/"
|
||||
days={30}
|
||||
onJobCompleted={(job) => {
|
||||
console.log('🎉 Background job completed:', job);
|
||||
// Refresh analytics data when job completes
|
||||
forceRefresh();
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{/* Debug Section - Show data structure for all platforms */}
|
||||
<Box sx={{ mt: 3 }}>
|
||||
|
||||
Reference in New Issue
Block a user