diff --git a/scripts/create-tool.mjs b/scripts/create-tool.mjs index a8c1715..e57693a 100644 --- a/scripts/create-tool.mjs +++ b/scripts/create-tool.mjs @@ -83,6 +83,7 @@ export const tool = defineTool('${type}', { path: '${toolName}', // image, description: '', + shortDescription: '', keywords: ['${toolName.split('-').join('\', \'')}'], component: lazy(() => import('./index')) }); diff --git a/src/assets/tools.png b/src/assets/tools.png new file mode 100644 index 0000000..3dc90a5 Binary files /dev/null and b/src/assets/tools.png differ diff --git a/src/components/Hero.tsx b/src/components/Hero.tsx new file mode 100644 index 0000000..e9ed495 --- /dev/null +++ b/src/components/Hero.tsx @@ -0,0 +1,100 @@ +import { Autocomplete, Box, Stack, TextField } from '@mui/material'; +import Typography from '@mui/material/Typography'; +import SearchIcon from '@mui/icons-material/Search'; +import Grid from '@mui/material/Grid'; +import { useState } from 'react'; +import { DefinedTool } from '@tools/defineTool'; +import { filterTools, tools } from '@tools/index'; +import { useNavigate } from 'react-router-dom'; + +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: 'Change GIF speed', url: '' }, + { label: 'Pick a random item', url: '' }, + { label: 'Find and replace text', url: '' }, + { label: 'Convert emoji to image', url: '' }, + { label: 'Split a string', url: '/string/split' }, + { label: 'Calculate number sum', url: '/number/sum' }, + { label: 'Pixelate an image', url: '' } +]; +export default function Hero() { + const [inputValue, setInputValue] = useState(''); + const [filteredTools, setFilteredTools] = useState(tools); + const navigate = useNavigate(); + const handleInputChange = ( + event: React.ChangeEvent<{}>, + newInputValue: string + ) => { + setInputValue(newInputValue); + setFilteredTools(filterTools(tools, newInputValue)); + }; + return ( + + + Transform Your Workflow with + + Omni Tools + + + + Boost your productivity with Omni Tools, the ultimate toolkit for + getting things done quickly! Access thousands of user-friendly utilities + for editing images, text, lists, and data, all directly from your + browser. + + + option.name} + renderInput={(params) => ( + + }} + onChange={(event) => handleInputChange(event, event.target.value)} + /> + )} + renderOption={(props, option) => ( + navigate(option.path)}> + + {option.name} + {option.shortDescription} + + + )} + /> + + {exampleTools.map((tool) => ( + navigate(tool.url)} item xs={4} key={tool.label}> + + {tool.label} + + + ))} + + + ); +} diff --git a/src/components/ToolInputAndResult.tsx b/src/components/ToolInputAndResult.tsx index b34999c..3aefb8c 100644 --- a/src/components/ToolInputAndResult.tsx +++ b/src/components/ToolInputAndResult.tsx @@ -9,7 +9,7 @@ export default function ToolInputAndResult({ result: ReactNode; }) { return ( - + {input} diff --git a/src/components/ToolLayout.tsx b/src/components/ToolLayout.tsx index 2e03a07..4cdb8fc 100644 --- a/src/components/ToolLayout.tsx +++ b/src/components/ToolLayout.tsx @@ -2,6 +2,10 @@ import { Box } from '@mui/material'; import React, { ReactNode } from 'react'; import { Helmet } from 'react-helmet'; import ToolHeader from './ToolHeader'; +import Separator from '@tools/Separator'; +import AllTools from './allTools/AllTools'; +import { getToolsByCategory } from '@tools/index'; +import { capitalizeFirstLetter } from '../utils/string'; export default function ToolLayout({ children, @@ -15,7 +19,18 @@ export default function ToolLayout({ image?: string; type: string; children: ReactNode; + type: string; }) { + const otherCategoryTools = + getToolsByCategory() + .find((category) => category.type === type) + ?.tools.filter((tool) => tool.name !== title) + .map((tool) => ({ + title: tool.name, + description: tool.shortDescription, + link: '/' + tool.path + })) ?? []; + return ( {children} + + ); diff --git a/src/components/allTools/AllTools.tsx b/src/components/allTools/AllTools.tsx new file mode 100644 index 0000000..e0a5e1f --- /dev/null +++ b/src/components/allTools/AllTools.tsx @@ -0,0 +1,36 @@ +import { Box, Grid, Stack, Typography } from '@mui/material'; +import ToolCard from './ToolCard'; + +export interface ToolCardProps { + title: string; + description: string; + link: string; +} + +interface AllToolsProps { + title: string; + toolCards: ToolCardProps[]; +} + +export default function AllTools({ title, toolCards }: AllToolsProps) { + return ( + + + {title} + + + + {toolCards.map((card, index) => ( + + + + ))} + + + + ); +} diff --git a/src/components/allTools/ToolCard.tsx b/src/components/allTools/ToolCard.tsx new file mode 100644 index 0000000..7a2dc98 --- /dev/null +++ b/src/components/allTools/ToolCard.tsx @@ -0,0 +1,44 @@ +import { Box, Link, Card, CardContent, Typography } from '@mui/material'; +import { ToolCardProps } from './AllTools'; +import ChevronRightIcon from '@mui/icons-material/ChevronRight'; +import { useNavigate } from 'react-router-dom'; + +export default function ToolCard({ title, description, link }: ToolCardProps) { + const navigate = useNavigate(); + return ( + navigate(link)} + raised + sx={{ + borderRadius: 2, + bgcolor: '#5581b5', + borderColor: '#5581b5', + color: '#fff', + boxShadow: '6px 6px 12px #b8b9be, -6px -6px 12px #fff', + cursor: 'pointer' + }} + > + + + + {title} + + + + + + + {description} + + + + ); +} diff --git a/src/components/examples/ExampleCard.tsx b/src/components/examples/ExampleCard.tsx new file mode 100644 index 0000000..e756fa5 --- /dev/null +++ b/src/components/examples/ExampleCard.tsx @@ -0,0 +1,114 @@ +import { ExampleCardProps } from './Examples'; +import { + Box, + Stack, + Card, + CardContent, + Typography, + TextField, + useTheme +} from '@mui/material'; +import ArrowDownwardIcon from '@mui/icons-material/ArrowDownward'; +import RequiredOptions from './RequiredOptions'; + +export default function ExampleCard({ + title, + description, + sampleText, + sampleResult, + requiredOptions, + changeInputResult +}: ExampleCardProps) { + const theme = useTheme(); + return ( + + + + + {title} + + + + + {description} + + + changeInputResult(sampleText, sampleResult)} + sx={{ + display: 'flex', + zIndex: '2', + width: '100%', + height: '100%', + bgcolor: 'transparent', + padding: '5px 10px', + borderRadius: '5px', + cursor: 'pointer', + boxShadow: 'inset 2px 2px 5px #b8b9be, inset -3px -3px 7px #fff;' + }} + > + + + + + changeInputResult(sampleText, sampleResult)} + sx={{ + display: 'flex', + zIndex: '2', + width: '100%', + height: '100%', + bgcolor: 'transparent', + padding: '5px 10px', + borderRadius: '5px', + cursor: 'pointer', + boxShadow: 'inset 2px 2px 5px #b8b9be, inset -3px -3px 7px #fff;' + }} + > + + + + + + + + ); +} diff --git a/src/components/examples/Examples.tsx b/src/components/examples/Examples.tsx new file mode 100644 index 0000000..6f31bd3 --- /dev/null +++ b/src/components/examples/Examples.tsx @@ -0,0 +1,59 @@ +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 new file mode 100644 index 0000000..d79a325 --- /dev/null +++ b/src/components/examples/RequiredOptions.tsx @@ -0,0 +1,78 @@ +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/input/ToolTextInput.tsx b/src/components/input/ToolTextInput.tsx index d5a666d..cc2d344 100644 --- a/src/components/input/ToolTextInput.tsx +++ b/src/components/input/ToolTextInput.tsx @@ -46,7 +46,7 @@ export default function ToolTextInput({ fileInputRef.current?.click(); }; return ( - + void; + disabled?: boolean; }) => { const handleChange = (event: React.ChangeEvent) => { onChange(event.target.checked); @@ -20,7 +22,11 @@ const CheckboxWithDesc = ({ + } label={title} /> diff --git a/src/config/routesConfig.tsx b/src/config/routesConfig.tsx index 54f2445..a68b548 100644 --- a/src/config/routesConfig.tsx +++ b/src/config/routesConfig.tsx @@ -3,12 +3,17 @@ import { Navigate } from 'react-router-dom'; import { lazy } from 'react'; const Home = lazy(() => import('../pages/home')); +const ToolsByCategory = lazy(() => import('../pages/tools-by-category')); const routes: RouteObject[] = [ { path: '/', element: }, + { + path: '/categories/:categoryName', + element: + }, { path: '*', element: diff --git a/src/pages/home/index.tsx b/src/pages/home/index.tsx index 775f7bd..5a1c51b 100644 --- a/src/pages/home/index.tsx +++ b/src/pages/home/index.tsx @@ -1,45 +1,14 @@ -import { - Autocomplete, - Box, - Card, - CardContent, - Stack, - TextField -} from '@mui/material'; +import { Box, Card, CardContent, Stack } from '@mui/material'; import Grid from '@mui/material/Grid'; import Typography from '@mui/material/Typography'; -import SearchIcon from '@mui/icons-material/Search'; import { Link, useNavigate } from 'react-router-dom'; -import { filterTools, getToolsByCategory, tools } from '../../tools'; -import { useState } from 'react'; -import { DefinedTool } from '@tools/defineTool'; +import { getToolsByCategory } from '../../tools'; import Button from '@mui/material/Button'; +import Hero from 'components/Hero'; -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: 'Change GIF speed', url: '' }, - { label: 'Pick a random item', url: '' }, - { label: 'Find and replace text', url: '' }, - { label: 'Convert emoji to image', url: '' }, - { label: 'Split a string', url: '/string/split' }, - { label: 'Calculate number sum', url: '/number/sum' }, - { label: 'Pixelate an image', url: '' } -]; export default function Home() { const navigate = useNavigate(); - const [inputValue, setInputValue] = useState(''); - const [filteredTools, setFilteredTools] = useState(tools); - const handleInputChange = ( - event: React.ChangeEvent<{}>, - newInputValue: string - ) => { - setInputValue(newInputValue); - setFilteredTools(filterTools(tools, newInputValue)); - }; + return ( - - - Transform Your Workflow with - - Omni Tools - - - - Boost your productivity with Omni Tools, the ultimate toolkit for - getting things done quickly! Access thousands of user-friendly - utilities for editing images, text, lists, and data, all directly from - your browser. - - - option.name} - renderInput={(params) => ( - - }} - onChange={(event) => handleInputChange(event, event.target.value)} - /> - )} - renderOption={(props, option) => ( - navigate(option.path)} - > - - {option.name} - {option.description} - - - )} - /> - - {exampleTools.map((tool) => ( - navigate(tool.url)} - item - xs={4} - key={tool.label} - > - - {tool.label} - - - ))} - - - {getToolsByCategory().map((category) => ( - - - - - {category.title} - - {category.description} - - - - - - - - ))} - - + + + {getToolsByCategory().map((category) => ( + + + + + {category.title} + + {category.description} + + + + + + + + ))} + ); } diff --git a/src/pages/image/png/change-colors-in-png/meta.ts b/src/pages/image/png/change-colors-in-png/meta.ts index dd87e03..bdf461c 100644 --- a/src/pages/image/png/change-colors-in-png/meta.ts +++ b/src/pages/image/png/change-colors-in-png/meta.ts @@ -8,6 +8,7 @@ export const tool = defineTool('png', { image, 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', keywords: ['change', 'colors', 'in', 'png'], component: lazy(() => import('./index')) }); diff --git a/src/pages/image/png/create-transparent/meta.ts b/src/pages/image/png/create-transparent/meta.ts index 8362377..af2daf3 100644 --- a/src/pages/image/png/create-transparent/meta.ts +++ b/src/pages/image/png/create-transparent/meta.ts @@ -6,6 +6,7 @@ export const tool = defineTool('png', { name: 'Create transparent PNG', path: 'create-transparent', image, + 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.", keywords: ['create', 'transparent'], diff --git a/src/pages/number/sum/meta.ts b/src/pages/number/sum/meta.ts index ffc8520..fc96799 100644 --- a/src/pages/number/sum/meta.ts +++ b/src/pages/number/sum/meta.ts @@ -8,6 +8,7 @@ export const tool = defineTool('number', { // image, description: 'Quickly calculate the sum of numbers in your browser. To get your sum, just enter your list of numbers in the input field, adjust the separator between the numbers in the options below, and this utility will add up all these numbers.', + shortDescription: 'Quickly sum numbers', keywords: ['sum'], component: lazy(() => import('./index')) }); diff --git a/src/pages/string/join/Info.tsx b/src/pages/string/join/Info.tsx new file mode 100644 index 0000000..a6d01a7 --- /dev/null +++ b/src/pages/string/join/Info.tsx @@ -0,0 +1,19 @@ +import { Box, Stack, Typography } from '@mui/material'; + +interface ExampleProps { + title: string; + description: string; +} + +export default function Example({ title, description }: ExampleProps) { + return ( + + + + {title} + + {description} + + + ); +} diff --git a/src/pages/string/join/index.tsx b/src/pages/string/join/index.tsx index a41dcb4..33fa5df 100644 --- a/src/pages/string/join/index.tsx +++ b/src/pages/string/join/index.tsx @@ -12,6 +12,11 @@ import CheckboxWithDesc from '../../../components/options/CheckboxWithDesc'; import ToolOptionGroups from '../../../components/options/ToolOptionGroups'; import ToolInputAndResult from '../../../components/ToolInputAndResult'; +import Info from './Info'; +import Separator from '../../../tools/Separator'; +import AllTools from '../../../components/allTools/AllTools'; +import Examples from '../../../components/examples/Examples'; + const initialValues = { joinCharacter: '', deleteBlank: true, @@ -48,6 +53,66 @@ const blankTrailingOptions: { } ]; +const exampleCards = [ + { + title: 'Merge a To-Do List', + description: + "In this example, we merge a bullet point list into one sentence, separating each item by the word 'and'. We also remove all empty lines and trailing spaces. If we didn't remove the empty lines, then they'd be joined with the separator word, making the separator word appear multiple times. If we didn't remove the trailing tabs and spaces, then they'd create extra spacing in the joined text and it wouldn't look nice.", + sampleText: `clean the house + +go shopping +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: { + joinCharacter: 'and', + deleteBlankLines: true, + deleteTrailingSpaces: true + } + }, + { + title: 'Comma Separated List', + description: + 'This example joins a column of words into a comma separated list of words.', + sampleText: `computer +memory +processor +mouse +keyboard`, + sampleResult: `computer, memory, processor, mouse, keyboard`, + requiredOptions: { + joinCharacter: ',', + deleteBlankLines: false, + deleteTrailingSpaces: false + } + }, + { + title: 'Vertical Word to Horizontal', + description: + 'This example rotates words from a vertical position to horizontal. An empty separator is used for this purpose.', + sampleText: `T +e +x +t +a +b +u +l +o +u +s +!`, + sampleResult: `Textabulous!`, + requiredOptions: { + joinCharacter: '', + deleteBlankLines: false, + deleteTrailingSpaces: false + } + } +]; + export default function JoinText() { const [input, setInput] = useState(''); const { showSnackBar } = useContext(CustomSnackBarContext); @@ -69,6 +134,16 @@ export default function JoinText() { return null; }; + function changeInputResult(input: string, result: string) { + setInput(input); + setResult(result); + + const toolsElement = document.getElementById('tool'); + if (toolsElement) { + toolsElement.scrollIntoView({ behavior: 'smooth' }); + } + } + return ( + + + ({ + ...card, + changeInputResult + }))} + /> ); } diff --git a/src/pages/string/join/meta.ts b/src/pages/string/join/meta.ts index 1686be5..aedfe3f 100644 --- a/src/pages/string/join/meta.ts +++ b/src/pages/string/join/meta.ts @@ -8,6 +8,7 @@ export const tool = defineTool('string', { image, 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', keywords: ['text', 'join'], component: lazy(() => import('./index')) }); diff --git a/src/pages/string/split/meta.ts b/src/pages/string/split/meta.ts index 09b4f40..01acca2 100644 --- a/src/pages/string/split/meta.ts +++ b/src/pages/string/split/meta.ts @@ -8,6 +8,7 @@ export const tool = defineTool('string', { image, 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', keywords: ['text', 'split'], component: lazy(() => import('./index')) }); diff --git a/src/pages/string/to-morse/meta.ts b/src/pages/string/to-morse/meta.ts index 80b2b4d..78774f5 100644 --- a/src/pages/string/to-morse/meta.ts +++ b/src/pages/string/to-morse/meta.ts @@ -8,6 +8,7 @@ export const tool = defineTool('string', { // image, description: "World's simplest browser-based utility for converting text to Morse code. Load your text in the input form on the left and you'll instantly get Morse code in the output area. Powerful, free, and fast. Load text – get Morse code.", + shortDescription: 'Quickly encode text to morse', keywords: ['to', 'morse'], component: lazy(() => import('./index')) }); diff --git a/src/pages/tools-by-category/index.tsx b/src/pages/tools-by-category/index.tsx new file mode 100644 index 0000000..658151f --- /dev/null +++ b/src/pages/tools-by-category/index.tsx @@ -0,0 +1,74 @@ +import { + Box, + Card, + CardContent, + Divider, + Stack, + useTheme +} from '@mui/material'; +import Grid from '@mui/material/Grid'; +import Typography from '@mui/material/Typography'; +import { Link, useNavigate, useParams } from 'react-router-dom'; +import { getToolsByCategory, tools } from '../../tools'; +import Button from '@mui/material/Button'; +import Hero from 'components/Hero'; +import AllTools from '../../components/allTools/AllTools'; +import { capitalizeFirstLetter } from '../../utils/string'; +import toolsPng from '@assets/tools.png'; + +export default function Home() { + const navigate = useNavigate(); + const theme = useTheme(); + const { categoryName } = useParams(); + return ( + + + + + + + {`All ${capitalizeFirstLetter(categoryName)} Tools`} + + {getToolsByCategory() + .find(({ type }) => type === categoryName) + ?.tools?.map((tool) => ( + + navigate('/' + tool.path)} + direction={'row'} + spacing={2} + padding={2} + border={1} + borderRadius={2} + > + + + {tool.name} + + {tool.shortDescription} + + + + + ))} + + + + ); +} diff --git a/src/tools/Separator.tsx b/src/tools/Separator.tsx new file mode 100644 index 0000000..ec93302 --- /dev/null +++ b/src/tools/Separator.tsx @@ -0,0 +1,23 @@ +import { Divider } from '@mui/material'; +import React from 'react'; + +type SeparatorProps = { + backgroundColor: string; + margin: string; +}; + +export default function Separator({ backgroundColor, margin }: SeparatorProps) { + return ( + + ); +} diff --git a/src/tools/defineTool.tsx b/src/tools/defineTool.tsx index 55b4a28..41d34a2 100644 --- a/src/tools/defineTool.tsx +++ b/src/tools/defineTool.tsx @@ -8,6 +8,7 @@ interface ToolOptions { image?: string; name: string; description: string; + shortDescription: string; } export interface DefinedTool { @@ -15,6 +16,7 @@ export interface DefinedTool { path: string; name: string; description: string; + shortDescription: string; image?: string; keywords: string[]; component: () => JSX.Element; @@ -24,7 +26,15 @@ export const defineTool = ( basePath: string, options: ToolOptions ): DefinedTool => { - const { image, path, name, description, keywords, component } = options; + const { + image, + path, + name, + description, + keywords, + component, + shortDescription + } = options; const Component = component; return { type: basePath, @@ -32,6 +42,7 @@ export const defineTool = ( name, image, description, + shortDescription, keywords, component: () => { return ( diff --git a/src/tools/index.ts b/src/tools/index.ts index f460aa9..e0f645a 100644 --- a/src/tools/index.ts +++ b/src/tools/index.ts @@ -38,6 +38,7 @@ export const filterTools = ( (tool) => tool.name.toLowerCase().includes(lowerCaseQuery) || tool.description.toLowerCase().includes(lowerCaseQuery) || + tool.shortDescription.toLowerCase().includes(lowerCaseQuery) || tool.keywords.some((keyword) => keyword.toLowerCase().includes(lowerCaseQuery) ) @@ -49,6 +50,7 @@ export const getToolsByCategory = (): { description: string; type: string; example: { title: string; path: string }; + tools: DefinedTool[]; }[] => { const grouped: Partial> = Object.groupBy( tools, @@ -60,6 +62,7 @@ export const getToolsByCategory = (): { 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 2d08599..7662701 100644 --- a/src/utils/string.ts +++ b/src/utils/string.ts @@ -1,3 +1,4 @@ -export function capitalizeFirstLetter(string: string) { +export function capitalizeFirstLetter(string: string | undefined) { + if (!string) return ''; return string.charAt(0).toUpperCase() + string.slice(1); }