diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 4c6164b1..06462f60 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -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", diff --git a/frontend/src/components/App.js b/frontend/src/components/App.js index bb524e7e..4cde78d3 100644 --- a/frontend/src/components/App.js +++ b/frontend/src/components/App.js @@ -107,24 +107,18 @@ export default class App extends Component { this.setState({ openLearn: true })} > this.handleThemeChange()} > {this.state.theme.palette.mode === 'dark' ? : } - this.setState({ openLearn: true })} - > - - diff --git a/frontend/src/components/BookPage.js b/frontend/src/components/BookPage.js deleted file mode 100644 index 4c2c6983..00000000 --- a/frontend/src/components/BookPage.js +++ /dev/null @@ -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 ( - - - - {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, - })} - - -
- - - - - {t('Be the first one to create an order')} -
-
-
-
- ); - }; - - 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 ( - - - 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} - /> - - - - - - ); - } else { - if (this.state.view === 'depth') { - return ( - - ); - } else { - return ( - 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 ( - <> - - - - {t('I want to')} - -
- - - {t('Buy')} - - - {t('Sell')} - - -
-
-
- - - - - {this.props.type == 0 - ? t('and receive') - : this.props.type == 1 - ? t('and pay with') - : t('and use')} - - - - - - ); - }; - - 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 ( - - {this.mainFilters()} - - - {this.getTitle(doubleView)} - - - - {this.mainView(doubleView, widthEm, heightEm)} - - - - {!this.props.bookNotFound ? ( - <> - - {doubleView ? null : ( - - )} - - ) : null} - - - - - ); - } -} - -export default withTranslation()(BookPage); diff --git a/frontend/src/components/BookPage/BookControl.tsx b/frontend/src/components/BookPage/BookControl.tsx new file mode 100644 index 00000000..76651011 --- /dev/null +++ b/frontend/src/components/BookPage/BookControl.tsx @@ -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 ( + + + {width > verboseToolbarWidth ? ( + + + {t('I want to')} + + + ) : null} + + + + + {t('Buy')} + + + {t('Sell')} + + + + + {width > verboseToolbarWidth ? ( + + + {t('and use')} + + + ) : null} + + + + + + {width > verboseToolbarWidth ? ( + + + {currency == 1000 ? t('swap to') : t('pay with')} + + + ) : null} + + {width > mediumToolbarWidth ? ( + + + + ) : null} + + {width > smallestToolbarWidth && width < mediumToolbarWidth ? ( + + + + ) : null} + + + + ); +}; + +export default BookControl; diff --git a/frontend/src/components/BookPage/BookPage.tsx b/frontend/src/components/BookPage/BookPage.tsx new file mode 100644 index 00000000..e760c854 --- /dev/null +++ b/frontend/src/components/BookPage/BookPage.tsx @@ -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(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(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 ( + + + + {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()], + })} + + + + + {t('Be the first one to create an order')} + + + + ); + }; + + const NavButtons = function () { + return ( + + + {doubleView ? ( + <> + ) : ( + + )} + + + ); + }; + return ( + + {openMaker ? ( + setOpenMaker(false)}> + + + + + ) : null} + + + {doubleView ? ( + + + 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} + /> + + + + + + ) : view === 'depth' ? ( + + ) : ( + 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} + /> + )} + + + + + + + ); +}; + +export default BookPage; diff --git a/frontend/src/components/BookTable.tsx b/frontend/src/components/BookPage/BookTable.tsx similarity index 80% rename from frontend/src/components/BookTable.tsx rename to frontend/src/components/BookPage/BookTable.tsx index a6453d8a..e83c29ce 100644 --- a/frontend/src/components/BookTable.tsx +++ b/frontend/src/components/BookPage/BookTable.tsx @@ -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([]); // 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 ( @@ -179,7 +191,7 @@ const BookTable = ({ field: 'maker_nick', headerName: t('Robot'), width: width * fontSize, - renderCell: (params) => { + renderCell: (params: any) => { return (
@@ -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 (
{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 (
{ + renderCell: (params: any) => { return (
{ + renderCell: (params: any) => { return (
{ + renderCell: (params: any) => { const currencyCode = currencyDict[params.row.currency.toString()]; return (
{`${pn(params.row.price)} ${currencyCode}/BTC`}
@@ -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 ( -
- - {parseFloat(parseFloat(params.row.premium).toFixed(4)) + '%'} - -
+ +
+ + {parseFloat(parseFloat(params.row.premium).toFixed(4)) + '%'} + +
+
); }, }; @@ -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
{hours > 0 ? `${hours}h` : `${minutes}m`}
; @@ -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 (
{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 (
@@ -610,14 +628,8 @@ const BookTable = ({ const [columns, width] = filteredColumns(fullscreen ? fullWidth : maxWidth); - const gridComponents = { - LoadingOverlay: LinearProgress, - NoResultsOverlay: () => ( - - {t('Filter has no results')} - - ), - Footer: () => ( + const Footer = function () { + return ( @@ -638,7 +650,47 @@ const BookTable = ({ - ), + ); + }; + + interface GridComponentProps { + LoadingOverlay: JSX.Element; + NoResultsOverlay?: JSX.Element; + NoRowsOverlay?: JSX.Element; + Footer?: JSX.Element; + Toolbar?: JSX.Element; + } + + const Controls = function () { + return ( + + ); + }; + + 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 = ({ - (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. /> ); @@ -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. /> diff --git a/frontend/src/components/BookPage/index.ts b/frontend/src/components/BookPage/index.ts new file mode 100644 index 00000000..12ea491e --- /dev/null +++ b/frontend/src/components/BookPage/index.ts @@ -0,0 +1,4 @@ +import BookPage from './BookPage'; +export default BookPage; + +export { default as BookTable } from './BookTable'; diff --git a/frontend/src/components/BottomBar.js b/frontend/src/components/BottomBar.js index c1dbb2b0..61e4b839 100644 --- a/frontend/src/components/BottomBar.js +++ b/frontend/src/components/BottomBar.js @@ -192,7 +192,7 @@ class BottomBar extends Component { secondaryTypographyProps: { fontSize: (fontSize * 12) / 14 }, }; return ( - +
@@ -485,7 +485,7 @@ class BottomBar extends Component { this.props.avatarLoaded ); return ( - +
diff --git a/frontend/src/components/Charts/DepthChart/index.tsx b/frontend/src/components/Charts/DepthChart/index.tsx index e8ab70ef..172dbaf5 100644 --- a/frontend/src/components/Charts/DepthChart/index.tsx +++ b/frontend/src/components/Charts/DepthChart/index.tsx @@ -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'; diff --git a/frontend/src/components/FlagWithProps/FlagWithProps.tsx b/frontend/src/components/FlagWithProps/FlagWithProps.tsx index 501e8009..517f73f1 100644 --- a/frontend/src/components/FlagWithProps/FlagWithProps.tsx +++ b/frontend/src/components/FlagWithProps/FlagWithProps.tsx @@ -91,7 +91,7 @@ const FlagWithProps = ({ code }: Props): JSX.Element => { if (code === 'XAU') flag = ; if (code === 'BTC') flag = ; - return
{flag}
; + return
{flag}
; }; export default FlagWithProps; diff --git a/frontend/src/components/HomePage.js b/frontend/src/components/HomePage.js index c1e6d40e..ff23a840 100644 --- a/frontend/src/components/HomePage.js +++ b/frontend/src/components/HomePage.js @@ -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 {
- this.setState({ - limits: data, - loadingLimits: false, - minAmount: this.state.amount - ? parseFloat((this.state.amount / 2).toPrecision(2)) - : parseFloat(Number(data[this.state.currency].max_amount * 0.25).toPrecision(2)), - maxAmount: this.state.amount - ? this.state.amount - : parseFloat(Number(data[this.state.currency].max_amount * 0.75).toPrecision(2)), - minTradeSats: data['1000'].min_amount * 100000000, - maxTradeSats: data['1000'].max_amount * 100000000, - maxBondlessSats: data['1000'].max_bondless_amount * 100000000, - }), - ); - } - - recalcBounds = () => { - this.setState({ - minAmount: this.state.amount - ? parseFloat((this.state.amount / 2).toPrecision(2)) - : parseFloat( - Number(this.state.limits[this.state.currency].max_amount * 0.25).toPrecision(2), - ), - maxAmount: this.state.amount - ? this.state.amount - : parseFloat( - Number(this.state.limits[this.state.currency].max_amount * 0.75).toPrecision(2), - ), - }); - }; - - a11yProps(index) { - return { - id: `simple-tab-${index}`, - 'aria-controls': `simple-tabpanel-${index}`, - }; - } - - handleCurrencyChange = (e) => { - const currencyCode = this.getCurrencyCode(e.target.value); - this.setState({ - currency: e.target.value, - currencyCode, - }); - this.props.setAppState({ - currency: e.target.value, - bookCurrencyCode: currencyCode, - }); - if (this.state.enableAmountRange) { - this.setState({ - minAmount: parseFloat( - Number(this.state.limits[e.target.value].max_amount * 0.25).toPrecision(2), - ), - maxAmount: parseFloat( - Number(this.state.limits[e.target.value].max_amount * 0.75).toPrecision(2), - ), - }); - } - }; - - handleAmountChange = (e) => { - this.setState({ - amount: e.target.value, - }); - }; - - handleMinAmountChange = (e) => { - this.setState({ - minAmount: parseFloat(Number(e.target.value).toPrecision(e.target.value < 100 ? 2 : 3)), - }); - }; - - handleMaxAmountChange = (e) => { - this.setState({ - maxAmount: parseFloat(Number(e.target.value).toPrecision(e.target.value < 100 ? 2 : 3)), - }); - }; - - handleRangeAmountChange = (e, newValue, activeThumb) => { - const maxAmount = this.getMaxAmount(); - const minAmount = this.getMinAmount(); - let lowerValue = e.target.value[0]; - let upperValue = e.target.value[1]; - const minRange = this.minRangeAmountMultiple; - const maxRange = this.maxRangeAmountMultiple; - - if (lowerValue > maxAmount / minRange) { - lowerValue = maxAmount / minRange; - } - if (upperValue < minRange * minAmount) { - upperValue = minRange * minAmount; - } - - if (lowerValue > upperValue / minRange) { - if (activeThumb === 0) { - upperValue = minRange * lowerValue; - } else { - lowerValue = upperValue / minRange; - } - } else if (lowerValue < upperValue / maxRange) { - if (activeThumb === 0) { - upperValue = maxRange * lowerValue; - } else { - lowerValue = upperValue / maxRange; - } - } - - this.setState({ - minAmount: parseFloat(Number(lowerValue).toPrecision(lowerValue < 100 ? 2 : 3)), - maxAmount: parseFloat(Number(upperValue).toPrecision(upperValue < 100 ? 2 : 3)), - }); - }; - - handlePaymentMethodChange = (value) => { - if (value.length > 50) { - this.setState({ - badPaymentMethod: true, - }); - } else { - this.setState({ - payment_method: value.substring(0, 53), - badPaymentMethod: value.length > 50, - }); - } - }; - - handlePremiumChange = (e) => { - const { t } = this.props; - const max = 999; - const min = -100; - let premium = e.target.value; - if (e.target.value > 999) { - var bad_premium = t('Must be less than {{max}}%', { max }); - } - if (e.target.value <= -100) { - var bad_premium = t('Must be more than {{min}}%', { min }); - } - - if (premium == '') { - premium = 0; - } else { - premium = Number(Math.round(premium + 'e' + 2) + 'e-' + 2); - } - this.setState({ - premium, - badPremium: bad_premium, - }); - }; - - handleSatoshisChange = (e) => { - const { t } = this.props; - if (e.target.value > this.state.maxTradeSats) { - var bad_sats = t('Must be less than {{maxSats}', { maxSats: pn(this.state.maxTradeSats) }); - } - if (e.target.value < this.state.minTradeSats) { - var bad_sats = t('Must be more than {{minSats}}', { minSats: pn(this.state.minTradeSats) }); - } - - this.setState({ - satoshis: e.target.value, - badSatoshis: bad_sats, - }); - }; - - handleClickRelative = (e) => { - this.setState({ - is_explicit: false, - }); - this.handlePremiumChange(); - }; - - handleClickExplicit = (e) => { - if (!this.state.enableAmountRange) { - this.setState({ - is_explicit: true, - }); - this.handleSatoshisChange(); - } - }; - - handleCreateOfferButtonPressed = () => { - this.state.amount == null ? this.setState({ amount: 0 }) : null; - const body = { - type: this.props.type == 0 ? 1 : 0, - currency: this.state.currency, - amount: this.state.has_range ? null : this.state.amount, - has_range: this.state.enableAmountRange, - min_amount: this.state.minAmount, - max_amount: this.state.maxAmount, - payment_method: - this.state.payment_method === '' ? this.defaultPaymentMethod : this.state.payment_method, - is_explicit: this.state.is_explicit, - premium: this.state.is_explicit ? null : this.state.premium == '' ? 0 : this.state.premium, - satoshis: this.state.is_explicit ? this.state.satoshis : null, - public_duration: this.state.publicDuration, - escrow_duration: this.state.escrowDuration, - bond_size: this.state.bondSize, - bondless_taker: this.state.allowBondless, - }; - apiClient - .post('/api/make/', body) - .then( - (data) => - this.setState({ badRequest: data.bad_request }) & - (data.id ? this.props.history.push('/order/' + data.id) : ''), - ); - this.setState({ openStoreToken: false }); - }; - - getCurrencyCode(val) { - return currencyDict[val.toString()]; - } - - handleInputBondSizeChange = (event) => { - this.setState({ bondSize: event.target.value === '' ? 1 : Number(event.target.value) }); - }; - - priceNow = () => { - if (this.state.loadingLimits) { - return '...'; - } else if (this.state.is_explicit & (this.state.amount > 0) & (this.state.satoshis > 0)) { - return parseFloat( - Number(this.state.amount / (this.state.satoshis / 100000000)).toPrecision(5), - ); - } else if (!this.state.is_explicit) { - const price = this.state.limits[this.state.currency].price; - return parseFloat(Number(price * (1 + this.state.premium / 100)).toPrecision(5)); - } - return '...'; - }; - - StandardMakerOptions = () => { - const { t } = this.props; - return ( - - - - - {t('Buy or Sell Bitcoin?')} - -
- - - - -
-
-
- - - -
- - this.getMaxAmount()) & - (this.state.amount != '') - ) - } - helperText={ - (this.state.amount < this.getMinAmount()) & (this.state.amount != '') - ? t('Must be more than {{minAmount}}', { minAmount: this.getMinAmount() }) - : (this.state.amount > this.getMaxAmount()) & (this.state.amount != '') - ? t('Must be less than {{maxAmount}}', { maxAmount: this.getMaxAmount() }) - : null - } - label={t('Amount')} - type='number' - required={true} - value={this.state.amount} - inputProps={{ - min: 0, - style: { textAlign: 'center' }, - }} - onChange={this.handleAmountChange} - /> - -
-
- -
-
- - -
- - - - - - {t('Choose a Pricing Method')} - - - - } - label={t('Relative')} - labelPlacement='end' - onClick={this.handleClickRelative} - /> - - - } - label={t('Explicit')} - labelPlacement='end' - onClick={this.handleClickExplicit} - /> - - - - - {/* conditional shows either Premium % field or Satoshis field based on pricing method */} - -
- -
-
- -
- -
-
- -
-
- - - {(this.state.is_explicit ? t('Order rate:') : t('Order current rate:')) + - ' ' + - pn(this.priceNow()) + - ' ' + - this.state.currencyCode + - '/BTC'} - - -
- - - - ); - }; - - handleChangePublicDuration = (date) => { - const d = new Date(date); - const hours = d.getHours(); - const minutes = d.getMinutes(); - - const total_secs = hours * 60 * 60 + minutes * 60; - - this.setState({ - publicExpiryTime: date, - publicDuration: total_secs, - }); - }; - - handleChangeEscrowDuration = (date) => { - const d = new Date(date); - const hours = d.getHours(); - const minutes = d.getMinutes(); - - const total_secs = hours * 60 * 60 + minutes * 60; - - this.setState({ - escrowExpiryTime: date, - escrowDuration: total_secs, - }); - }; - - getMaxAmount = () => { - if (this.state.limits == null) { - var max_amount = null; - } else { - var max_amount = - this.state.limits[this.state.currency].max_amount * (1 + this.state.premium / 100); - } - // times 0.98 to allow a bit of margin with respect to the backend minimum - return parseFloat(Number(max_amount * 0.98).toPrecision(2)); - }; - - getMinAmount = () => { - if (this.state.limits == null) { - var min_amount = null; - } else { - var min_amount = - this.state.limits[this.state.currency].min_amount * (1 + this.state.premium / 100); - } - // times 1.1 to allow a bit of margin with respect to the backend minimum - return parseFloat(Number(min_amount * 1.1).toPrecision(2)); - }; - - RangeThumbComponent(props) { - const { children, ...other } = props; - return ( - - {children} - - - - - ); - } - - minAmountError = () => { - return ( - this.state.minAmount < this.getMinAmount() || - this.state.maxAmount < this.state.minAmount || - this.state.minAmount < this.state.maxAmount / (this.maxRangeAmountMultiple + 0.15) || - this.state.minAmount * (this.minRangeAmountMultiple - 0.1) > this.state.maxAmount - ); - }; - - maxAmountError = () => { - return ( - this.state.maxAmount > this.getMaxAmount() || - this.state.maxAmount < this.state.minAmount || - this.state.minAmount < this.state.maxAmount / (this.maxRangeAmountMultiple + 0.15) || - this.state.minAmount * (this.minRangeAmountMultiple - 0.1) > this.state.maxAmount - ); - }; - - rangeText = () => { - const { t } = this.props; - return ( -
- {t('From')} - - {t('to')} - - - {this.state.currencyCode} - -
- ); - }; - - AdvancedMakerOptions = () => { - const { t } = this.props; - return ( - - - - - - - - this.setState({ enableAmountRange: e.target.checked, is_explicit: false }) & - this.recalcBounds() - } - /> - {this.state.enableAmountRange & (this.state.minAmount != null) - ? this.rangeText() - : t('Enable Amount Range')} - - -
- -
-
- - parseFloat(Number(x).toPrecision(x < 100 ? 2 : 3)) + - ' ' + - this.state.currencyCode - } - marks={ - this.state.limits == null - ? null - : [ - { - value: this.getMinAmount(), - label: this.getMinAmount() + ' ' + this.state.currencyCode, - }, - { - value: this.getMaxAmount(), - label: this.getMaxAmount() + ' ' + this.state.currencyCode, - }, - ] - } - min={this.getMinAmount()} - max={this.getMaxAmount()} - onChange={this.handleRangeAmountChange} - /> -
-
-
- - - - }> - - {t('Expiry Timers')} - - - - - - - - - - ), - }} - renderInput={(props) => } - label={t('Public Duration (HH:mm)')} - value={this.state.publicExpiryTime} - onChange={this.handleChangePublicDuration} - minTime={new Date(0, 0, 0, 0, 10)} - maxTime={new Date(0, 0, 0, 23, 59)} - /> - - - - - - - - - ), - }} - renderInput={(props) => } - label={t('Escrow Deposit Time-Out (HH:mm)')} - value={this.state.escrowExpiryTime} - onChange={this.handleChangeEscrowDuration} - minTime={new Date(0, 0, 0, 1, 0)} - maxTime={new Date(0, 0, 0, 8, 0)} - /> - - - - - - - - - - - - {t('Fidelity Bond Size')}{' '} - - - - 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) => this.setState({ bondSize: e.target.value })} - /> - - - - - - this.setState({ allowBondless: !this.state.allowBondless })} - /> - } - /> - - -
-
- ); - }; - - makeOrderBox = () => { - const { t } = this.props; - return ( - - - - this.setState({ tabValue: 0 })} - /> - this.setState({ tabValue: 1 })} - /> - - - -
- {this.StandardMakerOptions()} -
-
- {this.AdvancedMakerOptions()} -
-
-
- ); - }; - - render() { - const { t } = this.props; - return ( - - {systemClient.getCookie('robot_token') ? ( - this.setState({ openStoreToken: false })} - onClickCopy={() => - systemClient.copyToClipboard(systemClient.getCookie('robot_token')) & - this.props.setAppState({ copiedToken: true }) - } - copyIconColor={this.props.copiedToken ? 'inherit' : 'primary'} - onClickBack={() => this.setState({ openStoreToken: false })} - onClickDone={this.handleCreateOfferButtonPressed} - /> - ) : ( - this.setState({ openStoreToken: false })} - /> - )} - - - {this.makeOrderBox()} - - - - {/* conditions to disable the make button */} - {this.props.type == null || - (this.state.amount == null) & - (this.state.enableAmountRange == false || this.state.loadingLimits) || - this.state.enableAmountRange & (this.minAmountError() || this.maxAmountError()) || - (this.state.amount <= 0) & !this.state.enableAmountRange || - this.state.is_explicit & - (this.state.badSatoshis != null || this.state.satoshis == null) || - !this.state.is_explicit & (this.state.badPremium != null) ? ( - -
- -
-
- ) : ( - - )} -
- - {this.state.badRequest ? ( - - {this.state.badRequest}
-
- ) : ( - '' - )} - -
- {this.props.type == null - ? t('Create an order for ') - : this.props.type == 1 - ? t('Create a BTC buy order for ') - : t('Create a BTC sell order for ')} - {this.state.enableAmountRange & (this.state.minAmount != null) - ? this.state.minAmount + '-' + this.state.maxAmount - : pn(this.state.amount)} - {' ' + this.state.currencyCode} - {this.state.is_explicit - ? t(' of {{satoshis}} Satoshis', { satoshis: pn(this.state.satoshis) }) - : this.state.premium == 0 - ? t(' at market price') - : this.state.premium > 0 - ? t(' at a {{premium}}% premium', { premium: this.state.premium }) - : t(' at a {{discount}}% discount', { discount: -this.state.premium })} -
-
- - - -
-
- ); - } -} - -export default withTranslation()(MakerPage); diff --git a/frontend/src/components/MakerPage/AmountRange.tsx b/frontend/src/components/MakerPage/AmountRange.tsx new file mode 100644 index 00000000..5b0ac99d --- /dev/null +++ b/frontend/src/components/MakerPage/AmountRange.tsx @@ -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 ( + + {children} + + + + + ); +}; + +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 ( + + + + + + {t('From')} + + + + {t('to')} + + +
+ + + + + + 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} + /> + + + + + ); +} + +export default AmountRange; diff --git a/frontend/src/components/AutocompletePayments.js b/frontend/src/components/MakerPage/AutocompletePayments.js similarity index 61% rename from frontend/src/components/AutocompletePayments.js rename to frontend/src/components/MakerPage/AutocompletePayments.js index 2833eebf..55578632 100644 --- a/frontend/src/components/AutocompletePayments.js +++ b/frontend/src/components/MakerPage/AutocompletePayments.js @@ -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 ( -
- + +
+ +
+
{value.map((option, index) => ( - + ))} - + {value.length > 0 && props.isFilter ? null : }
- {groupedOptions.length > 0 ? ( - -
- - {props.listHeaderText + ' '} {' '} - -
- {groupedOptions.map((option, index) => ( + 0}> + + {!props.isFilter ? ( +
+ + {props.listHeaderText + ' '} {' '} + +
+ ) : null} + {fewerOptions.map((option, index) => (
  • -
    +
  • ))} - {val != null ? ( + {val != null || !props.isFilter ? ( val.length > 2 ? ( ) : null ) : null}
    - ) : // Here goes what happens if there is no groupedOptions - getInputProps().value.length > 0 ? ( +
    + + {/* Here goes what happens if there is no fewerOptions */} + 0 && !props.isFilter && fewerOptions.length === 0}> - ) : null} +
    ); } diff --git a/frontend/src/components/MakerPage/MakerForm.tsx b/frontend/src/components/MakerPage/MakerForm.tsx new file mode 100644 index 00000000..29676f9d --- /dev/null +++ b/frontend/src/components/MakerPage/MakerForm.tsx @@ -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(null); + const [advancedOptions, setAdvancedOptions] = useState(false); + const [amountLimits, setAmountLimits] = useState([1, 1000]); + const [satoshisLimits, setSatoshisLimits] = useState([20000, 4000000]); + const [currentPrice, setCurrentPrice] = useState('...'); + const [currencyCode, setCurrencyCode] = useState('USD'); + + const [openDialogs, setOpenDialogs] = useState(false); + const [submittingRequest, setSubmittingRequest] = useState(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 ( + + {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 })} + + ); + }; + + const ConfirmationDialogs = function () { + return systemClient.getCookie('robot_token') ? ( + setOpenDialogs(false)} + onClickCopy={() => systemClient.copyToClipboard(systemClient.getCookie('robot_token'))} + copyIconColor={'primary'} + onClickBack={() => setOpenDialogs(false)} + onClickDone={handleCreateOrder} + /> + ) : ( + setOpenDialogs(false)} /> + ); + }; + return ( + + + +
    + +
    +
    + + + + + + + + + + + +
    + + +
    +
    +
    +
    +
    + + + + + + + {t('Buy or Sell Bitcoin?')} + +
    + + + + +
    +
    +
    + + + + + + + + + + + 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 })} + /> + + + + + + + + + + + + + + + + {!advancedOptions && pricingMethods ? ( + + + + + {t('Choose a Pricing Method')} + + + + } + label={t('Relative')} + labelPlacement='end' + onClick={handleClickRelative} + /> + + + } + label={t('Exact')} + labelPlacement='end' + onClick={handleClickExplicit} + /> + + + + + + ) : null} + +
    + +
    +
    + +
    +
    + + + + + + + + + ), + style: { + backgroundColor: theme.palette.background.paper, + borderRadius: '4px', + }, + }} + renderInput={(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)} + /> + + + + + + + + + ), + style: { + backgroundColor: theme.palette.background.paper, + borderRadius: '4px', + }, + }} + renderInput={(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)} + /> + + + + + + + + + + {t('Fidelity Bond Size')}{' '} + + + + + + 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 })} + /> + + + + + + + +
    +
    + + + + + + + + + + {/* conditions to disable the make button */} + {disableSubmit() ? ( + +
    + +
    +
    + ) : ( + { + disableRequest ? onSubmit() : setOpenDialogs(true); + }} + > + {t(submitButtonLabel)} + + )} +
    + {collapseAll ? ( + + + + + + + + + + ) : null} +
    +
    + + + + {badRequest} + + + + + + + {(maker.isExplicit ? t('Order rate:') : t('Order current rate:')) + + ` ${pn(currentPrice)} ${currencyCode}/BTC`} + + + +
    +
    + ); +}; + +export default MakerForm; diff --git a/frontend/src/components/MakerPage/MakerPage.tsx b/frontend/src/components/MakerPage/MakerPage.tsx new file mode 100644 index 00000000..1ef2caad --- /dev/null +++ b/frontend/src/components/MakerPage/MakerPage.tsx @@ -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(defaultMaker); + const maxHeight = windowHeight ? windowHeight * 0.85 - 7 : 1000; + const [showMatches, setShowMatches] = useState(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 ( + + + 0 && showMatches}> + + + {t('Existing orders match yours!')} + + + 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} + /> + + + + + + + 0 && !showMatches} + collapseAll={showMatches} + onSubmit={() => setShowMatches(matches.length > 0)} + onReset={() => setShowMatches(false)} + submitButtonLabel={matches.length > 0 && !showMatches ? 'Submit' : 'Create order'} + /> + + + + + + + ); +}; + +export default MakerPage; diff --git a/frontend/src/components/RangeSlider.js b/frontend/src/components/MakerPage/RangeSlider.js similarity index 100% rename from frontend/src/components/RangeSlider.js rename to frontend/src/components/MakerPage/RangeSlider.js diff --git a/frontend/src/components/MakerPage/index.ts b/frontend/src/components/MakerPage/index.ts new file mode 100644 index 00000000..b1b76490 --- /dev/null +++ b/frontend/src/components/MakerPage/index.ts @@ -0,0 +1,5 @@ +import MakerPage from './MakerPage'; +export default MakerPage; + +export { default as MakerForm } from './MakerForm'; +export { default as AutocompletePayments } from './AutocompletePayments'; diff --git a/frontend/src/components/TorConnection.tsx b/frontend/src/components/TorConnection.tsx index 96ae0e00..57754e87 100644 --- a/frontend/src/components/TorConnection.tsx +++ b/frontend/src/components/TorConnection.tsx @@ -59,7 +59,7 @@ const TorConnection = (): JSX.Element => { }); }, []); - if (!window?.NativeRobosats) { + if (window?.NativeRobosats == null) { return <>; } diff --git a/frontend/src/components/payment-methods/Icons.js b/frontend/src/components/payment-methods/Icons.js index f02655b9..55416036 100644 --- a/frontend/src/components/payment-methods/Icons.js +++ b/frontend/src/components/payment-methods/Icons.js @@ -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 ( ); + } else { + return ( + + ); } - return ( - - ); } } diff --git a/frontend/src/models/Maker.model.ts b/frontend/src/models/Maker.model.ts new file mode 100644 index 00000000..caf880f0 --- /dev/null +++ b/frontend/src/models/Maker.model.ts @@ -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; diff --git a/frontend/src/models/index.ts b/frontend/src/models/index.ts new file mode 100644 index 00000000..bbc7a87c --- /dev/null +++ b/frontend/src/models/index.ts @@ -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'; diff --git a/frontend/src/services/api/ApiNativeClient/index.ts b/frontend/src/services/api/ApiNativeClient/index.ts index 5cf5178e..5c05cb37 100644 --- a/frontend/src/services/api/ApiNativeClient/index.ts +++ b/frontend/src/services/api/ApiNativeClient/index.ts @@ -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(async (resolve, reject) => { @@ -95,7 +95,7 @@ class ApiNativeClient implements ApiClient { resolve(this.assetsCache[path]); }); - return this.assetsPromises[path]; + return await this.assetsPromises[path]; }; } diff --git a/frontend/src/utils/filterOrders.ts b/frontend/src/utils/filterOrders.ts new file mode 100644 index 00000000..9d00db2f --- /dev/null +++ b/frontend/src/utils/filterOrders.ts @@ -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; diff --git a/frontend/static/locales/ca.json b/frontend/static/locales/ca.json index 0bf2a933..e151611c 100644 --- a/frontend/static/locales/ca.json +++ b/frontend/static/locales/ca.json @@ -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", diff --git a/frontend/static/locales/cs.json b/frontend/static/locales/cs.json index 4ee80dd7..2b1a7286 100644 --- a/frontend/static/locales/cs.json +++ b/frontend/static/locales/cs.json @@ -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", diff --git a/frontend/static/locales/de.json b/frontend/static/locales/de.json index eb2ab41a..d2737af4 100644 --- a/frontend/static/locales/de.json +++ b/frontend/static/locales/de.json @@ -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", diff --git a/frontend/static/locales/en.json b/frontend/static/locales/en.json index e28d79f4..edd0ee79 100644 --- a/frontend/static/locales/en.json +++ b/frontend/static/locales/en.json @@ -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", diff --git a/frontend/static/locales/es.json b/frontend/static/locales/es.json index e3e74c07..a557695b 100644 --- a/frontend/static/locales/es.json +++ b/frontend/static/locales/es.json @@ -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", diff --git a/frontend/static/locales/eu.json b/frontend/static/locales/eu.json index 1886a57e..594d0131 100644 --- a/frontend/static/locales/eu.json +++ b/frontend/static/locales/eu.json @@ -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", diff --git a/frontend/static/locales/it.json b/frontend/static/locales/it.json index 1653ad31..27c6f1d6 100644 --- a/frontend/static/locales/it.json +++ b/frontend/static/locales/it.json @@ -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", diff --git a/frontend/static/locales/pl.json b/frontend/static/locales/pl.json index d67ec1a2..1cf30ced 100644 --- a/frontend/static/locales/pl.json +++ b/frontend/static/locales/pl.json @@ -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", diff --git a/frontend/static/locales/pt.json b/frontend/static/locales/pt.json index 8a2d64e9..2b2ffb9e 100644 --- a/frontend/static/locales/pt.json +++ b/frontend/static/locales/pt.json @@ -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", diff --git a/frontend/static/locales/ru.json b/frontend/static/locales/ru.json index 9547015b..6737ef45 100644 --- a/frontend/static/locales/ru.json +++ b/frontend/static/locales/ru.json @@ -92,8 +92,7 @@ "Buyer": "Покупатель", "I want to": "Я хочу", "Select Order Type": "Выбрать тип ордера", - "ANY_type": "Любой тип", - "ANY_currency": "Любую валюту", + "ANY": "Любую валюту", "BUY": "Купить", "SELL": "Продать", "and receive": "и получить", diff --git a/frontend/static/locales/sv.json b/frontend/static/locales/sv.json index 188605cc..62cea9df 100644 --- a/frontend/static/locales/sv.json +++ b/frontend/static/locales/sv.json @@ -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", diff --git a/frontend/static/locales/th.json b/frontend/static/locales/th.json index 16411936..f21b498b 100644 --- a/frontend/static/locales/th.json +++ b/frontend/static/locales/th.json @@ -94,8 +94,7 @@ "Buyer": "ผู้ซื้อ", "I want to": "ฉันต้องการ", "Select Order Type": "เลือกชนิดรายการ", - "ANY_type": "ANY", - "ANY_currency": "ANY", + "ANY": "ANY", "BUY": "ซื้อ", "SELL": "ขาย", "and receive": "และรับเงินเป็น",