Merge remote-tracking branch 'origin/main' into truncate
# Conflicts: # src/pages/tools/string/index.ts
1
.codebuddy/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
db/
|
39
.codebuddy/summary.md
Normal 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
@@ -0,0 +1,2 @@
|
|||||||
|
github: [iib0011]
|
||||||
|
buy_me_a_coffee: iib0011
|
2
.gitignore
vendored
@@ -38,3 +38,5 @@ yarn-error.log*
|
|||||||
|
|
||||||
/test-results
|
/test-results
|
||||||
/playwright-report
|
/playwright-report
|
||||||
|
|
||||||
|
dist.zip
|
||||||
|
166
.idea/shelf/Uncommitted_changes_before_Checkout_at_2_27_2025_11_44_AM_[Changes]/shelved.patch
generated
Normal file
0
.idea/shelf/Uncommitted_changes_before_Checkout_at_2_27_2025_11_44_AM_[Changes]1/shelved.patch
generated
Normal file
4
.idea/shelf/Uncommitted_changes_before_Checkout_at_2_27_2025_11_44_AM__Changes_.xml
generated
Normal 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
@@ -1 +1 @@
|
|||||||
* @iib0011
|
* @iib0011 @Chesterkxng
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
FROM node:20 as build
|
FROM node:20 AS build
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
|
101
README.md
@@ -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.
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
## 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
Before Width: | Height: | Size: 97 KiB After Width: | Height: | Size: 140 KiB |
21
index.html
@@ -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
@@ -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",
|
||||||
|
@@ -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",
|
||||||
|
1
public/assets/background.svg
Normal 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 |
17
public/assets/fonts/plus-jakarta/plus-jakarta.css
Normal 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");
|
||||||
|
}
|
@@ -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}`);
|
||||||
|
Before Width: | Height: | Size: 4.7 KiB |
Before Width: | Height: | Size: 6.2 KiB |
Before Width: | Height: | Size: 14 KiB |
BIN
src/assets/logo.png
Normal file
After Width: | Height: | Size: 23 KiB |
Before Width: | Height: | Size: 22 KiB |
@@ -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>
|
||||||
|
@@ -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>
|
||||||
|
100
src/components/ToolContent.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
@@ -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>
|
||||||
)}
|
)}
|
||||||
|
@@ -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}
|
||||||
|
@@ -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>
|
||||||
))}
|
))}
|
||||||
|
@@ -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>
|
||||||
|
@@ -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>
|
||||||
|
19
src/components/examples/ExampleOptions.tsx
Normal 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
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
@@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
@@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
68
src/components/examples/ToolExamples.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
@@ -5,3 +5,7 @@ a {
|
|||||||
a:hover {
|
a:hover {
|
||||||
color: #030362;
|
color: #030362;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
* {
|
||||||
|
font-family: Plus Jakarta Sans, sans-serif;
|
||||||
|
}
|
||||||
|
@@ -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 ? (
|
||||||
|
@@ -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
|
||||||
|
@@ -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>
|
||||||
|
@@ -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}
|
||||||
>
|
>
|
||||||
|
@@ -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 && (
|
||||||
|
@@ -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' }}
|
||||||
/>
|
/>
|
||||||
|
@@ -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'
|
||||||
|
];
|
||||||
|
94
src/pages/home/Categories.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
@@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@@ -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");
|
|
||||||
});
|
|
||||||
});
|
|
@@ -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);
|
|
||||||
}
|
|
@@ -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'))
|
|
||||||
});
|
|
@@ -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>;
|
|
||||||
}
|
|
@@ -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>;
|
|
||||||
}
|
|
@@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
@@ -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('');
|
|
||||||
});
|
|
||||||
});
|
|
@@ -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');
|
|
||||||
|
|
||||||
}
|
|
@@ -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');
|
|
||||||
});
|
|
||||||
});
|
|
@@ -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');
|
|
||||||
}
|
|
@@ -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');
|
|
||||||
});
|
|
||||||
});
|
|
@@ -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);
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
@@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
@@ -1,6 +0,0 @@
|
|||||||
export function randomizeCase(input: string): string {
|
|
||||||
return input
|
|
||||||
.split('')
|
|
||||||
.map(char => (Math.random() < 0.5 ? char.toLowerCase() : char.toUpperCase()))
|
|
||||||
.join('');
|
|
||||||
}
|
|
@@ -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');
|
|
||||||
});
|
|
||||||
});
|
|
@@ -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');
|
|
||||||
}
|
|
@@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
@@ -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');
|
|
||||||
});
|
|
||||||
});
|
|
@@ -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>
|
||||||
|
@@ -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 }) => {
|
@@ -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>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
@@ -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',
|
Before Width: | Height: | Size: 3.6 KiB After Width: | Height: | Size: 3.6 KiB |
113
src/pages/tools/image/png/compress-png/index.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
14
src/pages/tools/image/png/compress-png/meta.ts
Normal 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'))
|
||||||
|
});
|
0
src/pages/tools/image/png/compress-png/service.ts
Normal file
@@ -148,7 +148,6 @@ export default function ConvertJgpToPng() {
|
|||||||
]}
|
]}
|
||||||
initialValues={initialValues}
|
initialValues={initialValues}
|
||||||
input={input}
|
input={input}
|
||||||
validationSchema={validationSchema}
|
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
@@ -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',
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |
@@ -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>
|
||||||
);
|
);
|
@@ -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.",
|
Before Width: | Height: | Size: 3.6 KiB After Width: | Height: | Size: 3.6 KiB |
@@ -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
|
||||||
];
|
];
|
3
src/pages/tools/json/index.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
import { tool as jsonPrettify } from './prettify/meta';
|
||||||
|
|
||||||
|
export const jsonTools = [jsonPrettify];
|
190
src/pages/tools/json/prettify/index.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
13
src/pages/tools/json/prettify/meta.ts
Normal 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'))
|
||||||
|
});
|
16
src/pages/tools/json/prettify/service.ts
Normal 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);
|
||||||
|
};
|
66
src/pages/tools/list/duplicate/duplicate.service.test.ts
Normal 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"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
@@ -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>;
|
||||||
}
|
}
|
@@ -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'))
|
||||||
});
|
});
|
81
src/pages/tools/list/duplicate/service.ts
Normal 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);
|
||||||
|
}
|
@@ -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', () => {
|
@@ -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>
|
||||||
);
|
);
|
@@ -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'],
|
@@ -1,4 +1,4 @@
|
|||||||
import { expect, describe, it } from 'vitest';
|
import { describe, expect } from 'vitest';
|
||||||
|
|
||||||
import { findUniqueCompute } from './service';
|
import { findUniqueCompute } from './service';
|
||||||
|
|
@@ -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>
|
||||||
);
|
);
|
@@ -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'],
|
@@ -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';
|
||||||
|
|