diff --git a/.idea/workspace.xml b/.idea/workspace.xml
index 9948720..6b54db0 100644
--- a/.idea/workspace.xml
+++ b/.idea/workspace.xml
@@ -4,9 +4,12 @@
-
+
-
+
+
+
+
@@ -24,10 +27,23 @@
+
+
+
+
+
@@ -40,198 +56,217 @@
},
{
"state": "OPEN"
+ },
+ {
+ "searchQuery": "filter",
+ "state": "OPEN"
}
],
"lastFilter": {
+ "searchQuery": "filter",
"state": "OPEN"
}
}
- {
+ "prStates": [
{
- "id": {
- "id": "PR_kwDOMJIfts51PkS9",
- "number": 22
+ "id": {
+ "id": "PR_kwDOMJIfts51PkS9",
+ "number": 22
},
- "lastSeen": 1741207144695
+ "lastSeen": 1741207144695
},
{
- "id": {
- "id": "PR_kwDOMJIfts6NiNYl",
- "number": 32
+ "id": {
+ "id": "PR_kwDOMJIfts6NiNYl",
+ "number": 32
},
- "lastSeen": 1741209723869
+ "lastSeen": 1741209723869
},
{
- "id": {
- "id": "PR_kwDOMJIfts6Nheyd",
- "number": 31
+ "id": {
+ "id": "PR_kwDOMJIfts6Nheyd",
+ "number": 31
},
- "lastSeen": 1741213371410
+ "lastSeen": 1741213371410
},
{
- "id": {
- "id": "PR_kwDOMJIfts6NmRBs",
- "number": 33
+ "id": {
+ "id": "PR_kwDOMJIfts6NmRBs",
+ "number": 33
},
- "lastSeen": 1741282429036
+ "lastSeen": 1741282429036
},
{
- "id": {
- "id": "PR_kwDOMJIfts5zyFTs",
- "number": 15
+ "id": {
+ "id": "PR_kwDOMJIfts5zyFTs",
+ "number": 15
},
- "lastSeen": 1741535540953
+ "lastSeen": 1741535540953
},
{
- "id": {
- "id": "PR_kwDOMJIfts6QQB3c",
- "number": 59
+ "id": {
+ "id": "PR_kwDOMJIfts6QQB3c",
+ "number": 59
},
- "lastSeen": 1743018960900
+ "lastSeen": 1743018960900
},
{
- "id": {
- "id": "PR_kwDOMJIfts6QMPEg",
- "number": 58
+ "id": {
+ "id": "PR_kwDOMJIfts6QMPEg",
+ "number": 58
},
- "lastSeen": 1743019452983
+ "lastSeen": 1743019452983
},
{
- "id": {
- "id": "PR_kwDOMJIfts6QZvRI",
- "number": 61
+ "id": {
+ "id": "PR_kwDOMJIfts6QZvRI",
+ "number": 61
},
- "lastSeen": 1743103196866
+ "lastSeen": 1743103196866
},
{
- "id": {
- "id": "PR_kwDOMJIfts6QqPrQ",
- "number": 73
+ "id": {
+ "id": "PR_kwDOMJIfts6QqPrQ",
+ "number": 73
},
- "lastSeen": 1743265865001
+ "lastSeen": 1743265865001
},
{
- "id": {
- "id": "PR_kwDOMJIfts6Qp5nI",
- "number": 72
+ "id": {
+ "id": "PR_kwDOMJIfts6Qp5nI",
+ "number": 72
},
- "lastSeen": 1743338472110
+ "lastSeen": 1743338472110
},
{
- "id": {
- "id": "PR_kwDOMJIfts6QsjlS",
- "number": 76
+ "id": {
+ "id": "PR_kwDOMJIfts6QsjlS",
+ "number": 76
},
- "lastSeen": 1743352150953
+ "lastSeen": 1743352150953
},
{
- "id": {
- "id": "PR_kwDOMJIfts6Q0JBe",
- "number": 82
+ "id": {
+ "id": "PR_kwDOMJIfts6Q0JBe",
+ "number": 82
},
- "lastSeen": 1743470267269
+ "lastSeen": 1743470267269
},
{
- "id": {
- "id": "PR_kwDOMJIfts6UE9-x",
- "number": 102
+ "id": {
+ "id": "PR_kwDOMJIfts6UE9-x",
+ "number": 102
},
- "lastSeen": 1747171977348
+ "lastSeen": 1747171977348
},
{
- "id": {
- "id": "PR_kwDOMJIfts6XPua_",
- "number": 117
+ "id": {
+ "id": "PR_kwDOMJIfts6XPua_",
+ "number": 117
},
- "lastSeen": 1747929835864
+ "lastSeen": 1747929835864
},
{
- "id": {
- "id": "PR_kwDOMJIfts6XY-mZ",
- "number": 119
+ "id": {
+ "id": "PR_kwDOMJIfts6XY-mZ",
+ "number": 119
},
- "lastSeen": 1748028108508
+ "lastSeen": 1748028108508
},
{
- "id": {
- "id": "PR_kwDOMJIfts6Xdz4n",
- "number": 120
+ "id": {
+ "id": "PR_kwDOMJIfts6Xdz4n",
+ "number": 120
},
- "lastSeen": 1748282672214
+ "lastSeen": 1748282672214
},
{
- "id": {
- "id": "PR_kwDOMJIfts6X_zxl",
- "number": 131
+ "id": {
+ "id": "PR_kwDOMJIfts6X_zxl",
+ "number": 131
},
- "lastSeen": 1748881279494
+ "lastSeen": 1748881279494
},
{
- "id": {
- "id": "PR_kwDOMJIfts6bhieT",
- "number": 152
+ "id": {
+ "id": "PR_kwDOMJIfts6bhieT",
+ "number": 152
},
- "lastSeen": 1751848489082
+ "lastSeen": 1751848489082
},
{
- "id": {
- "id": "PR_kwDOMJIfts6dOyRk",
- "number": 154
+ "id": {
+ "id": "PR_kwDOMJIfts6dOyRk",
+ "number": 154
},
- "lastSeen": 1751849436454
+ "lastSeen": 1751849436454
},
{
- "id": {
- "id": "PR_kwDOMJIfts6cHjNi",
- "number": 153
+ "id": {
+ "id": "PR_kwDOMJIfts6cHjNi",
+ "number": 153
},
- "lastSeen": 1751849501498
+ "lastSeen": 1751849501498
},
{
- "id": {
- "id": "PR_kwDOMJIfts6Zs1FN",
- "number": 145
+ "id": {
+ "id": "PR_kwDOMJIfts6Zs1FN",
+ "number": 145
},
- "lastSeen": 1751849770308
+ "lastSeen": 1751849770308
},
{
- "id": {
- "id": "PR_kwDOMJIfts6bgKi9",
- "number": 150
+ "id": {
+ "id": "PR_kwDOMJIfts6bgKi9",
+ "number": 150
},
- "lastSeen": 1751850367300
+ "lastSeen": 1751850367300
},
{
- "id": {
- "id": "PR_kwDOMJIfts6eUKC-",
- "number": 176
+ "id": {
+ "id": "PR_kwDOMJIfts6eUKC-",
+ "number": 176
},
- "lastSeen": 1752158748013
+ "lastSeen": 1752158748013
},
{
- "id": {
- "id": "PR_kwDOMJIfts6eqzP7",
- "number": 190
+ "id": {
+ "id": "PR_kwDOMJIfts6eqzP7",
+ "number": 190
},
- "lastSeen": 1752404173008
+ "lastSeen": 1752404173008
},
{
- "id": {
- "id": "PR_kwDOMJIfts6et6vx",
- "number": 192
+ "id": {
+ "id": "PR_kwDOMJIfts6et6vx",
+ "number": 192
},
- "lastSeen": 1752585709582
+ "lastSeen": 1752585709582
},
{
- "id": {
- "id": "PR_kwDOMJIfts6d36mi",
- "number": 168
+ "id": {
+ "id": "PR_kwDOMJIfts6d36mi",
+ "number": 168
},
- "lastSeen": 1752805763664
+ "lastSeen": 1752805763664
+ },
+ {
+ "id": {
+ "id": "PR_kwDOMJIfts6fnXKf",
+ "number": 208
+ },
+ "lastSeen": 1752862212326
+ },
+ {
+ "id": {
+ "id": "PR_kwDOMJIfts6fo_ig",
+ "number": 209
+ },
+ "lastSeen": 1753201966322
}
]
-}]]>
+}
{
"selectedUrlAndAccountId": {
"url": "https://github.com/iib0011/omni-tools.git",
@@ -291,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": "main",
+ "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",
@@ -316,8 +351,9 @@
"project.structure.last.edited": "Problems",
"project.structure.proportion": "0.0",
"project.structure.side.proportion": "0.2",
- "settings.editor.selected.configurable": "refactai_advanced_settings",
+ "settings.editor.selected.configurable": "preferences.pluginManager",
"ts.external.directory.path": "C:\\Users\\Ibrahima\\IdeaProjects\\omni-tools\\node_modules\\typescript\\lib",
+ "ts.rename.search.for.js.occurrences": "false",
"vue.rearranger.settings.migration": "true"
}
}]]>
@@ -422,9 +458,9 @@
+
-
@@ -538,46 +574,12 @@
-
-
-
- 1748027090253
-
-
-
- 1748027090253
-
-
-
- 1748027889103
-
-
-
- 1748027889103
-
-
-
- 1748028055669
-
-
-
- 1748028055669
-
-
-
- 1748881153433
-
-
-
- 1748881153433
-
-
-
- 1749147227565
-
-
-
- 1749147227565
+
+
+
+
+
+
@@ -931,7 +933,47 @@
1752805853344
-
+
+
+ 1753124389709
+
+
+
+ 1753124389709
+
+
+
+ 1753206794968
+
+
+
+ 1753206794968
+
+
+
+ 1753207817041
+
+
+
+ 1753207817041
+
+
+
+ 1753209484099
+
+
+
+ 1753209484099
+
+
+
+ 1753210033390
+
+
+
+ 1753210033390
+
+
@@ -978,11 +1020,6 @@
-
-
-
-
-
@@ -1003,7 +1040,12 @@
-
+
+
+
+
+
+
false
diff --git a/package-lock.json b/package-lock.json
index 835edf1..781c65a 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -34,7 +34,9 @@
"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",
"jimp": "^0.22.12",
"js-quantities": "^1.8.0",
@@ -7024,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",
@@ -7134,6 +7142,15 @@
}
}
},
+ "node_modules/i18next-browser-languagedetector": {
+ "version": "8.2.0",
+ "resolved": "https://registry.npmjs.org/i18next-browser-languagedetector/-/i18next-browser-languagedetector-8.2.0.tgz",
+ "integrity": "sha512-P+3zEKLnOF0qmiesW383vsLdtQVyKtCNA9cjSoKCppTKPQVfKd2W8hbVo5ZhNJKDqeM7BOcvNoKJOjpHh4Js9g==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/runtime": "^7.23.2"
+ }
+ },
"node_modules/i18next-http-backend": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/i18next-http-backend/-/i18next-http-backend-3.0.2.tgz",
diff --git a/package.json b/package.json
index 494c547..ecd8f8e 100644
--- a/package.json
+++ b/package.json
@@ -53,7 +53,9 @@
"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",
"jimp": "^0.22.12",
"js-quantities": "^1.8.0",
diff --git a/public/locales/en/number.json b/public/locales/en/number.json
index 9d57f64..dd7dc64 100644
--- a/public/locales/en/number.json
+++ b/public/locales/en/number.json
@@ -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."
+ }
}
}
diff --git a/public/locales/en/pdf.json b/public/locales/en/pdf.json
index eb4b98d..d6b2860 100644
--- a/public/locales/en/pdf.json
+++ b/public/locales/en/pdf.json
@@ -11,6 +11,7 @@
"highCompression": "High Compression",
"highCompressionDescription": "Maximum file size reduction with some quality loss",
"inputTitle": "Input PDF",
+ "longDescription": "Compress PDF files securely in your browser using Ghostscript. Your files never leave your device, ensuring complete privacy while reducing file sizes for email sharing, uploading to websites, or saving storage space. Powered by WebAssembly technology.",
"lowCompression": "Low Compression",
"lowCompressionDescription": "Slightly reduce file size with minimal quality loss",
"mediumCompression": "Medium Compression",
diff --git a/public/locales/en/string.json b/public/locales/en/string.json
index 61d0042..fdb0117 100644
--- a/public/locales/en/string.json
+++ b/public/locales/en/string.json
@@ -258,30 +258,30 @@
"shortDescription": "Convert text to uppercase",
"title": "Convert to Uppercase"
},
- "urlEncode": {
- "toolInfo": {
- "description": "Load your string and it will automatically get URL-escaped.",
- "shortDescription": "Quickly URL-escape a string.",
- "longDescription": "This tool URL-encodes a string. Special URL characters get converted to percent-sign encoding. This encoding is called percent-encoding because each character's numeric value gets converted to a percent sign followed by a two-digit hexadecimal value. The hex values are determined based on the character's codepoint value. For example, a space gets escaped to %20, a colon to %3a, a slash to %2f. Characters that are not special stay unchanged. In case you also need to convert non-special characters to percent-encoding, then we've also added an extra option that lets you do that. Select the encode-non-special-chars option to enable this behavior.",
- "title": "String URL encoder"
- },
- "encodingOption": {
- "title": "Encoding Options",
- "nonSpecialCharPlaceholder": "Encode non-special characters",
- "nonSpecialCharDescription": "If selected, then all characters in the input string will be converted to URL-encoding (not just special)."
- },
- "inputTitle": "Input String",
- "resultTitle": "Url-escaped String"
- },
"urlDecode": {
+ "inputTitle": "Input String(URL-escaped)",
+ "resultTitle": "Output string",
"toolInfo": {
"description": "Load your string and it will automatically get URL-unescaped.",
- "shortDescription": "Quickly URL-unescape a string.",
"longDescription": "This tool URL-decodes a previously URL-encoded string. URL-decoding is the inverse operation of URL-encoding. All percent-encoded characters get decoded to characters that you can understand. Some of the most well known percent-encoded values are %20 for a space, %3a for a colon, %2f for a slash, and %3f for a question mark. The two digits following the percent sign are character's char code values in hex.",
+ "shortDescription": "Quickly URL-unescape a string.",
"title": "String URL decoder"
+ }
+ },
+ "urlEncode": {
+ "encodingOption": {
+ "nonSpecialCharDescription": "If selected, then all characters in the input string will be converted to URL-encoding (not just special).",
+ "nonSpecialCharPlaceholder": "Encode non-special characters",
+ "title": "Encoding Options"
},
- "inputTitle": "Input String(URL-escaped)",
- "resultTitle": "Output string"
+ "inputTitle": "Input String",
+ "resultTitle": "Url-escaped String",
+ "toolInfo": {
+ "description": "Load your string and it will automatically get URL-escaped.",
+ "longDescription": "This tool URL-encodes a string. Special URL characters get converted to percent-sign encoding. This encoding is called percent-encoding because each character's numeric value gets converted to a percent sign followed by a two-digit hexadecimal value. The hex values are determined based on the character's codepoint value. For example, a space gets escaped to %20, a colon to %3a, a slash to %2f. Characters that are not special stay unchanged. In case you also need to convert non-special characters to percent-encoding, then we've also added an extra option that lets you do that. Select the encode-non-special-chars option to enable this behavior.",
+ "shortDescription": "Quickly URL-escape a string.",
+ "title": "String URL encoder"
+ }
},
"hiddenCharacterDetector": {
"title": "Hidden Character Detector",
diff --git a/public/locales/en/time.json b/public/locales/en/time.json
index 1eb65bd..6bfb88e 100644
--- a/public/locales/en/time.json
+++ b/public/locales/en/time.json
@@ -58,6 +58,21 @@
"title": "Convert Time to Seconds"
}
},
+ "convertUnixToDate": {
+ "addUtcLabel": "Add 'UTC' suffix",
+ "addUtcLabelDescription": "Display 'UTC' after the converted date (only for UTC mode)",
+ "description": "Convert a Unix timestamp to a human-readable date.",
+ "outputOptions": "Output Options",
+ "shortDescription": "Convert Unix timestamp to date",
+ "title": "Convert Unix to Date",
+ "toolInfo": {
+ "description": "This tool converts a Unix timestamp (in seconds) into a human-readable date format (e.g., YYYY-MM-DD HH:MM:SS). It supports both local and UTC output, making it useful for quickly interpreting timestamps from logs, APIs, or systems that use Unix time.",
+ "title": "Convert Unix to Date"
+ },
+ "useLocalTime": "Use Local Time",
+ "useLocalTimeDescription": "Show converted date in your local timezone instead of UTC",
+ "withLabel": "Options"
+ },
"crontabGuru": {
"description": "Generate and understand cron expressions. Create cron schedules for automated tasks and system jobs.",
"shortDescription": "Generate and understand cron expressions",
@@ -98,21 +113,5 @@
"zeroPaddingDescription": "Make all time components always be two digits wide.",
"zeroPrintDescription": "Display the dropped parts as zero values \"00\".",
"zeroPrintTruncatedParts": "Zero-print Truncated Parts"
- },
- "convertUnixToDate": {
- "title": "Convert Unix to Date",
- "description": "Convert a Unix timestamp to a human-readable date.",
- "shortDescription": "Convert Unix timestamp to date",
- "longDescription": "",
- "withLabel": "Options",
- "outputOptions": "Output Options",
- "addUtcLabel": "Add 'UTC' suffix",
- "addUtcLabelDescription": "Display 'UTC' after the converted date (only for UTC mode)",
- "useLocalTime": "Use Local Time",
- "useLocalTimeDescription": "Show converted date in your local timezone instead of UTC",
- "toolInfo": {
- "title": "Convert Unix to Date",
- "description": "This tool converts a Unix timestamp (in seconds) into a human-readable date format (e.g., YYYY-MM-DD HH:MM:SS). It supports both local and UTC output, making it useful for quickly interpreting timestamps from logs, APIs, or systems that use Unix time."
- }
}
}
diff --git a/public/locales/en/translation.json b/public/locales/en/translation.json
index 789e161..00f7878 100644
--- a/public/locales/en/translation.json
+++ b/public/locales/en/translation.json
@@ -246,5 +246,9 @@
"copyFailed": "Failed to copy: {{error}}",
"loading": "Loading... This may take a moment.",
"result": "Result"
+ },
+ "userTypes": {
+ "developers": "Developers",
+ "generalUsers": "General users"
}
}
diff --git a/public/locales/en/video.json b/public/locales/en/video.json
index 397df0e..cb9d961 100644
--- a/public/locales/en/video.json
+++ b/public/locales/en/video.json
@@ -38,6 +38,7 @@
"height": "Height",
"inputTitle": "Input Video",
"loadVideoForDimensions": "Load a video to see dimensions",
+ "longDescription": "This tool allows you to crop video files to remove unwanted areas or focus on specific parts of the video. Useful for removing black bars, adjusting aspect ratios, or focusing on important content. Supports various video formats including MP4, MOV, and AVI.",
"resultTitle": "Cropped Video",
"shortDescription": "Crop video to remove unwanted areas",
"title": "Crop Video",
diff --git a/public/locales/es/translation.json b/public/locales/es/translation.json
index 3bec9ef..1b3ca06 100644
--- a/public/locales/es/translation.json
+++ b/public/locales/es/translation.json
@@ -246,5 +246,9 @@
"copyFailed": "No se pudo copiar: {{error}}",
"loading": "Cargando... Esto puede tardar un momento.",
"result": "Resultado"
+ },
+ "userTypes": {
+ "developers": "Desarrolladores",
+ "generalUsers": "Usuarios generales"
}
}
diff --git a/public/locales/fr/time.json b/public/locales/fr/time.json
index c8f6fbb..2ccf157 100644
--- a/public/locales/fr/time.json
+++ b/public/locales/fr/time.json
@@ -56,6 +56,22 @@
"title": "Convertir le temps en secondes"
}
},
+ "convertUnixToDate": {
+ "addUtcLabel": "Ajouter le suffixe 'UTC'",
+ "addUtcLabelDescription": "Affiche 'UTC' après la date convertie (uniquement en mode UTC)",
+ "description": "Convertit un timestamp Unix en une date lisible par un humain.",
+ "longDescription": "Cet outil permet de convertir un timestamp Unix (en secondes) en une date lisible au format AAAA-MM-JJ HH:MM:SS. Il prend en charge l'affichage en UTC ou dans le fuseau horaire local, ce qui est pratique pour interpréter rapidement des horodatages issus de journaux, d'API ou de systèmes utilisant le temps Unix.",
+ "outputOptions": "Options de sortie",
+ "shortDescription": "Conversion de timestamp Unix en date",
+ "title": "Convertir un timestamp Unix en date",
+ "toolInfo": {
+ "description": "Cet outil convertit un timestamp Unix (en secondes) en une date lisible (par ex. AAAA-MM-JJ HH:MM:SS). Il prend en charge l'affichage en heure locale ou en UTC, ce qui le rend utile pour analyser rapidement des données issues de journaux ou d’APIs.",
+ "title": "Convertir un timestamp Unix en date"
+ },
+ "useLocalTime": "Utiliser l’heure locale",
+ "useLocalTimeDescription": "Affiche la date convertie dans votre fuseau horaire local au lieu de l’heure UTC",
+ "withLabel": "Options"
+ },
"crontabGuru": {
"description": "Générez et comprenez les expressions Cron. Créez des planifications Cron pour les tâches automatisées et les tâches système.",
"shortDescription": "Générer et comprendre les expressions cron",
@@ -96,21 +112,5 @@
"zeroPaddingDescription": "Faites en sorte que tous les composants de temps aient toujours une largeur de deux chiffres.",
"zeroPrintDescription": "Afficher les parties supprimées sous forme de valeurs nulles « 00 ».",
"zeroPrintTruncatedParts": "Parties tronquées sans impression"
- },
- "convertUnixToDate": {
- "title": "Convertir un timestamp Unix en date",
- "description": "Convertit un timestamp Unix en une date lisible par un humain.",
- "shortDescription": "Conversion de timestamp Unix en date",
- "longDescription": "Cet outil permet de convertir un timestamp Unix (en secondes) en une date lisible au format AAAA-MM-JJ HH:MM:SS. Il prend en charge l'affichage en UTC ou dans le fuseau horaire local, ce qui est pratique pour interpréter rapidement des horodatages issus de journaux, d'API ou de systèmes utilisant le temps Unix.",
- "withLabel": "Options",
- "outputOptions": "Options de sortie",
- "addUtcLabel": "Ajouter le suffixe 'UTC'",
- "addUtcLabelDescription": "Affiche 'UTC' après la date convertie (uniquement en mode UTC)",
- "useLocalTime": "Utiliser l’heure locale",
- "useLocalTimeDescription": "Affiche la date convertie dans votre fuseau horaire local au lieu de l’heure UTC",
- "toolInfo": {
- "title": "Convertir un timestamp Unix en date",
- "description": "Cet outil convertit un timestamp Unix (en secondes) en une date lisible (par ex. AAAA-MM-JJ HH:MM:SS). Il prend en charge l'affichage en heure locale ou en UTC, ce qui le rend utile pour analyser rapidement des données issues de journaux ou d’APIs."
- }
}
}
diff --git a/public/locales/fr/translation.json b/public/locales/fr/translation.json
index 3257c16..15fa9bb 100644
--- a/public/locales/fr/translation.json
+++ b/public/locales/fr/translation.json
@@ -246,5 +246,9 @@
"copyFailed": "Échec de la copie : {{error}}",
"loading": "Chargement... Cela peut prendre un moment.",
"result": "Résultat"
+ },
+ "userTypes": {
+ "developers": "Développeurs",
+ "generalUsers": "Grand public"
}
}
diff --git a/src/components/App.tsx b/src/components/App.tsx
index b71abd8..a578cc8 100644
--- a/src/components/App.tsx
+++ b/src/components/App.tsx
@@ -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() {
}}
>
-
- {
- setMode((prev) => nextMode(prev));
- localStorage.setItem('theme', nextMode(mode));
- }}
- />
- }>
-
-
-
+
+
+ {
+ setMode((prev) => nextMode(prev));
+ localStorage.setItem('theme', nextMode(mode));
+ }}
+ />
+ }>
+
+
+
+
diff --git a/src/components/Hero.tsx b/src/components/Hero.tsx
index 91143d6..0404216 100644
--- a/src/components/Hero.tsx
+++ b/src/components/Hero.tsx
@@ -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('');
const theme = useTheme();
+ const { selectedUserTypes } = useUserTypeFilter();
const [filteredTools, setFilteredTools] = useState(tools);
const [bookmarkedToolPaths, setBookmarkedToolPaths] = useState(
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();
for (const tool of filteredTools) {
toolsMap.set(tool.path, {
diff --git a/src/components/ToolHeader.tsx b/src/components/ToolHeader.tsx
index a39c0db..8388274 100644
--- a/src/components/ToolHeader.tsx
+++ b/src/components/ToolHeader.tsx
@@ -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
diff --git a/src/components/ToolLayout.tsx b/src/components/ToolLayout.tsx
index 5559d49..40ad761 100644
--- a/src/components/ToolLayout.tsx
+++ b/src/components/ToolLayout.tsx
@@ -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({
category.type === type)!
- .title
+ getToolsByCategory([], t).find(
+ (category) => category.type === type
+ )!.title
)
})}
toolCards={otherCategoryTools}
diff --git a/src/components/UserTypeFilter.tsx b/src/components/UserTypeFilter.tsx
new file mode 100644
index 0000000..7ca45cc
--- /dev/null
+++ b/src/components/UserTypeFilter.tsx
@@ -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 (
+
+ {userTypes.map((userType) => (
+ {
+ const isSelected = selectedUserTypes.includes(userType);
+ const newUserTypes = isSelected
+ ? selectedUserTypes.filter((ut) => ut !== userType)
+ : [...selectedUserTypes, userType];
+ onUserTypesChange(newUserTypes);
+ }}
+ sx={{ cursor: 'pointer' }}
+ />
+ ))}
+
+ );
+}
diff --git a/src/components/input/ToolCodeInput.tsx b/src/components/input/ToolCodeInput.tsx
index c23553b..bc3ce4a 100644
--- a/src/components/input/ToolCodeInput.tsx
+++ b/src/components/input/ToolCodeInput.tsx
@@ -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 (
-
- onChange(value ?? '')}
- />
+
+ ({
+ 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'
+ }
+ })}
+ >
+ onChange(value ?? '')}
+ options={{
+ scrollbar: {
+ vertical: 'visible',
+ horizontal: 'visible',
+ verticalScrollbarSize: 10,
+ horizontalScrollbarSize: 10,
+ alwaysConsumeMouseWheel: false
+ }
+ }}
+ />
+
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
}
});
diff --git a/src/pages/home/Categories.tsx b/src/pages/home/Categories.tsx
index 81bf1b6..9ec1546 100644
--- a/src/pages/home/Categories.tsx
+++ b/src/pages/home/Categories.tsx
@@ -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 (infer ElementType)[] ? ElementType : never;
@@ -84,10 +84,11 @@ const SingleCategory = function ({
{categoryDescription}
-
+
);
};
+
export default function Categories() {
+ const { selectedUserTypes } = useUserTypeFilter();
const { t } = useTranslation();
+ const categories = getToolsByCategory(selectedUserTypes, t);
+
return (
-
- {getToolsByCategory(t).map((category, index) => (
+
+ {categories.map((category, index) => (
))}
diff --git a/src/pages/home/index.tsx b/src/pages/home/index.tsx
index bf891d8..c65d24f 100644
--- a/src/pages/home/index.tsx
+++ b/src/pages/home/index.tsx
@@ -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 (
+
+
+
);
diff --git a/src/pages/tools-by-category/index.tsx b/src/pages/tools-by-category/index.tsx
index 2129bfe..436311c 100644
--- a/src/pages/tools-by-category/index.tsx
+++ b/src/pages/tools-by-category/index.tsx
@@ -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(null);
const { categoryName } = useParams();
const [searchTerm, setSearchTerm] = React.useState('');
+ 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)}
/>
-
+
+
+
+
{categoryTools.map((tool, index) => (
import('./index'))
});
diff --git a/src/pages/tools/audio/trim/meta.ts b/src/pages/tools/audio/trim/meta.ts
index 5509d79..1ad9e91 100644
--- a/src/pages/tools/audio/trim/meta.ts
+++ b/src/pages/tools/audio/trim/meta.ts
@@ -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'))
});
diff --git a/src/pages/tools/csv/csv-to-yaml/meta.ts b/src/pages/tools/csv/csv-to-yaml/meta.ts
index 7623c7a..67b7f9a 100644
--- a/src/pages/tools/csv/csv-to-yaml/meta.ts
+++ b/src/pages/tools/csv/csv-to-yaml/meta.ts
@@ -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'))
});
diff --git a/src/pages/tools/csv/insert-csv-columns/meta.ts b/src/pages/tools/csv/insert-csv-columns/meta.ts
index 66c4cf6..97c6608 100644
--- a/src/pages/tools/csv/insert-csv-columns/meta.ts
+++ b/src/pages/tools/csv/insert-csv-columns/meta.ts
@@ -12,6 +12,5 @@ export const tool = defineTool('csv', {
icon: 'hugeicons:column-insert',
keywords: ['insert', 'csv', 'columns', 'append', 'prepend'],
-
component: lazy(() => import('./index'))
});
diff --git a/src/pages/tools/csv/transpose-csv/meta.ts b/src/pages/tools/csv/transpose-csv/meta.ts
index ddda470..76920bc 100644
--- a/src/pages/tools/csv/transpose-csv/meta.ts
+++ b/src/pages/tools/csv/transpose-csv/meta.ts
@@ -13,6 +13,5 @@ export const tool = defineTool('csv', {
icon: 'carbon:transpose',
keywords: ['transpose', 'csv'],
-
component: lazy(() => import('./index'))
});
diff --git a/src/pages/tools/image/generic/compress/meta.ts b/src/pages/tools/image/generic/compress/meta.ts
index 7cd8c6c..9116b29 100644
--- a/src/pages/tools/image/generic/compress/meta.ts
+++ b/src/pages/tools/image/generic/compress/meta.ts
@@ -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'))
});
diff --git a/src/pages/tools/image/generic/remove-background/index.tsx b/src/pages/tools/image/generic/remove-background/index.tsx
index 8c70633..4d0d312 100644
--- a/src/pages/tools/image/generic/remove-background/index.tsx
+++ b/src/pages/tools/image/generic/remove-background/index.tsx
@@ -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'
}
diff --git a/src/pages/tools/image/generic/resize/meta.ts b/src/pages/tools/image/generic/resize/meta.ts
index fbc4d1f..dac23a6 100644
--- a/src/pages/tools/image/generic/resize/meta.ts
+++ b/src/pages/tools/image/generic/resize/meta.ts
@@ -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',
diff --git a/src/pages/tools/json/escape-json/index.tsx b/src/pages/tools/json/escape-json/index.tsx
index 17173ae..41c5d6e 100644
--- a/src/pages/tools/json/escape-json/index.tsx
+++ b/src/pages/tools/json/escape-json/index.tsx
@@ -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({
+
}
resultComponent={
+
}
resultComponent={
diff --git a/src/pages/tools/json/minify/index.tsx b/src/pages/tools/json/minify/index.tsx
index 69c0646..379d0f3 100644
--- a/src/pages/tools/json/minify/index.tsx
+++ b/src/pages/tools/json/minify/index.tsx
@@ -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) {
}
resultComponent={
diff --git a/src/pages/tools/json/prettify/index.tsx b/src/pages/tools/json/prettify/index.tsx
index c24b825..0d43d27 100644
--- a/src/pages/tools/json/prettify/index.tsx
+++ b/src/pages/tools/json/prettify/index.tsx
@@ -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={
-
}
resultComponent={
diff --git a/src/pages/tools/json/stringify/index.tsx b/src/pages/tools/json/stringify/index.tsx
index 653fb36..e8ba3bf 100644
--- a/src/pages/tools/json/stringify/index.tsx
+++ b/src/pages/tools/json/stringify/index.tsx
@@ -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={
-
}
resultComponent={
diff --git a/src/pages/tools/json/tsv-to-json/index.tsx b/src/pages/tools/json/tsv-to-json/index.tsx
index b7435a8..8b290d7 100644
--- a/src/pages/tools/json/tsv-to-json/index.tsx
+++ b/src/pages/tools/json/tsv-to-json/index.tsx
@@ -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={
-
+
}
resultComponent={
diff --git a/src/pages/tools/json/validateJson/index.tsx b/src/pages/tools/json/validateJson/index.tsx
index 7642b59..ca8ec1f 100644
--- a/src/pages/tools/json/validateJson/index.tsx
+++ b/src/pages/tools/json/validateJson/index.tsx
@@ -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) {
}
resultComponent={
diff --git a/src/pages/tools/json/validateJson/meta.ts b/src/pages/tools/json/validateJson/meta.ts
index c18b58b..1f91bb4 100644
--- a/src/pages/tools/json/validateJson/meta.ts
+++ b/src/pages/tools/json/validateJson/meta.ts
@@ -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'
+ }
+});
diff --git a/src/pages/tools/list/duplicate/meta.ts b/src/pages/tools/list/duplicate/meta.ts
index e9d2dbd..e594c67 100644
--- a/src/pages/tools/list/duplicate/meta.ts
+++ b/src/pages/tools/list/duplicate/meta.ts
@@ -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']
}
});
diff --git a/src/pages/tools/list/find-most-popular/meta.ts b/src/pages/tools/list/find-most-popular/meta.ts
index 20a8258..c1f732f 100644
--- a/src/pages/tools/list/find-most-popular/meta.ts
+++ b/src/pages/tools/list/find-most-popular/meta.ts
@@ -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']
}
});
diff --git a/src/pages/tools/list/find-unique/meta.ts b/src/pages/tools/list/find-unique/meta.ts
index d9828a3..ea09c2b 100644
--- a/src/pages/tools/list/find-unique/meta.ts
+++ b/src/pages/tools/list/find-unique/meta.ts
@@ -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']
}
});
diff --git a/src/pages/tools/list/group/meta.ts b/src/pages/tools/list/group/meta.ts
index 394b61c..d6fb646 100644
--- a/src/pages/tools/list/group/meta.ts
+++ b/src/pages/tools/list/group/meta.ts
@@ -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']
}
});
diff --git a/src/pages/tools/list/reverse/meta.ts b/src/pages/tools/list/reverse/meta.ts
index ff500fa..4ea7e53 100644
--- a/src/pages/tools/list/reverse/meta.ts
+++ b/src/pages/tools/list/reverse/meta.ts
@@ -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'))
});
diff --git a/src/pages/tools/list/rotate/meta.ts b/src/pages/tools/list/rotate/meta.ts
index 70a0691..f046133 100644
--- a/src/pages/tools/list/rotate/meta.ts
+++ b/src/pages/tools/list/rotate/meta.ts
@@ -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']
}
});
diff --git a/src/pages/tools/list/shuffle/meta.ts b/src/pages/tools/list/shuffle/meta.ts
index bb01cd7..704db7a 100644
--- a/src/pages/tools/list/shuffle/meta.ts
+++ b/src/pages/tools/list/shuffle/meta.ts
@@ -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']
}
});
diff --git a/src/pages/tools/list/sort/meta.ts b/src/pages/tools/list/sort/meta.ts
index 6b7fac3..a77eb3d 100644
--- a/src/pages/tools/list/sort/meta.ts
+++ b/src/pages/tools/list/sort/meta.ts
@@ -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']
}
});
diff --git a/src/pages/tools/list/truncate/meta.ts b/src/pages/tools/list/truncate/meta.ts
index 8e42e91..6ba3158 100644
--- a/src/pages/tools/list/truncate/meta.ts
+++ b/src/pages/tools/list/truncate/meta.ts
@@ -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']
}
});
diff --git a/src/pages/tools/list/unwrap/meta.ts b/src/pages/tools/list/unwrap/meta.ts
index 9f52494..d0c2b03 100644
--- a/src/pages/tools/list/unwrap/meta.ts
+++ b/src/pages/tools/list/unwrap/meta.ts
@@ -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']
}
});
diff --git a/src/pages/tools/list/wrap/meta.ts b/src/pages/tools/list/wrap/meta.ts
index de7783a..f4e00bd 100644
--- a/src/pages/tools/list/wrap/meta.ts
+++ b/src/pages/tools/list/wrap/meta.ts
@@ -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']
}
});
diff --git a/src/pages/tools/number/arithmetic-sequence/meta.ts b/src/pages/tools/number/arithmetic-sequence/meta.ts
index 20a14b6..5a29578 100644
--- a/src/pages/tools/number/arithmetic-sequence/meta.ts
+++ b/src/pages/tools/number/arithmetic-sequence/meta.ts
@@ -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']
}
});
diff --git a/src/pages/tools/number/index.ts b/src/pages/tools/number/index.ts
index f26f6fe..fa4facc 100644
--- a/src/pages/tools/number/index.ts
+++ b/src/pages/tools/number/index.ts
@@ -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
];
diff --git a/src/pages/tools/number/random-number-generator/index.tsx b/src/pages/tools/number/random-number-generator/index.tsx
new file mode 100644
index 0000000..ae4f471
--- /dev/null
+++ b/src/pages/tools/number/random-number-generator/index.tsx
@@ -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(null);
+ const [error, setError] = useState(null);
+ const [formattedResult, setFormattedResult] = useState('');
+
+ 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 | null = ({
+ values,
+ updateField
+ }) => [
+ {
+ title: t('randomNumberGenerator.options.range.title'),
+ component: (
+
+
+ updateField('minValue', parseInt(value) || 1)
+ }
+ description={t(
+ 'randomNumberGenerator.options.range.minDescription'
+ )}
+ inputProps={{
+ type: 'number',
+ 'data-testid': 'min-value-input'
+ }}
+ />
+
+ updateField('maxValue', parseInt(value) || 100)
+ }
+ description={t(
+ 'randomNumberGenerator.options.range.maxDescription'
+ )}
+ inputProps={{
+ type: 'number',
+ 'data-testid': 'max-value-input'
+ }}
+ />
+
+ )
+ },
+ {
+ title: t('randomNumberGenerator.options.generation.title'),
+ component: (
+
+ updateField('count', parseInt(value) || 10)}
+ description={t(
+ 'randomNumberGenerator.options.generation.countDescription'
+ )}
+ inputProps={{
+ type: 'number',
+ min: 1,
+ max: 10000,
+ 'data-testid': 'count-input'
+ }}
+ />
+
+ updateField('allowDecimals', value)}
+ description={t(
+ 'randomNumberGenerator.options.generation.allowDecimals.description'
+ )}
+ />
+
+ updateField('allowDuplicates', value)}
+ description={t(
+ 'randomNumberGenerator.options.generation.allowDuplicates.description'
+ )}
+ />
+
+ updateField('sortResults', value)}
+ description={t(
+ 'randomNumberGenerator.options.generation.sortResults.description'
+ )}
+ />
+
+ )
+ },
+ {
+ title: t('randomNumberGenerator.options.output.title'),
+ component: (
+
+ updateField('separator', value)}
+ description={t(
+ 'randomNumberGenerator.options.output.separatorDescription'
+ )}
+ inputProps={{
+ 'data-testid': 'separator-input'
+ }}
+ />
+
+ )
+ }
+ ];
+
+ return (
+
+ {error && (
+
+ {error}
+
+ )}
+
+ {result && (
+
+ )}
+
+ }
+ toolInfo={{
+ title: t('randomNumberGenerator.info.title'),
+ description:
+ longDescription || t('randomNumberGenerator.info.description')
+ }}
+ />
+ );
+}
diff --git a/src/pages/tools/number/random-number-generator/meta.ts b/src/pages/tools/number/random-number-generator/meta.ts
new file mode 100644
index 0000000..4974436
--- /dev/null
+++ b/src/pages/tools/number/random-number-generator/meta.ts
@@ -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'))
+});
diff --git a/src/pages/tools/number/random-number-generator/random-number-generator.service.test.ts b/src/pages/tools/number/random-number-generator/random-number-generator.service.test.ts
new file mode 100644
index 0000000..ef42d51
--- /dev/null
+++ b/src/pages/tools/number/random-number-generator/random-number-generator.service.test.ts
@@ -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('');
+ });
+ });
+});
diff --git a/src/pages/tools/number/random-number-generator/service.ts b/src/pages/tools/number/random-number-generator/service.ts
new file mode 100644
index 0000000..104a725
--- /dev/null
+++ b/src/pages/tools/number/random-number-generator/service.ts
@@ -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();
+
+ // 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();
+ 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;
+}
diff --git a/src/pages/tools/number/random-number-generator/types.ts b/src/pages/tools/number/random-number-generator/types.ts
new file mode 100644
index 0000000..9e311ce
--- /dev/null
+++ b/src/pages/tools/number/random-number-generator/types.ts
@@ -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;
+};
diff --git a/src/pages/tools/number/random-port-generator/index.tsx b/src/pages/tools/number/random-port-generator/index.tsx
new file mode 100644
index 0000000..338fa91
--- /dev/null
+++ b/src/pages/tools/number/random-port-generator/index.tsx
@@ -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(null);
+ const [error, setError] = useState(null);
+ const [formattedResult, setFormattedResult] = useState('');
+
+ 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 | null = ({
+ values,
+ updateField
+ }) => [
+ {
+ title: t('randomPortGenerator.options.range.title'),
+ component: (
+
+ {portOptions.map((option) => (
+ updateField('portRange', option.value)}
+ />
+ ))}
+
+ {values.portRange === 'custom' && (
+
+
+ updateField('minPort', parseInt(value) || 1024)
+ }
+ description={t(
+ 'randomPortGenerator.options.range.minPortDescription'
+ )}
+ inputProps={{
+ type: 'number',
+ min: 1,
+ max: 65535,
+ 'data-testid': 'min-port-input'
+ }}
+ />
+
+ updateField('maxPort', parseInt(value) || 49151)
+ }
+ description={t(
+ 'randomPortGenerator.options.range.maxPortDescription'
+ )}
+ inputProps={{
+ type: 'number',
+ min: 1,
+ max: 65535,
+ 'data-testid': 'max-port-input'
+ }}
+ />
+
+ )}
+
+
+ {getPortRangeInfo(values.portRange).name}
+
+ {getPortRangeInfo(values.portRange).description}
+
+
+ )
+ },
+ {
+ title: t('randomPortGenerator.options.generation.title'),
+ component: (
+
+ updateField('count', parseInt(value) || 5)}
+ description={t(
+ 'randomPortGenerator.options.generation.countDescription'
+ )}
+ inputProps={{
+ type: 'number',
+ min: 1,
+ max: 1000,
+ 'data-testid': 'count-input'
+ }}
+ />
+
+ updateField('allowDuplicates', value)}
+ description={t(
+ 'randomPortGenerator.options.generation.allowDuplicates.description'
+ )}
+ />
+
+ updateField('sortResults', value)}
+ description={t(
+ 'randomPortGenerator.options.generation.sortResults.description'
+ )}
+ />
+
+ )
+ },
+ {
+ title: t('randomPortGenerator.options.output.title'),
+ component: (
+
+ updateField('separator', value)}
+ description={t(
+ 'randomPortGenerator.options.output.separatorDescription'
+ )}
+ inputProps={{
+ 'data-testid': 'separator-input'
+ }}
+ />
+
+ )
+ }
+ ];
+
+ return (
+
+ {error && (
+
+ {error}
+
+ )}
+
+ {result && (
+
+ )}
+
+ }
+ toolInfo={{
+ title: t('randomPortGenerator.info.title'),
+ description:
+ longDescription || t('randomPortGenerator.info.description')
+ }}
+ />
+ );
+}
diff --git a/src/pages/tools/number/random-port-generator/meta.ts b/src/pages/tools/number/random-port-generator/meta.ts
new file mode 100644
index 0000000..580de1a
--- /dev/null
+++ b/src/pages/tools/number/random-port-generator/meta.ts
@@ -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'))
+});
diff --git a/src/pages/tools/number/random-port-generator/random-port-generator.service.test.ts b/src/pages/tools/number/random-port-generator/random-port-generator.service.test.ts
new file mode 100644
index 0000000..59d4dc1
--- /dev/null
+++ b/src/pages/tools/number/random-port-generator/random-port-generator.service.test.ts
@@ -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);
+ });
+ });
+});
diff --git a/src/pages/tools/number/random-port-generator/service.ts b/src/pages/tools/number/random-port-generator/service.ts
new file mode 100644
index 0000000..fc85e3c
--- /dev/null
+++ b/src/pages/tools/number/random-port-generator/service.ts
@@ -0,0 +1,214 @@
+import { InitialValuesType, RandomPortResult, PortRange } from './types';
+
+// Standard port ranges according to IANA
+export const PORT_RANGES: Record = {
+ '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();
+
+ // 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();
+ 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 = {
+ 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';
+}
diff --git a/src/pages/tools/number/random-port-generator/types.ts b/src/pages/tools/number/random-port-generator/types.ts
new file mode 100644
index 0000000..71bc88c
--- /dev/null
+++ b/src/pages/tools/number/random-port-generator/types.ts
@@ -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;
+};
diff --git a/src/pages/tools/number/sum/meta.ts b/src/pages/tools/number/sum/meta.ts
index 4b7b9b9..7023d61 100644
--- a/src/pages/tools/number/sum/meta.ts
+++ b/src/pages/tools/number/sum/meta.ts
@@ -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']
}
});
diff --git a/src/pages/tools/pdf/compress-pdf/meta.ts b/src/pages/tools/pdf/compress-pdf/meta.ts
index bbe0990..f0a414c 100644
--- a/src/pages/tools/pdf/compress-pdf/meta.ts
+++ b/src/pages/tools/pdf/compress-pdf/meta.ts
@@ -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']
}
});
diff --git a/src/pages/tools/pdf/editor/meta.ts b/src/pages/tools/pdf/editor/meta.ts
index 9d9901f..e55694a 100644
--- a/src/pages/tools/pdf/editor/meta.ts
+++ b/src/pages/tools/pdf/editor/meta.ts
@@ -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',
diff --git a/src/pages/tools/pdf/merge-pdf/meta.ts b/src/pages/tools/pdf/merge-pdf/meta.ts
index b393bf2..33ba46c 100644
--- a/src/pages/tools/pdf/merge-pdf/meta.ts
+++ b/src/pages/tools/pdf/merge-pdf/meta.ts
@@ -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']
}
});
diff --git a/src/pages/tools/pdf/pdf-to-epub/meta.ts b/src/pages/tools/pdf/pdf-to-epub/meta.ts
index 411814f..8aada08 100644
--- a/src/pages/tools/pdf/pdf-to-epub/meta.ts
+++ b/src/pages/tools/pdf/pdf-to-epub/meta.ts
@@ -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']
}
});
diff --git a/src/pages/tools/pdf/pdf-to-png/meta.ts b/src/pages/tools/pdf/pdf-to-png/meta.ts
index dc3efb5..919de1f 100644
--- a/src/pages/tools/pdf/pdf-to-png/meta.ts
+++ b/src/pages/tools/pdf/pdf-to-png/meta.ts
@@ -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'))
});
diff --git a/src/pages/tools/pdf/protect-pdf/meta.ts b/src/pages/tools/pdf/protect-pdf/meta.ts
index e3b8a14..7876f1d 100644
--- a/src/pages/tools/pdf/protect-pdf/meta.ts
+++ b/src/pages/tools/pdf/protect-pdf/meta.ts
@@ -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']
}
});
diff --git a/src/pages/tools/pdf/rotate-pdf/meta.ts b/src/pages/tools/pdf/rotate-pdf/meta.ts
index f0ab83f..e037ff4 100644
--- a/src/pages/tools/pdf/rotate-pdf/meta.ts
+++ b/src/pages/tools/pdf/rotate-pdf/meta.ts
@@ -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'))
});
diff --git a/src/pages/tools/pdf/split-pdf/meta.ts b/src/pages/tools/pdf/split-pdf/meta.ts
index 04d8ac2..0352c5b 100644
--- a/src/pages/tools/pdf/split-pdf/meta.ts
+++ b/src/pages/tools/pdf/split-pdf/meta.ts
@@ -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']
}
});
diff --git a/src/pages/tools/string/base64/meta.ts b/src/pages/tools/string/base64/meta.ts
index a334e77..d51eb7f 100644
--- a/src/pages/tools/string/base64/meta.ts
+++ b/src/pages/tools/string/base64/meta.ts
@@ -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']
}
});
diff --git a/src/pages/tools/string/censor/meta.ts b/src/pages/tools/string/censor/meta.ts
index bda557b..12d8af8 100644
--- a/src/pages/tools/string/censor/meta.ts
+++ b/src/pages/tools/string/censor/meta.ts
@@ -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']
}
});
diff --git a/src/pages/tools/string/create-palindrome/meta.ts b/src/pages/tools/string/create-palindrome/meta.ts
index cadd578..b523ced 100644
--- a/src/pages/tools/string/create-palindrome/meta.ts
+++ b/src/pages/tools/string/create-palindrome/meta.ts
@@ -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']
}
});
diff --git a/src/pages/tools/string/extract-substring/meta.ts b/src/pages/tools/string/extract-substring/meta.ts
index 7a7a361..01cfb69 100644
--- a/src/pages/tools/string/extract-substring/meta.ts
+++ b/src/pages/tools/string/extract-substring/meta.ts
@@ -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']
}
});
diff --git a/src/pages/tools/string/join/meta.ts b/src/pages/tools/string/join/meta.ts
index 5fcc936..e0c0e03 100644
--- a/src/pages/tools/string/join/meta.ts
+++ b/src/pages/tools/string/join/meta.ts
@@ -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']
}
});
diff --git a/src/pages/tools/string/palindrome/meta.ts b/src/pages/tools/string/palindrome/meta.ts
index 5c56ba0..fd16fe9 100644
--- a/src/pages/tools/string/palindrome/meta.ts
+++ b/src/pages/tools/string/palindrome/meta.ts
@@ -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']
}
});
diff --git a/src/pages/tools/string/quote/meta.ts b/src/pages/tools/string/quote/meta.ts
index 7bb86e9..fb88052 100644
--- a/src/pages/tools/string/quote/meta.ts
+++ b/src/pages/tools/string/quote/meta.ts
@@ -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']
}
});
diff --git a/src/pages/tools/string/randomize-case/meta.ts b/src/pages/tools/string/randomize-case/meta.ts
index 4eb3af1..92100a2 100644
--- a/src/pages/tools/string/randomize-case/meta.ts
+++ b/src/pages/tools/string/randomize-case/meta.ts
@@ -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']
}
});
diff --git a/src/pages/tools/string/remove-duplicate-lines/meta.ts b/src/pages/tools/string/remove-duplicate-lines/meta.ts
index debe033..5f4facc 100644
--- a/src/pages/tools/string/remove-duplicate-lines/meta.ts
+++ b/src/pages/tools/string/remove-duplicate-lines/meta.ts
@@ -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']
}
});
diff --git a/src/pages/tools/string/repeat/meta.ts b/src/pages/tools/string/repeat/meta.ts
index 02c2910..2ac9fc7 100644
--- a/src/pages/tools/string/repeat/meta.ts
+++ b/src/pages/tools/string/repeat/meta.ts
@@ -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']
}
});
diff --git a/src/pages/tools/string/reverse/meta.ts b/src/pages/tools/string/reverse/meta.ts
index 1f30e9b..865199c 100644
--- a/src/pages/tools/string/reverse/meta.ts
+++ b/src/pages/tools/string/reverse/meta.ts
@@ -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']
}
});
diff --git a/src/pages/tools/string/rot13/meta.ts b/src/pages/tools/string/rot13/meta.ts
index 8e6be8c..76d3c65 100644
--- a/src/pages/tools/string/rot13/meta.ts
+++ b/src/pages/tools/string/rot13/meta.ts
@@ -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',
diff --git a/src/pages/tools/string/rotate/meta.ts b/src/pages/tools/string/rotate/meta.ts
index 66c4117..468d806 100644
--- a/src/pages/tools/string/rotate/meta.ts
+++ b/src/pages/tools/string/rotate/meta.ts
@@ -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',
diff --git a/src/pages/tools/string/split/meta.ts b/src/pages/tools/string/split/meta.ts
index 8b7b6a9..4d8ac4b 100644
--- a/src/pages/tools/string/split/meta.ts
+++ b/src/pages/tools/string/split/meta.ts
@@ -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']
}
});
diff --git a/src/pages/tools/string/statistic/meta.ts b/src/pages/tools/string/statistic/meta.ts
index f56b7d4..7aba6da 100644
--- a/src/pages/tools/string/statistic/meta.ts
+++ b/src/pages/tools/string/statistic/meta.ts
@@ -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']
}
});
diff --git a/src/pages/tools/string/text-replacer/meta.ts b/src/pages/tools/string/text-replacer/meta.ts
index d02502b..4d48b07 100644
--- a/src/pages/tools/string/text-replacer/meta.ts
+++ b/src/pages/tools/string/text-replacer/meta.ts
@@ -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',
diff --git a/src/pages/tools/string/to-morse/meta.ts b/src/pages/tools/string/to-morse/meta.ts
index 5244c3a..b15a757 100644
--- a/src/pages/tools/string/to-morse/meta.ts
+++ b/src/pages/tools/string/to-morse/meta.ts
@@ -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']
}
});
diff --git a/src/pages/tools/string/truncate/meta.ts b/src/pages/tools/string/truncate/meta.ts
index a62eebc..256c92c 100644
--- a/src/pages/tools/string/truncate/meta.ts
+++ b/src/pages/tools/string/truncate/meta.ts
@@ -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']
}
});
diff --git a/src/pages/tools/string/uppercase/meta.ts b/src/pages/tools/string/uppercase/meta.ts
index 9c2e25c..4b221a6 100644
--- a/src/pages/tools/string/uppercase/meta.ts
+++ b/src/pages/tools/string/uppercase/meta.ts
@@ -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']
}
});
diff --git a/src/pages/tools/string/url-decode/meta.ts b/src/pages/tools/string/url-decode/meta.ts
index 986a396..d814fd9 100644
--- a/src/pages/tools/string/url-decode/meta.ts
+++ b/src/pages/tools/string/url-decode/meta.ts
@@ -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',
diff --git a/src/pages/tools/string/url-encode/meta.ts b/src/pages/tools/string/url-encode/meta.ts
index 3774fca..b83b406 100644
--- a/src/pages/tools/string/url-encode/meta.ts
+++ b/src/pages/tools/string/url-encode/meta.ts
@@ -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',
diff --git a/src/pages/tools/time/check-leap-years/meta.ts b/src/pages/tools/time/check-leap-years/meta.ts
index 661f4a6..b275f97 100644
--- a/src/pages/tools/time/check-leap-years/meta.ts
+++ b/src/pages/tools/time/check-leap-years/meta.ts
@@ -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']
}
});
diff --git a/src/pages/tools/time/convert-days-to-hours/meta.ts b/src/pages/tools/time/convert-days-to-hours/meta.ts
index a94823b..938ed56 100644
--- a/src/pages/tools/time/convert-days-to-hours/meta.ts
+++ b/src/pages/tools/time/convert-days-to-hours/meta.ts
@@ -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']
}
});
diff --git a/src/pages/tools/time/convert-hours-to-days/meta.ts b/src/pages/tools/time/convert-hours-to-days/meta.ts
index fd49c3f..c9c505e 100644
--- a/src/pages/tools/time/convert-hours-to-days/meta.ts
+++ b/src/pages/tools/time/convert-hours-to-days/meta.ts
@@ -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']
}
});
diff --git a/src/pages/tools/time/convert-seconds-to-time/meta.ts b/src/pages/tools/time/convert-seconds-to-time/meta.ts
index 574623d..91ce4ab 100644
--- a/src/pages/tools/time/convert-seconds-to-time/meta.ts
+++ b/src/pages/tools/time/convert-seconds-to-time/meta.ts
@@ -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']
}
});
diff --git a/src/pages/tools/time/convert-time-to-seconds/meta.ts b/src/pages/tools/time/convert-time-to-seconds/meta.ts
index 988e6c0..c71988f 100644
--- a/src/pages/tools/time/convert-time-to-seconds/meta.ts
+++ b/src/pages/tools/time/convert-time-to-seconds/meta.ts
@@ -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']
}
});
diff --git a/src/pages/tools/time/convert-unix-to-date/meta.ts b/src/pages/tools/time/convert-unix-to-date/meta.ts
index a3128f0..7f8e556 100644
--- a/src/pages/tools/time/convert-unix-to-date/meta.ts
+++ b/src/pages/tools/time/convert-unix-to-date/meta.ts
@@ -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',
diff --git a/src/pages/tools/time/crontab-guru/meta.ts b/src/pages/tools/time/crontab-guru/meta.ts
index 2c5d2e0..2ca4304 100644
--- a/src/pages/tools/time/crontab-guru/meta.ts
+++ b/src/pages/tools/time/crontab-guru/meta.ts
@@ -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']
}
});
diff --git a/src/pages/tools/time/time-between-dates/meta.ts b/src/pages/tools/time/time-between-dates/meta.ts
index 028617b..34f18a5 100644
--- a/src/pages/tools/time/time-between-dates/meta.ts
+++ b/src/pages/tools/time/time-between-dates/meta.ts
@@ -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']
}
});
diff --git a/src/pages/tools/time/truncate-clock-time/meta.ts b/src/pages/tools/time/truncate-clock-time/meta.ts
index 9a9498a..b64ea67 100644
--- a/src/pages/tools/time/truncate-clock-time/meta.ts
+++ b/src/pages/tools/time/truncate-clock-time/meta.ts
@@ -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']
}
});
diff --git a/src/pages/tools/video/change-speed/meta.ts b/src/pages/tools/video/change-speed/meta.ts
index c881cb6..da97073 100644
--- a/src/pages/tools/video/change-speed/meta.ts
+++ b/src/pages/tools/video/change-speed/meta.ts
@@ -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']
}
});
diff --git a/src/pages/tools/video/compress/meta.ts b/src/pages/tools/video/compress/meta.ts
index fb9a09d..bab6d6f 100644
--- a/src/pages/tools/video/compress/meta.ts
+++ b/src/pages/tools/video/compress/meta.ts
@@ -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']
}
});
diff --git a/src/pages/tools/video/crop-video/meta.ts b/src/pages/tools/video/crop-video/meta.ts
index 22a23e3..0026022 100644
--- a/src/pages/tools/video/crop-video/meta.ts
+++ b/src/pages/tools/video/crop-video/meta.ts
@@ -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'))
});
diff --git a/src/pages/tools/video/flip/meta.ts b/src/pages/tools/video/flip/meta.ts
index 24d4e96..f15ed8e 100644
--- a/src/pages/tools/video/flip/meta.ts
+++ b/src/pages/tools/video/flip/meta.ts
@@ -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']
}
});
diff --git a/src/pages/tools/video/loop/meta.ts b/src/pages/tools/video/loop/meta.ts
index 1176b2f..5484ed3 100644
--- a/src/pages/tools/video/loop/meta.ts
+++ b/src/pages/tools/video/loop/meta.ts
@@ -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']
}
});
diff --git a/src/pages/tools/video/rotate/meta.ts b/src/pages/tools/video/rotate/meta.ts
index d49d001..0489519 100644
--- a/src/pages/tools/video/rotate/meta.ts
+++ b/src/pages/tools/video/rotate/meta.ts
@@ -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']
}
});
diff --git a/src/pages/tools/video/trim/meta.ts b/src/pages/tools/video/trim/meta.ts
index 98d7e97..bfcfc80 100644
--- a/src/pages/tools/video/trim/meta.ts
+++ b/src/pages/tools/video/trim/meta.ts
@@ -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']
}
});
diff --git a/src/pages/tools/video/video-to-gif/meta.ts b/src/pages/tools/video/video-to-gif/meta.ts
index 9cec755..b323686 100644
--- a/src/pages/tools/video/video-to-gif/meta.ts
+++ b/src/pages/tools/video/video-to-gif/meta.ts
@@ -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']
}
});
diff --git a/src/providers/UserTypeFilterProvider.tsx b/src/providers/UserTypeFilterProvider.tsx
new file mode 100644
index 0000000..b989ba7
--- /dev/null
+++ b/src/providers/UserTypeFilterProvider.tsx
@@ -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(
+ null
+);
+
+interface UserTypeFilterProviderProps {
+ children: ReactNode;
+}
+
+export function UserTypeFilterProvider({
+ children
+}: UserTypeFilterProviderProps) {
+ const [selectedUserTypes, setSelectedUserTypes] = useState(() => {
+ 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 (
+
+ {children}
+
+ );
+}
+
+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 .'
+ );
+ }
+
+ return context;
+}
diff --git a/src/tools/defineTool.tsx b/src/tools/defineTool.tsx
index ed19326..104c83b 100644
--- a/src/tools/defineTool.tsx
+++ b/src/tools/defineTool.tsx
@@ -4,6 +4,8 @@ import { IconifyIcon } from '@iconify/react';
import { FullI18nKey, validNamespaces } from '../i18n';
import { useTranslation } from 'react-i18next';
+export type UserType = 'generalUsers' | 'developers';
+
export interface ToolMeta {
path: string;
component: LazyExoticComponent>;
@@ -14,21 +16,22 @@ export interface ToolMeta {
description: FullI18nKey;
shortDescription: FullI18nKey;
longDescription?: FullI18nKey;
+ userTypes?: UserType[];
};
}
export type ToolCategory =
| 'string'
+ | 'image-generic'
| 'png'
| 'number'
| 'gif'
- | 'video'
| 'list'
| 'json'
| 'time'
| 'csv'
+ | 'video'
| 'pdf'
- | 'image-generic'
| 'audio'
| 'xml';
@@ -41,6 +44,7 @@ export interface DefinedTool {
icon: IconifyIcon | string;
keywords: string[];
component: () => JSX.Element;
+ userTypes?: UserType[];
}
export interface ToolComponentProps {
@@ -62,6 +66,7 @@ export const defineTool = (
description: i18n.description,
shortDescription: i18n.shortDescription,
keywords,
+ userTypes: i18n.userTypes,
component: function ToolComponent() {
const { t } = useTranslation(validNamespaces);
return (
diff --git a/src/tools/index.ts b/src/tools/index.ts
index dd1b723..e13d3fc 100644
--- a/src/tools/index.ts
+++ b/src/tools/index.ts
@@ -1,6 +1,6 @@
import { stringTools } from '../pages/tools/string';
import { imageTools } from '../pages/tools/image';
-import { DefinedTool, ToolCategory } from './defineTool';
+import { DefinedTool, ToolCategory, UserType } from './defineTool';
import { capitalizeFirstLetter } from '@utils/string';
import { numberTools } from '../pages/tools/number';
import { videoTools } from '../pages/tools/video';
@@ -136,6 +136,36 @@ const categoriesConfig: {
title: 'translation:categories.xml.title'
}
];
+const CATEGORIES_USER_TYPES_MAPPINGS: Partial> =
+ {
+ xml: 'developers',
+ csv: 'developers',
+ json: 'developers',
+ gif: 'generalUsers',
+ png: 'generalUsers',
+ 'image-generic': 'generalUsers',
+ video: 'generalUsers',
+ audio: 'generalUsers'
+ };
+// Filter tools by user types
+export const filterToolsByUserTypes = (
+ tools: DefinedTool[],
+ userTypes: UserType[]
+): DefinedTool[] => {
+ if (userTypes.length === 0) return tools;
+
+ return tools.filter((tool) => {
+ if (CATEGORIES_USER_TYPES_MAPPINGS[tool.type]) {
+ return userTypes.includes(CATEGORIES_USER_TYPES_MAPPINGS[tool.type]!);
+ }
+ // If tool has no userTypes defined, show it to all users
+ if (!tool.userTypes || tool.userTypes.length === 0) return true;
+
+ // Check if tool has any of the selected user types
+ return tool.userTypes.some((userType) => userTypes.includes(userType));
+ });
+};
+
// use for changelogs
// console.log(
// 'tools',
@@ -144,12 +174,22 @@ const categoriesConfig: {
export const filterTools = (
tools: DefinedTool[],
query: string,
+ userTypes: UserType[] = [],
t: TFunction
): DefinedTool[] => {
- if (!query) return tools;
+ let filteredTools = tools;
+
+ // First filter by user types
+ if (userTypes.length > 0) {
+ filteredTools = filterToolsByUserTypes(tools, userTypes);
+ }
+
+ // Then filter by search query
+ if (!query) return filteredTools;
const lowerCaseQuery = query.toLowerCase();
- return tools.filter(
+
+ return filteredTools.filter(
(tool) =>
t(tool.name).toLowerCase().includes(lowerCaseQuery) ||
t(tool.description).toLowerCase().includes(lowerCaseQuery) ||
@@ -161,6 +201,7 @@ export const filterTools = (
};
export const getToolsByCategory = (
+ userTypes: UserType[] = [],
t: TFunction
): {
title: string;
@@ -170,14 +211,28 @@ export const getToolsByCategory = (
type: ToolCategory;
example: { title: string; path: string };
tools: DefinedTool[];
+ userTypes: UserType[]; // <-- Add this line
}[] => {
const groupedByType: Partial> =
Object.groupBy(tools, ({ type }) => type);
+
return (Object.entries(groupedByType) as Entries)
.map(([type, tools]) => {
const categoryConfig = categoriesConfig.find(
(config) => config.type === type
);
+
+ // Filter tools by user types if specified
+ const filteredTools =
+ userTypes.length > 0
+ ? filterToolsByUserTypes(tools ?? [], userTypes)
+ : tools ?? [];
+
+ // Aggregate unique userTypes from all tools in this category
+ const aggregatedUserTypes = Array.from(
+ new Set((filteredTools ?? []).flatMap((tool) => tool.userTypes ?? []))
+ );
+
return {
rawTitle: categoryConfig?.title
? t(categoryConfig.title)
@@ -188,12 +243,22 @@ export const getToolsByCategory = (
description: categoryConfig?.value ? t(categoryConfig.value) : '',
type,
icon: categoryConfig!.icon,
- tools: tools ?? [],
- example: tools
- ? { title: tools[0].name, path: tools[0].path }
- : { title: '', path: '' }
+ tools: filteredTools,
+ example:
+ filteredTools.length > 0
+ ? { title: filteredTools[0].name, path: filteredTools[0].path }
+ : { title: '', path: '' },
+ userTypes: aggregatedUserTypes // <-- Add this line
};
})
+ .filter((category) => category.tools.length > 0)
+ .filter((category) =>
+ userTypes.length > 0
+ ? [...category.userTypes, CATEGORIES_USER_TYPES_MAPPINGS[category.type]]
+ .filter(Boolean)
+ .some((categoryUserType) => userTypes.includes(categoryUserType!))
+ : true
+ ) // Only show categories with tools
.sort(
(a, b) =>
toolCategoriesOrder.indexOf(a.type) -
diff --git a/src/utils/string.ts b/src/utils/string.ts
index 4207348..b5a4cc0 100644
--- a/src/utils/string.ts
+++ b/src/utils/string.ts
@@ -114,7 +114,7 @@ export const getToolCategoryTitle = (
categoryName: string,
t: TFunction
): string =>
- getToolsByCategory(t).find((category) => category.type === categoryName)!
+ getToolsByCategory([], t).find((category) => category.type === categoryName)!
.rawTitle;
// Type guard to check if a value is a valid I18nNamespaces