Fixes to Generate Pillar Chips

This commit is contained in:
ajaysi
2025-09-06 18:34:42 +05:30
parent ae42720c2a
commit 7ac72c5382
7 changed files with 769 additions and 219 deletions

View File

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

View File

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

View File

@@ -35,77 +35,107 @@ const SearchFilter: React.FC<SearchFilterProps> = ({
}
return 0;
};
// Descriptions for category tooltips
const categoryDescriptions: Record<string, string> = {
'Generate Content': 'AI multimodal generators: Blog, Image, Audio, Video.',
'SEO Tools': 'Enterprise SEO analysis, technical tools, and optimization utilities.',
'Social Media': 'Platform writers for Facebook, LinkedIn, Twitter, Instagram, YouTube.',
'Dashboards': 'Analytics dashboards: SEO, Social, Website, Strategy, and Calendar.'
};
return (
<SearchContainer>
<Box sx={{ display: 'flex', gap: 2, alignItems: 'center', mb: 3 }}>
<TextField
fullWidth
placeholder="Search tools..."
value={searchQuery}
onChange={(e) => onSearchChange(e.target.value)}
InputProps={{
startAdornment: (
<InputAdornment position="start">
<SearchIcon sx={{ color: 'rgba(255, 255, 255, 0.7)' }} />
</InputAdornment>
),
endAdornment: searchQuery && (
<InputAdornment position="end">
<IconButton onClick={onClearSearch} size="small">
<ClearIcon sx={{ color: 'rgba(255, 255, 255, 0.7)' }} />
</IconButton>
</InputAdornment>
),
}}
sx={{
'& .MuiOutlinedInput-root': {
color: 'white',
'& fieldset': {
borderColor: 'rgba(255, 255, 255, 0.3)',
{/* Single Row Layout: Search Input + Category Filters */}
<Box sx={{ display: 'flex', gap: 2, alignItems: 'center', flexWrap: 'wrap' }}>
{/* Search Input - Takes available space */}
<Box sx={{ flex: '1 1 300px', minWidth: '250px' }}>
<TextField
fullWidth
placeholder="Search tools..."
value={searchQuery}
onChange={(e) => onSearchChange(e.target.value)}
InputProps={{
startAdornment: (
<InputAdornment position="start">
<SearchIcon sx={{ color: 'rgba(255, 255, 255, 0.85)' }} />
</InputAdornment>
),
endAdornment: searchQuery && (
<InputAdornment position="end">
<IconButton onClick={onClearSearch} size="small">
<ClearIcon sx={{ color: 'rgba(255, 255, 255, 0.85)' }} />
</IconButton>
</InputAdornment>
),
}}
sx={{
'& .MuiOutlinedInput-root': {
color: 'white',
background: 'linear-gradient(135deg, rgba(255,255,255,0.14) 0%, rgba(255,255,255,0.08) 100%)',
borderRadius: 2.5,
boxShadow: 'inset 0 1px 0 rgba(255,255,255,0.2), 0 6px 18px rgba(0,0,0,0.25)',
'& fieldset': {
borderColor: 'rgba(255, 255, 255, 0.28)',
},
'&:hover fieldset': {
borderColor: 'rgba(255, 255, 255, 0.5)',
},
'&.Mui-focused fieldset': {
borderColor: 'rgba(255, 255, 255, 0.85)',
},
'& input::placeholder': {
color: 'rgba(255, 255, 255, 0.85)',
opacity: 1,
},
},
'&:hover fieldset': {
borderColor: 'rgba(255, 255, 255, 0.5)',
},
'&.Mui-focused fieldset': {
borderColor: 'rgba(255, 255, 255, 0.8)',
},
'& input::placeholder': {
color: 'rgba(255, 255, 255, 0.6)',
opacity: 1,
},
},
}}
/>
}}
/>
</Box>
{/* Filter Icon */}
<Tooltip title="Filter by category">
<IconButton sx={{ color: 'rgba(255, 255, 255, 0.7)' }}>
<FilterIcon />
</IconButton>
</Tooltip>
</Box>
{/* Enhanced Category Filter with Tool Count Badges */}
<Box sx={{ display: 'flex', gap: 1, flexWrap: 'wrap' }}>
<CategoryChip
label="All Tools"
onClick={() => onCategoryChange(null)}
active={selectedCategory === null}
theme={theme}
toolCount={Object.values(toolCategories).reduce((total, category) => total + getToolCount(category), 0)}
/>
{Object.keys(toolCategories).map((category) => (
{/* Category Filter Chips - Inline with search */}
<Box sx={{ display: 'flex', gap: 1, flexWrap: 'wrap', alignItems: 'center' }}>
<CategoryChip
key={category}
label={category}
onClick={() => onCategoryChange(category)}
active={selectedCategory === category}
label="All Tools"
onClick={() => onCategoryChange(null)}
active={selectedCategory === null}
theme={theme}
toolCount={getToolCount(toolCategories[category])}
toolCount={Object.values(toolCategories).reduce((total, category) => total + getToolCount(category), 0)}
/>
))}
{Object.keys(toolCategories).map((category) => {
const cat = toolCategories[category] as any;
const gradient = (cat && cat.gradient) || undefined;
const desc = categoryDescriptions[category] || `Filter tools by ${category}.`;
return (
<Tooltip
key={category}
title={`${desc} Total tools: ${getToolCount(cat)}.`}
placement="top"
arrow
enterDelay={300}
>
<CategoryChip
label={category}
onClick={() => onCategoryChange(category)}
active={selectedCategory === category}
theme={theme}
toolCount={getToolCount(cat)}
gradient={gradient}
/>
</Tooltip>
);
})}
</Box>
</Box>
{/* Sub-category Filter for SEO & Analytics */}
{selectedCategory === 'SEO & Analytics' && 'subCategories' in toolCategories['SEO & Analytics'] && (
{/* Sub-category Filter for SEO Tools */}
{selectedCategory === 'SEO Tools' && 'subCategories' in toolCategories['SEO Tools'] && (
<Box sx={{ mt: 2 }}>
<Typography variant="body2" sx={{ color: 'rgba(255, 255, 255, 0.8)', mb: 1, fontWeight: 600 }}>
Filter by sub-category:
@@ -117,7 +147,7 @@ const SearchFilter: React.FC<SearchFilterProps> = ({
active={selectedSubCategory === null}
theme={theme}
/>
{Object.keys(toolCategories['SEO & Analytics'].subCategories).map((subCategory) => (
{Object.keys(toolCategories['SEO Tools'].subCategories).map((subCategory) => (
<CategoryChip
key={subCategory}
label={subCategory}

View File

@@ -10,7 +10,8 @@ import {
} from '@mui/material';
import {
Star as StarIcon,
StarBorder as StarBorderIcon
StarBorder as StarBorderIcon,
LockOutlined as LockIcon
} from '@mui/icons-material';
import { ToolCardProps } from './types';
import { getStatusConfig } from './utils';
@@ -22,6 +23,7 @@ const ToolCard: React.FC<ToolCardProps> = ({
onToggleFavorite
}) => {
const config = getStatusConfig(tool.status);
const isLocked = tool.status === 'premium' || tool.status === 'pro';
return (
<Card
@@ -30,17 +32,17 @@ const ToolCard: React.FC<ToolCardProps> = ({
backdropFilter: 'blur(24px)',
border: '1px solid rgba(255, 255, 255, 0.12)',
borderRadius: 3,
cursor: 'pointer',
cursor: isLocked ? 'not-allowed' : 'pointer',
transition: 'all 0.3s ease',
position: 'relative',
overflow: 'hidden',
'&:hover': {
transform: 'translateY(-8px) scale(1.02)',
boxShadow: '0 20px 40px rgba(0, 0, 0, 0.3)',
border: '1px solid rgba(255, 255, 255, 0.2)',
transform: isLocked ? 'none' : 'translateY(-8px) scale(1.02)',
boxShadow: isLocked ? 'none' : '0 20px 40px rgba(0, 0, 0, 0.3)',
border: isLocked ? '1px solid rgba(255, 255, 255, 0.12)' : '1px solid rgba(255, 255, 255, 0.2)',
},
}}
onClick={() => onToolClick(tool)}
onClick={() => { if (!isLocked) onToolClick(tool); }}
>
<CardContent sx={{ p: 3 }}>
{/* Header with Icon and Status */}
@@ -59,8 +61,9 @@ const ToolCard: React.FC<ToolCardProps> = ({
background: `${config.color}20`,
color: config.color,
border: `1px solid ${config.color}40`,
fontWeight: 600,
fontWeight: 700,
fontSize: '0.75rem',
textTransform: 'capitalize',
}}
/>
</Box>
@@ -102,28 +105,48 @@ const ToolCard: React.FC<ToolCardProps> = ({
Features:
</Typography>
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 0.5 }}>
{tool.features.slice(0, 3).map((feature, index) => (
<Chip
key={index}
label={feature}
size="small"
sx={{
background: 'rgba(255, 255, 255, 0.1)',
color: 'rgba(255, 255, 255, 0.8)',
fontSize: '0.7rem',
height: '20px',
}}
/>
))}
{tool.features.slice(0, 3).map((feature, index) => {
const isDashboard = tool.name.toLowerCase().includes('dashboard');
return (
<Chip
key={index}
label={feature}
size="small"
sx={{
background: isDashboard
? 'linear-gradient(135deg, rgba(156, 39, 176, 0.3) 0%, rgba(123, 31, 162, 0.2) 100%)'
: 'rgba(255, 255, 255, 0.1)',
color: isDashboard ? 'rgba(255, 255, 255, 0.95)' : 'rgba(255, 255, 255, 0.8)',
fontSize: '0.7rem',
height: '22px',
border: isDashboard ? '1px solid rgba(156, 39, 176, 0.4)' : 'none',
fontWeight: isDashboard ? 600 : 400,
boxShadow: isDashboard ? '0 2px 8px rgba(156, 39, 176, 0.2)' : 'none',
transition: 'all 0.2s ease',
'&:hover': isDashboard ? {
background: 'linear-gradient(135deg, rgba(156, 39, 176, 0.4) 0%, rgba(123, 31, 162, 0.3) 100%)',
transform: 'translateY(-1px)',
boxShadow: '0 4px 12px rgba(156, 39, 176, 0.3)',
} : {},
}}
/>
);
})}
{tool.features.length > 3 && (
<Chip
label={`+${tool.features.length - 3} more`}
size="small"
sx={{
background: 'rgba(255, 255, 255, 0.1)',
color: 'rgba(255, 255, 255, 0.6)',
background: tool.name.toLowerCase().includes('dashboard')
? 'linear-gradient(135deg, rgba(156, 39, 176, 0.2) 0%, rgba(123, 31, 162, 0.1) 100%)'
: 'rgba(255, 255, 255, 0.1)',
color: tool.name.toLowerCase().includes('dashboard')
? 'rgba(255, 255, 255, 0.8)'
: 'rgba(255, 255, 255, 0.6)',
fontSize: '0.7rem',
height: '20px',
height: '22px',
border: tool.name.toLowerCase().includes('dashboard') ? '1px solid rgba(156, 39, 176, 0.3)' : 'none',
fontWeight: tool.name.toLowerCase().includes('dashboard') ? 600 : 400,
}}
/>
)}
@@ -131,6 +154,40 @@ const ToolCard: React.FC<ToolCardProps> = ({
</Box>
)}
</CardContent>
{/* Locked overlay for Premium/Pro */}
{isLocked && (
<Box
sx={{
position: 'absolute',
inset: 0,
background: 'linear-gradient(180deg, rgba(0,0,0,0.45) 0%, rgba(0,0,0,0.65) 100%)',
backdropFilter: 'blur(2px)',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
pointerEvents: 'none',
}}
>
<Box sx={{
display: 'flex',
alignItems: 'center',
gap: 1,
color: 'rgba(255,255,255,0.95)',
background: 'rgba(255,255,255,0.08)',
border: '1px solid rgba(255,255,255,0.25)',
px: 1.5,
py: 0.75,
borderRadius: 2,
boxShadow: '0 8px 24px rgba(0,0,0,0.35)'
}}>
<LockIcon fontSize="small" />
<Typography variant="body2" sx={{ fontWeight: 700 }}>
{(config.label || 'Pro') + ' • Locked'}
</Typography>
</Box>
</Box>
)}
</Card>
);
};

View File

@@ -96,28 +96,35 @@ export const SearchContainer = styled(Box)(({ theme }) => ({
}));
export const CategoryChip = styled(Chip, {
shouldForwardProp: (prop) => prop !== 'active' && prop !== 'toolCount',
})<{ active?: boolean; toolCount?: number }>(({ theme, active, toolCount }) => ({
shouldForwardProp: (prop) => prop !== 'active' && prop !== 'toolCount' && prop !== 'gradient',
})<{ active?: boolean; toolCount?: number; gradient?: string }>(({ theme, active, toolCount, gradient }) => ({
background: active
? 'linear-gradient(135deg, rgba(255, 255, 255, 0.3) 0%, rgba(255, 255, 255, 0.2) 100%)'
: 'rgba(255, 255, 255, 0.1)',
? (gradient || 'linear-gradient(135deg, rgba(76, 175, 80, 0.4) 0%, rgba(139, 195, 74, 0.3) 50%, rgba(255, 255, 255, 0.2) 100%)')
: 'linear-gradient(135deg, rgba(255, 255, 255, 0.15) 0%, rgba(255, 255, 255, 0.08) 50%, rgba(255, 255, 255, 0.05) 100%)',
color: 'white',
fontWeight: active ? 700 : 600,
fontSize: '0.9rem',
padding: theme.spacing(1, 2),
border: active
? '2px solid rgba(255, 255, 255, 0.6)'
: '1px solid rgba(255, 255, 255, 0.2)',
? '2px solid rgba(255, 255, 255, 0.6)'
: '1px solid rgba(255, 255, 255, 0.25)',
boxShadow: active
? '0 6px 20px rgba(255, 255, 255, 0.2), 0 0 0 1px rgba(255,255,255,0.1)'
: 'none',
? '0 6px 20px rgba(0, 0, 0, 0.25), 0 0 0 1px rgba(255, 255, 255, 0.15), inset 0 1px 0 rgba(255, 255, 255, 0.2)'
: '0 2px 8px rgba(0, 0, 0, 0.1), inset 0 1px 0 rgba(255, 255, 255, 0.1)',
transform: active ? 'translateY(-2px) scale(1.05)' : 'none',
transition: 'all 0.3s cubic-bezier(0.4, 0, 0.2, 1)',
position: 'relative',
'&:hover': {
background: 'rgba(255, 255, 255, 0.25)',
background: active
? (gradient || 'linear-gradient(135deg, rgba(76, 175, 80, 0.5) 0%, rgba(139, 195, 74, 0.4) 50%, rgba(255, 255, 255, 0.25) 100%)')
: 'linear-gradient(135deg, rgba(255, 255, 255, 0.25) 0%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.1) 100%)',
transform: 'translateY(-2px)',
boxShadow: '0 4px 12px rgba(0, 0, 0, 0.15)',
boxShadow: active
? '0 8px 25px rgba(76, 175, 80, 0.4), 0 0 0 1px rgba(76, 175, 80, 0.3), inset 0 1px 0 rgba(255, 255, 255, 0.3)'
: '0 4px 15px rgba(0, 0, 0, 0.2), inset 0 1px 0 rgba(255, 255, 255, 0.2)',
border: active
? '2px solid rgba(76, 175, 80, 0.8)'
: '1px solid rgba(255, 255, 255, 0.4)',
},
'& .MuiChip-label': {
padding: theme.spacing(0.5, 1),

View File

@@ -6,7 +6,12 @@ export const getToolsForCategory = (category: Category, selectedSubCategory: str
if (selectedSubCategory && category.subCategories[selectedSubCategory]) {
return category.subCategories[selectedSubCategory].tools;
}
return [];
// When no subcategory is selected, return all tools from all subcategories
const allTools: Tool[] = [];
Object.values(category.subCategories).forEach(subCategory => {
allTools.push(...subCategory.tools);
});
return allTools;
}
return category.tools;
};
@@ -19,7 +24,9 @@ export const getFilteredCategories = (
const filtered: ToolCategories = {};
Object.entries(toolCategories).forEach(([categoryName, category]) => {
if (selectedCategory && categoryName !== selectedCategory) {
// If there's a search query, search across ALL categories regardless of selected category
// If no search query, respect the selected category filter
if (!searchQuery && selectedCategory && categoryName !== selectedCategory) {
return;
}
@@ -60,6 +67,14 @@ export const getStatusConfig = (status: string) => {
return { color: '#FF9800', icon: '⚠', label: 'Good' };
case 'needs_action':
return { color: '#F44336', icon: '✗', label: 'Needs Action' };
case 'premium':
return { color: '#9C27B0', icon: '⭐', label: 'Premium' };
case 'beta':
return { color: '#FF9800', icon: '🧪', label: 'Beta' };
case 'pro':
return { color: '#2196F3', icon: '💎', label: 'Pro' };
case 'active':
return { color: '#4CAF50', icon: '✓', label: 'Active' };
default:
return { color: '#9E9E9E', icon: '', label: 'Unknown' };
}
@@ -74,6 +89,14 @@ export const getStatusColor = (status: string) => {
return '#FF9800';
case 'needs_action':
return '#F44336';
case 'premium':
return '#9C27B0';
case 'beta':
return '#FF9800';
case 'pro':
return '#2196F3';
case 'active':
return '#4CAF50';
default:
return '#9E9E9E';
}
@@ -88,6 +111,14 @@ export const getStatusIcon = (status: string) => {
return '⚠';
case 'needs_action':
return '✗';
case 'premium':
return '⭐';
case 'beta':
return '🧪';
case 'pro':
return '💎';
case 'active':
return '✓';
default:
return '';
}