diff --git a/.gitignore b/.gitignore
index b50a9c1..50d1bf2 100644
--- a/.gitignore
+++ b/.gitignore
@@ -35,3 +35,5 @@ yarn-error.log*
# vercel
.vercel
+
+test-results
diff --git a/.idea/workspace.xml b/.idea/workspace.xml
index 5188fba..d664249 100644
--- a/.idea/workspace.xml
+++ b/.idea/workspace.xml
@@ -4,10 +4,12 @@
-
+
-
-
+
+
+
+
@@ -48,6 +50,8 @@
"keyToString": {
"ASKED_ADD_EXTERNAL_FILES": "true",
"ASKED_SHARE_PROJECT_CONFIGURATION_FILES": "true",
+ "Playwright.JoinText Component.executor": "Run",
+ "Playwright.JoinText Component.should merge text pieces with specified join character.executor": "Run",
"RunOnceActivity.OpenProjectViewOnStart": "true",
"RunOnceActivity.ShowReadmeOnStart": "true",
"Vitest.compute function (1).executor": "Run",
@@ -69,6 +73,7 @@
"npm.prebuild.executor": "Run",
"npm.script:create:tool.executor": "Run",
"npm.test.executor": "Run",
+ "npm.test:e2e.executor": "Run",
"prettierjs.PrettierConfiguration.Package": "C:\\Users\\HP\\IdeaProjects\\omni-tools\\node_modules\\prettier",
"project.structure.last.edited": "Problems",
"project.structure.proportion": "0.0",
@@ -96,35 +101,35 @@
+
-
+
-
+
-
-
+
-
+
-
+
-
+
-
-
-
+
+
-
+
+
@@ -148,11 +153,11 @@
-
+
-
+
@@ -161,10 +166,10 @@
-
-
-
+
+
+
@@ -194,110 +199,12 @@
-
-
-
- 1718999826771
-
-
-
- 1718999826771
-
-
-
- 1719003559965
-
-
-
- 1719003559965
-
-
-
- 1719005757859
-
-
-
- 1719005757859
-
-
-
- 1719006274218
-
-
-
- 1719006274218
-
-
-
- 1719006515710
-
-
-
- 1719006515710
-
-
-
- 1719006829016
-
-
-
- 1719006829016
-
-
-
- 1719007125575
-
-
-
- 1719007125575
-
-
-
- 1719007195103
-
-
-
- 1719007195103
-
-
-
- 1719023377131
-
-
-
- 1719023377131
-
-
-
- 1719023691491
-
-
-
- 1719023691491
-
-
-
- 1719024346455
-
-
-
- 1719024346455
-
-
-
- 1719085085537
-
-
-
- 1719085085537
-
-
-
- 1719090379202
-
-
-
- 1719090379202
+
+
+
+
+
+
@@ -587,7 +494,111 @@
1719349760930
-
+
+
+ 1719350066784
+
+
+
+ 1719350066784
+
+
+
+ 1719358195260
+
+
+
+ 1719358195260
+
+
+
+ 1719359243293
+
+
+
+ 1719359243294
+
+
+
+ 1719359368236
+
+
+
+ 1719359368236
+
+
+
+ 1719360545177
+
+
+
+ 1719360545177
+
+
+
+ 1719363656541
+
+
+
+ 1719363656541
+
+
+
+ 1719384439535
+
+
+
+ 1719384439535
+
+
+
+ 1719388760134
+
+
+
+ 1719388760134
+
+
+
+ 1719388927238
+
+
+
+ 1719388927238
+
+
+
+ 1719488380275
+
+
+
+ 1719488380275
+
+
+
+ 1719488576835
+
+
+
+ 1719488576835
+
+
+
+ 1719491470485
+
+
+
+ 1719491470485
+
+
+
+ 1719499895862
+
+
+
+ 1719499895862
+
+
@@ -608,19 +619,6 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
@@ -633,7 +631,20 @@
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/package-lock.json b/package-lock.json
index d332906..1943824 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -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",
diff --git a/package.json b/package.json
index 0034b7f..dd3378a 100644
--- a/package.json
+++ b/package.json
@@ -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",
diff --git a/playwright.config.ts b/playwright.config.ts
new file mode 100644
index 0000000..8672300
--- /dev/null
+++ b/playwright.config.ts
@@ -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'] }
+ }
+ ]
+});
diff --git a/src/components/Hero.tsx b/src/components/Hero.tsx
index e9ed495..4fe6dc1 100644
--- a/src/components/Hero.tsx
+++ b/src/components/Hero.tsx
@@ -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('');
- const [filteredTools, setFilteredTools] = useState(tools);
+ const [filteredTools, setFilteredTools] = useState(
+ _.shuffle(tools)
+ );
const navigate = useNavigate();
const handleInputChange = (
event: React.ChangeEvent<{}>,
newInputValue: string
) => {
setInputValue(newInputValue);
- setFilteredTools(filterTools(tools, newInputValue));
+ setFilteredTools(_.shuffle(filterTools(tools, newInputValue)));
};
return (
-
-
- Transform Your Workflow with
-
- Omni Tools
+
+
+
+ Transform Your Workflow with{' '}
+
+ Omni Tools
+
-
+
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) => (
{exampleTools.map((tool) => (
- navigate(tool.url)} item xs={4} key={tool.label}>
+ navigate(tool.url)}
+ item
+ xs={12}
+ md={6}
+ lg={4}
+ key={tool.label}
+ >
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);
+ }
+}
diff --git a/src/components/Loading.tsx b/src/components/Loading.tsx
index dfa8473..431f6d1 100644
--- a/src/components/Loading.tsx
+++ b/src/components/Loading.tsx
@@ -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 (
-
-
- Loading
-
+
+ Loading
-
+
);
}
-export default FuseLoading;
+export default Loading;
diff --git a/src/components/Navbar/index.tsx b/src/components/Navbar/index.tsx
index 8fbc178..14abec3 100644
--- a/src/components/Navbar/index.tsx
+++ b/src/components/Navbar/index.tsx
@@ -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 = (
+
+ navigate('/features')}>
+
+
+ navigate('/about-us')}>
+
+
+
+
+ Star us
+
+
+ );
+
return (
{
>
OmniTools
-
-
-
-
-
- Star us
-
-
+
+ Star us
+
+
+ )}
);
diff --git a/src/tools/Separator.tsx b/src/components/Separator.tsx
similarity index 100%
rename from src/tools/Separator.tsx
rename to src/components/Separator.tsx
diff --git a/src/components/ToolBreadcrumb.tsx b/src/components/ToolBreadcrumb.tsx
index dabbed3..9720ece 100644
--- a/src/components/ToolBreadcrumb.tsx
+++ b/src/components/ToolBreadcrumb.tsx
@@ -14,7 +14,7 @@ interface BreadcrumbComponentProps {
const ToolBreadcrumb: React.FC = ({ items }) => {
const theme = useTheme();
return (
-
+
{items.map((item, index) => {
if (index === items.length - 1 || !item.link) {
return (
diff --git a/src/components/ToolHeader.tsx b/src/components/ToolHeader.tsx
index 86faa74..c6d529d 100644
--- a/src/components/ToolHeader.tsx
+++ b/src/components/ToolHeader.tsx
@@ -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 (
-
-
- Use This Tool
-
-
- See Examples
-
-
- Learn How to Use
-
-
+
+
+
+ Use This Tool
+
+
+
+
+ See Examples
+
+
+
+
+ Learn How to Use
+
+
+
);
}
@@ -44,16 +51,23 @@ export default function ToolHeader({
{ title }
]}
/>
-
-
+
+
{title}
{description}
-
- {image &&
}
-
+
+
+ {image && (
+
+
+
+
+
+ )}
+
);
}
diff --git a/src/pages/string/join/Info.tsx b/src/components/ToolInfo.tsx
similarity index 84%
rename from src/pages/string/join/Info.tsx
rename to src/components/ToolInfo.tsx
index a6d01a7..de87db7 100644
--- a/src/pages/string/join/Info.tsx
+++ b/src/components/ToolInfo.tsx
@@ -5,7 +5,7 @@ interface ExampleProps {
description: string;
}
-export default function Example({ title, description }: ExampleProps) {
+export default function ToolInfo({ title, description }: ExampleProps) {
return (
diff --git a/src/components/ToolInputAndResult.tsx b/src/components/ToolInputAndResult.tsx
index 3aefb8c..a07721f 100644
--- a/src/components/ToolInputAndResult.tsx
+++ b/src/components/ToolInputAndResult.tsx
@@ -5,15 +5,17 @@ export default function ToolInputAndResult({
input,
result
}: {
- input: ReactNode;
+ input?: ReactNode;
result: ReactNode;
}) {
return (
-
- {input}
-
-
+ {input && (
+
+ {input}
+
+ )}
+
{result}
diff --git a/src/components/ToolLayout.tsx b/src/components/ToolLayout.tsx
index 5dd49a3..4737ea1 100644
--- a/src/components/ToolLayout.tsx
+++ b/src/components/ToolLayout.tsx
@@ -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';
diff --git a/src/components/allTools/AllTools.tsx b/src/components/allTools/AllTools.tsx
index e0a5e1f..b3c701f 100644
--- a/src/components/allTools/AllTools.tsx
+++ b/src/components/allTools/AllTools.tsx
@@ -21,7 +21,7 @@ export default function AllTools({ title, toolCards }: AllToolsProps) {
{toolCards.map((card, index) => (
-
+
{exampleCards.map((card, index) => (
-
+
({
@@ -38,7 +34,10 @@ const RadioWithTextField = ({
/>
{
+ if (typeof val === 'string') onTextChange(val);
+ else onTextChange(val.target.value);
+ }}
description={description}
/>
diff --git a/src/components/options/TextFieldWithDesc.tsx b/src/components/options/TextFieldWithDesc.tsx
index 7d184ea..dad5ba9 100644
--- a/src/components/options/TextFieldWithDesc.tsx
+++ b/src/components/options/TextFieldWithDesc.tsx
@@ -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 (
onChange(event.target.value)}
+ {...props}
/>
{description}
diff --git a/src/components/options/ToolOptionGroups.tsx b/src/components/options/ToolOptionGroups.tsx
index 76c48f9..5f1d4a6 100644
--- a/src/components/options/ToolOptionGroups.tsx
+++ b/src/components/options/ToolOptionGroups.tsx
@@ -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 (
-
+
{groups.map((group) => (
-
+
{group.title}
{group.component}
-
+
))}
-
+
);
}
diff --git a/src/components/options/ToolOptions.tsx b/src/components/options/ToolOptions.tsx
index f6c5955..89baf83 100644
--- a/src/components/options/ToolOptions.tsx
+++ b/src/components/options/ToolOptions.tsx
@@ -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 = ({
+ initialValues,
+ input,
+ compute
+}: {
+ initialValues: T;
+ input: any;
+ compute: (optionsValues: T, input: any) => void;
+}) => {
+ const { values } = useFormikContext();
+ 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({
+ 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) => ToolOptionGroup[];
+ formRef?: RefObject>;
+}) {
const theme = useTheme();
+
return (
Tool options
- {children}
+
+ {}}
+ >
+ {(formikProps) => (
+
+
+
+ {children}
+
+ )}
+
+
);
}
diff --git a/src/components/result/ToolTextResult.tsx b/src/components/result/ToolTextResult.tsx
index e672068..2a5e596 100644
--- a/src/components/result/ToolTextResult.tsx
+++ b/src/components/result/ToolTextResult.tsx
@@ -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 (
-
+
);
diff --git a/src/config/routesConfig.tsx b/src/config/routesConfig.tsx
index a68b548..78f47e6 100644
--- a/src/config/routesConfig.tsx
+++ b/src/config/routesConfig.tsx
@@ -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'));
diff --git a/src/pages/home/index.tsx b/src/pages/home/index.tsx
index 5a1c51b..d9be29e 100644
--- a/src/pages/home/index.tsx
+++ b/src/pages/home/index.tsx
@@ -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 (
{getToolsByCategory().map((category) => (
-
-
+
+
{category.description}
-
- navigate('/categories/' + category.type)}
- variant={'contained'}
- >{`See all ${category.title}`}
- navigate(category.example.path)}
- variant={'outlined'}
- >{`Try ${category.example.title}`}
-
+
+
+ navigate('/categories/' + category.type)}
+ variant={'contained'}
+ >{`See all ${category.title}`}
+
+
+ navigate(category.example.path)}
+ variant={'outlined'}
+ >{`Try ${category.example.title}`}
+
+
diff --git a/src/pages/image/png/change-colors-in-png/index.tsx b/src/pages/image/png/change-colors-in-png/index.tsx
index 621a259..6c83e1d 100644
--- a/src/pages/image/png/change-colors-in-png/index.tsx
+++ b/src/pages/image/png/change-colors-in-png/index.tsx
@@ -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,86 +21,81 @@ export default function ChangeColorsInPng() {
const [input, setInput] = useState(null);
const [result, setResult] = useState(null);
- const FormikListenerComponent = ({ input }: { input: File }) => {
- const { values } = useFormikContext();
- const { fromColor, toColor, similarity } = values;
+ const compute = (optionsValues: typeof initialValues, input: any) => {
+ const { fromColor, toColor, similarity } = optionsValues;
+ 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();
- 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
+ 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]
) => {
- 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)
+ 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 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');
};
+ const maxColorDistance = Math.sqrt(
+ Math.pow(255, 2) + Math.pow(255, 2) + Math.pow(255, 2)
+ );
+ const similarityThreshold = (similarity / 100) * maxColorDistance;
- processImage(input, fromRgb, toRgb, Number(similarity));
- }, [input, fromColor, toColor]);
+ 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
+ }
+ }
- return null;
+ 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));
};
-
return (
}
/>
-
- {}}
- >
- {({ setFieldValue, values }) => (
-
- {input && }
-
- setFieldValue('fromColor', val)}
- description={'Replace this color (from color)'}
- />
- setFieldValue('toColor', val)}
- description={'With this color (to color)'}
- />
- 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.'
- }
- />
-
- )
+ [
+ {
+ title: 'From color and to color',
+ component: (
+
+ setFieldValue('fromColor', val)}
+ description={'Replace this color (from color)'}
+ />
+ setFieldValue('toColor', val)}
+ description={'With this color (to color)'}
+ />
+ 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.'
}
- ]}
- />
-
- )}
-
-
+ />
+
+ )
+ }
+ ]}
+ initialValues={initialValues}
+ input={input}
+ validationSchema={validationSchema}
+ />
);
}
diff --git a/src/pages/image/png/create-transparent/index.tsx b/src/pages/image/png/create-transparent/index.tsx
index fd9e3a7..726c728 100644
--- a/src/pages/image/png/create-transparent/index.tsx
+++ b/src/pages/image/png/create-transparent/index.tsx
@@ -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,78 +20,73 @@ export default function ChangeColorsInPng() {
const [input, setInput] = useState(null);
const [result, setResult] = useState(null);
- const FormikListenerComponent = ({ input }: { input: File }) => {
- const { values } = useFormikContext();
- const { fromColor, similarity } = values;
+ const compute = (optionsValues: typeof initialValues, input: any) => {
+ const { fromColor, similarity } = optionsValues;
- 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
+ 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]
) => {
- 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)
+ 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 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');
};
+ const maxColorDistance = Math.sqrt(
+ Math.pow(255, 2) + Math.pow(255, 2) + Math.pow(255, 2)
+ );
+ const similarityThreshold = (similarity / 100) * maxColorDistance;
- processImage(input, fromRgb, Number(similarity));
- }, [input, fromColor]);
+ 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)
+ }
+ }
- return null;
+ 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));
};
return (
@@ -115,42 +108,33 @@ export default function ChangeColorsInPng() {
/>
}
/>
-
- {}}
- >
- {({ setFieldValue, values }) => (
-
- {input && }
-
- setFieldValue('fromColor', val)}
- description={'Replace this color (from color)'}
- />
- 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.'
- }
- />
-
- )
+ [
+ {
+ title: 'From color and similarity',
+ component: (
+
+ setFieldValue('fromColor', val)}
+ description={'Replace this color (from color)'}
+ />
+ 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.'
}
- ]}
- />
-
- )}
-
-
+ />
+
+ )
+ }
+ ]}
+ initialValues={initialValues}
+ input={input}
+ validationSchema={validationSchema}
+ />
);
}
diff --git a/src/pages/list/sort/service.ts b/src/pages/list/sort/service.ts
index 4c2beb6..9822ef1 100644
--- a/src/pages/list/sort/service.ts
+++ b/src/pages/list/sort/service.ts
@@ -113,4 +113,5 @@ export function Sort(
break;
}
return result;
-}
\ No newline at end of file
+}
+
diff --git a/src/pages/list/sort/sort.service.test.ts b/src/pages/list/sort/sort.service.test.ts
index 0c056d8..6d5a10e 100644
--- a/src/pages/list/sort/sort.service.test.ts
+++ b/src/pages/list/sort/sort.service.test.ts
@@ -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');
});
-});
\ No newline at end of file
+ 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 +');
+ });
+
+});
diff --git a/src/pages/number/generate/generate.service.test.ts b/src/pages/number/generate/generate.service.test.ts
index 4fbbcf8..75bd932 100644
--- a/src/pages/number/generate/generate.service.test.ts
+++ b/src/pages/number/generate/generate.service.test.ts
@@ -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
diff --git a/src/pages/number/generate/index.tsx b/src/pages/number/generate/index.tsx
index 2943432..e810407 100644
--- a/src/pages/number/generate/index.tsx
+++ b/src/pages/number/generate/index.tsx
@@ -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 = {};
-const validationSchema = Yup.object({
- // splitSeparator: Yup.string().required('The separator is required')
-});
-export default function Generate() {
- return Lorem ipsum;
+const initialValues = {
+ firstValue: '1',
+ numberOfNumbers: '10',
+ step: '1',
+ separator: '\\n'
+};
+export default function SplitText() {
+ const [result, setResult] = useState('');
+
+ const validationSchema = Yup.object({
+ // splitSeparator: Yup.string().required('The separator is required')
+ });
+
+ return (
+
+ }
+ />
+ [
+ {
+ title: 'Arithmetic sequence option',
+ component: (
+
+ setFieldValue('firstValue', val)}
+ type={'number'}
+ />
+ setFieldValue('step', val)}
+ type={'number'}
+ />
+ setFieldValue('numberOfNumbers', val)}
+ type={'number'}
+ />
+
+ )
+ },
+ {
+ title: 'Separator',
+ component: (
+ setFieldValue('separator', val)}
+ />
+ )
+ }
+ ]}
+ compute={(optionsValues) => {
+ const { firstValue, numberOfNumbers, separator, step } =
+ optionsValues;
+ setResult(
+ listOfIntegers(
+ Number(firstValue),
+ Number(numberOfNumbers),
+ Number(step),
+ separator
+ )
+ );
+ }}
+ initialValues={initialValues}
+ validationSchema={validationSchema}
+ />
+
+ );
}
diff --git a/src/pages/number/generate/meta.ts b/src/pages/number/generate/meta.ts
index 044d3ca..1c85aee 100644
--- a/src/pages/number/generate/meta.ts
+++ b/src/pages/number/generate/meta.ts
@@ -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,
diff --git a/src/pages/number/sum/index.tsx b/src/pages/number/sum/index.tsx
index 4a054f6..eff2e54 100644
--- a/src/pages/number/sum/index.tsx
+++ b/src/pages/number/sum/index.tsx
@@ -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('');
const [result, setResult] = useState('');
- // const formRef = useRef>(null);
- const { showSnackBar } = useContext(CustomSnackBarContext);
- const FormikListenerComponent = () => {
- const { values } = useFormikContext();
-
- 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,81 +52,71 @@ export default function SplitText() {
input={}
result={}
/>
-
- {}}
- >
- {({ setFieldValue, values }) => (
-
-
-
- withTextField ? (
-
- setFieldValue('extractionType', type)
- }
- onTextChange={(val) =>
- setFieldValue(textValueAccessor ?? '', val)
- }
- />
- ) : (
-
- setFieldValue('extractionType', type)
- }
- fieldName={'extractionType'}
- value={values.extractionType}
- description={description}
- title={title}
- />
- )
- )
- },
- {
- title: 'Running Sum',
- component: (
-
- setFieldValue('printRunningSum', value)
- }
- />
- )
- }
- ]}
+ [
+ {
+ title: 'Number extraction',
+ component: extractionTypes.map(
+ ({
+ title,
+ description,
+ type,
+ withTextField,
+ textValueAccessor
+ }) =>
+ withTextField ? (
+
+ setFieldValue('extractionType', type)
+ }
+ onTextChange={(val) =>
+ textValueAccessor
+ ? setFieldValue(textValueAccessor, val)
+ : null
+ }
+ />
+ ) : (
+ setFieldValue('extractionType', type)}
+ fieldName={'extractionType'}
+ value={values.extractionType}
+ description={description}
+ title={title}
+ />
+ )
+ )
+ },
+ {
+ title: 'Running Sum',
+ component: (
+ setFieldValue('printRunningSum', value)}
/>
-
- )}
-
-
+ )
+ }
+ ]}
+ compute={(optionsValues, input) => {
+ const { extractionType, printRunningSum, separator } = optionsValues;
+ setResult(compute(input, extractionType, printRunningSum, separator));
+ }}
+ initialValues={initialValues}
+ input={input}
+ validationSchema={validationSchema}
+ />
);
}
diff --git a/src/pages/number/sum/sum.service.test.ts b/src/pages/number/sum/sum.service.test.ts
index 1a3b21b..944a28a 100644
--- a/src/pages/number/sum/sum.service.test.ts
+++ b/src/pages/number/sum/sum.service.test.ts
@@ -1,4 +1,4 @@
-import { describe, it, expect } from 'vitest';
+import { describe, expect, it } from 'vitest';
import { compute } from './service';
describe('compute function', () => {
diff --git a/src/pages/string/join/index.tsx b/src/pages/string/join/index.tsx
index 33fa5df..7369976 100644
--- a/src/pages/string/join/index.tsx
+++ b/src/pages/string/join/index.tsx
@@ -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('');
- const { showSnackBar } = useContext(CustomSnackBarContext);
const [result, setResult] = useState('');
- const FormikListenerComponent = ({ input }: { input: string }) => {
- const { values } = useFormikContext();
- const { joinCharacter, deleteBlank, deleteTrailing } = values;
-
- useEffect(() => {
- try {
- setResult(mergeText(input, deleteBlank, deleteTrailing, joinCharacter));
- } catch (exception: unknown) {
- if (exception instanceof Error)
- showSnackBar(exception.message, 'error');
- }
- }, [values, input]);
-
- return null;
+ const compute = (optionsValues: typeof initialValues, input: any) => {
+ const { joinCharacter, deleteBlank, deleteTrailing } = optionsValues;
+ setResult(mergeText(input, deleteBlank, deleteTrailing, joinCharacter));
};
function changeInputResult(input: string, result: string) {
@@ -156,51 +140,40 @@ export default function JoinText() {
}
result={}
/>
-
- {}}
- >
- {({ setFieldValue, values }) => (
-
-
-
- setFieldValue(mergeOptions.accessor, value)
- }
- description={mergeOptions.description}
- />
- )
- },
- {
- title: 'Blank Lines and Trailing Spaces',
- component: blankTrailingOptions.map((option) => (
-
- setFieldValue(option.accessor, value)
- }
- description={option.description}
- />
- ))
- }
- ]}
+ [
+ {
+ title: 'Text Merged Options',
+ component: (
+
+ setFieldValue(mergeOptions.accessor, value)
+ }
+ description={mergeOptions.description}
/>
-
- )}
-
-
- (
+ setFieldValue(option.accessor, value)}
+ description={option.description}
+ />
+ ))
+ }
+ ]}
+ initialValues={initialValues}
+ input={input}
+ validationSchema={validationSchema}
+ />
+
diff --git a/src/pages/string/join/string-join.e2e.spec.ts b/src/pages/string/join/string-join.e2e.spec.ts
new file mode 100644
index 0000000..13b7e0d
--- /dev/null
+++ b/src/pages/string/join/string-join.e2e.spec.ts
@@ -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');
+ });
+});
diff --git a/src/pages/string/join/string-join.service.test.ts b/src/pages/string/join/string-join.service.test.ts
index 6e17f28..c2a90c0 100644
--- a/src/pages/string/join/string-join.service.test.ts
+++ b/src/pages/string/join/string-join.service.test.ts
@@ -1,4 +1,4 @@
-import { describe, it, expect } from 'vitest';
+import { describe, expect, it } from 'vitest';
import { mergeText } from './service';
describe('mergeText', () => {
diff --git a/src/pages/string/split/index.tsx b/src/pages/string/split/index.tsx
index ad4f23d..4c660b0 100644
--- a/src/pages/string/split/index.tsx
+++ b/src/pages/string/split/index.tsx
@@ -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,44 +78,31 @@ export default function SplitText() {
const [input, setInput] = useState('');
const [result, setResult] = useState('');
// const formRef = useRef>(null);
- const { showSnackBar } = useContext(CustomSnackBarContext);
+ const computeExternal = (optionsValues: typeof initialValues, input: any) => {
+ const {
+ splitSeparatorType,
+ outputSeparator,
+ charBeforeChunk,
+ charAfterChunk,
+ chunksValue,
+ symbolValue,
+ regexValue,
+ lengthValue
+ } = optionsValues;
- const FormikListenerComponent = () => {
- const { values } = useFormikContext();
-
- useEffect(() => {
- try {
- const {
- splitSeparatorType,
- outputSeparator,
- charBeforeChunk,
- charAfterChunk,
- chunksValue,
- symbolValue,
- regexValue,
- lengthValue
- } = values;
-
- setResult(
- compute(
- splitSeparatorType,
- input,
- symbolValue,
- regexValue,
- Number(lengthValue),
- Number(chunksValue),
- charBeforeChunk,
- charAfterChunk,
- outputSeparator
- )
- );
- } catch (exception: unknown) {
- if (exception instanceof Error)
- showSnackBar(exception.message, 'error');
- }
- }, [values, input]);
-
- return null; // This component doesn't render anything
+ setResult(
+ compute(
+ splitSeparatorType,
+ input,
+ symbolValue,
+ regexValue,
+ Number(lengthValue),
+ Number(chunksValue),
+ charBeforeChunk,
+ charAfterChunk,
+ outputSeparator
+ )
+ );
};
const validationSchema = Yup.object({
// splitSeparator: Yup.string().required('The separator is required')
@@ -131,57 +114,42 @@ export default function SplitText() {
input={}
result={}
/>
-
- {}}
- >
- {({ setFieldValue, values }) => (
-
-
- (
-
- setFieldValue('splitSeparatorType', type)
- }
- onTextChange={(val) =>
- setFieldValue(`${type}Value`, val)
- }
- />
- )
- )
- },
- {
- title: 'Output separator options',
- component: outputOptions.map((option) => (
-
- setFieldValue(option.accessor, value)
- }
- description={option.description}
- />
- ))
- }
- ]}
+ [
+ {
+ title: 'Split separator options',
+ component: splitOperators.map(({ title, description, type }) => (
+
+ setFieldValue('splitSeparatorType', type)
+ }
+ onTextChange={(val) => setFieldValue(`${type}Value`, val)}
/>
-
- )}
-
-
+ ))
+ },
+ {
+ title: 'Output separator options',
+ component: outputOptions.map((option) => (
+ setFieldValue(option.accessor, value)}
+ description={option.description}
+ />
+ ))
+ }
+ ]}
+ initialValues={initialValues}
+ input={input}
+ validationSchema={validationSchema}
+ />
);
}
diff --git a/src/pages/string/split/string-split.service.test.ts b/src/pages/string/split/string-split.service.test.ts
index 4917cdc..b1fe297 100644
--- a/src/pages/string/split/string-split.service.test.ts
+++ b/src/pages/string/split/string-split.service.test.ts
@@ -1,4 +1,4 @@
-import { describe, it, expect } from 'vitest';
+import { describe, expect, it } from 'vitest';
import { compute } from './service';
describe('compute function', () => {
diff --git a/src/pages/string/to-morse/index.tsx b/src/pages/string/to-morse/index.tsx
index 3894e9d..c06d1aa 100644
--- a/src/pages/string/to-morse/index.tsx
+++ b/src/pages/string/to-morse/index.tsx
@@ -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('');
const [result, setResult] = useState('');
// const formRef = useRef>(null);
- const { showSnackBar } = useContext(CustomSnackBarContext);
-
- const FormikListenerComponent = () => {
- const { values } = useFormikContext();
-
- 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 computeOptions = (optionsValues: typeof initialValues, input: any) => {
+ const { dotSymbol, dashSymbol } = optionsValues;
+ setResult(compute(input, dotSymbol, dashSymbol));
};
const validationSchema = Yup.object({
// splitSeparator: Yup.string().required('The separator is required')
@@ -49,47 +31,38 @@ export default function ToMorse() {
input={}
result={}
/>
-
- {}}
- >
- {({ setFieldValue, values }) => (
-
-
- setFieldValue('dotSymbol', val)}
- />
- )
- },
- {
- title: 'Long Signal',
- component: (
- setFieldValue('dashSymbol', val)}
- />
- )
- }
- ]}
+ [
+ {
+ title: 'Short Signal',
+ component: (
+ setFieldValue('dotSymbol', val)}
/>
-
- )}
-
-
+ )
+ },
+ {
+ title: 'Long Signal',
+ component: (
+ setFieldValue('dashSymbol', val)}
+ />
+ )
+ }
+ ]}
+ initialValues={initialValues}
+ input={input}
+ validationSchema={validationSchema}
+ />
);
}
diff --git a/src/pages/string/to-morse/to-morse.service.test.ts b/src/pages/string/to-morse/to-morse.service.test.ts
index 6f87b54..3575ebd 100644
--- a/src/pages/string/to-morse/to-morse.service.test.ts
+++ b/src/pages/string/to-morse/to-morse.service.test.ts
@@ -1,4 +1,4 @@
-import { describe, it, expect } from 'vitest';
+import { describe, expect, it } from 'vitest';
import { compute } from './service';
describe('compute function', () => {
diff --git a/src/pages/tools-by-category/index.tsx b/src/pages/tools-by-category/index.tsx
index 658151f..5687703 100644
--- a/src/pages/tools-by-category/index.tsx
+++ b/src/pages/tools-by-category/index.tsx
@@ -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 (
-
+
type === categoryName)
?.tools?.map((tool) => (
-
+
(null);
+ const [result, setResult] = useState(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 (
+
+
+ }
+ result={
+
+ }
+ />
+ [
+ {
+ title: 'New GIF speed',
+ component: (
+
+ setFieldValue('newSpeed', val)}
+ description={'Default new GIF speed.'}
+ InputProps={{ endAdornment: ms }}
+ type={'number'}
+ />
+
+ )
+ }
+ ]}
+ initialValues={initialValues}
+ input={input}
+ validationSchema={validationSchema}
+ />
+
+ );
+}
diff --git a/src/pages/video/gif/change-speed/meta.ts b/src/pages/video/gif/change-speed/meta.ts
new file mode 100644
index 0000000..3abe82a
--- /dev/null
+++ b/src/pages/video/gif/change-speed/meta.ts
@@ -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'))
+});
diff --git a/src/pages/video/gif/index.ts b/src/pages/video/gif/index.ts
new file mode 100644
index 0000000..92e5375
--- /dev/null
+++ b/src/pages/video/gif/index.ts
@@ -0,0 +1,3 @@
+import { tool as gifChangeSpeed } from './change-speed/meta';
+
+export const gifTools = [gifChangeSpeed];
diff --git a/src/pages/video/index.ts b/src/pages/video/index.ts
new file mode 100644
index 0000000..db8a652
--- /dev/null
+++ b/src/pages/video/index.ts
@@ -0,0 +1,3 @@
+import { gifTools } from './gif';
+
+export const videoTools = [...gifTools];
diff --git a/src/tools/defineTool.tsx b/src/tools/defineTool.tsx
index 41d34a2..2eb86ca 100644
--- a/src/tools/defineTool.tsx
+++ b/src/tools/defineTool.tsx
@@ -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;
diff --git a/src/tools/index.ts b/src/tools/index.ts
index e0f645a..9b2c682 100644
--- a/src/tools/index.ts
+++ b/src/tools/index.ts
@@ -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 = (
diff --git a/src/utils/gif.ts b/src/utils/gif.ts
new file mode 100644
index 0000000..e63f0d2
--- /dev/null
+++ b/src/utils/gif.ts
@@ -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 });
+}