ALwrity Version 0.5.0 (Fastapi + React )

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

View File

@@ -0,0 +1,101 @@
import React from 'react';
import { Box, Typography, Chip } from '@mui/material';
import { CategoryHeaderProps } from './types';
const CategoryHeader: React.FC<CategoryHeaderProps> = ({
categoryName,
category,
theme
}) => {
return (
<Box sx={{
display: 'flex',
alignItems: 'center',
gap: 2,
mb: 4,
p: 3,
background: 'rgba(255, 255, 255, 0.08)',
borderRadius: 3,
border: '1px solid rgba(255, 255, 255, 0.15)',
backdropFilter: 'blur(20px)',
position: 'relative',
overflow: 'hidden',
'&::before': {
content: '""',
position: 'absolute',
top: 0,
left: 0,
right: 0,
height: '3px',
background: category.gradient,
borderRadius: '3px 3px 0 0',
},
}}>
<Box
sx={{
width: 56,
height: 56,
borderRadius: 3,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
background: `${category.color}20`,
border: `2px solid ${category.color}40`,
boxShadow: `0 8px 24px ${category.color}30`,
position: 'relative',
'&::after': {
content: '""',
position: 'absolute',
top: -2,
left: -2,
right: -2,
bottom: -2,
background: category.gradient,
borderRadius: 3,
zIndex: -1,
opacity: 0.3,
},
}}
>
{category.icon}
</Box>
<Box sx={{ flex: 1 }}>
<Typography variant="h3" sx={{
fontWeight: 800,
color: 'white',
textShadow: '0 2px 4px rgba(0,0,0,0.3)',
fontSize: { xs: '1.75rem', md: '2.25rem' },
mb: 0.5,
}}>
{categoryName}
</Typography>
<Typography variant="body1" sx={{
color: 'rgba(255, 255, 255, 0.8)',
fontWeight: 500,
}}>
{'subCategories' in category ?
`${Object.keys(category.subCategories).length} sub-categories` :
`${category.tools.length} tools`
}
</Typography>
</Box>
<Chip
label={'subCategories' in category ?
`${Object.values(category.subCategories).flatMap(subCat => subCat.tools).length} tools` :
`${category.tools.length} tools`
}
size="medium"
sx={{
background: 'rgba(255, 255, 255, 0.15)',
color: 'rgba(255, 255, 255, 0.9)',
fontWeight: 700,
fontSize: '0.9rem',
height: '32px',
border: '1px solid rgba(255, 255, 255, 0.2)',
}}
/>
</Box>
);
};
export default CategoryHeader;

View File

@@ -0,0 +1,58 @@
import React from 'react';
import { Box, Typography, Chip } from '@mui/material';
import { ShimmerHeader } from './styled';
import { DashboardHeaderProps } from './types';
const DashboardHeader: React.FC<DashboardHeaderProps> = ({
title,
subtitle,
statusChips = []
}) => {
return (
<ShimmerHeader sx={{ mb: 5 }}>
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', mb: 3 }}>
<Box>
<Typography variant="h2" component="h1" sx={{
fontWeight: 800,
color: 'white',
textShadow: '0 4px 8px rgba(0,0,0,0.3)',
mb: 1,
fontSize: { xs: '2rem', md: '3rem' },
background: 'linear-gradient(135deg, #ffffff 0%, #f0f0f0 100%)',
backgroundClip: 'text',
WebkitBackgroundClip: 'text',
WebkitTextFillColor: 'transparent',
}}>
{title}
</Typography>
<Typography variant="h5" sx={{
color: 'rgba(255, 255, 255, 0.9)',
fontWeight: 400,
fontSize: { xs: '1rem', md: '1.25rem' },
}}>
{subtitle}
</Typography>
</Box>
{statusChips.length > 0 && (
<Box sx={{ display: 'flex', gap: 1.5 }}>
{statusChips.map((chip, index) => (
<Chip
key={index}
icon={chip.icon}
label={chip.label}
sx={{
background: `${chip.color}20`,
border: `1px solid ${chip.color}40`,
color: chip.color,
fontWeight: 700,
}}
/>
))}
</Box>
)}
</Box>
</ShimmerHeader>
);
};
export default DashboardHeader;

View File

@@ -0,0 +1,39 @@
import React from 'react';
import { Box, Typography, Button } from '@mui/material';
import { EmptyStateProps } from './types';
const EmptyState: React.FC<EmptyStateProps> = ({
title,
message,
onClearFilters,
clearButtonText = 'Clear Filters'
}) => {
return (
<Box sx={{ textAlign: 'center', py: 8 }}>
<Typography variant="h5" sx={{ color: 'rgba(255, 255, 255, 0.9)', mb: 2, fontWeight: 600 }}>
{title}
</Typography>
<Typography variant="body1" sx={{ color: 'rgba(255, 255, 255, 0.7)', mb: 3 }}>
{message}
</Typography>
{onClearFilters && (
<Button
variant="outlined"
onClick={onClearFilters}
sx={{
color: 'white',
borderColor: 'rgba(255, 255, 255, 0.3)',
'&:hover': {
borderColor: 'rgba(255, 255, 255, 0.5)',
background: 'rgba(255, 255, 255, 0.1)',
},
}}
>
{clearButtonText}
</Button>
)}
</Box>
);
};
export default EmptyState;

View File

@@ -0,0 +1,27 @@
import React from 'react';
import { Box, Container, Alert, Button } from '@mui/material';
import { DashboardContainer } from './styled';
import { ErrorDisplayProps } from './types';
const ErrorDisplay: React.FC<ErrorDisplayProps> = ({
error,
onRetry,
retryButtonText = 'Retry'
}) => {
return (
<DashboardContainer>
<Container maxWidth="xl">
<Alert severity="error" sx={{ mb: 3, borderRadius: 2 }}>
{error}
</Alert>
{onRetry && (
<Button onClick={onRetry} variant="contained">
{retryButtonText}
</Button>
)}
</Container>
</DashboardContainer>
);
};
export default ErrorDisplay;

View File

@@ -0,0 +1,29 @@
import React from 'react';
import { Box, Container, Skeleton, Grid } from '@mui/material';
import { DashboardContainer } from './styled';
import { LoadingSkeletonProps } from './types';
const LoadingSkeleton: React.FC<LoadingSkeletonProps> = ({
itemCount = 8,
itemHeight = 200,
headerHeight = 80
}) => {
return (
<DashboardContainer>
<Container maxWidth="xl">
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 3 }}>
<Skeleton variant="rectangular" height={headerHeight} sx={{ borderRadius: 2 }} />
<Grid container spacing={3}>
{Array.from({ length: itemCount }).map((_, index) => (
<Grid item xs={12} sm={6} md={4} lg={3} key={index}>
<Skeleton variant="rectangular" height={itemHeight} sx={{ borderRadius: 2 }} />
</Grid>
))}
</Grid>
</Box>
</Container>
</DashboardContainer>
);
};
export default LoadingSkeleton;

View File

@@ -0,0 +1,125 @@
import React from 'react';
import {
Box,
TextField,
InputAdornment,
IconButton,
Typography,
Tooltip
} from '@mui/material';
import {
Search as SearchIcon,
Clear as ClearIcon,
FilterList as FilterIcon
} from '@mui/icons-material';
import { SearchContainer, CategoryChip } from './styled';
import { SearchFilterProps } from './types';
const SearchFilter: React.FC<SearchFilterProps> = ({
searchQuery,
onSearchChange,
onClearSearch,
selectedCategory,
onCategoryChange,
selectedSubCategory,
onSubCategoryChange,
toolCategories,
theme
}) => {
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)',
},
'&: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,
},
},
}}
/>
<Tooltip title="Filter by category">
<IconButton sx={{ color: 'rgba(255, 255, 255, 0.7)' }}>
<FilterIcon />
</IconButton>
</Tooltip>
</Box>
{/* Enhanced Category Filter */}
<Box sx={{ display: 'flex', gap: 1, flexWrap: 'wrap' }}>
<CategoryChip
label="All Tools"
onClick={() => onCategoryChange(null)}
active={selectedCategory === null}
theme={theme}
/>
{Object.keys(toolCategories).map((category) => (
<CategoryChip
key={category}
label={category}
onClick={() => onCategoryChange(category)}
active={selectedCategory === category}
theme={theme}
/>
))}
</Box>
{/* Sub-category Filter for SEO & Analytics */}
{selectedCategory === 'SEO & Analytics' && 'subCategories' in toolCategories['SEO & Analytics'] && (
<Box sx={{ mt: 2 }}>
<Typography variant="body2" sx={{ color: 'rgba(255, 255, 255, 0.8)', mb: 1, fontWeight: 600 }}>
Filter by sub-category:
</Typography>
<Box sx={{ display: 'flex', gap: 1, flexWrap: 'wrap' }}>
<CategoryChip
label="All SEO Tools"
onClick={() => onSubCategoryChange(null)}
active={selectedSubCategory === null}
theme={theme}
/>
{Object.keys(toolCategories['SEO & Analytics'].subCategories).map((subCategory) => (
<CategoryChip
key={subCategory}
label={subCategory}
onClick={() => onSubCategoryChange(subCategory)}
active={selectedSubCategory === subCategory}
theme={theme}
/>
))}
</Box>
</Box>
)}
</SearchContainer>
);
};
export default SearchFilter;

View File

@@ -0,0 +1,138 @@
import React from 'react';
import {
Card,
CardContent,
Typography,
Chip,
Box,
IconButton,
Tooltip
} from '@mui/material';
import {
Star as StarIcon,
StarBorder as StarBorderIcon
} from '@mui/icons-material';
import { ToolCardProps } from './types';
import { getStatusConfig } from './utils';
const ToolCard: React.FC<ToolCardProps> = ({
tool,
onToolClick,
isFavorite,
onToggleFavorite
}) => {
const config = getStatusConfig(tool.status);
return (
<Card
sx={{
background: 'rgba(255, 255, 255, 0.08)',
backdropFilter: 'blur(24px)',
border: '1px solid rgba(255, 255, 255, 0.12)',
borderRadius: 3,
cursor: '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)',
},
}}
onClick={() => onToolClick(tool)}
>
<CardContent sx={{ p: 3 }}>
{/* Header with Icon and Status */}
<Box sx={{ display: 'flex', alignItems: 'center', mb: 2 }}>
<Box sx={{ mr: 2 }}>
{tool.icon}
</Box>
<Box sx={{ flex: 1 }}>
<Typography variant="h6" sx={{ color: 'white', fontWeight: 600, mb: 0.5 }}>
{tool.name}
</Typography>
<Chip
label={config.label || tool.status}
size="small"
sx={{
background: `${config.color}20`,
color: config.color,
border: `1px solid ${config.color}40`,
fontWeight: 600,
fontSize: '0.75rem',
}}
/>
</Box>
<Tooltip title={isFavorite ? 'Remove from favorites' : 'Add to favorites'}>
<IconButton
onClick={(e) => {
e.stopPropagation();
onToggleFavorite(tool.name);
}}
sx={{
color: isFavorite ? '#FFD700' : 'rgba(255, 255, 255, 0.7)',
'&:hover': {
color: isFavorite ? '#FFD700' : 'white',
},
}}
>
{isFavorite ? <StarIcon /> : <StarBorderIcon />}
</IconButton>
</Tooltip>
</Box>
{/* Description */}
<Typography
variant="body2"
sx={{
color: 'rgba(255, 255, 255, 0.8)',
mb: 2,
lineHeight: 1.6,
minHeight: '3.2em'
}}
>
{tool.description}
</Typography>
{/* Features */}
{tool.features && tool.features.length > 0 && (
<Box sx={{ mt: 2 }}>
<Typography variant="caption" sx={{ color: 'rgba(255, 255, 255, 0.6)', mb: 1, display: 'block' }}>
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.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)',
fontSize: '0.7rem',
height: '20px',
}}
/>
)}
</Box>
</Box>
)}
</CardContent>
</Card>
);
};
export default ToolCard;

View File

@@ -0,0 +1,17 @@
// Shared components exports
export { default as DashboardHeader } from './DashboardHeader';
export { default as SearchFilter } from './SearchFilter';
export { default as ToolCard } from './ToolCard';
export { default as CategoryHeader } from './CategoryHeader';
export { default as LoadingSkeleton } from './LoadingSkeleton';
export { default as ErrorDisplay } from './ErrorDisplay';
export { default as EmptyState } from './EmptyState';
// Shared styled components
export * from './styled';
// Shared types
export * from './types';
// Shared utilities
export * from './utils';

View File

@@ -0,0 +1,138 @@
import { Box, Card, Chip } from '@mui/material';
import { styled } from '@mui/material/styles';
// Shared styled components for dashboard components
export const DashboardContainer = styled(Box)(({ theme }) => ({
minHeight: '100vh',
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 50%, #f093fb 100%)',
padding: theme.spacing(4),
position: 'relative',
'&::before': {
content: '""',
position: 'absolute',
top: 0,
left: 0,
right: 0,
bottom: 0,
background: 'url("data:image/svg+xml,%3Csvg width="80" height="80" viewBox="0 0 80 80" xmlns="http://www.w3.org/2000/svg"%3E%3Cg fill="none" fill-rule="evenodd"%3E%3Cg fill="%23ffffff" fill-opacity="0.03"%3E%3Ccircle cx="40" cy="40" r="3"/%3E%3C/g%3E%3C/g%3E%3C/svg%3E")',
pointerEvents: 'none',
},
'&::after': {
content: '""',
position: 'absolute',
top: '50%',
left: '50%',
width: '600px',
height: '600px',
background: 'radial-gradient(circle, rgba(255,255,255,0.1) 0%, transparent 70%)',
transform: 'translate(-50%, -50%)',
pointerEvents: 'none',
zIndex: 0,
},
}));
export const GlassCard = styled(Card)(({ theme }) => ({
background: 'rgba(255, 255, 255, 0.08)',
backdropFilter: 'blur(24px)',
border: '1px solid rgba(255, 255, 255, 0.12)',
borderRadius: theme.spacing(3),
boxShadow: '0 12px 40px rgba(0, 0, 0, 0.12)',
transition: 'all 0.4s cubic-bezier(0.4, 0, 0.2, 1)',
position: 'relative',
overflow: 'hidden',
'&::before': {
content: '""',
position: 'absolute',
top: 0,
left: '-100%',
width: '100%',
height: '100%',
background: 'linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.08), transparent)',
transition: 'left 0.6s ease-in-out',
},
'&:hover': {
transform: 'translateY(-12px) scale(1.02)',
boxShadow: '0 24px 60px rgba(0, 0, 0, 0.18)',
border: '1px solid rgba(255, 255, 255, 0.2)',
'&::before': {
left: '100%',
},
},
}));
export const ShimmerHeader = styled(Box)(({ theme }) => ({
position: 'relative',
overflow: 'hidden',
'&::before': {
content: '""',
position: 'absolute',
top: 0,
left: '-100%',
width: '100%',
height: '3px',
background: 'linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.8), transparent)',
animation: 'shimmer 3s infinite',
},
'@keyframes shimmer': {
'0%': { left: '-100%' },
'100%': { left: '100%' },
},
}));
export const SearchContainer = styled(Box)(({ theme }) => ({
background: 'rgba(255, 255, 255, 0.1)',
backdropFilter: 'blur(20px)',
border: '1px solid rgba(255, 255, 255, 0.2)',
borderRadius: theme.spacing(3),
padding: theme.spacing(2),
marginBottom: theme.spacing(4),
transition: 'all 0.3s ease',
'&:hover': {
background: 'rgba(255, 255, 255, 0.15)',
border: '1px solid rgba(255, 255, 255, 0.3)',
},
}));
export const CategoryChip = styled(Chip, {
shouldForwardProp: (prop) => prop !== 'active',
})<{ active?: boolean }>(({ theme, active }) => ({
background: active ? 'rgba(255, 255, 255, 0.25)' : 'rgba(255, 255, 255, 0.1)',
color: 'white',
fontWeight: 600,
fontSize: '0.9rem',
padding: theme.spacing(1, 2),
border: `1px solid ${active ? 'rgba(255, 255, 255, 0.4)' : 'rgba(255, 255, 255, 0.2)'}`,
transition: 'all 0.3s cubic-bezier(0.4, 0, 0.2, 1)',
'&:hover': {
background: 'rgba(255, 255, 255, 0.25)',
transform: 'translateY(-2px)',
boxShadow: '0 4px 12px rgba(0, 0, 0, 0.15)',
},
'& .MuiChip-label': {
padding: theme.spacing(0.5, 1),
},
}));
export const EnhancedGlassCard = styled(GlassCard)(({ theme }) => ({
background: 'rgba(255, 255, 255, 0.12)',
border: '2px solid rgba(255, 255, 255, 0.2)',
'&:hover': {
border: '2px solid rgba(255, 255, 255, 0.3)',
},
}));
export const AIInsightsPanel = styled(GlassCard)(({ theme }) => ({
background: 'rgba(255, 255, 255, 0.1)',
border: '1px solid rgba(255, 255, 255, 0.15)',
position: 'relative',
'&::before': {
content: '""',
position: 'absolute',
top: 0,
left: 0,
right: 0,
height: '3px',
background: 'linear-gradient(90deg, #667eea, #764ba2, #f093fb)',
borderRadius: '3px 3px 0 0',
},
}));

View File

@@ -0,0 +1,225 @@
// Shared TypeScript interfaces for dashboard components
export interface Tool {
name: string;
description: string;
icon: React.ReactElement;
status: string;
path: string;
features: string[];
isPinned?: boolean;
isHighlighted?: boolean;
}
export interface SubCategory {
tools: Tool[];
}
export interface RegularCategory {
icon: React.ReactElement;
color: string;
gradient: string;
tools: Tool[];
}
export interface SubCategoryCategory {
icon: React.ReactElement;
color: string;
gradient: string;
subCategories: Record<string, SubCategory>;
}
export type Category = RegularCategory | SubCategoryCategory;
export interface ToolCategories {
[key: string]: Category;
}
export interface SnackbarState {
open: boolean;
message: string;
severity: 'success' | 'error' | 'info' | 'warning';
}
export interface DashboardState {
loading: boolean;
error: string | null;
searchQuery: string;
selectedCategory: string | null;
selectedSubCategory: string | null;
favorites: string[];
snackbar: SnackbarState;
}
export interface ToolCardProps {
tool: Tool;
onToolClick: (tool: Tool) => void;
isFavorite: boolean;
onToggleFavorite: (toolName: string) => void;
}
export interface CategoryHeaderProps {
categoryName: string;
category: Category;
theme: any;
}
export interface SearchFilterProps {
searchQuery: string;
onSearchChange: (query: string) => void;
onClearSearch: () => void;
selectedCategory: string | null;
onCategoryChange: (category: string | null) => void;
selectedSubCategory: string | null;
onSubCategoryChange: (subCategory: string | null) => void;
toolCategories: ToolCategories;
theme: any;
}
export interface DashboardHeaderProps {
title: string;
subtitle: string;
statusChips?: Array<{
label: string;
color: string;
icon: React.ReactElement;
}>;
}
export interface LoadingSkeletonProps {
itemCount?: number;
itemHeight?: number;
headerHeight?: number;
}
export interface ErrorDisplayProps {
error: string;
onRetry?: () => void;
retryButtonText?: string;
}
export interface EmptyStateProps {
icon: React.ReactElement;
title: string;
message: string;
onClearFilters?: () => void;
clearButtonText?: string;
}
// SEO Analysis Types
export interface SEOIssue {
type: string;
message: string;
location: string;
fix: string;
code_example?: string;
action: string;
current_value?: string;
}
export interface SEOWarning {
type: string;
message: string;
location: string;
fix: string;
code_example?: string;
action: string;
current_value?: string;
}
export interface SEORecommendation {
type: string;
message: string;
location: string;
fix: string;
code_example?: string;
action: string;
priority?: string;
description?: string;
}
export interface SEOAnalysisData {
url: string;
overall_score: number;
health_status: string;
critical_issues: SEOIssue[];
warnings: SEOWarning[];
recommendations: SEORecommendation[];
data: {
url_structure: any;
meta_data: any;
content_analysis: any;
technical_seo: any;
performance: any;
accessibility: any;
user_experience: any;
security_headers: any;
keyword_analysis?: any;
};
timestamp: string;
success?: boolean;
message?: string;
}
export interface SEOAnalyzerPanelProps {
analysisData: SEOAnalysisData | null;
onRunAnalysis: () => Promise<void>;
loading: boolean;
error: string | null;
}
export interface CategoryCardProps {
category: string;
data: any;
isExpanded: boolean;
onToggle: (category: string) => void;
onIssueClick: (issue: any) => void;
onAIAction: (action: string, issue: any) => void;
}
export interface IssueListProps {
issues: any[];
type: 'critical' | 'warning' | 'recommendation';
onIssueClick: (issue: any) => void;
onAIAction: (action: string, issue: any) => void;
}
export interface CriticalIssueCardProps {
issue: any;
index: number;
onClick: (issue: any) => void;
onAIAction: (action: string, issue: any) => void;
}
export interface AnalysisTabsProps {
categorizedData: {
good: any[];
bad: any[];
ugly: any[];
};
expandedCategories: Set<string>;
onToggleCategory: (category: string) => void;
onIssueClick: (issue: any) => void;
onAIAction: (action: string, issue: any) => void;
}
export interface IssueDetailsDialogProps {
open: boolean;
issue: any | null;
onClose: () => void;
onAIAction: (action: string, issue: any) => void;
}
export interface AnalysisDetailsDialogProps {
open: boolean;
onClose: () => void;
}
export interface SEOAnalysisLoadingProps {
loading: boolean;
}
export interface SEOAnalysisErrorProps {
error: string | null;
showError: boolean;
onCloseError: () => void;
}

View File

@@ -0,0 +1,149 @@
import { Category, Tool, ToolCategories } from './types';
// Utility functions for dashboard components
export const getToolsForCategory = (category: Category, selectedSubCategory: string | null): Tool[] => {
if ('subCategories' in category) {
if (selectedSubCategory && category.subCategories[selectedSubCategory]) {
return category.subCategories[selectedSubCategory].tools;
}
return [];
}
return category.tools;
};
export const getFilteredCategories = (
toolCategories: ToolCategories,
selectedCategory: string | null,
searchQuery: string
) => {
const filtered: ToolCategories = {};
Object.entries(toolCategories).forEach(([categoryName, category]) => {
if (selectedCategory && categoryName !== selectedCategory) {
return;
}
if ('subCategories' in category) {
const filteredSubCategories: Record<string, any> = {};
Object.entries(category.subCategories).forEach(([subCategoryName, subCategory]) => {
const filteredTools = subCategory.tools.filter(tool =>
tool.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
tool.description.toLowerCase().includes(searchQuery.toLowerCase())
);
if (filteredTools.length > 0) {
filteredSubCategories[subCategoryName] = { ...subCategory, tools: filteredTools };
}
});
if (Object.keys(filteredSubCategories).length > 0) {
filtered[categoryName] = { ...category, subCategories: filteredSubCategories };
}
} else {
const filteredTools = category.tools.filter(tool =>
tool.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
tool.description.toLowerCase().includes(searchQuery.toLowerCase())
);
if (filteredTools.length > 0) {
filtered[categoryName] = { ...category, tools: filteredTools };
}
}
});
return filtered;
};
export const getStatusConfig = (status: string) => {
switch (status) {
case 'excellent':
case 'strong':
return { color: '#4CAF50', icon: '✓', label: 'Excellent' };
case 'good':
return { color: '#FF9800', icon: '⚠', label: 'Good' };
case 'needs_action':
return { color: '#F44336', icon: '✗', label: 'Needs Action' };
default:
return { color: '#9E9E9E', icon: '', label: 'Unknown' };
}
};
export const getStatusColor = (status: string) => {
switch (status) {
case 'excellent':
case 'strong':
return '#4CAF50';
case 'good':
return '#FF9800';
case 'needs_action':
return '#F44336';
default:
return '#9E9E9E';
}
};
export const getStatusIcon = (status: string) => {
switch (status) {
case 'excellent':
case 'strong':
return '✓';
case 'good':
return '⚠';
case 'needs_action':
return '✗';
default:
return '';
}
};
export const formatNumber = (num: number): string => {
if (num >= 1000000) {
return (num / 1000000).toFixed(1) + 'M';
} else if (num >= 1000) {
return (num / 1000).toFixed(1) + 'K';
}
return num.toString();
};
export const formatPercentage = (num: number): string => {
return `${num > 0 ? '+' : ''}${num.toFixed(1)}%`;
};
export const getTrendColor = (trend: string): string => {
switch (trend) {
case 'up':
return '#4CAF50';
case 'down':
return '#F44336';
default:
return '#9E9E9E';
}
};
export const getTrendIcon = (trend: string): string => {
switch (trend) {
case 'up':
return '↗';
case 'down':
return '↘';
default:
return '→';
}
};
export const capitalizeFirst = (str: string): string => {
return str.charAt(0).toUpperCase() + str.slice(1);
};
export const truncateText = (text: string, maxLength: number): string => {
if (text.length <= maxLength) return text;
return text.substring(0, maxLength) + '...';
};
export const debounce = <T extends (...args: any[]) => any>(
func: T,
wait: number
): ((...args: Parameters<T>) => void) => {
let timeout: NodeJS.Timeout;
return (...args: Parameters<T>) => {
clearTimeout(timeout);
timeout = setTimeout(() => func(...args), wait);
};
};