Fixes to Generate Pillar Chips
This commit is contained in:
@@ -22,6 +22,7 @@ import LoadingSkeleton from '../shared/LoadingSkeleton';
|
||||
import ErrorDisplay from '../shared/ErrorDisplay';
|
||||
import EmptyState from '../shared/EmptyState';
|
||||
import ContentLifecyclePillars from './ContentLifecyclePillars';
|
||||
import AnalyticsInsights from './components/AnalyticsInsights';
|
||||
|
||||
// Shared types and utilities
|
||||
import { Tool } from '../shared/types';
|
||||
@@ -244,6 +245,9 @@ const MainDashboard: React.FC = () => {
|
||||
{/* Content Lifecycle Pillars - First Panel */}
|
||||
<ContentLifecyclePillars />
|
||||
|
||||
{/* Analytics Insights - Good/Bad/Ugly */}
|
||||
<AnalyticsInsights />
|
||||
|
||||
{/* Search and Filter */}
|
||||
<SearchFilter
|
||||
searchQuery={searchQuery}
|
||||
@@ -267,8 +271,8 @@ const MainDashboard: React.FC = () => {
|
||||
transition={{ duration: 0.5, delay: categoryIndex * 0.1 }}
|
||||
>
|
||||
<Box sx={{ mb: 5 }}>
|
||||
{/* Only show Category Header when no specific category is selected (showing all tools) */}
|
||||
{selectedCategory === null && (
|
||||
{/* Show Category Header when no specific category is selected OR when searching across all categories */}
|
||||
{(selectedCategory === null || searchQuery) && (
|
||||
<CategoryHeader
|
||||
categoryName={categoryName}
|
||||
category={category}
|
||||
|
||||
@@ -0,0 +1,433 @@
|
||||
import React from 'react';
|
||||
import {
|
||||
Box,
|
||||
Card,
|
||||
CardContent,
|
||||
Typography,
|
||||
Button,
|
||||
Tooltip,
|
||||
Modal,
|
||||
IconButton,
|
||||
Chip,
|
||||
Stack,
|
||||
Divider
|
||||
} from '@mui/material';
|
||||
import { styled } from '@mui/material/styles';
|
||||
import { keyframes } from '@mui/system';
|
||||
import {
|
||||
CheckCircle as CheckIcon,
|
||||
WarningAmber as WarningIcon,
|
||||
Error as ErrorIcon,
|
||||
Close as CloseIcon,
|
||||
TrendingUp as TrendingUpIcon,
|
||||
TrendingDown as TrendingDownIcon,
|
||||
Info as InfoIcon
|
||||
} from '@mui/icons-material';
|
||||
import { motion } from 'framer-motion';
|
||||
|
||||
interface Insight {
|
||||
id: string;
|
||||
title: string;
|
||||
description: string;
|
||||
metric: string;
|
||||
value: string;
|
||||
trend: 'up' | 'down' | 'stable';
|
||||
priority: 'low' | 'medium' | 'high' | 'critical';
|
||||
category: 'engagement' | 'reach' | 'conversion' | 'seo' | 'content';
|
||||
platform: 'facebook' | 'linkedin' | 'twitter' | 'instagram' | 'website';
|
||||
detailedAnalysis: string;
|
||||
recommendations: string[];
|
||||
impact: string;
|
||||
timeframe: string;
|
||||
}
|
||||
|
||||
interface AnalyticsData {
|
||||
theGood: Insight[];
|
||||
theBad: Insight[];
|
||||
theUgly: Insight[];
|
||||
}
|
||||
|
||||
interface AnalyticsInsightsProps {
|
||||
data?: AnalyticsData; // optional - falls back to mock
|
||||
onActionClick?: (action: 'alwrity' | 'ignore', insight: Insight) => void;
|
||||
}
|
||||
|
||||
const ColumnCard = styled(Card)(({ theme }) => ({
|
||||
background: 'linear-gradient(180deg, rgba(255,255,255,0.14) 0%, rgba(255,255,255,0.08) 100%)',
|
||||
border: '1px solid rgba(255,255,255,0.16)',
|
||||
backdropFilter: 'blur(18px)',
|
||||
WebkitBackdropFilter: 'blur(18px)',
|
||||
borderRadius: theme.spacing(2),
|
||||
overflow: 'hidden',
|
||||
boxShadow: '0 8px 20px rgba(0,0,0,0.28), inset 0 1px 0 rgba(255,255,255,0.22)',
|
||||
transition: 'transform 0.3s ease, box-shadow 0.3s ease',
|
||||
'&:hover': {
|
||||
transform: 'translateY(-3px)',
|
||||
boxShadow: '0 12px 28px rgba(0,0,0,0.35), inset 0 1px 0 rgba(255,255,255,0.28)'
|
||||
}
|
||||
}));
|
||||
|
||||
const Pill = styled('div')<{ color: string }>(() => ({
|
||||
width: 10,
|
||||
height: 10,
|
||||
borderRadius: 6,
|
||||
}));
|
||||
|
||||
const GradientHeader = styled(Box)<{ gradient: string }>(({ gradient }) => ({
|
||||
background: gradient,
|
||||
padding: '8px 12px',
|
||||
color: 'white',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 6,
|
||||
}));
|
||||
|
||||
const Badge = styled('span')(({ theme }) => ({
|
||||
background: 'rgba(255,255,255,0.15)',
|
||||
border: '1px solid rgba(255,255,255,0.35)',
|
||||
color: 'white',
|
||||
borderRadius: 999,
|
||||
padding: '1px 6px',
|
||||
fontWeight: 700,
|
||||
fontSize: '0.65rem'
|
||||
}));
|
||||
|
||||
// Subtle shimmer animation for the title text
|
||||
const shimmerText = keyframes`
|
||||
0% { background-position: -200% 0; }
|
||||
100% { background-position: 200% 0; }
|
||||
`;
|
||||
|
||||
const mockData: AnalyticsData = {
|
||||
theGood: [
|
||||
{
|
||||
id: 'good-1',
|
||||
title: 'LinkedIn Engagement Surge',
|
||||
description: 'LinkedIn engagement is up significantly this week.',
|
||||
metric: 'Engagement Rate',
|
||||
value: '+45%',
|
||||
trend: 'up',
|
||||
priority: 'high',
|
||||
category: 'engagement',
|
||||
platform: 'linkedin',
|
||||
detailedAnalysis: 'Recent posts on AI topics resonated strongly with your B2B audience.',
|
||||
recommendations: ['Post 3x/week on AI trends', 'Engage with comments within 2 hours'],
|
||||
impact: 'High lead-gen potential',
|
||||
timeframe: 'Last 7 days'
|
||||
},
|
||||
{
|
||||
id: 'good-2',
|
||||
title: 'Website Traffic Growth',
|
||||
description: 'Organic traffic increased due to improved SEO.',
|
||||
metric: 'Organic Traffic',
|
||||
value: '+23%',
|
||||
trend: 'up',
|
||||
priority: 'medium',
|
||||
category: 'seo',
|
||||
platform: 'website',
|
||||
detailedAnalysis: 'Technical fixes and content refresh improved rankings.',
|
||||
recommendations: ['Create 2 pillar pages', 'Refresh 5 top posts'],
|
||||
impact: 'Improved visibility',
|
||||
timeframe: 'Last 30 days'
|
||||
},
|
||||
{
|
||||
id: 'good-3',
|
||||
title: 'Top-Performing Post',
|
||||
description: 'A recent LinkedIn post outperformed baseline by 2.1x',
|
||||
metric: 'Engagement Index',
|
||||
value: '2.1x',
|
||||
trend: 'up',
|
||||
priority: 'medium',
|
||||
category: 'engagement',
|
||||
platform: 'linkedin',
|
||||
detailedAnalysis: 'Carousel format and thought leadership angle worked well.',
|
||||
recommendations: ['Use carousel weekly', 'Add CTA to subscribe'],
|
||||
impact: 'Audience growth',
|
||||
timeframe: 'This week'
|
||||
}
|
||||
],
|
||||
theBad: [
|
||||
{
|
||||
id: 'bad-1',
|
||||
title: 'Facebook Reach Decline',
|
||||
description: 'Facebook post reach dropped this month.',
|
||||
metric: 'Reach',
|
||||
value: '-18%',
|
||||
trend: 'down',
|
||||
priority: 'medium',
|
||||
category: 'reach',
|
||||
platform: 'facebook',
|
||||
detailedAnalysis: 'Algorithm change likely impacting page distribution.',
|
||||
recommendations: ['Test short video posts', 'Boost first-hour engagement'],
|
||||
impact: 'Lower awareness',
|
||||
timeframe: 'Last 30 days'
|
||||
},
|
||||
{
|
||||
id: 'bad-2',
|
||||
title: 'Email CTR Stagnant',
|
||||
description: 'Content CTR plateaued across campaigns.',
|
||||
metric: 'CTR',
|
||||
value: '0.9%',
|
||||
trend: 'stable',
|
||||
priority: 'low',
|
||||
category: 'content',
|
||||
platform: 'website',
|
||||
detailedAnalysis: 'Subject lines lack urgency; preview text uninspiring.',
|
||||
recommendations: ['A/B test subject lines', 'Add curiosity hook'],
|
||||
impact: 'Reduced visits',
|
||||
timeframe: 'Last 14 days'
|
||||
}
|
||||
],
|
||||
theUgly: [
|
||||
{
|
||||
id: 'ugly-1',
|
||||
title: 'Critical SEO Issues',
|
||||
description: '15 pages have broken internal links.',
|
||||
metric: 'Broken Links',
|
||||
value: '15 pages',
|
||||
trend: 'down',
|
||||
priority: 'critical',
|
||||
category: 'seo',
|
||||
platform: 'website',
|
||||
detailedAnalysis: 'Broken links hurt crawlability and user experience.',
|
||||
recommendations: ['Fix links immediately', 'Add automated link checks'],
|
||||
impact: 'Severe ranking risk',
|
||||
timeframe: 'Ongoing'
|
||||
},
|
||||
{
|
||||
id: 'ugly-2',
|
||||
title: 'Declining Conversions',
|
||||
description: 'Checkout conversion dropped vs prior month.',
|
||||
metric: 'CVR',
|
||||
value: '-12%',
|
||||
trend: 'down',
|
||||
priority: 'high',
|
||||
category: 'conversion',
|
||||
platform: 'website',
|
||||
detailedAnalysis: 'Funnel analysis shows friction on payment step.',
|
||||
recommendations: ['Simplify checkout', 'Add alternate payment'],
|
||||
impact: 'Direct revenue impact',
|
||||
timeframe: 'Last 30 days'
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
const getGradient = (type: 'good' | 'bad' | 'ugly') => {
|
||||
switch (type) {
|
||||
case 'good':
|
||||
return 'linear-gradient(135deg, rgba(76,175,80,0.55) 0%, rgba(139,195,74,0.55) 100%)';
|
||||
case 'bad':
|
||||
return 'linear-gradient(135deg, rgba(255,152,0,0.55) 0%, rgba(245,124,0,0.55) 100%)';
|
||||
default:
|
||||
return 'linear-gradient(135deg, rgba(244,67,54,0.55) 0%, rgba(233,30,99,0.55) 100%)';
|
||||
}
|
||||
};
|
||||
|
||||
const getIcon = (type: 'good' | 'bad' | 'ugly') => {
|
||||
switch (type) {
|
||||
case 'good':
|
||||
return <CheckIcon />;
|
||||
case 'bad':
|
||||
return <WarningIcon />;
|
||||
default:
|
||||
return <ErrorIcon />;
|
||||
}
|
||||
};
|
||||
|
||||
const TrendChip: React.FC<{ trend: Insight['trend'] }> = ({ trend }) => {
|
||||
if (trend === 'up') return <Chip size="small" icon={<TrendingUpIcon />} label="Up" sx={{ color: '#4CAF50', background: '#4CAF5022', border: '1px solid #4CAF5044', fontWeight: 700, fontSize: '0.6rem', height: 18 }} />;
|
||||
if (trend === 'down') return <Chip size="small" icon={<TrendingDownIcon />} label="Down" sx={{ color: '#F44336', background: '#F4433622', border: '1px solid #F4433644', fontWeight: 700, fontSize: '0.6rem', height: 18 }} />;
|
||||
return <Chip size="small" icon={<InfoIcon />} label="Stable" sx={{ color: '#90CAF9', background: '#90CAF922', border: '1px solid #90CAF944', fontWeight: 700, fontSize: '0.6rem', height: 18 }} />;
|
||||
};
|
||||
|
||||
const AnalyticsInsights: React.FC<AnalyticsInsightsProps> = ({ data, onActionClick }) => {
|
||||
const [hovered, setHovered] = React.useState<'good' | 'bad' | 'ugly' | null>(null);
|
||||
const [open, setOpen] = React.useState(false);
|
||||
const [selected, setSelected] = React.useState<Insight | null>(null);
|
||||
|
||||
const insights = data || mockData;
|
||||
|
||||
const columns: Array<{ key: 'good' | 'bad' | 'ugly'; title: string; items: Insight[] }> = [
|
||||
{ key: 'good', title: 'The Good', items: insights.theGood },
|
||||
{ key: 'bad', title: 'The Bad', items: insights.theBad },
|
||||
{ key: 'ugly', title: 'The Ugly', items: insights.theUgly },
|
||||
];
|
||||
|
||||
const handleKnowMore = (insight: Insight) => {
|
||||
setSelected(insight);
|
||||
setOpen(true);
|
||||
};
|
||||
|
||||
const handleClose = () => setOpen(false);
|
||||
|
||||
const handleAction = (action: 'alwrity' | 'ignore') => {
|
||||
if (selected && onActionClick) {
|
||||
onActionClick(action, selected);
|
||||
}
|
||||
setOpen(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<Box sx={{ mt: 2, mb: 2.5 }}>
|
||||
<Typography
|
||||
variant="h6"
|
||||
sx={{
|
||||
fontWeight: 800,
|
||||
mb: 1.5,
|
||||
fontSize: '1.1rem',
|
||||
background: 'linear-gradient(90deg, rgba(255,255,255,0.35), rgba(255,255,255,0.9) 50%, rgba(255,255,255,0.35))',
|
||||
WebkitBackgroundClip: 'text',
|
||||
backgroundClip: 'text',
|
||||
color: 'transparent',
|
||||
backgroundSize: '200% 100%',
|
||||
animation: `${shimmerText} 3.2s linear infinite`,
|
||||
}}
|
||||
>
|
||||
Analytics Insights
|
||||
</Typography>
|
||||
<Stack direction={{ xs: 'column', md: 'row' }} spacing={1.5}>
|
||||
{columns.map((col) => {
|
||||
const isHovered = hovered === col.key;
|
||||
const visibleItems = isHovered ? col.items : col.items.slice(0, 1);
|
||||
const gradient = getGradient(col.key);
|
||||
return (
|
||||
<motion.div key={col.key} style={{ flex: 1 }} onMouseEnter={() => setHovered(col.key)} onMouseLeave={() => setHovered(null)}>
|
||||
<ColumnCard>
|
||||
<GradientHeader gradient={gradient}>
|
||||
{getIcon(col.key)}
|
||||
<Typography variant="subtitle1" sx={{ fontWeight: 800, fontSize: '0.9rem' }}>{col.title}</Typography>
|
||||
<Badge>{col.items.length}</Badge>
|
||||
</GradientHeader>
|
||||
|
||||
<CardContent sx={{ p: 1.5 }}>
|
||||
<Stack spacing={1}>
|
||||
{visibleItems.map((insight) => (
|
||||
<Box key={insight.id} sx={{
|
||||
background: 'rgba(255,255,255,0.08)',
|
||||
border: '1px solid rgba(255,255,255,0.18)',
|
||||
borderRadius: 1.5,
|
||||
p: 1
|
||||
}}>
|
||||
<Stack direction="row" spacing={0.5} alignItems="center" sx={{ mb: 0.25 }}>
|
||||
<Typography variant="body2" sx={{ color: 'rgba(255,255,255,0.95)', fontWeight: 700, fontSize: '0.8rem' }}>
|
||||
{insight.title}
|
||||
</Typography>
|
||||
<TrendChip trend={insight.trend} />
|
||||
</Stack>
|
||||
<Typography variant="caption" sx={{ color: 'rgba(255,255,255,0.8)', fontSize: '0.7rem', lineHeight: 1.2 }}>
|
||||
{insight.description}
|
||||
</Typography>
|
||||
<Stack direction="row" spacing={0.5} sx={{ mt: 0.5 }}>
|
||||
<Chip size="small" label={`${insight.metric}: ${insight.value}`} sx={{ color: 'rgba(255,255,255,0.95)', background: 'rgba(255,255,255,0.12)', border: '1px solid rgba(255,255,255,0.24)', fontWeight: 700, fontSize: '0.65rem', height: 20 }} />
|
||||
<Chip size="small" label={insight.platform} sx={{ color: 'rgba(255,255,255,0.85)', background: 'rgba(255,255,255,0.08)', fontSize: '0.65rem', height: 20 }} />
|
||||
</Stack>
|
||||
</Box>
|
||||
))}
|
||||
</Stack>
|
||||
|
||||
{isHovered && (
|
||||
<Box sx={{ mt: 1, display: 'flex', justifyContent: 'flex-end' }}>
|
||||
<Tooltip title={`Open detailed insights for ${col.title.toLowerCase()}.`}>
|
||||
<span>
|
||||
<Button
|
||||
variant="contained"
|
||||
onClick={() => handleKnowMore(col.items[0])}
|
||||
size="small"
|
||||
sx={{
|
||||
textTransform: 'none',
|
||||
fontWeight: 800,
|
||||
background: gradient,
|
||||
boxShadow: '0 4px 12px rgba(0,0,0,0.3)',
|
||||
fontSize: '0.75rem',
|
||||
px: 2,
|
||||
py: 0.5
|
||||
}}
|
||||
>
|
||||
Know More
|
||||
</Button>
|
||||
</span>
|
||||
</Tooltip>
|
||||
</Box>
|
||||
)}
|
||||
</CardContent>
|
||||
</ColumnCard>
|
||||
</motion.div>
|
||||
);
|
||||
})}
|
||||
</Stack>
|
||||
|
||||
<Modal open={open} onClose={handleClose}>
|
||||
<Box sx={{
|
||||
position: 'absolute',
|
||||
top: '50%',
|
||||
left: '50%',
|
||||
transform: 'translate(-50%, -50%)',
|
||||
width: { xs: '92%', md: 900 },
|
||||
maxHeight: '80vh',
|
||||
overflowY: 'auto',
|
||||
background: 'linear-gradient(180deg, rgba(16,24,39,0.92) 0%, rgba(26,33,56,0.92) 100%)',
|
||||
border: '1px solid rgba(255,255,255,0.18)',
|
||||
borderRadius: 3,
|
||||
boxShadow: '0 26px 80px rgba(0,0,0,0.5)'
|
||||
}}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', p: 2.5, borderBottom: '1px solid rgba(255,255,255,0.15)' }}>
|
||||
<Typography variant="h6" sx={{ color: 'rgba(255,255,255,0.95)', fontWeight: 800 }}>
|
||||
{selected?.title}
|
||||
</Typography>
|
||||
<IconButton onClick={handleClose} sx={{ color: 'rgba(255,255,255,0.85)' }}>
|
||||
<CloseIcon />
|
||||
</IconButton>
|
||||
</Box>
|
||||
|
||||
<CardContent>
|
||||
<Stack spacing={1.5}>
|
||||
<Typography variant="body1" sx={{ color: 'rgba(255,255,255,0.9)' }}>
|
||||
{selected?.detailedAnalysis}
|
||||
</Typography>
|
||||
<Stack direction="row" spacing={1}>
|
||||
<Chip size="small" label={`${selected?.metric}: ${selected?.value}`} sx={{ color: 'rgba(255,255,255,0.95)', background: 'rgba(255,255,255,0.12)', border: '1px solid rgba(255,255,255,0.24)', fontWeight: 700 }} />
|
||||
{selected?.platform && (
|
||||
<Chip size="small" label={selected.platform} sx={{ color: 'rgba(255,255,255,0.85)', background: 'rgba(255,255,255,0.08)' }} />
|
||||
)}
|
||||
{selected?.impact && (
|
||||
<Chip size="small" label={`Impact: ${selected.impact}`} sx={{ color: 'rgba(255,255,255,0.85)', background: 'rgba(255,255,255,0.08)' }} />
|
||||
)}
|
||||
</Stack>
|
||||
|
||||
<Divider sx={{ my: 1.5, borderColor: 'rgba(255,255,255,0.15)' }} />
|
||||
<Typography variant="subtitle2" sx={{ color: 'rgba(255,255,255,0.9)', fontWeight: 800 }}>
|
||||
Recommendations
|
||||
</Typography>
|
||||
<Stack spacing={0.75}>
|
||||
{selected?.recommendations.map((rec, idx) => (
|
||||
<Typography key={idx} variant="body2" sx={{ color: 'rgba(255,255,255,0.8)' }}>• {rec}</Typography>
|
||||
))}
|
||||
</Stack>
|
||||
|
||||
<Box sx={{ display: 'flex', justifyContent: 'flex-end', gap: 1.5, mt: 2 }}>
|
||||
<Tooltip title="Save this as a memory for ALwrity AI to take action automatically.">
|
||||
<span>
|
||||
<Button variant="contained" color="success" onClick={() => handleAction('alwrity')} sx={{ textTransform: 'none', fontWeight: 800 }}>
|
||||
ALwrity it
|
||||
</Button>
|
||||
</span>
|
||||
</Tooltip>
|
||||
<Tooltip title="Dismiss for now. You can revisit later in analytics.">
|
||||
<span>
|
||||
<Button variant="outlined" color="inherit" onClick={() => handleAction('ignore')} sx={{ textTransform: 'none', fontWeight: 800 }}>
|
||||
Ignore it
|
||||
</Button>
|
||||
</span>
|
||||
</Tooltip>
|
||||
</Box>
|
||||
</Stack>
|
||||
</CardContent>
|
||||
</Box>
|
||||
</Modal>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default AnalyticsInsights;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user