mirror of
https://github.com/iib0011/omni-tools.git
synced 2025-12-29 16:16:02 +00:00
Merge branch 'iib0011:main' into tool/random-generators
This commit is contained in:
@@ -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 />
|
||||
|
||||
@@ -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, {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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}
|
||||
|
||||
47
src/components/UserTypeFilter.tsx
Normal file
47
src/components/UserTypeFilter.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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']
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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']
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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'))
|
||||
});
|
||||
|
||||
@@ -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'))
|
||||
});
|
||||
|
||||
@@ -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'))
|
||||
});
|
||||
|
||||
@@ -12,6 +12,5 @@ export const tool = defineTool('csv', {
|
||||
icon: 'hugeicons:column-insert',
|
||||
|
||||
keywords: ['insert', 'csv', 'columns', 'append', 'prepend'],
|
||||
|
||||
component: lazy(() => import('./index'))
|
||||
});
|
||||
|
||||
@@ -13,6 +13,5 @@ export const tool = defineTool('csv', {
|
||||
icon: 'carbon:transpose',
|
||||
|
||||
keywords: ['transpose', 'csv'],
|
||||
|
||||
component: lazy(() => import('./index'))
|
||||
});
|
||||
|
||||
@@ -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'))
|
||||
});
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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'
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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']
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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']
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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']
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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']
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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'))
|
||||
});
|
||||
|
||||
@@ -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']
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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']
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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']
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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']
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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']
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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']
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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']
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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']
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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']
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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']
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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']
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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'))
|
||||
});
|
||||
|
||||
@@ -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']
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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'))
|
||||
});
|
||||
|
||||
@@ -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']
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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']
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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']
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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']
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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']
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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']
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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']
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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']
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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']
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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']
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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']
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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']
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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']
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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']
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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']
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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']
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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']
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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']
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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']
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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']
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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']
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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']
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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']
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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']
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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']
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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']
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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']
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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'))
|
||||
});
|
||||
|
||||
@@ -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']
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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']
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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']
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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']
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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']
|
||||
}
|
||||
});
|
||||
|
||||
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,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 (
|
||||
|
||||
@@ -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) -
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user