mirror of
https://github.com/RoboSats/robosats.git
synced 2025-01-19 04:31:35 +00:00
Add MakerForm functional component (#265)
* Create maker form model * Add MakerForm functional component * Add advance options switch * Add range slider * Add timers, bond size, helpers and submit button * Style maker form * Add filter toolbar to maker page * Add new Maker Page * Fix MakerPage * Add match maker * Add order filter util, run lint:fix * Fix book filter by payment method * Add confirmation dialogs * Add Book page as functional component * Load orders and limits on app start (homepage) * Turn BookPage and MakerPage into modules * Add translation keys * Fixes * More fixes
This commit is contained in:
parent
bd073eac08
commit
ddd9f3fc32
21
frontend/package-lock.json
generated
21
frontend/package-lock.json
generated
@ -7052,20 +7052,6 @@
|
||||
"integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/fsevents": {
|
||||
"version": "2.3.2",
|
||||
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
|
||||
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/function-bind": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz",
|
||||
@ -20038,13 +20024,6 @@
|
||||
"integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==",
|
||||
"dev": true
|
||||
},
|
||||
"fsevents": {
|
||||
"version": "2.3.2",
|
||||
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
|
||||
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"function-bind": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz",
|
||||
|
@ -107,24 +107,18 @@ export default class App extends Component {
|
||||
<TorConnection />
|
||||
<IconButton
|
||||
color='inherit'
|
||||
sx={{ position: 'fixed', right: '34px' }}
|
||||
sx={{ position: 'fixed', right: '34px', color: 'text.secondary' }}
|
||||
onClick={() => this.setState({ openLearn: true })}
|
||||
>
|
||||
<SchoolIcon />
|
||||
</IconButton>
|
||||
<IconButton
|
||||
color='inherit'
|
||||
sx={{ position: 'fixed', right: '0px' }}
|
||||
sx={{ position: 'fixed', right: '0px', color: 'text.secondary' }}
|
||||
onClick={() => this.handleThemeChange()}
|
||||
>
|
||||
{this.state.theme.palette.mode === 'dark' ? <LightModeIcon /> : <DarkModeIcon />}
|
||||
</IconButton>
|
||||
<IconButton
|
||||
sx={{ position: 'fixed', right: '34px' }}
|
||||
onClick={() => this.setState({ openLearn: true })}
|
||||
>
|
||||
<SchoolIcon />
|
||||
</IconButton>
|
||||
<UnsafeAlert className='unsafeAlert' />
|
||||
<HomePage {...this.state} />
|
||||
</ThemeProvider>
|
||||
|
@ -1,350 +0,0 @@
|
||||
import React, { Component } from 'react';
|
||||
import { withTranslation } from 'react-i18next';
|
||||
import {
|
||||
Button,
|
||||
ToggleButtonGroup,
|
||||
ToggleButton,
|
||||
Typography,
|
||||
Grid,
|
||||
Select,
|
||||
MenuItem,
|
||||
FormControl,
|
||||
FormHelperText,
|
||||
IconButton,
|
||||
ButtonGroup,
|
||||
} from '@mui/material';
|
||||
import { Link } from 'react-router-dom';
|
||||
import currencyDict from '../../static/assets/currencies.json';
|
||||
import FlagWithProps from './FlagWithProps';
|
||||
import DepthChart from './Charts/DepthChart';
|
||||
import { apiClient } from '../services/api/index';
|
||||
|
||||
// Icons
|
||||
import { BarChart, FormatListBulleted, Refresh } from '@mui/icons-material';
|
||||
import BookTable from './BookTable';
|
||||
|
||||
class BookPage extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
pageSize: 6,
|
||||
view: 'list',
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount = () => {
|
||||
if (this.props.bookOrders.length < 1) {
|
||||
this.getOrderDetails(true, false);
|
||||
} else {
|
||||
this.getOrderDetails(false, true);
|
||||
}
|
||||
};
|
||||
|
||||
getOrderDetails(loading, refreshing) {
|
||||
this.props.setAppState({ bookLoading: loading, bookRefreshing: refreshing });
|
||||
apiClient.get('/api/book/').then((data) =>
|
||||
this.props.setAppState({
|
||||
bookNotFound: data.not_found,
|
||||
bookLoading: false,
|
||||
bookRefreshing: false,
|
||||
bookOrders: data,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
handleRowClick = (e) => {
|
||||
this.props.history.push('/order/' + e);
|
||||
};
|
||||
|
||||
handleCurrencyChange = (e) => {
|
||||
const currency = e.target.value;
|
||||
this.props.setAppState({
|
||||
currency,
|
||||
bookCurrencyCode: this.getCurrencyCode(currency),
|
||||
});
|
||||
};
|
||||
|
||||
getCurrencyCode(val) {
|
||||
const { t } = this.props;
|
||||
if (val) {
|
||||
return val == 0 ? t('ANY_currency') : currencyDict[val.toString()];
|
||||
} else {
|
||||
return t('ANY_currency');
|
||||
}
|
||||
}
|
||||
|
||||
handleTypeChange = (mouseEvent, val) => {
|
||||
this.props.setAppState({ type: val });
|
||||
};
|
||||
|
||||
handleClickView = () => {
|
||||
this.setState({ view: this.state.view == 'depth' ? 'list' : 'depth' });
|
||||
};
|
||||
|
||||
NoOrdersFound = () => {
|
||||
const { t } = this.props;
|
||||
|
||||
return (
|
||||
<Grid item xs={12} align='center'>
|
||||
<Grid item xs={12} align='center'>
|
||||
<Typography component='h5' variant='h5'>
|
||||
{this.props.type == 0
|
||||
? t('No orders found to sell BTC for {{currencyCode}}', {
|
||||
currencyCode: this.props.bookCurrencyCode,
|
||||
})
|
||||
: t('No orders found to buy BTC for {{currencyCode}}', {
|
||||
currencyCode: this.props.bookCurrencyCode,
|
||||
})}
|
||||
</Typography>
|
||||
</Grid>
|
||||
<br />
|
||||
<Grid item>
|
||||
<Button size='large' variant='contained' color='primary' to='/make/' component={Link}>
|
||||
{t('Make Order')}
|
||||
</Button>
|
||||
</Grid>
|
||||
<Typography color='primary' variant='body1'>
|
||||
<b>{t('Be the first one to create an order')}</b>
|
||||
<br />
|
||||
<br />
|
||||
</Typography>
|
||||
</Grid>
|
||||
);
|
||||
};
|
||||
|
||||
mainView = (doubleView, widthEm, heightEm) => {
|
||||
if (this.props.bookNotFound) {
|
||||
return this.NoOrdersFound();
|
||||
}
|
||||
|
||||
if (doubleView) {
|
||||
const width = widthEm * 0.9;
|
||||
const bookTableWidth = 85;
|
||||
const chartWidthEm = width - bookTableWidth;
|
||||
const tableWidthXS = (bookTableWidth / width) * 12;
|
||||
const chartWidthXS = (chartWidthEm / width) * 12;
|
||||
|
||||
return (
|
||||
<Grid
|
||||
container
|
||||
alignItems='center'
|
||||
justifyContent='flex-start'
|
||||
spacing={1}
|
||||
direction='row'
|
||||
style={{ width: `${widthEm}em`, position: 'relative', left: `${widthEm / 140}em` }}
|
||||
>
|
||||
<Grid item xs={tableWidthXS} style={{ width: `${bookTableWidth}em` }}>
|
||||
<BookTable
|
||||
loading={this.props.bookLoading}
|
||||
refreshing={this.props.bookRefreshing}
|
||||
clickRefresh={() => this.getOrderDetails(false, true)}
|
||||
orders={this.props.bookOrders}
|
||||
type={this.props.type}
|
||||
currency={this.props.currency}
|
||||
maxWidth={bookTableWidth} // EM units
|
||||
maxHeight={heightEm * 0.8 - 11} // EM units
|
||||
fullWidth={widthEm} // EM units
|
||||
fullHeight={heightEm} // EM units
|
||||
defaultFullscreen={false}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid
|
||||
item
|
||||
xs={chartWidthXS}
|
||||
style={{ width: `${chartWidthEm}em`, position: 'relative', left: '-10em' }}
|
||||
>
|
||||
<DepthChart
|
||||
bookLoading={this.props.bookLoading}
|
||||
orders={this.props.bookOrders}
|
||||
lastDayPremium={this.props.lastDayPremium}
|
||||
currency={this.props.currency}
|
||||
compact={true}
|
||||
setAppState={this.props.setAppState}
|
||||
limits={this.props.limits}
|
||||
maxWidth={chartWidthEm} // EM units
|
||||
maxHeight={heightEm * 0.8 - 11} // EM units
|
||||
/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
);
|
||||
} else {
|
||||
if (this.state.view === 'depth') {
|
||||
return (
|
||||
<DepthChart
|
||||
bookLoading={this.props.bookLoading}
|
||||
orders={this.props.bookOrders}
|
||||
lastDayPremium={this.props.lastDayPremium}
|
||||
currency={this.props.currency}
|
||||
compact={true}
|
||||
setAppState={this.props.setAppState}
|
||||
limits={this.props.limits}
|
||||
maxWidth={widthEm * 0.8} // EM units
|
||||
maxHeight={heightEm * 0.8 - 11} // EM units
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<BookTable
|
||||
loading={this.props.bookLoading}
|
||||
refreshing={this.props.bookRefreshing}
|
||||
clickRefresh={() => this.getOrderDetails(false, true)}
|
||||
orders={this.props.bookOrders}
|
||||
type={this.props.type}
|
||||
currency={this.props.currency}
|
||||
maxWidth={widthEm * 0.97} // EM units
|
||||
maxHeight={heightEm * 0.8 - 11} // EM units
|
||||
fullWidth={widthEm} // EM units
|
||||
fullHeight={heightEm} // EM units
|
||||
defaultFullscreen={false}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
getTitle = (doubleView) => {
|
||||
const { t } = this.props;
|
||||
|
||||
if (this.state.view == 'list' || doubleView) {
|
||||
if (this.props.type == 0) {
|
||||
return t('You are SELLING BTC for {{currencyCode}}', {
|
||||
currencyCode: this.props.bookCurrencyCode,
|
||||
});
|
||||
} else if (this.props.type == 1) {
|
||||
return t('You are BUYING BTC for {{currencyCode}}', {
|
||||
currencyCode: this.props.bookCurrencyCode,
|
||||
});
|
||||
} else {
|
||||
return t('You are looking at all');
|
||||
}
|
||||
} else if (this.state.view == 'depth') {
|
||||
return t('Depth chart');
|
||||
}
|
||||
};
|
||||
|
||||
mainFilters = () => {
|
||||
const { t } = this.props;
|
||||
return (
|
||||
<>
|
||||
<Grid item xs={6} align='right'>
|
||||
<FormControl align='center'>
|
||||
<FormHelperText align='center' sx={{ textAlign: 'center' }}>
|
||||
{t('I want to')}
|
||||
</FormHelperText>
|
||||
<div style={{ textAlign: 'center' }}>
|
||||
<ToggleButtonGroup
|
||||
sx={{ height: '3.52em' }}
|
||||
size='large'
|
||||
exclusive={true}
|
||||
value={this.props.type}
|
||||
onChange={this.handleTypeChange}
|
||||
>
|
||||
<ToggleButton value={1} color={'primary'}>
|
||||
{t('Buy')}
|
||||
</ToggleButton>
|
||||
<ToggleButton value={0} color={'secondary'}>
|
||||
{t('Sell')}
|
||||
</ToggleButton>
|
||||
</ToggleButtonGroup>
|
||||
</div>
|
||||
</FormControl>
|
||||
</Grid>
|
||||
|
||||
<Grid item xs={6} align='left'>
|
||||
<FormControl align='center'>
|
||||
<FormHelperText
|
||||
align='center'
|
||||
sx={{ textAlign: 'center', position: 'relative', left: '-5px' }}
|
||||
>
|
||||
{this.props.type == 0
|
||||
? t('and receive')
|
||||
: this.props.type == 1
|
||||
? t('and pay with')
|
||||
: t('and use')}
|
||||
</FormHelperText>
|
||||
<Select
|
||||
// autoWidth={true}
|
||||
sx={{ width: 120 }}
|
||||
label={t('Select Payment Currency')}
|
||||
required={true}
|
||||
value={this.props.currency}
|
||||
inputProps={{
|
||||
style: { textAlign: 'center' },
|
||||
}}
|
||||
onChange={this.handleCurrencyChange}
|
||||
>
|
||||
<MenuItem value={0}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', flexWrap: 'wrap' }}>
|
||||
<FlagWithProps code='ANY' />
|
||||
{' ' + t('ANY_currency')}
|
||||
</div>
|
||||
</MenuItem>
|
||||
{Object.entries(currencyDict).map(([key, value]) => (
|
||||
<MenuItem key={key} value={parseInt(key)}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', flexWrap: 'wrap' }}>
|
||||
<FlagWithProps code={value} />
|
||||
{' ' + value}
|
||||
</div>
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
</Grid>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
render() {
|
||||
const { t } = this.props;
|
||||
const widthEm = this.props.windowWidth / this.props.theme.typography.fontSize;
|
||||
const heightEm = this.props.windowHeight / this.props.theme.typography.fontSize;
|
||||
const doubleView = widthEm > 115;
|
||||
|
||||
return (
|
||||
<Grid className='orderBook' container spacing={1} sx={{ minWidth: 400 }}>
|
||||
{this.mainFilters()}
|
||||
<Grid item xs={12} align='center'>
|
||||
<Typography component='h5' variant='h5'>
|
||||
{this.getTitle(doubleView)}
|
||||
</Typography>
|
||||
</Grid>
|
||||
<Grid item xs={12} align='center'>
|
||||
{this.mainView(doubleView, widthEm, heightEm)}
|
||||
</Grid>
|
||||
<Grid item xs={12} align='center'>
|
||||
<ButtonGroup variant='contained' aria-label='outlined primary button group'>
|
||||
{!this.props.bookNotFound ? (
|
||||
<>
|
||||
<Button variant='contained' color='primary' to='/make/' component={Link}>
|
||||
{t('Make Order')}
|
||||
</Button>
|
||||
{doubleView ? null : (
|
||||
<Button
|
||||
color='inherit'
|
||||
style={{ color: '#111111' }}
|
||||
onClick={this.handleClickView}
|
||||
>
|
||||
{this.state.view == 'depth' ? (
|
||||
<>
|
||||
<FormatListBulleted /> {t('List')}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<BarChart /> {t('Chart')}
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
)}
|
||||
</>
|
||||
) : null}
|
||||
<Button color='secondary' variant='contained' to='/' component={Link}>
|
||||
{t('Back')}
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
</Grid>
|
||||
</Grid>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default withTranslation()(BookPage);
|
247
frontend/src/components/BookPage/BookControl.tsx
Normal file
247
frontend/src/components/BookPage/BookControl.tsx
Normal file
@ -0,0 +1,247 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import {
|
||||
Typography,
|
||||
Grid,
|
||||
ToggleButton,
|
||||
ToggleButtonGroup,
|
||||
Select,
|
||||
Divider,
|
||||
MenuItem,
|
||||
Box,
|
||||
} from '@mui/material';
|
||||
import currencyDict from '../../../static/assets/currencies.json';
|
||||
import { useTheme } from '@mui/system';
|
||||
import { AutocompletePayments } from '../MakerPage';
|
||||
import { paymentMethods, swapDestinations } from '../payment-methods/Methods';
|
||||
import FlagWithProps from '../FlagWithProps';
|
||||
import PaymentIcon from '../payment-methods/Icons';
|
||||
|
||||
interface BookControlProps {
|
||||
width: number;
|
||||
type: number;
|
||||
currency: number;
|
||||
paymentMethod: string[];
|
||||
onCurrencyChange: () => void;
|
||||
onTypeChange: () => void;
|
||||
setPaymentMethods: () => void;
|
||||
}
|
||||
|
||||
const BookControl = ({
|
||||
width,
|
||||
type,
|
||||
currency,
|
||||
onCurrencyChange,
|
||||
onTypeChange,
|
||||
paymentMethod,
|
||||
setPaymentMethods,
|
||||
}: BookControlProps): JSX.Element => {
|
||||
const { t } = useTranslation();
|
||||
const theme = useTheme();
|
||||
|
||||
const smallestToolbarWidth = (t('Buy').length + t('Sell').length) * 0.7 + 12;
|
||||
const mediumToolbarWidth = smallestToolbarWidth + 12;
|
||||
const verboseToolbarWidth =
|
||||
mediumToolbarWidth + (t('and use').length + t('pay with').length) * 0.6;
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<Grid
|
||||
container
|
||||
alignItems='flex-start'
|
||||
direction='row'
|
||||
justifyContent='center'
|
||||
spacing={0.5}
|
||||
sx={{ height: '3.4em', padding: '0.2em' }}
|
||||
>
|
||||
{width > verboseToolbarWidth ? (
|
||||
<Grid item sx={{ position: 'relative', top: '0.5em' }}>
|
||||
<Typography variant='caption' color='text.secondary'>
|
||||
{t('I want to')}
|
||||
</Typography>
|
||||
</Grid>
|
||||
) : null}
|
||||
|
||||
<Grid item>
|
||||
<ToggleButtonGroup
|
||||
sx={{
|
||||
height: '2.6em',
|
||||
backgroundColor: theme.palette.background.paper,
|
||||
border: '0.5px solid',
|
||||
borderColor: 'text.disabled',
|
||||
'&:hover': {
|
||||
borderColor: 'text.primary',
|
||||
border: '1px solid',
|
||||
},
|
||||
}}
|
||||
size='small'
|
||||
exclusive={true}
|
||||
value={type}
|
||||
onChange={onTypeChange}
|
||||
>
|
||||
<ToggleButton value={1} color={'primary'}>
|
||||
{t('Buy')}
|
||||
</ToggleButton>
|
||||
<ToggleButton value={0} color={'secondary'}>
|
||||
{t('Sell')}
|
||||
</ToggleButton>
|
||||
</ToggleButtonGroup>
|
||||
</Grid>
|
||||
|
||||
{width > verboseToolbarWidth ? (
|
||||
<Grid item sx={{ position: 'relative', top: '0.5em' }}>
|
||||
<Typography variant='caption' color='text.secondary'>
|
||||
{t('and use')}
|
||||
</Typography>
|
||||
</Grid>
|
||||
) : null}
|
||||
|
||||
<Grid item>
|
||||
<Select
|
||||
autoWidth
|
||||
sx={{
|
||||
height: '2.3em',
|
||||
border: '0.5px solid',
|
||||
backgroundColor: theme.palette.background.paper,
|
||||
borderRadius: '4px',
|
||||
borderColor: 'text.disabled',
|
||||
'&:hover': {
|
||||
borderColor: 'text.primary',
|
||||
},
|
||||
}}
|
||||
size='small'
|
||||
label={t('Select Payment Currency')}
|
||||
required={true}
|
||||
value={currency}
|
||||
inputProps={{
|
||||
style: { textAlign: 'center' },
|
||||
}}
|
||||
onChange={onCurrencyChange}
|
||||
>
|
||||
<MenuItem value={0}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', flexWrap: 'wrap' }}>
|
||||
<FlagWithProps code='ANY' />
|
||||
<Typography sx={{ width: '2em' }} align='right' color='text.secondary'>
|
||||
{' ' + t('ANY')}
|
||||
</Typography>
|
||||
</div>
|
||||
</MenuItem>
|
||||
{Object.entries(currencyDict).map(([key, value]) => (
|
||||
<MenuItem key={key} value={parseInt(key)} color='text.secondary'>
|
||||
<div style={{ display: 'flex', alignItems: 'center', flexWrap: 'wrap' }}>
|
||||
<FlagWithProps code={value} />
|
||||
<Typography sx={{ width: '2em' }} align='right' color='text.secondary'>
|
||||
{' ' + value}
|
||||
</Typography>
|
||||
</div>
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
</Grid>
|
||||
|
||||
{width > verboseToolbarWidth ? (
|
||||
<Grid item sx={{ position: 'relative', top: '0.5em' }}>
|
||||
<Typography variant='caption' color='text.secondary'>
|
||||
{currency == 1000 ? t('swap to') : t('pay with')}
|
||||
</Typography>
|
||||
</Grid>
|
||||
) : null}
|
||||
|
||||
{width > mediumToolbarWidth ? (
|
||||
<Grid item>
|
||||
<AutocompletePayments
|
||||
sx={{
|
||||
minHeight: '2.6em',
|
||||
width: '12em',
|
||||
maxHeight: '2.6em',
|
||||
border: '2px solid',
|
||||
borderColor: theme.palette.text.disabled,
|
||||
hoverBorderColor: 'text.primary',
|
||||
}}
|
||||
labelProps={{ sx: { top: '0.645em' } }}
|
||||
tagProps={{ sx: { height: '1.8em' } }}
|
||||
listBoxProps={{ sx: { width: '13em' } }}
|
||||
onAutocompleteChange={setPaymentMethods}
|
||||
value={paymentMethod}
|
||||
optionsType={currency == 1000 ? 'swap' : 'fiat'}
|
||||
error={false}
|
||||
helperText={''}
|
||||
label={currency == 1000 ? t('DESTINATION') : t('METHOD')}
|
||||
tooltipTitle=''
|
||||
listHeaderText=''
|
||||
addNewButtonText=''
|
||||
isFilter={true}
|
||||
/>
|
||||
</Grid>
|
||||
) : null}
|
||||
|
||||
{width > smallestToolbarWidth && width < mediumToolbarWidth ? (
|
||||
<Grid item>
|
||||
<Select
|
||||
sx={{
|
||||
height: '2.3em',
|
||||
border: '0.5px solid',
|
||||
backgroundColor: theme.palette.background.paper,
|
||||
borderRadius: '4px',
|
||||
borderColor: 'text.disabled',
|
||||
'&:hover': {
|
||||
borderColor: 'text.primary',
|
||||
},
|
||||
}}
|
||||
size='small'
|
||||
label={t('Select Payment Method')}
|
||||
required={true}
|
||||
renderValue={(value) => (
|
||||
<div style={{ display: 'flex', alignItems: 'center' }}>
|
||||
<PaymentIcon width={22} height={22} icon={value.icon} />
|
||||
</div>
|
||||
)}
|
||||
inputProps={{
|
||||
style: { textAlign: 'center' },
|
||||
}}
|
||||
value={paymentMethod[0] ? paymentMethod[0] : ''}
|
||||
onChange={(e) => setPaymentMethods([e.target.value])}
|
||||
>
|
||||
{currency === 1000
|
||||
? swapDestinations.map((method, index) => (
|
||||
<MenuItem
|
||||
style={{ width: '10em' }}
|
||||
key={index}
|
||||
value={method}
|
||||
color='text.secondary'
|
||||
>
|
||||
<div style={{ display: 'flex', alignItems: 'center', flexWrap: 'wrap' }}>
|
||||
<PaymentIcon width={22} height={22} icon={method.icon} />
|
||||
<div style={{ width: '0.3em' }} />
|
||||
<Typography sx={{ width: '2em' }} align='right' color='text.secondary'>
|
||||
{' ' + method.name}
|
||||
</Typography>
|
||||
</div>
|
||||
</MenuItem>
|
||||
))
|
||||
: paymentMethods.map((method, index) => (
|
||||
<MenuItem
|
||||
style={{ width: '14em' }}
|
||||
key={index}
|
||||
value={method}
|
||||
color='text.secondary'
|
||||
>
|
||||
<div style={{ display: 'flex', alignItems: 'center', flexWrap: 'wrap' }}>
|
||||
<PaymentIcon width={22} height={22} icon={method.icon} />
|
||||
<div style={{ width: '0.3em' }} />
|
||||
<Typography sx={{ width: '2em' }} align='right' color='text.secondary'>
|
||||
{' ' + method.name}
|
||||
</Typography>
|
||||
</div>
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
</Grid>
|
||||
) : null}
|
||||
</Grid>
|
||||
<Divider />
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default BookControl;
|
255
frontend/src/components/BookPage/BookPage.tsx
Normal file
255
frontend/src/components/BookPage/BookPage.tsx
Normal file
@ -0,0 +1,255 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Button, Typography, Grid, ButtonGroup, Dialog, Box } from '@mui/material';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import currencyDict from '../../../static/assets/currencies.json';
|
||||
import DepthChart from '../Charts/DepthChart';
|
||||
|
||||
import { Order, LimitList, Maker } from '../../models';
|
||||
|
||||
// Icons
|
||||
import { BarChart, FormatListBulleted } from '@mui/icons-material';
|
||||
import BookTable from './BookTable';
|
||||
import { MakerForm } from '../MakerPage';
|
||||
|
||||
interface BookPageProps {
|
||||
bookLoading?: boolean;
|
||||
bookRefreshing?: boolean;
|
||||
loadingLimits: boolean;
|
||||
lastDayPremium: number;
|
||||
orders: Order[];
|
||||
limits: LimitList;
|
||||
fetchLimits: () => void;
|
||||
type: number;
|
||||
currency: number;
|
||||
windowWidth: number;
|
||||
windowHeight: number;
|
||||
fetchBook: (loading: boolean, refreshing: boolean) => void;
|
||||
setAppState: (state: object) => void;
|
||||
}
|
||||
|
||||
const BookPage = ({
|
||||
bookLoading = false,
|
||||
bookRefreshing = false,
|
||||
lastDayPremium = 0,
|
||||
loadingLimits,
|
||||
orders = [],
|
||||
limits,
|
||||
fetchLimits,
|
||||
type,
|
||||
currency,
|
||||
windowWidth,
|
||||
windowHeight,
|
||||
setAppState,
|
||||
fetchBook,
|
||||
}: BookPageProps): JSX.Element => {
|
||||
const { t } = useTranslation();
|
||||
const history = useHistory();
|
||||
const [view, setView] = useState<'list' | 'depth'>('list');
|
||||
const [openMaker, setOpenMaker] = useState<boolean>(false);
|
||||
|
||||
const doubleView = windowWidth > 115;
|
||||
const width = windowWidth * 0.9;
|
||||
const maxBookTableWidth = 85;
|
||||
const chartWidthEm = width - maxBookTableWidth;
|
||||
|
||||
const defaultMaker: Maker = {
|
||||
isExplicit: false,
|
||||
amount: '',
|
||||
paymentMethods: [],
|
||||
paymentMethodsText: 'not specified',
|
||||
badPaymentMethod: false,
|
||||
premium: '',
|
||||
satoshis: '',
|
||||
publicExpiryTime: new Date(0, 0, 0, 23, 59),
|
||||
publicDuration: 86340,
|
||||
escrowExpiryTime: new Date(0, 0, 0, 3, 0),
|
||||
escrowDuration: 10800,
|
||||
bondSize: 3,
|
||||
minAmount: '',
|
||||
maxAmount: '',
|
||||
badPremiumText: '',
|
||||
badSatoshisText: '',
|
||||
};
|
||||
|
||||
const [maker, setMaker] = useState<Maker>(defaultMaker);
|
||||
|
||||
useEffect(() => {
|
||||
if (orders.length < 1) {
|
||||
fetchBook(true, false);
|
||||
} else {
|
||||
fetchBook(false, true);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handleCurrencyChange = function (e) {
|
||||
const currency = e.target.value;
|
||||
setAppState({ currency });
|
||||
};
|
||||
|
||||
const handleTypeChange = function (mouseEvent, val) {
|
||||
setAppState({ type: val });
|
||||
};
|
||||
|
||||
const NoOrdersFound = function () {
|
||||
return (
|
||||
<Grid
|
||||
container
|
||||
direction='column'
|
||||
justifyContent='center'
|
||||
alignItems='center'
|
||||
sx={{ width: '100%', height: '100%' }}
|
||||
>
|
||||
<Grid item>
|
||||
<Typography align='center' component='h5' variant='h5'>
|
||||
{type == 0
|
||||
? t('No orders found to sell BTC for {{currencyCode}}', {
|
||||
currencyCode: currency == 0 ? t('ANY') : currencyDict[currency.toString()],
|
||||
})
|
||||
: t('No orders found to buy BTC for {{currencyCode}}', {
|
||||
currencyCode: currency == 0 ? t('ANY') : currencyDict[currency.toString()],
|
||||
})}
|
||||
</Typography>
|
||||
</Grid>
|
||||
<Grid item>
|
||||
<Typography align='center' color='primary' variant='h6'>
|
||||
{t('Be the first one to create an order')}
|
||||
</Typography>
|
||||
</Grid>
|
||||
</Grid>
|
||||
);
|
||||
};
|
||||
|
||||
const NavButtons = function () {
|
||||
return (
|
||||
<ButtonGroup variant='contained' color='inherit'>
|
||||
<Button color='primary' onClick={() => setOpenMaker(true)}>
|
||||
{t('Create Order')}
|
||||
</Button>
|
||||
{doubleView ? (
|
||||
<></>
|
||||
) : (
|
||||
<Button
|
||||
color='inherit'
|
||||
style={{ color: '#111111' }}
|
||||
onClick={() => setView(view === 'depth' ? 'list' : 'depth')}
|
||||
>
|
||||
{view == 'depth' ? (
|
||||
<>
|
||||
<FormatListBulleted /> {t('List')}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<BarChart /> {t('Chart')}
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
)}
|
||||
<Button color='secondary' onClick={() => history.push('/')}>
|
||||
{t('Back')}
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
);
|
||||
};
|
||||
return (
|
||||
<Grid container direction='column' alignItems='center' spacing={1} sx={{ minWidth: 400 }}>
|
||||
{openMaker ? (
|
||||
<Dialog open={openMaker} onClose={() => setOpenMaker(false)}>
|
||||
<Box sx={{ maxWidth: '18em', padding: '0.5em' }}>
|
||||
<MakerForm
|
||||
limits={limits}
|
||||
fetchLimits={fetchLimits}
|
||||
loadingLimits={loadingLimits}
|
||||
pricingMethods={false}
|
||||
setAppState={setAppState}
|
||||
maker={maker}
|
||||
defaultMaker={defaultMaker}
|
||||
setMaker={setMaker}
|
||||
type={type}
|
||||
currency={currency}
|
||||
/>
|
||||
</Box>
|
||||
</Dialog>
|
||||
) : null}
|
||||
|
||||
<Grid item xs={12}>
|
||||
{doubleView ? (
|
||||
<Grid
|
||||
container
|
||||
alignItems='center'
|
||||
justifyContent='center'
|
||||
spacing={1}
|
||||
direction='row'
|
||||
style={{ width: `${windowWidth}em` }}
|
||||
>
|
||||
<Grid item>
|
||||
<BookTable
|
||||
loading={bookLoading}
|
||||
refreshing={bookRefreshing}
|
||||
clickRefresh={() => fetchBook(false, true)}
|
||||
orders={orders}
|
||||
type={type}
|
||||
currency={currency}
|
||||
maxWidth={maxBookTableWidth} // EM units
|
||||
maxHeight={windowHeight * 0.825 - 5} // EM units
|
||||
fullWidth={windowWidth} // EM units
|
||||
fullHeight={windowHeight} // EM units
|
||||
defaultFullscreen={false}
|
||||
onCurrencyChange={handleCurrencyChange}
|
||||
onTypeChange={handleTypeChange}
|
||||
noResultsOverlay={NoOrdersFound}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item>
|
||||
<DepthChart
|
||||
orders={orders}
|
||||
lastDayPremium={lastDayPremium}
|
||||
currency={currency}
|
||||
compact={true}
|
||||
setAppState={setAppState}
|
||||
limits={limits}
|
||||
maxWidth={chartWidthEm} // EM units
|
||||
maxHeight={windowHeight * 0.825 - 5} // EM units
|
||||
/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
) : view === 'depth' ? (
|
||||
<DepthChart
|
||||
bookLoading={bookLoading}
|
||||
orders={orders}
|
||||
lastDayPremium={lastDayPremium}
|
||||
currency={currency}
|
||||
compact={true}
|
||||
setAppState={setAppState}
|
||||
limits={limits}
|
||||
maxWidth={windowWidth * 0.8} // EM units
|
||||
maxHeight={windowHeight * 0.825 - 5} // EM units
|
||||
/>
|
||||
) : (
|
||||
<BookTable
|
||||
loading={bookLoading}
|
||||
refreshing={bookRefreshing}
|
||||
clickRefresh={() => fetchBook(false, true)}
|
||||
orders={orders}
|
||||
type={type}
|
||||
currency={currency}
|
||||
maxWidth={windowWidth * 0.97} // EM units
|
||||
maxHeight={windowHeight * 0.825 - 5} // EM units
|
||||
fullWidth={windowWidth} // EM units
|
||||
fullHeight={windowHeight} // EM units
|
||||
defaultFullscreen={false}
|
||||
onCurrencyChange={handleCurrencyChange}
|
||||
onTypeChange={handleTypeChange}
|
||||
noResultsOverlay={NoOrdersFound}
|
||||
/>
|
||||
)}
|
||||
</Grid>
|
||||
|
||||
<Grid item xs={12}>
|
||||
<NavButtons />
|
||||
</Grid>
|
||||
</Grid>
|
||||
);
|
||||
};
|
||||
|
||||
export default BookPage;
|
@ -7,7 +7,6 @@ import {
|
||||
Dialog,
|
||||
Typography,
|
||||
Paper,
|
||||
Stack,
|
||||
ListItemButton,
|
||||
ListItemText,
|
||||
ListItemAvatar,
|
||||
@ -15,38 +14,46 @@ import {
|
||||
CircularProgress,
|
||||
LinearProgress,
|
||||
IconButton,
|
||||
Tooltip,
|
||||
} from '@mui/material';
|
||||
import { DataGrid, GridPagination } from '@mui/x-data-grid';
|
||||
import currencyDict from '../../static/assets/currencies.json';
|
||||
import { Order } from '../models/Order.model';
|
||||
import currencyDict from '../../../static/assets/currencies.json';
|
||||
import { Order } from '../../models';
|
||||
import filterOrders from '../../utils/filterOrders';
|
||||
import BookControl from './BookControl';
|
||||
|
||||
import FlagWithProps from './FlagWithProps';
|
||||
import { pn, amountToString } from '../utils/prettyNumbers';
|
||||
import PaymentText from './PaymentText';
|
||||
import RobotAvatar from './Robots/RobotAvatar';
|
||||
import hexToRgb from '../utils/hexToRgb';
|
||||
import statusBadgeColor from '../utils/statusBadgeColor';
|
||||
import FlagWithProps from '../FlagWithProps';
|
||||
import { pn, amountToString } from '../../utils/prettyNumbers';
|
||||
import PaymentText from '../PaymentText';
|
||||
import RobotAvatar from '../Robots/RobotAvatar';
|
||||
import hexToRgb from '../../utils/hexToRgb';
|
||||
import statusBadgeColor from '../../utils/statusBadgeColor';
|
||||
|
||||
// Icons
|
||||
import { Fullscreen, FullscreenExit, Refresh } from '@mui/icons-material';
|
||||
import { Fullscreen, FullscreenExit, Refresh, WidthFull } from '@mui/icons-material';
|
||||
|
||||
interface Props {
|
||||
loading: boolean;
|
||||
refreshing: boolean;
|
||||
clickRefresh: () => void;
|
||||
loading?: boolean;
|
||||
refreshing?: boolean;
|
||||
clickRefresh?: () => void;
|
||||
orders: Order[];
|
||||
type: number;
|
||||
currency: number;
|
||||
maxWidth: number;
|
||||
maxHeight: number;
|
||||
fullWidth: number;
|
||||
fullHeight: number;
|
||||
fullWidth?: number;
|
||||
fullHeight?: number;
|
||||
defaultFullscreen: boolean;
|
||||
showControls?: boolean;
|
||||
showFooter?: boolean;
|
||||
onCurrencyChange?: () => void;
|
||||
onTypeChange?: () => void;
|
||||
noResultsOverlay?: JSX.Element;
|
||||
}
|
||||
|
||||
const BookTable = ({
|
||||
loading,
|
||||
refreshing,
|
||||
loading = false,
|
||||
refreshing = false,
|
||||
clickRefresh,
|
||||
orders,
|
||||
type,
|
||||
@ -55,21 +62,27 @@ const BookTable = ({
|
||||
maxHeight,
|
||||
fullWidth,
|
||||
fullHeight,
|
||||
defaultFullscreen,
|
||||
defaultFullscreen = false,
|
||||
showControls = true,
|
||||
showFooter = true,
|
||||
onCurrencyChange,
|
||||
onTypeChange,
|
||||
noResultsOverlay,
|
||||
}: Props): JSX.Element => {
|
||||
const { t } = useTranslation();
|
||||
const theme = useTheme();
|
||||
const history = useHistory();
|
||||
const [pageSize, setPageSize] = useState(0);
|
||||
const [fullscreen, setFullscreen] = useState(defaultFullscreen);
|
||||
const [paymentMethods, setPaymentMethods] = useState<string[]>([]);
|
||||
|
||||
// all sizes in 'em'
|
||||
const fontSize = theme.typography.fontSize;
|
||||
const verticalHeightFrame = 6.9075;
|
||||
const verticalHeightFrame = 3.625 + (showControls ? 3.7 : 0) + (showFooter ? 2.35 : 0);
|
||||
const verticalHeightRow = 3.25;
|
||||
const defaultPageSize = Math.max(
|
||||
Math.floor(
|
||||
((fullscreen ? fullHeight * 0.875 : maxHeight) - verticalHeightFrame) / verticalHeightRow,
|
||||
((fullscreen ? fullHeight * 0.9 : maxHeight) - verticalHeightFrame) / verticalHeightRow,
|
||||
),
|
||||
1,
|
||||
);
|
||||
@ -98,7 +111,6 @@ const BookTable = ({
|
||||
|
||||
const localeText = {
|
||||
MuiTablePagination: { labelRowsPerPage: t('Orders per page:') },
|
||||
noRowsLabel: t('No rows'),
|
||||
noResultsOverlayLabel: t('No results found.'),
|
||||
errorOverlayDefaultLabel: t('An error occurred.'),
|
||||
toolbarColumns: t('Columns'),
|
||||
@ -152,7 +164,7 @@ const BookTable = ({
|
||||
field: 'maker_nick',
|
||||
headerName: t('Robot'),
|
||||
width: width * fontSize,
|
||||
renderCell: (params) => {
|
||||
renderCell: (params: any) => {
|
||||
return (
|
||||
<ListItemButton style={{ cursor: 'pointer', position: 'relative', left: '-1.3em' }}>
|
||||
<ListItemAvatar>
|
||||
@ -179,7 +191,7 @@ const BookTable = ({
|
||||
field: 'maker_nick',
|
||||
headerName: t('Robot'),
|
||||
width: width * fontSize,
|
||||
renderCell: (params) => {
|
||||
renderCell: (params: any) => {
|
||||
return (
|
||||
<div style={{ position: 'relative', left: '-1.64em' }}>
|
||||
<ListItemButton style={{ cursor: 'pointer' }}>
|
||||
@ -205,7 +217,7 @@ const BookTable = ({
|
||||
field: 'type',
|
||||
headerName: t('Is'),
|
||||
width: width * fontSize,
|
||||
renderCell: (params) => (params.row.type ? t('Seller') : t('Buyer')),
|
||||
renderCell: (params: any) => (params.row.type ? t('Seller') : t('Buyer')),
|
||||
};
|
||||
};
|
||||
|
||||
@ -216,7 +228,7 @@ const BookTable = ({
|
||||
headerName: t('Amount'),
|
||||
type: 'number',
|
||||
width: width * fontSize,
|
||||
renderCell: (params) => {
|
||||
renderCell: (params: any) => {
|
||||
return (
|
||||
<div style={{ cursor: 'pointer' }}>
|
||||
{amountToString(
|
||||
@ -237,7 +249,7 @@ const BookTable = ({
|
||||
field: 'currency',
|
||||
headerName: t('Currency'),
|
||||
width: width * fontSize,
|
||||
renderCell: (params) => {
|
||||
renderCell: (params: any) => {
|
||||
const currencyCode = currencyDict[params.row.currency.toString()];
|
||||
return (
|
||||
<div
|
||||
@ -262,7 +274,7 @@ const BookTable = ({
|
||||
field: 'payment_method',
|
||||
headerName: t('Payment Method'),
|
||||
width: width * fontSize,
|
||||
renderCell: (params) => {
|
||||
renderCell: (params: any) => {
|
||||
return (
|
||||
<div style={{ cursor: 'pointer' }}>
|
||||
<PaymentText
|
||||
@ -283,14 +295,13 @@ const BookTable = ({
|
||||
field: 'payment_icons',
|
||||
headerName: t('Pay'),
|
||||
width: width * fontSize,
|
||||
renderCell: (params) => {
|
||||
renderCell: (params: any) => {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
position: 'relative',
|
||||
left: '-4px',
|
||||
cursor: 'pointer',
|
||||
align: 'center',
|
||||
}}
|
||||
>
|
||||
<PaymentText
|
||||
@ -311,7 +322,7 @@ const BookTable = ({
|
||||
headerName: t('Price'),
|
||||
type: 'number',
|
||||
width: width * fontSize,
|
||||
renderCell: (params) => {
|
||||
renderCell: (params: any) => {
|
||||
const currencyCode = currencyDict[params.row.currency.toString()];
|
||||
return (
|
||||
<div style={{ cursor: 'pointer' }}>{`${pn(params.row.price)} ${currencyCode}/BTC`}</div>
|
||||
@ -332,7 +343,8 @@ const BookTable = ({
|
||||
headerName: t('Premium'),
|
||||
type: 'number',
|
||||
width: width * fontSize,
|
||||
renderCell: (params) => {
|
||||
renderCell: (params: any) => {
|
||||
const currencyCode = currencyDict[params.row.currency.toString()];
|
||||
let fontColor = `rgb(0,0,0)`;
|
||||
if (params.row.type === 0) {
|
||||
var premiumPoint = params.row.premium / buyOutstandingPremium;
|
||||
@ -353,11 +365,17 @@ const BookTable = ({
|
||||
}
|
||||
const fontWeight = 400 + Math.round(premiumPoint * 5) * 100;
|
||||
return (
|
||||
<div style={{ cursor: 'pointer' }}>
|
||||
<Typography variant='inherit' color={fontColor} sx={{ fontWeight }}>
|
||||
{parseFloat(parseFloat(params.row.premium).toFixed(4)) + '%'}
|
||||
</Typography>
|
||||
</div>
|
||||
<Tooltip
|
||||
placement='left'
|
||||
enterTouchDelay={0}
|
||||
title={pn(params.row.price) + ' ' + currencyCode + '/BTC'}
|
||||
>
|
||||
<div style={{ cursor: 'pointer' }}>
|
||||
<Typography variant='inherit' color={fontColor} sx={{ fontWeight }}>
|
||||
{parseFloat(parseFloat(params.row.premium).toFixed(4)) + '%'}
|
||||
</Typography>
|
||||
</div>
|
||||
</Tooltip>
|
||||
);
|
||||
},
|
||||
};
|
||||
@ -370,7 +388,7 @@ const BookTable = ({
|
||||
headerName: t('Timer'),
|
||||
type: 'number',
|
||||
width: width * fontSize,
|
||||
renderCell: (params) => {
|
||||
renderCell: (params: any) => {
|
||||
const hours = Math.round(params.row.escrow_duration / 3600);
|
||||
const minutes = Math.round((params.row.escrow_duration - hours * 3600) / 60);
|
||||
return <div style={{ cursor: 'pointer' }}>{hours > 0 ? `${hours}h` : `${minutes}m`}</div>;
|
||||
@ -385,9 +403,9 @@ const BookTable = ({
|
||||
headerName: t('Expiry'),
|
||||
type: 'string',
|
||||
width: width * fontSize,
|
||||
renderCell: (params) => {
|
||||
const expiresAt = new Date(params.row.expires_at);
|
||||
const timeToExpiry = Math.abs(expiresAt - new Date());
|
||||
renderCell: (params: any) => {
|
||||
const expiresAt: Date = new Date(params.row.expires_at);
|
||||
const timeToExpiry: number = Math.abs(expiresAt - new Date());
|
||||
const percent = Math.round((timeToExpiry / (24 * 60 * 60 * 1000)) * 100);
|
||||
const hours = Math.round(timeToExpiry / (3600 * 1000));
|
||||
const minutes = Math.round((timeToExpiry - hours * (3600 * 1000)) / 60000);
|
||||
@ -429,7 +447,7 @@ const BookTable = ({
|
||||
headerName: t('Sats now'),
|
||||
type: 'number',
|
||||
width: width * fontSize,
|
||||
renderCell: (params) => {
|
||||
renderCell: (params: any) => {
|
||||
return (
|
||||
<div style={{ cursor: 'pointer' }}>
|
||||
{params.row.satoshis_now > 1000000
|
||||
@ -447,7 +465,7 @@ const BookTable = ({
|
||||
field: 'id',
|
||||
headerName: 'Order ID',
|
||||
width: width * fontSize,
|
||||
renderCell: (params) => {
|
||||
renderCell: (params: any) => {
|
||||
return (
|
||||
<div style={{ cursor: 'pointer' }}>
|
||||
<Typography variant='caption' color='text.secondary'>
|
||||
@ -610,14 +628,8 @@ const BookTable = ({
|
||||
|
||||
const [columns, width] = filteredColumns(fullscreen ? fullWidth : maxWidth);
|
||||
|
||||
const gridComponents = {
|
||||
LoadingOverlay: LinearProgress,
|
||||
NoResultsOverlay: () => (
|
||||
<Stack height='100%' alignItems='center' justifyContent='center'>
|
||||
{t('Filter has no results')}
|
||||
</Stack>
|
||||
),
|
||||
Footer: () => (
|
||||
const Footer = function () {
|
||||
return (
|
||||
<Grid container alignItems='center' direction='row' justifyContent='space-between'>
|
||||
<Grid item>
|
||||
<Grid container alignItems='center' direction='row'>
|
||||
@ -638,7 +650,47 @@ const BookTable = ({
|
||||
<GridPagination />
|
||||
</Grid>
|
||||
</Grid>
|
||||
),
|
||||
);
|
||||
};
|
||||
|
||||
interface GridComponentProps {
|
||||
LoadingOverlay: JSX.Element;
|
||||
NoResultsOverlay?: JSX.Element;
|
||||
NoRowsOverlay?: JSX.Element;
|
||||
Footer?: JSX.Element;
|
||||
Toolbar?: JSX.Element;
|
||||
}
|
||||
|
||||
const Controls = function () {
|
||||
return (
|
||||
<BookControl
|
||||
width={width}
|
||||
type={type}
|
||||
currency={currency}
|
||||
onCurrencyChange={onCurrencyChange}
|
||||
onTypeChange={onTypeChange}
|
||||
paymentMethod={paymentMethods}
|
||||
setPaymentMethods={setPaymentMethods}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const gridComponents = function () {
|
||||
const components: GridComponentProps = {
|
||||
LoadingOverlay: LinearProgress,
|
||||
};
|
||||
|
||||
if (noResultsOverlay != null) {
|
||||
components.NoResultsOverlay = noResultsOverlay;
|
||||
components.NoRowsOverlay = noResultsOverlay;
|
||||
}
|
||||
if (showFooter) {
|
||||
components.Footer = Footer;
|
||||
}
|
||||
if (showControls) {
|
||||
components.Toolbar = Controls;
|
||||
}
|
||||
return components;
|
||||
};
|
||||
|
||||
if (!fullscreen) {
|
||||
@ -646,20 +698,26 @@ const BookTable = ({
|
||||
<Paper style={{ width: `${width}em`, height: `${height}em`, overflow: 'auto' }}>
|
||||
<DataGrid
|
||||
localeText={localeText}
|
||||
rows={orders.filter(
|
||||
(order) =>
|
||||
(order.type == type || type == null) && (order.currency == currency || currency == 0),
|
||||
)}
|
||||
rows={
|
||||
showControls
|
||||
? filterOrders({
|
||||
orders,
|
||||
baseFilter: { currency, type },
|
||||
paymentMethods,
|
||||
})
|
||||
: orders
|
||||
}
|
||||
loading={loading || refreshing}
|
||||
columns={columns}
|
||||
components={gridComponents}
|
||||
hideFooter={!showFooter}
|
||||
components={gridComponents()}
|
||||
pageSize={loading ? 0 : pageSize}
|
||||
rowsPerPageOptions={[0, pageSize, defaultPageSize * 2, 50, 100]}
|
||||
rowsPerPageOptions={width < 22 ? [] : [0, pageSize, defaultPageSize * 2, 50, 100]}
|
||||
onPageSizeChange={(newPageSize) => {
|
||||
setPageSize(newPageSize);
|
||||
setUseDefaultPageSize(false);
|
||||
}}
|
||||
onRowClick={(params) => history.push('/order/' + params.row.id)} // Whole row is clickable, but the mouse only looks clickly in some places.
|
||||
onRowClick={(params: any) => history.push('/order/' + params.row.id)} // Whole row is clickable, but the mouse only looks clickly in some places.
|
||||
/>
|
||||
</Paper>
|
||||
);
|
||||
@ -676,14 +734,15 @@ const BookTable = ({
|
||||
)}
|
||||
loading={loading || refreshing}
|
||||
columns={columns}
|
||||
components={gridComponents}
|
||||
hideFooter={!showFooter}
|
||||
components={gridComponents()}
|
||||
pageSize={loading ? 0 : pageSize}
|
||||
rowsPerPageOptions={[0, pageSize, defaultPageSize * 2, 50, 100]}
|
||||
onPageSizeChange={(newPageSize) => {
|
||||
setPageSize(newPageSize);
|
||||
setUseDefaultPageSize(false);
|
||||
}}
|
||||
onRowClick={(params) => history.push('/order/' + params.row.id)} // Whole row is clickable, but the mouse only looks clickly in some places.
|
||||
onRowClick={(params: any) => history.push('/order/' + params.row.id)} // Whole row is clickable, but the mouse only looks clickly in some places.
|
||||
/>
|
||||
</Paper>
|
||||
</Dialog>
|
4
frontend/src/components/BookPage/index.ts
Normal file
4
frontend/src/components/BookPage/index.ts
Normal file
@ -0,0 +1,4 @@
|
||||
import BookPage from './BookPage';
|
||||
export default BookPage;
|
||||
|
||||
export { default as BookTable } from './BookTable';
|
@ -192,7 +192,7 @@ class BottomBar extends Component {
|
||||
secondaryTypographyProps: { fontSize: (fontSize * 12) / 14 },
|
||||
};
|
||||
return (
|
||||
<Paper elevation={6} style={{ height: 40 * fontSizeFactor, width: window.innerWidth }}>
|
||||
<Paper elevation={6} style={{ height: '2.85em', width: '100%' }}>
|
||||
<Grid container>
|
||||
<Grid item xs={1.9}>
|
||||
<div style={{ display: this.showProfileButton() ? '' : 'none' }}>
|
||||
@ -485,7 +485,7 @@ class BottomBar extends Component {
|
||||
this.props.avatarLoaded
|
||||
);
|
||||
return (
|
||||
<Paper elevation={6} style={{ height: 40 }}>
|
||||
<Paper elevation={6} style={{ height: '2.85em', width: '100%' }}>
|
||||
<Grid container>
|
||||
<Grid item xs={1.6}>
|
||||
<div style={{ display: this.showProfileButton() ? '' : 'none' }}>
|
||||
|
@ -21,8 +21,7 @@ import {
|
||||
import { AddCircleOutline, RemoveCircleOutline } from '@mui/icons-material';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import { Order } from '../../../models/Order.model';
|
||||
import { LimitList } from '../../../models/Limit.model';
|
||||
import { Order, LimitList } from '../../../models';
|
||||
import RobotAvatar from '../../Robots/RobotAvatar';
|
||||
import { amountToString } from '../../../utils/prettyNumbers';
|
||||
import currencyDict from '../../../../static/assets/currencies.json';
|
||||
|
@ -91,7 +91,7 @@ const FlagWithProps = ({ code }: Props): JSX.Element => {
|
||||
if (code === 'XAU') flag = <GoldIcon {...defaultProps} />;
|
||||
if (code === 'BTC') flag = <SwapCallsIcon color='primary' />;
|
||||
|
||||
return <div style={{ width: 28, height: 20 }}>{flag}</div>;
|
||||
return <div style={{ width: 28, height: 20, maxHeight: 20 }}>{flag}</div>;
|
||||
};
|
||||
|
||||
export default FlagWithProps;
|
||||
|
@ -7,6 +7,8 @@ import BookPage from './BookPage';
|
||||
import OrderPage from './OrderPage';
|
||||
import BottomBar from './BottomBar';
|
||||
|
||||
import { apiClient } from '../services/api';
|
||||
|
||||
export default class HomePage extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
@ -20,7 +22,7 @@ export default class HomePage extends Component {
|
||||
type: null,
|
||||
currency: 0,
|
||||
bookCurrencyCode: 'ANY',
|
||||
bookOrders: new Array(),
|
||||
orders: new Array(),
|
||||
bookLoading: true,
|
||||
bookRefreshing: false,
|
||||
activeOrderId: null,
|
||||
@ -29,14 +31,21 @@ export default class HomePage extends Component {
|
||||
referralCode: '',
|
||||
lastDayPremium: 0,
|
||||
limits: {},
|
||||
loadingLimits: true,
|
||||
maker: {},
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount = () => {
|
||||
if (typeof window !== undefined) {
|
||||
this.setState({ windowWidth: window.innerWidth, windowHeight: window.innerHeight });
|
||||
this.setState({
|
||||
windowWidth: window.innerWidth / this.props.theme.typography.fontSize,
|
||||
windowHeight: window.innerHeight / this.props.theme.typography.fontSize,
|
||||
});
|
||||
window.addEventListener('resize', this.onResize);
|
||||
}
|
||||
this.fetchBook(true, false);
|
||||
this.fetchLimits(true);
|
||||
};
|
||||
|
||||
componentWillUnmount = () => {
|
||||
@ -46,7 +55,10 @@ export default class HomePage extends Component {
|
||||
};
|
||||
|
||||
onResize = () => {
|
||||
this.setState({ windowWidth: window.innerWidth, windowHeight: window.innerHeight });
|
||||
this.setState({
|
||||
windowWidth: window.innerWidth / this.props.theme.typography.fontSize,
|
||||
windowHeight: window.innerHeight / this.props.theme.typography.fontSize,
|
||||
});
|
||||
};
|
||||
|
||||
setAppState = (newState) => {
|
||||
@ -62,10 +74,29 @@ export default class HomePage extends Component {
|
||||
// Only for Android
|
||||
return window.location.pathname;
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
fetchBook = (loading, refreshing) => {
|
||||
this.setState({ bookLoading: loading, bookRefreshing: refreshing });
|
||||
apiClient.get('/api/book/').then((data) =>
|
||||
this.setState({
|
||||
bookLoading: false,
|
||||
bookRefreshing: false,
|
||||
orders: data.not_found ? [] : data,
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
fetchLimits = (loading) => {
|
||||
this.setState({ loadingLimits: loading });
|
||||
const limits = apiClient.get('/api/limits/').then((data) => {
|
||||
this.setState({ limits: data, loadingLimits: false });
|
||||
return data;
|
||||
});
|
||||
return limits;
|
||||
};
|
||||
|
||||
render() {
|
||||
const fontSize = this.props.theme.typography.fontSize;
|
||||
const fontSizeFactor = fontSize / 14; // default fontSize is 14
|
||||
@ -105,6 +136,7 @@ export default class HomePage extends Component {
|
||||
{...props}
|
||||
{...this.state}
|
||||
{...this.props}
|
||||
fetchLimits={this.fetchLimits}
|
||||
setAppState={this.setAppState}
|
||||
/>
|
||||
)}
|
||||
@ -116,6 +148,8 @@ export default class HomePage extends Component {
|
||||
{...props}
|
||||
{...this.state}
|
||||
{...this.props}
|
||||
fetchBook={this.fetchBook}
|
||||
fetchLimits={this.fetchLimits}
|
||||
setAppState={this.setAppState}
|
||||
/>
|
||||
)}
|
||||
@ -135,7 +169,10 @@ export default class HomePage extends Component {
|
||||
</div>
|
||||
<div
|
||||
className='bottomBar'
|
||||
style={{ height: `${40 * fontSizeFactor}px`, width: this.state.windowWidth }}
|
||||
style={{
|
||||
height: `${40 * fontSizeFactor}px`,
|
||||
width: `${(this.state.windowWidth / 16) * 14}em`,
|
||||
}}
|
||||
>
|
||||
<BottomBar
|
||||
redirectTo={this.redirectTo}
|
||||
|
File diff suppressed because it is too large
Load Diff
191
frontend/src/components/MakerPage/AmountRange.tsx
Normal file
191
frontend/src/components/MakerPage/AmountRange.tsx
Normal file
@ -0,0 +1,191 @@
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import {
|
||||
SliderThumb,
|
||||
Grid,
|
||||
Typography,
|
||||
TextField,
|
||||
Select,
|
||||
MenuItem,
|
||||
Box,
|
||||
useTheme,
|
||||
} from '@mui/material';
|
||||
|
||||
import FlagWithProps from '../FlagWithProps';
|
||||
import RangeSlider from './RangeSlider';
|
||||
import currencyDict from '../../../static/assets/currencies.json';
|
||||
import { pn } from '../../utils/prettyNumbers';
|
||||
|
||||
const RangeThumbComponent = function (props: object) {
|
||||
const { children, ...other } = props;
|
||||
return (
|
||||
<SliderThumb {...other}>
|
||||
{children}
|
||||
<span className='range-bar' />
|
||||
<span className='range-bar' />
|
||||
<span className='range-bar' />
|
||||
</SliderThumb>
|
||||
);
|
||||
};
|
||||
|
||||
interface AmountRangeProps {
|
||||
minAmount: string;
|
||||
maxAmount: string;
|
||||
type: number;
|
||||
currency: number;
|
||||
handleRangeAmountChange: (e: any, activeThumb: any) => void;
|
||||
handleMaxAmountChange: () => void;
|
||||
handleMinAmountChange: () => void;
|
||||
handleCurrencyChange: () => void;
|
||||
maxAmountError: boolean;
|
||||
minAmountError: boolean;
|
||||
currencyCode: string;
|
||||
amountLimits: number[];
|
||||
}
|
||||
|
||||
function AmountRange({
|
||||
minAmount,
|
||||
handleRangeAmountChange,
|
||||
currency,
|
||||
currencyCode,
|
||||
handleCurrencyChange,
|
||||
amountLimits,
|
||||
maxAmount,
|
||||
minAmountError,
|
||||
maxAmountError,
|
||||
handleMinAmountChange,
|
||||
handleMaxAmountChange,
|
||||
}: AmountRangeProps) {
|
||||
const theme = useTheme();
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Grid item xs={12}>
|
||||
<Box
|
||||
sx={{
|
||||
padding: '0.5em',
|
||||
backgroundColor: 'background.paper',
|
||||
border: '1px solid',
|
||||
borderRadius: '4px',
|
||||
borderColor: theme.palette.mode === 'dark' ? '#434343' : '#c4c4c4',
|
||||
'&:hover': {
|
||||
borderColor: theme.palette.mode === 'dark' ? '#ffffff' : '#2f2f2f',
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Grid container direction='column' alignItems='center' spacing={0.5}>
|
||||
<Grid item sx={{ display: 'flex', alignItems: 'center', flexWrap: 'wrap' }}>
|
||||
<Typography
|
||||
sx={{
|
||||
width: `${t('From').length * 0.56 + 0.6}em`,
|
||||
textAlign: 'left',
|
||||
color: 'text.secondary',
|
||||
}}
|
||||
variant='caption'
|
||||
>
|
||||
{t('From')}
|
||||
</Typography>
|
||||
<TextField
|
||||
variant='standard'
|
||||
type='number'
|
||||
size='small'
|
||||
value={minAmount}
|
||||
onChange={handleMinAmountChange}
|
||||
error={minAmountError}
|
||||
sx={{
|
||||
width: `${minAmount.toString().length * 0.56}em`,
|
||||
minWidth: '0.56em',
|
||||
maxWidth: '2.8em',
|
||||
}}
|
||||
/>
|
||||
<Typography
|
||||
sx={{
|
||||
width: `${t('to').length * 0.56 + 0.6}em`,
|
||||
textAlign: 'center',
|
||||
color: 'text.secondary',
|
||||
}}
|
||||
variant='caption'
|
||||
>
|
||||
{t('to')}
|
||||
</Typography>
|
||||
<TextField
|
||||
variant='standard'
|
||||
size='small'
|
||||
type='number'
|
||||
value={maxAmount}
|
||||
onChange={handleMaxAmountChange}
|
||||
error={maxAmountError}
|
||||
sx={{
|
||||
width: `${maxAmount.toString().length * 0.56}em`,
|
||||
minWidth: '0.56em',
|
||||
maxWidth: '3.36em',
|
||||
}}
|
||||
/>
|
||||
<div style={{ width: '0.5em' }} />
|
||||
<Select
|
||||
sx={{ width: '3.8em' }}
|
||||
variant='standard'
|
||||
size='small'
|
||||
required={true}
|
||||
inputProps={{
|
||||
style: { textAlign: 'center' },
|
||||
}}
|
||||
value={currency == 0 ? 1 : currency}
|
||||
renderValue={() => currencyCode}
|
||||
onChange={(e) => handleCurrencyChange(e.target.value)}
|
||||
>
|
||||
{Object.entries(currencyDict).map(([key, value]) => (
|
||||
<MenuItem key={key} value={parseInt(key)}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', flexWrap: 'wrap' }}>
|
||||
<FlagWithProps code={value} />
|
||||
{' ' + value}
|
||||
</div>
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
</Grid>
|
||||
|
||||
<Grid
|
||||
item
|
||||
sx={{
|
||||
width: `calc(100% - ${Math.log10(amountLimits[1] * 0.65) + 2}em)`,
|
||||
}}
|
||||
>
|
||||
<RangeSlider
|
||||
disableSwap={true}
|
||||
value={[Number(minAmount), Number(maxAmount)]}
|
||||
step={(amountLimits[1] - amountLimits[0]) / 5000}
|
||||
valueLabelDisplay='auto'
|
||||
components={{ Thumb: RangeThumbComponent }}
|
||||
componentsProps={{
|
||||
thumb: { style: { backgroundColor: theme.palette.background.paper } },
|
||||
}}
|
||||
valueLabelFormat={(x) =>
|
||||
pn(parseFloat(Number(x).toPrecision(x < 100 ? 2 : 3))) + ' ' + currencyCode
|
||||
}
|
||||
marks={[
|
||||
{
|
||||
value: amountLimits[0],
|
||||
label: `${pn(
|
||||
parseFloat(Number(amountLimits[0]).toPrecision(3)),
|
||||
)} ${currencyCode}`,
|
||||
},
|
||||
{
|
||||
value: amountLimits[1],
|
||||
label: `${pn(
|
||||
parseFloat(Number(amountLimits[1]).toPrecision(3)),
|
||||
)} ${currencyCode}`,
|
||||
},
|
||||
]}
|
||||
min={amountLimits[0]}
|
||||
max={amountLimits[1]}
|
||||
onChange={handleRangeAmountChange}
|
||||
/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Box>
|
||||
</Grid>
|
||||
);
|
||||
}
|
||||
|
||||
export default AmountRange;
|
@ -3,57 +3,60 @@ import PropTypes from 'prop-types';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useAutocomplete } from '@mui/base/AutocompleteUnstyled';
|
||||
import { styled } from '@mui/material/styles';
|
||||
import { Button, Tooltip } from '@mui/material';
|
||||
import { paymentMethods, swapDestinations } from './payment-methods/Methods';
|
||||
import { Button, Fade, Tooltip, Typography, Grow } from '@mui/material';
|
||||
import { paymentMethods, swapDestinations } from '../payment-methods/Methods';
|
||||
|
||||
// Icons
|
||||
import DashboardCustomizeIcon from '@mui/icons-material/DashboardCustomize';
|
||||
import AddIcon from '@mui/icons-material/Add';
|
||||
import PaymentIcon from './payment-methods/Icons';
|
||||
import PaymentIcon from '../payment-methods/Icons';
|
||||
import CheckIcon from '@mui/icons-material/Check';
|
||||
import CloseIcon from '@mui/icons-material/Close';
|
||||
|
||||
const Root = styled('div')(
|
||||
({ theme }) => `
|
||||
color: ${theme.palette.mode === 'dark' ? 'rgba(255,255,255,0.65)' : 'rgba(0,0,0,.85)'};
|
||||
font-size: 14px;
|
||||
color: ${theme.palette.text.primary};
|
||||
font-size: ${theme.typography.fontSize};
|
||||
`,
|
||||
);
|
||||
|
||||
const Label = styled('label')(
|
||||
({ theme, error }) => `
|
||||
({ theme, error, sx }) => `
|
||||
color: ${
|
||||
theme.palette.mode === 'dark' ? (error ? '#f44336' : '#cfcfcf') : error ? '#dd0000' : '#717171'
|
||||
};
|
||||
align: center;
|
||||
padding: 0 0 4px;
|
||||
line-height: 1.5; f44336
|
||||
display: block;
|
||||
font-size: 13px;
|
||||
pointer-events: none;
|
||||
position: relative;
|
||||
left: 1em;
|
||||
top: ${sx.top};
|
||||
maxHeight: 0em;
|
||||
height: 0em;
|
||||
white-space: no-wrap;
|
||||
font-size: 1em;
|
||||
`,
|
||||
);
|
||||
|
||||
const InputWrapper = styled('div')(
|
||||
({ theme, error }) => `
|
||||
width: 244px;
|
||||
min-height: 44px;
|
||||
max-height: 124px;
|
||||
({ theme, error, sx }) => `
|
||||
min-height: ${sx.minHeight};
|
||||
max-height: ${sx.maxHeight};
|
||||
border: 1px solid ${
|
||||
theme.palette.mode === 'dark' ? (error ? '#f44336' : '#434343') : error ? '#dd0000' : '#c4c4c4'
|
||||
};
|
||||
background-color: ${theme.palette.mode === 'dark' ? '#141414' : '#fff'};
|
||||
border-radius: 4px;
|
||||
border-color: ${sx.borderColor ? `border-color ${sx.borderColor}` : ''}
|
||||
padding: 1px;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
overflow-y:auto;
|
||||
align-items: center;
|
||||
|
||||
&:hover {
|
||||
border-color: ${
|
||||
theme.palette.mode === 'dark'
|
||||
? error
|
||||
? '#f44336'
|
||||
: '#ffffff'
|
||||
: sx.hoverBorderColor
|
||||
: error
|
||||
? '#dd0000'
|
||||
: '#2f2f2f'
|
||||
@ -75,17 +78,17 @@ const InputWrapper = styled('div')(
|
||||
& input {
|
||||
background-color: ${theme.palette.mode === 'dark' ? '#141414' : '#fff'};
|
||||
color: ${theme.palette.mode === 'dark' ? 'rgba(255,255,255,0.65)' : 'rgba(0,0,0,.85)'};
|
||||
height: 30px;
|
||||
height: 2.15em;
|
||||
box-sizing: border-box;
|
||||
padding: 4px 6px;
|
||||
width: 0;
|
||||
min-width: 30px;
|
||||
font-size: 15px;
|
||||
min-width: 2.15em;
|
||||
font-size: ${theme.typography.fontSize * 1.0714};
|
||||
flex-grow: 1;
|
||||
border: 0;
|
||||
margin: 0;
|
||||
outline: 0;
|
||||
max-height: 124px;
|
||||
max-height: 8.6em;
|
||||
}
|
||||
`,
|
||||
);
|
||||
@ -110,12 +113,12 @@ Tag.propTypes = {
|
||||
};
|
||||
|
||||
const StyledTag = styled(Tag)(
|
||||
({ theme }) => `
|
||||
({ theme, sx }) => `
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 34px;
|
||||
height: ${sx.height};
|
||||
margin: 2px;
|
||||
line-height: 22px;
|
||||
line-height: 1.5em;
|
||||
background-color: ${theme.palette.mode === 'dark' ? 'rgba(255,255,255,0.08)' : '#fafafa'};
|
||||
border: 1px solid ${theme.palette.mode === 'dark' ? '#303030' : '#e8e8e8'};
|
||||
border-radius: 2px;
|
||||
@ -133,11 +136,11 @@ const StyledTag = styled(Tag)(
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
font-size: 15px;
|
||||
font-size: 0.928em;
|
||||
}
|
||||
|
||||
& svg {
|
||||
font-size: 15px;
|
||||
font-size: 0.857em;
|
||||
cursor: pointer;
|
||||
padding: 4px;
|
||||
}
|
||||
@ -152,27 +155,27 @@ const ListHeader = styled('span')(
|
||||
max-height: 10px;
|
||||
display: inline-block;
|
||||
background-color: ${theme.palette.mode === 'dark' ? '#141414' : '#ffffff'};
|
||||
font-size: 12px;
|
||||
font-size: 0.875em;
|
||||
pointer-events: none;
|
||||
`,
|
||||
);
|
||||
|
||||
const Listbox = styled('ul')(
|
||||
({ theme }) => `
|
||||
width: 244px;
|
||||
({ theme, sx }) => `
|
||||
width: ${sx ? sx.width : '15.6em'};
|
||||
margin: 2px 0 0;
|
||||
padding: 0;
|
||||
position: absolute;
|
||||
list-style: none;
|
||||
background-color: ${theme.palette.mode === 'dark' ? '#141414' : '#fff'};
|
||||
overflow: auto;
|
||||
max-height: 250px;
|
||||
max-height: 17em;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
|
||||
z-index: 999;
|
||||
|
||||
& li {
|
||||
padding: 5px 12px;
|
||||
padding: 0em 0em;
|
||||
display: flex;
|
||||
|
||||
& span {
|
||||
@ -219,26 +222,19 @@ export default function AutocompletePayments(props) {
|
||||
focused = 'true',
|
||||
setAnchorEl,
|
||||
} = useAutocomplete({
|
||||
sx: { width: '200px', align: 'left' },
|
||||
fullWidth: true,
|
||||
id: 'payment-methods',
|
||||
multiple: true,
|
||||
value: props.value,
|
||||
options: props.optionsType == 'fiat' ? paymentMethods : swapDestinations,
|
||||
getOptionLabel: (option) => option.name,
|
||||
onInputChange: (e) => setVal(e ? (e.target.value ? e.target.value : '') : ''),
|
||||
onChange: (event, value) => props.onAutocompleteChange(optionsToString(value)),
|
||||
onChange: (event, value) => props.onAutocompleteChange(value),
|
||||
onClose: () => setVal(() => ''),
|
||||
});
|
||||
|
||||
const [val, setVal] = useState();
|
||||
|
||||
function optionsToString(newValue) {
|
||||
let str = '';
|
||||
const arrayLength = newValue.length;
|
||||
for (let i = 0; i < arrayLength; i++) {
|
||||
str += newValue[i].name + ' ';
|
||||
}
|
||||
return str.slice(0, -1);
|
||||
}
|
||||
const [val, setVal] = useState('');
|
||||
const fewerOptions = groupedOptions.length > 8 ? groupedOptions.slice(0, 8) : groupedOptions;
|
||||
|
||||
function handleAddNew(inputProps) {
|
||||
paymentMethods.push({ name: inputProps.value, icon: 'custom' });
|
||||
@ -246,53 +242,75 @@ export default function AutocompletePayments(props) {
|
||||
setVal(() => '');
|
||||
|
||||
if (a || a == null) {
|
||||
props.onAutocompleteChange(optionsToString(value));
|
||||
props.onAutocompleteChange(value);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
return (
|
||||
<Root>
|
||||
<div style={{ height: '5px' }}></div>
|
||||
<Tooltip
|
||||
placement='top'
|
||||
enterTouchDelay={300}
|
||||
enterDelay={700}
|
||||
enterTouchDelay={props.tooltipTitle == '' ? 99999 : 300}
|
||||
enterDelay={props.tooltipTitle == '' ? 99999 : 700}
|
||||
enterNextDelay={2000}
|
||||
title={props.tooltipTitle}
|
||||
>
|
||||
<div {...getRootProps()}>
|
||||
<Label {...getInputLabelProps()} error={props.error ? 'error' : null}>
|
||||
{' '}
|
||||
{props.label}
|
||||
</Label>
|
||||
<Fade
|
||||
appear={false}
|
||||
in={fewerOptions.length == 0 && value.length == 0 && val.length == 0}
|
||||
>
|
||||
<div style={{ height: 0, display: 'flex', alignItems: 'flex-start' }}>
|
||||
<Label
|
||||
{...getInputLabelProps()}
|
||||
sx={{ top: '0.72em', ...(props.labelProps ? props.labelProps.sx : {}) }}
|
||||
error={props.error ? 'error' : null}
|
||||
>
|
||||
{props.label}
|
||||
</Label>
|
||||
</div>
|
||||
</Fade>
|
||||
<InputWrapper
|
||||
ref={setAnchorEl}
|
||||
error={props.error ? 'error' : null}
|
||||
className={focused ? 'focused' : ''}
|
||||
sx={{
|
||||
minHeight: '2.9em',
|
||||
maxHeight: '8.6em',
|
||||
hoverBorderColor: '#ffffff',
|
||||
...props.sx,
|
||||
}}
|
||||
>
|
||||
{value.map((option, index) => (
|
||||
<StyledTag label={t(option.name)} icon={option.icon} {...getTagProps({ index })} />
|
||||
<StyledTag
|
||||
label={t(option.name)}
|
||||
icon={option.icon}
|
||||
sx={{ height: '2.1em', ...(props.tagProps ? props.tagProps.sx : {}) }}
|
||||
{...getTagProps({ index })}
|
||||
/>
|
||||
))}
|
||||
<input {...getInputProps()} value={val || ''} />
|
||||
{value.length > 0 && props.isFilter ? null : <input {...getInputProps()} value={val} />}
|
||||
</InputWrapper>
|
||||
</div>
|
||||
</Tooltip>
|
||||
{groupedOptions.length > 0 ? (
|
||||
<Listbox {...getListboxProps()}>
|
||||
<div
|
||||
style={{
|
||||
position: 'fixed',
|
||||
minHeight: '20px',
|
||||
marginLeft: 120 - props.listHeaderText.length * 3,
|
||||
marginTop: '-13px',
|
||||
}}
|
||||
>
|
||||
<ListHeader>
|
||||
<i>{props.listHeaderText + ' '} </i>{' '}
|
||||
</ListHeader>
|
||||
</div>
|
||||
{groupedOptions.map((option, index) => (
|
||||
<Grow in={fewerOptions.length > 0}>
|
||||
<Listbox sx={props.listBoxProps ? props.listBoxProps.sx : undefined} {...getListboxProps()}>
|
||||
{!props.isFilter ? (
|
||||
<div
|
||||
style={{
|
||||
position: 'fixed',
|
||||
minHeight: '1.428em',
|
||||
marginLeft: `${3 - props.listHeaderText.length * 0.05}em`,
|
||||
marginTop: '-0.928em',
|
||||
}}
|
||||
>
|
||||
<ListHeader>
|
||||
<i>{props.listHeaderText + ' '} </i>{' '}
|
||||
</ListHeader>
|
||||
</div>
|
||||
) : null}
|
||||
{fewerOptions.map((option, index) => (
|
||||
<li key={option.name} {...getOptionProps({ option, index })}>
|
||||
<Button
|
||||
fullWidth={true}
|
||||
@ -301,34 +319,38 @@ export default function AutocompletePayments(props) {
|
||||
sx={{ textTransform: 'none' }}
|
||||
style={{ justifyContent: 'flex-start' }}
|
||||
>
|
||||
<div style={{ position: 'relative', right: '4px', top: '4px' }}>
|
||||
<AddIcon style={{ color: '#1976d2' }} sx={{ width: 18, height: 18 }} />
|
||||
<div style={{ padding: '0.286em', position: 'relative', top: '0.35em' }}>
|
||||
<PaymentIcon width={22} height={22} icon={option.icon} />
|
||||
</div>
|
||||
{t(option.name)}
|
||||
<Typography variant='inherit' align='left'>
|
||||
{t(option.name)}
|
||||
</Typography>
|
||||
</Button>
|
||||
<div style={{ position: 'relative', top: '5px' }}>
|
||||
<div style={{ position: 'relative', top: '0.357em' }}>
|
||||
<CheckIcon />
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
{val != null ? (
|
||||
{val != null || !props.isFilter ? (
|
||||
val.length > 2 ? (
|
||||
<Button size='small' fullWidth={true} onClick={() => handleAddNew(getInputProps())}>
|
||||
<DashboardCustomizeIcon sx={{ width: 18, height: 18 }} />
|
||||
<DashboardCustomizeIcon sx={{ width: '1em', height: '1em' }} />
|
||||
{props.addNewButtonText}
|
||||
</Button>
|
||||
) : null
|
||||
) : null}
|
||||
</Listbox>
|
||||
) : // Here goes what happens if there is no groupedOptions
|
||||
getInputProps().value.length > 0 ? (
|
||||
</Grow>
|
||||
|
||||
{/* Here goes what happens if there is no fewerOptions */}
|
||||
<Grow in={getInputProps().value.length > 0 && !props.isFilter && fewerOptions.length === 0}>
|
||||
<Listbox {...getListboxProps()}>
|
||||
<Button fullWidth={true} onClick={() => handleAddNew(getInputProps())}>
|
||||
<DashboardCustomizeIcon sx={{ width: 20, height: 20 }} />
|
||||
<DashboardCustomizeIcon sx={{ width: '1.28em', height: '1.28em' }} />
|
||||
{props.addNewButtonText}
|
||||
</Button>
|
||||
</Listbox>
|
||||
) : null}
|
||||
</Grow>
|
||||
</Root>
|
||||
);
|
||||
}
|
970
frontend/src/components/MakerPage/MakerForm.tsx
Normal file
970
frontend/src/components/MakerPage/MakerForm.tsx
Normal file
@ -0,0 +1,970 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import {
|
||||
InputAdornment,
|
||||
LinearProgress,
|
||||
ButtonGroup,
|
||||
Slider,
|
||||
Switch,
|
||||
Tooltip,
|
||||
Button,
|
||||
Grid,
|
||||
Typography,
|
||||
TextField,
|
||||
Select,
|
||||
FormHelperText,
|
||||
MenuItem,
|
||||
FormControl,
|
||||
Radio,
|
||||
FormControlLabel,
|
||||
RadioGroup,
|
||||
Box,
|
||||
useTheme,
|
||||
Collapse,
|
||||
IconButton,
|
||||
} from '@mui/material';
|
||||
|
||||
import { LimitList, Maker, defaultMaker } from '../../models';
|
||||
|
||||
import { LocalizationProvider, TimePicker } from '@mui/x-date-pickers';
|
||||
import DateFnsUtils from '@date-io/date-fns';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import { StoreTokenDialog, NoRobotDialog } from '../Dialogs';
|
||||
import { apiClient } from '../../services/api';
|
||||
import { systemClient } from '../../services/System';
|
||||
|
||||
import FlagWithProps from '../FlagWithProps';
|
||||
import AutocompletePayments from './AutocompletePayments';
|
||||
import AmountRange from './AmountRange';
|
||||
import currencyDict from '../../../static/assets/currencies.json';
|
||||
import { pn } from '../../utils/prettyNumbers';
|
||||
|
||||
import { SelfImprovement, Lock, HourglassTop, DeleteSweep, Edit } from '@mui/icons-material';
|
||||
import { LoadingButton } from '@mui/lab';
|
||||
|
||||
interface MakerFormProps {
|
||||
limits: LimitList;
|
||||
fetchLimits: (loading) => void;
|
||||
loadingLimits: boolean;
|
||||
pricingMethods: boolean;
|
||||
maker: Maker;
|
||||
type: number;
|
||||
currency: number;
|
||||
setAppState: (state: object) => void;
|
||||
setMaker: (state: Maker) => void;
|
||||
disableRequest?: boolean;
|
||||
collapseAll?: boolean;
|
||||
onSubmit?: () => void;
|
||||
onReset?: () => void;
|
||||
submitButtonLabel?: string;
|
||||
}
|
||||
|
||||
const MakerForm = ({
|
||||
limits,
|
||||
fetchLimits,
|
||||
loadingLimits,
|
||||
pricingMethods,
|
||||
currency,
|
||||
type,
|
||||
setAppState,
|
||||
maker,
|
||||
setMaker,
|
||||
disableRequest = false,
|
||||
collapseAll = false,
|
||||
onSubmit = () => {},
|
||||
onReset = () => {},
|
||||
submitButtonLabel = 'Create Order',
|
||||
}: MakerFormProps): JSX.Element => {
|
||||
const { t } = useTranslation();
|
||||
const theme = useTheme();
|
||||
const history = useHistory();
|
||||
const [badRequest, setBadRequest] = useState<string | null>(null);
|
||||
const [advancedOptions, setAdvancedOptions] = useState<boolean>(false);
|
||||
const [amountLimits, setAmountLimits] = useState<number[]>([1, 1000]);
|
||||
const [satoshisLimits, setSatoshisLimits] = useState<number[]>([20000, 4000000]);
|
||||
const [currentPrice, setCurrentPrice] = useState<number | string>('...');
|
||||
const [currencyCode, setCurrencyCode] = useState<string>('USD');
|
||||
|
||||
const [openDialogs, setOpenDialogs] = useState<boolean>(false);
|
||||
const [submittingRequest, setSubmittingRequest] = useState<boolean>(false);
|
||||
|
||||
const maxRangeAmountMultiple = 7.8;
|
||||
const minRangeAmountMultiple = 1.6;
|
||||
const amountSafeThresholds = [1.03, 0.98];
|
||||
|
||||
useEffect(() => {
|
||||
setCurrencyCode(currencyDict[currency == 0 ? 1 : currency]);
|
||||
if (Object.keys(limits).length === 0) {
|
||||
setAppState({ loadingLimits: true });
|
||||
fetchLimits(true).then((data) => {
|
||||
updateAmountLimits(data, currency, maker.premium);
|
||||
updateCurrentPrice(data, currency, maker.premium);
|
||||
updateSatoshisLimits(data);
|
||||
});
|
||||
} else {
|
||||
updateAmountLimits(limits, currency, maker.premium);
|
||||
updateCurrentPrice(limits, currency, maker.premium);
|
||||
updateSatoshisLimits(limits);
|
||||
|
||||
fetchLimits(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const updateAmountLimits = function (limits: LimitList, currency: number, premium: number) {
|
||||
const index = currency === 0 ? 1 : currency;
|
||||
let minAmountLimit: number = limits[index].min_amount * (1 + premium / 100);
|
||||
let maxAmountLimit: number = limits[index].max_amount * (1 + premium / 100);
|
||||
|
||||
// apply thresholds to ensure good request
|
||||
minAmountLimit = minAmountLimit * amountSafeThresholds[0];
|
||||
maxAmountLimit = maxAmountLimit * amountSafeThresholds[1];
|
||||
setAmountLimits([minAmountLimit, maxAmountLimit]);
|
||||
};
|
||||
|
||||
const updateSatoshisLimits = function (limits: LimitList) {
|
||||
const minAmount: number = limits[1000].min_amount * 100000000;
|
||||
const maxAmount: number = limits[1000].max_amount * 100000000;
|
||||
setSatoshisLimits([minAmount, maxAmount]);
|
||||
};
|
||||
|
||||
const updateCurrentPrice = function (limits: LimitList, currency: number, premium: number) {
|
||||
const index = currency === 0 ? 1 : currency;
|
||||
let price = '...';
|
||||
if (maker.is_explicit && maker.amount > 0 && maker.satoshis > 0) {
|
||||
price = maker.amount / (maker.satoshis / 100000000);
|
||||
} else if (!maker.is_explicit) {
|
||||
price = limits[index].price * (1 + premium / 100);
|
||||
}
|
||||
setCurrentPrice(parseFloat(Number(price).toPrecision(5)));
|
||||
};
|
||||
|
||||
const handleCurrencyChange = function (newCurrency: number) {
|
||||
const currencyCode: string = currencyDict[newCurrency];
|
||||
setCurrencyCode(currencyCode);
|
||||
setAppState({
|
||||
currency: newCurrency,
|
||||
bookCurrencyCode: currencyCode,
|
||||
});
|
||||
updateAmountLimits(limits, newCurrency, maker.premium);
|
||||
updateCurrentPrice(limits, newCurrency, maker.premium);
|
||||
if (advancedOptions) {
|
||||
setMaker({
|
||||
...maker,
|
||||
minAmount: parseFloat(Number(limits[newCurrency].max_amount * 0.25).toPrecision(2)),
|
||||
maxAmount: parseFloat(Number(limits[newCurrency].max_amount * 0.75).toPrecision(2)),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handlePaymentMethodChange = function (paymentArray: string[]) {
|
||||
let str = '';
|
||||
const arrayLength = paymentArray.length;
|
||||
for (let i = 0; i < arrayLength; i++) {
|
||||
str += paymentArray[i].name + ' ';
|
||||
}
|
||||
const paymentMethodText = str.slice(0, -1);
|
||||
setMaker({
|
||||
...maker,
|
||||
paymentMethods: paymentArray,
|
||||
paymentMethodsText: paymentMethodText,
|
||||
badPaymentMethod: paymentMethodText.length > 50,
|
||||
});
|
||||
};
|
||||
|
||||
const handleMinAmountChange = function (e) {
|
||||
setMaker({
|
||||
...maker,
|
||||
minAmount: parseFloat(Number(e.target.value).toPrecision(e.target.value < 100 ? 2 : 3)),
|
||||
});
|
||||
};
|
||||
|
||||
const handleMaxAmountChange = function (e) {
|
||||
setMaker({
|
||||
...maker,
|
||||
maxAmount: parseFloat(Number(e.target.value).toPrecision(e.target.value < 100 ? 2 : 3)),
|
||||
});
|
||||
};
|
||||
|
||||
const handlePremiumChange = function (e: object) {
|
||||
const max = 999;
|
||||
const min = -100;
|
||||
const newPremium = Math.floor(e.target.value * Math.pow(10, 2)) / Math.pow(10, 2);
|
||||
let premium: number = newPremium;
|
||||
let badPremiumText: string = '';
|
||||
if (newPremium > 999) {
|
||||
badPremiumText = t('Must be less than {{max}}%', { max });
|
||||
premium = 999;
|
||||
} else if (newPremium <= -100) {
|
||||
badPremiumText = t('Must be more than {{min}}%', { min });
|
||||
premium = -99.99;
|
||||
}
|
||||
updateCurrentPrice(limits, currency, premium);
|
||||
updateAmountLimits(limits, currency, premium);
|
||||
setMaker({
|
||||
...maker,
|
||||
premium,
|
||||
badPremiumText,
|
||||
});
|
||||
};
|
||||
|
||||
const handleSatoshisChange = function (e: object) {
|
||||
const newSatoshis = e.target.value;
|
||||
let badSatoshisText: string = '';
|
||||
let satoshis: string = newSatoshis;
|
||||
if (newSatoshis > satoshisLimits[1]) {
|
||||
badSatoshisText = t('Must be less than {{maxSats}', { maxSats: pn(satoshisLimits[1]) });
|
||||
satoshis = satoshisLimits[1];
|
||||
}
|
||||
if (newSatoshis < satoshisLimits[0]) {
|
||||
badSatoshisText = t('Must be more than {{minSats}}', { minSats: pn(satoshisLimits[0]) });
|
||||
satoshis = satoshisLimits[0];
|
||||
}
|
||||
|
||||
setMaker({
|
||||
...maker,
|
||||
satoshis,
|
||||
badSatoshisText,
|
||||
});
|
||||
};
|
||||
|
||||
const handleClickRelative = function () {
|
||||
setMaker({
|
||||
...maker,
|
||||
isExplicit: false,
|
||||
});
|
||||
};
|
||||
|
||||
const handleClickExplicit = function () {
|
||||
if (!advancedOptions) {
|
||||
setMaker({
|
||||
...maker,
|
||||
isExplicit: true,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleCreateOrder = function () {
|
||||
if (!disableRequest) {
|
||||
setSubmittingRequest(true);
|
||||
const body = {
|
||||
type: type == 0 ? 1 : 0,
|
||||
currency: currency == 0 ? 1 : currency,
|
||||
amount: advancedOptions ? null : maker.amount,
|
||||
has_range: advancedOptions,
|
||||
min_amount: advancedOptions ? maker.minAmount : null,
|
||||
max_amount: advancedOptions ? maker.maxAmount : null,
|
||||
payment_method:
|
||||
maker.paymentMethodsText === '' ? 'not specified' : maker.paymentMethodsText,
|
||||
is_explicit: maker.isExplicit,
|
||||
premium: maker.isExplicit ? null : maker.premium == '' ? 0 : maker.premium,
|
||||
satoshis: maker.isExplicit ? maker.satoshis : null,
|
||||
public_duration: maker.publicDuration,
|
||||
escrow_duration: maker.escrowDuration,
|
||||
bond_size: maker.bondSize,
|
||||
};
|
||||
apiClient.post('/api/make/', body).then((data: object) => {
|
||||
setBadRequest(data.bad_request);
|
||||
data.id ? history.push('/order/' + data.id) : '';
|
||||
setSubmittingRequest(false);
|
||||
});
|
||||
}
|
||||
setOpenDialogs(false);
|
||||
};
|
||||
|
||||
const handleChangePublicDuration = function (date: Date) {
|
||||
const d = new Date(date);
|
||||
const hours: number = d.getHours();
|
||||
const minutes: number = d.getMinutes();
|
||||
|
||||
const total_secs: number = hours * 60 * 60 + minutes * 60;
|
||||
|
||||
setMaker({
|
||||
...maker,
|
||||
publicExpiryTime: date,
|
||||
publicDuration: total_secs,
|
||||
});
|
||||
};
|
||||
|
||||
const handleChangeEscrowDuration = function (date: Date) {
|
||||
const d = new Date(date);
|
||||
const hours: number = d.getHours();
|
||||
const minutes: number = d.getMinutes();
|
||||
|
||||
const total_secs: number = hours * 60 * 60 + minutes * 60;
|
||||
|
||||
setMaker({
|
||||
...maker,
|
||||
escrowExpiryTime: date,
|
||||
escrowDuration: total_secs,
|
||||
});
|
||||
};
|
||||
|
||||
const handleClickAdvanced = function () {
|
||||
if (advancedOptions) {
|
||||
handleClickRelative();
|
||||
} else {
|
||||
resetRange();
|
||||
}
|
||||
|
||||
setAdvancedOptions(!advancedOptions);
|
||||
};
|
||||
|
||||
const minAmountError = function () {
|
||||
return (
|
||||
maker.minAmount < amountLimits[0] * 0.99 ||
|
||||
maker.maxAmount < maker.minAmount ||
|
||||
maker.minAmount < maker.maxAmount / (maxRangeAmountMultiple + 0.15) ||
|
||||
maker.minAmount * (minRangeAmountMultiple - 0.1) > maker.maxAmount
|
||||
);
|
||||
};
|
||||
|
||||
const maxAmountError = function () {
|
||||
return (
|
||||
maker.maxAmount > amountLimits[1] * 1.01 ||
|
||||
maker.maxAmount < maker.minAmount ||
|
||||
maker.minAmount < maker.maxAmount / (maxRangeAmountMultiple + 0.15) ||
|
||||
maker.minAmount * (minRangeAmountMultiple - 0.1) > maker.maxAmount
|
||||
);
|
||||
};
|
||||
|
||||
const resetRange = function () {
|
||||
const index = currency === 0 ? 1 : currency;
|
||||
const minAmount = maker.amount
|
||||
? parseFloat((maker.amount / 2).toPrecision(2))
|
||||
: parseFloat(Number(limits[index].max_amount * 0.25).toPrecision(2));
|
||||
const maxAmount = maker.amount
|
||||
? parseFloat(maker.amount)
|
||||
: parseFloat(Number(limits[index].max_amount * 0.75).toPrecision(2));
|
||||
|
||||
setMaker({
|
||||
...maker,
|
||||
minAmount,
|
||||
maxAmount,
|
||||
});
|
||||
};
|
||||
|
||||
const handleRangeAmountChange = function (e: any, newValue, activeThumb: number) {
|
||||
let minAmount = e.target.value[0];
|
||||
let maxAmount = e.target.value[1];
|
||||
|
||||
minAmount = Math.min(
|
||||
(amountLimits[1] * amountSafeThresholds[1]) / minRangeAmountMultiple,
|
||||
minAmount,
|
||||
);
|
||||
maxAmount = Math.max(
|
||||
minRangeAmountMultiple * amountLimits[0] * amountSafeThresholds[0],
|
||||
maxAmount,
|
||||
);
|
||||
|
||||
if (minAmount > maxAmount / minRangeAmountMultiple) {
|
||||
if (activeThumb === 0) {
|
||||
maxAmount = minRangeAmountMultiple * minAmount;
|
||||
} else {
|
||||
minAmount = maxAmount / minRangeAmountMultiple;
|
||||
}
|
||||
} else if (minAmount < maxAmount / maxRangeAmountMultiple) {
|
||||
if (activeThumb === 0) {
|
||||
maxAmount = maxRangeAmountMultiple * minAmount;
|
||||
} else {
|
||||
minAmount = maxAmount / maxRangeAmountMultiple;
|
||||
}
|
||||
}
|
||||
|
||||
setMaker({
|
||||
...maker,
|
||||
minAmount: parseFloat(Number(minAmount).toPrecision(minAmount < 100 ? 2 : 3)),
|
||||
maxAmount: parseFloat(Number(maxAmount).toPrecision(maxAmount < 100 ? 2 : 3)),
|
||||
});
|
||||
};
|
||||
|
||||
const disableSubmit = function () {
|
||||
return (
|
||||
type == null ||
|
||||
(maker.amount != '' &&
|
||||
!advancedOptions &&
|
||||
(maker.amount < amountLimits[0] || maker.amount > amountLimits[1])) ||
|
||||
(maker.amount == null && (!advancedOptions || loadingLimits)) ||
|
||||
(advancedOptions && (minAmountError() || maxAmountError())) ||
|
||||
(maker.amount <= 0 && !advancedOptions) ||
|
||||
(maker.isExplicit && (maker.badSatoshisText != '' || maker.satoshis == '')) ||
|
||||
(!maker.isExplicit && maker.badPremiumText != '')
|
||||
);
|
||||
};
|
||||
|
||||
const clearMaker = function () {
|
||||
setAppState({ type: null });
|
||||
setMaker(defaultMaker);
|
||||
};
|
||||
|
||||
const SummaryText = function () {
|
||||
return (
|
||||
<Typography
|
||||
component='h2'
|
||||
variant='subtitle2'
|
||||
align='center'
|
||||
color={disableSubmit() ? 'text.secondary' : 'text.primary'}
|
||||
>
|
||||
{type == null ? t('Order for ') : type == 1 ? t('Buy order for ') : t('Sell order for ')}
|
||||
{advancedOptions && maker.minAmount != ''
|
||||
? pn(maker.minAmount) + '-' + pn(maker.maxAmount)
|
||||
: pn(maker.amount)}
|
||||
{' ' + currencyCode}
|
||||
{maker.isExplicit
|
||||
? t(' of {{satoshis}} Satoshis', { satoshis: pn(maker.satoshis) })
|
||||
: maker.premium == 0
|
||||
? t(' at market price')
|
||||
: maker.premium > 0
|
||||
? t(' at a {{premium}}% premium', { premium: maker.premium })
|
||||
: t(' at a {{discount}}% discount', { discount: -maker.premium })}
|
||||
</Typography>
|
||||
);
|
||||
};
|
||||
|
||||
const ConfirmationDialogs = function () {
|
||||
return systemClient.getCookie('robot_token') ? (
|
||||
<StoreTokenDialog
|
||||
open={openDialogs}
|
||||
onClose={() => setOpenDialogs(false)}
|
||||
onClickCopy={() => systemClient.copyToClipboard(systemClient.getCookie('robot_token'))}
|
||||
copyIconColor={'primary'}
|
||||
onClickBack={() => setOpenDialogs(false)}
|
||||
onClickDone={handleCreateOrder}
|
||||
/>
|
||||
) : (
|
||||
<NoRobotDialog open={openDialogs} onClose={() => setOpenDialogs(false)} />
|
||||
);
|
||||
};
|
||||
return (
|
||||
<Box>
|
||||
<ConfirmationDialogs />
|
||||
<Collapse in={loadingLimits}>
|
||||
<div style={{ display: loadingLimits ? '' : 'none' }}>
|
||||
<LinearProgress />
|
||||
</div>
|
||||
</Collapse>
|
||||
<Collapse in={!(loadingLimits || collapseAll)}>
|
||||
<Grid container justifyContent='space-between' spacing={0} sx={{ maxHeight: '1em' }}>
|
||||
<Grid item>
|
||||
<IconButton
|
||||
sx={{
|
||||
width: '1.3em',
|
||||
height: '1.3em',
|
||||
position: 'relative',
|
||||
bottom: '0.2em',
|
||||
right: '0.2em',
|
||||
color: 'text.secondary',
|
||||
}}
|
||||
onClick={clearMaker}
|
||||
>
|
||||
<Tooltip
|
||||
placement='top'
|
||||
enterTouchDelay={500}
|
||||
enterDelay={700}
|
||||
enterNextDelay={2000}
|
||||
title={t('Clear form')}
|
||||
>
|
||||
<DeleteSweep sx={{ width: '1em', height: '1em' }} />
|
||||
</Tooltip>
|
||||
</IconButton>
|
||||
</Grid>
|
||||
<Grid item>
|
||||
<Tooltip enterTouchDelay={0} placement='top' title={t('Enable advanced options')}>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
width: '4em',
|
||||
height: '1.1em',
|
||||
}}
|
||||
>
|
||||
<Switch
|
||||
size='small'
|
||||
disabled={loadingLimits}
|
||||
checked={advancedOptions}
|
||||
onChange={handleClickAdvanced}
|
||||
/>
|
||||
<SelfImprovement sx={{ color: 'text.secondary' }} />
|
||||
</div>
|
||||
</Tooltip>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Collapse>
|
||||
|
||||
<Collapse in={!collapseAll}>
|
||||
<Grid container spacing={1} justifyContent='center' alignItems='center'>
|
||||
<Grid item>
|
||||
<FormControl component='fieldset'>
|
||||
<FormHelperText sx={{ textAlign: 'center' }}>
|
||||
{t('Buy or Sell Bitcoin?')}
|
||||
</FormHelperText>
|
||||
<div style={{ textAlign: 'center' }}>
|
||||
<ButtonGroup>
|
||||
<Button
|
||||
size={advancedOptions ? 'small' : 'large'}
|
||||
variant='contained'
|
||||
onClick={() =>
|
||||
setAppState({
|
||||
type: 1,
|
||||
})
|
||||
}
|
||||
disableElevation={type == 1}
|
||||
sx={{
|
||||
backgroundColor: type == 1 ? 'primary.main' : 'background.paper',
|
||||
color: type == 1 ? 'background.paper' : 'text.secondary',
|
||||
':hover': {
|
||||
color: 'background.paper',
|
||||
},
|
||||
}}
|
||||
>
|
||||
{t('Buy')}
|
||||
</Button>
|
||||
<Button
|
||||
size={advancedOptions ? 'small' : 'large'}
|
||||
variant='contained'
|
||||
onClick={() =>
|
||||
setAppState({
|
||||
type: 0,
|
||||
})
|
||||
}
|
||||
disableElevation={type == 0}
|
||||
color='secondary'
|
||||
sx={{
|
||||
backgroundColor: type == 0 ? 'secondary.main' : 'background.paper',
|
||||
color: type == 0 ? 'background.secondary' : 'text.secondary',
|
||||
':hover': {
|
||||
color: 'background.paper',
|
||||
},
|
||||
}}
|
||||
>
|
||||
{t('Sell')}
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
</div>
|
||||
</FormControl>
|
||||
</Grid>
|
||||
|
||||
<Grid item>
|
||||
<Collapse in={advancedOptions}>
|
||||
<AmountRange
|
||||
minAmount={maker.minAmount}
|
||||
handleRangeAmountChange={handleRangeAmountChange}
|
||||
currency={currency}
|
||||
currencyCode={currencyCode}
|
||||
handleCurrencyChange={handleCurrencyChange}
|
||||
amountLimits={amountLimits}
|
||||
maxAmount={maker.maxAmount}
|
||||
minAmountError={minAmountError()}
|
||||
maxAmountError={maxAmountError()}
|
||||
handleMinAmountChange={handleMinAmountChange}
|
||||
handleMaxAmountChange={handleMaxAmountChange}
|
||||
/>
|
||||
</Collapse>
|
||||
<Collapse in={!advancedOptions}>
|
||||
<Grid item>
|
||||
<Grid container alignItems='stretch' style={{ display: 'flex' }}>
|
||||
<Grid item xs={6}>
|
||||
<Tooltip
|
||||
placement='top'
|
||||
enterTouchDelay={500}
|
||||
enterDelay={700}
|
||||
enterNextDelay={2000}
|
||||
title={t('Amount of fiat to exchange for bitcoin')}
|
||||
>
|
||||
<TextField
|
||||
fullWidth
|
||||
disabled={advancedOptions}
|
||||
variant={advancedOptions ? 'filled' : 'outlined'}
|
||||
error={
|
||||
maker.amount != '' &&
|
||||
(maker.amount < amountLimits[0] || maker.amount > amountLimits[1])
|
||||
}
|
||||
helperText={
|
||||
maker.amount < amountLimits[0] && maker.amount != ''
|
||||
? t('Must be more than {{minAmount}}', {
|
||||
minAmount: pn(parseFloat(amountLimits[0].toPrecision(2))),
|
||||
})
|
||||
: maker.amount > amountLimits[1] && maker.amount != ''
|
||||
? t('Must be less than {{maxAmount}}', {
|
||||
maxAmount: pn(parseFloat(amountLimits[1].toPrecision(2))),
|
||||
})
|
||||
: null
|
||||
}
|
||||
label={t('Amount')}
|
||||
required={true}
|
||||
value={maker.amount}
|
||||
type='number'
|
||||
inputProps={{
|
||||
min: 0,
|
||||
style: {
|
||||
textAlign: 'center',
|
||||
backgroundColor: theme.palette.background.paper,
|
||||
borderRadius: '4px',
|
||||
},
|
||||
}}
|
||||
onChange={(e) => setMaker({ ...maker, amount: e.target.value })}
|
||||
/>
|
||||
</Tooltip>
|
||||
</Grid>
|
||||
|
||||
<Grid item xs={6}>
|
||||
<Select
|
||||
fullWidth
|
||||
sx={{ backgroundColor: theme.palette.background.paper, borderRadius: '4px' }}
|
||||
required={true}
|
||||
inputProps={{
|
||||
style: { textAlign: 'center' },
|
||||
}}
|
||||
value={currency == 0 ? 1 : currency}
|
||||
onChange={(e) => handleCurrencyChange(e.target.value)}
|
||||
>
|
||||
{Object.entries(currencyDict).map(([key, value]) => (
|
||||
<MenuItem key={key} value={parseInt(key)}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', flexWrap: 'wrap' }}>
|
||||
<FlagWithProps code={value} />
|
||||
{' ' + value}
|
||||
</div>
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Collapse>
|
||||
</Grid>
|
||||
|
||||
<Grid item xs={12}>
|
||||
<AutocompletePayments
|
||||
onAutocompleteChange={handlePaymentMethodChange}
|
||||
// listBoxProps={{ sx: { width: '15.3em', maxHeight: '20em' } }}
|
||||
optionsType={currency == 1000 ? 'swap' : 'fiat'}
|
||||
error={maker.badPaymentMethod}
|
||||
helperText={maker.badPaymentMethod ? t('Must be shorter than 65 characters') : ''}
|
||||
label={currency == 1000 ? t('Swap Destination(s)') : t('Fiat Payment Method(s)')}
|
||||
tooltipTitle={t(
|
||||
'Enter your preferred fiat payment methods. Fast methods are highly recommended.',
|
||||
)}
|
||||
listHeaderText={t('You can add new methods')}
|
||||
addNewButtonText={t('Add New')}
|
||||
asFilter={false}
|
||||
value={maker.paymentMethods}
|
||||
/>
|
||||
</Grid>
|
||||
|
||||
{!advancedOptions && pricingMethods ? (
|
||||
<Grid item xs={12}>
|
||||
<Box
|
||||
sx={{
|
||||
padding: '0.5em',
|
||||
backgroundColor: 'background.paper',
|
||||
border: '1px solid',
|
||||
borderRadius: '4px',
|
||||
borderColor: theme.palette.mode === 'dark' ? '#434343' : '#c4c4c4',
|
||||
'&:hover': {
|
||||
borderColor: theme.palette.mode === 'dark' ? '#ffffff' : '#2f2f2f',
|
||||
},
|
||||
}}
|
||||
>
|
||||
<FormControl component='fieldset'>
|
||||
<FormHelperText sx={{ textAlign: 'center', position: 'relative', top: '0.2em' }}>
|
||||
{t('Choose a Pricing Method')}
|
||||
</FormHelperText>
|
||||
<RadioGroup row defaultValue='relative'>
|
||||
<Tooltip
|
||||
placement='top'
|
||||
enterTouchDelay={0}
|
||||
enterDelay={1000}
|
||||
enterNextDelay={2000}
|
||||
title={t('Let the price move with the market')}
|
||||
>
|
||||
<FormControlLabel
|
||||
value='relative'
|
||||
control={<Radio color='primary' />}
|
||||
label={t('Relative')}
|
||||
labelPlacement='end'
|
||||
onClick={handleClickRelative}
|
||||
/>
|
||||
</Tooltip>
|
||||
<Tooltip
|
||||
placement='top'
|
||||
enterTouchDelay={0}
|
||||
enterDelay={1000}
|
||||
enterNextDelay={2000}
|
||||
title={t('Set a fix amount of satoshis')}
|
||||
>
|
||||
<FormControlLabel
|
||||
disabled={advancedOptions}
|
||||
value='explicit'
|
||||
control={<Radio color='secondary' />}
|
||||
label={t('Exact')}
|
||||
labelPlacement='end'
|
||||
onClick={handleClickExplicit}
|
||||
/>
|
||||
</Tooltip>
|
||||
</RadioGroup>
|
||||
</FormControl>
|
||||
</Box>
|
||||
</Grid>
|
||||
) : null}
|
||||
<Grid item xs={12}>
|
||||
<div style={{ display: maker.isExplicit ? '' : 'none' }}>
|
||||
<TextField
|
||||
fullWidth
|
||||
label={t('Satoshis')}
|
||||
error={maker.badSatoshisText != ''}
|
||||
helperText={maker.badSatoshisText === '' ? null : maker.badSatoshisText}
|
||||
type='number'
|
||||
required={true}
|
||||
value={maker.satoshis}
|
||||
inputProps={{
|
||||
min: satoshisLimits[0],
|
||||
max: satoshisLimits[1],
|
||||
style: {
|
||||
textAlign: 'center',
|
||||
backgroundColor: theme.palette.background.paper,
|
||||
borderRadius: '4px',
|
||||
},
|
||||
}}
|
||||
onChange={handleSatoshisChange}
|
||||
/>
|
||||
</div>
|
||||
<div style={{ display: maker.isExplicit ? 'none' : '' }}>
|
||||
<TextField
|
||||
fullWidth
|
||||
error={maker.badPremiumText != ''}
|
||||
helperText={maker.badPremiumText === '' ? null : maker.badPremiumText}
|
||||
label={t('Premium over Market (%)')}
|
||||
type='number'
|
||||
value={maker.premium}
|
||||
inputProps={{
|
||||
min: -100,
|
||||
max: 999,
|
||||
style: {
|
||||
textAlign: 'center',
|
||||
backgroundColor: theme.palette.background.paper,
|
||||
borderRadius: '4px',
|
||||
},
|
||||
}}
|
||||
onChange={handlePremiumChange}
|
||||
/>
|
||||
</div>
|
||||
</Grid>
|
||||
<Grid item>
|
||||
<Collapse in={advancedOptions}>
|
||||
<Grid container spacing={1}>
|
||||
<Grid item xs={12}>
|
||||
<LocalizationProvider dateAdapter={DateFnsUtils}>
|
||||
<TimePicker
|
||||
ampm={false}
|
||||
openTo='hours'
|
||||
views={['hours', 'minutes']}
|
||||
inputFormat='HH:mm'
|
||||
mask='__:__'
|
||||
components={{
|
||||
OpenPickerIcon: HourglassTop,
|
||||
}}
|
||||
InputProps={{
|
||||
endAdornment: (
|
||||
<InputAdornment position='end'>
|
||||
<HourglassTop />
|
||||
</InputAdornment>
|
||||
),
|
||||
style: {
|
||||
backgroundColor: theme.palette.background.paper,
|
||||
borderRadius: '4px',
|
||||
},
|
||||
}}
|
||||
renderInput={(props) => <TextField {...props} />}
|
||||
label={t('Public Duration (HH:mm)')}
|
||||
value={maker.publicExpiryTime}
|
||||
onChange={handleChangePublicDuration}
|
||||
minTime={new Date(0, 0, 0, 0, 10)}
|
||||
maxTime={new Date(0, 0, 0, 23, 59)}
|
||||
/>
|
||||
</LocalizationProvider>
|
||||
</Grid>
|
||||
|
||||
<Grid item xs={12}>
|
||||
<LocalizationProvider dateAdapter={DateFnsUtils}>
|
||||
<TimePicker
|
||||
ampm={false}
|
||||
openTo='hours'
|
||||
views={['hours', 'minutes']}
|
||||
inputFormat='HH:mm'
|
||||
mask='__:__'
|
||||
components={{
|
||||
OpenPickerIcon: HourglassTop,
|
||||
}}
|
||||
InputProps={{
|
||||
endAdornment: (
|
||||
<InputAdornment position='end'>
|
||||
<HourglassTop />
|
||||
</InputAdornment>
|
||||
),
|
||||
style: {
|
||||
backgroundColor: theme.palette.background.paper,
|
||||
borderRadius: '4px',
|
||||
},
|
||||
}}
|
||||
renderInput={(props) => <TextField {...props} />}
|
||||
label={t('Escrow/Invoice Timer (HH:mm)')}
|
||||
value={maker.escrowExpiryTime}
|
||||
onChange={handleChangeEscrowDuration}
|
||||
minTime={new Date(0, 0, 0, 1, 0)}
|
||||
maxTime={new Date(0, 0, 0, 8, 0)}
|
||||
/>
|
||||
</LocalizationProvider>
|
||||
</Grid>
|
||||
|
||||
<Grid item xs={12}>
|
||||
<Box
|
||||
sx={{
|
||||
padding: '0.5em',
|
||||
backgroundColor: 'background.paper',
|
||||
border: '1px solid',
|
||||
borderRadius: '4px',
|
||||
borderColor: theme.palette.mode === 'dark' ? '#434343' : '#c4c4c4',
|
||||
'&:hover': {
|
||||
borderColor: theme.palette.mode === 'dark' ? '#ffffff' : '#2f2f2f',
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Grid container direction='column' alignItems='center' spacing={0.5}>
|
||||
<Grid
|
||||
item
|
||||
sx={{
|
||||
width: '100%',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
flexWrap: 'wrap',
|
||||
}}
|
||||
>
|
||||
<Tooltip
|
||||
enterDelay={800}
|
||||
enterTouchDelay={0}
|
||||
placement='top'
|
||||
title={t(
|
||||
'Set the skin-in-the-game, increase for higher safety assurance',
|
||||
)}
|
||||
>
|
||||
<Typography
|
||||
variant='caption'
|
||||
sx={{
|
||||
color: 'text.secondary',
|
||||
display: 'flex',
|
||||
flexWrap: 'wrap',
|
||||
}}
|
||||
>
|
||||
{t('Fidelity Bond Size')}{' '}
|
||||
<Lock sx={{ height: '0.8em', width: '0.8em' }} />
|
||||
</Typography>
|
||||
</Tooltip>
|
||||
</Grid>
|
||||
<Grid item sx={{ width: 'calc(100% - 2em)' }}>
|
||||
<Slider
|
||||
sx={{ width: '100%', align: 'center' }}
|
||||
aria-label='Bond Size (%)'
|
||||
defaultValue={3}
|
||||
value={maker.bondSize}
|
||||
valueLabelDisplay='auto'
|
||||
valueLabelFormat={(x: string) => x + '%'}
|
||||
step={0.25}
|
||||
marks={[
|
||||
{ value: 2, label: '2%' },
|
||||
{ value: 5, label: '5%' },
|
||||
{ value: 10, label: '10%' },
|
||||
{ value: 15, label: '15%' },
|
||||
]}
|
||||
min={2}
|
||||
max={15}
|
||||
onChange={(e) => setMaker({ ...maker, bondSize: e.target.value })}
|
||||
/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Box>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Collapse>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Collapse>
|
||||
|
||||
<Grid container direction='column' alignItems='center'>
|
||||
<Grid item>
|
||||
<SummaryText />
|
||||
</Grid>
|
||||
|
||||
<Grid item>
|
||||
<Grid container direction='row' justifyItems='center' alignItems='center' spacing={1}>
|
||||
<Grid item>
|
||||
{/* conditions to disable the make button */}
|
||||
{disableSubmit() ? (
|
||||
<Tooltip enterTouchDelay={0} title={t('You must fill the form correctly')}>
|
||||
<div>
|
||||
<Button disabled color='primary' variant='contained'>
|
||||
{t(submitButtonLabel)}
|
||||
</Button>
|
||||
</div>
|
||||
</Tooltip>
|
||||
) : (
|
||||
<LoadingButton
|
||||
loading={submittingRequest}
|
||||
color='primary'
|
||||
variant='contained'
|
||||
onClick={() => {
|
||||
disableRequest ? onSubmit() : setOpenDialogs(true);
|
||||
}}
|
||||
>
|
||||
{t(submitButtonLabel)}
|
||||
</LoadingButton>
|
||||
)}
|
||||
</Grid>
|
||||
{collapseAll ? (
|
||||
<Grid item>
|
||||
<Collapse in={collapseAll} orientation='vertical'>
|
||||
<IconButton onClick={onReset}>
|
||||
<Tooltip
|
||||
placement='top'
|
||||
enterTouchDelay={500}
|
||||
enterDelay={700}
|
||||
enterNextDelay={2000}
|
||||
title={t('Edit order')}
|
||||
>
|
||||
<Edit sx={{ width: '1.5em', height: '1.5em' }} />
|
||||
</Tooltip>
|
||||
</IconButton>
|
||||
</Collapse>
|
||||
</Grid>
|
||||
) : null}
|
||||
</Grid>
|
||||
</Grid>
|
||||
|
||||
<Grid item>
|
||||
<Typography align='center' component='h2' variant='subtitle2' color='secondary'>
|
||||
{badRequest}
|
||||
</Typography>
|
||||
</Grid>
|
||||
|
||||
<Collapse in={!loadingLimits}>
|
||||
<Tooltip
|
||||
placement='top'
|
||||
enterTouchDelay={0}
|
||||
enterDelay={1000}
|
||||
enterNextDelay={2000}
|
||||
title={
|
||||
maker.isExplicit
|
||||
? t('Your order fixed exchange rate')
|
||||
: t("Your order's current exchange rate. Rate will move with the market.")
|
||||
}
|
||||
>
|
||||
<Typography align='center' variant='caption' color='text.secondary'>
|
||||
{(maker.isExplicit ? t('Order rate:') : t('Order current rate:')) +
|
||||
` ${pn(currentPrice)} ${currencyCode}/BTC`}
|
||||
</Typography>
|
||||
</Tooltip>
|
||||
</Collapse>
|
||||
</Grid>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default MakerForm;
|
109
frontend/src/components/MakerPage/MakerPage.tsx
Normal file
109
frontend/src/components/MakerPage/MakerPage.tsx
Normal file
@ -0,0 +1,109 @@
|
||||
import React, { useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Button, Grid, Paper, Collapse, Typography } from '@mui/material';
|
||||
|
||||
import { LimitList, Maker, Order, defaultMaker } from '../../models';
|
||||
import MakerForm from './MakerForm';
|
||||
import BookTable from '../BookPage/BookTable';
|
||||
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import filterOrders from '../../utils/filterOrders';
|
||||
|
||||
interface MakerPageProps {
|
||||
limits: LimitList;
|
||||
fetchLimits: () => void;
|
||||
orders: Order[];
|
||||
loadingLimits: boolean;
|
||||
type: number;
|
||||
windowHeight: number;
|
||||
windowWidth: number;
|
||||
currency: number;
|
||||
setAppState: (state: object) => void;
|
||||
}
|
||||
|
||||
const MakerPage = ({
|
||||
limits,
|
||||
fetchLimits,
|
||||
orders,
|
||||
loadingLimits,
|
||||
currency,
|
||||
type,
|
||||
setAppState,
|
||||
windowHeight,
|
||||
windowWidth,
|
||||
}: MakerPageProps): JSX.Element => {
|
||||
const { t } = useTranslation();
|
||||
const history = useHistory();
|
||||
|
||||
const [maker, setMaker] = useState<Maker>(defaultMaker);
|
||||
const maxHeight = windowHeight ? windowHeight * 0.85 - 7 : 1000;
|
||||
const [showMatches, setShowMatches] = useState<boolean>(false);
|
||||
|
||||
const matches = filterOrders({
|
||||
orders,
|
||||
baseFilter: { currency: currency == 0 ? 1 : currency, type },
|
||||
paymentMethods: maker.paymentMethods,
|
||||
amountFilter: {
|
||||
amount: maker.amount,
|
||||
minAmount: maker.minAmount,
|
||||
maxAmount: maker.maxAmount,
|
||||
threshold: 0.7,
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<Grid container direction='column' alignItems='center' spacing={1}>
|
||||
<Grid item>
|
||||
<Collapse in={matches.length > 0 && showMatches}>
|
||||
<Grid container direction='column' alignItems='center' spacing={1}>
|
||||
<Grid item>
|
||||
<Typography variant='h5'>{t('Existing orders match yours!')}</Typography>
|
||||
</Grid>
|
||||
<Grid item>
|
||||
<BookTable
|
||||
orders={matches.slice(0, matches.length > 4 ? 4 : matches.length)}
|
||||
type={type}
|
||||
currency={currency}
|
||||
maxWidth={Math.min(windowWidth, 60)} // EM units
|
||||
maxHeight={Math.min(matches.length * 3.25 + 3.575, 16.575)} // EM units
|
||||
defaultFullscreen={false}
|
||||
showControls={false}
|
||||
showFooter={false}
|
||||
/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Collapse>
|
||||
</Grid>
|
||||
<Grid item>
|
||||
<Paper
|
||||
elevation={12}
|
||||
style={{ padding: 8, width: '17.25em', maxHeight: `${maxHeight}em`, overflow: 'auto' }}
|
||||
>
|
||||
<MakerForm
|
||||
limits={limits}
|
||||
fetchLimits={fetchLimits}
|
||||
loadingLimits={loadingLimits}
|
||||
pricingMethods={false}
|
||||
setAppState={setAppState}
|
||||
maker={maker}
|
||||
setMaker={setMaker}
|
||||
type={type}
|
||||
currency={currency}
|
||||
disableRequest={matches.length > 0 && !showMatches}
|
||||
collapseAll={showMatches}
|
||||
onSubmit={() => setShowMatches(matches.length > 0)}
|
||||
onReset={() => setShowMatches(false)}
|
||||
submitButtonLabel={matches.length > 0 && !showMatches ? 'Submit' : 'Create order'}
|
||||
/>
|
||||
</Paper>
|
||||
</Grid>
|
||||
<Grid item>
|
||||
<Button color='secondary' variant='contained' onClick={() => history.push('/')}>
|
||||
{t('Back')}
|
||||
</Button>
|
||||
</Grid>
|
||||
</Grid>
|
||||
);
|
||||
};
|
||||
|
||||
export default MakerPage;
|
5
frontend/src/components/MakerPage/index.ts
Normal file
5
frontend/src/components/MakerPage/index.ts
Normal file
@ -0,0 +1,5 @@
|
||||
import MakerPage from './MakerPage';
|
||||
export default MakerPage;
|
||||
|
||||
export { default as MakerForm } from './MakerForm';
|
||||
export { default as AutocompletePayments } from './AutocompletePayments';
|
@ -59,7 +59,7 @@ const TorConnection = (): JSX.Element => {
|
||||
});
|
||||
}, []);
|
||||
|
||||
if (!window?.NativeRobosats) {
|
||||
if (window?.NativeRobosats == null) {
|
||||
return <></>;
|
||||
}
|
||||
|
||||
|
@ -425,20 +425,23 @@ export default class PaymentIcon extends Component {
|
||||
}
|
||||
|
||||
render() {
|
||||
if (this.props.icon === 'custom') {
|
||||
if (this.props.icon === undefined) {
|
||||
return null;
|
||||
} else if (this.props.icon === 'custom') {
|
||||
return (
|
||||
<DashboardCustomizeIcon
|
||||
sx={{ ...this.props, filter: 'drop-shadow(1.5px 1.5px 1.5px rgba(0, 0, 0, 0.2))' }}
|
||||
color='primary'
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<img
|
||||
{...this.props}
|
||||
src={icons[this.props.icon].image}
|
||||
style={{ borderRadius: '23%', filter: 'drop-shadow(1.5px 1.5px 2px rgba(0, 0, 0, 0.2))' }}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<img
|
||||
{...this.props}
|
||||
src={icons[this.props.icon].image}
|
||||
style={{ borderRadius: '23%', filter: 'drop-shadow(1.5px 1.5px 2px rgba(0, 0, 0, 0.2))' }}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
39
frontend/src/models/Maker.model.ts
Normal file
39
frontend/src/models/Maker.model.ts
Normal file
@ -0,0 +1,39 @@
|
||||
export interface Maker {
|
||||
isExplicit: boolean;
|
||||
amount: string;
|
||||
paymentMethods: string[];
|
||||
paymentMethodsText: string;
|
||||
badPaymentMethod: boolean;
|
||||
premium: number | string;
|
||||
satoshis: string;
|
||||
publicExpiryTime: Date;
|
||||
publicDuration: number;
|
||||
escrowExpiryTime: Date;
|
||||
escrowDuration: number;
|
||||
bondSize: number;
|
||||
minAmount: string;
|
||||
maxAmount: string;
|
||||
badSatoshisText: string;
|
||||
badPremiumText: string;
|
||||
}
|
||||
|
||||
export const defaultMaker: Maker = {
|
||||
isExplicit: false,
|
||||
amount: '',
|
||||
paymentMethods: [],
|
||||
paymentMethodsText: 'not specified',
|
||||
badPaymentMethod: false,
|
||||
premium: '',
|
||||
satoshis: '',
|
||||
publicExpiryTime: new Date(0, 0, 0, 23, 59),
|
||||
publicDuration: 86340,
|
||||
escrowExpiryTime: new Date(0, 0, 0, 3, 0),
|
||||
escrowDuration: 10800,
|
||||
bondSize: 3,
|
||||
minAmount: '',
|
||||
maxAmount: '',
|
||||
badPremiumText: '',
|
||||
badSatoshisText: '',
|
||||
};
|
||||
|
||||
export default Maker;
|
5
frontend/src/models/index.ts
Normal file
5
frontend/src/models/index.ts
Normal file
@ -0,0 +1,5 @@
|
||||
export { default as LimitList } from './Limit.model';
|
||||
export { Limit } from './Limit.model';
|
||||
export { default as Maker } from './Maker.model';
|
||||
export { defaultMaker as defaultMaker } from './Maker.model';
|
||||
export { default as Order } from './Order.model';
|
@ -79,7 +79,7 @@ class ApiNativeClient implements ApiClient {
|
||||
if (this.assetsCache[path]) {
|
||||
return this.assetsCache[path];
|
||||
} else if (path in this.assetsPromises) {
|
||||
return this.assetsPromises[path];
|
||||
return await this.assetsPromises[path];
|
||||
}
|
||||
|
||||
this.assetsPromises[path] = new Promise<string>(async (resolve, reject) => {
|
||||
@ -95,7 +95,7 @@ class ApiNativeClient implements ApiClient {
|
||||
resolve(this.assetsCache[path]);
|
||||
});
|
||||
|
||||
return this.assetsPromises[path];
|
||||
return await this.assetsPromises[path];
|
||||
};
|
||||
}
|
||||
|
||||
|
67
frontend/src/utils/filterOrders.ts
Normal file
67
frontend/src/utils/filterOrders.ts
Normal file
@ -0,0 +1,67 @@
|
||||
import Order from '../models/Order.model';
|
||||
|
||||
interface BaseFilter {
|
||||
currency: number;
|
||||
type: number | null;
|
||||
}
|
||||
|
||||
interface AmountFilter {
|
||||
amount: string;
|
||||
minAmount: string;
|
||||
maxAmount: string;
|
||||
threshold: number;
|
||||
}
|
||||
|
||||
interface FilterOrders {
|
||||
orders: Order[];
|
||||
baseFilter: BaseFilter;
|
||||
amountFilter?: AmountFilter | null;
|
||||
paymentMethods?: string[];
|
||||
}
|
||||
|
||||
const filterByPayment = function (order: Order, paymentMethods: string[]) {
|
||||
if (paymentMethods.length === 0) {
|
||||
return true;
|
||||
} else {
|
||||
let result = false;
|
||||
paymentMethods.forEach((method) => {
|
||||
result = result || order.payment_method.includes(method.name);
|
||||
});
|
||||
return result;
|
||||
}
|
||||
};
|
||||
|
||||
const filterByAmount = function (order: Order, filter: AmountFilter) {
|
||||
const filterMaxAmount = filter.amount != '' ? filter.amount : filter.maxAmount;
|
||||
const filterMinAmount = filter.amount != '' ? filter.amount : filter.minAmount;
|
||||
const orderMinAmount =
|
||||
order.amount === '' || order.amount === null ? order.min_amount : order.amount;
|
||||
const orderMaxAmount =
|
||||
order.amount === '' || order.amount === null ? order.max_amount : order.amount;
|
||||
|
||||
return (
|
||||
orderMaxAmount < filterMaxAmount * (1 + filter.threshold) &&
|
||||
orderMinAmount > filterMinAmount * (1 - filter.threshold)
|
||||
);
|
||||
};
|
||||
|
||||
const filterOrders = function ({
|
||||
orders,
|
||||
baseFilter,
|
||||
paymentMethods = [],
|
||||
amountFilter = null,
|
||||
}: FilterOrders) {
|
||||
const filteredOrders = orders.filter((order) => {
|
||||
const typeChecks = order.type == baseFilter.type || baseFilter.type == null;
|
||||
const currencyChecks = order.currency == baseFilter.currency || baseFilter.currency == 0;
|
||||
const paymentMethodChecks =
|
||||
paymentMethods.length > 0 ? filterByPayment(order, paymentMethods) : true;
|
||||
const amountChecks = amountFilter != null ? filterByAmount(order, amountFilter) : true;
|
||||
|
||||
return typeChecks && currencyChecks && paymentMethodChecks && amountChecks;
|
||||
});
|
||||
|
||||
return filteredOrders;
|
||||
};
|
||||
|
||||
export default filterOrders;
|
@ -94,8 +94,7 @@
|
||||
"Buyer": "Compra",
|
||||
"I want to": "Vull",
|
||||
"Select Order Type": "Selecciona tipus d'ordre",
|
||||
"ANY_type": "TOT",
|
||||
"ANY_currency": "TOT",
|
||||
"ANY": "TOT",
|
||||
"BUY": "COMPRAR",
|
||||
"SELL": "VENDRE",
|
||||
"and receive": "i rebre",
|
||||
|
@ -92,8 +92,7 @@
|
||||
"Buyer": "Kupující",
|
||||
"I want to": "Já chci",
|
||||
"Select Order Type": "Vybrat druh nabídky",
|
||||
"ANY_type": "VŠE",
|
||||
"ANY_currency": "VŠE",
|
||||
"ANY": "VŠE",
|
||||
"BUY": "KOUPĚ",
|
||||
"SELL": "PRODEJ",
|
||||
"and receive": "získat",
|
||||
|
@ -92,8 +92,7 @@
|
||||
"Buyer": "Käufer",
|
||||
"I want to": "Ich möchte",
|
||||
"Select Order Type": "Order Typ auswählen",
|
||||
"ANY_type": "ALLE",
|
||||
"ANY_currency": "ALLE",
|
||||
"ANY": "ALLE",
|
||||
"BUY": "KAUFEN",
|
||||
"SELL": "VERKAUFEN",
|
||||
"and receive": "und erhalte",
|
||||
|
@ -61,17 +61,15 @@
|
||||
"to": "to",
|
||||
"Expiry Timers": "Expiry Timers",
|
||||
"Public Duration (HH:mm)": "Public Duration (HH:mm)",
|
||||
"Escrow Deposit Time-Out (HH:mm)": "Escrow Deposit Time-Out (HH:mm)",
|
||||
"Escrow/Invoice Timer (HH:mm)": "Escrow/Invoice Timer (HH:mm)",
|
||||
"Set the skin-in-the-game, increase for higher safety assurance": "Set the skin-in-the-game, increase for higher safety assurance",
|
||||
"Fidelity Bond Size": "Fidelity Bond Size",
|
||||
"Allow bondless takers": "Allow bondless takers",
|
||||
"COMING SOON - High risk! Limited to {{limitSats}}K Sats": "COMING SOON - High risk! Limited to {{limitSats}}K Sats",
|
||||
"You must fill the order correctly": "You must fill the order correctly",
|
||||
"Create Order": "Create Order",
|
||||
"Back": "Back",
|
||||
"Create an order for ": "Create an order for ",
|
||||
"Create a BTC buy order for ": "Create a BTC buy order for ",
|
||||
"Create a BTC sell order for ": "Create a BTC sell order for ",
|
||||
"Buy order for ": "Buy order for ",
|
||||
"Sell order for ": "Sell order for ",
|
||||
" of {{satoshis}} Satoshis": " of {{satoshis}} Satoshis",
|
||||
" at market price": " at market price",
|
||||
" at a {{premium}}% premium": " at a {{premium}}% premium",
|
||||
@ -85,6 +83,10 @@
|
||||
"Done": "Done",
|
||||
"You do not have a robot avatar": "You do not have a robot avatar",
|
||||
"You need to generate a robot avatar in order to become an order maker": "You need to generate a robot avatar in order to become an order maker",
|
||||
"Edit order": "Edit order",
|
||||
"Existing orders match yours!": "Existing orders match yours!",
|
||||
"Enable advanced options": "Enable advanced options",
|
||||
"Clear form": "Clear form",
|
||||
|
||||
"PAYMENT METHODS - autocompletePayments.js": "Payment method strings",
|
||||
"not specified": "Not specified",
|
||||
@ -94,17 +96,14 @@
|
||||
"Cash F2F": "Cash F2F",
|
||||
"On-Chain BTC": "On-Chain BTC",
|
||||
|
||||
"BOOK PAGE - BookPage.js": "The Book Order page",
|
||||
"BOOK PAGE - BookPage": "The Book Order page",
|
||||
"Seller": "Seller",
|
||||
"Buyer": "Buyer",
|
||||
"I want to": "I want to",
|
||||
"Select Order Type": "Select Order Type",
|
||||
"ANY_type": "ANY",
|
||||
"ANY_currency": "ANY",
|
||||
"ANY": "ANY",
|
||||
"BUY": "BUY",
|
||||
"SELL": "SELL",
|
||||
"and receive": "and receive",
|
||||
"and pay with": "and pay with",
|
||||
"and use": "and use",
|
||||
"Select Payment Currency": "Select Payment Currency",
|
||||
"Robot": "Robot",
|
||||
@ -122,7 +121,6 @@
|
||||
"Filter has no results": "Filter has no results",
|
||||
"Be the first one to create an order": "Be the first one to create an order",
|
||||
"Orders per page:": "Orders per page:",
|
||||
"No rows": "No rows",
|
||||
"No results found.": "No results found.",
|
||||
"An error occurred.": "An error occurred.",
|
||||
"Columns": "Columns",
|
||||
@ -168,6 +166,15 @@
|
||||
"no": "no",
|
||||
"Depth chart": "Depth chart",
|
||||
"Chart": "Chart",
|
||||
"List": "List",
|
||||
"pay with": "pay with",
|
||||
"Timer": "Timer",
|
||||
"Expiry": "Expiry",
|
||||
"Sats now": "Sats now",
|
||||
"Bond": "Bond",
|
||||
"swap to": "swap to",
|
||||
"DESTINATION": "DESTINATION",
|
||||
"METHOD": "METHOD",
|
||||
|
||||
"BOTTOM BAR AND MISC - BottomBar.js": "Bottom Bar user profile and miscellaneous dialogs",
|
||||
"Stats For Nerds": "Stats For Nerds",
|
||||
|
@ -56,7 +56,7 @@
|
||||
"to": "a ",
|
||||
"Expiry Timers": "Temporizadores",
|
||||
"Public Duration (HH:mm)": "Duración pública (HH:mm)",
|
||||
"Escrow Deposit Time-Out (HH:mm)": "Plazo límite depósito (HH:mm)",
|
||||
"Escrow/Invoice Timer (HH:mm)": "Plazo depósito/factura (HH:mm)",
|
||||
"Set the skin-in-the-game, increase for higher safety assurance": "Establece la implicación requerida (aumentar para mayor seguridad)",
|
||||
"Fidelity Bond Size": "Tamaño de la fianza",
|
||||
"Allow bondless takers": "Permitir tomadores sin fianza",
|
||||
@ -65,8 +65,8 @@
|
||||
"Create Order": "Crear orden",
|
||||
"Back": "Volver",
|
||||
"Create an order for ": "Crear una orden por ",
|
||||
"Create a BTC buy order for ": "Crear orden de compra de BTC por ",
|
||||
"Create a BTC sell order for ": "Crear orden de venta de BTC por ",
|
||||
"Buy order for ": "Compra de BTC por ",
|
||||
"Sell order for ": "Venta de BTC por ",
|
||||
" of {{satoshis}} Satoshis": " de {{satoshis}} Sats",
|
||||
" at market price": " a precio de mercado",
|
||||
" at a {{premium}}% premium": " con una prima del {{premium}}%",
|
||||
@ -80,6 +80,10 @@
|
||||
"Done": "Hecho",
|
||||
"You do not have a robot avatar": "No tienes un avatar robot",
|
||||
"You need to generate a robot avatar in order to become an order maker": "Necesitas generar un avatar robot antes de crear una orden",
|
||||
"Edit order": "Editar orden",
|
||||
"Existing orders match yours!": "¡Existen órdenes que coinciden!",
|
||||
"Enable advanced options": "Activar opciones avanzadas",
|
||||
"Clear form": "Borrar campos",
|
||||
|
||||
"PAYMENT METHODS - autocompletePayments.js": "Payment method strings",
|
||||
"not specified": "Sin especificar",
|
||||
@ -96,8 +100,7 @@
|
||||
"Buyer": "Compra",
|
||||
"I want to": "Quiero",
|
||||
"Select Order Type": "Selecciona tipo de orden",
|
||||
"ANY_type": "TODO",
|
||||
"ANY_currency": "TODO",
|
||||
"ANY": "TODO",
|
||||
"BUY": "COMPRAR",
|
||||
"SELL": "VENDER",
|
||||
"and receive": "y recibir",
|
||||
@ -165,6 +168,15 @@
|
||||
"no": "no",
|
||||
"Depth chart": "Gráfico de profundidad",
|
||||
"Chart": "Gráfico",
|
||||
"List": "Tabla",
|
||||
"pay with": "pagar con",
|
||||
"Timer": "Plazo",
|
||||
"Expiry": "Caduca en",
|
||||
"Sats now": "Sats ahora",
|
||||
"Bond": "Fianza",
|
||||
"swap to": "swap a",
|
||||
"DESTINATION": "DESTINO",
|
||||
"METHOD": "MÉTODO",
|
||||
|
||||
"BOTTOM BAR AND MISC - BottomBar.js": "Bottom Bar user profile and miscellaneous dialogs",
|
||||
"Stats For Nerds": "Estadísticas para nerds",
|
||||
|
@ -95,7 +95,6 @@
|
||||
"I want to": "Nahi dut",
|
||||
"Select Order Type": "Aukeratu eskaera mota",
|
||||
"ANY_type": "EDOZEIN",
|
||||
"ANY_currency": "EDOZEIN",
|
||||
"BUY": "EROSI",
|
||||
"SELL": "SALDU",
|
||||
"and receive": "eta jaso",
|
||||
|
@ -92,8 +92,7 @@
|
||||
"Buyer": "Acquirente",
|
||||
"I want to": "Voglio",
|
||||
"Select Order Type": "Selezione il tipo di ordine",
|
||||
"ANY_type": "QUALUNQUE",
|
||||
"ANY_currency": "QUALUNQUE",
|
||||
"ANY": "QUALUNQUE",
|
||||
"BUY": "COMPRA",
|
||||
"SELL": "VENDI",
|
||||
"and receive": "e ricevi",
|
||||
|
@ -76,8 +76,7 @@
|
||||
"Buyer": "Kupujący",
|
||||
"I want to": "chcę",
|
||||
"Select Order Type": "Wybierz typ zamówienia",
|
||||
"ANY_type": "KAŻDY",
|
||||
"ANY_currency": "KAŻDY",
|
||||
"ANY": "KAŻDY",
|
||||
"BUY": "KUPIĆ",
|
||||
"SELL": "SPRZEDAĆ",
|
||||
"and receive": "i odbierz",
|
||||
|
@ -88,8 +88,7 @@
|
||||
"Buyer": "Comprador",
|
||||
"I want to": "Eu quero",
|
||||
"Select Order Type": "Selecione o tipo de ordem",
|
||||
"ANY_type": "QUALQUER",
|
||||
"ANY_currency": "QUALQUER",
|
||||
"ANY": "QUALQUER",
|
||||
"BUY": "COMPRAR",
|
||||
"SELL": "VENDER",
|
||||
"and receive": "e receber",
|
||||
|
@ -92,8 +92,7 @@
|
||||
"Buyer": "Покупатель",
|
||||
"I want to": "Я хочу",
|
||||
"Select Order Type": "Выбрать тип ордера",
|
||||
"ANY_type": "Любой тип",
|
||||
"ANY_currency": "Любую валюту",
|
||||
"ANY": "Любую валюту",
|
||||
"BUY": "Купить",
|
||||
"SELL": "Продать",
|
||||
"and receive": "и получить",
|
||||
|
@ -92,8 +92,7 @@
|
||||
"Buyer": "Köpare",
|
||||
"I want to": "Jag vill",
|
||||
"Select Order Type": "Välj ordertyp",
|
||||
"ANY_type": "ALLA",
|
||||
"ANY_currency": "ALLA",
|
||||
"ANY": "ALLA",
|
||||
"BUY": "Köp",
|
||||
"SELL": "Sälj",
|
||||
"and receive": "och ta emot",
|
||||
|
@ -94,8 +94,7 @@
|
||||
"Buyer": "ผู้ซื้อ",
|
||||
"I want to": "ฉันต้องการ",
|
||||
"Select Order Type": "เลือกชนิดรายการ",
|
||||
"ANY_type": "ANY",
|
||||
"ANY_currency": "ANY",
|
||||
"ANY": "ANY",
|
||||
"BUY": "ซื้อ",
|
||||
"SELL": "ขาย",
|
||||
"and receive": "และรับเงินเป็น",
|
||||
|
Loading…
Reference in New Issue
Block a user