Analytics Insights and Tools Modal
This commit is contained in:
@@ -290,21 +290,48 @@ const PillarCard: React.FC<{
|
||||
onMouseEnter={() => setIsHovered(true)}
|
||||
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 */}
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', mb: 1.5, position: 'relative' }}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', mb: 1, position: 'relative' }}>
|
||||
<Box
|
||||
sx={{
|
||||
p: 0.8,
|
||||
p: 0.6,
|
||||
borderRadius: '50%',
|
||||
backgroundColor: 'rgba(255,255,255,0.2)',
|
||||
mr: 1.2,
|
||||
mr: 1,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center'
|
||||
}}
|
||||
>
|
||||
<IconComponent sx={{ fontSize: 18, color: 'white' }} />
|
||||
<IconComponent sx={{ fontSize: 16, color: 'white' }} />
|
||||
</Box>
|
||||
<Typography variant="h6" sx={{ fontWeight: 700, fontSize: '1rem' }}>
|
||||
{pillar.title}
|
||||
@@ -378,7 +405,7 @@ const PillarCard: React.FC<{
|
||||
transition={{ duration: 0.3, ease: 'easeInOut' }}
|
||||
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' ? (
|
||||
<>
|
||||
<motion.div
|
||||
|
||||
@@ -23,6 +23,7 @@ import ErrorDisplay from '../shared/ErrorDisplay';
|
||||
import EmptyState from '../shared/EmptyState';
|
||||
import ContentLifecyclePillars from './ContentLifecyclePillars';
|
||||
import AnalyticsInsights from './components/AnalyticsInsights';
|
||||
import ToolsModal from './components/ToolsModal';
|
||||
|
||||
// Shared types and utilities
|
||||
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
|
||||
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
|
||||
const handleStartWorkflow = async () => {
|
||||
try {
|
||||
@@ -170,6 +177,56 @@ const MainDashboard: React.FC = () => {
|
||||
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(
|
||||
toolCategories,
|
||||
selectedCategory,
|
||||
@@ -245,9 +302,6 @@ const MainDashboard: React.FC = () => {
|
||||
{/* Content Lifecycle Pillars - First Panel */}
|
||||
<ContentLifecyclePillars />
|
||||
|
||||
{/* Analytics Insights - Good/Bad/Ugly */}
|
||||
<AnalyticsInsights />
|
||||
|
||||
{/* Search and Filter */}
|
||||
<SearchFilter
|
||||
searchQuery={searchQuery}
|
||||
@@ -259,60 +313,24 @@ const MainDashboard: React.FC = () => {
|
||||
onSubCategoryChange={setSelectedSubCategory}
|
||||
toolCategories={toolCategories}
|
||||
theme={theme}
|
||||
onCategoryClick={handleCategoryClick}
|
||||
/>
|
||||
|
||||
{/* Enhanced Tools Grid */}
|
||||
<Box sx={{ mb: 4 }}>
|
||||
{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}
|
||||
/>
|
||||
)}
|
||||
{/* Analytics Insights - Good/Bad/Ugly */}
|
||||
<AnalyticsInsights />
|
||||
|
||||
<Grid container spacing={3}>
|
||||
{getToolsForCategory(category, selectedSubCategory).map((tool: Tool, toolIndex: number) => (
|
||||
<Grid item xs={12} sm={6} md={4} lg={3} key={tool.name}>
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.5, delay: (categoryIndex * 0.1) + (toolIndex * 0.05) }}
|
||||
>
|
||||
<ToolCard
|
||||
tool={tool}
|
||||
onToolClick={handleToolClick}
|
||||
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"
|
||||
/>
|
||||
)}
|
||||
{/* Tools Modal */}
|
||||
<ToolsModal
|
||||
open={toolsModalOpen}
|
||||
onClose={handleCloseModal}
|
||||
categoryName={modalCategoryName || undefined}
|
||||
category={modalCategory}
|
||||
searchQuery={searchQuery}
|
||||
searchResults={searchResults}
|
||||
onToolClick={handleToolClick}
|
||||
favorites={favorites}
|
||||
onToggleFavorite={toggleFavorite}
|
||||
/>
|
||||
</motion.div>
|
||||
</AnimatePresence>
|
||||
|
||||
|
||||
@@ -92,11 +92,6 @@ const Badge = styled('span')(({ theme }) => ({
|
||||
fontSize: '0.65rem'
|
||||
}));
|
||||
|
||||
// Subtle shimmer animation for the title text
|
||||
const shimmerText = keyframes`
|
||||
0% { background-position: -200% 0; }
|
||||
100% { background-position: 200% 0; }
|
||||
`;
|
||||
|
||||
const mockData: AnalyticsData = {
|
||||
theGood: [
|
||||
@@ -268,23 +263,20 @@ const AnalyticsInsights: React.FC<AnalyticsInsightsProps> = ({ data, onActionCli
|
||||
};
|
||||
|
||||
return (
|
||||
<Box sx={{ mt: 2, mb: 2.5 }}>
|
||||
<Typography
|
||||
variant="h6"
|
||||
sx={{
|
||||
fontWeight: 800,
|
||||
mb: 1.5,
|
||||
fontSize: '1.1rem',
|
||||
background: 'linear-gradient(90deg, rgba(255,255,255,0.35), rgba(255,255,255,0.9) 50%, rgba(255,255,255,0.35))',
|
||||
WebkitBackgroundClip: 'text',
|
||||
backgroundClip: 'text',
|
||||
color: 'transparent',
|
||||
backgroundSize: '200% 100%',
|
||||
animation: `${shimmerText} 3.2s linear infinite`,
|
||||
}}
|
||||
>
|
||||
Analytics Insights
|
||||
</Typography>
|
||||
<Box sx={{ mt: 2, mb: 2.9 }}>
|
||||
<Box sx={{ position: 'relative', overflow: 'hidden' }}>
|
||||
<Typography
|
||||
variant="h6"
|
||||
sx={{
|
||||
fontWeight: 800,
|
||||
mb: 1.5,
|
||||
fontSize: '1.1rem',
|
||||
color: 'rgba(255,255,255,0.95)',
|
||||
}}
|
||||
>
|
||||
Today's Analytics Insights
|
||||
</Typography>
|
||||
</Box>
|
||||
<Stack direction={{ xs: 'column', md: 'row' }} spacing={1.5}>
|
||||
{columns.map((col) => {
|
||||
const isHovered = hovered === col.key;
|
||||
|
||||
@@ -128,19 +128,27 @@ const EnhancedTodayModal: React.FC<EnhancedTodayModalProps> = ({
|
||||
};
|
||||
|
||||
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
|
||||
const incompleteTasks = pillarTasks.filter(task =>
|
||||
task.status !== 'completed' && task.status !== 'skipped'
|
||||
);
|
||||
|
||||
console.log('Incomplete tasks to complete:', incompleteTasks);
|
||||
|
||||
for (const task of incompleteTasks) {
|
||||
try {
|
||||
console.log('Completing task:', task.id);
|
||||
await completeTask(task.id);
|
||||
console.log('Task completed successfully:', task.id);
|
||||
} catch (error) {
|
||||
console.error(`Failed to complete task ${task.id}:`, error);
|
||||
}
|
||||
}
|
||||
|
||||
console.log('All tasks completed, closing modal');
|
||||
// Close the modal
|
||||
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%',
|
||||
height: '100%',
|
||||
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,
|
||||
},
|
||||
'&::after': {
|
||||
|
||||
@@ -24,7 +24,8 @@ const SearchFilter: React.FC<SearchFilterProps> = ({
|
||||
selectedSubCategory,
|
||||
onSubCategoryChange,
|
||||
toolCategories,
|
||||
theme
|
||||
theme,
|
||||
onCategoryClick
|
||||
}) => {
|
||||
// Helper function to get tool count from a category
|
||||
const getToolCount = (category: any): number => {
|
||||
@@ -103,7 +104,7 @@ const SearchFilter: React.FC<SearchFilterProps> = ({
|
||||
<Box sx={{ display: 'flex', gap: 1, flexWrap: 'wrap', alignItems: 'center' }}>
|
||||
<CategoryChip
|
||||
label="All Tools"
|
||||
onClick={() => onCategoryChange(null)}
|
||||
onClick={() => onCategoryClick ? onCategoryClick(null, toolCategories) : onCategoryChange(null)}
|
||||
active={selectedCategory === null}
|
||||
theme={theme}
|
||||
toolCount={Object.values(toolCategories).reduce((total, category) => total + getToolCount(category), 0)}
|
||||
@@ -122,7 +123,7 @@ const SearchFilter: React.FC<SearchFilterProps> = ({
|
||||
>
|
||||
<CategoryChip
|
||||
label={category}
|
||||
onClick={() => onCategoryChange(category)}
|
||||
onClick={() => onCategoryClick ? onCategoryClick(category, cat) : onCategoryChange(category)}
|
||||
active={selectedCategory === category}
|
||||
theme={theme}
|
||||
toolCount={getToolCount(cat)}
|
||||
|
||||
@@ -73,6 +73,7 @@ export interface SearchFilterProps {
|
||||
onSubCategoryChange: (subCategory: string | null) => void;
|
||||
toolCategories: ToolCategories;
|
||||
theme: any;
|
||||
onCategoryClick?: (category: string | null, categoryData?: any) => void;
|
||||
}
|
||||
|
||||
export interface DashboardHeaderProps {
|
||||
|
||||
@@ -1,19 +1,26 @@
|
||||
import { Category, Tool, ToolCategories } from './types';
|
||||
|
||||
// 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 (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
|
||||
const allTools: Tool[] = [];
|
||||
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 category.tools;
|
||||
return category.tools && Array.isArray(category.tools) ? category.tools : [];
|
||||
};
|
||||
|
||||
export const getFilteredCategories = (
|
||||
|
||||
Reference in New Issue
Block a user