Merge branch 'main' into chesterkxng

This commit is contained in:
Ibrahima G. Coulibaly
2024-06-27 18:47:24 +01:00
committed by GitHub
53 changed files with 1838 additions and 868 deletions

2
.gitignore vendored
View File

@@ -35,3 +35,5 @@ yarn-error.log*
# vercel
.vercel
test-results

287
.idea/workspace.xml generated
View File

@@ -4,10 +4,12 @@
<option name="autoReloadType" value="SELECTIVE" />
</component>
<component name="ChangeListManager">
<list default="true" id="b30e2810-c4c1-4aad-b134-794e52cc1c7d" name="Changes" comment="fix: generate numbers">
<list default="true" id="b30e2810-c4c1-4aad-b134-794e52cc1c7d" name="Changes" comment="style: lint">
<change beforePath="$PROJECT_DIR$/.idea/workspace.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/workspace.xml" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/components/ToolLayout.tsx" beforeDir="false" afterPath="$PROJECT_DIR$/src/components/ToolLayout.tsx" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/pages/number/generate/meta.ts" beforeDir="false" afterPath="$PROJECT_DIR$/src/pages/number/generate/meta.ts" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/components/ToolInputAndResult.tsx" beforeDir="false" afterPath="$PROJECT_DIR$/src/components/ToolInputAndResult.tsx" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/components/options/ToolOptions.tsx" beforeDir="false" afterPath="$PROJECT_DIR$/src/components/options/ToolOptions.tsx" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/pages/number/generate/index.tsx" beforeDir="false" afterPath="$PROJECT_DIR$/src/pages/number/generate/index.tsx" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/pages/number/generate/service.ts" beforeDir="false" afterPath="$PROJECT_DIR$/src/pages/number/generate/service.ts" afterDir="false" />
</list>
<option name="SHOW_DIALOG" value="false" />
<option name="HIGHLIGHT_CONFLICTS" value="true" />
@@ -48,6 +50,8 @@
&quot;keyToString&quot;: {
&quot;ASKED_ADD_EXTERNAL_FILES&quot;: &quot;true&quot;,
&quot;ASKED_SHARE_PROJECT_CONFIGURATION_FILES&quot;: &quot;true&quot;,
&quot;Playwright.JoinText Component.executor&quot;: &quot;Run&quot;,
&quot;Playwright.JoinText Component.should merge text pieces with specified join character.executor&quot;: &quot;Run&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;,
@@ -69,6 +73,7 @@
&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;npm.test:e2e.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;,
@@ -96,35 +101,35 @@
<recent name="C:\Users\HP\IdeaProjects\omni-tools\public" />
</key>
<key name="MoveFile.RECENT_KEYS">
<recent name="C:\Users\HP\IdeaProjects\omni-tools\src\components" />
<recent name="C:\Users\HP\IdeaProjects\omni-tools\src\components\options" />
<recent name="C:\Users\HP\IdeaProjects\omni-tools\src\pages\images\png\change-colors-in-png" />
<recent name="C:\Users\HP\IdeaProjects\omni-tools\src\tools" />
</key>
</component>
<component name="RunManager" selected="npm.dev">
<configuration name="compute function (1)" type="JavaScriptTestRunnerVitest" temporary="true" nameIsGenerated="true">
<configuration name="JoinText Component" type="JavaScriptTestRunnerPlaywright" temporary="true" nameIsGenerated="true">
<node-interpreter value="project" />
<vitest-package value="$PROJECT_DIR$/node_modules/vitest" />
<playwright-package value="$PROJECT_DIR$/node_modules/@playwright/test" />
<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-file value="$PROJECT_DIR$/src/pages/string/join/string-join.e2e.spec.ts" />
<test-names>
<test-name value="compute function" />
<test-name value="JoinText Component" />
</test-names>
<method v="2" />
</configuration>
<configuration name="compute function" type="JavaScriptTestRunnerVitest" temporary="true" nameIsGenerated="true">
<configuration name="JoinText Component.should merge text pieces with specified join character" type="JavaScriptTestRunnerPlaywright" temporary="true" nameIsGenerated="true">
<node-interpreter value="project" />
<vitest-package value="$PROJECT_DIR$/node_modules/vitest" />
<playwright-package value="$PROJECT_DIR$/node_modules/@playwright/test" />
<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" />
<scope-kind value="TEST" />
<test-file value="$PROJECT_DIR$/src/pages/string/join/string-join.e2e.spec.ts" />
<test-names>
<test-name value="compute function" />
<test-name value="JoinText Component" />
<test-name value="should merge text pieces with specified join character" />
</test-names>
<method v="2" />
</configuration>
@@ -148,11 +153,11 @@
<envs />
<method v="2" />
</configuration>
<configuration name="test" type="js.build_tools.npm" temporary="true" nameIsGenerated="true">
<configuration name="test:e2e" type="js.build_tools.npm" temporary="true" nameIsGenerated="true">
<package-json value="$PROJECT_DIR$/package.json" />
<command value="run" />
<scripts>
<script value="test" />
<script value="test:e2e" />
</scripts>
<node-interpreter value="project" />
<envs />
@@ -161,10 +166,10 @@
<recent_temporary>
<list>
<item itemvalue="npm.dev" />
<item itemvalue="npm.test" />
<item itemvalue="npm.lint" />
<item itemvalue="Vitest.compute function (1)" />
<item itemvalue="Vitest.compute function" />
<item itemvalue="npm.test:e2e" />
<item itemvalue="Playwright.JoinText Component.should merge text pieces with specified join character" />
<item itemvalue="Playwright.JoinText Component" />
</list>
</recent_temporary>
</component>
@@ -194,110 +199,12 @@
<workItem from="1719294110005" duration="3842000" />
<workItem from="1719339559458" duration="303000" />
<workItem from="1719340295244" duration="772000" />
</task>
<task id="LOCAL-00011" summary="chore: output selector">
<option name="closed" value="true" />
<created>1718999826771</created>
<option name="number" value="00011" />
<option name="presentableId" value="LOCAL-00011" />
<option name="project" value="LOCAL" />
<updated>1718999826771</updated>
</task>
<task id="LOCAL-00012" summary="feat: text split">
<option name="closed" value="true" />
<created>1719003559965</created>
<option name="number" value="00012" />
<option name="presentableId" value="LOCAL-00012" />
<option name="project" value="LOCAL" />
<updated>1719003559965</updated>
</task>
<task id="LOCAL-00013" summary="fix: text split try catch">
<option name="closed" value="true" />
<created>1719005757859</created>
<option name="number" value="00013" />
<option name="presentableId" value="LOCAL-00013" />
<option name="project" value="LOCAL" />
<updated>1719005757859</updated>
</task>
<task id="LOCAL-00014" summary="fix: readme">
<option name="closed" value="true" />
<created>1719006274218</created>
<option name="number" value="00014" />
<option name="presentableId" value="LOCAL-00014" />
<option name="project" value="LOCAL" />
<updated>1719006274218</updated>
</task>
<task id="LOCAL-00015" summary="ubf">
<option name="closed" value="true" />
<created>1719006515710</created>
<option name="number" value="00015" />
<option name="presentableId" value="LOCAL-00015" />
<option name="project" value="LOCAL" />
<updated>1719006515710</updated>
</task>
<task id="LOCAL-00016" summary="ubf jn">
<option name="closed" value="true" />
<created>1719006829016</created>
<option name="number" value="00016" />
<option name="presentableId" value="LOCAL-00016" />
<option name="project" value="LOCAL" />
<updated>1719006829016</updated>
</task>
<task id="LOCAL-00017" summary="ubf jn">
<option name="closed" value="true" />
<created>1719007125575</created>
<option name="number" value="00017" />
<option name="presentableId" value="LOCAL-00017" />
<option name="project" value="LOCAL" />
<updated>1719007125575</updated>
</task>
<task id="LOCAL-00018" summary="feat: conventional commit">
<option name="closed" value="true" />
<created>1719007195103</created>
<option name="number" value="00018" />
<option name="presentableId" value="LOCAL-00018" />
<option name="project" value="LOCAL" />
<updated>1719007195103</updated>
</task>
<task id="LOCAL-00019" summary="test: init">
<option name="closed" value="true" />
<created>1719023377131</created>
<option name="number" value="00019" />
<option name="presentableId" value="LOCAL-00019" />
<option name="project" value="LOCAL" />
<updated>1719023377131</updated>
</task>
<task id="LOCAL-00020" summary="chore: remove prebuild">
<option name="closed" value="true" />
<created>1719023691491</created>
<option name="number" value="00020" />
<option name="presentableId" value="LOCAL-00020" />
<option name="project" value="LOCAL" />
<updated>1719023691491</updated>
</task>
<task id="LOCAL-00021" summary="chore: idea config">
<option name="closed" value="true" />
<created>1719024346455</created>
<option name="number" value="00021" />
<option name="presentableId" value="LOCAL-00021" />
<option name="project" value="LOCAL" />
<updated>1719024346455</updated>
</task>
<task id="LOCAL-00022" summary="feat: react helmet">
<option name="closed" value="true" />
<created>1719085085537</created>
<option name="number" value="00022" />
<option name="presentableId" value="LOCAL-00022" />
<option name="project" value="LOCAL" />
<updated>1719085085537</updated>
</task>
<task id="LOCAL-00023" summary="feat: tools normalized">
<option name="closed" value="true" />
<created>1719090379202</created>
<option name="number" value="00023" />
<option name="presentableId" value="LOCAL-00023" />
<option name="project" value="LOCAL" />
<updated>1719090379202</updated>
<workItem from="1719363272227" duration="390000" />
<workItem from="1719379971872" duration="8943000" />
<workItem from="1719464673797" duration="38000" />
<workItem from="1719475764139" duration="14903000" />
<workItem from="1719492452780" duration="8000" />
<workItem from="1719496624579" duration="6148000" />
</task>
<task id="LOCAL-00024" summary="feat: search tools">
<option name="closed" value="true" />
@@ -587,7 +494,111 @@
<option name="project" value="LOCAL" />
<updated>1719349760930</updated>
</task>
<option name="localTasksCounter" value="60" />
<task id="LOCAL-00060" summary="fix: merging branches">
<option name="closed" value="true" />
<created>1719350066784</created>
<option name="number" value="00060" />
<option name="presentableId" value="LOCAL-00060" />
<option name="project" value="LOCAL" />
<updated>1719350066784</updated>
</task>
<task id="LOCAL-00061" summary="feat: make responsive">
<option name="closed" value="true" />
<created>1719358195260</created>
<option name="number" value="00061" />
<option name="presentableId" value="LOCAL-00061" />
<option name="project" value="LOCAL" />
<updated>1719358195260</updated>
</task>
<task id="LOCAL-00062" summary="feat: make tool responsive">
<option name="closed" value="true" />
<created>1719359243293</created>
<option name="number" value="00062" />
<option name="presentableId" value="LOCAL-00062" />
<option name="project" value="LOCAL" />
<updated>1719359243294</updated>
</task>
<task id="LOCAL-00063" summary="chore: make tool examples responsive">
<option name="closed" value="true" />
<created>1719359368236</created>
<option name="number" value="00063" />
<option name="presentableId" value="LOCAL-00063" />
<option name="project" value="LOCAL" />
<updated>1719359368236</updated>
</task>
<task id="LOCAL-00064" summary="chore: loading screen">
<option name="closed" value="true" />
<created>1719360545177</created>
<option name="number" value="00064" />
<option name="presentableId" value="LOCAL-00064" />
<option name="project" value="LOCAL" />
<updated>1719360545177</updated>
</task>
<task id="LOCAL-00065" summary="chore: shuffle tools search">
<option name="closed" value="true" />
<created>1719363656541</created>
<option name="number" value="00065" />
<option name="presentableId" value="LOCAL-00065" />
<option name="project" value="LOCAL" />
<updated>1719363656541</updated>
</task>
<task id="LOCAL-00066" summary="refactor: toolOptions">
<option name="closed" value="true" />
<created>1719384439535</created>
<option name="number" value="00066" />
<option name="presentableId" value="LOCAL-00066" />
<option name="project" value="LOCAL" />
<updated>1719384439535</updated>
</task>
<task id="LOCAL-00067" summary="feat: playwright">
<option name="closed" value="true" />
<created>1719388760134</created>
<option name="number" value="00067" />
<option name="presentableId" value="LOCAL-00067" />
<option name="project" value="LOCAL" />
<updated>1719388760134</updated>
</task>
<task id="LOCAL-00068" summary="refactor: optimize imports">
<option name="closed" value="true" />
<created>1719388927238</created>
<option name="number" value="00068" />
<option name="presentableId" value="LOCAL-00068" />
<option name="project" value="LOCAL" />
<updated>1719388927238</updated>
</task>
<task id="LOCAL-00069" summary="feat: change gif speed">
<option name="closed" value="true" />
<created>1719488380275</created>
<option name="number" value="00069" />
<option name="presentableId" value="LOCAL-00069" />
<option name="project" value="LOCAL" />
<updated>1719488380275</updated>
</task>
<task id="LOCAL-00070" summary="chore: remove unused deps">
<option name="closed" value="true" />
<created>1719488576835</created>
<option name="number" value="00070" />
<option name="presentableId" value="LOCAL-00070" />
<option name="project" value="LOCAL" />
<updated>1719488576835</updated>
</task>
<task id="LOCAL-00071" summary="fix: misc">
<option name="closed" value="true" />
<created>1719491470485</created>
<option name="number" value="00071" />
<option name="presentableId" value="LOCAL-00071" />
<option name="project" value="LOCAL" />
<updated>1719491470485</updated>
</task>
<task id="LOCAL-00072" summary="style: lint">
<option name="closed" value="true" />
<created>1719499895862</created>
<option name="number" value="00072" />
<option name="presentableId" value="LOCAL-00072" />
<option name="project" value="LOCAL" />
<updated>1719499895862</updated>
</task>
<option name="localTasksCounter" value="73" />
<servers />
</component>
<component name="TypeScriptGeneratedFilesManager">
@@ -608,19 +619,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="fix: join text service" />
<MESSAGE value="test: join service" />
<MESSAGE value="feat: result copy and download" />
<MESSAGE value="feat: contributors graph" />
<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" />
@@ -633,7 +631,20 @@
<MESSAGE value="feat: create transparent png" />
<MESSAGE value="fix: ToolFileInput.tsx" />
<MESSAGE value="fix: generate numbers" />
<option name="LAST_COMMIT_MESSAGE" value="fix: generate numbers" />
<MESSAGE value="fix: merging branches" />
<MESSAGE value="feat: make responsive" />
<MESSAGE value="feat: make tool responsive" />
<MESSAGE value="chore: make tool examples responsive" />
<MESSAGE value="chore: loading screen" />
<MESSAGE value="chore: shuffle tools search" />
<MESSAGE value="refactor: toolOptions" />
<MESSAGE value="feat: playwright" />
<MESSAGE value="refactor: optimize imports" />
<MESSAGE value="feat: change gif speed" />
<MESSAGE value="chore: remove unused deps" />
<MESSAGE value="fix: misc" />
<MESSAGE value="style: lint" />
<option name="LAST_COMMIT_MESSAGE" value="style: lint" />
</component>
<component name="XSLT-Support.FileAssociations.UIState">
<expand />

479
package-lock.json generated
View File

@@ -12,13 +12,17 @@
"@emotion/styled": "^11.11.5",
"@mui/icons-material": "^5.15.20",
"@mui/material": "^5.15.20",
"@playwright/test": "^1.45.0",
"@types/lodash": "^4.17.5",
"@types/morsee": "^1.0.2",
"@types/omggif": "^1.0.5",
"color": "^4.2.3",
"formik": "^2.4.6",
"lodash": "^4.17.21",
"morsee": "^1.0.9",
"notistack": "^3.0.1",
"omggif": "^1.0.10",
"playwright": "^1.45.0",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-helmet": "^6.1.0",
@@ -51,6 +55,7 @@
"husky": "^9.0.11",
"postcss": "^8.4.38",
"prettier": "3.1.1",
"start-server-and-test": "^2.0.4",
"tailwindcss": "^3.4.3",
"typescript": "^5.4.5",
"vite": "^5.2.11",
@@ -1376,6 +1381,21 @@
"resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.2.tgz",
"integrity": "sha512-J4yDIIthosAsRZ5CPYP/jQvUAQtlZTTD/4suA08/FEnlxqW3sKS9iAhgsa9VYLZ6vDHn/ixJgIqRQPotoBjxIw=="
},
"node_modules/@hapi/hoek": {
"version": "9.3.0",
"resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-9.3.0.tgz",
"integrity": "sha512-/c6rf4UJlmHlC9b5BaNvzAcFv7HZ2QHaV0D4/HNlBdvFnvQq8RI4kYdhyPCl7Xj+oWvTWQ8ujhqS53LIgAe6KQ==",
"dev": true
},
"node_modules/@hapi/topo": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/@hapi/topo/-/topo-5.1.0.tgz",
"integrity": "sha512-foQZKJig7Ob0BMAYBfcJk8d77QtOe7Wo4ox7ff1lQYoNNAb6jwcY1ncdoy2e9wQZzvNy7ODZCYJkK8kzmcAnAg==",
"dev": true,
"dependencies": {
"@hapi/hoek": "^9.0.0"
}
},
"node_modules/@humanwhocodes/config-array": {
"version": "0.11.14",
"resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz",
@@ -1844,6 +1864,20 @@
"url": "https://opencollective.com/unts"
}
},
"node_modules/@playwright/test": {
"version": "1.45.0",
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.45.0.tgz",
"integrity": "sha512-TVYsfMlGAaxeUllNkywbwek67Ncf8FRGn8ZlRdO291OL3NjG9oMbfVhyP82HQF0CZLMrYsvesqoUekxdWuF9Qw==",
"dependencies": {
"playwright": "1.45.0"
},
"bin": {
"playwright": "cli.js"
},
"engines": {
"node": ">=18"
}
},
"node_modules/@polka/url": {
"version": "1.0.0-next.25",
"resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.25.tgz",
@@ -2075,6 +2109,27 @@
"win32"
]
},
"node_modules/@sideway/address": {
"version": "4.1.5",
"resolved": "https://registry.npmjs.org/@sideway/address/-/address-4.1.5.tgz",
"integrity": "sha512-IqO/DUQHUkPeixNQ8n0JA6102hT9CmaljNTPmQ1u8MEhBo/R4Q8eKLN/vGZxuebwOroDB4cbpjheD4+/sKFK4Q==",
"dev": true,
"dependencies": {
"@hapi/hoek": "^9.0.0"
}
},
"node_modules/@sideway/formula": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/@sideway/formula/-/formula-3.0.1.tgz",
"integrity": "sha512-/poHZJJVjx3L+zVD6g9KgHfYnb443oi7wLu/XKojDviHy6HOEOA6z1Trk5aR1dGcmPenJEgb2sK2I80LeS3MIg==",
"dev": true
},
"node_modules/@sideway/pinpoint": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/@sideway/pinpoint/-/pinpoint-2.0.0.tgz",
"integrity": "sha512-RNiOoTPkptFtSVzQevY/yWtZwf/RxyVnPy/OcA9HBM3MlGDnBEYL5B41H0MTn0Uec8Hi+2qUtTfG2WWZBmMejQ==",
"dev": true
},
"node_modules/@sinclair/typebox": {
"version": "0.27.8",
"resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz",
@@ -2492,6 +2547,11 @@
"undici-types": "~5.26.4"
}
},
"node_modules/@types/omggif": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/@types/omggif/-/omggif-1.0.5.tgz",
"integrity": "sha512-gDQJflz1rOgEcUXkMAl80bDGN46f5mp8GbcM5dyvq+zsFV6YRBRtmNxlJJ5mjY77T7BRkRFzdIBVmK90QYhCxA=="
},
"node_modules/@types/parse-json": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.2.tgz",
@@ -3211,6 +3271,12 @@
"node": "*"
}
},
"node_modules/asynckit": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
"dev": true
},
"node_modules/autoprefixer": {
"version": "10.4.19",
"resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.19.tgz",
@@ -3263,6 +3329,17 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/axios": {
"version": "1.7.2",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.7.2.tgz",
"integrity": "sha512-2A8QhOMrbomlDuiLeK9XibIBzuHeRcqqNOHp0Cyp5EoJ1IFDh+XZH3A6BkXtv0K4gFGCI0Y4BM7B1wOEi0Rmgw==",
"dev": true,
"dependencies": {
"follow-redirects": "^1.15.6",
"form-data": "^4.0.0",
"proxy-from-env": "^1.1.0"
}
},
"node_modules/babel-plugin-macros": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/babel-plugin-macros/-/babel-plugin-macros-3.1.0.tgz",
@@ -3311,6 +3388,12 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/bluebird": {
"version": "3.7.2",
"resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz",
"integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==",
"dev": true
},
"node_modules/brace-expansion": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
@@ -3472,6 +3555,15 @@
"node": "*"
}
},
"node_modules/check-more-types": {
"version": "2.24.0",
"resolved": "https://registry.npmjs.org/check-more-types/-/check-more-types-2.24.0.tgz",
"integrity": "sha512-Pj779qHxV2tuapviy1bSZNEL1maXr13bPYpsvSDB68HlYcYuhlDrmGd63i0JHMCLKzc7rUSNIrpdJlhVlNwrxA==",
"dev": true,
"engines": {
"node": ">= 0.8.0"
}
},
"node_modules/chokidar": {
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
@@ -3604,6 +3696,18 @@
"simple-swizzle": "^0.2.2"
}
},
"node_modules/combined-stream": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
"dev": true,
"dependencies": {
"delayed-stream": "~1.0.0"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/commander": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz",
@@ -3913,6 +4017,15 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/delayed-stream": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
"dev": true,
"engines": {
"node": ">=0.4.0"
}
},
"node_modules/dequal": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz",
@@ -3994,6 +4107,12 @@
"node": ">=8"
}
},
"node_modules/duplexer": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.2.tgz",
"integrity": "sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==",
"dev": true
},
"node_modules/eastasianwidth": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz",
@@ -4611,6 +4730,21 @@
"node": ">=0.10.0"
}
},
"node_modules/event-stream": {
"version": "3.3.4",
"resolved": "https://registry.npmjs.org/event-stream/-/event-stream-3.3.4.tgz",
"integrity": "sha512-QHpkERcGsR0T7Qm3HNJSyXKEEj8AHNxkY3PK8TS2KJvQ7NiSHe3DDpwVKKtoYprL/AreyzFBeIkBIWChAqn60g==",
"dev": true,
"dependencies": {
"duplexer": "~0.1.1",
"from": "~0",
"map-stream": "~0.1.0",
"pause-stream": "0.0.11",
"split": "0.3",
"stream-combiner": "~0.0.4",
"through": "~2.3.1"
}
},
"node_modules/execa": {
"version": "8.0.1",
"resolved": "https://registry.npmjs.org/execa/-/execa-8.0.1.tgz",
@@ -4766,6 +4900,26 @@
"integrity": "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==",
"dev": true
},
"node_modules/follow-redirects": {
"version": "1.15.6",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz",
"integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==",
"dev": true,
"funding": [
{
"type": "individual",
"url": "https://github.com/sponsors/RubenVerborgh"
}
],
"engines": {
"node": ">=4.0"
},
"peerDependenciesMeta": {
"debug": {
"optional": true
}
}
},
"node_modules/for-each": {
"version": "0.3.3",
"resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz",
@@ -4791,6 +4945,20 @@
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/form-data": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz",
"integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==",
"dev": true,
"dependencies": {
"asynckit": "^0.4.0",
"combined-stream": "^1.0.8",
"mime-types": "^2.1.12"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/formik": {
"version": "2.4.6",
"resolved": "https://registry.npmjs.org/formik/-/formik-2.4.6.tgz",
@@ -4828,6 +4996,12 @@
"url": "https://github.com/sponsors/rawify"
}
},
"node_modules/from": {
"version": "0.1.7",
"resolved": "https://registry.npmjs.org/from/-/from-0.1.7.tgz",
"integrity": "sha512-twe20eF1OxVxp/ML/kq2p1uc6KvFK/+vs8WjEbeKmV2He22MKm7YF2ANIt+EOqhJ5L3K/SuuPhk0hWQDjOM23g==",
"dev": true
},
"node_modules/fs.realpath": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
@@ -5826,6 +6000,19 @@
"jiti": "bin/jiti.js"
}
},
"node_modules/joi": {
"version": "17.13.3",
"resolved": "https://registry.npmjs.org/joi/-/joi-17.13.3.tgz",
"integrity": "sha512-otDA4ldcIx+ZXsKHWmp0YizCweVRZG96J10b0FevjfuncLO1oX59THoAmHkNubYJ+9gWsYsp5k8v4ib6oDv1fA==",
"dev": true,
"dependencies": {
"@hapi/hoek": "^9.3.0",
"@hapi/topo": "^5.1.0",
"@sideway/address": "^4.1.5",
"@sideway/formula": "^3.0.1",
"@sideway/pinpoint": "^2.0.0"
}
},
"node_modules/js-tokens": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
@@ -5926,6 +6113,15 @@
"json-buffer": "3.0.1"
}
},
"node_modules/lazy-ass": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/lazy-ass/-/lazy-ass-1.6.0.tgz",
"integrity": "sha512-cc8oEVoctTvsFZ/Oje/kGnHbpWHYBe8IAJe4C0QNc3t8uM/0Y8+erSz/7Y1ALuXTEZTMvxXwO6YbX1ey3ujiZw==",
"dev": true,
"engines": {
"node": "> 0.8"
}
},
"node_modules/levn": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz",
@@ -6095,6 +6291,12 @@
"@jridgewell/sourcemap-codec": "^1.4.15"
}
},
"node_modules/map-stream": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/map-stream/-/map-stream-0.1.0.tgz",
"integrity": "sha512-CkYQrPYZfWnu/DAmVCpTSX/xHpKZ80eKh2lAkyA6AJTef6bW+6JpbQZN5rofum7da+SyN1bi5ctTm+lTfcCW3g==",
"dev": true
},
"node_modules/meow": {
"version": "12.1.1",
"resolved": "https://registry.npmjs.org/meow/-/meow-12.1.1.tgz",
@@ -6135,6 +6337,27 @@
"node": ">=8.6"
}
},
"node_modules/mime-db": {
"version": "1.52.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
"dev": true,
"engines": {
"node": ">= 0.6"
}
},
"node_modules/mime-types": {
"version": "2.1.35",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
"dev": true,
"dependencies": {
"mime-db": "1.52.0"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/mimic-fn": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-4.0.0.tgz",
@@ -6470,6 +6693,11 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/omggif": {
"version": "1.0.10",
"resolved": "https://registry.npmjs.org/omggif/-/omggif-1.0.10.tgz",
"integrity": "sha512-LMJTtvgc/nugXj0Vcrrs68Mn2D1r0zf630VNtqtpI1FEO7e+O9FP4gqs9AcnBaSEeoHIPm28u6qgPR0oyEpGSw=="
},
"node_modules/once": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
@@ -6646,6 +6874,15 @@
"node": "*"
}
},
"node_modules/pause-stream": {
"version": "0.0.11",
"resolved": "https://registry.npmjs.org/pause-stream/-/pause-stream-0.0.11.tgz",
"integrity": "sha512-e3FBlXLmN/D1S+zHzanP4E/4Z60oFAa3O051qt1pxa7DEJWKAyil6upYVXCWadEnuoqa4Pkc9oUx9zsxYeRv8A==",
"dev": true,
"dependencies": {
"through": "~2.3"
}
},
"node_modules/picocolors": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.1.tgz",
@@ -6692,6 +6929,47 @@
"pathe": "^1.1.2"
}
},
"node_modules/playwright": {
"version": "1.45.0",
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.45.0.tgz",
"integrity": "sha512-4z3ac3plDfYzGB6r0Q3LF8POPR20Z8D0aXcxbJvmfMgSSq1hkcgvFRXJk9rUq5H/MJ0Ktal869hhOdI/zUTeLA==",
"dependencies": {
"playwright-core": "1.45.0"
},
"bin": {
"playwright": "cli.js"
},
"engines": {
"node": ">=18"
},
"optionalDependencies": {
"fsevents": "2.3.2"
}
},
"node_modules/playwright-core": {
"version": "1.45.0",
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.45.0.tgz",
"integrity": "sha512-lZmHlFQ0VYSpAs43dRq1/nJ9G/6SiTI7VPqidld9TDefL9tX87bTKExWZZUF5PeRyqtXqd8fQi2qmfIedkwsNQ==",
"bin": {
"playwright-core": "cli.js"
},
"engines": {
"node": ">=18"
}
},
"node_modules/playwright/node_modules/fsevents": {
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
"hasInstallScript": true,
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
}
},
"node_modules/possible-typed-array-names": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.0.0.tgz",
@@ -6949,6 +7227,27 @@
"resolved": "https://registry.npmjs.org/property-expr/-/property-expr-2.0.6.tgz",
"integrity": "sha512-SVtmxhRE/CGkn3eZY1T6pC8Nln6Fr/lu1mKSgRud0eC73whjGfoAogbn78LkD8aFL0zz3bAFerKSnOl7NlErBA=="
},
"node_modules/proxy-from-env": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
"dev": true
},
"node_modules/ps-tree": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/ps-tree/-/ps-tree-1.2.0.tgz",
"integrity": "sha512-0VnamPPYHl4uaU/nSFeZZpR21QAWRz+sRv4iW9+v/GS/J5U5iZB5BNN6J0RMoOvdx2gWM2+ZFMIm58q24e4UYA==",
"dev": true,
"dependencies": {
"event-stream": "=3.3.4"
},
"bin": {
"ps-tree": "bin/ps-tree.js"
},
"engines": {
"node": ">= 0.10"
}
},
"node_modules/punycode": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
@@ -7289,6 +7588,15 @@
"queue-microtask": "^1.2.2"
}
},
"node_modules/rxjs": {
"version": "7.8.1",
"resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz",
"integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==",
"dev": true,
"dependencies": {
"tslib": "^2.1.0"
}
},
"node_modules/safe-array-concat": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.2.tgz",
@@ -7492,6 +7800,18 @@
"node": ">=0.10.0"
}
},
"node_modules/split": {
"version": "0.3.3",
"resolved": "https://registry.npmjs.org/split/-/split-0.3.3.tgz",
"integrity": "sha512-wD2AeVmxXRBoX44wAycgjVpMhvbwdI2aZjCkvfNcH1YqHQvJVa1duWc73OyVGJUc05fhFaTZeQ/PYsrmyH0JVA==",
"dev": true,
"dependencies": {
"through": "2"
},
"engines": {
"node": "*"
}
},
"node_modules/split2": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz",
@@ -7507,6 +7827,137 @@
"integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==",
"dev": true
},
"node_modules/start-server-and-test": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/start-server-and-test/-/start-server-and-test-2.0.4.tgz",
"integrity": "sha512-CKNeBTcP0hVqIlNismHMudb9q3lLdAjcVPO13/7gfI66fcJpeIb/o4NzQd1JK/CD+lfWVqr10ZH9Y14+OwlJuw==",
"dev": true,
"dependencies": {
"arg": "^5.0.2",
"bluebird": "3.7.2",
"check-more-types": "2.24.0",
"debug": "4.3.5",
"execa": "5.1.1",
"lazy-ass": "1.6.0",
"ps-tree": "1.2.0",
"wait-on": "7.2.0"
},
"bin": {
"server-test": "src/bin/start.js",
"start-server-and-test": "src/bin/start.js",
"start-test": "src/bin/start.js"
},
"engines": {
"node": ">=16"
}
},
"node_modules/start-server-and-test/node_modules/execa": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz",
"integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==",
"dev": true,
"dependencies": {
"cross-spawn": "^7.0.3",
"get-stream": "^6.0.0",
"human-signals": "^2.1.0",
"is-stream": "^2.0.0",
"merge-stream": "^2.0.0",
"npm-run-path": "^4.0.1",
"onetime": "^5.1.2",
"signal-exit": "^3.0.3",
"strip-final-newline": "^2.0.0"
},
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/sindresorhus/execa?sponsor=1"
}
},
"node_modules/start-server-and-test/node_modules/get-stream": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz",
"integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==",
"dev": true,
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/start-server-and-test/node_modules/human-signals": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz",
"integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==",
"dev": true,
"engines": {
"node": ">=10.17.0"
}
},
"node_modules/start-server-and-test/node_modules/is-stream": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz",
"integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==",
"dev": true,
"engines": {
"node": ">=8"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/start-server-and-test/node_modules/mimic-fn": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz",
"integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==",
"dev": true,
"engines": {
"node": ">=6"
}
},
"node_modules/start-server-and-test/node_modules/npm-run-path": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz",
"integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==",
"dev": true,
"dependencies": {
"path-key": "^3.0.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/start-server-and-test/node_modules/onetime": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz",
"integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==",
"dev": true,
"dependencies": {
"mimic-fn": "^2.1.0"
},
"engines": {
"node": ">=6"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/start-server-and-test/node_modules/signal-exit": {
"version": "3.0.7",
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz",
"integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==",
"dev": true
},
"node_modules/start-server-and-test/node_modules/strip-final-newline": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz",
"integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==",
"dev": true,
"engines": {
"node": ">=6"
}
},
"node_modules/std-env": {
"version": "3.7.0",
"resolved": "https://registry.npmjs.org/std-env/-/std-env-3.7.0.tgz",
@@ -7525,6 +7976,15 @@
"node": ">= 0.4"
}
},
"node_modules/stream-combiner": {
"version": "0.0.4",
"resolved": "https://registry.npmjs.org/stream-combiner/-/stream-combiner-0.0.4.tgz",
"integrity": "sha512-rT00SPnTVyRsaSz5zgSPma/aHSOic5U1prhYdRy5HS2kTZviFpmDgzilbtsJsxiroqACmayynDN/9VzIbX5DOw==",
"dev": true,
"dependencies": {
"duplexer": "~0.1.1"
}
},
"node_modules/string-width": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz",
@@ -8422,6 +8882,25 @@
}
}
},
"node_modules/wait-on": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/wait-on/-/wait-on-7.2.0.tgz",
"integrity": "sha512-wCQcHkRazgjG5XoAq9jbTMLpNIjoSlZslrJ2+N9MxDsGEv1HnFoVjOCexL0ESva7Y9cu350j+DWADdk54s4AFQ==",
"dev": true,
"dependencies": {
"axios": "^1.6.1",
"joi": "^17.11.0",
"lodash": "^4.17.21",
"minimist": "^1.2.8",
"rxjs": "^7.8.1"
},
"bin": {
"wait-on": "bin/wait-on"
},
"engines": {
"node": ">=12.0.0"
}
},
"node_modules/webidl-conversions": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz",

View File

@@ -17,6 +17,8 @@
"build": "tsc && vite build",
"serve": "vite preview",
"test": "vitest",
"test:e2e": "start-server-and-test dev http://localhost:5173 test:e2e:run",
"test:e2e:run": "playwright test",
"test:ui": "vitest --ui",
"script:create:tool": "node scripts/create-tool.mjs",
"lint": "eslint src --max-warnings=0",
@@ -28,13 +30,17 @@
"@emotion/styled": "^11.11.5",
"@mui/icons-material": "^5.15.20",
"@mui/material": "^5.15.20",
"@playwright/test": "^1.45.0",
"@types/lodash": "^4.17.5",
"@types/morsee": "^1.0.2",
"@types/omggif": "^1.0.5",
"color": "^4.2.3",
"formik": "^2.4.6",
"lodash": "^4.17.21",
"morsee": "^1.0.9",
"notistack": "^3.0.1",
"omggif": "^1.0.10",
"playwright": "^1.45.0",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-helmet": "^6.1.0",
@@ -67,6 +73,7 @@
"husky": "^9.0.11",
"postcss": "^8.4.38",
"prettier": "3.1.1",
"start-server-and-test": "^2.0.4",
"tailwindcss": "^3.4.3",
"typescript": "^5.4.5",
"vite": "^5.2.11",

27
playwright.config.ts Normal file
View File

@@ -0,0 +1,27 @@
import { defineConfig, devices } from '@playwright/test';
export default defineConfig({
testDir: './src',
testMatch: /\.e2e\.(spec\.)?ts$/,
fullyParallel: true,
retries: 1,
use: {
baseURL: 'http://localhost:5173',
trace: 'on-first-retry'
},
projects: [
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] }
},
{
name: 'firefox',
use: { ...devices['Desktop Firefox'] }
},
{
name: 'webkit',
use: { ...devices['Desktop Safari'] }
}
]
});

View File

@@ -6,6 +6,7 @@ import { useState } from 'react';
import { DefinedTool } from '@tools/defineTool';
import { filterTools, tools } from '@tools/index';
import { useNavigate } from 'react-router-dom';
import _ from 'lodash';
const exampleTools: { label: string; url: string }[] = [
{
@@ -13,7 +14,7 @@ const exampleTools: { label: string; url: string }[] = [
url: '/png/create-transparent'
},
{ label: 'Convert text to morse code', url: '/string/to-morse' },
{ label: 'Change GIF speed', url: '' },
{ label: 'Change GIF speed', url: '/gif/change-speed' },
{ label: 'Pick a random item', url: '' },
{ label: 'Find and replace text', url: '' },
{ label: 'Convert emoji to image', url: '' },
@@ -23,24 +24,36 @@ const exampleTools: { label: string; url: string }[] = [
];
export default function Hero() {
const [inputValue, setInputValue] = useState<string>('');
const [filteredTools, setFilteredTools] = useState<DefinedTool[]>(tools);
const [filteredTools, setFilteredTools] = useState<DefinedTool[]>(
_.shuffle(tools)
);
const navigate = useNavigate();
const handleInputChange = (
event: React.ChangeEvent<{}>,
newInputValue: string
) => {
setInputValue(newInputValue);
setFilteredTools(filterTools(tools, newInputValue));
setFilteredTools(_.shuffle(filterTools(tools, newInputValue)));
};
return (
<Box width={'60%'}>
<Stack mb={1} direction={'row'} spacing={1}>
<Typography fontSize={30}>Transform Your Workflow with </Typography>
<Typography fontSize={30} color={'primary'}>
<Box width={{ xs: '90%', md: '80%', lg: '60%' }}>
<Stack mb={1} direction={'row'} spacing={1} justifyContent={'center'}>
<Typography sx={{ textAlign: 'center' }} fontSize={{ xs: 25, md: 30 }}>
Transform Your Workflow with{' '}
<Typography
fontSize={{ xs: 25, md: 30 }}
display={'inline'}
color={'primary'}
>
Omni Tools
</Typography>
</Typography>
</Stack>
<Typography fontSize={20} mb={2}>
<Typography
sx={{ textAlign: 'center' }}
fontSize={{ xs: 15, md: 20 }}
mb={2}
>
Boost your productivity with Omni Tools, the ultimate toolkit for
getting things done quickly! Access thousands of user-friendly utilities
for editing images, text, lists, and data, all directly from your
@@ -51,6 +64,7 @@ export default function Hero() {
sx={{ mb: 2 }}
autoHighlight
options={filteredTools}
inputValue={inputValue}
getOptionLabel={(option) => option.name}
renderInput={(params) => (
<TextField
@@ -76,7 +90,14 @@ export default function Hero() {
/>
<Grid container spacing={2} mt={2}>
{exampleTools.map((tool) => (
<Grid onClick={() => navigate(tool.url)} item xs={4} key={tool.label}>
<Grid
onClick={() => navigate(tool.url)}
item
xs={12}
md={6}
lg={4}
key={tool.label}
>
<Box
sx={{
display: 'flex',

View File

@@ -0,0 +1,51 @@
#spinner {
display: flex;
align-items: center;
justify-content: space-between;
margin-top: 40px;
width: 56px;
}
#spinner > div {
width: 12px;
height: 12px;
background-color: #1e96f7;
border-radius: 100%;
display: inline-block;
-webkit-animation: fuse-bouncedelay 1s infinite ease-in-out both;
animation: fuse-bouncedelay 1s infinite ease-in-out both;
}
#spinner .bounce1 {
-webkit-animation-delay: -0.32s;
animation-delay: -0.32s;
}
#spinner .bounce2 {
-webkit-animation-delay: -0.16s;
animation-delay: -0.16s;
}
@-webkit-keyframes fuse-bouncedelay {
0%,
80%,
100% {
-webkit-transform: scale(0);
}
40% {
-webkit-transform: scale(1);
}
}
@keyframes fuse-bouncedelay {
0%,
80%,
100% {
-webkit-transform: scale(0);
transform: scale(0);
}
40% {
-webkit-transform: scale(1);
transform: scale(1);
}
}

View File

@@ -1,32 +1,20 @@
import Typography from '@mui/material/Typography';
import { useState } from 'react';
import Box from '@mui/material/Box';
import { useTimeout } from '../hooks';
export type FuseLoadingProps = {
delay?: number;
className?: string;
};
/**
* FuseLoading displays a loading state with an optional delay
*/
function FuseLoading(props: FuseLoadingProps) {
const { delay = 0, className } = props;
const [showLoading, setShowLoading] = useState(!delay);
useTimeout(() => {
setShowLoading(true);
}, delay);
import './Loading.css';
function Loading() {
return (
<div>
<Typography
className="text-13 sm:text-20 -mb-16 font-medium"
color="text.secondary"
<Box
sx={{
width: '100%',
height: 0.8 * window.innerHeight,
display: 'flex',
flexDirection: 'column',
justifyContent: 'center',
alignItems: 'center'
}}
>
Loading
</Typography>
<Typography color="primary">Loading</Typography>
<Box
id="spinner"
sx={{
@@ -39,8 +27,8 @@ function FuseLoading(props: FuseLoadingProps) {
<div className="bounce2" />
<div className="bounce3" />
</Box>
</div>
</Box>
);
}
export default FuseLoading;
export default Loading;

View File

@@ -1,15 +1,56 @@
import React from 'react';
import React, { useState } from 'react';
import AppBar from '@mui/material/AppBar';
import Toolbar from '@mui/material/Toolbar';
import Typography from '@mui/material/Typography';
import Button from '@mui/material/Button';
import IconButton from '@mui/material/IconButton';
import MenuIcon from '@mui/icons-material/Menu';
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';
import {
Drawer,
List,
ListItemButton,
ListItemText,
Stack
} from '@mui/material';
import useMediaQuery from '@mui/material/useMediaQuery';
import { useTheme } from '@mui/material/styles';
const Navbar: React.FC = () => {
const navigate = useNavigate();
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down('md'));
const [drawerOpen, setDrawerOpen] = useState(false);
const toggleDrawer = (open: boolean) => () => {
setDrawerOpen(open);
};
const drawerList = (
<List>
<ListItemButton onClick={() => navigate('/features')}>
<ListItemText primary="Features" />
</ListItemButton>
<ListItemButton onClick={() => navigate('/about-us')}>
<ListItemText primary="About Us" />
</ListItemButton>
<ListItemButton
component="a"
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>
</ListItemButton>
</List>
);
return (
<AppBar
position="static"
@@ -24,6 +65,20 @@ const Navbar: React.FC = () => {
>
OmniTools
</Typography>
{isMobile ? (
<>
<IconButton color="inherit" onClick={toggleDrawer(true)}>
<MenuIcon />
</IconButton>
<Drawer
anchor="right"
open={drawerOpen}
onClose={toggleDrawer(false)}
>
{drawerList}
</Drawer>
</>
) : (
<Stack direction={'row'}>
<Button color="inherit">
<Link
@@ -55,6 +110,7 @@ const Navbar: React.FC = () => {
<Typography variant="button">Star us</Typography>
</IconButton>
</Stack>
)}
</Toolbar>
</AppBar>
);

View File

@@ -14,7 +14,7 @@ interface BreadcrumbComponentProps {
const ToolBreadcrumb: React.FC<BreadcrumbComponentProps> = ({ items }) => {
const theme = useTheme();
return (
<Breadcrumbs aria-label="breadcrumb">
<Breadcrumbs>
{items.map((item, index) => {
if (index === items.length - 1 || !item.link) {
return (

View File

@@ -1,7 +1,8 @@
import { Button, Box, Stack } from '@mui/material';
import { Box, Button } from '@mui/material';
import Typography from '@mui/material/Typography';
import ToolBreadcrumb from './ToolBreadcrumb';
import { capitalizeFirstLetter } from '../utils/string';
import Grid from '@mui/material/Grid';
interface ToolHeaderProps {
title: string;
@@ -12,17 +13,23 @@ interface ToolHeaderProps {
function ToolLinks() {
return (
<Box display="flex" gap={2} my={2}>
<Button variant="outlined" href="#tool">
<Grid container spacing={2} mt={1}>
<Grid item md={12} lg={4}>
<Button fullWidth variant="outlined" href="#tool">
Use This Tool
</Button>
<Button variant="outlined" href="#examples">
</Grid>
<Grid item md={12} lg={4}>
<Button fullWidth variant="outlined" href="#examples">
See Examples
</Button>
<Button variant="outlined" href="#tour">
</Grid>
<Grid item md={12} lg={4}>
<Button fullWidth variant="outlined" href="#tour">
Learn How to Use
</Button>
</Box>
</Grid>
</Grid>
);
}
@@ -44,16 +51,23 @@ export default function ToolHeader({
{ title }
]}
/>
<Stack direction={'row'} alignItems={'center'} spacing={2}>
<Box>
<Grid mt={1} container spacing={2}>
<Grid item xs={12} md={8}>
<Typography mb={2} fontSize={30} color={'primary'}>
{title}
</Typography>
<Typography fontSize={20}>{description}</Typography>
<ToolLinks />
</Grid>
{image && (
<Grid item xs={12} md={4}>
<Box sx={{ display: 'flex', justifyContent: 'center' }}>
<img width={'250'} src={image} />
</Box>
{image && <img width={'250'} src={image} />}
</Stack>
</Grid>
)}
</Grid>
</Box>
);
}

View File

@@ -5,7 +5,7 @@ interface ExampleProps {
description: string;
}
export default function Example({ title, description }: ExampleProps) {
export default function ToolInfo({ title, description }: ExampleProps) {
return (
<Stack direction={'row'} alignItems={'center'} spacing={2} mt={4}>
<Box>

View File

@@ -5,15 +5,17 @@ export default function ToolInputAndResult({
input,
result
}: {
input: ReactNode;
input?: ReactNode;
result: ReactNode;
}) {
return (
<Grid id="tool" container spacing={2}>
<Grid item xs={6}>
{input && (
<Grid item xs={12} md={6}>
{input}
</Grid>
<Grid item xs={6}>
)}
<Grid item xs={12} md={input ? 6 : 12}>
{result}
</Grid>
</Grid>

View File

@@ -2,7 +2,7 @@ import { Box } from '@mui/material';
import React, { ReactNode } from 'react';
import { Helmet } from 'react-helmet';
import ToolHeader from './ToolHeader';
import Separator from '@tools/Separator';
import Separator from './Separator';
import AllTools from './allTools/AllTools';
import { getToolsByCategory } from '@tools/index';
import { capitalizeFirstLetter } from '../utils/string';

View File

@@ -21,7 +21,7 @@ export default function AllTools({ title, toolCards }: AllToolsProps) {
<Stack direction={'row'} alignItems={'center'} spacing={2}>
<Grid container spacing={2}>
{toolCards.map((card, index) => (
<Grid item xs={4} key={index}>
<Grid item xs={12} md={6} lg={4} key={index}>
<ToolCard
title={card.title}
description={card.description}

View File

@@ -1,4 +1,4 @@
import { Box, Link, Card, CardContent, Typography } from '@mui/material';
import { Box, Card, CardContent, Link, Typography } from '@mui/material';
import { ToolCardProps } from './AllTools';
import ChevronRightIcon from '@mui/icons-material/ChevronRight';
import { useNavigate } from 'react-router-dom';

View File

@@ -1,11 +1,11 @@
import { ExampleCardProps } from './Examples';
import {
Box,
Stack,
Card,
CardContent,
Typography,
Stack,
TextField,
Typography,
useTheme
} from '@mui/material';
import ArrowDownwardIcon from '@mui/icons-material/ArrowDownward';

View File

@@ -41,7 +41,7 @@ export default function Examples({
<Stack direction={'row'} alignItems={'center'} spacing={2}>
<Grid container spacing={2}>
{exampleCards.map((card, index) => (
<Grid item xs={4} key={index}>
<Grid item xs={12} md={6} lg={4} key={index}>
<ExampleCard
title={card.title}
description={card.description}

View File

@@ -1,4 +1,4 @@
import { Box, styled, TextField, useTheme } from '@mui/material';
import { Box, useTheme } from '@mui/material';
import Typography from '@mui/material/Typography';
import React, { useContext, useEffect, useRef, useState } from 'react';
import InputHeader from '../InputHeader';

View File

@@ -1,8 +1,4 @@
import { Box, Stack, TextField } from '@mui/material';
import Typography from '@mui/material/Typography';
import Button from '@mui/material/Button';
import PublishIcon from '@mui/icons-material/Publish';
import ContentPasteIcon from '@mui/icons-material/ContentPaste';
import { Box, TextField } from '@mui/material';
import React, { useContext, useRef } from 'react';
import { CustomSnackBarContext } from '../../contexts/CustomSnackBarContext';
import InputHeader from '../InputHeader';
@@ -54,6 +50,7 @@ export default function ToolTextInput({
fullWidth
multiline
rows={10}
inputProps={{ 'data-testid': 'text-input' }}
/>
<InputFooter handleCopy={handleCopy} handleImport={handleImportClick} />
<input

View File

@@ -1,4 +1,4 @@
import React, { useState, ChangeEvent, useRef } from 'react';
import React, { ChangeEvent, useRef, useState } from 'react';
import { Box, Stack, TextField } from '@mui/material';
import PaletteIcon from '@mui/icons-material/Palette';
import IconButton from '@mui/material/IconButton';

View File

@@ -1,10 +1,6 @@
import { SplitOperatorType } from '../../pages/string/split/service';
import { Box, Stack } from '@mui/material';
import { Field } from 'formik';
import Typography from '@mui/material/Typography';
import { Box } from '@mui/material';
import React from 'react';
import TextFieldWithDesc from './TextFieldWithDesc';
import { globalDescriptionFontSize } from '../../config/uiConfig';
import SimpleRadio from './SimpleRadio';
const RadioWithTextField = <T,>({
@@ -38,7 +34,10 @@ const RadioWithTextField = <T,>({
/>
<TextFieldWithDesc
value={value}
onChange={onTextChange}
onChange={(val) => {
if (typeof val === 'string') onTextChange(val);
else onTextChange(val.target.value);
}}
description={description}
/>
</Box>

View File

@@ -1,18 +1,20 @@
import { Box, TextField } from '@mui/material';
import { Box, TextField, TextFieldProps } from '@mui/material';
import Typography from '@mui/material/Typography';
import React from 'react';
type OwnProps = {
description: string;
value: string | number;
onChange: (value: string) => void;
placeholder?: string;
};
const TextFieldWithDesc = ({
description,
value,
onChange,
placeholder
}: {
description: string;
value: string;
onChange: (value: string) => void;
placeholder?: string;
}) => {
placeholder,
...props
}: TextFieldProps & OwnProps) => {
return (
<Box>
<TextField
@@ -20,6 +22,7 @@ const TextFieldWithDesc = ({
sx={{ backgroundColor: 'white' }}
value={value}
onChange={(event) => onChange(event.target.value)}
{...props}
/>
<Typography fontSize={12} mt={1}>
{description}

View File

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

View File

@@ -1,10 +1,52 @@
import { Box, Stack, useTheme } from '@mui/material';
import SettingsIcon from '@mui/icons-material/Settings';
import Typography from '@mui/material/Typography';
import React, { ReactNode } from 'react';
import React, { ReactNode, RefObject, useContext, useEffect } from 'react';
import { Formik, FormikProps, FormikValues, useFormikContext } from 'formik';
import ToolOptionGroups, { ToolOptionGroup } from './ToolOptionGroups';
import { CustomSnackBarContext } from '../../contexts/CustomSnackBarContext';
export default function ToolOptions({ children }: { children: ReactNode }) {
const FormikListenerComponent = <T,>({
initialValues,
input,
compute
}: {
initialValues: T;
input: any;
compute: (optionsValues: T, input: any) => void;
}) => {
const { values } = useFormikContext<typeof initialValues>();
const { showSnackBar } = useContext(CustomSnackBarContext);
useEffect(() => {
try {
compute(values, input);
} catch (exception: unknown) {
if (exception instanceof Error) showSnackBar(exception.message, 'error');
}
}, [values, input]);
return null; // This component doesn't render anything
};
export default function ToolOptions<T extends FormikValues>({
children,
initialValues,
validationSchema,
compute,
input,
getGroups,
formRef
}: {
children?: ReactNode;
initialValues: T;
validationSchema: any | (() => any);
compute: (optionsValues: T, input: any) => void;
input?: any;
getGroups: (formikProps: FormikProps<T>) => ToolOptionGroup[];
formRef?: RefObject<FormikProps<T>>;
}) {
const theme = useTheme();
return (
<Box
sx={{
@@ -19,7 +61,26 @@ export default function ToolOptions({ children }: { children: ReactNode }) {
<SettingsIcon />
<Typography fontSize={22}>Tool options</Typography>
</Stack>
<Box mt={2}>{children}</Box>
<Box mt={2}>
<Formik
innerRef={formRef}
initialValues={initialValues}
validationSchema={validationSchema}
onSubmit={() => {}}
>
{(formikProps) => (
<Stack direction={'row'} spacing={2}>
<FormikListenerComponent
compute={compute}
input={input}
initialValues={initialValues}
/>
<ToolOptionGroups groups={getGroups(formikProps)} />
{children}
</Stack>
)}
</Formik>
</Box>
</Box>
);
}

View File

@@ -1,8 +1,4 @@
import Typography from '@mui/material/Typography';
import { Box, Stack, TextField } from '@mui/material';
import Button from '@mui/material/Button';
import DownloadIcon from '@mui/icons-material/Download';
import ContentPasteIcon from '@mui/icons-material/ContentPaste';
import { Box, TextField } from '@mui/material';
import React, { useContext } from 'react';
import { CustomSnackBarContext } from '../../contexts/CustomSnackBarContext';
import InputHeader from '../InputHeader';
@@ -40,7 +36,13 @@ export default function ToolTextResult({
return (
<Box>
<InputHeader title={title} />
<TextField value={value} fullWidth multiline rows={10} />
<TextField
value={value}
fullWidth
multiline
rows={10}
inputProps={{ 'data-testid': 'text-result' }}
/>
<ResultFooter handleCopy={handleCopy} handleDownload={handleDownload} />
</Box>
);

View File

@@ -1,5 +1,4 @@
import { RouteObject } from 'react-router-dom';
import { Navigate } from 'react-router-dom';
import { Navigate, RouteObject } from 'react-router-dom';
import { lazy } from 'react';
const Home = lazy(() => import('../pages/home'));

View File

@@ -1,4 +1,4 @@
import { Box, Card, CardContent, Stack } from '@mui/material';
import { Box, Card, CardContent } from '@mui/material';
import Grid from '@mui/material/Grid';
import Typography from '@mui/material/Typography';
import { Link, useNavigate } from 'react-router-dom';
@@ -11,7 +11,7 @@ export default function Home() {
return (
<Box
padding={5}
padding={{ xs: 1, md: 3, lg: 5 }}
display={'flex'}
flexDirection={'column'}
alignItems={'center'}
@@ -21,8 +21,8 @@ export default function Home() {
<Hero />
<Grid width={'80%'} container mt={2} spacing={2}>
{getToolsByCategory().map((category) => (
<Grid key={category.type} item xs={6}>
<Card>
<Grid key={category.type} item xs={12} md={6}>
<Card sx={{ height: '100%' }}>
<CardContent>
<Link
style={{ fontSize: 20 }}
@@ -31,20 +31,22 @@ export default function Home() {
{category.title}
</Link>
<Typography sx={{ mt: 2 }}>{category.description}</Typography>
<Stack
mt={2}
direction={'row'}
justifyContent={'space-between'}
>
<Grid mt={1} container spacing={2}>
<Grid item xs={12} md={6}>
<Button
fullWidth
onClick={() => navigate('/categories/' + category.type)}
variant={'contained'}
>{`See all ${category.title}`}</Button>
</Grid>
<Grid item xs={12} md={6}>
<Button
fullWidth
onClick={() => navigate(category.example.path)}
variant={'outlined'}
>{`Try ${category.example.title}`}</Button>
</Stack>
</Grid>
</Grid>
</CardContent>
</Card>
</Grid>

View File

@@ -1,15 +1,13 @@
import { Box } from '@mui/material';
import React, { useEffect, useState } from 'react';
import React, { 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',
@@ -23,11 +21,8 @@ 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, toColor, similarity } = values;
useEffect(() => {
const compute = (optionsValues: typeof initialValues, input: any) => {
const { fromColor, toColor, similarity } = optionsValues;
let fromRgb: [number, number, number];
let toRgb: [number, number, number];
try {
@@ -91,18 +86,16 @@ export default function ChangeColorsInPng() {
canvas.toBlob((blob) => {
if (blob) {
const newFile = new File([blob], file.name, { type: 'image/png' });
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
@@ -122,17 +115,9 @@ export default function ChangeColorsInPng() {
/>
}
/>
<ToolOptions>
<Formik
initialValues={initialValues}
validationSchema={validationSchema}
onSubmit={() => {}}
>
{({ setFieldValue, values }) => (
<Box>
{input && <FormikListenerComponent input={input} />}
<ToolOptionGroups
groups={[
<ToolOptions
compute={compute}
getGroups={({ values, setFieldValue }) => [
{
title: 'From color and to color',
component: (
@@ -158,11 +143,10 @@ export default function ChangeColorsInPng() {
)
}
]}
initialValues={initialValues}
input={input}
validationSchema={validationSchema}
/>
</Box>
)}
</Formik>
</ToolOptions>
</Box>
);
}

View File

@@ -1,15 +1,13 @@
import { Box } from '@mui/material';
import React, { useEffect, useState } from 'react';
import React, { 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',
@@ -22,11 +20,9 @@ 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;
const compute = (optionsValues: typeof initialValues, input: any) => {
const { fromColor, similarity } = optionsValues;
useEffect(() => {
let fromRgb: [number, number, number];
try {
//@ts-ignore
@@ -91,9 +87,6 @@ export default function ChangeColorsInPng() {
};
processImage(input, fromRgb, Number(similarity));
}, [input, fromColor]);
return null;
};
return (
@@ -115,17 +108,9 @@ export default function ChangeColorsInPng() {
/>
}
/>
<ToolOptions>
<Formik
initialValues={initialValues}
validationSchema={validationSchema}
onSubmit={() => {}}
>
{({ setFieldValue, values }) => (
<Box>
{input && <FormikListenerComponent input={input} />}
<ToolOptionGroups
groups={[
<ToolOptions
compute={compute}
getGroups={({ values, setFieldValue }) => [
{
title: 'From color and similarity',
component: (
@@ -146,11 +131,10 @@ export default function ChangeColorsInPng() {
)
}
]}
initialValues={initialValues}
input={input}
validationSchema={validationSchema}
/>
</Box>
)}
</Formik>
</ToolOptions>
</Box>
);
}

View File

@@ -114,3 +114,4 @@ export function Sort(
}
return result;
}

View File

@@ -23,9 +23,8 @@ describe('numericSort function', () => {
const separator = ' - ';
const removeDuplicated: boolean = true;
const result = numericSort(array, increasing, separator, removeDuplicated);
expect(result).toBe('9 - 7 - 6 - 4 - 2');
const result = lengthSort(array, increasing, separator, removeDuplicated);
expect(result).toBe('3, 12, 126, 1523, 415689521');
});
it('should sort a list with numbers and characters and remove duplicated elements', () => {
@@ -35,8 +34,30 @@ describe('numericSort function', () => {
const removeDuplicated: boolean = true;
const result = numericSort(array, increasing, separator, removeDuplicated);
expect(result).toBe('5 6 7 9 d h n p');
const result = lengthSort(array, increasing, separator, removeDuplicated);
expect(result).toBe('d p h 9 7 ddd nfg 6555 5556');
});
});
// Define test cases for the alphabeticSort function
describe('alphabeticSort function', () => {
// NON CASE SENSITIVE TEST
it('should sort a list of string in increasing order with comma separator ', () => {
const array: any[] = ['apple', 'pineaple', 'lemon', 'orange'];
const increasing: boolean = true;
const separator = ', ';
const removeDuplicated: boolean = false;
const caseSensitive: boolean = false;
const result = alphabeticSort(
array,
increasing,
separator,
removeDuplicated,
caseSensitive
);
expect(result).toBe('apple, lemon, orange, pineaple');
});
// Define test cases for the lengthSort function
@@ -72,6 +93,56 @@ describe('numericSort function', () => {
});
it('should sort a list of string and symbols (uppercase and lower) in increasing order with comma separator ', () => {
const array: any[] = [
'Apple',
'pineaple',
'lemon',
'Orange',
1,
9,
'@',
'+'
];
const increasing: boolean = true;
const separator = ' ';
const removeDuplicated: boolean = true;
const caseSensitive: boolean = false;
const result = alphabeticSort(
array,
increasing,
separator,
removeDuplicated,
caseSensitive
);
expect(result).toBe('@ + 1 9 Apple lemon Orange pineaple');
});
it('should sort a list of string and symbols (uppercase and lower) in decreasing order with comma separator ', () => {
const array: any[] = [
'Apple',
'pineaple',
'lemon',
'Orange',
1,
9,
'@',
'+'
];
const increasing: boolean = false;
const separator = ' ';
const removeDuplicated: boolean = true;
const caseSensitive: boolean = false;
const result = alphabeticSort(
array,
increasing,
separator,
removeDuplicated,
caseSensitive
);
expect(result).toBe('pineaple Orange lemon Apple 9 1 + @');
});
// Define test cases for the alphabeticSort function
@@ -210,6 +281,56 @@ describe('numericSort function', () => {
});
it('should sort a list of string and symbols (uppercase and lower) in decreasing order with comma separator ', () => {
const array: any[] = [
'Apple',
'pineaple',
'lemon',
'Orange',
1,
9,
'@',
'+'
];
const increasing: boolean = true;
const separator = ' ';
const removeDuplicated: boolean = true;
const caseSensitive: boolean = true;
const result = alphabeticSort(
array,
increasing,
separator,
removeDuplicated,
caseSensitive
);
expect(result).toBe('+ 1 9 @ Apple Orange lemon pineaple');
});
it('should sort a list of string and symbols (uppercase and lower) in decreasing order with comma separator ', () => {
const array: any[] = [
'Apple',
'pineaple',
'lemon',
'Orange',
1,
9,
'@',
'+'
];
const increasing: boolean = false;
const separator = ' ';
const removeDuplicated: boolean = true;
const caseSensitive: boolean = true;
const result = alphabeticSort(
array,
increasing,
separator,
removeDuplicated,
caseSensitive
);
expect(result).toBe('pineaple lemon Orange Apple @ 9 1 +');
});
});

View File

@@ -1,5 +1,5 @@
// Import necessary modules and functions
import { describe, it, expect } from 'vitest';
import { describe, expect, it } from 'vitest';
import { listOfIntegers } from './service';
// Define test cases for the listOfIntegers function

View File

@@ -1,11 +1,85 @@
import { Box } from '@mui/material';
import React from 'react';
import React, { useState } from 'react';
import ToolTextResult from '../../../components/result/ToolTextResult';
import * as Yup from 'yup';
import ToolOptions from '../../../components/options/ToolOptions';
import { listOfIntegers } from './service';
import ToolInputAndResult from '../../../components/ToolInputAndResult';
import TextFieldWithDesc from '../../../components/options/TextFieldWithDesc';
const initialValues = {
firstValue: '1',
numberOfNumbers: '10',
step: '1',
separator: '\\n'
};
export default function SplitText() {
const [result, setResult] = useState<string>('');
const initialValues = {};
const validationSchema = Yup.object({
// splitSeparator: Yup.string().required('The separator is required')
});
export default function Generate() {
return <Box>Lorem ipsum</Box>;
return (
<Box>
<ToolInputAndResult
result={<ToolTextResult title={'Total'} value={result} />}
/>
<ToolOptions
getGroups={({ values, setFieldValue }) => [
{
title: 'Arithmetic sequence option',
component: (
<Box>
<TextFieldWithDesc
description={'Start sequence from this number.'}
value={values.firstValue}
onChange={(val) => setFieldValue('firstValue', val)}
type={'number'}
/>
<TextFieldWithDesc
description={'Increase each element by this amount'}
value={values.step}
onChange={(val) => setFieldValue('step', val)}
type={'number'}
/>
<TextFieldWithDesc
description={'Number of elements in sequence.'}
value={values.numberOfNumbers}
onChange={(val) => setFieldValue('numberOfNumbers', val)}
type={'number'}
/>
</Box>
)
},
{
title: 'Separator',
component: (
<TextFieldWithDesc
description={
'Separate elements in the arithmetic sequence by this character.'
}
value={values.separator}
onChange={(val) => setFieldValue('separator', val)}
/>
)
}
]}
compute={(optionsValues) => {
const { firstValue, numberOfNumbers, separator, step } =
optionsValues;
setResult(
listOfIntegers(
Number(firstValue),
Number(numberOfNumbers),
Number(step),
separator
)
);
}}
initialValues={initialValues}
validationSchema={validationSchema}
/>
</Box>
);
}

View File

@@ -3,7 +3,7 @@ import { lazy } from 'react';
// import image from '@assets/text.png';
export const tool = defineTool('number', {
name: 'Generate',
name: 'Generate numbers',
path: 'generate',
shortDescription: 'Quickly calculate a list of integers in your browser',
// image,

View File

@@ -1,14 +1,11 @@
import { Box, Stack } from '@mui/material';
import React, { useContext, useEffect, useState } from 'react';
import { Box } from '@mui/material';
import React, { 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';
@@ -44,25 +41,7 @@ const extractionTypes: {
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')
});
@@ -73,17 +52,8 @@ export default function SplitText() {
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={[
<ToolOptions
getGroups={({ values, setFieldValue }) => [
{
title: 'Number extraction',
component: extractionTypes.map(
@@ -110,15 +80,15 @@ export default function SplitText() {
setFieldValue('extractionType', type)
}
onTextChange={(val) =>
setFieldValue(textValueAccessor ?? '', val)
textValueAccessor
? setFieldValue(textValueAccessor, val)
: null
}
/>
) : (
<SimpleRadio
key={title}
onChange={() =>
setFieldValue('extractionType', type)
}
onChange={() => setFieldValue('extractionType', type)}
fieldName={'extractionType'}
value={values.extractionType}
description={description}
@@ -132,22 +102,21 @@ export default function SplitText() {
component: (
<CheckboxWithDesc
title={'Print Running Sum'}
description={
"Display the sum as it's calculated step by step."
}
description={"Display the sum as it's calculated step by step."}
checked={values.printRunningSum}
onChange={(value) =>
setFieldValue('printRunningSum', value)
}
onChange={(value) => setFieldValue('printRunningSum', value)}
/>
)
}
]}
compute={(optionsValues, input) => {
const { extractionType, printRunningSum, separator } = optionsValues;
setResult(compute(input, extractionType, printRunningSum, separator));
}}
initialValues={initialValues}
input={input}
validationSchema={validationSchema}
/>
</Stack>
)}
</Formik>
</ToolOptions>
</Box>
);
}

View File

@@ -1,4 +1,4 @@
import { describe, it, expect } from 'vitest';
import { describe, expect, it } from 'vitest';
import { compute } from './service';
describe('compute function', () => {

View File

@@ -1,20 +1,16 @@
import { Box, Grid, Stack, Typography } from '@mui/material';
import React, { useContext, useEffect, useState } from 'react';
import { Formik, useFormikContext } from 'formik';
import { Box } from '@mui/material';
import React, { useState } from 'react';
import * as Yup from 'yup';
import ToolTextInput from '../../../components/input/ToolTextInput';
import ToolTextResult from '../../../components/result/ToolTextResult';
import ToolOptions from '../../../components/options/ToolOptions';
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';
import AllTools from '../../../components/allTools/AllTools';
import ToolInfo from '../../../components/ToolInfo';
import Separator from '../../../components/Separator';
import Examples from '../../../components/examples/Examples';
const initialValues = {
@@ -115,23 +111,11 @@ s
export default function JoinText() {
const [input, setInput] = useState<string>('');
const { showSnackBar } = useContext(CustomSnackBarContext);
const [result, setResult] = useState<string>('');
const FormikListenerComponent = ({ input }: { input: string }) => {
const { values } = useFormikContext<typeof initialValues>();
const { joinCharacter, deleteBlank, deleteTrailing } = values;
useEffect(() => {
try {
const compute = (optionsValues: typeof initialValues, input: any) => {
const { joinCharacter, deleteBlank, deleteTrailing } = optionsValues;
setResult(mergeText(input, deleteBlank, deleteTrailing, joinCharacter));
} catch (exception: unknown) {
if (exception instanceof Error)
showSnackBar(exception.message, 'error');
}
}, [values, input]);
return null;
};
function changeInputResult(input: string, result: string) {
@@ -156,17 +140,9 @@ export default function JoinText() {
}
result={<ToolTextResult title={'Joined Text'} value={result} />}
/>
<ToolOptions>
<Formik
initialValues={initialValues}
validationSchema={validationSchema}
onSubmit={() => {}}
>
{({ setFieldValue, values }) => (
<Stack direction={'row'} spacing={2}>
<FormikListenerComponent input={input} />
<ToolOptionGroups
groups={[
<ToolOptions
compute={compute}
getGroups={({ values, setFieldValue }) => [
{
title: 'Text Merged Options',
component: (
@@ -187,20 +163,17 @@ export default function JoinText() {
key={option.accessor}
title={option.title}
checked={!!values[option.accessor]}
onChange={(value) =>
setFieldValue(option.accessor, value)
}
onChange={(value) => setFieldValue(option.accessor, value)}
description={option.description}
/>
))
}
]}
initialValues={initialValues}
input={input}
validationSchema={validationSchema}
/>
</Stack>
)}
</Formik>
</ToolOptions>
<Info
<ToolInfo
title="What Is a Text Joiner?"
description="With this tool you can join parts of the text together. It takes a list of text values, separated by newlines, and merges them together. You can set the character that will be placed between the parts of the combined text. Also, you can ignore all empty lines and remove spaces and tabs at the end of all lines. Textabulous!"
/>

View File

@@ -0,0 +1,18 @@
import { expect, test } from '@playwright/test';
test.describe('JoinText Component', () => {
test.beforeEach(async ({ page }) => {
await page.goto('/string/join');
});
test('should merge text pieces with specified join character', async ({
page
}) => {
// Input the text pieces
await page.getByTestId('text-input').fill('1\n2');
const result = await page.getByTestId('text-result').inputValue();
expect(result).toBe('12');
});
});

View File

@@ -1,4 +1,4 @@
import { describe, it, expect } from 'vitest';
import { describe, expect, it } from 'vitest';
import { mergeText } from './service';
describe('mergeText', () => {

View File

@@ -1,16 +1,12 @@
import { Box, Stack } from '@mui/material';
import Grid from '@mui/material/Grid';
import React, { useContext, useEffect, useState } from 'react';
import { Box } from '@mui/material';
import React, { 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, 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 = {
@@ -82,13 +78,7 @@ 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 computeExternal = (optionsValues: typeof initialValues, input: any) => {
const {
splitSeparatorType,
outputSeparator,
@@ -98,7 +88,7 @@ export default function SplitText() {
symbolValue,
regexValue,
lengthValue
} = values;
} = optionsValues;
setResult(
compute(
@@ -113,13 +103,6 @@ export default function SplitText() {
outputSeparator
)
);
} 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')
@@ -131,21 +114,12 @@ export default function SplitText() {
input={<ToolTextInput value={input} onChange={setInput} />}
result={<ToolTextResult title={'Text pieces'} value={result} />}
/>
<ToolOptions>
<Formik
initialValues={initialValues}
validationSchema={validationSchema}
onSubmit={() => {}}
>
{({ setFieldValue, values }) => (
<Stack direction={'row'} spacing={2}>
<FormikListenerComponent />
<ToolOptionGroups
groups={[
<ToolOptions
compute={computeExternal}
getGroups={({ values, setFieldValue }) => [
{
title: 'Split separator options',
component: splitOperators.map(
({ title, description, type }) => (
component: splitOperators.map(({ title, description, type }) => (
<RadioWithTextField
key={type}
radioValue={type}
@@ -156,12 +130,9 @@ export default function SplitText() {
onRadioChange={(type) =>
setFieldValue('splitSeparatorType', type)
}
onTextChange={(val) =>
setFieldValue(`${type}Value`, val)
}
onTextChange={(val) => setFieldValue(`${type}Value`, val)}
/>
)
)
))
},
{
title: 'Output separator options',
@@ -169,19 +140,16 @@ export default function SplitText() {
<TextFieldWithDesc
key={option.accessor}
value={values[option.accessor]}
onChange={(value) =>
setFieldValue(option.accessor, value)
}
onChange={(value) => setFieldValue(option.accessor, value)}
description={option.description}
/>
))
}
]}
initialValues={initialValues}
input={input}
validationSchema={validationSchema}
/>
</Stack>
)}
</Formik>
</ToolOptions>
</Box>
);
}

View File

@@ -1,4 +1,4 @@
import { describe, it, expect } from 'vitest';
import { describe, expect, it } from 'vitest';
import { compute } from './service';
describe('compute function', () => {

View File

@@ -1,15 +1,11 @@
import { Box, Stack } from '@mui/material';
import Grid from '@mui/material/Grid';
import React, { useContext, useEffect, useState } from 'react';
import { Box } from '@mui/material';
import React, { 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 = {
@@ -21,23 +17,9 @@ 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;
const computeOptions = (optionsValues: typeof initialValues, input: any) => {
const { dotSymbol, dashSymbol } = optionsValues;
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')
@@ -49,17 +31,9 @@ export default function ToMorse() {
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={[
<ToolOptions
compute={computeOptions}
getGroups={({ values, setFieldValue }) => [
{
title: 'Short Signal',
component: (
@@ -85,11 +59,10 @@ export default function ToMorse() {
)
}
]}
initialValues={initialValues}
input={input}
validationSchema={validationSchema}
/>
</Stack>
)}
</Formik>
</ToolOptions>
</Box>
);
}

View File

@@ -1,4 +1,4 @@
import { describe, it, expect } from 'vitest';
import { describe, expect, it } from 'vitest';
import { compute } from './service';
describe('compute function', () => {

View File

@@ -1,18 +1,9 @@
import {
Box,
Card,
CardContent,
Divider,
Stack,
useTheme
} from '@mui/material';
import { Box, Divider, Stack, useTheme } from '@mui/material';
import Grid from '@mui/material/Grid';
import Typography from '@mui/material/Typography';
import { Link, useNavigate, useParams } from 'react-router-dom';
import { getToolsByCategory, tools } from '../../tools';
import Button from '@mui/material/Button';
import { getToolsByCategory } from '../../tools';
import Hero from 'components/Hero';
import AllTools from '../../components/allTools/AllTools';
import { capitalizeFirstLetter } from '../../utils/string';
import toolsPng from '@assets/tools.png';
@@ -23,7 +14,7 @@ export default function Home() {
return (
<Box>
<Box
padding={5}
padding={{ xs: 1, md: 3, lg: 5 }}
display={'flex'}
flexDirection={'column'}
alignItems={'center'}
@@ -33,7 +24,7 @@ export default function Home() {
<Hero />
</Box>
<Divider sx={{ borderColor: theme.palette.primary.main }} />
<Box width={'100%'} mt={3} ml={7} padding={3}>
<Box width={'100%'} mt={3} ml={{ xs: 1, md: 2, lg: 3 }} padding={3}>
<Typography
fontSize={22}
color={theme.palette.primary.main}
@@ -42,7 +33,7 @@ export default function Home() {
{getToolsByCategory()
.find(({ type }) => type === categoryName)
?.tools?.map((tool) => (
<Grid item xs={12} md={4} key={tool.path}>
<Grid item xs={12} md={6} lg={4} key={tool.path}>
<Stack
sx={{
cursor: 'pointer',

View File

@@ -0,0 +1,148 @@
import { Box } from '@mui/material';
import React, { 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 TextFieldWithDesc from 'components/options/TextFieldWithDesc';
import ToolInputAndResult from '../../../../components/ToolInputAndResult';
import Typography from '@mui/material/Typography';
import { FrameOptions, GifReader, GifWriter } from 'omggif';
import { gifBinaryToFile } from '../../../../utils/gif';
const initialValues = {
newSpeed: 200
};
const validationSchema = Yup.object({
// splitSeparator: Yup.string().required('The separator is required')
});
export default function ChangeSpeed() {
const [input, setInput] = useState<File | null>(null);
const [result, setResult] = useState<File | null>(null);
const compute = (optionsValues: typeof initialValues, input: File) => {
const { newSpeed } = optionsValues;
const processImage = async (file: File, newSpeed: number) => {
const reader = new FileReader();
reader.readAsArrayBuffer(file);
reader.onload = async () => {
const arrayBuffer = reader.result;
if (arrayBuffer instanceof ArrayBuffer) {
const intArray = new Uint8Array(arrayBuffer);
const reader = new GifReader(intArray as Buffer);
const info = reader.frameInfo(0);
const imageDataArr: ImageData[] = new Array(reader.numFrames())
.fill(0)
.map((_, k) => {
const image = new ImageData(info.width, info.height);
reader.decodeAndBlitFrameRGBA(k, image.data);
return image;
});
const gif = new GifWriter(
[],
imageDataArr[0].width,
imageDataArr[0].height,
{ loop: 20 }
);
imageDataArr.forEach((imageData) => {
const palette = [];
const pixels = new Uint8Array(imageData.width * imageData.height);
const { data } = imageData;
for (let j = 0, k = 0, jl = data.length; j < jl; j += 4, k++) {
const r = Math.floor(data[j] * 0.1) * 10;
const g = Math.floor(data[j + 1] * 0.1) * 10;
const b = Math.floor(data[j + 2] * 0.1) * 10;
const color = (r << 16) | (g << 8) | (b << 0);
const index = palette.indexOf(color);
if (index === -1) {
pixels[k] = palette.length;
palette.push(color);
} else {
pixels[k] = index;
}
}
// Force palette to be power of 2
let powof2 = 1;
while (powof2 < palette.length) powof2 <<= 1;
palette.length = powof2;
const delay = newSpeed / 10; // Delay in hundredths of a sec (100 = 1s)
const options: FrameOptions = {
palette,
delay
};
gif.addFrame(
0,
0,
imageData.width,
imageData.height,
// @ts-ignore
pixels,
options
);
});
const newFile = gifBinaryToFile(gif.getOutputBuffer(), file.name);
setResult(newFile);
}
};
};
processImage(input, newSpeed);
};
return (
<Box>
<ToolInputAndResult
input={
<ToolFileInput
value={input}
onChange={setInput}
accept={['image/gif']}
title={'Input GIF'}
/>
}
result={
<ToolFileResult
title={'Output GIF with new speed'}
value={result}
extension={'gif'}
/>
}
/>
<ToolOptions
compute={compute}
getGroups={({ values, setFieldValue }) => [
{
title: 'New GIF speed',
component: (
<Box>
<TextFieldWithDesc
value={values.newSpeed}
onChange={(val) => setFieldValue('newSpeed', val)}
description={'Default new GIF speed.'}
InputProps={{ endAdornment: <Typography>ms</Typography> }}
type={'number'}
/>
</Box>
)
}
]}
initialValues={initialValues}
input={input}
validationSchema={validationSchema}
/>
</Box>
);
}

View File

@@ -0,0 +1,14 @@
import { defineTool } from '@tools/defineTool';
import { lazy } from 'react';
// import image from '@assets/text.png';
export const tool = defineTool('gif', {
name: 'Change speed',
path: 'change-speed',
// image,
description:
'This online utility lets you change the speed of a GIF animation. You can speed it up or slow it down. You can set the same constant delay between all frames or change the delays of individual frames. You can also play both the input and output GIFs at the same time and compare their speeds',
shortDescription: 'Quickly change GIF speed',
keywords: ['change', 'speed'],
component: lazy(() => import('./index'))
});

View File

@@ -0,0 +1,3 @@
import { tool as gifChangeSpeed } from './change-speed/meta';
export const gifTools = [gifChangeSpeed];

3
src/pages/video/index.ts Normal file
View File

@@ -0,0 +1,3 @@
import { gifTools } from './gif';
export const videoTools = [...gifTools];

View File

@@ -1,5 +1,5 @@
import ToolLayout from '../components/ToolLayout';
import React, { LazyExoticComponent, JSXElementConstructor } from 'react';
import React, { JSXElementConstructor, LazyExoticComponent } from 'react';
interface ToolOptions {
path: string;

View File

@@ -3,11 +3,13 @@ import { imageTools } from '../pages/image';
import { DefinedTool } from './defineTool';
import { capitalizeFirstLetter } from '../utils/string';
import { numberTools } from '../pages/number';
import { videoTools } from '../pages/video';
export const tools: DefinedTool[] = [
...imageTools,
...stringTools,
...numberTools
...numberTools,
...videoTools
];
const categoriesDescriptions: { type: string; value: string }[] = [
{
@@ -24,6 +26,11 @@ const categoriesDescriptions: { type: string; value: string }[] = [
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.'
},
{
type: 'gif',
value:
'Tools for working with GIF animations create transparent GIFs, extract GIF frames, add text to GIF, crop, rotate, reverse GIFs, and much more.'
}
];
export const filterTools = (

18
src/utils/gif.ts Normal file
View File

@@ -0,0 +1,18 @@
import { GifBinary } from 'omggif';
export function gifBinaryToFile(
gifBinary: GifBinary,
fileName: string,
mimeType: string = 'image/gif'
): File {
// Convert GifBinary to Uint8Array
const uint8Array = new Uint8Array(gifBinary.length);
for (let i = 0; i < gifBinary.length; i++) {
uint8Array[i] = gifBinary[i];
}
const blob = new Blob([uint8Array], { type: mimeType });
// Create File from Blob
return new File([blob], fileName, { type: mimeType });
}