Added user types to filter the tools based on targeted audience

This commit is contained in:
AshAnand34
2025-07-11 15:25:01 -07:00
parent 3b702b260c
commit 18ba3f70d8
11 changed files with 289 additions and 85 deletions

View File

@@ -18,6 +18,7 @@ import { useNavigate } from 'react-router-dom';
import _ from 'lodash'; import _ from 'lodash';
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';
const GroupHeader = styled('div')(({ theme }) => ({ const GroupHeader = styled('div')(({ theme }) => ({
position: 'sticky', position: 'sticky',
@@ -47,17 +48,25 @@ const exampleTools: { label: string; url: string }[] = [
{ label: 'Trim video', url: '/video/trim' }, { label: 'Trim video', url: '/video/trim' },
{ label: 'Calculate number sum', url: '/number/sum' } { label: 'Calculate number sum', url: '/number/sum' }
]; ];
export default function Hero() { export default function Hero() {
const [inputValue, setInputValue] = useState<string>(''); const [inputValue, setInputValue] = useState<string>('');
const theme = useTheme(); const theme = useTheme();
const { selectedUserTypes, setSelectedUserTypes } = useUserTypeFilter();
const [filteredTools, setFilteredTools] = useState<DefinedTool[]>(tools); const [filteredTools, setFilteredTools] = useState<DefinedTool[]>(tools);
const navigate = useNavigate(); const navigate = useNavigate();
const handleInputChange = ( const handleInputChange = (
event: React.ChangeEvent<{}>, event: React.ChangeEvent<{}>,
newInputValue: string newInputValue: string
) => { ) => {
setInputValue(newInputValue); setInputValue(newInputValue);
setFilteredTools(filterTools(tools, newInputValue)); setFilteredTools(filterTools(tools, newInputValue, selectedUserTypes));
};
const handleUserTypesChange = (userTypes: string[]) => {
setSelectedUserTypes(userTypes as any);
setFilteredTools(filterTools(tools, inputValue, userTypes as any));
}; };
return ( return (
@@ -84,59 +93,69 @@ export default function Hero() {
editing images, text, lists, and data, all directly from your browser. editing images, text, lists, and data, all directly from your browser.
</Typography> </Typography>
<Autocomplete <Stack direction={{ xs: 'column', md: 'row' }} spacing={2} mb={2}>
sx={{ mb: 2 }} <Autocomplete
autoHighlight sx={{ flex: 1 }}
options={filteredTools} autoHighlight
groupBy={(option) => option.type} options={filteredTools}
renderGroup={(params) => { groupBy={(option) => option.type}
return ( renderGroup={(params) => {
<li key={params.key}> return (
<GroupHeader>{getToolCategoryTitle(params.group)}</GroupHeader> <li key={params.key}>
<GroupItems>{params.children}</GroupItems> <GroupHeader>{getToolCategoryTitle(params.group)}</GroupHeader>
</li> <GroupItems>{params.children}</GroupItems>
); </li>
}} );
inputValue={inputValue} }}
getOptionLabel={(option) => option.name} inputValue={inputValue}
renderInput={(params) => ( getOptionLabel={(option) => option.name}
<TextField renderInput={(params) => (
{...params} <TextField
fullWidth {...params}
placeholder={'Search all tools'} fullWidth
InputProps={{ placeholder={'Search all tools'}
...params.InputProps, InputProps={{
endAdornment: <SearchIcon />, ...params.InputProps,
sx: { endAdornment: <SearchIcon />,
borderRadius: 4, sx: {
backgroundColor: 'background.paper' borderRadius: 4,
} backgroundColor: 'background.paper'
}} }
onChange={(event) => handleInputChange(event, event.target.value)} }}
/> onChange={(event) => handleInputChange(event, event.target.value)}
)} />
renderOption={(props, option) => ( )}
<Box renderOption={(props, option) => (
component="li" <Box
{...props} component="li"
onClick={() => navigate('/' + option.path)} {...props}
> onClick={() => navigate('/' + option.path)}
<Stack direction={'row'} spacing={2} alignItems={'center'}> >
<Icon fontSize={20} icon={option.icon} /> <Stack direction={'row'} spacing={2} alignItems={'center'}>
<Box> <Icon fontSize={20} icon={option.icon} />
<Typography fontWeight={'bold'}>{option.name}</Typography> <Box>
<Typography fontSize={12}>{option.shortDescription}</Typography> <Typography fontWeight={'bold'}>{option.name}</Typography>
</Box> <Typography fontSize={12}>
</Stack> {option.shortDescription}
</Box> </Typography>
)} </Box>
onChange={(event, newValue) => { </Stack>
if (newValue) { </Box>
navigate('/' + newValue.path); )}
} onChange={(event, newValue) => {
}} if (newValue) {
/> navigate('/' + newValue.path);
<Grid container spacing={2} mt={2}> }
}}
/>
<UserTypeFilter
selectedUserTypes={selectedUserTypes}
onUserTypesChange={handleUserTypesChange}
label="User Type"
/>
</Stack>
<Grid container spacing={2}>
{exampleTools.map((tool) => ( {exampleTools.map((tool) => (
<Grid <Grid
onClick={() => onClick={() =>

View File

@@ -0,0 +1,105 @@
import React, { useState, useEffect } from 'react';
import {
Box,
Chip,
FormControl,
InputLabel,
MenuItem,
OutlinedInput,
Select,
SelectChangeEvent,
Typography,
useTheme
} from '@mui/material';
import { UserType } from '@tools/defineTool';
const userTypes: UserType[] = [
'General Users',
'Developers',
'Designers',
'Students',
'CyberSec'
];
interface UserTypeFilterProps {
selectedUserTypes: UserType[];
onUserTypesChange: (userTypes: UserType[]) => void;
label?: string;
}
export default function UserTypeFilter({
selectedUserTypes,
onUserTypesChange,
label = 'Filter by User Type'
}: UserTypeFilterProps) {
const theme = useTheme();
const handleChange = (event: SelectChangeEvent<UserType[]>) => {
const {
target: { value }
} = event;
const newUserTypes =
typeof value === 'string' ? (value.split(',') as UserType[]) : value;
onUserTypesChange(newUserTypes);
};
return (
<Box sx={{ minWidth: 200 }}>
<FormControl fullWidth>
<InputLabel id="user-type-filter-label">{label}</InputLabel>
<Select
labelId="user-type-filter-label"
id="user-type-filter"
multiple
value={selectedUserTypes}
onChange={handleChange}
input={<OutlinedInput label={label} />}
renderValue={(selected) => (
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 0.5 }}>
{selected.map((value) => (
<Chip
key={value}
label={value}
size="small"
sx={{
backgroundColor: theme.palette.primary.main,
color: 'white',
'& .MuiChip-deleteIcon': {
color: 'white'
}
}}
/>
))}
</Box>
)}
>
{userTypes.map((userType) => (
<MenuItem key={userType} value={userType}>
{userType}
</MenuItem>
))}
</Select>
</FormControl>
</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,6 +7,7 @@ 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';
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;
@@ -65,7 +66,7 @@ const SingleCategory = function ({
</Stack> </Stack>
<Typography sx={{ mt: 2 }}>{category.description}</Typography> <Typography sx={{ mt: 2 }}>{category.description}</Typography>
</Box> </Box>
<Grid mt={1} container spacing={2}> <Grid container spacing={2} mt={2}>
<Grid item xs={12} md={6}> <Grid item xs={12} md={6}>
<Button <Button
fullWidth fullWidth
@@ -88,10 +89,14 @@ const SingleCategory = function ({
</Grid> </Grid>
); );
}; };
export default function Categories() { export default function Categories() {
const { selectedUserTypes } = useUserTypeFilter();
const categories = getToolsByCategory(selectedUserTypes);
return ( return (
<Grid width={'80%'} container mt={2} spacing={2}> <Grid width={'80%'} container mt={2} spacing={2}>
{getToolsByCategory().map((category, index) => ( {categories.map((category, index) => (
<SingleCategory key={category.type} category={category} index={index} /> <SingleCategory key={category.type} category={category} index={index} />
))} ))}
</Grid> </Grid>

View File

@@ -21,18 +21,21 @@ import BackButton from '@components/BackButton';
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';
const StyledLink = styled(Link)(({ theme }) => ({ const StyledLink = styled(Link)(({ theme }) => ({
'&:hover': { '&:hover': {
color: theme.palette.mode === 'dark' ? 'white' : theme.palette.primary.light color: theme.palette.mode === 'dark' ? 'white' : theme.palette.primary.light
} }
})); }));
export default function ToolsByCategory() { export default function ToolsByCategory() {
const navigate = useNavigate(); const navigate = useNavigate();
const theme = useTheme(); const theme = useTheme();
const mainContentRef = React.useRef<HTMLDivElement>(null); const mainContentRef = React.useRef<HTMLDivElement>(null);
const { categoryName } = useParams(); const { categoryName } = useParams();
const [searchTerm, setSearchTerm] = React.useState<string>(''); const [searchTerm, setSearchTerm] = React.useState<string>('');
const { selectedUserTypes, setSelectedUserTypes } = useUserTypeFilter();
const rawTitle = getToolCategoryTitle(categoryName as string); const rawTitle = getToolCategoryTitle(categoryName as string);
useEffect(() => { useEffect(() => {
@@ -41,6 +44,21 @@ export default function ToolsByCategory() {
} }
}, []); }, []);
const handleUserTypesChange = (userTypes: string[]) => {
setSelectedUserTypes(userTypes as any);
};
const categoryTools =
getToolsByCategory(selectedUserTypes).find(
({ type }) => type === categoryName
)?.tools ?? [];
const filteredTools = filterTools(
categoryTools,
searchTerm,
selectedUserTypes
);
return ( return (
<Box sx={{ backgroundColor: 'background.default' }}> <Box sx={{ backgroundColor: 'background.default' }}>
<Helmet> <Helmet>
@@ -68,25 +86,28 @@ export default function ToolsByCategory() {
color={theme.palette.primary.main} color={theme.palette.primary.main}
>{`All ${rawTitle} Tools`}</Typography> >{`All ${rawTitle} Tools`}</Typography>
</Stack> </Stack>
<TextField <Stack direction={'row'} spacing={2} sx={{ minWidth: 0 }}>
placeholder={'Search'} <TextField
InputProps={{ placeholder={'Search'}
endAdornment: <SearchIcon />, InputProps={{
sx: { endAdornment: <SearchIcon />,
borderRadius: 4, sx: {
backgroundColor: 'background.paper', borderRadius: 4,
maxWidth: 400 backgroundColor: 'background.paper',
} 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}> <Grid container spacing={2} mt={2}>
{filterTools( {filteredTools.map((tool, index) => (
getToolsByCategory().find(({ type }) => type === categoryName)
?.tools ?? [],
searchTerm
).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
sx={{ sx={{

View File

@@ -17,5 +17,6 @@ export const tool = defineTool('json', {
'json', 'json',
'string' 'string'
], ],
userTypes: ['Developers'],
component: lazy(() => import('./index')) component: lazy(() => import('./index'))
}); });

View File

@@ -9,5 +9,6 @@ export const tool = defineTool('string', {
'A simple tool to encode or decode data using Base64, which is commonly used in web applications.', 'A simple tool to encode or decode data using Base64, which is commonly used in web applications.',
shortDescription: 'Encode or decode data using Base64.', shortDescription: 'Encode or decode data using Base64.',
keywords: ['base64'], keywords: ['base64'],
userTypes: ['Developers', 'CyberSec'],
component: lazy(() => import('./index')) component: lazy(() => import('./index'))
}); });

View File

@@ -12,5 +12,6 @@ export const tool = defineTool('string', {
longDescription: longDescription:
'With this online tool, you can censor certain words in any text. You can specify a list of unwanted words (such as swear words or secret words) and the program will replace them with alternative words and create a safe-to-read text. The words can be specified in a multi-line text field in the options by entering one word per line.', 'With this online tool, you can censor certain words in any text. You can specify a list of unwanted words (such as swear words or secret words) and the program will replace them with alternative words and create a safe-to-read text. The words can be specified in a multi-line text field in the options by entering one word per line.',
keywords: ['text', 'censor', 'words', 'characters'], keywords: ['text', 'censor', 'words', 'characters'],
userTypes: ['General Users', 'Students'],
component: lazy(() => import('./index')) component: lazy(() => import('./index'))
}); });

View File

@@ -10,5 +10,6 @@ export const tool = defineTool('string', {
'A simple tool to encode or decode text using the ROT13 cipher, which replaces each letter with the letter 13 positions after it in the alphabet.', 'A simple tool to encode or decode text using the ROT13 cipher, which replaces each letter with the letter 13 positions after it in the alphabet.',
shortDescription: 'Encode or decode text using ROT13 cipher.', shortDescription: 'Encode or decode text using ROT13 cipher.',
keywords: ['rot13'], keywords: ['rot13'],
userTypes: ['Developers', 'CyberSec', 'Students'],
component: lazy(() => import('./index')) component: lazy(() => import('./index'))
}); });

View File

@@ -11,5 +11,6 @@ export const tool = defineTool('string', {
longDescription: longDescription:
'This tool provides various statistics about the text you input, including the number of lines, words, and characters. You can also choose to include empty lines in the count. it can count words and characters based on custom delimiters, allowing for flexible text analysis. Additionally, it can provide frequency statistics for words and characters, helping you understand the distribution of terms in your text.', 'This tool provides various statistics about the text you input, including the number of lines, words, and characters. You can also choose to include empty lines in the count. it can count words and characters based on custom delimiters, allowing for flexible text analysis. Additionally, it can provide frequency statistics for words and characters, helping you understand the distribution of terms in your text.',
keywords: ['text', 'statistics', 'count', 'lines', 'words', 'characters'], keywords: ['text', 'statistics', 'count', 'lines', 'words', 'characters'],
userTypes: ['General Users', 'Students', 'Developers'],
component: lazy(() => import('./index')) component: lazy(() => import('./index'))
}); });

View File

@@ -2,6 +2,13 @@ import ToolLayout from '../components/ToolLayout';
import React, { JSXElementConstructor, LazyExoticComponent } from 'react'; import React, { JSXElementConstructor, LazyExoticComponent } from 'react';
import { IconifyIcon } from '@iconify/react'; import { IconifyIcon } from '@iconify/react';
export type UserType =
| 'General Users'
| 'Developers'
| 'Designers'
| 'Students'
| 'CyberSec';
export interface ToolMeta { export interface ToolMeta {
path: string; path: string;
component: LazyExoticComponent<JSXElementConstructor<ToolComponentProps>>; component: LazyExoticComponent<JSXElementConstructor<ToolComponentProps>>;
@@ -11,20 +18,21 @@ export interface ToolMeta {
description: string; description: string;
shortDescription: string; shortDescription: string;
longDescription?: string; longDescription?: string;
userTypes?: UserType[];
} }
export type ToolCategory = export type ToolCategory =
| 'string' | 'string'
| 'image-generic'
| 'png' | 'png'
| 'number' | 'number'
| 'gif' | 'gif'
| 'video'
| 'list' | 'list'
| 'json' | 'json'
| 'time' | 'time'
| 'csv' | 'csv'
| 'video'
| 'pdf' | 'pdf'
| 'image-generic'
| 'audio' | 'audio'
| 'xml'; | 'xml';
@@ -37,6 +45,7 @@ export interface DefinedTool {
icon: IconifyIcon | string; icon: IconifyIcon | string;
keywords: string[]; keywords: string[];
component: () => JSX.Element; component: () => JSX.Element;
userTypes?: UserType[];
} }
export interface ToolComponentProps { export interface ToolComponentProps {
@@ -56,7 +65,8 @@ export const defineTool = (
keywords, keywords,
component, component,
shortDescription, shortDescription,
longDescription longDescription,
userTypes
} = options; } = options;
const Component = component; const Component = component;
return { return {
@@ -67,6 +77,7 @@ export const defineTool = (
description, description,
shortDescription, shortDescription,
keywords, keywords,
userTypes,
component: () => { component: () => {
return ( return (
<ToolLayout <ToolLayout

View File

@@ -1,6 +1,6 @@
import { stringTools } from '../pages/tools/string'; import { stringTools } from '../pages/tools/string';
import { imageTools } from '../pages/tools/image'; import { imageTools } from '../pages/tools/image';
import { DefinedTool, ToolCategory } from './defineTool'; import { DefinedTool, ToolCategory, UserType } from './defineTool';
import { capitalizeFirstLetter } from '@utils/string'; import { capitalizeFirstLetter } from '@utils/string';
import { numberTools } from '../pages/tools/number'; import { numberTools } from '../pages/tools/number';
import { videoTools } from '../pages/tools/video'; import { videoTools } from '../pages/tools/video';
@@ -136,6 +136,23 @@ const categoriesConfig: {
'Tools for working with XML data structures - viewer, beautifier, validator and much more' 'Tools for working with XML data structures - viewer, beautifier, validator and much more'
} }
]; ];
// Filter tools by user types
export const filterToolsByUserTypes = (
tools: DefinedTool[],
userTypes: UserType[]
): DefinedTool[] => {
if (userTypes.length === 0) return tools;
return tools.filter((tool) => {
// If tool has no userTypes defined, show it to all users
if (!tool.userTypes || tool.userTypes.length === 0) return true;
// Check if tool has any of the selected user types
return tool.userTypes.some((userType) => userTypes.includes(userType));
});
};
// use for changelogs // use for changelogs
// console.log( // console.log(
// 'tools', // 'tools',
@@ -143,13 +160,22 @@ const categoriesConfig: {
// ); // );
export const filterTools = ( export const filterTools = (
tools: DefinedTool[], tools: DefinedTool[],
query: string query: string,
userTypes: UserType[] = []
): DefinedTool[] => { ): DefinedTool[] => {
if (!query) return tools; let filteredTools = tools;
// First filter by user types
if (userTypes.length > 0) {
filteredTools = filterToolsByUserTypes(tools, userTypes);
}
// Then filter by search query
if (!query) return filteredTools;
const lowerCaseQuery = query.toLowerCase(); const lowerCaseQuery = query.toLowerCase();
return tools.filter( return filteredTools.filter(
(tool) => (tool) =>
tool.name.toLowerCase().includes(lowerCaseQuery) || tool.name.toLowerCase().includes(lowerCaseQuery) ||
tool.description.toLowerCase().includes(lowerCaseQuery) || tool.description.toLowerCase().includes(lowerCaseQuery) ||
@@ -160,7 +186,9 @@ export const filterTools = (
); );
}; };
export const getToolsByCategory = (): { export const getToolsByCategory = (
userTypes: UserType[] = []
): {
title: string; title: string;
rawTitle: string; rawTitle: string;
description: string; description: string;
@@ -171,23 +199,33 @@ export const getToolsByCategory = (): {
}[] => { }[] => {
const groupedByType: Partial<Record<ToolCategory, DefinedTool[]>> = const groupedByType: Partial<Record<ToolCategory, DefinedTool[]>> =
Object.groupBy(tools, ({ type }) => type); Object.groupBy(tools, ({ type }) => type);
return (Object.entries(groupedByType) as Entries<typeof groupedByType>) return (Object.entries(groupedByType) as Entries<typeof groupedByType>)
.map(([type, tools]) => { .map(([type, tools]) => {
const categoryConfig = categoriesConfig.find( const categoryConfig = categoriesConfig.find(
(config) => config.type === type (config) => config.type === type
); );
// Filter tools by user types if specified
const filteredTools =
userTypes.length > 0
? filterToolsByUserTypes(tools ?? [], userTypes)
: tools ?? [];
return { return {
rawTitle: categoryConfig?.title ?? capitalizeFirstLetter(type), rawTitle: categoryConfig?.title ?? capitalizeFirstLetter(type),
title: `${categoryConfig?.title ?? capitalizeFirstLetter(type)} Tools`, title: `${categoryConfig?.title ?? capitalizeFirstLetter(type)} Tools`,
description: categoryConfig?.value ?? '', description: categoryConfig?.value ?? '',
type, type,
icon: categoryConfig!.icon, icon: categoryConfig!.icon,
tools: tools ?? [], tools: filteredTools,
example: tools example:
? { title: tools[0].name, path: tools[0].path } filteredTools.length > 0
: { title: '', path: '' } ? { title: filteredTools[0].name, path: filteredTools[0].path }
: { title: '', path: '' }
}; };
}) })
.filter((category) => category.tools.length > 0) // Only show categories with tools
.sort( .sort(
(a, b) => (a, b) =>
toolCategoriesOrder.indexOf(a.type) - toolCategoriesOrder.indexOf(a.type) -