ALwrity Version 0.5.0 (Fastapi + React )
This commit is contained in:
205
frontend/src/components/SEODashboard/SEODashboard.tsx
Normal file
205
frontend/src/components/SEODashboard/SEODashboard.tsx
Normal file
@@ -0,0 +1,205 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import {
|
||||
Box,
|
||||
Container,
|
||||
Grid,
|
||||
Typography,
|
||||
Alert,
|
||||
Skeleton,
|
||||
useTheme
|
||||
} from '@mui/material';
|
||||
import { motion, AnimatePresence } from 'framer-motion';
|
||||
|
||||
// Shared components
|
||||
import { DashboardContainer, GlassCard } from '../shared/styled';
|
||||
import SEOAnalyzerPanel from './components/SEOAnalyzerPanel';
|
||||
|
||||
// Zustand store
|
||||
import { useSEODashboardStore } from '../../stores/seoDashboardStore';
|
||||
|
||||
// API
|
||||
import { userDataAPI } from '../../api/userData';
|
||||
|
||||
// SEO Dashboard component
|
||||
const SEODashboard: React.FC = () => {
|
||||
const theme = useTheme();
|
||||
|
||||
// Zustand store hooks
|
||||
const {
|
||||
loading,
|
||||
error,
|
||||
data,
|
||||
analysisData,
|
||||
analysisLoading,
|
||||
analysisError,
|
||||
setData,
|
||||
setLoading,
|
||||
setError,
|
||||
runSEOAnalysis,
|
||||
checkAndRunInitialAnalysis,
|
||||
} = useSEODashboardStore();
|
||||
|
||||
useEffect(() => {
|
||||
// Simulate fetching dashboard data
|
||||
const fetchData = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
// Try to get the website URL from the database
|
||||
let websiteUrl = null;
|
||||
try {
|
||||
websiteUrl = await userDataAPI.getWebsiteURL();
|
||||
console.log('Fetched website URL from database:', websiteUrl);
|
||||
} catch (error) {
|
||||
console.warn('Could not fetch website URL from database:', error);
|
||||
}
|
||||
|
||||
// Mock data for now
|
||||
const mockData = {
|
||||
health_score: {
|
||||
score: 85,
|
||||
change: 5,
|
||||
trend: 'up',
|
||||
label: 'GOOD',
|
||||
color: '#4CAF50'
|
||||
},
|
||||
key_insight: 'Your SEO is performing well with room for improvement',
|
||||
priority_alert: 'No critical issues detected',
|
||||
metrics: {
|
||||
traffic: { value: 12500, change: 12, trend: 'up', description: 'Organic traffic', color: '#4CAF50' },
|
||||
rankings: { value: 8.5, change: -0.3, trend: 'down', description: 'Average ranking', color: '#2196F3' },
|
||||
mobile: { value: 92, change: 3, trend: 'up', description: 'Mobile speed', color: '#FF9800' },
|
||||
keywords: { value: 150, change: 5, trend: 'up', description: 'Keywords tracked', color: '#9C27B0' }
|
||||
},
|
||||
platforms: {
|
||||
google: { status: 'connected', connected: true, last_sync: '2024-01-15T10:30:00Z', data_points: 1250 },
|
||||
bing: { status: 'connected', connected: true, last_sync: '2024-01-15T09:45:00Z', data_points: 850 },
|
||||
yandex: { status: 'disconnected', connected: false }
|
||||
},
|
||||
ai_insights: [
|
||||
{
|
||||
insight: 'Consider adding more internal links to improve page authority',
|
||||
priority: 'medium',
|
||||
category: 'content',
|
||||
action_required: false
|
||||
},
|
||||
{
|
||||
insight: 'Mobile page speed could be optimized further',
|
||||
priority: 'high',
|
||||
category: 'performance',
|
||||
action_required: true,
|
||||
tool_path: '/seo-dashboard'
|
||||
}
|
||||
],
|
||||
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');
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
fetchData();
|
||||
}, [setData, setLoading, setError]);
|
||||
|
||||
useEffect(() => {
|
||||
// Run initial SEO analysis if no data exists
|
||||
if (!loading && !error && data) {
|
||||
checkAndRunInitialAnalysis();
|
||||
}
|
||||
}, [loading, error, data, checkAndRunInitialAnalysis]);
|
||||
|
||||
if (loading) {
|
||||
return <Skeleton variant="rectangular" height={200} />;
|
||||
}
|
||||
|
||||
if (error || !data) {
|
||||
return <Alert severity="error">Failed to load dashboard data</Alert>;
|
||||
}
|
||||
|
||||
return (
|
||||
<DashboardContainer>
|
||||
<Container maxWidth="xl">
|
||||
<AnimatePresence>
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.6 }}
|
||||
>
|
||||
{/* Header */}
|
||||
<Box sx={{ mb: 4 }}>
|
||||
<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>
|
||||
|
||||
{/* Executive Summary */}
|
||||
<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>
|
||||
</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>
|
||||
|
||||
{/* SEO Analyzer Panel */}
|
||||
<SEOAnalyzerPanel
|
||||
analysisData={analysisData}
|
||||
onRunAnalysis={runSEOAnalysis}
|
||||
loading={analysisLoading}
|
||||
error={analysisError}
|
||||
/>
|
||||
</motion.div>
|
||||
</AnimatePresence>
|
||||
</Container>
|
||||
</DashboardContainer>
|
||||
);
|
||||
};
|
||||
|
||||
export default SEODashboard;
|
||||
@@ -0,0 +1,103 @@
|
||||
import React from 'react';
|
||||
import { Box, Typography, Button, Avatar } from '@mui/material';
|
||||
import { CheckCircle as CheckCircleIcon } from '@mui/icons-material';
|
||||
import { AIInsightsPanel as StyledAIInsightsPanel } from '../../shared/styled';
|
||||
import { AIInsight } from '../../../api/seoDashboard';
|
||||
|
||||
interface AIInsightsPanelProps {
|
||||
insights: AIInsight[];
|
||||
}
|
||||
|
||||
const AIInsightsPanel: React.FC<AIInsightsPanelProps> = ({ insights }) => {
|
||||
return (
|
||||
<StyledAIInsightsPanel>
|
||||
<Box sx={{ p: 3 }}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2, mb: 3 }}>
|
||||
<Avatar sx={{
|
||||
background: 'linear-gradient(135deg, #667eea, #764ba2)',
|
||||
width: 48,
|
||||
height: 48
|
||||
}}>
|
||||
🤖
|
||||
</Avatar>
|
||||
<Box>
|
||||
<Typography variant="h6" sx={{ color: 'white', fontWeight: 600 }}>
|
||||
AI SEO Assistant
|
||||
</Typography>
|
||||
<Typography variant="caption" sx={{ color: 'rgba(255, 255, 255, 0.7)' }}>
|
||||
Analyzing your data...
|
||||
</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
<Typography variant="body1" sx={{
|
||||
color: 'rgba(255, 255, 255, 0.9)',
|
||||
mb: 3,
|
||||
lineHeight: 1.6
|
||||
}}>
|
||||
💡 Based on your current performance, here are my recommendations:
|
||||
</Typography>
|
||||
|
||||
<Box sx={{ mb: 3 }}>
|
||||
{insights.map((insight, index) => (
|
||||
<Box key={index} sx={{
|
||||
display: 'flex',
|
||||
alignItems: 'flex-start',
|
||||
gap: 2,
|
||||
mb: 2,
|
||||
p: 2,
|
||||
background: 'rgba(255, 255, 255, 0.05)',
|
||||
borderRadius: 2,
|
||||
border: '1px solid rgba(255, 255, 255, 0.1)'
|
||||
}}>
|
||||
<CheckCircleIcon sx={{
|
||||
color: '#4CAF50',
|
||||
fontSize: 20,
|
||||
mt: 0.5
|
||||
}} />
|
||||
<Typography variant="body2" sx={{
|
||||
color: 'rgba(255, 255, 255, 0.8)',
|
||||
flex: 1
|
||||
}}>
|
||||
{insight.insight}
|
||||
</Typography>
|
||||
</Box>
|
||||
))}
|
||||
</Box>
|
||||
|
||||
<Box sx={{ display: 'flex', gap: 2 }}>
|
||||
<Button
|
||||
variant="contained"
|
||||
size="small"
|
||||
sx={{
|
||||
background: 'linear-gradient(135deg, #667eea, #764ba2)',
|
||||
color: 'white',
|
||||
fontWeight: 600,
|
||||
'&:hover': {
|
||||
background: 'linear-gradient(135deg, #5a6fd8, #6a4190)',
|
||||
},
|
||||
}}
|
||||
>
|
||||
Optimize Now
|
||||
</Button>
|
||||
<Button
|
||||
variant="outlined"
|
||||
size="small"
|
||||
sx={{
|
||||
color: 'white',
|
||||
borderColor: 'rgba(255, 255, 255, 0.3)',
|
||||
'&:hover': {
|
||||
borderColor: 'rgba(255, 255, 255, 0.5)',
|
||||
background: 'rgba(255, 255, 255, 0.1)',
|
||||
},
|
||||
}}
|
||||
>
|
||||
Learn More
|
||||
</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
</StyledAIInsightsPanel>
|
||||
);
|
||||
};
|
||||
|
||||
export default AIInsightsPanel;
|
||||
@@ -0,0 +1,83 @@
|
||||
import React from 'react';
|
||||
import {
|
||||
Dialog,
|
||||
DialogTitle,
|
||||
DialogContent,
|
||||
DialogActions,
|
||||
Button,
|
||||
Typography,
|
||||
Grid,
|
||||
Paper,
|
||||
List,
|
||||
ListItem,
|
||||
ListItemIcon,
|
||||
ListItemText
|
||||
} from '@mui/material';
|
||||
import {
|
||||
CheckCircle as CheckCircleIcon
|
||||
} from '@mui/icons-material';
|
||||
import { AnalysisDetailsDialogProps } from '../../shared/types';
|
||||
import { getAnalysisDetails } from './seoUtils';
|
||||
|
||||
const AnalysisDetailsDialog: React.FC<AnalysisDetailsDialogProps> = ({
|
||||
open,
|
||||
onClose
|
||||
}) => {
|
||||
const analysisDetails = getAnalysisDetails();
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
open={open}
|
||||
onClose={onClose}
|
||||
maxWidth="md"
|
||||
fullWidth
|
||||
>
|
||||
<DialogTitle sx={{ color: 'white', fontWeight: 600 }}>
|
||||
📊 SEO Analysis Details
|
||||
</DialogTitle>
|
||||
<DialogContent>
|
||||
<Typography variant="body2" sx={{ color: 'rgba(0, 0, 0, 0.7)', mb: 3 }}>
|
||||
Our comprehensive SEO analyzer performs detailed tests across multiple categories to provide you with actionable insights.
|
||||
</Typography>
|
||||
|
||||
<Grid container spacing={2}>
|
||||
{analysisDetails.map((detail, index) => (
|
||||
<Grid item xs={12} md={6} key={index}>
|
||||
<Paper sx={{ p: 2, background: '#f8f9fa', height: '100%' }}>
|
||||
<Typography variant="h6" sx={{ color: '#1976d2', mb: 1, fontWeight: 600 }}>
|
||||
{detail.title}
|
||||
</Typography>
|
||||
<Typography variant="body2" sx={{ color: 'rgba(0, 0, 0, 0.7)', mb: 2 }}>
|
||||
{detail.description}
|
||||
</Typography>
|
||||
<Typography variant="subtitle2" sx={{ fontWeight: 600, mb: 1 }}>
|
||||
Tests Performed:
|
||||
</Typography>
|
||||
<List dense>
|
||||
{detail.tests.map((test, testIndex) => (
|
||||
<ListItem key={testIndex} sx={{ py: 0.5 }}>
|
||||
<ListItemIcon sx={{ minWidth: 24 }}>
|
||||
<CheckCircleIcon sx={{ fontSize: 16, color: '#4CAF50' }} />
|
||||
</ListItemIcon>
|
||||
<ListItemText
|
||||
primary={test}
|
||||
primaryTypographyProps={{ variant: 'body2', fontSize: '0.875rem' }}
|
||||
/>
|
||||
</ListItem>
|
||||
))}
|
||||
</List>
|
||||
</Paper>
|
||||
</Grid>
|
||||
))}
|
||||
</Grid>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={onClose}>
|
||||
Close
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
export default AnalysisDetailsDialog;
|
||||
178
frontend/src/components/SEODashboard/components/AnalysisTabs.tsx
Normal file
178
frontend/src/components/SEODashboard/components/AnalysisTabs.tsx
Normal file
@@ -0,0 +1,178 @@
|
||||
import React from 'react';
|
||||
import {
|
||||
Box,
|
||||
Typography,
|
||||
Tabs,
|
||||
Tab,
|
||||
Badge
|
||||
} from '@mui/material';
|
||||
import {
|
||||
ThumbUp as ThumbUpIcon,
|
||||
ThumbDown as ThumbDownIcon,
|
||||
Warning as WarningIcon2
|
||||
} from '@mui/icons-material';
|
||||
import { AnalysisTabsProps } from '../../shared/types';
|
||||
import CategoryCard from './CategoryCard';
|
||||
import TabPanel from './TabPanel';
|
||||
|
||||
const AnalysisTabs: React.FC<AnalysisTabsProps> = ({
|
||||
categorizedData,
|
||||
expandedCategories,
|
||||
onToggleCategory,
|
||||
onIssueClick,
|
||||
onAIAction
|
||||
}) => {
|
||||
const [tabValue, setTabValue] = React.useState(0);
|
||||
|
||||
const handleTabChange = (event: React.SyntheticEvent, newValue: number) => {
|
||||
setTabValue(newValue);
|
||||
};
|
||||
|
||||
return (
|
||||
<Box sx={{ width: '100%' }}>
|
||||
{/* Styled Tabs */}
|
||||
<Box sx={{
|
||||
borderBottom: 1,
|
||||
borderColor: 'rgba(255, 255, 255, 0.2)',
|
||||
mb: 2,
|
||||
background: 'rgba(255, 255, 255, 0.03)',
|
||||
borderRadius: 2,
|
||||
p: 1
|
||||
}}>
|
||||
<Tabs
|
||||
value={tabValue}
|
||||
onChange={handleTabChange}
|
||||
variant="fullWidth"
|
||||
sx={{
|
||||
'& .MuiTab-root': {
|
||||
color: 'rgba(255, 255, 255, 0.7)',
|
||||
fontWeight: 600,
|
||||
fontSize: '0.875rem',
|
||||
textTransform: 'none',
|
||||
minHeight: 48,
|
||||
'&.Mui-selected': {
|
||||
color: 'white',
|
||||
background: 'rgba(255, 255, 255, 0.1)',
|
||||
borderRadius: 1,
|
||||
},
|
||||
},
|
||||
'& .MuiTabs-indicator': {
|
||||
display: 'none',
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Tab
|
||||
label={
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||
<ThumbUpIcon sx={{ color: '#388E3C' }} />
|
||||
The Good
|
||||
<Badge
|
||||
badgeContent={categorizedData.good.length}
|
||||
color="success"
|
||||
sx={{ '& .MuiBadge-badge': { fontSize: '0.7rem' } }}
|
||||
/>
|
||||
</Box>
|
||||
}
|
||||
/>
|
||||
<Tab
|
||||
label={
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||
<WarningIcon2 sx={{ color: '#F57C00' }} />
|
||||
The Bad
|
||||
<Badge
|
||||
badgeContent={categorizedData.bad.length}
|
||||
color="warning"
|
||||
sx={{ '& .MuiBadge-badge': { fontSize: '0.7rem' } }}
|
||||
/>
|
||||
</Box>
|
||||
}
|
||||
/>
|
||||
<Tab
|
||||
label={
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||
<ThumbDownIcon sx={{ color: '#D32F2F' }} />
|
||||
The Ugly
|
||||
<Badge
|
||||
badgeContent={categorizedData.ugly.length}
|
||||
color="error"
|
||||
sx={{ '& .MuiBadge-badge': { fontSize: '0.7rem' } }}
|
||||
/>
|
||||
</Box>
|
||||
}
|
||||
/>
|
||||
</Tabs>
|
||||
</Box>
|
||||
|
||||
<TabPanel value={tabValue} index={0}>
|
||||
<Typography variant="h6" sx={{ color: '#388E3C', mb: 2, fontWeight: 600 }}>
|
||||
✅ Good Performance ({categorizedData.good.length} categories)
|
||||
</Typography>
|
||||
{categorizedData.good.length > 0 ? (
|
||||
categorizedData.good.map(({ category, data }) =>
|
||||
<CategoryCard
|
||||
key={category}
|
||||
category={category}
|
||||
data={data}
|
||||
isExpanded={expandedCategories.has(category)}
|
||||
onToggle={onToggleCategory}
|
||||
onIssueClick={onIssueClick}
|
||||
onAIAction={onAIAction}
|
||||
/>
|
||||
)
|
||||
) : (
|
||||
<Typography variant="body2" sx={{ color: 'rgba(255, 255, 255, 0.7)', textAlign: 'center', py: 4 }}>
|
||||
No excellent performing categories found
|
||||
</Typography>
|
||||
)}
|
||||
</TabPanel>
|
||||
|
||||
<TabPanel value={tabValue} index={1}>
|
||||
<Typography variant="h6" sx={{ color: '#F57C00', mb: 2, fontWeight: 600 }}>
|
||||
⚠️ Needs Improvement ({categorizedData.bad.length} categories)
|
||||
</Typography>
|
||||
{categorizedData.bad.length > 0 ? (
|
||||
categorizedData.bad.map(({ category, data }) =>
|
||||
<CategoryCard
|
||||
key={category}
|
||||
category={category}
|
||||
data={data}
|
||||
isExpanded={expandedCategories.has(category)}
|
||||
onToggle={onToggleCategory}
|
||||
onIssueClick={onIssueClick}
|
||||
onAIAction={onAIAction}
|
||||
/>
|
||||
)
|
||||
) : (
|
||||
<Typography variant="body2" sx={{ color: 'rgba(255, 255, 255, 0.7)', textAlign: 'center', py: 4 }}>
|
||||
No categories needing improvement
|
||||
</Typography>
|
||||
)}
|
||||
</TabPanel>
|
||||
|
||||
<TabPanel value={tabValue} index={2}>
|
||||
<Typography variant="h6" sx={{ color: '#D32F2F', mb: 2, fontWeight: 600 }}>
|
||||
❌ Critical Issues ({categorizedData.ugly.length} categories)
|
||||
</Typography>
|
||||
{categorizedData.ugly.length > 0 ? (
|
||||
categorizedData.ugly.map(({ category, data }) =>
|
||||
<CategoryCard
|
||||
key={category}
|
||||
category={category}
|
||||
data={data}
|
||||
isExpanded={expandedCategories.has(category)}
|
||||
onToggle={onToggleCategory}
|
||||
onIssueClick={onIssueClick}
|
||||
onAIAction={onAIAction}
|
||||
/>
|
||||
)
|
||||
) : (
|
||||
<Typography variant="body2" sx={{ color: 'rgba(255, 255, 255, 0.7)', textAlign: 'center', py: 4 }}>
|
||||
No critical issues found
|
||||
</Typography>
|
||||
)}
|
||||
</TabPanel>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default AnalysisTabs;
|
||||
136
frontend/src/components/SEODashboard/components/CategoryCard.tsx
Normal file
136
frontend/src/components/SEODashboard/components/CategoryCard.tsx
Normal file
@@ -0,0 +1,136 @@
|
||||
import React from 'react';
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
Typography,
|
||||
Chip,
|
||||
LinearProgress,
|
||||
Collapse,
|
||||
IconButton,
|
||||
Divider,
|
||||
Box
|
||||
} from '@mui/material';
|
||||
import {
|
||||
ExpandMore as ExpandMoreIcon,
|
||||
ExpandLess as ExpandLessIcon
|
||||
} from '@mui/icons-material';
|
||||
import { CategoryCardProps } from '../../shared/types';
|
||||
import { getCategoryIcon, getCategoryTitle, getStatusColor } from './seoUtils';
|
||||
import IssueList from './IssueList';
|
||||
|
||||
const CategoryCard: React.FC<CategoryCardProps> = ({
|
||||
category,
|
||||
data,
|
||||
isExpanded,
|
||||
onToggle,
|
||||
onIssueClick,
|
||||
onAIAction
|
||||
}) => {
|
||||
const score = data.score;
|
||||
const status = score >= 80 ? 'excellent' : score >= 60 ? 'good' : score >= 40 ? 'needs_improvement' : 'poor';
|
||||
|
||||
return (
|
||||
<Card
|
||||
sx={{
|
||||
background: 'rgba(255, 255, 255, 0.08)',
|
||||
border: '1px solid rgba(255, 255, 255, 0.15)',
|
||||
cursor: 'pointer',
|
||||
transition: 'all 0.3s ease',
|
||||
mb: 2,
|
||||
'&:hover': {
|
||||
background: 'rgba(255, 255, 255, 0.12)',
|
||||
transform: 'translateY(-2px)',
|
||||
boxShadow: '0 8px 25px rgba(0,0,0,0.3)',
|
||||
},
|
||||
}}
|
||||
onClick={() => onToggle(category)}
|
||||
>
|
||||
<CardContent sx={{ p: 2 }}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', mb: 1 }}>
|
||||
{getCategoryIcon(category)}
|
||||
<Typography variant="subtitle2" sx={{ color: 'white', ml: 1, flex: 1, fontWeight: 600 }}>
|
||||
{getCategoryTitle(category)}
|
||||
</Typography>
|
||||
<Chip
|
||||
label={score}
|
||||
size="small"
|
||||
sx={{
|
||||
backgroundColor: getStatusColor(status),
|
||||
color: 'white',
|
||||
fontWeight: 600,
|
||||
fontSize: '0.75rem',
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
<LinearProgress
|
||||
variant="determinate"
|
||||
value={score}
|
||||
sx={{
|
||||
height: 4,
|
||||
borderRadius: 2,
|
||||
backgroundColor: 'rgba(255, 255, 255, 0.1)',
|
||||
'& .MuiLinearProgress-bar': {
|
||||
backgroundColor: getStatusColor(status),
|
||||
borderRadius: 2,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
|
||||
<IconButton
|
||||
size="small"
|
||||
sx={{
|
||||
color: 'rgba(255, 255, 255, 0.7)',
|
||||
mt: 1,
|
||||
'&:hover': { color: 'white' }
|
||||
}}
|
||||
>
|
||||
{isExpanded ? <ExpandLessIcon /> : <ExpandMoreIcon />}
|
||||
</IconButton>
|
||||
</CardContent>
|
||||
|
||||
<Collapse in={isExpanded}>
|
||||
<Divider sx={{ borderColor: 'rgba(255, 255, 255, 0.1)' }} />
|
||||
<Box sx={{ p: 2, pt: 1 }}>
|
||||
<IssueList
|
||||
issues={data.issues || []}
|
||||
type="critical"
|
||||
onIssueClick={onIssueClick}
|
||||
onAIAction={onAIAction}
|
||||
/>
|
||||
<IssueList
|
||||
issues={data.warnings || []}
|
||||
type="warning"
|
||||
onIssueClick={onIssueClick}
|
||||
onAIAction={onAIAction}
|
||||
/>
|
||||
<IssueList
|
||||
issues={data.recommendations || []}
|
||||
type="recommendation"
|
||||
onIssueClick={onIssueClick}
|
||||
onAIAction={onAIAction}
|
||||
/>
|
||||
|
||||
{/* Show key metrics if available */}
|
||||
{data.load_time && (
|
||||
<Typography variant="caption" sx={{ color: '#666', display: 'block', mt: 1 }}>
|
||||
Load Time: {data.load_time.toFixed(2)}s
|
||||
</Typography>
|
||||
)}
|
||||
{data.word_count && (
|
||||
<Typography variant="caption" sx={{ color: '#666', display: 'block' }}>
|
||||
Words: {data.word_count}
|
||||
</Typography>
|
||||
)}
|
||||
{data.total_headers !== undefined && (
|
||||
<Typography variant="caption" sx={{ color: '#666', display: 'block' }}>
|
||||
Security Headers: {data.total_headers}/6
|
||||
</Typography>
|
||||
)}
|
||||
</Box>
|
||||
</Collapse>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
export default CategoryCard;
|
||||
@@ -0,0 +1,76 @@
|
||||
import React from 'react';
|
||||
import {
|
||||
Paper,
|
||||
Typography,
|
||||
Button
|
||||
} from '@mui/material';
|
||||
import {
|
||||
Build as BuildIcon
|
||||
} from '@mui/icons-material';
|
||||
import { CriticalIssueCardProps } from '../../shared/types';
|
||||
import { formatMessage } from './seoUtils';
|
||||
|
||||
const CriticalIssueCard: React.FC<CriticalIssueCardProps> = ({
|
||||
issue,
|
||||
index,
|
||||
onClick,
|
||||
onAIAction
|
||||
}) => {
|
||||
const { title, details } = formatMessage(issue.message);
|
||||
|
||||
return (
|
||||
<Paper sx={{
|
||||
p: 2,
|
||||
mb: 1,
|
||||
background: 'rgba(211, 47, 47, 0.08)',
|
||||
border: '1px solid rgba(211, 47, 47, 0.2)',
|
||||
cursor: 'pointer',
|
||||
'&:hover': { background: 'rgba(211, 47, 47, 0.12)' }
|
||||
}}
|
||||
onClick={() => onClick(issue)}
|
||||
>
|
||||
<Typography variant="subtitle2" sx={{ color: '#D32F2F', fontWeight: 600, mb: 1 }}>
|
||||
{title}
|
||||
</Typography>
|
||||
|
||||
{details && (
|
||||
<Typography variant="body2" sx={{
|
||||
color: 'rgba(255, 255, 255, 0.9)',
|
||||
mb: 1,
|
||||
fontSize: '0.875rem',
|
||||
lineHeight: 1.4,
|
||||
wordBreak: 'break-word'
|
||||
}}>
|
||||
{details}
|
||||
</Typography>
|
||||
)}
|
||||
|
||||
<Typography variant="caption" sx={{
|
||||
color: 'rgba(255, 255, 255, 0.8)',
|
||||
display: 'block',
|
||||
mb: 1,
|
||||
fontSize: '0.75rem'
|
||||
}}>
|
||||
Location: {issue.location}
|
||||
</Typography>
|
||||
|
||||
<Button
|
||||
size="small"
|
||||
variant="contained"
|
||||
startIcon={<BuildIcon />}
|
||||
sx={{
|
||||
backgroundColor: '#D32F2F',
|
||||
'&:hover': { backgroundColor: '#B71C1C' }
|
||||
}}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onAIAction(issue.action, issue);
|
||||
}}
|
||||
>
|
||||
Fix with AI
|
||||
</Button>
|
||||
</Paper>
|
||||
);
|
||||
};
|
||||
|
||||
export default CriticalIssueCard;
|
||||
@@ -0,0 +1,72 @@
|
||||
import React from 'react';
|
||||
import { Box, Typography, Chip, LinearProgress } from '@mui/material';
|
||||
import { TrendingUp as TrendingUpIcon, TrendingDown as TrendingDownIcon } from '@mui/icons-material';
|
||||
import { EnhancedGlassCard } from '../../shared/styled';
|
||||
import { SEOHealthScore } from '../../../api/seoDashboard';
|
||||
|
||||
interface HealthScoreProps {
|
||||
score: SEOHealthScore;
|
||||
}
|
||||
|
||||
const HealthScore: React.FC<HealthScoreProps> = ({ score }) => {
|
||||
return (
|
||||
<EnhancedGlassCard>
|
||||
<Box sx={{ p: 3 }}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', mb: 2 }}>
|
||||
<Typography variant="h6" sx={{ color: 'white', fontWeight: 600 }}>
|
||||
🎯 SEO Health Score
|
||||
</Typography>
|
||||
<Chip
|
||||
label={score.label}
|
||||
size="small"
|
||||
sx={{
|
||||
background: `${score.color}20`,
|
||||
color: score.color,
|
||||
border: `1px solid ${score.color}40`,
|
||||
fontWeight: 600,
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2, mb: 2 }}>
|
||||
<Typography variant="h2" sx={{
|
||||
color: 'white',
|
||||
fontWeight: 800,
|
||||
fontSize: { xs: '2.5rem', md: '3.5rem' }
|
||||
}}>
|
||||
{score.score}/100
|
||||
</Typography>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||
{score.trend === 'up' ? (
|
||||
<TrendingUpIcon sx={{ color: '#4CAF50', fontSize: 24 }} />
|
||||
) : (
|
||||
<TrendingDownIcon sx={{ color: '#F44336', fontSize: 24 }} />
|
||||
)}
|
||||
<Typography variant="h6" sx={{
|
||||
color: score.trend === 'up' ? '#4CAF50' : '#F44336',
|
||||
fontWeight: 600
|
||||
}}>
|
||||
{score.trend === 'up' ? '+' : ''}{score.change} this month
|
||||
</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
<LinearProgress
|
||||
variant="determinate"
|
||||
value={score.score}
|
||||
sx={{
|
||||
height: 8,
|
||||
borderRadius: 4,
|
||||
backgroundColor: 'rgba(255, 255, 255, 0.2)',
|
||||
'& .MuiLinearProgress-bar': {
|
||||
background: `linear-gradient(90deg, ${score.color}, ${score.color}80)`,
|
||||
borderRadius: 4,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
</EnhancedGlassCard>
|
||||
);
|
||||
};
|
||||
|
||||
export default HealthScore;
|
||||
@@ -0,0 +1,101 @@
|
||||
import React from 'react';
|
||||
import {
|
||||
Dialog,
|
||||
DialogTitle,
|
||||
DialogContent,
|
||||
DialogActions,
|
||||
Button,
|
||||
Typography,
|
||||
Box,
|
||||
Paper
|
||||
} from '@mui/material';
|
||||
import {
|
||||
Build as BuildIcon
|
||||
} from '@mui/icons-material';
|
||||
import { IssueDetailsDialogProps } from '../../shared/types';
|
||||
|
||||
const IssueDetailsDialog: React.FC<IssueDetailsDialogProps> = ({
|
||||
open,
|
||||
issue,
|
||||
onClose,
|
||||
onAIAction
|
||||
}) => {
|
||||
if (!issue) return null;
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
open={open}
|
||||
onClose={onClose}
|
||||
maxWidth="md"
|
||||
fullWidth
|
||||
>
|
||||
<DialogTitle sx={{
|
||||
color: issue.type === 'critical' ? '#D32F2F' :
|
||||
issue.type === 'warning' ? '#F57C00' : '#388E3C',
|
||||
fontWeight: 600
|
||||
}}>
|
||||
{issue.message}
|
||||
</DialogTitle>
|
||||
<DialogContent>
|
||||
<Box sx={{ mb: 2 }}>
|
||||
<Typography variant="subtitle2" sx={{ fontWeight: 600, mb: 1 }}>
|
||||
Location:
|
||||
</Typography>
|
||||
<Typography variant="body2" sx={{ color: 'rgba(0, 0, 0, 0.7)' }}>
|
||||
{issue.location}
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
{issue.current_value && (
|
||||
<Box sx={{ mb: 2 }}>
|
||||
<Typography variant="subtitle2" sx={{ fontWeight: 600, mb: 1 }}>
|
||||
Current Value:
|
||||
</Typography>
|
||||
<Typography variant="body2" sx={{ color: 'rgba(0, 0, 0, 0.7)' }}>
|
||||
{issue.current_value}
|
||||
</Typography>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
<Box sx={{ mb: 2 }}>
|
||||
<Typography variant="subtitle2" sx={{ fontWeight: 600, mb: 1 }}>
|
||||
Recommended Fix:
|
||||
</Typography>
|
||||
<Typography variant="body2" sx={{ color: 'rgba(0, 0, 0, 0.7)', mb: 1 }}>
|
||||
{issue.fix}
|
||||
</Typography>
|
||||
{issue.code_example && (
|
||||
<Paper sx={{ p: 2, background: '#f5f5f5', fontFamily: 'monospace', fontSize: '0.875rem' }}>
|
||||
{issue.code_example}
|
||||
</Paper>
|
||||
)}
|
||||
</Box>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={onClose}>
|
||||
Close
|
||||
</Button>
|
||||
<Button
|
||||
variant="contained"
|
||||
startIcon={<BuildIcon />}
|
||||
onClick={() => {
|
||||
onAIAction(issue.action, issue);
|
||||
onClose();
|
||||
}}
|
||||
sx={{
|
||||
backgroundColor: issue.type === 'critical' ? '#D32F2F' :
|
||||
issue.type === 'warning' ? '#F57C00' : '#388E3C',
|
||||
'&:hover': {
|
||||
backgroundColor: issue.type === 'critical' ? '#B71C1C' :
|
||||
issue.type === 'warning' ? '#F57C00' : '#388E3C'
|
||||
}
|
||||
}}
|
||||
>
|
||||
Fix with AI
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
export default IssueDetailsDialog;
|
||||
123
frontend/src/components/SEODashboard/components/IssueList.tsx
Normal file
123
frontend/src/components/SEODashboard/components/IssueList.tsx
Normal file
@@ -0,0 +1,123 @@
|
||||
import React from 'react';
|
||||
import {
|
||||
Box,
|
||||
Typography,
|
||||
List,
|
||||
ListItem,
|
||||
ListItemIcon,
|
||||
ListItemText,
|
||||
Button
|
||||
} from '@mui/material';
|
||||
import {
|
||||
Error as ErrorIcon,
|
||||
Warning as WarningIcon,
|
||||
Info as InfoIcon,
|
||||
PlayArrow as PlayArrowIcon
|
||||
} from '@mui/icons-material';
|
||||
import { IssueListProps } from '../../shared/types';
|
||||
|
||||
const IssueList: React.FC<IssueListProps> = ({
|
||||
issues,
|
||||
type,
|
||||
onIssueClick,
|
||||
onAIAction
|
||||
}) => {
|
||||
if (!issues || issues.length === 0) return null;
|
||||
|
||||
const colors = {
|
||||
critical: '#D32F2F', // Softer red instead of bright #F44336
|
||||
warning: '#F57C00', // Softer orange instead of bright #FF9800
|
||||
recommendation: '#388E3C' // Softer green instead of bright #4CAF50
|
||||
};
|
||||
|
||||
const icons = {
|
||||
critical: <ErrorIcon sx={{ fontSize: 16, color: colors.critical }} />,
|
||||
warning: <WarningIcon sx={{ fontSize: 16, color: colors.warning }} />,
|
||||
recommendation: <InfoIcon sx={{ fontSize: 16, color: colors.recommendation }} />
|
||||
};
|
||||
|
||||
const typeLabels = {
|
||||
critical: 'Critical Issues',
|
||||
warning: 'Warnings',
|
||||
recommendation: 'Recommendations'
|
||||
};
|
||||
|
||||
return (
|
||||
<Box sx={{ mt: 1 }}>
|
||||
<Typography variant="subtitle2" sx={{
|
||||
color: colors[type],
|
||||
fontWeight: 600,
|
||||
mb: 1,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 0.5
|
||||
}}>
|
||||
{icons[type]}
|
||||
{typeLabels[type]} ({issues.length})
|
||||
</Typography>
|
||||
<List dense>
|
||||
{issues.slice(0, 3).map((issue, index) => (
|
||||
<ListItem
|
||||
key={index}
|
||||
sx={{
|
||||
p: 1,
|
||||
mb: 0.5,
|
||||
background: 'rgba(255, 255, 255, 0.05)',
|
||||
borderRadius: 1,
|
||||
cursor: 'pointer',
|
||||
'&:hover': { background: 'rgba(255, 255, 255, 0.1)' }
|
||||
}}
|
||||
onClick={() => onIssueClick(issue)}
|
||||
>
|
||||
<ListItemIcon sx={{ minWidth: 32 }}>
|
||||
{icons[type]}
|
||||
</ListItemIcon>
|
||||
<ListItemText
|
||||
primary={issue.message}
|
||||
secondary={`Location: ${issue.location}`}
|
||||
primaryTypographyProps={{
|
||||
variant: 'body2',
|
||||
color: colors[type],
|
||||
fontWeight: 500
|
||||
}}
|
||||
secondaryTypographyProps={{
|
||||
variant: 'caption',
|
||||
color: 'rgba(255, 255, 255, 0.7)'
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
size="small"
|
||||
variant="outlined"
|
||||
startIcon={<PlayArrowIcon />}
|
||||
sx={{
|
||||
color: colors[type],
|
||||
borderColor: colors[type],
|
||||
'&:hover': { borderColor: colors[type], backgroundColor: `${colors[type]}20` }
|
||||
}}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onAIAction(issue.action, issue);
|
||||
}}
|
||||
>
|
||||
Fix with AI
|
||||
</Button>
|
||||
</ListItem>
|
||||
))}
|
||||
{issues.length > 3 && (
|
||||
<ListItem sx={{ p: 1 }}>
|
||||
<ListItemText
|
||||
primary={`... and ${issues.length - 3} more`}
|
||||
primaryTypographyProps={{
|
||||
variant: 'body2',
|
||||
color: colors[type],
|
||||
fontSize: '0.875rem'
|
||||
}}
|
||||
/>
|
||||
</ListItem>
|
||||
)}
|
||||
</List>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default IssueList;
|
||||
@@ -0,0 +1,79 @@
|
||||
import React from 'react';
|
||||
import { Box, Typography } from '@mui/material';
|
||||
import { TrendingUp as TrendingUpIcon, TrendingDown as TrendingDownIcon } from '@mui/icons-material';
|
||||
import { GlassCard } from '../../shared/styled';
|
||||
import { SEOMetric } from '../../../api/seoDashboard';
|
||||
|
||||
interface MetricCardProps {
|
||||
title: string;
|
||||
metric: SEOMetric;
|
||||
icon: React.ReactNode;
|
||||
color?: string;
|
||||
}
|
||||
|
||||
const MetricCard: React.FC<MetricCardProps> = ({
|
||||
title,
|
||||
metric,
|
||||
icon,
|
||||
color = '#2196F3'
|
||||
}) => {
|
||||
return (
|
||||
<GlassCard>
|
||||
<Box sx={{ p: 3 }}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', mb: 2 }}>
|
||||
<Box
|
||||
sx={{
|
||||
width: 48,
|
||||
height: 48,
|
||||
borderRadius: 3,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
background: `${color}20`,
|
||||
border: `1px solid ${color}40`,
|
||||
}}
|
||||
>
|
||||
{icon}
|
||||
</Box>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||
{metric.trend === 'up' ? (
|
||||
<TrendingUpIcon sx={{ color: '#4CAF50', fontSize: 20 }} />
|
||||
) : (
|
||||
<TrendingDownIcon sx={{ color: '#F44336', fontSize: 20 }} />
|
||||
)}
|
||||
<Typography variant="body2" sx={{
|
||||
color: metric.trend === 'up' ? '#4CAF50' : '#F44336',
|
||||
fontWeight: 600
|
||||
}}>
|
||||
{metric.change > 0 ? '+' : ''}{metric.change}%
|
||||
</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
<Typography variant="h4" sx={{
|
||||
color: 'white',
|
||||
fontWeight: 700,
|
||||
mb: 1
|
||||
}}>
|
||||
{metric.value.toLocaleString()}
|
||||
</Typography>
|
||||
|
||||
<Typography variant="body2" sx={{
|
||||
color: 'rgba(255, 255, 255, 0.8)',
|
||||
mb: 2
|
||||
}}>
|
||||
{title}
|
||||
</Typography>
|
||||
|
||||
<Typography variant="caption" sx={{
|
||||
color: 'rgba(255, 255, 255, 0.6)',
|
||||
fontStyle: 'italic'
|
||||
}}>
|
||||
{metric.description}
|
||||
</Typography>
|
||||
</Box>
|
||||
</GlassCard>
|
||||
);
|
||||
};
|
||||
|
||||
export default MetricCard;
|
||||
@@ -0,0 +1,126 @@
|
||||
import React from 'react';
|
||||
import { Box, Typography, Chip, Button } from '@mui/material';
|
||||
import {
|
||||
CheckCircle as CheckCircleIcon,
|
||||
Warning as WarningIcon,
|
||||
Error as ErrorIcon,
|
||||
Info as InfoIcon
|
||||
} from '@mui/icons-material';
|
||||
import { GlassCard } from '../../shared/styled';
|
||||
import { PlatformStatus as PlatformStatusType } from '../../../api/seoDashboard';
|
||||
import { getStatusColor, getStatusIcon } from '../../shared/utils';
|
||||
|
||||
interface PlatformStatusProps {
|
||||
platforms: Record<string, PlatformStatusType>;
|
||||
}
|
||||
|
||||
const PlatformStatus: React.FC<PlatformStatusProps> = ({ platforms }) => {
|
||||
const getStatusIconComponent = (status: string) => {
|
||||
switch (status) {
|
||||
case 'excellent':
|
||||
case 'strong':
|
||||
return <CheckCircleIcon />;
|
||||
case 'good':
|
||||
return <WarningIcon />;
|
||||
case 'needs_action':
|
||||
return <ErrorIcon />;
|
||||
default:
|
||||
return <InfoIcon />;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<GlassCard>
|
||||
<Box sx={{ p: 3 }}>
|
||||
<Typography variant="h6" sx={{
|
||||
color: 'white',
|
||||
fontWeight: 600,
|
||||
mb: 3
|
||||
}}>
|
||||
🌐 Platform Overview
|
||||
</Typography>
|
||||
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
|
||||
{Object.entries(platforms).map(([platform, data]) => (
|
||||
<Box key={platform} sx={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
p: 2,
|
||||
background: 'rgba(255, 255, 255, 0.05)',
|
||||
borderRadius: 2,
|
||||
border: '1px solid rgba(255, 255, 255, 0.1)'
|
||||
}}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2 }}>
|
||||
{getStatusIconComponent(data.status)}
|
||||
<Typography variant="body2" sx={{
|
||||
color: 'rgba(255, 255, 255, 0.9)',
|
||||
textTransform: 'capitalize'
|
||||
}}>
|
||||
{platform.replace(/([A-Z])/g, ' $1').replace(/_/g, ' ').trim()}
|
||||
</Typography>
|
||||
</Box>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||
<Chip
|
||||
label={data.status.replace('_', ' ')}
|
||||
size="small"
|
||||
sx={{
|
||||
background: `${getStatusColor(data.status)}20`,
|
||||
color: getStatusColor(data.status),
|
||||
border: `1px solid ${getStatusColor(data.status)}40`,
|
||||
fontWeight: 600,
|
||||
}}
|
||||
/>
|
||||
{data.connected && (
|
||||
<Chip
|
||||
label="Connected"
|
||||
size="small"
|
||||
sx={{
|
||||
background: 'rgba(76, 175, 80, 0.2)',
|
||||
color: '#4CAF50',
|
||||
border: '1px solid rgba(76, 175, 80, 0.4)',
|
||||
fontWeight: 600,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
))}
|
||||
</Box>
|
||||
|
||||
<Box sx={{ display: 'flex', gap: 2, mt: 3 }}>
|
||||
<Button
|
||||
variant="outlined"
|
||||
size="small"
|
||||
sx={{
|
||||
color: 'white',
|
||||
borderColor: 'rgba(255, 255, 255, 0.3)',
|
||||
'&:hover': {
|
||||
borderColor: 'rgba(255, 255, 255, 0.5)',
|
||||
background: 'rgba(255, 255, 255, 0.1)',
|
||||
},
|
||||
}}
|
||||
>
|
||||
View Detailed Analysis
|
||||
</Button>
|
||||
<Button
|
||||
variant="outlined"
|
||||
size="small"
|
||||
sx={{
|
||||
color: 'white',
|
||||
borderColor: 'rgba(255, 255, 255, 0.3)',
|
||||
'&:hover': {
|
||||
borderColor: 'rgba(255, 255, 255, 0.5)',
|
||||
background: 'rgba(255, 255, 255, 0.1)',
|
||||
},
|
||||
}}
|
||||
>
|
||||
Compare Platforms
|
||||
</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
</GlassCard>
|
||||
);
|
||||
};
|
||||
|
||||
export default PlatformStatus;
|
||||
@@ -0,0 +1,37 @@
|
||||
import React from 'react';
|
||||
import {
|
||||
Alert,
|
||||
IconButton
|
||||
} from '@mui/material';
|
||||
import {
|
||||
Close as CloseIcon
|
||||
} from '@mui/icons-material';
|
||||
import { SEOAnalysisErrorProps } from '../../shared/types';
|
||||
|
||||
const SEOAnalysisError: React.FC<SEOAnalysisErrorProps> = ({
|
||||
error,
|
||||
showError,
|
||||
onCloseError
|
||||
}) => {
|
||||
if (!error || !showError) return null;
|
||||
|
||||
return (
|
||||
<Alert
|
||||
severity="error"
|
||||
sx={{ mb: 2 }}
|
||||
action={
|
||||
<IconButton
|
||||
color="inherit"
|
||||
size="small"
|
||||
onClick={onCloseError}
|
||||
>
|
||||
<CloseIcon />
|
||||
</IconButton>
|
||||
}
|
||||
>
|
||||
{error}
|
||||
</Alert>
|
||||
);
|
||||
};
|
||||
|
||||
export default SEOAnalysisError;
|
||||
@@ -0,0 +1,35 @@
|
||||
import React from 'react';
|
||||
import {
|
||||
Box,
|
||||
Typography,
|
||||
LinearProgress
|
||||
} from '@mui/material';
|
||||
import { SEOAnalysisLoadingProps } from '../../shared/types';
|
||||
|
||||
const SEOAnalysisLoading: React.FC<SEOAnalysisLoadingProps> = ({ loading }) => {
|
||||
if (!loading) return null;
|
||||
|
||||
return (
|
||||
<Box sx={{ mb: 3 }}>
|
||||
<Typography variant="body1" sx={{ color: 'white', mb: 2 }}>
|
||||
🤖 AI is analyzing your website...
|
||||
</Typography>
|
||||
<Typography variant="body2" sx={{ color: 'rgba(255, 255, 255, 0.7)', mb: 2 }}>
|
||||
Identifying specific issues and generating actionable fixes...
|
||||
</Typography>
|
||||
<LinearProgress
|
||||
sx={{
|
||||
height: 6,
|
||||
borderRadius: 3,
|
||||
backgroundColor: 'rgba(255, 255, 255, 0.1)',
|
||||
'& .MuiLinearProgress-bar': {
|
||||
background: 'linear-gradient(90deg, #2196F3, #4CAF50)',
|
||||
borderRadius: 3,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default SEOAnalysisLoading;
|
||||
@@ -0,0 +1,267 @@
|
||||
import React, { useState } from 'react';
|
||||
import {
|
||||
Box,
|
||||
Typography,
|
||||
Button,
|
||||
Grid,
|
||||
Chip,
|
||||
LinearProgress,
|
||||
IconButton,
|
||||
Tooltip,
|
||||
Stack
|
||||
} from '@mui/material';
|
||||
import {
|
||||
Refresh as RefreshIcon,
|
||||
Language as LanguageIcon,
|
||||
Help as HelpIcon
|
||||
} from '@mui/icons-material';
|
||||
import { motion, AnimatePresence } from 'framer-motion';
|
||||
|
||||
// Shared styled components
|
||||
import { GlassCard } from '../../shared/styled';
|
||||
|
||||
// Types
|
||||
import { SEOAnalyzerPanelProps } from '../../shared/types';
|
||||
|
||||
// Utilities
|
||||
import {
|
||||
getStatusColor,
|
||||
getStatusIcon,
|
||||
categorizeAnalysisData
|
||||
} from './seoUtils';
|
||||
|
||||
// Components
|
||||
import CategoryCard from './CategoryCard';
|
||||
import CriticalIssueCard from './CriticalIssueCard';
|
||||
import AnalysisTabs from './AnalysisTabs';
|
||||
import IssueDetailsDialog from './IssueDetailsDialog';
|
||||
import AnalysisDetailsDialog from './AnalysisDetailsDialog';
|
||||
import SEOAnalysisLoading from './SEOAnalysisLoading';
|
||||
import SEOAnalysisError from './SEOAnalysisError';
|
||||
|
||||
const SEOAnalyzerPanel: React.FC<SEOAnalyzerPanelProps> = ({
|
||||
analysisData,
|
||||
onRunAnalysis,
|
||||
loading,
|
||||
error
|
||||
}) => {
|
||||
const [expandedCategories, setExpandedCategories] = useState<Set<string>>(new Set());
|
||||
const [showError, setShowError] = useState(true);
|
||||
const [selectedIssue, setSelectedIssue] = useState<any>(null);
|
||||
const [showIssueDialog, setShowIssueDialog] = useState(false);
|
||||
const [showDetailsDialog, setShowDetailsDialog] = useState(false);
|
||||
|
||||
// Debug logging
|
||||
console.log('SEOAnalyzerPanel received data:', {
|
||||
analysisData,
|
||||
loading,
|
||||
error,
|
||||
hasUrl: analysisData?.url,
|
||||
hasData: analysisData?.data,
|
||||
criticalIssues: analysisData?.critical_issues?.length
|
||||
});
|
||||
|
||||
const toggleCategory = (category: string) => {
|
||||
const newExpanded = new Set(expandedCategories);
|
||||
if (newExpanded.has(category)) {
|
||||
newExpanded.delete(category);
|
||||
} else {
|
||||
newExpanded.add(category);
|
||||
}
|
||||
setExpandedCategories(newExpanded);
|
||||
};
|
||||
|
||||
const handleIssueClick = (issue: any) => {
|
||||
setSelectedIssue(issue);
|
||||
setShowIssueDialog(true);
|
||||
};
|
||||
|
||||
const handleAIAction = (action: string, issue: any) => {
|
||||
// This would integrate with AI to generate specific fixes
|
||||
console.log(`AI Action: ${action} for issue:`, issue);
|
||||
// In a real implementation, this would call an AI service
|
||||
};
|
||||
|
||||
const categorizedData = categorizeAnalysisData(analysisData);
|
||||
|
||||
return (
|
||||
<>
|
||||
<GlassCard sx={{ p: 3, mb: 3 }}>
|
||||
{/* Header */}
|
||||
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 3 }}>
|
||||
<Typography variant="h5" sx={{ color: 'white', fontWeight: 600 }}>
|
||||
🔍 AI-Powered SEO Analysis
|
||||
</Typography>
|
||||
|
||||
<Stack direction="row" spacing={2}>
|
||||
{/* Index Entire Website Button - Region 1 */}
|
||||
<Tooltip
|
||||
title="Pro Feature: Index your entire website with AI-powered analysis. Get comprehensive insights across all pages, blog posts, and content. Coming soon!"
|
||||
placement="top"
|
||||
>
|
||||
<span>
|
||||
<Button
|
||||
variant="outlined"
|
||||
startIcon={<LanguageIcon />}
|
||||
disabled
|
||||
sx={{
|
||||
borderColor: 'rgba(255, 255, 255, 0.3)',
|
||||
color: 'rgba(255, 255, 255, 0.7)',
|
||||
'&:hover': {
|
||||
borderColor: 'rgba(255, 255, 255, 0.5)',
|
||||
backgroundColor: 'rgba(255, 255, 255, 0.05)'
|
||||
},
|
||||
'&.Mui-disabled': {
|
||||
borderColor: 'rgba(255, 255, 255, 0.2)',
|
||||
color: 'rgba(255, 255, 255, 0.5)'
|
||||
}
|
||||
}}
|
||||
>
|
||||
Index Entire Website
|
||||
</Button>
|
||||
</span>
|
||||
</Tooltip>
|
||||
|
||||
<Button
|
||||
variant="contained"
|
||||
startIcon={<RefreshIcon />}
|
||||
onClick={onRunAnalysis}
|
||||
disabled={loading}
|
||||
sx={{
|
||||
background: 'linear-gradient(45deg, #2196F3, #21CBF3)',
|
||||
color: 'white',
|
||||
'&:hover': {
|
||||
background: 'linear-gradient(45deg, #1976D2, #1E88E5)',
|
||||
},
|
||||
}}
|
||||
>
|
||||
{loading ? 'Analyzing...' : 'Run Analysis'}
|
||||
</Button>
|
||||
</Stack>
|
||||
</Box>
|
||||
|
||||
{/* Error Display */}
|
||||
<SEOAnalysisError
|
||||
error={error}
|
||||
showError={showError}
|
||||
onCloseError={() => setShowError(false)}
|
||||
/>
|
||||
|
||||
{/* Loading State */}
|
||||
<SEOAnalysisLoading loading={loading} />
|
||||
|
||||
{/* Analysis Results */}
|
||||
<AnimatePresence>
|
||||
{analysisData && analysisData.url && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.6 }}
|
||||
>
|
||||
<Grid container spacing={3}>
|
||||
{/* Left Column - Overall Score & Critical Issues */}
|
||||
<Grid item xs={12} md={4}>
|
||||
{/* Overall Score - Region 2 */}
|
||||
<Box sx={{ mb: 3, p: 2, background: 'rgba(255, 255, 255, 0.05)', borderRadius: 2 }}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', mb: 1 }}>
|
||||
{getStatusIcon(analysisData.health_status)}
|
||||
<Typography variant="h6" sx={{ color: 'white', ml: 1, fontWeight: 600 }}>
|
||||
Overall Score: {analysisData.overall_score}/100
|
||||
</Typography>
|
||||
<Chip
|
||||
label={analysisData.health_status.replace('_', ' ').toUpperCase()}
|
||||
sx={{
|
||||
ml: 2,
|
||||
backgroundColor: getStatusColor(analysisData.health_status),
|
||||
color: 'white',
|
||||
fontWeight: 600,
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
<LinearProgress
|
||||
variant="determinate"
|
||||
value={analysisData.overall_score}
|
||||
sx={{
|
||||
height: 8,
|
||||
borderRadius: 4,
|
||||
backgroundColor: 'rgba(255, 255, 255, 0.1)',
|
||||
'& .MuiLinearProgress-bar': {
|
||||
backgroundColor: getStatusColor(analysisData.health_status),
|
||||
borderRadius: 4,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
|
||||
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mt: 1 }}>
|
||||
<Typography variant="body2" sx={{ color: 'rgba(255, 255, 255, 0.7)' }}>
|
||||
Analyzed: {analysisData.url}
|
||||
</Typography>
|
||||
|
||||
<Tooltip title="View detailed information about all SEO tests performed">
|
||||
<IconButton
|
||||
size="small"
|
||||
onClick={() => setShowDetailsDialog(true)}
|
||||
sx={{
|
||||
color: 'rgba(255, 255, 255, 0.7)',
|
||||
'&:hover': { color: 'white' }
|
||||
}}
|
||||
>
|
||||
<HelpIcon fontSize="small" />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
{/* Critical Issues Summary - Region 4 */}
|
||||
{analysisData.critical_issues && analysisData.critical_issues.length > 0 && (
|
||||
<Box sx={{ mb: 3 }}>
|
||||
<Typography variant="h6" sx={{ color: '#D32F2F', fontWeight: 600, mb: 2 }}>
|
||||
🚨 Critical Issues ({analysisData.critical_issues.length})
|
||||
</Typography>
|
||||
{analysisData.critical_issues.slice(0, 2).map((issue, index) => (
|
||||
<CriticalIssueCard
|
||||
key={index}
|
||||
issue={issue}
|
||||
index={index}
|
||||
onClick={handleIssueClick}
|
||||
onAIAction={handleAIAction}
|
||||
/>
|
||||
))}
|
||||
</Box>
|
||||
)}
|
||||
</Grid>
|
||||
|
||||
{/* Right Column - Detailed Analysis Tabs (Area A) */}
|
||||
<Grid item xs={12} md={8}>
|
||||
<AnalysisTabs
|
||||
categorizedData={categorizedData}
|
||||
expandedCategories={expandedCategories}
|
||||
onToggleCategory={toggleCategory}
|
||||
onIssueClick={handleIssueClick}
|
||||
onAIAction={handleAIAction}
|
||||
/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</GlassCard>
|
||||
|
||||
{/* Dialogs */}
|
||||
<IssueDetailsDialog
|
||||
open={showIssueDialog}
|
||||
issue={selectedIssue}
|
||||
onClose={() => setShowIssueDialog(false)}
|
||||
onAIAction={handleAIAction}
|
||||
/>
|
||||
|
||||
<AnalysisDetailsDialog
|
||||
open={showDetailsDialog}
|
||||
onClose={() => setShowDetailsDialog(false)}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default SEOAnalyzerPanel;
|
||||
28
frontend/src/components/SEODashboard/components/TabPanel.tsx
Normal file
28
frontend/src/components/SEODashboard/components/TabPanel.tsx
Normal file
@@ -0,0 +1,28 @@
|
||||
import React from 'react';
|
||||
import { Box } from '@mui/material';
|
||||
|
||||
interface TabPanelProps {
|
||||
children?: React.ReactNode;
|
||||
index: number;
|
||||
value: number;
|
||||
}
|
||||
|
||||
const TabPanel: React.FC<TabPanelProps> = ({ children, value, index, ...other }) => {
|
||||
return (
|
||||
<div
|
||||
role="tabpanel"
|
||||
hidden={value !== index}
|
||||
id={`analysis-tabpanel-${index}`}
|
||||
aria-labelledby={`analysis-tab-${index}`}
|
||||
{...other}
|
||||
>
|
||||
{value === index && (
|
||||
<Box sx={{ p: 2 }}>
|
||||
{children}
|
||||
</Box>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default TabPanel;
|
||||
17
frontend/src/components/SEODashboard/components/index.ts
Normal file
17
frontend/src/components/SEODashboard/components/index.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
// SEO Analysis Components
|
||||
export { default as SEOAnalyzerPanel } from './SEOAnalyzerPanel';
|
||||
export { default as CategoryCard } from './CategoryCard';
|
||||
export { default as IssueList } from './IssueList';
|
||||
export { default as CriticalIssueCard } from './CriticalIssueCard';
|
||||
export { default as AnalysisTabs } from './AnalysisTabs';
|
||||
export { default as TabPanel } from './TabPanel';
|
||||
export { default as IssueDetailsDialog } from './IssueDetailsDialog';
|
||||
export { default as AnalysisDetailsDialog } from './AnalysisDetailsDialog';
|
||||
export { default as SEOAnalysisLoading } from './SEOAnalysisLoading';
|
||||
export { default as SEOAnalysisError } from './SEOAnalysisError';
|
||||
|
||||
// Existing components
|
||||
export { default as PlatformStatus } from './PlatformStatus';
|
||||
export { default as AIInsightsPanel } from './AIInsightsPanel';
|
||||
export { default as MetricCard } from './MetricCard';
|
||||
export { default as HealthScore } from './HealthScore';
|
||||
162
frontend/src/components/SEODashboard/components/seoUtils.tsx
Normal file
162
frontend/src/components/SEODashboard/components/seoUtils.tsx
Normal file
@@ -0,0 +1,162 @@
|
||||
import React from 'react';
|
||||
import {
|
||||
CheckCircle as CheckCircleIcon,
|
||||
Warning as WarningIcon,
|
||||
Error as ErrorIcon,
|
||||
Info as InfoIcon,
|
||||
Speed as SpeedIcon,
|
||||
Security as SecurityIcon,
|
||||
Code as CodeIcon,
|
||||
Accessibility as AccessibilityIcon,
|
||||
MobileFriendly as MobileIcon,
|
||||
Search as SearchIcon,
|
||||
Article as ArticleIcon
|
||||
} from '@mui/icons-material';
|
||||
|
||||
// SEO Analysis Utilities
|
||||
export const getStatusColor = (status: string) => {
|
||||
switch (status) {
|
||||
case 'excellent':
|
||||
return '#00C853';
|
||||
case 'good':
|
||||
return '#4CAF50';
|
||||
case 'needs_improvement':
|
||||
return '#FF9800';
|
||||
case 'poor':
|
||||
return '#D32F2F'; // Softer red instead of bright #F44336
|
||||
default:
|
||||
return '#9E9E9E';
|
||||
}
|
||||
};
|
||||
|
||||
export const getStatusIcon = (status: string) => {
|
||||
switch (status) {
|
||||
case 'excellent':
|
||||
return <CheckCircleIcon sx={{ color: '#00C853' }} />;
|
||||
case 'good':
|
||||
return <CheckCircleIcon sx={{ color: '#4CAF50' }} />;
|
||||
case 'needs_improvement':
|
||||
return <WarningIcon sx={{ color: '#FF9800' }} />;
|
||||
case 'poor':
|
||||
return <ErrorIcon sx={{ color: '#D32F2F' }} />; // Softer red
|
||||
default:
|
||||
return <InfoIcon sx={{ color: '#9E9E9E' }} />;
|
||||
}
|
||||
};
|
||||
|
||||
export const getCategoryIcon = (category: string) => {
|
||||
switch (category) {
|
||||
case 'url_structure':
|
||||
return <SearchIcon sx={{ color: '#2196F3' }} />;
|
||||
case 'meta_data':
|
||||
return <ArticleIcon sx={{ color: '#FF9800' }} />;
|
||||
case 'content_analysis':
|
||||
return <ArticleIcon sx={{ color: '#4CAF50' }} />;
|
||||
case 'technical_seo':
|
||||
return <CodeIcon sx={{ color: '#9C27B0' }} />;
|
||||
case 'performance':
|
||||
return <SpeedIcon sx={{ color: '#00BCD4' }} />;
|
||||
case 'accessibility':
|
||||
return <AccessibilityIcon sx={{ color: '#FF5722' }} />;
|
||||
case 'user_experience':
|
||||
return <MobileIcon sx={{ color: '#795548' }} />;
|
||||
case 'security_headers':
|
||||
return <SecurityIcon sx={{ color: '#E91E63' }} />;
|
||||
default:
|
||||
return <InfoIcon sx={{ color: '#607D8B' }} />;
|
||||
}
|
||||
};
|
||||
|
||||
export const getCategoryTitle = (category: string) => {
|
||||
const titles: { [key: string]: string } = {
|
||||
'url_structure': 'URL Structure & Security',
|
||||
'meta_data': 'Meta Data & Technical SEO',
|
||||
'content_analysis': 'Content Analysis',
|
||||
'technical_seo': 'Technical SEO',
|
||||
'performance': 'Performance',
|
||||
'accessibility': 'Accessibility',
|
||||
'user_experience': 'User Experience',
|
||||
'security_headers': 'Security Headers',
|
||||
'keyword_analysis': 'Keyword Analysis'
|
||||
};
|
||||
return titles[category] || category.replace('_', ' ').toUpperCase();
|
||||
};
|
||||
|
||||
export const getAnalysisDetails = () => {
|
||||
return [
|
||||
{
|
||||
title: "URL Structure & Security",
|
||||
description: "Analyzes URL format, length, special characters, and security protocols like HTTPS.",
|
||||
tests: ["URL length check", "Special character analysis", "HTTPS implementation", "URL readability"]
|
||||
},
|
||||
{
|
||||
title: "Meta Data & Technical SEO",
|
||||
description: "Examines title tags, meta descriptions, viewport settings, and character encoding.",
|
||||
tests: ["Title tag optimization", "Meta description length", "Viewport meta tag", "Character encoding"]
|
||||
},
|
||||
{
|
||||
title: "Content Analysis",
|
||||
description: "Evaluates content quality, word count, heading structure, and readability.",
|
||||
tests: ["Content length analysis", "Heading hierarchy", "Readability scoring", "Internal linking"]
|
||||
},
|
||||
{
|
||||
title: "Technical SEO",
|
||||
description: "Checks robots.txt, sitemaps, structured data, and canonical URLs.",
|
||||
tests: ["Robots.txt accessibility", "XML sitemap presence", "Structured data markup", "Canonical URLs"]
|
||||
},
|
||||
{
|
||||
title: "Performance",
|
||||
description: "Measures page load speed, compression, caching, and optimization.",
|
||||
tests: ["Page load time", "GZIP compression", "Caching headers", "Resource optimization"]
|
||||
},
|
||||
{
|
||||
title: "Accessibility",
|
||||
description: "Ensures alt text, form labels, heading structure, and color contrast.",
|
||||
tests: ["Image alt text", "Form accessibility", "Heading hierarchy", "Color contrast"]
|
||||
},
|
||||
{
|
||||
title: "User Experience",
|
||||
description: "Checks mobile responsiveness, navigation, contact info, and social links.",
|
||||
tests: ["Mobile optimization", "Navigation structure", "Contact information", "Social media links"]
|
||||
},
|
||||
{
|
||||
title: "Security Headers",
|
||||
description: "Analyzes security headers for protection against common vulnerabilities.",
|
||||
tests: ["X-Frame-Options", "X-Content-Type-Options", "X-XSS-Protection", "Content-Security-Policy"]
|
||||
}
|
||||
];
|
||||
};
|
||||
|
||||
export const categorizeAnalysisData = (analysisData: any) => {
|
||||
if (!analysisData?.data) return { good: [], bad: [], ugly: [] };
|
||||
|
||||
const categories = Object.entries(analysisData.data);
|
||||
const categorized = {
|
||||
good: [] as any[],
|
||||
bad: [] as any[],
|
||||
ugly: [] as any[]
|
||||
};
|
||||
|
||||
categories.forEach(([category, data]) => {
|
||||
if (!data || typeof data !== 'object' || !(data as any).score) return;
|
||||
|
||||
const score = (data as any).score;
|
||||
if (score >= 80) {
|
||||
categorized.good.push({ category, data });
|
||||
} else if (score >= 60) {
|
||||
categorized.bad.push({ category, data });
|
||||
} else {
|
||||
categorized.ugly.push({ category, data });
|
||||
}
|
||||
});
|
||||
|
||||
return categorized;
|
||||
};
|
||||
|
||||
export const formatMessage = (message: string) => {
|
||||
if (message.includes(':')) {
|
||||
const [title, details] = message.split(':');
|
||||
return { title: title.trim(), details: details.trim() };
|
||||
}
|
||||
return { title: message, details: null };
|
||||
};
|
||||
Reference in New Issue
Block a user