diff --git a/.codebuddy/.gitignore b/.codebuddy/.gitignore new file mode 100644 index 0000000..9f4c740 --- /dev/null +++ b/.codebuddy/.gitignore @@ -0,0 +1 @@ +db/ \ No newline at end of file diff --git a/.codebuddy/summary.md b/.codebuddy/summary.md new file mode 100644 index 0000000..d8d7f32 --- /dev/null +++ b/.codebuddy/summary.md @@ -0,0 +1,39 @@ +# Project Summary + +## Overview of Technologies Used +This project is primarily built using the following technologies: +- **Languages**: TypeScript, JavaScript, HTML, CSS +- **Frameworks**: + - React (for building user interfaces) + - Playwright (for end-to-end testing) +- **Main Libraries**: + - Tailwind CSS (for styling) + - MUI (Material-UI for components) + - pnpm (for package management) + +## Purpose of the Project +The project appears to be a web application that provides various tools for image, JSON, list, number, and string manipulations. It is designed to offer users functionalities such as converting image formats, generating random numbers, and manipulating strings. The structure indicates a focus on modular components, making it easy to extend or modify specific tools without affecting the entire application. + +## Build and Configuration Files +The following files are relevant for the configuration and building of the project: +- `Dockerfile`: `/Dockerfile` +- `package.json`: `/package.json` +- `pnpm-lock.yaml`: `/pnpm-lock.yaml` +- `playwright.config.ts`: `/playwright.config.ts` +- `postcss.config.mjs`: `/postcss.config.mjs` +- `tailwind.config.mjs`: `/tailwind.config.mjs` +- `tsconfig.json`: `/tsconfig.json` +- `vite.config.ts`: `/vite.config.ts` +- `commitlint.config.js`: `/commitlint.config.js` + +## Source Files Directory +The source files can be found in the following directory: +- `/src` + +## Documentation Files Location +Documentation files are located in the root directory: +- `README.md`: `/README.md` +- `LICENSE`: `/LICENSE` +- `CODEOWNERS`: `/CODEOWNERS` + +This summary encapsulates the key aspects of the project, including its technological stack, purpose, file structure, and documentation locations. \ No newline at end of file diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000..3fcd7fb --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,2 @@ +github: [iib0011] +buy_me_a_coffee: iib0011 \ No newline at end of file diff --git a/.gitignore b/.gitignore index 9f099c8..162be48 100644 --- a/.gitignore +++ b/.gitignore @@ -38,3 +38,5 @@ yarn-error.log* /test-results /playwright-report + +dist.zip diff --git a/.idea/shelf/Uncommitted_changes_before_Checkout_at_2_27_2025_11_44_AM_[Changes]/shelved.patch b/.idea/shelf/Uncommitted_changes_before_Checkout_at_2_27_2025_11_44_AM_[Changes]/shelved.patch new file mode 100644 index 0000000..e1aeb07 --- /dev/null +++ b/.idea/shelf/Uncommitted_changes_before_Checkout_at_2_27_2025_11_44_AM_[Changes]/shelved.patch @@ -0,0 +1,166 @@ +Index: .idea/workspace.xml +IDEA additional info: +Subsystem: com.intellij.openapi.diff.impl.patch.BaseRevisionTextPatchEP +<+>\r\n\r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n {\r\n "lastFilter": {\r\n "state": "OPEN",\r\n "assignee": "iib0011"\r\n }\r\n}\r\n {\r\n "selectedUrlAndAccountId": {\r\n "url": "https://github.com/iib0011/omni-tools.git",\r\n "accountId": "45f8cd51-000f-4ba4-a4c6-c4d96ac9b1e5"\r\n }\r\n}\r\n {\r\n "isMigrated": true\r\n}\r\n \r\n \r\n {\r\n "associatedIndex": 0\r\n}\r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n - + + + + + + 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'} + />, + , + + ]; const drawerList = ( - navigate('/features')}> - - - navigate('/about-us')}> - - - - GitHub - 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 ? ( <> - + { ) : ( - - - - - GitHub - 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 ( - - + - - - - - + + {/**/} + {/* */} + {/* 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} + + + + + + + + + + + + + + ); +}; +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} - - - - - - - - - - - - ))} - + ); } 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/*" ] } },