diff --git a/.eslintrc b/.eslintrc
index 6c7bbbd..e857efc 100644
--- a/.eslintrc
+++ b/.eslintrc
@@ -42,6 +42,8 @@
"tailwindcss/classnames-order": "warn",
"tailwindcss/no-custom-classname": "warn",
"tailwindcss/no-contradicting-classname": "error",
- "@typescript-eslint/ban-types": "off"
+ "@typescript-eslint/ban-types": "off",
+ "@typescript-eslint/ban-ts-comment": "off",
+ "@typescript-eslint/no-explicit-any": "off"
}
}
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index a848dcf..33a1482 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -3,15 +3,14 @@ name: CI
on:
push:
branches:
- - main # or the branch you want to trigger the workflow on
+ - main
pull_request:
branches:
- main
jobs:
- build-and-deploy:
+ build-and-test:
runs-on: ubuntu-latest
-
steps:
- name: Checkout code
uses: actions/checkout@v3
@@ -19,7 +18,7 @@ jobs:
- name: Set up Node.js
uses: actions/setup-node@v3
with:
- node-version: '18' # Specify the Node.js version you want to use
+ node-version: '18'
- name: Install dependencies
run: npm install
@@ -30,6 +29,25 @@ jobs:
- name: Build project
run: npm run build
+ deploy:
+ if: github.ref == 'refs/heads/main'
+ needs: build-and-test
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v3
+
+ - name: Set up Node.js
+ uses: actions/setup-node@v3
+ with:
+ node-version: '18'
+
+ - name: Install dependencies
+ run: npm install
+
+ - name: Build project
+ run: npm run build
+
- name: Deploy to Netlify
uses: nwtgck/actions-netlify@v1.2
with:
@@ -42,4 +60,4 @@ jobs:
env:
NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }}
NETLIFY_SITE_ID: ${{ secrets.NETLIFY_SITE_ID }}
- timeout-minutes: 1
+ timeout-minutes: 20
diff --git a/.idea/workspace.xml b/.idea/workspace.xml
index 3e7d00d..142bd3f 100644
--- a/.idea/workspace.xml
+++ b/.idea/workspace.xml
@@ -4,12 +4,10 @@
-
+
-
-
-
-
+
+
@@ -38,37 +36,39 @@
- {
+ "keyToString": {
+ "ASKED_ADD_EXTERNAL_FILES": "true",
+ "ASKED_SHARE_PROJECT_CONFIGURATION_FILES": "true",
+ "RunOnceActivity.OpenProjectViewOnStart": "true",
+ "RunOnceActivity.ShowReadmeOnStart": "true",
+ "Vitest.compute function (1).executor": "Run",
+ "Vitest.compute function.executor": "Run",
+ "Vitest.mergeText.executor": "Run",
+ "Vitest.mergeText.should merge lines and preserve blank lines when deleteBlankLines is false.executor": "Run",
+ "Vitest.mergeText.should merge lines, preserve blank lines and trailing spaces when both deleteBlankLines and deleteTrailingSpaces are false.executor": "Run",
+ "git-widget-placeholder": "main",
+ "ignore.virus.scanning.warn.message": "true",
+ "kotlin-language-version-configured": "true",
+ "last_opened_file_path": "C:/Users/HP/IdeaProjects/omni-tools/src/assets",
+ "node.js.detected.package.eslint": "true",
+ "node.js.detected.package.tslint": "true",
+ "node.js.selected.package.eslint": "(autodetect)",
+ "node.js.selected.package.tslint": "(autodetect)",
+ "nodejs_package_manager_path": "npm",
+ "npm.dev.executor": "Run",
+ "npm.prebuild.executor": "Run",
+ "npm.script:create:tool.executor": "Run",
+ "npm.test.executor": "Run",
+ "prettierjs.PrettierConfiguration.Package": "C:\\Users\\HP\\IdeaProjects\\omni-tools\\node_modules\\prettier",
+ "project.structure.last.edited": "Problems",
+ "project.structure.proportion": "0.0",
+ "project.structure.side.proportion": "0.2",
+ "settings.editor.selected.configurable": "settings.typescriptcompiler",
+ "ts.external.directory.path": "C:\\Users\\HP\\IdeaProjects\\omni-tools\\node_modules\\typescript\\lib",
+ "vue.rearranger.settings.migration": "true"
}
-}]]>
+}
+
-
@@ -92,7 +92,33 @@
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -103,16 +129,6 @@
-
-
-
-
-
-
-
-
-
-
@@ -135,10 +151,11 @@
+
+
+
-
-
@@ -162,71 +179,10 @@
-
-
-
-
- 1718816900959
-
-
-
- 1718816900959
-
-
-
- 1718820618078
-
-
-
- 1718820618078
-
-
-
- 1718821853210
-
-
-
- 1718821853210
-
-
-
- 1718822309352
-
-
-
- 1718822309352
-
-
-
- 1718828318468
-
-
-
- 1718828318468
-
-
-
- 1718831745853
-
-
-
- 1718831745853
-
-
-
- 1718833519062
-
-
-
- 1718833519062
-
-
-
- 1718836910916
-
-
-
- 1718836910916
+
+
+
+
@@ -468,7 +424,159 @@
1719171905785
-
+
+
+ 1719172147719
+
+
+
+ 1719172147719
+
+
+
+ 1719184899883
+
+
+
+ 1719184899883
+
+
+
+ 1719185893875
+
+
+
+ 1719185893875
+
+
+
+ 1719186106818
+
+
+
+ 1719186106818
+
+
+
+ 1719187088285
+
+
+
+ 1719187088285
+
+
+
+ 1719187190823
+
+
+
+ 1719187190823
+
+
+
+ 1719188162583
+
+
+
+ 1719188162583
+
+
+
+ 1719193884293
+
+
+
+ 1719193884293
+
+
+
+ 1719197875189
+
+
+
+ 1719197875189
+
+
+
+ 1719274243788
+
+
+
+ 1719274243788
+
+
+
+ 1719275214988
+
+
+
+ 1719275214988
+
+
+
+ 1719277679968
+
+
+
+ 1719277679969
+
+
+
+ 1719281510362
+
+
+
+ 1719281510362
+
+
+
+ 1719281605998
+
+
+
+ 1719281605999
+
+
+
+ 1719282009150
+
+
+
+ 1719282009150
+
+
+
+ 1719282131977
+
+
+
+ 1719282131977
+
+
+
+ 1719283122691
+
+
+
+ 1719283122691
+
+
+
+ 1719296145698
+
+
+
+ 1719296145699
+
+
+
+ 1719297880629
+
+
+
+ 1719297880629
+
+
@@ -489,21 +597,6 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
@@ -513,8 +606,23 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
-
+
+
+
diff --git a/CODEOWNERS b/CODEOWNERS
new file mode 100644
index 0000000..798553f
--- /dev/null
+++ b/CODEOWNERS
@@ -0,0 +1 @@
+* @iib0011
diff --git a/Readme.md b/README.md
similarity index 89%
rename from Readme.md
rename to README.md
index 67270fa..1d6b1b4 100644
--- a/Readme.md
+++ b/README.md
@@ -7,7 +7,7 @@ all available for free and open for community contributions. Please don't forget
## Table of Contents
- [Features](#features)
-- [Installation](#installation)
+- [Contribute](#contribute)
- [License](#license)
- [Contact](#contact)
@@ -52,12 +52,16 @@ npm run dev
npm run script:create:tool my-tool-name folder1/folder2
```
-## Contributors
+Use `folder1\folder2` on Windows
+
+### Contributors
+[//]: # (
)
+
## License
This project is licensed under the MIT License. See the [LICENSE](LICENSE) file for details.
diff --git a/package-lock.json b/package-lock.json
index 9808b59..d332906 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -13,8 +13,11 @@
"@mui/icons-material": "^5.15.20",
"@mui/material": "^5.15.20",
"@types/lodash": "^4.17.5",
+ "@types/morsee": "^1.0.2",
+ "color": "^4.2.3",
"formik": "^2.4.6",
"lodash": "^4.17.21",
+ "morsee": "^1.0.9",
"notistack": "^3.0.1",
"react": "^18.3.1",
"react-dom": "^18.3.1",
@@ -27,6 +30,8 @@
"@commitlint/config-conventional": "^19.2.2",
"@testing-library/jest-dom": "^6.4.5",
"@testing-library/react": "^14.3.1",
+ "@types/color": "^3.0.6",
+ "@types/color-rgba": "^2.1.2",
"@types/node": "^20.12.12",
"@types/react": "^18.3.3",
"@types/react-dom": "^18.3.0",
@@ -2408,6 +2413,36 @@
"integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==",
"dev": true
},
+ "node_modules/@types/color": {
+ "version": "3.0.6",
+ "resolved": "https://registry.npmjs.org/@types/color/-/color-3.0.6.tgz",
+ "integrity": "sha512-NMiNcZFRUAiUUCCf7zkAelY8eV3aKqfbzyFQlXpPIEeoNDbsEHGpb854V3gzTsGKYj830I5zPuOwU/TP5/cW6A==",
+ "dev": true,
+ "dependencies": {
+ "@types/color-convert": "*"
+ }
+ },
+ "node_modules/@types/color-convert": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/@types/color-convert/-/color-convert-2.0.3.tgz",
+ "integrity": "sha512-2Q6wzrNiuEvYxVQqhh7sXM2mhIhvZR/Paq4FdsQkOMgWsCIkKvSGj8Le1/XalulrmgOzPMqNa0ix+ePY4hTrfg==",
+ "dev": true,
+ "dependencies": {
+ "@types/color-name": "*"
+ }
+ },
+ "node_modules/@types/color-name": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/@types/color-name/-/color-name-1.1.4.tgz",
+ "integrity": "sha512-hulKeREDdLFesGQjl96+4aoJSHY5b2GRjagzzcqCfIrWhe5vkCqIvrLbqzBaI1q94Vg8DNJZZqTR5ocdWmWclg==",
+ "dev": true
+ },
+ "node_modules/@types/color-rgba": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/@types/color-rgba/-/color-rgba-2.1.2.tgz",
+ "integrity": "sha512-gDV/fgs4Mpc+hcHygYnM2EDgcxaHmvIGrAVxZJjP38f2IXQKHiGf0XMHhFd+dz8EVPSNTwHL5DJ6yXsxEiCQkg==",
+ "dev": true
+ },
"node_modules/@types/conventional-commits-parser": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/@types/conventional-commits-parser/-/conventional-commits-parser-5.0.0.tgz",
@@ -2443,6 +2478,11 @@
"resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.5.tgz",
"integrity": "sha512-MBIOHVZqVqgfro1euRDWX7OO0fBVUUMrN6Pwm8LQsz8cWhEpihlvR70ENj3f40j58TNxZaWv2ndSkInykNBBJw=="
},
+ "node_modules/@types/morsee": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/@types/morsee/-/morsee-1.0.2.tgz",
+ "integrity": "sha512-WANv1kCyQtmGZTiov9FzFdt1X4wRtXYZA6B4YR3CghKgx4ychU7d1gkOx7oD+ddVGI+SWmWOPccco7pAc6wXeA=="
+ },
"node_modules/@types/node": {
"version": "20.14.6",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.14.6.tgz",
@@ -3527,11 +3567,22 @@
"node": ">=6"
}
},
+ "node_modules/color": {
+ "version": "4.2.3",
+ "resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz",
+ "integrity": "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==",
+ "dependencies": {
+ "color-convert": "^2.0.1",
+ "color-string": "^1.9.0"
+ },
+ "engines": {
+ "node": ">=12.5.0"
+ }
+ },
"node_modules/color-convert": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
- "dev": true,
"dependencies": {
"color-name": "~1.1.4"
},
@@ -3542,8 +3593,16 @@
"node_modules/color-name": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
- "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
- "dev": true
+ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="
+ },
+ "node_modules/color-string": {
+ "version": "1.9.1",
+ "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz",
+ "integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==",
+ "dependencies": {
+ "color-name": "^1.0.0",
+ "simple-swizzle": "^0.2.2"
+ }
},
"node_modules/commander": {
"version": "4.1.1",
@@ -6142,6 +6201,11 @@
"ufo": "^1.5.3"
}
},
+ "node_modules/morsee": {
+ "version": "1.0.9",
+ "resolved": "https://registry.npmjs.org/morsee/-/morsee-1.0.9.tgz",
+ "integrity": "sha512-8X8jKVUmZBHKpET9Ap6FPiwlAAASvv60M1K25/YwCU7veuj5MfYgaWX3oEPHtMGgC44IIkIKzyD73fduEKB/9g=="
+ },
"node_modules/mrmime": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.0.tgz",
@@ -7375,6 +7439,19 @@
"url": "https://github.com/sponsors/isaacs"
}
},
+ "node_modules/simple-swizzle": {
+ "version": "0.2.2",
+ "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz",
+ "integrity": "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==",
+ "dependencies": {
+ "is-arrayish": "^0.3.1"
+ }
+ },
+ "node_modules/simple-swizzle/node_modules/is-arrayish": {
+ "version": "0.3.2",
+ "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz",
+ "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ=="
+ },
"node_modules/sirv": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/sirv/-/sirv-2.0.4.tgz",
diff --git a/package.json b/package.json
index 4a5dec7..0034b7f 100644
--- a/package.json
+++ b/package.json
@@ -29,8 +29,11 @@
"@mui/icons-material": "^5.15.20",
"@mui/material": "^5.15.20",
"@types/lodash": "^4.17.5",
+ "@types/morsee": "^1.0.2",
+ "color": "^4.2.3",
"formik": "^2.4.6",
"lodash": "^4.17.21",
+ "morsee": "^1.0.9",
"notistack": "^3.0.1",
"react": "^18.3.1",
"react-dom": "^18.3.1",
@@ -43,6 +46,8 @@
"@commitlint/config-conventional": "^19.2.2",
"@testing-library/jest-dom": "^6.4.5",
"@testing-library/react": "^14.3.1",
+ "@types/color": "^3.0.6",
+ "@types/color-rgba": "^2.1.2",
"@types/node": "^20.12.12",
"@types/react": "^18.3.3",
"@types/react-dom": "^18.3.0",
diff --git a/scripts/create-tool.mjs b/scripts/create-tool.mjs
index 4739730..a8c1715 100644
--- a/scripts/create-tool.mjs
+++ b/scripts/create-tool.mjs
@@ -30,7 +30,7 @@ function createFolderStructure(basePath, foldersToCreateIndexCount) {
}
const indexPath = join(currentPath, 'index.ts')
if (!fs.existsSync(indexPath) && index < folderArray.length - 1 && index >= folderArray.length - 1 - foldersToCreateIndexCount) {
- fs.writeFileSync(indexPath, '// index.ts file')
+ fs.writeFileSync(indexPath, `export const ${currentPath.split(sep)[currentPath.split(sep).length - 1]}Tools = [];\n`)
console.log(`File created: ${indexPath}`)
}
// Recursively create the next folder
@@ -45,7 +45,7 @@ const toolNameCamelCase = toolName.replace(/-./g, (x) => x[1].toUpperCase())
const toolNameTitleCase =
toolName[0].toUpperCase() + toolName.slice(1).replace(/-/g, ' ')
const toolDir = join(toolsDir, toolName)
-
+const type = folder.split(sep)[folder.split(sep).length - 1]
await createFolderStructure(toolDir, folder.split(sep).length)
console.log(`Directory created: ${toolDir}`)
@@ -71,7 +71,6 @@ export default function ${capitalizeFirstLetter(toolNameCamelCase)}() {
}
`
)
-
createToolFile(
`meta.ts`,
`
@@ -79,9 +78,9 @@ import { defineTool } from '@tools/defineTool';
import { lazy } from 'react';
// import image from '@assets/text.png';
-export const tool = defineTool('${folder}', {
+export const tool = defineTool('${type}', {
name: '${toolNameTitleCase}',
- path: '/${toolName}',
+ path: '${toolName}',
// image,
description: '',
keywords: ['${toolName.split('-').join('\', \'')}'],
@@ -133,7 +132,7 @@ const indexContent = await readFile(toolsIndex, { encoding: 'utf-8' }).then(
indexContent.splice(
0,
0,
- `import { tool as ${toolNameCamelCase} } from './${toolName}/meta';`
+ `import { tool as ${type}${capitalizeFirstLetter(toolNameCamelCase)} } from './${toolName}/meta';`
)
writeFile(toolsIndex, indexContent.join('\n'))
console.log(`Added import in: ${toolsIndex}`)
diff --git a/src/assets/grey-pattern.png b/src/assets/grey-pattern.png
new file mode 100644
index 0000000..b734005
Binary files /dev/null and b/src/assets/grey-pattern.png differ
diff --git a/src/assets/image.png b/src/assets/image.png
new file mode 100644
index 0000000..5e01b1e
Binary files /dev/null and b/src/assets/image.png differ
diff --git a/src/components/InputHeader.tsx b/src/components/InputHeader.tsx
new file mode 100644
index 0000000..de0884a
--- /dev/null
+++ b/src/components/InputHeader.tsx
@@ -0,0 +1,10 @@
+import Typography from '@mui/material/Typography';
+import React from 'react';
+
+export default function InputHeader({ title }: { title: string }) {
+ return (
+
+ {title}
+
+ );
+}
diff --git a/src/components/Navbar/index.tsx b/src/components/Navbar/index.tsx
index 460a9ca..8fbc178 100644
--- a/src/components/Navbar/index.tsx
+++ b/src/components/Navbar/index.tsx
@@ -6,6 +6,7 @@ import Button from '@mui/material/Button';
import IconButton from '@mui/material/IconButton';
import { Link, useNavigate } from 'react-router-dom';
import githubIcon from '@assets/github-mark.png'; // Adjust the path to your GitHub icon
+import { Stack } from '@mui/material';
const Navbar: React.FC = () => {
const navigate = useNavigate();
@@ -14,46 +15,46 @@ const Navbar: React.FC = () => {
position="static"
style={{ backgroundColor: 'white', color: 'black' }}
>
-
+
navigate('/')}
fontSize={20}
- sx={{ flexGrow: 1, cursor: 'pointer' }}
+ sx={{ cursor: 'pointer' }}
color={'primary'}
>
OmniTools
-
-
-
-
- About Us
-
-
-
-
-
- Star us
-
+
+ Star us
+
+
);
diff --git a/src/components/ToolHeader.tsx b/src/components/ToolHeader.tsx
index 570f11b..ded1fff 100644
--- a/src/components/ToolHeader.tsx
+++ b/src/components/ToolHeader.tsx
@@ -29,7 +29,7 @@ export default function ToolHeader({
description
}: ToolHeaderProps) {
return (
-
+
{title}
diff --git a/src/components/ToolInputAndResult.tsx b/src/components/ToolInputAndResult.tsx
new file mode 100644
index 0000000..b34999c
--- /dev/null
+++ b/src/components/ToolInputAndResult.tsx
@@ -0,0 +1,21 @@
+import React, { ReactNode } from 'react';
+import Grid from '@mui/material/Grid';
+
+export default function ToolInputAndResult({
+ input,
+ result
+}: {
+ input: ReactNode;
+ result: ReactNode;
+}) {
+ return (
+
+
+ {input}
+
+
+ {result}
+
+
+ );
+}
diff --git a/src/components/input/InputFooter.tsx b/src/components/input/InputFooter.tsx
new file mode 100644
index 0000000..c8e93f6
--- /dev/null
+++ b/src/components/input/InputFooter.tsx
@@ -0,0 +1,24 @@
+import { Stack } from '@mui/material';
+import Button from '@mui/material/Button';
+import PublishIcon from '@mui/icons-material/Publish';
+import ContentPasteIcon from '@mui/icons-material/ContentPaste';
+import React from 'react';
+
+export default function InputFooter({
+ handleImport,
+ handleCopy
+}: {
+ handleImport: () => void;
+ handleCopy: () => void;
+}) {
+ return (
+
+ }>
+ Import from file
+
+ }>
+ Copy to clipboard
+
+
+ );
+}
diff --git a/src/components/input/ToolFileInput.tsx b/src/components/input/ToolFileInput.tsx
new file mode 100644
index 0000000..b2189ea
--- /dev/null
+++ b/src/components/input/ToolFileInput.tsx
@@ -0,0 +1,115 @@
+import { Box, styled, TextField, useTheme } from '@mui/material';
+import Typography from '@mui/material/Typography';
+import React, { useContext, useEffect, useRef, useState } from 'react';
+import InputHeader from '../InputHeader';
+import InputFooter from './InputFooter';
+import { CustomSnackBarContext } from '../../contexts/CustomSnackBarContext';
+import greyPattern from '@assets/grey-pattern.png';
+import { globalInputHeight } from '../../config/uiConfig';
+
+interface ToolFileInputProps {
+ value: File | null;
+ onChange: (file: File) => void;
+ accept: string[];
+ title?: string;
+}
+
+export default function ToolFileInput({
+ value,
+ onChange,
+ accept,
+ title = 'File'
+}: ToolFileInputProps) {
+ const [preview, setPreview] = useState(null);
+ const theme = useTheme();
+ const { showSnackBar } = useContext(CustomSnackBarContext);
+ const fileInputRef = useRef(null);
+
+ const handleCopy = () => {
+ navigator.clipboard
+ .writeText(value?.name ?? '')
+ .then(() => showSnackBar('Text copied', 'success'))
+ .catch((err) => {
+ showSnackBar('Failed to copy: ' + err, 'error');
+ });
+ };
+ useEffect(() => {
+ if (value) {
+ const objectUrl = URL.createObjectURL(value);
+ setPreview(objectUrl);
+
+ // Clean up memory when the component is unmounted or the file changes
+ return () => URL.revokeObjectURL(objectUrl);
+ } else {
+ setPreview(null);
+ }
+ }, [value]);
+
+ const handleFileChange = (event: React.ChangeEvent) => {
+ const file = event.target.files?.[0];
+ if (file) onChange(file);
+ };
+ const handleImportClick = () => {
+ fileInputRef.current?.click();
+ };
+ return (
+
+
+
+ {preview ? (
+
+
+
+ ) : (
+
+
+ Click here to select an image from your device, press Ctrl+V to
+ use an image from your clipboard, drag and drop a file from
+ desktop
+
+
+ )}
+
+
+
+
+ );
+}
diff --git a/src/components/input/ToolTextInput.tsx b/src/components/input/ToolTextInput.tsx
index b086c3e..d5a666d 100644
--- a/src/components/input/ToolTextInput.tsx
+++ b/src/components/input/ToolTextInput.tsx
@@ -5,6 +5,8 @@ import PublishIcon from '@mui/icons-material/Publish';
import ContentPasteIcon from '@mui/icons-material/ContentPaste';
import React, { useContext, useRef } from 'react';
import { CustomSnackBarContext } from '../../contexts/CustomSnackBarContext';
+import InputHeader from '../InputHeader';
+import InputFooter from './InputFooter';
export default function ToolTextInput({
value,
@@ -45,9 +47,7 @@ export default function ToolTextInput({
};
return (
-
- {title}
-
+
onChange(event.target.value)}
@@ -55,14 +55,7 @@ export default function ToolTextInput({
multiline
rows={10}
/>
-
- }>
- Import from file
-
- }>
- Copy to clipboard
-
-
+
void;
+ description: string;
+}
+
+const ColorSelector: React.FC = ({
+ value = '#ffffff',
+ onChange,
+ description
+}) => {
+ const [color, setColor] = useState(value);
+ const inputRef = useRef(null);
+
+ const handleColorChange = (event: ChangeEvent) => {
+ const val = event.target.value;
+ setColor(val);
+ onChange(val);
+ };
+
+ return (
+
+
+
+ inputRef.current?.click()}>
+
+
+
+
+
+ {description}
+
+
+ );
+};
+
+export default ColorSelector;
diff --git a/src/components/options/RadioWithTextField.tsx b/src/components/options/RadioWithTextField.tsx
index d82ca0c..75ce672 100644
--- a/src/components/options/RadioWithTextField.tsx
+++ b/src/components/options/RadioWithTextField.tsx
@@ -4,37 +4,38 @@ import { Field } from 'formik';
import Typography from '@mui/material/Typography';
import React from 'react';
import TextFieldWithDesc from './TextFieldWithDesc';
+import { globalDescriptionFontSize } from '../../config/uiConfig';
+import SimpleRadio from './SimpleRadio';
const RadioWithTextField = ({
fieldName,
- type,
+ radioValue,
title,
- onTypeChange,
+ onRadioChange,
value,
description,
- onTextChange
+ onTextChange,
+ typeDescription
}: {
fieldName: string;
title: string;
- type: T;
- onTypeChange: (val: T) => void;
+ radioValue: T;
+ onRadioChange: (val: T) => void;
value: string;
description: string;
onTextChange: (value: string) => void;
+ typeDescription?: string;
}) => {
- const onChange = () => onTypeChange(type);
+ const onChange = () => onRadioChange(radioValue);
return (
-
-
- {title}
-
+
void;
+ fieldName: string;
+ value: any;
+ title: string;
+ description?: string;
+}
+
+export default function SimpleRadio({
+ onChange,
+ fieldName,
+ value,
+ title,
+ description
+}: SimpleRadioProps) {
+ return (
+
+
+
+ {title}
+
+ {description && (
+
+ {description}
+
+ )}
+
+ );
+}
diff --git a/src/components/options/ToolOptionGroups.tsx b/src/components/options/ToolOptionGroups.tsx
new file mode 100644
index 0000000..76c48f9
--- /dev/null
+++ b/src/components/options/ToolOptionGroups.tsx
@@ -0,0 +1,27 @@
+import Typography from '@mui/material/Typography';
+import React, { ReactNode } from 'react';
+import { Box, Stack } from '@mui/material';
+
+interface ToolOptionGroup {
+ title: string;
+ component: ReactNode;
+}
+
+export default function ToolOptionGroups({
+ groups
+}: {
+ groups: ToolOptionGroup[];
+}) {
+ return (
+
+ {groups.map((group) => (
+
+
+ {group.title}
+
+ {group.component}
+
+ ))}
+
+ );
+}
diff --git a/src/components/result/ResultFooter.tsx b/src/components/result/ResultFooter.tsx
new file mode 100644
index 0000000..4de3176
--- /dev/null
+++ b/src/components/result/ResultFooter.tsx
@@ -0,0 +1,34 @@
+import { Stack } from '@mui/material';
+import Button from '@mui/material/Button';
+import DownloadIcon from '@mui/icons-material/Download';
+import ContentPasteIcon from '@mui/icons-material/ContentPaste';
+import React from 'react';
+
+export default function ResultFooter({
+ handleDownload,
+ handleCopy,
+ disabled
+}: {
+ handleDownload: () => void;
+ handleCopy: () => void;
+ disabled?: boolean;
+}) {
+ return (
+
+ }
+ >
+ Save as
+
+ }
+ >
+ Copy to clipboard
+
+
+ );
+}
diff --git a/src/components/result/ToolFileResult.tsx b/src/components/result/ToolFileResult.tsx
new file mode 100644
index 0000000..a76a04f
--- /dev/null
+++ b/src/components/result/ToolFileResult.tsx
@@ -0,0 +1,99 @@
+import { Box } from '@mui/material';
+import React, { useContext } from 'react';
+import InputHeader from '../InputHeader';
+import greyPattern from '@assets/grey-pattern.png';
+import { globalInputHeight } from '../../config/uiConfig';
+import ResultFooter from './ResultFooter';
+import { CustomSnackBarContext } from '../../contexts/CustomSnackBarContext';
+
+export default function ToolFileResult({
+ title = 'Result',
+ value,
+ extension
+}: {
+ title?: string;
+ value: File | null;
+ extension: string;
+}) {
+ const [preview, setPreview] = React.useState(null);
+ const { showSnackBar } = useContext(CustomSnackBarContext);
+
+ React.useEffect(() => {
+ if (value) {
+ const objectUrl = URL.createObjectURL(value);
+ setPreview(objectUrl);
+
+ return () => URL.revokeObjectURL(objectUrl);
+ } else {
+ setPreview(null);
+ }
+ }, [value]);
+
+ const handleCopy = () => {
+ if (value) {
+ const blob = new Blob([value], { type: value.type });
+ const clipboardItem = new ClipboardItem({ [value.type]: blob });
+
+ navigator.clipboard
+ .write([clipboardItem])
+ .then(() => showSnackBar('File copied', 'success'))
+ .catch((err) => {
+ showSnackBar('Failed to copy: ' + err, 'error');
+ });
+ }
+ };
+
+ const handleDownload = () => {
+ if (value) {
+ const filename = 'output-omni-tools.' + extension;
+
+ const blob = new Blob([value], { type: value.type });
+ const url = window.URL.createObjectURL(blob);
+ const a = document.createElement('a');
+ a.href = url;
+ a.download = filename;
+ document.body.appendChild(a);
+ a.click();
+ document.body.removeChild(a);
+ window.URL.revokeObjectURL(url);
+ }
+ };
+ return (
+
+
+
+ {preview && (
+
+
+
+ )}
+
+
+
+ );
+}
diff --git a/src/components/result/ToolTextResult.tsx b/src/components/result/ToolTextResult.tsx
index 3a4c80b..e672068 100644
--- a/src/components/result/ToolTextResult.tsx
+++ b/src/components/result/ToolTextResult.tsx
@@ -5,6 +5,8 @@ import DownloadIcon from '@mui/icons-material/Download';
import ContentPasteIcon from '@mui/icons-material/ContentPaste';
import React, { useContext } from 'react';
import { CustomSnackBarContext } from '../../contexts/CustomSnackBarContext';
+import InputHeader from '../InputHeader';
+import ResultFooter from './ResultFooter';
export default function ToolTextResult({
title = 'Result',
@@ -37,18 +39,9 @@ export default function ToolTextResult({
};
return (
-
- {title}
-
+
-
- }>
- Save as
-
- }>
- Copy to clipboard
-
-
+
);
}
diff --git a/src/config/uiConfig.ts b/src/config/uiConfig.ts
new file mode 100644
index 0000000..a9455c0
--- /dev/null
+++ b/src/config/uiConfig.ts
@@ -0,0 +1,2 @@
+export const globalInputHeight = 300;
+export const globalDescriptionFontSize = 12;
diff --git a/src/pages/home/index.tsx b/src/pages/home/index.tsx
index d5a3e87..775f7bd 100644
--- a/src/pages/home/index.tsx
+++ b/src/pages/home/index.tsx
@@ -12,21 +12,21 @@ import SearchIcon from '@mui/icons-material/Search';
import { Link, useNavigate } from 'react-router-dom';
import { filterTools, getToolsByCategory, tools } from '../../tools';
import { useState } from 'react';
-import { DefinedTool } from '../../tools/defineTool';
+import { DefinedTool } from '@tools/defineTool';
import Button from '@mui/material/Button';
const exampleTools: { label: string; url: string }[] = [
{
label: 'Create a transparent image',
- url: ''
+ url: '/png/create-transparent'
},
- { label: 'Convert text to morse code', url: '' },
+ { label: 'Convert text to morse code', url: '/string/to-morse' },
{ label: 'Change GIF speed', url: '' },
{ label: 'Pick a random item', url: '' },
{ label: 'Find and replace text', url: '' },
{ label: 'Convert emoji to image', url: '' },
{ label: 'Split a string', url: '/string/split' },
- { label: 'Calculate number sum', url: '' },
+ { label: 'Calculate number sum', url: '/number/sum' },
{ label: 'Pixelate an image', url: '' }
];
export default function Home() {
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 b08b174..621a259 100644
--- a/src/pages/image/png/change-colors-in-png/index.tsx
+++ b/src/pages/image/png/change-colors-in-png/index.tsx
@@ -1,11 +1,168 @@
import { Box } from '@mui/material';
-import React from 'react';
+import React, { useEffect, useState } from 'react';
import * as Yup from 'yup';
+import ToolFileInput from '../../../../components/input/ToolFileInput';
+import ToolFileResult from '../../../../components/result/ToolFileResult';
+import ToolOptions from '../../../../components/options/ToolOptions';
+import { Formik, useFormikContext } from 'formik';
+import ColorSelector from '../../../../components/options/ColorSelector';
+import Color from 'color';
+import TextFieldWithDesc from 'components/options/TextFieldWithDesc';
+import ToolInputAndResult from '../../../../components/ToolInputAndResult';
+import ToolOptionGroups from '../../../../components/options/ToolOptionGroups';
-const initialValues = {};
+const initialValues = {
+ fromColor: 'white',
+ toColor: 'black',
+ similarity: '10'
+};
const validationSchema = Yup.object({
// splitSeparator: Yup.string().required('The separator is required')
});
export default function ChangeColorsInPng() {
- return Lorem ipsum;
-}
\ No newline at end of file
+ const [input, setInput] = useState(null);
+ const [result, setResult] = useState(null);
+
+ const FormikListenerComponent = ({ input }: { input: File }) => {
+ const { values } = useFormikContext();
+ const { fromColor, toColor, similarity } = values;
+
+ useEffect(() => {
+ let fromRgb: [number, number, number];
+ let toRgb: [number, number, number];
+ try {
+ //@ts-ignore
+ fromRgb = Color(fromColor).rgb().array();
+ //@ts-ignore
+ toRgb = Color(toColor).rgb().array();
+ } catch (err) {
+ return;
+ }
+ const processImage = async (
+ file: File,
+ fromColor: [number, number, number],
+ toColor: [number, number, number],
+ similarity: number
+ ) => {
+ const canvas = document.createElement('canvas');
+ const ctx = canvas.getContext('2d');
+ if (ctx == null) return;
+ const img = new Image();
+
+ img.src = URL.createObjectURL(file);
+ await img.decode();
+
+ canvas.width = img.width;
+ canvas.height = img.height;
+ ctx.drawImage(img, 0, 0);
+
+ const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
+ const data: Uint8ClampedArray = imageData.data;
+
+ const colorDistance = (
+ c1: [number, number, number],
+ c2: [number, number, number]
+ ) => {
+ return Math.sqrt(
+ Math.pow(c1[0] - c2[0], 2) +
+ Math.pow(c1[1] - c2[1], 2) +
+ Math.pow(c1[2] - c2[2], 2)
+ );
+ };
+ const maxColorDistance = Math.sqrt(
+ Math.pow(255, 2) + Math.pow(255, 2) + Math.pow(255, 2)
+ );
+ const similarityThreshold = (similarity / 100) * maxColorDistance;
+
+ for (let i = 0; i < data.length; i += 4) {
+ const currentColor: [number, number, number] = [
+ data[i],
+ data[i + 1],
+ data[i + 2]
+ ];
+ if (colorDistance(currentColor, fromColor) <= similarityThreshold) {
+ data[i] = toColor[0]; // Red
+ data[i + 1] = toColor[1]; // Green
+ data[i + 2] = toColor[2]; // Blue
+ }
+ }
+
+ ctx.putImageData(imageData, 0, 0);
+
+ canvas.toBlob((blob) => {
+ if (blob) {
+ const newFile = new File([blob], file.name, { type: 'image/png' });
+ setResult(newFile);
+ }
+ }, 'image/png');
+ };
+
+ processImage(input, fromRgb, toRgb, Number(similarity));
+ }, [input, fromColor, toColor]);
+
+ return null;
+ };
+
+ return (
+
+
+ }
+ result={
+
+ }
+ />
+
+ {}}
+ >
+ {({ 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.'
+ }
+ />
+
+ )
+ }
+ ]}
+ />
+
+ )}
+
+
+
+ );
+}
diff --git a/src/pages/image/png/change-colors-in-png/meta.ts b/src/pages/image/png/change-colors-in-png/meta.ts
index d165de9..dd87e03 100644
--- a/src/pages/image/png/change-colors-in-png/meta.ts
+++ b/src/pages/image/png/change-colors-in-png/meta.ts
@@ -1,12 +1,13 @@
import { defineTool } from '@tools/defineTool';
import { lazy } from 'react';
-import image from '@assets/text.png';
+import image from '@assets/image.png';
-export const tool = defineTool('image/png', {
+export const tool = defineTool('png', {
name: 'Change colors in png',
- path: '/change-colors-in-png',
+ path: 'change-colors-in-png',
image,
- description: '',
+ description:
+ "World's simplest online Portable Network Graphics (PNG) color changer. Just import your PNG image in the editor on the left, select which colors to change, and you'll instantly get a new PNG with the new colors on the right. Free, quick, and very powerful. Import a PNG – replace its colors.",
keywords: ['change', 'colors', 'in', 'png'],
component: lazy(() => import('./index'))
});
diff --git a/src/pages/image/png/change-colors-in-png/service.ts b/src/pages/image/png/change-colors-in-png/service.ts
deleted file mode 100644
index e69de29..0000000
diff --git a/src/pages/image/png/create-transparent/index.tsx b/src/pages/image/png/create-transparent/index.tsx
new file mode 100644
index 0000000..fd9e3a7
--- /dev/null
+++ b/src/pages/image/png/create-transparent/index.tsx
@@ -0,0 +1,156 @@
+import { Box } from '@mui/material';
+import React, { useEffect, useState } from 'react';
+import * as Yup from 'yup';
+import ToolFileInput from '../../../../components/input/ToolFileInput';
+import ToolFileResult from '../../../../components/result/ToolFileResult';
+import ToolOptions from '../../../../components/options/ToolOptions';
+import { Formik, useFormikContext } from 'formik';
+import ColorSelector from '../../../../components/options/ColorSelector';
+import Color from 'color';
+import TextFieldWithDesc from 'components/options/TextFieldWithDesc';
+import ToolInputAndResult from '../../../../components/ToolInputAndResult';
+import ToolOptionGroups from '../../../../components/options/ToolOptionGroups';
+
+const initialValues = {
+ fromColor: 'white',
+ similarity: '10'
+};
+const validationSchema = Yup.object({
+ // splitSeparator: Yup.string().required('The separator is required')
+});
+export default function ChangeColorsInPng() {
+ const [input, setInput] = useState(null);
+ const [result, setResult] = useState(null);
+
+ const FormikListenerComponent = ({ input }: { input: File }) => {
+ const { values } = useFormikContext();
+ const { fromColor, similarity } = values;
+
+ useEffect(() => {
+ let fromRgb: [number, number, number];
+ try {
+ //@ts-ignore
+ fromRgb = Color(fromColor).rgb().array();
+ } catch (err) {
+ return;
+ }
+ const processImage = async (
+ file: File,
+ fromColor: [number, number, number],
+ similarity: number
+ ) => {
+ const canvas = document.createElement('canvas');
+ const ctx = canvas.getContext('2d');
+ if (ctx == null) return;
+ const img = new Image();
+
+ img.src = URL.createObjectURL(file);
+ await img.decode();
+
+ canvas.width = img.width;
+ canvas.height = img.height;
+ ctx.drawImage(img, 0, 0);
+
+ const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
+ const data: Uint8ClampedArray = imageData.data;
+
+ const colorDistance = (
+ c1: [number, number, number],
+ c2: [number, number, number]
+ ) => {
+ return Math.sqrt(
+ Math.pow(c1[0] - c2[0], 2) +
+ Math.pow(c1[1] - c2[1], 2) +
+ Math.pow(c1[2] - c2[2], 2)
+ );
+ };
+ const maxColorDistance = Math.sqrt(
+ Math.pow(255, 2) + Math.pow(255, 2) + Math.pow(255, 2)
+ );
+ const similarityThreshold = (similarity / 100) * maxColorDistance;
+
+ for (let i = 0; i < data.length; i += 4) {
+ const currentColor: [number, number, number] = [
+ data[i],
+ data[i + 1],
+ data[i + 2]
+ ];
+ if (colorDistance(currentColor, fromColor) <= similarityThreshold) {
+ data[i + 3] = 0; // Set alpha to 0 (transparent)
+ }
+ }
+
+ ctx.putImageData(imageData, 0, 0);
+
+ canvas.toBlob((blob) => {
+ if (blob) {
+ const newFile = new File([blob], file.name, { type: 'image/png' });
+ setResult(newFile);
+ }
+ }, 'image/png');
+ };
+
+ processImage(input, fromRgb, Number(similarity));
+ }, [input, fromColor]);
+
+ return null;
+ };
+
+ return (
+
+
+ }
+ result={
+
+ }
+ />
+
+ {}}
+ >
+ {({ 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.'
+ }
+ />
+
+ )
+ }
+ ]}
+ />
+
+ )}
+
+
+
+ );
+}
diff --git a/src/pages/image/png/create-transparent/meta.ts b/src/pages/image/png/create-transparent/meta.ts
new file mode 100644
index 0000000..8362377
--- /dev/null
+++ b/src/pages/image/png/create-transparent/meta.ts
@@ -0,0 +1,13 @@
+import { defineTool } from '@tools/defineTool';
+import { lazy } from 'react';
+import image from '@assets/image.png';
+
+export const tool = defineTool('png', {
+ name: 'Create transparent PNG',
+ path: 'create-transparent',
+ image,
+ description:
+ "World's simplest online Portable Network Graphics transparency maker. Just import your PNG image in the editor on the left and you will instantly get a transparent PNG on the right. Free, quick, and very powerful. Import a PNG – get a transparent PNG.",
+ keywords: ['create', 'transparent'],
+ component: lazy(() => import('./index'))
+});
diff --git a/src/pages/image/png/index.ts b/src/pages/image/png/index.ts
index 1f59860..5cda167 100644
--- a/src/pages/image/png/index.ts
+++ b/src/pages/image/png/index.ts
@@ -1,3 +1,4 @@
+import { tool as pngCreateTransparent } from './create-transparent/meta';
import { tool as changeColorsInPng } from './change-colors-in-png/meta';
-export const pngTools = [changeColorsInPng];
+export const pngTools = [changeColorsInPng, pngCreateTransparent];
diff --git a/src/pages/number/index.ts b/src/pages/number/index.ts
new file mode 100644
index 0000000..5bead17
--- /dev/null
+++ b/src/pages/number/index.ts
@@ -0,0 +1,3 @@
+import { tool as numberSum } from './sum/meta';
+
+export const numberTools = [numberSum];
diff --git a/src/pages/number/sum/index.tsx b/src/pages/number/sum/index.tsx
new file mode 100644
index 0000000..4a054f6
--- /dev/null
+++ b/src/pages/number/sum/index.tsx
@@ -0,0 +1,153 @@
+import { Box, Stack } from '@mui/material';
+import React, { useContext, useEffect, useState } from 'react';
+import ToolTextInput from '../../../components/input/ToolTextInput';
+import ToolTextResult from '../../../components/result/ToolTextResult';
+import { Formik, useFormikContext } from 'formik';
+import * as Yup from 'yup';
+import ToolOptions from '../../../components/options/ToolOptions';
+import { compute, NumberExtractionType } from './service';
+import { CustomSnackBarContext } from '../../../contexts/CustomSnackBarContext';
+import RadioWithTextField from '../../../components/options/RadioWithTextField';
+import ToolOptionGroups from '../../../components/options/ToolOptionGroups';
+import SimpleRadio from '../../../components/options/SimpleRadio';
+import CheckboxWithDesc from '../../../components/options/CheckboxWithDesc';
+import ToolInputAndResult from '../../../components/ToolInputAndResult';
+
+const initialValues = {
+ extractionType: 'smart' as NumberExtractionType,
+ separator: '\\n',
+ printRunningSum: false
+};
+const extractionTypes: {
+ title: string;
+ description: string;
+ type: NumberExtractionType;
+ withTextField: boolean;
+ textValueAccessor?: keyof typeof initialValues;
+}[] = [
+ {
+ title: 'Smart sum',
+ description: 'Auto detect numbers in the input.',
+ type: 'smart',
+ withTextField: false
+ },
+ {
+ title: 'Number Delimiter',
+ type: 'delimiter',
+ description:
+ 'Input SeparatorCustomize the number separator here. (By default a line break.)',
+ withTextField: true,
+ textValueAccessor: 'separator'
+ }
+];
+
+export default function SplitText() {
+ const [input, setInput] = useState('');
+ 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')
+ });
+
+ return (
+
+ }
+ 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)
+ }
+ />
+ )
+ }
+ ]}
+ />
+
+ )}
+
+
+
+ );
+}
diff --git a/src/pages/number/sum/meta.ts b/src/pages/number/sum/meta.ts
new file mode 100644
index 0000000..ffc8520
--- /dev/null
+++ b/src/pages/number/sum/meta.ts
@@ -0,0 +1,13 @@
+import { defineTool } from '@tools/defineTool';
+import { lazy } from 'react';
+// import image from '@assets/text.png';
+
+export const tool = defineTool('number', {
+ name: 'Number Sum Calculator',
+ path: 'sum',
+ // image,
+ description:
+ 'Quickly calculate the sum of numbers in your browser. To get your sum, just enter your list of numbers in the input field, adjust the separator between the numbers in the options below, and this utility will add up all these numbers.',
+ keywords: ['sum'],
+ component: lazy(() => import('./index'))
+});
diff --git a/src/pages/number/sum/service.ts b/src/pages/number/sum/service.ts
new file mode 100644
index 0000000..19cd968
--- /dev/null
+++ b/src/pages/number/sum/service.ts
@@ -0,0 +1,37 @@
+export type NumberExtractionType = 'smart' | 'delimiter';
+
+function getAllNumbers(text: string): number[] {
+ const regex = /\d+/g;
+ const matches = text.match(regex);
+ return matches ? matches.map(Number) : [];
+}
+
+export const compute = (
+ input: string,
+ extractionType: NumberExtractionType,
+ printRunningSum: boolean,
+ separator: string
+): string => {
+ let numbers: number[] = [];
+ if (extractionType === 'smart') {
+ numbers = getAllNumbers(input);
+ } else {
+ const parts = input.split(separator);
+ // Filter out and convert parts that are numbers
+ numbers = parts
+ .filter((part) => !isNaN(Number(part)) && part.trim() !== '')
+ .map(Number);
+ }
+ if (printRunningSum) {
+ let result: string = '';
+ let sum: number = 0;
+ for (const i of numbers) {
+ sum = sum + i;
+ result = result + sum + '\n';
+ }
+ return result;
+ } else
+ return numbers
+ .reduce((previousValue, currentValue) => previousValue + currentValue, 0)
+ .toString();
+};
diff --git a/src/pages/number/sum/sum.service.test.ts b/src/pages/number/sum/sum.service.test.ts
new file mode 100644
index 0000000..1a3b21b
--- /dev/null
+++ b/src/pages/number/sum/sum.service.test.ts
@@ -0,0 +1,64 @@
+import { describe, it, expect } from 'vitest';
+import { compute } from './service';
+
+describe('compute function', () => {
+ it('should correctly sum numbers in smart extraction mode', () => {
+ const input = 'The 2 cats have 4 and 7 kittens';
+ const result = compute(input, 'smart', false, ',');
+ expect(result).toBe('13');
+ });
+
+ it('should correctly sum numbers with custom delimiter', () => {
+ const input = '2,4,7';
+ const result = compute(input, 'delimiter', false, ',');
+ expect(result).toBe('13');
+ });
+
+ it('should return running sum in smart extraction mode', () => {
+ const input = 'The 2 cats have 4 and 7 kittens';
+ const result = compute(input, 'smart', true, ',');
+ expect(result).toBe('2\n6\n13\n');
+ });
+
+ it('should return running sum with custom delimiter', () => {
+ const input = '2,4,7';
+ const result = compute(input, 'delimiter', true, ',');
+ expect(result).toBe('2\n6\n13\n');
+ });
+
+ it('should handle empty input gracefully in smart mode', () => {
+ const input = '';
+ const result = compute(input, 'smart', false, ',');
+ expect(result).toBe('0');
+ });
+
+ it('should handle empty input gracefully in delimiter mode', () => {
+ const input = '';
+ const result = compute(input, 'delimiter', false, ',');
+ expect(result).toBe('0');
+ });
+
+ it('should handle input with no numbers in smart mode', () => {
+ const input = 'There are no numbers here';
+ const result = compute(input, 'smart', false, ',');
+ expect(result).toBe('0');
+ });
+
+ it('should handle input with no numbers in delimiter mode', () => {
+ const input = 'a,b,c';
+ const result = compute(input, 'delimiter', false, ',');
+ expect(result).toBe('0');
+ });
+
+ it('should ignore non-numeric parts in delimiter mode', () => {
+ const input = '2,a,4,b,7';
+ const result = compute(input, 'delimiter', false, ',');
+ expect(result).toBe('13');
+ });
+
+ it('should handle different separators', () => {
+ const input = '2;4;7';
+ const result = compute(input, 'delimiter', false, ';');
+ expect(result).toBe('13');
+ });
+});
diff --git a/src/pages/string/index.ts b/src/pages/string/index.ts
index b53d52c..3011347 100644
--- a/src/pages/string/index.ts
+++ b/src/pages/string/index.ts
@@ -1,4 +1,5 @@
+import { tool as stringToMorse } from './to-morse/meta';
import { tool as stringSplit } from './split/meta';
import { tool as stringJoin } from './join/meta';
-export const stringTools = [stringSplit, stringJoin];
+export const stringTools = [stringSplit, stringJoin, stringToMorse];
diff --git a/src/pages/string/join/index.tsx b/src/pages/string/join/index.tsx
index e61e714..8ff3f9d 100644
--- a/src/pages/string/join/index.tsx
+++ b/src/pages/string/join/index.tsx
@@ -9,6 +9,8 @@ import { mergeText } from './service';
import { CustomSnackBarContext } from '../../../contexts/CustomSnackBarContext';
import TextFieldWithDesc from '../../../components/options/TextFieldWithDesc';
import CheckboxWithDesc from '../../../components/options/CheckboxWithDesc';
+import ToolOptionGroups from '../../../components/options/ToolOptionGroups';
+import ToolInputAndResult from '../../../components/ToolInputAndResult';
import Info from './Info';
import Separator from '../../../tools/Separator';
@@ -56,12 +58,12 @@ const exampleCards = [
title: 'Merge a To-Do List',
description:
"In this example, we merge a bullet point list into one sentence, separating each item by the word 'and'. We also remove all empty lines and trailing spaces. If we didn't remove the empty lines, then they'd be joined with the separator word, making the separator word appear multiple times. If we didn't remove the trailing tabs and spaces, then they'd create extra spacing in the joined text and it wouldn't look nice.",
- sampleText: `clean the house
+ sampleText: `clean the house
-go shopping
+go shopping
feed the cat
-make dinner
+make dinner
build a rocket ship and fly away`,
sampleResult: `clean the house and go shopping and feed the cat and make dinner and build a rocket ship and fly away`,
requiredOptions: {
@@ -177,18 +179,16 @@ export default function JoinText() {
return (
-
-
+
-
-
-
-
-
+ }
+ result={}
+ />
(
-
- Text Merged Options
-
- setFieldValue(mergeOptions.accessor, value)
+
+ setFieldValue(mergeOptions.accessor, value)
+ }
+ description={mergeOptions.description}
+ />
+ )
+ },
+ {
+ title: 'Blank Lines and Trailing Spaces',
+ component: blankTrailingOptions.map((option) => (
+
+ setFieldValue(option.accessor, value)
+ }
+ description={option.description}
+ />
+ ))
}
- description={mergeOptions.description}
- />
-
-
-
- Blank Lines and Trailing Spaces
-
- {blankTrailingOptions.map((option, index) => (
- setFieldValue(option.accessor, value)}
- description={option.description}
- />
- ))}
-
+ ]}
+ />
)}
diff --git a/src/pages/string/join/service.ts b/src/pages/string/join/service.ts
index b736dd6..e636238 100644
--- a/src/pages/string/join/service.ts
+++ b/src/pages/string/join/service.ts
@@ -5,23 +5,13 @@ export function mergeText(
joinCharacter: string = ''
): string {
let processedLines: string[] = text.split('\n');
-
if (deleteTrailingSpaces) {
processedLines = processedLines.map((line) => line.trimEnd());
}
if (deleteBlankLines) {
- processedLines = processedLines.filter((line) => line.trim() !== '');
- } else {
- processedLines = processedLines.map((line) =>
- line.trim() === '' ? line + '\r\n\n' : line
- );
+ processedLines = processedLines.filter((line) => line.trim());
}
+
return processedLines.join(joinCharacter);
}
-
-// Example text to use
-`This is a line with trailing spaces
-Another line with trailing spaces
-
-Final line without trailing spaces`;
diff --git a/src/pages/string/split/index.tsx b/src/pages/string/split/index.tsx
index c28e229..ad4f23d 100644
--- a/src/pages/string/split/index.tsx
+++ b/src/pages/string/split/index.tsx
@@ -1,16 +1,17 @@
-import { Box, Stack, TextField } from '@mui/material';
+import { Box, Stack } from '@mui/material';
import Grid from '@mui/material/Grid';
-import Typography from '@mui/material/Typography';
-import React, { useContext, useEffect, useRef, useState } from 'react';
+import React, { useContext, useEffect, useState } from 'react';
import ToolTextInput from '../../../components/input/ToolTextInput';
import ToolTextResult from '../../../components/result/ToolTextResult';
-import { Field, Formik, FormikProps, useFormikContext } from 'formik';
+import { Formik, useFormikContext } from 'formik';
import * as Yup from 'yup';
import ToolOptions from '../../../components/options/ToolOptions';
import { compute, SplitOperatorType } from './service';
import { CustomSnackBarContext } from '../../../contexts/CustomSnackBarContext';
import RadioWithTextField from '../../../components/options/RadioWithTextField';
import TextFieldWithDesc from '../../../components/options/TextFieldWithDesc';
+import ToolOptionGroups from '../../../components/options/ToolOptionGroups';
+import ToolInputAndResult from '../../../components/ToolInputAndResult';
const initialValues = {
splitSeparatorType: 'symbol' as SplitOperatorType,
@@ -126,14 +127,10 @@ export default function SplitText() {
return (
-
-
-
-
-
-
-
-
+ }
+ result={}
+ />
(
-
- Split separator options
- {splitOperators.map(({ title, description, type }) => (
-
- setFieldValue('splitSeparatorType', type)
- }
- onTextChange={(val) => setFieldValue(`${type}Value`, val)}
- />
- ))}
-
-
- Output separator options
- {outputOptions.map((option) => (
- setFieldValue(option.accessor, value)}
- description={option.description}
- />
- ))}
-
+ (
+
+ setFieldValue('splitSeparatorType', type)
+ }
+ onTextChange={(val) =>
+ setFieldValue(`${type}Value`, val)
+ }
+ />
+ )
+ )
+ },
+ {
+ title: 'Output separator options',
+ component: outputOptions.map((option) => (
+
+ setFieldValue(option.accessor, value)
+ }
+ description={option.description}
+ />
+ ))
+ }
+ ]}
+ />
)}
diff --git a/src/pages/string/to-morse/index.tsx b/src/pages/string/to-morse/index.tsx
new file mode 100644
index 0000000..3894e9d
--- /dev/null
+++ b/src/pages/string/to-morse/index.tsx
@@ -0,0 +1,95 @@
+import { Box, Stack } from '@mui/material';
+import Grid from '@mui/material/Grid';
+import React, { useContext, useEffect, useState } from 'react';
+import ToolTextInput from '../../../components/input/ToolTextInput';
+import ToolTextResult from '../../../components/result/ToolTextResult';
+import { Formik, useFormikContext } from 'formik';
+import * as Yup from 'yup';
+import ToolOptions from '../../../components/options/ToolOptions';
+import { compute } from './service';
+import { CustomSnackBarContext } from '../../../contexts/CustomSnackBarContext';
+import TextFieldWithDesc from '../../../components/options/TextFieldWithDesc';
+import ToolOptionGroups from '../../../components/options/ToolOptionGroups';
+import ToolInputAndResult from '../../../components/ToolInputAndResult';
+
+const initialValues = {
+ dotSymbol: '.',
+ dashSymbol: '-'
+};
+
+export default function ToMorse() {
+ const [input, setInput] = useState('');
+ 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 validationSchema = Yup.object({
+ // splitSeparator: Yup.string().required('The separator is required')
+ });
+
+ return (
+
+ }
+ result={}
+ />
+
+ {}}
+ >
+ {({ setFieldValue, values }) => (
+
+
+ setFieldValue('dotSymbol', val)}
+ />
+ )
+ },
+ {
+ title: 'Long Signal',
+ component: (
+ setFieldValue('dashSymbol', val)}
+ />
+ )
+ }
+ ]}
+ />
+
+ )}
+
+
+
+ );
+}
diff --git a/src/pages/string/to-morse/meta.ts b/src/pages/string/to-morse/meta.ts
new file mode 100644
index 0000000..80b2b4d
--- /dev/null
+++ b/src/pages/string/to-morse/meta.ts
@@ -0,0 +1,13 @@
+import { defineTool } from '@tools/defineTool';
+import { lazy } from 'react';
+// import image from '@assets/text.png';
+
+export const tool = defineTool('string', {
+ name: 'String To morse',
+ path: 'to-morse',
+ // image,
+ description:
+ "World's simplest browser-based utility for converting text to Morse code. Load your text in the input form on the left and you'll instantly get Morse code in the output area. Powerful, free, and fast. Load text – get Morse code.",
+ keywords: ['to', 'morse'],
+ component: lazy(() => import('./index'))
+});
diff --git a/src/pages/string/to-morse/service.ts b/src/pages/string/to-morse/service.ts
new file mode 100644
index 0000000..6f81b6e
--- /dev/null
+++ b/src/pages/string/to-morse/service.ts
@@ -0,0 +1,9 @@
+import { encode } from 'morsee';
+
+export const compute = (
+ input: string,
+ dotSymbol: string,
+ dashSymbol: string
+): string => {
+ return encode(input).replaceAll('.', dotSymbol).replaceAll('-', dashSymbol);
+};
diff --git a/src/pages/string/to-morse/to-morse.service.test.ts b/src/pages/string/to-morse/to-morse.service.test.ts
new file mode 100644
index 0000000..6f87b54
--- /dev/null
+++ b/src/pages/string/to-morse/to-morse.service.test.ts
@@ -0,0 +1,50 @@
+import { describe, it, expect } from 'vitest';
+import { compute } from './service';
+
+describe('compute function', () => {
+ it('should replace dots and dashes with specified symbols', () => {
+ const input = 'test';
+ const dotSymbol = '*';
+ const dashSymbol = '#';
+ const result = compute(input, dotSymbol, dashSymbol);
+ const expected = '# * *** #';
+ expect(result).toBe(expected);
+ });
+
+ it('should return an empty string for empty input', () => {
+ const input = '';
+ const dotSymbol = '*';
+ const dashSymbol = '#';
+ const result = compute(input, dotSymbol, dashSymbol);
+ expect(result).toBe('');
+ });
+
+ // Test case 3: Special characters handling
+ it('should handle input with special characters', () => {
+ const input = 'hello, world!';
+ const dotSymbol = '*';
+ const dashSymbol = '#';
+ const result = compute(input, dotSymbol, dashSymbol);
+ const expected =
+ '**** * *#** *#** ### ##**## / *## ### *#* *#** #** #*#*##';
+ expect(result).toBe(expected);
+ });
+
+ it('should work with different symbols for dots and dashes', () => {
+ const input = 'morse';
+ const dotSymbol = '!';
+ const dashSymbol = '@';
+ const result = compute(input, dotSymbol, dashSymbol);
+ const expected = '@@ @@@ !@! !!! !';
+ expect(result).toBe(expected);
+ });
+
+ it('should handle numeric input correctly', () => {
+ const input = '12345';
+ const dotSymbol = '*';
+ const dashSymbol = '#';
+ const result = compute(input, dotSymbol, dashSymbol);
+ const expected = '*#### **### ***## ****# *****'; // This depends on how "12345" is encoded in morse code
+ expect(result).toBe(expected);
+ });
+});
diff --git a/src/tools/index.ts b/src/tools/index.ts
index 2f5e19b..f460aa9 100644
--- a/src/tools/index.ts
+++ b/src/tools/index.ts
@@ -2,8 +2,13 @@ import { stringTools } from '../pages/string';
import { imageTools } from '../pages/image';
import { DefinedTool } from './defineTool';
import { capitalizeFirstLetter } from '../utils/string';
+import { numberTools } from '../pages/number';
-export const tools: DefinedTool[] = [...imageTools, ...stringTools];
+export const tools: DefinedTool[] = [
+ ...imageTools,
+ ...stringTools,
+ ...numberTools
+];
const categoriesDescriptions: { type: string; value: string }[] = [
{
type: 'string',
@@ -14,6 +19,11 @@ const categoriesDescriptions: { type: string; value: string }[] = [
type: 'png',
value:
'Tools for working with PNG images – convert PNGs to JPGs, create transparent PNGs, change PNG colors, crop, rotate, resize PNGs, and much more.'
+ },
+ {
+ type: 'number',
+ value:
+ 'Tools for working with numbers – generate number sequences, convert numbers to words and words to numbers, sort, round, factor numbers, and much more.'
}
];
export const filterTools = (