mirror of
https://github.com/iib0011/omni-tools.git
synced 2025-11-06 17:04:56 +05:30
Merge branch 'main' into tool/hidden-character-detector
This commit is contained in:
commit
fe3634a671
115 changed files with 2440 additions and 382 deletions
362
.idea/workspace.xml
generated
362
.idea/workspace.xml
generated
|
|
@ -4,9 +4,12 @@
|
|||
<option name="autoReloadType" value="SELECTIVE" />
|
||||
</component>
|
||||
<component name="ChangeListManager">
|
||||
<list default="true" id="b30e2810-c4c1-4aad-b134-794e52cc1c7d" name="Changes" comment="chore: sync locize">
|
||||
<list default="true" id="b30e2810-c4c1-4aad-b134-794e52cc1c7d" name="Changes" comment="chore: translate userTypes">
|
||||
<change beforePath="$PROJECT_DIR$/.idea/workspace.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/workspace.xml" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/src/components/input/ToolCodeInput.tsx" beforeDir="false" afterPath="$PROJECT_DIR$/src/components/input/ToolCodeInput.tsx" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/src/pages/tools/number/index.ts" beforeDir="false" afterPath="$PROJECT_DIR$/src/pages/tools/number/index.ts" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/src/pages/tools/number/random-number-generator/index.tsx" beforeDir="false" afterPath="$PROJECT_DIR$/src/pages/tools/number/random-number-generator/index.tsx" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/src/pages/tools/number/random-port-generator/index.tsx" beforeDir="false" afterPath="$PROJECT_DIR$/src/pages/tools/number/random-port-generator/index.tsx" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/src/pages/tools/number/random-port-generator/meta.ts" beforeDir="false" afterPath="$PROJECT_DIR$/src/pages/tools/number/random-port-generator/meta.ts" afterDir="false" />
|
||||
</list>
|
||||
<option name="SHOW_DIALOG" value="false" />
|
||||
<option name="HIGHLIGHT_CONFLICTS" value="true" />
|
||||
|
|
@ -24,10 +27,23 @@
|
|||
<option name="BRANCH" value="origin/main" />
|
||||
</component>
|
||||
<component name="Git.Settings">
|
||||
<excluded-from-favorite>
|
||||
<branch-storage>
|
||||
<map>
|
||||
<entry type="LOCAL">
|
||||
<value>
|
||||
<list>
|
||||
<branch-info repo="$PROJECT_DIR$" source="main" />
|
||||
</list>
|
||||
</value>
|
||||
</entry>
|
||||
</map>
|
||||
</branch-storage>
|
||||
</excluded-from-favorite>
|
||||
<option name="PUSH_AUTO_UPDATE" value="true" />
|
||||
<option name="RECENT_BRANCH_BY_REPOSITORY">
|
||||
<map>
|
||||
<entry key="$PROJECT_DIR$" value="fork/bhavesh158/json-compare" />
|
||||
<entry key="$PROJECT_DIR$" value="fork/AshAnand34/tool/random-generators" />
|
||||
</map>
|
||||
</option>
|
||||
<option name="RECENT_GIT_ROOT_PATH" value="$PROJECT_DIR$" />
|
||||
|
|
@ -40,198 +56,217 @@
|
|||
},
|
||||
{
|
||||
"state": "OPEN"
|
||||
},
|
||||
{
|
||||
"searchQuery": "filter",
|
||||
"state": "OPEN"
|
||||
}
|
||||
],
|
||||
"lastFilter": {
|
||||
"searchQuery": "filter",
|
||||
"state": "OPEN"
|
||||
}
|
||||
}</component>
|
||||
<component name="GitHubPullRequestState"><![CDATA[{
|
||||
"prStates": [
|
||||
<component name="GitHubPullRequestState">{
|
||||
"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
|
||||
}
|
||||
]
|
||||
}]]></component>
|
||||
}</component>
|
||||
<component name="GithubPullRequestsUISettings">{
|
||||
"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"
|
||||
}
|
||||
}]]></component>
|
||||
|
|
@ -422,9 +458,9 @@
|
|||
<list>
|
||||
<item itemvalue="npm.dev" />
|
||||
<item itemvalue="npm.i18n:sync" />
|
||||
<item itemvalue="Vitest.generatePassword" />
|
||||
<item itemvalue="npm.i18n:pull" />
|
||||
<item itemvalue="npm.i18n:extract" />
|
||||
<item itemvalue="Vitest.generatePassword" />
|
||||
</list>
|
||||
</recent_temporary>
|
||||
</component>
|
||||
|
|
@ -538,46 +574,12 @@
|
|||
<workItem from="1752403829295" duration="13253000" />
|
||||
<workItem from="1752493585622" duration="11629000" />
|
||||
<workItem from="1752507105323" duration="9008000" />
|
||||
</task>
|
||||
<task id="LOCAL-00196" summary="chore: revert create-tool.mjs">
|
||||
<option name="closed" value="true" />
|
||||
<created>1748027090253</created>
|
||||
<option name="number" value="00196" />
|
||||
<option name="presentableId" value="LOCAL-00196" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1748027090253</updated>
|
||||
</task>
|
||||
<task id="LOCAL-00197" summary="fix: misc">
|
||||
<option name="closed" value="true" />
|
||||
<created>1748027889103</created>
|
||||
<option name="number" value="00197" />
|
||||
<option name="presentableId" value="LOCAL-00197" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1748027889103</updated>
|
||||
</task>
|
||||
<task id="LOCAL-00198" summary="chore: remove unnecessary prop">
|
||||
<option name="closed" value="true" />
|
||||
<created>1748028055669</created>
|
||||
<option name="number" value="00198" />
|
||||
<option name="presentableId" value="LOCAL-00198" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1748028055669</updated>
|
||||
</task>
|
||||
<task id="LOCAL-00199" summary="fix: compute flow">
|
||||
<option name="closed" value="true" />
|
||||
<created>1748881153433</created>
|
||||
<option name="number" value="00199" />
|
||||
<option name="presentableId" value="LOCAL-00199" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1748881153433</updated>
|
||||
</task>
|
||||
<task id="LOCAL-00200" summary="feat: qr code generation init">
|
||||
<option name="closed" value="true" />
|
||||
<created>1749147227565</created>
|
||||
<option name="number" value="00200" />
|
||||
<option name="presentableId" value="LOCAL-00200" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1749147227565</updated>
|
||||
<workItem from="1752875788332" duration="3566000" />
|
||||
<workItem from="1753099267173" duration="21000" />
|
||||
<workItem from="1753123130080" duration="4054000" />
|
||||
<workItem from="1753201599796" duration="4449000" />
|
||||
<workItem from="1753206561770" duration="119000" />
|
||||
<workItem from="1753206717510" duration="3599000" />
|
||||
</task>
|
||||
<task id="LOCAL-00201" summary="chore: rename from Omni Tools to OmniTools">
|
||||
<option name="closed" value="true" />
|
||||
|
|
@ -931,7 +933,47 @@
|
|||
<option name="project" value="LOCAL" />
|
||||
<updated>1752805853344</updated>
|
||||
</task>
|
||||
<option name="localTasksCounter" value="245" />
|
||||
<task id="LOCAL-00245" summary="feat: language browser detection">
|
||||
<option name="closed" value="true" />
|
||||
<created>1753124389709</created>
|
||||
<option name="number" value="00245" />
|
||||
<option name="presentableId" value="LOCAL-00245" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1753124389709</updated>
|
||||
</task>
|
||||
<task id="LOCAL-00246" summary="fix: misc">
|
||||
<option name="closed" value="true" />
|
||||
<created>1753206794968</created>
|
||||
<option name="number" value="00246" />
|
||||
<option name="presentableId" value="LOCAL-00246" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1753206794968</updated>
|
||||
</task>
|
||||
<task id="LOCAL-00247" summary="chore: show only necessary tags on a category">
|
||||
<option name="closed" value="true" />
|
||||
<created>1753207817041</created>
|
||||
<option name="number" value="00247" />
|
||||
<option name="presentableId" value="LOCAL-00247" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1753207817041</updated>
|
||||
</task>
|
||||
<task id="LOCAL-00248" summary="chore: CATEGORIES_USER_TYPES_MAPPINGS">
|
||||
<option name="closed" value="true" />
|
||||
<created>1753209484099</created>
|
||||
<option name="number" value="00248" />
|
||||
<option name="presentableId" value="LOCAL-00248" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1753209484099</updated>
|
||||
</task>
|
||||
<task id="LOCAL-00249" summary="chore: translate userTypes">
|
||||
<option name="closed" value="true" />
|
||||
<created>1753210033390</created>
|
||||
<option name="number" value="00249" />
|
||||
<option name="presentableId" value="LOCAL-00249" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1753210033390</updated>
|
||||
</task>
|
||||
<option name="localTasksCounter" value="250" />
|
||||
<servers />
|
||||
</component>
|
||||
<component name="TypeScriptGeneratedFilesManager">
|
||||
|
|
@ -978,11 +1020,6 @@
|
|||
<option name="CHECK_CODE_SMELLS_BEFORE_PROJECT_COMMIT" value="false" />
|
||||
<option name="CHECK_NEW_TODO" value="false" />
|
||||
<option name="ADD_EXTERNAL_FILES_SILENTLY" value="true" />
|
||||
<MESSAGE value="chore: style link" />
|
||||
<MESSAGE value="refactor: PDF editor" />
|
||||
<MESSAGE value="docs: edit pdf meta" />
|
||||
<MESSAGE value="fix: misc" />
|
||||
<MESSAGE value="chore: locize upload" />
|
||||
<MESSAGE value="chore: i18n in meta" />
|
||||
<MESSAGE value="chore: add i18n to meta script" />
|
||||
<MESSAGE value="chore: bundle translations at build time" />
|
||||
|
|
@ -1003,7 +1040,12 @@
|
|||
<MESSAGE value="fix: i18n tsc" />
|
||||
<MESSAGE value="chore: i18n pull dutch" />
|
||||
<MESSAGE value="chore: sync locize" />
|
||||
<option name="LAST_COMMIT_MESSAGE" value="chore: sync locize" />
|
||||
<MESSAGE value="feat: language browser detection" />
|
||||
<MESSAGE value="fix: misc" />
|
||||
<MESSAGE value="chore: show only necessary tags on a category" />
|
||||
<MESSAGE value="chore: CATEGORIES_USER_TYPES_MAPPINGS" />
|
||||
<MESSAGE value="chore: translate userTypes" />
|
||||
<option name="LAST_COMMIT_MESSAGE" value="chore: translate userTypes" />
|
||||
</component>
|
||||
<component name="VgoProject">
|
||||
<integration-enabled>false</integration-enabled>
|
||||
|
|
|
|||
17
package-lock.json
generated
17
package-lock.json
generated
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -246,5 +246,9 @@
|
|||
"copyFailed": "Failed to copy: {{error}}",
|
||||
"loading": "Loading... This may take a moment.",
|
||||
"result": "Result"
|
||||
},
|
||||
"userTypes": {
|
||||
"developers": "Developers",
|
||||
"generalUsers": "General users"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ import { darkTheme, lightTheme } from '../config/muiConfig';
|
|||
import ScrollToTopButton from './ScrollToTopButton';
|
||||
import { I18nextProvider } from 'react-i18next';
|
||||
import i18n from '../i18n';
|
||||
import { UserTypeFilterProvider } from 'providers/UserTypeFilterProvider';
|
||||
|
||||
export type Mode = 'dark' | 'light' | 'system';
|
||||
|
||||
|
|
@ -57,18 +58,20 @@ function App() {
|
|||
}}
|
||||
>
|
||||
<CustomSnackBarProvider>
|
||||
<BrowserRouter>
|
||||
<Navbar
|
||||
mode={mode}
|
||||
onChangeMode={() => {
|
||||
setMode((prev) => nextMode(prev));
|
||||
localStorage.setItem('theme', nextMode(mode));
|
||||
}}
|
||||
/>
|
||||
<Suspense fallback={<Loading />}>
|
||||
<AppRoutes />
|
||||
</Suspense>
|
||||
</BrowserRouter>
|
||||
<UserTypeFilterProvider>
|
||||
<BrowserRouter>
|
||||
<Navbar
|
||||
mode={mode}
|
||||
onChangeMode={() => {
|
||||
setMode((prev) => nextMode(prev));
|
||||
localStorage.setItem('theme', nextMode(mode));
|
||||
}}
|
||||
/>
|
||||
<Suspense fallback={<Loading />}>
|
||||
<AppRoutes />
|
||||
</Suspense>
|
||||
</BrowserRouter>
|
||||
</UserTypeFilterProvider>
|
||||
</CustomSnackBarProvider>
|
||||
</SnackbarProvider>
|
||||
<ScrollToTopButton />
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ import {
|
|||
toggleBookmarked
|
||||
} from '@utils/bookmark';
|
||||
import IconButton from '@mui/material/IconButton';
|
||||
import { useUserTypeFilter } from '../providers/UserTypeFilterProvider';
|
||||
|
||||
const GroupHeader = styled('div')(({ theme }) => ({
|
||||
position: 'sticky',
|
||||
|
|
@ -50,6 +51,7 @@ export default function Hero() {
|
|||
const { t } = useTranslation(validNamespaces);
|
||||
const [inputValue, setInputValue] = useState<string>('');
|
||||
const theme = useTheme();
|
||||
const { selectedUserTypes } = useUserTypeFilter();
|
||||
const [filteredTools, setFilteredTools] = useState<DefinedTool[]>(tools);
|
||||
const [bookmarkedToolPaths, setBookmarkedToolPaths] = useState<string[]>(
|
||||
getBookmarkedToolPaths()
|
||||
|
|
@ -96,12 +98,13 @@ export default function Hero() {
|
|||
];
|
||||
|
||||
const handleInputChange = (
|
||||
event: React.ChangeEvent<{}>,
|
||||
_event: React.ChangeEvent<{}>,
|
||||
newInputValue: string
|
||||
) => {
|
||||
setInputValue(newInputValue);
|
||||
setFilteredTools(filterTools(tools, newInputValue, t));
|
||||
setFilteredTools(filterTools(tools, newInputValue, selectedUserTypes, t));
|
||||
};
|
||||
|
||||
const toolsMap = new Map<string, ToolInfo>();
|
||||
for (const tool of filteredTools) {
|
||||
toolsMap.set(tool.path, {
|
||||
|
|
|
|||
|
|
@ -103,7 +103,7 @@ export default function ToolHeader({
|
|||
items={[
|
||||
{ title: 'All tools', link: '/' },
|
||||
{
|
||||
title: getToolsByCategory(t).find(
|
||||
title: getToolsByCategory([], t).find(
|
||||
(category) => category.type === type
|
||||
)!.rawTitle,
|
||||
link: '/categories/' + type
|
||||
|
|
|
|||
|
|
@ -43,7 +43,7 @@ export default function ToolLayout({
|
|||
const toolDescription: string = t(i18n.description);
|
||||
|
||||
const otherCategoryTools =
|
||||
getToolsByCategory(t)
|
||||
getToolsByCategory([], t)
|
||||
.find((category) => category.type === type)
|
||||
?.tools.filter((tool) => t(tool.name) !== toolTitle)
|
||||
.map((tool) => ({
|
||||
|
|
@ -77,8 +77,9 @@ export default function ToolLayout({
|
|||
<AllTools
|
||||
title={t('translation:toolLayout.allToolsTitle', '', {
|
||||
type: capitalizeFirstLetter(
|
||||
getToolsByCategory(t).find((category) => category.type === type)!
|
||||
.title
|
||||
getToolsByCategory([], t).find(
|
||||
(category) => category.type === type
|
||||
)!.title
|
||||
)
|
||||
})}
|
||||
toolCards={otherCategoryTools}
|
||||
|
|
|
|||
47
src/components/UserTypeFilter.tsx
Normal file
47
src/components/UserTypeFilter.tsx
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
import React from 'react';
|
||||
import { Box, Chip } from '@mui/material';
|
||||
import { UserType } from '@tools/defineTool';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
interface UserTypeFilterProps {
|
||||
selectedUserTypes: UserType[];
|
||||
userTypes?: UserType[];
|
||||
onUserTypesChange: (userTypes: UserType[]) => void;
|
||||
}
|
||||
|
||||
export default function UserTypeFilter({
|
||||
selectedUserTypes,
|
||||
onUserTypesChange,
|
||||
userTypes = ['generalUsers', 'developers']
|
||||
}: UserTypeFilterProps) {
|
||||
const { t } = useTranslation('translation');
|
||||
if (userTypes.length <= 1) return null;
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
flexWrap: 'wrap',
|
||||
gap: 1,
|
||||
minWidth: 200,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center'
|
||||
}}
|
||||
>
|
||||
{userTypes.map((userType) => (
|
||||
<Chip
|
||||
key={userType}
|
||||
label={t(`userTypes.${userType}`)}
|
||||
color={selectedUserTypes.includes(userType) ? 'primary' : 'default'}
|
||||
onClick={() => {
|
||||
const isSelected = selectedUserTypes.includes(userType);
|
||||
const newUserTypes = isSelected
|
||||
? selectedUserTypes.filter((ut) => ut !== userType)
|
||||
: [...selectedUserTypes, userType];
|
||||
onUserTypesChange(newUserTypes);
|
||||
}}
|
||||
sx={{ cursor: 'pointer' }}
|
||||
/>
|
||||
))}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
|
@ -5,7 +5,10 @@ import InputHeader from '../InputHeader';
|
|||
import InputFooter from './InputFooter';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import Editor from '@monaco-editor/react';
|
||||
import { globalInputHeight } from '../../config/uiConfig';
|
||||
import {
|
||||
globalInputHeight,
|
||||
codeInputHeightOffset
|
||||
} from '../../config/uiConfig';
|
||||
|
||||
export default function ToolCodeInput({
|
||||
value,
|
||||
|
|
@ -53,14 +56,62 @@ export default function ToolCodeInput({
|
|||
return (
|
||||
<Box>
|
||||
<InputHeader title={title || t('toolTextInput.input')} />
|
||||
<Box height={globalInputHeight}>
|
||||
<Editor
|
||||
height={'87%'}
|
||||
language={language}
|
||||
theme={theme.palette.mode === 'dark' ? 'vs-dark' : 'light'}
|
||||
value={value}
|
||||
onChange={(value) => onChange(value ?? '')}
|
||||
/>
|
||||
<Box
|
||||
height={`${globalInputHeight + codeInputHeightOffset}px`} // The +codeInputHeightOffset compensates for internal padding/border differences between Monaco Editor and MUI TextField
|
||||
sx={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column'
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={(theme) => ({
|
||||
height: '100%',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
backgroundColor: 'background.paper',
|
||||
'.monaco-editor': {
|
||||
height: '100% !important',
|
||||
outline: 'none !important',
|
||||
'.overflow-guard': {
|
||||
height: '100% !important',
|
||||
border:
|
||||
theme.palette.mode === 'light'
|
||||
? '1px solid rgba(0, 0, 0, 0.23)'
|
||||
: '1px solid rgba(255, 255, 255, 0.23)',
|
||||
borderRadius: 1,
|
||||
transition: theme.transitions.create(
|
||||
['border-color', 'background-color'],
|
||||
{
|
||||
duration: theme.transitions.duration.shorter
|
||||
}
|
||||
)
|
||||
},
|
||||
'&:hover .overflow-guard': {
|
||||
borderColor: theme.palette.text.primary
|
||||
}
|
||||
},
|
||||
'.decorationsOverviewRuler': {
|
||||
display: 'none !important'
|
||||
}
|
||||
})}
|
||||
>
|
||||
<Editor
|
||||
height="100%"
|
||||
language={language}
|
||||
theme={theme.palette.mode === 'dark' ? 'vs-dark' : 'light'}
|
||||
value={value}
|
||||
onChange={(value) => onChange(value ?? '')}
|
||||
options={{
|
||||
scrollbar: {
|
||||
vertical: 'visible',
|
||||
horizontal: 'visible',
|
||||
verticalScrollbarSize: 10,
|
||||
horizontalScrollbarSize: 10,
|
||||
alwaysConsumeMouseWheel: false
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
<InputFooter handleCopy={handleCopy} handleImport={handleImportClick} />
|
||||
<input
|
||||
type="file"
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
export const globalInputHeight = 300;
|
||||
export const codeInputHeightOffset = 7; // Offset to visually match Monaco and MUI TextField heights
|
||||
export const globalDescriptionFontSize = 12;
|
||||
export const categoriesColors: string[] = [
|
||||
'#8FBC5D',
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import i18n, { Namespace, ParseKeys } from 'i18next';
|
||||
import { initReactI18next } from 'react-i18next';
|
||||
import Backend from 'i18next-http-backend';
|
||||
import LanguageDetector from 'i18next-browser-languagedetector';
|
||||
|
||||
export const validNamespaces = [
|
||||
'string',
|
||||
|
|
@ -24,15 +25,20 @@ export type FullI18nKey = {
|
|||
|
||||
i18n
|
||||
.use(Backend)
|
||||
.use(LanguageDetector)
|
||||
.use(initReactI18next)
|
||||
.init({
|
||||
lng: localStorage.getItem('lang') || 'en',
|
||||
supportedLngs: ['en', 'de', 'es', 'fr', 'pt', 'ja', 'hi', 'nl', 'ru', 'zh'],
|
||||
fallbackLng: 'en',
|
||||
interpolation: {
|
||||
escapeValue: false // react already safes from xss => https://www.i18next.com/translation-function/interpolation#unescape
|
||||
},
|
||||
backend: {
|
||||
loadPath: '/locales/{{lng}}/{{ns}}.json'
|
||||
},
|
||||
detection: {
|
||||
lookupLocalStorage: 'lang',
|
||||
caches: ['localStorage'] // cache the detected lang back to localStorage
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ import { categoriesColors } from 'config/uiConfig';
|
|||
import { Icon } from '@iconify/react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { getI18nNamespaceFromToolCategory } from '@utils/string';
|
||||
import { validNamespaces } from '../../i18n';
|
||||
import { useUserTypeFilter } from '../../providers/UserTypeFilterProvider';
|
||||
|
||||
type ArrayElement<ArrayType extends readonly unknown[]> =
|
||||
ArrayType extends readonly (infer ElementType)[] ? ElementType : never;
|
||||
|
|
@ -84,10 +84,11 @@ const SingleCategory = function ({
|
|||
</Stack>
|
||||
<Typography sx={{ mt: 2 }}>{categoryDescription}</Typography>
|
||||
</Box>
|
||||
<Grid mt={1} container spacing={2}>
|
||||
<Grid container spacing={2} mt={2}>
|
||||
<Grid item xs={12} md={6}>
|
||||
<Button
|
||||
fullWidth
|
||||
sx={{ height: '100%' }}
|
||||
onClick={() => navigate('/categories/' + category.type)}
|
||||
variant={'contained'}
|
||||
>
|
||||
|
|
@ -96,7 +97,7 @@ const SingleCategory = function ({
|
|||
</Grid>
|
||||
<Grid item xs={12} md={6}>
|
||||
<Button
|
||||
sx={{ backgroundColor: 'background.default' }}
|
||||
sx={{ backgroundColor: 'background.default', height: '100%' }}
|
||||
fullWidth
|
||||
onClick={() => navigate(category.example.path)}
|
||||
variant={'outlined'}
|
||||
|
|
@ -111,11 +112,15 @@ const SingleCategory = function ({
|
|||
</Grid>
|
||||
);
|
||||
};
|
||||
|
||||
export default function Categories() {
|
||||
const { selectedUserTypes } = useUserTypeFilter();
|
||||
const { t } = useTranslation();
|
||||
const categories = getToolsByCategory(selectedUserTypes, t);
|
||||
|
||||
return (
|
||||
<Grid width={'80%'} container mt={2} spacing={2}>
|
||||
{getToolsByCategory(t).map((category, index) => (
|
||||
<Grid width={'80%'} container spacing={2}>
|
||||
{categories.map((category, index) => (
|
||||
<SingleCategory key={category.type} category={category} index={index} />
|
||||
))}
|
||||
</Grid>
|
||||
|
|
|
|||
|
|
@ -2,9 +2,12 @@ import { Box, useTheme } from '@mui/material';
|
|||
import Hero from 'components/Hero';
|
||||
import Categories from './Categories';
|
||||
import { Helmet } from 'react-helmet';
|
||||
import { useUserTypeFilter } from 'providers/UserTypeFilterProvider';
|
||||
import UserTypeFilter from '@components/UserTypeFilter';
|
||||
|
||||
export default function Home() {
|
||||
const theme = useTheme();
|
||||
const { selectedUserTypes, setSelectedUserTypes } = useUserTypeFilter();
|
||||
return (
|
||||
<Box
|
||||
padding={{
|
||||
|
|
@ -28,6 +31,12 @@ export default function Home() {
|
|||
>
|
||||
<Helmet title={'OmniTools'} />
|
||||
<Hero />
|
||||
<Box my={3}>
|
||||
<UserTypeFilter
|
||||
selectedUserTypes={selectedUserTypes}
|
||||
onUserTypesChange={setSelectedUserTypes}
|
||||
/>
|
||||
</Box>
|
||||
<Categories />
|
||||
</Box>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -22,28 +22,38 @@ import IconButton from '@mui/material/IconButton';
|
|||
import ArrowBackIcon from '@mui/icons-material/ArrowBack';
|
||||
import SearchIcon from '@mui/icons-material/Search';
|
||||
import { Helmet } from 'react-helmet';
|
||||
import UserTypeFilter from '@components/UserTypeFilter';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { I18nNamespaces, validNamespaces } from '../../i18n';
|
||||
import { useUserTypeFilter } from '../../providers/UserTypeFilterProvider';
|
||||
|
||||
const StyledLink = styled(Link)(({ theme }) => ({
|
||||
'&:hover': {
|
||||
color: theme.palette.mode === 'dark' ? 'white' : theme.palette.primary.light
|
||||
}
|
||||
}));
|
||||
|
||||
export default function ToolsByCategory() {
|
||||
const navigate = useNavigate();
|
||||
const theme = useTheme();
|
||||
const mainContentRef = React.useRef<HTMLDivElement>(null);
|
||||
const { categoryName } = useParams();
|
||||
const [searchTerm, setSearchTerm] = React.useState<string>('');
|
||||
const { selectedUserTypes, setSelectedUserTypes } = useUserTypeFilter();
|
||||
const { t } = useTranslation(validNamespaces);
|
||||
const rawTitle = getToolCategoryTitle(categoryName as string, t);
|
||||
// First get tools by category without filtering
|
||||
const toolsByCategory =
|
||||
getToolsByCategory(t).find(({ type }) => type === categoryName)?.tools ??
|
||||
[];
|
||||
const toolsByCategory = getToolsByCategory(selectedUserTypes, t).find(
|
||||
({ type }) => type === categoryName
|
||||
);
|
||||
const categoryDefinedTools = toolsByCategory?.tools ?? [];
|
||||
|
||||
const categoryTools = filterTools(toolsByCategory, searchTerm, t);
|
||||
const categoryTools = filterTools(
|
||||
categoryDefinedTools,
|
||||
searchTerm,
|
||||
selectedUserTypes,
|
||||
t
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (mainContentRef.current) {
|
||||
|
|
@ -90,7 +100,20 @@ export default function ToolsByCategory() {
|
|||
onChange={(event) => setSearchTerm(event.target.value)}
|
||||
/>
|
||||
</Stack>
|
||||
<Grid container spacing={2} mt={2}>
|
||||
<Box
|
||||
width={'100%'}
|
||||
display={'flex'}
|
||||
alignItems={'center'}
|
||||
justifyContent={'center'}
|
||||
my={2}
|
||||
>
|
||||
<UserTypeFilter
|
||||
userTypes={toolsByCategory?.userTypes ?? undefined}
|
||||
selectedUserTypes={selectedUserTypes}
|
||||
onUserTypesChange={setSelectedUserTypes}
|
||||
/>
|
||||
</Box>
|
||||
<Grid container spacing={2}>
|
||||
{categoryTools.map((tool, index) => (
|
||||
<Grid item xs={12} md={6} lg={4} key={tool.path}>
|
||||
<Stack
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ export const tool = defineTool('audio', {
|
|||
i18n: {
|
||||
name: 'audio:changeSpeed.title',
|
||||
description: 'audio:changeSpeed.description',
|
||||
shortDescription: 'audio:changeSpeed.shortDescription'
|
||||
shortDescription: 'audio:changeSpeed.shortDescription',
|
||||
userTypes: ['generalUsers', 'developers']
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ export const tool = defineTool('audio', {
|
|||
i18n: {
|
||||
name: 'audio:extractAudio.title',
|
||||
description: 'audio:extractAudio.description',
|
||||
shortDescription: 'audio:extractAudio.shortDescription'
|
||||
shortDescription: 'audio:extractAudio.shortDescription',
|
||||
userTypes: ['generalUsers', 'developers']
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -6,7 +6,8 @@ export const tool = defineTool('audio', {
|
|||
name: 'audio:mergeAudio.title',
|
||||
description: 'audio:mergeAudio.description',
|
||||
shortDescription: 'audio:mergeAudio.shortDescription',
|
||||
longDescription: 'audio:mergeAudio.longDescription'
|
||||
longDescription: 'audio:mergeAudio.longDescription',
|
||||
userTypes: ['generalUsers', 'developers']
|
||||
},
|
||||
|
||||
path: 'merge-audio',
|
||||
|
|
@ -24,6 +25,5 @@ export const tool = defineTool('audio', {
|
|||
'audio editing',
|
||||
'multiple files'
|
||||
],
|
||||
|
||||
component: lazy(() => import('./index'))
|
||||
});
|
||||
|
|
|
|||
|
|
@ -6,7 +6,8 @@ export const tool = defineTool('audio', {
|
|||
name: 'audio:trim.title',
|
||||
description: 'audio:trim.description',
|
||||
shortDescription: 'audio:trim.shortDescription',
|
||||
longDescription: 'audio:trim.longDescription'
|
||||
longDescription: 'audio:trim.longDescription',
|
||||
userTypes: ['generalUsers', 'developers']
|
||||
},
|
||||
|
||||
path: 'trim',
|
||||
|
|
@ -24,6 +25,5 @@ export const tool = defineTool('audio', {
|
|||
'audio editing',
|
||||
'time'
|
||||
],
|
||||
|
||||
component: lazy(() => import('./index'))
|
||||
});
|
||||
|
|
|
|||
|
|
@ -12,6 +12,5 @@ export const tool = defineTool('csv', {
|
|||
path: 'csv-to-yaml',
|
||||
icon: 'nonicons:yaml-16',
|
||||
keywords: ['csv', 'to', 'yaml'],
|
||||
|
||||
component: lazy(() => import('./index'))
|
||||
});
|
||||
|
|
|
|||
|
|
@ -12,6 +12,5 @@ export const tool = defineTool('csv', {
|
|||
icon: 'hugeicons:column-insert',
|
||||
|
||||
keywords: ['insert', 'csv', 'columns', 'append', 'prepend'],
|
||||
|
||||
component: lazy(() => import('./index'))
|
||||
});
|
||||
|
|
|
|||
|
|
@ -13,6 +13,5 @@ export const tool = defineTool('csv', {
|
|||
icon: 'carbon:transpose',
|
||||
|
||||
keywords: ['transpose', 'csv'],
|
||||
|
||||
component: lazy(() => import('./index'))
|
||||
});
|
||||
|
|
|
|||
|
|
@ -5,12 +5,13 @@ export const tool = defineTool('image-generic', {
|
|||
i18n: {
|
||||
name: 'image:compress.title',
|
||||
description: 'image:compress.description',
|
||||
shortDescription: 'image:compress.shortDescription'
|
||||
shortDescription: 'image:compress.shortDescription',
|
||||
userTypes: ['generalUsers']
|
||||
},
|
||||
|
||||
path: 'compress',
|
||||
component: lazy(() => import('./index')),
|
||||
icon: 'material-symbols-light:compress-rounded',
|
||||
|
||||
keywords: ['image', 'compress', 'reduce', 'quality']
|
||||
keywords: ['image', 'compress', 'reduce', 'quality'],
|
||||
component: lazy(() => import('./index'))
|
||||
});
|
||||
|
|
|
|||
|
|
@ -5,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'
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,7 +5,8 @@ export const tool = defineTool('image-generic', {
|
|||
i18n: {
|
||||
name: 'image:resize.title',
|
||||
description: 'image:resize.description',
|
||||
shortDescription: 'image:resize.shortDescription'
|
||||
shortDescription: 'image:resize.shortDescription',
|
||||
userTypes: ['generalUsers']
|
||||
},
|
||||
|
||||
path: 'resize',
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import React, { useState } from 'react';
|
||||
import ToolContent from '@components/ToolContent';
|
||||
import ToolTextInput from '@components/input/ToolTextInput';
|
||||
import ToolCodeInput from '@components/input/ToolCodeInput';
|
||||
import ToolTextResult from '@components/result/ToolTextResult';
|
||||
import { escapeJson } from './service';
|
||||
import { CardExampleType } from '@components/examples/ToolExamples';
|
||||
|
|
@ -88,7 +88,12 @@ export default function EscapeJsonTool({
|
|||
<ToolContent
|
||||
title={title}
|
||||
inputComponent={
|
||||
<ToolTextInput title="Input JSON" value={input} onChange={setInput} />
|
||||
<ToolCodeInput
|
||||
title="Input JSON"
|
||||
value={input}
|
||||
onChange={setInput}
|
||||
language="json"
|
||||
/>
|
||||
}
|
||||
resultComponent={
|
||||
<ToolTextResult
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { useState } from 'react';
|
||||
import ToolContent from '@components/ToolContent';
|
||||
import ToolTextInput from '@components/input/ToolTextInput';
|
||||
import ToolCodeInput from '@components/input/ToolCodeInput';
|
||||
import ToolTextResult from '@components/result/ToolTextResult';
|
||||
import { convertJsonToXml } from './service';
|
||||
import { CardExampleType } from '@components/examples/ToolExamples';
|
||||
|
|
@ -84,7 +84,12 @@ export default function JsonToXml({ title }: ToolComponentProps) {
|
|||
compute={compute}
|
||||
exampleCards={exampleCards}
|
||||
inputComponent={
|
||||
<ToolTextInput title="Input Json" value={input} onChange={setInput} />
|
||||
<ToolCodeInput
|
||||
title="Input Json"
|
||||
value={input}
|
||||
onChange={setInput}
|
||||
language="json"
|
||||
/>
|
||||
}
|
||||
resultComponent={
|
||||
<ToolTextResult title="Output XML" value={result} extension={'xml'} />
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import React, { useState } from 'react';
|
||||
import ToolContent from '@components/ToolContent';
|
||||
import ToolTextInput from '@components/input/ToolTextInput';
|
||||
import ToolCodeInput from '@components/input/ToolCodeInput';
|
||||
import ToolTextResult from '@components/result/ToolTextResult';
|
||||
import { minifyJson } from './service';
|
||||
import { CardExampleType } from '@components/examples/ToolExamples';
|
||||
|
|
@ -60,10 +60,11 @@ export default function MinifyJson({ title }: ToolComponentProps) {
|
|||
<ToolContent
|
||||
title={title}
|
||||
inputComponent={
|
||||
<ToolTextInput
|
||||
<ToolCodeInput
|
||||
title={t('minify.inputTitle')}
|
||||
value={input}
|
||||
onChange={setInput}
|
||||
language="json"
|
||||
/>
|
||||
}
|
||||
resultComponent={
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { Box } from '@mui/material';
|
||||
import React, { useRef, useState } from 'react';
|
||||
import ToolTextInput from '@components/input/ToolTextInput';
|
||||
import ToolCodeInput from '@components/input/ToolCodeInput';
|
||||
import ToolTextResult from '@components/result/ToolTextResult';
|
||||
import { beautifyJson } from './service';
|
||||
import ToolInfo from '@components/ToolInfo';
|
||||
|
|
@ -130,10 +130,11 @@ export default function PrettifyJson({ title }: ToolComponentProps) {
|
|||
title={title}
|
||||
input={input}
|
||||
inputComponent={
|
||||
<ToolTextInput
|
||||
<ToolCodeInput
|
||||
title={t('prettify.inputTitle')}
|
||||
value={input}
|
||||
onChange={setInput}
|
||||
language="json"
|
||||
/>
|
||||
}
|
||||
resultComponent={
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { Box } from '@mui/material';
|
||||
import React, { useState } from 'react';
|
||||
import ToolContent from '@components/ToolContent';
|
||||
import ToolTextInput from '@components/input/ToolTextInput';
|
||||
import ToolCodeInput from '@components/input/ToolCodeInput';
|
||||
import ToolTextResult from '@components/result/ToolTextResult';
|
||||
import { stringifyJson } from './service';
|
||||
import { ToolComponentProps } from '@tools/defineTool';
|
||||
|
|
@ -103,10 +103,11 @@ export default function StringifyJson({ title }: ToolComponentProps) {
|
|||
compute={compute}
|
||||
exampleCards={exampleCards}
|
||||
inputComponent={
|
||||
<ToolTextInput
|
||||
<ToolCodeInput
|
||||
title="JavaScript Object/Array"
|
||||
value={input}
|
||||
onChange={setInput}
|
||||
language="json"
|
||||
/>
|
||||
}
|
||||
resultComponent={
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import React, { useState } from 'react';
|
||||
import ToolContent from '@components/ToolContent';
|
||||
import ToolTextInput from '@components/input/ToolTextInput';
|
||||
import ToolCodeInput from '@components/input/ToolCodeInput';
|
||||
import ToolTextResult from '@components/result/ToolTextResult';
|
||||
import { convertTsvToJson } from './service';
|
||||
import { CardExampleType } from '@components/examples/ToolExamples';
|
||||
|
|
@ -216,7 +216,12 @@ export default function TsvToJson({
|
|||
exampleCards={exampleCards}
|
||||
getGroups={getGroups}
|
||||
inputComponent={
|
||||
<ToolTextInput title="Input TSV" value={input} onChange={setInput} />
|
||||
<ToolCodeInput
|
||||
title="Input TSV"
|
||||
value={input}
|
||||
onChange={setInput}
|
||||
language="tsv"
|
||||
/>
|
||||
}
|
||||
resultComponent={
|
||||
<ToolTextResult title="Output JSON" value={result} extension={'json'} />
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import React, { useState } from 'react';
|
||||
import ToolTextInput from '@components/input/ToolTextInput';
|
||||
import ToolCodeInput from '@components/input/ToolCodeInput';
|
||||
import ToolTextResult from '@components/result/ToolTextResult';
|
||||
import { CardExampleType } from '@components/examples/ToolExamples';
|
||||
import { validateJson } from './service';
|
||||
|
|
@ -65,10 +65,11 @@ export default function ValidateJson({ title }: ToolComponentProps) {
|
|||
<ToolContent
|
||||
title={title}
|
||||
inputComponent={
|
||||
<ToolTextInput
|
||||
<ToolCodeInput
|
||||
title={t('validateJson.inputTitle')}
|
||||
value={input}
|
||||
onChange={setInput}
|
||||
language="json"
|
||||
/>
|
||||
}
|
||||
resultComponent={
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import { defineTool } from '@tools/defineTool';
|
|||
import { lazy } from 'react';
|
||||
|
||||
export const tool = defineTool('json', {
|
||||
path: 'validate-json',
|
||||
path: 'validateJson',
|
||||
icon: 'material-symbols:check-circle',
|
||||
|
||||
keywords: ['json', 'validate', 'check', 'syntax', 'errors'],
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ export const tool = defineTool('list', {
|
|||
i18n: {
|
||||
name: 'list:duplicate.title',
|
||||
description: 'list:duplicate.description',
|
||||
shortDescription: 'list:duplicate.shortDescription'
|
||||
shortDescription: 'list:duplicate.shortDescription',
|
||||
userTypes: ['generalUsers', 'developers']
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ export const tool = defineTool('list', {
|
|||
i18n: {
|
||||
name: 'list:findMostPopular.title',
|
||||
description: 'list:findMostPopular.description',
|
||||
shortDescription: 'list:findMostPopular.shortDescription'
|
||||
shortDescription: 'list:findMostPopular.shortDescription',
|
||||
userTypes: ['generalUsers', 'developers']
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ export const tool = defineTool('list', {
|
|||
i18n: {
|
||||
name: 'list:findUnique.title',
|
||||
description: 'list:findUnique.description',
|
||||
shortDescription: 'list:findUnique.shortDescription'
|
||||
shortDescription: 'list:findUnique.shortDescription',
|
||||
userTypes: ['generalUsers', 'developers']
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ export const tool = defineTool('list', {
|
|||
i18n: {
|
||||
name: 'list:group.title',
|
||||
description: 'list:group.description',
|
||||
shortDescription: 'list:group.shortDescription'
|
||||
shortDescription: 'list:group.shortDescription',
|
||||
userTypes: ['generalUsers', 'developers']
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -5,12 +5,12 @@ import { lazy } from 'react';
|
|||
export const tool = defineTool('list', {
|
||||
path: 'reverse',
|
||||
icon: 'proicons:reverse',
|
||||
|
||||
keywords: ['reverse'],
|
||||
component: lazy(() => import('./index')),
|
||||
i18n: {
|
||||
name: 'list:reverse.title',
|
||||
description: 'list:reverse.description',
|
||||
shortDescription: 'list:reverse.shortDescription'
|
||||
}
|
||||
shortDescription: 'list:reverse.shortDescription',
|
||||
userTypes: ['generalUsers']
|
||||
},
|
||||
component: lazy(() => import('./index'))
|
||||
});
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ export const tool = defineTool('list', {
|
|||
i18n: {
|
||||
name: 'list:rotate.title',
|
||||
description: 'list:rotate.description',
|
||||
shortDescription: 'list:rotate.shortDescription'
|
||||
shortDescription: 'list:rotate.shortDescription',
|
||||
userTypes: ['generalUsers']
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ export const tool = defineTool('list', {
|
|||
i18n: {
|
||||
name: 'list:shuffle.title',
|
||||
description: 'list:shuffle.description',
|
||||
shortDescription: 'list:shuffle.shortDescription'
|
||||
shortDescription: 'list:shuffle.shortDescription',
|
||||
userTypes: ['generalUsers']
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ export const tool = defineTool('list', {
|
|||
i18n: {
|
||||
name: 'list:sort.title',
|
||||
description: 'list:sort.description',
|
||||
shortDescription: 'list:sort.shortDescription'
|
||||
shortDescription: 'list:sort.shortDescription',
|
||||
userTypes: ['generalUsers', 'developers']
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ export const tool = defineTool('list', {
|
|||
i18n: {
|
||||
name: 'list:truncate.title',
|
||||
description: 'list:truncate.description',
|
||||
shortDescription: 'list:truncate.shortDescription'
|
||||
shortDescription: 'list:truncate.shortDescription',
|
||||
userTypes: ['generalUsers']
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ export const tool = defineTool('list', {
|
|||
i18n: {
|
||||
name: 'list:unwrap.title',
|
||||
description: 'list:unwrap.description',
|
||||
shortDescription: 'list:unwrap.shortDescription'
|
||||
shortDescription: 'list:unwrap.shortDescription',
|
||||
userTypes: ['generalUsers', 'developers']
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ export const tool = defineTool('list', {
|
|||
i18n: {
|
||||
name: 'list:wrap.title',
|
||||
description: 'list:wrap.description',
|
||||
shortDescription: 'list:wrap.shortDescription'
|
||||
shortDescription: 'list:wrap.shortDescription',
|
||||
userTypes: ['generalUsers', 'developers']
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ export const tool = defineTool('number', {
|
|||
i18n: {
|
||||
name: 'number:arithmeticSequence.title',
|
||||
description: 'number:arithmeticSequence.description',
|
||||
shortDescription: 'number:arithmeticSequence.shortDescription'
|
||||
shortDescription: 'number:arithmeticSequence.shortDescription',
|
||||
userTypes: ['generalUsers']
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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
|
||||
];
|
||||
|
|
|
|||
200
src/pages/tools/number/random-number-generator/index.tsx
Normal file
200
src/pages/tools/number/random-number-generator/index.tsx
Normal file
|
|
@ -0,0 +1,200 @@
|
|||
import { Alert, Box } from '@mui/material';
|
||||
import { useState } from 'react';
|
||||
import ToolContent from '@components/ToolContent';
|
||||
import { ToolComponentProps } from '@tools/defineTool';
|
||||
import ToolTextResult from '@components/result/ToolTextResult';
|
||||
import { GetGroupsType } from '@components/options/ToolOptions';
|
||||
import { formatNumbers, generateRandomNumbers, validateInput } from './service';
|
||||
import { InitialValuesType, RandomNumberResult } from './types';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import TextFieldWithDesc from '@components/options/TextFieldWithDesc';
|
||||
import CheckboxWithDesc from '@components/options/CheckboxWithDesc';
|
||||
|
||||
const initialValues: InitialValuesType = {
|
||||
minValue: 1,
|
||||
maxValue: 100,
|
||||
count: 10,
|
||||
allowDecimals: false,
|
||||
allowDuplicates: true,
|
||||
sortResults: false,
|
||||
separator: ', '
|
||||
};
|
||||
|
||||
export default function RandomNumberGenerator({
|
||||
title,
|
||||
longDescription
|
||||
}: ToolComponentProps) {
|
||||
const { t } = useTranslation('number');
|
||||
const [result, setResult] = useState<RandomNumberResult | null>(null);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [formattedResult, setFormattedResult] = useState<string>('');
|
||||
|
||||
const compute = (values: InitialValuesType) => {
|
||||
try {
|
||||
setError(null);
|
||||
setResult(null);
|
||||
setFormattedResult('');
|
||||
|
||||
// Validate input
|
||||
const validationError = validateInput(values);
|
||||
if (validationError) {
|
||||
setError(validationError);
|
||||
return;
|
||||
}
|
||||
|
||||
// Generate random numbers
|
||||
const randomResult = generateRandomNumbers(values);
|
||||
setResult(randomResult);
|
||||
|
||||
// Format for display
|
||||
const formatted = formatNumbers(
|
||||
randomResult.numbers,
|
||||
values.separator,
|
||||
values.allowDecimals
|
||||
);
|
||||
setFormattedResult(formatted);
|
||||
} catch (err) {
|
||||
console.error('Random number generation failed:', err);
|
||||
setError(t('randomNumberGenerator.error.generationFailed'));
|
||||
}
|
||||
};
|
||||
|
||||
const getGroups: GetGroupsType<InitialValuesType> | null = ({
|
||||
values,
|
||||
updateField
|
||||
}) => [
|
||||
{
|
||||
title: t('randomNumberGenerator.options.range.title'),
|
||||
component: (
|
||||
<Box>
|
||||
<TextFieldWithDesc
|
||||
value={values.minValue.toString()}
|
||||
onOwnChange={(value) =>
|
||||
updateField('minValue', parseInt(value) || 1)
|
||||
}
|
||||
description={t(
|
||||
'randomNumberGenerator.options.range.minDescription'
|
||||
)}
|
||||
inputProps={{
|
||||
type: 'number',
|
||||
'data-testid': 'min-value-input'
|
||||
}}
|
||||
/>
|
||||
<TextFieldWithDesc
|
||||
value={values.maxValue.toString()}
|
||||
onOwnChange={(value) =>
|
||||
updateField('maxValue', parseInt(value) || 100)
|
||||
}
|
||||
description={t(
|
||||
'randomNumberGenerator.options.range.maxDescription'
|
||||
)}
|
||||
inputProps={{
|
||||
type: 'number',
|
||||
'data-testid': 'max-value-input'
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
)
|
||||
},
|
||||
{
|
||||
title: t('randomNumberGenerator.options.generation.title'),
|
||||
component: (
|
||||
<Box>
|
||||
<TextFieldWithDesc
|
||||
value={values.count.toString()}
|
||||
onOwnChange={(value) => updateField('count', parseInt(value) || 10)}
|
||||
description={t(
|
||||
'randomNumberGenerator.options.generation.countDescription'
|
||||
)}
|
||||
inputProps={{
|
||||
type: 'number',
|
||||
min: 1,
|
||||
max: 10000,
|
||||
'data-testid': 'count-input'
|
||||
}}
|
||||
/>
|
||||
|
||||
<CheckboxWithDesc
|
||||
title={t(
|
||||
'randomNumberGenerator.options.generation.allowDecimals.title'
|
||||
)}
|
||||
checked={values.allowDecimals}
|
||||
onChange={(value) => updateField('allowDecimals', value)}
|
||||
description={t(
|
||||
'randomNumberGenerator.options.generation.allowDecimals.description'
|
||||
)}
|
||||
/>
|
||||
|
||||
<CheckboxWithDesc
|
||||
title={t(
|
||||
'randomNumberGenerator.options.generation.allowDuplicates.title'
|
||||
)}
|
||||
checked={values.allowDuplicates}
|
||||
onChange={(value) => updateField('allowDuplicates', value)}
|
||||
description={t(
|
||||
'randomNumberGenerator.options.generation.allowDuplicates.description'
|
||||
)}
|
||||
/>
|
||||
|
||||
<CheckboxWithDesc
|
||||
title={t(
|
||||
'randomNumberGenerator.options.generation.sortResults.title'
|
||||
)}
|
||||
checked={values.sortResults}
|
||||
onChange={(value) => updateField('sortResults', value)}
|
||||
description={t(
|
||||
'randomNumberGenerator.options.generation.sortResults.description'
|
||||
)}
|
||||
/>
|
||||
</Box>
|
||||
)
|
||||
},
|
||||
{
|
||||
title: t('randomNumberGenerator.options.output.title'),
|
||||
component: (
|
||||
<Box>
|
||||
<TextFieldWithDesc
|
||||
value={values.separator}
|
||||
onOwnChange={(value) => updateField('separator', value)}
|
||||
description={t(
|
||||
'randomNumberGenerator.options.output.separatorDescription'
|
||||
)}
|
||||
inputProps={{
|
||||
'data-testid': 'separator-input'
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
];
|
||||
|
||||
return (
|
||||
<ToolContent
|
||||
title={title}
|
||||
initialValues={initialValues}
|
||||
compute={compute}
|
||||
getGroups={getGroups}
|
||||
resultComponent={
|
||||
<Box>
|
||||
{error && (
|
||||
<Alert severity="error" sx={{ mb: 2 }}>
|
||||
{error}
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
{result && (
|
||||
<ToolTextResult
|
||||
title={t('randomNumberGenerator.result.title')}
|
||||
value={formattedResult}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
}
|
||||
toolInfo={{
|
||||
title: t('randomNumberGenerator.info.title'),
|
||||
description:
|
||||
longDescription || t('randomNumberGenerator.info.description')
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
26
src/pages/tools/number/random-number-generator/meta.ts
Normal file
26
src/pages/tools/number/random-number-generator/meta.ts
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
import { defineTool } from '@tools/defineTool';
|
||||
import { lazy } from 'react';
|
||||
|
||||
export const tool = defineTool('number', {
|
||||
i18n: {
|
||||
name: 'number:randomNumberGenerator.title',
|
||||
description: 'number:randomNumberGenerator.description',
|
||||
shortDescription: 'number:randomNumberGenerator.shortDescription',
|
||||
longDescription: 'number:randomNumberGenerator.longDescription',
|
||||
userTypes: ['generalUsers']
|
||||
},
|
||||
path: 'random-number-generator',
|
||||
icon: 'mdi:dice-multiple',
|
||||
keywords: [
|
||||
'random',
|
||||
'number',
|
||||
'generator',
|
||||
'range',
|
||||
'min',
|
||||
'max',
|
||||
'integer',
|
||||
'decimal',
|
||||
'float'
|
||||
],
|
||||
component: lazy(() => import('./index'))
|
||||
});
|
||||
|
|
@ -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('');
|
||||
});
|
||||
});
|
||||
});
|
||||
157
src/pages/tools/number/random-number-generator/service.ts
Normal file
157
src/pages/tools/number/random-number-generator/service.ts
Normal file
|
|
@ -0,0 +1,157 @@
|
|||
import { InitialValuesType, RandomNumberResult } from './types';
|
||||
|
||||
/**
|
||||
* Generate random numbers within a specified range
|
||||
*/
|
||||
export function generateRandomNumbers(
|
||||
options: InitialValuesType
|
||||
): RandomNumberResult {
|
||||
const {
|
||||
minValue,
|
||||
maxValue,
|
||||
count,
|
||||
allowDecimals,
|
||||
allowDuplicates,
|
||||
sortResults
|
||||
} = options;
|
||||
|
||||
if (minValue >= maxValue) {
|
||||
throw new Error('Minimum value must be less than maximum value');
|
||||
}
|
||||
|
||||
if (count <= 0) {
|
||||
throw new Error('Count must be greater than 0');
|
||||
}
|
||||
|
||||
if (!allowDuplicates && count > maxValue - minValue + 1) {
|
||||
throw new Error(
|
||||
'Cannot generate unique numbers: count exceeds available range'
|
||||
);
|
||||
}
|
||||
|
||||
const numbers: number[] = [];
|
||||
|
||||
if (allowDuplicates) {
|
||||
// Generate random numbers with possible duplicates
|
||||
for (let i = 0; i < count; i++) {
|
||||
const randomNumber = generateRandomNumber(
|
||||
minValue,
|
||||
maxValue,
|
||||
allowDecimals
|
||||
);
|
||||
numbers.push(randomNumber);
|
||||
}
|
||||
} else {
|
||||
// Generate unique random numbers
|
||||
const availableNumbers = new Set<number>();
|
||||
|
||||
// Create a pool of available numbers
|
||||
for (let i = minValue; i <= maxValue; i++) {
|
||||
if (allowDecimals) {
|
||||
// For decimals, we need to generate more granular values
|
||||
for (let j = 0; j < 100; j++) {
|
||||
availableNumbers.add(i + j / 100);
|
||||
}
|
||||
} else {
|
||||
availableNumbers.add(i);
|
||||
}
|
||||
}
|
||||
|
||||
const availableArray = Array.from(availableNumbers);
|
||||
|
||||
// Shuffle the available numbers
|
||||
for (let i = availableArray.length - 1; i > 0; i--) {
|
||||
const j = Math.floor(Math.random() * (i + 1));
|
||||
[availableArray[i], availableArray[j]] = [
|
||||
availableArray[j],
|
||||
availableArray[i]
|
||||
];
|
||||
}
|
||||
|
||||
// Take the first 'count' numbers
|
||||
for (let i = 0; i < Math.min(count, availableArray.length); i++) {
|
||||
numbers.push(availableArray[i]);
|
||||
}
|
||||
}
|
||||
|
||||
// Sort if requested
|
||||
if (sortResults) {
|
||||
numbers.sort((a, b) => a - b);
|
||||
}
|
||||
|
||||
return {
|
||||
numbers,
|
||||
min: minValue,
|
||||
max: maxValue,
|
||||
count,
|
||||
hasDuplicates: !allowDuplicates && hasDuplicatesInArray(numbers),
|
||||
isSorted: sortResults
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a single random number within the specified range
|
||||
*/
|
||||
function generateRandomNumber(
|
||||
min: number,
|
||||
max: number,
|
||||
allowDecimals: boolean
|
||||
): number {
|
||||
if (allowDecimals) {
|
||||
return Math.random() * (max - min) + min;
|
||||
} else {
|
||||
return Math.floor(Math.random() * (max - min + 1)) + min;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if an array has duplicate values
|
||||
*/
|
||||
function hasDuplicatesInArray(arr: number[]): boolean {
|
||||
const seen = new Set<number>();
|
||||
for (const num of arr) {
|
||||
if (seen.has(num)) {
|
||||
return true;
|
||||
}
|
||||
seen.add(num);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Format numbers for display
|
||||
*/
|
||||
export function formatNumbers(
|
||||
numbers: number[],
|
||||
separator: string,
|
||||
allowDecimals: boolean
|
||||
): string {
|
||||
return numbers
|
||||
.map((num) => (allowDecimals ? num.toFixed(2) : Math.round(num).toString()))
|
||||
.join(separator);
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate input parameters
|
||||
*/
|
||||
export function validateInput(options: InitialValuesType): string | null {
|
||||
const { minValue, maxValue, count } = options;
|
||||
|
||||
if (minValue >= maxValue) {
|
||||
return 'Minimum value must be less than maximum value';
|
||||
}
|
||||
|
||||
if (count <= 0) {
|
||||
return 'Count must be greater than 0';
|
||||
}
|
||||
|
||||
if (count > 10000) {
|
||||
return 'Count cannot exceed 10,000';
|
||||
}
|
||||
|
||||
if (maxValue - minValue > 1000000) {
|
||||
return 'Range cannot exceed 1,000,000';
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
18
src/pages/tools/number/random-number-generator/types.ts
Normal file
18
src/pages/tools/number/random-number-generator/types.ts
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
export type InitialValuesType = {
|
||||
minValue: number;
|
||||
maxValue: number;
|
||||
count: number;
|
||||
allowDecimals: boolean;
|
||||
allowDuplicates: boolean;
|
||||
sortResults: boolean;
|
||||
separator: string;
|
||||
};
|
||||
|
||||
export type RandomNumberResult = {
|
||||
numbers: number[];
|
||||
min: number;
|
||||
max: number;
|
||||
count: number;
|
||||
hasDuplicates: boolean;
|
||||
isSorted: boolean;
|
||||
};
|
||||
233
src/pages/tools/number/random-port-generator/index.tsx
Normal file
233
src/pages/tools/number/random-port-generator/index.tsx
Normal file
|
|
@ -0,0 +1,233 @@
|
|||
import { Alert, Box } from '@mui/material';
|
||||
import { useState } from 'react';
|
||||
import ToolContent from '@components/ToolContent';
|
||||
import { ToolComponentProps } from '@tools/defineTool';
|
||||
import ToolTextResult from '@components/result/ToolTextResult';
|
||||
import { GetGroupsType } from '@components/options/ToolOptions';
|
||||
import {
|
||||
formatPorts,
|
||||
generateRandomPorts,
|
||||
getPortRangeInfo,
|
||||
validateInput
|
||||
} from './service';
|
||||
import { InitialValuesType, RandomPortResult } from './types';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import TextFieldWithDesc from '@components/options/TextFieldWithDesc';
|
||||
import CheckboxWithDesc from '@components/options/CheckboxWithDesc';
|
||||
import SimpleRadio from '@components/options/SimpleRadio';
|
||||
|
||||
const initialValues: InitialValuesType = {
|
||||
portRange: 'registered',
|
||||
minPort: 1024,
|
||||
maxPort: 49151,
|
||||
count: 5,
|
||||
allowDuplicates: false,
|
||||
sortResults: false,
|
||||
separator: ', '
|
||||
};
|
||||
|
||||
export default function RandomPortGenerator({
|
||||
title,
|
||||
longDescription
|
||||
}: ToolComponentProps) {
|
||||
const { t } = useTranslation('number');
|
||||
const [result, setResult] = useState<RandomPortResult | null>(null);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [formattedResult, setFormattedResult] = useState<string>('');
|
||||
|
||||
const compute = (values: InitialValuesType) => {
|
||||
try {
|
||||
setError(null);
|
||||
setResult(null);
|
||||
setFormattedResult('');
|
||||
|
||||
// Validate input
|
||||
const validationError = validateInput(values);
|
||||
if (validationError) {
|
||||
setError(validationError);
|
||||
return;
|
||||
}
|
||||
|
||||
// Generate random ports
|
||||
const randomResult = generateRandomPorts(values);
|
||||
setResult(randomResult);
|
||||
|
||||
// Format for display
|
||||
const formatted = formatPorts(randomResult.ports, values.separator);
|
||||
setFormattedResult(formatted);
|
||||
} catch (err) {
|
||||
console.error('Random port generation failed:', err);
|
||||
setError(t('randomPortGenerator.error.generationFailed'));
|
||||
}
|
||||
};
|
||||
const portOptions = [
|
||||
{
|
||||
value: 'well-known',
|
||||
label: t('randomPortGenerator.options.range.wellKnown')
|
||||
},
|
||||
{
|
||||
value: 'registered',
|
||||
label: t('randomPortGenerator.options.range.registered')
|
||||
},
|
||||
{
|
||||
value: 'dynamic',
|
||||
label: t('randomPortGenerator.options.range.dynamic')
|
||||
},
|
||||
{
|
||||
value: 'custom',
|
||||
label: t('randomPortGenerator.options.range.custom')
|
||||
}
|
||||
] as const;
|
||||
const getGroups: GetGroupsType<InitialValuesType> | null = ({
|
||||
values,
|
||||
updateField
|
||||
}) => [
|
||||
{
|
||||
title: t('randomPortGenerator.options.range.title'),
|
||||
component: (
|
||||
<Box>
|
||||
{portOptions.map((option) => (
|
||||
<SimpleRadio
|
||||
key={option.value}
|
||||
title={option.label}
|
||||
checked={values.portRange === option.value}
|
||||
onClick={() => updateField('portRange', option.value)}
|
||||
/>
|
||||
))}
|
||||
|
||||
{values.portRange === 'custom' && (
|
||||
<Box sx={{ mt: 2 }}>
|
||||
<TextFieldWithDesc
|
||||
value={values.minPort.toString()}
|
||||
onOwnChange={(value) =>
|
||||
updateField('minPort', parseInt(value) || 1024)
|
||||
}
|
||||
description={t(
|
||||
'randomPortGenerator.options.range.minPortDescription'
|
||||
)}
|
||||
inputProps={{
|
||||
type: 'number',
|
||||
min: 1,
|
||||
max: 65535,
|
||||
'data-testid': 'min-port-input'
|
||||
}}
|
||||
/>
|
||||
<TextFieldWithDesc
|
||||
value={values.maxPort.toString()}
|
||||
onOwnChange={(value) =>
|
||||
updateField('maxPort', parseInt(value) || 49151)
|
||||
}
|
||||
description={t(
|
||||
'randomPortGenerator.options.range.maxPortDescription'
|
||||
)}
|
||||
inputProps={{
|
||||
type: 'number',
|
||||
min: 1,
|
||||
max: 65535,
|
||||
'data-testid': 'max-port-input'
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
<Box
|
||||
sx={{ mt: 2, p: 2, bgcolor: 'background.paper', borderRadius: 1 }}
|
||||
>
|
||||
<strong>{getPortRangeInfo(values.portRange).name}</strong>
|
||||
<br />
|
||||
{getPortRangeInfo(values.portRange).description}
|
||||
</Box>
|
||||
</Box>
|
||||
)
|
||||
},
|
||||
{
|
||||
title: t('randomPortGenerator.options.generation.title'),
|
||||
component: (
|
||||
<Box>
|
||||
<TextFieldWithDesc
|
||||
value={values.count.toString()}
|
||||
onOwnChange={(value) => updateField('count', parseInt(value) || 5)}
|
||||
description={t(
|
||||
'randomPortGenerator.options.generation.countDescription'
|
||||
)}
|
||||
inputProps={{
|
||||
type: 'number',
|
||||
min: 1,
|
||||
max: 1000,
|
||||
'data-testid': 'count-input'
|
||||
}}
|
||||
/>
|
||||
|
||||
<CheckboxWithDesc
|
||||
title={t(
|
||||
'randomPortGenerator.options.generation.allowDuplicates.title'
|
||||
)}
|
||||
checked={values.allowDuplicates}
|
||||
onChange={(value) => updateField('allowDuplicates', value)}
|
||||
description={t(
|
||||
'randomPortGenerator.options.generation.allowDuplicates.description'
|
||||
)}
|
||||
/>
|
||||
|
||||
<CheckboxWithDesc
|
||||
title={t(
|
||||
'randomPortGenerator.options.generation.sortResults.title'
|
||||
)}
|
||||
checked={values.sortResults}
|
||||
onChange={(value) => updateField('sortResults', value)}
|
||||
description={t(
|
||||
'randomPortGenerator.options.generation.sortResults.description'
|
||||
)}
|
||||
/>
|
||||
</Box>
|
||||
)
|
||||
},
|
||||
{
|
||||
title: t('randomPortGenerator.options.output.title'),
|
||||
component: (
|
||||
<Box>
|
||||
<TextFieldWithDesc
|
||||
value={values.separator}
|
||||
onOwnChange={(value) => updateField('separator', value)}
|
||||
description={t(
|
||||
'randomPortGenerator.options.output.separatorDescription'
|
||||
)}
|
||||
inputProps={{
|
||||
'data-testid': 'separator-input'
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
];
|
||||
|
||||
return (
|
||||
<ToolContent
|
||||
title={title}
|
||||
initialValues={initialValues}
|
||||
compute={compute}
|
||||
getGroups={getGroups}
|
||||
resultComponent={
|
||||
<Box>
|
||||
{error && (
|
||||
<Alert severity="error" sx={{ mb: 2 }}>
|
||||
{error}
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
{result && (
|
||||
<ToolTextResult
|
||||
title={t('randomPortGenerator.result.title')}
|
||||
value={formattedResult}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
}
|
||||
toolInfo={{
|
||||
title: t('randomPortGenerator.info.title'),
|
||||
description:
|
||||
longDescription || t('randomPortGenerator.info.description')
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
26
src/pages/tools/number/random-port-generator/meta.ts
Normal file
26
src/pages/tools/number/random-port-generator/meta.ts
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
import { defineTool } from '@tools/defineTool';
|
||||
import { lazy } from 'react';
|
||||
|
||||
export const tool = defineTool('number', {
|
||||
i18n: {
|
||||
name: 'number:randomPortGenerator.title',
|
||||
description: 'number:randomPortGenerator.description',
|
||||
shortDescription: 'number:randomPortGenerator.shortDescription',
|
||||
longDescription: 'number:randomPortGenerator.longDescription',
|
||||
userTypes: ['developers']
|
||||
},
|
||||
path: 'random-port-generator',
|
||||
icon: 'mdi:network',
|
||||
keywords: [
|
||||
'random',
|
||||
'port',
|
||||
'generator',
|
||||
'network',
|
||||
'tcp',
|
||||
'udp',
|
||||
'server',
|
||||
'client',
|
||||
'development'
|
||||
],
|
||||
component: lazy(() => import('./index'))
|
||||
});
|
||||
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
214
src/pages/tools/number/random-port-generator/service.ts
Normal file
214
src/pages/tools/number/random-port-generator/service.ts
Normal file
|
|
@ -0,0 +1,214 @@
|
|||
import { InitialValuesType, RandomPortResult, PortRange } from './types';
|
||||
|
||||
// Standard port ranges according to IANA
|
||||
export const PORT_RANGES: Record<string, PortRange> = {
|
||||
'well-known': {
|
||||
name: 'Well-Known Ports',
|
||||
min: 1,
|
||||
max: 1023,
|
||||
description:
|
||||
'System ports (1-1023) - Reserved for common services like HTTP, HTTPS, SSH, etc.'
|
||||
},
|
||||
registered: {
|
||||
name: 'Registered Ports',
|
||||
min: 1024,
|
||||
max: 49151,
|
||||
description:
|
||||
'User ports (1024-49151) - Available for applications and services'
|
||||
},
|
||||
dynamic: {
|
||||
name: 'Dynamic Ports',
|
||||
min: 49152,
|
||||
max: 65535,
|
||||
description:
|
||||
'Private ports (49152-65535) - Available for temporary or private use'
|
||||
},
|
||||
custom: {
|
||||
name: 'Custom Range',
|
||||
min: 1,
|
||||
max: 65535,
|
||||
description: 'Custom port range - Specify your own min and max values'
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Generate random network ports within a specified range
|
||||
*/
|
||||
export function generateRandomPorts(
|
||||
options: InitialValuesType
|
||||
): RandomPortResult {
|
||||
const { portRange, minPort, maxPort, count, allowDuplicates, sortResults } =
|
||||
options;
|
||||
|
||||
// Get the appropriate port range
|
||||
const range = PORT_RANGES[portRange];
|
||||
const actualMin = portRange === 'custom' ? minPort : range.min;
|
||||
const actualMax = portRange === 'custom' ? maxPort : range.max;
|
||||
|
||||
if (actualMin >= actualMax) {
|
||||
throw new Error('Minimum port must be less than maximum port');
|
||||
}
|
||||
|
||||
if (count <= 0) {
|
||||
throw new Error('Count must be greater than 0');
|
||||
}
|
||||
|
||||
if (actualMin < 1 || actualMax > 65535) {
|
||||
throw new Error('Ports must be between 1 and 65535');
|
||||
}
|
||||
|
||||
if (!allowDuplicates && count > actualMax - actualMin + 1) {
|
||||
throw new Error(
|
||||
'Cannot generate unique ports: count exceeds available range'
|
||||
);
|
||||
}
|
||||
|
||||
const ports: number[] = [];
|
||||
|
||||
if (allowDuplicates) {
|
||||
// Generate random ports with possible duplicates
|
||||
for (let i = 0; i < count; i++) {
|
||||
const randomPort = generateRandomPort(actualMin, actualMax);
|
||||
ports.push(randomPort);
|
||||
}
|
||||
} else {
|
||||
// Generate unique random ports
|
||||
const availablePorts = new Set<number>();
|
||||
|
||||
// Create a pool of available ports
|
||||
for (let i = actualMin; i <= actualMax; i++) {
|
||||
availablePorts.add(i);
|
||||
}
|
||||
|
||||
const availableArray = Array.from(availablePorts);
|
||||
|
||||
// Shuffle the available ports
|
||||
for (let i = availableArray.length - 1; i > 0; i--) {
|
||||
const j = Math.floor(Math.random() * (i + 1));
|
||||
[availableArray[i], availableArray[j]] = [
|
||||
availableArray[j],
|
||||
availableArray[i]
|
||||
];
|
||||
}
|
||||
|
||||
// Take the first 'count' ports
|
||||
for (let i = 0; i < Math.min(count, availableArray.length); i++) {
|
||||
ports.push(availableArray[i]);
|
||||
}
|
||||
}
|
||||
|
||||
// Sort if requested
|
||||
if (sortResults) {
|
||||
ports.sort((a, b) => a - b);
|
||||
}
|
||||
|
||||
return {
|
||||
ports,
|
||||
range: {
|
||||
...range,
|
||||
min: actualMin,
|
||||
max: actualMax
|
||||
},
|
||||
count,
|
||||
hasDuplicates: !allowDuplicates && hasDuplicatesInArray(ports),
|
||||
isSorted: sortResults
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a single random port within the specified range
|
||||
*/
|
||||
function generateRandomPort(min: number, max: number): number {
|
||||
return Math.floor(Math.random() * (max - min + 1)) + min;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if an array has duplicate values
|
||||
*/
|
||||
function hasDuplicatesInArray(arr: number[]): boolean {
|
||||
const seen = new Set<number>();
|
||||
for (const num of arr) {
|
||||
if (seen.has(num)) {
|
||||
return true;
|
||||
}
|
||||
seen.add(num);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Format ports for display
|
||||
*/
|
||||
export function formatPorts(ports: number[], separator: string): string {
|
||||
return ports.map((port) => port.toString()).join(separator);
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate input parameters
|
||||
*/
|
||||
export function validateInput(options: InitialValuesType): string | null {
|
||||
const { portRange, minPort, maxPort, count } = options;
|
||||
|
||||
if (count <= 0) {
|
||||
return 'Count must be greater than 0';
|
||||
}
|
||||
|
||||
if (count > 1000) {
|
||||
return 'Count cannot exceed 1,000';
|
||||
}
|
||||
|
||||
if (portRange === 'custom') {
|
||||
if (minPort >= maxPort) {
|
||||
return 'Minimum port must be less than maximum port';
|
||||
}
|
||||
|
||||
if (minPort < 1 || maxPort > 65535) {
|
||||
return 'Ports must be between 1 and 65535';
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get port range information
|
||||
*/
|
||||
export function getPortRangeInfo(portRange: string): PortRange {
|
||||
return PORT_RANGES[portRange] || PORT_RANGES['custom'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a port is commonly used
|
||||
*/
|
||||
export function isCommonPort(port: number): boolean {
|
||||
const commonPorts = [
|
||||
20, 21, 22, 23, 25, 53, 80, 110, 143, 443, 993, 995, 3306, 5432, 6379, 8080
|
||||
];
|
||||
return commonPorts.includes(port);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get port service information
|
||||
*/
|
||||
export function getPortService(port: number): string {
|
||||
const portServices: Record<number, string> = {
|
||||
20: 'FTP Data',
|
||||
21: 'FTP Control',
|
||||
22: 'SSH',
|
||||
23: 'Telnet',
|
||||
25: 'SMTP',
|
||||
53: 'DNS',
|
||||
80: 'HTTP',
|
||||
110: 'POP3',
|
||||
143: 'IMAP',
|
||||
443: 'HTTPS',
|
||||
993: 'IMAPS',
|
||||
995: 'POP3S',
|
||||
3306: 'MySQL',
|
||||
5432: 'PostgreSQL',
|
||||
6379: 'Redis',
|
||||
8080: 'HTTP Alternative'
|
||||
};
|
||||
|
||||
return portServices[port] || 'Unknown';
|
||||
}
|
||||
24
src/pages/tools/number/random-port-generator/types.ts
Normal file
24
src/pages/tools/number/random-port-generator/types.ts
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
export type InitialValuesType = {
|
||||
portRange: 'well-known' | 'registered' | 'dynamic' | 'custom';
|
||||
minPort: number;
|
||||
maxPort: number;
|
||||
count: number;
|
||||
allowDuplicates: boolean;
|
||||
sortResults: boolean;
|
||||
separator: string;
|
||||
};
|
||||
|
||||
export type PortRange = {
|
||||
name: string;
|
||||
min: number;
|
||||
max: number;
|
||||
description: string;
|
||||
};
|
||||
|
||||
export type RandomPortResult = {
|
||||
ports: number[];
|
||||
range: PortRange;
|
||||
count: number;
|
||||
hasDuplicates: boolean;
|
||||
isSorted: boolean;
|
||||
};
|
||||
|
|
@ -11,6 +11,7 @@ export const tool = defineTool('number', {
|
|||
i18n: {
|
||||
name: 'number:sum.title',
|
||||
description: 'number:sum.description',
|
||||
shortDescription: 'number:sum.shortDescription'
|
||||
shortDescription: 'number:sum.shortDescription',
|
||||
userTypes: ['generalUsers']
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -19,11 +19,11 @@ export const tool = defineTool('pdf', {
|
|||
'browser',
|
||||
'webassembly'
|
||||
],
|
||||
|
||||
component: lazy(() => import('./index')),
|
||||
i18n: {
|
||||
name: 'pdf:compressPdf.title',
|
||||
description: 'pdf:compressPdf.description',
|
||||
shortDescription: 'pdf:compressPdf.shortDescription'
|
||||
shortDescription: 'pdf:compressPdf.shortDescription',
|
||||
userTypes: ['generalUsers']
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -5,7 +5,8 @@ export const tool = defineTool('pdf', {
|
|||
i18n: {
|
||||
name: 'pdf:editor.title',
|
||||
description: 'pdf:editor.description',
|
||||
shortDescription: 'pdf:editor.shortDescription'
|
||||
shortDescription: 'pdf:editor.shortDescription',
|
||||
userTypes: ['generalUsers']
|
||||
},
|
||||
|
||||
path: 'editor',
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ export const meta = defineTool('pdf', {
|
|||
i18n: {
|
||||
name: 'pdf:mergePdf.title',
|
||||
description: 'pdf:mergePdf.description',
|
||||
shortDescription: 'pdf:mergePdf.shortDescription'
|
||||
shortDescription: 'pdf:mergePdf.shortDescription',
|
||||
userTypes: ['generalUsers']
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ export const meta = defineTool('pdf', {
|
|||
i18n: {
|
||||
name: 'pdf:pdfToEpub.title',
|
||||
description: 'pdf:pdfToEpub.description',
|
||||
shortDescription: 'pdf:pdfToEpub.shortDescription'
|
||||
shortDescription: 'pdf:pdfToEpub.shortDescription',
|
||||
userTypes: ['generalUsers']
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -6,13 +6,13 @@ export const tool = defineTool('pdf', {
|
|||
name: 'pdf:pdfToPng.title',
|
||||
description: 'pdf:pdfToPng.description',
|
||||
shortDescription: 'pdf:pdfToPng.shortDescription',
|
||||
longDescription: 'pdf:pdfToPng.longDescription'
|
||||
longDescription: 'pdf:pdfToPng.longDescription',
|
||||
userTypes: ['generalUsers']
|
||||
},
|
||||
|
||||
path: 'pdf-to-png',
|
||||
icon: 'mdi:image-multiple', // Iconify icon ID
|
||||
|
||||
keywords: ['pdf', 'png', 'convert', 'image', 'extract', 'pages'],
|
||||
|
||||
component: lazy(() => import('./index'))
|
||||
});
|
||||
|
|
|
|||
|
|
@ -18,11 +18,11 @@ export const tool = defineTool('pdf', {
|
|||
'browser',
|
||||
'encryption'
|
||||
],
|
||||
|
||||
component: lazy(() => import('./index')),
|
||||
i18n: {
|
||||
name: 'pdf:protectPdf.title',
|
||||
description: 'pdf:protectPdf.description',
|
||||
shortDescription: 'pdf:protectPdf.shortDescription'
|
||||
shortDescription: 'pdf:protectPdf.shortDescription',
|
||||
userTypes: ['generalUsers']
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -6,13 +6,13 @@ export const tool = defineTool('pdf', {
|
|||
name: 'pdf:rotatePdf.title',
|
||||
description: 'pdf:rotatePdf.description',
|
||||
shortDescription: 'pdf:rotatePdf.shortDescription',
|
||||
longDescription: 'pdf:rotatePdf.longDescription'
|
||||
longDescription: 'pdf:rotatePdf.longDescription',
|
||||
userTypes: ['generalUsers']
|
||||
},
|
||||
|
||||
path: 'rotate-pdf',
|
||||
icon: 'carbon:rotate',
|
||||
|
||||
keywords: ['pdf', 'rotate', 'rotation', 'document', 'pages', 'orientation'],
|
||||
|
||||
component: lazy(() => import('./index'))
|
||||
});
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ export const meta = defineTool('pdf', {
|
|||
i18n: {
|
||||
name: 'pdf:splitPdf.title',
|
||||
description: 'pdf:splitPdf.description',
|
||||
shortDescription: 'pdf:splitPdf.shortDescription'
|
||||
shortDescription: 'pdf:splitPdf.shortDescription',
|
||||
userTypes: ['generalUsers']
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ export const tool = defineTool('string', {
|
|||
i18n: {
|
||||
name: 'string:base64.title',
|
||||
description: 'string:base64.description',
|
||||
shortDescription: 'string:base64.shortDescription'
|
||||
shortDescription: 'string:base64.shortDescription',
|
||||
userTypes: ['generalUsers', 'developers']
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ export const tool = defineTool('string', {
|
|||
i18n: {
|
||||
name: 'string:censor.title',
|
||||
description: 'string:censor.description',
|
||||
shortDescription: 'string:censor.shortDescription'
|
||||
shortDescription: 'string:censor.shortDescription',
|
||||
userTypes: ['generalUsers']
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ export const tool = defineTool('string', {
|
|||
i18n: {
|
||||
name: 'string:createPalindrome.title',
|
||||
description: 'string:createPalindrome.description',
|
||||
shortDescription: 'string:createPalindrome.shortDescription'
|
||||
shortDescription: 'string:createPalindrome.shortDescription',
|
||||
userTypes: ['generalUsers']
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ export const tool = defineTool('string', {
|
|||
i18n: {
|
||||
name: 'string:extractSubstring.title',
|
||||
description: 'string:extractSubstring.description',
|
||||
shortDescription: 'string:extractSubstring.shortDescription'
|
||||
shortDescription: 'string:extractSubstring.shortDescription',
|
||||
userTypes: ['generalUsers', 'developers']
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -3,13 +3,15 @@ import { lazy } from 'react';
|
|||
|
||||
export const tool = defineTool('string', {
|
||||
path: 'join',
|
||||
|
||||
icon: 'material-symbols-light:join',
|
||||
|
||||
keywords: ['join'],
|
||||
keywords: ['text', 'join'],
|
||||
component: lazy(() => import('./index')),
|
||||
i18n: {
|
||||
name: 'string:join.title',
|
||||
description: 'string:join.description',
|
||||
shortDescription: 'string:join.shortDescription'
|
||||
shortDescription: 'string:join.shortDescription',
|
||||
userTypes: ['generalUsers']
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ export const tool = defineTool('string', {
|
|||
i18n: {
|
||||
name: 'string:palindrome.title',
|
||||
description: 'string:palindrome.description',
|
||||
shortDescription: 'string:palindrome.shortDescription'
|
||||
shortDescription: 'string:palindrome.shortDescription',
|
||||
userTypes: ['generalUsers']
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ export const tool = defineTool('string', {
|
|||
i18n: {
|
||||
name: 'string:quote.title',
|
||||
description: 'string:quote.description',
|
||||
shortDescription: 'string:quote.shortDescription'
|
||||
shortDescription: 'string:quote.shortDescription',
|
||||
userTypes: ['generalUsers', 'developers']
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ export const tool = defineTool('string', {
|
|||
i18n: {
|
||||
name: 'string:randomizeCase.title',
|
||||
description: 'string:randomizeCase.description',
|
||||
shortDescription: 'string:randomizeCase.shortDescription'
|
||||
shortDescription: 'string:randomizeCase.shortDescription',
|
||||
userTypes: ['generalUsers']
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ export const tool = defineTool('string', {
|
|||
i18n: {
|
||||
name: 'string:removeDuplicateLines.title',
|
||||
description: 'string:removeDuplicateLines.description',
|
||||
shortDescription: 'string:removeDuplicateLines.shortDescription'
|
||||
shortDescription: 'string:removeDuplicateLines.shortDescription',
|
||||
userTypes: ['generalUsers', 'developers']
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ export const tool = defineTool('string', {
|
|||
i18n: {
|
||||
name: 'string:repeat.title',
|
||||
description: 'string:repeat.description',
|
||||
shortDescription: 'string:repeat.shortDescription'
|
||||
shortDescription: 'string:repeat.shortDescription',
|
||||
userTypes: ['generalUsers', 'developers']
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ export const tool = defineTool('string', {
|
|||
i18n: {
|
||||
name: 'string:reverse.title',
|
||||
description: 'string:reverse.description',
|
||||
shortDescription: 'string:reverse.shortDescription'
|
||||
shortDescription: 'string:reverse.shortDescription',
|
||||
userTypes: ['generalUsers', 'developers']
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -6,7 +6,8 @@ export const tool = defineTool('string', {
|
|||
i18n: {
|
||||
name: 'string:rot13.title',
|
||||
description: 'string:rot13.description',
|
||||
shortDescription: 'string:rot13.shortDescription'
|
||||
shortDescription: 'string:rot13.shortDescription',
|
||||
userTypes: ['generalUsers', 'developers']
|
||||
},
|
||||
|
||||
path: 'rot13',
|
||||
|
|
|
|||
|
|
@ -6,7 +6,8 @@ export const tool = defineTool('string', {
|
|||
i18n: {
|
||||
name: 'string:rotate.title',
|
||||
description: 'string:rotate.description',
|
||||
shortDescription: 'string:rotate.shortDescription'
|
||||
shortDescription: 'string:rotate.shortDescription',
|
||||
userTypes: ['generalUsers', 'developers']
|
||||
},
|
||||
|
||||
path: 'rotate',
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import { lazy } from 'react';
|
|||
|
||||
export const tool = defineTool('string', {
|
||||
path: 'split',
|
||||
|
||||
icon: 'material-symbols-light:call-split',
|
||||
|
||||
keywords: ['split'],
|
||||
|
|
@ -10,6 +11,7 @@ export const tool = defineTool('string', {
|
|||
i18n: {
|
||||
name: 'string:split.title',
|
||||
description: 'string:split.description',
|
||||
shortDescription: 'string:split.shortDescription'
|
||||
shortDescription: 'string:split.shortDescription',
|
||||
userTypes: ['generalUsers', 'developers']
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ export const tool = defineTool('string', {
|
|||
i18n: {
|
||||
name: 'string:statistic.title',
|
||||
description: 'string:statistic.description',
|
||||
shortDescription: 'string:statistic.shortDescription'
|
||||
shortDescription: 'string:statistic.shortDescription',
|
||||
userTypes: ['generalUsers', 'developers']
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -5,7 +5,8 @@ export const tool = defineTool('string', {
|
|||
i18n: {
|
||||
name: 'string:textReplacer.title',
|
||||
description: 'string:textReplacer.description',
|
||||
shortDescription: 'string:textReplacer.shortDescription'
|
||||
shortDescription: 'string:textReplacer.shortDescription',
|
||||
userTypes: ['generalUsers', 'developers']
|
||||
},
|
||||
|
||||
path: 'replacer',
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ export const tool = defineTool('string', {
|
|||
i18n: {
|
||||
name: 'string:toMorse.title',
|
||||
description: 'string:toMorse.description',
|
||||
shortDescription: 'string:toMorse.shortDescription'
|
||||
shortDescription: 'string:toMorse.shortDescription',
|
||||
userTypes: ['generalUsers', 'developers']
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ export const tool = defineTool('string', {
|
|||
i18n: {
|
||||
name: 'string:truncate.title',
|
||||
description: 'string:truncate.description',
|
||||
shortDescription: 'string:truncate.shortDescription'
|
||||
shortDescription: 'string:truncate.shortDescription',
|
||||
userTypes: ['generalUsers', 'developers']
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ export const tool = defineTool('string', {
|
|||
i18n: {
|
||||
name: 'string:uppercase.title',
|
||||
description: 'string:uppercase.description',
|
||||
shortDescription: 'string:uppercase.shortDescription'
|
||||
shortDescription: 'string:uppercase.shortDescription',
|
||||
userTypes: ['generalUsers', 'developers']
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ export const tool = defineTool('time', {
|
|||
i18n: {
|
||||
name: 'time:checkLeapYears.title',
|
||||
description: 'time:checkLeapYears.description',
|
||||
shortDescription: 'time:checkLeapYears.shortDescription'
|
||||
shortDescription: 'time:checkLeapYears.shortDescription',
|
||||
userTypes: ['generalUsers']
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ export const tool = defineTool('time', {
|
|||
i18n: {
|
||||
name: 'time:convertDaysToHours.title',
|
||||
description: 'time:convertDaysToHours.description',
|
||||
shortDescription: 'time:convertDaysToHours.shortDescription'
|
||||
shortDescription: 'time:convertDaysToHours.shortDescription',
|
||||
userTypes: ['generalUsers']
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ export const tool = defineTool('time', {
|
|||
i18n: {
|
||||
name: 'time:convertHoursToDays.title',
|
||||
description: 'time:convertHoursToDays.description',
|
||||
shortDescription: 'time:convertHoursToDays.shortDescription'
|
||||
shortDescription: 'time:convertHoursToDays.shortDescription',
|
||||
userTypes: ['generalUsers']
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ export const tool = defineTool('time', {
|
|||
i18n: {
|
||||
name: 'time:convertSecondsToTime.title',
|
||||
description: 'time:convertSecondsToTime.description',
|
||||
shortDescription: 'time:convertSecondsToTime.shortDescription'
|
||||
shortDescription: 'time:convertSecondsToTime.shortDescription',
|
||||
userTypes: ['generalUsers']
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -5,11 +5,12 @@ export const tool = defineTool('time', {
|
|||
path: 'convert-time-to-seconds',
|
||||
icon: 'material-symbols:schedule',
|
||||
|
||||
keywords: ['time', 'seconds', 'convert', 'format'],
|
||||
keywords: ['time', 'seconds', 'convert', 'format', 'HH:MM:SS'],
|
||||
component: lazy(() => import('./index')),
|
||||
i18n: {
|
||||
name: 'time:convertTimeToSeconds.title',
|
||||
description: 'time:convertTimeToSeconds.description',
|
||||
shortDescription: 'time:convertTimeToSeconds.shortDescription'
|
||||
shortDescription: 'time:convertTimeToSeconds.shortDescription',
|
||||
userTypes: ['generalUsers', 'developers']
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -5,8 +5,7 @@ export const tool = defineTool('time', {
|
|||
i18n: {
|
||||
name: 'time:convertUnixToDate.title',
|
||||
description: 'time:convertUnixToDate.description',
|
||||
shortDescription: 'time:convertUnixToDate.shortDescription',
|
||||
longDescription: 'time:convertUnixToDate.longDescription'
|
||||
shortDescription: 'time:convertUnixToDate.shortDescription'
|
||||
},
|
||||
path: 'convert-unix-to-date',
|
||||
icon: 'material-symbols:schedule',
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue