Compare commits

...

16 commits

Author SHA1 Message Date
Ibrahima G. Coulibaly
fafa36a02b
Merge pull request #212
Some checks are pending
CI / test-and-build (push) Waiting to run
CI / Playwright Tests (push) Waiting to run
CI / Build and Push Multi-Platform Docker Image (push) Blocked by required conditions
CI / deploy (push) Blocked by required conditions
fix (string): correct keywords for url  encoder and decoder features
2025-07-24 22:59:13 +01:00
Ibrahima G. Coulibaly
4d1a4e7e99
Merge pull request #213
fix(ui): improve code editor styling to match MUI components
2025-07-24 22:58:26 +01:00
Ibrahima G. Coulibaly
14e090a5e7
Merge pull request #218 from AshAnand34/tool/random-generators
Random Port and Number generators
2025-07-24 22:50:40 +01:00
Ibrahima G. Coulibaly
7a6e1c0e07 chore: userTypes 2025-07-24 22:48:22 +01:00
Ibrahima G. Coulibaly
75b08de646 fix: misc 2025-07-24 22:46:43 +01:00
Ibrahima G. Coulibaly
ebe0858e03
Merge pull request #215
feat: replace text inputs with code editor in JSON tools
2025-07-24 22:24:14 +01:00
Ibrahima G. Coulibaly
d8b214e57e
Merge pull request #219
Heic background remover
2025-07-24 22:21:46 +01:00
Aashish Anand
ad58d5c584
Merge branch 'iib0011:main' into tool/random-generators 2025-07-22 12:33:35 -07:00
AshAnand34
2b27329d7f feat: add support for HEIC image conversion using heic2any 2025-07-20 00:24:26 -07:00
AshAnand34
164c2b6ecc Revert "feat: add random number and port generators with customizable options and validations"
This reverts commit b8d6924abc.
2025-07-20 00:09:39 -07:00
AshAnand34
1734b4dff4 feat: add random number and port generators with customizable options and validations 2025-07-19 23:06:44 -07:00
AshAnand34
b8d6924abc feat: add random number and port generators with customizable options and validations 2025-07-19 20:17:27 -07:00
Bhavesh Kshatriya
4cec13da63 feat: replace text inputs with code editor in JSON tools 2025-07-19 19:04:55 +05:30
Bhavesh Kshatriya
4ba23ca127 fix: configure scrollbar settings for Monaco Editor 2025-07-19 18:38:38 +05:30
Bhavesh Kshatriya
6942b0b0a4 fix(ui): improve code editor styling to match MUI components 2025-07-19 18:32:33 +05:30
Chesterkxng
52ebb681fd fix (string): correct keywords for url encoder and decoder features 2025-07-19 02:47:41 +02:00
27 changed files with 1710 additions and 56 deletions

36
.idea/workspace.xml generated
View file

@ -6,30 +6,10 @@
<component name="ChangeListManager">
<list default="true" id="b30e2810-c4c1-4aad-b134-794e52cc1c7d" name="Changes" comment="chore: translate userTypes">
<change beforePath="$PROJECT_DIR$/.idea/workspace.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/workspace.xml" afterDir="false" />
<change beforePath="$PROJECT_DIR$/public/locales/de/pdf.json" beforeDir="false" afterPath="$PROJECT_DIR$/public/locales/de/pdf.json" afterDir="false" />
<change beforePath="$PROJECT_DIR$/public/locales/en/audio.json" beforeDir="false" afterPath="$PROJECT_DIR$/public/locales/en/audio.json" afterDir="false" />
<change beforePath="$PROJECT_DIR$/public/locales/en/csv.json" beforeDir="false" afterPath="$PROJECT_DIR$/public/locales/en/csv.json" afterDir="false" />
<change beforePath="$PROJECT_DIR$/public/locales/en/image.json" beforeDir="false" afterPath="$PROJECT_DIR$/public/locales/en/image.json" afterDir="false" />
<change beforePath="$PROJECT_DIR$/public/locales/en/json.json" beforeDir="false" afterPath="$PROJECT_DIR$/public/locales/en/json.json" afterDir="false" />
<change beforePath="$PROJECT_DIR$/public/locales/en/list.json" beforeDir="false" afterPath="$PROJECT_DIR$/public/locales/en/list.json" afterDir="false" />
<change beforePath="$PROJECT_DIR$/public/locales/en/number.json" beforeDir="false" afterPath="$PROJECT_DIR$/public/locales/en/number.json" afterDir="false" />
<change beforePath="$PROJECT_DIR$/public/locales/en/pdf.json" beforeDir="false" afterPath="$PROJECT_DIR$/public/locales/en/pdf.json" afterDir="false" />
<change beforePath="$PROJECT_DIR$/public/locales/en/string.json" beforeDir="false" afterPath="$PROJECT_DIR$/public/locales/en/string.json" afterDir="false" />
<change beforePath="$PROJECT_DIR$/public/locales/en/time.json" beforeDir="false" afterPath="$PROJECT_DIR$/public/locales/en/time.json" afterDir="false" />
<change beforePath="$PROJECT_DIR$/public/locales/en/translation.json" beforeDir="false" afterPath="$PROJECT_DIR$/public/locales/en/translation.json" afterDir="false" />
<change beforePath="$PROJECT_DIR$/public/locales/en/video.json" beforeDir="false" afterPath="$PROJECT_DIR$/public/locales/en/video.json" afterDir="false" />
<change beforePath="$PROJECT_DIR$/public/locales/es/pdf.json" beforeDir="false" afterPath="$PROJECT_DIR$/public/locales/es/pdf.json" afterDir="false" />
<change beforePath="$PROJECT_DIR$/public/locales/es/translation.json" beforeDir="false" afterPath="$PROJECT_DIR$/public/locales/es/translation.json" afterDir="false" />
<change beforePath="$PROJECT_DIR$/public/locales/fr/pdf.json" beforeDir="false" afterPath="$PROJECT_DIR$/public/locales/fr/pdf.json" afterDir="false" />
<change beforePath="$PROJECT_DIR$/public/locales/fr/time.json" beforeDir="false" afterPath="$PROJECT_DIR$/public/locales/fr/time.json" afterDir="false" />
<change beforePath="$PROJECT_DIR$/public/locales/fr/translation.json" beforeDir="false" afterPath="$PROJECT_DIR$/public/locales/fr/translation.json" afterDir="false" />
<change beforePath="$PROJECT_DIR$/public/locales/hi/json.json" beforeDir="false" afterPath="$PROJECT_DIR$/public/locales/hi/json.json" afterDir="false" />
<change beforePath="$PROJECT_DIR$/public/locales/hi/pdf.json" beforeDir="false" afterPath="$PROJECT_DIR$/public/locales/hi/pdf.json" afterDir="false" />
<change beforePath="$PROJECT_DIR$/public/locales/ja/pdf.json" beforeDir="false" afterPath="$PROJECT_DIR$/public/locales/ja/pdf.json" afterDir="false" />
<change beforePath="$PROJECT_DIR$/public/locales/nl/pdf.json" beforeDir="false" afterPath="$PROJECT_DIR$/public/locales/nl/pdf.json" afterDir="false" />
<change beforePath="$PROJECT_DIR$/public/locales/pt/pdf.json" beforeDir="false" afterPath="$PROJECT_DIR$/public/locales/pt/pdf.json" afterDir="false" />
<change beforePath="$PROJECT_DIR$/public/locales/ru/pdf.json" beforeDir="false" afterPath="$PROJECT_DIR$/public/locales/ru/pdf.json" afterDir="false" />
<change beforePath="$PROJECT_DIR$/public/locales/zh/pdf.json" beforeDir="false" afterPath="$PROJECT_DIR$/public/locales/zh/pdf.json" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/pages/tools/number/index.ts" beforeDir="false" afterPath="$PROJECT_DIR$/src/pages/tools/number/index.ts" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/pages/tools/number/random-number-generator/index.tsx" beforeDir="false" afterPath="$PROJECT_DIR$/src/pages/tools/number/random-number-generator/index.tsx" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/pages/tools/number/random-port-generator/index.tsx" beforeDir="false" afterPath="$PROJECT_DIR$/src/pages/tools/number/random-port-generator/index.tsx" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/pages/tools/number/random-port-generator/meta.ts" beforeDir="false" afterPath="$PROJECT_DIR$/src/pages/tools/number/random-port-generator/meta.ts" afterDir="false" />
</list>
<option name="SHOW_DIALOG" value="false" />
<option name="HIGHLIGHT_CONFLICTS" value="true" />
@ -63,7 +43,7 @@
<option name="PUSH_AUTO_UPDATE" value="true" />
<option name="RECENT_BRANCH_BY_REPOSITORY">
<map>
<entry key="$PROJECT_DIR$" value="main" />
<entry key="$PROJECT_DIR$" value="fork/AshAnand34/tool/random-generators" />
</map>
</option>
<option name="RECENT_GIT_ROOT_PATH" value="$PROJECT_DIR$" />
@ -346,7 +326,7 @@
"Vitest.replaceText function (regexp mode).should return the original text when passed an invalid regexp.executor": "Run",
"Vitest.replaceText function.executor": "Run",
"Vitest.timeBetweenDates.executor": "Run",
"git-widget-placeholder": "#209 on fork/AshAnand34/tools-filtering",
"git-widget-placeholder": "#218 on fork/AshAnand34/tool/random-generators",
"ignore.virus.scanning.warn.message": "true",
"kotlin-language-version-configured": "true",
"last_opened_file_path": "C:/Users/Ibrahima/IdeaProjects/omni-tools",
@ -402,7 +382,7 @@
<recent name="C:\Users\Ibrahima\IdeaProjects\omni-tools\src\pages\categories" />
</key>
</component>
<component name="RunManager" selected="npm.i18n:sync">
<component name="RunManager" selected="npm.dev">
<configuration name="generatePassword" type="JavaScriptTestRunnerVitest" temporary="true" nameIsGenerated="true">
<node-interpreter value="project" />
<vitest-package value="$PROJECT_DIR$/node_modules/vitest" />
@ -476,8 +456,8 @@
</list>
<recent_temporary>
<list>
<item itemvalue="npm.i18n:sync" />
<item itemvalue="npm.dev" />
<item itemvalue="npm.i18n:sync" />
<item itemvalue="Vitest.generatePassword" />
<item itemvalue="npm.i18n:pull" />
<item itemvalue="npm.i18n:extract" />

7
package-lock.json generated
View file

@ -34,6 +34,7 @@
"dayjs": "^1.11.13",
"fast-xml-parser": "^5.2.5",
"formik": "^2.4.6",
"heic2any": "^0.0.4",
"i18next": "^25.3.2",
"i18next-browser-languagedetector": "^8.2.0",
"i18next-http-backend": "^3.0.2",
@ -7025,6 +7026,12 @@
"node": ">= 0.4"
}
},
"node_modules/heic2any": {
"version": "0.0.4",
"resolved": "https://registry.npmjs.org/heic2any/-/heic2any-0.0.4.tgz",
"integrity": "sha512-3lLnZiDELfabVH87htnRolZ2iehX9zwpRyGNz22GKXIu0fznlblf0/ftppXKNqS26dqFSeqfIBhAmAj/uSp0cA==",
"license": "MIT"
},
"node_modules/hoist-non-react-statics": {
"version": "3.3.2",
"resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz",

View file

@ -53,6 +53,7 @@
"dayjs": "^1.11.13",
"fast-xml-parser": "^5.2.5",
"formik": "^2.4.6",
"heic2any": "^0.0.4",
"i18next": "^25.3.2",
"i18next-browser-languagedetector": "^8.2.0",
"i18next-http-backend": "^3.0.2",

View file

@ -93,5 +93,100 @@
"longDescription": "This calculator helps determine the voltage drop and power loss in a two-conductor electrical cable. It takes into account the cable length, wire gauge (cross-sectional area), material resistivity, and current flow. The tool calculates the round-trip voltage drop, total resistance of the cable, and the power dissipated as heat. This is particularly useful for electrical engineers, electricians, and hobbyists when designing electrical systems to ensure voltage levels remain within acceptable limits at the load.",
"shortDescription": "Calculate voltage drop and power loss in electrical cables based on length, material, and current",
"title": "Round trip voltage drop in cable"
},
"randomNumberGenerator": {
"title": "Random Number Generator",
"description": "Generate random numbers within a specified range with customizable options.",
"shortDescription": "Generate random numbers in custom ranges",
"longDescription": "Generate random numbers within a specified range with options for integers or decimals, allowing or preventing duplicates, and sorting results. Perfect for simulations, testing, games, and statistical analysis.",
"options": {
"range": {
"title": "Range Settings",
"minDescription": "Minimum value (inclusive)",
"maxDescription": "Maximum value (inclusive)"
},
"generation": {
"title": "Generation Options",
"countDescription": "Number of random numbers to generate (1-10,000)",
"allowDecimals": {
"title": "Allow Decimal Numbers",
"description": "Generate decimal numbers instead of integers"
},
"allowDuplicates": {
"title": "Allow Duplicates",
"description": "Allow the same number to appear multiple times"
},
"sortResults": {
"title": "Sort Results",
"description": "Sort the generated numbers in ascending order"
}
},
"output": {
"title": "Output Settings",
"separatorDescription": "Character(s) to separate the generated numbers"
}
},
"result": {
"title": "Generated Random Numbers",
"range": "Range",
"count": "Count",
"hasDuplicates": "Contains Duplicates",
"isSorted": "Sorted"
},
"error": {
"generationFailed": "Failed to generate random numbers. Please check your input parameters."
},
"info": {
"title": "What is a Random Number Generator?",
"description": "A random number generator creates unpredictable numbers within a specified range. This tool uses cryptographically secure random number generation to ensure truly random results. Useful for simulations, games, statistical sampling, and testing scenarios."
}
},
"randomPortGenerator": {
"title": "Random Port Generator",
"description": "Generate random network ports within specified ranges with customizable options.",
"shortDescription": "Generate random network ports",
"longDescription": "Generate random network ports within specified ranges (well-known, registered, dynamic, or custom). Perfect for development, testing, and network configuration. Includes port service identification for common ports.",
"options": {
"range": {
"title": "Port Range Settings",
"wellKnown": "Well-Known Ports (1-1023)",
"registered": "Registered Ports (1024-49151)",
"dynamic": "Dynamic Ports (49152-65535)",
"custom": "Custom Range",
"minPortDescription": "Minimum port number (1-65535)",
"maxPortDescription": "Maximum port number (1-65535)"
},
"generation": {
"title": "Generation Options",
"countDescription": "Number of random ports to generate (1-1,000)",
"allowDuplicates": {
"title": "Allow Duplicates",
"description": "Allow the same port to appear multiple times"
},
"sortResults": {
"title": "Sort Results",
"description": "Sort the generated ports in ascending order"
}
},
"output": {
"title": "Output Settings",
"separatorDescription": "Character(s) to separate the generated ports"
}
},
"result": {
"title": "Generated Random Ports",
"range": "Port Range",
"count": "Count",
"hasDuplicates": "Contains Duplicates",
"isSorted": "Sorted",
"portDetails": "Port Details"
},
"error": {
"generationFailed": "Failed to generate random ports. Please check your input parameters."
},
"info": {
"title": "What is a Random Port Generator?",
"description": "A random port generator creates unpredictable network port numbers within specified ranges. This tool follows IANA port number standards and includes identification of common services. Useful for development, testing, network configuration, and avoiding port conflicts."
}
}
}

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}>
<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={'87%'}
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

@ -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

@ -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,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

@ -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',