Merge remote-tracking branch 'origin/main' into truncate

# Conflicts:
#	src/pages/tools/string/index.ts
This commit is contained in:
Ibrahima G. Coulibaly
2025-03-07 22:13:09 +00:00
201 changed files with 4915 additions and 2145 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

BIN
src/assets/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

View File

@@ -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<string>('');
@@ -35,11 +36,12 @@ export default function Hero() {
setInputValue(newInputValue);
setFilteredTools(_.shuffle(filterTools(tools, newInputValue)));
};
return (
<Box width={{ xs: '90%', md: '80%', lg: '60%' }}>
<Stack mb={1} direction={'row'} spacing={1} justifyContent={'center'}>
<Typography sx={{ textAlign: 'center' }} fontSize={{ xs: 25, md: 30 }}>
Transform Your Workflow with{' '}
Get Things Done Quickly with{' '}
<Typography
fontSize={{ xs: 25, md: 30 }}
display={'inline'}
@@ -71,10 +73,13 @@ export default function Hero() {
{...params}
fullWidth
placeholder={'Search all tools'}
sx={{ borderRadius: 2 }}
InputProps={{
...params.InputProps,
endAdornment: <SearchIcon />
endAdornment: <SearchIcon />,
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)}
>
<Box>
<Typography fontWeight={'bold'}>{option.name}</Typography>
<Typography fontSize={12}>{option.shortDescription}</Typography>
</Box>
<Stack direction={'row'} spacing={2} alignItems={'center'}>
<Icon fontSize={20} icon={option.icon} />
<Box>
<Typography fontWeight={'bold'}>{option.name}</Typography>
<Typography fontSize={12}>{option.shortDescription}</Typography>
</Box>
</Stack>
</Box>
)}
onChange={(event, newValue) => {
if (newValue) {
navigate('/' + newValue.path);
}
}}
/>
<Grid container spacing={2} mt={2}>
{exampleTools.map((tool) => (
<Grid
onClick={() => 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' }
}}
>
<Typography>{tool.label}</Typography>

View File

@@ -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[] = [
<Icon
onClick={() => window.open('https://discord.gg/SDbbn3hT4b', '_blank')}
style={{ cursor: 'pointer' }}
fontSize={30}
icon={'ic:baseline-discord'}
/>,
<iframe
src="https://ghbtns.com/github-btn.html?user=iib0011&repo=omni-tools&type=star&count=true&size=large"
frameBorder="0"
scrolling="0"
width="130"
height="30"
title="GitHub"
></iframe>,
<Button
onClick={() => {
window.open('https://buymeacoffee.com/iib0011', '_blank');
}}
sx={{ borderRadius: '100px' }}
variant={'contained'}
startIcon={
<Icon
style={{ cursor: 'pointer' }}
fontSize={25}
icon={'mdi:heart-outline'}
/>
}
>
Buy me a coffee
</Button>
];
const drawerList = (
<List>
<ListItemButton onClick={() => navigate('/features')}>
<ListItemText primary="Features" />
</ListItemButton>
<ListItemButton onClick={() => navigate('/about-us')}>
<ListItemText primary="About Us" />
</ListItemButton>
<ListItemButton
component="a"
href="https://github.com/iib0011/omni-tools"
target="_blank"
rel="noopener noreferrer"
>
<img
src={githubIcon}
alt="GitHub"
style={{ height: '24px', marginRight: '8px' }}
/>
<Typography variant="button">Star us</Typography>
</ListItemButton>
{navItems.map((navItem) => (
<ListItemButton
key={navItem.path}
onClick={() => navigate(navItem.path)}
>
<ListItemText primary={navItem.label} />
</ListItemButton>
))}
{buttons.map((button) => (
<ListItem>{button}</ListItem>
))}
</List>
);
return (
<AppBar
position="static"
style={{ backgroundColor: 'white', color: 'black' }}
style={{
backgroundColor: '#F5F5FA',
color: 'black'
}}
>
<Toolbar sx={{ justifyContent: 'space-between', alignItems: 'center' }}>
<Typography
<Toolbar
sx={{
justifyContent: 'space-between',
alignItems: 'center'
}}
>
<img
onClick={() => navigate('/')}
fontSize={20}
sx={{ cursor: 'pointer' }}
color={'primary'}
>
OmniTools
</Typography>
style={{ cursor: 'pointer' }}
src={logo}
width={isMobile ? '80px' : '150px'}
/>
{isMobile ? (
<>
<IconButton color="inherit" onClick={toggleDrawer(true)}>
<IconButton
color="inherit"
onClick={toggleDrawer(true)}
sx={{
'&:hover': {
backgroundColor: theme.palette.primary.main
}
}}
>
<MenuIcon />
</IconButton>
<Drawer
@@ -79,36 +120,28 @@ const Navbar: React.FC = () => {
</Drawer>
</>
) : (
<Stack direction={'row'}>
<Button color="inherit">
<Link
to="/features"
style={{ textDecoration: 'none', color: 'inherit' }}
<Stack direction={'row'} spacing={3} alignItems={'center'}>
{navItems.map((item) => (
<Button
key={item.label}
color="inherit"
sx={{
'&:hover': {
color: theme.palette.primary.main,
transition: 'color 0.3s ease',
backgroundColor: 'white'
}
}}
>
Features
</Link>
</Button>
<Button color="inherit">
<Link
to="/about-us"
style={{ textDecoration: 'none', color: 'inherit' }}
>
About Us
</Link>
</Button>
<IconButton
color="primary"
href="https://github.com/iib0011/omni-tools"
target="_blank"
rel="noopener noreferrer"
>
<img
src={githubIcon}
alt="GitHub"
style={{ height: '24px', marginRight: '8px' }}
/>
<Typography variant="button">Star us</Typography>
</IconButton>
<Link
to={item.path}
style={{ textDecoration: 'none', color: 'inherit' }}
>
{item.label}
</Link>
</Button>
))}
{buttons}
</Stack>
)}
</Toolbar>

View File

@@ -0,0 +1,100 @@
import React, { useRef, useState, ReactNode } from 'react';
import { Box } from '@mui/material';
import { FormikProps, FormikValues } from 'formik';
import ToolOptions, { GetGroupsType } from '@components/options/ToolOptions';
import ToolInputAndResult from '@components/ToolInputAndResult';
import ToolInfo from '@components/ToolInfo';
import Separator from '@components/Separator';
import ToolExamples, {
CardExampleType
} from '@components/examples/ToolExamples';
import { ToolComponentProps } from '@tools/defineTool';
interface ToolContentPropsBase<T, I> extends ToolComponentProps {
// Input/Output components
inputComponent: ReactNode;
resultComponent: ReactNode;
// Tool options
initialValues: T;
getGroups: GetGroupsType<T>;
// Computation function
compute: (optionsValues: T, input: I) => void;
// Tool info (optional)
toolInfo?: {
title: string;
description: string;
};
// Input value to pass to the compute function
input: I;
// Validation schema (optional)
validationSchema?: any;
}
interface ToolContentPropsWithExamples<T, I>
extends ToolContentPropsBase<T, I> {
exampleCards: CardExampleType<T>[];
setInput: React.Dispatch<React.SetStateAction<I>>;
}
interface ToolContentPropsWithoutExamples<T, I>
extends ToolContentPropsBase<T, I> {
exampleCards?: never;
setInput?: never;
}
type ToolContentProps<T, I> =
| ToolContentPropsWithExamples<T, I>
| ToolContentPropsWithoutExamples<T, I>;
export default function ToolContent<T extends FormikValues, I>({
title,
inputComponent,
resultComponent,
initialValues,
getGroups,
compute,
toolInfo,
exampleCards,
input,
setInput,
validationSchema
}: ToolContentProps<T, I>) {
const formRef = useRef<FormikProps<T>>(null);
return (
<Box>
<ToolInputAndResult input={inputComponent} result={resultComponent} />
<ToolOptions
formRef={formRef}
compute={compute}
getGroups={getGroups}
initialValues={initialValues}
input={input}
validationSchema={validationSchema}
/>
{toolInfo && (
<ToolInfo title={toolInfo.title} description={toolInfo.description} />
)}
{exampleCards && exampleCards.length > 0 && (
<>
<Separator backgroundColor="#5581b5" margin="50px" />
<ToolExamples
title={title}
exampleCards={exampleCards}
getGroups={getGroups}
formRef={formRef}
setInput={setInput}
/>
</>
)}
</Box>
);
}

View File

@@ -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 (
<Grid container spacing={2} mt={1}>
<Grid item md={12} lg={4}>
<Button fullWidth variant="outlined" href="#tool">
<Grid item md={12} lg={6}>
<StyledButton
sx={{ backgroundColor: 'white' }}
fullWidth
variant="outlined"
href="#tool"
>
Use This Tool
</Button>
</StyledButton>
</Grid>
<Grid item md={12} lg={4}>
<Button fullWidth variant="outlined" href="#examples">
<Grid item md={12} lg={6}>
<StyledButton fullWidth variant="outlined" href="#examples">
See Examples
</Button>
</Grid>
<Grid item md={12} lg={4}>
<Button fullWidth variant="outlined" href="#tour">
Learn How to Use
</Button>
</StyledButton>
</Grid>
{/*<Grid item md={12} lg={4}>*/}
{/* <StyledButton fullWidth variant="outlined" href="#tour">*/}
{/* Learn How to Use*/}
{/* </StyledButton>*/}
{/*</Grid>*/}
</Grid>
);
}
export default function ToolHeader({
image,
icon,
title,
description,
type
@@ -60,10 +77,18 @@ export default function ToolHeader({
<ToolLinks />
</Grid>
{image && (
{icon && (
<Grid item xs={12} md={4}>
<Box sx={{ display: 'flex', justifyContent: 'center' }}>
<img width={'250'} src={image} />
<Icon
icon={icon}
fontSize={'250'}
color={
categoriesColors[
Math.floor(Math.random() * categoriesColors.length)
]
}
/>
</Box>
</Grid>
)}

View File

@@ -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' }}
>
<Helmet>
<title>{`${title} - Omni Tools`}</title>
@@ -44,7 +47,7 @@ export default function ToolLayout({
<ToolHeader
title={title}
description={description}
image={image}
icon={icon}
type={type}
/>
{children}

View File

@@ -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}
/>
</Grid>
))}

View File

@@ -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 (
<Card
@@ -28,9 +34,12 @@ export default function ToolCard({ title, description, link }: ToolCardProps) {
borderColor: '#ffffff70'
}}
>
<Typography variant="h5" component="h2">
{title}
</Typography>
<Stack direction={'row'} spacing={2} alignItems={'center'}>
<Icon icon={icon} fontSize={25} />
<Typography variant="h5" component="h2">
{title}
</Typography>
</Stack>
<Link href={link} underline="none" sx={{ color: '#fff' }}>
<ChevronRightIcon />
</Link>

View File

@@ -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<T> {
title: string;
description: string;
sampleText: string;
sampleResult: string;
sampleOptions: T;
changeInputResult: (newInput: string, newOptions: T) => void;
getGroups: GetGroupsType<T>;
}
export default function ExampleCard<T>({
title,
description,
sampleText,
sampleResult,
requiredOptions,
changeInputResult
}: ExampleCardProps) {
sampleOptions,
changeInputResult,
getGroups
}: ExampleCardProps<T>) {
const theme = useTheme();
return (
<Card
raised
onClick={() => {
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({
</Typography>
<Box
onClick={() => 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({
<ArrowDownwardIcon />
<Box
onClick={() => changeInputResult(sampleText, sampleResult)}
sx={{
display: 'flex',
zIndex: '2',
@@ -106,7 +118,7 @@ export default function ExampleCard({
/>
</Box>
<RequiredOptions options={requiredOptions} />
<ExampleOptions options={sampleOptions} getGroups={getGroups} />
</Stack>
</CardContent>
</Card>

View File

@@ -0,0 +1,19 @@
import ToolOptionGroups from '@components/options/ToolOptionGroups';
import { GetGroupsType } from '@components/options/ToolOptions';
import React from 'react';
export default function ExampleOptions<T>({
options,
getGroups
}: {
options: T;
getGroups: GetGroupsType<T>;
}) {
return (
<ToolOptionGroups
// @ts-ignore
groups={getGroups({ values: options })}
vertical
/>
);
}

View File

@@ -1,59 +0,0 @@
import { Box, Grid, Stack, Typography } from '@mui/material';
import ExampleCard from './ExampleCard';
export interface ExampleCardProps {
title: string;
description: string;
sampleText: string;
sampleResult: string;
requiredOptions: RequiredOptionsProps;
changeInputResult: (input: string, result: string) => void;
}
export interface RequiredOptionsProps {
joinCharacter: string;
deleteBlankLines: boolean;
deleteTrailingSpaces: boolean;
}
interface ExampleProps {
title: string;
subtitle: string;
exampleCards: ExampleCardProps[];
}
export default function Examples({
title,
subtitle,
exampleCards
}: ExampleProps) {
return (
<Box id={'examples'} mt={4}>
<Box mt={4} display="flex" gap={1} alignItems="center">
<Typography mb={2} fontSize={30} color={'primary'}>
{title}
</Typography>
<Typography mb={2} fontSize={30} color={'secondary'}>
{subtitle}
</Typography>
</Box>
<Stack direction={'row'} alignItems={'center'} spacing={2}>
<Grid container spacing={2}>
{exampleCards.map((card, index) => (
<Grid item xs={12} md={6} lg={4} key={index}>
<ExampleCard
title={card.title}
description={card.description}
sampleText={card.sampleText}
sampleResult={card.sampleResult}
requiredOptions={card.requiredOptions}
changeInputResult={card.changeInputResult}
/>
</Grid>
))}
</Grid>
</Stack>
</Box>
);
}

View File

@@ -1,78 +0,0 @@
import { Box, Stack, TextField, Typography } from '@mui/material';
import { RequiredOptionsProps } from './Examples';
import CheckboxWithDesc from 'components/options/CheckboxWithDesc';
export default function RequiredOptions({
options
}: {
options: RequiredOptionsProps;
}) {
const { joinCharacter, deleteBlankLines, deleteTrailingSpaces } = options;
const handleBoxClick = () => {
const toolsElement = document.getElementById('tool');
if (toolsElement) {
toolsElement.scrollIntoView({ behavior: 'smooth' });
}
};
return (
<Stack direction={'column'} alignItems={'left'} spacing={2}>
<Typography variant="h5" component="h3" sx={{ marginTop: '5px' }}>
Required options
</Typography>
<Typography variant="body2" component="p">
These options will be used automatically if you select this example.
</Typography>
<Box
onClick={handleBoxClick}
sx={{
zIndex: '2',
cursor: 'pointer',
bgcolor: 'transparent',
width: '100%',
height: '100%',
display: 'flex'
}}
>
<TextField
disabled
value={joinCharacter}
fullWidth
rows={1}
sx={{
'& .MuiOutlinedInput-root': {
zIndex: '-1'
}
}}
/>
</Box>
{deleteBlankLines ? (
<Box onClick={handleBoxClick}>
<CheckboxWithDesc
title="Delete Blank Lines"
checked={deleteBlankLines}
onChange={() => {}}
description="Delete lines that don't have text symbols."
/>
</Box>
) : (
''
)}
{deleteTrailingSpaces ? (
<Box onClick={handleBoxClick}>
<CheckboxWithDesc
title="Delete Training Spaces"
checked={deleteTrailingSpaces}
onChange={() => {}}
description="Remove spaces and tabs at the end of the lines."
/>
</Box>
) : (
''
)}
</Stack>
);
}

View File

@@ -0,0 +1,68 @@
import { Box, Grid, Stack, Typography } from '@mui/material';
import ExampleCard, { ExampleCardProps } from './ExampleCard';
import React from 'react';
import { GetGroupsType } from '@components/options/ToolOptions';
import { FormikProps } from 'formik';
export type CardExampleType<T> = Omit<
ExampleCardProps<T>,
'getGroups' | 'changeInputResult'
>;
export interface ExampleProps<T> {
title: string;
subtitle?: string;
exampleCards: CardExampleType<T>[];
getGroups: GetGroupsType<T>;
formRef: React.RefObject<FormikProps<T>>;
setInput: React.Dispatch<React.SetStateAction<any>>;
}
export default function ToolExamples<T>({
title,
subtitle,
exampleCards,
getGroups,
formRef,
setInput
}: ExampleProps<T>) {
function changeInputResult(newInput: string, newOptions: T) {
setInput(newInput);
formRef.current?.setValues(newOptions);
const toolsElement = document.getElementById('tool');
if (toolsElement) {
toolsElement.scrollIntoView({ behavior: 'smooth' });
}
}
return (
<Box id={'examples'} mt={4}>
<Box mt={4} display="flex" gap={1} alignItems="center">
<Typography mb={2} fontSize={30} color={'primary'}>
{`${title} Examples`}
</Typography>
<Typography mb={2} fontSize={30} color={'secondary'}>
{subtitle ?? 'Click to try!'}
</Typography>
</Box>
<Stack direction={'row'} alignItems={'center'} spacing={2}>
<Grid container spacing={2}>
{exampleCards.map((card, index) => (
<Grid item xs={12} md={6} lg={4} key={index}>
<ExampleCard
title={card.title}
description={card.description}
sampleText={card.sampleText}
sampleResult={card.sampleResult}
sampleOptions={card.sampleOptions}
getGroups={getGroups}
changeInputResult={changeInputResult}
/>
</Grid>
))}
</Grid>
</Stack>
</Box>
);
}

View File

@@ -5,3 +5,7 @@ a {
a:hover {
color: #030362;
}
* {
font-family: Plus Jakarta Sans, sans-serif;
}

View File

@@ -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 (
<Box>
<InputHeader title={title} />
@@ -66,7 +83,8 @@ export default function ToolFileInput({
height: globalInputHeight,
border: preview ? 0 : 1,
borderRadius: 2,
boxShadow: '5'
boxShadow: '5',
bgcolor: 'white'
}}
>
{preview ? (

View File

@@ -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'
}}
/>
<InputFooter handleCopy={handleCopy} handleImport={handleImportClick} />
<input

View File

@@ -8,14 +8,16 @@ export interface ToolOptionGroup {
}
export default function ToolOptionGroups({
groups
groups,
vertical
}: {
groups: ToolOptionGroup[];
vertical?: boolean;
}) {
return (
<Grid container spacing={2}>
{groups.map((group) => (
<Grid item xs={12} md={4} key={group.title}>
<Grid item xs={12} md={vertical ? 12 : 4} key={group.title}>
<Typography mb={1} fontSize={22}>
{group.title}
</Typography>

View File

@@ -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<T> = <Y extends keyof T>(field: Y, value: T[Y]) => void;
export type UpdateField<T> = <Y extends keyof T>(field: Y, value: T[Y]) => void;
const FormikListenerComponent = <T,>({
initialValues,
@@ -67,6 +68,10 @@ const ToolBody = <T,>({
</Stack>
);
};
export type GetGroupsType<T> = (
formikProps: FormikProps<T> & { updateField: UpdateField<T> }
) => ToolOptionGroup[];
export default function ToolOptions<T extends FormikValues>({
children,
initialValues,
@@ -78,12 +83,10 @@ export default function ToolOptions<T extends FormikValues>({
}: {
children?: ReactNode;
initialValues: T;
validationSchema: any | (() => any);
validationSchema?: any | (() => any);
compute: (optionsValues: T, input: any) => void;
input?: any;
getGroups: (
formikProps: FormikProps<T> & { updateField: UpdateField<T> }
) => ToolOptionGroup[];
getGroups: GetGroupsType<T>;
formRef?: RefObject<FormikProps<T>>;
}) {
const theme = useTheme();
@@ -93,7 +96,8 @@ export default function ToolOptions<T extends FormikValues>({
mb: 2,
borderRadius: 2,
padding: 2,
backgroundColor: theme.palette.background.default
backgroundColor: theme.palette.background.default,
boxShadow: '2'
}}
mt={2}
>

View File

@@ -67,7 +67,8 @@ export default function ToolFileResult({
height: globalInputHeight,
border: preview ? 0 : 1,
borderRadius: 2,
boxShadow: '5'
boxShadow: '5',
bgcolor: 'white'
}}
>
{preview && (

View File

@@ -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' }}
/>

View File

@@ -1,2 +1,8 @@
export const globalInputHeight = 300;
export const globalDescriptionFontSize = 12;
export const categoriesColors: string[] = [
'#8FBC5D',
'#3CB6E2',
'#FFD400',
'#AB6993'
];

View File

@@ -0,0 +1,94 @@
import { getToolsByCategory } from '@tools/index';
import Grid from '@mui/material/Grid';
import { Box, Card, CardContent, Stack } from '@mui/material';
import { Link, useNavigate } from 'react-router-dom';
import Typography from '@mui/material/Typography';
import Button from '@mui/material/Button';
import { useState } from 'react';
import { categoriesColors } from 'config/uiConfig';
import { Icon } from '@iconify/react';
type ArrayElement<ArrayType extends readonly unknown[]> =
ArrayType extends readonly (infer ElementType)[] ? ElementType : never;
const SingleCategory = function ({
category,
index
}: {
category: ArrayElement<ReturnType<typeof getToolsByCategory>>;
index: number;
}) {
const navigate = useNavigate();
const [hovered, setHovered] = useState<boolean>(false);
const toggleHover = () => setHovered((prevState) => !prevState);
return (
<Grid
item
xs={12}
md={6}
onMouseEnter={toggleHover}
onMouseLeave={toggleHover}
>
<Card
sx={{
height: '100%',
backgroundColor: hovered ? '#FAFAFD' : 'white'
}}
>
<CardContent sx={{ height: '100%' }}>
<Stack
direction={'column'}
height={'100%'}
justifyContent={'space-between'}
>
<Box>
<Stack direction={'row'} spacing={2} alignItems={'center'}>
<Icon
icon={category.icon}
fontSize={'60px'}
style={{
transform: `scale(${hovered ? 1.1 : 1}`
}}
color={categoriesColors[index % categoriesColors.length]}
/>
<Link
style={{ fontSize: 20, fontWeight: 700, color: 'black' }}
to={'/categories/' + category.type}
>
{category.title}
</Link>
</Stack>
<Typography sx={{ mt: 2 }}>{category.description}</Typography>
</Box>
<Grid mt={1} container spacing={2}>
<Grid item xs={12} md={6}>
<Button
fullWidth
onClick={() => navigate('/categories/' + category.type)}
variant={'contained'}
>{`See all ${category.title}`}</Button>
</Grid>
<Grid item xs={12} md={6}>
<Button
sx={{ backgroundColor: 'white' }}
fullWidth
onClick={() => navigate(category.example.path)}
variant={'outlined'}
>{`Try ${category.example.title}`}</Button>
</Grid>
</Grid>
</Stack>
</CardContent>
</Card>
</Grid>
);
};
export default function Categories() {
return (
<Grid width={'80%'} container mt={2} spacing={2}>
{getToolsByCategory().map((category, index) => (
<SingleCategory key={category.type} category={category} index={index} />
))}
</Grid>
);
}

View File

@@ -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 (
<Box
padding={{ xs: 1, md: 3, lg: 5 }}
padding={{
xs: 1,
md: 3,
lg: 5,
background: `url(/assets/background.svg)`,
backgroundColor: '#F5F5FA'
}}
display={'flex'}
flexDirection={'column'}
alignItems={'center'}
@@ -19,39 +19,7 @@ export default function Home() {
width={'100%'}
>
<Hero />
<Grid width={'80%'} container mt={2} spacing={2}>
{getToolsByCategory().map((category) => (
<Grid key={category.type} item xs={12} md={6}>
<Card sx={{ height: '100%' }}>
<CardContent>
<Link
style={{ fontSize: 20 }}
to={'/categories/' + category.type}
>
{category.title}
</Link>
<Typography sx={{ mt: 2 }}>{category.description}</Typography>
<Grid mt={1} container spacing={2}>
<Grid item xs={12} md={6}>
<Button
fullWidth
onClick={() => navigate('/categories/' + category.type)}
variant={'contained'}
>{`See all ${category.title}`}</Button>
</Grid>
<Grid item xs={12} md={6}>
<Button
fullWidth
onClick={() => navigate(category.example.path)}
variant={'outlined'}
>{`Try ${category.example.title}`}</Button>
</Grid>
</Grid>
</CardContent>
</Card>
</Grid>
))}
</Grid>
<Categories />
</Box>
);
}

View File

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

View File

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

View File

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

View File

@@ -1,11 +0,0 @@
import { Box } from '@mui/material';
import React from 'react';
import * as Yup from 'yup';
const initialValues = {};
const validationSchema = Yup.object({
// splitSeparator: Yup.string().required('The separator is required')
});
export default function Rotate() {
return <Box>Lorem ipsum</Box>;
}

View File

@@ -1,11 +0,0 @@
import { Box } from '@mui/material';
import React from 'react';
import * as Yup from 'yup';
const initialValues = {};
const validationSchema = Yup.object({
// splitSeparator: Yup.string().required('The separator is required')
});
export default function Shuffle() {
return <Box>Lorem ipsum</Box>;
}

View File

@@ -1,119 +0,0 @@
import { Box } from '@mui/material';
import React, { useState } from 'react';
import ToolTextInput from '../../../components/input/ToolTextInput';
import ToolTextResult from '../../../components/result/ToolTextResult';
import * as Yup from 'yup';
import ToolOptions from '../../../components/options/ToolOptions';
import { compute, NumberExtractionType } from './service';
import RadioWithTextField from '../../../components/options/RadioWithTextField';
import SimpleRadio from '../../../components/options/SimpleRadio';
import CheckboxWithDesc from '../../../components/options/CheckboxWithDesc';
import ToolInputAndResult from '../../../components/ToolInputAndResult';
const initialValues = {
extractionType: 'smart' as NumberExtractionType,
separator: '\\n',
printRunningSum: false
};
const extractionTypes: {
title: string;
description: string;
type: NumberExtractionType;
withTextField: boolean;
textValueAccessor?: keyof typeof initialValues;
}[] = [
{
title: 'Smart sum',
description: 'Auto detect numbers in the input.',
type: 'smart',
withTextField: false
},
{
title: 'Number Delimiter',
type: 'delimiter',
description:
'Input SeparatorCustomize the number separator here. (By default a line break.)',
withTextField: true,
textValueAccessor: 'separator'
}
];
export default function SplitText() {
const [input, setInput] = useState<string>('');
const [result, setResult] = useState<string>('');
const validationSchema = Yup.object({
// splitSeparator: Yup.string().required('The separator is required')
});
return (
<Box>
<ToolInputAndResult
input={<ToolTextInput value={input} onChange={setInput} />}
result={<ToolTextResult title={'Total'} value={result} />}
/>
<ToolOptions
getGroups={({ values, updateField }) => [
{
title: 'Number extraction',
component: extractionTypes.map(
({
title,
description,
type,
withTextField,
textValueAccessor
}) =>
withTextField ? (
<RadioWithTextField
key={type}
checked={type === values.extractionType}
title={title}
fieldName={'extractionType'}
description={description}
value={
textValueAccessor
? values[textValueAccessor].toString()
: ''
}
onRadioClick={() => updateField('extractionType', type)}
onTextChange={(val) =>
textValueAccessor
? updateField(textValueAccessor, val)
: null
}
/>
) : (
<SimpleRadio
key={title}
onClick={() => updateField('extractionType', type)}
checked={values.extractionType === type}
description={description}
title={title}
/>
)
)
},
{
title: 'Running Sum',
component: (
<CheckboxWithDesc
title={'Print Running Sum'}
description={"Display the sum as it's calculated step by step."}
checked={values.printRunningSum}
onChange={(value) => updateField('printRunningSum', value)}
/>
)
}
]}
compute={(optionsValues, input) => {
const { extractionType, printRunningSum, separator } = optionsValues;
setResult(compute(input, extractionType, printRunningSum, separator));
}}
initialValues={initialValues}
input={input}
validationSchema={validationSchema}
/>
</Box>
);
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,6 +0,0 @@
export function randomizeCase(input: string): string {
return input
.split('')
.map(char => (Math.random() < 0.5 ? char.toLowerCase() : char.toUpperCase()))
.join('');
}

View File

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

View File

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

View File

@@ -1,153 +0,0 @@
import { Box } from '@mui/material';
import React, { useState } from 'react';
import ToolTextInput from '../../../components/input/ToolTextInput';
import ToolTextResult from '../../../components/result/ToolTextResult';
import * as Yup from 'yup';
import ToolOptions from '../../../components/options/ToolOptions';
import { compute, SplitOperatorType } from './service';
import RadioWithTextField from '../../../components/options/RadioWithTextField';
import TextFieldWithDesc from '../../../components/options/TextFieldWithDesc';
import ToolInputAndResult from '../../../components/ToolInputAndResult';
const initialValues = {
splitSeparatorType: 'symbol' as SplitOperatorType,
symbolValue: ' ',
regexValue: '/\\s+/',
lengthValue: '16',
chunksValue: '4',
outputSeparator: '\\n',
charBeforeChunk: '',
charAfterChunk: ''
};
const splitOperators: {
title: string;
description: string;
type: SplitOperatorType;
}[] = [
{
title: 'Use a Symbol for Splitting',
description:
'Character that will be used to\n' +
'break text into parts.\n' +
'(Space by default.)',
type: 'symbol'
},
{
title: 'Use a Regex for Splitting',
type: 'regex',
description:
'Regular expression that will be\n' +
'used to break text into parts.\n' +
'(Multiple spaces by default.)'
},
{
title: 'Use Length for Splitting',
description:
'Number of symbols that will be\n' + 'put in each output chunk.',
type: 'length'
},
{
title: 'Use a Number of Chunks',
description: 'Number of chunks of equal\n' + 'length in the output.',
type: 'chunks'
}
];
const outputOptions: {
description: string;
accessor: keyof typeof initialValues;
}[] = [
{
description:
'Character that will be put\n' +
'between the split chunks.\n' +
'(It\'s newline "\\n" by default.)',
accessor: 'outputSeparator'
},
{
description: 'Character before each chunk',
accessor: 'charBeforeChunk'
},
{
description: 'Character after each chunk',
accessor: 'charAfterChunk'
}
];
export default function SplitText() {
const [input, setInput] = useState<string>('');
const [result, setResult] = useState<string>('');
// const formRef = useRef<FormikProps<typeof initialValues>>(null);
const computeExternal = (optionsValues: typeof initialValues, input: any) => {
const {
splitSeparatorType,
outputSeparator,
charBeforeChunk,
charAfterChunk,
chunksValue,
symbolValue,
regexValue,
lengthValue
} = optionsValues;
setResult(
compute(
splitSeparatorType,
input,
symbolValue,
regexValue,
Number(lengthValue),
Number(chunksValue),
charBeforeChunk,
charAfterChunk,
outputSeparator
)
);
};
const validationSchema = Yup.object({
// splitSeparator: Yup.string().required('The separator is required')
});
return (
<Box>
<ToolInputAndResult
input={<ToolTextInput value={input} onChange={setInput} />}
result={<ToolTextResult title={'Text pieces'} value={result} />}
/>
<ToolOptions
compute={computeExternal}
getGroups={({ values, updateField }) => [
{
title: 'Split separator options',
component: splitOperators.map(({ title, description, type }) => (
<RadioWithTextField
key={type}
checked={type === values.splitSeparatorType}
title={title}
fieldName={'splitSeparatorType'}
description={description}
value={values[`${type}Value`]}
onRadioClick={() => updateField('splitSeparatorType', type)}
onTextChange={(val) => updateField(`${type}Value`, val)}
/>
))
},
{
title: 'Output separator options',
component: outputOptions.map((option) => (
<TextFieldWithDesc
key={option.accessor}
value={values[option.accessor]}
onOwnChange={(value) => updateField(option.accessor, value)}
description={option.description}
/>
))
}
]}
initialValues={initialValues}
input={input}
validationSchema={validationSchema}
/>
</Box>
);
}

View File

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

View File

@@ -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 (
<Box>
<Box sx={{ backgroundColor: '#F5F5FA' }}>
<Box
padding={{ xs: 1, md: 3, lg: 5 }}
display={'flex'}
@@ -32,10 +33,12 @@ export default function Home() {
<Grid container spacing={2} mt={2}>
{getToolsByCategory()
.find(({ type }) => type === categoryName)
?.tools?.map((tool) => (
?.tools?.map((tool, index) => (
<Grid item xs={12} md={6} lg={4} key={tool.path}>
<Stack
sx={{
backgroundColor: 'white',
boxShadow: '5px 4px 2px #E9E9ED',
cursor: 'pointer',
'&:hover': {
backgroundColor: theme.palette.background.default // Change this to your desired hover color
@@ -43,14 +46,21 @@ export default function Home() {
}}
onClick={() => navigate('/' + tool.path)}
direction={'row'}
alignItems={'center'}
spacing={2}
padding={2}
border={1}
border={`1px solid ${theme.palette.background.default}`}
borderRadius={2}
>
<img width={100} src={tool.image ?? toolsPng} />
<Icon
icon={tool.icon ?? 'ph:compass-tool-thin'}
fontSize={'60px'}
color={categoriesColors[index % categoriesColors.length]}
/>
<Box>
<Link to={'/' + tool.path}>{tool.name}</Link>
<Link style={{ fontSize: 20 }} to={'/' + tool.path}>
{tool.name}
</Link>
<Typography sx={{ mt: 2 }}>
{tool.shortDescription}
</Typography>

View File

@@ -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 }) => {

View File

@@ -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<File | null>(null);
const [result, setResult] = useState<File | null>(null);
@@ -83,59 +85,65 @@ export default function ChangeColorsInPng() {
processImage(input, fromRgb, toRgb, Number(similarity));
};
const getGroups: GetGroupsType<typeof initialValues> = ({
values,
updateField
}) => [
{
title: 'From color and to color',
component: (
<Box>
<ColorSelector
value={values.fromColor}
onColorChange={(val) => updateField('fromColor', val)}
description={'Replace this color (from color)'}
inputProps={{ 'data-testid': 'from-color-input' }}
/>
<ColorSelector
value={values.toColor}
onColorChange={(val) => updateField('toColor', val)}
description={'With this color (to color)'}
inputProps={{ 'data-testid': 'to-color-input' }}
/>
<TextFieldWithDesc
value={values.similarity}
onOwnChange={(val) => updateField('similarity', val)}
description={
'Match this % of similar colors of the from color. For example, 10% white will match white and a little bit of gray.'
}
/>
</Box>
)
}
];
return (
<Box>
<ToolInputAndResult
input={
<ToolFileInput
value={input}
onChange={setInput}
accept={['image/png']}
title={'Input PNG'}
/>
}
result={
<ToolFileResult
title={'Output PNG with new colors'}
value={result}
extension={'png'}
/>
}
/>
<ToolOptions
compute={compute}
getGroups={({ values, updateField }) => [
{
title: 'From color and to color',
component: (
<Box>
<ColorSelector
value={values.fromColor}
onColorChange={(val) => updateField('fromColor', val)}
description={'Replace this color (from color)'}
inputProps={{ 'data-testid': 'from-color-input' }}
/>
<ColorSelector
value={values.toColor}
onColorChange={(val) => updateField('toColor', val)}
description={'With this color (to color)'}
inputProps={{ 'data-testid': 'to-color-input' }}
/>
<TextFieldWithDesc
value={values.similarity}
onOwnChange={(val) => updateField('similarity', val)}
description={
'Match this % of similar colors of the from color. For example, 10% white will match white and a little bit of gray.'
}
/>
</Box>
)
}
]}
initialValues={initialValues}
input={input}
validationSchema={validationSchema}
/>
</Box>
<ToolContent
title={title}
initialValues={initialValues}
getGroups={getGroups}
compute={compute}
input={input}
validationSchema={validationSchema}
inputComponent={
<ToolFileInput
value={input}
onChange={setInput}
accept={['image/png']}
title={'Input PNG'}
/>
}
resultComponent={
<ToolFileResult
title={'Transparent PNG'}
value={result}
extension={'png'}
/>
}
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.'
}}
/>
);
}

View File

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

View File

Before

Width:  |  Height:  |  Size: 3.6 KiB

After

Width:  |  Height:  |  Size: 3.6 KiB

View File

@@ -0,0 +1,113 @@
import { Box } from '@mui/material';
import React, { useState } from 'react';
import * as Yup from 'yup';
import ToolFileInput from '@components/input/ToolFileInput';
import ToolFileResult from '@components/result/ToolFileResult';
import ToolOptions from '@components/options/ToolOptions';
import TextFieldWithDesc from 'components/options/TextFieldWithDesc';
import ToolInputAndResult from '@components/ToolInputAndResult';
import imageCompression from 'browser-image-compression';
import Typography from '@mui/material/Typography';
const initialValues = {
rate: '50'
};
const validationSchema = Yup.object({
// splitSeparator: Yup.string().required('The separator is required')
});
export default function ChangeColorsInPng() {
const [input, setInput] = useState<File | null>(null);
const [result, setResult] = useState<File | null>(null);
const [originalSize, setOriginalSize] = useState<number | null>(null); // Store original file size
const [compressedSize, setCompressedSize] = useState<number | null>(null); // Store compressed file size
const compressImage = async (file: File, rate: number) => {
if (!file) return;
// Set original file size
setOriginalSize(file.size);
const options = {
maxSizeMB: 1, // Maximum size in MB
maxWidthOrHeight: 1024, // Maximum width or height
quality: rate / 100, // Convert percentage to decimal (e.g., 50% becomes 0.5)
useWebWorker: true
};
try {
const compressedFile = await imageCompression(file, options);
setResult(compressedFile);
setCompressedSize(compressedFile.size); // Set compressed file size
} catch (error) {
console.error('Error during compression:', error);
}
};
const compute = (optionsValues: typeof initialValues, input: any) => {
if (!input) return;
const { rate } = optionsValues;
compressImage(input, Number(rate)); // Pass the rate as a number
};
return (
<Box>
<ToolInputAndResult
input={
<ToolFileInput
value={input}
onChange={setInput}
accept={['image/png']}
title={'Input PNG'}
/>
}
result={
<ToolFileResult
title={'Compressed PNG'}
value={result}
extension={'png'}
/>
}
/>
<ToolOptions
compute={compute}
getGroups={({ values, updateField }) => [
{
title: 'Compression options',
component: (
<Box>
<TextFieldWithDesc
value={values.rate}
onOwnChange={(val) => updateField('rate', val)}
description={'Compression rate (1-100)'}
/>
</Box>
)
},
{
title: 'File sizes',
component: (
<Box>
<Box>
{originalSize !== null && (
<Typography>
Original Size: {(originalSize / 1024).toFixed(2)} KB
</Typography>
)}
{compressedSize !== null && (
<Typography>
Compressed Size: {(compressedSize / 1024).toFixed(2)} KB
</Typography>
)}
</Box>
</Box>
)
}
]}
initialValues={initialValues}
input={input}
/>
</Box>
);
}

View File

@@ -0,0 +1,14 @@
import { defineTool } from '@tools/defineTool';
import { lazy } from 'react';
// import image from '@assets/text.png';
export const tool = defineTool('png', {
name: 'Compress png',
path: 'compress-png',
icon: 'material-symbols-light:compress',
description:
'This is a program that compresses PNG pictures. As soon as you paste your PNG picture in the input area, the program will compress it and show the result in the output area. In the options, you can adjust the compression level, as well as find the old and new picture file sizes.',
shortDescription: 'Quicly compress a PNG',
keywords: ['compress', 'png'],
component: lazy(() => import('./index'))
});

View File

@@ -148,7 +148,6 @@ export default function ConvertJgpToPng() {
]}
initialValues={initialValues}
input={input}
validationSchema={validationSchema}
/>
</Box>
);

View File

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

View File

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

View File

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

View File

@@ -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.",

View File

Before

Width:  |  Height:  |  Size: 3.6 KiB

After

Width:  |  Height:  |  Size: 3.6 KiB

View File

@@ -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
];

View File

@@ -0,0 +1,3 @@
import { tool as jsonPrettify } from './prettify/meta';
export const jsonTools = [jsonPrettify];

View File

@@ -0,0 +1,190 @@
import { Box } from '@mui/material';
import React, { useRef, useState } from 'react';
import ToolTextInput from '@components/input/ToolTextInput';
import ToolTextResult from '@components/result/ToolTextResult';
import ToolOptions, { GetGroupsType } from '@components/options/ToolOptions';
import { beautifyJson } from './service';
import ToolInputAndResult from '@components/ToolInputAndResult';
import ToolInfo from '@components/ToolInfo';
import Separator from '@components/Separator';
import ToolExamples, {
CardExampleType
} from '@components/examples/ToolExamples';
import { FormikProps } from 'formik';
import { ToolComponentProps } from '@tools/defineTool';
import RadioWithTextField from '@components/options/RadioWithTextField';
import SimpleRadio from '@components/options/SimpleRadio';
import { isNumber } from '../../../../utils/string';
type InitialValuesType = {
indentationType: 'tab' | 'space';
spacesCount: number;
};
const initialValues: InitialValuesType = {
indentationType: 'space',
spacesCount: 2
};
const exampleCards: CardExampleType<InitialValuesType>[] = [
{
title: 'Beautify an Ugly JSON Array',
description:
'In this example, we prettify an ugly JSON array. The input data is a one-dimensional array of numbers [1,2,3] but they are all over the place. This array gets cleaned up and transformed into a more readable format where each element is on a new line with an appropriate indentation using four spaces.',
sampleText: `[
1,
2,3
]`,
sampleResult: `[
1,
2,
3
]`,
sampleOptions: {
indentationType: 'space',
spacesCount: 4
}
},
{
title: 'Prettify a Complex JSON Object',
description:
'In this example, we prettify a complex JSON data structure consisting of arrays and objects. The input data is a minified JSON object with multiple data structure depth levels. To make it neat and readable, we add two spaces for indentation to each depth level, making the JSON structure clear and easy to understand.',
sampleText: `{"names":["jack","john","alex"],"hobbies":{"jack":["programming","rock climbing"],"john":["running","racing"],"alex":["dancing","fencing"]}}`,
sampleResult: `{
"names": [
"jack",
"john",
"alex"
],
"hobbies": {
"jack": [
"programming",
"rock climbing"
],
"john": [
"running",
"racing"
],
"alex": [
"dancing",
"fencing"
]
}
}`,
sampleOptions: {
indentationType: 'space',
spacesCount: 2
}
},
{
title: 'Beautify a JSON with Excessive Whitespace',
description:
"In this example, we show how the JSON prettify tool can handle code with excessive whitespace. The input file has many leading and trailing spaces as well as spaces within the objects. The excessive whitespace makes the file bulky and hard to read and leads to a bad impression of the programmer who wrote it. The program removes all these unnecessary spaces and creates a proper data hierarchy that's easy to work with by adding indentation via tabs.",
sampleText: `
{
"name": "The Name of the Wind",
"author" : "Patrick Rothfuss",
"genre" : "Fantasy",
"published" : 2007,
"rating" : {
"average" : 4.6,
"goodreads" : 4.58,
"amazon" : 4.4
},
"is_fiction" : true
}
`,
sampleResult: `{
\t"name": "The Name of the Wind",
\t"author": "Patrick Rothfuss",
\t"genre": "Fantasy",
\t"published": 2007,
\t"rating": {
\t\t"average": 4.6,
\t\t"goodreads": 4.58,
\t\t"amazon": 4.4
\t},
\t"is_fiction": true
}`,
sampleOptions: {
indentationType: 'tab',
spacesCount: 0
}
}
];
export default function PrettifyJson({ title }: ToolComponentProps) {
const [input, setInput] = useState<string>('');
const [result, setResult] = useState<string>('');
const formRef = useRef<FormikProps<InitialValuesType>>(null);
const compute = (optionsValues: InitialValuesType, input: any) => {
const { indentationType, spacesCount } = optionsValues;
if (input) setResult(beautifyJson(input, indentationType, spacesCount));
};
const getGroups: GetGroupsType<InitialValuesType> = ({
values,
updateField
}) => [
{
title: 'Indentation',
component: (
<Box>
<RadioWithTextField
checked={values.indentationType === 'space'}
title={'Use Spaces'}
fieldName={'indentationType'}
description={'Indent output with spaces'}
value={values.spacesCount.toString()}
onRadioClick={() => updateField('indentationType', 'space')}
onTextChange={(val) =>
isNumber(val) ? updateField('spacesCount', Number(val)) : null
}
/>
<SimpleRadio
onClick={() => updateField('indentationType', 'tab')}
checked={values.indentationType === 'tab'}
description={'Indent output with tabs.'}
title={'Use Tabs'}
/>
</Box>
)
}
];
return (
<Box>
<ToolInputAndResult
input={
<ToolTextInput
title={'Input JSON'}
value={input}
onChange={setInput}
/>
}
result={<ToolTextResult title={'Pretty JSON'} value={result} />}
/>
<ToolOptions
formRef={formRef}
compute={compute}
getGroups={getGroups}
initialValues={initialValues}
input={input}
/>
<ToolInfo
title="What Is a JSON Prettifier?"
description="This tool adds consistent formatting to the data in JavaScript Object Notation (JSON) format. This transformation makes the JSON code more readable, making it easier to understand and edit. The program parses the JSON data structure into tokens and then reformats them by adding indentation and line breaks. If the data is hierarchial, then it adds indentation at the beginning of lines to visually show the depth of the JSON and adds newlines to break long single-line JSON arrays into multiple shorter, more readable ones. Additionally, this utility can remove unnecessary spaces and tabs from your JSON code (especially leading and trailing whitespaces), making it more compact. You can choose the line indentation method in the options: indent with spaces or indent with tabs. When using spaces, you can also specify how many spaces to use for each indentation level (usually 2 or 4 spaces). "
/>
<Separator backgroundColor="#5581b5" margin="50px" />
<ToolExamples
title={title}
exampleCards={exampleCards}
getGroups={getGroups}
formRef={formRef}
setInput={setInput}
/>
</Box>
);
}

View File

@@ -0,0 +1,13 @@
import { defineTool } from '@tools/defineTool';
import { lazy } from 'react';
export const tool = defineTool('json', {
name: 'Prettify JSON',
path: 'prettify',
icon: 'lets-icons:json-light',
description:
"Just load your JSON in the input field and it will automatically get prettified. In the tool options, you can choose whether to use spaces or tabs for indentation and if you're using spaces, you can specify the number of spaces to add per indentation level.",
shortDescription: 'Quickly beautify a JSON data structure.',
keywords: ['prettify'],
component: lazy(() => import('./index'))
});

View File

@@ -0,0 +1,16 @@
export const beautifyJson = (
text: string,
indentationType: 'tab' | 'space',
spacesCount: number
) => {
let parsedJson;
try {
parsedJson = JSON.parse(text);
} catch (e) {
throw new Error('Invalid JSON string');
}
const indent = indentationType === 'tab' ? '\t' : spacesCount;
return JSON.stringify(parsedJson, null, indent);
};

View File

@@ -0,0 +1,66 @@
import { describe, expect, it } from 'vitest';
import { duplicateList } from './service';
describe('duplicateList function', () => {
it('should duplicate elements correctly with symbol split', () => {
const input = 'Hello World';
const result = duplicateList('symbol', ' ', ' ', input, true, false, 2);
expect(result).toBe('Hello World Hello World');
});
it('should duplicate elements correctly with regex split', () => {
const input = 'Hello||World';
const result = duplicateList('regex', '\\|\\|', ' ', input, true, false, 2);
expect(result).toBe('Hello World Hello World');
});
it('should handle fractional duplication', () => {
const input = 'Hello World';
const result = duplicateList('symbol', ' ', ' ', input, true, false, 1.5);
expect(result).toBe('Hello World Hello');
});
it('should handle reverse option correctly', () => {
const input = 'Hello World';
const result = duplicateList('symbol', ' ', ' ', input, true, true, 2);
expect(result).toBe('Hello World World Hello');
});
it('should handle concatenate option correctly', () => {
const input = 'Hello World';
const result = duplicateList('symbol', ' ', ' ', input, false, false, 2);
expect(result).toBe('Hello Hello World World');
});
it('should handle interweaving option correctly', () => {
const input = 'Hello World';
const result = duplicateList('symbol', ' ', ' ', input, false, false, 2);
expect(result).toBe('Hello Hello World World');
});
it('should throw an error for negative copies', () => {
expect(() =>
duplicateList('symbol', ' ', ' ', 'Hello World', true, false, -1)
).toThrow('Number of copies cannot be negative');
});
it('should handle interweaving option correctly 2', () => {
const input = "je m'appelle king";
const result = duplicateList('symbol', ' ', ', ', input, false, true, 2.1);
expect(result).toBe("je, king, m'appelle, m'appelle, king, je");
});
it('should handle interweaving option correctly 3', () => {
const input = "je m'appelle king";
const result = duplicateList('symbol', ' ', ', ', input, false, true, 1);
expect(result).toBe("je, m'appelle, king");
});
it('should handle interweaving option correctly 3', () => {
const input = "je m'appelle king";
const result = duplicateList('symbol', ' ', ', ', input, true, true, 2.7);
expect(result).toBe(
"je, m'appelle, king, king, m'appelle, je, king, m'appelle"
);
});
});

View File

@@ -8,4 +8,4 @@ const validationSchema = Yup.object({
});
export default function Duplicate() {
return <Box>Lorem ipsum</Box>;
}
}

View File

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

View File

@@ -0,0 +1,81 @@
export type SplitOperatorType = 'symbol' | 'regex';
function interweave(array1: string[], array2: string[]) {
const result: string[] = [];
const maxLength = Math.max(array1.length, array2.length);
for (let i = 0; i < maxLength; i++) {
if (i < array1.length) result.push(array1[i]);
if (i < array2.length) result.push(array2[i]);
}
return result;
}
function duplicate(
input: string[],
concatenate: boolean,
reverse: boolean,
copy?: number
) {
if (copy) {
if (copy > 0) {
let result: string[] = [];
let toAdd: string[] = [];
let WholePart: string[] = [];
let fractionalPart: string[] = [];
const whole = Math.floor(copy);
const fractional = copy - whole;
if (!reverse) {
WholePart = concatenate
? Array(whole).fill(input).flat()
: Array(whole - 1)
.fill(input)
.flat();
fractionalPart = input.slice(0, Math.floor(input.length * fractional));
toAdd = WholePart.concat(fractionalPart);
result = concatenate
? WholePart.concat(fractionalPart)
: interweave(input, toAdd);
} else {
WholePart = Array(whole - 1)
.fill(input)
.flat()
.reverse();
fractionalPart = input
.slice()
.reverse()
.slice(0, Math.floor(input.length * fractional));
toAdd = WholePart.concat(fractionalPart);
result = concatenate ? input.concat(toAdd) : interweave(input, toAdd);
}
return result;
}
throw new Error('Number of copies cannot be negative');
}
throw new Error('Number of copies must be a valid number');
}
export function duplicateList(
splitOperatorType: SplitOperatorType,
splitSeparator: string,
joinSeparator: string,
input: string,
concatenate: boolean,
reverse: boolean,
copy?: number
): string {
let array: string[];
let result: string[];
switch (splitOperatorType) {
case 'symbol':
array = input.split(splitSeparator);
break;
case 'regex':
array = input
.split(new RegExp(splitSeparator))
.filter((item) => item !== '');
break;
}
result = duplicate(array, concatenate, reverse, copy);
return result.join(joinSeparator);
}

View File

@@ -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', () => {

View File

@@ -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 (
<Box>
@@ -165,7 +161,6 @@ export default function FindMostPopular() {
]}
initialValues={initialValues}
input={input}
validationSchema={validationSchema}
/>
</Box>
);

View File

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

View File

@@ -1,4 +1,4 @@
import { expect, describe, it } from 'vitest';
import { describe, expect } from 'vitest';
import { findUniqueCompute } from './service';

View File

@@ -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 (
<Box>
@@ -156,7 +152,6 @@ export default function FindUnique() {
]}
initialValues={initialValues}
input={input}
validationSchema={validationSchema}
/>
</Box>
);

View File

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

View File

@@ -1,4 +1,4 @@
import { expect, describe, it } from 'vitest';
import { describe, expect, it } from 'vitest';
import { groupList, SplitOperatorType } from './service';

View File

@@ -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 (
<Box>
@@ -181,7 +177,6 @@ export default function FindUnique() {
]}
initialValues={initialValues}
input={input}
validationSchema={validationSchema}
/>
</Box>
);

View File

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

View File

@@ -17,8 +17,9 @@ export const listTools = [
listFindUnique,
listFindMostPopular,
listGroup,
listWrap,
// listWrap,
listRotate,
listShuffle,
listTruncate
listShuffle
// listTruncate,
// listDuplicate
];

View File

@@ -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<InitialValuesType>[] = [
{
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<string>('');
const [result, setResult] = useState<string>('');
const getGroups: GetGroupsType<InitialValuesType> = ({
values,
updateField
}) => [
{
title: 'Splitter Mode',
component: (
<Box>
{splitOperators.map(({ title, description, type }) => (
<SimpleRadio
key={type}
onClick={() => updateField('splitOperatorType', type)}
title={title}
description={description}
checked={values.splitOperatorType === type}
/>
))}
</Box>
)
},
{
title: 'Item Separator',
component: (
<Box>
<TextFieldWithDesc
description={'Set a delimiting symbol or regular expression.'}
value={values.splitSeparator}
onOwnChange={(val) => updateField('splitSeparator', val)}
/>
</Box>
)
},
{
title: 'Output List Options',
component: (
<Box>
<TextFieldWithDesc
description={'Output list item separator.'}
value={values.joinSeparator}
onOwnChange={(val) => updateField('joinSeparator', val)}
/>
</Box>
)
}
];
const compute = (optionsValues: typeof initialValues, input: any) => {
const { splitOperatorType, splitSeparator, joinSeparator } = optionsValues;
setResult(
reverseList(splitOperatorType, splitSeparator, joinSeparator, input)
);
};
return (
<ToolContent
title={title}
initialValues={initialValues}
getGroups={getGroups}
compute={compute}
input={input}
setInput={setInput}
inputComponent={
<ToolTextInput title={'Input list'} value={input} onChange={setInput} />
}
resultComponent={
<ToolTextResult title={'Reversed list'} value={result} />
}
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}
/>
);
}

View File

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

View File

@@ -1,4 +1,4 @@
import { expect, describe, it } from 'vitest';
import { describe, expect } from 'vitest';
import { reverseList } from './service';
describe('reverseList Function', () => {

View File

@@ -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<string>('');
const [result, setResult] = useState<string>('');
const compute = (optionsValues: typeof initialValues, input: any) => {
const { splitOperatorType, splitSeparator, joinSeparator, right, step } =
optionsValues;
setResult(
rotateList(
splitOperatorType,
input,
splitSeparator,
joinSeparator,
right,
step
)
);
};
return (
<Box>
<ToolInputAndResult
input={
<ToolTextInput
title={'Input list'}
value={input}
onChange={setInput}
/>
}
result={<ToolTextResult title={'Rotated list'} value={result} />}
/>
<ToolOptions
compute={compute}
getGroups={({ values, updateField }) => [
{
title: 'Item split mode',
component: (
<Box>
{splitOperators.map(({ title, description, type }) => (
<SimpleRadio
key={type}
onClick={() => updateField('splitOperatorType', type)}
title={title}
description={description}
checked={values.splitOperatorType === type}
/>
))}
<TextFieldWithDesc
description={'Set a delimiting symbol or regular expression.'}
value={values.splitSeparator}
onOwnChange={(val) => updateField('splitSeparator', val)}
/>
</Box>
)
},
{
title: 'Rotation Direction and Count',
component: (
<Box>
{rotationDirections.map(({ title, description, value }) => (
<SimpleRadio
key={`${value}`}
onClick={() => updateField('right', value)}
title={title}
description={description}
checked={values.right === value}
/>
))}
<TextFieldWithDesc
description={'Number of items to rotate'}
value={values.step}
onOwnChange={(val) =>
updateField('step', formatNumber(val, 1))
}
/>
</Box>
)
},
{
title: 'Rotated List Joining Symbol',
component: (
<Box>
<TextFieldWithDesc
value={values.joinSeparator}
onOwnChange={(value) => updateField('joinSeparator', value)}
description={
'Enter the character that goes between items in the rotated list.'
}
/>
</Box>
)
}
]}
initialValues={initialValues}
input={input}
/>
</Box>
);
}

View File

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

View File

@@ -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', () => {

View File

@@ -1,4 +1,3 @@
import { isNumber } from 'utils/string';
export type SplitOperatorType = 'symbol' | 'regex';
function rotateArray(array: string[], step: number, right: boolean): string[] {

View File

@@ -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<string>('');
const [result, setResult] = useState<string>('');
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 (
<Box>
@@ -55,13 +60,13 @@ export default function Reverse() {
onChange={setInput}
/>
}
result={<ToolTextResult title={'Reversed list'} value={result} />}
result={<ToolTextResult title={'Shuffled list'} value={result} />}
/>
<ToolOptions
compute={compute}
getGroups={({ values, updateField }) => [
{
title: 'Splitter Mode',
title: 'Input list separator',
component: (
<Box>
{splitOperators.map(({ title, description, type }) => (
@@ -73,13 +78,6 @@ export default function Reverse() {
checked={values.splitOperatorType === type}
/>
))}
</Box>
)
},
{
title: 'Item Separator',
component: (
<Box>
<TextFieldWithDesc
description={'Set a delimiting symbol or regular expression.'}
value={values.splitSeparator}
@@ -89,13 +87,25 @@ export default function Reverse() {
)
},
{
title: 'Output List Options',
title: 'Shuffled List Length',
component: (
<Box>
<TextFieldWithDesc
description={'Output this many random items'}
value={values.length}
onOwnChange={(val) => updateField('length', val)}
/>
</Box>
)
},
{
title: 'Shuffled List Separator',
component: (
<Box>
<TextFieldWithDesc
description={'Output list item separator.'}
value={values.joinSeparator}
onOwnChange={(val) => updateField('joinSeparator', val)}
onOwnChange={(value) => updateField('joinSeparator', value)}
description={'Use this separator in the randomized list.'}
/>
</Box>
)
@@ -103,7 +113,6 @@ export default function Reverse() {
]}
initialValues={initialValues}
input={input}
validationSchema={validationSchema}
/>
</Box>
);

View File

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

View File

@@ -1,4 +1,4 @@
import { expect, describe, it } from 'vitest';
import { describe, expect, it } from 'vitest';
import { shuffleList, SplitOperatorType } from './service';
describe('shuffle function', () => {

View File

@@ -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 (
<Box>
@@ -163,7 +159,6 @@ export default function SplitText() {
]}
initialValues={initialValues}
input={input}
validationSchema={validationSchema}
/>
</Box>
);

View File

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

View File

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

Some files were not shown because too many files have changed in this diff Show More