Fixes to Generate Pillar Chips
This commit is contained in:
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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 'ℹ';
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user