ui-update 2

This commit is contained in:
sahil-tgs 2024-08-31 04:26:49 +05:30
parent bae02688d8
commit 739e6e1345
5 changed files with 758 additions and 649 deletions

View File

@ -2,38 +2,45 @@ import React, { useState, useContext, useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import { useNavigate } from 'react-router-dom';
import {
Button,
Grid,
LinearProgress,
Typography,
Alert,
LinearProgress,
Select,
MenuItem,
Box,
TextField,
SelectChangeEvent,
useTheme,
Tooltip,
type SelectChangeEvent,
useMediaQuery,
styled,
} from '@mui/material';
import { Bolt, Add, DeleteSweep, Logout, Download } from '@mui/icons-material';
import { Bolt, Add, DeleteSweep, Logout, Download, FileCopy } from '@mui/icons-material';
import RobotAvatar from '../../components/RobotAvatar';
import TokenInput from './TokenInput';
import { type Slot, type Robot } from '../../models';
import { AppContext, type UseAppStoreType } from '../../contexts/AppContext';
import { AppContext, UseAppStoreType } from '../../contexts/AppContext';
import { genBase62Token } from '../../utils';
import { LoadingButton } from '@mui/lab';
import { GarageContext, type UseGarageStoreType } from '../../contexts/GarageContext';
import { FederationContext, type UseFederationStoreType } from '../../contexts/FederationContext';
import { GarageContext, UseGarageStoreType } from '../../contexts/GarageContext';
import { FederationContext, UseFederationStoreType } from '../../contexts/FederationContext';
const BUTTON_COLORS = {
primary: '#2196f3',
secondary: '#9c27b0',
text: '#ffffff',
hoverPrimary: '#4dabf5',
hoverSecondary: '#af52bf',
activePrimary: '#1976d2',
activeSecondary: '#7b1fa2',
deleteHover: '#ff6666',
};
const COLORS = {
shadow: '#000000',
};
interface RobotProfileProps {
robot: Robot;
setRobot: (state: Robot) => void;
setView: (state: 'welcome' | 'onboarding' | 'recovery' | 'profile') => void;
getGenerateRobot: (token: string, slot?: number) => void;
inputToken: string;
getGenerateRobot: (token: string) => void;
setInputToken: (token: string) => void;
logoutRobot: () => void;
setInputToken: (state: string) => void;
width: number;
baseUrl: string;
setView: (view: string) => void;
}
const RobotProfile = ({
@ -42,7 +49,6 @@ const RobotProfile = ({
setInputToken,
logoutRobot,
setView,
width,
}: RobotProfileProps): JSX.Element => {
const { windowSize } = useContext<UseAppStoreType>(AppContext);
const { garage, robotUpdatedAt, orderUpdatedAt } = useContext<UseGarageStoreType>(GarageContext);
@ -51,6 +57,7 @@ const RobotProfile = ({
const { t } = useTranslation();
const theme = useTheme();
const navigate = useNavigate();
const isMobile = useMediaQuery(theme.breakpoints.down('sm'));
const [loading, setLoading] = useState<boolean>(true);
@ -82,278 +89,309 @@ const RobotProfile = ({
).length;
return (
<Grid container direction='column' alignItems='center' spacing={1} padding={1} paddingTop={2}>
<Grid
item
container
direction='column'
alignItems='center'
spacing={1}
sx={{ width: '100%' }}
>
<Grid item sx={{ height: '2.3em', position: 'relative' }}>
{slot?.nickname ? (
<Typography align='center' component='h5' variant='h5'>
<div
style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
flexWrap: 'wrap',
}}
>
{width < 19 ? null : (
<Bolt
sx={{
color: '#fcba03',
height: '1.5em',
width: '1.5em',
}}
/>
)}
<b>{slot?.nickname}</b>
{width < 19 ? null : (
<Bolt
sx={{
color: '#fcba03',
height: '1.5em',
width: '1.5em',
}}
/>
)}
</div>
</Typography>
) : (
<>
<b>{t('Building your robot!')}</b>
<LinearProgress />
</>
)}
</Grid>
<Grid item sx={{ width: `13.5em` }}>
<RobotAvatar
hashId={slot?.hashId}
smooth={true}
style={{ maxWidth: '12.5em', maxHeight: '12.5em' }}
placeholderType='generating'
imageStyle={{
transform: '',
border: '2px solid #555',
filter: 'drop-shadow(1px 1px 1px #000000)',
height: `12.4em`,
width: `12.4em`,
<ProfileContainer $isMobile={isMobile}>
<InfoSection colors={COLORS} $isMobile={isMobile}>
<NicknameTypography variant={isMobile ? "h6" : "h5"} align="center" $isMobile={isMobile}>
<BoltIcon $isMobile={isMobile} />
{slot?.nickname}
<BoltIcon $isMobile={isMobile} />
</NicknameTypography>
<StyledRobotAvatar
hashId={slot?.hashId}
smooth={true}
placeholderType='generating'
style={{ width: isMobile ? '80px' : '120px', height: isMobile ? '80px' : '120px' }}
/>
<StatusTypography variant={isMobile ? "body2" : "body1"} align="center" $isMobile={isMobile}>
{loadingCoordinators > 0 && !robot?.activeOrderId ? t('Looking for orders!') : t('Ready to Trade')}
</StatusTypography>
{loadingCoordinators > 0 && !robot?.activeOrderId && <StyledLinearProgress $isMobile={isMobile} />}
<TokenBox $isMobile={isMobile}>
<CustomIconButton onClick={() => {
logoutRobot();
setView('welcome');
}}>
<StyledLogoutIcon $isMobile={isMobile} />
</CustomIconButton>
<StyledTextField
fullWidth
value={inputToken}
variant="standard"
$isMobile={isMobile}
InputProps={{
readOnly: true,
disableUnderline: true,
endAdornment: (
<CustomIconButton onClick={() => navigator.clipboard.writeText(inputToken)}>
<StyledFileCopyIcon $isMobile={isMobile} />
</CustomIconButton>
),
}}
tooltip={t('This is your trading avatar')}
tooltipPosition='top'
/>
{robot?.found && Boolean(slot?.lastShortAlias) ? (
<Typography align='center' variant='h6'>
{t('Welcome back!')}
</Typography>
</TokenBox>
</InfoSection>
<RightSection $isMobile={isMobile}>
<TitleSection>
<TitleTypography variant={isMobile ? "subtitle1" : "h6"} align="center">
{t('Robot Garage')}
</TitleTypography>
</TitleSection>
<StyledSelect
value={loading ? 'loading' : garage.currentSlot}
onChange={handleChangeSlot}
$isMobile={isMobile}
>
{loading ? (
<MenuItem key={'loading'} value={'loading'}>
<Typography variant={isMobile ? "body2" : "body1"}>{t('Building...')}</Typography>
</MenuItem>
) : (
<></>
Object.values(garage.slots).map((slot: Slot, index: number) => (
<StyledMenuItem key={index} value={slot.token} $isMobile={isMobile}>
<MenuItemContent>
<StyledMenuItemAvatar
hashId={slot?.hashId}
smooth={true}
$isMobile={isMobile}
placeholderType='loading'
small={true}
/>
<Typography variant={isMobile ? "body2" : "body1"}>{slot?.nickname}</Typography>
</MenuItemContent>
</StyledMenuItem>
))
)}
</Grid>
{loadingCoordinators > 0 && !robot?.activeOrderId ? (
<Grid>
<b>{t('Looking for orders!')}</b>
<LinearProgress />
</Grid>
) : null}
{Boolean(robot?.activeOrderId) && Boolean(slot?.hashId) ? (
<Grid item>
<Button
onClick={() => {
setCurrentOrderId({ id: robot?.activeOrderId, shortAlias: slot?.activeShortAlias });
navigate(
`/order/${String(slot?.activeShortAlias)}/${String(robot?.activeOrderId)}`,
);
}}
>
{t('Active order #{{orderID}}', { orderID: robot?.activeOrderId })}
</Button>
</Grid>
) : null}
{Boolean(robot?.lastOrderId) && Boolean(slot?.hashId) ? (
<Grid item container direction='column' alignItems='center'>
<Grid item>
<Button
onClick={() => {
setCurrentOrderId({ id: robot?.lastOrderId, shortAlias: slot?.activeShortAlias });
navigate(`/order/${String(slot?.lastShortAlias)}/${String(robot?.lastOrderId)}`);
}}
>
{t('Last order #{{orderID}}', { orderID: robot?.lastOrderId })}
</Button>
</Grid>
<Grid item>
<Alert severity='warning'>
<Grid container direction='column' alignItems='center'>
<Grid item>
{t(
'Reusing trading identity degrades your privacy against other users, coordinators and observers.',
)}
</Grid>
<Grid item sx={{ position: 'relative', right: '1em' }}>
<Button color='success' size='small' onClick={handleAddRobot}>
<Add />
{t('Add a new Robot')}
</Button>
</Grid>
</Grid>
</Alert>
</Grid>
</Grid>
) : null}
{!robot?.activeOrderId &&
slot?.hashId &&
!robot?.lastOrderId &&
loadingCoordinators === 0 ? (
<Grid item>{t('No existing orders found')}</Grid>
) : null}
<Grid
item
container
direction='row'
justifyContent='stretch'
alignItems='stretch'
sx={{ width: '100%' }}
>
<Grid
item
xs={2}
sx={{ display: 'flex', justifyContent: 'stretch', alignItems: 'stretch' }}
</StyledSelect>
<ButtonContainer>
<StyledButton
$buttonColor={BUTTON_COLORS.primary}
$hoverColor={BUTTON_COLORS.hoverPrimary}
$textColor={BUTTON_COLORS.text}
$isMobile={isMobile}
onClick={handleAddRobot}
>
<Tooltip enterTouchDelay={0} enterDelay={300} enterNextDelay={1000} title={t('Logout')}>
<Button
sx={{ minWidth: '2em', width: '100%' }}
color='primary'
variant='outlined'
onClick={() => {
logoutRobot();
setView('welcome');
}}
>
<Logout />
</Button>
</Tooltip>
</Grid>
<Grid item xs={10}>
<TokenInput
inputToken={inputToken}
editable={false}
label={t('Store your token safely')}
setInputToken={setInputToken}
onPressEnter={() => null}
/>
</Grid>
</Grid>
</Grid>
<Grid item sx={{ width: '100%' }}>
<Box
sx={{
backgroundColor: 'background.paper',
border: '1px solid',
borderRadius: '4px',
borderColor: theme.palette.mode === 'dark' ? '#434343' : '#c4c4c4',
}}
>
<Grid container direction='column' alignItems='center' spacing={2} padding={2}>
<Grid item sx={{ width: '100%' }}>
<Typography variant='caption'>{t('Robot Garage')}</Typography>
<Select
fullWidth
required={true}
inputProps={{
style: { textAlign: 'center' },
}}
value={loading ? 'loading' : garage.currentSlot}
onChange={handleChangeSlot}
>
{loading ? (
<MenuItem key={'loading'} value={'loading'}>
<Typography>{t('Building...')}</Typography>
</MenuItem>
) : (
Object.values(garage.slots).map((slot: Slot, index: number) => {
return (
<MenuItem key={index} value={slot.token}>
<Grid
container
direction='row'
justifyContent='flex-start'
alignItems='center'
style={{ height: '2.8em' }}
spacing={1}
>
<Grid item>
<RobotAvatar
hashId={slot?.hashId}
smooth={true}
style={{ width: '2.6em', height: '2.6em' }}
placeholderType='loading'
small={true}
/>
</Grid>
<Grid item>
<Typography variant={windowSize.width < 26 ? 'caption' : undefined}>
{slot?.nickname}
</Typography>
</Grid>
</Grid>
</MenuItem>
);
})
)}
</Select>
</Grid>
<Grid item container direction='row' alignItems='center' justifyContent='space-evenly'>
<Grid item>
<LoadingButton loading={loading} color='primary' onClick={handleAddRobot}>
<Add /> <div style={{ width: '0.5em' }} />
{t('Add Robot')}
</LoadingButton>
</Grid>
{window.NativeRobosats === undefined ? (
<Grid item>
<Button
color='primary'
onClick={() => {
garage.download();
}}
>
<Download />
</Button>
</Grid>
) : null}
<Grid item>
<Button
color='primary'
onClick={() => {
garage.delete();
logoutRobot();
setView('welcome');
}}
>
<DeleteSweep /> <div style={{ width: '0.5em' }} />
{t('Delete Garage')}
</Button>
</Grid>
</Grid>
</Grid>
</Box>
</Grid>
</Grid>
<StyledAddIcon $isMobile={isMobile} /> {t('ADD ROBOT')}
</StyledButton>
{window.NativeRobosats === undefined && (
<StyledButton
$buttonColor={BUTTON_COLORS.secondary}
$hoverColor={BUTTON_COLORS.hoverSecondary}
$textColor={BUTTON_COLORS.text}
$isMobile={isMobile}
onClick={() => garage.download()}
>
<StyledDownloadIcon $isMobile={isMobile} /> {t('DOWNLOAD')}
</StyledButton>
)}
<StyledButton
$buttonColor="transparent"
$hoverColor={BUTTON_COLORS.deleteHover}
$textColor="red"
$isMobile={isMobile}
onClick={() => {
garage.delete();
logoutRobot();
setView('welcome');
}}
>
<StyledDeleteSweepIcon $isMobile={isMobile} /> {t('DELETE GARAGE')}
</StyledButton>
</ButtonContainer>
</RightSection>
</ProfileContainer>
);
};
export default RobotProfile;
// Styled components
const ProfileContainer = styled(Box)<{ $isMobile: boolean }>(({ theme, $isMobile }) => ({
width: '100%',
maxWidth: $isMobile ? '100%' : 1000,
margin: '0 auto',
display: 'flex',
flexDirection: $isMobile ? 'column' : 'row',
border: $isMobile ? '1px solid #000' : '2px solid #000',
borderRadius: '8px',
overflow: 'hidden',
boxShadow: $isMobile ? '4px 4px 0px #000000' : '8px 8px 0px #000000',
}));
const InfoSection = styled(Box)<{ colors: typeof COLORS; $isMobile: boolean }>(({ theme, colors, $isMobile }) => ({
flexGrow: 1,
flexBasis: $isMobile ? 'auto' : 0,
backgroundColor: theme.palette.background.paper,
display: 'flex',
flexDirection: 'column',
justifyContent: 'center',
alignItems: 'center',
padding: $isMobile ? theme.spacing(2) : theme.spacing(4),
borderBottom: $isMobile ? '1px solid #000' : 'none',
borderRight: $isMobile ? 'none' : '2px solid #000',
}));
const NicknameTypography = styled(Typography)<{ $isMobile: boolean }>(({ $isMobile }) => ({
display: 'flex',
alignItems: 'center',
marginBottom: $isMobile ? '8px' : '16px',
}));
const BoltIcon = styled(Bolt)<{ $isMobile: boolean }>(({ $isMobile }) => ({
color: '#fcba03',
height: $isMobile ? '0.8em' : '1em',
width: $isMobile ? '0.8em' : '1em',
}));
const StyledRobotAvatar = styled(RobotAvatar)({
'& img': {
border: '2px solid #555',
borderRadius: '50%',
},
});
const StatusTypography = styled(Typography)<{ $isMobile: boolean }>(({ $isMobile }) => ({
marginBottom: $isMobile ? '8px' : '16px',
}));
const StyledLinearProgress = styled(LinearProgress)<{ $isMobile: boolean }>(({ $isMobile }) => ({
width: '100%',
marginBottom: $isMobile ? '8px' : '16px',
}));
const TokenBox = styled(Box)<{ $isMobile: boolean }>(({ $isMobile }) => ({
display: 'flex',
alignItems: 'center',
width: '100%',
border: $isMobile ? '1px solid #000' : '2px solid #000',
borderRadius: '4px',
}));
const StyledTextField = styled(TextField)<{ $isMobile: boolean }>(({ $isMobile }) => ({
'& .MuiInputBase-root': {
height: $isMobile ? '36px' : '48px',
padding: $isMobile ? '2px 4px' : '4px 8px',
fontSize: $isMobile ? '0.8rem' : '1rem',
}
}));
const RightSection = styled(Box)<{ $isMobile: boolean }>(({ $isMobile }) => ({
flexGrow: 1,
flexBasis: $isMobile ? 'auto' : 0,
display: 'flex',
flexDirection: 'column',
overflow: 'hidden',
}));
const TitleSection = styled(Box)(({ theme }) => ({
padding: theme.spacing(2),
borderBottom: `2px solid #000`,
}));
const TitleTypography = styled(Typography)({
fontWeight: 'bold',
});
const StyledSelect = styled(Select)<{ $isMobile: boolean }>(({ $isMobile }) => ({
width: '100%',
height: $isMobile ? '50px' : '80px',
borderBottom: $isMobile ? '1px solid #000' : '2px solid #000',
borderRadius: 0,
'& .MuiOutlinedInput-notchedOutline': {
border: 'none',
},
}));
const StyledMenuItem = styled(MenuItem)<{ $isMobile: boolean }>(({ $isMobile }) => ({
height: $isMobile ? '50px' : '80px',
}));
const MenuItemContent = styled(Box)({
display: 'flex',
alignItems: 'center',
});
const StyledMenuItemAvatar = styled(RobotAvatar)<{ $isMobile: boolean }>(({ $isMobile }) => ({
width: $isMobile ? '24px' : '30px',
height: $isMobile ? '24px' : '30px',
marginRight: '8px',
}));
const ButtonContainer = styled(Box)({
display: 'flex',
flexDirection: 'column',
width: '100%',
});
const StyledButton = styled('button')<{
$buttonColor: string;
$hoverColor: string;
$textColor: string;
$isMobile: boolean;
}>(({ theme, $buttonColor, $hoverColor, $textColor, $isMobile }) => ({
justifyContent: 'center',
alignItems: 'center',
textAlign: 'center',
padding: theme.spacing(2),
border: 'none',
borderRadius: 0,
backgroundColor: $buttonColor,
color: $textColor,
cursor: 'pointer',
display: 'flex',
transition: 'background-color 0.3s ease, color 0.3s ease',
width: '100%',
height: $isMobile ? '40px' : '60px',
borderBottom: $isMobile ? '1px solid #000' : '2px solid #000',
'&:hover': {
backgroundColor: $hoverColor,
color: $buttonColor === 'transparent' ? '#fff' : $textColor,
},
'&:active': {
backgroundColor: $buttonColor === BUTTON_COLORS.primary ? BUTTON_COLORS.activePrimary : BUTTON_COLORS.activeSecondary,
},
'&:focus': {
outline: 'none',
},
}));
const CustomIconButton = styled('button')({
background: 'transparent',
border: 'none',
color: '#1976d2',
padding: '4px',
cursor: 'pointer',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
transition: 'color 0.3s ease',
'&:hover': {
color: '#0d47a1',
},
'&:active': {
color: '#002171',
},
});
const StyledLogoutIcon = styled(Logout)<{ $isMobile: boolean }>(({ $isMobile }) => ({
fontSize: $isMobile ? '1rem' : '1.5rem',
}));
const StyledFileCopyIcon = styled(FileCopy)<{ $isMobile: boolean }>(({ $isMobile }) => ({
fontSize: $isMobile ? '1rem' : '1.5rem',
}));
const StyledAddIcon = styled(Add)<{ $isMobile: boolean }>(({ $isMobile }) => ({
marginRight: '8px',
fontSize: $isMobile ? '1rem' : '1.5rem',
}));
const StyledDownloadIcon = styled(Download)<{ $isMobile: boolean }>(({ $isMobile }) => ({
marginRight: '8px',
fontSize: $isMobile ? '1rem' : '1.5rem',
}));
const StyledDeleteSweepIcon = styled(DeleteSweep)<{ $isMobile: boolean }>(({ $isMobile }) => ({
marginRight: '8px',
fontSize: $isMobile ? '1rem' : '1.5rem',
}));
export default RobotProfile;

View File

@ -1,7 +1,6 @@
import React, { useContext, useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import {
Paper,
Grid,
CircularProgress,
Box,
@ -129,42 +128,40 @@ const RobotPage = (): JSX.Element => {
} else {
return (
<StyledMainBox>
<StyledPaper>
{view === 'welcome' && (
<Welcome setView={setView} getGenerateRobot={getGenerateRobot} width={1200} />
)}
{view === 'welcome' && (
<Welcome setView={setView} getGenerateRobot={getGenerateRobot} width={1200} />
)}
{view === 'onboarding' && (
<Onboarding
setView={setView}
badToken={badToken}
inputToken={inputToken}
setInputToken={setInputToken}
getGenerateRobot={getGenerateRobot}
/>
)}
{view === 'onboarding' && (
<Onboarding
setView={setView}
badToken={badToken}
inputToken={inputToken}
setInputToken={setInputToken}
getGenerateRobot={getGenerateRobot}
/>
)}
{view === 'profile' && (
<RobotProfile
setView={setView}
logoutRobot={logoutRobot}
width={1200}
inputToken={inputToken}
setInputToken={setInputToken}
getGenerateRobot={getGenerateRobot}
/>
)}
{view === 'profile' && (
<RobotProfile
setView={setView}
logoutRobot={logoutRobot}
width={1200}
inputToken={inputToken}
setInputToken={setInputToken}
getGenerateRobot={getGenerateRobot}
/>
)}
{view === 'recovery' && (
<Recovery
setView={setView}
badToken={badToken}
inputToken={inputToken}
setInputToken={setInputToken}
getRecoverRobot={getGenerateRobot}
/>
)}
</StyledPaper>
{view === 'recovery' && (
<Recovery
setView={setView}
badToken={badToken}
inputToken={inputToken}
setInputToken={setInputToken}
getRecoverRobot={getGenerateRobot}
/>
)}
</StyledMainBox>
);
}
@ -174,11 +171,13 @@ const RobotPage = (): JSX.Element => {
const StyledConnectingBox = styled(Box)({
width: '100vw',
height: 'auto',
backgroundColor: 'transparent',
});
const StyledTorIconBox = styled(Box)({
position: 'fixed',
top: '4.6em',
backgroundColor: 'transparent',
});
const StyledMainBox = styled(Box)({
@ -188,18 +187,9 @@ const StyledMainBox = styled(Box)({
justifyContent: 'center',
alignItems: 'center',
padding: '2em',
});
const StyledPaper = styled(Paper)(({ theme }) => ({
width: '80vw',
maxWidth: '1200px',
maxHeight: '85vh',
overflow: 'auto',
overflowX: 'clip',
backgroundColor: 'transparent',
border: 'none',
boxShadow: 'none',
padding: '1em',
}));
boxShadow: 'none',
});
export default RobotPage;

View File

@ -1,19 +1,20 @@
import React, { useContext, useState } from 'react';
import { Button, Grid, List, ListItem, Paper, TextField, Typography } from '@mui/material';
import { Button, Grid, Paper, TextField, Typography, Box } from '@mui/material';
import SettingsForm from '../../components/SettingsForm';
import { AppContext, type UseAppStoreType } from '../../contexts/AppContext';
import FederationTable from '../../components/FederationTable';
import { t } from 'i18next';
import { FederationContext, type UseFederationStoreType } from '../../contexts/FederationContext';
import { styled } from '@mui/system';
const SettingsPage = (): JSX.Element => {
const { windowSize, navbarHeight } = useContext<UseAppStoreType>(AppContext);
const { federation, addNewCoordinator } = useContext<UseFederationStoreType>(FederationContext);
const maxHeight = (windowSize.height * 0.65)
const maxHeight = (windowSize.height * 0.65);
const [newAlias, setNewAlias] = useState<string>('');
const [newUrl, setNewUrl] = useState<string>('');
const [error, setError] = useState<string>();
// Regular expression to match a valid .onion URL
const onionUrlPattern = /^((http|https):\/\/)?[a-zA-Z2-7]{16,56}\.onion$/;
const addCoordinator: () => void = () => {
@ -35,31 +36,22 @@ const SettingsPage = (): JSX.Element => {
};
return (
<Paper
elevation={12}
sx={{
padding: '0.6em',
width: '20.5em',
maxHeight: `${maxHeight}em`,
overflow: 'auto',
overflowX: 'clip',
}}
>
<SettingsContainer elevation={12}>
<Grid container>
<Grid item>
<LeftGrid item xs={12} md={6}>
<SettingsForm />
</Grid>
<Grid item>
<FederationTable maxHeight={18} />
</Grid>
<Grid item>
<Typography align='center' component='h2' variant='subtitle2' color='secondary'>
{error}
</Typography>
</Grid>
<List>
<ListItem>
<TextField
</LeftGrid>
<RightGrid item xs={12} md={6}>
<FederationTableWrapper>
<FederationTable maxHeight={18} />
</FederationTableWrapper>
{error && (
<ErrorTypography align='center' component='h2' variant='subtitle2' color='secondary'>
{error}
</ErrorTypography>
)}
<InputContainer>
<StyledTextField
id='outlined-basic'
label={t('Alias')}
variant='outlined'
@ -69,7 +61,7 @@ const SettingsPage = (): JSX.Element => {
setNewAlias(e.target.value);
}}
/>
<TextField
<StyledTextField
id='outlined-basic'
label={t('URL')}
variant='outlined'
@ -79,8 +71,7 @@ const SettingsPage = (): JSX.Element => {
setNewUrl(e.target.value);
}}
/>
<Button
sx={{ maxHeight: 38 }}
<StyledButton
disabled={false}
onClick={addCoordinator}
variant='contained'
@ -89,12 +80,83 @@ const SettingsPage = (): JSX.Element => {
type='submit'
>
{t('Add')}
</Button>
</ListItem>
</List>
</StyledButton>
</InputContainer>
</RightGrid>
</Grid>
</Paper>
</SettingsContainer>
);
};
export default SettingsPage;
// Styled Components
const SettingsContainer = styled(Paper)(({ theme }) => ({
display: 'flex',
width: '80vw',
height: '70vh',
margin: '0 auto',
border: '2px solid #000',
borderRadius: '8px',
overflow: 'hidden',
boxShadow: '8px 8px 0px #000',
[theme.breakpoints.down('md')]: {
flexDirection: 'column',
width: '90vw',
height: 'fit-content',
marginTop: '30rem',
},
}));
const LeftGrid = styled(Grid)(({ theme }) => ({
padding: '2rem',
borderRight: '2px solid #000',
[theme.breakpoints.down('md')]: {
padding: '1rem',
borderRight: 'none',
},
}));
const RightGrid = styled(Grid)(({ theme }) => ({
padding: '2rem',
display: 'flex',
flexDirection: 'column',
[theme.breakpoints.down('md')]: {
padding: '1rem',
},
}));
const FederationTableWrapper = styled(Box)({
flexGrow: 1,
'& > *': {
width: '100% !important',
},
});
const ErrorTypography = styled(Typography)({
// You can add specific styles for the error message here if needed
});
const InputContainer = styled(Box)(({ theme }) => ({
display: 'flex',
flexDirection: 'row',
gap: theme.spacing(1),
marginTop: theme.spacing(2),
[theme.breakpoints.down('sm')]: {
flexDirection: 'column',
},
}));
const StyledTextField = styled(TextField)(({ theme }) => ({
flexGrow: 1,
[theme.breakpoints.down('sm')]: {
marginBottom: theme.spacing(2),
},
}));
const StyledButton = styled(Button)(({ theme }) => ({
maxHeight: 40,
[theme.breakpoints.down('sm')]: {
width: '100%',
},
}));
export default SettingsPage;

View File

@ -1,78 +1,38 @@
import React, { useCallback, useEffect, useState, useContext, useMemo } from 'react';
import React, { useCallback, useContext, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { Box, useTheme, Checkbox, CircularProgress, Typography, Grid } from '@mui/material';
import { Box, Checkbox, CircularProgress, Grid, Typography } from '@mui/material';
import { DataGrid, type GridColDef, type GridValidRowModel } from '@mui/x-data-grid';
import { type Coordinator } from '../../models';
import RobotAvatar from '../RobotAvatar';
import { Link, LinkOff } from '@mui/icons-material';
import { AppContext, type UseAppStoreType } from '../../contexts/AppContext';
import { type UseFederationStoreType, FederationContext } from '../../contexts/FederationContext';
import headerStyleFix from '../DataGrid/HeaderFix';
import { styled } from '@mui/system';
interface FederationTableProps {
maxWidth?: number;
maxHeight?: number;
fillContainer?: boolean;
}
const FederationTable = ({
maxWidth = 90,
maxHeight = 50,
fillContainer = false,
}: FederationTableProps): JSX.Element => {
const FederationTable = ({ maxWidth = 90, fillContainer = false }: FederationTableProps): JSX.Element => {
const { t } = useTranslation();
const { federation, sortedCoordinators, coordinatorUpdatedAt, federationUpdatedAt } =
useContext<UseFederationStoreType>(FederationContext);
const { federation, sortedCoordinators, coordinatorUpdatedAt } = useContext<UseFederationStoreType>(FederationContext);
const { setOpen, settings } = useContext<UseAppStoreType>(AppContext);
const theme = useTheme();
const [pageSize, setPageSize] = useState<number>(0);
// all sizes in 'em'
const fontSize = theme.typography.fontSize;
const verticalHeightFrame = 3.3;
const verticalHeightRow = 3.27;
const defaultPageSize = Math.max(
Math.floor((maxHeight - verticalHeightFrame) / verticalHeightRow),
1,
);
const height = defaultPageSize * verticalHeightRow + verticalHeightFrame;
const [useDefaultPageSize, setUseDefaultPageSize] = useState(true);
useEffect(() => {
if (useDefaultPageSize) {
setPageSize(defaultPageSize);
}
}, [coordinatorUpdatedAt, federationUpdatedAt]);
const localeText = {
MuiTablePagination: { labelRowsPerPage: t('Coordinators per page:') },
noResultsOverlayLabel: t('No coordinators found.'),
};
const onClickCoordinator = function (shortAlias: string): void {
setOpen((open) => {
return { ...open, coordinator: shortAlias };
});
};
const aliasObj = useCallback((width: number) => {
return {
field: 'longAlias',
headerName: t('Coordinator'),
width: width * fontSize,
width: width,
renderCell: (params: any) => {
const coordinator = federation.coordinators[params.row.shortAlias];
return (
<Grid
<CoordinatorGrid
container
direction='row'
sx={{ cursor: 'pointer', position: 'relative', left: '-0.3em', width: '50em' }}
wrap='nowrap'
onClick={() => {
onClickCoordinator(params.row.shortAlias);
}}
alignItems='center'
direction="row"
wrap="nowrap"
onClick={() => onClickCoordinator(params.row.shortAlias)}
alignItems="center"
spacing={1}
>
<Grid item>
@ -87,31 +47,29 @@ const FederationTable = ({
<Grid item>
<Typography>{params.row.longAlias}</Typography>
</Grid>
</Grid>
</CoordinatorGrid>
);
},
};
}, []);
}, [federation.coordinators]);
const enabledObj = useCallback(
(width: number) => {
return {
field: 'enabled',
headerName: t('Enabled'),
width: width * fontSize,
width: width,
renderCell: (params: any) => {
return (
<Checkbox
checked={params.row.enabled}
onClick={() => {
onEnableChange(params.row.shortAlias);
}}
onClick={() => onEnableChange(params.row.shortAlias)}
/>
);
},
};
},
[coordinatorUpdatedAt],
[coordinatorUpdatedAt]
);
const upObj = useCallback(
@ -119,52 +77,41 @@ const FederationTable = ({
return {
field: 'up',
headerName: t('Up'),
width: width * fontSize,
width: width,
renderCell: (params: any) => {
return (
<div
style={{ cursor: 'pointer' }}
onClick={() => {
onClickCoordinator(params.row.shortAlias);
}}
>
{Boolean(params.row.loadingInfo) && Boolean(params.row.enabled) ? (
<CircularProgress thickness={0.35 * fontSize} size={1.5 * fontSize} />
<UpStatusContainer onClick={() => onClickCoordinator(params.row.shortAlias)}>
{params.row.loadingInfo && params.row.enabled ? (
<CircularProgress thickness={2} size={24} />
) : params.row.info !== undefined ? (
<Link color='success' />
<Link color="success" />
) : (
<LinkOff color='error' />
<LinkOff color="error" />
)}
</div>
</UpStatusContainer>
);
},
};
},
[coordinatorUpdatedAt],
[coordinatorUpdatedAt]
);
const columnSpecs = {
alias: {
priority: 2,
order: 1,
normal: {
width: 12.1,
width: 200,
object: aliasObj,
},
},
up: {
priority: 3,
order: 2,
normal: {
width: 3.5,
width: 70,
object: upObj,
},
},
enabled: {
priority: 1,
order: 3,
normal: {
width: 5,
width: 90,
object: enabledObj,
},
},
@ -174,29 +121,17 @@ const FederationTable = ({
columns: Array<GridColDef<GridValidRowModel>>;
width: number;
} {
const useSmall = maxWidth < 30;
const selectedColumns: object[] = [];
let width: number = 0;
for (const value of Object.values(columnSpecs)) {
const colWidth = Number(
useSmall && Boolean(value.small) ? value.small.width : value.normal.width,
);
const colObject = useSmall && Boolean(value.small) ? value.small.object : value.normal.object;
const colWidth = value.normal.width;
const colObject = value.normal.object;
if (width + colWidth < maxWidth || selectedColumns.length < 2) {
width = width + colWidth;
selectedColumns.push([colObject(colWidth, false), value.order]);
} else {
selectedColumns.push([colObject(colWidth, true), value.order]);
}
width += colWidth;
selectedColumns.push([colObject(colWidth), value.order]);
}
// sort columns by column.order value
selectedColumns.sort(function (first, second) {
return first[1] - second[1];
});
const columns: Array<GridColDef<GridValidRowModel>> = selectedColumns.map(function (item) {
return item[0];
});
@ -204,7 +139,7 @@ const FederationTable = ({
return { columns, width: width * 0.9 };
};
const { columns, width } = filteredColumns();
const { columns } = filteredColumns();
const onEnableChange = function (shortAlias: string): void {
if (federation.getCoordinator(shortAlias).enabled === true) {
@ -217,38 +152,65 @@ const FederationTable = ({
const reorderedCoordinators = useMemo(() => {
return sortedCoordinators.reduce((coordinators, key) => {
coordinators[key] = federation.coordinators[key];
return coordinators;
}, {});
}, [settings.network, federationUpdatedAt]);
}, [settings.network, coordinatorUpdatedAt]);
const onClickCoordinator = (shortAlias: string): void => {
setOpen((open) => {
return { ...open, coordinator: shortAlias };
});
};
return (
<Box
sx={
fillContainer
? { width: '100%', height: '100%' }
: { width: `${width}em`, height: `${height}em`, overflow: 'auto' }
}
>
<DataGrid
sx={headerStyleFix}
localeText={localeText}
rowHeight={3.714 * theme.typography.fontSize}
headerHeight={3.25 * theme.typography.fontSize}
<TableContainer fillContainer={fillContainer} maxWidth={maxWidth}>
<StyledDataGrid
rows={Object.values(reorderedCoordinators)}
getRowId={(params: Coordinator) => params.shortAlias}
columns={columns}
checkboxSelection={false}
pageSize={pageSize}
rowsPerPageOptions={width < 22 ? [] : [0, pageSize, defaultPageSize * 2, 50, 100]}
onPageSizeChange={(newPageSize) => {
setPageSize(newPageSize);
setUseDefaultPageSize(false);
}}
hideFooter={true}
autoHeight
hideFooter
/>
</Box>
</TableContainer>
);
};
export default FederationTable;
// Styled Components
const TableContainer = styled(Box, {
shouldForwardProp: (prop) => prop !== 'fillContainer' && prop !== 'maxWidth',
})<{ fillContainer: boolean; maxWidth: number }>(({ theme, fillContainer, maxWidth }) => ({
width: fillContainer ? '100%' : `${maxWidth}em`,
height: 'fit-content',
border: '2px solid black',
overflow: 'hidden',
borderRadius: '8px',
boxShadow: 'none',
}));
const StyledDataGrid = styled(DataGrid)(({ theme }) => ({
'& .MuiDataGrid-root': {
fontSize: { xs: '0.8rem', sm: '0.9rem', md: '1rem' },
},
'& .MuiDataGrid-cell': {
padding: { xs: '0.5rem', sm: '1rem' },
},
'& .MuiDataGrid-columnHeaders': {
backgroundColor: theme.palette.background.default,
borderBottom: '2px solid black',
fontSize: { xs: '0.8rem', sm: '0.9rem', md: '1rem' },
},
}));
const CoordinatorGrid = styled(Grid)({
cursor: 'pointer',
position: 'relative',
left: '-0.3em',
width: '50em',
});
const UpStatusContainer = styled('div')({
cursor: 'pointer',
});
export default FederationTable;

View File

@ -1,36 +1,36 @@
import React, { useContext } from 'react';
import { useTranslation } from 'react-i18next';
import { useTheme } from '@mui/material/styles';
import { type UseAppStoreType, AppContext } from '../../contexts/AppContext';
import {
Grid,
Paper,
Switch,
useTheme,
FormControlLabel,
List,
ListItem,
ListItemIcon,
Slider,
Typography,
ToggleButtonGroup,
ToggleButton,
Box,
Slider,
} from '@mui/material';
import SelectLanguage from './SelectLanguage';
import {
Translate,
Palette,
LightMode,
DarkMode,
SettingsOverscan,
Link,
AccountBalance,
AttachMoney,
QrCode,
DarkMode,
} from '@mui/icons-material';
import { systemClient } from '../../services/System';
import { TorIcon } from '../Icons';
import SwapCalls from '@mui/icons-material/SwapCalls';
import { apiClient } from '../../services/api';
import { styled } from '@mui/system';
interface SettingsFormProps {
dense?: boolean;
@ -38,8 +38,9 @@ interface SettingsFormProps {
const SettingsForm = ({ dense = false }: SettingsFormProps): JSX.Element => {
const { fav, setFav, settings, setSettings } = useContext<UseAppStoreType>(AppContext);
const theme = useTheme();
const { t } = useTranslation();
const theme = useTheme();
const fontSizes = [
{ label: 'XS', value: { basic: 12, pro: 10 } },
{ label: 'S', value: { basic: 13, pro: 11 } },
@ -48,115 +49,74 @@ const SettingsForm = ({ dense = false }: SettingsFormProps): JSX.Element => {
{ label: 'XL', value: { basic: 16, pro: 14 } },
];
const handleToggleChange = (e, newValue) => {
if (newValue !== null) {
setFav({ ...fav, mode: newValue, currency: newValue === 'fiat' ? 0 : 1000 });
}
};
const handleNetworkChange = (e, newValue) => {
if (newValue !== null) {
setSettings({ ...settings, network: newValue });
systemClient.setItem('settings_network', newValue);
}
};
return (
<Grid container spacing={1}>
<Grid item>
<Grid container spacing={2}>
<Grid item xs={12}>
<List dense={dense}>
<ListItem>
<ListItemIcon>
<Translate />
</ListItemIcon>
<SelectLanguage
{/* Language Settings */}
<StyledListItem>
<SettingHeader>
<ListItemIcon>
<Translate />
</ListItemIcon>
<Typography variant="subtitle1">
{t('Language Settings')}
</Typography>
</SettingHeader>
<StyledSelectLanguage
language={settings.language}
setLanguage={(language) => {
setSettings({ ...settings, language });
systemClient.setItem('settings_language', language);
}}
/>
</ListItem>
</StyledListItem>
<ListItem>
<ListItemIcon>
<Palette />
</ListItemIcon>
<FormControlLabel
labelPlacement='end'
label={settings.mode === 'dark' ? t('Dark') : t('Light')}
control={
<Switch
checked={settings.mode === 'dark'}
checkedIcon={
<Paper
elevation={3}
sx={{
width: '1.2em',
height: '1.2em',
borderRadius: '0.4em',
backgroundColor: 'white',
position: 'relative',
top: `${7 - 0.5 * theme.typography.fontSize}px`,
}}
>
<DarkMode sx={{ width: '0.8em', height: '0.8em', color: '#666' }} />
</Paper>
}
icon={
<Paper
elevation={3}
sx={{
width: '1.2em',
height: '1.2em',
borderRadius: '0.4em',
backgroundColor: 'white',
padding: '0.07em',
position: 'relative',
top: `${7 - 0.5 * theme.typography.fontSize}px`,
}}
>
<LightMode sx={{ width: '0.67em', height: '0.67em', color: '#666' }} />
</Paper>
}
onChange={(e) => {
const mode = e.target.checked ? 'dark' : 'light';
setSettings({ ...settings, mode });
systemClient.setItem('settings_mode', mode);
}}
/>
}
/>
{settings.mode === 'dark' ? (
<>
<ListItemIcon>
<QrCode />
</ListItemIcon>
<FormControlLabel
sx={{ position: 'relative', right: '1.5em', width: '3em' }}
labelPlacement='end'
label={settings.lightQRs ? t('Light') : t('Dark')}
{/* Appearance Settings */}
<StyledListItem>
<SettingHeader>
<ListItemIcon>
<Palette />
</ListItemIcon>
<Typography variant="subtitle1">
{t('Appearance Settings')}
</Typography>
</SettingHeader>
<AppearanceSettingsBox>
<FormControlLabel
labelPlacement="end"
label={t('Dark Mode')}
control={
<Switch
checked={settings.mode === 'dark'}
onChange={(e) => {
const mode = e.target.checked ? 'dark' : 'light';
setSettings({ ...settings, mode });
systemClient.setItem('settings_mode', mode);
}}
/>
}
/>
{settings.mode === 'dark' && (
<QRCodeSwitch
labelPlacement="end"
label={t('QR Code Color')}
control={
<Switch
checked={!settings.lightQRs}
checkedIcon={
<Paper
elevation={3}
sx={{
width: '1.2em',
height: '1.2em',
borderRadius: '0.4em',
backgroundColor: 'white',
position: 'relative',
top: `${7 - 0.5 * theme.typography.fontSize}px`,
}}
>
<DarkMode sx={{ width: '0.8em', height: '0.8em', color: '#666' }} />
</Paper>
}
icon={
<Paper
elevation={3}
sx={{
width: '1.2em',
height: '1.2em',
borderRadius: '0.4em',
backgroundColor: 'white',
padding: '0.07em',
position: 'relative',
top: `${7 - 0.5 * theme.typography.fontSize}px`,
}}
>
<LightMode sx={{ width: '0.67em', height: '0.67em', color: '#666' }} />
</Paper>
}
onChange={(e) => {
const lightQRs = !e.target.checked;
setSettings({ ...settings, lightQRs });
@ -165,17 +125,21 @@ const SettingsForm = ({ dense = false }: SettingsFormProps): JSX.Element => {
/>
}
/>
</>
) : (
<></>
)}
</ListItem>
)}
</AppearanceSettingsBox>
</StyledListItem>
<ListItem>
<ListItemIcon>
<SettingsOverscan />
</ListItemIcon>
<Slider
{/* Font Size Settings */}
<StyledListItem>
<SettingHeader>
<ListItemIcon>
<SettingsOverscan />
</ListItemIcon>
<Typography variant="subtitle1">
{t('Font Size')}
</Typography>
</SettingHeader>
<StyledSlider
value={settings.fontSize}
min={settings.frontend === 'basic' ? 12 : 10}
max={settings.frontend === 'basic' ? 16 : 14}
@ -185,80 +149,98 @@ const SettingsForm = ({ dense = false }: SettingsFormProps): JSX.Element => {
setSettings({ ...settings, fontSize });
systemClient.setItem(`settings_fontsize_${settings.frontend}`, fontSize.toString());
}}
valueLabelDisplay='off'
valueLabelDisplay="off"
track={false}
marks={fontSizes.map(({ label, value }) => ({
label: <Typography variant='caption'>{t(label)}</Typography>,
label: <Typography variant="caption">{t(label)}</Typography>,
value: settings.frontend === 'basic' ? value.basic : value.pro,
}))}
track={false}
/>
</ListItem>
</StyledListItem>
<ListItem>
<ListItemIcon>
<AccountBalance />
</ListItemIcon>
<ToggleButtonGroup
{/* Currency Settings */}
<StyledListItem>
<SettingHeader>
<ListItemIcon>
<AccountBalance />
</ListItemIcon>
<Typography variant="subtitle1">
{t('Currency Settings')}
</Typography>
</SettingHeader>
<StyledToggleButtonGroup
exclusive={true}
value={fav.mode}
onChange={(e, mode) => {
setFav({ ...fav, mode, currency: mode === 'fiat' ? 0 : 1000 });
}}
onChange={handleToggleChange}
fullWidth
>
<ToggleButton value='fiat' color='primary'>
<StyledToggleButton value="fiat">
<AttachMoney />
{t('Fiat')}
</ToggleButton>
<ToggleButton value='swap' color='secondary'>
</StyledToggleButton>
<StyledToggleButton value="swap">
<SwapCalls />
{t('Swaps')}
</ToggleButton>
</ToggleButtonGroup>
</ListItem>
</StyledToggleButton>
</StyledToggleButtonGroup>
</StyledListItem>
<ListItem>
<ListItemIcon>
<Link />
</ListItemIcon>
<ToggleButtonGroup
{/* Network Settings */}
<StyledListItem>
<SettingHeader>
<ListItemIcon>
<Link />
</ListItemIcon>
<Typography variant="subtitle1">
{t('Network Settings')}
</Typography>
</SettingHeader>
<StyledToggleButtonGroup
exclusive={true}
value={settings.network}
onChange={(e, network) => {
setSettings({ ...settings, network });
systemClient.setItem('settings_network', network);
}}
onChange={handleNetworkChange}
fullWidth
>
<ToggleButton value='mainnet' color='primary'>
<StyledToggleButton value="mainnet">
{t('Mainnet')}
</ToggleButton>
<ToggleButton value='testnet' color='secondary'>
</StyledToggleButton>
<StyledToggleButton value="testnet">
{t('Testnet')}
</ToggleButton>
</ToggleButtonGroup>
</ListItem>
</StyledToggleButton>
</StyledToggleButtonGroup>
</StyledListItem>
{/* Proxy Settings */}
{window.NativeRobosats !== undefined && (
<ListItem>
<ListItemIcon>
<TorIcon />
</ListItemIcon>
<ToggleButtonGroup
<StyledListItem>
<SettingHeader>
<ListItemIcon>
<TorIcon />
</ListItemIcon>
<Typography variant="subtitle1">
{t('Proxy Settings')}
</Typography>
</SettingHeader>
<StyledToggleButtonGroup
exclusive={true}
value={settings.useProxy}
onChange={(_e, useProxy) => {
setSettings({ ...settings, useProxy });
systemClient.setItem('settings_use_proxy', String(useProxy));
apiClient.useProxy = useProxy;
if (useProxy !== null) {
setSettings({ ...settings, useProxy });
systemClient.setItem('settings_use_proxy', String(useProxy));
apiClient.useProxy = useProxy;
}
}}
fullWidth
>
<ToggleButton value={true} color='primary'>
{t('Build-in')}
</ToggleButton>
<ToggleButton value={false} color='secondary'>
<StyledToggleButton value={true}>
{t('Built-in')}
</StyledToggleButton>
<StyledToggleButton value={false}>
{t('Disabled')}
</ToggleButton>
</ToggleButtonGroup>
</ListItem>
</StyledToggleButton>
</StyledToggleButtonGroup>
</StyledListItem>
)}
</List>
</Grid>
@ -266,4 +248,79 @@ const SettingsForm = ({ dense = false }: SettingsFormProps): JSX.Element => {
);
};
export default SettingsForm;
// Styled Components
const StyledListItem = styled(ListItem)({
display: 'block',
});
const SettingHeader = styled(Box)({
display: 'flex',
alignItems: 'center',
marginBottom: '0.5em',
});
const StyledSelectLanguage = styled(SelectLanguage)(({ theme }) => ({
'& .MuiOutlinedInput-root': {
borderRadius: '8px',
border: '2px solid black',
boxShadow: '4px 4px 0px rgba(0, 0, 0, 1)',
width: '100%',
},
}));
const AppearanceSettingsBox = styled(Box)(({ theme }) => ({
display: 'flex',
alignItems: 'center',
flexDirection: 'column',
gap: theme.spacing(2),
[theme.breakpoints.up('sm')]: {
flexDirection: 'row',
gap: theme.spacing(1),
},
}));
const QRCodeSwitch = styled(FormControlLabel)(({ theme }) => ({
marginLeft: 0,
[theme.breakpoints.up('sm')]: {
marginLeft: theme.spacing(2),
},
}));
const StyledSlider = styled(Slider)(({ theme }) => ({
'& .MuiSlider-thumb': {
borderRadius: '8px',
border: '2px solid black',
},
'& .MuiSlider-track': {
borderRadius: '8px',
border: '2px solid black',
},
'& .MuiSlider-rail': {
borderRadius: '8px',
border: '2px solid black',
},
}));
const StyledToggleButtonGroup = styled(ToggleButtonGroup)({
width: '100%',
});
const StyledToggleButton = styled(ToggleButton)(({ theme }) => ({
borderRadius: '8px',
border: '2px solid black',
boxShadow: 'none',
fontWeight: 'bold',
width: '100%',
'&.Mui-selected': {
backgroundColor: theme.palette.primary.main,
color: theme.palette.primary.contrastText,
'&:hover': {
backgroundColor: theme.palette.primary.main,
},
},
'&:hover': {
backgroundColor: 'initial',
},
}));
export default SettingsForm;