Merge branch 'main' into tool/hidden-character-detector

This commit is contained in:
Aashish Anand
2025-07-24 23:51:06 -07:00
committed by GitHub
115 changed files with 2440 additions and 382 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

@@ -5,7 +5,10 @@ import InputHeader from '../InputHeader';
import InputFooter from './InputFooter';
import { useTranslation } from 'react-i18next';
import Editor from '@monaco-editor/react';
import { globalInputHeight } from '../../config/uiConfig';
import {
globalInputHeight,
codeInputHeightOffset
} from '../../config/uiConfig';
export default function ToolCodeInput({
value,
@@ -53,14 +56,62 @@ export default function ToolCodeInput({
return (
<Box>
<InputHeader title={title || t('toolTextInput.input')} />
<Box height={globalInputHeight}>
<Editor
height={'87%'}
language={language}
theme={theme.palette.mode === 'dark' ? 'vs-dark' : 'light'}
value={value}
onChange={(value) => onChange(value ?? '')}
/>
<Box
height={`${globalInputHeight + codeInputHeightOffset}px`} // The +codeInputHeightOffset compensates for internal padding/border differences between Monaco Editor and MUI TextField
sx={{
display: 'flex',
flexDirection: 'column'
}}
>
<Box
sx={(theme) => ({
height: '100%',
display: 'flex',
flexDirection: 'column',
backgroundColor: 'background.paper',
'.monaco-editor': {
height: '100% !important',
outline: 'none !important',
'.overflow-guard': {
height: '100% !important',
border:
theme.palette.mode === 'light'
? '1px solid rgba(0, 0, 0, 0.23)'
: '1px solid rgba(255, 255, 255, 0.23)',
borderRadius: 1,
transition: theme.transitions.create(
['border-color', 'background-color'],
{
duration: theme.transitions.duration.shorter
}
)
},
'&:hover .overflow-guard': {
borderColor: theme.palette.text.primary
}
},
'.decorationsOverviewRuler': {
display: 'none !important'
}
})}
>
<Editor
height="100%"
language={language}
theme={theme.palette.mode === 'dark' ? 'vs-dark' : 'light'}
value={value}
onChange={(value) => onChange(value ?? '')}
options={{
scrollbar: {
vertical: 'visible',
horizontal: 'visible',
verticalScrollbarSize: 10,
horizontalScrollbarSize: 10,
alwaysConsumeMouseWheel: false
}
}}
/>
</Box>
<InputFooter handleCopy={handleCopy} handleImport={handleImportClick} />
<input
type="file"

View File

@@ -1,4 +1,5 @@
export const globalInputHeight = 300;
export const codeInputHeightOffset = 7; // Offset to visually match Monaco and MUI TextField heights
export const globalDescriptionFontSize = 12;
export const categoriesColors: string[] = [
'#8FBC5D',

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,6 +5,7 @@ import ToolContent from '@components/ToolContent';
import { ToolComponentProps } from '@tools/defineTool';
import ToolImageInput from '@components/input/ToolImageInput';
import { removeBackground } from '@imgly/background-removal';
import * as heic2any from 'heic2any';
const initialValues = {};
@@ -23,8 +24,33 @@ export default function RemoveBackgroundFromImage({
setIsProcessing(true);
try {
// Convert the input file to a Blob URL
const inputUrl = URL.createObjectURL(input);
let fileToProcess = input;
// Check if the file is HEIC (by MIME type or extension)
if (
input.type === 'image/heic' ||
input.name?.toLowerCase().endsWith('.heic')
) {
// Convert HEIC to PNG using heic2any
const convertedBlob = await heic2any.default({
blob: input,
toType: 'image/png'
});
// heic2any returns a Blob or an array of Blobs
let pngBlob;
if (Array.isArray(convertedBlob)) {
pngBlob = convertedBlob[0];
} else {
pngBlob = convertedBlob;
}
fileToProcess = new File(
[pngBlob],
input.name.replace(/\.[^/.]+$/, '') + '.png',
{ type: 'image/png' }
);
}
// Convert the file to a Blob URL
const inputUrl = URL.createObjectURL(fileToProcess);
// Process the image with the background removal library
const blob = await removeBackground(inputUrl, {
@@ -36,7 +62,7 @@ export default function RemoveBackgroundFromImage({
// Create a new file from the blob
const newFile = new File(
[blob],
input.name.replace(/\.[^/.]+$/, '') + '-no-bg.png',
fileToProcess.name.replace(/\.[^/.]+$/, '') + '-no-bg.png',
{
type: 'image/png'
}

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,6 +1,6 @@
import React, { useState } from 'react';
import ToolContent from '@components/ToolContent';
import ToolTextInput from '@components/input/ToolTextInput';
import ToolCodeInput from '@components/input/ToolCodeInput';
import ToolTextResult from '@components/result/ToolTextResult';
import { escapeJson } from './service';
import { CardExampleType } from '@components/examples/ToolExamples';
@@ -88,7 +88,12 @@ export default function EscapeJsonTool({
<ToolContent
title={title}
inputComponent={
<ToolTextInput title="Input JSON" value={input} onChange={setInput} />
<ToolCodeInput
title="Input JSON"
value={input}
onChange={setInput}
language="json"
/>
}
resultComponent={
<ToolTextResult

View File

@@ -1,6 +1,6 @@
import { useState } from 'react';
import ToolContent from '@components/ToolContent';
import ToolTextInput from '@components/input/ToolTextInput';
import ToolCodeInput from '@components/input/ToolCodeInput';
import ToolTextResult from '@components/result/ToolTextResult';
import { convertJsonToXml } from './service';
import { CardExampleType } from '@components/examples/ToolExamples';
@@ -84,7 +84,12 @@ export default function JsonToXml({ title }: ToolComponentProps) {
compute={compute}
exampleCards={exampleCards}
inputComponent={
<ToolTextInput title="Input Json" value={input} onChange={setInput} />
<ToolCodeInput
title="Input Json"
value={input}
onChange={setInput}
language="json"
/>
}
resultComponent={
<ToolTextResult title="Output XML" value={result} extension={'xml'} />

View File

@@ -1,6 +1,6 @@
import React, { useState } from 'react';
import ToolContent from '@components/ToolContent';
import ToolTextInput from '@components/input/ToolTextInput';
import ToolCodeInput from '@components/input/ToolCodeInput';
import ToolTextResult from '@components/result/ToolTextResult';
import { minifyJson } from './service';
import { CardExampleType } from '@components/examples/ToolExamples';
@@ -60,10 +60,11 @@ export default function MinifyJson({ title }: ToolComponentProps) {
<ToolContent
title={title}
inputComponent={
<ToolTextInput
<ToolCodeInput
title={t('minify.inputTitle')}
value={input}
onChange={setInput}
language="json"
/>
}
resultComponent={

View File

@@ -1,6 +1,6 @@
import { Box } from '@mui/material';
import React, { useRef, useState } from 'react';
import ToolTextInput from '@components/input/ToolTextInput';
import ToolCodeInput from '@components/input/ToolCodeInput';
import ToolTextResult from '@components/result/ToolTextResult';
import { beautifyJson } from './service';
import ToolInfo from '@components/ToolInfo';
@@ -130,10 +130,11 @@ export default function PrettifyJson({ title }: ToolComponentProps) {
title={title}
input={input}
inputComponent={
<ToolTextInput
<ToolCodeInput
title={t('prettify.inputTitle')}
value={input}
onChange={setInput}
language="json"
/>
}
resultComponent={

View File

@@ -1,7 +1,7 @@
import { Box } from '@mui/material';
import React, { useState } from 'react';
import ToolContent from '@components/ToolContent';
import ToolTextInput from '@components/input/ToolTextInput';
import ToolCodeInput from '@components/input/ToolCodeInput';
import ToolTextResult from '@components/result/ToolTextResult';
import { stringifyJson } from './service';
import { ToolComponentProps } from '@tools/defineTool';
@@ -103,10 +103,11 @@ export default function StringifyJson({ title }: ToolComponentProps) {
compute={compute}
exampleCards={exampleCards}
inputComponent={
<ToolTextInput
<ToolCodeInput
title="JavaScript Object/Array"
value={input}
onChange={setInput}
language="json"
/>
}
resultComponent={

View File

@@ -1,6 +1,6 @@
import React, { useState } from 'react';
import ToolContent from '@components/ToolContent';
import ToolTextInput from '@components/input/ToolTextInput';
import ToolCodeInput from '@components/input/ToolCodeInput';
import ToolTextResult from '@components/result/ToolTextResult';
import { convertTsvToJson } from './service';
import { CardExampleType } from '@components/examples/ToolExamples';
@@ -216,7 +216,12 @@ export default function TsvToJson({
exampleCards={exampleCards}
getGroups={getGroups}
inputComponent={
<ToolTextInput title="Input TSV" value={input} onChange={setInput} />
<ToolCodeInput
title="Input TSV"
value={input}
onChange={setInput}
language="tsv"
/>
}
resultComponent={
<ToolTextResult title="Output JSON" value={result} extension={'json'} />

View File

@@ -1,5 +1,5 @@
import React, { useState } from 'react';
import ToolTextInput from '@components/input/ToolTextInput';
import ToolCodeInput from '@components/input/ToolCodeInput';
import ToolTextResult from '@components/result/ToolTextResult';
import { CardExampleType } from '@components/examples/ToolExamples';
import { validateJson } from './service';
@@ -65,10 +65,11 @@ export default function ValidateJson({ title }: ToolComponentProps) {
<ToolContent
title={title}
inputComponent={
<ToolTextInput
<ToolCodeInput
title={t('validateJson.inputTitle')}
value={input}
onChange={setInput}
language="json"
/>
}
resultComponent={

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

@@ -1,3 +1,5 @@
import { tool as numberRandomPortGenerator } from './random-port-generator/meta';
import { tool as numberRandomNumberGenerator } from './random-number-generator/meta';
import { tool as numberSum } from './sum/meta';
import { tool as numberGenerate } from './generate/meta';
import { tool as numberArithmeticSequence } from './arithmetic-sequence/meta';
@@ -6,5 +8,7 @@ export const numberTools = [
numberSum,
numberGenerate,
numberArithmeticSequence,
numberRandomPortGenerator,
numberRandomNumberGenerator,
...genericCalcTools
];

View File

@@ -0,0 +1,200 @@
import { Alert, Box } from '@mui/material';
import { useState } from 'react';
import ToolContent from '@components/ToolContent';
import { ToolComponentProps } from '@tools/defineTool';
import ToolTextResult from '@components/result/ToolTextResult';
import { GetGroupsType } from '@components/options/ToolOptions';
import { formatNumbers, generateRandomNumbers, validateInput } from './service';
import { InitialValuesType, RandomNumberResult } from './types';
import { useTranslation } from 'react-i18next';
import TextFieldWithDesc from '@components/options/TextFieldWithDesc';
import CheckboxWithDesc from '@components/options/CheckboxWithDesc';
const initialValues: InitialValuesType = {
minValue: 1,
maxValue: 100,
count: 10,
allowDecimals: false,
allowDuplicates: true,
sortResults: false,
separator: ', '
};
export default function RandomNumberGenerator({
title,
longDescription
}: ToolComponentProps) {
const { t } = useTranslation('number');
const [result, setResult] = useState<RandomNumberResult | null>(null);
const [error, setError] = useState<string | null>(null);
const [formattedResult, setFormattedResult] = useState<string>('');
const compute = (values: InitialValuesType) => {
try {
setError(null);
setResult(null);
setFormattedResult('');
// Validate input
const validationError = validateInput(values);
if (validationError) {
setError(validationError);
return;
}
// Generate random numbers
const randomResult = generateRandomNumbers(values);
setResult(randomResult);
// Format for display
const formatted = formatNumbers(
randomResult.numbers,
values.separator,
values.allowDecimals
);
setFormattedResult(formatted);
} catch (err) {
console.error('Random number generation failed:', err);
setError(t('randomNumberGenerator.error.generationFailed'));
}
};
const getGroups: GetGroupsType<InitialValuesType> | null = ({
values,
updateField
}) => [
{
title: t('randomNumberGenerator.options.range.title'),
component: (
<Box>
<TextFieldWithDesc
value={values.minValue.toString()}
onOwnChange={(value) =>
updateField('minValue', parseInt(value) || 1)
}
description={t(
'randomNumberGenerator.options.range.minDescription'
)}
inputProps={{
type: 'number',
'data-testid': 'min-value-input'
}}
/>
<TextFieldWithDesc
value={values.maxValue.toString()}
onOwnChange={(value) =>
updateField('maxValue', parseInt(value) || 100)
}
description={t(
'randomNumberGenerator.options.range.maxDescription'
)}
inputProps={{
type: 'number',
'data-testid': 'max-value-input'
}}
/>
</Box>
)
},
{
title: t('randomNumberGenerator.options.generation.title'),
component: (
<Box>
<TextFieldWithDesc
value={values.count.toString()}
onOwnChange={(value) => updateField('count', parseInt(value) || 10)}
description={t(
'randomNumberGenerator.options.generation.countDescription'
)}
inputProps={{
type: 'number',
min: 1,
max: 10000,
'data-testid': 'count-input'
}}
/>
<CheckboxWithDesc
title={t(
'randomNumberGenerator.options.generation.allowDecimals.title'
)}
checked={values.allowDecimals}
onChange={(value) => updateField('allowDecimals', value)}
description={t(
'randomNumberGenerator.options.generation.allowDecimals.description'
)}
/>
<CheckboxWithDesc
title={t(
'randomNumberGenerator.options.generation.allowDuplicates.title'
)}
checked={values.allowDuplicates}
onChange={(value) => updateField('allowDuplicates', value)}
description={t(
'randomNumberGenerator.options.generation.allowDuplicates.description'
)}
/>
<CheckboxWithDesc
title={t(
'randomNumberGenerator.options.generation.sortResults.title'
)}
checked={values.sortResults}
onChange={(value) => updateField('sortResults', value)}
description={t(
'randomNumberGenerator.options.generation.sortResults.description'
)}
/>
</Box>
)
},
{
title: t('randomNumberGenerator.options.output.title'),
component: (
<Box>
<TextFieldWithDesc
value={values.separator}
onOwnChange={(value) => updateField('separator', value)}
description={t(
'randomNumberGenerator.options.output.separatorDescription'
)}
inputProps={{
'data-testid': 'separator-input'
}}
/>
</Box>
)
}
];
return (
<ToolContent
title={title}
initialValues={initialValues}
compute={compute}
getGroups={getGroups}
resultComponent={
<Box>
{error && (
<Alert severity="error" sx={{ mb: 2 }}>
{error}
</Alert>
)}
{result && (
<ToolTextResult
title={t('randomNumberGenerator.result.title')}
value={formattedResult}
/>
)}
</Box>
}
toolInfo={{
title: t('randomNumberGenerator.info.title'),
description:
longDescription || t('randomNumberGenerator.info.description')
}}
/>
);
}

View File

@@ -0,0 +1,26 @@
import { defineTool } from '@tools/defineTool';
import { lazy } from 'react';
export const tool = defineTool('number', {
i18n: {
name: 'number:randomNumberGenerator.title',
description: 'number:randomNumberGenerator.description',
shortDescription: 'number:randomNumberGenerator.shortDescription',
longDescription: 'number:randomNumberGenerator.longDescription',
userTypes: ['generalUsers']
},
path: 'random-number-generator',
icon: 'mdi:dice-multiple',
keywords: [
'random',
'number',
'generator',
'range',
'min',
'max',
'integer',
'decimal',
'float'
],
component: lazy(() => import('./index'))
});

View File

@@ -0,0 +1,248 @@
import { expect, describe, it } from 'vitest';
import { generateRandomNumbers, validateInput, formatNumbers } from './service';
import { InitialValuesType } from './types';
describe('Random Number Generator Service', () => {
describe('generateRandomNumbers', () => {
it('should generate random numbers within the specified range', () => {
const options: InitialValuesType = {
minValue: 1,
maxValue: 10,
count: 5,
allowDecimals: false,
allowDuplicates: true,
sortResults: false,
separator: ', '
};
const result = generateRandomNumbers(options);
expect(result.numbers).toHaveLength(5);
expect(result.min).toBe(1);
expect(result.max).toBe(10);
expect(result.count).toBe(5);
// Check that all numbers are within range
result.numbers.forEach((num) => {
expect(num).toBeGreaterThanOrEqual(1);
expect(num).toBeLessThanOrEqual(10);
expect(Number.isInteger(num)).toBe(true);
});
});
it('should generate decimal numbers when allowDecimals is true', () => {
const options: InitialValuesType = {
minValue: 0,
maxValue: 1,
count: 3,
allowDecimals: true,
allowDuplicates: true,
sortResults: false,
separator: ', '
};
const result = generateRandomNumbers(options);
expect(result.numbers).toHaveLength(3);
// Check that numbers are within range and can be decimals
result.numbers.forEach((num) => {
expect(num).toBeGreaterThanOrEqual(0);
expect(num).toBeLessThanOrEqual(1);
});
});
it('should generate unique numbers when allowDuplicates is false', () => {
const options: InitialValuesType = {
minValue: 1,
maxValue: 5,
count: 3,
allowDecimals: false,
allowDuplicates: false,
sortResults: false,
separator: ', '
};
const result = generateRandomNumbers(options);
expect(result.numbers).toHaveLength(3);
// Check for uniqueness
const uniqueNumbers = new Set(result.numbers);
expect(uniqueNumbers.size).toBe(3);
});
it('should sort results when sortResults is true', () => {
const options: InitialValuesType = {
minValue: 1,
maxValue: 10,
count: 5,
allowDecimals: false,
allowDuplicates: true,
sortResults: true,
separator: ', '
};
const result = generateRandomNumbers(options);
expect(result.numbers).toHaveLength(5);
expect(result.isSorted).toBe(true);
// Check that numbers are sorted
for (let i = 1; i < result.numbers.length; i++) {
expect(result.numbers[i]).toBeGreaterThanOrEqual(result.numbers[i - 1]);
}
});
it('should throw error when minValue >= maxValue', () => {
const options: InitialValuesType = {
minValue: 10,
maxValue: 5,
count: 5,
allowDecimals: false,
allowDuplicates: true,
sortResults: false,
separator: ', '
};
expect(() => generateRandomNumbers(options)).toThrow(
'Minimum value must be less than maximum value'
);
});
it('should throw error when count <= 0', () => {
const options: InitialValuesType = {
minValue: 1,
maxValue: 10,
count: 0,
allowDecimals: false,
allowDuplicates: true,
sortResults: false,
separator: ', '
};
expect(() => generateRandomNumbers(options)).toThrow(
'Count must be greater than 0'
);
});
it('should throw error when unique count exceeds available range', () => {
const options: InitialValuesType = {
minValue: 1,
maxValue: 5,
count: 10,
allowDecimals: false,
allowDuplicates: false,
sortResults: false,
separator: ', '
};
expect(() => generateRandomNumbers(options)).toThrow(
'Cannot generate unique numbers: count exceeds available range'
);
});
});
describe('validateInput', () => {
it('should return null for valid input', () => {
const options: InitialValuesType = {
minValue: 1,
maxValue: 10,
count: 5,
allowDecimals: false,
allowDuplicates: true,
sortResults: false,
separator: ', '
};
const result = validateInput(options);
expect(result).toBeNull();
});
it('should return error when minValue >= maxValue', () => {
const options: InitialValuesType = {
minValue: 10,
maxValue: 5,
count: 5,
allowDecimals: false,
allowDuplicates: true,
sortResults: false,
separator: ', '
};
const result = validateInput(options);
expect(result).toBe('Minimum value must be less than maximum value');
});
it('should return error when count <= 0', () => {
const options: InitialValuesType = {
minValue: 1,
maxValue: 10,
count: 0,
allowDecimals: false,
allowDuplicates: true,
sortResults: false,
separator: ', '
};
const result = validateInput(options);
expect(result).toBe('Count must be greater than 0');
});
it('should return error when count > 10000', () => {
const options: InitialValuesType = {
minValue: 1,
maxValue: 10,
count: 10001,
allowDecimals: false,
allowDuplicates: true,
sortResults: false,
separator: ', '
};
const result = validateInput(options);
expect(result).toBe('Count cannot exceed 10,000');
});
it('should return error when range > 1000000', () => {
const options: InitialValuesType = {
minValue: 1,
maxValue: 1000002,
count: 5,
allowDecimals: false,
allowDuplicates: true,
sortResults: false,
separator: ', '
};
const result = validateInput(options);
expect(result).toBe('Range cannot exceed 1,000,000');
});
});
describe('formatNumbers', () => {
it('should format integers correctly', () => {
const numbers = [1, 2, 3, 4, 5];
const result = formatNumbers(numbers, ', ', false);
expect(result).toBe('1, 2, 3, 4, 5');
});
it('should format decimals correctly', () => {
const numbers = [1.5, 2.7, 3.2];
const result = formatNumbers(numbers, ' | ', true);
expect(result).toBe('1.50 | 2.70 | 3.20');
});
it('should handle custom separators', () => {
const numbers = [1, 2, 3];
const result = formatNumbers(numbers, ' -> ', false);
expect(result).toBe('1 -> 2 -> 3');
});
it('should handle empty array', () => {
const numbers: number[] = [];
const result = formatNumbers(numbers, ', ', false);
expect(result).toBe('');
});
});
});

View File

@@ -0,0 +1,157 @@
import { InitialValuesType, RandomNumberResult } from './types';
/**
* Generate random numbers within a specified range
*/
export function generateRandomNumbers(
options: InitialValuesType
): RandomNumberResult {
const {
minValue,
maxValue,
count,
allowDecimals,
allowDuplicates,
sortResults
} = options;
if (minValue >= maxValue) {
throw new Error('Minimum value must be less than maximum value');
}
if (count <= 0) {
throw new Error('Count must be greater than 0');
}
if (!allowDuplicates && count > maxValue - minValue + 1) {
throw new Error(
'Cannot generate unique numbers: count exceeds available range'
);
}
const numbers: number[] = [];
if (allowDuplicates) {
// Generate random numbers with possible duplicates
for (let i = 0; i < count; i++) {
const randomNumber = generateRandomNumber(
minValue,
maxValue,
allowDecimals
);
numbers.push(randomNumber);
}
} else {
// Generate unique random numbers
const availableNumbers = new Set<number>();
// Create a pool of available numbers
for (let i = minValue; i <= maxValue; i++) {
if (allowDecimals) {
// For decimals, we need to generate more granular values
for (let j = 0; j < 100; j++) {
availableNumbers.add(i + j / 100);
}
} else {
availableNumbers.add(i);
}
}
const availableArray = Array.from(availableNumbers);
// Shuffle the available numbers
for (let i = availableArray.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[availableArray[i], availableArray[j]] = [
availableArray[j],
availableArray[i]
];
}
// Take the first 'count' numbers
for (let i = 0; i < Math.min(count, availableArray.length); i++) {
numbers.push(availableArray[i]);
}
}
// Sort if requested
if (sortResults) {
numbers.sort((a, b) => a - b);
}
return {
numbers,
min: minValue,
max: maxValue,
count,
hasDuplicates: !allowDuplicates && hasDuplicatesInArray(numbers),
isSorted: sortResults
};
}
/**
* Generate a single random number within the specified range
*/
function generateRandomNumber(
min: number,
max: number,
allowDecimals: boolean
): number {
if (allowDecimals) {
return Math.random() * (max - min) + min;
} else {
return Math.floor(Math.random() * (max - min + 1)) + min;
}
}
/**
* Check if an array has duplicate values
*/
function hasDuplicatesInArray(arr: number[]): boolean {
const seen = new Set<number>();
for (const num of arr) {
if (seen.has(num)) {
return true;
}
seen.add(num);
}
return false;
}
/**
* Format numbers for display
*/
export function formatNumbers(
numbers: number[],
separator: string,
allowDecimals: boolean
): string {
return numbers
.map((num) => (allowDecimals ? num.toFixed(2) : Math.round(num).toString()))
.join(separator);
}
/**
* Validate input parameters
*/
export function validateInput(options: InitialValuesType): string | null {
const { minValue, maxValue, count } = options;
if (minValue >= maxValue) {
return 'Minimum value must be less than maximum value';
}
if (count <= 0) {
return 'Count must be greater than 0';
}
if (count > 10000) {
return 'Count cannot exceed 10,000';
}
if (maxValue - minValue > 1000000) {
return 'Range cannot exceed 1,000,000';
}
return null;
}

View File

@@ -0,0 +1,18 @@
export type InitialValuesType = {
minValue: number;
maxValue: number;
count: number;
allowDecimals: boolean;
allowDuplicates: boolean;
sortResults: boolean;
separator: string;
};
export type RandomNumberResult = {
numbers: number[];
min: number;
max: number;
count: number;
hasDuplicates: boolean;
isSorted: boolean;
};

View File

@@ -0,0 +1,233 @@
import { Alert, Box } from '@mui/material';
import { useState } from 'react';
import ToolContent from '@components/ToolContent';
import { ToolComponentProps } from '@tools/defineTool';
import ToolTextResult from '@components/result/ToolTextResult';
import { GetGroupsType } from '@components/options/ToolOptions';
import {
formatPorts,
generateRandomPorts,
getPortRangeInfo,
validateInput
} from './service';
import { InitialValuesType, RandomPortResult } from './types';
import { useTranslation } from 'react-i18next';
import TextFieldWithDesc from '@components/options/TextFieldWithDesc';
import CheckboxWithDesc from '@components/options/CheckboxWithDesc';
import SimpleRadio from '@components/options/SimpleRadio';
const initialValues: InitialValuesType = {
portRange: 'registered',
minPort: 1024,
maxPort: 49151,
count: 5,
allowDuplicates: false,
sortResults: false,
separator: ', '
};
export default function RandomPortGenerator({
title,
longDescription
}: ToolComponentProps) {
const { t } = useTranslation('number');
const [result, setResult] = useState<RandomPortResult | null>(null);
const [error, setError] = useState<string | null>(null);
const [formattedResult, setFormattedResult] = useState<string>('');
const compute = (values: InitialValuesType) => {
try {
setError(null);
setResult(null);
setFormattedResult('');
// Validate input
const validationError = validateInput(values);
if (validationError) {
setError(validationError);
return;
}
// Generate random ports
const randomResult = generateRandomPorts(values);
setResult(randomResult);
// Format for display
const formatted = formatPorts(randomResult.ports, values.separator);
setFormattedResult(formatted);
} catch (err) {
console.error('Random port generation failed:', err);
setError(t('randomPortGenerator.error.generationFailed'));
}
};
const portOptions = [
{
value: 'well-known',
label: t('randomPortGenerator.options.range.wellKnown')
},
{
value: 'registered',
label: t('randomPortGenerator.options.range.registered')
},
{
value: 'dynamic',
label: t('randomPortGenerator.options.range.dynamic')
},
{
value: 'custom',
label: t('randomPortGenerator.options.range.custom')
}
] as const;
const getGroups: GetGroupsType<InitialValuesType> | null = ({
values,
updateField
}) => [
{
title: t('randomPortGenerator.options.range.title'),
component: (
<Box>
{portOptions.map((option) => (
<SimpleRadio
key={option.value}
title={option.label}
checked={values.portRange === option.value}
onClick={() => updateField('portRange', option.value)}
/>
))}
{values.portRange === 'custom' && (
<Box sx={{ mt: 2 }}>
<TextFieldWithDesc
value={values.minPort.toString()}
onOwnChange={(value) =>
updateField('minPort', parseInt(value) || 1024)
}
description={t(
'randomPortGenerator.options.range.minPortDescription'
)}
inputProps={{
type: 'number',
min: 1,
max: 65535,
'data-testid': 'min-port-input'
}}
/>
<TextFieldWithDesc
value={values.maxPort.toString()}
onOwnChange={(value) =>
updateField('maxPort', parseInt(value) || 49151)
}
description={t(
'randomPortGenerator.options.range.maxPortDescription'
)}
inputProps={{
type: 'number',
min: 1,
max: 65535,
'data-testid': 'max-port-input'
}}
/>
</Box>
)}
<Box
sx={{ mt: 2, p: 2, bgcolor: 'background.paper', borderRadius: 1 }}
>
<strong>{getPortRangeInfo(values.portRange).name}</strong>
<br />
{getPortRangeInfo(values.portRange).description}
</Box>
</Box>
)
},
{
title: t('randomPortGenerator.options.generation.title'),
component: (
<Box>
<TextFieldWithDesc
value={values.count.toString()}
onOwnChange={(value) => updateField('count', parseInt(value) || 5)}
description={t(
'randomPortGenerator.options.generation.countDescription'
)}
inputProps={{
type: 'number',
min: 1,
max: 1000,
'data-testid': 'count-input'
}}
/>
<CheckboxWithDesc
title={t(
'randomPortGenerator.options.generation.allowDuplicates.title'
)}
checked={values.allowDuplicates}
onChange={(value) => updateField('allowDuplicates', value)}
description={t(
'randomPortGenerator.options.generation.allowDuplicates.description'
)}
/>
<CheckboxWithDesc
title={t(
'randomPortGenerator.options.generation.sortResults.title'
)}
checked={values.sortResults}
onChange={(value) => updateField('sortResults', value)}
description={t(
'randomPortGenerator.options.generation.sortResults.description'
)}
/>
</Box>
)
},
{
title: t('randomPortGenerator.options.output.title'),
component: (
<Box>
<TextFieldWithDesc
value={values.separator}
onOwnChange={(value) => updateField('separator', value)}
description={t(
'randomPortGenerator.options.output.separatorDescription'
)}
inputProps={{
'data-testid': 'separator-input'
}}
/>
</Box>
)
}
];
return (
<ToolContent
title={title}
initialValues={initialValues}
compute={compute}
getGroups={getGroups}
resultComponent={
<Box>
{error && (
<Alert severity="error" sx={{ mb: 2 }}>
{error}
</Alert>
)}
{result && (
<ToolTextResult
title={t('randomPortGenerator.result.title')}
value={formattedResult}
/>
)}
</Box>
}
toolInfo={{
title: t('randomPortGenerator.info.title'),
description:
longDescription || t('randomPortGenerator.info.description')
}}
/>
);
}

View File

@@ -0,0 +1,26 @@
import { defineTool } from '@tools/defineTool';
import { lazy } from 'react';
export const tool = defineTool('number', {
i18n: {
name: 'number:randomPortGenerator.title',
description: 'number:randomPortGenerator.description',
shortDescription: 'number:randomPortGenerator.shortDescription',
longDescription: 'number:randomPortGenerator.longDescription',
userTypes: ['developers']
},
path: 'random-port-generator',
icon: 'mdi:network',
keywords: [
'random',
'port',
'generator',
'network',
'tcp',
'udp',
'server',
'client',
'development'
],
component: lazy(() => import('./index'))
});

View File

@@ -0,0 +1,315 @@
import { expect, describe, it } from 'vitest';
import {
generateRandomPorts,
validateInput,
formatPorts,
getPortRangeInfo,
isCommonPort,
getPortService,
PORT_RANGES
} from './service';
import { InitialValuesType } from './types';
describe('Random Port Generator Service', () => {
describe('generateRandomPorts', () => {
it('should generate random ports within the well-known range', () => {
const options: InitialValuesType = {
portRange: 'well-known',
minPort: 1,
maxPort: 1023,
count: 5,
allowDuplicates: true,
sortResults: false,
separator: ', '
};
const result = generateRandomPorts(options);
expect(result.ports).toHaveLength(5);
expect(result.range.min).toBe(1);
expect(result.range.max).toBe(1023);
expect(result.count).toBe(5);
// Check that all ports are within range
result.ports.forEach((port) => {
expect(port).toBeGreaterThanOrEqual(1);
expect(port).toBeLessThanOrEqual(1023);
expect(Number.isInteger(port)).toBe(true);
});
});
it('should generate random ports within the registered range', () => {
const options: InitialValuesType = {
portRange: 'registered',
minPort: 1024,
maxPort: 49151,
count: 3,
allowDuplicates: false,
sortResults: false,
separator: ', '
};
const result = generateRandomPorts(options);
expect(result.ports).toHaveLength(3);
expect(result.range.min).toBe(1024);
expect(result.range.max).toBe(49151);
// Check for uniqueness
const uniquePorts = new Set(result.ports);
expect(uniquePorts.size).toBe(3);
});
it('should generate random ports within custom range', () => {
const options: InitialValuesType = {
portRange: 'custom',
minPort: 8000,
maxPort: 8100,
count: 4,
allowDuplicates: true,
sortResults: true,
separator: ', '
};
const result = generateRandomPorts(options);
expect(result.ports).toHaveLength(4);
expect(result.range.min).toBe(8000);
expect(result.range.max).toBe(8100);
expect(result.isSorted).toBe(true);
// Check that numbers are sorted
for (let i = 1; i < result.ports.length; i++) {
expect(result.ports[i]).toBeGreaterThanOrEqual(result.ports[i - 1]);
}
});
it('should throw error when minPort >= maxPort', () => {
const options: InitialValuesType = {
portRange: 'custom',
minPort: 1000,
maxPort: 500,
count: 5,
allowDuplicates: true,
sortResults: false,
separator: ', '
};
expect(() => generateRandomPorts(options)).toThrow(
'Minimum port must be less than maximum port'
);
});
it('should throw error when count <= 0', () => {
const options: InitialValuesType = {
portRange: 'registered',
minPort: 1024,
maxPort: 49151,
count: 0,
allowDuplicates: true,
sortResults: false,
separator: ', '
};
expect(() => generateRandomPorts(options)).toThrow(
'Count must be greater than 0'
);
});
it('should throw error when ports are outside valid range', () => {
const options: InitialValuesType = {
portRange: 'custom',
minPort: 0,
maxPort: 70000,
count: 5,
allowDuplicates: true,
sortResults: false,
separator: ', '
};
expect(() => generateRandomPorts(options)).toThrow(
'Ports must be between 1 and 65535'
);
});
it('should throw error when unique count exceeds available range', () => {
const options: InitialValuesType = {
portRange: 'custom',
minPort: 1,
maxPort: 5,
count: 10,
allowDuplicates: false,
sortResults: false,
separator: ', '
};
expect(() => generateRandomPorts(options)).toThrow(
'Cannot generate unique ports: count exceeds available range'
);
});
});
describe('validateInput', () => {
it('should return null for valid input', () => {
const options: InitialValuesType = {
portRange: 'registered',
minPort: 1024,
maxPort: 49151,
count: 5,
allowDuplicates: true,
sortResults: false,
separator: ', '
};
const result = validateInput(options);
expect(result).toBeNull();
});
it('should return error when count <= 0', () => {
const options: InitialValuesType = {
portRange: 'registered',
minPort: 1024,
maxPort: 49151,
count: 0,
allowDuplicates: true,
sortResults: false,
separator: ', '
};
const result = validateInput(options);
expect(result).toBe('Count must be greater than 0');
});
it('should return error when count > 1000', () => {
const options: InitialValuesType = {
portRange: 'registered',
minPort: 1024,
maxPort: 49151,
count: 1001,
allowDuplicates: true,
sortResults: false,
separator: ', '
};
const result = validateInput(options);
expect(result).toBe('Count cannot exceed 1,000');
});
it('should return error when custom range has invalid ports', () => {
const options: InitialValuesType = {
portRange: 'custom',
minPort: 0,
maxPort: 70000,
count: 5,
allowDuplicates: true,
sortResults: false,
separator: ', '
};
const result = validateInput(options);
expect(result).toBe('Ports must be between 1 and 65535');
});
it('should return error when custom range has minPort >= maxPort', () => {
const options: InitialValuesType = {
portRange: 'custom',
minPort: 1000,
maxPort: 500,
count: 5,
allowDuplicates: true,
sortResults: false,
separator: ', '
};
const result = validateInput(options);
expect(result).toBe('Minimum port must be less than maximum port');
});
});
describe('formatPorts', () => {
it('should format ports correctly', () => {
const ports = [80, 443, 8080, 3000];
const result = formatPorts(ports, ', ');
expect(result).toBe('80, 443, 8080, 3000');
});
it('should handle custom separators', () => {
const ports = [80, 443, 8080];
const result = formatPorts(ports, ' -> ');
expect(result).toBe('80 -> 443 -> 8080');
});
it('should handle empty array', () => {
const ports: number[] = [];
const result = formatPorts(ports, ', ');
expect(result).toBe('');
});
});
describe('getPortRangeInfo', () => {
it('should return correct port range info for well-known', () => {
const result = getPortRangeInfo('well-known');
expect(result.name).toBe('Well-Known Ports');
expect(result.min).toBe(1);
expect(result.max).toBe(1023);
});
it('should return correct port range info for registered', () => {
const result = getPortRangeInfo('registered');
expect(result.name).toBe('Registered Ports');
expect(result.min).toBe(1024);
expect(result.max).toBe(49151);
});
it('should return correct port range info for dynamic', () => {
const result = getPortRangeInfo('dynamic');
expect(result.name).toBe('Dynamic Ports');
expect(result.min).toBe(49152);
expect(result.max).toBe(65535);
});
it('should return custom range for unknown range', () => {
const result = getPortRangeInfo('unknown');
expect(result.name).toBe('Custom Range');
});
});
describe('isCommonPort', () => {
it('should identify common ports correctly', () => {
expect(isCommonPort(80)).toBe(true);
expect(isCommonPort(443)).toBe(true);
expect(isCommonPort(22)).toBe(true);
expect(isCommonPort(3306)).toBe(true);
});
it('should return false for uncommon ports', () => {
expect(isCommonPort(12345)).toBe(false);
expect(isCommonPort(54321)).toBe(false);
});
});
describe('getPortService', () => {
it('should return correct service names for common ports', () => {
expect(getPortService(80)).toBe('HTTP');
expect(getPortService(443)).toBe('HTTPS');
expect(getPortService(22)).toBe('SSH');
expect(getPortService(3306)).toBe('MySQL');
});
it('should return "Unknown" for uncommon ports', () => {
expect(getPortService(12345)).toBe('Unknown');
expect(getPortService(54321)).toBe('Unknown');
});
});
describe('PORT_RANGES', () => {
it('should have correct port range definitions', () => {
expect(PORT_RANGES['well-known'].min).toBe(1);
expect(PORT_RANGES['well-known'].max).toBe(1023);
expect(PORT_RANGES['registered'].min).toBe(1024);
expect(PORT_RANGES['registered'].max).toBe(49151);
expect(PORT_RANGES['dynamic'].min).toBe(49152);
expect(PORT_RANGES['dynamic'].max).toBe(65535);
});
});
});

View File

@@ -0,0 +1,214 @@
import { InitialValuesType, RandomPortResult, PortRange } from './types';
// Standard port ranges according to IANA
export const PORT_RANGES: Record<string, PortRange> = {
'well-known': {
name: 'Well-Known Ports',
min: 1,
max: 1023,
description:
'System ports (1-1023) - Reserved for common services like HTTP, HTTPS, SSH, etc.'
},
registered: {
name: 'Registered Ports',
min: 1024,
max: 49151,
description:
'User ports (1024-49151) - Available for applications and services'
},
dynamic: {
name: 'Dynamic Ports',
min: 49152,
max: 65535,
description:
'Private ports (49152-65535) - Available for temporary or private use'
},
custom: {
name: 'Custom Range',
min: 1,
max: 65535,
description: 'Custom port range - Specify your own min and max values'
}
};
/**
* Generate random network ports within a specified range
*/
export function generateRandomPorts(
options: InitialValuesType
): RandomPortResult {
const { portRange, minPort, maxPort, count, allowDuplicates, sortResults } =
options;
// Get the appropriate port range
const range = PORT_RANGES[portRange];
const actualMin = portRange === 'custom' ? minPort : range.min;
const actualMax = portRange === 'custom' ? maxPort : range.max;
if (actualMin >= actualMax) {
throw new Error('Minimum port must be less than maximum port');
}
if (count <= 0) {
throw new Error('Count must be greater than 0');
}
if (actualMin < 1 || actualMax > 65535) {
throw new Error('Ports must be between 1 and 65535');
}
if (!allowDuplicates && count > actualMax - actualMin + 1) {
throw new Error(
'Cannot generate unique ports: count exceeds available range'
);
}
const ports: number[] = [];
if (allowDuplicates) {
// Generate random ports with possible duplicates
for (let i = 0; i < count; i++) {
const randomPort = generateRandomPort(actualMin, actualMax);
ports.push(randomPort);
}
} else {
// Generate unique random ports
const availablePorts = new Set<number>();
// Create a pool of available ports
for (let i = actualMin; i <= actualMax; i++) {
availablePorts.add(i);
}
const availableArray = Array.from(availablePorts);
// Shuffle the available ports
for (let i = availableArray.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[availableArray[i], availableArray[j]] = [
availableArray[j],
availableArray[i]
];
}
// Take the first 'count' ports
for (let i = 0; i < Math.min(count, availableArray.length); i++) {
ports.push(availableArray[i]);
}
}
// Sort if requested
if (sortResults) {
ports.sort((a, b) => a - b);
}
return {
ports,
range: {
...range,
min: actualMin,
max: actualMax
},
count,
hasDuplicates: !allowDuplicates && hasDuplicatesInArray(ports),
isSorted: sortResults
};
}
/**
* Generate a single random port within the specified range
*/
function generateRandomPort(min: number, max: number): number {
return Math.floor(Math.random() * (max - min + 1)) + min;
}
/**
* Check if an array has duplicate values
*/
function hasDuplicatesInArray(arr: number[]): boolean {
const seen = new Set<number>();
for (const num of arr) {
if (seen.has(num)) {
return true;
}
seen.add(num);
}
return false;
}
/**
* Format ports for display
*/
export function formatPorts(ports: number[], separator: string): string {
return ports.map((port) => port.toString()).join(separator);
}
/**
* Validate input parameters
*/
export function validateInput(options: InitialValuesType): string | null {
const { portRange, minPort, maxPort, count } = options;
if (count <= 0) {
return 'Count must be greater than 0';
}
if (count > 1000) {
return 'Count cannot exceed 1,000';
}
if (portRange === 'custom') {
if (minPort >= maxPort) {
return 'Minimum port must be less than maximum port';
}
if (minPort < 1 || maxPort > 65535) {
return 'Ports must be between 1 and 65535';
}
}
return null;
}
/**
* Get port range information
*/
export function getPortRangeInfo(portRange: string): PortRange {
return PORT_RANGES[portRange] || PORT_RANGES['custom'];
}
/**
* Check if a port is commonly used
*/
export function isCommonPort(port: number): boolean {
const commonPorts = [
20, 21, 22, 23, 25, 53, 80, 110, 143, 443, 993, 995, 3306, 5432, 6379, 8080
];
return commonPorts.includes(port);
}
/**
* Get port service information
*/
export function getPortService(port: number): string {
const portServices: Record<number, string> = {
20: 'FTP Data',
21: 'FTP Control',
22: 'SSH',
23: 'Telnet',
25: 'SMTP',
53: 'DNS',
80: 'HTTP',
110: 'POP3',
143: 'IMAP',
443: 'HTTPS',
993: 'IMAPS',
995: 'POP3S',
3306: 'MySQL',
5432: 'PostgreSQL',
6379: 'Redis',
8080: 'HTTP Alternative'
};
return portServices[port] || 'Unknown';
}

View File

@@ -0,0 +1,24 @@
export type InitialValuesType = {
portRange: 'well-known' | 'registered' | 'dynamic' | 'custom';
minPort: number;
maxPort: number;
count: number;
allowDuplicates: boolean;
sortResults: boolean;
separator: string;
};
export type PortRange = {
name: string;
min: number;
max: number;
description: string;
};
export type RandomPortResult = {
ports: number[];
range: PortRange;
count: number;
hasDuplicates: boolean;
isSorted: boolean;
};

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

@@ -5,7 +5,16 @@ export const tool = defineTool('string', {
path: 'url-decode-string',
icon: 'codicon:symbol-string',
keywords: ['uppercase'],
keywords: [
'url',
'decode',
'string',
'url decode',
'unescape',
'encoding',
'percent',
'decode url'
],
component: lazy(() => import('./index')),
i18n: {
name: 'string:urlDecode.toolInfo.title',

View File

@@ -5,7 +5,7 @@ export const tool = defineTool('string', {
path: 'url-encode-string',
icon: 'ic:baseline-percentage',
keywords: ['uppercase'],
keywords: ['url', 'encode', 'string', 'url encode', 'encoding', 'percent'],
component: lazy(() => import('./index')),
i18n: {
name: 'string:urlEncode.toolInfo.title',

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;
}

Some files were not shown because too many files have changed in this diff Show More