mirror of
https://github.com/iib0011/omni-tools.git
synced 2025-09-19 05:59:34 +02:00
Merge remote-tracking branch 'origin/main' into string-join
# Conflicts: # src/pages/string/join/index.tsx
This commit is contained in:
10
src/components/InputHeader.tsx
Normal file
10
src/components/InputHeader.tsx
Normal 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>
|
||||
);
|
||||
}
|
@@ -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>
|
||||
);
|
||||
|
@@ -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}
|
||||
|
21
src/components/ToolInputAndResult.tsx
Normal file
21
src/components/ToolInputAndResult.tsx
Normal 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>
|
||||
);
|
||||
}
|
24
src/components/input/InputFooter.tsx
Normal file
24
src/components/input/InputFooter.tsx
Normal 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>
|
||||
);
|
||||
}
|
115
src/components/input/ToolFileInput.tsx
Normal file
115
src/components/input/ToolFileInput.tsx
Normal 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>
|
||||
);
|
||||
}
|
@@ -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="*"
|
||||
|
54
src/components/options/ColorSelector.tsx
Normal file
54
src/components/options/ColorSelector.tsx
Normal 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;
|
@@ -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}
|
||||
|
46
src/components/options/SimpleRadio.tsx
Normal file
46
src/components/options/SimpleRadio.tsx
Normal 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>
|
||||
);
|
||||
}
|
27
src/components/options/ToolOptionGroups.tsx
Normal file
27
src/components/options/ToolOptionGroups.tsx
Normal 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>
|
||||
);
|
||||
}
|
34
src/components/result/ResultFooter.tsx
Normal file
34
src/components/result/ResultFooter.tsx
Normal 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>
|
||||
);
|
||||
}
|
99
src/components/result/ToolFileResult.tsx
Normal file
99
src/components/result/ToolFileResult.tsx
Normal 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>
|
||||
);
|
||||
}
|
@@ -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>
|
||||
);
|
||||
}
|
||||
|
Reference in New Issue
Block a user