Analytics Insights and Tools Modal
This commit is contained in:
@@ -290,21 +290,48 @@ const PillarCard: React.FC<{
|
|||||||
onMouseEnter={() => setIsHovered(true)}
|
onMouseEnter={() => setIsHovered(true)}
|
||||||
onMouseLeave={() => setIsHovered(false)}
|
onMouseLeave={() => setIsHovered(false)}
|
||||||
>
|
>
|
||||||
<CardContent sx={{ p: 2, height: '100%', display: 'flex', flexDirection: 'column' }}>
|
{/* Shooting star border animation */}
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
position: 'absolute',
|
||||||
|
top: 0,
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
bottom: 0,
|
||||||
|
borderRadius: 'inherit',
|
||||||
|
overflow: 'hidden',
|
||||||
|
zIndex: 1,
|
||||||
|
'&::before': {
|
||||||
|
content: '""',
|
||||||
|
position: 'absolute',
|
||||||
|
top: 0,
|
||||||
|
left: '-100%',
|
||||||
|
width: '100%',
|
||||||
|
height: '2px',
|
||||||
|
background: 'linear-gradient(90deg, transparent, rgba(255,255,255,0.8), transparent)',
|
||||||
|
animation: 'shootingStar 8s linear infinite',
|
||||||
|
},
|
||||||
|
'@keyframes shootingStar': {
|
||||||
|
'0%': { left: '-100%' },
|
||||||
|
'100%': { left: '100%' },
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<CardContent sx={{ p: 1.5, height: '100%', display: 'flex', flexDirection: 'column', position: 'relative', zIndex: 2 }}>
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<Box sx={{ display: 'flex', alignItems: 'center', mb: 1.5, position: 'relative' }}>
|
<Box sx={{ display: 'flex', alignItems: 'center', mb: 1, position: 'relative' }}>
|
||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
p: 0.8,
|
p: 0.6,
|
||||||
borderRadius: '50%',
|
borderRadius: '50%',
|
||||||
backgroundColor: 'rgba(255,255,255,0.2)',
|
backgroundColor: 'rgba(255,255,255,0.2)',
|
||||||
mr: 1.2,
|
mr: 1,
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
justifyContent: 'center'
|
justifyContent: 'center'
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<IconComponent sx={{ fontSize: 18, color: 'white' }} />
|
<IconComponent sx={{ fontSize: 16, color: 'white' }} />
|
||||||
</Box>
|
</Box>
|
||||||
<Typography variant="h6" sx={{ fontWeight: 700, fontSize: '1rem' }}>
|
<Typography variant="h6" sx={{ fontWeight: 700, fontSize: '1rem' }}>
|
||||||
{pillar.title}
|
{pillar.title}
|
||||||
@@ -378,7 +405,7 @@ const PillarCard: React.FC<{
|
|||||||
transition={{ duration: 0.3, ease: 'easeInOut' }}
|
transition={{ duration: 0.3, ease: 'easeInOut' }}
|
||||||
style={{ overflow: 'hidden' }}
|
style={{ overflow: 'hidden' }}
|
||||||
>
|
>
|
||||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1, mt: 1 }}>
|
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 0.5, mt: 0.5 }}>
|
||||||
{pillar.id === 'plan' ? (
|
{pillar.id === 'plan' ? (
|
||||||
<>
|
<>
|
||||||
<motion.div
|
<motion.div
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import ErrorDisplay from '../shared/ErrorDisplay';
|
|||||||
import EmptyState from '../shared/EmptyState';
|
import EmptyState from '../shared/EmptyState';
|
||||||
import ContentLifecyclePillars from './ContentLifecyclePillars';
|
import ContentLifecyclePillars from './ContentLifecyclePillars';
|
||||||
import AnalyticsInsights from './components/AnalyticsInsights';
|
import AnalyticsInsights from './components/AnalyticsInsights';
|
||||||
|
import ToolsModal from './components/ToolsModal';
|
||||||
|
|
||||||
// Shared types and utilities
|
// Shared types and utilities
|
||||||
import { Tool } from '../shared/types';
|
import { Tool } from '../shared/types';
|
||||||
@@ -99,6 +100,12 @@ const MainDashboard: React.FC = () => {
|
|||||||
// State to track if we need to start a newly generated workflow
|
// State to track if we need to start a newly generated workflow
|
||||||
const [shouldStartWorkflow, setShouldStartWorkflow] = React.useState(false);
|
const [shouldStartWorkflow, setShouldStartWorkflow] = React.useState(false);
|
||||||
|
|
||||||
|
// Tools Modal state
|
||||||
|
const [toolsModalOpen, setToolsModalOpen] = React.useState(false);
|
||||||
|
const [modalCategoryName, setModalCategoryName] = React.useState<string | null>(null);
|
||||||
|
const [modalCategory, setModalCategory] = React.useState<any>(null);
|
||||||
|
const [searchResults, setSearchResults] = React.useState<Tool[]>([]);
|
||||||
|
|
||||||
// Handle workflow start
|
// Handle workflow start
|
||||||
const handleStartWorkflow = async () => {
|
const handleStartWorkflow = async () => {
|
||||||
try {
|
try {
|
||||||
@@ -170,6 +177,56 @@ const MainDashboard: React.FC = () => {
|
|||||||
showSnackbar(`Launching ${tool.name}...`, 'info');
|
showSnackbar(`Launching ${tool.name}...`, 'info');
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Handle category click to open modal
|
||||||
|
const handleCategoryClick = (categoryName: string | null, categoryData?: any) => {
|
||||||
|
setModalCategoryName(categoryName);
|
||||||
|
setModalCategory(categoryData);
|
||||||
|
setToolsModalOpen(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Handle search to show results in modal with debouncing
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (searchQuery && searchQuery.length >= 2) { // Only search after 2+ characters
|
||||||
|
const timeoutId = setTimeout(() => {
|
||||||
|
// Get all tools from all categories that match search
|
||||||
|
const allTools: Tool[] = [];
|
||||||
|
Object.values(toolCategories).forEach(category => {
|
||||||
|
if (category) {
|
||||||
|
const tools = getToolsForCategory(category, null);
|
||||||
|
allTools.push(...tools);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const filtered = allTools.filter(tool =>
|
||||||
|
tool.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||||
|
tool.description.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||||
|
tool.features.some(feature => feature.toLowerCase().includes(searchQuery.toLowerCase()))
|
||||||
|
);
|
||||||
|
|
||||||
|
setSearchResults(filtered);
|
||||||
|
setModalCategoryName(null);
|
||||||
|
setModalCategory(null);
|
||||||
|
setToolsModalOpen(true);
|
||||||
|
}, 500); // 500ms delay
|
||||||
|
|
||||||
|
return () => clearTimeout(timeoutId);
|
||||||
|
} else if (searchQuery && searchQuery.length < 2) {
|
||||||
|
// Close modal if search query is too short
|
||||||
|
setToolsModalOpen(false);
|
||||||
|
}
|
||||||
|
}, [searchQuery, toolCategories]);
|
||||||
|
|
||||||
|
// Close modal and clear search
|
||||||
|
const handleCloseModal = () => {
|
||||||
|
setToolsModalOpen(false);
|
||||||
|
setModalCategoryName(null);
|
||||||
|
setModalCategory(null);
|
||||||
|
setSearchResults([]);
|
||||||
|
if (searchQuery) {
|
||||||
|
setSearchQuery('');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const filteredCategories = getFilteredCategories(
|
const filteredCategories = getFilteredCategories(
|
||||||
toolCategories,
|
toolCategories,
|
||||||
selectedCategory,
|
selectedCategory,
|
||||||
@@ -245,9 +302,6 @@ const MainDashboard: React.FC = () => {
|
|||||||
{/* Content Lifecycle Pillars - First Panel */}
|
{/* Content Lifecycle Pillars - First Panel */}
|
||||||
<ContentLifecyclePillars />
|
<ContentLifecyclePillars />
|
||||||
|
|
||||||
{/* Analytics Insights - Good/Bad/Ugly */}
|
|
||||||
<AnalyticsInsights />
|
|
||||||
|
|
||||||
{/* Search and Filter */}
|
{/* Search and Filter */}
|
||||||
<SearchFilter
|
<SearchFilter
|
||||||
searchQuery={searchQuery}
|
searchQuery={searchQuery}
|
||||||
@@ -259,60 +313,24 @@ const MainDashboard: React.FC = () => {
|
|||||||
onSubCategoryChange={setSelectedSubCategory}
|
onSubCategoryChange={setSelectedSubCategory}
|
||||||
toolCategories={toolCategories}
|
toolCategories={toolCategories}
|
||||||
theme={theme}
|
theme={theme}
|
||||||
|
onCategoryClick={handleCategoryClick}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Enhanced Tools Grid */}
|
{/* Analytics Insights - Good/Bad/Ugly */}
|
||||||
<Box sx={{ mb: 4 }}>
|
<AnalyticsInsights />
|
||||||
{Object.entries(filteredCategories).map(([categoryName, category], categoryIndex) => (
|
|
||||||
<motion.div
|
|
||||||
key={categoryName}
|
|
||||||
initial={{ opacity: 0, y: 20 }}
|
|
||||||
animate={{ opacity: 1, y: 0 }}
|
|
||||||
transition={{ duration: 0.5, delay: categoryIndex * 0.1 }}
|
|
||||||
>
|
|
||||||
<Box sx={{ mb: 5 }}>
|
|
||||||
{/* Show Category Header when no specific category is selected OR when searching across all categories */}
|
|
||||||
{(selectedCategory === null || searchQuery) && (
|
|
||||||
<CategoryHeader
|
|
||||||
categoryName={categoryName}
|
|
||||||
category={category}
|
|
||||||
theme={theme}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<Grid container spacing={3}>
|
{/* Tools Modal */}
|
||||||
{getToolsForCategory(category, selectedSubCategory).map((tool: Tool, toolIndex: number) => (
|
<ToolsModal
|
||||||
<Grid item xs={12} sm={6} md={4} lg={3} key={tool.name}>
|
open={toolsModalOpen}
|
||||||
<motion.div
|
onClose={handleCloseModal}
|
||||||
initial={{ opacity: 0, y: 20 }}
|
categoryName={modalCategoryName || undefined}
|
||||||
animate={{ opacity: 1, y: 0 }}
|
category={modalCategory}
|
||||||
transition={{ duration: 0.5, delay: (categoryIndex * 0.1) + (toolIndex * 0.05) }}
|
searchQuery={searchQuery}
|
||||||
>
|
searchResults={searchResults}
|
||||||
<ToolCard
|
onToolClick={handleToolClick}
|
||||||
tool={tool}
|
favorites={favorites}
|
||||||
onToolClick={handleToolClick}
|
onToggleFavorite={toggleFavorite}
|
||||||
isFavorite={favorites.includes(tool.name)}
|
/>
|
||||||
onToggleFavorite={toggleFavorite}
|
|
||||||
/>
|
|
||||||
</motion.div>
|
|
||||||
</Grid>
|
|
||||||
))}
|
|
||||||
</Grid>
|
|
||||||
</Box>
|
|
||||||
</motion.div>
|
|
||||||
))}
|
|
||||||
</Box>
|
|
||||||
|
|
||||||
{/* Empty State */}
|
|
||||||
{Object.keys(filteredCategories).length === 0 && (
|
|
||||||
<EmptyState
|
|
||||||
icon={<span>🔍</span>}
|
|
||||||
title="No tools found matching your criteria"
|
|
||||||
message="Try adjusting your search or category filter"
|
|
||||||
onClearFilters={clearFilters}
|
|
||||||
clearButtonText="Clear Filters"
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</motion.div>
|
</motion.div>
|
||||||
</AnimatePresence>
|
</AnimatePresence>
|
||||||
|
|
||||||
|
|||||||
@@ -92,11 +92,6 @@ const Badge = styled('span')(({ theme }) => ({
|
|||||||
fontSize: '0.65rem'
|
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 = {
|
const mockData: AnalyticsData = {
|
||||||
theGood: [
|
theGood: [
|
||||||
@@ -268,23 +263,20 @@ const AnalyticsInsights: React.FC<AnalyticsInsightsProps> = ({ data, onActionCli
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box sx={{ mt: 2, mb: 2.5 }}>
|
<Box sx={{ mt: 2, mb: 2.9 }}>
|
||||||
<Typography
|
<Box sx={{ position: 'relative', overflow: 'hidden' }}>
|
||||||
variant="h6"
|
<Typography
|
||||||
sx={{
|
variant="h6"
|
||||||
fontWeight: 800,
|
sx={{
|
||||||
mb: 1.5,
|
fontWeight: 800,
|
||||||
fontSize: '1.1rem',
|
mb: 1.5,
|
||||||
background: 'linear-gradient(90deg, rgba(255,255,255,0.35), rgba(255,255,255,0.9) 50%, rgba(255,255,255,0.35))',
|
fontSize: '1.1rem',
|
||||||
WebkitBackgroundClip: 'text',
|
color: 'rgba(255,255,255,0.95)',
|
||||||
backgroundClip: 'text',
|
}}
|
||||||
color: 'transparent',
|
>
|
||||||
backgroundSize: '200% 100%',
|
Today's Analytics Insights
|
||||||
animation: `${shimmerText} 3.2s linear infinite`,
|
</Typography>
|
||||||
}}
|
</Box>
|
||||||
>
|
|
||||||
Analytics Insights
|
|
||||||
</Typography>
|
|
||||||
<Stack direction={{ xs: 'column', md: 'row' }} spacing={1.5}>
|
<Stack direction={{ xs: 'column', md: 'row' }} spacing={1.5}>
|
||||||
{columns.map((col) => {
|
{columns.map((col) => {
|
||||||
const isHovered = hovered === col.key;
|
const isHovered = hovered === col.key;
|
||||||
|
|||||||
@@ -128,19 +128,27 @@ const EnhancedTodayModal: React.FC<EnhancedTodayModalProps> = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleWorkflowComplete = async () => {
|
const handleWorkflowComplete = async () => {
|
||||||
|
console.log('Workflow Complete clicked for pillar:', pillarId);
|
||||||
|
console.log('Current pillar tasks:', pillarTasks);
|
||||||
|
|
||||||
// Mark all remaining tasks in this pillar as completed
|
// Mark all remaining tasks in this pillar as completed
|
||||||
const incompleteTasks = pillarTasks.filter(task =>
|
const incompleteTasks = pillarTasks.filter(task =>
|
||||||
task.status !== 'completed' && task.status !== 'skipped'
|
task.status !== 'completed' && task.status !== 'skipped'
|
||||||
);
|
);
|
||||||
|
|
||||||
|
console.log('Incomplete tasks to complete:', incompleteTasks);
|
||||||
|
|
||||||
for (const task of incompleteTasks) {
|
for (const task of incompleteTasks) {
|
||||||
try {
|
try {
|
||||||
|
console.log('Completing task:', task.id);
|
||||||
await completeTask(task.id);
|
await completeTask(task.id);
|
||||||
|
console.log('Task completed successfully:', task.id);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`Failed to complete task ${task.id}:`, error);
|
console.error(`Failed to complete task ${task.id}:`, error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
console.log('All tasks completed, closing modal');
|
||||||
// Close the modal
|
// Close the modal
|
||||||
onClose();
|
onClose();
|
||||||
};
|
};
|
||||||
|
|||||||
221
frontend/src/components/MainDashboard/components/ToolsModal.tsx
Normal file
221
frontend/src/components/MainDashboard/components/ToolsModal.tsx
Normal file
@@ -0,0 +1,221 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import {
|
||||||
|
Box,
|
||||||
|
Modal,
|
||||||
|
Typography,
|
||||||
|
IconButton,
|
||||||
|
Grid,
|
||||||
|
Stack,
|
||||||
|
Chip,
|
||||||
|
Divider
|
||||||
|
} from '@mui/material';
|
||||||
|
import { motion, AnimatePresence } from 'framer-motion';
|
||||||
|
import {
|
||||||
|
Close as CloseIcon,
|
||||||
|
Search as SearchIcon
|
||||||
|
} from '@mui/icons-material';
|
||||||
|
import ToolCard from '../../shared/ToolCard';
|
||||||
|
import { Tool } from '../../shared/types';
|
||||||
|
import { getToolsForCategory } from '../../shared/utils';
|
||||||
|
|
||||||
|
interface ToolsModalProps {
|
||||||
|
open: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
categoryName?: string;
|
||||||
|
category?: any;
|
||||||
|
searchQuery?: string;
|
||||||
|
searchResults?: Tool[];
|
||||||
|
onToolClick: (tool: Tool) => void;
|
||||||
|
favorites: string[];
|
||||||
|
onToggleFavorite: (toolName: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ToolsModal: React.FC<ToolsModalProps> = ({
|
||||||
|
open,
|
||||||
|
onClose,
|
||||||
|
categoryName,
|
||||||
|
category,
|
||||||
|
searchQuery,
|
||||||
|
searchResults,
|
||||||
|
onToolClick,
|
||||||
|
favorites,
|
||||||
|
onToggleFavorite
|
||||||
|
}) => {
|
||||||
|
const isSearchMode = !!searchQuery;
|
||||||
|
|
||||||
|
// Handle different modes: search, all tools, or specific category
|
||||||
|
let tools: Tool[] = [];
|
||||||
|
if (isSearchMode) {
|
||||||
|
tools = searchResults || [];
|
||||||
|
} else if (categoryName === null) {
|
||||||
|
// All Tools mode - get tools from all categories
|
||||||
|
const allTools: Tool[] = [];
|
||||||
|
if (category && typeof category === 'object') {
|
||||||
|
// category is the entire toolCategories object
|
||||||
|
Object.values(category).forEach((cat: any) => {
|
||||||
|
if (cat && typeof cat === 'object') {
|
||||||
|
// Check if this is a valid category with tools or subCategories
|
||||||
|
if ('tools' in cat || 'subCategories' in cat) {
|
||||||
|
const categoryTools = getToolsForCategory(cat, null);
|
||||||
|
if (categoryTools && Array.isArray(categoryTools)) {
|
||||||
|
allTools.push(...categoryTools);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
tools = allTools;
|
||||||
|
} else {
|
||||||
|
// Specific category mode
|
||||||
|
const categoryTools = getToolsForCategory(category || null, null);
|
||||||
|
tools = categoryTools && Array.isArray(categoryTools) ? categoryTools : [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure tools is always an array
|
||||||
|
if (!Array.isArray(tools)) {
|
||||||
|
tools = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const title = isSearchMode ? `Search Results for "${searchQuery}"` : categoryName || 'All Tools';
|
||||||
|
const subtitle = isSearchMode ? `${tools.length} tools found` : `${tools.length} tools available`;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
open={open}
|
||||||
|
onClose={onClose}
|
||||||
|
sx={{
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'flex-start',
|
||||||
|
justifyContent: 'center',
|
||||||
|
p: 1,
|
||||||
|
mt: 2
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, scale: 0.95, y: 20 }}
|
||||||
|
animate={{ opacity: 1, scale: 1, y: 0 }}
|
||||||
|
exit={{ opacity: 0, scale: 0.95, y: 20 }}
|
||||||
|
transition={{ duration: 0.3 }}
|
||||||
|
style={{ width: '100%', height: '100%' }}
|
||||||
|
>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
width: '98%',
|
||||||
|
maxWidth: 'none',
|
||||||
|
height: '95vh',
|
||||||
|
overflow: 'hidden',
|
||||||
|
background: 'linear-gradient(180deg, rgba(16,24,39,0.98) 0%, rgba(26,33,56,0.98) 100%)',
|
||||||
|
border: '1px solid rgba(255,255,255,0.2)',
|
||||||
|
borderRadius: 2,
|
||||||
|
boxShadow: '0 32px 100px rgba(0,0,0,0.6)',
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
backdropFilter: 'blur(20px)'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{/* Header */}
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
p: 3,
|
||||||
|
borderBottom: '1px solid rgba(255,255,255,0.15)',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'space-between'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2 }}>
|
||||||
|
{isSearchMode ? (
|
||||||
|
<SearchIcon sx={{ color: 'rgba(255,255,255,0.8)', fontSize: 28 }} />
|
||||||
|
) : categoryName === null ? (
|
||||||
|
<SearchIcon sx={{ color: 'rgba(255,255,255,0.8)', fontSize: 28 }} />
|
||||||
|
) : (
|
||||||
|
category?.icon && (
|
||||||
|
<Box sx={{ color: 'rgba(255,255,255,0.8)', fontSize: 28 }}>
|
||||||
|
{category.icon}
|
||||||
|
</Box>
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
<Box>
|
||||||
|
<Typography variant="h5" sx={{ color: 'rgba(255,255,255,0.95)', fontWeight: 800 }}>
|
||||||
|
{title}
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body2" sx={{ color: 'rgba(255,255,255,0.7)' }}>
|
||||||
|
{subtitle}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
<IconButton
|
||||||
|
onClick={onClose}
|
||||||
|
sx={{
|
||||||
|
color: 'rgba(255,255,255,0.8)',
|
||||||
|
backgroundColor: 'rgba(255,255,255,0.1)',
|
||||||
|
'&:hover': {
|
||||||
|
backgroundColor: 'rgba(255,255,255,0.2)'
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<CloseIcon />
|
||||||
|
</IconButton>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{/* Content */}
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
flex: 1,
|
||||||
|
overflow: 'auto',
|
||||||
|
p: 3
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{tools.length === 0 ? (
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
py: 8,
|
||||||
|
textAlign: 'center'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Typography variant="h6" sx={{ color: 'rgba(255,255,255,0.7)', mb: 2 }}>
|
||||||
|
{isSearchMode ? 'No tools found' : 'No tools available'}
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body2" sx={{ color: 'rgba(255,255,255,0.5)' }}>
|
||||||
|
{isSearchMode
|
||||||
|
? 'Try adjusting your search terms or browse categories'
|
||||||
|
: 'This category is currently empty'
|
||||||
|
}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
) : (
|
||||||
|
<Grid container spacing={2.5}>
|
||||||
|
<AnimatePresence>
|
||||||
|
{tools.map((tool: Tool, index: number) => (
|
||||||
|
<Grid item xs={12} sm={6} md={4} lg={3} xl={2.4} key={tool.name}>
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
exit={{ opacity: 0, y: -20 }}
|
||||||
|
transition={{ duration: 0.3, delay: index * 0.03 }}
|
||||||
|
style={{ height: '100%' }}
|
||||||
|
>
|
||||||
|
<ToolCard
|
||||||
|
tool={tool}
|
||||||
|
onToolClick={onToolClick}
|
||||||
|
isFavorite={favorites.includes(tool.name)}
|
||||||
|
onToggleFavorite={onToggleFavorite}
|
||||||
|
/>
|
||||||
|
</motion.div>
|
||||||
|
</Grid>
|
||||||
|
))}
|
||||||
|
</AnimatePresence>
|
||||||
|
</Grid>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
</motion.div>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ToolsModal;
|
||||||
@@ -171,7 +171,9 @@ const DashboardHeader: React.FC<DashboardHeaderProps> = ({
|
|||||||
width: '100%',
|
width: '100%',
|
||||||
height: '100%',
|
height: '100%',
|
||||||
background: 'linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.6), transparent)',
|
background: 'linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.6), transparent)',
|
||||||
animation: 'shimmer 2.5s infinite',
|
animation: workflowControls.completedTasks === workflowControls.totalTasks
|
||||||
|
? 'none'
|
||||||
|
: 'shimmer 2.5s infinite',
|
||||||
zIndex: 1,
|
zIndex: 1,
|
||||||
},
|
},
|
||||||
'&::after': {
|
'&::after': {
|
||||||
|
|||||||
@@ -24,7 +24,8 @@ const SearchFilter: React.FC<SearchFilterProps> = ({
|
|||||||
selectedSubCategory,
|
selectedSubCategory,
|
||||||
onSubCategoryChange,
|
onSubCategoryChange,
|
||||||
toolCategories,
|
toolCategories,
|
||||||
theme
|
theme,
|
||||||
|
onCategoryClick
|
||||||
}) => {
|
}) => {
|
||||||
// Helper function to get tool count from a category
|
// Helper function to get tool count from a category
|
||||||
const getToolCount = (category: any): number => {
|
const getToolCount = (category: any): number => {
|
||||||
@@ -103,7 +104,7 @@ const SearchFilter: React.FC<SearchFilterProps> = ({
|
|||||||
<Box sx={{ display: 'flex', gap: 1, flexWrap: 'wrap', alignItems: 'center' }}>
|
<Box sx={{ display: 'flex', gap: 1, flexWrap: 'wrap', alignItems: 'center' }}>
|
||||||
<CategoryChip
|
<CategoryChip
|
||||||
label="All Tools"
|
label="All Tools"
|
||||||
onClick={() => onCategoryChange(null)}
|
onClick={() => onCategoryClick ? onCategoryClick(null, toolCategories) : onCategoryChange(null)}
|
||||||
active={selectedCategory === null}
|
active={selectedCategory === null}
|
||||||
theme={theme}
|
theme={theme}
|
||||||
toolCount={Object.values(toolCategories).reduce((total, category) => total + getToolCount(category), 0)}
|
toolCount={Object.values(toolCategories).reduce((total, category) => total + getToolCount(category), 0)}
|
||||||
@@ -122,7 +123,7 @@ const SearchFilter: React.FC<SearchFilterProps> = ({
|
|||||||
>
|
>
|
||||||
<CategoryChip
|
<CategoryChip
|
||||||
label={category}
|
label={category}
|
||||||
onClick={() => onCategoryChange(category)}
|
onClick={() => onCategoryClick ? onCategoryClick(category, cat) : onCategoryChange(category)}
|
||||||
active={selectedCategory === category}
|
active={selectedCategory === category}
|
||||||
theme={theme}
|
theme={theme}
|
||||||
toolCount={getToolCount(cat)}
|
toolCount={getToolCount(cat)}
|
||||||
|
|||||||
@@ -73,6 +73,7 @@ export interface SearchFilterProps {
|
|||||||
onSubCategoryChange: (subCategory: string | null) => void;
|
onSubCategoryChange: (subCategory: string | null) => void;
|
||||||
toolCategories: ToolCategories;
|
toolCategories: ToolCategories;
|
||||||
theme: any;
|
theme: any;
|
||||||
|
onCategoryClick?: (category: string | null, categoryData?: any) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface DashboardHeaderProps {
|
export interface DashboardHeaderProps {
|
||||||
|
|||||||
@@ -1,19 +1,26 @@
|
|||||||
import { Category, Tool, ToolCategories } from './types';
|
import { Category, Tool, ToolCategories } from './types';
|
||||||
|
|
||||||
// Utility functions for dashboard components
|
// Utility functions for dashboard components
|
||||||
export const getToolsForCategory = (category: Category, selectedSubCategory: string | null): Tool[] => {
|
export const getToolsForCategory = (category: Category | null, selectedSubCategory: string | null): Tool[] => {
|
||||||
|
if (!category) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
if ('subCategories' in category) {
|
if ('subCategories' in category) {
|
||||||
if (selectedSubCategory && category.subCategories[selectedSubCategory]) {
|
if (selectedSubCategory && category.subCategories[selectedSubCategory]) {
|
||||||
return category.subCategories[selectedSubCategory].tools;
|
const subCategory = category.subCategories[selectedSubCategory];
|
||||||
|
return subCategory && subCategory.tools ? subCategory.tools : [];
|
||||||
}
|
}
|
||||||
// When no subcategory is selected, return all tools from all subcategories
|
// When no subcategory is selected, return all tools from all subcategories
|
||||||
const allTools: Tool[] = [];
|
const allTools: Tool[] = [];
|
||||||
Object.values(category.subCategories).forEach(subCategory => {
|
Object.values(category.subCategories).forEach(subCategory => {
|
||||||
allTools.push(...subCategory.tools);
|
if (subCategory && subCategory.tools && Array.isArray(subCategory.tools)) {
|
||||||
|
allTools.push(...subCategory.tools);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
return allTools;
|
return allTools;
|
||||||
}
|
}
|
||||||
return category.tools;
|
return category.tools && Array.isArray(category.tools) ? category.tools : [];
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getFilteredCategories = (
|
export const getFilteredCategories = (
|
||||||
|
|||||||
Reference in New Issue
Block a user