ALwrity Version 0.5.0 (Fastapi + React )

This commit is contained in:
ajaysi
2025-08-06 12:48:02 +05:30
parent f28a919caa
commit 32f97fa6b3
476 changed files with 115544 additions and 28747 deletions

View 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;

View File

@@ -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;

View File

@@ -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;

View 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;

View 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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View 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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View 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;

View 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';

View 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 };
};