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 { Icon } from '@iconify/react';
import { getToolCategoryTitle } from '@utils/string';
import UserTypeFilter, { useUserTypeFilter } from './UserTypeFilter';
const GroupHeader = styled('div')(({ theme }) => ({
position: 'sticky',
@@ -47,17 +48,25 @@ const exampleTools: { label: string; url: string }[] = [
{ label: 'Trim video', url: '/video/trim' },
{ label: 'Calculate number sum', url: '/number/sum' }
];
export default function Hero() {
const [inputValue, setInputValue] = useState<string>('');
const theme = useTheme();
const { selectedUserTypes, setSelectedUserTypes } = useUserTypeFilter();
const [filteredTools, setFilteredTools] = useState<DefinedTool[]>(tools);
const navigate = useNavigate();
const handleInputChange = (
event: React.ChangeEvent<{}>,
newInputValue: string
) => {
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 (
@@ -84,8 +93,9 @@ export default function Hero() {
editing images, text, lists, and data, all directly from your browser.
</Typography>
<Stack direction={{ xs: 'column', md: 'row' }} spacing={2} mb={2}>
<Autocomplete
sx={{ mb: 2 }}
sx={{ flex: 1 }}
autoHighlight
options={filteredTools}
groupBy={(option) => option.type}
@@ -125,7 +135,9 @@ export default function Hero() {
<Icon fontSize={20} icon={option.icon} />
<Box>
<Typography fontWeight={'bold'}>{option.name}</Typography>
<Typography fontSize={12}>{option.shortDescription}</Typography>
<Typography fontSize={12}>
{option.shortDescription}
</Typography>
</Box>
</Stack>
</Box>
@@ -136,7 +148,14 @@ export default function Hero() {
}
}}
/>
<Grid container spacing={2} mt={2}>
<UserTypeFilter
selectedUserTypes={selectedUserTypes}
onUserTypesChange={handleUserTypesChange}
label="User Type"
/>
</Stack>
<Grid container spacing={2}>
{exampleTools.map((tool) => (
<Grid
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 { categoriesColors } from 'config/uiConfig';
import { Icon } from '@iconify/react';
import { useUserTypeFilter } from '@components/UserTypeFilter';
type ArrayElement<ArrayType extends readonly unknown[]> =
ArrayType extends readonly (infer ElementType)[] ? ElementType : never;
@@ -65,7 +66,7 @@ const SingleCategory = function ({
</Stack>
<Typography sx={{ mt: 2 }}>{category.description}</Typography>
</Box>
<Grid mt={1} container spacing={2}>
<Grid container spacing={2} mt={2}>
<Grid item xs={12} md={6}>
<Button
fullWidth
@@ -88,10 +89,14 @@ const SingleCategory = function ({
</Grid>
);
};
export default function Categories() {
const { selectedUserTypes } = useUserTypeFilter();
const categories = getToolsByCategory(selectedUserTypes);
return (
<Grid width={'80%'} container mt={2} spacing={2}>
{getToolsByCategory().map((category, index) => (
{categories.map((category, index) => (
<SingleCategory key={category.type} category={category} index={index} />
))}
</Grid>

View File

@@ -21,18 +21,21 @@ import BackButton from '@components/BackButton';
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';
const StyledLink = styled(Link)(({ theme }) => ({
'&:hover': {
color: theme.palette.mode === 'dark' ? 'white' : theme.palette.primary.light
}
}));
export default function ToolsByCategory() {
const navigate = useNavigate();
const theme = useTheme();
const mainContentRef = React.useRef<HTMLDivElement>(null);
const { categoryName } = useParams();
const [searchTerm, setSearchTerm] = React.useState<string>('');
const { selectedUserTypes, setSelectedUserTypes } = useUserTypeFilter();
const rawTitle = getToolCategoryTitle(categoryName as string);
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 (
<Box sx={{ backgroundColor: 'background.default' }}>
<Helmet>
@@ -68,6 +86,7 @@ export default function ToolsByCategory() {
color={theme.palette.primary.main}
>{`All ${rawTitle} Tools`}</Typography>
</Stack>
<Stack direction={'row'} spacing={2} sx={{ minWidth: 0 }}>
<TextField
placeholder={'Search'}
InputProps={{
@@ -80,13 +99,15 @@ export default function ToolsByCategory() {
}}
onChange={(event) => setSearchTerm(event.target.value)}
/>
<UserTypeFilter
selectedUserTypes={selectedUserTypes}
onUserTypesChange={handleUserTypesChange}
label="User Type"
/>
</Stack>
</Stack>
<Grid container spacing={2} mt={2}>
{filterTools(
getToolsByCategory().find(({ type }) => type === categoryName)
?.tools ?? [],
searchTerm
).map((tool, index) => (
{filteredTools.map((tool, index) => (
<Grid item xs={12} md={6} lg={4} key={tool.path}>
<Stack
sx={{

View File

@@ -17,5 +17,6 @@ export const tool = defineTool('json', {
'json',
'string'
],
userTypes: ['Developers'],
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.',
shortDescription: 'Encode or decode data using Base64.',
keywords: ['base64'],
userTypes: ['Developers', 'CyberSec'],
component: lazy(() => import('./index'))
});

View File

@@ -12,5 +12,6 @@ export const tool = defineTool('string', {
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.',
keywords: ['text', 'censor', 'words', 'characters'],
userTypes: ['General Users', 'Students'],
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.',
shortDescription: 'Encode or decode text using ROT13 cipher.',
keywords: ['rot13'],
userTypes: ['Developers', 'CyberSec', 'Students'],
component: lazy(() => import('./index'))
});

View File

@@ -11,5 +11,6 @@ export const tool = defineTool('string', {
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.',
keywords: ['text', 'statistics', 'count', 'lines', 'words', 'characters'],
userTypes: ['General Users', 'Students', 'Developers'],
component: lazy(() => import('./index'))
});

View File

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

View File

@@ -1,6 +1,6 @@
import { stringTools } from '../pages/tools/string';
import { imageTools } from '../pages/tools/image';
import { DefinedTool, ToolCategory } from './defineTool';
import { DefinedTool, ToolCategory, UserType } from './defineTool';
import { capitalizeFirstLetter } from '@utils/string';
import { numberTools } from '../pages/tools/number';
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'
}
];
// 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
// console.log(
// 'tools',
@@ -143,13 +160,22 @@ const categoriesConfig: {
// );
export const filterTools = (
tools: DefinedTool[],
query: string
query: string,
userTypes: UserType[] = []
): 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();
return tools.filter(
return filteredTools.filter(
(tool) =>
tool.name.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;
rawTitle: string;
description: string;
@@ -171,23 +199,33 @@ export const getToolsByCategory = (): {
}[] => {
const groupedByType: Partial<Record<ToolCategory, DefinedTool[]>> =
Object.groupBy(tools, ({ type }) => type);
return (Object.entries(groupedByType) as Entries<typeof groupedByType>)
.map(([type, tools]) => {
const categoryConfig = categoriesConfig.find(
(config) => config.type === type
);
// Filter tools by user types if specified
const filteredTools =
userTypes.length > 0
? filterToolsByUserTypes(tools ?? [], userTypes)
: tools ?? [];
return {
rawTitle: categoryConfig?.title ?? capitalizeFirstLetter(type),
title: `${categoryConfig?.title ?? capitalizeFirstLetter(type)} Tools`,
description: categoryConfig?.value ?? '',
type,
icon: categoryConfig!.icon,
tools: tools ?? [],
example: tools
? { title: tools[0].name, path: tools[0].path }
tools: filteredTools,
example:
filteredTools.length > 0
? { title: filteredTools[0].name, path: filteredTools[0].path }
: { title: '', path: '' }
};
})
.filter((category) => category.tools.length > 0) // Only show categories with tools
.sort(
(a, b) =>
toolCategoriesOrder.indexOf(a.type) -