Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .eslintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -103,4 +103,4 @@
}
]
}
}
}
99 changes: 99 additions & 0 deletions __TESTS__/sorting.spec.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
/* eslint-disable import/no-extraneous-dependencies */
import { fireEvent, render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
/* eslint-enable import/no-extraneous-dependencies */
import React from 'react';

import Sorting from '../pages/sorting';

describe('Sorting', () => {
it('should sort', () => {
render(<Sorting />);

const CommaInputs = [
'c,a,b',
'c,a,b',
'cc,a,bbb',
'cc,a,bbb',
'c,a,b',
];
const otherSeparatorsInputs = [
'c a b',
'c.a.b',
'c;a;b',
'c\na\nb',
];
const CommaOutputs = [
'a,b,c',
'c,b,a',
'a,cc,bbb',
'bbb,cc,a',
'b,a,c',
];
const otherSeparatorOutputs = [
'a b c',
'a.b.c',
'a;b;c',
'a\nb\nc',
];

const SortStyleList = [
'ascending',
'descending',
'ascending2',
'descending2',
'reverse',
];

const SortButton = screen.getByRole('button', {
name: 'Sort',
}) as HTMLButtonElement;

CommaInputs.forEach((input, index) => {
const inputText = screen.getByLabelText(
'Input',
) as HTMLInputElement;
userEvent.type(inputText, input).then(() => {
const sortStyle = screen.getByLabelText(
'Sort Style',
) as HTMLInputElement;
fireEvent.change(sortStyle, {
target: { value: SortStyleList[index] },
});
userEvent.click(SortButton).then(() => {
const outputText = screen.getByLabelText('Output');
expect(outputText.innerText).toBe(CommaOutputs[index]);
});
});
});

const SeparatorsList = ['space', 'dot', 'semicolon', 'lineBreak'];
otherSeparatorsInputs.forEach((input, index) => {
const inputText = screen.getByLabelText(
'Input',
) as HTMLInputElement;
userEvent.type(inputText, input).then(() => {
const separator = screen.getByLabelText(
'Separator',
) as HTMLInputElement;
fireEvent.change(separator, {
target: { value: SeparatorsList[index] },
});

const sortStyle = screen.getByLabelText(
'Sort Style',
) as HTMLInputElement;
fireEvent.change(sortStyle, {
target: { value: SortStyleList[index] },
});

userEvent.click(SortButton).then(() => {
const outputText = screen.getByLabelText('Output');
expect(outputText.innerText).toBe(
otherSeparatorOutputs[index],
);
});
});
});
});
});
6 changes: 6 additions & 0 deletions data/nav.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import Fingerprint from '@mui/icons-material/Fingerprint';
import HomeIcon from '@mui/icons-material/Home';
import LinkIcon from '@mui/icons-material/Link';
import Looks6 from '@mui/icons-material/Looks6';
import SortByAlphaIcon from '@mui/icons-material/SortByAlpha';

// Icons Index: https://mui.com/material-ui/material-icons/

Expand Down Expand Up @@ -46,6 +47,11 @@ const navItems = [
href: '/url-encode',
Icon: LinkIcon,
},
{
title: 'Sorting',
href: '/sorting',
Icon: SortByAlphaIcon,
},
{
title: 'HTML',
href: '/html',
Expand Down
251 changes: 251 additions & 0 deletions pages/sorting.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,251 @@
import ClearIcon from '@mui/icons-material/Clear';
import ContentCopyIcon from '@mui/icons-material/ContentCopy';
import ContentPasteGoIcon from '@mui/icons-material/ContentPasteGo';
import { Typography } from '@mui/material';
import Box from '@mui/material/Box';
import Button from '@mui/material/Button';
import FormControl from '@mui/material/FormControl';
import InputLabel from '@mui/material/InputLabel';
import MenuItem from '@mui/material/MenuItem';
import Select, { SelectChangeEvent } from '@mui/material/Select';
import TextField from '@mui/material/TextField';
import React, { useState } from 'react';

import Heading from '../components/Heading';
import Layout from '../components/Layout';
import Toast, { ToastProps } from '../components/Toast';
import useLocalState from '../hooks/useLocalState';
import useSupportsClipboardRead from '../hooks/useSupportsClipboardRead';

export default function Sorting() {
const supportsClipboardRead = useSupportsClipboardRead();
const [inputText, setInputText] = useLocalState<string>({
key: 'inputTextLocalState',
defaultValue: '',
});

const [sortStyle, setSortStyle] = useLocalState<string>({
key: 'sortStyleLocalState',
defaultValue: 'ascending',
});

const [separator, setSeparator] = useLocalState<string>({
key: 'separatorLocalState',
defaultValue: ',',
});

const [outputText, setOutputText] = useLocalState<string>({
key: 'outputTextLocalState',
defaultValue: '',
});

const [toastOpen, setToastOpen] = useState<boolean>(false);
const [toastMessage, setToastMessage] = useState<string>('');
const [toastSeverity, setToastSeverity] =
useState<ToastProps['severity']>('success');

const handleSortStyleChange = (event: SelectChangeEvent) => {
setSortStyle(event.target.value);
};

const handleSeparatorChange = (event: SelectChangeEvent) => {
setSeparator(event.target.value);
};

const handleClear = () => {
setInputText('');
setOutputText('');
};

const handlePaste = async () => {
const textCopied = await navigator.clipboard.readText();
setInputText(textCopied);
setOutputText(decodeURIComponent(textCopied));
};

const handleSort = () => {
if (inputText === '') {
setToastMessage('Please enter some text to sort.');
setToastSeverity('error');
setToastOpen(true);
return;
}

if (separator === '') {
setToastMessage('Please enter a separator.');
setToastSeverity('error');
setToastOpen(true);
return;
}

let inputTextArray;

if (separator === 'newLine') {
inputTextArray = inputText.split(/\r\n|\n\r|\n|\r/);
} else {
inputTextArray = inputText.split(separator);
}
const outputTextArray =
sortStyle === 'reverse'
? inputTextArray.reverse()
: inputTextArray.sort((a, b) => {
if (sortStyle === 'ascending') {
return a.localeCompare(b);
}
if (sortStyle === 'descending') {
return b.localeCompare(a);
}
if (sortStyle === 'ascending2') {
if (a.length === b.length) {
return a.localeCompare(b);
}
return a.length - b.length;
}
if (sortStyle === 'descending2') {
if (a.length === b.length) {
return b.localeCompare(a);
}
return b.length - a.length;
}
return 0;
});

if (separator === 'newLine')
setOutputText(outputTextArray.join('\n'));
else setOutputText(outputTextArray.join(separator));
};

return (
<Layout>
<Heading>Sorting Tool</Heading>
<Typography paragraph>
Paste or Type an list and select options from the dropdown to
sort
</Typography>

<Box
display='flex'
flexDirection='column'
justifyContent='stretch'
gap={6}
width={1000}
maxWidth='100%'
>
<Box
display='flex'
flexDirection='column'
>
<TextField
label='Input'
value={inputText}
onChange={(event) => setInputText(event.target.value)}
multiline
/>
<Box
display='flex'
flexWrap='wrap'
justifyContent='end'
>
<Button
startIcon={<ClearIcon />}
onClick={handleClear}
>
Clear
</Button>
{supportsClipboardRead && (
<Button
startIcon={<ContentPasteGoIcon />}
onClick={handlePaste}
>
Paste
</Button>
)}

<FormControl sx={{ m: 2, minWidth: 250 }}>
<InputLabel id='sortStyle'>Sort Style</InputLabel>
<Select
value={sortStyle}
label='Sort Style'
onChange={handleSortStyleChange}
>
<MenuItem value='ascending'>
<em>ascending</em>
</MenuItem>
<MenuItem value='descending'>descending</MenuItem>
<MenuItem value='ascending2'>
ascending (length)
</MenuItem>
<MenuItem value='descending2'>
descending (length)
</MenuItem>
<MenuItem value='reverse'>reverse</MenuItem>
</Select>
</FormControl>

<FormControl sx={{ m: 2, minWidth: 250 }}>
<InputLabel id='separator'>Separator</InputLabel>
<Select
value={separator}
label='Separator'
onChange={handleSeparatorChange}
>
<MenuItem value=','>
<em>Comma (,)</em>
</MenuItem>
<MenuItem value={' '}>Space ( )</MenuItem>
<MenuItem value='.'>Full Stop (.)</MenuItem>
<MenuItem value=';'>Semicolon (;)</MenuItem>
<MenuItem value='newLine'>New Line</MenuItem>
</Select>
</FormControl>

<Button onClick={handleSort}>Sort</Button>
</Box>
</Box>

<Box
display='flex'
flexDirection='column'
>
<TextField
label='Output'
value={outputText}
multiline
/>
<Box
display='flex'
flexWrap='wrap'
justifyContent='end'
>
<Button
startIcon={<ContentCopyIcon />}
disabled={!outputText}
onClick={() => {
navigator.clipboard.writeText(outputText || '').then(
() => {
setToastMessage('Copied to clipboard');
setToastSeverity('success');
setToastOpen(true);
},
() => {
setToastMessage('Failed to copy to clipboard');
setToastSeverity('error');
setToastOpen(true);
},
);
}}
>
Copy
</Button>
</Box>
</Box>
</Box>
<Toast
open={toastOpen}
message={toastMessage}
severity={toastSeverity}
onClose={() => setToastOpen(false)}
/>
</Layout>
);
}