diff --git a/CODEOWNERS b/CODEOWNERS
index 798553f..6718f93 100644
--- a/CODEOWNERS
+++ b/CODEOWNERS
@@ -1 +1 @@
-* @iib0011
+* @iib0011 @Chesterkxng
diff --git a/Dockerfile b/Dockerfile
index 41e429b..6cb54d9 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,4 +1,4 @@
-FROM node:20 as build
+FROM node:20 AS build
WORKDIR /app
diff --git a/README.md b/README.md
index eb70e6a..7ca174e 100644
--- a/README.md
+++ b/README.md
@@ -1,45 +1,82 @@
-# OmniTools
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-Welcome to **OmniTools**, an open-source alternative to OnlineTools.com.
-This project offers a variety of online tools to help with everyday tasks,
-all available for free and open for community contributions. Please don't forget to star the repo to support us.
-Here is the [live](https://omnitools.netlify.app/) website.
+Welcome to OmniTools, a self-hosted web app offering a variety of online tools to simplify everyday tasks.
+Whether you are coding, manipulating images or crunching numbers, OmniTools has you covered. Please don't forget to
+star the repo to support us.
+Here is the [demo](https://omnitools.netlify.app/) website.

## Table of Contents
- [Features](#features)
-- [Self-host](#self-host)
+- [Self-host](#self-hostrun)
- [Contribute](#contribute)
- [License](#license)
- [Contact](#contact)
## 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
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
+This is a React Project with Typescript Material UI.
+
### Project setup
```bash
@@ -59,7 +98,7 @@ npm run dev
### Create a new tool
```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
@@ -76,20 +115,30 @@ npm run test
npm run test:e2e
```
+
+
+## 🤝 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
-[//]: # ( )
+## Contact
+
+For any questions or suggestions, feel free to open an issue or contact me at:
+[ibracool99@gmail.com](mailto:ibracool99@gmail.com)
## License
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
diff --git a/img.png b/img.png
index e29bb36..252a3d5 100644
Binary files a/img.png and b/img.png differ
diff --git a/index.html b/index.html
index e830244..70ea518 100644
--- a/index.html
+++ b/index.html
@@ -1,13 +1,14 @@
-
-
-
-
- Omni Tools
-
-
-
-
-
+
+
+
+
+
+ Omni Tools
+
+
+
+
+
diff --git a/package-lock.json b/package-lock.json
index 1247a0e..c1b655f 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -16,6 +16,7 @@
"@types/lodash": "^4.17.5",
"@types/morsee": "^1.0.2",
"@types/omggif": "^1.0.5",
+ "browser-image-compression": "^2.0.2",
"color": "^4.2.3",
"formik": "^2.4.6",
"jimp": "^0.22.12",
@@ -28,11 +29,13 @@
"react-dom": "^18.3.1",
"react-helmet": "^6.1.0",
"react-router-dom": "^6.23.1",
+ "type-fest": "^4.35.0",
"yup": "^1.4.0"
},
"devDependencies": {
"@commitlint/cli": "^19.3.0",
"@commitlint/config-conventional": "^19.2.2",
+ "@iconify/react": "^5.2.0",
"@testing-library/jest-dom": "^6.4.5",
"@testing-library/react": "^14.3.1",
"@types/color": "^3.0.6",
@@ -1454,6 +1457,29 @@
"deprecated": "Use @eslint/object-schema instead",
"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": {
"version": "8.0.2",
"resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz",
@@ -3843,6 +3869,15 @@
"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": {
"version": "4.23.1",
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.1.tgz",
@@ -5728,6 +5763,19 @@
"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": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz",
@@ -9351,12 +9399,12 @@
}
},
"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,
+ "version": "4.35.0",
+ "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.35.0.tgz",
+ "integrity": "sha512-2/AwEFQDFEy30iOLjrvHDIH7e4HEWH+f1Yl1bI5XMqzuoCUqwYCdxachgsgv0og/JdVZUhbfjcJAoHj5L1753A==",
+ "license": "(MIT OR CC0-1.0)",
"engines": {
- "node": ">=10"
+ "node": ">=16"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
@@ -9539,6 +9587,12 @@
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
"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": {
"version": "5.3.1",
"resolved": "https://registry.npmjs.org/vite/-/vite-5.3.1.tgz",
diff --git a/package.json b/package.json
index 05f56b7..b9e8a99 100644
--- a/package.json
+++ b/package.json
@@ -33,6 +33,7 @@
"@types/lodash": "^4.17.5",
"@types/morsee": "^1.0.2",
"@types/omggif": "^1.0.5",
+ "browser-image-compression": "^2.0.2",
"color": "^4.2.3",
"formik": "^2.4.6",
"jimp": "^0.22.12",
@@ -45,11 +46,13 @@
"react-dom": "^18.3.1",
"react-helmet": "^6.1.0",
"react-router-dom": "^6.23.1",
+ "type-fest": "^4.35.0",
"yup": "^1.4.0"
},
"devDependencies": {
"@commitlint/cli": "^19.3.0",
"@commitlint/config-conventional": "^19.2.2",
+ "@iconify/react": "^5.2.0",
"@testing-library/jest-dom": "^6.4.5",
"@testing-library/react": "^14.3.1",
"@types/color": "^3.0.6",
diff --git a/public/assets/background.svg b/public/assets/background.svg
new file mode 100644
index 0000000..651dc67
--- /dev/null
+++ b/public/assets/background.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/public/assets/fonts/plus-jakarta/PlusJakartaSans-Italic-VariableFont_wght.ttf b/public/assets/fonts/plus-jakarta/PlusJakartaSans-Italic-VariableFont_wght.ttf
new file mode 100644
index 0000000..7de3b5b
Binary files /dev/null and b/public/assets/fonts/plus-jakarta/PlusJakartaSans-Italic-VariableFont_wght.ttf differ
diff --git a/public/assets/fonts/plus-jakarta/PlusJakartaSans-VariableFont_wght.ttf b/public/assets/fonts/plus-jakarta/PlusJakartaSans-VariableFont_wght.ttf
new file mode 100644
index 0000000..bdd4985
Binary files /dev/null and b/public/assets/fonts/plus-jakarta/PlusJakartaSans-VariableFont_wght.ttf differ
diff --git a/public/assets/fonts/plus-jakarta/plus-jakarta.css b/public/assets/fonts/plus-jakarta/plus-jakarta.css
new file mode 100644
index 0000000..19af058
--- /dev/null
+++ b/public/assets/fonts/plus-jakarta/plus-jakarta.css
@@ -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");
+}
diff --git a/scripts/create-tool.mjs b/scripts/create-tool.mjs
index e57693a..03628db 100644
--- a/scripts/create-tool.mjs
+++ b/scripts/create-tool.mjs
@@ -1,59 +1,75 @@
-import { readFile, writeFile } from 'fs/promises'
-import fs from 'fs'
-import { dirname, join, sep } from 'path'
-import { fileURLToPath } from 'url'
+import { readFile, writeFile } from 'fs/promises';
+import fs from 'fs';
+import { dirname, join, sep } from 'path';
+import { fileURLToPath } from 'url';
-const currentDirname = dirname(fileURLToPath(import.meta.url))
+const currentDirname = dirname(fileURLToPath(import.meta.url));
-const toolName = process.argv[2]
-const folder = process.argv[3]
+const toolName = process.argv[2];
+const folder = process.argv[3];
-const toolsDir = join(currentDirname, '..', 'src', 'pages', folder ?? '')
+const toolsDir = join(
+ currentDirname,
+ '..',
+ 'src',
+ 'pages',
+ 'tools',
+ folder ?? ''
+);
if (!toolName) {
- throw new Error('Please specify a toolname.')
+ throw new Error('Please specify a toolname.');
}
function capitalizeFirstLetter(string) {
- return string.charAt(0).toUpperCase() + string.slice(1)
+ return string.charAt(0).toUpperCase() + string.slice(1);
}
function createFolderStructure(basePath, foldersToCreateIndexCount) {
- const folderArray = basePath.split(sep)
+ const folderArray = basePath.split(sep);
function recursiveCreate(currentBase, index) {
if (index >= folderArray.length) {
- return
+ return;
}
- const currentPath = join(currentBase, folderArray[index])
+ const currentPath = join(currentBase, folderArray[index]);
if (!fs.existsSync(currentPath)) {
- fs.mkdirSync(currentPath, { recursive: true })
+ fs.mkdirSync(currentPath, { recursive: true });
}
- const indexPath = join(currentPath, 'index.ts')
- if (!fs.existsSync(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}`)
+ const indexPath = join(currentPath, 'index.ts');
+ if (
+ !fs.existsSync(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
- recursiveCreate(currentPath, index + 1)
+ recursiveCreate(currentPath, index + 1);
}
// 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 =
- toolName[0].toUpperCase() + toolName.slice(1).replace(/-/g, ' ')
-const toolDir = join(toolsDir, toolName)
-const type = folder.split(sep)[folder.split(sep).length - 1]
-await createFolderStructure(toolDir, folder.split(sep).length)
-console.log(`Directory created: ${toolDir}`)
+ toolName[0].toUpperCase() + toolName.slice(1).replace(/-/g, ' ');
+const toolDir = join(toolsDir, toolName);
+const type = folder.split(sep)[folder.split(sep).length - 1];
+await createFolderStructure(toolDir, folder.split(sep).length);
+console.log(`Directory created: ${toolDir}`);
const createToolFile = async (name, content) => {
- const filePath = join(toolDir, name)
- await writeFile(filePath, content.trim())
- console.log(`File created: ${filePath}`)
-}
+ const filePath = join(toolDir, name);
+ await writeFile(filePath, content.trim());
+ console.log(`File created: ${filePath}`);
+};
createToolFile(
`index.tsx`,
@@ -62,7 +78,8 @@ import { Box } from '@mui/material';
import React from 'react';
import * as Yup from 'yup';
-const initialValues = {};
+type InitialValuesType = {};
+const initialValues: InitialValuesType = {};
const validationSchema = Yup.object({
// splitSeparator: Yup.string().required('The separator is required')
});
@@ -70,27 +87,26 @@ export default function ${capitalizeFirstLetter(toolNameCamelCase)}() {
return Lorem ipsum ;
}
`
-)
+);
createToolFile(
`meta.ts`,
`
import { defineTool } from '@tools/defineTool';
import { lazy } from 'react';
-// import image from '@assets/text.png';
export const tool = defineTool('${type}', {
name: '${toolNameTitleCase}',
path: '${toolName}',
- // image,
+ icon: '',
description: '',
shortDescription: '',
- keywords: ['${toolName.split('-').join('\', \'')}'],
+ keywords: ['${toolName.split('-').join("', '")}'],
component: lazy(() => import('./index'))
});
`
-)
+);
-createToolFile(`service.ts`, ``)
+createToolFile(`service.ts`, ``);
createToolFile(
`${toolName}.service.test.ts`,
`
@@ -101,7 +117,7 @@ import { expect, describe, it } from 'vitest';
//
// })
`
-)
+);
// createToolFile(
// `${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(
(r) => r.split('\n')
-)
+);
indexContent.splice(
0,
0,
- `import { tool as ${type}${capitalizeFirstLetter(toolNameCamelCase)} } from './${toolName}/meta';`
-)
-writeFile(toolsIndex, indexContent.join('\n'))
-console.log(`Added import in: ${toolsIndex}`)
+ `import { tool as ${type}${capitalizeFirstLetter(
+ toolNameCamelCase
+ )} } from './${toolName}/meta';`
+);
+writeFile(toolsIndex, indexContent.join('\n'));
+console.log(`Added import in: ${toolsIndex}`);
diff --git a/src/assets/github-mark-white.png b/src/assets/github-mark-white.png
deleted file mode 100644
index 50b8175..0000000
Binary files a/src/assets/github-mark-white.png and /dev/null differ
diff --git a/src/assets/github-mark.png b/src/assets/github-mark.png
deleted file mode 100644
index 6cb3b70..0000000
Binary files a/src/assets/github-mark.png and /dev/null differ
diff --git a/src/assets/image.png b/src/assets/image.png
deleted file mode 100644
index 5e01b1e..0000000
Binary files a/src/assets/image.png and /dev/null differ
diff --git a/src/assets/logo.png b/src/assets/logo.png
new file mode 100644
index 0000000..559d193
Binary files /dev/null and b/src/assets/logo.png differ
diff --git a/src/assets/tools.png b/src/assets/tools.png
deleted file mode 100644
index 3dc90a5..0000000
Binary files a/src/assets/tools.png and /dev/null differ
diff --git a/src/components/Hero.tsx b/src/components/Hero.tsx
index 72545f6..d5ff305 100644
--- a/src/components/Hero.tsx
+++ b/src/components/Hero.tsx
@@ -7,20 +7,21 @@ import { DefinedTool } from '@tools/defineTool';
import { filterTools, tools } from '@tools/index';
import { useNavigate } from 'react-router-dom';
import _ from 'lodash';
+import { Icon } from '@iconify/react';
const exampleTools: { label: string; url: string }[] = [
{
label: 'Create a transparent image',
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: 'Pick a random item', url: '' },
- { label: 'Find and replace text', url: '' },
- { label: 'Convert emoji to image', url: '' },
- { label: 'Split a string', url: '/string/split' },
+ { label: 'Sort a list', url: '/list/sort' },
+ { label: 'Compress PNG', url: '/png/compress-png' },
+ { label: 'Split a text', url: '/string/split' },
{ 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() {
const [inputValue, setInputValue] = useState('');
@@ -35,11 +36,12 @@ export default function Hero() {
setInputValue(newInputValue);
setFilteredTools(_.shuffle(filterTools(tools, newInputValue)));
};
+
return (
- Transform Your Workflow with{' '}
+ Get Things Done Quickly with{' '}
+ endAdornment: ,
+ sx: {
+ borderRadius: 4,
+ backgroundColor: 'white'
+ }
}}
onChange={(event) => handleInputChange(event, event.target.value)}
/>
@@ -85,17 +90,27 @@ export default function Hero() {
{...props}
onClick={() => navigate('/' + option.path)}
>
-
- {option.name}
- {option.shortDescription}
-
+
+
+
+ {option.name}
+ {option.shortDescription}
+
+
)}
+ onChange={(event, newValue) => {
+ if (newValue) {
+ navigate('/' + newValue.path);
+ }
+ }}
/>
{exampleTools.map((tool) => (
navigate(tool.url)}
+ onClick={() =>
+ navigate(tool.url.startsWith('/') ? tool.url : `/${tool.url}`)
+ }
item
xs={12}
md={6}
@@ -112,7 +127,9 @@ export default function Hero() {
borderRadius: 3,
borderColor: 'grey',
borderStyle: 'solid',
- cursor: 'pointer'
+ backgroundColor: 'white',
+ cursor: 'pointer',
+ '&:hover': { backgroundColor: '#FAFAFD' }
}}
>
{tool.label}
diff --git a/src/components/Navbar/index.tsx b/src/components/Navbar/index.tsx
index 14abec3..83285a9 100644
--- a/src/components/Navbar/index.tsx
+++ b/src/components/Navbar/index.tsx
@@ -1,73 +1,114 @@
-import React, { useState } from 'react';
+import React, { ReactNode, useState } from 'react';
import AppBar from '@mui/material/AppBar';
import Toolbar from '@mui/material/Toolbar';
-import Typography from '@mui/material/Typography';
import Button from '@mui/material/Button';
import IconButton from '@mui/material/IconButton';
import MenuIcon from '@mui/icons-material/Menu';
import { Link, useNavigate } from 'react-router-dom';
-import githubIcon from '@assets/github-mark.png'; // Adjust the path to your GitHub icon
+import logo from 'assets/logo.png';
import {
Drawer,
List,
+ ListItem,
ListItemButton,
ListItemText,
Stack
} from '@mui/material';
import useMediaQuery from '@mui/material/useMediaQuery';
import { useTheme } from '@mui/material/styles';
+import { Icon } from '@iconify/react';
const Navbar: React.FC = () => {
const navigate = useNavigate();
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down('md'));
const [drawerOpen, setDrawerOpen] = useState(false);
-
const toggleDrawer = (open: boolean) => () => {
setDrawerOpen(open);
};
-
+ const navItems: { label: string; path: string }[] = [
+ // { label: 'Features', path: '/features' }
+ // { label: 'About Us', path: '/about-us' }
+ ];
+ const buttons: ReactNode[] = [
+ window.open('https://discord.gg/SDbbn3hT4b', '_blank')}
+ style={{ cursor: 'pointer' }}
+ fontSize={30}
+ icon={'ic:baseline-discord'}
+ />,
+ ,
+ {
+ window.open('https://buymeacoffee.com/iib0011', '_blank');
+ }}
+ sx={{ borderRadius: '100px' }}
+ variant={'contained'}
+ startIcon={
+
+ }
+ >
+ Buy me a coffee
+
+ ];
const drawerList = (
- navigate('/features')}>
-
-
- navigate('/about-us')}>
-
-
-
-
- Star us
-
+ {navItems.map((navItem) => (
+ navigate(navItem.path)}
+ >
+
+
+ ))}
+ {buttons.map((button) => (
+ {button}
+ ))}
);
return (
-
-
+ navigate('/')}
- fontSize={20}
- sx={{ cursor: 'pointer' }}
- color={'primary'}
- >
- OmniTools
-
+ style={{ cursor: 'pointer' }}
+ src={logo}
+ width={isMobile ? '80px' : '150px'}
+ />
{isMobile ? (
<>
-
+
{
>
) : (
-
-
-
+ {navItems.map((item) => (
+
- Features
-
-
-
-
- About Us
-
-
-
-
- Star us
-
+
+ {item.label}
+
+
+ ))}
+ {buttons}
)}
diff --git a/src/components/ToolContent.tsx b/src/components/ToolContent.tsx
new file mode 100644
index 0000000..4b45a9a
--- /dev/null
+++ b/src/components/ToolContent.tsx
@@ -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 extends ToolComponentProps {
+ // Input/Output components
+ inputComponent: ReactNode;
+ resultComponent: ReactNode;
+
+ // Tool options
+ initialValues: T;
+ getGroups: GetGroupsType;
+
+ // 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
+ extends ToolContentPropsBase {
+ exampleCards: CardExampleType[];
+ setInput: React.Dispatch>;
+}
+
+interface ToolContentPropsWithoutExamples
+ extends ToolContentPropsBase {
+ exampleCards?: never;
+ setInput?: never;
+}
+
+type ToolContentProps =
+ | ToolContentPropsWithExamples
+ | ToolContentPropsWithoutExamples;
+
+export default function ToolContent({
+ title,
+ inputComponent,
+ resultComponent,
+ initialValues,
+ getGroups,
+ compute,
+ toolInfo,
+ exampleCards,
+ input,
+ setInput,
+ validationSchema
+}: ToolContentProps) {
+ const formRef = useRef>(null);
+
+ return (
+
+
+
+
+
+ {toolInfo && (
+
+ )}
+
+ {exampleCards && exampleCards.length > 0 && (
+ <>
+
+
+ >
+ )}
+
+ );
+}
diff --git a/src/components/ToolHeader.tsx b/src/components/ToolHeader.tsx
index c6d529d..84b0236 100644
--- a/src/components/ToolHeader.tsx
+++ b/src/components/ToolHeader.tsx
@@ -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 ToolBreadcrumb from './ToolBreadcrumb';
import { capitalizeFirstLetter } from '../utils/string';
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 {
title: string;
description: string;
- image?: string;
+ icon?: IconifyIcon | string;
type: string;
}
function ToolLinks() {
+ const theme = useTheme();
+
return (
-
-
+
+
Use This Tool
-
+
-
-
+
+
See Examples
-
-
-
-
- Learn How to Use
-
+
+ {/**/}
+ {/* */}
+ {/* Learn How to Use*/}
+ {/* */}
+ {/* */}
);
}
export default function ToolHeader({
- image,
+ icon,
title,
description,
type
@@ -60,10 +77,18 @@ export default function ToolHeader({
- {image && (
+ {icon && (
-
+
)}
diff --git a/src/components/ToolLayout.tsx b/src/components/ToolLayout.tsx
index 4737ea1..90569b0 100644
--- a/src/components/ToolLayout.tsx
+++ b/src/components/ToolLayout.tsx
@@ -6,17 +6,18 @@ import Separator from './Separator';
import AllTools from './allTools/AllTools';
import { getToolsByCategory } from '@tools/index';
import { capitalizeFirstLetter } from '../utils/string';
+import { IconifyIcon } from '@iconify/react';
export default function ToolLayout({
children,
title,
description,
- image,
+ icon,
type
}: {
title: string;
description: string;
- image?: string;
+ icon?: IconifyIcon | string;
type: string;
children: ReactNode;
}) {
@@ -27,7 +28,8 @@ export default function ToolLayout({
.map((tool) => ({
title: tool.name,
description: tool.shortDescription,
- link: '/' + tool.path
+ link: '/' + tool.path,
+ icon: tool.icon
})) ?? [];
return (
@@ -36,6 +38,7 @@ export default function ToolLayout({
display={'flex'}
flexDirection={'column'}
alignItems={'center'}
+ sx={{ backgroundColor: '#F5F5FA' }}
>
{`${title} - Omni Tools`}
@@ -44,7 +47,7 @@ export default function ToolLayout({
{children}
diff --git a/src/components/allTools/AllTools.tsx b/src/components/allTools/AllTools.tsx
index b3c701f..06d663b 100644
--- a/src/components/allTools/AllTools.tsx
+++ b/src/components/allTools/AllTools.tsx
@@ -1,10 +1,12 @@
import { Box, Grid, Stack, Typography } from '@mui/material';
import ToolCard from './ToolCard';
+import { IconifyIcon } from '@iconify/react';
export interface ToolCardProps {
title: string;
description: string;
link: string;
+ icon: IconifyIcon | string;
}
interface AllToolsProps {
@@ -26,6 +28,7 @@ export default function AllTools({ title, toolCards }: AllToolsProps) {
title={card.title}
description={card.description}
link={card.link}
+ icon={card.icon}
/>
))}
diff --git a/src/components/allTools/ToolCard.tsx b/src/components/allTools/ToolCard.tsx
index 763a5e0..0767e18 100644
--- a/src/components/allTools/ToolCard.tsx
+++ b/src/components/allTools/ToolCard.tsx
@@ -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 ChevronRightIcon from '@mui/icons-material/ChevronRight';
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();
return (
-
- {title}
-
+
+
+
+ {title}
+
+
diff --git a/src/components/examples/ExampleCard.tsx b/src/components/examples/ExampleCard.tsx
index 32126bc..cada618 100644
--- a/src/components/examples/ExampleCard.tsx
+++ b/src/components/examples/ExampleCard.tsx
@@ -1,4 +1,3 @@
-import { ExampleCardProps } from './Examples';
import {
Box,
Card,
@@ -9,26 +8,42 @@ import {
useTheme
} from '@mui/material';
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 {
+ title: string;
+ description: string;
+ sampleText: string;
+ sampleResult: string;
+ sampleOptions: T;
+ changeInputResult: (newInput: string, newOptions: T) => void;
+ getGroups: GetGroupsType;
+}
+
+export default function ExampleCard({
title,
description,
sampleText,
sampleResult,
- requiredOptions,
- changeInputResult
-}: ExampleCardProps) {
+ sampleOptions,
+ changeInputResult,
+ getGroups
+}: ExampleCardProps) {
const theme = useTheme();
return (
{
+ changeInputResult(sampleText, sampleOptions);
+ }}
sx={{
bgcolor: theme.palette.background.default,
height: '100%',
overflow: 'hidden',
borderRadius: 2,
transition: 'background-color 0.3s ease',
+ cursor: 'pointer',
'&:hover': {
boxShadow: '12px 9px 11px 2px #b8b9be, -6px -6px 12px #fff'
}
@@ -46,7 +61,6 @@ export default function ExampleCard({
changeInputResult(sampleText, sampleResult)}
sx={{
display: 'flex',
zIndex: '2',
@@ -55,7 +69,6 @@ export default function ExampleCard({
bgcolor: 'transparent',
padding: '5px 10px',
borderRadius: '5px',
- cursor: 'pointer',
boxShadow: 'inset 2px 2px 5px #b8b9be, inset -3px -3px 7px #fff;'
}}
>
@@ -77,7 +90,6 @@ export default function ExampleCard({
changeInputResult(sampleText, sampleResult)}
sx={{
display: 'flex',
zIndex: '2',
@@ -106,7 +118,7 @@ export default function ExampleCard({
/>
-
+
diff --git a/src/components/examples/ExampleOptions.tsx b/src/components/examples/ExampleOptions.tsx
new file mode 100644
index 0000000..cac9c94
--- /dev/null
+++ b/src/components/examples/ExampleOptions.tsx
@@ -0,0 +1,19 @@
+import ToolOptionGroups from '@components/options/ToolOptionGroups';
+import { GetGroupsType } from '@components/options/ToolOptions';
+import React from 'react';
+
+export default function ExampleOptions({
+ options,
+ getGroups
+}: {
+ options: T;
+ getGroups: GetGroupsType;
+}) {
+ return (
+
+ );
+}
diff --git a/src/components/examples/Examples.tsx b/src/components/examples/Examples.tsx
deleted file mode 100644
index d534b2e..0000000
--- a/src/components/examples/Examples.tsx
+++ /dev/null
@@ -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 (
-
-
-
- {title}
-
-
- {subtitle}
-
-
-
-
-
- {exampleCards.map((card, index) => (
-
-
-
- ))}
-
-
-
- );
-}
diff --git a/src/components/examples/RequiredOptions.tsx b/src/components/examples/RequiredOptions.tsx
deleted file mode 100644
index d79a325..0000000
--- a/src/components/examples/RequiredOptions.tsx
+++ /dev/null
@@ -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 (
-
-
- Required options
-
-
- These options will be used automatically if you select this example.
-
-
-
-
-
-
- {deleteBlankLines ? (
-
- {}}
- description="Delete lines that don't have text symbols."
- />
-
- ) : (
- ''
- )}
- {deleteTrailingSpaces ? (
-
- {}}
- description="Remove spaces and tabs at the end of the lines."
- />
-
- ) : (
- ''
- )}
-
- );
-}
diff --git a/src/components/examples/ToolExamples.tsx b/src/components/examples/ToolExamples.tsx
new file mode 100644
index 0000000..8bd236c
--- /dev/null
+++ b/src/components/examples/ToolExamples.tsx
@@ -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 = Omit<
+ ExampleCardProps,
+ 'getGroups' | 'changeInputResult'
+>;
+
+export interface ExampleProps {
+ title: string;
+ subtitle?: string;
+ exampleCards: CardExampleType[];
+ getGroups: GetGroupsType;
+ formRef: React.RefObject>;
+ setInput: React.Dispatch>;
+}
+
+export default function ToolExamples({
+ title,
+ subtitle,
+ exampleCards,
+ getGroups,
+ formRef,
+ setInput
+}: ExampleProps) {
+ function changeInputResult(newInput: string, newOptions: T) {
+ setInput(newInput);
+ formRef.current?.setValues(newOptions);
+ const toolsElement = document.getElementById('tool');
+ if (toolsElement) {
+ toolsElement.scrollIntoView({ behavior: 'smooth' });
+ }
+ }
+
+ return (
+
+
+
+ {`${title} Examples`}
+
+
+ {subtitle ?? 'Click to try!'}
+
+
+
+
+
+ {exampleCards.map((card, index) => (
+
+
+
+ ))}
+
+
+
+ );
+}
diff --git a/src/components/index.css b/src/components/index.css
index 8844a3f..53b3bbe 100644
--- a/src/components/index.css
+++ b/src/components/index.css
@@ -5,3 +5,7 @@ a {
a:hover {
color: #030362;
}
+
+* {
+ font-family: Plus Jakarta Sans, sans-serif;
+}
diff --git a/src/components/input/ToolFileInput.tsx b/src/components/input/ToolFileInput.tsx
index 67ba26c..e1287d0 100644
--- a/src/components/input/ToolFileInput.tsx
+++ b/src/components/input/ToolFileInput.tsx
@@ -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(() => {
if (value) {
const objectUrl = URL.createObjectURL(value);
@@ -57,6 +65,15 @@ export default function ToolFileInput({
const handleImportClick = () => {
fileInputRef.current?.click();
};
+
+ useEffect(() => {
+ window.addEventListener('paste', handlePaste);
+
+ return () => {
+ window.removeEventListener('paste', handlePaste);
+ };
+ }, [handlePaste]);
+
return (
@@ -66,7 +83,8 @@ export default function ToolFileInput({
height: globalInputHeight,
border: preview ? 0 : 1,
borderRadius: 2,
- boxShadow: '5'
+ boxShadow: '5',
+ bgcolor: 'white'
}}
>
{preview ? (
diff --git a/src/components/input/ToolTextInput.tsx b/src/components/input/ToolTextInput.tsx
index 6222db6..319c942 100644
--- a/src/components/input/ToolTextInput.tsx
+++ b/src/components/input/ToolTextInput.tsx
@@ -50,7 +50,14 @@ export default function ToolTextInput({
fullWidth
multiline
rows={10}
- inputProps={{ 'data-testid': 'text-input' }}
+ sx={{
+ '&.MuiTextField-root': {
+ backgroundColor: 'white'
+ }
+ }}
+ inputProps={{
+ 'data-testid': 'text-input'
+ }}
/>
{groups.map((group) => (
-
+
{group.title}
diff --git a/src/components/options/ToolOptions.tsx b/src/components/options/ToolOptions.tsx
index 1547492..0061562 100644
--- a/src/components/options/ToolOptions.tsx
+++ b/src/components/options/ToolOptions.tsx
@@ -5,8 +5,9 @@ import React, { ReactNode, RefObject, useContext, useEffect } from 'react';
import { Formik, FormikProps, FormikValues, useFormikContext } from 'formik';
import ToolOptionGroups, { ToolOptionGroup } from './ToolOptionGroups';
import { CustomSnackBarContext } from '../../contexts/CustomSnackBarContext';
+import * as Yup from 'yup';
-type UpdateField = (field: Y, value: T[Y]) => void;
+export type UpdateField = (field: Y, value: T[Y]) => void;
const FormikListenerComponent = ({
initialValues,
@@ -67,6 +68,10 @@ const ToolBody = ({
);
};
+
+export type GetGroupsType = (
+ formikProps: FormikProps & { updateField: UpdateField }
+) => ToolOptionGroup[];
export default function ToolOptions({
children,
initialValues,
@@ -78,12 +83,10 @@ export default function ToolOptions({
}: {
children?: ReactNode;
initialValues: T;
- validationSchema: any | (() => any);
+ validationSchema?: any | (() => any);
compute: (optionsValues: T, input: any) => void;
input?: any;
- getGroups: (
- formikProps: FormikProps & { updateField: UpdateField }
- ) => ToolOptionGroup[];
+ getGroups: GetGroupsType;
formRef?: RefObject>;
}) {
const theme = useTheme();
@@ -93,7 +96,8 @@ export default function ToolOptions({
mb: 2,
borderRadius: 2,
padding: 2,
- backgroundColor: theme.palette.background.default
+ backgroundColor: theme.palette.background.default,
+ boxShadow: '2'
}}
mt={2}
>
diff --git a/src/components/result/ToolFileResult.tsx b/src/components/result/ToolFileResult.tsx
index a76a04f..e9b7ca6 100644
--- a/src/components/result/ToolFileResult.tsx
+++ b/src/components/result/ToolFileResult.tsx
@@ -67,7 +67,8 @@ export default function ToolFileResult({
height: globalInputHeight,
border: preview ? 0 : 1,
borderRadius: 2,
- boxShadow: '5'
+ boxShadow: '5',
+ bgcolor: 'white'
}}
>
{preview && (
diff --git a/src/components/result/ToolTextResult.tsx b/src/components/result/ToolTextResult.tsx
index 9baf4cc..1d85720 100644
--- a/src/components/result/ToolTextResult.tsx
+++ b/src/components/result/ToolTextResult.tsx
@@ -41,6 +41,11 @@ export default function ToolTextResult({
value={replaceSpecialCharacters(value)}
fullWidth
multiline
+ sx={{
+ '&.MuiTextField-root': {
+ backgroundColor: 'white'
+ }
+ }}
rows={10}
inputProps={{ 'data-testid': 'text-result' }}
/>
diff --git a/src/config/uiConfig.ts b/src/config/uiConfig.ts
index a9455c0..b5bc07e 100644
--- a/src/config/uiConfig.ts
+++ b/src/config/uiConfig.ts
@@ -1,2 +1,8 @@
export const globalInputHeight = 300;
export const globalDescriptionFontSize = 12;
+export const categoriesColors: string[] = [
+ '#8FBC5D',
+ '#3CB6E2',
+ '#FFD400',
+ '#AB6993'
+];
diff --git a/src/pages/home/Categories.tsx b/src/pages/home/Categories.tsx
new file mode 100644
index 0000000..afd9664
--- /dev/null
+++ b/src/pages/home/Categories.tsx
@@ -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 (infer ElementType)[] ? ElementType : never;
+
+const SingleCategory = function ({
+ category,
+ index
+}: {
+ category: ArrayElement>;
+ index: number;
+}) {
+ const navigate = useNavigate();
+ const [hovered, setHovered] = useState(false);
+ const toggleHover = () => setHovered((prevState) => !prevState);
+ return (
+
+
+
+
+
+
+
+
+ {category.title}
+
+
+ {category.description}
+
+
+
+ navigate('/categories/' + category.type)}
+ variant={'contained'}
+ >{`See all ${category.title}`}
+
+
+ navigate(category.example.path)}
+ variant={'outlined'}
+ >{`Try ${category.example.title}`}
+
+
+
+
+
+
+ );
+};
+export default function Categories() {
+ return (
+
+ {getToolsByCategory().map((category, index) => (
+
+ ))}
+
+ );
+}
diff --git a/src/pages/home/index.tsx b/src/pages/home/index.tsx
index d9be29e..b54e460 100644
--- a/src/pages/home/index.tsx
+++ b/src/pages/home/index.tsx
@@ -1,17 +1,17 @@
-import { Box, Card, CardContent } from '@mui/material';
-import Grid from '@mui/material/Grid';
-import Typography from '@mui/material/Typography';
-import { Link, useNavigate } from 'react-router-dom';
-import { getToolsByCategory } from '../../tools';
-import Button from '@mui/material/Button';
+import { Box } from '@mui/material';
import Hero from 'components/Hero';
+import Categories from './Categories';
export default function Home() {
- const navigate = useNavigate();
-
return (
-
- {getToolsByCategory().map((category) => (
-
-
-
-
- {category.title}
-
- {category.description}
-
-
- navigate('/categories/' + category.type)}
- variant={'contained'}
- >{`See all ${category.title}`}
-
-
- navigate(category.example.path)}
- variant={'outlined'}
- >{`Try ${category.example.title}`}
-
-
-
-
-
- ))}
-
+
);
}
diff --git a/src/pages/list/duplicate/duplicate.service.test.ts b/src/pages/list/duplicate/duplicate.service.test.ts
deleted file mode 100644
index c1554e6..0000000
--- a/src/pages/list/duplicate/duplicate.service.test.ts
+++ /dev/null
@@ -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");
- });
-});
\ No newline at end of file
diff --git a/src/pages/list/duplicate/service.ts b/src/pages/list/duplicate/service.ts
deleted file mode 100644
index c1c2bbd..0000000
--- a/src/pages/list/duplicate/service.ts
+++ /dev/null
@@ -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);
-}
\ No newline at end of file
diff --git a/src/pages/list/reverse/meta.ts b/src/pages/list/reverse/meta.ts
deleted file mode 100644
index 3500928..0000000
--- a/src/pages/list/reverse/meta.ts
+++ /dev/null
@@ -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'))
-});
diff --git a/src/pages/list/rotate/index.tsx b/src/pages/list/rotate/index.tsx
deleted file mode 100644
index 4f8cbb8..0000000
--- a/src/pages/list/rotate/index.tsx
+++ /dev/null
@@ -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 Lorem ipsum ;
-}
diff --git a/src/pages/list/shuffle/index.tsx b/src/pages/list/shuffle/index.tsx
deleted file mode 100644
index 3e4f97a..0000000
--- a/src/pages/list/shuffle/index.tsx
+++ /dev/null
@@ -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 Lorem ipsum ;
-}
diff --git a/src/pages/number/sum/index.tsx b/src/pages/number/sum/index.tsx
deleted file mode 100644
index e156ccc..0000000
--- a/src/pages/number/sum/index.tsx
+++ /dev/null
@@ -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('');
- const [result, setResult] = useState('');
-
- const validationSchema = Yup.object({
- // splitSeparator: Yup.string().required('The separator is required')
- });
-
- return (
-
- }
- result={ }
- />
- [
- {
- title: 'Number extraction',
- component: extractionTypes.map(
- ({
- title,
- description,
- type,
- withTextField,
- textValueAccessor
- }) =>
- withTextField ? (
- updateField('extractionType', type)}
- onTextChange={(val) =>
- textValueAccessor
- ? updateField(textValueAccessor, val)
- : null
- }
- />
- ) : (
- updateField('extractionType', type)}
- checked={values.extractionType === type}
- description={description}
- title={title}
- />
- )
- )
- },
- {
- title: 'Running Sum',
- component: (
- updateField('printRunningSum', value)}
- />
- )
- }
- ]}
- compute={(optionsValues, input) => {
- const { extractionType, printRunningSum, separator } = optionsValues;
- setResult(compute(input, extractionType, printRunningSum, separator));
- }}
- initialValues={initialValues}
- input={input}
- validationSchema={validationSchema}
- />
-
- );
-}
diff --git a/src/pages/string/create-palindrome/create-palindrome.service.test.ts b/src/pages/string/create-palindrome/create-palindrome.service.test.ts
deleted file mode 100644
index ddd712b..0000000
--- a/src/pages/string/create-palindrome/create-palindrome.service.test.ts
+++ /dev/null
@@ -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('');
- });
-});
\ No newline at end of file
diff --git a/src/pages/string/create-palindrome/service.ts b/src/pages/string/create-palindrome/service.ts
deleted file mode 100644
index 1f87c6d..0000000
--- a/src/pages/string/create-palindrome/service.ts
+++ /dev/null
@@ -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');
-
-}
\ No newline at end of file
diff --git a/src/pages/string/extract-substring/extract-substring.service.test.ts b/src/pages/string/extract-substring/extract-substring.service.test.ts
deleted file mode 100644
index ecacc55..0000000
--- a/src/pages/string/extract-substring/extract-substring.service.test.ts
+++ /dev/null
@@ -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');
- });
-});
\ No newline at end of file
diff --git a/src/pages/string/extract-substring/service.ts b/src/pages/string/extract-substring/service.ts
deleted file mode 100644
index c06a10f..0000000
--- a/src/pages/string/extract-substring/service.ts
+++ /dev/null
@@ -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');
-}
\ No newline at end of file
diff --git a/src/pages/string/palindrome/palindrome.service.test.ts b/src/pages/string/palindrome/palindrome.service.test.ts
deleted file mode 100644
index 10d5f2e..0000000
--- a/src/pages/string/palindrome/palindrome.service.test.ts
+++ /dev/null
@@ -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');
- });
-});
\ No newline at end of file
diff --git a/src/pages/string/palindrome/service.ts b/src/pages/string/palindrome/service.ts
deleted file mode 100644
index e383fd4..0000000
--- a/src/pages/string/palindrome/service.ts
+++ /dev/null
@@ -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);
-
-}
-
diff --git a/src/pages/string/randomize-case/randomize-case.service.test.ts b/src/pages/string/randomize-case/randomize-case.service.test.ts
deleted file mode 100644
index 7e44a7a..0000000
--- a/src/pages/string/randomize-case/randomize-case.service.test.ts
+++ /dev/null
@@ -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);
- }
- }
- });
-});
\ No newline at end of file
diff --git a/src/pages/string/randomize-case/service.ts b/src/pages/string/randomize-case/service.ts
deleted file mode 100644
index a6f6860..0000000
--- a/src/pages/string/randomize-case/service.ts
+++ /dev/null
@@ -1,6 +0,0 @@
-export function randomizeCase(input: string): string {
- return input
- .split('')
- .map(char => (Math.random() < 0.5 ? char.toLowerCase() : char.toUpperCase()))
- .join('');
-}
\ No newline at end of file
diff --git a/src/pages/string/reverse/reverse.service.test.ts b/src/pages/string/reverse/reverse.service.test.ts
deleted file mode 100644
index 0e6ed45..0000000
--- a/src/pages/string/reverse/reverse.service.test.ts
+++ /dev/null
@@ -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');
- });
-});
\ No newline at end of file
diff --git a/src/pages/string/reverse/service.ts b/src/pages/string/reverse/service.ts
deleted file mode 100644
index c8c68a6..0000000
--- a/src/pages/string/reverse/service.ts
+++ /dev/null
@@ -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');
-}
\ No newline at end of file
diff --git a/src/pages/string/split/index.tsx b/src/pages/string/split/index.tsx
deleted file mode 100644
index 2ffe7f2..0000000
--- a/src/pages/string/split/index.tsx
+++ /dev/null
@@ -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('');
- const [result, setResult] = useState('');
- // const formRef = useRef>(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 (
-
- }
- result={ }
- />
- [
- {
- title: 'Split separator options',
- component: splitOperators.map(({ title, description, type }) => (
- updateField('splitSeparatorType', type)}
- onTextChange={(val) => updateField(`${type}Value`, val)}
- />
- ))
- },
- {
- title: 'Output separator options',
- component: outputOptions.map((option) => (
- updateField(option.accessor, value)}
- description={option.description}
- />
- ))
- }
- ]}
- initialValues={initialValues}
- input={input}
- validationSchema={validationSchema}
- />
-
- );
-}
diff --git a/src/pages/string/uppercase/uppercase.service.test.ts b/src/pages/string/uppercase/uppercase.service.test.ts
deleted file mode 100644
index 6bb3f11..0000000
--- a/src/pages/string/uppercase/uppercase.service.test.ts
+++ /dev/null
@@ -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');
- });
-});
\ No newline at end of file
diff --git a/src/pages/tools-by-category/index.tsx b/src/pages/tools-by-category/index.tsx
index 5687703..d9f1a8c 100644
--- a/src/pages/tools-by-category/index.tsx
+++ b/src/pages/tools-by-category/index.tsx
@@ -5,14 +5,15 @@ import { Link, useNavigate, useParams } from 'react-router-dom';
import { getToolsByCategory } from '../../tools';
import Hero from 'components/Hero';
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() {
const navigate = useNavigate();
const theme = useTheme();
const { categoryName } = useParams();
return (
-
+
{getToolsByCategory()
.find(({ type }) => type === categoryName)
- ?.tools?.map((tool) => (
+ ?.tools?.map((tool, index) => (
navigate('/' + tool.path)}
direction={'row'}
+ alignItems={'center'}
spacing={2}
padding={2}
- border={1}
+ border={`1px solid ${theme.palette.background.default}`}
borderRadius={2}
>
-
+
- {tool.name}
+
+ {tool.name}
+
{tool.shortDescription}
diff --git a/src/pages/image/index.ts b/src/pages/tools/image/index.ts
similarity index 100%
rename from src/pages/image/index.ts
rename to src/pages/tools/image/index.ts
diff --git a/src/pages/image/png/change-colors-in-png/change-colors-in-png.e2e.spec.ts b/src/pages/tools/image/png/change-colors-in-png/change-colors-in-png.e2e.spec.ts
similarity index 95%
rename from src/pages/image/png/change-colors-in-png/change-colors-in-png.e2e.spec.ts
rename to src/pages/tools/image/png/change-colors-in-png/change-colors-in-png.e2e.spec.ts
index 38fa772..dcaebc0 100644
--- a/src/pages/image/png/change-colors-in-png/change-colors-in-png.e2e.spec.ts
+++ b/src/pages/tools/image/png/change-colors-in-png/change-colors-in-png.e2e.spec.ts
@@ -2,7 +2,7 @@ import { expect, test } from '@playwright/test';
import { Buffer } from 'buffer';
import path from 'path';
import Jimp from 'jimp';
-import { convertHexToRGBA } from '../../../../utils/color';
+import { convertHexToRGBA } from '../../../../../utils/color';
test.describe('Change colors in png', () => {
test.beforeEach(async ({ page }) => {
diff --git a/src/pages/image/png/change-colors-in-png/index.tsx b/src/pages/tools/image/png/change-colors-in-png/index.tsx
similarity index 50%
rename from src/pages/image/png/change-colors-in-png/index.tsx
rename to src/pages/tools/image/png/change-colors-in-png/index.tsx
index 83722c3..2dad38b 100644
--- a/src/pages/image/png/change-colors-in-png/index.tsx
+++ b/src/pages/tools/image/png/change-colors-in-png/index.tsx
@@ -1,14 +1,16 @@
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 ColorSelector from '../../../../components/options/ColorSelector';
+import ToolFileInput from '@components/input/ToolFileInput';
+import ToolFileResult from '@components/result/ToolFileResult';
+import ToolOptions, { GetGroupsType } from '@components/options/ToolOptions';
+import ColorSelector from '@components/options/ColorSelector';
import Color from 'color';
import TextFieldWithDesc from 'components/options/TextFieldWithDesc';
-import ToolInputAndResult from '../../../../components/ToolInputAndResult';
+import ToolInputAndResult from '@components/ToolInputAndResult';
import { areColorsSimilar } from 'utils/color';
+import ToolContent from '@components/ToolContent';
+import { ToolComponentProps } from '@tools/defineTool';
const initialValues = {
fromColor: 'white',
@@ -18,7 +20,7 @@ const initialValues = {
const validationSchema = Yup.object({
// splitSeparator: Yup.string().required('The separator is required')
});
-export default function ChangeColorsInPng() {
+export default function ChangeColorsInPng({ title }: ToolComponentProps) {
const [input, setInput] = useState(null);
const [result, setResult] = useState(null);
@@ -83,59 +85,65 @@ export default function ChangeColorsInPng() {
processImage(input, fromRgb, toRgb, Number(similarity));
};
+ const getGroups: GetGroupsType = ({
+ values,
+ updateField
+ }) => [
+ {
+ title: 'From color and to color',
+ component: (
+
+ updateField('fromColor', val)}
+ description={'Replace this color (from color)'}
+ inputProps={{ 'data-testid': 'from-color-input' }}
+ />
+ updateField('toColor', val)}
+ description={'With this color (to color)'}
+ inputProps={{ 'data-testid': 'to-color-input' }}
+ />
+ 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.'
+ }
+ />
+
+ )
+ }
+ ];
return (
-
-
- }
- result={
-
- }
- />
- [
- {
- title: 'From color and to color',
- component: (
-
- updateField('fromColor', val)}
- description={'Replace this color (from color)'}
- inputProps={{ 'data-testid': 'from-color-input' }}
- />
- updateField('toColor', val)}
- description={'With this color (to color)'}
- inputProps={{ 'data-testid': 'to-color-input' }}
- />
- 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.'
- }
- />
-
- )
- }
- ]}
- initialValues={initialValues}
- input={input}
- validationSchema={validationSchema}
- />
-
+
+ }
+ resultComponent={
+
+ }
+ toolInfo={{
+ title: 'Make Colors Transparent',
+ description:
+ '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.'
+ }}
+ />
);
}
diff --git a/src/pages/image/png/change-colors-in-png/meta.ts b/src/pages/tools/image/png/change-colors-in-png/meta.ts
similarity index 93%
rename from src/pages/image/png/change-colors-in-png/meta.ts
rename to src/pages/tools/image/png/change-colors-in-png/meta.ts
index bdf461c..d2e92ef 100644
--- a/src/pages/image/png/change-colors-in-png/meta.ts
+++ b/src/pages/tools/image/png/change-colors-in-png/meta.ts
@@ -1,11 +1,10 @@
import { defineTool } from '@tools/defineTool';
import { lazy } from 'react';
-import image from '@assets/image.png';
export const tool = defineTool('png', {
name: 'Change colors in png',
path: 'change-colors-in-png',
- image,
+ icon: 'cil:color-fill',
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.",
shortDescription: 'Quickly swap colors in a PNG image',
diff --git a/src/pages/image/png/change-colors-in-png/test.png b/src/pages/tools/image/png/change-colors-in-png/test.png
similarity index 100%
rename from src/pages/image/png/change-colors-in-png/test.png
rename to src/pages/tools/image/png/change-colors-in-png/test.png
diff --git a/src/pages/tools/image/png/compress-png/index.tsx b/src/pages/tools/image/png/compress-png/index.tsx
new file mode 100644
index 0000000..1208521
--- /dev/null
+++ b/src/pages/tools/image/png/compress-png/index.tsx
@@ -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(null);
+ const [result, setResult] = useState(null);
+ const [originalSize, setOriginalSize] = useState(null); // Store original file size
+ const [compressedSize, setCompressedSize] = useState(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 (
+
+
+ }
+ result={
+
+ }
+ />
+ [
+ {
+ title: 'Compression options',
+ component: (
+
+ updateField('rate', val)}
+ description={'Compression rate (1-100)'}
+ />
+
+ )
+ },
+ {
+ title: 'File sizes',
+ component: (
+
+
+ {originalSize !== null && (
+
+ Original Size: {(originalSize / 1024).toFixed(2)} KB
+
+ )}
+ {compressedSize !== null && (
+
+ Compressed Size: {(compressedSize / 1024).toFixed(2)} KB
+
+ )}
+
+
+ )
+ }
+ ]}
+ initialValues={initialValues}
+ input={input}
+ />
+
+ );
+}
diff --git a/src/pages/tools/image/png/compress-png/meta.ts b/src/pages/tools/image/png/compress-png/meta.ts
new file mode 100644
index 0000000..1e84c1f
--- /dev/null
+++ b/src/pages/tools/image/png/compress-png/meta.ts
@@ -0,0 +1,14 @@
+import { defineTool } from '@tools/defineTool';
+import { lazy } from 'react';
+// import image from '@assets/text.png';
+
+export const tool = defineTool('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'))
+});
diff --git a/src/pages/tools/image/png/compress-png/service.ts b/src/pages/tools/image/png/compress-png/service.ts
new file mode 100644
index 0000000..e69de29
diff --git a/src/pages/image/png/convert-jgp-to-png/convert-jgp-to-png.e2e.spec.ts b/src/pages/tools/image/png/convert-jgp-to-png/convert-jgp-to-png.e2e.spec.ts
similarity index 100%
rename from src/pages/image/png/convert-jgp-to-png/convert-jgp-to-png.e2e.spec.ts
rename to src/pages/tools/image/png/convert-jgp-to-png/convert-jgp-to-png.e2e.spec.ts
diff --git a/src/pages/image/png/convert-jgp-to-png/index.tsx b/src/pages/tools/image/png/convert-jgp-to-png/index.tsx
similarity index 99%
rename from src/pages/image/png/convert-jgp-to-png/index.tsx
rename to src/pages/tools/image/png/convert-jgp-to-png/index.tsx
index 53eee2e..1d213bf 100644
--- a/src/pages/image/png/convert-jgp-to-png/index.tsx
+++ b/src/pages/tools/image/png/convert-jgp-to-png/index.tsx
@@ -148,7 +148,6 @@ export default function ConvertJgpToPng() {
]}
initialValues={initialValues}
input={input}
- validationSchema={validationSchema}
/>
);
diff --git a/src/pages/image/png/convert-jgp-to-png/meta.ts b/src/pages/tools/image/png/convert-jgp-to-png/meta.ts
similarity index 90%
rename from src/pages/image/png/convert-jgp-to-png/meta.ts
rename to src/pages/tools/image/png/convert-jgp-to-png/meta.ts
index abcb39d..b7c8da7 100644
--- a/src/pages/image/png/convert-jgp-to-png/meta.ts
+++ b/src/pages/tools/image/png/convert-jgp-to-png/meta.ts
@@ -1,11 +1,10 @@
import { defineTool } from '@tools/defineTool';
import { lazy } from 'react';
-import image from '@assets/image.png';
export const tool = defineTool('png', {
name: 'Convert JPG to PNG',
path: 'convert-jgp-to-png',
- image,
+ icon: 'ph:file-jpg-thin',
description:
'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',
diff --git a/src/pages/image/png/convert-jgp-to-png/test.jpg b/src/pages/tools/image/png/convert-jgp-to-png/test.jpg
similarity index 100%
rename from src/pages/image/png/convert-jgp-to-png/test.jpg
rename to src/pages/tools/image/png/convert-jgp-to-png/test.jpg
diff --git a/src/pages/image/png/create-transparent/create-transparent.e2e.spec.ts b/src/pages/tools/image/png/create-transparent/create-transparent.e2e.spec.ts
similarity index 100%
rename from src/pages/image/png/create-transparent/create-transparent.e2e.spec.ts
rename to src/pages/tools/image/png/create-transparent/create-transparent.e2e.spec.ts
diff --git a/src/pages/image/png/create-transparent/index.tsx b/src/pages/tools/image/png/create-transparent/index.tsx
similarity index 89%
rename from src/pages/image/png/create-transparent/index.tsx
rename to src/pages/tools/image/png/create-transparent/index.tsx
index 3892e48..9021c58 100644
--- a/src/pages/image/png/create-transparent/index.tsx
+++ b/src/pages/tools/image/png/create-transparent/index.tsx
@@ -1,13 +1,13 @@
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 ColorSelector from '../../../../components/options/ColorSelector';
+import ToolFileInput from '@components/input/ToolFileInput';
+import ToolFileResult from '@components/result/ToolFileResult';
+import ToolOptions from '@components/options/ToolOptions';
+import ColorSelector from '@components/options/ColorSelector';
import Color from 'color';
import TextFieldWithDesc from 'components/options/TextFieldWithDesc';
-import ToolInputAndResult from '../../../../components/ToolInputAndResult';
+import ToolInputAndResult from '@components/ToolInputAndResult';
import { areColorsSimilar } from 'utils/color';
const initialValues = {
@@ -121,7 +121,6 @@ export default function ChangeColorsInPng() {
]}
initialValues={initialValues}
input={input}
- validationSchema={validationSchema}
/>
);
diff --git a/src/pages/image/png/create-transparent/meta.ts b/src/pages/tools/image/png/create-transparent/meta.ts
similarity index 92%
rename from src/pages/image/png/create-transparent/meta.ts
rename to src/pages/tools/image/png/create-transparent/meta.ts
index af2daf3..3ee1a9a 100644
--- a/src/pages/image/png/create-transparent/meta.ts
+++ b/src/pages/tools/image/png/create-transparent/meta.ts
@@ -1,11 +1,10 @@
import { defineTool } from '@tools/defineTool';
import { lazy } from 'react';
-import image from '@assets/image.png';
export const tool = defineTool('png', {
name: 'Create transparent PNG',
path: 'create-transparent',
- image,
+ icon: 'mdi:circle-transparent',
shortDescription: 'Quickly make a PNG image transparent',
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.",
diff --git a/src/pages/image/png/create-transparent/test.png b/src/pages/tools/image/png/create-transparent/test.png
similarity index 100%
rename from src/pages/image/png/create-transparent/test.png
rename to src/pages/tools/image/png/create-transparent/test.png
diff --git a/src/pages/image/png/index.ts b/src/pages/tools/image/png/index.ts
similarity index 79%
rename from src/pages/image/png/index.ts
rename to src/pages/tools/image/png/index.ts
index 8a1646a..fabac34 100644
--- a/src/pages/image/png/index.ts
+++ b/src/pages/tools/image/png/index.ts
@@ -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 pngCreateTransparent } from './create-transparent/meta';
import { tool as changeColorsInPng } from './change-colors-in-png/meta';
export const pngTools = [
- changeColorsInPng,
+ pngCompressPng,
pngCreateTransparent,
+ changeColorsInPng,
convertJgpToPng
];
diff --git a/src/pages/tools/json/index.ts b/src/pages/tools/json/index.ts
new file mode 100644
index 0000000..48d4333
--- /dev/null
+++ b/src/pages/tools/json/index.ts
@@ -0,0 +1,3 @@
+import { tool as jsonPrettify } from './prettify/meta';
+
+export const jsonTools = [jsonPrettify];
diff --git a/src/pages/tools/json/prettify/index.tsx b/src/pages/tools/json/prettify/index.tsx
new file mode 100644
index 0000000..77dab89
--- /dev/null
+++ b/src/pages/tools/json/prettify/index.tsx
@@ -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[] = [
+ {
+ 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('');
+ const [result, setResult] = useState('');
+ const formRef = useRef>(null);
+ const compute = (optionsValues: InitialValuesType, input: any) => {
+ const { indentationType, spacesCount } = optionsValues;
+ if (input) setResult(beautifyJson(input, indentationType, spacesCount));
+ };
+
+ const getGroups: GetGroupsType = ({
+ values,
+ updateField
+ }) => [
+ {
+ title: 'Indentation',
+ component: (
+
+ updateField('indentationType', 'space')}
+ onTextChange={(val) =>
+ isNumber(val) ? updateField('spacesCount', Number(val)) : null
+ }
+ />
+ updateField('indentationType', 'tab')}
+ checked={values.indentationType === 'tab'}
+ description={'Indent output with tabs.'}
+ title={'Use Tabs'}
+ />
+
+ )
+ }
+ ];
+ return (
+
+
+ }
+ result={ }
+ />
+
+
+
+
+
+ );
+}
diff --git a/src/pages/tools/json/prettify/meta.ts b/src/pages/tools/json/prettify/meta.ts
new file mode 100644
index 0000000..0a97e83
--- /dev/null
+++ b/src/pages/tools/json/prettify/meta.ts
@@ -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'))
+});
diff --git a/src/pages/tools/json/prettify/service.ts b/src/pages/tools/json/prettify/service.ts
new file mode 100644
index 0000000..b1f0281
--- /dev/null
+++ b/src/pages/tools/json/prettify/service.ts
@@ -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);
+};
diff --git a/src/pages/tools/list/duplicate/duplicate.service.test.ts b/src/pages/tools/list/duplicate/duplicate.service.test.ts
new file mode 100644
index 0000000..1968b40
--- /dev/null
+++ b/src/pages/tools/list/duplicate/duplicate.service.test.ts
@@ -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"
+ );
+ });
+});
diff --git a/src/pages/list/duplicate/index.tsx b/src/pages/tools/list/duplicate/index.tsx
similarity index 99%
rename from src/pages/list/duplicate/index.tsx
rename to src/pages/tools/list/duplicate/index.tsx
index c833d45..f5f5aeb 100644
--- a/src/pages/list/duplicate/index.tsx
+++ b/src/pages/tools/list/duplicate/index.tsx
@@ -8,4 +8,4 @@ const validationSchema = Yup.object({
});
export default function Duplicate() {
return Lorem ipsum ;
-}
\ No newline at end of file
+}
diff --git a/src/pages/list/duplicate/meta.ts b/src/pages/tools/list/duplicate/meta.ts
similarity index 95%
rename from src/pages/list/duplicate/meta.ts
rename to src/pages/tools/list/duplicate/meta.ts
index 89f9fe1..a7da466 100644
--- a/src/pages/list/duplicate/meta.ts
+++ b/src/pages/tools/list/duplicate/meta.ts
@@ -5,9 +5,9 @@ import { lazy } from 'react';
export const tool = defineTool('list', {
name: 'Duplicate',
path: 'duplicate',
- // image,
+ icon: '',
description: '',
shortDescription: '',
keywords: ['duplicate'],
component: lazy(() => import('./index'))
-});
\ No newline at end of file
+});
diff --git a/src/pages/tools/list/duplicate/service.ts b/src/pages/tools/list/duplicate/service.ts
new file mode 100644
index 0000000..cc711fc
--- /dev/null
+++ b/src/pages/tools/list/duplicate/service.ts
@@ -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);
+}
diff --git a/src/pages/list/find-most-popular/find-most-popular.service.test.ts b/src/pages/tools/list/find-most-popular/find-most-popular.service.test.ts
similarity index 93%
rename from src/pages/list/find-most-popular/find-most-popular.service.test.ts
rename to src/pages/tools/list/find-most-popular/find-most-popular.service.test.ts
index b5e0248..17101d4 100644
--- a/src/pages/list/find-most-popular/find-most-popular.service.test.ts
+++ b/src/pages/tools/list/find-most-popular/find-most-popular.service.test.ts
@@ -1,10 +1,5 @@
-import { expect, describe, it } from 'vitest';
-import {
- TopItemsList,
- SplitOperatorType,
- SortingMethod,
- DisplayFormat
-} from './service';
+import { describe, expect, it } from 'vitest';
+import { TopItemsList } from './service';
describe('TopItemsList function', () => {
it('should handle sorting alphabetically ignoring case', () => {
diff --git a/src/pages/list/find-most-popular/index.tsx b/src/pages/tools/list/find-most-popular/index.tsx
similarity index 86%
rename from src/pages/list/find-most-popular/index.tsx
rename to src/pages/tools/list/find-most-popular/index.tsx
index 6d5da58..c3a746d 100644
--- a/src/pages/list/find-most-popular/index.tsx
+++ b/src/pages/tools/list/find-most-popular/index.tsx
@@ -1,20 +1,19 @@
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 ToolTextInput from '@components/input/ToolTextInput';
+import ToolTextResult from '@components/result/ToolTextResult';
+import ToolOptions from '@components/options/ToolOptions';
import {
DisplayFormat,
SortingMethod,
SplitOperatorType,
TopItemsList
} from './service';
-import ToolInputAndResult from '../../../components/ToolInputAndResult';
-import SimpleRadio from '../../../components/options/SimpleRadio';
-import TextFieldWithDesc from '../../../components/options/TextFieldWithDesc';
-import CheckboxWithDesc from '../../../components/options/CheckboxWithDesc';
-import SelectWithDesc from '../../../components/options/SelectWithDesc';
+import ToolInputAndResult from '@components/ToolInputAndResult';
+import SimpleRadio from '@components/options/SimpleRadio';
+import TextFieldWithDesc from '@components/options/TextFieldWithDesc';
+import CheckboxWithDesc from '@components/options/CheckboxWithDesc';
+import SelectWithDesc from '@components/options/SelectWithDesc';
const initialValues = {
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 (
@@ -165,7 +161,6 @@ export default function FindMostPopular() {
]}
initialValues={initialValues}
input={input}
- validationSchema={validationSchema}
/>
);
diff --git a/src/pages/list/find-most-popular/meta.ts b/src/pages/tools/list/find-most-popular/meta.ts
similarity index 88%
rename from src/pages/list/find-most-popular/meta.ts
rename to src/pages/tools/list/find-most-popular/meta.ts
index f973acb..4a03c7c 100644
--- a/src/pages/list/find-most-popular/meta.ts
+++ b/src/pages/tools/list/find-most-popular/meta.ts
@@ -5,7 +5,7 @@ import { lazy } from 'react';
export const tool = defineTool('list', {
name: 'Find most popular',
path: 'find-most-popular',
- // image,
+ icon: 'material-symbols-light:query-stats',
description: '',
shortDescription: '',
keywords: ['find', 'most', 'popular'],
diff --git a/src/pages/list/find-most-popular/service.ts b/src/pages/tools/list/find-most-popular/service.ts
similarity index 100%
rename from src/pages/list/find-most-popular/service.ts
rename to src/pages/tools/list/find-most-popular/service.ts
diff --git a/src/pages/list/find-unique/find-unique.service.test.ts b/src/pages/tools/list/find-unique/find-unique.service.test.ts
similarity index 98%
rename from src/pages/list/find-unique/find-unique.service.test.ts
rename to src/pages/tools/list/find-unique/find-unique.service.test.ts
index 769335f..05db2cf 100644
--- a/src/pages/list/find-unique/find-unique.service.test.ts
+++ b/src/pages/tools/list/find-unique/find-unique.service.test.ts
@@ -1,4 +1,4 @@
-import { expect, describe, it } from 'vitest';
+import { describe, expect } from 'vitest';
import { findUniqueCompute } from './service';
diff --git a/src/pages/list/find-unique/index.tsx b/src/pages/tools/list/find-unique/index.tsx
similarity index 86%
rename from src/pages/list/find-unique/index.tsx
rename to src/pages/tools/list/find-unique/index.tsx
index 5d5305e..d18259d 100644
--- a/src/pages/list/find-unique/index.tsx
+++ b/src/pages/tools/list/find-unique/index.tsx
@@ -1,14 +1,13 @@
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 ToolTextInput from '@components/input/ToolTextInput';
+import ToolTextResult from '@components/result/ToolTextResult';
+import ToolOptions from '@components/options/ToolOptions';
import { findUniqueCompute, SplitOperatorType } from './service';
-import ToolInputAndResult from '../../../components/ToolInputAndResult';
-import SimpleRadio from '../../../components/options/SimpleRadio';
-import TextFieldWithDesc from '../../../components/options/TextFieldWithDesc';
-import CheckboxWithDesc from '../../../components/options/CheckboxWithDesc';
+import ToolInputAndResult from '@components/ToolInputAndResult';
+import SimpleRadio from '@components/options/SimpleRadio';
+import TextFieldWithDesc from '@components/options/TextFieldWithDesc';
+import CheckboxWithDesc from '@components/options/CheckboxWithDesc';
const initialValues = {
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 (
@@ -156,7 +152,6 @@ export default function FindUnique() {
]}
initialValues={initialValues}
input={input}
- validationSchema={validationSchema}
/>
);
diff --git a/src/pages/list/find-unique/meta.ts b/src/pages/tools/list/find-unique/meta.ts
similarity index 93%
rename from src/pages/list/find-unique/meta.ts
rename to src/pages/tools/list/find-unique/meta.ts
index aba8456..ee38e9d 100644
--- a/src/pages/list/find-unique/meta.ts
+++ b/src/pages/tools/list/find-unique/meta.ts
@@ -5,7 +5,7 @@ import { lazy } from 'react';
export const tool = defineTool('list', {
name: 'Find unique',
path: 'find-unique',
- // image,
+ icon: 'mynaui:one',
description: '',
shortDescription: '',
keywords: ['find', 'unique'],
diff --git a/src/pages/list/find-unique/service.ts b/src/pages/tools/list/find-unique/service.ts
similarity index 100%
rename from src/pages/list/find-unique/service.ts
rename to src/pages/tools/list/find-unique/service.ts
diff --git a/src/pages/list/group/group.service.test.ts b/src/pages/tools/list/group/group.service.test.ts
similarity index 98%
rename from src/pages/list/group/group.service.test.ts
rename to src/pages/tools/list/group/group.service.test.ts
index ae8cd5c..5eed8d0 100644
--- a/src/pages/list/group/group.service.test.ts
+++ b/src/pages/tools/list/group/group.service.test.ts
@@ -1,4 +1,4 @@
-import { expect, describe, it } from 'vitest';
+import { describe, expect, it } from 'vitest';
import { groupList, SplitOperatorType } from './service';
diff --git a/src/pages/list/group/index.tsx b/src/pages/tools/list/group/index.tsx
similarity index 87%
rename from src/pages/list/group/index.tsx
rename to src/pages/tools/list/group/index.tsx
index c5cebbd..23c9576 100644
--- a/src/pages/list/group/index.tsx
+++ b/src/pages/tools/list/group/index.tsx
@@ -1,15 +1,14 @@
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 ToolTextInput from '@components/input/ToolTextInput';
+import ToolTextResult from '@components/result/ToolTextResult';
+import ToolOptions from '@components/options/ToolOptions';
import { groupList, SplitOperatorType } from './service';
-import ToolInputAndResult from '../../../components/ToolInputAndResult';
-import SimpleRadio from '../../../components/options/SimpleRadio';
-import TextFieldWithDesc from '../../../components/options/TextFieldWithDesc';
-import CheckboxWithDesc from '../../../components/options/CheckboxWithDesc';
-import { formatNumber } from '../../../utils/number';
+import ToolInputAndResult from '@components/ToolInputAndResult';
+import SimpleRadio from '@components/options/SimpleRadio';
+import TextFieldWithDesc from '@components/options/TextFieldWithDesc';
+import CheckboxWithDesc from '@components/options/CheckboxWithDesc';
+import { formatNumber } from '../../../../utils/number';
const initialValues = {
splitOperatorType: 'symbol' as SplitOperatorType,
@@ -73,9 +72,6 @@ export default function FindUnique() {
)
);
};
- const validationSchema = Yup.object({
- // splitSeparator: Yup.string().required('The separator is required')
- });
return (
@@ -181,7 +177,6 @@ export default function FindUnique() {
]}
initialValues={initialValues}
input={input}
- validationSchema={validationSchema}
/>
);
diff --git a/src/pages/list/group/meta.ts b/src/pages/tools/list/group/meta.ts
similarity index 92%
rename from src/pages/list/group/meta.ts
rename to src/pages/tools/list/group/meta.ts
index 84bda84..0391c0a 100644
--- a/src/pages/list/group/meta.ts
+++ b/src/pages/tools/list/group/meta.ts
@@ -5,7 +5,7 @@ import { lazy } from 'react';
export const tool = defineTool('list', {
name: 'Group',
path: 'group',
- // image,
+ icon: 'pajamas:group',
description: '',
shortDescription: '',
keywords: ['group'],
diff --git a/src/pages/list/group/service.ts b/src/pages/tools/list/group/service.ts
similarity index 100%
rename from src/pages/list/group/service.ts
rename to src/pages/tools/list/group/service.ts
diff --git a/src/pages/list/index.ts b/src/pages/tools/list/index.ts
similarity index 91%
rename from src/pages/list/index.ts
rename to src/pages/tools/list/index.ts
index 21fa55b..8a3c7bf 100644
--- a/src/pages/list/index.ts
+++ b/src/pages/tools/list/index.ts
@@ -17,8 +17,9 @@ export const listTools = [
listFindUnique,
listFindMostPopular,
listGroup,
- listWrap,
+ // listWrap,
listRotate,
- listShuffle,
- listTruncate
+ listShuffle
+ // listTruncate,
+ // listDuplicate
];
diff --git a/src/pages/tools/list/reverse/index.tsx b/src/pages/tools/list/reverse/index.tsx
new file mode 100644
index 0000000..9de94f8
--- /dev/null
+++ b/src/pages/tools/list/reverse/index.tsx
@@ -0,0 +1,192 @@
+import { Box } from '@mui/material';
+import React, { useState } from 'react';
+import ToolTextInput from '@components/input/ToolTextInput';
+import ToolTextResult from '@components/result/ToolTextResult';
+import { GetGroupsType } from '@components/options/ToolOptions';
+import { reverseList, SplitOperatorType } from './service';
+import SimpleRadio from '@components/options/SimpleRadio';
+import TextFieldWithDesc from '@components/options/TextFieldWithDesc';
+import { CardExampleType } from '@components/examples/ToolExamples';
+import { ToolComponentProps } from '@tools/defineTool';
+import ToolContent from '@components/ToolContent';
+
+const initialValues = {
+ splitOperatorType: 'symbol' as SplitOperatorType,
+ splitSeparator: ',',
+ joinSeparator: '\\n'
+};
+type InitialValuesType = typeof initialValues;
+const splitOperators: {
+ title: string;
+ description: string;
+ type: SplitOperatorType;
+}[] = [
+ {
+ title: 'Use a Symbol for Splitting',
+ description: 'Delimit input list items with a character.',
+ type: 'symbol'
+ },
+ {
+ title: 'Use a Regex for Splitting',
+ type: 'regex',
+ description: 'Delimit input list items with a regular expression.'
+ }
+];
+
+const exampleCards: CardExampleType[] = [
+ {
+ title: 'Reverse a List of Digits',
+ description:
+ 'In this example, we load a list of digits in the input. The digits are separated by a mix of dot, comma, and semicolon characters, so we use the regular expression split mode and enter a regular expression that matches all these characters as the input item separator. In the output, we get a reversed list of digits that all use the semicolon as a separator.',
+ sampleText: `2, 9, 6; 3; 7. 4. 4. 2, 1; 4, 8. 4; 4. 8, 2, 5; 1; 7; 7. 0`,
+ sampleResult: `0; 7; 7; 1; 5; 2; 8; 4; 4; 8; 4; 1; 2; 4; 4; 7; 3; 6; 9; 2`,
+ sampleOptions: {
+ splitOperatorType: 'regex',
+ splitSeparator: '[;,.]\\s*',
+ joinSeparator: '; '
+ }
+ },
+ {
+ title: 'Reverse a Column of Words',
+ description:
+ 'This example reverses a column of twenty three-syllable nouns and prints all the words from the bottom to top. To separate the list items, it uses the \n character as input item separator, which means that each item is on its own line..',
+ sampleText: `argument
+pollution
+emphasis
+vehicle
+family
+property
+preference
+studio
+suggestion
+accident
+analyst
+permission
+reaction
+promotion
+quantity
+inspection
+chemistry
+conclusion
+confusion
+memory`,
+ sampleResult: `memory
+confusion
+conclusion
+chemistry
+inspection
+quantity
+promotion
+reaction
+permission
+analyst
+accident
+suggestion
+studio
+preference
+property
+family
+vehicle
+emphasis
+pollution
+argument`,
+ sampleOptions: {
+ splitOperatorType: 'symbol',
+ splitSeparator: '\\n',
+ joinSeparator: '\\n'
+ }
+ },
+ {
+ title: 'Reverse a Random List',
+ description:
+ 'In this example, the list elements are random cities, zip codes, and weather conditions. To reverse list elements, we first need to identify them and separate them apart. The input list incorrectly uses the dash symbol to separate the elements but the output list fixes this and uses commas.',
+ sampleText: `Hamburg-21334-Dhaka-Sunny-Managua-Rainy-Chongqing-95123-Oakland`,
+ sampleResult: `Oakland, 95123, Chongqing, Rainy, Managua, Sunny, Dhaka, 21334, Hamburg`,
+ sampleOptions: {
+ splitOperatorType: 'symbol',
+ splitSeparator: '-',
+ joinSeparator: ', '
+ }
+ }
+];
+
+export default function Reverse({ title }: ToolComponentProps) {
+ const [input, setInput] = useState('');
+ const [result, setResult] = useState('');
+
+ const getGroups: GetGroupsType = ({
+ values,
+ updateField
+ }) => [
+ {
+ title: 'Splitter Mode',
+ component: (
+
+ {splitOperators.map(({ title, description, type }) => (
+ updateField('splitOperatorType', type)}
+ title={title}
+ description={description}
+ checked={values.splitOperatorType === type}
+ />
+ ))}
+
+ )
+ },
+ {
+ title: 'Item Separator',
+ component: (
+
+ updateField('splitSeparator', val)}
+ />
+
+ )
+ },
+ {
+ title: 'Output List Options',
+ component: (
+
+ updateField('joinSeparator', val)}
+ />
+
+ )
+ }
+ ];
+ const compute = (optionsValues: typeof initialValues, input: any) => {
+ const { splitOperatorType, splitSeparator, joinSeparator } = optionsValues;
+
+ setResult(
+ reverseList(splitOperatorType, splitSeparator, joinSeparator, input)
+ );
+ };
+
+ return (
+
+ }
+ resultComponent={
+
+ }
+ toolInfo={{
+ title: 'What Is a List Reverser?',
+ description:
+ 'With this utility, you can reverse the order of items in a list. The utility first splits the input list into individual items and then iterates through them from the last item to the first item, printing each item to the output during the iteration. The input list may contain anything that can be represented as textual data, which includes digits, numbers, strings, words, sentences, etc. The input item separator can also be a regular expression. For example, the regex /[;,]/ will allow you to use items that are either comma- or semicolon-separated. The input and output list items delimiters can be customized in the options. By default, both input and output lists are comma-separated. Listabulous!'
+ }}
+ exampleCards={exampleCards}
+ />
+ );
+}
diff --git a/src/pages/tools/list/reverse/meta.ts b/src/pages/tools/list/reverse/meta.ts
new file mode 100644
index 0000000..9ff21db
--- /dev/null
+++ b/src/pages/tools/list/reverse/meta.ts
@@ -0,0 +1,13 @@
+import { defineTool } from '@tools/defineTool';
+import { lazy } from 'react';
+// import image from '@assets/text.png';
+
+export const tool = defineTool('list', {
+ name: 'Reverse',
+ path: 'reverse',
+ icon: 'proicons:reverse',
+ description: 'This is a super simple browser-based application prints all list items in reverse. The input items can be separated by any symbol and you can also change the separator of the reversed list items.',
+ shortDescription: 'Quickly reverse a list',
+ keywords: ['reverse'],
+ component: lazy(() => import('./index'))
+});
diff --git a/src/pages/list/reverse/reverse.service.test.ts b/src/pages/tools/list/reverse/reverse.service.test.ts
similarity index 94%
rename from src/pages/list/reverse/reverse.service.test.ts
rename to src/pages/tools/list/reverse/reverse.service.test.ts
index 3f7bc2b..e206300 100644
--- a/src/pages/list/reverse/reverse.service.test.ts
+++ b/src/pages/tools/list/reverse/reverse.service.test.ts
@@ -1,4 +1,4 @@
-import { expect, describe, it } from 'vitest';
+import { describe, expect } from 'vitest';
import { reverseList } from './service';
describe('reverseList Function', () => {
diff --git a/src/pages/list/reverse/service.ts b/src/pages/tools/list/reverse/service.ts
similarity index 100%
rename from src/pages/list/reverse/service.ts
rename to src/pages/tools/list/reverse/service.ts
diff --git a/src/pages/tools/list/rotate/index.tsx b/src/pages/tools/list/rotate/index.tsx
new file mode 100644
index 0000000..bca71d6
--- /dev/null
+++ b/src/pages/tools/list/rotate/index.tsx
@@ -0,0 +1,153 @@
+import { Box } from '@mui/material';
+import React, { useState } from 'react';
+import ToolTextInput from '@components/input/ToolTextInput';
+import ToolTextResult from '@components/result/ToolTextResult';
+import ToolOptions from '@components/options/ToolOptions';
+import { rotateList, SplitOperatorType } from './service';
+import ToolInputAndResult from '@components/ToolInputAndResult';
+import SimpleRadio from '@components/options/SimpleRadio';
+import TextFieldWithDesc from '@components/options/TextFieldWithDesc';
+import { formatNumber } from '../../../../utils/number';
+
+const initialValues = {
+ splitOperatorType: 'symbol' as SplitOperatorType,
+ input: '',
+ splitSeparator: ',',
+ joinSeparator: ',',
+ right: true,
+ step: 1
+};
+const splitOperators: {
+ title: string;
+ description: string;
+ type: SplitOperatorType;
+}[] = [
+ {
+ title: 'Use a Symbol for Splitting',
+ description: 'Delimit input list items with a character.',
+ type: 'symbol'
+ },
+ {
+ title: 'Use a Regex for Splitting',
+ type: 'regex',
+ description: 'Delimit input list items with a regular expression.'
+ }
+];
+const rotationDirections: {
+ title: string;
+ description: string;
+ value: boolean;
+}[] = [
+ {
+ title: 'Rotate forward',
+ description:
+ 'Rotate list items to the right. (Down if a vertical column list.)',
+ value: true
+ },
+ {
+ title: 'Rotate backward',
+ description:
+ 'Rotate list items to the left. (Up if a vertical column list.)',
+ value: false
+ }
+];
+
+export default function Rotate() {
+ const [input, setInput] = useState('');
+ const [result, setResult] = useState('');
+ const compute = (optionsValues: typeof initialValues, input: any) => {
+ const { splitOperatorType, splitSeparator, joinSeparator, right, step } =
+ optionsValues;
+
+ setResult(
+ rotateList(
+ splitOperatorType,
+ input,
+ splitSeparator,
+ joinSeparator,
+ right,
+ step
+ )
+ );
+ };
+
+ return (
+
+
+ }
+ result={ }
+ />
+ [
+ {
+ title: 'Item split mode',
+ component: (
+
+ {splitOperators.map(({ title, description, type }) => (
+ updateField('splitOperatorType', type)}
+ title={title}
+ description={description}
+ checked={values.splitOperatorType === type}
+ />
+ ))}
+ updateField('splitSeparator', val)}
+ />
+
+ )
+ },
+ {
+ title: 'Rotation Direction and Count',
+ component: (
+
+ {rotationDirections.map(({ title, description, value }) => (
+ updateField('right', value)}
+ title={title}
+ description={description}
+ checked={values.right === value}
+ />
+ ))}
+
+ updateField('step', formatNumber(val, 1))
+ }
+ />
+
+ )
+ },
+ {
+ title: 'Rotated List Joining Symbol',
+ component: (
+
+ updateField('joinSeparator', value)}
+ description={
+ 'Enter the character that goes between items in the rotated list.'
+ }
+ />
+
+ )
+ }
+ ]}
+ initialValues={initialValues}
+ input={input}
+ />
+
+ );
+}
diff --git a/src/pages/list/rotate/meta.ts b/src/pages/tools/list/rotate/meta.ts
similarity index 86%
rename from src/pages/list/rotate/meta.ts
rename to src/pages/tools/list/rotate/meta.ts
index 07ddfd6..6086301 100644
--- a/src/pages/list/rotate/meta.ts
+++ b/src/pages/tools/list/rotate/meta.ts
@@ -5,7 +5,7 @@ import { lazy } from 'react';
export const tool = defineTool('list', {
name: 'Rotate',
path: 'rotate',
- // image,
+ icon: 'material-symbols-light:rotate-right',
description: '',
shortDescription: '',
keywords: ['rotate'],
diff --git a/src/pages/list/rotate/rotate.service.test.ts b/src/pages/tools/list/rotate/rotate.service.test.ts
similarity index 96%
rename from src/pages/list/rotate/rotate.service.test.ts
rename to src/pages/tools/list/rotate/rotate.service.test.ts
index ba5e543..585b706 100644
--- a/src/pages/list/rotate/rotate.service.test.ts
+++ b/src/pages/tools/list/rotate/rotate.service.test.ts
@@ -1,5 +1,5 @@
-import { expect, describe, it } from 'vitest';
-import { SplitOperatorType, rotateList } from './service';
+import { describe, expect, it } from 'vitest';
+import { rotateList, SplitOperatorType } from './service';
describe('rotate function', () => {
it('should rotate right side if right is set to true', () => {
diff --git a/src/pages/list/rotate/service.ts b/src/pages/tools/list/rotate/service.ts
similarity index 96%
rename from src/pages/list/rotate/service.ts
rename to src/pages/tools/list/rotate/service.ts
index 65d183b..b0a8ea2 100644
--- a/src/pages/list/rotate/service.ts
+++ b/src/pages/tools/list/rotate/service.ts
@@ -1,4 +1,3 @@
-import { isNumber } from 'utils/string';
export type SplitOperatorType = 'symbol' | 'regex';
function rotateArray(array: string[], step: number, right: boolean): string[] {
diff --git a/src/pages/list/reverse/index.tsx b/src/pages/tools/list/shuffle/index.tsx
similarity index 61%
rename from src/pages/list/reverse/index.tsx
rename to src/pages/tools/list/shuffle/index.tsx
index 100b7bf..9c07d22 100644
--- a/src/pages/list/reverse/index.tsx
+++ b/src/pages/tools/list/shuffle/index.tsx
@@ -1,18 +1,19 @@
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 { reverseList, SplitOperatorType } from './service';
-import ToolInputAndResult from '../../../components/ToolInputAndResult';
-import SimpleRadio from '../../../components/options/SimpleRadio';
-import TextFieldWithDesc from '../../../components/options/TextFieldWithDesc';
+import ToolTextInput from '@components/input/ToolTextInput';
+import ToolTextResult from '@components/result/ToolTextResult';
+import ToolOptions from '@components/options/ToolOptions';
+import { shuffleList, SplitOperatorType } from './service';
+import ToolInputAndResult from '@components/ToolInputAndResult';
+import SimpleRadio from '@components/options/SimpleRadio';
+import TextFieldWithDesc from '@components/options/TextFieldWithDesc';
+import { isNumber } from '../../../../utils/string';
const initialValues = {
splitOperatorType: 'symbol' as SplitOperatorType,
splitSeparator: ',',
- joinSeparator: '\\n'
+ joinSeparator: ',',
+ length: ''
};
const splitOperators: {
title: string;
@@ -31,19 +32,23 @@ const splitOperators: {
}
];
-export default function Reverse() {
+export default function Shuffle() {
const [input, setInput] = useState('');
const [result, setResult] = useState('');
const compute = (optionsValues: typeof initialValues, input: any) => {
- const { splitOperatorType, splitSeparator, joinSeparator } = optionsValues;
+ const { splitOperatorType, splitSeparator, joinSeparator, length } =
+ optionsValues;
setResult(
- reverseList(splitOperatorType, splitSeparator, joinSeparator, input)
+ shuffleList(
+ splitOperatorType,
+ input,
+ splitSeparator,
+ joinSeparator,
+ isNumber(length) ? Number(length) : undefined
+ )
);
};
- const validationSchema = Yup.object({
- // splitSeparator: Yup.string().required('The separator is required')
- });
return (
@@ -55,13 +60,13 @@ export default function Reverse() {
onChange={setInput}
/>
}
- result={ }
+ result={ }
/>
[
{
- title: 'Splitter Mode',
+ title: 'Input list separator',
component: (
{splitOperators.map(({ title, description, type }) => (
@@ -73,13 +78,6 @@ export default function Reverse() {
checked={values.splitOperatorType === type}
/>
))}
-
- )
- },
- {
- title: 'Item Separator',
- component: (
-
+ updateField('length', val)}
+ />
+
+ )
+ },
+ {
+ title: 'Shuffled List Separator',
component: (
updateField('joinSeparator', val)}
+ onOwnChange={(value) => updateField('joinSeparator', value)}
+ description={'Use this separator in the randomized list.'}
/>
)
@@ -103,7 +113,6 @@ export default function Reverse() {
]}
initialValues={initialValues}
input={input}
- validationSchema={validationSchema}
/>
);
diff --git a/src/pages/list/shuffle/meta.ts b/src/pages/tools/list/shuffle/meta.ts
similarity index 88%
rename from src/pages/list/shuffle/meta.ts
rename to src/pages/tools/list/shuffle/meta.ts
index e193363..c94be06 100644
--- a/src/pages/list/shuffle/meta.ts
+++ b/src/pages/tools/list/shuffle/meta.ts
@@ -5,7 +5,7 @@ import { lazy } from 'react';
export const tool = defineTool('list', {
name: 'Shuffle',
path: 'shuffle',
- // image,
+ icon: 'material-symbols-light:shuffle',
description: '',
shortDescription: '',
keywords: ['shuffle'],
diff --git a/src/pages/list/shuffle/service.ts b/src/pages/tools/list/shuffle/service.ts
similarity index 100%
rename from src/pages/list/shuffle/service.ts
rename to src/pages/tools/list/shuffle/service.ts
diff --git a/src/pages/list/shuffle/shuffle.service.test.ts b/src/pages/tools/list/shuffle/shuffle.service.test.ts
similarity index 98%
rename from src/pages/list/shuffle/shuffle.service.test.ts
rename to src/pages/tools/list/shuffle/shuffle.service.test.ts
index 58808c9..6b09828 100644
--- a/src/pages/list/shuffle/shuffle.service.test.ts
+++ b/src/pages/tools/list/shuffle/shuffle.service.test.ts
@@ -1,4 +1,4 @@
-import { expect, describe, it } from 'vitest';
+import { describe, expect, it } from 'vitest';
import { shuffleList, SplitOperatorType } from './service';
describe('shuffle function', () => {
diff --git a/src/pages/list/sort/index.tsx b/src/pages/tools/list/sort/index.tsx
similarity index 86%
rename from src/pages/list/sort/index.tsx
rename to src/pages/tools/list/sort/index.tsx
index 0e5c063..46b7df4 100644
--- a/src/pages/list/sort/index.tsx
+++ b/src/pages/tools/list/sort/index.tsx
@@ -1,15 +1,14 @@
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 ToolTextInput from '@components/input/ToolTextInput';
+import ToolTextResult from '@components/result/ToolTextResult';
+import ToolOptions from '@components/options/ToolOptions';
import { Sort, SortingMethod, SplitOperatorType } from './service';
-import ToolInputAndResult from '../../../components/ToolInputAndResult';
-import SimpleRadio from '../../../components/options/SimpleRadio';
-import TextFieldWithDesc from '../../../components/options/TextFieldWithDesc';
-import CheckboxWithDesc from '../../../components/options/CheckboxWithDesc';
-import SelectWithDesc from '../../../components/options/SelectWithDesc';
+import ToolInputAndResult from '@components/ToolInputAndResult';
+import SimpleRadio from '@components/options/SimpleRadio';
+import TextFieldWithDesc from '@components/options/TextFieldWithDesc';
+import CheckboxWithDesc from '@components/options/CheckboxWithDesc';
+import SelectWithDesc from '@components/options/SelectWithDesc';
const initialValues = {
splitSeparatorType: 'symbol' as SplitOperatorType,
@@ -64,9 +63,6 @@ export default function SplitText() {
)
);
};
- const validationSchema = Yup.object({
- // splitSeparator: Yup.string().required('The separator is required')
- });
return (
@@ -163,7 +159,6 @@ export default function SplitText() {
]}
initialValues={initialValues}
input={input}
- validationSchema={validationSchema}
/>
);
diff --git a/src/pages/list/sort/meta.ts b/src/pages/tools/list/sort/meta.ts
similarity index 96%
rename from src/pages/list/sort/meta.ts
rename to src/pages/tools/list/sort/meta.ts
index 536aa24..b2e7cfc 100644
--- a/src/pages/list/sort/meta.ts
+++ b/src/pages/tools/list/sort/meta.ts
@@ -5,7 +5,7 @@ import { lazy } from 'react';
export const tool = defineTool('list', {
name: 'Sort',
path: 'sort',
- // image,
+ icon: 'basil:sort-outline',
description:
'This is a super simple browser-based application that sorts items in a list and arranges them in increasing or decreasing order. You can sort the items alphabetically, numerically, or by their length. You can also remove duplicate and empty items, as well as trim individual items that have whitespace around them. You can use any separator character to separate the input list items or alternatively use a regular expression to separate them. Additionally, you can create a new delimiter for the sorted output list.',
shortDescription: 'Quickly sort a list',
diff --git a/src/pages/list/sort/service.ts b/src/pages/tools/list/sort/service.ts
similarity index 100%
rename from src/pages/list/sort/service.ts
rename to src/pages/tools/list/sort/service.ts
diff --git a/src/pages/list/sort/sort.service.test.ts b/src/pages/tools/list/sort/sort.service.test.ts
similarity index 99%
rename from src/pages/list/sort/sort.service.test.ts
rename to src/pages/tools/list/sort/sort.service.test.ts
index de9c8df..3becba4 100644
--- a/src/pages/list/sort/sort.service.test.ts
+++ b/src/pages/tools/list/sort/sort.service.test.ts
@@ -1,12 +1,12 @@
// Import necessary modules and functions
-import { describe, it, expect } from 'vitest';
+import { describe, expect, it } from 'vitest';
import {
alphabeticSort,
lengthSort,
numericSort,
Sort,
- SplitOperatorType,
- SortingMethod
+ SortingMethod,
+ SplitOperatorType
} from './service';
// Define test cases for the numericSort function
diff --git a/src/pages/list/truncate/index.tsx b/src/pages/tools/list/truncate/index.tsx
similarity index 100%
rename from src/pages/list/truncate/index.tsx
rename to src/pages/tools/list/truncate/index.tsx
diff --git a/src/pages/list/truncate/meta.ts b/src/pages/tools/list/truncate/meta.ts
similarity index 96%
rename from src/pages/list/truncate/meta.ts
rename to src/pages/tools/list/truncate/meta.ts
index 95cc0ec..d7eeb53 100644
--- a/src/pages/list/truncate/meta.ts
+++ b/src/pages/tools/list/truncate/meta.ts
@@ -5,7 +5,7 @@ import { lazy } from 'react';
export const tool = defineTool('list', {
name: 'Truncate',
path: 'truncate',
- // image,
+ icon: '',
description: '',
shortDescription: '',
keywords: ['truncate'],
diff --git a/src/pages/list/truncate/service.ts b/src/pages/tools/list/truncate/service.ts
similarity index 100%
rename from src/pages/list/truncate/service.ts
rename to src/pages/tools/list/truncate/service.ts
diff --git a/src/pages/list/truncate/truncate.service.test.ts b/src/pages/tools/list/truncate/truncate.service.test.ts
similarity index 99%
rename from src/pages/list/truncate/truncate.service.test.ts
rename to src/pages/tools/list/truncate/truncate.service.test.ts
index 79d1763..8e6a9fb 100644
--- a/src/pages/list/truncate/truncate.service.test.ts
+++ b/src/pages/tools/list/truncate/truncate.service.test.ts
@@ -1,4 +1,4 @@
-import { expect, describe, it } from 'vitest';
+import { describe, expect, it } from 'vitest';
import { SplitOperatorType, truncateList } from './service';
diff --git a/src/pages/list/unwrap/index.tsx b/src/pages/tools/list/unwrap/index.tsx
similarity index 100%
rename from src/pages/list/unwrap/index.tsx
rename to src/pages/tools/list/unwrap/index.tsx
diff --git a/src/pages/list/unwrap/meta.ts b/src/pages/tools/list/unwrap/meta.ts
similarity index 93%
rename from src/pages/list/unwrap/meta.ts
rename to src/pages/tools/list/unwrap/meta.ts
index c526721..9ae3652 100644
--- a/src/pages/list/unwrap/meta.ts
+++ b/src/pages/tools/list/unwrap/meta.ts
@@ -5,7 +5,7 @@ import { lazy } from 'react';
export const tool = defineTool('list', {
name: 'Unwrap',
path: 'unwrap',
- // image,
+ icon: 'mdi:unwrap',
description: '',
shortDescription: '',
keywords: ['unwrap'],
diff --git a/src/pages/list/unwrap/service.ts b/src/pages/tools/list/unwrap/service.ts
similarity index 100%
rename from src/pages/list/unwrap/service.ts
rename to src/pages/tools/list/unwrap/service.ts
diff --git a/src/pages/list/unwrap/unwrap.service.test.ts b/src/pages/tools/list/unwrap/unwrap.service.test.ts
similarity index 98%
rename from src/pages/list/unwrap/unwrap.service.test.ts
rename to src/pages/tools/list/unwrap/unwrap.service.test.ts
index 98ac30b..6812fa6 100644
--- a/src/pages/list/unwrap/unwrap.service.test.ts
+++ b/src/pages/tools/list/unwrap/unwrap.service.test.ts
@@ -1,4 +1,4 @@
-import { expect, describe, it } from 'vitest';
+import { describe, expect, it } from 'vitest';
import { unwrapList } from './service';
describe('unwrapList function', () => {
diff --git a/src/pages/list/wrap/index.tsx b/src/pages/tools/list/wrap/index.tsx
similarity index 100%
rename from src/pages/list/wrap/index.tsx
rename to src/pages/tools/list/wrap/index.tsx
diff --git a/src/pages/list/wrap/meta.ts b/src/pages/tools/list/wrap/meta.ts
similarity index 96%
rename from src/pages/list/wrap/meta.ts
rename to src/pages/tools/list/wrap/meta.ts
index 38cee24..3ff7f99 100644
--- a/src/pages/list/wrap/meta.ts
+++ b/src/pages/tools/list/wrap/meta.ts
@@ -5,7 +5,7 @@ import { lazy } from 'react';
export const tool = defineTool('list', {
name: 'Wrap',
path: 'wrap',
- // image,
+ icon: '',
description: '',
shortDescription: '',
keywords: ['wrap'],
diff --git a/src/pages/list/wrap/service.ts b/src/pages/tools/list/wrap/service.ts
similarity index 100%
rename from src/pages/list/wrap/service.ts
rename to src/pages/tools/list/wrap/service.ts
diff --git a/src/pages/list/wrap/wrap.service.test.ts b/src/pages/tools/list/wrap/wrap.service.test.ts
similarity index 98%
rename from src/pages/list/wrap/wrap.service.test.ts
rename to src/pages/tools/list/wrap/wrap.service.test.ts
index ef8c0e4..dad9f61 100644
--- a/src/pages/list/wrap/wrap.service.test.ts
+++ b/src/pages/tools/list/wrap/wrap.service.test.ts
@@ -1,4 +1,4 @@
-import { expect, describe, it } from 'vitest';
+import { describe, expect, it } from 'vitest';
import { SplitOperatorType, wrapList } from './service';
describe('wrap function', () => {
diff --git a/src/pages/number/generate/generate.service.test.ts b/src/pages/tools/number/generate/generate.service.test.ts
similarity index 100%
rename from src/pages/number/generate/generate.service.test.ts
rename to src/pages/tools/number/generate/generate.service.test.ts
diff --git a/src/pages/number/generate/index.tsx b/src/pages/tools/number/generate/index.tsx
similarity index 82%
rename from src/pages/number/generate/index.tsx
rename to src/pages/tools/number/generate/index.tsx
index 33de7f9..e643764 100644
--- a/src/pages/number/generate/index.tsx
+++ b/src/pages/tools/number/generate/index.tsx
@@ -1,11 +1,10 @@
import { Box } from '@mui/material';
import React, { useState } from 'react';
-import ToolTextResult from '../../../components/result/ToolTextResult';
-import * as Yup from 'yup';
-import ToolOptions from '../../../components/options/ToolOptions';
+import ToolTextResult from '@components/result/ToolTextResult';
+import ToolOptions from '@components/options/ToolOptions';
import { listOfIntegers } from './service';
-import ToolInputAndResult from '../../../components/ToolInputAndResult';
-import TextFieldWithDesc from '../../../components/options/TextFieldWithDesc';
+import ToolInputAndResult from '@components/ToolInputAndResult';
+import TextFieldWithDesc from '@components/options/TextFieldWithDesc';
const initialValues = {
firstValue: '1',
@@ -16,10 +15,6 @@ const initialValues = {
export default function SplitText() {
const [result, setResult] = useState('');
- const validationSchema = Yup.object({
- // splitSeparator: Yup.string().required('The separator is required')
- });
-
return (
);
diff --git a/src/pages/number/generate/meta.ts b/src/pages/tools/number/generate/meta.ts
similarity index 94%
rename from src/pages/number/generate/meta.ts
rename to src/pages/tools/number/generate/meta.ts
index 1c85aee..a58e227 100644
--- a/src/pages/number/generate/meta.ts
+++ b/src/pages/tools/number/generate/meta.ts
@@ -6,7 +6,7 @@ export const tool = defineTool('number', {
name: 'Generate numbers',
path: 'generate',
shortDescription: 'Quickly calculate a list of integers in your browser',
- // image,
+ icon: 'lsicon:number-filled',
description:
'Quickly calculate a list of integers in your browser. To get your list, just specify the first integer, change value and total count in the options below, and this utility will generate that many integers',
keywords: ['generate'],
diff --git a/src/pages/number/generate/service.ts b/src/pages/tools/number/generate/service.ts
similarity index 100%
rename from src/pages/number/generate/service.ts
rename to src/pages/tools/number/generate/service.ts
diff --git a/src/pages/number/index.ts b/src/pages/tools/number/index.ts
similarity index 100%
rename from src/pages/number/index.ts
rename to src/pages/tools/number/index.ts
diff --git a/src/pages/tools/number/sum/index.tsx b/src/pages/tools/number/sum/index.tsx
new file mode 100644
index 0000000..3c2b3e8
--- /dev/null
+++ b/src/pages/tools/number/sum/index.tsx
@@ -0,0 +1,207 @@
+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 { 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';
+import ToolExamples, {
+ CardExampleType
+} from '@components/examples/ToolExamples';
+import ToolInfo from '@components/ToolInfo';
+import Separator from '@components/Separator';
+import { ToolComponentProps } from '@tools/defineTool';
+import { FormikProps } from 'formik';
+
+const initialValues = {
+ extractionType: 'smart' as NumberExtractionType,
+ separator: '\\n',
+ printRunningSum: false
+};
+type InitialValuesType = typeof initialValues;
+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'
+ }
+];
+
+const exampleCards: CardExampleType[] = [
+ {
+ title: 'Sum of Ten Positive Numbers',
+ description:
+ 'In this example, we calculate the sum of ten positive integers. These integers are listed as a column and their total sum equals 19494.',
+ sampleText: `0
+1
+20
+33
+400
+505
+660
+777
+8008
+9090`,
+ sampleResult: `19494`,
+ sampleOptions: {
+ extractionType: 'delimiter',
+ separator: '\\n',
+ printRunningSum: false
+ }
+ },
+ {
+ title: 'Count Trees in the Park',
+ description:
+ 'This example reverses a column of twenty three-syllable nouns and prints all the words from the bottom to top. To separate the list items, it uses the \n character as input item separator, which means that each item is on its own line..',
+ sampleText: `This year gardeners have planted 20 red maples, 35 sweetgum, 13 quaking aspen, and 7 white oaks in the central park of the city.`,
+ sampleResult: `75`,
+ sampleOptions: {
+ extractionType: 'smart',
+ separator: '\\n',
+ printRunningSum: false
+ }
+ },
+ {
+ title: 'Sum of Integers and Decimals',
+ description:
+ 'In this example, we add together ninety different values – positive numbers, negative numbers, integers and decimal fractions. We set the input separator to a comma and after adding all of them together, we get 0 as output.',
+ sampleText: `1, 2, 3, 4, 5, 6, 7, 8, 9, -1.1, -2.1, -3.1, -4.1, -5.1, -6.1, -7.1, -8.1, -9.1, 10, 20, 30, 40, 50, 60, 70, 80, 90, -10.2, -20.2, -30.2, -40.2, -50.2, -60.2, -70.2, -80.2, -90.2, 100, 200, 300, 400, 500, 600, 700, 800, 900, -100.3, -200.3, -300.3, -400.3, -500.3, -600.3, -700.3, -800.3, -900.3, 1000, 2000, 3000, 4000, 5000, 6000, 7000, 8000, 9000, -1000.4, -2000.4, -3000.4, -4000.4, -5000.4, -6000.4, -7000.4, -8000.4, -9000.4, 10001, 20001, 30001, 40001, 50001, 60001, 70001, 80001, 90001, -10000, -20000, -30000, -40000, -50000, -60000, -70000, -80000, -90000`,
+ sampleResult: `0`,
+ sampleOptions: {
+ extractionType: 'delimiter',
+ separator: ', ',
+ printRunningSum: false
+ }
+ },
+ {
+ title: 'Running Sum of Numbers',
+ description:
+ 'In this example, we calculate the sum of all ten digits and enable the option "Print Running Sum". We get the intermediate values of the sum in the process of addition. Thus, we have the following sequence in the output: 0, 1 (0 + 1), 3 (0 + 1 + 2), 6 (0 + 1 + 2 + 3), 10 (0 + 1 + 2 + 3 + 4), and so on.',
+ sampleText: `0
+1
+2
+3
+4
+5
+6
+7
+8
+9`,
+ sampleResult: `0
+1
+3
+6
+10
+15
+21
+28
+36
+45`,
+ sampleOptions: {
+ extractionType: 'delimiter',
+ separator: '\\n',
+ printRunningSum: true
+ }
+ }
+];
+
+export default function SumNumbers({ title }: ToolComponentProps) {
+ const [input, setInput] = useState('');
+ const [result, setResult] = useState('');
+ const formRef = useRef>(null);
+
+ const getGroups: GetGroupsType = ({
+ values,
+ updateField
+ }) => [
+ {
+ title: 'Number extraction',
+ component: extractionTypes.map(
+ ({ title, description, type, withTextField, textValueAccessor }) =>
+ withTextField ? (
+ updateField('extractionType', type)}
+ onTextChange={(val) =>
+ textValueAccessor ? updateField(textValueAccessor, val) : null
+ }
+ />
+ ) : (
+ updateField('extractionType', type)}
+ checked={values.extractionType === type}
+ description={description}
+ title={title}
+ />
+ )
+ )
+ },
+ {
+ title: 'Running Sum',
+ component: (
+ updateField('printRunningSum', value)}
+ />
+ )
+ }
+ ];
+ return (
+
+ }
+ result={ }
+ />
+ {
+ const { extractionType, printRunningSum, separator } = optionsValues;
+ setResult(compute(input, extractionType, printRunningSum, separator));
+ }}
+ initialValues={initialValues}
+ input={input}
+ />
+
+
+
+
+ );
+}
diff --git a/src/pages/number/sum/meta.ts b/src/pages/tools/number/sum/meta.ts
similarity index 93%
rename from src/pages/number/sum/meta.ts
rename to src/pages/tools/number/sum/meta.ts
index fc96799..02de1f1 100644
--- a/src/pages/number/sum/meta.ts
+++ b/src/pages/tools/number/sum/meta.ts
@@ -5,7 +5,7 @@ import { lazy } from 'react';
export const tool = defineTool('number', {
name: 'Number Sum Calculator',
path: 'sum',
- // image,
+ icon: 'fluent:autosum-20-regular',
description:
'Quickly calculate the sum of numbers in your browser. To get your sum, just enter your list of numbers in the input field, adjust the separator between the numbers in the options below, and this utility will add up all these numbers.',
shortDescription: 'Quickly sum numbers',
diff --git a/src/pages/number/sum/service.ts b/src/pages/tools/number/sum/service.ts
similarity index 100%
rename from src/pages/number/sum/service.ts
rename to src/pages/tools/number/sum/service.ts
diff --git a/src/pages/number/sum/sum.service.test.ts b/src/pages/tools/number/sum/sum.service.test.ts
similarity index 100%
rename from src/pages/number/sum/sum.service.test.ts
rename to src/pages/tools/number/sum/sum.service.test.ts
diff --git a/src/pages/tools/string/create-palindrome/create-palindrome.service.test.ts b/src/pages/tools/string/create-palindrome/create-palindrome.service.test.ts
new file mode 100644
index 0000000..3d579d1
--- /dev/null
+++ b/src/pages/tools/string/create-palindrome/create-palindrome.service.test.ts
@@ -0,0 +1,66 @@
+import { describe, expect } from 'vitest';
+import { createPalindrome, createPalindromeList } 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('');
+ });
+});
diff --git a/src/pages/string/create-palindrome/index.tsx b/src/pages/tools/string/create-palindrome/index.tsx
similarity index 99%
rename from src/pages/string/create-palindrome/index.tsx
rename to src/pages/tools/string/create-palindrome/index.tsx
index da75227..5a45556 100644
--- a/src/pages/string/create-palindrome/index.tsx
+++ b/src/pages/tools/string/create-palindrome/index.tsx
@@ -8,4 +8,4 @@ const validationSchema = Yup.object({
});
export default function CreatePalindrome() {
return Lorem ipsum ;
-}
\ No newline at end of file
+}
diff --git a/src/pages/string/create-palindrome/meta.ts b/src/pages/tools/string/create-palindrome/meta.ts
similarity index 95%
rename from src/pages/string/create-palindrome/meta.ts
rename to src/pages/tools/string/create-palindrome/meta.ts
index 5b21620..a8731e1 100644
--- a/src/pages/string/create-palindrome/meta.ts
+++ b/src/pages/tools/string/create-palindrome/meta.ts
@@ -5,9 +5,9 @@ import { lazy } from 'react';
export const tool = defineTool('string', {
name: 'Create palindrome',
path: 'create-palindrome',
- // image,
+ icon: '',
description: '',
shortDescription: '',
keywords: ['create', 'palindrome'],
component: lazy(() => import('./index'))
-});
\ No newline at end of file
+});
diff --git a/src/pages/tools/string/create-palindrome/service.ts b/src/pages/tools/string/create-palindrome/service.ts
new file mode 100644
index 0000000..9cf5cff
--- /dev/null
+++ b/src/pages/tools/string/create-palindrome/service.ts
@@ -0,0 +1,36 @@
+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[];
+ const 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');
+}
diff --git a/src/pages/tools/string/extract-substring/extract-substring.service.test.ts b/src/pages/tools/string/extract-substring/extract-substring.service.test.ts
new file mode 100644
index 0000000..74f4dc8
--- /dev/null
+++ b/src/pages/tools/string/extract-substring/extract-substring.service.test.ts
@@ -0,0 +1,61 @@
+import { describe, expect, 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');
+ });
+});
diff --git a/src/pages/string/extract-substring/index.tsx b/src/pages/tools/string/extract-substring/index.tsx
similarity index 99%
rename from src/pages/string/extract-substring/index.tsx
rename to src/pages/tools/string/extract-substring/index.tsx
index be249e8..bbd3e7d 100644
--- a/src/pages/string/extract-substring/index.tsx
+++ b/src/pages/tools/string/extract-substring/index.tsx
@@ -8,4 +8,4 @@ const validationSchema = Yup.object({
});
export default function ExtractSubstring() {
return Lorem ipsum ;
-}
\ No newline at end of file
+}
diff --git a/src/pages/string/extract-substring/meta.ts b/src/pages/tools/string/extract-substring/meta.ts
similarity index 95%
rename from src/pages/string/extract-substring/meta.ts
rename to src/pages/tools/string/extract-substring/meta.ts
index 30e8f74..1cc7fd2 100644
--- a/src/pages/string/extract-substring/meta.ts
+++ b/src/pages/tools/string/extract-substring/meta.ts
@@ -5,9 +5,9 @@ import { lazy } from 'react';
export const tool = defineTool('string', {
name: 'Extract substring',
path: 'extract-substring',
- // image,
+ icon: '',
description: '',
shortDescription: '',
keywords: ['extract', 'substring'],
component: lazy(() => import('./index'))
-});
\ No newline at end of file
+});
diff --git a/src/pages/tools/string/extract-substring/service.ts b/src/pages/tools/string/extract-substring/service.ts
new file mode 100644
index 0000000..4aacab3
--- /dev/null
+++ b/src/pages/tools/string/extract-substring/service.ts
@@ -0,0 +1,36 @@
+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');
+}
diff --git a/src/pages/string/index.ts b/src/pages/tools/string/index.ts
similarity index 59%
rename from src/pages/string/index.ts
rename to src/pages/tools/string/index.ts
index db674d4..632cab2 100644
--- a/src/pages/string/index.ts
+++ b/src/pages/tools/string/index.ts
@@ -1,3 +1,4 @@
+import { tool as stringRemoveDuplicateLines } from './remove-duplicate-lines/meta';
import { tool as stringRotate } from './rotate/meta';
import { tool as stringQuote } from './quote/meta';
import { tool as stringRot13 } from './rot13/meta';
@@ -10,5 +11,20 @@ import { tool as stringPalindrome } from './palindrome/meta';
import { tool as stringToMorse } from './to-morse/meta';
import { tool as stringSplit } from './split/meta';
import { tool as stringJoin } from './join/meta';
+import { tool as stringReplace } from './text-replacer/meta';
+import { tool as stringRepeat } from './repeat/meta';
-export const stringTools = [stringSplit, stringJoin, stringToMorse];
+export const stringTools = [
+ stringSplit,
+ stringJoin,
+ stringRemoveDuplicateLines,
+ stringToMorse,
+ stringReplace,
+ stringRepeat
+ // stringReverse,
+ // stringRandomizeCase,
+ // stringUppercase,
+ // stringExtractSubstring,
+ // stringCreatePalindrome,
+ // stringPalindrome
+];
diff --git a/src/pages/string/join/index.tsx b/src/pages/tools/string/join/index.tsx
similarity index 57%
rename from src/pages/string/join/index.tsx
rename to src/pages/tools/string/join/index.tsx
index 04dc576..472f562 100644
--- a/src/pages/string/join/index.tsx
+++ b/src/pages/tools/string/join/index.tsx
@@ -1,24 +1,28 @@
import { Box } from '@mui/material';
-import React, { useState } from 'react';
+import React, { useRef, useState } from 'react';
import * as Yup from 'yup';
-import ToolTextInput from '../../../components/input/ToolTextInput';
-import ToolTextResult from '../../../components/result/ToolTextResult';
-import ToolOptions from '../../../components/options/ToolOptions';
+import ToolTextInput from '@components/input/ToolTextInput';
+import ToolTextResult from '@components/result/ToolTextResult';
+import ToolOptions, { GetGroupsType } from '@components/options/ToolOptions';
import { mergeText } from './service';
-import TextFieldWithDesc from '../../../components/options/TextFieldWithDesc';
-import CheckboxWithDesc from '../../../components/options/CheckboxWithDesc';
-import ToolInputAndResult from '../../../components/ToolInputAndResult';
+import TextFieldWithDesc from '@components/options/TextFieldWithDesc';
+import CheckboxWithDesc from '@components/options/CheckboxWithDesc';
+import ToolInputAndResult from '@components/ToolInputAndResult';
-import ToolInfo from '../../../components/ToolInfo';
-import Separator from '../../../components/Separator';
-import Examples from '../../../components/examples/Examples';
+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';
const initialValues = {
joinCharacter: '',
deleteBlank: true,
deleteTrailing: true
};
-
+type InitialValuesType = typeof initialValues;
const validationSchema = Yup.object().shape({
joinCharacter: Yup.string().required('Join character is required'),
deleteBlank: Yup.boolean().required('Delete blank is required'),
@@ -29,13 +33,13 @@ const mergeOptions = {
placeholder: 'Join Character',
description:
'Symbol that connects broken\n' + 'pieces of text. (Space by default.)\n',
- accessor: 'joinCharacter' as keyof typeof initialValues
+ accessor: 'joinCharacter' as keyof InitialValuesType
};
const blankTrailingOptions: {
title: string;
description: string;
- accessor: keyof typeof initialValues;
+ accessor: keyof InitialValuesType;
}[] = [
{
title: 'Delete Blank Lines',
@@ -49,7 +53,7 @@ const blankTrailingOptions: {
}
];
-const exampleCards = [
+const exampleCards: CardExampleType[] = [
{
title: 'Merge a To-Do List',
description:
@@ -62,10 +66,10 @@ feed the cat
make dinner
build a rocket ship and fly away`,
sampleResult: `clean the house and go shopping and feed the cat and make dinner and build a rocket ship and fly away`,
- requiredOptions: {
+ sampleOptions: {
joinCharacter: 'and',
- deleteBlankLines: true,
- deleteTrailingSpaces: true
+ deleteBlank: true,
+ deleteTrailing: true
}
},
{
@@ -78,10 +82,10 @@ processor
mouse
keyboard`,
sampleResult: `computer, memory, processor, mouse, keyboard`,
- requiredOptions: {
+ sampleOptions: {
joinCharacter: ',',
- deleteBlankLines: false,
- deleteTrailingSpaces: false
+ deleteBlank: false,
+ deleteTrailing: false
}
},
{
@@ -101,33 +105,51 @@ u
s
!`,
sampleResult: `Textabulous!`,
- requiredOptions: {
+ sampleOptions: {
joinCharacter: '',
- deleteBlankLines: false,
- deleteTrailingSpaces: false
+ deleteBlank: false,
+ deleteTrailing: false
}
}
];
-export default function JoinText() {
+export default function JoinText({ title }: ToolComponentProps) {
const [input, setInput] = useState('');
const [result, setResult] = useState('');
-
- const compute = (optionsValues: typeof initialValues, input: any) => {
+ const formRef = useRef>(null);
+ const compute = (optionsValues: InitialValuesType, input: any) => {
const { joinCharacter, deleteBlank, deleteTrailing } = optionsValues;
setResult(mergeText(input, deleteBlank, deleteTrailing, joinCharacter));
};
- function changeInputResult(input: string, result: string) {
- setInput(input);
- setResult(result);
-
- const toolsElement = document.getElementById('tool');
- if (toolsElement) {
- toolsElement.scrollIntoView({ behavior: 'smooth' });
+ const getGroups: GetGroupsType = ({
+ values,
+ updateField
+ }) => [
+ {
+ title: 'Text Merged Options',
+ component: (
+ updateField(mergeOptions.accessor, value)}
+ description={mergeOptions.description}
+ />
+ )
+ },
+ {
+ title: 'Blank Lines and Trailing Spaces',
+ component: blankTrailingOptions.map((option) => (
+ updateField(option.accessor, value)}
+ description={option.description}
+ />
+ ))
}
- }
-
+ ];
return (
}
/>
[
- {
- title: 'Text Merged Options',
- component: (
-
- updateField(mergeOptions.accessor, value)
- }
- description={mergeOptions.description}
- />
- )
- },
- {
- title: 'Blank Lines and Trailing Spaces',
- component: blankTrailingOptions.map((option) => (
- updateField(option.accessor, value)}
- description={option.description}
- />
- ))
- }
- ]}
+ getGroups={getGroups}
initialValues={initialValues}
input={input}
- validationSchema={validationSchema}
/>
- ({
- ...card,
- changeInputResult
- }))}
+
);
diff --git a/src/pages/string/join/meta.ts b/src/pages/tools/string/join/meta.ts
similarity index 92%
rename from src/pages/string/join/meta.ts
rename to src/pages/tools/string/join/meta.ts
index aedfe3f..ce895b4 100644
--- a/src/pages/string/join/meta.ts
+++ b/src/pages/tools/string/join/meta.ts
@@ -1,11 +1,10 @@
import { defineTool } from '@tools/defineTool';
import { lazy } from 'react';
-import image from '@assets/text.png';
export const tool = defineTool('string', {
path: 'join',
name: 'Text Joiner',
- image,
+ icon: 'tabler:arrows-join',
description:
"World's Simplest Text Tool World's simplest browser-based utility for joining text. Load your text in the input form on the left and you'll automatically get merged text on the right. Powerful, free, and fast. Load text – get joined lines",
shortDescription: 'Quickly merge texts',
diff --git a/src/pages/string/join/service.ts b/src/pages/tools/string/join/service.ts
similarity index 100%
rename from src/pages/string/join/service.ts
rename to src/pages/tools/string/join/service.ts
diff --git a/src/pages/string/join/string-join.e2e.spec.ts b/src/pages/tools/string/join/string-join.e2e.spec.ts
similarity index 100%
rename from src/pages/string/join/string-join.e2e.spec.ts
rename to src/pages/tools/string/join/string-join.e2e.spec.ts
diff --git a/src/pages/string/join/string-join.service.test.ts b/src/pages/tools/string/join/string-join.service.test.ts
similarity index 100%
rename from src/pages/string/join/string-join.service.test.ts
rename to src/pages/tools/string/join/string-join.service.test.ts
diff --git a/src/pages/string/palindrome/index.tsx b/src/pages/tools/string/palindrome/index.tsx
similarity index 99%
rename from src/pages/string/palindrome/index.tsx
rename to src/pages/tools/string/palindrome/index.tsx
index a7104cb..9b5d8ad 100644
--- a/src/pages/string/palindrome/index.tsx
+++ b/src/pages/tools/string/palindrome/index.tsx
@@ -8,4 +8,4 @@ const validationSchema = Yup.object({
});
export default function Palindrome() {
return Lorem ipsum ;
-}
\ No newline at end of file
+}
diff --git a/src/pages/string/palindrome/meta.ts b/src/pages/tools/string/palindrome/meta.ts
similarity index 95%
rename from src/pages/string/palindrome/meta.ts
rename to src/pages/tools/string/palindrome/meta.ts
index 868cff3..96c7d2e 100644
--- a/src/pages/string/palindrome/meta.ts
+++ b/src/pages/tools/string/palindrome/meta.ts
@@ -5,9 +5,9 @@ import { lazy } from 'react';
export const tool = defineTool('string', {
name: 'Palindrome',
path: 'palindrome',
- // image,
+ icon: '',
description: '',
shortDescription: '',
keywords: ['palindrome'],
component: lazy(() => import('./index'))
-});
\ No newline at end of file
+});
diff --git a/src/pages/tools/string/palindrome/palindrome.service.test.ts b/src/pages/tools/string/palindrome/palindrome.service.test.ts
new file mode 100644
index 0000000..868a9d7
--- /dev/null
+++ b/src/pages/tools/string/palindrome/palindrome.service.test.ts
@@ -0,0 +1,60 @@
+import { describe, expect } 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');
+ });
+});
diff --git a/src/pages/tools/string/palindrome/service.ts b/src/pages/tools/string/palindrome/service.ts
new file mode 100644
index 0000000..b00e788
--- /dev/null
+++ b/src/pages/tools/string/palindrome/service.ts
@@ -0,0 +1,41 @@
+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[] {
+ const 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);
+}
diff --git a/src/pages/string/randomize-case/index.tsx b/src/pages/tools/string/randomize-case/index.tsx
similarity index 99%
rename from src/pages/string/randomize-case/index.tsx
rename to src/pages/tools/string/randomize-case/index.tsx
index fbb7733..33f6b00 100644
--- a/src/pages/string/randomize-case/index.tsx
+++ b/src/pages/tools/string/randomize-case/index.tsx
@@ -8,4 +8,4 @@ const validationSchema = Yup.object({
});
export default function RandomizeCase() {
return Lorem ipsum ;
-}
\ No newline at end of file
+}
diff --git a/src/pages/string/randomize-case/meta.ts b/src/pages/tools/string/randomize-case/meta.ts
similarity index 95%
rename from src/pages/string/randomize-case/meta.ts
rename to src/pages/tools/string/randomize-case/meta.ts
index be2da07..1dcb612 100644
--- a/src/pages/string/randomize-case/meta.ts
+++ b/src/pages/tools/string/randomize-case/meta.ts
@@ -5,9 +5,9 @@ import { lazy } from 'react';
export const tool = defineTool('string', {
name: 'Randomize case',
path: 'randomize-case',
- // image,
+ icon: '',
description: '',
shortDescription: '',
keywords: ['randomize', 'case'],
component: lazy(() => import('./index'))
-});
\ No newline at end of file
+});
diff --git a/src/pages/tools/string/randomize-case/randomize-case.service.test.ts b/src/pages/tools/string/randomize-case/randomize-case.service.test.ts
new file mode 100644
index 0000000..582dd80
--- /dev/null
+++ b/src/pages/tools/string/randomize-case/randomize-case.service.test.ts
@@ -0,0 +1,50 @@
+import { describe, expect, 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);
+ }
+ }
+ });
+});
diff --git a/src/pages/tools/string/randomize-case/service.ts b/src/pages/tools/string/randomize-case/service.ts
new file mode 100644
index 0000000..8d50d5f
--- /dev/null
+++ b/src/pages/tools/string/randomize-case/service.ts
@@ -0,0 +1,8 @@
+export function randomizeCase(input: string): string {
+ return input
+ .split('')
+ .map((char) =>
+ Math.random() < 0.5 ? char.toLowerCase() : char.toUpperCase()
+ )
+ .join('');
+}
diff --git a/src/pages/tools/string/remove-duplicate-lines/index.tsx b/src/pages/tools/string/remove-duplicate-lines/index.tsx
new file mode 100644
index 0000000..06a6caf
--- /dev/null
+++ b/src/pages/tools/string/remove-duplicate-lines/index.tsx
@@ -0,0 +1,260 @@
+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 SimpleRadio from '@components/options/SimpleRadio';
+import CheckboxWithDesc from '@components/options/CheckboxWithDesc';
+import ToolInputAndResult from '@components/ToolInputAndResult';
+import ToolExamples, {
+ CardExampleType
+} from '@components/examples/ToolExamples';
+import { ToolComponentProps } from '@tools/defineTool';
+import { FormikProps } from 'formik';
+import removeDuplicateLines, {
+ DuplicateRemovalMode,
+ DuplicateRemoverOptions,
+ NewlineOption
+} from './service';
+
+// Initial values for our form
+const initialValues: DuplicateRemoverOptions = {
+ mode: 'all',
+ newlines: 'filter',
+ sortLines: false,
+ trimTextLines: false
+};
+
+// Operation mode options
+const operationModes = [
+ {
+ title: 'Remove All Duplicate Lines',
+ description:
+ 'If this option is selected, then all repeated lines across entire text are removed, starting from the second occurrence.',
+ value: 'all' as DuplicateRemovalMode
+ },
+ {
+ title: 'Remove Consecutive Duplicate Lines',
+ description:
+ 'If this option is selected, then only consecutive repeated lines are removed.',
+ value: 'consecutive' as DuplicateRemovalMode
+ },
+ {
+ title: 'Leave Absolutely Unique Text Lines',
+ description:
+ 'If this option is selected, then all lines that appear more than once are removed.',
+ value: 'unique' as DuplicateRemovalMode
+ }
+];
+
+// Newlines options
+const newlineOptions = [
+ {
+ title: 'Preserve All Newlines',
+ description: 'Leave all empty lines in the output.',
+ value: 'preserve' as NewlineOption
+ },
+ {
+ title: 'Filter All Newlines',
+ description: 'Process newlines as regular lines.',
+ value: 'filter' as NewlineOption
+ },
+ {
+ title: 'Delete All Newlines',
+ description: 'Before filtering uniques, remove all newlines.',
+ value: 'delete' as NewlineOption
+ }
+];
+
+// Example cards for demonstration
+const exampleCards: CardExampleType[] = [
+ {
+ title: 'Remove Duplicate Items from List',
+ description:
+ 'Removes duplicate items from a shopping list, keeping only the first occurrence of each item.',
+ sampleText: `Apples
+Bananas
+Milk
+Eggs
+Bread
+Milk
+Cheese
+Apples
+Yogurt`,
+ sampleResult: `Apples
+Bananas
+Milk
+Eggs
+Bread
+Cheese
+Yogurt`,
+ sampleOptions: {
+ ...initialValues,
+ mode: 'all',
+ newlines: 'filter',
+ sortLines: false,
+ trimTextLines: false
+ }
+ },
+ {
+ title: 'Clean Consecutive Duplicates',
+ description:
+ 'Removes consecutive duplicates from log entries, which often happen when a system repeatedly logs the same error.',
+ sampleText: `[INFO] Application started
+[ERROR] Connection failed
+[ERROR] Connection failed
+[ERROR] Connection failed
+[INFO] Retrying connection
+[ERROR] Authentication error
+[ERROR] Authentication error
+[INFO] Connection established`,
+ sampleResult: `[INFO] Application started
+[ERROR] Connection failed
+[INFO] Retrying connection
+[ERROR] Authentication error
+[INFO] Connection established`,
+ sampleOptions: {
+ ...initialValues,
+ mode: 'consecutive',
+ newlines: 'filter',
+ sortLines: false,
+ trimTextLines: false
+ }
+ },
+ {
+ title: 'Extract Unique Entries Only',
+ description:
+ 'Filters a list to keep only entries that appear exactly once, removing any duplicated items entirely.',
+ sampleText: `Red
+Blue
+Green
+Blue
+Yellow
+Purple
+Red
+Orange`,
+ sampleResult: `Green
+Yellow
+Purple
+Orange`,
+ sampleOptions: {
+ ...initialValues,
+ mode: 'unique',
+ newlines: 'filter',
+ sortLines: false,
+ trimTextLines: false
+ }
+ },
+ {
+ title: 'Sort and Clean Data',
+ description:
+ 'Removes duplicate items from a list, trims whitespace, and sorts the results alphabetically.',
+ sampleText: ` Apple
+Banana
+ Cherry
+Apple
+ Banana
+Dragonfruit
+ Elderberry `,
+ sampleResult: `Apple
+Banana
+Cherry
+Dragonfruit
+Elderberry`,
+ sampleOptions: {
+ ...initialValues,
+ mode: 'all',
+ newlines: 'filter',
+ sortLines: true,
+ trimTextLines: true
+ }
+ }
+];
+
+export default function RemoveDuplicateLines({ title }: ToolComponentProps) {
+ const [input, setInput] = useState('');
+ const [result, setResult] = useState('');
+ const formRef = useRef>(null);
+
+ const computeExternal = (
+ optionsValues: typeof initialValues,
+ inputText: string
+ ) => {
+ setResult(removeDuplicateLines(inputText, optionsValues));
+ };
+
+ const getGroups: GetGroupsType = ({
+ values,
+ updateField
+ }) => [
+ {
+ title: 'Operation Mode',
+ component: operationModes.map(({ title, description, value }) => (
+ updateField('mode', value)}
+ />
+ ))
+ },
+ {
+ title: 'Newlines, Tabs and Spaces',
+ component: [
+ ...newlineOptions.map(({ title, description, value }) => (
+ updateField('newlines', value)}
+ />
+ )),
+ updateField('trimTextLines', checked)}
+ />
+ ]
+ },
+ {
+ title: 'Sort Lines',
+ component: [
+ updateField('sortLines', checked)}
+ />
+ ]
+ }
+ ];
+
+ return (
+
+ }
+ result={
+
+ }
+ />
+
+
+
+ );
+}
diff --git a/src/pages/tools/string/remove-duplicate-lines/meta.ts b/src/pages/tools/string/remove-duplicate-lines/meta.ts
new file mode 100644
index 0000000..a1a6a8b
--- /dev/null
+++ b/src/pages/tools/string/remove-duplicate-lines/meta.ts
@@ -0,0 +1,13 @@
+import { defineTool } from '@tools/defineTool';
+import { lazy } from 'react';
+
+export const tool = defineTool('string', {
+ name: 'Remove duplicate lines',
+ path: 'remove-duplicate-lines',
+ icon: 'pepicons-print:duplicate-off',
+ description:
+ "Load your text in the input form on the left and you'll instantly get text with no duplicate lines in the output area. Powerful, free, and fast. Load text lines – get unique text lines",
+ shortDescription: 'Quickly delete all repeated lines from text',
+ keywords: ['remove', 'duplicate', 'lines'],
+ component: lazy(() => import('./index'))
+});
diff --git a/src/pages/tools/string/remove-duplicate-lines/remove-duplicate-lines.service.test.ts b/src/pages/tools/string/remove-duplicate-lines/remove-duplicate-lines.service.test.ts
new file mode 100644
index 0000000..9d8769a
--- /dev/null
+++ b/src/pages/tools/string/remove-duplicate-lines/remove-duplicate-lines.service.test.ts
@@ -0,0 +1,200 @@
+import { describe, expect, it } from 'vitest';
+import removeDuplicateLines, { DuplicateRemoverOptions } from './service';
+
+describe('removeDuplicateLines function', () => {
+ // Test for 'all' duplicate removal mode
+ describe('mode: all', () => {
+ it('should remove all duplicates keeping first occurrence', () => {
+ const input = 'line1\nline2\nline1\nline3\nline2';
+ const options: DuplicateRemoverOptions = {
+ mode: 'all',
+ newlines: 'filter',
+ sortLines: false,
+ trimTextLines: false
+ };
+ const result = removeDuplicateLines(input, options);
+ expect(result).toBe('line1\nline2\nline3');
+ });
+
+ it('should handle case-sensitive duplicates correctly', () => {
+ const input = 'Line1\nline1\nLine2\nline2';
+ const options: DuplicateRemoverOptions = {
+ mode: 'all',
+ newlines: 'filter',
+ sortLines: false,
+ trimTextLines: false
+ };
+ const result = removeDuplicateLines(input, options);
+ expect(result).toBe('Line1\nline1\nLine2\nline2');
+ });
+ });
+
+ // Test for 'consecutive' duplicate removal mode
+ describe('mode: consecutive', () => {
+ it('should remove only consecutive duplicates', () => {
+ const input = 'line1\nline1\nline2\nline3\nline3\nline1';
+ const options: DuplicateRemoverOptions = {
+ mode: 'consecutive',
+ newlines: 'filter',
+ sortLines: false,
+ trimTextLines: false
+ };
+ const result = removeDuplicateLines(input, options);
+ expect(result).toBe('line1\nline2\nline3\nline1');
+ });
+ });
+
+ // Test for 'unique' duplicate removal mode
+ describe('mode: unique', () => {
+ it('should keep only lines that appear exactly once', () => {
+ const input = 'line1\nline2\nline1\nline3\nline4\nline4';
+ const options: DuplicateRemoverOptions = {
+ mode: 'unique',
+ newlines: 'filter',
+ sortLines: false,
+ trimTextLines: false
+ };
+ const result = removeDuplicateLines(input, options);
+ expect(result).toBe('line2\nline3');
+ });
+ });
+
+ // Test for newlines handling
+ describe('newlines option', () => {
+ it('should filter newlines when newlines is set to filter', () => {
+ const input = 'line1\n\nline2\n\n\nline3';
+ const options: DuplicateRemoverOptions = {
+ mode: 'all',
+ newlines: 'filter',
+ sortLines: false,
+ trimTextLines: false
+ };
+ const result = removeDuplicateLines(input, options);
+ expect(result).toBe('line1\n\nline2\nline3');
+ });
+
+ it('should delete newlines when newlines is set to delete', () => {
+ const input = 'line1\n\nline2\n\n\nline3';
+ const options: DuplicateRemoverOptions = {
+ mode: 'all',
+ newlines: 'delete',
+ sortLines: false,
+ trimTextLines: false
+ };
+ const result = removeDuplicateLines(input, options);
+ expect(result).toBe('line1\nline2\nline3');
+ });
+
+ it('should preserve newlines when newlines is set to preserve', () => {
+ const input = 'line1\n\nline2\n\nline2\nline3';
+ const options: DuplicateRemoverOptions = {
+ mode: 'all',
+ newlines: 'preserve',
+ sortLines: false,
+ trimTextLines: false
+ };
+ const result = removeDuplicateLines(input, options);
+ // This test needs careful consideration of the expected behavior
+ expect(result).not.toContain('line2\nline2');
+ expect(result).toContain('line1');
+ expect(result).toContain('line2');
+ expect(result).toContain('line3');
+ });
+ });
+
+ // Test for sorting
+ describe('sortLines option', () => {
+ it('should sort lines when sortLines is true', () => {
+ const input = 'line3\nline1\nline2';
+ const options: DuplicateRemoverOptions = {
+ mode: 'all',
+ newlines: 'filter',
+ sortLines: true,
+ trimTextLines: false
+ };
+ const result = removeDuplicateLines(input, options);
+ expect(result).toBe('line1\nline2\nline3');
+ });
+ });
+
+ // Test for trimming
+ describe('trimTextLines option', () => {
+ it('should trim lines when trimTextLines is true', () => {
+ const input = ' line1 \n line2 \nline3';
+ const options: DuplicateRemoverOptions = {
+ mode: 'all',
+ newlines: 'filter',
+ sortLines: false,
+ trimTextLines: true
+ };
+ const result = removeDuplicateLines(input, options);
+ expect(result).toBe('line1\nline2\nline3');
+ });
+
+ it('should consider trimmed lines as duplicates', () => {
+ const input = ' line1 \nline1\n line2\nline2 ';
+ const options: DuplicateRemoverOptions = {
+ mode: 'all',
+ newlines: 'filter',
+ sortLines: false,
+ trimTextLines: true
+ };
+ const result = removeDuplicateLines(input, options);
+ expect(result).toBe('line1\nline2');
+ });
+ });
+
+ // Combined scenarios
+ describe('combined options', () => {
+ it('should handle all options together correctly', () => {
+ const input = ' line3 \nline1\n\nline3\nline2\nline1';
+ const options: DuplicateRemoverOptions = {
+ mode: 'all',
+ newlines: 'delete',
+ sortLines: true,
+ trimTextLines: true
+ };
+ const result = removeDuplicateLines(input, options);
+ expect(result).toBe('line1\nline2\nline3');
+ });
+ });
+
+ // Edge cases
+ describe('edge cases', () => {
+ it('should handle empty input', () => {
+ const input = '';
+ const options: DuplicateRemoverOptions = {
+ mode: 'all',
+ newlines: 'filter',
+ sortLines: false,
+ trimTextLines: false
+ };
+ const result = removeDuplicateLines(input, options);
+ expect(result).toBe('');
+ });
+
+ it('should handle input with only newlines', () => {
+ const input = '\n\n\n';
+ const options: DuplicateRemoverOptions = {
+ mode: 'all',
+ newlines: 'filter',
+ sortLines: false,
+ trimTextLines: false
+ };
+ const result = removeDuplicateLines(input, options);
+ expect(result).toBe('');
+ });
+
+ it('should handle input with only whitespace', () => {
+ const input = ' \n \n ';
+ const options: DuplicateRemoverOptions = {
+ mode: 'all',
+ newlines: 'filter',
+ sortLines: false,
+ trimTextLines: true
+ };
+ const result = removeDuplicateLines(input, options);
+ expect(result).toBe('');
+ });
+ });
+});
diff --git a/src/pages/tools/string/remove-duplicate-lines/service.ts b/src/pages/tools/string/remove-duplicate-lines/service.ts
new file mode 100644
index 0000000..3338cc8
--- /dev/null
+++ b/src/pages/tools/string/remove-duplicate-lines/service.ts
@@ -0,0 +1,88 @@
+export type NewlineOption = 'preserve' | 'filter' | 'delete';
+export type DuplicateRemovalMode = 'all' | 'consecutive' | 'unique';
+
+export interface DuplicateRemoverOptions {
+ mode: DuplicateRemovalMode;
+ newlines: NewlineOption;
+ sortLines: boolean;
+ trimTextLines: boolean;
+}
+
+/**
+ * Removes duplicate lines from text based on specified options
+ * @param text The input text to process
+ * @param options Configuration options for text processing
+ * @returns Processed text with duplicates removed according to options
+ */
+export default function removeDuplicateLines(
+ text: string,
+ options: DuplicateRemoverOptions
+): string {
+ // Split the text into individual lines
+ let lines = text.split('\n');
+
+ // Process newlines based on option
+ if (options.newlines === 'delete') {
+ // Remove all empty lines
+ lines = lines.filter((line) => line.trim() !== '');
+ }
+
+ // Trim lines if option is selected
+ if (options.trimTextLines) {
+ lines = lines.map((line) => line.trim());
+ }
+
+ // Remove duplicates based on mode
+ let processedLines: string[] = [];
+
+ if (options.mode === 'all') {
+ // Remove all duplicates, keeping only first occurrence
+ const seen = new Set();
+ processedLines = lines.filter((line) => {
+ if (seen.has(line)) {
+ return false;
+ }
+ seen.add(line);
+ return true;
+ });
+ } else if (options.mode === 'consecutive') {
+ // Remove only consecutive duplicates
+ processedLines = lines.filter((line, index, arr) => {
+ return index === 0 || line !== arr[index - 1];
+ });
+ } else if (options.mode === 'unique') {
+ // Leave only absolutely unique lines
+ const lineCount = new Map();
+ lines.forEach((line) => {
+ lineCount.set(line, (lineCount.get(line) || 0) + 1);
+ });
+
+ processedLines = lines.filter((line) => lineCount.get(line) === 1);
+ }
+
+ // Sort lines if option is selected
+ if (options.sortLines) {
+ processedLines.sort();
+ }
+
+ // Process newlines for output
+ if (options.newlines === 'filter') {
+ // Process newlines as regular lines (already done by default)
+ } else if (options.newlines === 'preserve') {
+ // Make sure empty lines are preserved in the output
+ processedLines = text.split('\n').map((line) => {
+ if (line.trim() === '') return line;
+ return processedLines.includes(line) ? line : '';
+ });
+ }
+
+ return processedLines.join('\n');
+}
+
+// Example usage:
+// const result = removeDuplicateLines(inputText, {
+// mode: 'all',
+// newlines: 'filter',
+// sortLines: false,
+// trimTextLines: true
+// });
diff --git a/src/pages/tools/string/repeat/index.tsx b/src/pages/tools/string/repeat/index.tsx
new file mode 100644
index 0000000..0a954e2
--- /dev/null
+++ b/src/pages/tools/string/repeat/index.tsx
@@ -0,0 +1,114 @@
+import { Box } from '@mui/material';
+import { useState } from 'react';
+import ToolTextResult from '@components/result/ToolTextResult';
+import { GetGroupsType } from '@components/options/ToolOptions';
+import { repeatText } from './service';
+import TextFieldWithDesc from '@components/options/TextFieldWithDesc';
+import ToolTextInput from '@components/input/ToolTextInput';
+import { initialValues, InitialValuesType } from './initialValues';
+import ToolContent from '@components/ToolContent';
+import { CardExampleType } from '@components/examples/ToolExamples';
+import { ToolComponentProps } from '@tools/defineTool';
+
+const exampleCards: CardExampleType[] = [
+ {
+ title: 'Repeat word five times',
+ description: 'Repeats "Hello!" five times without any delimiter.',
+ sampleText: 'Hello! ',
+ sampleResult: 'Hello! Hello! Hello! Hello! Hello! ',
+ sampleOptions: {
+ textToRepeat: 'Hello! ',
+ repeatAmount: '5',
+ delimiter: ''
+ }
+ },
+ {
+ title: 'Repeat phrase with comma',
+ description:
+ 'Repeats "Good job" three times, separated by commas and spaces.',
+ sampleText: 'Good job',
+ sampleResult: 'Good job, Good job, Good job',
+ sampleOptions: {
+ textToRepeat: 'Good job',
+ repeatAmount: '3',
+ delimiter: ', '
+ }
+ },
+ {
+ title: 'Repeat number with space',
+ description: 'Repeats the number "42" four times, separated by spaces.',
+ sampleText: '42',
+ sampleResult: '42 42 42 42',
+ sampleOptions: {
+ textToRepeat: '42',
+ repeatAmount: '4',
+ delimiter: ' '
+ }
+ }
+];
+
+export default function Replacer({ title }: ToolComponentProps) {
+ const [input, setInput] = useState('');
+ const [result, setResult] = useState('');
+
+ function compute(optionsValues: InitialValuesType, input: string) {
+ setResult(repeatText(optionsValues, input));
+ }
+
+ const getGroups: GetGroupsType = ({
+ values,
+ updateField
+ }) => [
+ {
+ title: 'Text Repetitions',
+ component: (
+
+ updateField('repeatAmount', val)}
+ type={'number'}
+ />
+
+ )
+ },
+ {
+ title: 'Repetitions Delimiter',
+ component: (
+
+ updateField('delimiter', val)}
+ type={'text'}
+ />
+
+ )
+ }
+ ];
+
+ return (
+
+ }
+ resultComponent={
+
+ }
+ toolInfo={{
+ title: 'Repeat text',
+ description:
+ 'This tool allows you to repeat a given text multiple times with an optional separator.'
+ }}
+ exampleCards={exampleCards}
+ />
+ );
+}
diff --git a/src/pages/tools/string/repeat/initialValues.ts b/src/pages/tools/string/repeat/initialValues.ts
new file mode 100644
index 0000000..9e543be
--- /dev/null
+++ b/src/pages/tools/string/repeat/initialValues.ts
@@ -0,0 +1,11 @@
+export type InitialValuesType = {
+ textToRepeat: string;
+ repeatAmount: string;
+ delimiter: string;
+};
+
+export const initialValues: InitialValuesType = {
+ textToRepeat: '',
+ repeatAmount: '5',
+ delimiter: ''
+};
diff --git a/src/pages/tools/string/repeat/meta.ts b/src/pages/tools/string/repeat/meta.ts
new file mode 100644
index 0000000..69f5d79
--- /dev/null
+++ b/src/pages/tools/string/repeat/meta.ts
@@ -0,0 +1,13 @@
+import { defineTool } from '@tools/defineTool';
+import { lazy } from 'react';
+
+export const tool = defineTool('string', {
+ name: 'Repeat text',
+ path: 'repeat',
+ shortDescription: 'Repeat text multiple times',
+ icon: 'material-symbols-light:replay',
+ description:
+ 'This tool allows you to repeat a given text multiple times with an optional separator.',
+ keywords: ['text', 'repeat'],
+ component: lazy(() => import('./index'))
+});
diff --git a/src/pages/tools/string/repeat/repeatText.service.test.ts b/src/pages/tools/string/repeat/repeatText.service.test.ts
new file mode 100644
index 0000000..6a8a0c1
--- /dev/null
+++ b/src/pages/tools/string/repeat/repeatText.service.test.ts
@@ -0,0 +1,55 @@
+import { describe, expect, it } from 'vitest';
+import { repeatText } from './service';
+import { initialValues } from './initialValues';
+
+describe('repeatText function', () => {
+ it('should repeat the letter correctly', () => {
+ const text = 'i';
+ const repeatAmount = '5';
+ const result = repeatText({ ...initialValues, repeatAmount }, text);
+ expect(result).toBe('iiiii');
+ });
+
+ it('should repeat the word correctly', () => {
+ const text = 'hello';
+ const repeatAmount = '3';
+ const result = repeatText({ ...initialValues, repeatAmount }, text);
+ expect(result).toBe('hellohellohello');
+ });
+
+ it('should repeat the word with a space delimiter correctly', () => {
+ const text = 'word';
+ const repeatAmount = '3';
+ const delimiter = ' ';
+ const result = repeatText(
+ { ...initialValues, repeatAmount, delimiter },
+ text
+ );
+ expect(result).toBe('word word word');
+ });
+
+ it('should repeat the word with a space and a comma delimiter correctly', () => {
+ const text = 'test';
+ const repeatAmount = '3';
+ const delimiter = ', ';
+ const result = repeatText(
+ { ...initialValues, repeatAmount, delimiter },
+ text
+ );
+ expect(result).toBe('test, test, test');
+ });
+
+ it('Should not repeat text if repeatAmount is zero', () => {
+ const text = 'something';
+ const repeatAmount = '0';
+ const result = repeatText({ ...initialValues, repeatAmount }, text);
+ expect(result).toBe('');
+ });
+
+ it('Should not repeat text if repeatAmount is not entered', () => {
+ const text = 'something';
+ const repeatAmount = '';
+ const result = repeatText({ ...initialValues, repeatAmount }, text);
+ expect(result).toBe('');
+ });
+});
diff --git a/src/pages/tools/string/repeat/service.ts b/src/pages/tools/string/repeat/service.ts
new file mode 100644
index 0000000..a45130e
--- /dev/null
+++ b/src/pages/tools/string/repeat/service.ts
@@ -0,0 +1,9 @@
+import { InitialValuesType } from './initialValues';
+
+export function repeatText(options: InitialValuesType, text: string) {
+ const { repeatAmount, delimiter } = options;
+
+ const parsedAmount = parseInt(repeatAmount) || 0;
+
+ return Array(parsedAmount).fill(text).join(delimiter);
+}
diff --git a/src/pages/string/reverse/index.tsx b/src/pages/tools/string/reverse/index.tsx
similarity index 99%
rename from src/pages/string/reverse/index.tsx
rename to src/pages/tools/string/reverse/index.tsx
index e017afe..1e40781 100644
--- a/src/pages/string/reverse/index.tsx
+++ b/src/pages/tools/string/reverse/index.tsx
@@ -8,4 +8,4 @@ const validationSchema = Yup.object({
});
export default function Reverse() {
return Lorem ipsum ;
-}
\ No newline at end of file
+}
diff --git a/src/pages/string/reverse/meta.ts b/src/pages/tools/string/reverse/meta.ts
similarity index 95%
rename from src/pages/string/reverse/meta.ts
rename to src/pages/tools/string/reverse/meta.ts
index 886b604..f38b599 100644
--- a/src/pages/string/reverse/meta.ts
+++ b/src/pages/tools/string/reverse/meta.ts
@@ -5,9 +5,9 @@ import { lazy } from 'react';
export const tool = defineTool('string', {
name: 'Reverse',
path: 'reverse',
- // image,
+ icon: '',
description: '',
shortDescription: '',
keywords: ['reverse'],
component: lazy(() => import('./index'))
-});
\ No newline at end of file
+});
diff --git a/src/pages/tools/string/reverse/reverse.service.test.ts b/src/pages/tools/string/reverse/reverse.service.test.ts
new file mode 100644
index 0000000..c602f56
--- /dev/null
+++ b/src/pages/tools/string/reverse/reverse.service.test.ts
@@ -0,0 +1,52 @@
+import { describe, expect, 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');
+ });
+});
diff --git a/src/pages/tools/string/reverse/service.ts b/src/pages/tools/string/reverse/service.ts
new file mode 100644
index 0000000..b76206f
--- /dev/null
+++ b/src/pages/tools/string/reverse/service.ts
@@ -0,0 +1,30 @@
+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');
+}
diff --git a/src/pages/tools/string/split/index.tsx b/src/pages/tools/string/split/index.tsx
new file mode 100644
index 0000000..4c847ad
--- /dev/null
+++ b/src/pages/tools/string/split/index.tsx
@@ -0,0 +1,218 @@
+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 { compute, SplitOperatorType } from './service';
+import RadioWithTextField from '@components/options/RadioWithTextField';
+import TextFieldWithDesc from '@components/options/TextFieldWithDesc';
+import ToolInputAndResult from '@components/ToolInputAndResult';
+import ToolExamples, {
+ CardExampleType
+} from '@components/examples/ToolExamples';
+import { ToolComponentProps } from '@tools/defineTool';
+import { FormikProps } from 'formik';
+
+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'
+ }
+];
+
+const exampleCards: CardExampleType[] = [
+ {
+ title: 'Split German Numbers',
+ description:
+ 'In this example, we break the text into pieces by two characters – a comma and space. As a result, we get a column of numbers from 1 to 10 in German.',
+ sampleText: `1 - eins, 2 - zwei, 3 - drei, 4 - vier, 5 - fünf, 6 - sechs, 7 - sieben, 8 - acht, 9 - neun, 10 - zehn`,
+ sampleResult: `1 - eins
+2 - zwei
+3 - drei
+4 - vier
+5 - fünf
+6 - sechs
+7 - sieben
+8 - acht
+9 - neun
+10 - zehn`,
+ sampleOptions: {
+ ...initialValues,
+ symbolValue: ',',
+ splitSeparatorType: 'symbol',
+ outputSeparator: '\n'
+ }
+ },
+ {
+ title: 'Text Cleanup via a Regular Expression',
+ description:
+ 'In this example, we use a super smart regular expression trick to clean-up the text. This regexp finds all non-alphabetic characters and splits the text into pieces by these non-alphabetic chars. As a result, we extract only those parts of the text that contain Latin letters and words.',
+ sampleText: `Finding%№1.65*;?words()is'12#easy_`,
+ sampleResult: `Finding
+words
+is
+easy`,
+ sampleOptions: {
+ ...initialValues,
+ regexValue: '[^a-zA-Z]+',
+ splitSeparatorType: 'regex',
+ outputSeparator: '\n'
+ }
+ },
+ {
+ title: 'Three-dot Output Separator',
+ description:
+ 'This example splits the text by spaces and then places three dots between the words.',
+ sampleText: `If you started with $0.01 and doubled your money every day, it would take 27 days to become a millionaire.`,
+ sampleResult: `If...you...started...with...$0.01...and...doubled...your...money...every...day,...it...would...take...27...days...to...become...a...millionaire.!`,
+ sampleOptions: {
+ ...initialValues,
+ symbolValue: '',
+ splitSeparatorType: 'symbol',
+ outputSeparator: '...'
+ }
+ }
+];
+
+export default function SplitText({ title }: ToolComponentProps) {
+ const [input, setInput] = useState('');
+ const [result, setResult] = useState('');
+ const formRef = useRef>(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 getGroups: GetGroupsType = ({
+ values,
+ updateField
+ }) => [
+ {
+ title: 'Split separator options',
+ component: splitOperators.map(({ title, description, type }) => (
+ updateField('splitSeparatorType', type)}
+ onTextChange={(val) => updateField(`${type}Value`, val)}
+ />
+ ))
+ },
+ {
+ title: 'Output separator options',
+ component: outputOptions.map((option) => (
+ updateField(option.accessor, value)}
+ description={option.description}
+ />
+ ))
+ }
+ ];
+ return (
+
+ }
+ result={ }
+ />
+
+
+
+ );
+}
diff --git a/src/pages/string/split/meta.ts b/src/pages/tools/string/split/meta.ts
similarity index 92%
rename from src/pages/string/split/meta.ts
rename to src/pages/tools/string/split/meta.ts
index 01acca2..1cd519b 100644
--- a/src/pages/string/split/meta.ts
+++ b/src/pages/tools/string/split/meta.ts
@@ -5,7 +5,7 @@ import image from '@assets/text.png';
export const tool = defineTool('string', {
path: 'split',
name: 'Text splitter',
- image,
+ icon: 'material-symbols-light:arrow-split',
description:
"World's simplest browser-based utility for splitting text. Load your text in the input form on the left and you'll automatically get pieces of this text on the right. Powerful, free, and fast. Load text – get chunks.",
shortDescription: 'Quickly split a text',
diff --git a/src/pages/string/split/service.ts b/src/pages/tools/string/split/service.ts
similarity index 100%
rename from src/pages/string/split/service.ts
rename to src/pages/tools/string/split/service.ts
diff --git a/src/pages/string/split/string-split.service.test.ts b/src/pages/tools/string/split/string-split.service.test.ts
similarity index 100%
rename from src/pages/string/split/string-split.service.test.ts
rename to src/pages/tools/string/split/string-split.service.test.ts
diff --git a/src/pages/tools/string/text-replacer/index.tsx b/src/pages/tools/string/text-replacer/index.tsx
new file mode 100644
index 0000000..ee99fe0
--- /dev/null
+++ b/src/pages/tools/string/text-replacer/index.tsx
@@ -0,0 +1,147 @@
+import { Box } from '@mui/material';
+import { useState } from 'react';
+import ToolTextResult from '@components/result/ToolTextResult';
+import { GetGroupsType } from '@components/options/ToolOptions';
+import { replaceText } from './service';
+import TextFieldWithDesc from '@components/options/TextFieldWithDesc';
+import ToolTextInput from '@components/input/ToolTextInput';
+import SimpleRadio from '@components/options/SimpleRadio';
+import { initialValues, InitialValuesType } from './initialValues';
+import ToolContent from '@components/ToolContent';
+import { CardExampleType } from '@components/examples/ToolExamples';
+import { ToolComponentProps } from '@tools/defineTool';
+
+const exampleCards: CardExampleType[] = [
+ {
+ title: 'Replace specific word in text',
+ description:
+ 'In this example we will replace the word "hello" with the word "hi". This example doesn\'t use regular expressions.',
+ sampleText: 'hello, how are you today? hello!',
+ sampleResult: 'hi, how are you today? hi!',
+ sampleOptions: {
+ textToReplace: 'hello, how are you today? hello!',
+ searchValue: 'hello',
+ searchRegexp: '',
+ replaceValue: 'hi',
+ mode: 'text'
+ }
+ },
+ {
+ title: 'Replace all numbers in text',
+ description:
+ 'In this example we will replace all numbers in numbers with * using regexp. In the output we will get text with numbers replaced with *.',
+ sampleText: 'The price is 100$, and the discount is 20%.',
+ sampleResult: 'The price is X$, and the discount is X%.',
+ sampleOptions: {
+ textToReplace: 'The price is 100$, and the discount is 20%.',
+ searchValue: '',
+ searchRegexp: '/\\d+/g',
+ replaceValue: '*',
+ mode: 'regexp'
+ }
+ },
+ {
+ title: 'Replace all dates in text',
+ description:
+ 'In this example we will replace all dates in the format YYYY-MM-DD with the word DATE using regexp. The output will have all the dates replaced with the word DATE.',
+ sampleText:
+ 'The event will take place on 2025-03-10, and the deadline is 2025-03-15.',
+ sampleResult:
+ 'The event will take place on DATE, and the deadline is DATE.',
+ sampleOptions: {
+ textToReplace:
+ 'The event will take place on 2025-03-10, and the deadline is 2025-03-15.',
+ searchValue: '',
+ searchRegexp: '/\\d{4}-\\d{2}-\\d{2}/g',
+ replaceValue: 'DATE',
+ mode: 'regexp'
+ }
+ }
+];
+
+export default function Replacer({ title }: ToolComponentProps) {
+ const [input, setInput] = useState('');
+ const [result, setResult] = useState('');
+
+ function compute(optionsValues: InitialValuesType, input: string) {
+ setResult(replaceText(optionsValues, input));
+ }
+
+ const getGroups: GetGroupsType = ({
+ values,
+ updateField
+ }) => [
+ {
+ title: 'Search text',
+ component: (
+
+ updateField('mode', 'text')}
+ checked={values.mode === 'text'}
+ title={'Find This Pattern in Text'}
+ />
+ updateField('searchValue', val)}
+ type={'text'}
+ />
+ updateField('mode', 'regexp')}
+ checked={values.mode === 'regexp'}
+ title={'Find a Pattern Using a RegExp'}
+ />
+ updateField('searchRegexp', val)}
+ type={'text'}
+ />
+
+ )
+ },
+ {
+ title: 'Replace Text',
+ component: (
+
+ updateField('replaceValue', val)}
+ type={'text'}
+ />
+
+ )
+ }
+ ];
+
+ return (
+
+ }
+ resultComponent={
+
+ }
+ toolInfo={{
+ title: 'Text Replacer',
+ description:
+ 'Easily replace specific text in your content with this simple, browser-based tool. Just input your text, set the text you want to replace and the replacement value, and instantly get the updated version.'
+ }}
+ exampleCards={exampleCards}
+ />
+ );
+}
diff --git a/src/pages/tools/string/text-replacer/initialValues.ts b/src/pages/tools/string/text-replacer/initialValues.ts
new file mode 100644
index 0000000..9c52e84
--- /dev/null
+++ b/src/pages/tools/string/text-replacer/initialValues.ts
@@ -0,0 +1,15 @@
+export type InitialValuesType = {
+ textToReplace: string;
+ searchValue: string;
+ searchRegexp: string;
+ replaceValue: string;
+ mode: 'text' | 'regexp';
+};
+
+export const initialValues: InitialValuesType = {
+ textToReplace: '',
+ searchValue: '',
+ searchRegexp: '',
+ replaceValue: '',
+ mode: 'text'
+};
diff --git a/src/pages/tools/string/text-replacer/meta.ts b/src/pages/tools/string/text-replacer/meta.ts
new file mode 100644
index 0000000..a7162f7
--- /dev/null
+++ b/src/pages/tools/string/text-replacer/meta.ts
@@ -0,0 +1,13 @@
+import { defineTool } from '@tools/defineTool';
+import { lazy } from 'react';
+
+export const tool = defineTool('string', {
+ name: 'Text Replacer',
+ path: 'replacer',
+ shortDescription: 'Quickly replace text in your content',
+ icon: 'material-symbols-light:find-replace',
+ description:
+ 'Easily replace specific text in your content with this simple, browser-based tool. Just input your text, set the text you want to replace and the replacement value, and instantly get the updated version.',
+ keywords: ['text', 'replace'],
+ component: lazy(() => import('./index'))
+});
diff --git a/src/pages/tools/string/text-replacer/replaceText.service.test.ts b/src/pages/tools/string/text-replacer/replaceText.service.test.ts
new file mode 100644
index 0000000..5df2aae
--- /dev/null
+++ b/src/pages/tools/string/text-replacer/replaceText.service.test.ts
@@ -0,0 +1,175 @@
+import { describe, expect, it } from 'vitest';
+import { replaceText } from './service';
+import { initialValues } from './initialValues';
+
+describe('replaceText function (text mode)', () => {
+ const mode = 'text';
+
+ it('should replace the word in the text correctly', () => {
+ const text = 'Lorem ipsum odor amet, consectetuer adipiscing elit.';
+ const searchValue = 'ipsum';
+ const replaceValue = 'vitae';
+ const result = replaceText(
+ { ...initialValues, searchValue, replaceValue, mode },
+ text
+ );
+ expect(result).toBe('Lorem vitae odor amet, consectetuer adipiscing elit.');
+ });
+
+ it('should replace letters in the text correctly', () => {
+ const text =
+ 'Luctus penatibus montes elementum lacus mus vivamus lacus laoreet.';
+ const searchValue = 'e';
+ const replaceValue = 'u';
+ const result = replaceText(
+ { ...initialValues, searchValue, replaceValue, mode },
+ text
+ );
+ expect(result).toBe(
+ 'Luctus punatibus montus ulumuntum lacus mus vivamus lacus laoruut.'
+ );
+ });
+
+ it('should return the original text if one of the required arguments is an empty string', () => {
+ const text =
+ 'Nostra netus quisque ornare neque dolor sem nostra venenatis.';
+ expect(
+ replaceText(
+ { ...initialValues, searchValue: '', replaceValue: 'test', mode },
+ text
+ )
+ ).toBe('Nostra netus quisque ornare neque dolor sem nostra venenatis.');
+ expect(
+ replaceText(
+ { ...initialValues, searchValue: 'ornare', replaceValue: 'test', mode },
+ ''
+ )
+ ).toBe('');
+ });
+
+ it('should replace multiple occurrences of the word correctly', () => {
+ const text = 'apple orange apple banana apple';
+ const searchValue = 'apple';
+ const replaceValue = 'grape';
+ const result = replaceText(
+ { ...initialValues, searchValue, replaceValue, mode },
+ text
+ );
+ expect(result).toBe('grape orange grape banana grape');
+ });
+
+ it('should return the original text if the replace value is an empty string', () => {
+ const text = 'apple orange apple banana apple';
+ const searchValue = 'apple';
+ const replaceValue = '';
+ const result = replaceText(
+ { ...initialValues, searchValue, replaceValue, mode },
+ text
+ );
+ expect(result).toBe(' orange banana ');
+ });
+
+ it('should return the original text if the search value is not found', () => {
+ const text = 'apple orange banana';
+ const searchValue = 'grape';
+ const replaceValue = 'melon';
+ const result = replaceText(
+ { ...initialValues, searchValue, replaceValue, mode },
+ text
+ );
+ expect(result).toBe('apple orange banana');
+ });
+});
+
+describe('replaceText function (regexp mode)', () => {
+ const mode = 'regexp';
+
+ it('should replace a word in text using regexp correctly', () => {
+ const text = 'Egestas lobortis facilisi convallis rhoncus nunc.';
+ const searchRegexp = '/nunc/';
+ const replaceValue = 'hello';
+ const result = replaceText(
+ { ...initialValues, searchRegexp, replaceValue, mode },
+ text
+ );
+ expect(result).toBe('Egestas lobortis facilisi convallis rhoncus hello.');
+ });
+
+ it('should replace all words in the text with regexp correctly', () => {
+ const text =
+ 'Parturient porta ultricies tellus ultricies suscipit quisque torquent.';
+ const searchRegexp = '/ultricies/g';
+ const replaceValue = 'hello';
+ const result = replaceText(
+ { ...initialValues, searchRegexp, replaceValue, mode },
+ text
+ );
+ expect(result).toBe(
+ 'Parturient porta hello tellus hello suscipit quisque torquent.'
+ );
+ });
+
+ it('should replace words in text with regexp using alternation operator correctly', () => {
+ const text =
+ 'Commodo maximus nullam dis placerat fermentum curabitur semper.';
+ const searchRegexp = '/nullam|fermentum/g';
+ const replaceValue = 'test';
+ const result = replaceText(
+ { ...initialValues, searchRegexp, replaceValue, mode },
+ text
+ );
+ expect(result).toBe(
+ 'Commodo maximus test dis placerat test curabitur semper.'
+ );
+ });
+
+ it('should return the original text when passed an invalid regexp', () => {
+ const text =
+ 'Commodo maximus nullam dis placerat fermentum curabitur semper.';
+ const searchRegexp = '/(/';
+ const replaceValue = 'test';
+ const result = replaceText(
+ { ...initialValues, searchRegexp, replaceValue, mode },
+ text
+ );
+ expect(result).toBe(
+ 'Commodo maximus nullam dis placerat fermentum curabitur semper.'
+ );
+ });
+
+ it('should remove brackets from text correctly using regexp', () => {
+ const text =
+ 'Porta nulla (magna) lectus, [taciti] habitant nunc urna maximus metus.';
+ const searchRegexp = '/[()\\[\\]]/g';
+ const replaceValue = '';
+ const result = replaceText(
+ { ...initialValues, searchRegexp, replaceValue, mode },
+ text
+ );
+ expect(result).toBe(
+ 'Porta nulla magna lectus, taciti habitant nunc urna maximus metus.'
+ );
+ });
+
+ it('should replace case-insensitive words correctly', () => {
+ const text = 'Porta cras ad laoreet porttitor feRmeNtum consectetur?';
+ const searchRegexp = '/porta|fermentum/gi';
+ const replaceValue = 'test';
+ const result = replaceText(
+ { ...initialValues, searchRegexp, replaceValue, mode },
+ text
+ );
+ expect(result).toBe('test cras ad laoreet porttitor test consectetur?');
+ });
+
+ it('should replace words with digits and symbols correctly', () => {
+ const text = 'The price is 100$, and the discount is 20%.';
+ const searchRegexp = '/\\d+/g';
+ const replaceValue = 'X';
+ const result = replaceText(
+ { ...initialValues, searchRegexp, replaceValue, mode },
+ text
+ );
+ expect(result).toBe('The price is X$, and the discount is X%.');
+ });
+});
diff --git a/src/pages/tools/string/text-replacer/service.ts b/src/pages/tools/string/text-replacer/service.ts
new file mode 100644
index 0000000..e633b12
--- /dev/null
+++ b/src/pages/tools/string/text-replacer/service.ts
@@ -0,0 +1,40 @@
+import { InitialValuesType } from './initialValues';
+
+function isFieldsEmpty(textField: string, searchField: string) {
+ return !textField.trim() || !searchField.trim();
+}
+
+export function replaceText(options: InitialValuesType, text: string) {
+ const { searchValue, searchRegexp, replaceValue, mode } = options;
+
+ switch (mode) {
+ case 'text':
+ if (isFieldsEmpty(text, searchValue)) return text;
+ return text.replaceAll(searchValue, replaceValue);
+ case 'regexp':
+ if (isFieldsEmpty(text, searchRegexp)) return text;
+ return replaceTextWithRegexp(text, searchRegexp, replaceValue);
+ }
+}
+
+function replaceTextWithRegexp(
+ text: string,
+ searchRegexp: string,
+ replaceValue: string
+) {
+ try {
+ const match = searchRegexp.match(/^\/(.*)\/([a-z]*)$/i);
+
+ if (match) {
+ // Input is in /pattern/flags format
+ const [, pattern, flags] = match;
+ return text.replace(new RegExp(pattern, flags), replaceValue);
+ } else {
+ // Input is a raw pattern - don't escape it
+ return text.replace(new RegExp(searchRegexp, 'g'), replaceValue);
+ }
+ } catch (err) {
+ console.error('Invalid regular expression:', err);
+ return text;
+ }
+}
diff --git a/src/pages/string/to-morse/index.tsx b/src/pages/tools/string/to-morse/index.tsx
similarity index 75%
rename from src/pages/string/to-morse/index.tsx
rename to src/pages/tools/string/to-morse/index.tsx
index 42d1d4c..3225da9 100644
--- a/src/pages/string/to-morse/index.tsx
+++ b/src/pages/tools/string/to-morse/index.tsx
@@ -1,12 +1,11 @@
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 ToolTextInput from '@components/input/ToolTextInput';
+import ToolTextResult from '@components/result/ToolTextResult';
+import ToolOptions from '@components/options/ToolOptions';
import { compute } from './service';
-import TextFieldWithDesc from '../../../components/options/TextFieldWithDesc';
-import ToolInputAndResult from '../../../components/ToolInputAndResult';
+import TextFieldWithDesc from '@components/options/TextFieldWithDesc';
+import ToolInputAndResult from '@components/ToolInputAndResult';
const initialValues = {
dotSymbol: '.',
@@ -21,9 +20,6 @@ export default function ToMorse() {
const { dotSymbol, dashSymbol } = optionsValues;
setResult(compute(input, dotSymbol, dashSymbol));
};
- const validationSchema = Yup.object({
- // splitSeparator: Yup.string().required('The separator is required')
- });
return (
@@ -61,7 +57,6 @@ export default function ToMorse() {
]}
initialValues={initialValues}
input={input}
- validationSchema={validationSchema}
/>
);
diff --git a/src/pages/string/to-morse/meta.ts b/src/pages/tools/string/to-morse/meta.ts
similarity index 95%
rename from src/pages/string/to-morse/meta.ts
rename to src/pages/tools/string/to-morse/meta.ts
index 78774f5..71aa30f 100644
--- a/src/pages/string/to-morse/meta.ts
+++ b/src/pages/tools/string/to-morse/meta.ts
@@ -5,7 +5,7 @@ import { lazy } from 'react';
export const tool = defineTool('string', {
name: 'String To morse',
path: 'to-morse',
- // image,
+ icon: 'arcticons:morse',
description:
"World's simplest browser-based utility for converting text to Morse code. Load your text in the input form on the left and you'll instantly get Morse code in the output area. Powerful, free, and fast. Load text – get Morse code.",
shortDescription: 'Quickly encode text to morse',
diff --git a/src/pages/string/to-morse/service.ts b/src/pages/tools/string/to-morse/service.ts
similarity index 100%
rename from src/pages/string/to-morse/service.ts
rename to src/pages/tools/string/to-morse/service.ts
diff --git a/src/pages/string/to-morse/to-morse.service.test.ts b/src/pages/tools/string/to-morse/to-morse.service.test.ts
similarity index 100%
rename from src/pages/string/to-morse/to-morse.service.test.ts
rename to src/pages/tools/string/to-morse/to-morse.service.test.ts
diff --git a/src/pages/string/uppercase/index.tsx b/src/pages/tools/string/uppercase/index.tsx
similarity index 99%
rename from src/pages/string/uppercase/index.tsx
rename to src/pages/tools/string/uppercase/index.tsx
index 727cc03..c9ee21f 100644
--- a/src/pages/string/uppercase/index.tsx
+++ b/src/pages/tools/string/uppercase/index.tsx
@@ -8,4 +8,4 @@ const validationSchema = Yup.object({
});
export default function Uppercase() {
return Lorem ipsum ;
-}
\ No newline at end of file
+}
diff --git a/src/pages/string/uppercase/meta.ts b/src/pages/tools/string/uppercase/meta.ts
similarity index 95%
rename from src/pages/string/uppercase/meta.ts
rename to src/pages/tools/string/uppercase/meta.ts
index 014f2cd..7fbe7d3 100644
--- a/src/pages/string/uppercase/meta.ts
+++ b/src/pages/tools/string/uppercase/meta.ts
@@ -5,9 +5,9 @@ import { lazy } from 'react';
export const tool = defineTool('string', {
name: 'Uppercase',
path: 'uppercase',
- // image,
+ icon: '',
description: '',
shortDescription: '',
keywords: ['uppercase'],
component: lazy(() => import('./index'))
-});
\ No newline at end of file
+});
diff --git a/src/pages/string/uppercase/service.ts b/src/pages/tools/string/uppercase/service.ts
similarity index 62%
rename from src/pages/string/uppercase/service.ts
rename to src/pages/tools/string/uppercase/service.ts
index 1ffc0fa..106b6e4 100644
--- a/src/pages/string/uppercase/service.ts
+++ b/src/pages/tools/string/uppercase/service.ts
@@ -1,3 +1,3 @@
export function UppercaseInput(input: string): string {
- return input.toUpperCase();
-}
\ No newline at end of file
+ return input.toUpperCase();
+}
diff --git a/src/pages/tools/string/uppercase/uppercase.service.test.ts b/src/pages/tools/string/uppercase/uppercase.service.test.ts
new file mode 100644
index 0000000..9b9979f
--- /dev/null
+++ b/src/pages/tools/string/uppercase/uppercase.service.test.ts
@@ -0,0 +1,34 @@
+import { describe, expect, 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');
+ });
+});
diff --git a/src/pages/video/gif/change-speed/index.tsx b/src/pages/tools/video/gif/change-speed/index.tsx
similarity index 91%
rename from src/pages/video/gif/change-speed/index.tsx
rename to src/pages/tools/video/gif/change-speed/index.tsx
index f45b8e0..7600960 100644
--- a/src/pages/video/gif/change-speed/index.tsx
+++ b/src/pages/tools/video/gif/change-speed/index.tsx
@@ -1,14 +1,14 @@
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 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 ToolInputAndResult from '@components/ToolInputAndResult';
import Typography from '@mui/material/Typography';
import { FrameOptions, GifReader, GifWriter } from 'omggif';
-import { gifBinaryToFile } from '../../../../utils/gif';
+import { gifBinaryToFile } from '../../../../../utils/gif';
const initialValues = {
newSpeed: 200
@@ -142,7 +142,6 @@ export default function ChangeSpeed() {
]}
initialValues={initialValues}
input={input}
- validationSchema={validationSchema}
/>
);
diff --git a/src/pages/video/gif/change-speed/meta.ts b/src/pages/tools/video/gif/change-speed/meta.ts
similarity index 93%
rename from src/pages/video/gif/change-speed/meta.ts
rename to src/pages/tools/video/gif/change-speed/meta.ts
index 3abe82a..4beca99 100644
--- a/src/pages/video/gif/change-speed/meta.ts
+++ b/src/pages/tools/video/gif/change-speed/meta.ts
@@ -5,7 +5,7 @@ import { lazy } from 'react';
export const tool = defineTool('gif', {
name: 'Change speed',
path: 'change-speed',
- // image,
+ icon: 'material-symbols-light:speed-outline',
description:
'This online utility lets you change the speed of a GIF animation. You can speed it up or slow it down. You can set the same constant delay between all frames or change the delays of individual frames. You can also play both the input and output GIFs at the same time and compare their speeds',
shortDescription: 'Quickly change GIF speed',
diff --git a/src/pages/video/gif/index.ts b/src/pages/tools/video/gif/index.ts
similarity index 100%
rename from src/pages/video/gif/index.ts
rename to src/pages/tools/video/gif/index.ts
diff --git a/src/pages/video/index.ts b/src/pages/tools/video/index.ts
similarity index 100%
rename from src/pages/video/index.ts
rename to src/pages/tools/video/index.ts
diff --git a/src/tools/defineTool.tsx b/src/tools/defineTool.tsx
index 2eb86ca..0e4dcbd 100644
--- a/src/tools/defineTool.tsx
+++ b/src/tools/defineTool.tsx
@@ -1,33 +1,46 @@
import ToolLayout from '../components/ToolLayout';
import React, { JSXElementConstructor, LazyExoticComponent } from 'react';
+import { IconifyIcon } from '@iconify/react';
interface ToolOptions {
path: string;
- component: LazyExoticComponent>>;
+ component: LazyExoticComponent>;
keywords: string[];
- image?: string;
+ icon: IconifyIcon | string;
name: string;
description: string;
shortDescription: string;
}
+export type ToolCategory =
+ | 'string'
+ | 'png'
+ | 'number'
+ | 'gif'
+ | 'list'
+ | 'json';
+
export interface DefinedTool {
- type: string;
+ type: ToolCategory;
path: string;
name: string;
description: string;
shortDescription: string;
- image?: string;
+ icon: IconifyIcon | string;
keywords: string[];
component: () => JSX.Element;
}
+export interface ToolComponentProps {
+ title?: any;
+}
+
export const defineTool = (
- basePath: string,
+ basePath: ToolCategory,
options: ToolOptions
): DefinedTool => {
const {
- image,
+ icon,
path,
name,
description,
@@ -40,7 +53,7 @@ export const defineTool = (
type: basePath,
path: `${basePath}/${path}`,
name,
- image,
+ icon,
description,
shortDescription,
keywords,
@@ -49,10 +62,10 @@ export const defineTool = (
-
+
);
}
diff --git a/src/tools/index.ts b/src/tools/index.ts
index 594a721..0e33529 100644
--- a/src/tools/index.ts
+++ b/src/tools/index.ts
@@ -1,43 +1,64 @@
-import { stringTools } from '../pages/string';
-import { imageTools } from '../pages/image';
-import { DefinedTool } from './defineTool';
+import { stringTools } from '../pages/tools/string';
+import { imageTools } from '../pages/tools/image';
+import { DefinedTool, ToolCategory } from './defineTool';
import { capitalizeFirstLetter } from '../utils/string';
-import { numberTools } from '../pages/number';
-import { videoTools } from '../pages/video';
-import { listTools } from '../pages/list';
+import { numberTools } from '../pages/tools/number';
+import { videoTools } from '../pages/tools/video';
+import { listTools } from '../pages/tools/list';
+import { Entries } from 'type-fest';
+import { jsonTools } from '../pages/tools/json';
+import { IconifyIcon } from '@iconify/react';
export const tools: DefinedTool[] = [
...imageTools,
...stringTools,
- ...numberTools,
+ ...jsonTools,
+ ...listTools,
...videoTools,
- ...listTools
+ ...numberTools
];
-const categoriesDescriptions: { type: string; value: string }[] = [
+const categoriesConfig: {
+ type: ToolCategory;
+ value: string;
+ title?: string;
+ icon: IconifyIcon | string;
+}[] = [
{
type: 'string',
+ title: 'Text',
+ icon: 'solar:text-bold-duotone',
value:
'Tools for working with text – convert text to images, find and replace text, split text into fragments, join text lines, repeat text, and much more.'
},
{
type: 'png',
+ icon: 'ph:file-png-thin',
value:
'Tools for working with PNG images – convert PNGs to JPGs, create transparent PNGs, change PNG colors, crop, rotate, resize PNGs, and much more.'
},
{
type: 'number',
+ icon: 'lsicon:number-filled',
value:
'Tools for working with numbers – generate number sequences, convert numbers to words and words to numbers, sort, round, factor numbers, and much more.'
},
{
type: 'gif',
+ icon: 'material-symbols-light:gif-rounded',
value:
'Tools for working with GIF animations – create transparent GIFs, extract GIF frames, add text to GIF, crop, rotate, reverse GIFs, and much more.'
},
{
type: 'list',
+ icon: 'solar:list-bold-duotone',
value:
'Tools for working with lists – sort, reverse, randomize lists, find unique and duplicate list items, change list item separators, and much more.'
+ },
+ {
+ type: 'json',
+ icon: 'lets-icons:json-light',
+ value:
+ 'Tools for working with JSON data structures – prettify and minify JSON objects, flatten JSON arrays, stringify JSON values, analyze data, and much more'
}
];
export const filterTools = (
@@ -62,24 +83,28 @@ export const filterTools = (
export const getToolsByCategory = (): {
title: string;
description: string;
+ icon: IconifyIcon | string;
type: string;
example: { title: string; path: string };
tools: DefinedTool[];
}[] => {
- const grouped: Partial> = Object.groupBy(
- tools,
- ({ type }) => type
+ const groupedByType: Partial> =
+ Object.groupBy(tools, ({ type }) => type);
+ return (Object.entries(groupedByType) as Entries).map(
+ ([type, tools]) => {
+ const categoryConfig = categoriesConfig.find(
+ (config) => config.type === type
+ );
+ return {
+ title: `${categoryConfig?.title ?? capitalizeFirstLetter(type)} Tools`,
+ description: categoryConfig?.value ?? '',
+ type,
+ icon: categoryConfig!.icon,
+ tools: tools ?? [],
+ example: tools
+ ? { title: tools[0].name, path: tools[0].path }
+ : { title: '', path: '' }
+ };
+ }
);
- return Object.entries(grouped).map(([type, tls]) => {
- return {
- title: `${capitalizeFirstLetter(type)} Tools`,
- description:
- categoriesDescriptions.find((desc) => desc.type === type)?.value ?? '',
- type,
- tools: tls ?? [],
- example: tls
- ? { title: tls[0].name, path: tls[0].path }
- : { title: '', path: '' }
- };
- });
};
diff --git a/src/utils/string.ts b/src/utils/string.ts
index f918364..d2ce2a9 100644
--- a/src/utils/string.ts
+++ b/src/utils/string.ts
@@ -9,6 +9,7 @@ export function isNumber(number: any) {
export const replaceSpecialCharacters = (str: string) => {
return str
+ .replace(/\\"/g, '"')
.replace(/\\n/g, '\n')
.replace(/\\t/g, '\t')
.replace(/\\r/g, '\r')
diff --git a/tsconfig.json b/tsconfig.json
index 9eca3e5..aa0f1e3 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -30,6 +30,9 @@
],
"@assets/*": [
"./assets/*"
+ ],
+ "@components/*": [
+ "./components/*"
]
}
},