Merge branch 'iib0011:main' into tool/random-generators

This commit is contained in:
Aashish Anand
2025-07-22 12:33:35 -07:00
committed by GitHub
91 changed files with 760 additions and 357 deletions

View File

@@ -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() {
}}
>
<CustomSnackBarProvider>
<BrowserRouter>
<Navbar
mode={mode}
onChangeMode={() => {
setMode((prev) => nextMode(prev));
localStorage.setItem('theme', nextMode(mode));
}}
/>
<Suspense fallback={<Loading />}>
<AppRoutes />
</Suspense>
</BrowserRouter>
<UserTypeFilterProvider>
<BrowserRouter>
<Navbar
mode={mode}
onChangeMode={() => {
setMode((prev) => nextMode(prev));
localStorage.setItem('theme', nextMode(mode));
}}
/>
<Suspense fallback={<Loading />}>
<AppRoutes />
</Suspense>
</BrowserRouter>
</UserTypeFilterProvider>
</CustomSnackBarProvider>
</SnackbarProvider>
<ScrollToTopButton />

View File

@@ -25,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',
@@ -50,6 +51,7 @@ export default function Hero() {
const { t } = useTranslation(validNamespaces);
const [inputValue, setInputValue] = useState<string>('');
const theme = useTheme();
const { selectedUserTypes } = useUserTypeFilter();
const [filteredTools, setFilteredTools] = useState<DefinedTool[]>(tools);
const [bookmarkedToolPaths, setBookmarkedToolPaths] = useState<string[]>(
getBookmarkedToolPaths()
@@ -96,12 +98,13 @@ export default function Hero() {
];
const handleInputChange = (
event: React.ChangeEvent<{}>,
_event: React.ChangeEvent<{}>,
newInputValue: string
) => {
setInputValue(newInputValue);
setFilteredTools(filterTools(tools, newInputValue, t));
setFilteredTools(filterTools(tools, newInputValue, selectedUserTypes, t));
};
const toolsMap = new Map<string, ToolInfo>();
for (const tool of filteredTools) {
toolsMap.set(tool.path, {

View File

@@ -103,7 +103,7 @@ export default function ToolHeader({
items={[
{ title: 'All tools', link: '/' },
{
title: getToolsByCategory(t).find(
title: getToolsByCategory([], t).find(
(category) => category.type === type
)!.rawTitle,
link: '/categories/' + type

View File

@@ -43,7 +43,7 @@ export default function ToolLayout({
const toolDescription: string = t(i18n.description);
const otherCategoryTools =
getToolsByCategory(t)
getToolsByCategory([], t)
.find((category) => category.type === type)
?.tools.filter((tool) => t(tool.name) !== toolTitle)
.map((tool) => ({
@@ -77,8 +77,9 @@ export default function ToolLayout({
<AllTools
title={t('translation:toolLayout.allToolsTitle', '', {
type: capitalizeFirstLetter(
getToolsByCategory(t).find((category) => category.type === type)!
.title
getToolsByCategory([], t).find(
(category) => category.type === type
)!.title
)
})}
toolCards={otherCategoryTools}

View File

@@ -0,0 +1,47 @@
import React from 'react';
import { Box, Chip } from '@mui/material';
import { UserType } from '@tools/defineTool';
import { useTranslation } from 'react-i18next';
interface UserTypeFilterProps {
selectedUserTypes: UserType[];
userTypes?: UserType[];
onUserTypesChange: (userTypes: UserType[]) => void;
}
export default function UserTypeFilter({
selectedUserTypes,
onUserTypesChange,
userTypes = ['generalUsers', 'developers']
}: UserTypeFilterProps) {
const { t } = useTranslation('translation');
if (userTypes.length <= 1) return null;
return (
<Box
sx={{
display: 'flex',
flexWrap: 'wrap',
gap: 1,
minWidth: 200,
alignItems: 'center',
justifyContent: 'center'
}}
>
{userTypes.map((userType) => (
<Chip
key={userType}
label={t(`userTypes.${userType}`)}
color={selectedUserTypes.includes(userType) ? 'primary' : 'default'}
onClick={() => {
const isSelected = selectedUserTypes.includes(userType);
const newUserTypes = isSelected
? selectedUserTypes.filter((ut) => ut !== userType)
: [...selectedUserTypes, userType];
onUserTypesChange(newUserTypes);
}}
sx={{ cursor: 'pointer' }}
/>
))}
</Box>
);
}

View File

@@ -1,6 +1,7 @@
import i18n, { Namespace, ParseKeys } from 'i18next';
import { initReactI18next } from 'react-i18next';
import Backend from 'i18next-http-backend';
import LanguageDetector from 'i18next-browser-languagedetector';
export const validNamespaces = [
'string',
@@ -24,15 +25,20 @@ export type FullI18nKey = {
i18n
.use(Backend)
.use(LanguageDetector)
.use(initReactI18next)
.init({
lng: localStorage.getItem('lang') || 'en',
supportedLngs: ['en', 'de', 'es', 'fr', 'pt', 'ja', 'hi', 'nl', 'ru', 'zh'],
fallbackLng: 'en',
interpolation: {
escapeValue: false // react already safes from xss => https://www.i18next.com/translation-function/interpolation#unescape
},
backend: {
loadPath: '/locales/{{lng}}/{{ns}}.json'
},
detection: {
lookupLocalStorage: 'lang',
caches: ['localStorage'] // cache the detected lang back to localStorage
}
});

View File

@@ -9,7 +9,7 @@ import { categoriesColors } from 'config/uiConfig';
import { Icon } from '@iconify/react';
import { useTranslation } from 'react-i18next';
import { getI18nNamespaceFromToolCategory } from '@utils/string';
import { validNamespaces } from '../../i18n';
import { useUserTypeFilter } from '../../providers/UserTypeFilterProvider';
type ArrayElement<ArrayType extends readonly unknown[]> =
ArrayType extends readonly (infer ElementType)[] ? ElementType : never;
@@ -84,10 +84,11 @@ const SingleCategory = function ({
</Stack>
<Typography sx={{ mt: 2 }}>{categoryDescription}</Typography>
</Box>
<Grid mt={1} container spacing={2}>
<Grid container spacing={2} mt={2}>
<Grid item xs={12} md={6}>
<Button
fullWidth
sx={{ height: '100%' }}
onClick={() => navigate('/categories/' + category.type)}
variant={'contained'}
>
@@ -96,7 +97,7 @@ const SingleCategory = function ({
</Grid>
<Grid item xs={12} md={6}>
<Button
sx={{ backgroundColor: 'background.default' }}
sx={{ backgroundColor: 'background.default', height: '100%' }}
fullWidth
onClick={() => navigate(category.example.path)}
variant={'outlined'}
@@ -111,11 +112,15 @@ const SingleCategory = function ({
</Grid>
);
};
export default function Categories() {
const { selectedUserTypes } = useUserTypeFilter();
const { t } = useTranslation();
const categories = getToolsByCategory(selectedUserTypes, t);
return (
<Grid width={'80%'} container mt={2} spacing={2}>
{getToolsByCategory(t).map((category, index) => (
<Grid width={'80%'} container spacing={2}>
{categories.map((category, index) => (
<SingleCategory key={category.type} category={category} index={index} />
))}
</Grid>

View File

@@ -2,9 +2,12 @@ import { Box, useTheme } from '@mui/material';
import Hero from 'components/Hero';
import Categories from './Categories';
import { Helmet } from 'react-helmet';
import { useUserTypeFilter } from 'providers/UserTypeFilterProvider';
import UserTypeFilter from '@components/UserTypeFilter';
export default function Home() {
const theme = useTheme();
const { selectedUserTypes, setSelectedUserTypes } = useUserTypeFilter();
return (
<Box
padding={{
@@ -28,6 +31,12 @@ export default function Home() {
>
<Helmet title={'OmniTools'} />
<Hero />
<Box my={3}>
<UserTypeFilter
selectedUserTypes={selectedUserTypes}
onUserTypesChange={setSelectedUserTypes}
/>
</Box>
<Categories />
</Box>
);

View File

@@ -22,28 +22,38 @@ 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 from '@components/UserTypeFilter';
import { useTranslation } from 'react-i18next';
import { I18nNamespaces, validNamespaces } from '../../i18n';
import { useUserTypeFilter } from '../../providers/UserTypeFilterProvider';
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 { t } = useTranslation(validNamespaces);
const rawTitle = getToolCategoryTitle(categoryName as string, t);
// First get tools by category without filtering
const toolsByCategory =
getToolsByCategory(t).find(({ type }) => type === categoryName)?.tools ??
[];
const toolsByCategory = getToolsByCategory(selectedUserTypes, t).find(
({ type }) => type === categoryName
);
const categoryDefinedTools = toolsByCategory?.tools ?? [];
const categoryTools = filterTools(toolsByCategory, searchTerm, t);
const categoryTools = filterTools(
categoryDefinedTools,
searchTerm,
selectedUserTypes,
t
);
useEffect(() => {
if (mainContentRef.current) {
@@ -90,7 +100,20 @@ export default function ToolsByCategory() {
onChange={(event) => setSearchTerm(event.target.value)}
/>
</Stack>
<Grid container spacing={2} mt={2}>
<Box
width={'100%'}
display={'flex'}
alignItems={'center'}
justifyContent={'center'}
my={2}
>
<UserTypeFilter
userTypes={toolsByCategory?.userTypes ?? undefined}
selectedUserTypes={selectedUserTypes}
onUserTypesChange={setSelectedUserTypes}
/>
</Box>
<Grid container spacing={2}>
{categoryTools.map((tool, index) => (
<Grid item xs={12} md={6} lg={4} key={tool.path}>
<Stack

View File

@@ -20,6 +20,7 @@ export const tool = defineTool('audio', {
i18n: {
name: 'audio:changeSpeed.title',
description: 'audio:changeSpeed.description',
shortDescription: 'audio:changeSpeed.shortDescription'
shortDescription: 'audio:changeSpeed.shortDescription',
userTypes: ['generalUsers', 'developers']
}
});

View File

@@ -21,6 +21,7 @@ export const tool = defineTool('audio', {
i18n: {
name: 'audio:extractAudio.title',
description: 'audio:extractAudio.description',
shortDescription: 'audio:extractAudio.shortDescription'
shortDescription: 'audio:extractAudio.shortDescription',
userTypes: ['generalUsers', 'developers']
}
});

View File

@@ -6,7 +6,8 @@ export const tool = defineTool('audio', {
name: 'audio:mergeAudio.title',
description: 'audio:mergeAudio.description',
shortDescription: 'audio:mergeAudio.shortDescription',
longDescription: 'audio:mergeAudio.longDescription'
longDescription: 'audio:mergeAudio.longDescription',
userTypes: ['generalUsers', 'developers']
},
path: 'merge-audio',
@@ -24,6 +25,5 @@ export const tool = defineTool('audio', {
'audio editing',
'multiple files'
],
component: lazy(() => import('./index'))
});

View File

@@ -6,7 +6,8 @@ export const tool = defineTool('audio', {
name: 'audio:trim.title',
description: 'audio:trim.description',
shortDescription: 'audio:trim.shortDescription',
longDescription: 'audio:trim.longDescription'
longDescription: 'audio:trim.longDescription',
userTypes: ['generalUsers', 'developers']
},
path: 'trim',
@@ -24,6 +25,5 @@ export const tool = defineTool('audio', {
'audio editing',
'time'
],
component: lazy(() => import('./index'))
});

View File

@@ -12,6 +12,5 @@ export const tool = defineTool('csv', {
path: 'csv-to-yaml',
icon: 'nonicons:yaml-16',
keywords: ['csv', 'to', 'yaml'],
component: lazy(() => import('./index'))
});

View File

@@ -12,6 +12,5 @@ export const tool = defineTool('csv', {
icon: 'hugeicons:column-insert',
keywords: ['insert', 'csv', 'columns', 'append', 'prepend'],
component: lazy(() => import('./index'))
});

View File

@@ -13,6 +13,5 @@ export const tool = defineTool('csv', {
icon: 'carbon:transpose',
keywords: ['transpose', 'csv'],
component: lazy(() => import('./index'))
});

View File

@@ -5,12 +5,13 @@ export const tool = defineTool('image-generic', {
i18n: {
name: 'image:compress.title',
description: 'image:compress.description',
shortDescription: 'image:compress.shortDescription'
shortDescription: 'image:compress.shortDescription',
userTypes: ['generalUsers']
},
path: 'compress',
component: lazy(() => import('./index')),
icon: 'material-symbols-light:compress-rounded',
keywords: ['image', 'compress', 'reduce', 'quality']
keywords: ['image', 'compress', 'reduce', 'quality'],
component: lazy(() => import('./index'))
});

View File

@@ -5,7 +5,8 @@ export const tool = defineTool('image-generic', {
i18n: {
name: 'image:resize.title',
description: 'image:resize.description',
shortDescription: 'image:resize.shortDescription'
shortDescription: 'image:resize.shortDescription',
userTypes: ['generalUsers']
},
path: 'resize',

View File

@@ -1,15 +1,15 @@
import { defineTool } from '@tools/defineTool';
import { lazy } from 'react';
export const tool = defineTool('json', {
path: 'validate-json',
icon: 'material-symbols:check-circle',
keywords: ['json', 'validate', 'check', 'syntax', 'errors'],
component: lazy(() => import('./index')),
i18n: {
name: 'json:validateJson.title',
description: 'json:validateJson.description',
shortDescription: 'json:validateJson.shortDescription'
}
});
import { defineTool } from '@tools/defineTool';
import { lazy } from 'react';
export const tool = defineTool('json', {
path: 'validateJson',
icon: 'material-symbols:check-circle',
keywords: ['json', 'validate', 'check', 'syntax', 'errors'],
component: lazy(() => import('./index')),
i18n: {
name: 'json:validateJson.title',
description: 'json:validateJson.description',
shortDescription: 'json:validateJson.shortDescription'
}
});

View File

@@ -11,6 +11,7 @@ export const tool = defineTool('list', {
i18n: {
name: 'list:duplicate.title',
description: 'list:duplicate.description',
shortDescription: 'list:duplicate.shortDescription'
shortDescription: 'list:duplicate.shortDescription',
userTypes: ['generalUsers', 'developers']
}
});

View File

@@ -11,6 +11,7 @@ export const tool = defineTool('list', {
i18n: {
name: 'list:findMostPopular.title',
description: 'list:findMostPopular.description',
shortDescription: 'list:findMostPopular.shortDescription'
shortDescription: 'list:findMostPopular.shortDescription',
userTypes: ['generalUsers', 'developers']
}
});

View File

@@ -10,6 +10,7 @@ export const tool = defineTool('list', {
i18n: {
name: 'list:findUnique.title',
description: 'list:findUnique.description',
shortDescription: 'list:findUnique.shortDescription'
shortDescription: 'list:findUnique.shortDescription',
userTypes: ['generalUsers', 'developers']
}
});

View File

@@ -10,6 +10,7 @@ export const tool = defineTool('list', {
i18n: {
name: 'list:group.title',
description: 'list:group.description',
shortDescription: 'list:group.shortDescription'
shortDescription: 'list:group.shortDescription',
userTypes: ['generalUsers', 'developers']
}
});

View File

@@ -5,12 +5,12 @@ import { lazy } from 'react';
export const tool = defineTool('list', {
path: 'reverse',
icon: 'proicons:reverse',
keywords: ['reverse'],
component: lazy(() => import('./index')),
i18n: {
name: 'list:reverse.title',
description: 'list:reverse.description',
shortDescription: 'list:reverse.shortDescription'
}
shortDescription: 'list:reverse.shortDescription',
userTypes: ['generalUsers']
},
component: lazy(() => import('./index'))
});

View File

@@ -11,6 +11,7 @@ export const tool = defineTool('list', {
i18n: {
name: 'list:rotate.title',
description: 'list:rotate.description',
shortDescription: 'list:rotate.shortDescription'
shortDescription: 'list:rotate.shortDescription',
userTypes: ['generalUsers']
}
});

View File

@@ -11,6 +11,7 @@ export const tool = defineTool('list', {
i18n: {
name: 'list:shuffle.title',
description: 'list:shuffle.description',
shortDescription: 'list:shuffle.shortDescription'
shortDescription: 'list:shuffle.shortDescription',
userTypes: ['generalUsers']
}
});

View File

@@ -11,6 +11,7 @@ export const tool = defineTool('list', {
i18n: {
name: 'list:sort.title',
description: 'list:sort.description',
shortDescription: 'list:sort.shortDescription'
shortDescription: 'list:sort.shortDescription',
userTypes: ['generalUsers', 'developers']
}
});

View File

@@ -10,6 +10,7 @@ export const tool = defineTool('list', {
i18n: {
name: 'list:truncate.title',
description: 'list:truncate.description',
shortDescription: 'list:truncate.shortDescription'
shortDescription: 'list:truncate.shortDescription',
userTypes: ['generalUsers']
}
});

View File

@@ -11,6 +11,7 @@ export const tool = defineTool('list', {
i18n: {
name: 'list:unwrap.title',
description: 'list:unwrap.description',
shortDescription: 'list:unwrap.shortDescription'
shortDescription: 'list:unwrap.shortDescription',
userTypes: ['generalUsers', 'developers']
}
});

View File

@@ -11,6 +11,7 @@ export const tool = defineTool('list', {
i18n: {
name: 'list:wrap.title',
description: 'list:wrap.description',
shortDescription: 'list:wrap.shortDescription'
shortDescription: 'list:wrap.shortDescription',
userTypes: ['generalUsers', 'developers']
}
});

View File

@@ -10,6 +10,7 @@ export const tool = defineTool('number', {
i18n: {
name: 'number:arithmeticSequence.title',
description: 'number:arithmeticSequence.description',
shortDescription: 'number:arithmeticSequence.shortDescription'
shortDescription: 'number:arithmeticSequence.shortDescription',
userTypes: ['generalUsers']
}
});

View File

@@ -11,6 +11,7 @@ export const tool = defineTool('number', {
i18n: {
name: 'number:sum.title',
description: 'number:sum.description',
shortDescription: 'number:sum.shortDescription'
shortDescription: 'number:sum.shortDescription',
userTypes: ['generalUsers']
}
});

View File

@@ -19,11 +19,11 @@ export const tool = defineTool('pdf', {
'browser',
'webassembly'
],
component: lazy(() => import('./index')),
i18n: {
name: 'pdf:compressPdf.title',
description: 'pdf:compressPdf.description',
shortDescription: 'pdf:compressPdf.shortDescription'
shortDescription: 'pdf:compressPdf.shortDescription',
userTypes: ['generalUsers']
}
});

View File

@@ -5,7 +5,8 @@ export const tool = defineTool('pdf', {
i18n: {
name: 'pdf:editor.title',
description: 'pdf:editor.description',
shortDescription: 'pdf:editor.shortDescription'
shortDescription: 'pdf:editor.shortDescription',
userTypes: ['generalUsers']
},
path: 'editor',

View File

@@ -9,6 +9,7 @@ export const meta = defineTool('pdf', {
i18n: {
name: 'pdf:mergePdf.title',
description: 'pdf:mergePdf.description',
shortDescription: 'pdf:mergePdf.shortDescription'
shortDescription: 'pdf:mergePdf.shortDescription',
userTypes: ['generalUsers']
}
});

View File

@@ -9,6 +9,7 @@ export const meta = defineTool('pdf', {
i18n: {
name: 'pdf:pdfToEpub.title',
description: 'pdf:pdfToEpub.description',
shortDescription: 'pdf:pdfToEpub.shortDescription'
shortDescription: 'pdf:pdfToEpub.shortDescription',
userTypes: ['generalUsers']
}
});

View File

@@ -6,13 +6,13 @@ export const tool = defineTool('pdf', {
name: 'pdf:pdfToPng.title',
description: 'pdf:pdfToPng.description',
shortDescription: 'pdf:pdfToPng.shortDescription',
longDescription: 'pdf:pdfToPng.longDescription'
longDescription: 'pdf:pdfToPng.longDescription',
userTypes: ['generalUsers']
},
path: 'pdf-to-png',
icon: 'mdi:image-multiple', // Iconify icon ID
keywords: ['pdf', 'png', 'convert', 'image', 'extract', 'pages'],
component: lazy(() => import('./index'))
});

View File

@@ -18,11 +18,11 @@ export const tool = defineTool('pdf', {
'browser',
'encryption'
],
component: lazy(() => import('./index')),
i18n: {
name: 'pdf:protectPdf.title',
description: 'pdf:protectPdf.description',
shortDescription: 'pdf:protectPdf.shortDescription'
shortDescription: 'pdf:protectPdf.shortDescription',
userTypes: ['generalUsers']
}
});

View File

@@ -6,13 +6,13 @@ export const tool = defineTool('pdf', {
name: 'pdf:rotatePdf.title',
description: 'pdf:rotatePdf.description',
shortDescription: 'pdf:rotatePdf.shortDescription',
longDescription: 'pdf:rotatePdf.longDescription'
longDescription: 'pdf:rotatePdf.longDescription',
userTypes: ['generalUsers']
},
path: 'rotate-pdf',
icon: 'carbon:rotate',
keywords: ['pdf', 'rotate', 'rotation', 'document', 'pages', 'orientation'],
component: lazy(() => import('./index'))
});

View File

@@ -9,6 +9,7 @@ export const meta = defineTool('pdf', {
i18n: {
name: 'pdf:splitPdf.title',
description: 'pdf:splitPdf.description',
shortDescription: 'pdf:splitPdf.shortDescription'
shortDescription: 'pdf:splitPdf.shortDescription',
userTypes: ['generalUsers']
}
});

View File

@@ -10,6 +10,7 @@ export const tool = defineTool('string', {
i18n: {
name: 'string:base64.title',
description: 'string:base64.description',
shortDescription: 'string:base64.shortDescription'
shortDescription: 'string:base64.shortDescription',
userTypes: ['generalUsers', 'developers']
}
});

View File

@@ -11,6 +11,7 @@ export const tool = defineTool('string', {
i18n: {
name: 'string:censor.title',
description: 'string:censor.description',
shortDescription: 'string:censor.shortDescription'
shortDescription: 'string:censor.shortDescription',
userTypes: ['generalUsers']
}
});

View File

@@ -11,6 +11,7 @@ export const tool = defineTool('string', {
i18n: {
name: 'string:createPalindrome.title',
description: 'string:createPalindrome.description',
shortDescription: 'string:createPalindrome.shortDescription'
shortDescription: 'string:createPalindrome.shortDescription',
userTypes: ['generalUsers']
}
});

View File

@@ -11,6 +11,7 @@ export const tool = defineTool('string', {
i18n: {
name: 'string:extractSubstring.title',
description: 'string:extractSubstring.description',
shortDescription: 'string:extractSubstring.shortDescription'
shortDescription: 'string:extractSubstring.shortDescription',
userTypes: ['generalUsers', 'developers']
}
});

View File

@@ -3,13 +3,15 @@ import { lazy } from 'react';
export const tool = defineTool('string', {
path: 'join',
icon: 'material-symbols-light:join',
keywords: ['join'],
keywords: ['text', 'join'],
component: lazy(() => import('./index')),
i18n: {
name: 'string:join.title',
description: 'string:join.description',
shortDescription: 'string:join.shortDescription'
shortDescription: 'string:join.shortDescription',
userTypes: ['generalUsers']
}
});

View File

@@ -11,6 +11,7 @@ export const tool = defineTool('string', {
i18n: {
name: 'string:palindrome.title',
description: 'string:palindrome.description',
shortDescription: 'string:palindrome.shortDescription'
shortDescription: 'string:palindrome.shortDescription',
userTypes: ['generalUsers']
}
});

View File

@@ -11,6 +11,7 @@ export const tool = defineTool('string', {
i18n: {
name: 'string:quote.title',
description: 'string:quote.description',
shortDescription: 'string:quote.shortDescription'
shortDescription: 'string:quote.shortDescription',
userTypes: ['generalUsers', 'developers']
}
});

View File

@@ -11,6 +11,7 @@ export const tool = defineTool('string', {
i18n: {
name: 'string:randomizeCase.title',
description: 'string:randomizeCase.description',
shortDescription: 'string:randomizeCase.shortDescription'
shortDescription: 'string:randomizeCase.shortDescription',
userTypes: ['generalUsers']
}
});

View File

@@ -10,6 +10,7 @@ export const tool = defineTool('string', {
i18n: {
name: 'string:removeDuplicateLines.title',
description: 'string:removeDuplicateLines.description',
shortDescription: 'string:removeDuplicateLines.shortDescription'
shortDescription: 'string:removeDuplicateLines.shortDescription',
userTypes: ['generalUsers', 'developers']
}
});

View File

@@ -11,6 +11,7 @@ export const tool = defineTool('string', {
i18n: {
name: 'string:repeat.title',
description: 'string:repeat.description',
shortDescription: 'string:repeat.shortDescription'
shortDescription: 'string:repeat.shortDescription',
userTypes: ['generalUsers', 'developers']
}
});

View File

@@ -10,6 +10,7 @@ export const tool = defineTool('string', {
i18n: {
name: 'string:reverse.title',
description: 'string:reverse.description',
shortDescription: 'string:reverse.shortDescription'
shortDescription: 'string:reverse.shortDescription',
userTypes: ['generalUsers', 'developers']
}
});

View File

@@ -6,7 +6,8 @@ export const tool = defineTool('string', {
i18n: {
name: 'string:rot13.title',
description: 'string:rot13.description',
shortDescription: 'string:rot13.shortDescription'
shortDescription: 'string:rot13.shortDescription',
userTypes: ['generalUsers', 'developers']
},
path: 'rot13',

View File

@@ -6,7 +6,8 @@ export const tool = defineTool('string', {
i18n: {
name: 'string:rotate.title',
description: 'string:rotate.description',
shortDescription: 'string:rotate.shortDescription'
shortDescription: 'string:rotate.shortDescription',
userTypes: ['generalUsers', 'developers']
},
path: 'rotate',

View File

@@ -3,6 +3,7 @@ import { lazy } from 'react';
export const tool = defineTool('string', {
path: 'split',
icon: 'material-symbols-light:call-split',
keywords: ['split'],
@@ -10,6 +11,7 @@ export const tool = defineTool('string', {
i18n: {
name: 'string:split.title',
description: 'string:split.description',
shortDescription: 'string:split.shortDescription'
shortDescription: 'string:split.shortDescription',
userTypes: ['generalUsers', 'developers']
}
});

View File

@@ -11,6 +11,7 @@ export const tool = defineTool('string', {
i18n: {
name: 'string:statistic.title',
description: 'string:statistic.description',
shortDescription: 'string:statistic.shortDescription'
shortDescription: 'string:statistic.shortDescription',
userTypes: ['generalUsers', 'developers']
}
});

View File

@@ -5,7 +5,8 @@ export const tool = defineTool('string', {
i18n: {
name: 'string:textReplacer.title',
description: 'string:textReplacer.description',
shortDescription: 'string:textReplacer.shortDescription'
shortDescription: 'string:textReplacer.shortDescription',
userTypes: ['generalUsers', 'developers']
},
path: 'replacer',

View File

@@ -10,6 +10,7 @@ export const tool = defineTool('string', {
i18n: {
name: 'string:toMorse.title',
description: 'string:toMorse.description',
shortDescription: 'string:toMorse.shortDescription'
shortDescription: 'string:toMorse.shortDescription',
userTypes: ['generalUsers', 'developers']
}
});

View File

@@ -10,6 +10,7 @@ export const tool = defineTool('string', {
i18n: {
name: 'string:truncate.title',
description: 'string:truncate.description',
shortDescription: 'string:truncate.shortDescription'
shortDescription: 'string:truncate.shortDescription',
userTypes: ['generalUsers', 'developers']
}
});

View File

@@ -10,6 +10,7 @@ export const tool = defineTool('string', {
i18n: {
name: 'string:uppercase.title',
description: 'string:uppercase.description',
shortDescription: 'string:uppercase.shortDescription'
shortDescription: 'string:uppercase.shortDescription',
userTypes: ['generalUsers', 'developers']
}
});

View File

@@ -10,6 +10,7 @@ export const tool = defineTool('time', {
i18n: {
name: 'time:checkLeapYears.title',
description: 'time:checkLeapYears.description',
shortDescription: 'time:checkLeapYears.shortDescription'
shortDescription: 'time:checkLeapYears.shortDescription',
userTypes: ['generalUsers']
}
});

View File

@@ -10,6 +10,7 @@ export const tool = defineTool('time', {
i18n: {
name: 'time:convertDaysToHours.title',
description: 'time:convertDaysToHours.description',
shortDescription: 'time:convertDaysToHours.shortDescription'
shortDescription: 'time:convertDaysToHours.shortDescription',
userTypes: ['generalUsers']
}
});

View File

@@ -10,6 +10,7 @@ export const tool = defineTool('time', {
i18n: {
name: 'time:convertHoursToDays.title',
description: 'time:convertHoursToDays.description',
shortDescription: 'time:convertHoursToDays.shortDescription'
shortDescription: 'time:convertHoursToDays.shortDescription',
userTypes: ['generalUsers']
}
});

View File

@@ -10,6 +10,7 @@ export const tool = defineTool('time', {
i18n: {
name: 'time:convertSecondsToTime.title',
description: 'time:convertSecondsToTime.description',
shortDescription: 'time:convertSecondsToTime.shortDescription'
shortDescription: 'time:convertSecondsToTime.shortDescription',
userTypes: ['generalUsers']
}
});

View File

@@ -5,11 +5,12 @@ export const tool = defineTool('time', {
path: 'convert-time-to-seconds',
icon: 'material-symbols:schedule',
keywords: ['time', 'seconds', 'convert', 'format'],
keywords: ['time', 'seconds', 'convert', 'format', 'HH:MM:SS'],
component: lazy(() => import('./index')),
i18n: {
name: 'time:convertTimeToSeconds.title',
description: 'time:convertTimeToSeconds.description',
shortDescription: 'time:convertTimeToSeconds.shortDescription'
shortDescription: 'time:convertTimeToSeconds.shortDescription',
userTypes: ['generalUsers', 'developers']
}
});

View File

@@ -5,8 +5,7 @@ export const tool = defineTool('time', {
i18n: {
name: 'time:convertUnixToDate.title',
description: 'time:convertUnixToDate.description',
shortDescription: 'time:convertUnixToDate.shortDescription',
longDescription: 'time:convertUnixToDate.longDescription'
shortDescription: 'time:convertUnixToDate.shortDescription'
},
path: 'convert-unix-to-date',
icon: 'material-symbols:schedule',

View File

@@ -4,12 +4,21 @@ import { lazy } from 'react';
export const tool = defineTool('time', {
path: 'crontab-guru',
icon: 'material-symbols:schedule',
keywords: ['cron', 'schedule', 'automation', 'expression'],
keywords: [
'crontab',
'cron',
'schedule',
'guru',
'time',
'expression',
'parser',
'explain'
],
component: lazy(() => import('./index')),
i18n: {
name: 'time:crontabGuru.title',
description: 'time:crontabGuru.description',
shortDescription: 'time:crontabGuru.shortDescription'
shortDescription: 'time:crontabGuru.shortDescription',
userTypes: ['developers']
}
});

View File

@@ -10,6 +10,7 @@ export const tool = defineTool('time', {
i18n: {
name: 'time:timeBetweenDates.title',
description: 'time:timeBetweenDates.description',
shortDescription: 'time:timeBetweenDates.shortDescription'
shortDescription: 'time:timeBetweenDates.shortDescription',
userTypes: ['generalUsers', 'developers']
}
});

View File

@@ -10,6 +10,7 @@ export const tool = defineTool('time', {
i18n: {
name: 'time:truncateClockTime.title',
description: 'time:truncateClockTime.description',
shortDescription: 'time:truncateClockTime.shortDescription'
shortDescription: 'time:truncateClockTime.shortDescription',
userTypes: ['generalUsers', 'developers']
}
});

View File

@@ -10,6 +10,7 @@ export const tool = defineTool('video', {
i18n: {
name: 'video:changeSpeed.title',
description: 'video:changeSpeed.description',
shortDescription: 'video:changeSpeed.shortDescription'
shortDescription: 'video:changeSpeed.shortDescription',
userTypes: ['generalUsers']
}
});

View File

@@ -8,15 +8,20 @@ export const tool = defineTool('video', {
keywords: [
'compress',
'video',
'resize',
'scale',
'resolution',
'reduce size'
'reduce',
'size',
'optimize',
'mp4',
'mov',
'avi',
'video editing',
'shrink'
],
component: lazy(() => import('./index')),
i18n: {
name: 'video:compress.title',
description: 'video:compress.description',
shortDescription: 'video:compress.shortDescription'
shortDescription: 'video:compress.shortDescription',
userTypes: ['generalUsers']
}
});

View File

@@ -4,12 +4,22 @@ import { lazy } from 'react';
export const tool = defineTool('video', {
path: 'crop-video',
icon: 'material-symbols:crop',
keywords: ['video', 'crop', 'trim', 'edit', 'resize'],
component: lazy(() => import('./index')),
keywords: [
'crop',
'video',
'trim',
'aspect ratio',
'mp4',
'mov',
'avi',
'video editing',
'resize'
],
i18n: {
name: 'video:cropVideo.title',
description: 'video:cropVideo.description',
shortDescription: 'video:cropVideo.shortDescription'
}
shortDescription: 'video:cropVideo.shortDescription',
userTypes: ['generalUsers']
},
component: lazy(() => import('./index'))
});

View File

@@ -10,6 +10,7 @@ export const tool = defineTool('video', {
i18n: {
name: 'video:flip.title',
description: 'video:flip.description',
shortDescription: 'video:flip.shortDescription'
shortDescription: 'video:flip.shortDescription',
userTypes: ['generalUsers']
}
});

View File

@@ -10,6 +10,7 @@ export const tool = defineTool('video', {
i18n: {
name: 'video:loop.title',
description: 'video:loop.description',
shortDescription: 'video:loop.shortDescription'
shortDescription: 'video:loop.shortDescription',
userTypes: ['generalUsers']
}
});

View File

@@ -10,6 +10,7 @@ export const tool = defineTool('video', {
i18n: {
name: 'video:rotate.title',
description: 'video:rotate.description',
shortDescription: 'video:rotate.shortDescription'
shortDescription: 'video:rotate.shortDescription',
userTypes: ['generalUsers']
}
});

View File

@@ -9,6 +9,7 @@ export const tool = defineTool('video', {
i18n: {
name: 'video:trim.title',
description: 'video:trim.description',
shortDescription: 'video:trim.shortDescription'
shortDescription: 'video:trim.shortDescription',
userTypes: ['generalUsers']
}
});

View File

@@ -9,6 +9,7 @@ export const tool = defineTool('video', {
i18n: {
name: 'video:videoToGif.title',
description: 'video:videoToGif.description',
shortDescription: 'video:videoToGif.shortDescription'
shortDescription: 'video:videoToGif.shortDescription',
userTypes: ['generalUsers']
}
});

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,6 +4,8 @@ import { IconifyIcon } from '@iconify/react';
import { FullI18nKey, validNamespaces } from '../i18n';
import { useTranslation } from 'react-i18next';
export type UserType = 'generalUsers' | 'developers';
export interface ToolMeta {
path: string;
component: LazyExoticComponent<JSXElementConstructor<ToolComponentProps>>;
@@ -14,21 +16,22 @@ export interface ToolMeta {
description: FullI18nKey;
shortDescription: FullI18nKey;
longDescription?: FullI18nKey;
userTypes?: UserType[];
};
}
export type ToolCategory =
| 'string'
| 'image-generic'
| 'png'
| 'number'
| 'gif'
| 'video'
| 'list'
| 'json'
| 'time'
| 'csv'
| 'video'
| 'pdf'
| 'image-generic'
| 'audio'
| 'xml';
@@ -41,6 +44,7 @@ export interface DefinedTool {
icon: IconifyIcon | string;
keywords: string[];
component: () => JSX.Element;
userTypes?: UserType[];
}
export interface ToolComponentProps {
@@ -62,6 +66,7 @@ export const defineTool = (
description: i18n.description,
shortDescription: i18n.shortDescription,
keywords,
userTypes: i18n.userTypes,
component: function ToolComponent() {
const { t } = useTranslation(validNamespaces);
return (

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,36 @@ const categoriesConfig: {
title: 'translation:categories.xml.title'
}
];
const CATEGORIES_USER_TYPES_MAPPINGS: Partial<Record<ToolCategory, UserType>> =
{
xml: 'developers',
csv: 'developers',
json: 'developers',
gif: 'generalUsers',
png: 'generalUsers',
'image-generic': 'generalUsers',
video: 'generalUsers',
audio: 'generalUsers'
};
// Filter tools by user types
export const filterToolsByUserTypes = (
tools: DefinedTool[],
userTypes: UserType[]
): DefinedTool[] => {
if (userTypes.length === 0) return tools;
return tools.filter((tool) => {
if (CATEGORIES_USER_TYPES_MAPPINGS[tool.type]) {
return userTypes.includes(CATEGORIES_USER_TYPES_MAPPINGS[tool.type]!);
}
// 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',
@@ -144,12 +174,22 @@ const categoriesConfig: {
export const filterTools = (
tools: DefinedTool[],
query: string,
userTypes: UserType[] = [],
t: TFunction<I18nNamespaces[]>
): 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) =>
t(tool.name).toLowerCase().includes(lowerCaseQuery) ||
t(tool.description).toLowerCase().includes(lowerCaseQuery) ||
@@ -161,6 +201,7 @@ export const filterTools = (
};
export const getToolsByCategory = (
userTypes: UserType[] = [],
t: TFunction<I18nNamespaces[]>
): {
title: string;
@@ -170,14 +211,28 @@ export const getToolsByCategory = (
type: ToolCategory;
example: { title: string; path: string };
tools: DefinedTool[];
userTypes: UserType[]; // <-- Add this line
}[] => {
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 ?? [];
// Aggregate unique userTypes from all tools in this category
const aggregatedUserTypes = Array.from(
new Set((filteredTools ?? []).flatMap((tool) => tool.userTypes ?? []))
);
return {
rawTitle: categoryConfig?.title
? t(categoryConfig.title)
@@ -188,12 +243,22 @@ export const getToolsByCategory = (
description: categoryConfig?.value ? t(categoryConfig.value) : '',
type,
icon: categoryConfig!.icon,
tools: tools ?? [],
example: tools
? { title: tools[0].name, path: tools[0].path }
: { title: '', path: '' }
tools: filteredTools,
example:
filteredTools.length > 0
? { title: filteredTools[0].name, path: filteredTools[0].path }
: { title: '', path: '' },
userTypes: aggregatedUserTypes // <-- Add this line
};
})
.filter((category) => category.tools.length > 0)
.filter((category) =>
userTypes.length > 0
? [...category.userTypes, CATEGORIES_USER_TYPES_MAPPINGS[category.type]]
.filter(Boolean)
.some((categoryUserType) => userTypes.includes(categoryUserType!))
: true
) // Only show categories with tools
.sort(
(a, b) =>
toolCategoriesOrder.indexOf(a.type) -

View File

@@ -114,7 +114,7 @@ export const getToolCategoryTitle = (
categoryName: string,
t: TFunction<I18nNamespaces[]>
): string =>
getToolsByCategory(t).find((category) => category.type === categoryName)!
getToolsByCategory([], t).find((category) => category.type === categoryName)!
.rawTitle;
// Type guard to check if a value is a valid I18nNamespaces