ALwrity Version 0.5.0 (Fastapi + React )
This commit is contained in:
101
frontend/src/components/shared/CategoryHeader.tsx
Normal file
101
frontend/src/components/shared/CategoryHeader.tsx
Normal 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;
|
||||
58
frontend/src/components/shared/DashboardHeader.tsx
Normal file
58
frontend/src/components/shared/DashboardHeader.tsx
Normal 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;
|
||||
39
frontend/src/components/shared/EmptyState.tsx
Normal file
39
frontend/src/components/shared/EmptyState.tsx
Normal 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;
|
||||
27
frontend/src/components/shared/ErrorDisplay.tsx
Normal file
27
frontend/src/components/shared/ErrorDisplay.tsx
Normal 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;
|
||||
29
frontend/src/components/shared/LoadingSkeleton.tsx
Normal file
29
frontend/src/components/shared/LoadingSkeleton.tsx
Normal 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;
|
||||
125
frontend/src/components/shared/SearchFilter.tsx
Normal file
125
frontend/src/components/shared/SearchFilter.tsx
Normal 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;
|
||||
138
frontend/src/components/shared/ToolCard.tsx
Normal file
138
frontend/src/components/shared/ToolCard.tsx
Normal 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;
|
||||
17
frontend/src/components/shared/index.ts
Normal file
17
frontend/src/components/shared/index.ts
Normal 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';
|
||||
138
frontend/src/components/shared/styled.ts
Normal file
138
frontend/src/components/shared/styled.ts
Normal 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',
|
||||
},
|
||||
}));
|
||||
225
frontend/src/components/shared/types.ts
Normal file
225
frontend/src/components/shared/types.ts
Normal 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;
|
||||
}
|
||||
149
frontend/src/components/shared/utils.ts
Normal file
149
frontend/src/components/shared/utils.ts
Normal 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);
|
||||
};
|
||||
};
|
||||
Reference in New Issue
Block a user