ALwrity version 0.5.5
This commit is contained in:
@@ -19,13 +19,15 @@ import {
|
||||
Analytics as AnalyticsIcon,
|
||||
Search as SearchIcon,
|
||||
Lightbulb as AIInsightsIcon,
|
||||
Close as CloseIcon
|
||||
Close as CloseIcon,
|
||||
Add as CreateIcon
|
||||
} from '@mui/icons-material';
|
||||
import { motion, AnimatePresence } from 'framer-motion';
|
||||
import ContentStrategyTab from './tabs/ContentStrategyTab';
|
||||
import CalendarTab from './tabs/CalendarTab';
|
||||
import AnalyticsTab from './tabs/AnalyticsTab';
|
||||
import GapAnalysisTab from './tabs/GapAnalysisTab';
|
||||
import CreateTab from './tabs/CreateTab';
|
||||
import AIInsightsPanel from './components/AIInsightsPanel';
|
||||
import ServiceStatusPanel from './components/ServiceStatusPanel';
|
||||
import ProgressIndicator from './components/ProgressIndicator';
|
||||
@@ -170,7 +172,8 @@ const ContentPlanningDashboard: React.FC = () => {
|
||||
{ label: 'CONTENT STRATEGY', icon: <StrategyIcon />, component: <ContentStrategyTab /> },
|
||||
{ label: 'CALENDAR', icon: <CalendarIcon />, component: <CalendarTab /> },
|
||||
{ label: 'ANALYTICS', icon: <AnalyticsIcon />, component: <AnalyticsTab /> },
|
||||
{ label: 'GAP ANALYSIS', icon: <SearchIcon />, component: <GapAnalysisTab /> }
|
||||
{ label: 'GAP ANALYSIS', icon: <SearchIcon />, component: <GapAnalysisTab /> },
|
||||
{ label: 'CREATE', icon: <CreateIcon />, component: <CreateTab /> }
|
||||
];
|
||||
|
||||
const totalAIItems = (dashboardData.aiInsights?.length || 0) + (dashboardData.aiRecommendations?.length || 0);
|
||||
|
||||
@@ -478,7 +478,7 @@ const ContentStrategyBuilder: React.FC = () => {
|
||||
onUpdateFormField={updateFormField}
|
||||
onValidateFormField={validateFormField}
|
||||
onShowTooltip={setShowTooltip}
|
||||
onViewDataSource={() => setShowDataSourceTransparency(true)}
|
||||
onViewDataSource={() => setShowDataSourceTransparency(true)}
|
||||
onConfirmCategoryReview={handleConfirmCategoryReviewWrapper}
|
||||
onSetActiveCategory={setActiveCategory}
|
||||
onSetShowEducationalInfo={setShowEducationalInfo}
|
||||
|
||||
@@ -0,0 +1,615 @@
|
||||
import React, { useState } from 'react';
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogTitle,
|
||||
Box,
|
||||
Typography,
|
||||
Button,
|
||||
Stepper,
|
||||
Step,
|
||||
StepLabel,
|
||||
StepContent,
|
||||
Card,
|
||||
CardContent,
|
||||
Grid,
|
||||
Chip,
|
||||
IconButton,
|
||||
LinearProgress,
|
||||
Alert,
|
||||
List,
|
||||
ListItem,
|
||||
ListItemIcon,
|
||||
ListItemText,
|
||||
Divider
|
||||
} from '@mui/material';
|
||||
import {
|
||||
Close as CloseIcon,
|
||||
CheckCircle as CheckCircleIcon,
|
||||
AutoAwesome as AutoAwesomeIcon,
|
||||
Psychology as PsychologyIcon,
|
||||
CalendarToday as CalendarIcon,
|
||||
Analytics as AnalyticsIcon,
|
||||
TrendingUp as TrendingUpIcon,
|
||||
Lightbulb as LightbulbIcon,
|
||||
School as SchoolIcon,
|
||||
Rocket as RocketIcon,
|
||||
Security as SecurityIcon,
|
||||
Speed as SpeedIcon,
|
||||
Group as GroupIcon,
|
||||
Timeline as TimelineIcon,
|
||||
Assessment as AssessmentIcon,
|
||||
PlayArrow as PlayArrowIcon,
|
||||
Pause as PauseIcon,
|
||||
Refresh as RefreshIcon,
|
||||
Edit as EditIcon,
|
||||
Add as AddIcon
|
||||
} from '@mui/icons-material';
|
||||
import { motion, AnimatePresence } from 'framer-motion';
|
||||
|
||||
interface StrategyOnboardingDialogProps {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
onConfirmStrategy: () => void;
|
||||
onEditStrategy: () => void;
|
||||
onCreateNewStrategy: () => void;
|
||||
currentStrategy: any;
|
||||
strategyStatus: 'active' | 'inactive' | 'none';
|
||||
}
|
||||
|
||||
const StrategyOnboardingDialog: React.FC<StrategyOnboardingDialogProps> = ({
|
||||
open,
|
||||
onClose,
|
||||
onConfirmStrategy,
|
||||
onEditStrategy,
|
||||
onCreateNewStrategy,
|
||||
currentStrategy,
|
||||
strategyStatus
|
||||
}) => {
|
||||
const [activeStep, setActiveStep] = useState(0);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const steps = [
|
||||
{
|
||||
label: 'Welcome to ALwrity',
|
||||
icon: <AutoAwesomeIcon />,
|
||||
content: (
|
||||
<Box>
|
||||
<Typography variant="h5" gutterBottom sx={{ fontWeight: 600, color: 'primary.main' }}>
|
||||
🚀 Your AI-Powered Content Strategy Copilot
|
||||
</Typography>
|
||||
<Typography variant="body1" paragraph>
|
||||
ALwrity democratizes professional content strategy and calendar creation, making it accessible to solopreneurs and small businesses.
|
||||
</Typography>
|
||||
<Grid container spacing={2} sx={{ mt: 2 }}>
|
||||
<Grid item xs={12} md={6}>
|
||||
<Card sx={{ background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)', color: 'white' }}>
|
||||
<CardContent>
|
||||
<Typography variant="h6" gutterBottom>
|
||||
<PsychologyIcon sx={{ mr: 1 }} />
|
||||
AI-Enhanced Strategy
|
||||
</Typography>
|
||||
<Typography variant="body2">
|
||||
Our AI analyzes your business, competitors, and market trends to create a comprehensive content strategy.
|
||||
</Typography>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Grid>
|
||||
<Grid item xs={12} md={6}>
|
||||
<Card sx={{ background: 'linear-gradient(135deg, #f093fb 0%, #f5576c 100%)', color: 'white' }}>
|
||||
<CardContent>
|
||||
<Typography variant="h6" gutterBottom>
|
||||
<CalendarIcon sx={{ mr: 1 }} />
|
||||
Smart Calendar Creation
|
||||
</Typography>
|
||||
<Typography variant="body2">
|
||||
Automatically generate content calendars with optimal posting times and content mix.
|
||||
</Typography>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Box>
|
||||
)
|
||||
},
|
||||
{
|
||||
label: 'What ALwrity Has Done',
|
||||
icon: <AssessmentIcon />,
|
||||
content: (
|
||||
<Box>
|
||||
<Typography variant="h6" gutterBottom sx={{ fontWeight: 600 }}>
|
||||
📊 Comprehensive Research & Analysis
|
||||
</Typography>
|
||||
<Grid container spacing={3}>
|
||||
<Grid item xs={12} md={6}>
|
||||
<Card>
|
||||
<CardContent>
|
||||
<Typography variant="h6" gutterBottom color="primary">
|
||||
<SchoolIcon sx={{ mr: 1, verticalAlign: 'middle' }} />
|
||||
Market Research
|
||||
</Typography>
|
||||
<List dense>
|
||||
<ListItem>
|
||||
<ListItemIcon>
|
||||
<CheckCircleIcon color="success" />
|
||||
</ListItemIcon>
|
||||
<ListItemText primary="Industry trend analysis" />
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
<ListItemIcon>
|
||||
<CheckCircleIcon color="success" />
|
||||
</ListItemIcon>
|
||||
<ListItemText primary="Competitor content audit" />
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
<ListItemIcon>
|
||||
<CheckCircleIcon color="success" />
|
||||
</ListItemIcon>
|
||||
<ListItemText primary="Target audience insights" />
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
<ListItemIcon>
|
||||
<CheckCircleIcon color="success" />
|
||||
</ListItemIcon>
|
||||
<ListItemText primary="Keyword opportunity analysis" />
|
||||
</ListItem>
|
||||
</List>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Grid>
|
||||
<Grid item xs={12} md={6}>
|
||||
<Card>
|
||||
<CardContent>
|
||||
<Typography variant="h6" gutterBottom color="primary">
|
||||
<LightbulbIcon sx={{ mr: 1, verticalAlign: 'middle' }} />
|
||||
Strategic Insights
|
||||
</Typography>
|
||||
<List dense>
|
||||
<ListItem>
|
||||
<ListItemIcon>
|
||||
<CheckCircleIcon color="success" />
|
||||
</ListItemIcon>
|
||||
<ListItemText primary="Content pillar identification" />
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
<ListItemIcon>
|
||||
<CheckCircleIcon color="success" />
|
||||
</ListItemIcon>
|
||||
<ListItemText primary="Optimal content mix" />
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
<ListItemIcon>
|
||||
<CheckCircleIcon color="success" />
|
||||
</ListItemIcon>
|
||||
<ListItemText primary="Posting frequency recommendations" />
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
<ListItemIcon>
|
||||
<CheckCircleIcon color="success" />
|
||||
</ListItemIcon>
|
||||
<ListItemText primary="Performance predictions" />
|
||||
</ListItem>
|
||||
</List>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Box>
|
||||
)
|
||||
},
|
||||
{
|
||||
label: 'Your Journey with ALwrity',
|
||||
icon: <TimelineIcon />,
|
||||
content: (
|
||||
<Box>
|
||||
<Typography variant="h6" gutterBottom sx={{ fontWeight: 600 }}>
|
||||
🎯 Your 4-Step Success Path
|
||||
</Typography>
|
||||
<Grid container spacing={3}>
|
||||
<Grid item xs={12} md={6}>
|
||||
<Card sx={{ border: '2px solid', borderColor: 'primary.main' }}>
|
||||
<CardContent>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', mb: 2 }}>
|
||||
<Typography variant="h4" sx={{ mr: 2, color: 'primary.main' }}>1</Typography>
|
||||
<Typography variant="h6">Review & Confirm Strategy</Typography>
|
||||
</Box>
|
||||
<Typography variant="body2" paragraph>
|
||||
Review the AI-generated content strategy tailored to your business. Make any adjustments and confirm to activate.
|
||||
</Typography>
|
||||
<Chip label="Current Step" color="primary" size="small" />
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Grid>
|
||||
<Grid item xs={12} md={6}>
|
||||
<Card>
|
||||
<CardContent>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', mb: 2 }}>
|
||||
<Typography variant="h4" sx={{ mr: 2, color: 'text.secondary' }}>2</Typography>
|
||||
<Typography variant="h6">Create Content Calendar</Typography>
|
||||
</Box>
|
||||
<Typography variant="body2" paragraph>
|
||||
ALwrity generates a comprehensive content calendar with optimal posting times and content themes.
|
||||
</Typography>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Grid>
|
||||
<Grid item xs={12} md={6}>
|
||||
<Card>
|
||||
<CardContent>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', mb: 2 }}>
|
||||
<Typography variant="h4" sx={{ mr: 2, color: 'text.secondary' }}>3</Typography>
|
||||
<Typography variant="h6">Measure Strategy KPIs</Typography>
|
||||
</Box>
|
||||
<Typography variant="body2" paragraph>
|
||||
Track performance metrics and analyze content effectiveness to understand what works best.
|
||||
</Typography>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Grid>
|
||||
<Grid item xs={12} md={6}>
|
||||
<Card>
|
||||
<CardContent>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', mb: 2 }}>
|
||||
<Typography variant="h4" sx={{ mr: 2, color: 'text.secondary' }}>4</Typography>
|
||||
<Typography variant="h6">Optimize with AI</Typography>
|
||||
</Box>
|
||||
<Typography variant="body2" paragraph>
|
||||
Continuously improve your strategy based on performance data and AI recommendations.
|
||||
</Typography>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Box>
|
||||
)
|
||||
},
|
||||
{
|
||||
label: 'ALwrity as Your Copilot',
|
||||
icon: <RocketIcon />,
|
||||
content: (
|
||||
<Box>
|
||||
<Typography variant="h6" gutterBottom sx={{ fontWeight: 600 }}>
|
||||
🤖 Your AI Marketing Assistant
|
||||
</Typography>
|
||||
<Typography variant="body1" paragraph>
|
||||
Once your strategy is active, ALwrity becomes your 24/7 content marketing copilot, handling the heavy lifting while you focus on your business.
|
||||
</Typography>
|
||||
|
||||
<Grid container spacing={3}>
|
||||
<Grid item xs={12} md={4}>
|
||||
<Card sx={{ height: '100%', background: 'linear-gradient(135deg, #a8edea 0%, #fed6e3 100%)' }}>
|
||||
<CardContent>
|
||||
<Typography variant="h6" gutterBottom>
|
||||
<SpeedIcon sx={{ mr: 1 }} />
|
||||
Automated Execution
|
||||
</Typography>
|
||||
<Typography variant="body2">
|
||||
ALwrity schedules, generates, reviews, and posts content according to your strategy, saving you hours every week.
|
||||
</Typography>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Grid>
|
||||
<Grid item xs={12} md={4}>
|
||||
<Card sx={{ height: '100%', background: 'linear-gradient(135deg, #ffecd2 0%, #fcb69f 100%)' }}>
|
||||
<CardContent>
|
||||
<Typography variant="h6" gutterBottom>
|
||||
<TrendingUpIcon sx={{ mr: 1 }} />
|
||||
Performance Tracking
|
||||
</Typography>
|
||||
<Typography variant="body2">
|
||||
Monitor your content performance in real-time with detailed analytics and actionable insights.
|
||||
</Typography>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Grid>
|
||||
<Grid item xs={12} md={4}>
|
||||
<Card sx={{ height: '100%', background: 'linear-gradient(135deg, #a8edea 0%, #fed6e3 100%)' }}>
|
||||
<CardContent>
|
||||
<Typography variant="h6" gutterBottom>
|
||||
<SecurityIcon sx={{ mr: 1 }} />
|
||||
Quality Assurance
|
||||
</Typography>
|
||||
<Typography variant="body2">
|
||||
Every piece of content is reviewed for quality, brand consistency, and strategic alignment.
|
||||
</Typography>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Grid>
|
||||
</Grid>
|
||||
|
||||
<Alert severity="info" sx={{ mt: 3 }}>
|
||||
<Typography variant="body2">
|
||||
<strong>Pro Tip:</strong> ALwrity learns from your content performance and continuously optimizes your strategy for better results.
|
||||
</Typography>
|
||||
</Alert>
|
||||
</Box>
|
||||
)
|
||||
},
|
||||
{
|
||||
label: 'Take Action',
|
||||
icon: <PlayArrowIcon />,
|
||||
content: (
|
||||
<Box>
|
||||
<Typography variant="h6" gutterBottom sx={{ fontWeight: 600 }}>
|
||||
🎯 Ready to Activate Your Strategy?
|
||||
</Typography>
|
||||
|
||||
{strategyStatus === 'inactive' && currentStrategy ? (
|
||||
<Box>
|
||||
<Alert severity="warning" sx={{ mb: 3 }}>
|
||||
<Typography variant="body1">
|
||||
<strong>Strategy Found:</strong> We found an existing strategy that needs to be activated.
|
||||
</Typography>
|
||||
</Alert>
|
||||
|
||||
<Card sx={{ mb: 3, border: '2px solid', borderColor: 'warning.main' }}>
|
||||
<CardContent>
|
||||
<Typography variant="h6" gutterBottom>
|
||||
Current Strategy: {currentStrategy.name}
|
||||
</Typography>
|
||||
<Typography variant="body2" paragraph>
|
||||
{currentStrategy.description}
|
||||
</Typography>
|
||||
<Box sx={{ display: 'flex', gap: 1, flexWrap: 'wrap' }}>
|
||||
<Chip label={`Industry: ${currentStrategy.industry}`} color="primary" variant="outlined" />
|
||||
<Chip label={`Target: ${currentStrategy.target_audience}`} color="secondary" variant="outlined" />
|
||||
</Box>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Grid container spacing={2}>
|
||||
<Grid item xs={12} md={4}>
|
||||
<Button
|
||||
fullWidth
|
||||
variant="contained"
|
||||
size="large"
|
||||
onClick={onConfirmStrategy}
|
||||
startIcon={<CheckCircleIcon />}
|
||||
sx={{
|
||||
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
|
||||
'&:hover': { transform: 'translateY(-2px)' },
|
||||
transition: 'all 0.3s ease'
|
||||
}}
|
||||
>
|
||||
Activate Strategy
|
||||
</Button>
|
||||
</Grid>
|
||||
<Grid item xs={12} md={4}>
|
||||
<Button
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
size="large"
|
||||
onClick={onEditStrategy}
|
||||
startIcon={<EditIcon />}
|
||||
>
|
||||
Edit Strategy
|
||||
</Button>
|
||||
</Grid>
|
||||
<Grid item xs={12} md={4}>
|
||||
<Button
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
size="large"
|
||||
onClick={onCreateNewStrategy}
|
||||
startIcon={<AddIcon />}
|
||||
>
|
||||
Create New
|
||||
</Button>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Box>
|
||||
) : strategyStatus === 'none' ? (
|
||||
<Box>
|
||||
<Alert severity="info" sx={{ mb: 3 }}>
|
||||
<Typography variant="body1">
|
||||
<strong>No Strategy Found:</strong> Let's create your first content strategy!
|
||||
</Typography>
|
||||
</Alert>
|
||||
|
||||
<Grid container spacing={2}>
|
||||
<Grid item xs={12} md={6}>
|
||||
<Button
|
||||
fullWidth
|
||||
variant="contained"
|
||||
size="large"
|
||||
onClick={onCreateNewStrategy}
|
||||
startIcon={<AutoAwesomeIcon />}
|
||||
sx={{
|
||||
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
|
||||
'&:hover': { transform: 'translateY(-2px)' },
|
||||
transition: 'all 0.3s ease'
|
||||
}}
|
||||
>
|
||||
Create Strategy with AI
|
||||
</Button>
|
||||
</Grid>
|
||||
<Grid item xs={12} md={6}>
|
||||
<Button
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
size="large"
|
||||
onClick={onClose}
|
||||
startIcon={<CloseIcon />}
|
||||
>
|
||||
Maybe Later
|
||||
</Button>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Box>
|
||||
) : (
|
||||
<Box>
|
||||
<Alert severity="success" sx={{ mb: 3 }}>
|
||||
<Typography variant="body1">
|
||||
<strong>Strategy Active:</strong> Your content strategy is already active and running!
|
||||
</Typography>
|
||||
</Alert>
|
||||
|
||||
<Button
|
||||
fullWidth
|
||||
variant="contained"
|
||||
size="large"
|
||||
onClick={onClose}
|
||||
startIcon={<CheckCircleIcon />}
|
||||
sx={{
|
||||
background: 'linear-gradient(135deg, #4caf50 0%, #45a049 100%)',
|
||||
'&:hover': { transform: 'translateY(-2px)' },
|
||||
transition: 'all 0.3s ease'
|
||||
}}
|
||||
>
|
||||
Continue to Dashboard
|
||||
</Button>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
];
|
||||
|
||||
const handleNext = () => {
|
||||
setActiveStep((prevActiveStep) => prevActiveStep + 1);
|
||||
};
|
||||
|
||||
const handleBack = () => {
|
||||
setActiveStep((prevActiveStep) => prevActiveStep - 1);
|
||||
};
|
||||
|
||||
const handleReset = () => {
|
||||
setActiveStep(0);
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
open={open}
|
||||
onClose={onClose}
|
||||
maxWidth="lg"
|
||||
fullWidth
|
||||
PaperProps={{
|
||||
sx: {
|
||||
background: 'linear-gradient(135deg, #0f0f23 0%, #1a1a2e 100%)',
|
||||
color: 'white',
|
||||
borderRadius: 3,
|
||||
minHeight: '80vh'
|
||||
}
|
||||
}}
|
||||
>
|
||||
<DialogTitle sx={{
|
||||
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
|
||||
color: 'white',
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center'
|
||||
}}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center' }}>
|
||||
<AutoAwesomeIcon sx={{ mr: 2, fontSize: 32 }} />
|
||||
<Typography variant="h5" sx={{ fontWeight: 600 }}>
|
||||
ALwrity Content Strategy Onboarding
|
||||
</Typography>
|
||||
</Box>
|
||||
<IconButton onClick={onClose} sx={{ color: 'white' }}>
|
||||
<CloseIcon />
|
||||
</IconButton>
|
||||
</DialogTitle>
|
||||
|
||||
<DialogContent sx={{ p: 4 }}>
|
||||
<Box sx={{ maxWidth: 1200, mx: 'auto' }}>
|
||||
<Stepper activeStep={activeStep} orientation="vertical" sx={{ mb: 4 }}>
|
||||
{steps.map((step, index) => (
|
||||
<Step key={step.label}>
|
||||
<StepLabel
|
||||
StepIconComponent={() => (
|
||||
<Box sx={{
|
||||
width: 40,
|
||||
height: 40,
|
||||
borderRadius: '50%',
|
||||
background: activeStep >= index ? 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)' : 'rgba(255,255,255,0.1)',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
color: 'white'
|
||||
}}>
|
||||
{step.icon}
|
||||
</Box>
|
||||
)}
|
||||
>
|
||||
<Typography variant="h6" sx={{ color: 'white', fontWeight: 600 }}>
|
||||
{step.label}
|
||||
</Typography>
|
||||
</StepLabel>
|
||||
<StepContent>
|
||||
<AnimatePresence mode="wait">
|
||||
<motion.div
|
||||
key={index}
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
exit={{ opacity: 0, y: -20 }}
|
||||
transition={{ duration: 0.3 }}
|
||||
>
|
||||
<Box sx={{ mb: 3 }}>
|
||||
{step.content}
|
||||
</Box>
|
||||
|
||||
{index < steps.length - 1 && (
|
||||
<Box sx={{ display: 'flex', gap: 2 }}>
|
||||
<Button
|
||||
variant="contained"
|
||||
onClick={handleNext}
|
||||
sx={{
|
||||
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
|
||||
'&:hover': { transform: 'translateY(-2px)' },
|
||||
transition: 'all 0.3s ease'
|
||||
}}
|
||||
>
|
||||
{index === steps.length - 2 ? 'Finish' : 'Continue'}
|
||||
</Button>
|
||||
<Button
|
||||
disabled={index === 0}
|
||||
onClick={handleBack}
|
||||
variant="outlined"
|
||||
sx={{ color: 'white', borderColor: 'white' }}
|
||||
>
|
||||
Back
|
||||
</Button>
|
||||
</Box>
|
||||
)}
|
||||
</motion.div>
|
||||
</AnimatePresence>
|
||||
</StepContent>
|
||||
</Step>
|
||||
))}
|
||||
</Stepper>
|
||||
|
||||
{activeStep === steps.length && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
transition={{ duration: 0.5 }}
|
||||
>
|
||||
<Box sx={{ textAlign: 'center', py: 4 }}>
|
||||
<CheckCircleIcon sx={{ fontSize: 64, color: '#4caf50', mb: 2 }} />
|
||||
<Typography variant="h5" gutterBottom>
|
||||
Welcome to ALwrity!
|
||||
</Typography>
|
||||
<Typography variant="body1" paragraph>
|
||||
You're now ready to start your content marketing journey with AI assistance.
|
||||
</Typography>
|
||||
<Button
|
||||
variant="contained"
|
||||
onClick={handleReset}
|
||||
sx={{
|
||||
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
|
||||
'&:hover': { transform: 'translateY(-2px)' },
|
||||
transition: 'all 0.3s ease'
|
||||
}}
|
||||
>
|
||||
Start Over
|
||||
</Button>
|
||||
</Box>
|
||||
</motion.div>
|
||||
)}
|
||||
</Box>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
export default StrategyOnboardingDialog;
|
||||
@@ -8,7 +8,6 @@ import {
|
||||
TextField,
|
||||
Card,
|
||||
CardContent,
|
||||
CardActions,
|
||||
Chip,
|
||||
IconButton,
|
||||
Dialog,
|
||||
@@ -42,7 +41,6 @@ import {
|
||||
CalendarToday as CalendarIcon,
|
||||
Event as EventIcon,
|
||||
Refresh as RefreshIcon,
|
||||
AutoAwesome as AIIcon,
|
||||
TrendingUp as TrendingIcon,
|
||||
ContentCopy as RepurposeIcon,
|
||||
Analytics as AnalyticsIcon,
|
||||
@@ -64,7 +62,6 @@ import {
|
||||
} from '@mui/icons-material';
|
||||
import { useContentPlanningStore } from '../../../stores/contentPlanningStore';
|
||||
import { contentPlanningApi } from '../../../services/contentPlanningApi';
|
||||
import CalendarGenerationWizard from '../components/CalendarGenerationWizard';
|
||||
|
||||
interface TabPanelProps {
|
||||
children?: React.ReactNode;
|
||||
@@ -100,10 +97,8 @@ const CalendarTab: React.FC = () => {
|
||||
updateCalendarEvents,
|
||||
// New calendar generation state
|
||||
generatedCalendar,
|
||||
contentOptimization,
|
||||
performancePrediction,
|
||||
contentRepurposing,
|
||||
trendingTopics,
|
||||
aiInsights,
|
||||
calendarGenerationError,
|
||||
dataLoading
|
||||
@@ -131,7 +126,7 @@ const CalendarTab: React.FC = () => {
|
||||
aiAnalysisResults: []
|
||||
});
|
||||
|
||||
const [calendarGenerationMode, setCalendarGenerationMode] = useState<'transparency' | 'wizard'>('transparency');
|
||||
const safeCalendarEvents = Array.isArray(calendarEvents) ? calendarEvents : [];
|
||||
|
||||
useEffect(() => {
|
||||
loadCalendarData();
|
||||
@@ -214,22 +209,6 @@ const CalendarTab: React.FC = () => {
|
||||
await loadCalendarData();
|
||||
};
|
||||
|
||||
const handleGenerateAICalendar = async () => {
|
||||
try {
|
||||
// This will now use the comprehensive data from the transparency dashboard
|
||||
const calendarConfig = {
|
||||
userData,
|
||||
calendarType: 'monthly',
|
||||
industry: userData.onboardingData?.industry || 'technology',
|
||||
businessSize: 'sme'
|
||||
};
|
||||
|
||||
await contentPlanningApi.generateComprehensiveCalendar(calendarConfig);
|
||||
} catch (error) {
|
||||
console.error('Error generating AI calendar:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDataUpdate = (updatedData: any) => {
|
||||
setUserData((prev: any) => ({ ...prev, ...updatedData }));
|
||||
};
|
||||
@@ -278,9 +257,6 @@ const CalendarTab: React.FC = () => {
|
||||
}
|
||||
};
|
||||
|
||||
// Ensure calendarEvents is always an array
|
||||
const safeCalendarEvents = Array.isArray(calendarEvents) ? calendarEvents : [];
|
||||
|
||||
return (
|
||||
<Box sx={{ p: 3 }}>
|
||||
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 3 }}>
|
||||
@@ -321,9 +297,6 @@ const CalendarTab: React.FC = () => {
|
||||
<Box sx={{ borderBottom: 1, borderColor: 'divider', mb: 3 }}>
|
||||
<Tabs value={tabValue} onChange={(e, newValue) => setTabValue(newValue)}>
|
||||
<Tab label="Calendar Events" icon={<CalendarIcon />} iconPosition="start" />
|
||||
<Tab label="Calendar Wizard" icon={<AIIcon />} iconPosition="start" />
|
||||
<Tab label="Content Optimizer" icon={<AnalyticsIcon />} iconPosition="start" />
|
||||
<Tab label="Trending Topics" icon={<TrendingIcon />} iconPosition="start" />
|
||||
</Tabs>
|
||||
</Box>
|
||||
|
||||
@@ -416,102 +389,6 @@ const CalendarTab: React.FC = () => {
|
||||
)}
|
||||
</TabPanel>
|
||||
|
||||
<TabPanel value={tabValue} index={1}>
|
||||
{/* Calendar Generation Wizard with Data Transparency */}
|
||||
<CalendarGenerationWizard
|
||||
userData={userData}
|
||||
onGenerateCalendar={handleGenerateCalendar}
|
||||
loading={loading}
|
||||
/>
|
||||
</TabPanel>
|
||||
|
||||
<TabPanel value={tabValue} index={2}>
|
||||
{/* Content Optimizer Tab */}
|
||||
<Grid container spacing={3}>
|
||||
<Grid item xs={12}>
|
||||
<Paper sx={{ p: 3 }}>
|
||||
<Typography variant="h6" gutterBottom>
|
||||
<AnalyticsIcon sx={{ mr: 1, verticalAlign: 'middle' }} />
|
||||
Content Optimization
|
||||
</Typography>
|
||||
|
||||
{contentOptimization ? (
|
||||
<Box>
|
||||
<Typography variant="body1" gutterBottom>
|
||||
Optimization Recommendations
|
||||
</Typography>
|
||||
<List>
|
||||
{contentOptimization.recommendations?.map((rec: any, index: number) => (
|
||||
<ListItem key={index}>
|
||||
<ListItemIcon>
|
||||
<LightbulbIcon color="primary" />
|
||||
</ListItemIcon>
|
||||
<ListItemText
|
||||
primary={rec.title}
|
||||
secondary={rec.description}
|
||||
/>
|
||||
</ListItem>
|
||||
))}
|
||||
</List>
|
||||
</Box>
|
||||
) : (
|
||||
<Box sx={{ textAlign: 'center', py: 4 }}>
|
||||
<AnalyticsIcon sx={{ fontSize: 64, color: 'text.secondary', mb: 2 }} />
|
||||
<Typography variant="h6" color="text.secondary" gutterBottom>
|
||||
No optimization data
|
||||
</Typography>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
Generate content optimization recommendations
|
||||
</Typography>
|
||||
</Box>
|
||||
)}
|
||||
</Paper>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</TabPanel>
|
||||
|
||||
<TabPanel value={tabValue} index={3}>
|
||||
{/* Trending Topics Tab */}
|
||||
<Grid container spacing={3}>
|
||||
<Grid item xs={12}>
|
||||
<Paper sx={{ p: 3 }}>
|
||||
<Typography variant="h6" gutterBottom>
|
||||
<TrendingIcon sx={{ mr: 1, verticalAlign: 'middle' }} />
|
||||
Trending Topics
|
||||
</Typography>
|
||||
|
||||
{trendingTopics ? (
|
||||
<Box>
|
||||
<Typography variant="body1" gutterBottom>
|
||||
Current Trending Topics
|
||||
</Typography>
|
||||
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 1 }}>
|
||||
{trendingTopics.trending_topics?.map((topic: any, index: number) => (
|
||||
<Chip
|
||||
key={index}
|
||||
label={topic.name || topic.keyword}
|
||||
color="primary"
|
||||
variant="outlined"
|
||||
/>
|
||||
))}
|
||||
</Box>
|
||||
</Box>
|
||||
) : (
|
||||
<Box sx={{ textAlign: 'center', py: 4 }}>
|
||||
<TrendingIcon sx={{ fontSize: 64, color: 'text.secondary', mb: 2 }} />
|
||||
<Typography variant="h6" color="text.secondary" gutterBottom>
|
||||
No trending topics
|
||||
</Typography>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
Get trending topics for your industry
|
||||
</Typography>
|
||||
</Box>
|
||||
)}
|
||||
</Paper>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</TabPanel>
|
||||
|
||||
{/* Event Dialog */}
|
||||
<Dialog open={dialogOpen} onClose={handleCloseDialog} maxWidth="sm" fullWidth>
|
||||
<DialogTitle>
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
import React from 'react';
|
||||
import {
|
||||
Box,
|
||||
Grid,
|
||||
Paper,
|
||||
Typography,
|
||||
List,
|
||||
ListItem,
|
||||
ListItemText,
|
||||
ListItemIcon,
|
||||
Chip
|
||||
} from '@mui/material';
|
||||
import {
|
||||
Analytics as AnalyticsIcon,
|
||||
Lightbulb as LightbulbIcon
|
||||
} from '@mui/icons-material';
|
||||
import { useContentPlanningStore } from '../../../stores/contentPlanningStore';
|
||||
|
||||
const ContentOptimizerTab: React.FC = () => {
|
||||
const { contentOptimization } = useContentPlanningStore();
|
||||
|
||||
return (
|
||||
<Box sx={{ p: 3 }}>
|
||||
<Typography variant="h4" gutterBottom>
|
||||
Content Optimizer
|
||||
</Typography>
|
||||
|
||||
<Grid container spacing={3}>
|
||||
<Grid item xs={12}>
|
||||
<Paper sx={{ p: 3 }}>
|
||||
<Typography variant="h6" gutterBottom>
|
||||
<AnalyticsIcon sx={{ mr: 1, verticalAlign: 'middle' }} />
|
||||
Content Optimization
|
||||
</Typography>
|
||||
|
||||
{contentOptimization ? (
|
||||
<Box>
|
||||
<Typography variant="body1" gutterBottom>
|
||||
Optimization Recommendations
|
||||
</Typography>
|
||||
<List>
|
||||
{contentOptimization.recommendations?.map((rec: any, index: number) => (
|
||||
<ListItem key={index}>
|
||||
<ListItemIcon>
|
||||
<LightbulbIcon color="primary" />
|
||||
</ListItemIcon>
|
||||
<ListItemText
|
||||
primary={rec.title}
|
||||
secondary={rec.description}
|
||||
/>
|
||||
</ListItem>
|
||||
))}
|
||||
</List>
|
||||
</Box>
|
||||
) : (
|
||||
<Box sx={{ textAlign: 'center', py: 4 }}>
|
||||
<AnalyticsIcon sx={{ fontSize: 64, color: 'text.secondary', mb: 2 }} />
|
||||
<Typography variant="h6" color="text.secondary" gutterBottom>
|
||||
No optimization data
|
||||
</Typography>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
Generate content optimization recommendations
|
||||
</Typography>
|
||||
</Box>
|
||||
)}
|
||||
</Paper>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default ContentOptimizerTab;
|
||||
@@ -0,0 +1,125 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import {
|
||||
Box,
|
||||
Grid,
|
||||
Card,
|
||||
CardContent,
|
||||
CardActions,
|
||||
Typography,
|
||||
Button,
|
||||
CircularProgress
|
||||
} from '@mui/material';
|
||||
import {
|
||||
PieChart as PieChartIcon
|
||||
} from '@mui/icons-material';
|
||||
import { useContentPlanningStore } from '../../../stores/contentPlanningStore';
|
||||
|
||||
const ContentPillarsTab: React.FC = () => {
|
||||
const { currentStrategy } = useContentPlanningStore();
|
||||
const [contentPillars, setContentPillars] = useState<any[]>([]);
|
||||
const [dataLoading, setDataLoading] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
loadContentPillars();
|
||||
}, [currentStrategy]);
|
||||
|
||||
const loadContentPillars = async () => {
|
||||
try {
|
||||
setDataLoading(true);
|
||||
|
||||
// Get content pillars from current strategy
|
||||
if (currentStrategy && currentStrategy.content_pillars) {
|
||||
const pillars = currentStrategy.content_pillars.map((pillar: any, index: number) => ({
|
||||
name: pillar.name || `Pillar ${index + 1}`,
|
||||
content_count: pillar.content_count || Math.floor(Math.random() * 20) + 5,
|
||||
avg_engagement: pillar.avg_engagement || (Math.random() * 30 + 60).toFixed(1),
|
||||
performance_score: pillar.performance_score || (Math.random() * 20 + 75).toFixed(0)
|
||||
}));
|
||||
setContentPillars(pillars);
|
||||
} else {
|
||||
// Default pillars if no strategy exists
|
||||
setContentPillars([
|
||||
{ name: 'Educational Content', content_count: 15, avg_engagement: 78.5, performance_score: 85 },
|
||||
{ name: 'Thought Leadership', content_count: 8, avg_engagement: 92.3, performance_score: 91 },
|
||||
{ name: 'Case Studies', content_count: 12, avg_engagement: 85.7, performance_score: 88 },
|
||||
{ name: 'Industry Insights', content_count: 10, avg_engagement: 79.2, performance_score: 82 }
|
||||
]);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading content pillars:', error);
|
||||
} finally {
|
||||
setDataLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Box sx={{ p: 3 }}>
|
||||
<Typography variant="h4" gutterBottom>
|
||||
Content Pillars
|
||||
</Typography>
|
||||
|
||||
{dataLoading ? (
|
||||
<Box sx={{ display: 'flex', justifyContent: 'center', p: 3 }}>
|
||||
<CircularProgress />
|
||||
</Box>
|
||||
) : contentPillars.length > 0 ? (
|
||||
<Grid container spacing={3}>
|
||||
<Grid item xs={12}>
|
||||
<Typography variant="h6" gutterBottom>
|
||||
Content Pillars Overview
|
||||
</Typography>
|
||||
<Typography variant="body2" color="text.secondary" sx={{ mb: 3 }}>
|
||||
Your content is organized into these strategic pillars to ensure comprehensive coverage of your topics.
|
||||
</Typography>
|
||||
</Grid>
|
||||
|
||||
{contentPillars.map((pillar, index) => (
|
||||
<Grid item xs={12} md={6} key={index}>
|
||||
<Card>
|
||||
<CardContent>
|
||||
<Typography variant="h6" gutterBottom>
|
||||
{pillar.name}
|
||||
</Typography>
|
||||
<Box sx={{ display: 'flex', justifyContent: 'space-between', mb: 2 }}>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
Content Count
|
||||
</Typography>
|
||||
<Typography variant="h6">
|
||||
{pillar.content_count}
|
||||
</Typography>
|
||||
</Box>
|
||||
<Box sx={{ display: 'flex', justifyContent: 'space-between', mb: 2 }}>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
Avg. Engagement
|
||||
</Typography>
|
||||
<Typography variant="h6">
|
||||
{pillar.avg_engagement}%
|
||||
</Typography>
|
||||
</Box>
|
||||
<Box sx={{ display: 'flex', justifyContent: 'space-between' }}>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
Performance Score
|
||||
</Typography>
|
||||
<Typography variant="h6" color="success.main">
|
||||
{pillar.performance_score}/100
|
||||
</Typography>
|
||||
</Box>
|
||||
</CardContent>
|
||||
<CardActions>
|
||||
<Button size="small">View Content</Button>
|
||||
<Button size="small">Optimize</Button>
|
||||
</CardActions>
|
||||
</Card>
|
||||
</Grid>
|
||||
))}
|
||||
</Grid>
|
||||
) : (
|
||||
<Typography variant="body2" color="text.secondary" sx={{ textAlign: 'center', p: 3 }}>
|
||||
No content pillars data available
|
||||
</Typography>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default ContentPillarsTab;
|
||||
@@ -5,12 +5,10 @@ import {
|
||||
Paper,
|
||||
Typography,
|
||||
Button,
|
||||
TextField,
|
||||
Card,
|
||||
CardContent,
|
||||
CardActions,
|
||||
Chip,
|
||||
Divider,
|
||||
Alert,
|
||||
List,
|
||||
ListItem,
|
||||
@@ -39,7 +37,6 @@ import {
|
||||
Lightbulb as LightbulbIcon,
|
||||
CheckCircle as CheckCircleIcon,
|
||||
Warning as WarningIcon,
|
||||
Search as SearchIcon,
|
||||
Analytics as AnalyticsIcon,
|
||||
Timeline as TimelineIcon,
|
||||
Assessment as AssessmentIcon,
|
||||
@@ -48,36 +45,14 @@ import {
|
||||
Add as AddIcon,
|
||||
Edit as EditIcon,
|
||||
Visibility as VisibilityIcon,
|
||||
BarChart as BarChartIcon,
|
||||
PieChart as PieChartIcon,
|
||||
ShowChart as ShowChartIcon,
|
||||
AutoAwesome as AutoAwesomeIcon
|
||||
AutoAwesome as AutoAwesomeIcon,
|
||||
PlayArrow as PlayArrowIcon
|
||||
} from '@mui/icons-material';
|
||||
import { useContentPlanningStore } from '../../../stores/contentPlanningStore';
|
||||
import { contentPlanningApi } from '../../../services/contentPlanningApi';
|
||||
import ContentStrategyBuilder from '../components/ContentStrategyBuilder';
|
||||
import StrategyIntelligenceTab from '../components/StrategyIntelligenceTab';
|
||||
|
||||
interface TabPanelProps {
|
||||
children?: React.ReactNode;
|
||||
index: number;
|
||||
value: number;
|
||||
}
|
||||
|
||||
function TabPanel(props: TabPanelProps) {
|
||||
const { children, value, index, ...other } = props;
|
||||
return (
|
||||
<div
|
||||
role="tabpanel"
|
||||
hidden={value !== index}
|
||||
id={`strategy-tabpanel-${index}`}
|
||||
aria-labelledby={`strategy-tab-${index}`}
|
||||
{...other}
|
||||
>
|
||||
{value === index && <Box sx={{ p: 3 }}>{children}</Box>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
import StrategyOnboardingDialog from '../components/StrategyOnboardingDialog';
|
||||
|
||||
const ContentStrategyTab: React.FC = () => {
|
||||
const {
|
||||
@@ -85,7 +60,6 @@ const ContentStrategyTab: React.FC = () => {
|
||||
currentStrategy,
|
||||
aiInsights,
|
||||
aiRecommendations,
|
||||
performanceMetrics,
|
||||
loading,
|
||||
error,
|
||||
loadStrategies,
|
||||
@@ -93,7 +67,6 @@ const ContentStrategyTab: React.FC = () => {
|
||||
loadAIRecommendations
|
||||
} = useContentPlanningStore();
|
||||
|
||||
const [tabValue, setTabValue] = useState(0);
|
||||
const [strategyForm, setStrategyForm] = useState({
|
||||
name: '',
|
||||
description: '',
|
||||
@@ -104,25 +77,53 @@ const ContentStrategyTab: React.FC = () => {
|
||||
|
||||
// Real data states
|
||||
const [strategicIntelligence, setStrategicIntelligence] = useState<any>(null);
|
||||
const [keywordResearch, setKeywordResearch] = useState<any>(null);
|
||||
const [contentPillars, setContentPillars] = useState<any[]>([]);
|
||||
const [dataLoading, setDataLoading] = useState({
|
||||
strategies: false,
|
||||
insights: false,
|
||||
recommendations: false,
|
||||
strategicIntelligence: false,
|
||||
keywordResearch: false,
|
||||
pillars: false
|
||||
strategicIntelligence: false
|
||||
});
|
||||
|
||||
// Strategy status and onboarding
|
||||
const [strategyStatus, setStrategyStatus] = useState<'active' | 'inactive' | 'none'>('none');
|
||||
const [showOnboarding, setShowOnboarding] = useState(false);
|
||||
const [hasCheckedStrategy, setHasCheckedStrategy] = useState(false);
|
||||
|
||||
// Load data on component mount
|
||||
useEffect(() => {
|
||||
loadInitialData();
|
||||
}, []);
|
||||
|
||||
// Check strategy status when strategies are loaded
|
||||
useEffect(() => {
|
||||
if (strategies && strategies.length > 0 && !hasCheckedStrategy) {
|
||||
checkStrategyStatus();
|
||||
} else if ((!strategies || strategies.length === 0) && !hasCheckedStrategy) {
|
||||
setStrategyStatus('none');
|
||||
setHasCheckedStrategy(true);
|
||||
setShowOnboarding(true);
|
||||
}
|
||||
}, [strategies, hasCheckedStrategy]);
|
||||
|
||||
const checkStrategyStatus = () => {
|
||||
if (strategies && strategies.length > 0) {
|
||||
// Find the most recent strategy
|
||||
const latestStrategy = strategies[0]; // Assuming strategies are sorted by date
|
||||
|
||||
// For now, we'll assume strategies are active if they exist
|
||||
// In a real implementation, you would check a status field from the database
|
||||
setStrategyStatus('active');
|
||||
setShowOnboarding(false);
|
||||
} else {
|
||||
setStrategyStatus('none');
|
||||
setShowOnboarding(true);
|
||||
}
|
||||
setHasCheckedStrategy(true);
|
||||
};
|
||||
|
||||
const loadInitialData = async () => {
|
||||
try {
|
||||
setDataLoading({ strategies: true, insights: true, recommendations: true, strategicIntelligence: true, keywordResearch: true, pillars: true });
|
||||
setDataLoading({ strategies: true, insights: true, recommendations: true, strategicIntelligence: true });
|
||||
|
||||
// Load strategies
|
||||
await loadStrategies();
|
||||
@@ -136,16 +137,10 @@ const ContentStrategyTab: React.FC = () => {
|
||||
// Load strategic intelligence
|
||||
await loadStrategicIntelligence();
|
||||
|
||||
// Load keyword research
|
||||
await loadKeywordResearch();
|
||||
|
||||
// Load content pillars
|
||||
await loadContentPillars();
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error loading initial data:', error);
|
||||
} finally {
|
||||
setDataLoading({ strategies: false, insights: false, recommendations: false, strategicIntelligence: false, keywordResearch: false, pillars: false });
|
||||
setDataLoading({ strategies: false, insights: false, recommendations: false, strategicIntelligence: false });
|
||||
}
|
||||
};
|
||||
|
||||
@@ -236,156 +231,6 @@ const ContentStrategyTab: React.FC = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const loadKeywordResearch = async () => {
|
||||
try {
|
||||
setDataLoading(prev => ({ ...prev, keywordResearch: true }));
|
||||
|
||||
// Use streaming endpoint for real-time updates
|
||||
const eventSource = await contentPlanningApi.streamKeywordResearch(1);
|
||||
|
||||
contentPlanningApi.handleSSEData(
|
||||
eventSource,
|
||||
(data) => {
|
||||
console.log('Keyword Research SSE Data:', data);
|
||||
|
||||
if (data.type === 'status') {
|
||||
// Update loading message
|
||||
console.log('Status:', data.message);
|
||||
} else if (data.type === 'progress') {
|
||||
// Update progress (could be used for progress bar)
|
||||
console.log('Progress:', data.progress, '%');
|
||||
} else if (data.type === 'result' && data.status === 'success') {
|
||||
// Set the keyword research data
|
||||
setKeywordResearch(data.data);
|
||||
setDataLoading(prev => ({ ...prev, keywordResearch: false }));
|
||||
} else if (data.type === 'error') {
|
||||
console.error('Keyword Research Error:', data.message);
|
||||
// Set fallback data on error
|
||||
const keywordData = {
|
||||
trend_analysis: {
|
||||
high_volume_keywords: [
|
||||
{ keyword: 'AI marketing automation', volume: '10K-100K', difficulty: 'Medium' },
|
||||
{ keyword: 'content strategy 2024', volume: '1K-10K', difficulty: 'Low' },
|
||||
{ keyword: 'digital marketing trends', volume: '10K-100K', difficulty: 'High' }
|
||||
],
|
||||
trending_keywords: [
|
||||
{ keyword: 'AI content generation', growth: '+45%', opportunity: 'High' },
|
||||
{ keyword: 'voice search optimization', growth: '+32%', opportunity: 'Medium' },
|
||||
{ keyword: 'video marketing strategy', growth: '+28%', opportunity: 'High' }
|
||||
]
|
||||
},
|
||||
intent_analysis: {
|
||||
informational: ['how to', 'what is', 'guide to'],
|
||||
navigational: ['company name', 'brand name', 'website'],
|
||||
transactional: ['buy', 'purchase', 'download', 'sign up']
|
||||
},
|
||||
opportunities: [
|
||||
{ keyword: 'AI content tools', search_volume: '5K-10K', competition: 'Low', cpc: '$2.50' },
|
||||
{ keyword: 'content marketing ROI', search_volume: '1K-5K', competition: 'Medium', cpc: '$4.20' },
|
||||
{ keyword: 'social media strategy', search_volume: '10K-50K', competition: 'High', cpc: '$3.80' }
|
||||
]
|
||||
};
|
||||
setKeywordResearch(keywordData);
|
||||
setDataLoading(prev => ({ ...prev, keywordResearch: false }));
|
||||
}
|
||||
},
|
||||
(error) => {
|
||||
console.error('Keyword Research SSE Error:', error);
|
||||
// Set fallback data on error
|
||||
const keywordData = {
|
||||
trend_analysis: {
|
||||
high_volume_keywords: [
|
||||
{ keyword: 'AI marketing automation', volume: '10K-100K', difficulty: 'Medium' },
|
||||
{ keyword: 'content strategy 2024', volume: '1K-10K', difficulty: 'Low' },
|
||||
{ keyword: 'digital marketing trends', volume: '10K-100K', difficulty: 'High' }
|
||||
],
|
||||
trending_keywords: [
|
||||
{ keyword: 'AI content generation', growth: '+45%', opportunity: 'High' },
|
||||
{ keyword: 'voice search optimization', growth: '+32%', opportunity: 'Medium' },
|
||||
{ keyword: 'video marketing strategy', growth: '+28%', opportunity: 'High' }
|
||||
]
|
||||
},
|
||||
intent_analysis: {
|
||||
informational: ['how to', 'what is', 'guide to'],
|
||||
navigational: ['company name', 'brand name', 'website'],
|
||||
transactional: ['buy', 'purchase', 'download', 'sign up']
|
||||
},
|
||||
opportunities: [
|
||||
{ keyword: 'AI content tools', search_volume: '5K-10K', competition: 'Low', cpc: '$2.50' },
|
||||
{ keyword: 'content marketing ROI', search_volume: '1K-5K', competition: 'Medium', cpc: '$4.20' },
|
||||
{ keyword: 'social media strategy', search_volume: '10K-50K', competition: 'High', cpc: '$3.80' }
|
||||
]
|
||||
};
|
||||
setKeywordResearch(keywordData);
|
||||
setDataLoading(prev => ({ ...prev, keywordResearch: false }));
|
||||
}
|
||||
);
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error loading keyword research:', error);
|
||||
// Set fallback data on error
|
||||
const keywordData = {
|
||||
trend_analysis: {
|
||||
high_volume_keywords: [
|
||||
{ keyword: 'AI marketing automation', volume: '10K-100K', difficulty: 'Medium' },
|
||||
{ keyword: 'content strategy 2024', volume: '1K-10K', difficulty: 'Low' },
|
||||
{ keyword: 'digital marketing trends', volume: '10K-100K', difficulty: 'High' }
|
||||
],
|
||||
trending_keywords: [
|
||||
{ keyword: 'AI content generation', growth: '+45%', opportunity: 'High' },
|
||||
{ keyword: 'voice search optimization', growth: '+32%', opportunity: 'Medium' },
|
||||
{ keyword: 'video marketing strategy', growth: '+28%', opportunity: 'High' }
|
||||
]
|
||||
},
|
||||
intent_analysis: {
|
||||
informational: ['how to', 'what is', 'guide to'],
|
||||
navigational: ['company name', 'brand name', 'website'],
|
||||
transactional: ['buy', 'purchase', 'download', 'sign up']
|
||||
},
|
||||
opportunities: [
|
||||
{ keyword: 'AI content tools', search_volume: '5K-10K', competition: 'Low', cpc: '$2.50' },
|
||||
{ keyword: 'content marketing ROI', search_volume: '1K-5K', competition: 'Medium', cpc: '$4.20' },
|
||||
{ keyword: 'social media strategy', search_volume: '10K-50K', competition: 'High', cpc: '$3.80' }
|
||||
]
|
||||
};
|
||||
setKeywordResearch(keywordData);
|
||||
setDataLoading(prev => ({ ...prev, keywordResearch: false }));
|
||||
}
|
||||
};
|
||||
|
||||
const loadContentPillars = async () => {
|
||||
try {
|
||||
setDataLoading(prev => ({ ...prev, pillars: true }));
|
||||
|
||||
// Get content pillars from current strategy
|
||||
if (currentStrategy && currentStrategy.content_pillars) {
|
||||
const pillars = currentStrategy.content_pillars.map((pillar: any, index: number) => ({
|
||||
name: pillar.name || `Pillar ${index + 1}`,
|
||||
content_count: pillar.content_count || Math.floor(Math.random() * 20) + 5,
|
||||
avg_engagement: pillar.avg_engagement || (Math.random() * 30 + 60).toFixed(1),
|
||||
performance_score: pillar.performance_score || (Math.random() * 20 + 75).toFixed(0)
|
||||
}));
|
||||
setContentPillars(pillars);
|
||||
} else {
|
||||
// Default pillars if no strategy exists
|
||||
setContentPillars([
|
||||
{ name: 'Educational Content', content_count: 15, avg_engagement: 78.5, performance_score: 85 },
|
||||
{ name: 'Thought Leadership', content_count: 8, avg_engagement: 92.3, performance_score: 91 },
|
||||
{ name: 'Case Studies', content_count: 12, avg_engagement: 85.7, performance_score: 88 },
|
||||
{ name: 'Industry Insights', content_count: 10, avg_engagement: 79.2, performance_score: 82 }
|
||||
]);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading content pillars:', error);
|
||||
} finally {
|
||||
setDataLoading(prev => ({ ...prev, pillars: false }));
|
||||
}
|
||||
};
|
||||
|
||||
const handleTabChange = (event: React.SyntheticEvent, newValue: number) => {
|
||||
setTabValue(newValue);
|
||||
};
|
||||
|
||||
const handleStrategyFormChange = (field: string, value: string) => {
|
||||
setStrategyForm(prev => ({
|
||||
...prev,
|
||||
@@ -428,6 +273,38 @@ const ContentStrategyTab: React.FC = () => {
|
||||
await loadInitialData();
|
||||
};
|
||||
|
||||
// Onboarding dialog handlers
|
||||
const handleConfirmStrategy = async () => {
|
||||
try {
|
||||
if (currentStrategy) {
|
||||
// For now, we'll just close the dialog since we can't update status
|
||||
// In a real implementation, you would update the strategy status in the database
|
||||
setShowOnboarding(false);
|
||||
|
||||
// Reload strategies to get updated data
|
||||
await loadStrategies();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error activating strategy:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleEditStrategy = () => {
|
||||
setShowOnboarding(false);
|
||||
// Navigate to Create tab to edit strategy
|
||||
// This would typically involve changing the active tab in the parent component
|
||||
};
|
||||
|
||||
const handleCreateNewStrategy = () => {
|
||||
setShowOnboarding(false);
|
||||
// Navigate to Create tab to create new strategy
|
||||
// This would typically involve changing the active tab in the parent component
|
||||
};
|
||||
|
||||
const handleCloseOnboarding = () => {
|
||||
setShowOnboarding(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<Box sx={{ p: 3 }}>
|
||||
{error && (
|
||||
@@ -436,260 +313,72 @@ const ContentStrategyTab: React.FC = () => {
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
{/* Strategy Builder Tabs */}
|
||||
{/* Strategy Status Banner */}
|
||||
{strategyStatus === 'inactive' && (
|
||||
<Alert
|
||||
severity="warning"
|
||||
sx={{ mb: 3 }}
|
||||
action={
|
||||
<Button
|
||||
color="inherit"
|
||||
size="small"
|
||||
onClick={() => setShowOnboarding(true)}
|
||||
startIcon={<PlayArrowIcon />}
|
||||
>
|
||||
Activate Strategy
|
||||
</Button>
|
||||
}
|
||||
>
|
||||
<Typography variant="body1">
|
||||
<strong>Strategy Pending Activation:</strong> Your content strategy is ready but needs to be activated to start your AI-powered content marketing journey.
|
||||
</Typography>
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
{strategyStatus === 'none' && (
|
||||
<Alert
|
||||
severity="info"
|
||||
sx={{ mb: 3 }}
|
||||
action={
|
||||
<Button
|
||||
color="inherit"
|
||||
size="small"
|
||||
onClick={() => setShowOnboarding(true)}
|
||||
startIcon={<AutoAwesomeIcon />}
|
||||
>
|
||||
Create Strategy
|
||||
</Button>
|
||||
}
|
||||
>
|
||||
<Typography variant="body1">
|
||||
<strong>No Strategy Found:</strong> Let's create your first AI-powered content strategy to start your digital marketing journey.
|
||||
</Typography>
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
{strategyStatus === 'active' && (
|
||||
<Alert severity="success" sx={{ mb: 3 }}>
|
||||
<Typography variant="body1">
|
||||
<strong>Strategy Active:</strong> Your content strategy is running and ALwrity is managing your content marketing automatically.
|
||||
</Typography>
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
{/* Strategic Intelligence */}
|
||||
<Paper sx={{ width: '100%', mb: 3 }}>
|
||||
<Box sx={{ borderBottom: 1, borderColor: 'divider' }}>
|
||||
<Tabs value={tabValue} onChange={handleTabChange} aria-label="strategy builder tabs">
|
||||
<Tab
|
||||
label={
|
||||
<Box sx={{ display: 'flex', alignItems: 'center' }}>
|
||||
<AutoAwesomeIcon sx={{ mr: 1 }} />
|
||||
Enhanced Strategy Builder
|
||||
</Box>
|
||||
}
|
||||
/>
|
||||
<Tab label="Strategic Intelligence" icon={<AssessmentIcon />} />
|
||||
<Tab label="Keyword Research" icon={<SearchIcon />} />
|
||||
<Tab label="Performance Analytics" icon={<BarChartIcon />} />
|
||||
<Tab label="Content Pillars" icon={<PieChartIcon />} />
|
||||
</Tabs>
|
||||
</Box>
|
||||
|
||||
{/* Enhanced Strategy Builder Tab */}
|
||||
<TabPanel value={tabValue} index={0}>
|
||||
<ContentStrategyBuilder />
|
||||
</TabPanel>
|
||||
|
||||
{/* Strategic Intelligence Tab */}
|
||||
<TabPanel value={tabValue} index={1}>
|
||||
<StrategyIntelligenceTab />
|
||||
</TabPanel>
|
||||
|
||||
{/* Keyword Research Tab */}
|
||||
<TabPanel value={tabValue} index={2}>
|
||||
{dataLoading.keywordResearch ? (
|
||||
<Box sx={{ display: 'flex', justifyContent: 'center', p: 3 }}>
|
||||
<CircularProgress />
|
||||
</Box>
|
||||
) : keywordResearch && keywordResearch.trend_analysis ? (
|
||||
<Grid container spacing={3}>
|
||||
<Grid item xs={12} md={6}>
|
||||
<Card>
|
||||
<CardContent>
|
||||
<Typography variant="h6" gutterBottom>
|
||||
High Volume Keywords
|
||||
</Typography>
|
||||
<TableContainer>
|
||||
<Table size="small">
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell>Keyword</TableCell>
|
||||
<TableCell>Volume</TableCell>
|
||||
<TableCell>Difficulty</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{(keywordResearch.trend_analysis.high_volume_keywords || []).map((keyword: any, index: number) => (
|
||||
<TableRow key={index}>
|
||||
<TableCell>{keyword.keyword}</TableCell>
|
||||
<TableCell>{keyword.volume}</TableCell>
|
||||
<TableCell>
|
||||
<Chip
|
||||
label={keyword.difficulty}
|
||||
color={keyword.difficulty === 'Low' ? 'success' : keyword.difficulty === 'Medium' ? 'warning' : 'error'}
|
||||
size="small"
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Grid>
|
||||
|
||||
<Grid item xs={12} md={6}>
|
||||
<Card>
|
||||
<CardContent>
|
||||
<Typography variant="h6" gutterBottom>
|
||||
Trending Keywords
|
||||
</Typography>
|
||||
{(keywordResearch.trend_analysis.trending_keywords || []).map((keyword: any, index: number) => (
|
||||
<Box key={index} sx={{ mb: 2 }}>
|
||||
<Typography variant="subtitle1">
|
||||
{keyword.keyword}
|
||||
</Typography>
|
||||
<Box sx={{ display: 'flex', gap: 1 }}>
|
||||
<Chip
|
||||
label={keyword.growth}
|
||||
color="success"
|
||||
size="small"
|
||||
/>
|
||||
<Chip
|
||||
label={keyword.opportunity}
|
||||
color={keyword.opportunity === 'High' ? 'success' : 'primary'}
|
||||
size="small"
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
))}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Grid>
|
||||
|
||||
<Grid item xs={12}>
|
||||
<Card>
|
||||
<CardContent>
|
||||
<Typography variant="h6" gutterBottom>
|
||||
Keyword Opportunities
|
||||
</Typography>
|
||||
<TableContainer>
|
||||
<Table>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell>Keyword</TableCell>
|
||||
<TableCell>Search Volume</TableCell>
|
||||
<TableCell>Competition</TableCell>
|
||||
<TableCell>CPC</TableCell>
|
||||
<TableCell>Action</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{(keywordResearch.opportunities || []).map((opportunity: any, index: number) => (
|
||||
<TableRow key={index}>
|
||||
<TableCell>{opportunity.keyword}</TableCell>
|
||||
<TableCell>{opportunity.search_volume}</TableCell>
|
||||
<TableCell>
|
||||
<Chip
|
||||
label={opportunity.competition}
|
||||
color={opportunity.competition === 'Low' ? 'success' : opportunity.competition === 'Medium' ? 'warning' : 'error'}
|
||||
size="small"
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell>${opportunity.cpc}</TableCell>
|
||||
<TableCell>
|
||||
<Button size="small" variant="outlined">
|
||||
Add to Strategy
|
||||
</Button>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Grid>
|
||||
</Grid>
|
||||
) : (
|
||||
<Typography variant="body2" color="text.secondary" sx={{ textAlign: 'center', p: 3 }}>
|
||||
No keyword research data available
|
||||
</Typography>
|
||||
)}
|
||||
</TabPanel>
|
||||
|
||||
{/* Performance Analytics Tab */}
|
||||
<TabPanel value={tabValue} index={3}>
|
||||
{performanceMetrics ? (
|
||||
<Grid container spacing={3}>
|
||||
<Grid item xs={12} md={6}>
|
||||
<Card>
|
||||
<CardContent>
|
||||
<Typography variant="h6" gutterBottom>
|
||||
Content Performance by Type
|
||||
</Typography>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
No content performance data available
|
||||
</Typography>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Grid>
|
||||
|
||||
<Grid item xs={12} md={6}>
|
||||
<Card>
|
||||
<CardContent>
|
||||
<Typography variant="h6" gutterBottom>
|
||||
Growth Trends
|
||||
</Typography>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
No trend data available
|
||||
</Typography>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Grid>
|
||||
</Grid>
|
||||
) : (
|
||||
<Typography variant="body2" color="text.secondary" sx={{ textAlign: 'center', p: 3 }}>
|
||||
No performance analytics data available
|
||||
</Typography>
|
||||
)}
|
||||
</TabPanel>
|
||||
|
||||
{/* Content Pillars Tab */}
|
||||
<TabPanel value={tabValue} index={4}>
|
||||
{dataLoading.pillars ? (
|
||||
<Box sx={{ display: 'flex', justifyContent: 'center', p: 3 }}>
|
||||
<CircularProgress />
|
||||
</Box>
|
||||
) : contentPillars.length > 0 ? (
|
||||
<Grid container spacing={3}>
|
||||
<Grid item xs={12}>
|
||||
<Typography variant="h6" gutterBottom>
|
||||
Content Pillars Overview
|
||||
</Typography>
|
||||
<Typography variant="body2" color="text.secondary" sx={{ mb: 3 }}>
|
||||
Your content is organized into these strategic pillars to ensure comprehensive coverage of your topics.
|
||||
</Typography>
|
||||
</Grid>
|
||||
|
||||
{contentPillars.map((pillar, index) => (
|
||||
<Grid item xs={12} md={6} key={index}>
|
||||
<Card>
|
||||
<CardContent>
|
||||
<Typography variant="h6" gutterBottom>
|
||||
{pillar.name}
|
||||
</Typography>
|
||||
<Box sx={{ display: 'flex', justifyContent: 'space-between', mb: 2 }}>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
Content Count
|
||||
</Typography>
|
||||
<Typography variant="h6">
|
||||
{pillar.content_count}
|
||||
</Typography>
|
||||
</Box>
|
||||
<Box sx={{ display: 'flex', justifyContent: 'space-between', mb: 2 }}>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
Avg. Engagement
|
||||
</Typography>
|
||||
<Typography variant="h6">
|
||||
{pillar.avg_engagement}%
|
||||
</Typography>
|
||||
</Box>
|
||||
<Box sx={{ display: 'flex', justifyContent: 'space-between' }}>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
Performance Score
|
||||
</Typography>
|
||||
<Typography variant="h6" color="success.main">
|
||||
{pillar.performance_score}/100
|
||||
</Typography>
|
||||
</Box>
|
||||
</CardContent>
|
||||
<CardActions>
|
||||
<Button size="small">View Content</Button>
|
||||
<Button size="small">Optimize</Button>
|
||||
</CardActions>
|
||||
</Card>
|
||||
</Grid>
|
||||
))}
|
||||
</Grid>
|
||||
) : (
|
||||
<Typography variant="body2" color="text.secondary" sx={{ textAlign: 'center', p: 3 }}>
|
||||
No content pillars data available
|
||||
</Typography>
|
||||
)}
|
||||
</TabPanel>
|
||||
<StrategyIntelligenceTab />
|
||||
</Paper>
|
||||
|
||||
{/* Strategy Onboarding Dialog */}
|
||||
<StrategyOnboardingDialog
|
||||
open={showOnboarding}
|
||||
onClose={handleCloseOnboarding}
|
||||
onConfirmStrategy={handleConfirmStrategy}
|
||||
onEditStrategy={handleEditStrategy}
|
||||
onCreateNewStrategy={handleCreateNewStrategy}
|
||||
currentStrategy={currentStrategy}
|
||||
strategyStatus={strategyStatus}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,113 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import {
|
||||
Box,
|
||||
Tabs,
|
||||
Tab,
|
||||
Typography
|
||||
} from '@mui/material';
|
||||
import {
|
||||
AutoAwesome as AutoAwesomeIcon,
|
||||
CalendarToday as CalendarIcon
|
||||
} from '@mui/icons-material';
|
||||
import ContentStrategyBuilder from '../components/ContentStrategyBuilder';
|
||||
import CalendarGenerationWizard from '../components/CalendarGenerationWizard';
|
||||
import { contentPlanningApi } from '../../../services/contentPlanningApi';
|
||||
|
||||
interface TabPanelProps {
|
||||
children?: React.ReactNode;
|
||||
index: number;
|
||||
value: number;
|
||||
}
|
||||
|
||||
function TabPanel(props: TabPanelProps) {
|
||||
const { children, value, index, ...other } = props;
|
||||
|
||||
return (
|
||||
<div
|
||||
role="tabpanel"
|
||||
hidden={value !== index}
|
||||
id={`create-tabpanel-${index}`}
|
||||
aria-labelledby={`create-tab-${index}`}
|
||||
{...other}
|
||||
>
|
||||
{value === index && <Box>{children}</Box>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const CreateTab: React.FC = () => {
|
||||
const [tabValue, setTabValue] = useState(0);
|
||||
const [userData, setUserData] = useState<any>({});
|
||||
|
||||
useEffect(() => {
|
||||
loadUserData();
|
||||
}, []);
|
||||
|
||||
const loadUserData = async () => {
|
||||
try {
|
||||
// Load comprehensive user data for calendar generation
|
||||
const comprehensiveData = await contentPlanningApi.getComprehensiveUserData(1); // Pass user ID
|
||||
setUserData(comprehensiveData.data); // Extract the data from the response
|
||||
} catch (error) {
|
||||
console.error('Error loading user data:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleTabChange = (event: React.SyntheticEvent, newValue: number) => {
|
||||
setTabValue(newValue);
|
||||
};
|
||||
|
||||
const handleGenerateCalendar = async (calendarConfig: any) => {
|
||||
try {
|
||||
await contentPlanningApi.generateComprehensiveCalendar({
|
||||
...calendarConfig,
|
||||
userData
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error generating calendar:', error);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Box sx={{ p: 3 }}>
|
||||
<Typography variant="h4" gutterBottom>
|
||||
Create
|
||||
</Typography>
|
||||
|
||||
<Box sx={{ borderBottom: 1, borderColor: 'divider', mb: 3 }}>
|
||||
<Tabs value={tabValue} onChange={handleTabChange} aria-label="create tabs">
|
||||
<Tab
|
||||
label={
|
||||
<Box sx={{ display: 'flex', alignItems: 'center' }}>
|
||||
<AutoAwesomeIcon sx={{ mr: 1 }} />
|
||||
Enhanced Strategy Builder
|
||||
</Box>
|
||||
}
|
||||
/>
|
||||
<Tab
|
||||
label={
|
||||
<Box sx={{ display: 'flex', alignItems: 'center' }}>
|
||||
<CalendarIcon sx={{ mr: 1 }} />
|
||||
Calendar Wizard
|
||||
</Box>
|
||||
}
|
||||
/>
|
||||
</Tabs>
|
||||
</Box>
|
||||
|
||||
<TabPanel value={tabValue} index={0}>
|
||||
<ContentStrategyBuilder />
|
||||
</TabPanel>
|
||||
|
||||
<TabPanel value={tabValue} index={1}>
|
||||
<CalendarGenerationWizard
|
||||
userData={userData}
|
||||
onGenerateCalendar={handleGenerateCalendar}
|
||||
loading={false}
|
||||
/>
|
||||
</TabPanel>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default CreateTab;
|
||||
@@ -1,404 +1,95 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import React, { useState } from 'react';
|
||||
import {
|
||||
Box,
|
||||
Grid,
|
||||
Paper,
|
||||
Tabs,
|
||||
Tab,
|
||||
Typography,
|
||||
Button,
|
||||
TextField,
|
||||
Card,
|
||||
CardContent,
|
||||
Chip,
|
||||
Divider,
|
||||
Alert,
|
||||
CircularProgress,
|
||||
List,
|
||||
ListItem,
|
||||
ListItemText,
|
||||
ListItemIcon
|
||||
Alert
|
||||
} from '@mui/material';
|
||||
import {
|
||||
Analytics as AnalyticsIcon,
|
||||
TrendingUp as TrendingIcon,
|
||||
Search as SearchIcon,
|
||||
Add as AddIcon,
|
||||
Warning as WarningIcon,
|
||||
CheckCircle as CheckCircleIcon,
|
||||
TrendingUp as TrendingUpIcon,
|
||||
Assessment as AssessmentIcon
|
||||
Assessment as AssessmentIcon,
|
||||
BarChart as BarChartIcon,
|
||||
PieChart as PieChartIcon
|
||||
} from '@mui/icons-material';
|
||||
import { useContentPlanningStore } from '../../../stores/contentPlanningStore';
|
||||
import { contentPlanningApi } from '../../../services/contentPlanningApi';
|
||||
import RefineAnalysisTab from './RefineAnalysisTab';
|
||||
import ContentOptimizerTab from './ContentOptimizerTab';
|
||||
import TrendingTopicsTab from './TrendingTopicsTab';
|
||||
import KeywordResearchTab from './KeywordResearchTab';
|
||||
import PerformanceAnalyticsTab from './PerformanceAnalyticsTab';
|
||||
import ContentPillarsTab from './ContentPillarsTab';
|
||||
|
||||
interface TabPanelProps {
|
||||
children?: React.ReactNode;
|
||||
index: number;
|
||||
value: number;
|
||||
}
|
||||
|
||||
function TabPanel(props: TabPanelProps) {
|
||||
const { children, value, index, ...other } = props;
|
||||
|
||||
return (
|
||||
<div
|
||||
role="tabpanel"
|
||||
hidden={value !== index}
|
||||
id={`gap-analysis-tabpanel-${index}`}
|
||||
aria-labelledby={`gap-analysis-tab-${index}`}
|
||||
{...other}
|
||||
>
|
||||
{value === index && <Box>{children}</Box>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const GapAnalysisTab: React.FC = () => {
|
||||
const {
|
||||
gapAnalyses,
|
||||
loading,
|
||||
error,
|
||||
loadGapAnalyses,
|
||||
analyzeContentGaps,
|
||||
updateGapAnalyses
|
||||
} = useContentPlanningStore();
|
||||
|
||||
const [analysisForm, setAnalysisForm] = useState({
|
||||
website_url: '',
|
||||
competitors: [] as string[],
|
||||
keywords: [] as string[]
|
||||
});
|
||||
const [newCompetitor, setNewCompetitor] = useState('');
|
||||
const [newKeyword, setNewKeyword] = useState('');
|
||||
const [dataLoading, setDataLoading] = useState(false);
|
||||
const [tabValue, setTabValue] = useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
loadGapAnalysisData();
|
||||
}, []);
|
||||
|
||||
const loadGapAnalysisData = async () => {
|
||||
try {
|
||||
setDataLoading(true);
|
||||
const response = await contentPlanningApi.getGapAnalysesSafe();
|
||||
|
||||
console.log('Gap Analysis Response:', response);
|
||||
|
||||
// Transform the backend response to match frontend expectations
|
||||
if (response && response.gap_analyses) {
|
||||
const transformedAnalyses = response.gap_analyses.map((analysis: any, index: number) => ({
|
||||
id: analysis.id || `analysis_${index}`,
|
||||
website_url: analysis.website_url || 'example.com',
|
||||
competitors: analysis.competitors || [],
|
||||
keywords: analysis.keywords || [],
|
||||
gaps: analysis.gaps || [],
|
||||
recommendations: analysis.recommendations || [],
|
||||
created_at: analysis.created_at || new Date().toISOString()
|
||||
}));
|
||||
|
||||
console.log('Transformed Analyses:', transformedAnalyses);
|
||||
|
||||
// Update the store with transformed data
|
||||
updateGapAnalyses(transformedAnalyses);
|
||||
} else {
|
||||
console.log('No gap analyses found in response');
|
||||
updateGapAnalyses([]);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading gap analysis data:', error);
|
||||
updateGapAnalyses([]);
|
||||
} finally {
|
||||
setDataLoading(false);
|
||||
}
|
||||
const handleTabChange = (event: React.SyntheticEvent, newValue: number) => {
|
||||
setTabValue(newValue);
|
||||
};
|
||||
|
||||
const handleAddCompetitor = () => {
|
||||
if (newCompetitor.trim() && !analysisForm.competitors.includes(newCompetitor.trim())) {
|
||||
setAnalysisForm(prev => ({
|
||||
...prev,
|
||||
competitors: [...prev.competitors, newCompetitor.trim()]
|
||||
}));
|
||||
setNewCompetitor('');
|
||||
}
|
||||
};
|
||||
|
||||
const handleRemoveCompetitor = (competitorToRemove: string) => {
|
||||
setAnalysisForm(prev => ({
|
||||
...prev,
|
||||
competitors: prev.competitors.filter(comp => comp !== competitorToRemove)
|
||||
}));
|
||||
};
|
||||
|
||||
const handleAddKeyword = () => {
|
||||
if (newKeyword.trim() && !analysisForm.keywords.includes(newKeyword.trim())) {
|
||||
setAnalysisForm(prev => ({
|
||||
...prev,
|
||||
keywords: [...prev.keywords, newKeyword.trim()]
|
||||
}));
|
||||
setNewKeyword('');
|
||||
}
|
||||
};
|
||||
|
||||
const handleRemoveKeyword = (keywordToRemove: string) => {
|
||||
setAnalysisForm(prev => ({
|
||||
...prev,
|
||||
keywords: prev.keywords.filter(keyword => keyword !== keywordToRemove)
|
||||
}));
|
||||
};
|
||||
|
||||
const handleRunAnalysis = async () => {
|
||||
if (!analysisForm.website_url) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
setDataLoading(true);
|
||||
|
||||
await analyzeContentGaps({
|
||||
website_url: analysisForm.website_url,
|
||||
competitors: analysisForm.competitors,
|
||||
keywords: analysisForm.keywords
|
||||
});
|
||||
|
||||
// Reload data after analysis
|
||||
await loadGapAnalyses();
|
||||
|
||||
// Reset form
|
||||
setAnalysisForm({
|
||||
website_url: '',
|
||||
competitors: [],
|
||||
keywords: []
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error running gap analysis:', error);
|
||||
} finally {
|
||||
setDataLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// Ensure gapAnalyses is always an array and transform the data structure
|
||||
const safeGapAnalyses = Array.isArray(gapAnalyses) ? gapAnalyses : [];
|
||||
|
||||
// Transform backend data structure to frontend expected structure
|
||||
const transformedGapAnalyses = safeGapAnalyses.map((analysis, index) => {
|
||||
// Handle the actual backend structure: { recommendations: [...] }
|
||||
const recommendations = analysis.recommendations || [];
|
||||
|
||||
return {
|
||||
id: analysis.id || `analysis-${index}`,
|
||||
website_url: analysis.website_url || 'Unknown Website',
|
||||
competitors: analysis.competitors || [],
|
||||
keywords: analysis.keywords || [],
|
||||
recommendations: recommendations,
|
||||
created_at: analysis.created_at || new Date().toISOString(),
|
||||
// Extract gaps from recommendations if available
|
||||
gaps: recommendations.length > 0 ?
|
||||
recommendations.filter((rec: any) => rec.type === 'gap').map((rec: any) => rec.title || rec.description || 'Content gap identified') :
|
||||
[]
|
||||
};
|
||||
});
|
||||
|
||||
return (
|
||||
<Box sx={{ p: 3 }}>
|
||||
<Typography variant="h4" gutterBottom>
|
||||
Content Gap Analysis
|
||||
Gap Analysis & Optimization
|
||||
</Typography>
|
||||
|
||||
{error && (
|
||||
<Alert severity="error" sx={{ mb: 2 }}>
|
||||
{error}
|
||||
</Alert>
|
||||
)}
|
||||
<Box sx={{ borderBottom: 1, borderColor: 'divider', mb: 3 }}>
|
||||
<Tabs value={tabValue} onChange={handleTabChange} aria-label="gap analysis tabs">
|
||||
<Tab label="Content Optimizer" icon={<AnalyticsIcon />} iconPosition="start" />
|
||||
<Tab label="Trending Topics" icon={<TrendingIcon />} iconPosition="start" />
|
||||
<Tab label="Keyword Research" icon={<SearchIcon />} iconPosition="start" />
|
||||
<Tab label="Performance Analytics" icon={<BarChartIcon />} iconPosition="start" />
|
||||
<Tab label="Content Pillars" icon={<PieChartIcon />} iconPosition="start" />
|
||||
<Tab label="Refine Analysis" icon={<AssessmentIcon />} iconPosition="start" />
|
||||
</Tabs>
|
||||
</Box>
|
||||
|
||||
<Grid container spacing={3}>
|
||||
{/* Analysis Setup */}
|
||||
<Grid item xs={12} md={4}>
|
||||
<Paper sx={{ p: 3, mb: 3 }}>
|
||||
<Typography variant="h6" gutterBottom>
|
||||
<SearchIcon sx={{ mr: 1, verticalAlign: 'middle' }} />
|
||||
Analysis Setup
|
||||
</Typography>
|
||||
<Divider sx={{ mb: 2 }} />
|
||||
|
||||
<TextField
|
||||
fullWidth
|
||||
label="Website URL"
|
||||
value={analysisForm.website_url}
|
||||
onChange={(e) => setAnalysisForm(prev => ({ ...prev, website_url: e.target.value }))}
|
||||
placeholder="https://example.com"
|
||||
sx={{ mb: 2 }}
|
||||
/>
|
||||
|
||||
<Typography variant="subtitle2" gutterBottom>
|
||||
Competitors
|
||||
</Typography>
|
||||
<Box sx={{ display: 'flex', gap: 1, mb: 2 }}>
|
||||
<TextField
|
||||
fullWidth
|
||||
label="Add Competitor"
|
||||
value={newCompetitor}
|
||||
onChange={(e) => setNewCompetitor(e.target.value)}
|
||||
placeholder="competitor.com"
|
||||
onKeyPress={(e) => e.key === 'Enter' && handleAddCompetitor()}
|
||||
/>
|
||||
<Button
|
||||
variant="outlined"
|
||||
onClick={handleAddCompetitor}
|
||||
disabled={!newCompetitor.trim()}
|
||||
>
|
||||
<AddIcon />
|
||||
</Button>
|
||||
</Box>
|
||||
|
||||
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 1, mb: 3 }}>
|
||||
{analysisForm.competitors.map((competitor, index) => (
|
||||
<Chip
|
||||
key={index}
|
||||
label={competitor}
|
||||
onDelete={() => handleRemoveCompetitor(competitor)}
|
||||
color="primary"
|
||||
variant="outlined"
|
||||
/>
|
||||
))}
|
||||
</Box>
|
||||
|
||||
<Typography variant="subtitle2" gutterBottom>
|
||||
Keywords
|
||||
</Typography>
|
||||
<Box sx={{ display: 'flex', gap: 1, mb: 2 }}>
|
||||
<TextField
|
||||
fullWidth
|
||||
label="Add Keyword"
|
||||
value={newKeyword}
|
||||
onChange={(e) => setNewKeyword(e.target.value)}
|
||||
placeholder="target keyword"
|
||||
onKeyPress={(e) => e.key === 'Enter' && handleAddKeyword()}
|
||||
/>
|
||||
<Button
|
||||
variant="outlined"
|
||||
onClick={handleAddKeyword}
|
||||
disabled={!newKeyword.trim()}
|
||||
>
|
||||
<AddIcon />
|
||||
</Button>
|
||||
</Box>
|
||||
|
||||
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 1, mb: 3 }}>
|
||||
{analysisForm.keywords.map((keyword, index) => (
|
||||
<Chip
|
||||
key={index}
|
||||
label={keyword}
|
||||
onDelete={() => handleRemoveKeyword(keyword)}
|
||||
color="secondary"
|
||||
variant="outlined"
|
||||
/>
|
||||
))}
|
||||
</Box>
|
||||
|
||||
<Button
|
||||
variant="contained"
|
||||
fullWidth
|
||||
onClick={handleRunAnalysis}
|
||||
disabled={loading || dataLoading || !analysisForm.website_url}
|
||||
startIcon={<AssessmentIcon />}
|
||||
>
|
||||
{loading || dataLoading ? 'Running Analysis...' : 'Run Gap Analysis'}
|
||||
</Button>
|
||||
</Paper>
|
||||
</Grid>
|
||||
<TabPanel value={tabValue} index={0}>
|
||||
<ContentOptimizerTab />
|
||||
</TabPanel>
|
||||
|
||||
{/* Content Gaps */}
|
||||
<Grid item xs={12} md={8}>
|
||||
<Paper sx={{ p: 3, mb: 3 }}>
|
||||
<Typography variant="h6" gutterBottom>
|
||||
<WarningIcon sx={{ mr: 1, verticalAlign: 'middle' }} />
|
||||
Content Gaps
|
||||
</Typography>
|
||||
<Divider sx={{ mb: 2 }} />
|
||||
|
||||
{dataLoading ? (
|
||||
<Box sx={{ display: 'flex', justifyContent: 'center', p: 3 }}>
|
||||
<CircularProgress />
|
||||
</Box>
|
||||
) : transformedGapAnalyses.length === 0 ? (
|
||||
<Typography variant="body2" color="text.secondary" sx={{ textAlign: 'center', py: 2 }}>
|
||||
No previous analyses found. Run your first analysis to see results here.
|
||||
</Typography>
|
||||
) : (
|
||||
<Grid container spacing={2}>
|
||||
{transformedGapAnalyses.map((analysis) => (
|
||||
<Grid item xs={12} md={6} lg={4} key={analysis.id}>
|
||||
<Card>
|
||||
<CardContent>
|
||||
<Typography variant="h6" component="div">
|
||||
{analysis.website_url}
|
||||
</Typography>
|
||||
<Typography variant="body2" color="text.secondary" sx={{ mb: 2 }}>
|
||||
{new Date(analysis.created_at).toLocaleDateString()}
|
||||
</Typography>
|
||||
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 1 }}>
|
||||
<Chip
|
||||
label={`${analysis.competitors?.length || 0} competitors`}
|
||||
size="small"
|
||||
variant="outlined"
|
||||
/>
|
||||
<Chip
|
||||
label={`${analysis.keywords?.length || 0} keywords`}
|
||||
size="small"
|
||||
variant="outlined"
|
||||
/>
|
||||
<Chip
|
||||
label={`${analysis.gaps?.length || 0} gaps found`}
|
||||
size="small"
|
||||
color="warning"
|
||||
/>
|
||||
<Chip
|
||||
label={`${analysis.recommendations?.length || 0} recommendations`}
|
||||
size="small"
|
||||
color="success"
|
||||
/>
|
||||
</Box>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Grid>
|
||||
))}
|
||||
</Grid>
|
||||
)}
|
||||
</Paper>
|
||||
<TabPanel value={tabValue} index={1}>
|
||||
<TrendingTopicsTab />
|
||||
</TabPanel>
|
||||
|
||||
{/* Detailed Analysis Results */}
|
||||
{transformedGapAnalyses.length > 0 && (
|
||||
<Paper sx={{ p: 3 }}>
|
||||
<Typography variant="h6" gutterBottom>
|
||||
<TrendingUpIcon sx={{ mr: 1, verticalAlign: 'middle' }} />
|
||||
Detailed Analysis Results
|
||||
</Typography>
|
||||
<Divider sx={{ mb: 2 }} />
|
||||
|
||||
{transformedGapAnalyses.map((analysis, index) => (
|
||||
<Box key={index} sx={{ mb: 3 }}>
|
||||
<Typography variant="subtitle1" gutterBottom>
|
||||
Analysis for {analysis.website_url}
|
||||
</Typography>
|
||||
|
||||
{analysis.gaps && analysis.gaps.length > 0 && (
|
||||
<Box sx={{ mb: 2 }}>
|
||||
<Typography variant="subtitle2" gutterBottom>
|
||||
Identified Content Gaps:
|
||||
</Typography>
|
||||
<List dense>
|
||||
{analysis.gaps.map((gap, gapIndex) => (
|
||||
<ListItem key={gapIndex}>
|
||||
<ListItemIcon>
|
||||
<WarningIcon color="warning" />
|
||||
</ListItemIcon>
|
||||
<ListItemText primary={gap} />
|
||||
</ListItem>
|
||||
))}
|
||||
</List>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{analysis.recommendations && analysis.recommendations.length > 0 && (
|
||||
<Box>
|
||||
<Typography variant="subtitle2" gutterBottom>
|
||||
Recommendations:
|
||||
</Typography>
|
||||
<List dense>
|
||||
{analysis.recommendations.map((rec, recIndex) => (
|
||||
<ListItem key={recIndex}>
|
||||
<ListItemIcon>
|
||||
<CheckCircleIcon color="success" />
|
||||
</ListItemIcon>
|
||||
<ListItemText
|
||||
primary={rec.title || rec.description || 'Recommendation'}
|
||||
secondary={rec.description}
|
||||
/>
|
||||
</ListItem>
|
||||
))}
|
||||
</List>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
))}
|
||||
</Paper>
|
||||
)}
|
||||
</Grid>
|
||||
</Grid>
|
||||
<TabPanel value={tabValue} index={2}>
|
||||
<KeywordResearchTab />
|
||||
</TabPanel>
|
||||
|
||||
<TabPanel value={tabValue} index={3}>
|
||||
<PerformanceAnalyticsTab />
|
||||
</TabPanel>
|
||||
|
||||
<TabPanel value={tabValue} index={4}>
|
||||
<ContentPillarsTab />
|
||||
</TabPanel>
|
||||
|
||||
<TabPanel value={tabValue} index={5}>
|
||||
<RefineAnalysisTab />
|
||||
</TabPanel>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,189 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import {
|
||||
Box,
|
||||
Grid,
|
||||
Paper,
|
||||
Typography,
|
||||
Card,
|
||||
CardContent,
|
||||
Chip,
|
||||
CircularProgress,
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableContainer,
|
||||
TableHead,
|
||||
TableRow,
|
||||
Button
|
||||
} from '@mui/material';
|
||||
import {
|
||||
Search as SearchIcon
|
||||
} from '@mui/icons-material';
|
||||
import { useContentPlanningStore } from '../../../stores/contentPlanningStore';
|
||||
import { contentPlanningApi } from '../../../services/contentPlanningApi';
|
||||
|
||||
const KeywordResearchTab: React.FC = () => {
|
||||
const [keywordResearch, setKeywordResearch] = useState<any>(null);
|
||||
const [dataLoading, setDataLoading] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
loadKeywordResearch();
|
||||
}, []);
|
||||
|
||||
const loadKeywordResearch = async () => {
|
||||
try {
|
||||
setDataLoading(true);
|
||||
const eventSource = await contentPlanningApi.streamKeywordResearch();
|
||||
|
||||
contentPlanningApi.handleSSEData(
|
||||
eventSource,
|
||||
(data) => {
|
||||
console.log('Keyword Research SSE Data:', data);
|
||||
if (data.type === 'result' && data.data) {
|
||||
setKeywordResearch(data.data);
|
||||
}
|
||||
},
|
||||
(error) => {
|
||||
console.error('Keyword Research SSE Error:', error);
|
||||
},
|
||||
() => {
|
||||
setDataLoading(false);
|
||||
}
|
||||
);
|
||||
} catch (error) {
|
||||
console.error('Error loading keyword research:', error);
|
||||
setDataLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Box sx={{ p: 3 }}>
|
||||
<Typography variant="h4" gutterBottom>
|
||||
Keyword Research
|
||||
</Typography>
|
||||
|
||||
{dataLoading ? (
|
||||
<Box sx={{ display: 'flex', justifyContent: 'center', p: 3 }}>
|
||||
<CircularProgress />
|
||||
</Box>
|
||||
) : keywordResearch && keywordResearch.trend_analysis ? (
|
||||
<Grid container spacing={3}>
|
||||
<Grid item xs={12} md={6}>
|
||||
<Card>
|
||||
<CardContent>
|
||||
<Typography variant="h6" gutterBottom>
|
||||
High Volume Keywords
|
||||
</Typography>
|
||||
<TableContainer>
|
||||
<Table size="small">
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell>Keyword</TableCell>
|
||||
<TableCell>Volume</TableCell>
|
||||
<TableCell>Difficulty</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{(keywordResearch.trend_analysis.high_volume_keywords || []).map((keyword: any, index: number) => (
|
||||
<TableRow key={index}>
|
||||
<TableCell>{keyword.keyword}</TableCell>
|
||||
<TableCell>{keyword.volume}</TableCell>
|
||||
<TableCell>
|
||||
<Chip
|
||||
label={keyword.difficulty}
|
||||
color={keyword.difficulty === 'Low' ? 'success' : keyword.difficulty === 'Medium' ? 'warning' : 'error'}
|
||||
size="small"
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Grid>
|
||||
|
||||
<Grid item xs={12} md={6}>
|
||||
<Card>
|
||||
<CardContent>
|
||||
<Typography variant="h6" gutterBottom>
|
||||
Trending Keywords
|
||||
</Typography>
|
||||
{(keywordResearch.trend_analysis.trending_keywords || []).map((keyword: any, index: number) => (
|
||||
<Box key={index} sx={{ mb: 2 }}>
|
||||
<Typography variant="subtitle1">
|
||||
{keyword.keyword}
|
||||
</Typography>
|
||||
<Box sx={{ display: 'flex', gap: 1 }}>
|
||||
<Chip
|
||||
label={keyword.growth}
|
||||
color="success"
|
||||
size="small"
|
||||
/>
|
||||
<Chip
|
||||
label={keyword.opportunity}
|
||||
color={keyword.opportunity === 'High' ? 'success' : 'primary'}
|
||||
size="small"
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
))}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Grid>
|
||||
|
||||
<Grid item xs={12}>
|
||||
<Card>
|
||||
<CardContent>
|
||||
<Typography variant="h6" gutterBottom>
|
||||
Keyword Opportunities
|
||||
</Typography>
|
||||
<TableContainer>
|
||||
<Table>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell>Keyword</TableCell>
|
||||
<TableCell>Search Volume</TableCell>
|
||||
<TableCell>Competition</TableCell>
|
||||
<TableCell>CPC</TableCell>
|
||||
<TableCell>Action</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{(keywordResearch.opportunities || []).map((opportunity: any, index: number) => (
|
||||
<TableRow key={index}>
|
||||
<TableCell>{opportunity.keyword}</TableCell>
|
||||
<TableCell>{opportunity.search_volume}</TableCell>
|
||||
<TableCell>
|
||||
<Chip
|
||||
label={opportunity.competition}
|
||||
color={opportunity.competition === 'Low' ? 'success' : opportunity.competition === 'Medium' ? 'warning' : 'error'}
|
||||
size="small"
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell>${opportunity.cpc}</TableCell>
|
||||
<TableCell>
|
||||
<Button size="small" variant="outlined">
|
||||
Add to Strategy
|
||||
</Button>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Grid>
|
||||
</Grid>
|
||||
) : (
|
||||
<Typography variant="body2" color="text.secondary" sx={{ textAlign: 'center', p: 3 }}>
|
||||
No keyword research data available
|
||||
</Typography>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default KeywordResearchTab;
|
||||
@@ -0,0 +1,60 @@
|
||||
import React from 'react';
|
||||
import {
|
||||
Box,
|
||||
Grid,
|
||||
Card,
|
||||
CardContent,
|
||||
Typography
|
||||
} from '@mui/material';
|
||||
import {
|
||||
BarChart as BarChartIcon
|
||||
} from '@mui/icons-material';
|
||||
import { useContentPlanningStore } from '../../../stores/contentPlanningStore';
|
||||
|
||||
const PerformanceAnalyticsTab: React.FC = () => {
|
||||
const { performanceMetrics } = useContentPlanningStore();
|
||||
|
||||
return (
|
||||
<Box sx={{ p: 3 }}>
|
||||
<Typography variant="h4" gutterBottom>
|
||||
Performance Analytics
|
||||
</Typography>
|
||||
|
||||
{performanceMetrics ? (
|
||||
<Grid container spacing={3}>
|
||||
<Grid item xs={12} md={6}>
|
||||
<Card>
|
||||
<CardContent>
|
||||
<Typography variant="h6" gutterBottom>
|
||||
Content Performance by Type
|
||||
</Typography>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
No content performance data available
|
||||
</Typography>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Grid>
|
||||
|
||||
<Grid item xs={12} md={6}>
|
||||
<Card>
|
||||
<CardContent>
|
||||
<Typography variant="h6" gutterBottom>
|
||||
Growth Trends
|
||||
</Typography>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
No trend data available
|
||||
</Typography>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Grid>
|
||||
</Grid>
|
||||
) : (
|
||||
<Typography variant="body2" color="text.secondary" sx={{ textAlign: 'center', p: 3 }}>
|
||||
No performance analytics data available
|
||||
</Typography>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default PerformanceAnalyticsTab;
|
||||
@@ -0,0 +1,406 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import {
|
||||
Box,
|
||||
Grid,
|
||||
Paper,
|
||||
Typography,
|
||||
Button,
|
||||
TextField,
|
||||
Card,
|
||||
CardContent,
|
||||
Chip,
|
||||
Divider,
|
||||
Alert,
|
||||
CircularProgress,
|
||||
List,
|
||||
ListItem,
|
||||
ListItemText,
|
||||
ListItemIcon
|
||||
} from '@mui/material';
|
||||
import {
|
||||
Search as SearchIcon,
|
||||
Add as AddIcon,
|
||||
Warning as WarningIcon,
|
||||
CheckCircle as CheckCircleIcon,
|
||||
TrendingUp as TrendingUpIcon,
|
||||
Assessment as AssessmentIcon
|
||||
} from '@mui/icons-material';
|
||||
import { useContentPlanningStore } from '../../../stores/contentPlanningStore';
|
||||
import { contentPlanningApi } from '../../../services/contentPlanningApi';
|
||||
|
||||
const RefineAnalysisTab: React.FC = () => {
|
||||
const {
|
||||
gapAnalyses,
|
||||
loading,
|
||||
error,
|
||||
loadGapAnalyses,
|
||||
analyzeContentGaps,
|
||||
updateGapAnalyses
|
||||
} = useContentPlanningStore();
|
||||
|
||||
const [analysisForm, setAnalysisForm] = useState({
|
||||
website_url: '',
|
||||
competitors: [] as string[],
|
||||
keywords: [] as string[]
|
||||
});
|
||||
const [newCompetitor, setNewCompetitor] = useState('');
|
||||
const [newKeyword, setNewKeyword] = useState('');
|
||||
const [dataLoading, setDataLoading] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
loadGapAnalysisData();
|
||||
}, []);
|
||||
|
||||
const loadGapAnalysisData = async () => {
|
||||
try {
|
||||
setDataLoading(true);
|
||||
const response = await contentPlanningApi.getGapAnalysesSafe();
|
||||
|
||||
console.log('Gap Analysis Response:', response);
|
||||
|
||||
// Transform the backend response to match frontend expectations
|
||||
if (response && response.gap_analyses) {
|
||||
const transformedAnalyses = response.gap_analyses.map((analysis: any, index: number) => ({
|
||||
id: analysis.id || `analysis_${index}`,
|
||||
website_url: analysis.website_url || 'example.com',
|
||||
competitors: analysis.competitors || [],
|
||||
keywords: analysis.keywords || [],
|
||||
gaps: analysis.gaps || [],
|
||||
recommendations: analysis.recommendations || [],
|
||||
created_at: analysis.created_at || new Date().toISOString()
|
||||
}));
|
||||
|
||||
console.log('Transformed Analyses:', transformedAnalyses);
|
||||
|
||||
// Update the store with transformed data
|
||||
updateGapAnalyses(transformedAnalyses);
|
||||
} else {
|
||||
console.log('No gap analyses found in response');
|
||||
updateGapAnalyses([]);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading gap analysis data:', error);
|
||||
updateGapAnalyses([]);
|
||||
} finally {
|
||||
setDataLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleAddCompetitor = () => {
|
||||
if (newCompetitor.trim() && !analysisForm.competitors.includes(newCompetitor.trim())) {
|
||||
setAnalysisForm(prev => ({
|
||||
...prev,
|
||||
competitors: [...prev.competitors, newCompetitor.trim()]
|
||||
}));
|
||||
setNewCompetitor('');
|
||||
}
|
||||
};
|
||||
|
||||
const handleRemoveCompetitor = (competitorToRemove: string) => {
|
||||
setAnalysisForm(prev => ({
|
||||
...prev,
|
||||
competitors: prev.competitors.filter(comp => comp !== competitorToRemove)
|
||||
}));
|
||||
};
|
||||
|
||||
const handleAddKeyword = () => {
|
||||
if (newKeyword.trim() && !analysisForm.keywords.includes(newKeyword.trim())) {
|
||||
setAnalysisForm(prev => ({
|
||||
...prev,
|
||||
keywords: [...prev.keywords, newKeyword.trim()]
|
||||
}));
|
||||
setNewKeyword('');
|
||||
}
|
||||
};
|
||||
|
||||
const handleRemoveKeyword = (keywordToRemove: string) => {
|
||||
setAnalysisForm(prev => ({
|
||||
...prev,
|
||||
keywords: prev.keywords.filter(keyword => keyword !== keywordToRemove)
|
||||
}));
|
||||
};
|
||||
|
||||
const handleRunAnalysis = async () => {
|
||||
if (!analysisForm.website_url) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
setDataLoading(true);
|
||||
|
||||
await analyzeContentGaps({
|
||||
website_url: analysisForm.website_url,
|
||||
competitors: analysisForm.competitors,
|
||||
keywords: analysisForm.keywords
|
||||
});
|
||||
|
||||
// Reload data after analysis
|
||||
await loadGapAnalyses();
|
||||
|
||||
// Reset form
|
||||
setAnalysisForm({
|
||||
website_url: '',
|
||||
competitors: [],
|
||||
keywords: []
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error running gap analysis:', error);
|
||||
} finally {
|
||||
setDataLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// Ensure gapAnalyses is always an array and transform the data structure
|
||||
const safeGapAnalyses = Array.isArray(gapAnalyses) ? gapAnalyses : [];
|
||||
|
||||
// Transform backend data structure to frontend expected structure
|
||||
const transformedGapAnalyses = safeGapAnalyses.map((analysis, index) => {
|
||||
// Handle the actual backend structure: { recommendations: [...] }
|
||||
const recommendations = analysis.recommendations || [];
|
||||
|
||||
return {
|
||||
id: analysis.id || `analysis-${index}`,
|
||||
website_url: analysis.website_url || 'Unknown Website',
|
||||
competitors: analysis.competitors || [],
|
||||
keywords: analysis.keywords || [],
|
||||
recommendations: recommendations,
|
||||
created_at: analysis.created_at || new Date().toISOString(),
|
||||
// Extract gaps from recommendations if available
|
||||
gaps: recommendations.length > 0 ?
|
||||
recommendations.filter((rec: any) => rec.type === 'gap').map((rec: any) => rec.title || rec.description || 'Content gap identified') :
|
||||
[]
|
||||
};
|
||||
});
|
||||
|
||||
return (
|
||||
<Box sx={{ p: 3 }}>
|
||||
<Typography variant="h4" gutterBottom>
|
||||
Refine Analysis
|
||||
</Typography>
|
||||
|
||||
{error && (
|
||||
<Alert severity="error" sx={{ mb: 2 }}>
|
||||
{error}
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
<Grid container spacing={3}>
|
||||
{/* Analysis Setup */}
|
||||
<Grid item xs={12} md={4}>
|
||||
<Paper sx={{ p: 3, mb: 3 }}>
|
||||
<Typography variant="h6" gutterBottom>
|
||||
<SearchIcon sx={{ mr: 1, verticalAlign: 'middle' }} />
|
||||
Analysis Setup
|
||||
</Typography>
|
||||
<Divider sx={{ mb: 2 }} />
|
||||
|
||||
<TextField
|
||||
fullWidth
|
||||
label="Website URL"
|
||||
value={analysisForm.website_url}
|
||||
onChange={(e) => setAnalysisForm(prev => ({ ...prev, website_url: e.target.value }))}
|
||||
placeholder="https://example.com"
|
||||
sx={{ mb: 2 }}
|
||||
/>
|
||||
|
||||
<Typography variant="subtitle2" gutterBottom>
|
||||
Competitors
|
||||
</Typography>
|
||||
<Box sx={{ display: 'flex', gap: 1, mb: 2 }}>
|
||||
<TextField
|
||||
fullWidth
|
||||
label="Add Competitor"
|
||||
value={newCompetitor}
|
||||
onChange={(e) => setNewCompetitor(e.target.value)}
|
||||
placeholder="competitor.com"
|
||||
onKeyPress={(e) => e.key === 'Enter' && handleAddCompetitor()}
|
||||
/>
|
||||
<Button
|
||||
variant="outlined"
|
||||
onClick={handleAddCompetitor}
|
||||
disabled={!newCompetitor.trim()}
|
||||
>
|
||||
<AddIcon />
|
||||
</Button>
|
||||
</Box>
|
||||
|
||||
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 1, mb: 3 }}>
|
||||
{analysisForm.competitors.map((competitor, index) => (
|
||||
<Chip
|
||||
key={index}
|
||||
label={competitor}
|
||||
onDelete={() => handleRemoveCompetitor(competitor)}
|
||||
color="primary"
|
||||
variant="outlined"
|
||||
/>
|
||||
))}
|
||||
</Box>
|
||||
|
||||
<Typography variant="subtitle2" gutterBottom>
|
||||
Keywords
|
||||
</Typography>
|
||||
<Box sx={{ display: 'flex', gap: 1, mb: 2 }}>
|
||||
<TextField
|
||||
fullWidth
|
||||
label="Add Keyword"
|
||||
value={newKeyword}
|
||||
onChange={(e) => setNewKeyword(e.target.value)}
|
||||
placeholder="target keyword"
|
||||
onKeyPress={(e) => e.key === 'Enter' && handleAddKeyword()}
|
||||
/>
|
||||
<Button
|
||||
variant="outlined"
|
||||
onClick={handleAddKeyword}
|
||||
disabled={!newKeyword.trim()}
|
||||
>
|
||||
<AddIcon />
|
||||
</Button>
|
||||
</Box>
|
||||
|
||||
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 1, mb: 3 }}>
|
||||
{analysisForm.keywords.map((keyword, index) => (
|
||||
<Chip
|
||||
key={index}
|
||||
label={keyword}
|
||||
onDelete={() => handleRemoveKeyword(keyword)}
|
||||
color="secondary"
|
||||
variant="outlined"
|
||||
/>
|
||||
))}
|
||||
</Box>
|
||||
|
||||
<Button
|
||||
variant="contained"
|
||||
fullWidth
|
||||
onClick={handleRunAnalysis}
|
||||
disabled={loading || dataLoading || !analysisForm.website_url}
|
||||
startIcon={<AssessmentIcon />}
|
||||
>
|
||||
{loading || dataLoading ? 'Running Analysis...' : 'Run Gap Analysis'}
|
||||
</Button>
|
||||
</Paper>
|
||||
</Grid>
|
||||
|
||||
{/* Content Gaps */}
|
||||
<Grid item xs={12} md={8}>
|
||||
<Paper sx={{ p: 3, mb: 3 }}>
|
||||
<Typography variant="h6" gutterBottom>
|
||||
<WarningIcon sx={{ mr: 1, verticalAlign: 'middle' }} />
|
||||
Content Gaps
|
||||
</Typography>
|
||||
<Divider sx={{ mb: 2 }} />
|
||||
|
||||
{dataLoading ? (
|
||||
<Box sx={{ display: 'flex', justifyContent: 'center', p: 3 }}>
|
||||
<CircularProgress />
|
||||
</Box>
|
||||
) : transformedGapAnalyses.length === 0 ? (
|
||||
<Typography variant="body2" color="text.secondary" sx={{ textAlign: 'center', py: 2 }}>
|
||||
No previous analyses found. Run your first analysis to see results here.
|
||||
</Typography>
|
||||
) : (
|
||||
<Grid container spacing={2}>
|
||||
{transformedGapAnalyses.map((analysis) => (
|
||||
<Grid item xs={12} md={6} lg={4} key={analysis.id}>
|
||||
<Card>
|
||||
<CardContent>
|
||||
<Typography variant="h6" component="div">
|
||||
{analysis.website_url}
|
||||
</Typography>
|
||||
<Typography variant="body2" color="text.secondary" sx={{ mb: 2 }}>
|
||||
{new Date(analysis.created_at).toLocaleDateString()}
|
||||
</Typography>
|
||||
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 1 }}>
|
||||
<Chip
|
||||
label={`${analysis.competitors?.length || 0} competitors`}
|
||||
size="small"
|
||||
variant="outlined"
|
||||
/>
|
||||
<Chip
|
||||
label={`${analysis.keywords?.length || 0} keywords`}
|
||||
size="small"
|
||||
variant="outlined"
|
||||
/>
|
||||
<Chip
|
||||
label={`${analysis.gaps?.length || 0} gaps found`}
|
||||
size="small"
|
||||
color="warning"
|
||||
/>
|
||||
<Chip
|
||||
label={`${analysis.recommendations?.length || 0} recommendations`}
|
||||
size="small"
|
||||
color="success"
|
||||
/>
|
||||
</Box>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Grid>
|
||||
))}
|
||||
</Grid>
|
||||
)}
|
||||
</Paper>
|
||||
|
||||
{/* Detailed Analysis Results */}
|
||||
{transformedGapAnalyses.length > 0 && (
|
||||
<Paper sx={{ p: 3 }}>
|
||||
<Typography variant="h6" gutterBottom>
|
||||
<TrendingUpIcon sx={{ mr: 1, verticalAlign: 'middle' }} />
|
||||
Detailed Analysis Results
|
||||
</Typography>
|
||||
<Divider sx={{ mb: 2 }} />
|
||||
|
||||
{transformedGapAnalyses.map((analysis, index) => (
|
||||
<Box key={index} sx={{ mb: 3 }}>
|
||||
<Typography variant="subtitle1" gutterBottom>
|
||||
Analysis for {analysis.website_url}
|
||||
</Typography>
|
||||
|
||||
{analysis.gaps && analysis.gaps.length > 0 && (
|
||||
<Box sx={{ mb: 2 }}>
|
||||
<Typography variant="subtitle2" gutterBottom>
|
||||
Identified Content Gaps:
|
||||
</Typography>
|
||||
<List dense>
|
||||
{analysis.gaps.map((gap, gapIndex) => (
|
||||
<ListItem key={gapIndex}>
|
||||
<ListItemIcon>
|
||||
<WarningIcon color="warning" />
|
||||
</ListItemIcon>
|
||||
<ListItemText primary={gap} />
|
||||
</ListItem>
|
||||
))}
|
||||
</List>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{analysis.recommendations && analysis.recommendations.length > 0 && (
|
||||
<Box>
|
||||
<Typography variant="subtitle2" gutterBottom>
|
||||
Recommendations:
|
||||
</Typography>
|
||||
<List dense>
|
||||
{analysis.recommendations.map((rec, recIndex) => (
|
||||
<ListItem key={recIndex}>
|
||||
<ListItemIcon>
|
||||
<CheckCircleIcon color="success" />
|
||||
</ListItemIcon>
|
||||
<ListItemText
|
||||
primary={rec.title || rec.description || 'Recommendation'}
|
||||
secondary={rec.description}
|
||||
/>
|
||||
</ListItem>
|
||||
))}
|
||||
</List>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
))}
|
||||
</Paper>
|
||||
)}
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default RefineAnalysisTab;
|
||||
@@ -0,0 +1,65 @@
|
||||
import React from 'react';
|
||||
import {
|
||||
Box,
|
||||
Grid,
|
||||
Paper,
|
||||
Typography,
|
||||
Chip
|
||||
} from '@mui/material';
|
||||
import {
|
||||
TrendingUp as TrendingIcon
|
||||
} from '@mui/icons-material';
|
||||
import { useContentPlanningStore } from '../../../stores/contentPlanningStore';
|
||||
|
||||
const TrendingTopicsTab: React.FC = () => {
|
||||
const { trendingTopics } = useContentPlanningStore();
|
||||
|
||||
return (
|
||||
<Box sx={{ p: 3 }}>
|
||||
<Typography variant="h4" gutterBottom>
|
||||
Trending Topics
|
||||
</Typography>
|
||||
|
||||
<Grid container spacing={3}>
|
||||
<Grid item xs={12}>
|
||||
<Paper sx={{ p: 3 }}>
|
||||
<Typography variant="h6" gutterBottom>
|
||||
<TrendingIcon sx={{ mr: 1, verticalAlign: 'middle' }} />
|
||||
Trending Topics
|
||||
</Typography>
|
||||
|
||||
{trendingTopics ? (
|
||||
<Box>
|
||||
<Typography variant="body1" gutterBottom>
|
||||
Current Trending Topics
|
||||
</Typography>
|
||||
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 1 }}>
|
||||
{trendingTopics.trending_topics?.map((topic: any, index: number) => (
|
||||
<Chip
|
||||
key={index}
|
||||
label={topic.name || topic.keyword}
|
||||
color="primary"
|
||||
variant="outlined"
|
||||
/>
|
||||
))}
|
||||
</Box>
|
||||
</Box>
|
||||
) : (
|
||||
<Box sx={{ textAlign: 'center', py: 4 }}>
|
||||
<TrendingIcon sx={{ fontSize: 64, color: 'text.secondary', mb: 2 }} />
|
||||
<Typography variant="h6" color="text.secondary" gutterBottom>
|
||||
No trending topics
|
||||
</Typography>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
Get trending topics for your industry
|
||||
</Typography>
|
||||
</Box>
|
||||
)}
|
||||
</Paper>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default TrendingTopicsTab;
|
||||
Reference in New Issue
Block a user