Merge remote-tracking branch 'origin/main' into string-join

# Conflicts:
#	src/pages/string/join/index.tsx
This commit is contained in:
Ibrahima G. Coulibaly
2024-06-25 07:55:21 +01:00
46 changed files with 1733 additions and 320 deletions

View File

@@ -42,6 +42,8 @@
"tailwindcss/classnames-order": "warn",
"tailwindcss/no-custom-classname": "warn",
"tailwindcss/no-contradicting-classname": "error",
"@typescript-eslint/ban-types": "off"
"@typescript-eslint/ban-types": "off",
"@typescript-eslint/ban-ts-comment": "off",
"@typescript-eslint/no-explicit-any": "off"
}
}

View File

@@ -3,15 +3,14 @@ name: CI
on:
push:
branches:
- main # or the branch you want to trigger the workflow on
- main
pull_request:
branches:
- main
jobs:
build-and-deploy:
build-and-test:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v3
@@ -19,7 +18,7 @@ jobs:
- name: Set up Node.js
uses: actions/setup-node@v3
with:
node-version: '18' # Specify the Node.js version you want to use
node-version: '18'
- name: Install dependencies
run: npm install
@@ -30,6 +29,25 @@ jobs:
- name: Build project
run: npm run build
deploy:
if: github.ref == 'refs/heads/main'
needs: build-and-test
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Set up Node.js
uses: actions/setup-node@v3
with:
node-version: '18'
- name: Install dependencies
run: npm install
- name: Build project
run: npm run build
- name: Deploy to Netlify
uses: nwtgck/actions-netlify@v1.2
with:
@@ -42,4 +60,4 @@ jobs:
env:
NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }}
NETLIFY_SITE_ID: ${{ secrets.NETLIFY_SITE_ID }}
timeout-minutes: 1
timeout-minutes: 20

370
.idea/workspace.xml generated
View File

@@ -4,12 +4,10 @@
<option name="autoReloadType" value="SELECTIVE" />
</component>
<component name="ChangeListManager">
<list default="true" id="b30e2810-c4c1-4aad-b134-794e52cc1c7d" name="Changes" comment="fix: readme">
<list default="true" id="b30e2810-c4c1-4aad-b134-794e52cc1c7d" name="Changes" comment="feat: create transparent png">
<change beforePath="$PROJECT_DIR$/.idea/workspace.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/workspace.xml" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/components/Navbar/index.tsx" beforeDir="false" afterPath="$PROJECT_DIR$/src/components/Navbar/index.tsx" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/pages/image/png/change-colors-in-png/meta.ts" beforeDir="false" afterPath="$PROJECT_DIR$/src/pages/image/png/change-colors-in-png/meta.ts" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/pages/string/join/meta.ts" beforeDir="false" afterPath="$PROJECT_DIR$/src/pages/string/join/meta.ts" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/pages/string/split/meta.ts" beforeDir="false" afterPath="$PROJECT_DIR$/src/pages/string/split/meta.ts" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/pages/image/png/create-transparent/meta.ts" beforeDir="false" afterPath="$PROJECT_DIR$/src/pages/image/png/create-transparent/meta.ts" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/pages/image/png/create-transparent/service.ts" beforeDir="false" />
</list>
<option name="SHOW_DIALOG" value="false" />
<option name="HIGHLIGHT_CONFLICTS" value="true" />
@@ -38,37 +36,39 @@
<option name="hideEmptyMiddlePackages" value="true" />
<option name="showLibraryContents" value="true" />
</component>
<component name="PropertiesComponent"><![CDATA[{
"keyToString": {
"ASKED_ADD_EXTERNAL_FILES": "true",
"ASKED_SHARE_PROJECT_CONFIGURATION_FILES": "true",
"RunOnceActivity.OpenProjectViewOnStart": "true",
"RunOnceActivity.ShowReadmeOnStart": "true",
"Vitest.mergeText.executor": "Run",
"Vitest.mergeText.should merge lines and preserve blank lines when deleteBlankLines is false.executor": "Run",
"Vitest.mergeText.should merge lines, preserve blank lines and trailing spaces when both deleteBlankLines and deleteTrailingSpaces are false.executor": "Run",
"git-widget-placeholder": "main",
"ignore.virus.scanning.warn.message": "true",
"kotlin-language-version-configured": "true",
"last_opened_file_path": "C:/Users/HP/IdeaProjects/omni-tools/src/pages/string",
"node.js.detected.package.eslint": "true",
"node.js.detected.package.tslint": "true",
"node.js.selected.package.eslint": "(autodetect)",
"node.js.selected.package.tslint": "(autodetect)",
"nodejs_package_manager_path": "npm",
"npm.dev.executor": "Run",
"npm.prebuild.executor": "Run",
"npm.script:create:tool.executor": "Run",
"npm.test.executor": "Run",
"prettierjs.PrettierConfiguration.Package": "C:\\Users\\HP\\IdeaProjects\\omni-tools\\node_modules\\prettier",
"project.structure.last.edited": "Problems",
"project.structure.proportion": "0.0",
"project.structure.side.proportion": "0.2",
"settings.editor.selected.configurable": "settings.typescriptcompiler",
"ts.external.directory.path": "C:\\Users\\HP\\IdeaProjects\\omni-tools\\node_modules\\typescript\\lib",
"vue.rearranger.settings.migration": "true"
<component name="PropertiesComponent">{
&quot;keyToString&quot;: {
&quot;ASKED_ADD_EXTERNAL_FILES&quot;: &quot;true&quot;,
&quot;ASKED_SHARE_PROJECT_CONFIGURATION_FILES&quot;: &quot;true&quot;,
&quot;RunOnceActivity.OpenProjectViewOnStart&quot;: &quot;true&quot;,
&quot;RunOnceActivity.ShowReadmeOnStart&quot;: &quot;true&quot;,
&quot;Vitest.compute function (1).executor&quot;: &quot;Run&quot;,
&quot;Vitest.compute function.executor&quot;: &quot;Run&quot;,
&quot;Vitest.mergeText.executor&quot;: &quot;Run&quot;,
&quot;Vitest.mergeText.should merge lines and preserve blank lines when deleteBlankLines is false.executor&quot;: &quot;Run&quot;,
&quot;Vitest.mergeText.should merge lines, preserve blank lines and trailing spaces when both deleteBlankLines and deleteTrailingSpaces are false.executor&quot;: &quot;Run&quot;,
&quot;git-widget-placeholder&quot;: &quot;main&quot;,
&quot;ignore.virus.scanning.warn.message&quot;: &quot;true&quot;,
&quot;kotlin-language-version-configured&quot;: &quot;true&quot;,
&quot;last_opened_file_path&quot;: &quot;C:/Users/HP/IdeaProjects/omni-tools/src/assets&quot;,
&quot;node.js.detected.package.eslint&quot;: &quot;true&quot;,
&quot;node.js.detected.package.tslint&quot;: &quot;true&quot;,
&quot;node.js.selected.package.eslint&quot;: &quot;(autodetect)&quot;,
&quot;node.js.selected.package.tslint&quot;: &quot;(autodetect)&quot;,
&quot;nodejs_package_manager_path&quot;: &quot;npm&quot;,
&quot;npm.dev.executor&quot;: &quot;Run&quot;,
&quot;npm.prebuild.executor&quot;: &quot;Run&quot;,
&quot;npm.script:create:tool.executor&quot;: &quot;Run&quot;,
&quot;npm.test.executor&quot;: &quot;Run&quot;,
&quot;prettierjs.PrettierConfiguration.Package&quot;: &quot;C:\\Users\\HP\\IdeaProjects\\omni-tools\\node_modules\\prettier&quot;,
&quot;project.structure.last.edited&quot;: &quot;Problems&quot;,
&quot;project.structure.proportion&quot;: &quot;0.0&quot;,
&quot;project.structure.side.proportion&quot;: &quot;0.2&quot;,
&quot;settings.editor.selected.configurable&quot;: &quot;settings.typescriptcompiler&quot;,
&quot;ts.external.directory.path&quot;: &quot;C:\\Users\\HP\\IdeaProjects\\omni-tools\\node_modules\\typescript\\lib&quot;,
&quot;vue.rearranger.settings.migration&quot;: &quot;true&quot;
}
}]]></component>
}</component>
<component name="ReactDesignerToolWindowState">
<option name="myId2Visible">
<map>
@@ -80,11 +80,11 @@
</component>
<component name="RecentsManager">
<key name="CopyFile.RECENT_KEYS">
<recent name="C:\Users\HP\IdeaProjects\omni-tools\src\assets" />
<recent name="C:\Users\HP\IdeaProjects\omni-tools\src\pages\string" />
<recent name="C:\Users\HP\IdeaProjects\omni-tools\src\pages\string\split" />
<recent name="C:\Users\HP\IdeaProjects\omni-tools\src\pages\images" />
<recent name="C:\Users\HP\IdeaProjects\omni-tools\public" />
<recent name="C:\Users\HP\IdeaProjects\omni-tools\src\assets" />
</key>
<key name="MoveFile.RECENT_KEYS">
<recent name="C:\Users\HP\IdeaProjects\omni-tools\src\components\options" />
@@ -92,7 +92,33 @@
<recent name="C:\Users\HP\IdeaProjects\omni-tools\src\tools" />
</key>
</component>
<component name="RunManager" selected="npm.test">
<component name="RunManager" selected="npm.dev">
<configuration name="compute function (1)" type="JavaScriptTestRunnerVitest" temporary="true" nameIsGenerated="true">
<node-interpreter value="project" />
<vitest-package value="$PROJECT_DIR$/node_modules/vitest" />
<working-dir value="$PROJECT_DIR$" />
<vitest-options value="--run" />
<envs />
<scope-kind value="SUITE" />
<test-file value="$PROJECT_DIR$/src/pages/number/sum/sum.service.test.ts" />
<test-names>
<test-name value="compute function" />
</test-names>
<method v="2" />
</configuration>
<configuration name="compute function" type="JavaScriptTestRunnerVitest" temporary="true" nameIsGenerated="true">
<node-interpreter value="project" />
<vitest-package value="$PROJECT_DIR$/node_modules/vitest" />
<working-dir value="$PROJECT_DIR$" />
<vitest-options value="--run" />
<envs />
<scope-kind value="SUITE" />
<test-file value="$PROJECT_DIR$/src/pages/string/to-morse/to-morse.service.test.ts" />
<test-names>
<test-name value="compute function" />
</test-names>
<method v="2" />
</configuration>
<configuration name="dev" type="js.build_tools.npm" temporary="true" nameIsGenerated="true">
<package-json value="$PROJECT_DIR$/package.json" />
<command value="run" />
@@ -103,16 +129,6 @@
<envs />
<method v="2" />
</configuration>
<configuration name="prebuild" type="js.build_tools.npm" temporary="true" nameIsGenerated="true">
<package-json value="$PROJECT_DIR$/package.json" />
<command value="run" />
<scripts>
<script value="prebuild" />
</scripts>
<node-interpreter value="project" />
<envs />
<method v="2" />
</configuration>
<configuration name="script:create:tool" type="js.build_tools.npm" temporary="true" nameIsGenerated="true">
<package-json value="$PROJECT_DIR$/package.json" />
<command value="run" />
@@ -135,10 +151,11 @@
</configuration>
<recent_temporary>
<list>
<item itemvalue="npm.dev" />
<item itemvalue="Vitest.compute function (1)" />
<item itemvalue="Vitest.compute function" />
<item itemvalue="npm.test" />
<item itemvalue="npm.script:create:tool" />
<item itemvalue="npm.dev" />
<item itemvalue="npm.prebuild" />
</list>
</recent_temporary>
</component>
@@ -162,71 +179,10 @@
<workItem from="1719092003308" duration="14856000" />
<workItem from="1719164664347" duration="2033000" />
<workItem from="1719166718305" duration="1783000" />
<workItem from="1719168519203" duration="3583000" />
</task>
<task id="LOCAL-00001" summary="feat: use vite and ts">
<option name="closed" value="true" />
<created>1718816900959</created>
<option name="number" value="00001" />
<option name="presentableId" value="LOCAL-00001" />
<option name="project" value="LOCAL" />
<updated>1718816900959</updated>
</task>
<task id="LOCAL-00002" summary="feat: initial commit">
<option name="closed" value="true" />
<created>1718820618078</created>
<option name="number" value="00002" />
<option name="presentableId" value="LOCAL-00002" />
<option name="project" value="LOCAL" />
<updated>1718820618078</updated>
</task>
<task id="LOCAL-00003" summary="feat: initial commit">
<option name="closed" value="true" />
<created>1718821853210</created>
<option name="number" value="00003" />
<option name="presentableId" value="LOCAL-00003" />
<option name="project" value="LOCAL" />
<updated>1718821853210</updated>
</task>
<task id="LOCAL-00004" summary="feat: example tools">
<option name="closed" value="true" />
<created>1718822309352</created>
<option name="number" value="00004" />
<option name="presentableId" value="LOCAL-00004" />
<option name="project" value="LOCAL" />
<updated>1718822309352</updated>
</task>
<task id="LOCAL-00005" summary="feat: Split string">
<option name="closed" value="true" />
<created>1718828318468</created>
<option name="number" value="00005" />
<option name="presentableId" value="LOCAL-00005" />
<option name="project" value="LOCAL" />
<updated>1718828318468</updated>
</task>
<task id="LOCAL-00006" summary="fix: netlify">
<option name="closed" value="true" />
<created>1718831745853</created>
<option name="number" value="00006" />
<option name="presentableId" value="LOCAL-00006" />
<option name="project" value="LOCAL" />
<updated>1718831745853</updated>
</task>
<task id="LOCAL-00007" summary="fix: netlify">
<option name="closed" value="true" />
<created>1718833519062</created>
<option name="number" value="00007" />
<option name="presentableId" value="LOCAL-00007" />
<option name="project" value="LOCAL" />
<updated>1718833519062</updated>
</task>
<task id="LOCAL-00008" summary="fix: index title">
<option name="closed" value="true" />
<created>1718836910916</created>
<option name="number" value="00008" />
<option name="presentableId" value="LOCAL-00008" />
<option name="project" value="LOCAL" />
<updated>1718836910916</updated>
<workItem from="1719168519203" duration="17675000" />
<workItem from="1719197816332" duration="1453000" />
<workItem from="1719273044735" duration="9847000" />
<workItem from="1719294110005" duration="3842000" />
</task>
<task id="LOCAL-00009" summary="chore: split string tools ui">
<option name="closed" value="true" />
@@ -468,7 +424,159 @@
<option name="project" value="LOCAL" />
<updated>1719171905785</updated>
</task>
<option name="localTasksCounter" value="39" />
<task id="LOCAL-00039" summary="fix: build">
<option name="closed" value="true" />
<created>1719172147719</created>
<option name="number" value="00039" />
<option name="presentableId" value="LOCAL-00039" />
<option name="project" value="LOCAL" />
<updated>1719172147719</updated>
</task>
<task id="LOCAL-00040" summary="chore: CODEOWNERS">
<option name="closed" value="true" />
<created>1719184899883</created>
<option name="number" value="00040" />
<option name="presentableId" value="LOCAL-00040" />
<option name="project" value="LOCAL" />
<updated>1719184899883</updated>
</task>
<task id="LOCAL-00041" summary="ci: fix">
<option name="closed" value="true" />
<created>1719185893875</created>
<option name="number" value="00041" />
<option name="presentableId" value="LOCAL-00041" />
<option name="project" value="LOCAL" />
<updated>1719185893875</updated>
</task>
<task id="LOCAL-00042" summary="ci: fix">
<option name="closed" value="true" />
<created>1719186106818</created>
<option name="number" value="00042" />
<option name="presentableId" value="LOCAL-00042" />
<option name="project" value="LOCAL" />
<updated>1719186106818</updated>
</task>
<task id="LOCAL-00043" summary="fix: create-tool.mjs">
<option name="closed" value="true" />
<created>1719187088285</created>
<option name="number" value="00043" />
<option name="presentableId" value="LOCAL-00043" />
<option name="project" value="LOCAL" />
<updated>1719187088285</updated>
</task>
<task id="LOCAL-00044" summary="fix: readme">
<option name="closed" value="true" />
<created>1719187190823</created>
<option name="number" value="00044" />
<option name="presentableId" value="LOCAL-00044" />
<option name="project" value="LOCAL" />
<updated>1719187190823</updated>
</task>
<task id="LOCAL-00045" summary="fix: create-tool.mjs">
<option name="closed" value="true" />
<created>1719188162583</created>
<option name="number" value="00045" />
<option name="presentableId" value="LOCAL-00045" />
<option name="project" value="LOCAL" />
<updated>1719188162583</updated>
</task>
<task id="LOCAL-00046" summary="feat: change colors in png init">
<option name="closed" value="true" />
<created>1719193884293</created>
<option name="number" value="00046" />
<option name="presentableId" value="LOCAL-00046" />
<option name="project" value="LOCAL" />
<updated>1719193884293</updated>
</task>
<task id="LOCAL-00047" summary="feat: change colors in png">
<option name="closed" value="true" />
<created>1719197875189</created>
<option name="number" value="00047" />
<option name="presentableId" value="LOCAL-00047" />
<option name="project" value="LOCAL" />
<updated>1719197875189</updated>
</task>
<task id="LOCAL-00048" summary="chore: ResultFooter">
<option name="closed" value="true" />
<created>1719274243788</created>
<option name="number" value="00048" />
<option name="presentableId" value="LOCAL-00048" />
<option name="project" value="LOCAL" />
<updated>1719274243788</updated>
</task>
<task id="LOCAL-00049" summary="feat: change color in png finished">
<option name="closed" value="true" />
<created>1719275214988</created>
<option name="number" value="00049" />
<option name="presentableId" value="LOCAL-00049" />
<option name="project" value="LOCAL" />
<updated>1719275214988</updated>
</task>
<task id="LOCAL-00050" summary="feat: string to morse">
<option name="closed" value="true" />
<created>1719277679968</created>
<option name="number" value="00050" />
<option name="presentableId" value="LOCAL-00050" />
<option name="project" value="LOCAL" />
<updated>1719277679969</updated>
</task>
<task id="LOCAL-00051" summary="feat: sum numbers init">
<option name="closed" value="true" />
<created>1719281510362</created>
<option name="number" value="00051" />
<option name="presentableId" value="LOCAL-00051" />
<option name="project" value="LOCAL" />
<updated>1719281510362</updated>
</task>
<task id="LOCAL-00052" summary="fix: compilation">
<option name="closed" value="true" />
<created>1719281605998</created>
<option name="number" value="00052" />
<option name="presentableId" value="LOCAL-00052" />
<option name="project" value="LOCAL" />
<updated>1719281605999</updated>
</task>
<task id="LOCAL-00053" summary="chore: printRunningSum">
<option name="closed" value="true" />
<created>1719282009150</created>
<option name="number" value="00053" />
<option name="presentableId" value="LOCAL-00053" />
<option name="project" value="LOCAL" />
<updated>1719282009150</updated>
</task>
<task id="LOCAL-00054" summary="chore: sum tests">
<option name="closed" value="true" />
<created>1719282131977</created>
<option name="number" value="00054" />
<option name="presentableId" value="LOCAL-00054" />
<option name="project" value="LOCAL" />
<updated>1719282131977</updated>
</task>
<task id="LOCAL-00055" summary="fix: readme">
<option name="closed" value="true" />
<created>1719283122691</created>
<option name="number" value="00055" />
<option name="presentableId" value="LOCAL-00055" />
<option name="project" value="LOCAL" />
<updated>1719283122691</updated>
</task>
<task id="LOCAL-00056" summary="refactor: tool input and result">
<option name="closed" value="true" />
<created>1719296145698</created>
<option name="number" value="00056" />
<option name="presentableId" value="LOCAL-00056" />
<option name="project" value="LOCAL" />
<updated>1719296145699</updated>
</task>
<task id="LOCAL-00057" summary="feat: create transparent png">
<option name="closed" value="true" />
<created>1719297880629</created>
<option name="number" value="00057" />
<option name="presentableId" value="LOCAL-00057" />
<option name="project" value="LOCAL" />
<updated>1719297880629</updated>
</task>
<option name="localTasksCounter" value="58" />
<servers />
</component>
<component name="TypeScriptGeneratedFilesManager">
@@ -489,21 +597,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: use formik" />
<MESSAGE value="chore: output selector" />
<MESSAGE value="feat: text split" />
<MESSAGE value="fix: text split try catch" />
<MESSAGE value="ubf" />
<MESSAGE value="ubf jn" />
<MESSAGE value="feat: conventional commit" />
<MESSAGE value="test: init" />
<MESSAGE value="chore: remove prebuild" />
<MESSAGE value="chore: idea config" />
<MESSAGE value="feat: react helmet" />
<MESSAGE value="feat: tools normalized" />
<MESSAGE value="feat: search tools" />
<MESSAGE value="fix: deploy message" />
<MESSAGE value="chore: tools by category" />
<MESSAGE value="feat: copy and import file" />
<MESSAGE value="refactor: tool options components" />
<MESSAGE value="fix: join text service" />
@@ -513,8 +606,23 @@
<MESSAGE value="feat: create tool script" />
<MESSAGE value="fix: missing files" />
<MESSAGE value="fix: create tool" />
<MESSAGE value="fix: build" />
<MESSAGE value="chore: CODEOWNERS" />
<MESSAGE value="ci: fix" />
<MESSAGE value="fix: create-tool.mjs" />
<MESSAGE value="feat: change colors in png init" />
<MESSAGE value="feat: change colors in png" />
<MESSAGE value="chore: ResultFooter" />
<MESSAGE value="feat: change color in png finished" />
<MESSAGE value="feat: string to morse" />
<MESSAGE value="feat: sum numbers init" />
<MESSAGE value="fix: compilation" />
<MESSAGE value="chore: printRunningSum" />
<MESSAGE value="chore: sum tests" />
<MESSAGE value="fix: readme" />
<option name="LAST_COMMIT_MESSAGE" value="fix: readme" />
<MESSAGE value="refactor: tool input and result" />
<MESSAGE value="feat: create transparent png" />
<option name="LAST_COMMIT_MESSAGE" value="feat: create transparent png" />
</component>
<component name="XSLT-Support.FileAssociations.UIState">
<expand />

1
CODEOWNERS Normal file
View File

@@ -0,0 +1 @@
* @iib0011

View File

@@ -7,7 +7,7 @@ all available for free and open for community contributions. Please don't forget
## Table of Contents
- [Features](#features)
- [Installation](#installation)
- [Contribute](#contribute)
- [License](#license)
- [Contact](#contact)
@@ -52,12 +52,16 @@ npm run dev
npm run script:create:tool my-tool-name folder1/folder2
```
## Contributors
Use `folder1\folder2` on Windows
### Contributors
<a href="https://github.com/iib0011/omni-tools/graphs/contributors">
<img src="https://contrib.rocks/image?repo=iib0011/omni-tools" />
</a>
[//]: # (<img src="https://api.star-history.com/svg?repos=iib0011/omni-tools&type=Date">)
## License
This project is licensed under the MIT License. See the [LICENSE](LICENSE) file for details.

83
package-lock.json generated
View File

@@ -13,8 +13,11 @@
"@mui/icons-material": "^5.15.20",
"@mui/material": "^5.15.20",
"@types/lodash": "^4.17.5",
"@types/morsee": "^1.0.2",
"color": "^4.2.3",
"formik": "^2.4.6",
"lodash": "^4.17.21",
"morsee": "^1.0.9",
"notistack": "^3.0.1",
"react": "^18.3.1",
"react-dom": "^18.3.1",
@@ -27,6 +30,8 @@
"@commitlint/config-conventional": "^19.2.2",
"@testing-library/jest-dom": "^6.4.5",
"@testing-library/react": "^14.3.1",
"@types/color": "^3.0.6",
"@types/color-rgba": "^2.1.2",
"@types/node": "^20.12.12",
"@types/react": "^18.3.3",
"@types/react-dom": "^18.3.0",
@@ -2408,6 +2413,36 @@
"integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==",
"dev": true
},
"node_modules/@types/color": {
"version": "3.0.6",
"resolved": "https://registry.npmjs.org/@types/color/-/color-3.0.6.tgz",
"integrity": "sha512-NMiNcZFRUAiUUCCf7zkAelY8eV3aKqfbzyFQlXpPIEeoNDbsEHGpb854V3gzTsGKYj830I5zPuOwU/TP5/cW6A==",
"dev": true,
"dependencies": {
"@types/color-convert": "*"
}
},
"node_modules/@types/color-convert": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/@types/color-convert/-/color-convert-2.0.3.tgz",
"integrity": "sha512-2Q6wzrNiuEvYxVQqhh7sXM2mhIhvZR/Paq4FdsQkOMgWsCIkKvSGj8Le1/XalulrmgOzPMqNa0ix+ePY4hTrfg==",
"dev": true,
"dependencies": {
"@types/color-name": "*"
}
},
"node_modules/@types/color-name": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/@types/color-name/-/color-name-1.1.4.tgz",
"integrity": "sha512-hulKeREDdLFesGQjl96+4aoJSHY5b2GRjagzzcqCfIrWhe5vkCqIvrLbqzBaI1q94Vg8DNJZZqTR5ocdWmWclg==",
"dev": true
},
"node_modules/@types/color-rgba": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/@types/color-rgba/-/color-rgba-2.1.2.tgz",
"integrity": "sha512-gDV/fgs4Mpc+hcHygYnM2EDgcxaHmvIGrAVxZJjP38f2IXQKHiGf0XMHhFd+dz8EVPSNTwHL5DJ6yXsxEiCQkg==",
"dev": true
},
"node_modules/@types/conventional-commits-parser": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/@types/conventional-commits-parser/-/conventional-commits-parser-5.0.0.tgz",
@@ -2443,6 +2478,11 @@
"resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.5.tgz",
"integrity": "sha512-MBIOHVZqVqgfro1euRDWX7OO0fBVUUMrN6Pwm8LQsz8cWhEpihlvR70ENj3f40j58TNxZaWv2ndSkInykNBBJw=="
},
"node_modules/@types/morsee": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/@types/morsee/-/morsee-1.0.2.tgz",
"integrity": "sha512-WANv1kCyQtmGZTiov9FzFdt1X4wRtXYZA6B4YR3CghKgx4ychU7d1gkOx7oD+ddVGI+SWmWOPccco7pAc6wXeA=="
},
"node_modules/@types/node": {
"version": "20.14.6",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.14.6.tgz",
@@ -3527,11 +3567,22 @@
"node": ">=6"
}
},
"node_modules/color": {
"version": "4.2.3",
"resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz",
"integrity": "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==",
"dependencies": {
"color-convert": "^2.0.1",
"color-string": "^1.9.0"
},
"engines": {
"node": ">=12.5.0"
}
},
"node_modules/color-convert": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
"dev": true,
"dependencies": {
"color-name": "~1.1.4"
},
@@ -3542,8 +3593,16 @@
"node_modules/color-name": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
"dev": true
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="
},
"node_modules/color-string": {
"version": "1.9.1",
"resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz",
"integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==",
"dependencies": {
"color-name": "^1.0.0",
"simple-swizzle": "^0.2.2"
}
},
"node_modules/commander": {
"version": "4.1.1",
@@ -6142,6 +6201,11 @@
"ufo": "^1.5.3"
}
},
"node_modules/morsee": {
"version": "1.0.9",
"resolved": "https://registry.npmjs.org/morsee/-/morsee-1.0.9.tgz",
"integrity": "sha512-8X8jKVUmZBHKpET9Ap6FPiwlAAASvv60M1K25/YwCU7veuj5MfYgaWX3oEPHtMGgC44IIkIKzyD73fduEKB/9g=="
},
"node_modules/mrmime": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.0.tgz",
@@ -7375,6 +7439,19 @@
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/simple-swizzle": {
"version": "0.2.2",
"resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz",
"integrity": "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==",
"dependencies": {
"is-arrayish": "^0.3.1"
}
},
"node_modules/simple-swizzle/node_modules/is-arrayish": {
"version": "0.3.2",
"resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz",
"integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ=="
},
"node_modules/sirv": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/sirv/-/sirv-2.0.4.tgz",

View File

@@ -29,8 +29,11 @@
"@mui/icons-material": "^5.15.20",
"@mui/material": "^5.15.20",
"@types/lodash": "^4.17.5",
"@types/morsee": "^1.0.2",
"color": "^4.2.3",
"formik": "^2.4.6",
"lodash": "^4.17.21",
"morsee": "^1.0.9",
"notistack": "^3.0.1",
"react": "^18.3.1",
"react-dom": "^18.3.1",
@@ -43,6 +46,8 @@
"@commitlint/config-conventional": "^19.2.2",
"@testing-library/jest-dom": "^6.4.5",
"@testing-library/react": "^14.3.1",
"@types/color": "^3.0.6",
"@types/color-rgba": "^2.1.2",
"@types/node": "^20.12.12",
"@types/react": "^18.3.3",
"@types/react-dom": "^18.3.0",

View File

@@ -30,7 +30,7 @@ function createFolderStructure(basePath, foldersToCreateIndexCount) {
}
const indexPath = join(currentPath, 'index.ts')
if (!fs.existsSync(indexPath) && index < folderArray.length - 1 && index >= folderArray.length - 1 - foldersToCreateIndexCount) {
fs.writeFileSync(indexPath, '// index.ts file')
fs.writeFileSync(indexPath, `export const ${currentPath.split(sep)[currentPath.split(sep).length - 1]}Tools = [];\n`)
console.log(`File created: ${indexPath}`)
}
// Recursively create the next folder
@@ -45,7 +45,7 @@ const toolNameCamelCase = toolName.replace(/-./g, (x) => x[1].toUpperCase())
const toolNameTitleCase =
toolName[0].toUpperCase() + toolName.slice(1).replace(/-/g, ' ')
const toolDir = join(toolsDir, toolName)
const type = folder.split(sep)[folder.split(sep).length - 1]
await createFolderStructure(toolDir, folder.split(sep).length)
console.log(`Directory created: ${toolDir}`)
@@ -71,7 +71,6 @@ export default function ${capitalizeFirstLetter(toolNameCamelCase)}() {
}
`
)
createToolFile(
`meta.ts`,
`
@@ -79,9 +78,9 @@ import { defineTool } from '@tools/defineTool';
import { lazy } from 'react';
// import image from '@assets/text.png';
export const tool = defineTool('${folder}', {
export const tool = defineTool('${type}', {
name: '${toolNameTitleCase}',
path: '/${toolName}',
path: '${toolName}',
// image,
description: '',
keywords: ['${toolName.split('-').join('\', \'')}'],
@@ -133,7 +132,7 @@ const indexContent = await readFile(toolsIndex, { encoding: 'utf-8' }).then(
indexContent.splice(
0,
0,
`import { tool as ${toolNameCamelCase} } from './${toolName}/meta';`
`import { tool as ${type}${capitalizeFirstLetter(toolNameCamelCase)} } from './${toolName}/meta';`
)
writeFile(toolsIndex, indexContent.join('\n'))
console.log(`Added import in: ${toolsIndex}`)

BIN
src/assets/grey-pattern.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 77 KiB

BIN
src/assets/image.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

View File

@@ -0,0 +1,10 @@
import Typography from '@mui/material/Typography';
import React from 'react';
export default function InputHeader({ title }: { title: string }) {
return (
<Typography mb={1} fontSize={30} color={'primary'}>
{title}
</Typography>
);
}

View File

@@ -6,6 +6,7 @@ import Button from '@mui/material/Button';
import IconButton from '@mui/material/IconButton';
import { Link, useNavigate } from 'react-router-dom';
import githubIcon from '@assets/github-mark.png'; // Adjust the path to your GitHub icon
import { Stack } from '@mui/material';
const Navbar: React.FC = () => {
const navigate = useNavigate();
@@ -14,46 +15,46 @@ const Navbar: React.FC = () => {
position="static"
style={{ backgroundColor: 'white', color: 'black' }}
>
<Toolbar>
<Toolbar sx={{ justifyContent: 'space-between', alignItems: 'center' }}>
<Typography
onClick={() => navigate('/')}
fontSize={20}
sx={{ flexGrow: 1, cursor: 'pointer' }}
sx={{ cursor: 'pointer' }}
color={'primary'}
>
OmniTools
</Typography>
<Button color="inherit">
<Link
to="/features"
style={{ textDecoration: 'none', color: 'inherit' }}
<Stack direction={'row'}>
<Button color="inherit">
<Link
to="/features"
style={{ textDecoration: 'none', color: 'inherit' }}
>
Features
</Link>
</Button>
<Button color="inherit">
<Link
to="/about-us"
style={{ textDecoration: 'none', color: 'inherit' }}
>
About Us
</Link>
</Button>
<IconButton
color="primary"
href="https://github.com/iib0011/omni-tools"
target="_blank"
rel="noopener noreferrer"
>
Features
</Link>
</Button>
<Button color="inherit">
<Link
to="/about-us"
style={{ textDecoration: 'none', color: 'inherit' }}
>
About Us
</Link>
</Button>
<IconButton
color="primary"
href="https://github.com/iib0011/omni-tools"
target="_blank"
rel="noopener noreferrer"
>
<img
src={githubIcon}
alt="GitHub"
style={{ height: '24px', marginRight: '8px' }}
/>
<Typography variant="button">Star us</Typography>
</IconButton>
<img
src={githubIcon}
alt="GitHub"
style={{ height: '24px', marginRight: '8px' }}
/>
<Typography variant="button">Star us</Typography>
</IconButton>
</Stack>
</Toolbar>
</AppBar>
);

View File

@@ -29,7 +29,7 @@ export default function ToolHeader({
description
}: ToolHeaderProps) {
return (
<Stack direction={'row'} alignItems={'center'} spacing={2} mt={4}>
<Stack direction={'row'} alignItems={'center'} spacing={2} my={4}>
<Box>
<Typography mb={2} fontSize={30} color={'primary'}>
{title}

View File

@@ -0,0 +1,21 @@
import React, { ReactNode } from 'react';
import Grid from '@mui/material/Grid';
export default function ToolInputAndResult({
input,
result
}: {
input: ReactNode;
result: ReactNode;
}) {
return (
<Grid container spacing={2}>
<Grid item xs={6}>
{input}
</Grid>
<Grid item xs={6}>
{result}
</Grid>
</Grid>
);
}

View File

@@ -0,0 +1,24 @@
import { Stack } from '@mui/material';
import Button from '@mui/material/Button';
import PublishIcon from '@mui/icons-material/Publish';
import ContentPasteIcon from '@mui/icons-material/ContentPaste';
import React from 'react';
export default function InputFooter({
handleImport,
handleCopy
}: {
handleImport: () => void;
handleCopy: () => void;
}) {
return (
<Stack mt={1} direction={'row'} spacing={2}>
<Button onClick={handleImport} startIcon={<PublishIcon />}>
Import from file
</Button>
<Button onClick={handleCopy} startIcon={<ContentPasteIcon />}>
Copy to clipboard
</Button>
</Stack>
);
}

View File

@@ -0,0 +1,115 @@
import { Box, styled, TextField, useTheme } from '@mui/material';
import Typography from '@mui/material/Typography';
import React, { useContext, useEffect, useRef, useState } from 'react';
import InputHeader from '../InputHeader';
import InputFooter from './InputFooter';
import { CustomSnackBarContext } from '../../contexts/CustomSnackBarContext';
import greyPattern from '@assets/grey-pattern.png';
import { globalInputHeight } from '../../config/uiConfig';
interface ToolFileInputProps {
value: File | null;
onChange: (file: File) => void;
accept: string[];
title?: string;
}
export default function ToolFileInput({
value,
onChange,
accept,
title = 'File'
}: ToolFileInputProps) {
const [preview, setPreview] = useState<string | null>(null);
const theme = useTheme();
const { showSnackBar } = useContext(CustomSnackBarContext);
const fileInputRef = useRef<HTMLInputElement>(null);
const handleCopy = () => {
navigator.clipboard
.writeText(value?.name ?? '')
.then(() => showSnackBar('Text copied', 'success'))
.catch((err) => {
showSnackBar('Failed to copy: ' + err, 'error');
});
};
useEffect(() => {
if (value) {
const objectUrl = URL.createObjectURL(value);
setPreview(objectUrl);
// Clean up memory when the component is unmounted or the file changes
return () => URL.revokeObjectURL(objectUrl);
} else {
setPreview(null);
}
}, [value]);
const handleFileChange = (event: React.ChangeEvent<HTMLInputElement>) => {
const file = event.target.files?.[0];
if (file) onChange(file);
};
const handleImportClick = () => {
fileInputRef.current?.click();
};
return (
<Box>
<InputHeader title={title} />
<Box
sx={{
width: '100%',
height: globalInputHeight,
border: preview ? 0 : 1,
borderRadius: 2,
boxShadow: '5'
}}
>
{preview ? (
<Box
width={'100%'}
height={'100%'}
sx={{
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
backgroundImage: `url(${greyPattern})`
}}
>
<img
src={preview}
alt="Preview"
style={{ maxWidth: '100%', maxHeight: globalInputHeight }}
/>
</Box>
) : (
<Box
onClick={handleImportClick}
sx={{
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
padding: 5,
height: '100%',
cursor: 'pointer'
}}
>
<Typography color={theme.palette.grey['600']}>
Click here to select an image from your device, press Ctrl+V to
use an image from your clipboard, drag and drop a file from
desktop
</Typography>
</Box>
)}
</Box>
<InputFooter handleCopy={handleCopy} handleImport={handleImportClick} />
<input
ref={fileInputRef}
style={{ display: 'none' }}
type="file"
accept={accept.join(',')}
onChange={handleFileChange}
/>
</Box>
);
}

View File

@@ -5,6 +5,8 @@ import PublishIcon from '@mui/icons-material/Publish';
import ContentPasteIcon from '@mui/icons-material/ContentPaste';
import React, { useContext, useRef } from 'react';
import { CustomSnackBarContext } from '../../contexts/CustomSnackBarContext';
import InputHeader from '../InputHeader';
import InputFooter from './InputFooter';
export default function ToolTextInput({
value,
@@ -45,9 +47,7 @@ export default function ToolTextInput({
};
return (
<Box id="tool">
<Typography fontSize={30} color={'primary'}>
{title}
</Typography>
<InputHeader title={title} />
<TextField
value={value}
onChange={(event) => onChange(event.target.value)}
@@ -55,14 +55,7 @@ export default function ToolTextInput({
multiline
rows={10}
/>
<Stack mt={1} direction={'row'} spacing={2}>
<Button onClick={handleImportClick} startIcon={<PublishIcon />}>
Import from file
</Button>
<Button onClick={handleCopy} startIcon={<ContentPasteIcon />}>
Copy to clipboard
</Button>
</Stack>
<InputFooter handleCopy={handleCopy} handleImport={handleImportClick} />
<input
type="file"
accept="*"

View File

@@ -0,0 +1,54 @@
import React, { useState, ChangeEvent, useRef } from 'react';
import { Box, Stack, TextField } from '@mui/material';
import PaletteIcon from '@mui/icons-material/Palette';
import IconButton from '@mui/material/IconButton';
import Typography from '@mui/material/Typography';
import { globalDescriptionFontSize } from '../../config/uiConfig';
interface ColorSelectorProps {
value: string;
onChange: (val: string) => void;
description: string;
}
const ColorSelector: React.FC<ColorSelectorProps> = ({
value = '#ffffff',
onChange,
description
}) => {
const [color, setColor] = useState<string>(value);
const inputRef = useRef<HTMLInputElement>(null);
const handleColorChange = (event: ChangeEvent<HTMLInputElement>) => {
const val = event.target.value;
setColor(val);
onChange(val);
};
return (
<Box mb={1}>
<Stack direction={'row'}>
<TextField
sx={{ backgroundColor: 'white' }}
value={color}
onChange={handleColorChange}
/>
<IconButton onClick={() => inputRef.current?.click()}>
<PaletteIcon />
</IconButton>
<TextField
style={{ display: 'none' }}
inputRef={inputRef}
type="color"
value={color}
onChange={handleColorChange}
/>
</Stack>
<Typography fontSize={globalDescriptionFontSize}>
{description}
</Typography>
</Box>
);
};
export default ColorSelector;

View File

@@ -4,37 +4,38 @@ import { Field } from 'formik';
import Typography from '@mui/material/Typography';
import React from 'react';
import TextFieldWithDesc from './TextFieldWithDesc';
import { globalDescriptionFontSize } from '../../config/uiConfig';
import SimpleRadio from './SimpleRadio';
const RadioWithTextField = <T,>({
fieldName,
type,
radioValue,
title,
onTypeChange,
onRadioChange,
value,
description,
onTextChange
onTextChange,
typeDescription
}: {
fieldName: string;
title: string;
type: T;
onTypeChange: (val: T) => void;
radioValue: T;
onRadioChange: (val: T) => void;
value: string;
description: string;
onTextChange: (value: string) => void;
typeDescription?: string;
}) => {
const onChange = () => onTypeChange(type);
const onChange = () => onRadioChange(radioValue);
return (
<Box>
<Stack
direction={'row'}
sx={{ mt: 2, mb: 1, cursor: 'pointer' }}
onClick={onChange}
alignItems={'center'}
spacing={1}
>
<Field type="radio" name={fieldName} value={type} onChange={onChange} />
<Typography>{title}</Typography>
</Stack>
<SimpleRadio
value={radioValue}
onChange={onChange}
fieldName={fieldName}
title={title}
description={typeDescription}
/>
<TextFieldWithDesc
value={value}
onChange={onTextChange}

View File

@@ -0,0 +1,46 @@
import { Box, Stack } from '@mui/material';
import { Field } from 'formik';
import Typography from '@mui/material/Typography';
import { globalDescriptionFontSize } from '../../config/uiConfig';
import React from 'react';
interface SimpleRadioProps {
onChange: () => void;
fieldName: string;
value: any;
title: string;
description?: string;
}
export default function SimpleRadio({
onChange,
fieldName,
value,
title,
description
}: SimpleRadioProps) {
return (
<Box>
<Stack
direction={'row'}
sx={{ mt: 2, mb: 1, cursor: 'pointer' }}
onClick={onChange}
alignItems={'center'}
spacing={1}
>
<Field
type="radio"
name={fieldName}
value={value}
onChange={onChange}
/>
<Typography>{title}</Typography>
</Stack>
{description && (
<Typography ml={2} fontSize={globalDescriptionFontSize}>
{description}
</Typography>
)}
</Box>
);
}

View File

@@ -0,0 +1,27 @@
import Typography from '@mui/material/Typography';
import React, { ReactNode } from 'react';
import { Box, Stack } from '@mui/material';
interface ToolOptionGroup {
title: string;
component: ReactNode;
}
export default function ToolOptionGroups({
groups
}: {
groups: ToolOptionGroup[];
}) {
return (
<Stack direction={'row'} spacing={2}>
{groups.map((group) => (
<Box key={group.title}>
<Typography mb={1} fontSize={22}>
{group.title}
</Typography>
{group.component}
</Box>
))}
</Stack>
);
}

View File

@@ -0,0 +1,34 @@
import { Stack } from '@mui/material';
import Button from '@mui/material/Button';
import DownloadIcon from '@mui/icons-material/Download';
import ContentPasteIcon from '@mui/icons-material/ContentPaste';
import React from 'react';
export default function ResultFooter({
handleDownload,
handleCopy,
disabled
}: {
handleDownload: () => void;
handleCopy: () => void;
disabled?: boolean;
}) {
return (
<Stack mt={1} direction={'row'} spacing={2}>
<Button
disabled={disabled}
onClick={handleDownload}
startIcon={<DownloadIcon />}
>
Save as
</Button>
<Button
disabled={disabled}
onClick={handleCopy}
startIcon={<ContentPasteIcon />}
>
Copy to clipboard
</Button>
</Stack>
);
}

View File

@@ -0,0 +1,99 @@
import { Box } from '@mui/material';
import React, { useContext } from 'react';
import InputHeader from '../InputHeader';
import greyPattern from '@assets/grey-pattern.png';
import { globalInputHeight } from '../../config/uiConfig';
import ResultFooter from './ResultFooter';
import { CustomSnackBarContext } from '../../contexts/CustomSnackBarContext';
export default function ToolFileResult({
title = 'Result',
value,
extension
}: {
title?: string;
value: File | null;
extension: string;
}) {
const [preview, setPreview] = React.useState<string | null>(null);
const { showSnackBar } = useContext(CustomSnackBarContext);
React.useEffect(() => {
if (value) {
const objectUrl = URL.createObjectURL(value);
setPreview(objectUrl);
return () => URL.revokeObjectURL(objectUrl);
} else {
setPreview(null);
}
}, [value]);
const handleCopy = () => {
if (value) {
const blob = new Blob([value], { type: value.type });
const clipboardItem = new ClipboardItem({ [value.type]: blob });
navigator.clipboard
.write([clipboardItem])
.then(() => showSnackBar('File copied', 'success'))
.catch((err) => {
showSnackBar('Failed to copy: ' + err, 'error');
});
}
};
const handleDownload = () => {
if (value) {
const filename = 'output-omni-tools.' + extension;
const blob = new Blob([value], { type: value.type });
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = filename;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
window.URL.revokeObjectURL(url);
}
};
return (
<Box>
<InputHeader title={title} />
<Box
sx={{
width: '100%',
height: globalInputHeight,
border: preview ? 0 : 1,
borderRadius: 2,
boxShadow: '5'
}}
>
{preview && (
<Box
width={'100%'}
height={'100%'}
sx={{
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
backgroundImage: `url(${greyPattern})`
}}
>
<img
src={preview}
alt="Result"
style={{ maxWidth: '100%', maxHeight: globalInputHeight }}
/>
</Box>
)}
</Box>
<ResultFooter
disabled={!value}
handleCopy={handleCopy}
handleDownload={handleDownload}
/>
</Box>
);
}

View File

@@ -5,6 +5,8 @@ import DownloadIcon from '@mui/icons-material/Download';
import ContentPasteIcon from '@mui/icons-material/ContentPaste';
import React, { useContext } from 'react';
import { CustomSnackBarContext } from '../../contexts/CustomSnackBarContext';
import InputHeader from '../InputHeader';
import ResultFooter from './ResultFooter';
export default function ToolTextResult({
title = 'Result',
@@ -37,18 +39,9 @@ export default function ToolTextResult({
};
return (
<Box>
<Typography fontSize={30} color={'primary'}>
{title}
</Typography>
<InputHeader title={title} />
<TextField value={value} fullWidth multiline rows={10} />
<Stack mt={1} direction={'row'} spacing={2}>
<Button onClick={handleDownload} startIcon={<DownloadIcon />}>
Save as
</Button>
<Button onClick={handleCopy} startIcon={<ContentPasteIcon />}>
Copy to clipboard
</Button>
</Stack>
<ResultFooter handleCopy={handleCopy} handleDownload={handleDownload} />
</Box>
);
}

2
src/config/uiConfig.ts Normal file
View File

@@ -0,0 +1,2 @@
export const globalInputHeight = 300;
export const globalDescriptionFontSize = 12;

View File

@@ -12,21 +12,21 @@ import SearchIcon from '@mui/icons-material/Search';
import { Link, useNavigate } from 'react-router-dom';
import { filterTools, getToolsByCategory, tools } from '../../tools';
import { useState } from 'react';
import { DefinedTool } from '../../tools/defineTool';
import { DefinedTool } from '@tools/defineTool';
import Button from '@mui/material/Button';
const exampleTools: { label: string; url: string }[] = [
{
label: 'Create a transparent image',
url: ''
url: '/png/create-transparent'
},
{ label: 'Convert text to morse code', url: '' },
{ label: 'Convert text to morse code', url: '/string/to-morse' },
{ label: 'Change GIF speed', url: '' },
{ label: 'Pick a random item', url: '' },
{ label: 'Find and replace text', url: '' },
{ label: 'Convert emoji to image', url: '' },
{ label: 'Split a string', url: '/string/split' },
{ label: 'Calculate number sum', url: '' },
{ label: 'Calculate number sum', url: '/number/sum' },
{ label: 'Pixelate an image', url: '' }
];
export default function Home() {

View File

@@ -1,11 +1,168 @@
import { Box } from '@mui/material';
import React from 'react';
import React, { useEffect, useState } from 'react';
import * as Yup from 'yup';
import ToolFileInput from '../../../../components/input/ToolFileInput';
import ToolFileResult from '../../../../components/result/ToolFileResult';
import ToolOptions from '../../../../components/options/ToolOptions';
import { Formik, useFormikContext } from 'formik';
import ColorSelector from '../../../../components/options/ColorSelector';
import Color from 'color';
import TextFieldWithDesc from 'components/options/TextFieldWithDesc';
import ToolInputAndResult from '../../../../components/ToolInputAndResult';
import ToolOptionGroups from '../../../../components/options/ToolOptionGroups';
const initialValues = {};
const initialValues = {
fromColor: 'white',
toColor: 'black',
similarity: '10'
};
const validationSchema = Yup.object({
// splitSeparator: Yup.string().required('The separator is required')
});
export default function ChangeColorsInPng() {
return <Box>Lorem ipsum</Box>;
}
const [input, setInput] = useState<File | null>(null);
const [result, setResult] = useState<File | null>(null);
const FormikListenerComponent = ({ input }: { input: File }) => {
const { values } = useFormikContext<typeof initialValues>();
const { fromColor, toColor, similarity } = values;
useEffect(() => {
let fromRgb: [number, number, number];
let toRgb: [number, number, number];
try {
//@ts-ignore
fromRgb = Color(fromColor).rgb().array();
//@ts-ignore
toRgb = Color(toColor).rgb().array();
} catch (err) {
return;
}
const processImage = async (
file: File,
fromColor: [number, number, number],
toColor: [number, number, number],
similarity: number
) => {
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
if (ctx == null) return;
const img = new Image();
img.src = URL.createObjectURL(file);
await img.decode();
canvas.width = img.width;
canvas.height = img.height;
ctx.drawImage(img, 0, 0);
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
const data: Uint8ClampedArray = imageData.data;
const colorDistance = (
c1: [number, number, number],
c2: [number, number, number]
) => {
return Math.sqrt(
Math.pow(c1[0] - c2[0], 2) +
Math.pow(c1[1] - c2[1], 2) +
Math.pow(c1[2] - c2[2], 2)
);
};
const maxColorDistance = Math.sqrt(
Math.pow(255, 2) + Math.pow(255, 2) + Math.pow(255, 2)
);
const similarityThreshold = (similarity / 100) * maxColorDistance;
for (let i = 0; i < data.length; i += 4) {
const currentColor: [number, number, number] = [
data[i],
data[i + 1],
data[i + 2]
];
if (colorDistance(currentColor, fromColor) <= similarityThreshold) {
data[i] = toColor[0]; // Red
data[i + 1] = toColor[1]; // Green
data[i + 2] = toColor[2]; // Blue
}
}
ctx.putImageData(imageData, 0, 0);
canvas.toBlob((blob) => {
if (blob) {
const newFile = new File([blob], file.name, { type: 'image/png' });
setResult(newFile);
}
}, 'image/png');
};
processImage(input, fromRgb, toRgb, Number(similarity));
}, [input, fromColor, toColor]);
return null;
};
return (
<Box>
<ToolInputAndResult
input={
<ToolFileInput
value={input}
onChange={setInput}
accept={['image/png']}
title={'Input PNG'}
/>
}
result={
<ToolFileResult
title={'Output PNG with new colors'}
value={result}
extension={'png'}
/>
}
/>
<ToolOptions>
<Formik
initialValues={initialValues}
validationSchema={validationSchema}
onSubmit={() => {}}
>
{({ setFieldValue, values }) => (
<Box>
{input && <FormikListenerComponent input={input} />}
<ToolOptionGroups
groups={[
{
title: 'From color and to color',
component: (
<Box>
<ColorSelector
value={values.fromColor}
onChange={(val) => setFieldValue('fromColor', val)}
description={'Replace this color (from color)'}
/>
<ColorSelector
value={values.toColor}
onChange={(val) => setFieldValue('toColor', val)}
description={'With this color (to color)'}
/>
<TextFieldWithDesc
value={values.similarity}
onChange={(val) => setFieldValue('similarity', val)}
description={
'Match this % of similar colors of the from color. For example, 10% white will match white and a little bit of gray.'
}
/>
</Box>
)
}
]}
/>
</Box>
)}
</Formik>
</ToolOptions>
</Box>
);
}

View File

@@ -1,12 +1,13 @@
import { defineTool } from '@tools/defineTool';
import { lazy } from 'react';
import image from '@assets/text.png';
import image from '@assets/image.png';
export const tool = defineTool('image/png', {
export const tool = defineTool('png', {
name: 'Change colors in png',
path: '/change-colors-in-png',
path: 'change-colors-in-png',
image,
description: '',
description:
"World's simplest online Portable Network Graphics (PNG) color changer. Just import your PNG image in the editor on the left, select which colors to change, and you'll instantly get a new PNG with the new colors on the right. Free, quick, and very powerful. Import a PNG replace its colors.",
keywords: ['change', 'colors', 'in', 'png'],
component: lazy(() => import('./index'))
});

View File

@@ -0,0 +1,156 @@
import { Box } from '@mui/material';
import React, { useEffect, useState } from 'react';
import * as Yup from 'yup';
import ToolFileInput from '../../../../components/input/ToolFileInput';
import ToolFileResult from '../../../../components/result/ToolFileResult';
import ToolOptions from '../../../../components/options/ToolOptions';
import { Formik, useFormikContext } from 'formik';
import ColorSelector from '../../../../components/options/ColorSelector';
import Color from 'color';
import TextFieldWithDesc from 'components/options/TextFieldWithDesc';
import ToolInputAndResult from '../../../../components/ToolInputAndResult';
import ToolOptionGroups from '../../../../components/options/ToolOptionGroups';
const initialValues = {
fromColor: 'white',
similarity: '10'
};
const validationSchema = Yup.object({
// splitSeparator: Yup.string().required('The separator is required')
});
export default function ChangeColorsInPng() {
const [input, setInput] = useState<File | null>(null);
const [result, setResult] = useState<File | null>(null);
const FormikListenerComponent = ({ input }: { input: File }) => {
const { values } = useFormikContext<typeof initialValues>();
const { fromColor, similarity } = values;
useEffect(() => {
let fromRgb: [number, number, number];
try {
//@ts-ignore
fromRgb = Color(fromColor).rgb().array();
} catch (err) {
return;
}
const processImage = async (
file: File,
fromColor: [number, number, number],
similarity: number
) => {
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
if (ctx == null) return;
const img = new Image();
img.src = URL.createObjectURL(file);
await img.decode();
canvas.width = img.width;
canvas.height = img.height;
ctx.drawImage(img, 0, 0);
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
const data: Uint8ClampedArray = imageData.data;
const colorDistance = (
c1: [number, number, number],
c2: [number, number, number]
) => {
return Math.sqrt(
Math.pow(c1[0] - c2[0], 2) +
Math.pow(c1[1] - c2[1], 2) +
Math.pow(c1[2] - c2[2], 2)
);
};
const maxColorDistance = Math.sqrt(
Math.pow(255, 2) + Math.pow(255, 2) + Math.pow(255, 2)
);
const similarityThreshold = (similarity / 100) * maxColorDistance;
for (let i = 0; i < data.length; i += 4) {
const currentColor: [number, number, number] = [
data[i],
data[i + 1],
data[i + 2]
];
if (colorDistance(currentColor, fromColor) <= similarityThreshold) {
data[i + 3] = 0; // Set alpha to 0 (transparent)
}
}
ctx.putImageData(imageData, 0, 0);
canvas.toBlob((blob) => {
if (blob) {
const newFile = new File([blob], file.name, { type: 'image/png' });
setResult(newFile);
}
}, 'image/png');
};
processImage(input, fromRgb, Number(similarity));
}, [input, fromColor]);
return null;
};
return (
<Box>
<ToolInputAndResult
input={
<ToolFileInput
value={input}
onChange={setInput}
accept={['image/png']}
title={'Input PNG'}
/>
}
result={
<ToolFileResult
title={'Transparent PNG'}
value={result}
extension={'png'}
/>
}
/>
<ToolOptions>
<Formik
initialValues={initialValues}
validationSchema={validationSchema}
onSubmit={() => {}}
>
{({ setFieldValue, values }) => (
<Box>
{input && <FormikListenerComponent input={input} />}
<ToolOptionGroups
groups={[
{
title: 'From color and similarity',
component: (
<Box>
<ColorSelector
value={values.fromColor}
onChange={(val) => setFieldValue('fromColor', val)}
description={'Replace this color (from color)'}
/>
<TextFieldWithDesc
value={values.similarity}
onChange={(val) => setFieldValue('similarity', val)}
description={
'Match this % of similar colors of the from color. For example, 10% white will match white and a little bit of gray.'
}
/>
</Box>
)
}
]}
/>
</Box>
)}
</Formik>
</ToolOptions>
</Box>
);
}

View File

@@ -0,0 +1,13 @@
import { defineTool } from '@tools/defineTool';
import { lazy } from 'react';
import image from '@assets/image.png';
export const tool = defineTool('png', {
name: 'Create transparent PNG',
path: 'create-transparent',
image,
description:
"World's simplest online Portable Network Graphics transparency maker. Just import your PNG image in the editor on the left and you will instantly get a transparent PNG on the right. Free, quick, and very powerful. Import a PNG get a transparent PNG.",
keywords: ['create', 'transparent'],
component: lazy(() => import('./index'))
});

View File

@@ -1,3 +1,4 @@
import { tool as pngCreateTransparent } from './create-transparent/meta';
import { tool as changeColorsInPng } from './change-colors-in-png/meta';
export const pngTools = [changeColorsInPng];
export const pngTools = [changeColorsInPng, pngCreateTransparent];

View File

@@ -0,0 +1,3 @@
import { tool as numberSum } from './sum/meta';
export const numberTools = [numberSum];

View File

@@ -0,0 +1,153 @@
import { Box, Stack } from '@mui/material';
import React, { useContext, useEffect, useState } from 'react';
import ToolTextInput from '../../../components/input/ToolTextInput';
import ToolTextResult from '../../../components/result/ToolTextResult';
import { Formik, useFormikContext } from 'formik';
import * as Yup from 'yup';
import ToolOptions from '../../../components/options/ToolOptions';
import { compute, NumberExtractionType } from './service';
import { CustomSnackBarContext } from '../../../contexts/CustomSnackBarContext';
import RadioWithTextField from '../../../components/options/RadioWithTextField';
import ToolOptionGroups from '../../../components/options/ToolOptionGroups';
import SimpleRadio from '../../../components/options/SimpleRadio';
import CheckboxWithDesc from '../../../components/options/CheckboxWithDesc';
import ToolInputAndResult from '../../../components/ToolInputAndResult';
const initialValues = {
extractionType: 'smart' as NumberExtractionType,
separator: '\\n',
printRunningSum: false
};
const extractionTypes: {
title: string;
description: string;
type: NumberExtractionType;
withTextField: boolean;
textValueAccessor?: keyof typeof initialValues;
}[] = [
{
title: 'Smart sum',
description: 'Auto detect numbers in the input.',
type: 'smart',
withTextField: false
},
{
title: 'Number Delimiter',
type: 'delimiter',
description:
'Input SeparatorCustomize the number separator here. (By default a line break.)',
withTextField: true,
textValueAccessor: 'separator'
}
];
export default function SplitText() {
const [input, setInput] = useState<string>('');
const [result, setResult] = useState<string>('');
// const formRef = useRef<FormikProps<typeof initialValues>>(null);
const { showSnackBar } = useContext(CustomSnackBarContext);
const FormikListenerComponent = () => {
const { values } = useFormikContext<typeof initialValues>();
useEffect(() => {
try {
const { extractionType, printRunningSum, separator } = values;
setResult(compute(input, extractionType, printRunningSum, separator));
} catch (exception: unknown) {
if (exception instanceof Error)
showSnackBar(exception.message, 'error');
}
}, [values, input]);
return null; // This component doesn't render anything
};
const validationSchema = Yup.object({
// splitSeparator: Yup.string().required('The separator is required')
});
return (
<Box>
<ToolInputAndResult
input={<ToolTextInput value={input} onChange={setInput} />}
result={<ToolTextResult title={'Total'} value={result} />}
/>
<ToolOptions>
<Formik
initialValues={initialValues}
validationSchema={validationSchema}
onSubmit={() => {}}
>
{({ setFieldValue, values }) => (
<Stack direction={'row'} spacing={2}>
<FormikListenerComponent />
<ToolOptionGroups
groups={[
{
title: 'Number extraction',
component: extractionTypes.map(
({
title,
description,
type,
withTextField,
textValueAccessor
}) =>
withTextField ? (
<RadioWithTextField
key={type}
radioValue={type}
title={title}
fieldName={'extractionType'}
description={description}
value={
textValueAccessor
? values[textValueAccessor].toString()
: ''
}
onRadioChange={(type) =>
setFieldValue('extractionType', type)
}
onTextChange={(val) =>
setFieldValue(textValueAccessor ?? '', val)
}
/>
) : (
<SimpleRadio
key={title}
onChange={() =>
setFieldValue('extractionType', type)
}
fieldName={'extractionType'}
value={values.extractionType}
description={description}
title={title}
/>
)
)
},
{
title: 'Running Sum',
component: (
<CheckboxWithDesc
title={'Print Running Sum'}
description={
"Display the sum as it's calculated step by step."
}
checked={values.printRunningSum}
onChange={(value) =>
setFieldValue('printRunningSum', value)
}
/>
)
}
]}
/>
</Stack>
)}
</Formik>
</ToolOptions>
</Box>
);
}

View File

@@ -0,0 +1,13 @@
import { defineTool } from '@tools/defineTool';
import { lazy } from 'react';
// import image from '@assets/text.png';
export const tool = defineTool('number', {
name: 'Number Sum Calculator',
path: 'sum',
// image,
description:
'Quickly calculate the sum of numbers in your browser. To get your sum, just enter your list of numbers in the input field, adjust the separator between the numbers in the options below, and this utility will add up all these numbers.',
keywords: ['sum'],
component: lazy(() => import('./index'))
});

View File

@@ -0,0 +1,37 @@
export type NumberExtractionType = 'smart' | 'delimiter';
function getAllNumbers(text: string): number[] {
const regex = /\d+/g;
const matches = text.match(regex);
return matches ? matches.map(Number) : [];
}
export const compute = (
input: string,
extractionType: NumberExtractionType,
printRunningSum: boolean,
separator: string
): string => {
let numbers: number[] = [];
if (extractionType === 'smart') {
numbers = getAllNumbers(input);
} else {
const parts = input.split(separator);
// Filter out and convert parts that are numbers
numbers = parts
.filter((part) => !isNaN(Number(part)) && part.trim() !== '')
.map(Number);
}
if (printRunningSum) {
let result: string = '';
let sum: number = 0;
for (const i of numbers) {
sum = sum + i;
result = result + sum + '\n';
}
return result;
} else
return numbers
.reduce((previousValue, currentValue) => previousValue + currentValue, 0)
.toString();
};

View File

@@ -0,0 +1,64 @@
import { describe, it, expect } from 'vitest';
import { compute } from './service';
describe('compute function', () => {
it('should correctly sum numbers in smart extraction mode', () => {
const input = 'The 2 cats have 4 and 7 kittens';
const result = compute(input, 'smart', false, ',');
expect(result).toBe('13');
});
it('should correctly sum numbers with custom delimiter', () => {
const input = '2,4,7';
const result = compute(input, 'delimiter', false, ',');
expect(result).toBe('13');
});
it('should return running sum in smart extraction mode', () => {
const input = 'The 2 cats have 4 and 7 kittens';
const result = compute(input, 'smart', true, ',');
expect(result).toBe('2\n6\n13\n');
});
it('should return running sum with custom delimiter', () => {
const input = '2,4,7';
const result = compute(input, 'delimiter', true, ',');
expect(result).toBe('2\n6\n13\n');
});
it('should handle empty input gracefully in smart mode', () => {
const input = '';
const result = compute(input, 'smart', false, ',');
expect(result).toBe('0');
});
it('should handle empty input gracefully in delimiter mode', () => {
const input = '';
const result = compute(input, 'delimiter', false, ',');
expect(result).toBe('0');
});
it('should handle input with no numbers in smart mode', () => {
const input = 'There are no numbers here';
const result = compute(input, 'smart', false, ',');
expect(result).toBe('0');
});
it('should handle input with no numbers in delimiter mode', () => {
const input = 'a,b,c';
const result = compute(input, 'delimiter', false, ',');
expect(result).toBe('0');
});
it('should ignore non-numeric parts in delimiter mode', () => {
const input = '2,a,4,b,7';
const result = compute(input, 'delimiter', false, ',');
expect(result).toBe('13');
});
it('should handle different separators', () => {
const input = '2;4;7';
const result = compute(input, 'delimiter', false, ';');
expect(result).toBe('13');
});
});

View File

@@ -1,4 +1,5 @@
import { tool as stringToMorse } from './to-morse/meta';
import { tool as stringSplit } from './split/meta';
import { tool as stringJoin } from './join/meta';
export const stringTools = [stringSplit, stringJoin];
export const stringTools = [stringSplit, stringJoin, stringToMorse];

View File

@@ -9,6 +9,8 @@ import { mergeText } from './service';
import { CustomSnackBarContext } from '../../../contexts/CustomSnackBarContext';
import TextFieldWithDesc from '../../../components/options/TextFieldWithDesc';
import CheckboxWithDesc from '../../../components/options/CheckboxWithDesc';
import ToolOptionGroups from '../../../components/options/ToolOptionGroups';
import ToolInputAndResult from '../../../components/ToolInputAndResult';
import Info from './Info';
import Separator from '../../../tools/Separator';
@@ -56,12 +58,12 @@ const exampleCards = [
title: 'Merge a To-Do List',
description:
"In this example, we merge a bullet point list into one sentence, separating each item by the word 'and'. We also remove all empty lines and trailing spaces. If we didn't remove the empty lines, then they'd be joined with the separator word, making the separator word appear multiple times. If we didn't remove the trailing tabs and spaces, then they'd create extra spacing in the joined text and it wouldn't look nice.",
sampleText: `clean the house
sampleText: `clean the house
go shopping
go shopping
feed the cat
make dinner
make dinner
build a rocket ship and fly away`,
sampleResult: `clean the house and go shopping and feed the cat and make dinner and build a rocket ship and fly away`,
requiredOptions: {
@@ -177,18 +179,16 @@ export default function JoinText() {
return (
<Box>
<Grid container spacing={2}>
<Grid item xs={6}>
<ToolInputAndResult
input={
<ToolTextInput
title={'Text Pieces'}
value={input}
onChange={setInput}
/>
</Grid>
<Grid item xs={6}>
<ToolTextResult title={'Joined Text'} value={result} />
</Grid>
</Grid>
}
result={<ToolTextResult title={'Joined Text'} value={result} />}
/>
<ToolOptions>
<Formik
initialValues={initialValues}
@@ -198,31 +198,37 @@ export default function JoinText() {
{({ setFieldValue, values }) => (
<Stack direction={'row'} spacing={2}>
<FormikListenerComponent input={input} />
<Box>
<Typography fontSize={22}>Text Merged Options</Typography>
<TextFieldWithDesc
placeholder={mergeOptions.placeholder}
value={values['joinCharacter']}
onChange={(value) =>
setFieldValue(mergeOptions.accessor, value)
<ToolOptionGroups
groups={[
{
title: 'Text Merged Options',
component: (
<TextFieldWithDesc
placeholder={mergeOptions.placeholder}
value={values['joinCharacter']}
onChange={(value) =>
setFieldValue(mergeOptions.accessor, value)
}
description={mergeOptions.description}
/>
)
},
{
title: 'Blank Lines and Trailing Spaces',
component: blankTrailingOptions.map((option) => (
<CheckboxWithDesc
key={option.accessor}
title={option.title}
checked={!!values[option.accessor]}
onChange={(value) =>
setFieldValue(option.accessor, value)
}
description={option.description}
/>
))
}
description={mergeOptions.description}
/>
</Box>
<Box>
<Typography fontSize={22}>
Blank Lines and Trailing Spaces
</Typography>
{blankTrailingOptions.map((option, index) => (
<CheckboxWithDesc
key={index}
title={option.title}
checked={!!values[option.accessor]}
onChange={(value) => setFieldValue(option.accessor, value)}
description={option.description}
/>
))}
</Box>
]}
/>
</Stack>
)}
</Formik>

View File

@@ -5,23 +5,13 @@ export function mergeText(
joinCharacter: string = ''
): string {
let processedLines: string[] = text.split('\n');
if (deleteTrailingSpaces) {
processedLines = processedLines.map((line) => line.trimEnd());
}
if (deleteBlankLines) {
processedLines = processedLines.filter((line) => line.trim() !== '');
} else {
processedLines = processedLines.map((line) =>
line.trim() === '' ? line + '\r\n\n' : line
);
processedLines = processedLines.filter((line) => line.trim());
}
return processedLines.join(joinCharacter);
}
// Example text to use
`This is a line with trailing spaces
Another line with trailing spaces
Final line without trailing spaces`;

View File

@@ -1,16 +1,17 @@
import { Box, Stack, TextField } from '@mui/material';
import { Box, Stack } from '@mui/material';
import Grid from '@mui/material/Grid';
import Typography from '@mui/material/Typography';
import React, { useContext, useEffect, useRef, useState } from 'react';
import React, { useContext, useEffect, useState } from 'react';
import ToolTextInput from '../../../components/input/ToolTextInput';
import ToolTextResult from '../../../components/result/ToolTextResult';
import { Field, Formik, FormikProps, useFormikContext } from 'formik';
import { Formik, useFormikContext } from 'formik';
import * as Yup from 'yup';
import ToolOptions from '../../../components/options/ToolOptions';
import { compute, SplitOperatorType } from './service';
import { CustomSnackBarContext } from '../../../contexts/CustomSnackBarContext';
import RadioWithTextField from '../../../components/options/RadioWithTextField';
import TextFieldWithDesc from '../../../components/options/TextFieldWithDesc';
import ToolOptionGroups from '../../../components/options/ToolOptionGroups';
import ToolInputAndResult from '../../../components/ToolInputAndResult';
const initialValues = {
splitSeparatorType: 'symbol' as SplitOperatorType,
@@ -126,14 +127,10 @@ export default function SplitText() {
return (
<Box>
<Grid container spacing={2}>
<Grid item xs={6}>
<ToolTextInput value={input} onChange={setInput} />
</Grid>
<Grid item xs={6}>
<ToolTextResult title={'Text pieces'} value={result} />
</Grid>
</Grid>
<ToolInputAndResult
input={<ToolTextInput value={input} onChange={setInput} />}
result={<ToolTextResult title={'Text pieces'} value={result} />}
/>
<ToolOptions>
<Formik
initialValues={initialValues}
@@ -143,34 +140,44 @@ export default function SplitText() {
{({ setFieldValue, values }) => (
<Stack direction={'row'} spacing={2}>
<FormikListenerComponent />
<Box>
<Typography fontSize={22}>Split separator options</Typography>
{splitOperators.map(({ title, description, type }) => (
<RadioWithTextField
key={type}
type={type}
title={title}
fieldName={'splitSeparatorType'}
description={description}
value={values[`${type}Value`]}
onTypeChange={(type) =>
setFieldValue('splitSeparatorType', type)
}
onTextChange={(val) => setFieldValue(`${type}Value`, val)}
/>
))}
</Box>
<Box>
<Typography fontSize={22}>Output separator options</Typography>
{outputOptions.map((option) => (
<TextFieldWithDesc
key={option.accessor}
value={values[option.accessor]}
onChange={(value) => setFieldValue(option.accessor, value)}
description={option.description}
/>
))}
</Box>
<ToolOptionGroups
groups={[
{
title: 'Split separator options',
component: splitOperators.map(
({ title, description, type }) => (
<RadioWithTextField
key={type}
radioValue={type}
title={title}
fieldName={'splitSeparatorType'}
description={description}
value={values[`${type}Value`]}
onRadioChange={(type) =>
setFieldValue('splitSeparatorType', type)
}
onTextChange={(val) =>
setFieldValue(`${type}Value`, val)
}
/>
)
)
},
{
title: 'Output separator options',
component: outputOptions.map((option) => (
<TextFieldWithDesc
key={option.accessor}
value={values[option.accessor]}
onChange={(value) =>
setFieldValue(option.accessor, value)
}
description={option.description}
/>
))
}
]}
/>
</Stack>
)}
</Formik>

View File

@@ -0,0 +1,95 @@
import { Box, Stack } from '@mui/material';
import Grid from '@mui/material/Grid';
import React, { useContext, useEffect, useState } from 'react';
import ToolTextInput from '../../../components/input/ToolTextInput';
import ToolTextResult from '../../../components/result/ToolTextResult';
import { Formik, useFormikContext } from 'formik';
import * as Yup from 'yup';
import ToolOptions from '../../../components/options/ToolOptions';
import { compute } from './service';
import { CustomSnackBarContext } from '../../../contexts/CustomSnackBarContext';
import TextFieldWithDesc from '../../../components/options/TextFieldWithDesc';
import ToolOptionGroups from '../../../components/options/ToolOptionGroups';
import ToolInputAndResult from '../../../components/ToolInputAndResult';
const initialValues = {
dotSymbol: '.',
dashSymbol: '-'
};
export default function ToMorse() {
const [input, setInput] = useState<string>('');
const [result, setResult] = useState<string>('');
// const formRef = useRef<FormikProps<typeof initialValues>>(null);
const { showSnackBar } = useContext(CustomSnackBarContext);
const FormikListenerComponent = () => {
const { values } = useFormikContext<typeof initialValues>();
useEffect(() => {
try {
const { dotSymbol, dashSymbol } = values;
setResult(compute(input, dotSymbol, dashSymbol));
} catch (exception: unknown) {
if (exception instanceof Error)
showSnackBar(exception.message, 'error');
}
}, [values, input]);
return null; // This component doesn't render anything
};
const validationSchema = Yup.object({
// splitSeparator: Yup.string().required('The separator is required')
});
return (
<Box>
<ToolInputAndResult
input={<ToolTextInput value={input} onChange={setInput} />}
result={<ToolTextResult title={'Morse code'} value={result} />}
/>
<ToolOptions>
<Formik
initialValues={initialValues}
validationSchema={validationSchema}
onSubmit={() => {}}
>
{({ setFieldValue, values }) => (
<Stack direction={'row'} spacing={2}>
<FormikListenerComponent />
<ToolOptionGroups
groups={[
{
title: 'Short Signal',
component: (
<TextFieldWithDesc
description={
'Symbol that will correspond to the dot in Morse code.'
}
value={values.dotSymbol}
onChange={(val) => setFieldValue('dotSymbol', val)}
/>
)
},
{
title: 'Long Signal',
component: (
<TextFieldWithDesc
description={
'Symbol that will correspond to the dash in Morse code.'
}
value={values.dashSymbol}
onChange={(val) => setFieldValue('dashSymbol', val)}
/>
)
}
]}
/>
</Stack>
)}
</Formik>
</ToolOptions>
</Box>
);
}

View File

@@ -0,0 +1,13 @@
import { defineTool } from '@tools/defineTool';
import { lazy } from 'react';
// import image from '@assets/text.png';
export const tool = defineTool('string', {
name: 'String To morse',
path: 'to-morse',
// image,
description:
"World's simplest browser-based utility for converting text to Morse code. Load your text in the input form on the left and you'll instantly get Morse code in the output area. Powerful, free, and fast. Load text get Morse code.",
keywords: ['to', 'morse'],
component: lazy(() => import('./index'))
});

View File

@@ -0,0 +1,9 @@
import { encode } from 'morsee';
export const compute = (
input: string,
dotSymbol: string,
dashSymbol: string
): string => {
return encode(input).replaceAll('.', dotSymbol).replaceAll('-', dashSymbol);
};

View File

@@ -0,0 +1,50 @@
import { describe, it, expect } from 'vitest';
import { compute } from './service';
describe('compute function', () => {
it('should replace dots and dashes with specified symbols', () => {
const input = 'test';
const dotSymbol = '*';
const dashSymbol = '#';
const result = compute(input, dotSymbol, dashSymbol);
const expected = '# * *** #';
expect(result).toBe(expected);
});
it('should return an empty string for empty input', () => {
const input = '';
const dotSymbol = '*';
const dashSymbol = '#';
const result = compute(input, dotSymbol, dashSymbol);
expect(result).toBe('');
});
// Test case 3: Special characters handling
it('should handle input with special characters', () => {
const input = 'hello, world!';
const dotSymbol = '*';
const dashSymbol = '#';
const result = compute(input, dotSymbol, dashSymbol);
const expected =
'**** * *#** *#** ### ##**## / *## ### *#* *#** #** #*#*##';
expect(result).toBe(expected);
});
it('should work with different symbols for dots and dashes', () => {
const input = 'morse';
const dotSymbol = '!';
const dashSymbol = '@';
const result = compute(input, dotSymbol, dashSymbol);
const expected = '@@ @@@ !@! !!! !';
expect(result).toBe(expected);
});
it('should handle numeric input correctly', () => {
const input = '12345';
const dotSymbol = '*';
const dashSymbol = '#';
const result = compute(input, dotSymbol, dashSymbol);
const expected = '*#### **### ***## ****# *****'; // This depends on how "12345" is encoded in morse code
expect(result).toBe(expected);
});
});

View File

@@ -2,8 +2,13 @@ import { stringTools } from '../pages/string';
import { imageTools } from '../pages/image';
import { DefinedTool } from './defineTool';
import { capitalizeFirstLetter } from '../utils/string';
import { numberTools } from '../pages/number';
export const tools: DefinedTool[] = [...imageTools, ...stringTools];
export const tools: DefinedTool[] = [
...imageTools,
...stringTools,
...numberTools
];
const categoriesDescriptions: { type: string; value: string }[] = [
{
type: 'string',
@@ -14,6 +19,11 @@ const categoriesDescriptions: { type: string; value: string }[] = [
type: 'png',
value:
'Tools for working with PNG images convert PNGs to JPGs, create transparent PNGs, change PNG colors, crop, rotate, resize PNGs, and much more.'
},
{
type: 'number',
value:
'Tools for working with numbers generate number sequences, convert numbers to words and words to numbers, sort, round, factor numbers, and much more.'
}
];
export const filterTools = (