Merge remote-tracking branch 'origin/string-join'

# Conflicts:
#	.idea/workspace.xml
#	src/tools/defineTool.tsx
This commit is contained in:
Ibrahima G. Coulibaly
2024-06-25 22:12:04 +01:00
27 changed files with 730 additions and 143 deletions

View File

@@ -83,6 +83,7 @@ export const tool = defineTool('${type}', {
path: '${toolName}',
// image,
description: '',
shortDescription: '',
keywords: ['${toolName.split('-').join('\', \'')}'],
component: lazy(() => import('./index'))
});

BIN
src/assets/tools.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

100
src/components/Hero.tsx Normal file
View File

@@ -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<string>('');
const [filteredTools, setFilteredTools] = useState<DefinedTool[]>(tools);
const navigate = useNavigate();
const handleInputChange = (
event: React.ChangeEvent<{}>,
newInputValue: string
) => {
setInputValue(newInputValue);
setFilteredTools(filterTools(tools, newInputValue));
};
return (
<Box width={'60%'}>
<Stack mb={1} direction={'row'} spacing={1}>
<Typography fontSize={30}>Transform Your Workflow with </Typography>
<Typography fontSize={30} color={'primary'}>
Omni Tools
</Typography>
</Stack>
<Typography fontSize={20} mb={2}>
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.
</Typography>
<Autocomplete
sx={{ mb: 2 }}
autoHighlight
options={filteredTools}
getOptionLabel={(option) => option.name}
renderInput={(params) => (
<TextField
{...params}
fullWidth
placeholder={'Search all tools'}
sx={{ borderRadius: 2 }}
InputProps={{
...params.InputProps,
endAdornment: <SearchIcon />
}}
onChange={(event) => handleInputChange(event, event.target.value)}
/>
)}
renderOption={(props, option) => (
<Box component="li" {...props} onClick={() => navigate(option.path)}>
<Box>
<Typography fontWeight={'bold'}>{option.name}</Typography>
<Typography fontSize={12}>{option.shortDescription}</Typography>
</Box>
</Box>
)}
/>
<Grid container spacing={2} mt={2}>
{exampleTools.map((tool) => (
<Grid onClick={() => navigate(tool.url)} item xs={4} key={tool.label}>
<Box
sx={{
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
borderWidth: 1,
padding: 1,
borderRadius: 3,
borderColor: 'grey',
borderStyle: 'solid',
cursor: 'pointer'
}}
>
<Typography>{tool.label}</Typography>
</Box>
</Grid>
))}
</Grid>
</Box>
);
}

View File

@@ -9,7 +9,7 @@ export default function ToolInputAndResult({
result: ReactNode;
}) {
return (
<Grid container spacing={2}>
<Grid id="tool" container spacing={2}>
<Grid item xs={6}>
{input}
</Grid>

View File

@@ -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 (
<Box
width={'100%'}
@@ -34,6 +49,11 @@ export default function ToolLayout({
type={type}
/>
{children}
<Separator backgroundColor="#5581b5" margin="50px" />
<AllTools
title={`All ${capitalizeFirstLetter(type)} tools`}
toolCards={otherCategoryTools}
/>
</Box>
</Box>
);

View File

@@ -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 (
<Box mt={4} mb={10}>
<Typography mb={2} fontSize={30} color={'primary'}>
{title}
</Typography>
<Stack direction={'row'} alignItems={'center'} spacing={2}>
<Grid container spacing={2}>
{toolCards.map((card, index) => (
<Grid item xs={4} key={index}>
<ToolCard
title={card.title}
description={card.description}
link={card.link}
/>
</Grid>
))}
</Grid>
</Stack>
</Box>
);
}

View File

@@ -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 (
<Card
onClick={() => navigate(link)}
raised
sx={{
borderRadius: 2,
bgcolor: '#5581b5',
borderColor: '#5581b5',
color: '#fff',
boxShadow: '6px 6px 12px #b8b9be, -6px -6px 12px #fff',
cursor: 'pointer'
}}
>
<CardContent>
<Box
display="flex"
justifyContent="space-between"
sx={{
paddingBottom: 1,
borderBottomWidth: 1,
borderColor: '#ffffff70'
}}
>
<Typography variant="h5" component="h2">
{title}
</Typography>
<Link href={link} underline="none" sx={{ color: '#fff' }}>
<ChevronRightIcon />
</Link>
</Box>
<Typography variant="body2" mt={2} color="#fff">
{description}
</Typography>
</CardContent>
</Card>
);
}

View File

@@ -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 (
<Card
raised
sx={{
bgcolor: theme.palette.background.default,
height: '100%',
overflow: 'hidden',
borderRadius: 2,
transition: 'background-color 0.3s ease',
'&:hover': {
boxShadow: '12px 9px 11px 2px #b8b9be, -6px -6px 12px #fff'
}
}}
>
<CardContent>
<Box display="flex" justifyContent="space-between" borderRadius="5px">
<Typography variant="h5" component="h2">
{title}
</Typography>
</Box>
<Stack direction={'column'} alignItems={'center'} spacing={2}>
<Typography variant="body2" color="text.secondary">
{description}
</Typography>
<Box
onClick={() => 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;'
}}
>
<TextField
value={sampleText}
disabled
fullWidth
multiline
sx={{
'& .MuiOutlinedInput-root': {
zIndex: '-1',
'& fieldset': {
border: 'none'
}
}
}}
/>
</Box>
<ArrowDownwardIcon />
<Box
onClick={() => 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;'
}}
>
<TextField
value={sampleResult}
disabled
fullWidth
multiline
sx={{
'& .MuiOutlinedInput-root': {
zIndex: '-1',
'& fieldset': {
border: 'none'
}
}
}}
/>
</Box>
<RequiredOptions options={requiredOptions} />
</Stack>
</CardContent>
</Card>
);
}

View File

@@ -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 (
<Box id={'examples'} mt={4}>
<Box mt={4} display="flex" gap={1} alignItems="center">
<Typography mb={2} fontSize={30} color={'primary'}>
{title}
</Typography>
<Typography mb={2} fontSize={30} color={'secondary'}>
{subtitle}
</Typography>
</Box>
<Stack direction={'row'} alignItems={'center'} spacing={2}>
<Grid container spacing={2}>
{exampleCards.map((card, index) => (
<Grid item xs={4} key={index}>
<ExampleCard
title={card.title}
description={card.description}
sampleText={card.sampleText}
sampleResult={card.sampleResult}
requiredOptions={card.requiredOptions}
changeInputResult={card.changeInputResult}
/>
</Grid>
))}
</Grid>
</Stack>
</Box>
);
}

View File

@@ -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 (
<Stack direction={'column'} alignItems={'left'} spacing={2}>
<Typography variant="h5" component="h3" sx={{ marginTop: '5px' }}>
Required options
</Typography>
<Typography variant="body2" component="p">
These options will be used automatically if you select this example.
</Typography>
<Box
onClick={handleBoxClick}
sx={{
zIndex: '2',
cursor: 'pointer',
bgcolor: 'transparent',
width: '100%',
height: '100%',
display: 'flex'
}}
>
<TextField
disabled
value={joinCharacter}
fullWidth
rows={1}
sx={{
'& .MuiOutlinedInput-root': {
zIndex: '-1'
}
}}
/>
</Box>
{deleteBlankLines ? (
<Box onClick={handleBoxClick}>
<CheckboxWithDesc
title="Delete Blank Lines"
checked={deleteBlankLines}
onChange={() => {}}
description="Delete lines that don't have text symbols."
/>
</Box>
) : (
''
)}
{deleteTrailingSpaces ? (
<Box onClick={handleBoxClick}>
<CheckboxWithDesc
title="Delete Training Spaces"
checked={deleteTrailingSpaces}
onChange={() => {}}
description="Remove spaces and tabs at the end of the lines."
/>
</Box>
) : (
''
)}
</Stack>
);
}

View File

@@ -46,7 +46,7 @@ export default function ToolTextInput({
fileInputRef.current?.click();
};
return (
<Box id="tool">
<Box>
<InputHeader title={title} />
<TextField
value={value}

View File

@@ -5,12 +5,14 @@ const CheckboxWithDesc = ({
title,
description,
checked,
onChange
onChange,
disabled
}: {
title: string;
description: string;
checked: boolean;
onChange: (value: boolean) => void;
disabled?: boolean;
}) => {
const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
onChange(event.target.checked);
@@ -20,7 +22,11 @@ const CheckboxWithDesc = ({
<Box>
<FormControlLabel
control={
<Checkbox defaultChecked checked={checked} onChange={handleChange} />
<Checkbox
checked={checked}
onChange={handleChange}
disabled={disabled}
/>
}
label={title}
/>

View File

@@ -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: <Home />
},
{
path: '/categories/:categoryName',
element: <ToolsByCategory />
},
{
path: '*',
element: <Navigate to="404" />

View File

@@ -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<string>('');
const [filteredTools, setFilteredTools] = useState<DefinedTool[]>(tools);
const handleInputChange = (
event: React.ChangeEvent<{}>,
newInputValue: string
) => {
setInputValue(newInputValue);
setFilteredTools(filterTools(tools, newInputValue));
};
return (
<Box
padding={5}
@@ -49,108 +18,38 @@ export default function Home() {
justifyContent={'center'}
width={'100%'}
>
<Box width={'60%'}>
<Stack mb={1} direction={'row'} spacing={1}>
<Typography fontSize={30}>Transform Your Workflow with </Typography>
<Typography fontSize={30} color={'primary'}>
Omni Tools
</Typography>
</Stack>
<Typography fontSize={20} mb={2}>
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.
</Typography>
<Autocomplete
sx={{ mb: 2 }}
autoHighlight
options={filteredTools}
getOptionLabel={(option) => option.name}
renderInput={(params) => (
<TextField
{...params}
fullWidth
placeholder={'Search all tools'}
sx={{ borderRadius: 2 }}
InputProps={{
...params.InputProps,
endAdornment: <SearchIcon />
}}
onChange={(event) => handleInputChange(event, event.target.value)}
/>
)}
renderOption={(props, option) => (
<Box
component="li"
{...props}
onClick={() => navigate(option.path)}
>
<Box>
<Typography fontWeight={'bold'}>{option.name}</Typography>
<Typography fontSize={12}>{option.description}</Typography>
</Box>
</Box>
)}
/>
<Grid container spacing={2} mt={2}>
{exampleTools.map((tool) => (
<Grid
onClick={() => navigate(tool.url)}
item
xs={4}
key={tool.label}
>
<Box
sx={{
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
borderWidth: 1,
padding: 1,
borderRadius: 3,
borderColor: 'grey',
borderStyle: 'solid',
cursor: 'pointer'
}}
>
<Typography>{tool.label}</Typography>
</Box>
</Grid>
))}
</Grid>
<Grid container mt={2} spacing={2}>
{getToolsByCategory().map((category) => (
<Grid key={category.type} item xs={6}>
<Card>
<CardContent>
<Link
style={{ fontSize: 20 }}
to={'/categories/' + category.type}
>
{category.title}
</Link>
<Typography sx={{ mt: 2 }}>{category.description}</Typography>
<Stack
mt={2}
direction={'row'}
justifyContent={'space-between'}
>
<Button
variant={'contained'}
>{`See all ${category.title}`}</Button>
<Button
onClick={() => navigate(category.example.path)}
variant={'outlined'}
>{`Try ${category.example.title}`}</Button>
</Stack>
</CardContent>
</Card>
</Grid>
))}
</Grid>
</Box>
<Hero />
<Grid width={'80%'} container mt={2} spacing={2}>
{getToolsByCategory().map((category) => (
<Grid key={category.type} item xs={6}>
<Card>
<CardContent>
<Link
style={{ fontSize: 20 }}
to={'/categories/' + category.type}
>
{category.title}
</Link>
<Typography sx={{ mt: 2 }}>{category.description}</Typography>
<Stack
mt={2}
direction={'row'}
justifyContent={'space-between'}
>
<Button
onClick={() => navigate('/categories/' + category.type)}
variant={'contained'}
>{`See all ${category.title}`}</Button>
<Button
onClick={() => navigate(category.example.path)}
variant={'outlined'}
>{`Try ${category.example.title}`}</Button>
</Stack>
</CardContent>
</Card>
</Grid>
))}
</Grid>
</Box>
);
}

View File

@@ -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'))
});

View File

@@ -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'],

View File

@@ -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'))
});

View File

@@ -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 (
<Stack direction={'row'} alignItems={'center'} spacing={2} mt={4}>
<Box>
<Typography mb={2} fontSize={30} color={'primary'}>
{title}
</Typography>
<Typography fontSize={20}>{description}</Typography>
</Box>
</Stack>
);
}

View File

@@ -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<string>('');
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 (
<Box>
<ToolInputAndResult
@@ -125,6 +200,19 @@ export default function JoinText() {
)}
</Formik>
</ToolOptions>
<Info
title="What Is a Text Joiner?"
description="With this tool you can join parts of the text together. It takes a list of text values, separated by newlines, and merges them together. You can set the character that will be placed between the parts of the combined text. Also, you can ignore all empty lines and remove spaces and tabs at the end of all lines. Textabulous!"
/>
<Separator backgroundColor="#5581b5" margin="50px" />
<Examples
title="Text Joiner Examples"
subtitle="Click to try!"
exampleCards={exampleCards.map((card) => ({
...card,
changeInputResult
}))}
/>
</Box>
);
}

View File

@@ -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'))
});

View File

@@ -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'))
});

View File

@@ -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'))
});

View File

@@ -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 (
<Box>
<Box
padding={5}
display={'flex'}
flexDirection={'column'}
alignItems={'center'}
justifyContent={'center'}
width={'100%'}
>
<Hero />
</Box>
<Divider sx={{ borderColor: theme.palette.primary.main }} />
<Box width={'100%'} mt={3} ml={7} padding={3}>
<Typography
fontSize={22}
color={theme.palette.primary.main}
>{`All ${capitalizeFirstLetter(categoryName)} Tools`}</Typography>
<Grid container spacing={2} mt={2}>
{getToolsByCategory()
.find(({ type }) => type === categoryName)
?.tools?.map((tool) => (
<Grid item xs={12} md={4} key={tool.path}>
<Stack
sx={{
cursor: 'pointer',
'&:hover': {
backgroundColor: theme.palette.background.default // Change this to your desired hover color
}
}}
onClick={() => navigate('/' + tool.path)}
direction={'row'}
spacing={2}
padding={2}
border={1}
borderRadius={2}
>
<img width={100} src={tool.image ?? toolsPng} />
<Box>
<Link to={'/' + tool.path}>{tool.name}</Link>
<Typography sx={{ mt: 2 }}>
{tool.shortDescription}
</Typography>
</Box>
</Stack>
</Grid>
))}
</Grid>
</Box>
</Box>
);
}

23
src/tools/Separator.tsx Normal file
View File

@@ -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 (
<Divider
orientation="horizontal"
variant="fullWidth"
className="my-4"
sx={{
backgroundColor: backgroundColor,
height: '2px',
marginTop: margin,
marginBottom: margin
}}
/>
);
}

View File

@@ -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 (

View File

@@ -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<Record<string, DefinedTool[]>> = 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: '' }

View File

@@ -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);
}