Merge remote-tracking branch 'origin/main' into truncate

# Conflicts:
#	src/pages/tools/string/index.ts
This commit is contained in:
Ibrahima G. Coulibaly
2025-03-07 22:13:09 +00:00
201 changed files with 4915 additions and 2145 deletions

1
.codebuddy/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
db/

39
.codebuddy/summary.md Normal file
View File

@@ -0,0 +1,39 @@
# Project Summary
## Overview of Technologies Used
This project is primarily built using the following technologies:
- **Languages**: TypeScript, JavaScript, HTML, CSS
- **Frameworks**:
- React (for building user interfaces)
- Playwright (for end-to-end testing)
- **Main Libraries**:
- Tailwind CSS (for styling)
- MUI (Material-UI for components)
- pnpm (for package management)
## Purpose of the Project
The project appears to be a web application that provides various tools for image, JSON, list, number, and string manipulations. It is designed to offer users functionalities such as converting image formats, generating random numbers, and manipulating strings. The structure indicates a focus on modular components, making it easy to extend or modify specific tools without affecting the entire application.
## Build and Configuration Files
The following files are relevant for the configuration and building of the project:
- `Dockerfile`: `/Dockerfile`
- `package.json`: `/package.json`
- `pnpm-lock.yaml`: `/pnpm-lock.yaml`
- `playwright.config.ts`: `/playwright.config.ts`
- `postcss.config.mjs`: `/postcss.config.mjs`
- `tailwind.config.mjs`: `/tailwind.config.mjs`
- `tsconfig.json`: `/tsconfig.json`
- `vite.config.ts`: `/vite.config.ts`
- `commitlint.config.js`: `/commitlint.config.js`
## Source Files Directory
The source files can be found in the following directory:
- `/src`
## Documentation Files Location
Documentation files are located in the root directory:
- `README.md`: `/README.md`
- `LICENSE`: `/LICENSE`
- `CODEOWNERS`: `/CODEOWNERS`
This summary encapsulates the key aspects of the project, including its technological stack, purpose, file structure, and documentation locations.

2
.github/FUNDING.yml vendored Normal file
View File

@@ -0,0 +1,2 @@
github: [iib0011]
buy_me_a_coffee: iib0011

2
.gitignore vendored
View File

@@ -38,3 +38,5 @@ yarn-error.log*
/test-results /test-results
/playwright-report /playwright-report
dist.zip

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,4 @@
<changelist name="Uncommitted_changes_before_Checkout_at_2_27_2025_11_44_AM_[Changes]" date="1740656670640" recycled="false" toDelete="true">
<option name="PATH" value="$PROJECT_DIR$/.idea/shelf/Uncommitted_changes_before_Checkout_at_2_27_2025_11_44_AM_[Changes]/shelved.patch" />
<option name="DESCRIPTION" value="Uncommitted changes before Checkout at 2/27/2025 11:44 AM [Changes]" />
</changelist>

1105
.idea/workspace.xml generated

File diff suppressed because it is too large Load Diff

View File

@@ -1 +1 @@
* @iib0011 * @iib0011 @Chesterkxng

View File

@@ -1,4 +1,4 @@
FROM node:20 as build FROM node:20 AS build
WORKDIR /app WORKDIR /app

101
README.md
View File

@@ -1,45 +1,82 @@
# OmniTools <p align="center">
<img src="src/assets/logo.png" width="300" />
<br /><br />
<a href="https://github.com/iib0011/omni-tools/releases">
<img src="https://img.shields.io/badge/version-0.1.0-blue?style=for-the-badge" />
</a>
<a href="https://hub.docker.com/r/iib0011/omni-tools">
<img src="https://img.shields.io/docker/pulls/iib0011/omni-tools?style=for-the-badge&logo=docker" />
</a>
<a href="https://github.com/iib0011">
<img src="https://img.shields.io/github/stars/iib0011/omni-tools?style=for-the-badge&logo=github" />
</a>
<a href="https://github.com/iib0011/omni-tools/blob/main/LICENSE">
<img src="https://img.shields.io/github/license/iib0011/omni-tools?style=for-the-badge" />
</a>
<!--
<a href="https://discord.gg/SDbbn3hT4b">
<img src="https://img.shields.io/discord/1342971141823664179?label=Discord&style=for-the-badge" />
</a>
-->
<br /><br />
</p>
Welcome to **OmniTools**, an open-source alternative to OnlineTools.com. Welcome to OmniTools, a self-hosted web app offering a variety of online tools to simplify everyday tasks.
This project offers a variety of online tools to help with everyday tasks, Whether you are coding, manipulating images or crunching numbers, OmniTools has you covered. Please don't forget to
all available for free and open for community contributions. Please don't forget to star the repo to support us. star the repo to support us.
Here is the [live](https://omnitools.netlify.app/) website. Here is the [demo](https://omnitools.netlify.app/) website.
![img.png](img.png) ![img.png](img.png)
## Table of Contents ## Table of Contents
- [Features](#features) - [Features](#features)
- [Self-host](#self-host) - [Self-host](#self-hostrun)
- [Contribute](#contribute) - [Contribute](#contribute)
- [License](#license) - [License](#license)
- [Contact](#contact) - [Contact](#contact)
## Features ## Features
OmniTools includes a variety of tools, such as: We strive to offer a variety of tools, including:
1. **Image/Video/Binary tools** ## **Image/Video/Binary Tools**
- Image Resizer, Image converter, Video trimmer, video reverser, etc. - Image Resizer
- Image Converter
- Video Trimmer
- Video Reverser
- And more...
2. **Math tools** ## **String/List Tools**
- Generate prime numbers, generate perfect numbers etc. - Case Converters
- List Shuffler
- Text Formatters
- And more...
3. **String/List Tools** ## **Date and Time Tools**
- Case converters, shuffle list, text formatters, etc. - Date Calculators
- Time Zone Converters
- And more...
4. **Date and Time Tools** ## **Math Tools**
- Date calculators, time zone converters, etc. - Generate Prime Numbers
- Generate Perfect Numbers
- And more...
5. **Miscellaneous Tools** ## **Miscellaneous Tools**
- JSON, XML tools, CSV tools etc. - JSON Tools
- XML Tools
- CSV Tools
- And more...
## Self-host Stay tuned as we continue to expand and improve our collection!
## Self-host/Run
```bash ```bash
docker run -d --name omni-tools --restart unless-stopped -p 8080:80 iib0011/omni-tools:latest docker run -d --name omni-tools --restart unless-stopped -p 8080:80 iib0011/omni-tools:latest
@@ -47,6 +84,8 @@ docker run -d --name omni-tools --restart unless-stopped -p 8080:80 iib0011/omni
## Contribute ## Contribute
This is a React Project with Typescript Material UI.
### Project setup ### Project setup
```bash ```bash
@@ -59,7 +98,7 @@ npm run dev
### Create a new tool ### Create a new tool
```bash ```bash
npm run script:create:tool my-tool-name folder1/folder2 npm run script:create:tool my-tool-name folder1/folder2 # npm run script:create:tool compress image/png
``` ```
Use `folder1\folder2` on Windows Use `folder1\folder2` on Windows
@@ -76,20 +115,30 @@ npm run test
npm run test:e2e npm run test:e2e
``` ```
<img src="https://api.star-history.com/svg?repos=iib0011/omni-tools&type=Date"/>
## 🤝 Looking to contribute?
We welcome contributions! You can help by:
- ✅ Reporting bugs
- ✅ Suggesting new features in Github issues or [here](https://tally.so/r/nrkkx2)
- ✅ Improving documentation
- ✅ Submitting pull requests
You can also join our [Discord server](https://discord.gg/SDbbn3hT4b)
### Contributors ### Contributors
<a href="https://github.com/iib0011/omni-tools/graphs/contributors"> <a href="https://github.com/iib0011/omni-tools/graphs/contributors">
<img src="https://contrib.rocks/image?repo=iib0011/omni-tools" /> <img src="https://contrib.rocks/image?repo=iib0011/omni-tools" />
</a> </a>
[//]: # (<img src="https://api.star-history.com/svg?repos=iib0011/omni-tools&type=Date">) ## Contact
For any questions or suggestions, feel free to open an issue or contact me at:
[ibracool99@gmail.com](mailto:ibracool99@gmail.com)
## License ## License
This project is licensed under the MIT License. See the [LICENSE](LICENSE) file for details. This project is licensed under the MIT License. See the [LICENSE](LICENSE) file for details.
## Contact
For any questions or suggestions, feel free to open an issue or contact us at:
Email: ibracool99@gmail.com

BIN
img.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 97 KiB

After

Width:  |  Height:  |  Size: 140 KiB

View File

@@ -1,13 +1,14 @@
<!doctype html> <!doctype html>
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" /> <link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <link href="/assets/fonts/plus-jakarta/plus-jakarta.css" rel="stylesheet" />
<title>Omni Tools</title> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
</head> <title>Omni Tools</title>
<body> </head>
<div id="root"></div> <body>
<script type="module" src="/src/index.tsx"></script> <div id="root"></div>
</body> <script type="module" src="/src/index.tsx"></script>
</body>
</html> </html>

64
package-lock.json generated
View File

@@ -16,6 +16,7 @@
"@types/lodash": "^4.17.5", "@types/lodash": "^4.17.5",
"@types/morsee": "^1.0.2", "@types/morsee": "^1.0.2",
"@types/omggif": "^1.0.5", "@types/omggif": "^1.0.5",
"browser-image-compression": "^2.0.2",
"color": "^4.2.3", "color": "^4.2.3",
"formik": "^2.4.6", "formik": "^2.4.6",
"jimp": "^0.22.12", "jimp": "^0.22.12",
@@ -28,11 +29,13 @@
"react-dom": "^18.3.1", "react-dom": "^18.3.1",
"react-helmet": "^6.1.0", "react-helmet": "^6.1.0",
"react-router-dom": "^6.23.1", "react-router-dom": "^6.23.1",
"type-fest": "^4.35.0",
"yup": "^1.4.0" "yup": "^1.4.0"
}, },
"devDependencies": { "devDependencies": {
"@commitlint/cli": "^19.3.0", "@commitlint/cli": "^19.3.0",
"@commitlint/config-conventional": "^19.2.2", "@commitlint/config-conventional": "^19.2.2",
"@iconify/react": "^5.2.0",
"@testing-library/jest-dom": "^6.4.5", "@testing-library/jest-dom": "^6.4.5",
"@testing-library/react": "^14.3.1", "@testing-library/react": "^14.3.1",
"@types/color": "^3.0.6", "@types/color": "^3.0.6",
@@ -1454,6 +1457,29 @@
"deprecated": "Use @eslint/object-schema instead", "deprecated": "Use @eslint/object-schema instead",
"dev": true "dev": true
}, },
"node_modules/@iconify/react": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/@iconify/react/-/react-5.2.0.tgz",
"integrity": "sha512-7Sdjrqq3fkkQNks9SY3adGC37NQTHsBJL2PRKlQd455PoDi9s+Es9AUTY+vGLFOYs5yO9w9yCE42pmxCwG26WA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@iconify/types": "^2.0.0"
},
"funding": {
"url": "https://github.com/sponsors/cyberalien"
},
"peerDependencies": {
"react": ">=16"
}
},
"node_modules/@iconify/types": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/@iconify/types/-/types-2.0.0.tgz",
"integrity": "sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg==",
"dev": true,
"license": "MIT"
},
"node_modules/@isaacs/cliui": { "node_modules/@isaacs/cliui": {
"version": "8.0.2", "version": "8.0.2",
"resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz",
@@ -3843,6 +3869,15 @@
"node": ">=8" "node": ">=8"
} }
}, },
"node_modules/browser-image-compression": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/browser-image-compression/-/browser-image-compression-2.0.2.tgz",
"integrity": "sha512-pBLlQyUf6yB8SmmngrcOw3EoS4RpQ1BcylI3T9Yqn7+4nrQTXJD4sJDe5ODnJdrvNMaio5OicFo75rDyJD2Ucw==",
"license": "MIT",
"dependencies": {
"uzip": "0.20201231.0"
}
},
"node_modules/browserslist": { "node_modules/browserslist": {
"version": "4.23.1", "version": "4.23.1",
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.1.tgz", "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.1.tgz",
@@ -5728,6 +5763,19 @@
"url": "https://github.com/sponsors/sindresorhus" "url": "https://github.com/sponsors/sindresorhus"
} }
}, },
"node_modules/globals/node_modules/type-fest": {
"version": "0.20.2",
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz",
"integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==",
"dev": true,
"license": "(MIT OR CC0-1.0)",
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/globalthis": { "node_modules/globalthis": {
"version": "1.0.4", "version": "1.0.4",
"resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz", "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz",
@@ -9351,12 +9399,12 @@
} }
}, },
"node_modules/type-fest": { "node_modules/type-fest": {
"version": "0.20.2", "version": "4.35.0",
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.35.0.tgz",
"integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", "integrity": "sha512-2/AwEFQDFEy30iOLjrvHDIH7e4HEWH+f1Yl1bI5XMqzuoCUqwYCdxachgsgv0og/JdVZUhbfjcJAoHj5L1753A==",
"dev": true, "license": "(MIT OR CC0-1.0)",
"engines": { "engines": {
"node": ">=10" "node": ">=16"
}, },
"funding": { "funding": {
"url": "https://github.com/sponsors/sindresorhus" "url": "https://github.com/sponsors/sindresorhus"
@@ -9539,6 +9587,12 @@
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="
}, },
"node_modules/uzip": {
"version": "0.20201231.0",
"resolved": "https://registry.npmjs.org/uzip/-/uzip-0.20201231.0.tgz",
"integrity": "sha512-OZeJfZP+R0z9D6TmBgLq2LHzSSptGMGDGigGiEe0pr8UBe/7fdflgHlHBNDASTXB5jnFuxHpNaJywSg8YFeGng==",
"license": "MIT"
},
"node_modules/vite": { "node_modules/vite": {
"version": "5.3.1", "version": "5.3.1",
"resolved": "https://registry.npmjs.org/vite/-/vite-5.3.1.tgz", "resolved": "https://registry.npmjs.org/vite/-/vite-5.3.1.tgz",

View File

@@ -33,6 +33,7 @@
"@types/lodash": "^4.17.5", "@types/lodash": "^4.17.5",
"@types/morsee": "^1.0.2", "@types/morsee": "^1.0.2",
"@types/omggif": "^1.0.5", "@types/omggif": "^1.0.5",
"browser-image-compression": "^2.0.2",
"color": "^4.2.3", "color": "^4.2.3",
"formik": "^2.4.6", "formik": "^2.4.6",
"jimp": "^0.22.12", "jimp": "^0.22.12",
@@ -45,11 +46,13 @@
"react-dom": "^18.3.1", "react-dom": "^18.3.1",
"react-helmet": "^6.1.0", "react-helmet": "^6.1.0",
"react-router-dom": "^6.23.1", "react-router-dom": "^6.23.1",
"type-fest": "^4.35.0",
"yup": "^1.4.0" "yup": "^1.4.0"
}, },
"devDependencies": { "devDependencies": {
"@commitlint/cli": "^19.3.0", "@commitlint/cli": "^19.3.0",
"@commitlint/config-conventional": "^19.2.2", "@commitlint/config-conventional": "^19.2.2",
"@iconify/react": "^5.2.0",
"@testing-library/jest-dom": "^6.4.5", "@testing-library/jest-dom": "^6.4.5",
"@testing-library/react": "^14.3.1", "@testing-library/react": "^14.3.1",
"@types/color": "^3.0.6", "@types/color": "^3.0.6",

View File

@@ -0,0 +1 @@
<svg width="1389" height="1479" fill="none" xmlns="http://www.w3.org/2000/svg"><g opacity=".3"><mask id="a" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="0" y="0" width="1389" height="1479"><path fill="#D9D9D9" d="M0 0h1389v1479H0z"/></mask><g mask="url(#a)"><ellipse opacity=".5" cy="1007.5" rx="160" ry="160.5" fill="url(#b)"/><circle opacity=".5" cx="857.242" cy="375.085" r="91.111" fill="url(#c)"/><rect opacity=".5" x="-.664" y="273.555" width="386.866" height="386.866" rx="24" transform="rotate(-45 -.664 273.555)" fill="url(#d)"/><rect opacity=".5" x="288.662" y="1179.43" width="718.993" height="424.487" rx="32" transform="rotate(-45 288.662 1179.43)" fill="url(#e)"/><circle opacity=".5" cx="1389.13" cy="530.129" r="220.13" fill="url(#f)"/><circle opacity=".5" cx="1205.72" cy="1387.95" r="91.111" fill="url(#g)"/></g></g><defs><linearGradient id="b" x1="-61.873" y1="861.062" x2=".372" y2="1167.92" gradientUnits="userSpaceOnUse"><stop stop-color="#86B3FE"/><stop offset=".993" stop-color="#F5F5FA"/></linearGradient><linearGradient id="c" x1="766.131" y1="250.722" x2="857.242" y2="466.196" gradientUnits="userSpaceOnUse"><stop stop-color="#86B3FE"/><stop offset="1" stop-color="#F5F5FA"/></linearGradient><linearGradient id="d" x1="117.967" y1="290.503" x2="192.769" y2="660.421" gradientUnits="userSpaceOnUse"><stop stop-color="#86B3FE"/><stop offset=".993" stop-color="#F5F5FA"/></linearGradient><linearGradient id="e" x1="616.714" y1="1247.46" x2="920.97" y2="1639.19" gradientUnits="userSpaceOnUse"><stop offset=".091" stop-color="#F5F5FA"/><stop offset=".948" stop-color="#86B3FE"/></linearGradient><linearGradient id="f" x1="1456.96" y1="604.614" x2="1389.13" y2="750.259" gradientUnits="userSpaceOnUse"><stop stop-color="#86B3FE"/><stop offset=".993" stop-color="#F5F5FA"/></linearGradient><linearGradient id="g" x1="1242.3" y1="1427.85" x2="1140.55" y2="1333.41" gradientUnits="userSpaceOnUse"><stop stop-color="#86B3FE"/><stop offset="1" stop-color="#F5F5FA"/></linearGradient></defs></svg>

After

Width:  |  Height:  |  Size: 2.0 KiB

View File

@@ -0,0 +1,17 @@
@font-face {
font-family: "Plus Jakarta Sans";
font-weight: 100 900;
font-display: swap;
font-style: normal;
font-named-instance: "Regular";
src: url("PlusJakartaSans-VariableFont_wght.ttf");
}
@font-face {
font-family: "Plus Jakarta Sans";
font-weight: 100 900;
font-display: swap;
font-style: italic;
font-named-instance: "Italic";
src: url("PlusJakartaSans-Italic-VariableFont_wght.ttf");
}

View File

@@ -1,59 +1,75 @@
import { readFile, writeFile } from 'fs/promises' import { readFile, writeFile } from 'fs/promises';
import fs from 'fs' import fs from 'fs';
import { dirname, join, sep } from 'path' import { dirname, join, sep } from 'path';
import { fileURLToPath } from 'url' import { fileURLToPath } from 'url';
const currentDirname = dirname(fileURLToPath(import.meta.url)) const currentDirname = dirname(fileURLToPath(import.meta.url));
const toolName = process.argv[2] const toolName = process.argv[2];
const folder = process.argv[3] const folder = process.argv[3];
const toolsDir = join(currentDirname, '..', 'src', 'pages', folder ?? '') const toolsDir = join(
currentDirname,
'..',
'src',
'pages',
'tools',
folder ?? ''
);
if (!toolName) { if (!toolName) {
throw new Error('Please specify a toolname.') throw new Error('Please specify a toolname.');
} }
function capitalizeFirstLetter(string) { function capitalizeFirstLetter(string) {
return string.charAt(0).toUpperCase() + string.slice(1) return string.charAt(0).toUpperCase() + string.slice(1);
} }
function createFolderStructure(basePath, foldersToCreateIndexCount) { function createFolderStructure(basePath, foldersToCreateIndexCount) {
const folderArray = basePath.split(sep) const folderArray = basePath.split(sep);
function recursiveCreate(currentBase, index) { function recursiveCreate(currentBase, index) {
if (index >= folderArray.length) { if (index >= folderArray.length) {
return return;
} }
const currentPath = join(currentBase, folderArray[index]) const currentPath = join(currentBase, folderArray[index]);
if (!fs.existsSync(currentPath)) { if (!fs.existsSync(currentPath)) {
fs.mkdirSync(currentPath, { recursive: true }) fs.mkdirSync(currentPath, { recursive: true });
} }
const indexPath = join(currentPath, 'index.ts') const indexPath = join(currentPath, 'index.ts');
if (!fs.existsSync(indexPath) && index < folderArray.length - 1 && index >= folderArray.length - 1 - foldersToCreateIndexCount) { if (
fs.writeFileSync(indexPath, `export const ${currentPath.split(sep)[currentPath.split(sep).length - 1]}Tools = [];\n`) !fs.existsSync(indexPath) &&
console.log(`File created: ${indexPath}`) index < folderArray.length - 1 &&
index >= folderArray.length - 1 - foldersToCreateIndexCount
) {
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 // Recursively create the next folder
recursiveCreate(currentPath, index + 1) recursiveCreate(currentPath, index + 1);
} }
// Start the recursive folder creation // Start the recursive folder creation
recursiveCreate('.', 0) recursiveCreate('.', 0);
} }
const toolNameCamelCase = toolName.replace(/-./g, (x) => x[1].toUpperCase()) const toolNameCamelCase = toolName.replace(/-./g, (x) => x[1].toUpperCase());
const toolNameTitleCase = const toolNameTitleCase =
toolName[0].toUpperCase() + toolName.slice(1).replace(/-/g, ' ') toolName[0].toUpperCase() + toolName.slice(1).replace(/-/g, ' ');
const toolDir = join(toolsDir, toolName) const toolDir = join(toolsDir, toolName);
const type = folder.split(sep)[folder.split(sep).length - 1] const type = folder.split(sep)[folder.split(sep).length - 1];
await createFolderStructure(toolDir, folder.split(sep).length) await createFolderStructure(toolDir, folder.split(sep).length);
console.log(`Directory created: ${toolDir}`) console.log(`Directory created: ${toolDir}`);
const createToolFile = async (name, content) => { const createToolFile = async (name, content) => {
const filePath = join(toolDir, name) const filePath = join(toolDir, name);
await writeFile(filePath, content.trim()) await writeFile(filePath, content.trim());
console.log(`File created: ${filePath}`) console.log(`File created: ${filePath}`);
} };
createToolFile( createToolFile(
`index.tsx`, `index.tsx`,
@@ -62,7 +78,8 @@ import { Box } from '@mui/material';
import React from 'react'; import React from 'react';
import * as Yup from 'yup'; import * as Yup from 'yup';
const initialValues = {}; type InitialValuesType = {};
const initialValues: InitialValuesType = {};
const validationSchema = Yup.object({ const validationSchema = Yup.object({
// splitSeparator: Yup.string().required('The separator is required') // splitSeparator: Yup.string().required('The separator is required')
}); });
@@ -70,27 +87,26 @@ export default function ${capitalizeFirstLetter(toolNameCamelCase)}() {
return <Box>Lorem ipsum</Box>; return <Box>Lorem ipsum</Box>;
} }
` `
) );
createToolFile( createToolFile(
`meta.ts`, `meta.ts`,
` `
import { defineTool } from '@tools/defineTool'; import { defineTool } from '@tools/defineTool';
import { lazy } from 'react'; import { lazy } from 'react';
// import image from '@assets/text.png';
export const tool = defineTool('${type}', { export const tool = defineTool('${type}', {
name: '${toolNameTitleCase}', name: '${toolNameTitleCase}',
path: '${toolName}', path: '${toolName}',
// image, icon: '',
description: '', description: '',
shortDescription: '', shortDescription: '',
keywords: ['${toolName.split('-').join('\', \'')}'], keywords: ['${toolName.split('-').join("', '")}'],
component: lazy(() => import('./index')) component: lazy(() => import('./index'))
}); });
` `
) );
createToolFile(`service.ts`, ``) createToolFile(`service.ts`, ``);
createToolFile( createToolFile(
`${toolName}.service.test.ts`, `${toolName}.service.test.ts`,
` `
@@ -101,7 +117,7 @@ import { expect, describe, it } from 'vitest';
// //
// }) // })
` `
) );
// createToolFile( // createToolFile(
// `${toolName}.e2e.spec.ts`, // `${toolName}.e2e.spec.ts`,
@@ -125,15 +141,17 @@ import { expect, describe, it } from 'vitest';
// ` // `
// ) // )
const toolsIndex = join(toolsDir, 'index.ts') const toolsIndex = join(toolsDir, 'index.ts');
const indexContent = await readFile(toolsIndex, { encoding: 'utf-8' }).then( const indexContent = await readFile(toolsIndex, { encoding: 'utf-8' }).then(
(r) => r.split('\n') (r) => r.split('\n')
) );
indexContent.splice( indexContent.splice(
0, 0,
0, 0,
`import { tool as ${type}${capitalizeFirstLetter(toolNameCamelCase)} } from './${toolName}/meta';` `import { tool as ${type}${capitalizeFirstLetter(
) toolNameCamelCase
writeFile(toolsIndex, indexContent.join('\n')) )} } from './${toolName}/meta';`
console.log(`Added import in: ${toolsIndex}`) );
writeFile(toolsIndex, indexContent.join('\n'));
console.log(`Added import in: ${toolsIndex}`);

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

BIN
src/assets/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

View File

@@ -7,20 +7,21 @@ import { DefinedTool } from '@tools/defineTool';
import { filterTools, tools } from '@tools/index'; import { filterTools, tools } from '@tools/index';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import _ from 'lodash'; import _ from 'lodash';
import { Icon } from '@iconify/react';
const exampleTools: { label: string; url: string }[] = [ const exampleTools: { label: string; url: string }[] = [
{ {
label: 'Create a transparent image', label: 'Create a transparent image',
url: '/png/create-transparent' url: '/png/create-transparent'
}, },
{ label: 'Convert text to morse code', url: '/string/to-morse' }, { label: 'Prettify JSON', url: '/json/prettify' },
{ label: 'Change GIF speed', url: '/gif/change-speed' }, { label: 'Change GIF speed', url: '/gif/change-speed' },
{ label: 'Pick a random item', url: '' }, { label: 'Sort a list', url: '/list/sort' },
{ label: 'Find and replace text', url: '' }, { label: 'Compress PNG', url: '/png/compress-png' },
{ label: 'Convert emoji to image', url: '' }, { label: 'Split a text', url: '/string/split' },
{ label: 'Split a string', url: '/string/split' },
{ label: 'Calculate number sum', url: '/number/sum' }, { label: 'Calculate number sum', url: '/number/sum' },
{ label: 'Pixelate an image', url: '' } { label: 'Shuffle a list', url: '/list/shuffle' },
{ label: 'Change colors in image', url: '/png/change-colors-in-png' }
]; ];
export default function Hero() { export default function Hero() {
const [inputValue, setInputValue] = useState<string>(''); const [inputValue, setInputValue] = useState<string>('');
@@ -35,11 +36,12 @@ export default function Hero() {
setInputValue(newInputValue); setInputValue(newInputValue);
setFilteredTools(_.shuffle(filterTools(tools, newInputValue))); setFilteredTools(_.shuffle(filterTools(tools, newInputValue)));
}; };
return ( return (
<Box width={{ xs: '90%', md: '80%', lg: '60%' }}> <Box width={{ xs: '90%', md: '80%', lg: '60%' }}>
<Stack mb={1} direction={'row'} spacing={1} justifyContent={'center'}> <Stack mb={1} direction={'row'} spacing={1} justifyContent={'center'}>
<Typography sx={{ textAlign: 'center' }} fontSize={{ xs: 25, md: 30 }}> <Typography sx={{ textAlign: 'center' }} fontSize={{ xs: 25, md: 30 }}>
Transform Your Workflow with{' '} Get Things Done Quickly with{' '}
<Typography <Typography
fontSize={{ xs: 25, md: 30 }} fontSize={{ xs: 25, md: 30 }}
display={'inline'} display={'inline'}
@@ -71,10 +73,13 @@ export default function Hero() {
{...params} {...params}
fullWidth fullWidth
placeholder={'Search all tools'} placeholder={'Search all tools'}
sx={{ borderRadius: 2 }}
InputProps={{ InputProps={{
...params.InputProps, ...params.InputProps,
endAdornment: <SearchIcon /> endAdornment: <SearchIcon />,
sx: {
borderRadius: 4,
backgroundColor: 'white'
}
}} }}
onChange={(event) => handleInputChange(event, event.target.value)} onChange={(event) => handleInputChange(event, event.target.value)}
/> />
@@ -85,17 +90,27 @@ export default function Hero() {
{...props} {...props}
onClick={() => navigate('/' + option.path)} onClick={() => navigate('/' + option.path)}
> >
<Box> <Stack direction={'row'} spacing={2} alignItems={'center'}>
<Typography fontWeight={'bold'}>{option.name}</Typography> <Icon fontSize={20} icon={option.icon} />
<Typography fontSize={12}>{option.shortDescription}</Typography> <Box>
</Box> <Typography fontWeight={'bold'}>{option.name}</Typography>
<Typography fontSize={12}>{option.shortDescription}</Typography>
</Box>
</Stack>
</Box> </Box>
)} )}
onChange={(event, newValue) => {
if (newValue) {
navigate('/' + newValue.path);
}
}}
/> />
<Grid container spacing={2} mt={2}> <Grid container spacing={2} mt={2}>
{exampleTools.map((tool) => ( {exampleTools.map((tool) => (
<Grid <Grid
onClick={() => navigate(tool.url)} onClick={() =>
navigate(tool.url.startsWith('/') ? tool.url : `/${tool.url}`)
}
item item
xs={12} xs={12}
md={6} md={6}
@@ -112,7 +127,9 @@ export default function Hero() {
borderRadius: 3, borderRadius: 3,
borderColor: 'grey', borderColor: 'grey',
borderStyle: 'solid', borderStyle: 'solid',
cursor: 'pointer' backgroundColor: 'white',
cursor: 'pointer',
'&:hover': { backgroundColor: '#FAFAFD' }
}} }}
> >
<Typography>{tool.label}</Typography> <Typography>{tool.label}</Typography>

View File

@@ -1,73 +1,114 @@
import React, { useState } from 'react'; import React, { ReactNode, useState } from 'react';
import AppBar from '@mui/material/AppBar'; import AppBar from '@mui/material/AppBar';
import Toolbar from '@mui/material/Toolbar'; import Toolbar from '@mui/material/Toolbar';
import Typography from '@mui/material/Typography';
import Button from '@mui/material/Button'; import Button from '@mui/material/Button';
import IconButton from '@mui/material/IconButton'; import IconButton from '@mui/material/IconButton';
import MenuIcon from '@mui/icons-material/Menu'; import MenuIcon from '@mui/icons-material/Menu';
import { Link, useNavigate } from 'react-router-dom'; import { Link, useNavigate } from 'react-router-dom';
import githubIcon from '@assets/github-mark.png'; // Adjust the path to your GitHub icon import logo from 'assets/logo.png';
import { import {
Drawer, Drawer,
List, List,
ListItem,
ListItemButton, ListItemButton,
ListItemText, ListItemText,
Stack Stack
} from '@mui/material'; } from '@mui/material';
import useMediaQuery from '@mui/material/useMediaQuery'; import useMediaQuery from '@mui/material/useMediaQuery';
import { useTheme } from '@mui/material/styles'; import { useTheme } from '@mui/material/styles';
import { Icon } from '@iconify/react';
const Navbar: React.FC = () => { const Navbar: React.FC = () => {
const navigate = useNavigate(); const navigate = useNavigate();
const theme = useTheme(); const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down('md')); const isMobile = useMediaQuery(theme.breakpoints.down('md'));
const [drawerOpen, setDrawerOpen] = useState(false); const [drawerOpen, setDrawerOpen] = useState(false);
const toggleDrawer = (open: boolean) => () => { const toggleDrawer = (open: boolean) => () => {
setDrawerOpen(open); setDrawerOpen(open);
}; };
const navItems: { label: string; path: string }[] = [
// { label: 'Features', path: '/features' }
// { label: 'About Us', path: '/about-us' }
];
const buttons: ReactNode[] = [
<Icon
onClick={() => window.open('https://discord.gg/SDbbn3hT4b', '_blank')}
style={{ cursor: 'pointer' }}
fontSize={30}
icon={'ic:baseline-discord'}
/>,
<iframe
src="https://ghbtns.com/github-btn.html?user=iib0011&repo=omni-tools&type=star&count=true&size=large"
frameBorder="0"
scrolling="0"
width="130"
height="30"
title="GitHub"
></iframe>,
<Button
onClick={() => {
window.open('https://buymeacoffee.com/iib0011', '_blank');
}}
sx={{ borderRadius: '100px' }}
variant={'contained'}
startIcon={
<Icon
style={{ cursor: 'pointer' }}
fontSize={25}
icon={'mdi:heart-outline'}
/>
}
>
Buy me a coffee
</Button>
];
const drawerList = ( const drawerList = (
<List> <List>
<ListItemButton onClick={() => navigate('/features')}> {navItems.map((navItem) => (
<ListItemText primary="Features" /> <ListItemButton
</ListItemButton> key={navItem.path}
<ListItemButton onClick={() => navigate('/about-us')}> onClick={() => navigate(navItem.path)}
<ListItemText primary="About Us" /> >
</ListItemButton> <ListItemText primary={navItem.label} />
<ListItemButton </ListItemButton>
component="a" ))}
href="https://github.com/iib0011/omni-tools" {buttons.map((button) => (
target="_blank" <ListItem>{button}</ListItem>
rel="noopener noreferrer" ))}
>
<img
src={githubIcon}
alt="GitHub"
style={{ height: '24px', marginRight: '8px' }}
/>
<Typography variant="button">Star us</Typography>
</ListItemButton>
</List> </List>
); );
return ( return (
<AppBar <AppBar
position="static" position="static"
style={{ backgroundColor: 'white', color: 'black' }} style={{
backgroundColor: '#F5F5FA',
color: 'black'
}}
> >
<Toolbar sx={{ justifyContent: 'space-between', alignItems: 'center' }}> <Toolbar
<Typography sx={{
justifyContent: 'space-between',
alignItems: 'center'
}}
>
<img
onClick={() => navigate('/')} onClick={() => navigate('/')}
fontSize={20} style={{ cursor: 'pointer' }}
sx={{ cursor: 'pointer' }} src={logo}
color={'primary'} width={isMobile ? '80px' : '150px'}
> />
OmniTools
</Typography>
{isMobile ? ( {isMobile ? (
<> <>
<IconButton color="inherit" onClick={toggleDrawer(true)}> <IconButton
color="inherit"
onClick={toggleDrawer(true)}
sx={{
'&:hover': {
backgroundColor: theme.palette.primary.main
}
}}
>
<MenuIcon /> <MenuIcon />
</IconButton> </IconButton>
<Drawer <Drawer
@@ -79,36 +120,28 @@ const Navbar: React.FC = () => {
</Drawer> </Drawer>
</> </>
) : ( ) : (
<Stack direction={'row'}> <Stack direction={'row'} spacing={3} alignItems={'center'}>
<Button color="inherit"> {navItems.map((item) => (
<Link <Button
to="/features" key={item.label}
style={{ textDecoration: 'none', color: 'inherit' }} color="inherit"
sx={{
'&:hover': {
color: theme.palette.primary.main,
transition: 'color 0.3s ease',
backgroundColor: 'white'
}
}}
> >
Features <Link
</Link> to={item.path}
</Button> style={{ textDecoration: 'none', color: 'inherit' }}
<Button color="inherit"> >
<Link {item.label}
to="/about-us" </Link>
style={{ textDecoration: 'none', color: 'inherit' }} </Button>
> ))}
About Us {buttons}
</Link>
</Button>
<IconButton
color="primary"
href="https://github.com/iib0011/omni-tools"
target="_blank"
rel="noopener noreferrer"
>
<img
src={githubIcon}
alt="GitHub"
style={{ height: '24px', marginRight: '8px' }}
/>
<Typography variant="button">Star us</Typography>
</IconButton>
</Stack> </Stack>
)} )}
</Toolbar> </Toolbar>

View File

@@ -0,0 +1,100 @@
import React, { useRef, useState, ReactNode } from 'react';
import { Box } from '@mui/material';
import { FormikProps, FormikValues } from 'formik';
import ToolOptions, { GetGroupsType } from '@components/options/ToolOptions';
import ToolInputAndResult from '@components/ToolInputAndResult';
import ToolInfo from '@components/ToolInfo';
import Separator from '@components/Separator';
import ToolExamples, {
CardExampleType
} from '@components/examples/ToolExamples';
import { ToolComponentProps } from '@tools/defineTool';
interface ToolContentPropsBase<T, I> extends ToolComponentProps {
// Input/Output components
inputComponent: ReactNode;
resultComponent: ReactNode;
// Tool options
initialValues: T;
getGroups: GetGroupsType<T>;
// Computation function
compute: (optionsValues: T, input: I) => void;
// Tool info (optional)
toolInfo?: {
title: string;
description: string;
};
// Input value to pass to the compute function
input: I;
// Validation schema (optional)
validationSchema?: any;
}
interface ToolContentPropsWithExamples<T, I>
extends ToolContentPropsBase<T, I> {
exampleCards: CardExampleType<T>[];
setInput: React.Dispatch<React.SetStateAction<I>>;
}
interface ToolContentPropsWithoutExamples<T, I>
extends ToolContentPropsBase<T, I> {
exampleCards?: never;
setInput?: never;
}
type ToolContentProps<T, I> =
| ToolContentPropsWithExamples<T, I>
| ToolContentPropsWithoutExamples<T, I>;
export default function ToolContent<T extends FormikValues, I>({
title,
inputComponent,
resultComponent,
initialValues,
getGroups,
compute,
toolInfo,
exampleCards,
input,
setInput,
validationSchema
}: ToolContentProps<T, I>) {
const formRef = useRef<FormikProps<T>>(null);
return (
<Box>
<ToolInputAndResult input={inputComponent} result={resultComponent} />
<ToolOptions
formRef={formRef}
compute={compute}
getGroups={getGroups}
initialValues={initialValues}
input={input}
validationSchema={validationSchema}
/>
{toolInfo && (
<ToolInfo title={toolInfo.title} description={toolInfo.description} />
)}
{exampleCards && exampleCards.length > 0 && (
<>
<Separator backgroundColor="#5581b5" margin="50px" />
<ToolExamples
title={title}
exampleCards={exampleCards}
getGroups={getGroups}
formRef={formRef}
setInput={setInput}
/>
</>
)}
</Box>
);
}

View File

@@ -1,40 +1,57 @@
import { Box, Button } from '@mui/material'; import { Box, Button, styled, useTheme } from '@mui/material';
import Typography from '@mui/material/Typography'; import Typography from '@mui/material/Typography';
import ToolBreadcrumb from './ToolBreadcrumb'; import ToolBreadcrumb from './ToolBreadcrumb';
import { capitalizeFirstLetter } from '../utils/string'; import { capitalizeFirstLetter } from '../utils/string';
import Grid from '@mui/material/Grid'; import Grid from '@mui/material/Grid';
import { Icon, IconifyIcon } from '@iconify/react';
import { categoriesColors } from '../config/uiConfig';
const StyledButton = styled(Button)(({ theme }) => ({
backgroundColor: 'white',
'&:hover': {
backgroundColor: theme.palette.primary.main,
color: 'white'
}
}));
interface ToolHeaderProps { interface ToolHeaderProps {
title: string; title: string;
description: string; description: string;
image?: string; icon?: IconifyIcon | string;
type: string; type: string;
} }
function ToolLinks() { function ToolLinks() {
const theme = useTheme();
return ( return (
<Grid container spacing={2} mt={1}> <Grid container spacing={2} mt={1}>
<Grid item md={12} lg={4}> <Grid item md={12} lg={6}>
<Button fullWidth variant="outlined" href="#tool"> <StyledButton
sx={{ backgroundColor: 'white' }}
fullWidth
variant="outlined"
href="#tool"
>
Use This Tool Use This Tool
</Button> </StyledButton>
</Grid> </Grid>
<Grid item md={12} lg={4}> <Grid item md={12} lg={6}>
<Button fullWidth variant="outlined" href="#examples"> <StyledButton fullWidth variant="outlined" href="#examples">
See Examples See Examples
</Button> </StyledButton>
</Grid>
<Grid item md={12} lg={4}>
<Button fullWidth variant="outlined" href="#tour">
Learn How to Use
</Button>
</Grid> </Grid>
{/*<Grid item md={12} lg={4}>*/}
{/* <StyledButton fullWidth variant="outlined" href="#tour">*/}
{/* Learn How to Use*/}
{/* </StyledButton>*/}
{/*</Grid>*/}
</Grid> </Grid>
); );
} }
export default function ToolHeader({ export default function ToolHeader({
image, icon,
title, title,
description, description,
type type
@@ -60,10 +77,18 @@ export default function ToolHeader({
<ToolLinks /> <ToolLinks />
</Grid> </Grid>
{image && ( {icon && (
<Grid item xs={12} md={4}> <Grid item xs={12} md={4}>
<Box sx={{ display: 'flex', justifyContent: 'center' }}> <Box sx={{ display: 'flex', justifyContent: 'center' }}>
<img width={'250'} src={image} /> <Icon
icon={icon}
fontSize={'250'}
color={
categoriesColors[
Math.floor(Math.random() * categoriesColors.length)
]
}
/>
</Box> </Box>
</Grid> </Grid>
)} )}

View File

@@ -6,17 +6,18 @@ import Separator from './Separator';
import AllTools from './allTools/AllTools'; import AllTools from './allTools/AllTools';
import { getToolsByCategory } from '@tools/index'; import { getToolsByCategory } from '@tools/index';
import { capitalizeFirstLetter } from '../utils/string'; import { capitalizeFirstLetter } from '../utils/string';
import { IconifyIcon } from '@iconify/react';
export default function ToolLayout({ export default function ToolLayout({
children, children,
title, title,
description, description,
image, icon,
type type
}: { }: {
title: string; title: string;
description: string; description: string;
image?: string; icon?: IconifyIcon | string;
type: string; type: string;
children: ReactNode; children: ReactNode;
}) { }) {
@@ -27,7 +28,8 @@ export default function ToolLayout({
.map((tool) => ({ .map((tool) => ({
title: tool.name, title: tool.name,
description: tool.shortDescription, description: tool.shortDescription,
link: '/' + tool.path link: '/' + tool.path,
icon: tool.icon
})) ?? []; })) ?? [];
return ( return (
@@ -36,6 +38,7 @@ export default function ToolLayout({
display={'flex'} display={'flex'}
flexDirection={'column'} flexDirection={'column'}
alignItems={'center'} alignItems={'center'}
sx={{ backgroundColor: '#F5F5FA' }}
> >
<Helmet> <Helmet>
<title>{`${title} - Omni Tools`}</title> <title>{`${title} - Omni Tools`}</title>
@@ -44,7 +47,7 @@ export default function ToolLayout({
<ToolHeader <ToolHeader
title={title} title={title}
description={description} description={description}
image={image} icon={icon}
type={type} type={type}
/> />
{children} {children}

View File

@@ -1,10 +1,12 @@
import { Box, Grid, Stack, Typography } from '@mui/material'; import { Box, Grid, Stack, Typography } from '@mui/material';
import ToolCard from './ToolCard'; import ToolCard from './ToolCard';
import { IconifyIcon } from '@iconify/react';
export interface ToolCardProps { export interface ToolCardProps {
title: string; title: string;
description: string; description: string;
link: string; link: string;
icon: IconifyIcon | string;
} }
interface AllToolsProps { interface AllToolsProps {
@@ -26,6 +28,7 @@ export default function AllTools({ title, toolCards }: AllToolsProps) {
title={card.title} title={card.title}
description={card.description} description={card.description}
link={card.link} link={card.link}
icon={card.icon}
/> />
</Grid> </Grid>
))} ))}

View File

@@ -1,9 +1,15 @@
import { Box, Card, CardContent, Link, Typography } from '@mui/material'; import { Box, Card, CardContent, Link, Stack, Typography } from '@mui/material';
import { ToolCardProps } from './AllTools'; import { ToolCardProps } from './AllTools';
import ChevronRightIcon from '@mui/icons-material/ChevronRight'; import ChevronRightIcon from '@mui/icons-material/ChevronRight';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { Icon } from '@iconify/react';
export default function ToolCard({ title, description, link }: ToolCardProps) { export default function ToolCard({
title,
description,
link,
icon
}: ToolCardProps) {
const navigate = useNavigate(); const navigate = useNavigate();
return ( return (
<Card <Card
@@ -28,9 +34,12 @@ export default function ToolCard({ title, description, link }: ToolCardProps) {
borderColor: '#ffffff70' borderColor: '#ffffff70'
}} }}
> >
<Typography variant="h5" component="h2"> <Stack direction={'row'} spacing={2} alignItems={'center'}>
{title} <Icon icon={icon} fontSize={25} />
</Typography> <Typography variant="h5" component="h2">
{title}
</Typography>
</Stack>
<Link href={link} underline="none" sx={{ color: '#fff' }}> <Link href={link} underline="none" sx={{ color: '#fff' }}>
<ChevronRightIcon /> <ChevronRightIcon />
</Link> </Link>

View File

@@ -1,4 +1,3 @@
import { ExampleCardProps } from './Examples';
import { import {
Box, Box,
Card, Card,
@@ -9,26 +8,42 @@ import {
useTheme useTheme
} from '@mui/material'; } from '@mui/material';
import ArrowDownwardIcon from '@mui/icons-material/ArrowDownward'; import ArrowDownwardIcon from '@mui/icons-material/ArrowDownward';
import RequiredOptions from './RequiredOptions'; import ExampleOptions from './ExampleOptions';
import { GetGroupsType } from '@components/options/ToolOptions';
export default function ExampleCard({ export interface ExampleCardProps<T> {
title: string;
description: string;
sampleText: string;
sampleResult: string;
sampleOptions: T;
changeInputResult: (newInput: string, newOptions: T) => void;
getGroups: GetGroupsType<T>;
}
export default function ExampleCard<T>({
title, title,
description, description,
sampleText, sampleText,
sampleResult, sampleResult,
requiredOptions, sampleOptions,
changeInputResult changeInputResult,
}: ExampleCardProps) { getGroups
}: ExampleCardProps<T>) {
const theme = useTheme(); const theme = useTheme();
return ( return (
<Card <Card
raised raised
onClick={() => {
changeInputResult(sampleText, sampleOptions);
}}
sx={{ sx={{
bgcolor: theme.palette.background.default, bgcolor: theme.palette.background.default,
height: '100%', height: '100%',
overflow: 'hidden', overflow: 'hidden',
borderRadius: 2, borderRadius: 2,
transition: 'background-color 0.3s ease', transition: 'background-color 0.3s ease',
cursor: 'pointer',
'&:hover': { '&:hover': {
boxShadow: '12px 9px 11px 2px #b8b9be, -6px -6px 12px #fff' boxShadow: '12px 9px 11px 2px #b8b9be, -6px -6px 12px #fff'
} }
@@ -46,7 +61,6 @@ export default function ExampleCard({
</Typography> </Typography>
<Box <Box
onClick={() => changeInputResult(sampleText, sampleResult)}
sx={{ sx={{
display: 'flex', display: 'flex',
zIndex: '2', zIndex: '2',
@@ -55,7 +69,6 @@ export default function ExampleCard({
bgcolor: 'transparent', bgcolor: 'transparent',
padding: '5px 10px', padding: '5px 10px',
borderRadius: '5px', borderRadius: '5px',
cursor: 'pointer',
boxShadow: 'inset 2px 2px 5px #b8b9be, inset -3px -3px 7px #fff;' boxShadow: 'inset 2px 2px 5px #b8b9be, inset -3px -3px 7px #fff;'
}} }}
> >
@@ -77,7 +90,6 @@ export default function ExampleCard({
<ArrowDownwardIcon /> <ArrowDownwardIcon />
<Box <Box
onClick={() => changeInputResult(sampleText, sampleResult)}
sx={{ sx={{
display: 'flex', display: 'flex',
zIndex: '2', zIndex: '2',
@@ -106,7 +118,7 @@ export default function ExampleCard({
/> />
</Box> </Box>
<RequiredOptions options={requiredOptions} /> <ExampleOptions options={sampleOptions} getGroups={getGroups} />
</Stack> </Stack>
</CardContent> </CardContent>
</Card> </Card>

View File

@@ -0,0 +1,19 @@
import ToolOptionGroups from '@components/options/ToolOptionGroups';
import { GetGroupsType } from '@components/options/ToolOptions';
import React from 'react';
export default function ExampleOptions<T>({
options,
getGroups
}: {
options: T;
getGroups: GetGroupsType<T>;
}) {
return (
<ToolOptionGroups
// @ts-ignore
groups={getGroups({ values: options })}
vertical
/>
);
}

View File

@@ -1,59 +0,0 @@
import { Box, Grid, Stack, Typography } from '@mui/material';
import ExampleCard from './ExampleCard';
export interface ExampleCardProps {
title: string;
description: string;
sampleText: string;
sampleResult: string;
requiredOptions: RequiredOptionsProps;
changeInputResult: (input: string, result: string) => void;
}
export interface RequiredOptionsProps {
joinCharacter: string;
deleteBlankLines: boolean;
deleteTrailingSpaces: boolean;
}
interface ExampleProps {
title: string;
subtitle: string;
exampleCards: ExampleCardProps[];
}
export default function Examples({
title,
subtitle,
exampleCards
}: ExampleProps) {
return (
<Box id={'examples'} mt={4}>
<Box mt={4} display="flex" gap={1} alignItems="center">
<Typography mb={2} fontSize={30} color={'primary'}>
{title}
</Typography>
<Typography mb={2} fontSize={30} color={'secondary'}>
{subtitle}
</Typography>
</Box>
<Stack direction={'row'} alignItems={'center'} spacing={2}>
<Grid container spacing={2}>
{exampleCards.map((card, index) => (
<Grid item xs={12} md={6} lg={4} key={index}>
<ExampleCard
title={card.title}
description={card.description}
sampleText={card.sampleText}
sampleResult={card.sampleResult}
requiredOptions={card.requiredOptions}
changeInputResult={card.changeInputResult}
/>
</Grid>
))}
</Grid>
</Stack>
</Box>
);
}

View File

@@ -1,78 +0,0 @@
import { Box, Stack, TextField, Typography } from '@mui/material';
import { RequiredOptionsProps } from './Examples';
import CheckboxWithDesc from 'components/options/CheckboxWithDesc';
export default function RequiredOptions({
options
}: {
options: RequiredOptionsProps;
}) {
const { joinCharacter, deleteBlankLines, deleteTrailingSpaces } = options;
const handleBoxClick = () => {
const toolsElement = document.getElementById('tool');
if (toolsElement) {
toolsElement.scrollIntoView({ behavior: 'smooth' });
}
};
return (
<Stack direction={'column'} alignItems={'left'} spacing={2}>
<Typography variant="h5" component="h3" sx={{ marginTop: '5px' }}>
Required options
</Typography>
<Typography variant="body2" component="p">
These options will be used automatically if you select this example.
</Typography>
<Box
onClick={handleBoxClick}
sx={{
zIndex: '2',
cursor: 'pointer',
bgcolor: 'transparent',
width: '100%',
height: '100%',
display: 'flex'
}}
>
<TextField
disabled
value={joinCharacter}
fullWidth
rows={1}
sx={{
'& .MuiOutlinedInput-root': {
zIndex: '-1'
}
}}
/>
</Box>
{deleteBlankLines ? (
<Box onClick={handleBoxClick}>
<CheckboxWithDesc
title="Delete Blank Lines"
checked={deleteBlankLines}
onChange={() => {}}
description="Delete lines that don't have text symbols."
/>
</Box>
) : (
''
)}
{deleteTrailingSpaces ? (
<Box onClick={handleBoxClick}>
<CheckboxWithDesc
title="Delete Training Spaces"
checked={deleteTrailingSpaces}
onChange={() => {}}
description="Remove spaces and tabs at the end of the lines."
/>
</Box>
) : (
''
)}
</Stack>
);
}

View File

@@ -0,0 +1,68 @@
import { Box, Grid, Stack, Typography } from '@mui/material';
import ExampleCard, { ExampleCardProps } from './ExampleCard';
import React from 'react';
import { GetGroupsType } from '@components/options/ToolOptions';
import { FormikProps } from 'formik';
export type CardExampleType<T> = Omit<
ExampleCardProps<T>,
'getGroups' | 'changeInputResult'
>;
export interface ExampleProps<T> {
title: string;
subtitle?: string;
exampleCards: CardExampleType<T>[];
getGroups: GetGroupsType<T>;
formRef: React.RefObject<FormikProps<T>>;
setInput: React.Dispatch<React.SetStateAction<any>>;
}
export default function ToolExamples<T>({
title,
subtitle,
exampleCards,
getGroups,
formRef,
setInput
}: ExampleProps<T>) {
function changeInputResult(newInput: string, newOptions: T) {
setInput(newInput);
formRef.current?.setValues(newOptions);
const toolsElement = document.getElementById('tool');
if (toolsElement) {
toolsElement.scrollIntoView({ behavior: 'smooth' });
}
}
return (
<Box id={'examples'} mt={4}>
<Box mt={4} display="flex" gap={1} alignItems="center">
<Typography mb={2} fontSize={30} color={'primary'}>
{`${title} Examples`}
</Typography>
<Typography mb={2} fontSize={30} color={'secondary'}>
{subtitle ?? 'Click to try!'}
</Typography>
</Box>
<Stack direction={'row'} alignItems={'center'} spacing={2}>
<Grid container spacing={2}>
{exampleCards.map((card, index) => (
<Grid item xs={12} md={6} lg={4} key={index}>
<ExampleCard
title={card.title}
description={card.description}
sampleText={card.sampleText}
sampleResult={card.sampleResult}
sampleOptions={card.sampleOptions}
getGroups={getGroups}
changeInputResult={changeInputResult}
/>
</Grid>
))}
</Grid>
</Stack>
</Box>
);
}

View File

@@ -5,3 +5,7 @@ a {
a:hover { a:hover {
color: #030362; color: #030362;
} }
* {
font-family: Plus Jakarta Sans, sans-serif;
}

View File

@@ -38,6 +38,14 @@ export default function ToolFileInput({
}); });
} }
}; };
const handlePaste = (event: ClipboardEvent) => {
const clipboardItems = event.clipboardData?.items ?? [];
const item = clipboardItems[0];
if (item.type.includes('image')) {
const file = item.getAsFile();
onChange(file!);
}
};
useEffect(() => { useEffect(() => {
if (value) { if (value) {
const objectUrl = URL.createObjectURL(value); const objectUrl = URL.createObjectURL(value);
@@ -57,6 +65,15 @@ export default function ToolFileInput({
const handleImportClick = () => { const handleImportClick = () => {
fileInputRef.current?.click(); fileInputRef.current?.click();
}; };
useEffect(() => {
window.addEventListener('paste', handlePaste);
return () => {
window.removeEventListener('paste', handlePaste);
};
}, [handlePaste]);
return ( return (
<Box> <Box>
<InputHeader title={title} /> <InputHeader title={title} />
@@ -66,7 +83,8 @@ export default function ToolFileInput({
height: globalInputHeight, height: globalInputHeight,
border: preview ? 0 : 1, border: preview ? 0 : 1,
borderRadius: 2, borderRadius: 2,
boxShadow: '5' boxShadow: '5',
bgcolor: 'white'
}} }}
> >
{preview ? ( {preview ? (

View File

@@ -50,7 +50,14 @@ export default function ToolTextInput({
fullWidth fullWidth
multiline multiline
rows={10} rows={10}
inputProps={{ 'data-testid': 'text-input' }} sx={{
'&.MuiTextField-root': {
backgroundColor: 'white'
}
}}
inputProps={{
'data-testid': 'text-input'
}}
/> />
<InputFooter handleCopy={handleCopy} handleImport={handleImportClick} /> <InputFooter handleCopy={handleCopy} handleImport={handleImportClick} />
<input <input

View File

@@ -8,14 +8,16 @@ export interface ToolOptionGroup {
} }
export default function ToolOptionGroups({ export default function ToolOptionGroups({
groups groups,
vertical
}: { }: {
groups: ToolOptionGroup[]; groups: ToolOptionGroup[];
vertical?: boolean;
}) { }) {
return ( return (
<Grid container spacing={2}> <Grid container spacing={2}>
{groups.map((group) => ( {groups.map((group) => (
<Grid item xs={12} md={4} key={group.title}> <Grid item xs={12} md={vertical ? 12 : 4} key={group.title}>
<Typography mb={1} fontSize={22}> <Typography mb={1} fontSize={22}>
{group.title} {group.title}
</Typography> </Typography>

View File

@@ -5,8 +5,9 @@ import React, { ReactNode, RefObject, useContext, useEffect } from 'react';
import { Formik, FormikProps, FormikValues, useFormikContext } from 'formik'; import { Formik, FormikProps, FormikValues, useFormikContext } from 'formik';
import ToolOptionGroups, { ToolOptionGroup } from './ToolOptionGroups'; import ToolOptionGroups, { ToolOptionGroup } from './ToolOptionGroups';
import { CustomSnackBarContext } from '../../contexts/CustomSnackBarContext'; import { CustomSnackBarContext } from '../../contexts/CustomSnackBarContext';
import * as Yup from 'yup';
type UpdateField<T> = <Y extends keyof T>(field: Y, value: T[Y]) => void; export type UpdateField<T> = <Y extends keyof T>(field: Y, value: T[Y]) => void;
const FormikListenerComponent = <T,>({ const FormikListenerComponent = <T,>({
initialValues, initialValues,
@@ -67,6 +68,10 @@ const ToolBody = <T,>({
</Stack> </Stack>
); );
}; };
export type GetGroupsType<T> = (
formikProps: FormikProps<T> & { updateField: UpdateField<T> }
) => ToolOptionGroup[];
export default function ToolOptions<T extends FormikValues>({ export default function ToolOptions<T extends FormikValues>({
children, children,
initialValues, initialValues,
@@ -78,12 +83,10 @@ export default function ToolOptions<T extends FormikValues>({
}: { }: {
children?: ReactNode; children?: ReactNode;
initialValues: T; initialValues: T;
validationSchema: any | (() => any); validationSchema?: any | (() => any);
compute: (optionsValues: T, input: any) => void; compute: (optionsValues: T, input: any) => void;
input?: any; input?: any;
getGroups: ( getGroups: GetGroupsType<T>;
formikProps: FormikProps<T> & { updateField: UpdateField<T> }
) => ToolOptionGroup[];
formRef?: RefObject<FormikProps<T>>; formRef?: RefObject<FormikProps<T>>;
}) { }) {
const theme = useTheme(); const theme = useTheme();
@@ -93,7 +96,8 @@ export default function ToolOptions<T extends FormikValues>({
mb: 2, mb: 2,
borderRadius: 2, borderRadius: 2,
padding: 2, padding: 2,
backgroundColor: theme.palette.background.default backgroundColor: theme.palette.background.default,
boxShadow: '2'
}} }}
mt={2} mt={2}
> >

View File

@@ -67,7 +67,8 @@ export default function ToolFileResult({
height: globalInputHeight, height: globalInputHeight,
border: preview ? 0 : 1, border: preview ? 0 : 1,
borderRadius: 2, borderRadius: 2,
boxShadow: '5' boxShadow: '5',
bgcolor: 'white'
}} }}
> >
{preview && ( {preview && (

View File

@@ -41,6 +41,11 @@ export default function ToolTextResult({
value={replaceSpecialCharacters(value)} value={replaceSpecialCharacters(value)}
fullWidth fullWidth
multiline multiline
sx={{
'&.MuiTextField-root': {
backgroundColor: 'white'
}
}}
rows={10} rows={10}
inputProps={{ 'data-testid': 'text-result' }} inputProps={{ 'data-testid': 'text-result' }}
/> />

View File

@@ -1,2 +1,8 @@
export const globalInputHeight = 300; export const globalInputHeight = 300;
export const globalDescriptionFontSize = 12; export const globalDescriptionFontSize = 12;
export const categoriesColors: string[] = [
'#8FBC5D',
'#3CB6E2',
'#FFD400',
'#AB6993'
];

View File

@@ -0,0 +1,94 @@
import { getToolsByCategory } from '@tools/index';
import Grid from '@mui/material/Grid';
import { Box, Card, CardContent, Stack } from '@mui/material';
import { Link, useNavigate } from 'react-router-dom';
import Typography from '@mui/material/Typography';
import Button from '@mui/material/Button';
import { useState } from 'react';
import { categoriesColors } from 'config/uiConfig';
import { Icon } from '@iconify/react';
type ArrayElement<ArrayType extends readonly unknown[]> =
ArrayType extends readonly (infer ElementType)[] ? ElementType : never;
const SingleCategory = function ({
category,
index
}: {
category: ArrayElement<ReturnType<typeof getToolsByCategory>>;
index: number;
}) {
const navigate = useNavigate();
const [hovered, setHovered] = useState<boolean>(false);
const toggleHover = () => setHovered((prevState) => !prevState);
return (
<Grid
item
xs={12}
md={6}
onMouseEnter={toggleHover}
onMouseLeave={toggleHover}
>
<Card
sx={{
height: '100%',
backgroundColor: hovered ? '#FAFAFD' : 'white'
}}
>
<CardContent sx={{ height: '100%' }}>
<Stack
direction={'column'}
height={'100%'}
justifyContent={'space-between'}
>
<Box>
<Stack direction={'row'} spacing={2} alignItems={'center'}>
<Icon
icon={category.icon}
fontSize={'60px'}
style={{
transform: `scale(${hovered ? 1.1 : 1}`
}}
color={categoriesColors[index % categoriesColors.length]}
/>
<Link
style={{ fontSize: 20, fontWeight: 700, color: 'black' }}
to={'/categories/' + category.type}
>
{category.title}
</Link>
</Stack>
<Typography sx={{ mt: 2 }}>{category.description}</Typography>
</Box>
<Grid mt={1} container spacing={2}>
<Grid item xs={12} md={6}>
<Button
fullWidth
onClick={() => navigate('/categories/' + category.type)}
variant={'contained'}
>{`See all ${category.title}`}</Button>
</Grid>
<Grid item xs={12} md={6}>
<Button
sx={{ backgroundColor: 'white' }}
fullWidth
onClick={() => navigate(category.example.path)}
variant={'outlined'}
>{`Try ${category.example.title}`}</Button>
</Grid>
</Grid>
</Stack>
</CardContent>
</Card>
</Grid>
);
};
export default function Categories() {
return (
<Grid width={'80%'} container mt={2} spacing={2}>
{getToolsByCategory().map((category, index) => (
<SingleCategory key={category.type} category={category} index={index} />
))}
</Grid>
);
}

View File

@@ -1,17 +1,17 @@
import { Box, Card, CardContent } from '@mui/material'; import { Box } from '@mui/material';
import Grid from '@mui/material/Grid';
import Typography from '@mui/material/Typography';
import { Link, useNavigate } from 'react-router-dom';
import { getToolsByCategory } from '../../tools';
import Button from '@mui/material/Button';
import Hero from 'components/Hero'; import Hero from 'components/Hero';
import Categories from './Categories';
export default function Home() { export default function Home() {
const navigate = useNavigate();
return ( return (
<Box <Box
padding={{ xs: 1, md: 3, lg: 5 }} padding={{
xs: 1,
md: 3,
lg: 5,
background: `url(/assets/background.svg)`,
backgroundColor: '#F5F5FA'
}}
display={'flex'} display={'flex'}
flexDirection={'column'} flexDirection={'column'}
alignItems={'center'} alignItems={'center'}
@@ -19,39 +19,7 @@ export default function Home() {
width={'100%'} width={'100%'}
> >
<Hero /> <Hero />
<Grid width={'80%'} container mt={2} spacing={2}> <Categories />
{getToolsByCategory().map((category) => (
<Grid key={category.type} item xs={12} md={6}>
<Card sx={{ height: '100%' }}>
<CardContent>
<Link
style={{ fontSize: 20 }}
to={'/categories/' + category.type}
>
{category.title}
</Link>
<Typography sx={{ mt: 2 }}>{category.description}</Typography>
<Grid mt={1} container spacing={2}>
<Grid item xs={12} md={6}>
<Button
fullWidth
onClick={() => navigate('/categories/' + category.type)}
variant={'contained'}
>{`See all ${category.title}`}</Button>
</Grid>
<Grid item xs={12} md={6}>
<Button
fullWidth
onClick={() => navigate(category.example.path)}
variant={'outlined'}
>{`Try ${category.example.title}`}</Button>
</Grid>
</Grid>
</CardContent>
</Card>
</Grid>
))}
</Grid>
</Box> </Box>
); );
} }

View File

@@ -1,62 +0,0 @@
import { expect, describe, it } from 'vitest';
import { duplicateList } from './service';
describe('duplicateList function', () => {
it('should duplicate elements correctly with symbol split', () => {
const input = "Hello World";
const result = duplicateList('symbol', ' ', ' ', input, true, false, 2);
expect(result).toBe("Hello World Hello World");
});
it('should duplicate elements correctly with regex split', () => {
const input = "Hello||World";
const result = duplicateList('regex', '\\|\\|', ' ', input, true, false, 2);
expect(result).toBe("Hello World Hello World");
});
it('should handle fractional duplication', () => {
const input = "Hello World";
const result = duplicateList('symbol', ' ', ' ', input, true, false, 1.5);
expect(result).toBe("Hello World Hello");
});
it('should handle reverse option correctly', () => {
const input = "Hello World";
const result = duplicateList('symbol', ' ', ' ', input, true, true, 2);
expect(result).toBe("Hello World World Hello");
});
it('should handle concatenate option correctly', () => {
const input = "Hello World";
const result = duplicateList('symbol', ' ', ' ', input, false, false, 2);
expect(result).toBe("Hello Hello World World");
});
it('should handle interweaving option correctly', () => {
const input = "Hello World";
const result = duplicateList('symbol', ' ', ' ', input, false, false, 2);
expect(result).toBe("Hello Hello World World");
});
it('should throw an error for negative copies', () => {
expect(() => duplicateList('symbol', ' ', ' ', "Hello World", true, false, -1)).toThrow("Number of copies cannot be negative");
});
it('should handle interweaving option correctly 2', () => {
const input = "je m'appelle king";
const result = duplicateList('symbol', ' ', ', ', input, false, true, 2.1);
expect(result).toBe("je, king, m'appelle, m'appelle, king, je");
});
it('should handle interweaving option correctly 3', () => {
const input = "je m'appelle king";
const result = duplicateList('symbol', ' ', ', ', input, false, true, 1);
expect(result).toBe("je, m'appelle, king");
});
it('should handle interweaving option correctly 3', () => {
const input = "je m'appelle king";
const result = duplicateList('symbol', ' ', ', ', input, true, true, 2.7);
expect(result).toBe("je, m'appelle, king, king, m'appelle, je, king, m'appelle");
});
});

View File

@@ -1,69 +0,0 @@
export type SplitOperatorType = 'symbol' | 'regex';
function interweave(
array1: string[],
array2: string[]) {
const result: string[] = [];
const maxLength = Math.max(array1.length, array2.length);
for (let i = 0; i < maxLength; i++) {
if (i < array1.length) result.push(array1[i]);
if (i < array2.length) result.push(array2[i]);
}
return result;
}
function duplicate(
input: string[],
concatenate: boolean,
reverse: boolean,
copy?: number
) {
if (copy) {
if (copy > 0) {
let result: string[] = [];
let toAdd: string[] = [];
let WholePart: string[] = [];
let fractionalPart: string[] = [];
const whole = Math.floor(copy);
const fractional = copy - whole;
if (!reverse) {
WholePart = concatenate ? Array(whole).fill(input).flat() : Array(whole - 1).fill(input).flat();
fractionalPart = input.slice(0, Math.floor(input.length * fractional));
toAdd = WholePart.concat(fractionalPart);
result = concatenate ? WholePart.concat(fractionalPart) : interweave(input, toAdd);
} else {
WholePart = Array(whole - 1).fill(input).flat().reverse()
fractionalPart = input.slice().reverse().slice(0, Math.floor(input.length * fractional));
toAdd = WholePart.concat(fractionalPart);
result = concatenate ? input.concat(toAdd) : interweave(input, toAdd);
}
return result;
}
throw new Error("Number of copies cannot be negative");
}
throw new Error("Number of copies must be a valid number");
}
export function duplicateList(
splitOperatorType: SplitOperatorType,
splitSeparator: string,
joinSeparator: string,
input: string,
concatenate: boolean,
reverse: boolean,
copy?: number
): string {
let array: string[];
let result: string[];
switch (splitOperatorType) {
case 'symbol':
array = input.split(splitSeparator);
break;
case 'regex':
array = input.split(new RegExp(splitSeparator)).filter(item => item !== '');
break;
}
result = duplicate(array, concatenate, reverse, copy);
return result.join(joinSeparator);
}

View File

@@ -1,13 +0,0 @@
import { defineTool } from '@tools/defineTool';
import { lazy } from 'react';
// import image from '@assets/text.png';
export const tool = defineTool('list', {
name: 'Reverse',
path: 'reverse',
// image,
description: '',
shortDescription: '',
keywords: ['reverse'],
component: lazy(() => import('./index'))
});

View File

@@ -1,11 +0,0 @@
import { Box } from '@mui/material';
import React from 'react';
import * as Yup from 'yup';
const initialValues = {};
const validationSchema = Yup.object({
// splitSeparator: Yup.string().required('The separator is required')
});
export default function Rotate() {
return <Box>Lorem ipsum</Box>;
}

View File

@@ -1,11 +0,0 @@
import { Box } from '@mui/material';
import React from 'react';
import * as Yup from 'yup';
const initialValues = {};
const validationSchema = Yup.object({
// splitSeparator: Yup.string().required('The separator is required')
});
export default function Shuffle() {
return <Box>Lorem ipsum</Box>;
}

View File

@@ -1,119 +0,0 @@
import { Box } from '@mui/material';
import React, { useState } from 'react';
import ToolTextInput from '../../../components/input/ToolTextInput';
import ToolTextResult from '../../../components/result/ToolTextResult';
import * as Yup from 'yup';
import ToolOptions from '../../../components/options/ToolOptions';
import { compute, NumberExtractionType } from './service';
import RadioWithTextField from '../../../components/options/RadioWithTextField';
import SimpleRadio from '../../../components/options/SimpleRadio';
import CheckboxWithDesc from '../../../components/options/CheckboxWithDesc';
import ToolInputAndResult from '../../../components/ToolInputAndResult';
const initialValues = {
extractionType: 'smart' as NumberExtractionType,
separator: '\\n',
printRunningSum: false
};
const extractionTypes: {
title: string;
description: string;
type: NumberExtractionType;
withTextField: boolean;
textValueAccessor?: keyof typeof initialValues;
}[] = [
{
title: 'Smart sum',
description: 'Auto detect numbers in the input.',
type: 'smart',
withTextField: false
},
{
title: 'Number Delimiter',
type: 'delimiter',
description:
'Input SeparatorCustomize the number separator here. (By default a line break.)',
withTextField: true,
textValueAccessor: 'separator'
}
];
export default function SplitText() {
const [input, setInput] = useState<string>('');
const [result, setResult] = useState<string>('');
const validationSchema = Yup.object({
// splitSeparator: Yup.string().required('The separator is required')
});
return (
<Box>
<ToolInputAndResult
input={<ToolTextInput value={input} onChange={setInput} />}
result={<ToolTextResult title={'Total'} value={result} />}
/>
<ToolOptions
getGroups={({ values, updateField }) => [
{
title: 'Number extraction',
component: extractionTypes.map(
({
title,
description,
type,
withTextField,
textValueAccessor
}) =>
withTextField ? (
<RadioWithTextField
key={type}
checked={type === values.extractionType}
title={title}
fieldName={'extractionType'}
description={description}
value={
textValueAccessor
? values[textValueAccessor].toString()
: ''
}
onRadioClick={() => updateField('extractionType', type)}
onTextChange={(val) =>
textValueAccessor
? updateField(textValueAccessor, val)
: null
}
/>
) : (
<SimpleRadio
key={title}
onClick={() => updateField('extractionType', type)}
checked={values.extractionType === type}
description={description}
title={title}
/>
)
)
},
{
title: 'Running Sum',
component: (
<CheckboxWithDesc
title={'Print Running Sum'}
description={"Display the sum as it's calculated step by step."}
checked={values.printRunningSum}
onChange={(value) => updateField('printRunningSum', value)}
/>
)
}
]}
compute={(optionsValues, input) => {
const { extractionType, printRunningSum, separator } = optionsValues;
setResult(compute(input, extractionType, printRunningSum, separator));
}}
initialValues={initialValues}
input={input}
validationSchema={validationSchema}
/>
</Box>
);
}

View File

@@ -1,66 +0,0 @@
import { expect, describe, it } from 'vitest';
import { createPalindromeList, createPalindrome } from './service';
describe('createPalindrome', () => {
test('should create palindrome by reversing the entire string', () => {
const input = 'hello';
const result = createPalindrome(input, true);
expect(result).toBe('helloolleh');
});
test('should create palindrome by reversing the string excluding the last character', () => {
const input = 'hello';
const result = createPalindrome(input, false);
expect(result).toBe('hellolleh');
});
test('should return an empty string if input is empty', () => {
const input = '';
const result = createPalindrome(input, true);
expect(result).toBe('');
});
});
describe('createPalindromeList', () => {
test('should create palindrome for single-line input', () => {
const input = 'hello';
const result = createPalindromeList(input, true, false);
expect(result).toBe('helloolleh');
});
test('should create palindrome for single-line input considering trailing spaces', () => {
const input = 'hello ';
const result = createPalindromeList(input, true, false);
expect(result).toBe('hello olleh');
});
test('should create palindrome for single-line input ignoring trailing spaces if lastChar is set to false', () => {
const input = 'hello ';
const result = createPalindromeList(input, true, false);
expect(result).toBe('hello olleh');
});
test('should create palindrome for multi-line input', () => {
const input = 'hello\nworld';
const result = createPalindromeList(input, true, true);
expect(result).toBe('helloolleh\nworlddlrow');
});
test('should create palindrome for no multi-line input', () => {
const input = 'hello\nworld\n';
const result = createPalindromeList(input, true, false);
expect(result).toBe('hello\nworld\n\ndlrow\nolleh');
});
test('should handle multi-line input with lastChar set to false', () => {
const input = 'hello\nworld';
const result = createPalindromeList(input, false, true);
expect(result).toBe('hellolleh\nworldlrow');
});
test('should return an empty string if input is empty', () => {
const input = '';
const result = createPalindromeList(input, true, false);
expect(result).toBe('');
});
});

View File

@@ -1,35 +0,0 @@
import { reverseString } from 'utils/string'
export function createPalindrome(
input: string,
lastChar: boolean // only checkbox is need here to handle it [instead of two combo boxes]
) {
if (!input) return '';
let result: string;
let reversedString: string;
// reverse the whole input if lastChar enabled
reversedString = lastChar ? reverseString(input) : reverseString(input.slice(0, -1));
result = input.concat(reversedString);
return result;
}
export function createPalindromeList(
input: string,
lastChar: boolean,
multiLine: boolean
): string {
if (!input) return '';
let array: string[];
let result: string[] = [];
if (!multiLine) return createPalindrome(input, lastChar);
else {
array = input.split('\n');
for (const word of array) {
result.push(createPalindrome(word, lastChar));
}
}
return result.join('\n');
}

View File

@@ -1,57 +0,0 @@
import { expect, describe, it } from 'vitest';
import { extractSubstring } from './service';
describe('extractSubstring', () => {
it('should extract a substring from single-line input', () => {
const input = 'hello world';
const result = extractSubstring(input, 1, 4, false, false);
expect(result).toBe('hell');
});
it('should extract and reverse a substring from single-line input', () => {
const input = 'hello world';
const result = extractSubstring(input, 1, 5, false, true);
expect(result).toBe('olleh');
});
it('should extract substrings from multi-line input', () => {
const input = 'hello\nworld';
const result = extractSubstring(input, 1, 5, true, false);
expect(result).toBe('hello\nworld');
});
it('should extract and reverse substrings from multi-line input', () => {
const input = 'hello\nworld';
const result = extractSubstring(input, 1, 4, true, true);
expect(result).toBe('lleh\nlrow');
});
it('should handle empty input', () => {
const input = '';
const result = extractSubstring(input, 1, 5, false, false);
expect(result).toBe('');
});
it('should handle start and length out of bounds', () => {
const input = 'hello';
const result = extractSubstring(input, 10, 5, false, false);
expect(result).toBe('');
});
it('should handle negative start and length', () => {
expect(() => extractSubstring('hello', -1, 5, false, false)).toThrow("Start index must be greater than zero.");
expect(() => extractSubstring('hello', 1, -5, false, false)).toThrow("Length value must be greater than or equal to zero.");
});
it('should handle zero length', () => {
const input = 'hello';
const result = extractSubstring(input, 1, 0, false, false);
expect(result).toBe('');
});
it('should work', () => {
const input = 'je me nomme king\n22 est mon chiffre';
const result = extractSubstring(input, 12, 7, true, false);
expect(result).toBe(' king\nchiffre');
});
});

View File

@@ -1,36 +0,0 @@
import { reverseString } from 'utils/string'
export function extractSubstring(
input: string,
start: number,
length: number,
multiLine: boolean,
reverse: boolean
): string {
if (!input) return '';
// edge Cases
if (start <= 0) throw new Error("Start index must be greater than zero.");
if (length < 0) throw new Error("Length value must be greater than or equal to zero.");
if (length === 0) return '';
let array: string[];
let result: string[] = [];
const extract = (str: string, start: number, length: number): string => {
const end = start - 1 + length;
if (start - 1 >= str.length) return '';
return str.substring(start - 1, Math.min(end, str.length));
};
if (!multiLine) {
result.push(extract(input, start, length));
}
else {
array = input.split('\n');
for (const word of array) {
result.push(extract(word, start, length));
}
}
result = reverse ? result.map(word => reverseString(word)) : result;
return result.join('\n');
}

View File

@@ -1,60 +0,0 @@
import { expect, describe, it } from 'vitest';
import { palindromeList } from './service';
describe('palindromeList', () => {
test('should return true for single character words', () => {
const input = 'a|b|c';
const separator = '|';
const result = palindromeList('symbol', input, separator);
expect(result).toBe('true|true|true');
});
test('should return false for non-palindromes', () => {
const input = 'hello|world';
const separator = '|';
const result = palindromeList('symbol', input, separator);
expect(result).toBe('false|false');
});
test('should split using regex', () => {
const input = 'racecar,abba,hello';
const separator = ',';
const result = palindromeList('regex', input, separator);
expect(result).toBe('true,true,false');
});
test('should return empty string for empty input', () => {
const input = '';
const separator = '|';
const result = palindromeList('symbol', input, separator);
expect(result).toBe('');
});
test('should split using custom separator', () => {
const input = 'racecar;abba;hello';
const separator = ';';
const result = palindromeList('symbol', input, separator);
expect(result).toBe('true;true;false');
});
test('should handle leading and trailing spaces', () => {
const input = ' racecar | abba | hello ';
const separator = '|';
const result = palindromeList('symbol', input, separator);
expect(result).toBe('true|true|false');
});
test('should handle multilines checking with trimming', () => {
const input = ' racecar \n abba \n hello ';
const separator = '\n';
const result = palindromeList('symbol', input, separator);
expect(result).toBe('true\ntrue\nfalse');
});
test('should handle empty strings in input', () => {
const input = 'racecar||hello';
const separator = '|';
const result = palindromeList('symbol', input, separator);
expect(result).toBe('true|true|false');
});
});

View File

@@ -1,47 +0,0 @@
export type SplitOperatorType = 'symbol' | 'regex';
function isPalindrome(
word: string,
left: number,
right: number
): boolean {
if (left >= right) return true;
if (word[left] !== word[right]) return false;
return isPalindrome(word, left + 1, right - 1);
}
// check each word of the input and add the palindrome status in an array
function checkPalindromes(array: string[]): boolean[] {
let status: boolean[] = [];
for (const word of array) {
const palindromeStatus = isPalindrome(word, 0, word.length - 1);
status.push(palindromeStatus);
}
return status;
}
export function palindromeList(
splitOperatorType: SplitOperatorType,
input: string,
separator: string, // the splitting separator will be the joining separator for visual satisfaction
): string {
if (!input) return '';
let array: string[];
switch (splitOperatorType) {
case 'symbol':
array = input.split(separator);
break;
case 'regex':
array = input.split(new RegExp(separator));
break;
}
// trim all items to focus on the word and not biasing the result due to spaces (leading and trailing)
array = array.map((item) => item.trim());
const statusArray = checkPalindromes(array);
return statusArray.map(status => status.toString()).join(separator);
}

View File

@@ -1,48 +0,0 @@
import { expect, describe, it } from 'vitest';
import { randomizeCase } from './service';
describe('randomizeCase', () => {
it('should randomize the case of each character in the string', () => {
const input = 'hello world';
const result = randomizeCase(input);
// Ensure the output length is the same
expect(result).toHaveLength(input.length);
// Ensure each character in the input string appears in the result
for (let i = 0; i < input.length; i++) {
const inputChar = input[i];
const resultChar = result[i];
if (/[a-zA-Z]/.test(inputChar)) {
expect([inputChar.toLowerCase(), inputChar.toUpperCase()]).toContain(resultChar);
} else {
expect(inputChar).toBe(resultChar);
}
}
});
it('should handle an empty string', () => {
const input = '';
const result = randomizeCase(input);
expect(result).toBe('');
});
it('should handle a string with numbers and symbols', () => {
const input = '123 hello! @world';
const result = randomizeCase(input);
// Ensure the output length is the same
expect(result).toHaveLength(input.length);
// Ensure numbers and symbols remain unchanged
for (let i = 0; i < input.length; i++) {
const inputChar = input[i];
const resultChar = result[i];
if (!/[a-zA-Z]/.test(inputChar)) {
expect(inputChar).toBe(resultChar);
}
}
});
});

View File

@@ -1,6 +0,0 @@
export function randomizeCase(input: string): string {
return input
.split('')
.map(char => (Math.random() < 0.5 ? char.toLowerCase() : char.toUpperCase()))
.join('');
}

View File

@@ -1,52 +0,0 @@
import { expect, describe, it } from 'vitest';
import { stringReverser } from './service';
describe('stringReverser', () => {
it('should reverse a single-line string', () => {
const input = 'hello world';
const result = stringReverser(input, false, false, false);
expect(result).toBe('dlrow olleh');
});
it('should reverse each line in a multi-line string', () => {
const input = 'hello\nworld';
const result = stringReverser(input, true, false, false);
expect(result).toBe('olleh\ndlrow');
});
it('should remove empty items if emptyItems is true', () => {
const input = 'hello\n\nworld';
const result = stringReverser(input, true, true, false);
expect(result).toBe('olleh\ndlrow');
});
it('should trim each line if trim is true', () => {
const input = ' hello \n world ';
const result = stringReverser(input, true, false, true);
expect(result).toBe('olleh\ndlrow');
});
it('should handle empty input', () => {
const input = '';
const result = stringReverser(input, false, false, false);
expect(result).toBe('');
});
it('should handle a single line with emptyItems and trim', () => {
const input = ' hello world ';
const result = stringReverser(input, false, true, true);
expect(result).toBe('dlrow olleh');
});
it('should handle a single line with emptyItems and non trim', () => {
const input = ' hello world ';
const result = stringReverser(input, false, true, false);
expect(result).toBe(' dlrow olleh ');
});
it('should handle a multi line with emptyItems and non trim', () => {
const input = ' hello\n\n\n\nworld ';
const result = stringReverser(input, true, true, false);
expect(result).toBe('olleh \n dlrow');
});
});

View File

@@ -1,31 +0,0 @@
import { reverseString } from 'utils/string';
export function stringReverser(
input: string,
multiLine: boolean,
emptyItems: boolean,
trim: boolean
) {
let array: string[] = [];
let result: string[] = [];
// split the input in multiLine mode
if (multiLine) {
array = input.split('\n');
}
else {
array.push(input);
}
// handle empty items
if (emptyItems){
array = array.filter(Boolean);
}
// Handle trim
if (trim) {
array = array.map(line => line.trim());
}
result = array.map(element => reverseString(element));
return result.join('\n');
}

View File

@@ -1,153 +0,0 @@
import { Box } from '@mui/material';
import React, { useState } from 'react';
import ToolTextInput from '../../../components/input/ToolTextInput';
import ToolTextResult from '../../../components/result/ToolTextResult';
import * as Yup from 'yup';
import ToolOptions from '../../../components/options/ToolOptions';
import { compute, SplitOperatorType } from './service';
import RadioWithTextField from '../../../components/options/RadioWithTextField';
import TextFieldWithDesc from '../../../components/options/TextFieldWithDesc';
import ToolInputAndResult from '../../../components/ToolInputAndResult';
const initialValues = {
splitSeparatorType: 'symbol' as SplitOperatorType,
symbolValue: ' ',
regexValue: '/\\s+/',
lengthValue: '16',
chunksValue: '4',
outputSeparator: '\\n',
charBeforeChunk: '',
charAfterChunk: ''
};
const splitOperators: {
title: string;
description: string;
type: SplitOperatorType;
}[] = [
{
title: 'Use a Symbol for Splitting',
description:
'Character that will be used to\n' +
'break text into parts.\n' +
'(Space by default.)',
type: 'symbol'
},
{
title: 'Use a Regex for Splitting',
type: 'regex',
description:
'Regular expression that will be\n' +
'used to break text into parts.\n' +
'(Multiple spaces by default.)'
},
{
title: 'Use Length for Splitting',
description:
'Number of symbols that will be\n' + 'put in each output chunk.',
type: 'length'
},
{
title: 'Use a Number of Chunks',
description: 'Number of chunks of equal\n' + 'length in the output.',
type: 'chunks'
}
];
const outputOptions: {
description: string;
accessor: keyof typeof initialValues;
}[] = [
{
description:
'Character that will be put\n' +
'between the split chunks.\n' +
'(It\'s newline "\\n" by default.)',
accessor: 'outputSeparator'
},
{
description: 'Character before each chunk',
accessor: 'charBeforeChunk'
},
{
description: 'Character after each chunk',
accessor: 'charAfterChunk'
}
];
export default function SplitText() {
const [input, setInput] = useState<string>('');
const [result, setResult] = useState<string>('');
// const formRef = useRef<FormikProps<typeof initialValues>>(null);
const computeExternal = (optionsValues: typeof initialValues, input: any) => {
const {
splitSeparatorType,
outputSeparator,
charBeforeChunk,
charAfterChunk,
chunksValue,
symbolValue,
regexValue,
lengthValue
} = optionsValues;
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')
});
return (
<Box>
<ToolInputAndResult
input={<ToolTextInput value={input} onChange={setInput} />}
result={<ToolTextResult title={'Text pieces'} value={result} />}
/>
<ToolOptions
compute={computeExternal}
getGroups={({ values, updateField }) => [
{
title: 'Split separator options',
component: splitOperators.map(({ title, description, type }) => (
<RadioWithTextField
key={type}
checked={type === values.splitSeparatorType}
title={title}
fieldName={'splitSeparatorType'}
description={description}
value={values[`${type}Value`]}
onRadioClick={() => updateField('splitSeparatorType', type)}
onTextChange={(val) => updateField(`${type}Value`, val)}
/>
))
},
{
title: 'Output separator options',
component: outputOptions.map((option) => (
<TextFieldWithDesc
key={option.accessor}
value={values[option.accessor]}
onOwnChange={(value) => updateField(option.accessor, value)}
description={option.description}
/>
))
}
]}
initialValues={initialValues}
input={input}
validationSchema={validationSchema}
/>
</Box>
);
}

View File

@@ -1,34 +0,0 @@
import { expect, describe, it } from 'vitest';
import { UppercaseInput } from './service';
describe('UppercaseInput', () => {
it('should convert a lowercase string to uppercase', () => {
const input = 'hello';
const result = UppercaseInput(input);
expect(result).toBe('HELLO');
});
it('should convert a mixed case string to uppercase', () => {
const input = 'HeLLo WoRLd';
const result = UppercaseInput(input);
expect(result).toBe('HELLO WORLD');
});
it('should convert an already uppercase string to uppercase', () => {
const input = 'HELLO';
const result = UppercaseInput(input);
expect(result).toBe('HELLO');
});
it('should handle an empty string', () => {
const input = '';
const result = UppercaseInput(input);
expect(result).toBe('');
});
it('should handle a string with numbers and symbols', () => {
const input = '123 hello! @world';
const result = UppercaseInput(input);
expect(result).toBe('123 HELLO! @WORLD');
});
});

View File

@@ -5,14 +5,15 @@ import { Link, useNavigate, useParams } from 'react-router-dom';
import { getToolsByCategory } from '../../tools'; import { getToolsByCategory } from '../../tools';
import Hero from 'components/Hero'; import Hero from 'components/Hero';
import { capitalizeFirstLetter } from '../../utils/string'; import { capitalizeFirstLetter } from '../../utils/string';
import toolsPng from '@assets/tools.png'; import { Icon } from '@iconify/react';
import { categoriesColors } from 'config/uiConfig';
export default function Home() { export default function Home() {
const navigate = useNavigate(); const navigate = useNavigate();
const theme = useTheme(); const theme = useTheme();
const { categoryName } = useParams(); const { categoryName } = useParams();
return ( return (
<Box> <Box sx={{ backgroundColor: '#F5F5FA' }}>
<Box <Box
padding={{ xs: 1, md: 3, lg: 5 }} padding={{ xs: 1, md: 3, lg: 5 }}
display={'flex'} display={'flex'}
@@ -32,10 +33,12 @@ export default function Home() {
<Grid container spacing={2} mt={2}> <Grid container spacing={2} mt={2}>
{getToolsByCategory() {getToolsByCategory()
.find(({ type }) => type === categoryName) .find(({ type }) => type === categoryName)
?.tools?.map((tool) => ( ?.tools?.map((tool, index) => (
<Grid item xs={12} md={6} lg={4} key={tool.path}> <Grid item xs={12} md={6} lg={4} key={tool.path}>
<Stack <Stack
sx={{ sx={{
backgroundColor: 'white',
boxShadow: '5px 4px 2px #E9E9ED',
cursor: 'pointer', cursor: 'pointer',
'&:hover': { '&:hover': {
backgroundColor: theme.palette.background.default // Change this to your desired hover color backgroundColor: theme.palette.background.default // Change this to your desired hover color
@@ -43,14 +46,21 @@ export default function Home() {
}} }}
onClick={() => navigate('/' + tool.path)} onClick={() => navigate('/' + tool.path)}
direction={'row'} direction={'row'}
alignItems={'center'}
spacing={2} spacing={2}
padding={2} padding={2}
border={1} border={`1px solid ${theme.palette.background.default}`}
borderRadius={2} borderRadius={2}
> >
<img width={100} src={tool.image ?? toolsPng} /> <Icon
icon={tool.icon ?? 'ph:compass-tool-thin'}
fontSize={'60px'}
color={categoriesColors[index % categoriesColors.length]}
/>
<Box> <Box>
<Link to={'/' + tool.path}>{tool.name}</Link> <Link style={{ fontSize: 20 }} to={'/' + tool.path}>
{tool.name}
</Link>
<Typography sx={{ mt: 2 }}> <Typography sx={{ mt: 2 }}>
{tool.shortDescription} {tool.shortDescription}
</Typography> </Typography>

View File

@@ -2,7 +2,7 @@ import { expect, test } from '@playwright/test';
import { Buffer } from 'buffer'; import { Buffer } from 'buffer';
import path from 'path'; import path from 'path';
import Jimp from 'jimp'; import Jimp from 'jimp';
import { convertHexToRGBA } from '../../../../utils/color'; import { convertHexToRGBA } from '../../../../../utils/color';
test.describe('Change colors in png', () => { test.describe('Change colors in png', () => {
test.beforeEach(async ({ page }) => { test.beforeEach(async ({ page }) => {

View File

@@ -1,14 +1,16 @@
import { Box } from '@mui/material'; import { Box } from '@mui/material';
import React, { useState } from 'react'; import React, { useState } from 'react';
import * as Yup from 'yup'; import * as Yup from 'yup';
import ToolFileInput from '../../../../components/input/ToolFileInput'; import ToolFileInput from '@components/input/ToolFileInput';
import ToolFileResult from '../../../../components/result/ToolFileResult'; import ToolFileResult from '@components/result/ToolFileResult';
import ToolOptions from '../../../../components/options/ToolOptions'; import ToolOptions, { GetGroupsType } from '@components/options/ToolOptions';
import ColorSelector from '../../../../components/options/ColorSelector'; import ColorSelector from '@components/options/ColorSelector';
import Color from 'color'; import Color from 'color';
import TextFieldWithDesc from 'components/options/TextFieldWithDesc'; import TextFieldWithDesc from 'components/options/TextFieldWithDesc';
import ToolInputAndResult from '../../../../components/ToolInputAndResult'; import ToolInputAndResult from '@components/ToolInputAndResult';
import { areColorsSimilar } from 'utils/color'; import { areColorsSimilar } from 'utils/color';
import ToolContent from '@components/ToolContent';
import { ToolComponentProps } from '@tools/defineTool';
const initialValues = { const initialValues = {
fromColor: 'white', fromColor: 'white',
@@ -18,7 +20,7 @@ const initialValues = {
const validationSchema = Yup.object({ const validationSchema = Yup.object({
// splitSeparator: Yup.string().required('The separator is required') // splitSeparator: Yup.string().required('The separator is required')
}); });
export default function ChangeColorsInPng() { export default function ChangeColorsInPng({ title }: ToolComponentProps) {
const [input, setInput] = useState<File | null>(null); const [input, setInput] = useState<File | null>(null);
const [result, setResult] = useState<File | null>(null); const [result, setResult] = useState<File | null>(null);
@@ -83,59 +85,65 @@ export default function ChangeColorsInPng() {
processImage(input, fromRgb, toRgb, Number(similarity)); processImage(input, fromRgb, toRgb, Number(similarity));
}; };
const getGroups: GetGroupsType<typeof initialValues> = ({
values,
updateField
}) => [
{
title: 'From color and to color',
component: (
<Box>
<ColorSelector
value={values.fromColor}
onColorChange={(val) => updateField('fromColor', val)}
description={'Replace this color (from color)'}
inputProps={{ 'data-testid': 'from-color-input' }}
/>
<ColorSelector
value={values.toColor}
onColorChange={(val) => updateField('toColor', val)}
description={'With this color (to color)'}
inputProps={{ 'data-testid': 'to-color-input' }}
/>
<TextFieldWithDesc
value={values.similarity}
onOwnChange={(val) => updateField('similarity', val)}
description={
'Match this % of similar colors of the from color. For example, 10% white will match white and a little bit of gray.'
}
/>
</Box>
)
}
];
return ( return (
<Box> <ToolContent
<ToolInputAndResult title={title}
input={ initialValues={initialValues}
<ToolFileInput getGroups={getGroups}
value={input} compute={compute}
onChange={setInput} input={input}
accept={['image/png']} validationSchema={validationSchema}
title={'Input PNG'} inputComponent={
/> <ToolFileInput
} value={input}
result={ onChange={setInput}
<ToolFileResult accept={['image/png']}
title={'Output PNG with new colors'} title={'Input PNG'}
value={result} />
extension={'png'} }
/> resultComponent={
} <ToolFileResult
/> title={'Transparent PNG'}
<ToolOptions value={result}
compute={compute} extension={'png'}
getGroups={({ values, updateField }) => [ />
{ }
title: 'From color and to color', toolInfo={{
component: ( title: 'Make Colors Transparent',
<Box> description:
<ColorSelector 'This tool allows you to make specific colors in a PNG image transparent. You can select the color to replace and adjust the similarity threshold to include similar colors.'
value={values.fromColor} }}
onColorChange={(val) => updateField('fromColor', val)} />
description={'Replace this color (from color)'}
inputProps={{ 'data-testid': 'from-color-input' }}
/>
<ColorSelector
value={values.toColor}
onColorChange={(val) => updateField('toColor', val)}
description={'With this color (to color)'}
inputProps={{ 'data-testid': 'to-color-input' }}
/>
<TextFieldWithDesc
value={values.similarity}
onOwnChange={(val) => updateField('similarity', val)}
description={
'Match this % of similar colors of the from color. For example, 10% white will match white and a little bit of gray.'
}
/>
</Box>
)
}
]}
initialValues={initialValues}
input={input}
validationSchema={validationSchema}
/>
</Box>
); );
} }

View File

@@ -1,11 +1,10 @@
import { defineTool } from '@tools/defineTool'; import { defineTool } from '@tools/defineTool';
import { lazy } from 'react'; import { lazy } from 'react';
import image from '@assets/image.png';
export const tool = defineTool('png', { export const tool = defineTool('png', {
name: 'Change colors in png', name: 'Change colors in png',
path: 'change-colors-in-png', path: 'change-colors-in-png',
image, icon: 'cil:color-fill',
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.", "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.",
shortDescription: 'Quickly swap colors in a PNG image', shortDescription: 'Quickly swap colors in a PNG image',

View File

Before

Width:  |  Height:  |  Size: 3.6 KiB

After

Width:  |  Height:  |  Size: 3.6 KiB

View File

@@ -0,0 +1,113 @@
import { Box } from '@mui/material';
import React, { useState } from 'react';
import * as Yup from 'yup';
import ToolFileInput from '@components/input/ToolFileInput';
import ToolFileResult from '@components/result/ToolFileResult';
import ToolOptions from '@components/options/ToolOptions';
import TextFieldWithDesc from 'components/options/TextFieldWithDesc';
import ToolInputAndResult from '@components/ToolInputAndResult';
import imageCompression from 'browser-image-compression';
import Typography from '@mui/material/Typography';
const initialValues = {
rate: '50'
};
const validationSchema = Yup.object({
// splitSeparator: Yup.string().required('The separator is required')
});
export default function ChangeColorsInPng() {
const [input, setInput] = useState<File | null>(null);
const [result, setResult] = useState<File | null>(null);
const [originalSize, setOriginalSize] = useState<number | null>(null); // Store original file size
const [compressedSize, setCompressedSize] = useState<number | null>(null); // Store compressed file size
const compressImage = async (file: File, rate: number) => {
if (!file) return;
// Set original file size
setOriginalSize(file.size);
const options = {
maxSizeMB: 1, // Maximum size in MB
maxWidthOrHeight: 1024, // Maximum width or height
quality: rate / 100, // Convert percentage to decimal (e.g., 50% becomes 0.5)
useWebWorker: true
};
try {
const compressedFile = await imageCompression(file, options);
setResult(compressedFile);
setCompressedSize(compressedFile.size); // Set compressed file size
} catch (error) {
console.error('Error during compression:', error);
}
};
const compute = (optionsValues: typeof initialValues, input: any) => {
if (!input) return;
const { rate } = optionsValues;
compressImage(input, Number(rate)); // Pass the rate as a number
};
return (
<Box>
<ToolInputAndResult
input={
<ToolFileInput
value={input}
onChange={setInput}
accept={['image/png']}
title={'Input PNG'}
/>
}
result={
<ToolFileResult
title={'Compressed PNG'}
value={result}
extension={'png'}
/>
}
/>
<ToolOptions
compute={compute}
getGroups={({ values, updateField }) => [
{
title: 'Compression options',
component: (
<Box>
<TextFieldWithDesc
value={values.rate}
onOwnChange={(val) => updateField('rate', val)}
description={'Compression rate (1-100)'}
/>
</Box>
)
},
{
title: 'File sizes',
component: (
<Box>
<Box>
{originalSize !== null && (
<Typography>
Original Size: {(originalSize / 1024).toFixed(2)} KB
</Typography>
)}
{compressedSize !== null && (
<Typography>
Compressed Size: {(compressedSize / 1024).toFixed(2)} KB
</Typography>
)}
</Box>
</Box>
)
}
]}
initialValues={initialValues}
input={input}
/>
</Box>
);
}

View File

@@ -0,0 +1,14 @@
import { defineTool } from '@tools/defineTool';
import { lazy } from 'react';
// import image from '@assets/text.png';
export const tool = defineTool('png', {
name: 'Compress png',
path: 'compress-png',
icon: 'material-symbols-light:compress',
description:
'This is a program that compresses PNG pictures. As soon as you paste your PNG picture in the input area, the program will compress it and show the result in the output area. In the options, you can adjust the compression level, as well as find the old and new picture file sizes.',
shortDescription: 'Quicly compress a PNG',
keywords: ['compress', 'png'],
component: lazy(() => import('./index'))
});

View File

@@ -148,7 +148,6 @@ export default function ConvertJgpToPng() {
]} ]}
initialValues={initialValues} initialValues={initialValues}
input={input} input={input}
validationSchema={validationSchema}
/> />
</Box> </Box>
); );

View File

@@ -1,11 +1,10 @@
import { defineTool } from '@tools/defineTool'; import { defineTool } from '@tools/defineTool';
import { lazy } from 'react'; import { lazy } from 'react';
import image from '@assets/image.png';
export const tool = defineTool('png', { export const tool = defineTool('png', {
name: 'Convert JPG to PNG', name: 'Convert JPG to PNG',
path: 'convert-jgp-to-png', path: 'convert-jgp-to-png',
image, icon: 'ph:file-jpg-thin',
description: description:
'Quickly convert your JPG images to PNG. Just import your PNG image in the editor on the left', 'Quickly convert your JPG images to PNG. Just import your PNG image in the editor on the left',
shortDescription: 'Quickly convert your JPG images to PNG', shortDescription: 'Quickly convert your JPG images to PNG',

View File

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

View File

@@ -1,13 +1,13 @@
import { Box } from '@mui/material'; import { Box } from '@mui/material';
import React, { useState } from 'react'; import React, { useState } from 'react';
import * as Yup from 'yup'; import * as Yup from 'yup';
import ToolFileInput from '../../../../components/input/ToolFileInput'; import ToolFileInput from '@components/input/ToolFileInput';
import ToolFileResult from '../../../../components/result/ToolFileResult'; import ToolFileResult from '@components/result/ToolFileResult';
import ToolOptions from '../../../../components/options/ToolOptions'; import ToolOptions from '@components/options/ToolOptions';
import ColorSelector from '../../../../components/options/ColorSelector'; import ColorSelector from '@components/options/ColorSelector';
import Color from 'color'; import Color from 'color';
import TextFieldWithDesc from 'components/options/TextFieldWithDesc'; import TextFieldWithDesc from 'components/options/TextFieldWithDesc';
import ToolInputAndResult from '../../../../components/ToolInputAndResult'; import ToolInputAndResult from '@components/ToolInputAndResult';
import { areColorsSimilar } from 'utils/color'; import { areColorsSimilar } from 'utils/color';
const initialValues = { const initialValues = {
@@ -121,7 +121,6 @@ export default function ChangeColorsInPng() {
]} ]}
initialValues={initialValues} initialValues={initialValues}
input={input} input={input}
validationSchema={validationSchema}
/> />
</Box> </Box>
); );

View File

@@ -1,11 +1,10 @@
import { defineTool } from '@tools/defineTool'; import { defineTool } from '@tools/defineTool';
import { lazy } from 'react'; import { lazy } from 'react';
import image from '@assets/image.png';
export const tool = defineTool('png', { export const tool = defineTool('png', {
name: 'Create transparent PNG', name: 'Create transparent PNG',
path: 'create-transparent', path: 'create-transparent',
image, icon: 'mdi:circle-transparent',
shortDescription: 'Quickly make a PNG image transparent', shortDescription: 'Quickly make a PNG image transparent',
description: 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.", "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.",

View File

Before

Width:  |  Height:  |  Size: 3.6 KiB

After

Width:  |  Height:  |  Size: 3.6 KiB

View File

@@ -1,9 +1,11 @@
import { tool as pngCompressPng } from './compress-png/meta';
import { tool as convertJgpToPng } from './convert-jgp-to-png/meta'; import { tool as convertJgpToPng } from './convert-jgp-to-png/meta';
import { tool as pngCreateTransparent } from './create-transparent/meta'; import { tool as pngCreateTransparent } from './create-transparent/meta';
import { tool as changeColorsInPng } from './change-colors-in-png/meta'; import { tool as changeColorsInPng } from './change-colors-in-png/meta';
export const pngTools = [ export const pngTools = [
changeColorsInPng, pngCompressPng,
pngCreateTransparent, pngCreateTransparent,
changeColorsInPng,
convertJgpToPng convertJgpToPng
]; ];

View File

@@ -0,0 +1,3 @@
import { tool as jsonPrettify } from './prettify/meta';
export const jsonTools = [jsonPrettify];

View File

@@ -0,0 +1,190 @@
import { Box } from '@mui/material';
import React, { useRef, useState } from 'react';
import ToolTextInput from '@components/input/ToolTextInput';
import ToolTextResult from '@components/result/ToolTextResult';
import ToolOptions, { GetGroupsType } from '@components/options/ToolOptions';
import { beautifyJson } from './service';
import ToolInputAndResult from '@components/ToolInputAndResult';
import ToolInfo from '@components/ToolInfo';
import Separator from '@components/Separator';
import ToolExamples, {
CardExampleType
} from '@components/examples/ToolExamples';
import { FormikProps } from 'formik';
import { ToolComponentProps } from '@tools/defineTool';
import RadioWithTextField from '@components/options/RadioWithTextField';
import SimpleRadio from '@components/options/SimpleRadio';
import { isNumber } from '../../../../utils/string';
type InitialValuesType = {
indentationType: 'tab' | 'space';
spacesCount: number;
};
const initialValues: InitialValuesType = {
indentationType: 'space',
spacesCount: 2
};
const exampleCards: CardExampleType<InitialValuesType>[] = [
{
title: 'Beautify an Ugly JSON Array',
description:
'In this example, we prettify an ugly JSON array. The input data is a one-dimensional array of numbers [1,2,3] but they are all over the place. This array gets cleaned up and transformed into a more readable format where each element is on a new line with an appropriate indentation using four spaces.',
sampleText: `[
1,
2,3
]`,
sampleResult: `[
1,
2,
3
]`,
sampleOptions: {
indentationType: 'space',
spacesCount: 4
}
},
{
title: 'Prettify a Complex JSON Object',
description:
'In this example, we prettify a complex JSON data structure consisting of arrays and objects. The input data is a minified JSON object with multiple data structure depth levels. To make it neat and readable, we add two spaces for indentation to each depth level, making the JSON structure clear and easy to understand.',
sampleText: `{"names":["jack","john","alex"],"hobbies":{"jack":["programming","rock climbing"],"john":["running","racing"],"alex":["dancing","fencing"]}}`,
sampleResult: `{
"names": [
"jack",
"john",
"alex"
],
"hobbies": {
"jack": [
"programming",
"rock climbing"
],
"john": [
"running",
"racing"
],
"alex": [
"dancing",
"fencing"
]
}
}`,
sampleOptions: {
indentationType: 'space',
spacesCount: 2
}
},
{
title: 'Beautify a JSON with Excessive Whitespace',
description:
"In this example, we show how the JSON prettify tool can handle code with excessive whitespace. The input file has many leading and trailing spaces as well as spaces within the objects. The excessive whitespace makes the file bulky and hard to read and leads to a bad impression of the programmer who wrote it. The program removes all these unnecessary spaces and creates a proper data hierarchy that's easy to work with by adding indentation via tabs.",
sampleText: `
{
"name": "The Name of the Wind",
"author" : "Patrick Rothfuss",
"genre" : "Fantasy",
"published" : 2007,
"rating" : {
"average" : 4.6,
"goodreads" : 4.58,
"amazon" : 4.4
},
"is_fiction" : true
}
`,
sampleResult: `{
\t"name": "The Name of the Wind",
\t"author": "Patrick Rothfuss",
\t"genre": "Fantasy",
\t"published": 2007,
\t"rating": {
\t\t"average": 4.6,
\t\t"goodreads": 4.58,
\t\t"amazon": 4.4
\t},
\t"is_fiction": true
}`,
sampleOptions: {
indentationType: 'tab',
spacesCount: 0
}
}
];
export default function PrettifyJson({ title }: ToolComponentProps) {
const [input, setInput] = useState<string>('');
const [result, setResult] = useState<string>('');
const formRef = useRef<FormikProps<InitialValuesType>>(null);
const compute = (optionsValues: InitialValuesType, input: any) => {
const { indentationType, spacesCount } = optionsValues;
if (input) setResult(beautifyJson(input, indentationType, spacesCount));
};
const getGroups: GetGroupsType<InitialValuesType> = ({
values,
updateField
}) => [
{
title: 'Indentation',
component: (
<Box>
<RadioWithTextField
checked={values.indentationType === 'space'}
title={'Use Spaces'}
fieldName={'indentationType'}
description={'Indent output with spaces'}
value={values.spacesCount.toString()}
onRadioClick={() => updateField('indentationType', 'space')}
onTextChange={(val) =>
isNumber(val) ? updateField('spacesCount', Number(val)) : null
}
/>
<SimpleRadio
onClick={() => updateField('indentationType', 'tab')}
checked={values.indentationType === 'tab'}
description={'Indent output with tabs.'}
title={'Use Tabs'}
/>
</Box>
)
}
];
return (
<Box>
<ToolInputAndResult
input={
<ToolTextInput
title={'Input JSON'}
value={input}
onChange={setInput}
/>
}
result={<ToolTextResult title={'Pretty JSON'} value={result} />}
/>
<ToolOptions
formRef={formRef}
compute={compute}
getGroups={getGroups}
initialValues={initialValues}
input={input}
/>
<ToolInfo
title="What Is a JSON Prettifier?"
description="This tool adds consistent formatting to the data in JavaScript Object Notation (JSON) format. This transformation makes the JSON code more readable, making it easier to understand and edit. The program parses the JSON data structure into tokens and then reformats them by adding indentation and line breaks. If the data is hierarchial, then it adds indentation at the beginning of lines to visually show the depth of the JSON and adds newlines to break long single-line JSON arrays into multiple shorter, more readable ones. Additionally, this utility can remove unnecessary spaces and tabs from your JSON code (especially leading and trailing whitespaces), making it more compact. You can choose the line indentation method in the options: indent with spaces or indent with tabs. When using spaces, you can also specify how many spaces to use for each indentation level (usually 2 or 4 spaces). "
/>
<Separator backgroundColor="#5581b5" margin="50px" />
<ToolExamples
title={title}
exampleCards={exampleCards}
getGroups={getGroups}
formRef={formRef}
setInput={setInput}
/>
</Box>
);
}

View File

@@ -0,0 +1,13 @@
import { defineTool } from '@tools/defineTool';
import { lazy } from 'react';
export const tool = defineTool('json', {
name: 'Prettify JSON',
path: 'prettify',
icon: 'lets-icons:json-light',
description:
"Just load your JSON in the input field and it will automatically get prettified. In the tool options, you can choose whether to use spaces or tabs for indentation and if you're using spaces, you can specify the number of spaces to add per indentation level.",
shortDescription: 'Quickly beautify a JSON data structure.',
keywords: ['prettify'],
component: lazy(() => import('./index'))
});

View File

@@ -0,0 +1,16 @@
export const beautifyJson = (
text: string,
indentationType: 'tab' | 'space',
spacesCount: number
) => {
let parsedJson;
try {
parsedJson = JSON.parse(text);
} catch (e) {
throw new Error('Invalid JSON string');
}
const indent = indentationType === 'tab' ? '\t' : spacesCount;
return JSON.stringify(parsedJson, null, indent);
};

View File

@@ -0,0 +1,66 @@
import { describe, expect, it } from 'vitest';
import { duplicateList } from './service';
describe('duplicateList function', () => {
it('should duplicate elements correctly with symbol split', () => {
const input = 'Hello World';
const result = duplicateList('symbol', ' ', ' ', input, true, false, 2);
expect(result).toBe('Hello World Hello World');
});
it('should duplicate elements correctly with regex split', () => {
const input = 'Hello||World';
const result = duplicateList('regex', '\\|\\|', ' ', input, true, false, 2);
expect(result).toBe('Hello World Hello World');
});
it('should handle fractional duplication', () => {
const input = 'Hello World';
const result = duplicateList('symbol', ' ', ' ', input, true, false, 1.5);
expect(result).toBe('Hello World Hello');
});
it('should handle reverse option correctly', () => {
const input = 'Hello World';
const result = duplicateList('symbol', ' ', ' ', input, true, true, 2);
expect(result).toBe('Hello World World Hello');
});
it('should handle concatenate option correctly', () => {
const input = 'Hello World';
const result = duplicateList('symbol', ' ', ' ', input, false, false, 2);
expect(result).toBe('Hello Hello World World');
});
it('should handle interweaving option correctly', () => {
const input = 'Hello World';
const result = duplicateList('symbol', ' ', ' ', input, false, false, 2);
expect(result).toBe('Hello Hello World World');
});
it('should throw an error for negative copies', () => {
expect(() =>
duplicateList('symbol', ' ', ' ', 'Hello World', true, false, -1)
).toThrow('Number of copies cannot be negative');
});
it('should handle interweaving option correctly 2', () => {
const input = "je m'appelle king";
const result = duplicateList('symbol', ' ', ', ', input, false, true, 2.1);
expect(result).toBe("je, king, m'appelle, m'appelle, king, je");
});
it('should handle interweaving option correctly 3', () => {
const input = "je m'appelle king";
const result = duplicateList('symbol', ' ', ', ', input, false, true, 1);
expect(result).toBe("je, m'appelle, king");
});
it('should handle interweaving option correctly 3', () => {
const input = "je m'appelle king";
const result = duplicateList('symbol', ' ', ', ', input, true, true, 2.7);
expect(result).toBe(
"je, m'appelle, king, king, m'appelle, je, king, m'appelle"
);
});
});

View File

@@ -8,4 +8,4 @@ const validationSchema = Yup.object({
}); });
export default function Duplicate() { export default function Duplicate() {
return <Box>Lorem ipsum</Box>; return <Box>Lorem ipsum</Box>;
} }

View File

@@ -5,9 +5,9 @@ import { lazy } from 'react';
export const tool = defineTool('list', { export const tool = defineTool('list', {
name: 'Duplicate', name: 'Duplicate',
path: 'duplicate', path: 'duplicate',
// image, icon: '',
description: '', description: '',
shortDescription: '', shortDescription: '',
keywords: ['duplicate'], keywords: ['duplicate'],
component: lazy(() => import('./index')) component: lazy(() => import('./index'))
}); });

View File

@@ -0,0 +1,81 @@
export type SplitOperatorType = 'symbol' | 'regex';
function interweave(array1: string[], array2: string[]) {
const result: string[] = [];
const maxLength = Math.max(array1.length, array2.length);
for (let i = 0; i < maxLength; i++) {
if (i < array1.length) result.push(array1[i]);
if (i < array2.length) result.push(array2[i]);
}
return result;
}
function duplicate(
input: string[],
concatenate: boolean,
reverse: boolean,
copy?: number
) {
if (copy) {
if (copy > 0) {
let result: string[] = [];
let toAdd: string[] = [];
let WholePart: string[] = [];
let fractionalPart: string[] = [];
const whole = Math.floor(copy);
const fractional = copy - whole;
if (!reverse) {
WholePart = concatenate
? Array(whole).fill(input).flat()
: Array(whole - 1)
.fill(input)
.flat();
fractionalPart = input.slice(0, Math.floor(input.length * fractional));
toAdd = WholePart.concat(fractionalPart);
result = concatenate
? WholePart.concat(fractionalPart)
: interweave(input, toAdd);
} else {
WholePart = Array(whole - 1)
.fill(input)
.flat()
.reverse();
fractionalPart = input
.slice()
.reverse()
.slice(0, Math.floor(input.length * fractional));
toAdd = WholePart.concat(fractionalPart);
result = concatenate ? input.concat(toAdd) : interweave(input, toAdd);
}
return result;
}
throw new Error('Number of copies cannot be negative');
}
throw new Error('Number of copies must be a valid number');
}
export function duplicateList(
splitOperatorType: SplitOperatorType,
splitSeparator: string,
joinSeparator: string,
input: string,
concatenate: boolean,
reverse: boolean,
copy?: number
): string {
let array: string[];
let result: string[];
switch (splitOperatorType) {
case 'symbol':
array = input.split(splitSeparator);
break;
case 'regex':
array = input
.split(new RegExp(splitSeparator))
.filter((item) => item !== '');
break;
}
result = duplicate(array, concatenate, reverse, copy);
return result.join(joinSeparator);
}

View File

@@ -1,10 +1,5 @@
import { expect, describe, it } from 'vitest'; import { describe, expect, it } from 'vitest';
import { import { TopItemsList } from './service';
TopItemsList,
SplitOperatorType,
SortingMethod,
DisplayFormat
} from './service';
describe('TopItemsList function', () => { describe('TopItemsList function', () => {
it('should handle sorting alphabetically ignoring case', () => { it('should handle sorting alphabetically ignoring case', () => {

View File

@@ -1,20 +1,19 @@
import { Box } from '@mui/material'; import { Box } from '@mui/material';
import React, { useState } from 'react'; import React, { useState } from 'react';
import ToolTextInput from '../../../components/input/ToolTextInput'; import ToolTextInput from '@components/input/ToolTextInput';
import ToolTextResult from '../../../components/result/ToolTextResult'; import ToolTextResult from '@components/result/ToolTextResult';
import * as Yup from 'yup'; import ToolOptions from '@components/options/ToolOptions';
import ToolOptions from '../../../components/options/ToolOptions';
import { import {
DisplayFormat, DisplayFormat,
SortingMethod, SortingMethod,
SplitOperatorType, SplitOperatorType,
TopItemsList TopItemsList
} from './service'; } from './service';
import ToolInputAndResult from '../../../components/ToolInputAndResult'; import ToolInputAndResult from '@components/ToolInputAndResult';
import SimpleRadio from '../../../components/options/SimpleRadio'; import SimpleRadio from '@components/options/SimpleRadio';
import TextFieldWithDesc from '../../../components/options/TextFieldWithDesc'; import TextFieldWithDesc from '@components/options/TextFieldWithDesc';
import CheckboxWithDesc from '../../../components/options/CheckboxWithDesc'; import CheckboxWithDesc from '@components/options/CheckboxWithDesc';
import SelectWithDesc from '../../../components/options/SelectWithDesc'; import SelectWithDesc from '@components/options/SelectWithDesc';
const initialValues = { const initialValues = {
splitSeparatorType: 'symbol' as SplitOperatorType, splitSeparatorType: 'symbol' as SplitOperatorType,
@@ -69,9 +68,6 @@ export default function FindMostPopular() {
) )
); );
}; };
const validationSchema = Yup.object({
// splitSeparator: Yup.string().required('The separator is required')
});
return ( return (
<Box> <Box>
@@ -165,7 +161,6 @@ export default function FindMostPopular() {
]} ]}
initialValues={initialValues} initialValues={initialValues}
input={input} input={input}
validationSchema={validationSchema}
/> />
</Box> </Box>
); );

View File

@@ -5,7 +5,7 @@ import { lazy } from 'react';
export const tool = defineTool('list', { export const tool = defineTool('list', {
name: 'Find most popular', name: 'Find most popular',
path: 'find-most-popular', path: 'find-most-popular',
// image, icon: 'material-symbols-light:query-stats',
description: '', description: '',
shortDescription: '', shortDescription: '',
keywords: ['find', 'most', 'popular'], keywords: ['find', 'most', 'popular'],

View File

@@ -1,4 +1,4 @@
import { expect, describe, it } from 'vitest'; import { describe, expect } from 'vitest';
import { findUniqueCompute } from './service'; import { findUniqueCompute } from './service';

View File

@@ -1,14 +1,13 @@
import { Box } from '@mui/material'; import { Box } from '@mui/material';
import React, { useState } from 'react'; import React, { useState } from 'react';
import ToolTextInput from '../../../components/input/ToolTextInput'; import ToolTextInput from '@components/input/ToolTextInput';
import ToolTextResult from '../../../components/result/ToolTextResult'; import ToolTextResult from '@components/result/ToolTextResult';
import * as Yup from 'yup'; import ToolOptions from '@components/options/ToolOptions';
import ToolOptions from '../../../components/options/ToolOptions';
import { findUniqueCompute, SplitOperatorType } from './service'; import { findUniqueCompute, SplitOperatorType } from './service';
import ToolInputAndResult from '../../../components/ToolInputAndResult'; import ToolInputAndResult from '@components/ToolInputAndResult';
import SimpleRadio from '../../../components/options/SimpleRadio'; import SimpleRadio from '@components/options/SimpleRadio';
import TextFieldWithDesc from '../../../components/options/TextFieldWithDesc'; import TextFieldWithDesc from '@components/options/TextFieldWithDesc';
import CheckboxWithDesc from '../../../components/options/CheckboxWithDesc'; import CheckboxWithDesc from '@components/options/CheckboxWithDesc';
const initialValues = { const initialValues = {
splitOperatorType: 'symbol' as SplitOperatorType, splitOperatorType: 'symbol' as SplitOperatorType,
@@ -63,9 +62,6 @@ export default function FindUnique() {
) )
); );
}; };
const validationSchema = Yup.object({
// splitSeparator: Yup.string().required('The separator is required')
});
return ( return (
<Box> <Box>
@@ -156,7 +152,6 @@ export default function FindUnique() {
]} ]}
initialValues={initialValues} initialValues={initialValues}
input={input} input={input}
validationSchema={validationSchema}
/> />
</Box> </Box>
); );

View File

@@ -5,7 +5,7 @@ import { lazy } from 'react';
export const tool = defineTool('list', { export const tool = defineTool('list', {
name: 'Find unique', name: 'Find unique',
path: 'find-unique', path: 'find-unique',
// image, icon: 'mynaui:one',
description: '', description: '',
shortDescription: '', shortDescription: '',
keywords: ['find', 'unique'], keywords: ['find', 'unique'],

View File

@@ -1,4 +1,4 @@
import { expect, describe, it } from 'vitest'; import { describe, expect, it } from 'vitest';
import { groupList, SplitOperatorType } from './service'; import { groupList, SplitOperatorType } from './service';

Some files were not shown because too many files have changed in this diff Show More