From ed81954bf81c318171a16bec7b3b389c662ef8a8 Mon Sep 17 00:00:00 2001 From: "Ibrahima G. Coulibaly" Date: Tue, 22 Jul 2025 18:53:03 +0100 Subject: [PATCH] fix: misc --- src/components/App.tsx | 27 +++++---- src/components/Hero.tsx | 14 +---- src/components/UserTypeFilter.tsx | 77 +++++++++--------------- src/pages/home/Categories.tsx | 5 +- src/pages/home/index.tsx | 20 +++--- src/pages/tools-by-category/index.tsx | 52 ++++++++-------- src/providers/UserTypeFilterProvider.tsx | 77 ++++++++++++++++++++++++ src/tools/defineTool.tsx | 6 +- 8 files changed, 159 insertions(+), 119 deletions(-) create mode 100644 src/providers/UserTypeFilterProvider.tsx diff --git a/src/components/App.tsx b/src/components/App.tsx index b71abd8..a578cc8 100644 --- a/src/components/App.tsx +++ b/src/components/App.tsx @@ -12,6 +12,7 @@ import { darkTheme, lightTheme } from '../config/muiConfig'; import ScrollToTopButton from './ScrollToTopButton'; import { I18nextProvider } from 'react-i18next'; import i18n from '../i18n'; +import { UserTypeFilterProvider } from 'providers/UserTypeFilterProvider'; export type Mode = 'dark' | 'light' | 'system'; @@ -57,18 +58,20 @@ function App() { }} > - - { - setMode((prev) => nextMode(prev)); - localStorage.setItem('theme', nextMode(mode)); - }} - /> - }> - - - + + + { + setMode((prev) => nextMode(prev)); + localStorage.setItem('theme', nextMode(mode)); + }} + /> + }> + + + + diff --git a/src/components/Hero.tsx b/src/components/Hero.tsx index 0b4d2d3..0404216 100644 --- a/src/components/Hero.tsx +++ b/src/components/Hero.tsx @@ -17,7 +17,6 @@ import { filterTools, tools } from '@tools/index'; import { useNavigate } from 'react-router-dom'; import { Icon } from '@iconify/react'; import { getToolCategoryTitle } from '@utils/string'; -import UserTypeFilter, { useUserTypeFilter } from './UserTypeFilter'; import { useTranslation } from 'react-i18next'; import { FullI18nKey, validNamespaces } from '../i18n'; import { @@ -26,6 +25,7 @@ import { toggleBookmarked } from '@utils/bookmark'; import IconButton from '@mui/material/IconButton'; +import { useUserTypeFilter } from '../providers/UserTypeFilterProvider'; const GroupHeader = styled('div')(({ theme }) => ({ position: 'sticky', @@ -51,7 +51,7 @@ export default function Hero() { const { t } = useTranslation(validNamespaces); const [inputValue, setInputValue] = useState(''); const theme = useTheme(); - const { selectedUserTypes, setSelectedUserTypes } = useUserTypeFilter(); + const { selectedUserTypes } = useUserTypeFilter(); const [filteredTools, setFilteredTools] = useState(tools); const [bookmarkedToolPaths, setBookmarkedToolPaths] = useState( getBookmarkedToolPaths() @@ -105,11 +105,6 @@ export default function Hero() { setFilteredTools(filterTools(tools, newInputValue, selectedUserTypes, t)); }; - const handleUserTypesChange = (userTypes: string[]) => { - setSelectedUserTypes(userTypes as any); - setFilteredTools(filterTools(tools, inputValue, userTypes as any, t)); - }; - const toolsMap = new Map(); for (const tool of filteredTools) { toolsMap.set(tool.path, { @@ -225,11 +220,6 @@ export default function Hero() { /> - )} onChange={(event, newValue) => { diff --git a/src/components/UserTypeFilter.tsx b/src/components/UserTypeFilter.tsx index 0eea9b8..6ae2ac7 100644 --- a/src/components/UserTypeFilter.tsx +++ b/src/components/UserTypeFilter.tsx @@ -2,66 +2,43 @@ import React, { useState, useEffect } from 'react'; import { Box, Chip, Typography } from '@mui/material'; import { UserType } from '@tools/defineTool'; -const userTypes: UserType[] = [ - 'General Users', - 'Developers', - 'Designers', - 'CyberSec' -]; +const userTypes: UserType[] = ['General Users', 'Developers', 'CyberSec']; interface UserTypeFilterProps { selectedUserTypes: UserType[]; onUserTypesChange: (userTypes: UserType[]) => void; - label?: string; } export default function UserTypeFilter({ selectedUserTypes, - onUserTypesChange, - label = 'Filter by User Type' + onUserTypesChange }: UserTypeFilterProps) { return ( - - - {label} - - - {userTypes.map((userType) => ( - { - const isSelected = selectedUserTypes.includes(userType); - const newUserTypes = isSelected - ? selectedUserTypes.filter((ut) => ut !== userType) - : [...selectedUserTypes, userType]; - onUserTypesChange(newUserTypes); - }} - sx={{ cursor: 'pointer' }} - /> - ))} - + + {userTypes.map((userType) => ( + { + const isSelected = selectedUserTypes.includes(userType); + const newUserTypes = isSelected + ? selectedUserTypes.filter((ut) => ut !== userType) + : [...selectedUserTypes, userType]; + onUserTypesChange(newUserTypes); + }} + sx={{ cursor: 'pointer' }} + /> + ))} ); } - -// Hook to manage user type filter state with localStorage -export function useUserTypeFilter() { - const [selectedUserTypes, setSelectedUserTypes] = useState(() => { - const saved = localStorage.getItem('selectedUserTypes'); - return saved ? JSON.parse(saved) : []; - }); - - useEffect(() => { - localStorage.setItem( - 'selectedUserTypes', - JSON.stringify(selectedUserTypes) - ); - }, [selectedUserTypes]); - - return { - selectedUserTypes, - setSelectedUserTypes - }; -} diff --git a/src/pages/home/Categories.tsx b/src/pages/home/Categories.tsx index 99d943d..d5f7e27 100644 --- a/src/pages/home/Categories.tsx +++ b/src/pages/home/Categories.tsx @@ -7,10 +7,9 @@ import Button from '@mui/material/Button'; import { useState } from 'react'; import { categoriesColors } from 'config/uiConfig'; import { Icon } from '@iconify/react'; -import { useUserTypeFilter } from '@components/UserTypeFilter'; import { useTranslation } from 'react-i18next'; import { getI18nNamespaceFromToolCategory } from '@utils/string'; -import { validNamespaces } from '../../i18n'; +import { useUserTypeFilter } from '../../providers/UserTypeFilterProvider'; type ArrayElement = ArrayType extends readonly (infer ElementType)[] ? ElementType : never; @@ -119,7 +118,7 @@ export default function Categories() { const categories = getToolsByCategory(selectedUserTypes, t); return ( - + {categories.map((category, index) => ( ))} diff --git a/src/pages/home/index.tsx b/src/pages/home/index.tsx index 0d2fd92..c65d24f 100644 --- a/src/pages/home/index.tsx +++ b/src/pages/home/index.tsx @@ -2,17 +2,12 @@ import { Box, useTheme } from '@mui/material'; import Hero from 'components/Hero'; import Categories from './Categories'; import { Helmet } from 'react-helmet'; -import UserTypeFilter, { useUserTypeFilter } from 'components/UserTypeFilter'; -import { UserType } from '@tools/defineTool'; +import { useUserTypeFilter } from 'providers/UserTypeFilterProvider'; +import UserTypeFilter from '@components/UserTypeFilter'; export default function Home() { const theme = useTheme(); const { selectedUserTypes, setSelectedUserTypes } = useUserTypeFilter(); - - const handleUserTypesChange = (userTypes: UserType[]) => { - setSelectedUserTypes(userTypes); - }; - return ( - + + + ); diff --git a/src/pages/tools-by-category/index.tsx b/src/pages/tools-by-category/index.tsx index 24fb833..dae4f69 100644 --- a/src/pages/tools-by-category/index.tsx +++ b/src/pages/tools-by-category/index.tsx @@ -22,9 +22,10 @@ import IconButton from '@mui/material/IconButton'; import ArrowBackIcon from '@mui/icons-material/ArrowBack'; import SearchIcon from '@mui/icons-material/Search'; import { Helmet } from 'react-helmet'; -import UserTypeFilter, { useUserTypeFilter } from '@components/UserTypeFilter'; +import UserTypeFilter from '@components/UserTypeFilter'; import { useTranslation } from 'react-i18next'; import { I18nNamespaces, validNamespaces } from '../../i18n'; +import { useUserTypeFilter } from '../../providers/UserTypeFilterProvider'; const StyledLink = styled(Link)(({ theme }) => ({ '&:hover': { @@ -60,10 +61,6 @@ export default function ToolsByCategory() { } }, []); - const handleUserTypesChange = (userTypes: string[]) => { - setSelectedUserTypes(userTypes as any); - }; - return ( @@ -90,27 +87,32 @@ export default function ToolsByCategory() { {t('translation:toolLayout.allToolsTitle', { type: rawTitle })} - - , - sx: { - borderRadius: 4, - backgroundColor: 'background.paper', - maxWidth: 400 - } - }} - onChange={(event) => setSearchTerm(event.target.value)} - /> - - + , + sx: { + borderRadius: 4, + backgroundColor: 'background.paper', + maxWidth: 400 + } + }} + onChange={(event) => setSearchTerm(event.target.value)} + /> - + + + + {categoryTools.map((tool, index) => ( void; +} + +const UserTypeFilterContext = createContext( + null +); + +interface UserTypeFilterProviderProps { + children: ReactNode; +} + +export function UserTypeFilterProvider({ + children +}: UserTypeFilterProviderProps) { + const [selectedUserTypes, setSelectedUserTypes] = useState(() => { + try { + const saved = localStorage.getItem('selectedUserTypes'); + return saved ? JSON.parse(saved) : []; + } catch (error) { + console.error( + 'Error loading selectedUserTypes from localStorage:', + error + ); + return []; + } + }); + + useEffect(() => { + try { + localStorage.setItem( + 'selectedUserTypes', + JSON.stringify(selectedUserTypes) + ); + } catch (error) { + console.error('Error saving selectedUserTypes to localStorage:', error); + } + }, [selectedUserTypes]); + + const contextValue = useMemo( + () => ({ + selectedUserTypes, + setSelectedUserTypes + }), + [selectedUserTypes] + ); + + return ( + + {children} + + ); +} + +export function useUserTypeFilter(): UserTypeFilterContextType { + const context = useContext(UserTypeFilterContext); + + if (!context) { + throw new Error( + 'useUserTypeFilter must be used within a UserTypeFilterProvider. ' + + 'Make sure your component is wrapped with .' + ); + } + + return context; +} diff --git a/src/tools/defineTool.tsx b/src/tools/defineTool.tsx index 32f64fb..ae60e84 100644 --- a/src/tools/defineTool.tsx +++ b/src/tools/defineTool.tsx @@ -4,11 +4,7 @@ import { IconifyIcon } from '@iconify/react'; import { FullI18nKey, validNamespaces } from '../i18n'; import { useTranslation } from 'react-i18next'; -export type UserType = - | 'General Users' - | 'Developers' - | 'Designers' - | 'CyberSec'; +export type UserType = 'General Users' | 'Developers' | 'CyberSec'; export interface ToolMeta { path: string;