mirror of
https://github.com/iib0011/omni-tools.git
synced 2025-09-23 16:09:30 +02:00
fix: misc
This commit is contained in:
@@ -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 />
|
||||||
|
@@ -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) => {
|
||||||
|
@@ -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
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
@@ -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} />
|
||||||
))}
|
))}
|
||||||
|
@@ -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>
|
||||||
);
|
);
|
||||||
|
@@ -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
|
||||||
|
77
src/providers/UserTypeFilterProvider.tsx
Normal file
77
src/providers/UserTypeFilterProvider.tsx
Normal 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;
|
||||||
|
}
|
@@ -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;
|
||||||
|
Reference in New Issue
Block a user