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

# Conflicts:
#	src/pages/string/join/index.tsx
This commit is contained in:
Ibrahima G. Coulibaly
2024-06-25 07:55:21 +01:00
46 changed files with 1733 additions and 320 deletions

View File

@@ -0,0 +1,10 @@
import Typography from '@mui/material/Typography';
import React from 'react';
export default function InputHeader({ title }: { title: string }) {
return (
<Typography mb={1} fontSize={30} color={'primary'}>
{title}
</Typography>
);
}

View File

@@ -6,6 +6,7 @@ import Button from '@mui/material/Button';
import IconButton from '@mui/material/IconButton';
import { Link, useNavigate } from 'react-router-dom';
import githubIcon from '@assets/github-mark.png'; // Adjust the path to your GitHub icon
import { Stack } from '@mui/material';
const Navbar: React.FC = () => {
const navigate = useNavigate();
@@ -14,46 +15,46 @@ const Navbar: React.FC = () => {
position="static"
style={{ backgroundColor: 'white', color: 'black' }}
>
<Toolbar>
<Toolbar sx={{ justifyContent: 'space-between', alignItems: 'center' }}>
<Typography
onClick={() => navigate('/')}
fontSize={20}
sx={{ flexGrow: 1, cursor: 'pointer' }}
sx={{ cursor: 'pointer' }}
color={'primary'}
>
OmniTools
</Typography>
<Button color="inherit">
<Link
to="/features"
style={{ textDecoration: 'none', color: 'inherit' }}
<Stack direction={'row'}>
<Button color="inherit">
<Link
to="/features"
style={{ textDecoration: 'none', color: 'inherit' }}
>
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"
>
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>
<img
src={githubIcon}
alt="GitHub"
style={{ height: '24px', marginRight: '8px' }}
/>
<Typography variant="button">Star us</Typography>
</IconButton>
</Stack>
</Toolbar>
</AppBar>
);

View File

@@ -29,7 +29,7 @@ export default function ToolHeader({
description
}: ToolHeaderProps) {
return (
<Stack direction={'row'} alignItems={'center'} spacing={2} mt={4}>
<Stack direction={'row'} alignItems={'center'} spacing={2} my={4}>
<Box>
<Typography mb={2} fontSize={30} color={'primary'}>
{title}

View File

@@ -0,0 +1,21 @@
import React, { ReactNode } from 'react';
import Grid from '@mui/material/Grid';
export default function ToolInputAndResult({
input,
result
}: {
input: ReactNode;
result: ReactNode;
}) {
return (
<Grid container spacing={2}>
<Grid item xs={6}>
{input}
</Grid>
<Grid item xs={6}>
{result}
</Grid>
</Grid>
);
}

View File

@@ -0,0 +1,24 @@
import { Stack } from '@mui/material';
import Button from '@mui/material/Button';
import PublishIcon from '@mui/icons-material/Publish';
import ContentPasteIcon from '@mui/icons-material/ContentPaste';
import React from 'react';
export default function InputFooter({
handleImport,
handleCopy
}: {
handleImport: () => void;
handleCopy: () => void;
}) {
return (
<Stack mt={1} direction={'row'} spacing={2}>
<Button onClick={handleImport} startIcon={<PublishIcon />}>
Import from file
</Button>
<Button onClick={handleCopy} startIcon={<ContentPasteIcon />}>
Copy to clipboard
</Button>
</Stack>
);
}

View File

@@ -0,0 +1,115 @@
import { Box, styled, TextField, useTheme } from '@mui/material';
import Typography from '@mui/material/Typography';
import React, { useContext, useEffect, useRef, useState } from 'react';
import InputHeader from '../InputHeader';
import InputFooter from './InputFooter';
import { CustomSnackBarContext } from '../../contexts/CustomSnackBarContext';
import greyPattern from '@assets/grey-pattern.png';
import { globalInputHeight } from '../../config/uiConfig';
interface ToolFileInputProps {
value: File | null;
onChange: (file: File) => void;
accept: string[];
title?: string;
}
export default function ToolFileInput({
value,
onChange,
accept,
title = 'File'
}: ToolFileInputProps) {
const [preview, setPreview] = useState<string | null>(null);
const theme = useTheme();
const { showSnackBar } = useContext(CustomSnackBarContext);
const fileInputRef = useRef<HTMLInputElement>(null);
const handleCopy = () => {
navigator.clipboard
.writeText(value?.name ?? '')
.then(() => showSnackBar('Text copied', 'success'))
.catch((err) => {
showSnackBar('Failed to copy: ' + err, 'error');
});
};
useEffect(() => {
if (value) {
const objectUrl = URL.createObjectURL(value);
setPreview(objectUrl);
// Clean up memory when the component is unmounted or the file changes
return () => URL.revokeObjectURL(objectUrl);
} else {
setPreview(null);
}
}, [value]);
const handleFileChange = (event: React.ChangeEvent<HTMLInputElement>) => {
const file = event.target.files?.[0];
if (file) onChange(file);
};
const handleImportClick = () => {
fileInputRef.current?.click();
};
return (
<Box>
<InputHeader title={title} />
<Box
sx={{
width: '100%',
height: globalInputHeight,
border: preview ? 0 : 1,
borderRadius: 2,
boxShadow: '5'
}}
>
{preview ? (
<Box
width={'100%'}
height={'100%'}
sx={{
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
backgroundImage: `url(${greyPattern})`
}}
>
<img
src={preview}
alt="Preview"
style={{ maxWidth: '100%', maxHeight: globalInputHeight }}
/>
</Box>
) : (
<Box
onClick={handleImportClick}
sx={{
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
padding: 5,
height: '100%',
cursor: 'pointer'
}}
>
<Typography color={theme.palette.grey['600']}>
Click here to select an image from your device, press Ctrl+V to
use an image from your clipboard, drag and drop a file from
desktop
</Typography>
</Box>
)}
</Box>
<InputFooter handleCopy={handleCopy} handleImport={handleImportClick} />
<input
ref={fileInputRef}
style={{ display: 'none' }}
type="file"
accept={accept.join(',')}
onChange={handleFileChange}
/>
</Box>
);
}

View File

@@ -5,6 +5,8 @@ import PublishIcon from '@mui/icons-material/Publish';
import ContentPasteIcon from '@mui/icons-material/ContentPaste';
import React, { useContext, useRef } from 'react';
import { CustomSnackBarContext } from '../../contexts/CustomSnackBarContext';
import InputHeader from '../InputHeader';
import InputFooter from './InputFooter';
export default function ToolTextInput({
value,
@@ -45,9 +47,7 @@ export default function ToolTextInput({
};
return (
<Box id="tool">
<Typography fontSize={30} color={'primary'}>
{title}
</Typography>
<InputHeader title={title} />
<TextField
value={value}
onChange={(event) => onChange(event.target.value)}
@@ -55,14 +55,7 @@ export default function ToolTextInput({
multiline
rows={10}
/>
<Stack mt={1} direction={'row'} spacing={2}>
<Button onClick={handleImportClick} startIcon={<PublishIcon />}>
Import from file
</Button>
<Button onClick={handleCopy} startIcon={<ContentPasteIcon />}>
Copy to clipboard
</Button>
</Stack>
<InputFooter handleCopy={handleCopy} handleImport={handleImportClick} />
<input
type="file"
accept="*"

View File

@@ -0,0 +1,54 @@
import React, { useState, ChangeEvent, useRef } from 'react';
import { Box, Stack, TextField } from '@mui/material';
import PaletteIcon from '@mui/icons-material/Palette';
import IconButton from '@mui/material/IconButton';
import Typography from '@mui/material/Typography';
import { globalDescriptionFontSize } from '../../config/uiConfig';
interface ColorSelectorProps {
value: string;
onChange: (val: string) => void;
description: string;
}
const ColorSelector: React.FC<ColorSelectorProps> = ({
value = '#ffffff',
onChange,
description
}) => {
const [color, setColor] = useState<string>(value);
const inputRef = useRef<HTMLInputElement>(null);
const handleColorChange = (event: ChangeEvent<HTMLInputElement>) => {
const val = event.target.value;
setColor(val);
onChange(val);
};
return (
<Box mb={1}>
<Stack direction={'row'}>
<TextField
sx={{ backgroundColor: 'white' }}
value={color}
onChange={handleColorChange}
/>
<IconButton onClick={() => inputRef.current?.click()}>
<PaletteIcon />
</IconButton>
<TextField
style={{ display: 'none' }}
inputRef={inputRef}
type="color"
value={color}
onChange={handleColorChange}
/>
</Stack>
<Typography fontSize={globalDescriptionFontSize}>
{description}
</Typography>
</Box>
);
};
export default ColorSelector;

View File

@@ -4,37 +4,38 @@ import { Field } from 'formik';
import Typography from '@mui/material/Typography';
import React from 'react';
import TextFieldWithDesc from './TextFieldWithDesc';
import { globalDescriptionFontSize } from '../../config/uiConfig';
import SimpleRadio from './SimpleRadio';
const RadioWithTextField = <T,>({
fieldName,
type,
radioValue,
title,
onTypeChange,
onRadioChange,
value,
description,
onTextChange
onTextChange,
typeDescription
}: {
fieldName: string;
title: string;
type: T;
onTypeChange: (val: T) => void;
radioValue: T;
onRadioChange: (val: T) => void;
value: string;
description: string;
onTextChange: (value: string) => void;
typeDescription?: string;
}) => {
const onChange = () => onTypeChange(type);
const onChange = () => onRadioChange(radioValue);
return (
<Box>
<Stack
direction={'row'}
sx={{ mt: 2, mb: 1, cursor: 'pointer' }}
onClick={onChange}
alignItems={'center'}
spacing={1}
>
<Field type="radio" name={fieldName} value={type} onChange={onChange} />
<Typography>{title}</Typography>
</Stack>
<SimpleRadio
value={radioValue}
onChange={onChange}
fieldName={fieldName}
title={title}
description={typeDescription}
/>
<TextFieldWithDesc
value={value}
onChange={onTextChange}

View File

@@ -0,0 +1,46 @@
import { Box, Stack } from '@mui/material';
import { Field } from 'formik';
import Typography from '@mui/material/Typography';
import { globalDescriptionFontSize } from '../../config/uiConfig';
import React from 'react';
interface SimpleRadioProps {
onChange: () => void;
fieldName: string;
value: any;
title: string;
description?: string;
}
export default function SimpleRadio({
onChange,
fieldName,
value,
title,
description
}: SimpleRadioProps) {
return (
<Box>
<Stack
direction={'row'}
sx={{ mt: 2, mb: 1, cursor: 'pointer' }}
onClick={onChange}
alignItems={'center'}
spacing={1}
>
<Field
type="radio"
name={fieldName}
value={value}
onChange={onChange}
/>
<Typography>{title}</Typography>
</Stack>
{description && (
<Typography ml={2} fontSize={globalDescriptionFontSize}>
{description}
</Typography>
)}
</Box>
);
}

View File

@@ -0,0 +1,27 @@
import Typography from '@mui/material/Typography';
import React, { ReactNode } from 'react';
import { Box, Stack } from '@mui/material';
interface ToolOptionGroup {
title: string;
component: ReactNode;
}
export default function ToolOptionGroups({
groups
}: {
groups: ToolOptionGroup[];
}) {
return (
<Stack direction={'row'} spacing={2}>
{groups.map((group) => (
<Box key={group.title}>
<Typography mb={1} fontSize={22}>
{group.title}
</Typography>
{group.component}
</Box>
))}
</Stack>
);
}

View File

@@ -0,0 +1,34 @@
import { Stack } from '@mui/material';
import Button from '@mui/material/Button';
import DownloadIcon from '@mui/icons-material/Download';
import ContentPasteIcon from '@mui/icons-material/ContentPaste';
import React from 'react';
export default function ResultFooter({
handleDownload,
handleCopy,
disabled
}: {
handleDownload: () => void;
handleCopy: () => void;
disabled?: boolean;
}) {
return (
<Stack mt={1} direction={'row'} spacing={2}>
<Button
disabled={disabled}
onClick={handleDownload}
startIcon={<DownloadIcon />}
>
Save as
</Button>
<Button
disabled={disabled}
onClick={handleCopy}
startIcon={<ContentPasteIcon />}
>
Copy to clipboard
</Button>
</Stack>
);
}

View File

@@ -0,0 +1,99 @@
import { Box } from '@mui/material';
import React, { useContext } from 'react';
import InputHeader from '../InputHeader';
import greyPattern from '@assets/grey-pattern.png';
import { globalInputHeight } from '../../config/uiConfig';
import ResultFooter from './ResultFooter';
import { CustomSnackBarContext } from '../../contexts/CustomSnackBarContext';
export default function ToolFileResult({
title = 'Result',
value,
extension
}: {
title?: string;
value: File | null;
extension: string;
}) {
const [preview, setPreview] = React.useState<string | null>(null);
const { showSnackBar } = useContext(CustomSnackBarContext);
React.useEffect(() => {
if (value) {
const objectUrl = URL.createObjectURL(value);
setPreview(objectUrl);
return () => URL.revokeObjectURL(objectUrl);
} else {
setPreview(null);
}
}, [value]);
const handleCopy = () => {
if (value) {
const blob = new Blob([value], { type: value.type });
const clipboardItem = new ClipboardItem({ [value.type]: blob });
navigator.clipboard
.write([clipboardItem])
.then(() => showSnackBar('File copied', 'success'))
.catch((err) => {
showSnackBar('Failed to copy: ' + err, 'error');
});
}
};
const handleDownload = () => {
if (value) {
const filename = 'output-omni-tools.' + extension;
const blob = new Blob([value], { type: value.type });
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = filename;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
window.URL.revokeObjectURL(url);
}
};
return (
<Box>
<InputHeader title={title} />
<Box
sx={{
width: '100%',
height: globalInputHeight,
border: preview ? 0 : 1,
borderRadius: 2,
boxShadow: '5'
}}
>
{preview && (
<Box
width={'100%'}
height={'100%'}
sx={{
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
backgroundImage: `url(${greyPattern})`
}}
>
<img
src={preview}
alt="Result"
style={{ maxWidth: '100%', maxHeight: globalInputHeight }}
/>
</Box>
)}
</Box>
<ResultFooter
disabled={!value}
handleCopy={handleCopy}
handleDownload={handleDownload}
/>
</Box>
);
}

View File

@@ -5,6 +5,8 @@ import DownloadIcon from '@mui/icons-material/Download';
import ContentPasteIcon from '@mui/icons-material/ContentPaste';
import React, { useContext } from 'react';
import { CustomSnackBarContext } from '../../contexts/CustomSnackBarContext';
import InputHeader from '../InputHeader';
import ResultFooter from './ResultFooter';
export default function ToolTextResult({
title = 'Result',
@@ -37,18 +39,9 @@ export default function ToolTextResult({
};
return (
<Box>
<Typography fontSize={30} color={'primary'}>
{title}
</Typography>
<InputHeader title={title} />
<TextField value={value} fullWidth multiline rows={10} />
<Stack mt={1} direction={'row'} spacing={2}>
<Button onClick={handleDownload} startIcon={<DownloadIcon />}>
Save as
</Button>
<Button onClick={handleCopy} startIcon={<ContentPasteIcon />}>
Copy to clipboard
</Button>
</Stack>
<ResultFooter handleCopy={handleCopy} handleDownload={handleDownload} />
</Box>
);
}