fix: misc

This commit is contained in:
Ibrahima G. Coulibaly
2025-07-22 18:53:03 +01:00
parent d108fda6b5
commit ed81954bf8
8 changed files with 159 additions and 119 deletions

View File

@@ -12,6 +12,7 @@ import { darkTheme, lightTheme } from '../config/muiConfig';
import ScrollToTopButton from './ScrollToTopButton'; import ScrollToTopButton from './ScrollToTopButton';
import { I18nextProvider } from 'react-i18next'; import { I18nextProvider } from 'react-i18next';
import i18n from '../i18n'; import i18n from '../i18n';
import { UserTypeFilterProvider } from 'providers/UserTypeFilterProvider';
export type Mode = 'dark' | 'light' | 'system'; export type Mode = 'dark' | 'light' | 'system';
@@ -57,18 +58,20 @@ function App() {
}} }}
> >
<CustomSnackBarProvider> <CustomSnackBarProvider>
<BrowserRouter> <UserTypeFilterProvider>
<Navbar <BrowserRouter>
mode={mode} <Navbar
onChangeMode={() => { mode={mode}
setMode((prev) => nextMode(prev)); onChangeMode={() => {
localStorage.setItem('theme', nextMode(mode)); setMode((prev) => nextMode(prev));
}} localStorage.setItem('theme', nextMode(mode));
/> }}
<Suspense fallback={<Loading />}> />
<AppRoutes /> <Suspense fallback={<Loading />}>
</Suspense> <AppRoutes />
</BrowserRouter> </Suspense>
</BrowserRouter>
</UserTypeFilterProvider>
</CustomSnackBarProvider> </CustomSnackBarProvider>
</SnackbarProvider> </SnackbarProvider>
<ScrollToTopButton /> <ScrollToTopButton />

View File

@@ -17,7 +17,6 @@ import { filterTools, tools } from '@tools/index';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { Icon } from '@iconify/react'; import { Icon } from '@iconify/react';
import { getToolCategoryTitle } from '@utils/string'; import { getToolCategoryTitle } from '@utils/string';
import UserTypeFilter, { useUserTypeFilter } from './UserTypeFilter';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { FullI18nKey, validNamespaces } from '../i18n'; import { FullI18nKey, validNamespaces } from '../i18n';
import { import {
@@ -26,6 +25,7 @@ import {
toggleBookmarked toggleBookmarked
} from '@utils/bookmark'; } from '@utils/bookmark';
import IconButton from '@mui/material/IconButton'; import IconButton from '@mui/material/IconButton';
import { useUserTypeFilter } from '../providers/UserTypeFilterProvider';
const GroupHeader = styled('div')(({ theme }) => ({ const GroupHeader = styled('div')(({ theme }) => ({
position: 'sticky', position: 'sticky',
@@ -51,7 +51,7 @@ export default function Hero() {
const { t } = useTranslation(validNamespaces); const { t } = useTranslation(validNamespaces);
const [inputValue, setInputValue] = useState<string>(''); const [inputValue, setInputValue] = useState<string>('');
const theme = useTheme(); const theme = useTheme();
const { selectedUserTypes, setSelectedUserTypes } = useUserTypeFilter(); const { selectedUserTypes } = useUserTypeFilter();
const [filteredTools, setFilteredTools] = useState<DefinedTool[]>(tools); const [filteredTools, setFilteredTools] = useState<DefinedTool[]>(tools);
const [bookmarkedToolPaths, setBookmarkedToolPaths] = useState<string[]>( const [bookmarkedToolPaths, setBookmarkedToolPaths] = useState<string[]>(
getBookmarkedToolPaths() getBookmarkedToolPaths()
@@ -105,11 +105,6 @@ export default function Hero() {
setFilteredTools(filterTools(tools, newInputValue, selectedUserTypes, t)); 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<string, ToolInfo>(); const toolsMap = new Map<string, ToolInfo>();
for (const tool of filteredTools) { for (const tool of filteredTools) {
toolsMap.set(tool.path, { toolsMap.set(tool.path, {
@@ -225,11 +220,6 @@ export default function Hero() {
/> />
</IconButton> </IconButton>
</Stack> </Stack>
<UserTypeFilter
selectedUserTypes={selectedUserTypes}
onUserTypesChange={handleUserTypesChange}
label="User Type"
/>
</Box> </Box>
)} )}
onChange={(event, newValue) => { onChange={(event, newValue) => {

View File

@@ -2,66 +2,43 @@ import React, { useState, useEffect } from 'react';
import { Box, Chip, Typography } from '@mui/material'; import { Box, Chip, Typography } from '@mui/material';
import { UserType } from '@tools/defineTool'; import { UserType } from '@tools/defineTool';
const userTypes: UserType[] = [ const userTypes: UserType[] = ['General Users', 'Developers', 'CyberSec'];
'General Users',
'Developers',
'Designers',
'CyberSec'
];
interface UserTypeFilterProps { interface UserTypeFilterProps {
selectedUserTypes: UserType[]; selectedUserTypes: UserType[];
onUserTypesChange: (userTypes: UserType[]) => void; onUserTypesChange: (userTypes: UserType[]) => void;
label?: string;
} }
export default function UserTypeFilter({ export default function UserTypeFilter({
selectedUserTypes, selectedUserTypes,
onUserTypesChange, onUserTypesChange
label = 'Filter by User Type'
}: UserTypeFilterProps) { }: UserTypeFilterProps) {
return ( return (
<Box sx={{ minWidth: 200 }}> <Box
<Typography variant="subtitle2" sx={{ mb: 1 }}> sx={{
{label} display: 'flex',
</Typography> flexWrap: 'wrap',
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 1 }}> gap: 1,
{userTypes.map((userType) => ( minWidth: 200,
<Chip alignItems: 'center',
key={userType} justifyContent: 'center'
label={userType} }}
color={selectedUserTypes.includes(userType) ? 'primary' : 'default'} >
onClick={() => { {userTypes.map((userType) => (
const isSelected = selectedUserTypes.includes(userType); <Chip
const newUserTypes = isSelected key={userType}
? selectedUserTypes.filter((ut) => ut !== userType) label={userType}
: [...selectedUserTypes, userType]; color={selectedUserTypes.includes(userType) ? 'primary' : 'default'}
onUserTypesChange(newUserTypes); onClick={() => {
}} const isSelected = selectedUserTypes.includes(userType);
sx={{ cursor: 'pointer' }} const newUserTypes = isSelected
/> ? selectedUserTypes.filter((ut) => ut !== userType)
))} : [...selectedUserTypes, userType];
</Box> onUserTypesChange(newUserTypes);
}}
sx={{ cursor: 'pointer' }}
/>
))}
</Box> </Box>
); );
} }
// Hook to manage user type filter state with localStorage
export function useUserTypeFilter() {
const [selectedUserTypes, setSelectedUserTypes] = useState<UserType[]>(() => {
const saved = localStorage.getItem('selectedUserTypes');
return saved ? JSON.parse(saved) : [];
});
useEffect(() => {
localStorage.setItem(
'selectedUserTypes',
JSON.stringify(selectedUserTypes)
);
}, [selectedUserTypes]);
return {
selectedUserTypes,
setSelectedUserTypes
};
}

View File

@@ -7,10 +7,9 @@ import Button from '@mui/material/Button';
import { useState } from 'react'; import { useState } from 'react';
import { categoriesColors } from 'config/uiConfig'; import { categoriesColors } from 'config/uiConfig';
import { Icon } from '@iconify/react'; import { Icon } from '@iconify/react';
import { useUserTypeFilter } from '@components/UserTypeFilter';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { getI18nNamespaceFromToolCategory } from '@utils/string'; import { getI18nNamespaceFromToolCategory } from '@utils/string';
import { validNamespaces } from '../../i18n'; import { useUserTypeFilter } from '../../providers/UserTypeFilterProvider';
type ArrayElement<ArrayType extends readonly unknown[]> = type ArrayElement<ArrayType extends readonly unknown[]> =
ArrayType extends readonly (infer ElementType)[] ? ElementType : never; ArrayType extends readonly (infer ElementType)[] ? ElementType : never;
@@ -119,7 +118,7 @@ export default function Categories() {
const categories = getToolsByCategory(selectedUserTypes, t); const categories = getToolsByCategory(selectedUserTypes, t);
return ( return (
<Grid width={'80%'} container mt={2} spacing={2}> <Grid width={'80%'} container spacing={2}>
{categories.map((category, index) => ( {categories.map((category, index) => (
<SingleCategory key={category.type} category={category} index={index} /> <SingleCategory key={category.type} category={category} index={index} />
))} ))}

View File

@@ -2,17 +2,12 @@ import { Box, useTheme } from '@mui/material';
import Hero from 'components/Hero'; import Hero from 'components/Hero';
import Categories from './Categories'; import Categories from './Categories';
import { Helmet } from 'react-helmet'; import { Helmet } from 'react-helmet';
import UserTypeFilter, { useUserTypeFilter } from 'components/UserTypeFilter'; import { useUserTypeFilter } from 'providers/UserTypeFilterProvider';
import { UserType } from '@tools/defineTool'; import UserTypeFilter from '@components/UserTypeFilter';
export default function Home() { export default function Home() {
const theme = useTheme(); const theme = useTheme();
const { selectedUserTypes, setSelectedUserTypes } = useUserTypeFilter(); const { selectedUserTypes, setSelectedUserTypes } = useUserTypeFilter();
const handleUserTypesChange = (userTypes: UserType[]) => {
setSelectedUserTypes(userTypes);
};
return ( return (
<Box <Box
padding={{ padding={{
@@ -36,11 +31,12 @@ export default function Home() {
> >
<Helmet title={'OmniTools'} /> <Helmet title={'OmniTools'} />
<Hero /> <Hero />
<UserTypeFilter <Box my={3}>
selectedUserTypes={selectedUserTypes} <UserTypeFilter
onUserTypesChange={handleUserTypesChange} selectedUserTypes={selectedUserTypes}
label="Filter by User Type" onUserTypesChange={setSelectedUserTypes}
/> />
</Box>
<Categories /> <Categories />
</Box> </Box>
); );

View File

@@ -22,9 +22,10 @@ import IconButton from '@mui/material/IconButton';
import ArrowBackIcon from '@mui/icons-material/ArrowBack'; import ArrowBackIcon from '@mui/icons-material/ArrowBack';
import SearchIcon from '@mui/icons-material/Search'; import SearchIcon from '@mui/icons-material/Search';
import { Helmet } from 'react-helmet'; import { Helmet } from 'react-helmet';
import UserTypeFilter, { useUserTypeFilter } from '@components/UserTypeFilter'; import UserTypeFilter from '@components/UserTypeFilter';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { I18nNamespaces, validNamespaces } from '../../i18n'; import { I18nNamespaces, validNamespaces } from '../../i18n';
import { useUserTypeFilter } from '../../providers/UserTypeFilterProvider';
const StyledLink = styled(Link)(({ theme }) => ({ const StyledLink = styled(Link)(({ theme }) => ({
'&:hover': { '&:hover': {
@@ -60,10 +61,6 @@ export default function ToolsByCategory() {
} }
}, []); }, []);
const handleUserTypesChange = (userTypes: string[]) => {
setSelectedUserTypes(userTypes as any);
};
return ( return (
<Box sx={{ backgroundColor: 'background.default' }}> <Box sx={{ backgroundColor: 'background.default' }}>
<Helmet> <Helmet>
@@ -90,27 +87,32 @@ export default function ToolsByCategory() {
{t('translation:toolLayout.allToolsTitle', { type: rawTitle })} {t('translation:toolLayout.allToolsTitle', { type: rawTitle })}
</Typography> </Typography>
</Stack> </Stack>
<Stack direction={'row'} spacing={2} sx={{ minWidth: 0 }}> <TextField
<TextField placeholder={'Search'}
placeholder={'Search'} InputProps={{
InputProps={{ endAdornment: <SearchIcon />,
endAdornment: <SearchIcon />, sx: {
sx: { borderRadius: 4,
borderRadius: 4, backgroundColor: 'background.paper',
backgroundColor: 'background.paper', maxWidth: 400
maxWidth: 400 }
} }}
}} onChange={(event) => setSearchTerm(event.target.value)}
onChange={(event) => setSearchTerm(event.target.value)} />
/>
<UserTypeFilter
selectedUserTypes={selectedUserTypes}
onUserTypesChange={handleUserTypesChange}
label="User Type"
/>
</Stack>
</Stack> </Stack>
<Grid container spacing={2} mt={2}> <Box
width={'100%'}
display={'flex'}
alignItems={'center'}
justifyContent={'center'}
my={2}
>
<UserTypeFilter
selectedUserTypes={selectedUserTypes}
onUserTypesChange={setSelectedUserTypes}
/>
</Box>
<Grid container spacing={2}>
{categoryTools.map((tool, index) => ( {categoryTools.map((tool, index) => (
<Grid item xs={12} md={6} lg={4} key={tool.path}> <Grid item xs={12} md={6} lg={4} key={tool.path}>
<Stack <Stack

View File

@@ -0,0 +1,77 @@
import React, {
createContext,
useContext,
useState,
useEffect,
ReactNode,
useMemo
} from 'react';
import { UserType } from '@tools/defineTool';
interface UserTypeFilterContextType {
selectedUserTypes: UserType[];
setSelectedUserTypes: (userTypes: UserType[]) => void;
}
const UserTypeFilterContext = createContext<UserTypeFilterContextType | null>(
null
);
interface UserTypeFilterProviderProps {
children: ReactNode;
}
export function UserTypeFilterProvider({
children
}: UserTypeFilterProviderProps) {
const [selectedUserTypes, setSelectedUserTypes] = useState<UserType[]>(() => {
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 (
<UserTypeFilterContext.Provider value={contextValue}>
{children}
</UserTypeFilterContext.Provider>
);
}
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 <UserTypeFilterProvider>.'
);
}
return context;
}

View File

@@ -4,11 +4,7 @@ import { IconifyIcon } from '@iconify/react';
import { FullI18nKey, validNamespaces } from '../i18n'; import { FullI18nKey, validNamespaces } from '../i18n';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
export type UserType = export type UserType = 'General Users' | 'Developers' | 'CyberSec';
| 'General Users'
| 'Developers'
| 'Designers'
| 'CyberSec';
export interface ToolMeta { export interface ToolMeta {
path: string; path: string;