Book functional component (#256)

* Add bookTable functional component

* Implement responsive booktable

* Fix unwanted scroll bar on chromium browsers

* Add column self-organization and 3 new columns

* Add responsive behaviour on depth chart

* Run prettier

* Add minimum pageSize (book must at least be 1 row height)

* Adjust circular spinner div height

* Add order ID column style

* Refactor window resize event listener

* Add depth chart outline

* Review fixes
This commit is contained in:
Reckless_Satoshi 2022-09-27 15:52:00 +00:00 committed by GitHub
parent 33941ce359
commit e47d55b582
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 800 additions and 563 deletions

View File

@ -1,38 +1,27 @@
import React, { Component } from 'react';
import { withTranslation } from 'react-i18next';
import {
Tooltip,
Stack,
Paper,
Button,
ToggleButtonGroup,
ToggleButton,
ListItemButton,
Typography,
Grid,
Select,
MenuItem,
FormControl,
FormHelperText,
ListItemText,
ListItemAvatar,
IconButton,
ButtonGroup,
} from '@mui/material';
import { Link } from 'react-router-dom';
import { DataGrid } from '@mui/x-data-grid';
import currencyDict from '../../static/assets/currencies.json';
import MediaQuery from 'react-responsive';
import FlagWithProps from './FlagWithProps';
import { pn, amountToString } from '../utils/prettyNumbers';
import PaymentText from './PaymentText';
import DepthChart from './Charts/DepthChart';
import RobotAvatar from './Robots/RobotAvatar';
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) {
@ -43,11 +32,11 @@ class BookPage extends Component {
};
}
componentDidMount() {
this.getOrderDetails(2, 0);
}
componentDidMount = () => {
this.getOrderDetails();
};
getOrderDetails(type, currency) {
getOrderDetails() {
this.props.setAppState({ bookLoading: true });
apiClient.get('/api/book/').then((data) =>
this.props.setAppState({
@ -79,378 +68,6 @@ class BookPage extends Component {
}
}
// Colors for the status badges
statusBadgeColor(status) {
if (status === 'Active') {
return 'success';
}
if (status === 'Seen recently') {
return 'warning';
}
if (status === 'Inactive') {
return 'error';
}
}
dataGridLocaleText = () => {
const { t } = this.props;
return {
MuiTablePagination: { labelRowsPerPage: t('Orders per page:') },
noRowsLabel: t('No rows'),
noResultsOverlayLabel: t('No results found.'),
errorOverlayDefaultLabel: t('An error occurred.'),
toolbarColumns: t('Columns'),
toolbarColumnsLabel: t('Select columns'),
columnsPanelTextFieldLabel: t('Find column'),
columnsPanelTextFieldPlaceholder: t('Column title'),
columnsPanelDragIconLabel: t('Reorder column'),
columnsPanelShowAllButton: t('Show all'),
columnsPanelHideAllButton: t('Hide all'),
filterPanelAddFilter: t('Add filter'),
filterPanelDeleteIconLabel: t('Delete'),
filterPanelLinkOperator: t('Logic operator'),
filterPanelOperators: t('Operator'), // TODO v6: rename to filterPanelOperator
filterPanelOperatorAnd: t('And'),
filterPanelOperatorOr: t('Or'),
filterPanelColumns: t('Columns'),
filterPanelInputLabel: t('Value'),
filterPanelInputPlaceholder: t('Filter value'),
filterOperatorContains: t('contains'),
filterOperatorEquals: t('equals'),
filterOperatorStartsWith: t('starts with'),
filterOperatorEndsWith: t('ends with'),
filterOperatorIs: t('is'),
filterOperatorNot: t('is not'),
filterOperatorAfter: t('is after'),
filterOperatorOnOrAfter: t('is on or after'),
filterOperatorBefore: t('is before'),
filterOperatorOnOrBefore: t('is on or before'),
filterOperatorIsEmpty: t('is empty'),
filterOperatorIsNotEmpty: t('is not empty'),
filterOperatorIsAnyOf: t('is any of'),
filterValueAny: t('any'),
filterValueTrue: t('true'),
filterValueFalse: t('false'),
columnMenuLabel: t('Menu'),
columnMenuShowColumns: t('Show columns'),
columnMenuFilter: t('Filter'),
columnMenuHideColumn: t('Hide'),
columnMenuUnsort: t('Unsort'),
columnMenuSortAsc: t('Sort by ASC'),
columnMenuSortDesc: t('Sort by DESC'),
columnHeaderFiltersLabel: t('Show filters'),
columnHeaderSortIconLabel: t('Sort'),
booleanCellTrueLabel: t('yes'),
booleanCellFalseLabel: t('no'),
};
};
bookListTableDesktop = () => {
const { t } = this.props;
return (
<div style={{ height: 424, width: '100%' }}>
<DataGrid
localeText={this.dataGridLocaleText()}
rows={this.props.bookOrders.filter(
(order) =>
(order.type == this.props.type || this.props.type == null) &&
(order.currency == this.props.currency || this.props.currency == 0),
)}
loading={this.props.bookLoading}
columns={[
// { field: 'id', headerName: 'ID', width: 40 },
{
field: 'maker_nick',
headerName: t('Robot'),
width: 240,
renderCell: (params) => {
return (
<ListItemButton style={{ cursor: 'pointer' }}>
<ListItemAvatar>
<RobotAvatar
nickname={params.row.maker_nick}
style={{ width: 45, height: 45 }}
smooth={true}
orderType={params.row.type}
statusColor={this.statusBadgeColor(params.row.maker_status)}
tooltip={t(params.row.maker_status)}
/>
</ListItemAvatar>
<ListItemText primary={params.row.maker_nick} />
</ListItemButton>
);
},
},
{
field: 'type',
headerName: t('Is'),
width: 60,
renderCell: (params) => (params.row.type ? t('Seller') : t('Buyer')),
},
{
field: 'amount',
headerName: t('Amount'),
type: 'number',
width: 90,
renderCell: (params) => {
return (
<div style={{ cursor: 'pointer' }}>
{amountToString(
params.row.amount,
params.row.has_range,
params.row.min_amount,
params.row.max_amount,
)}
</div>
);
},
},
{
field: 'currency',
headerName: t('Currency'),
width: 100,
renderCell: (params) => {
const currencyCode = this.getCurrencyCode(params.row.currency);
return (
<div
style={{
cursor: 'pointer',
display: 'flex',
alignItems: 'center',
flexWrap: 'wrap',
}}
>
{currencyCode + ' '}
<FlagWithProps code={currencyCode} />
</div>
);
},
},
{
field: 'payment_method',
headerName: t('Payment Method'),
width: 180,
renderCell: (params) => {
return (
<div style={{ cursor: 'pointer' }}>
<PaymentText
othersText={t('Others')}
verbose={true}
size={24}
text={params.row.payment_method}
/>
</div>
);
},
},
{
field: 'price',
headerName: t('Price'),
type: 'number',
width: 140,
renderCell: (params) => {
const currencyCode = this.getCurrencyCode(params.row.currency);
return (
<div style={{ cursor: 'pointer' }}>
{pn(params.row.price) + ' ' + currencyCode + '/BTC'}
</div>
);
},
},
{
field: 'premium',
headerName: t('Premium'),
type: 'number',
width: 100,
renderCell: (params) => {
return (
<div style={{ cursor: 'pointer' }}>
{parseFloat(parseFloat(params.row.premium).toFixed(4)) + '%'}
</div>
);
},
},
]}
components={{
NoRowsOverlay: () => (
<Stack height='100%' alignItems='center' justifyContent='center'>
<div style={{ height: '220px' }} />
{this.NoOrdersFound()}
</Stack>
),
NoResultsOverlay: () => (
<Stack height='100%' alignItems='center' justifyContent='center'>
{t('Filter has no results')}
</Stack>
),
}}
pageSize={this.props.bookLoading ? 0 : this.state.pageSize}
rowsPerPageOptions={[0, 6, 20, 50]}
onPageSizeChange={(newPageSize) => this.setState({ pageSize: newPageSize })}
onRowClick={(params) => this.handleRowClick(params.row.id)} // Whole row is clickable, but the mouse only looks clickly in some places.
/>
</div>
);
};
bookListTablePhone = () => {
const { t } = this.props;
return (
<div style={{ height: 424, width: '100%' }}>
<DataGrid
localeText={this.dataGridLocaleText()}
loading={this.props.bookLoading}
rows={this.props.bookOrders.filter(
(order) =>
(order.type == this.props.type || this.props.type == null) &&
(order.currency == this.props.currency || this.props.currency == 0),
)}
columns={[
// { field: 'id', headerName: 'ID', width: 40 },
{
field: 'maker_nick',
headerName: t('Robot'),
width: 64,
renderCell: (params) => {
return (
<div style={{ position: 'relative', left: '-5px' }}>
<RobotAvatar
nickname={params.row.maker_nick}
smooth={true}
style={{ width: 45, height: 45 }}
orderType={params.row.type}
statusColor={this.statusBadgeColor(params.row.maker_status)}
tooltip={t(params.row.maker_status)}
/>
</div>
);
},
},
{
field: 'amount',
headerName: t('Amount'),
type: 'number',
width: 84,
renderCell: (params) => {
return (
<Tooltip
placement='right'
enterTouchDelay={0}
title={t(params.row.type ? 'Seller' : 'Buyer')}
>
<div style={{ cursor: 'pointer' }}>
{amountToString(
params.row.amount,
params.row.has_range,
params.row.min_amount,
params.row.max_amount,
)}
</div>
</Tooltip>
);
},
},
{
field: 'currency',
headerName: t('Currency'),
width: 85,
renderCell: (params) => {
const currencyCode = this.getCurrencyCode(params.row.currency);
return (
<div
style={{
cursor: 'pointer',
display: 'flex',
alignItems: 'center',
flexWrap: 'wrap',
}}
>
{currencyCode + ' '}
<FlagWithProps code={currencyCode} />
</div>
);
},
},
{ field: 'payment_method', headerName: t('Payment Method'), width: 180, hide: 'true' },
{
field: 'payment_icons',
headerName: t('Pay'),
width: 75,
renderCell: (params) => {
return (
<div
style={{
position: 'relative',
left: '-4px',
cursor: 'pointer',
align: 'center',
}}
>
<PaymentText
othersText={t('Others')}
size={16}
text={params.row.payment_method}
/>
</div>
);
},
},
{
field: 'price',
headerName: t('Price'),
type: 'number',
width: 140,
hide: 'true',
renderCell: (params) => {
return (
<div style={{ cursor: 'pointer' }}>
{pn(params.row.price) + ' ' + params.row.currency + '/BTC'}
</div>
);
},
},
{
field: 'premium',
headerName: t('Premium'),
type: 'number',
width: 85,
renderCell: (params) => {
return (
<Tooltip
placement='left'
enterTouchDelay={0}
title={pn(params.row.price) + ' ' + params.row.currency + '/BTC'}
>
<div style={{ cursor: 'pointer' }}>
{parseFloat(parseFloat(params.row.premium).toFixed(4)) + '%'}
</div>
</Tooltip>
);
},
},
]}
components={{
NoRowsOverlay: () => (
<Stack height='100%' alignItems='center' justifyContent='center'>
<div style={{ height: '220px' }} />
{this.NoOrdersFound()}
</Stack>
),
NoResultsOverlay: () => (
<Stack height='100%' alignItems='center' justifyContent='center'>
{t('Local filter returns no result')}
</Stack>
),
}}
pageSize={this.props.bookLoading ? 0 : this.state.pageSize}
rowsPerPageOptions={[0, 6, 20, 50]}
onPageSizeChange={(newPageSize) => this.setState({ pageSize: newPageSize })}
onRowClick={(params) => this.handleRowClick(params.row.id)} // Whole row is clickable, but the mouse only looks clickly in some places.
/>
</div>
);
};
handleTypeChange = (mouseEvent, val) => {
this.props.setAppState({ type: val });
};
@ -495,45 +112,32 @@ class BookPage extends Component {
return this.NoOrdersFound();
}
const components =
this.state.view == 'depth'
? [
<DepthChart
bookLoading={this.props.bookLoading}
orders={this.props.bookOrders}
lastDayPremium={this.props.lastDayPremium}
currency={this.props.currency}
setAppState={this.props.setAppState}
limits={this.props.limits}
/>,
<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}
/>,
]
: [this.bookListTableDesktop(), this.bookListTablePhone()];
return (
<>
{/* Desktop */}
<MediaQuery minWidth={930}>
<Paper elevation={0} style={{ width: 925, maxHeight: 510, overflow: 'auto' }}>
<div style={{ height: 424, width: '100%' }}>{components[0]}</div>
</Paper>
</MediaQuery>
{/* Smartphone */}
<MediaQuery maxWidth={929}>
<Paper elevation={0} style={{ width: 395, maxHeight: 460, overflow: 'auto' }}>
<div style={{ height: 424, width: '100%' }}>{components[1]}</div>
</Paper>
</MediaQuery>
</>
);
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={(this.props.windowWidth / this.props.theme.typography.fontSize) * 0.8} // EM units
maxHeight={(this.props.windowHeight / this.props.theme.typography.fontSize) * 0.8 - 11} // EM units
/>
);
} else {
return (
<BookTable
loading={this.props.bookLoading}
orders={this.props.bookOrders}
type={this.props.type}
currency={this.props.currency}
maxWidth={(this.props.windowWidth / this.props.theme.typography.fontSize) * 0.97} // EM units
maxHeight={(this.props.windowHeight / this.props.theme.typography.fontSize) * 0.8 - 11} // EM units
/>
);
}
};
getTitle = () => {
@ -562,7 +166,7 @@ class BookPage extends Component {
<Grid className='orderBook' container spacing={1} sx={{ minWidth: 400 }}>
<IconButton
sx={{ position: 'fixed', right: '0px', top: '30px' }}
onClick={() => this.setState({ loading: true }) & this.getOrderDetails(2, 0)}
onClick={() => this.setState({ loading: true }) & this.getOrderDetails()}
>
<Refresh />
</IconButton>
@ -631,15 +235,11 @@ class BookPage extends Component {
</Select>
</FormControl>
</Grid>
{this.props.bookNotFound ? (
<></>
) : (
<Grid item xs={12} align='center'>
<Typography component='h5' variant='h5'>
{this.getTitle()}
</Typography>
</Grid>
)}
<Grid item xs={12} align='center'>
<Typography component='h5' variant='h5'>
{this.getTitle()}
</Typography>
</Grid>
<Grid item xs={12} align='center'>
{this.mainView()}
</Grid>

View File

@ -0,0 +1,598 @@
import React, { useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useHistory } from 'react-router-dom';
import {
Box,
Typography,
Paper,
Stack,
ListItemButton,
ListItemText,
ListItemAvatar,
useTheme,
CircularProgress,
} from '@mui/material';
import { DataGrid } from '@mui/x-data-grid';
import currencyDict from '../../static/assets/currencies.json';
import { Order } from '../models/Order.model';
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';
interface Props {
loading: boolean;
orders: Order[];
type: number;
currency: number;
maxWidth: number;
maxHeight: number;
}
const BookTable = ({
loading,
orders,
type,
currency,
maxWidth,
maxHeight,
}: Props): JSX.Element => {
const { t } = useTranslation();
const theme = useTheme();
const history = useHistory();
const fontSize = theme.typography.fontSize;
// all sizes in 'em'
const verticalHeightFrame = 6.9075;
const verticalHeightRow = 3.25;
const defaultPageSize = Math.max(
Math.floor((maxHeight - verticalHeightFrame) / verticalHeightRow),
1,
);
const height = defaultPageSize * verticalHeightRow + verticalHeightFrame;
const [pageSize, setPageSize] = useState(0);
const [useDefaultPageSize, setUseDefaultPageSize] = useState(true);
useEffect(() => {
if (useDefaultPageSize) {
setPageSize(defaultPageSize);
}
});
const premiumColor = function (baseColor: string, accentColor: string, point: number) {
const baseRGB = hexToRgb(baseColor);
const accentRGB = hexToRgb(accentColor);
const redDiff = accentRGB[0] - baseRGB[0];
const red = baseRGB[0] + redDiff * point;
const greenDiff = accentRGB[1] - baseRGB[1];
const green = baseRGB[1] + greenDiff * point;
const blueDiff = accentRGB[2] - baseRGB[2];
const blue = baseRGB[2] + blueDiff * point;
return `rgb(${Math.round(red)}, ${Math.round(green)}, ${Math.round(blue)}, ${
0.7 + point * 0.3
})`;
};
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'),
toolbarColumnsLabel: t('Select columns'),
columnsPanelTextFieldLabel: t('Find column'),
columnsPanelTextFieldPlaceholder: t('Column title'),
columnsPanelDragIconLabel: t('Reorder column'),
columnsPanelShowAllButton: t('Show all'),
columnsPanelHideAllButton: t('Hide all'),
filterPanelAddFilter: t('Add filter'),
filterPanelDeleteIconLabel: t('Delete'),
filterPanelLinkOperator: t('Logic operator'),
filterPanelOperators: t('Operator'),
filterPanelOperatorAnd: t('And'),
filterPanelOperatorOr: t('Or'),
filterPanelColumns: t('Columns'),
filterPanelInputLabel: t('Value'),
filterPanelInputPlaceholder: t('Filter value'),
filterOperatorContains: t('contains'),
filterOperatorEquals: t('equals'),
filterOperatorStartsWith: t('starts with'),
filterOperatorEndsWith: t('ends with'),
filterOperatorIs: t('is'),
filterOperatorNot: t('is not'),
filterOperatorAfter: t('is after'),
filterOperatorOnOrAfter: t('is on or after'),
filterOperatorBefore: t('is before'),
filterOperatorOnOrBefore: t('is on or before'),
filterOperatorIsEmpty: t('is empty'),
filterOperatorIsNotEmpty: t('is not empty'),
filterOperatorIsAnyOf: t('is any of'),
filterValueAny: t('any'),
filterValueTrue: t('true'),
filterValueFalse: t('false'),
columnMenuLabel: t('Menu'),
columnMenuShowColumns: t('Show columns'),
columnMenuFilter: t('Filter'),
columnMenuHideColumn: t('Hide'),
columnMenuUnsort: t('Unsort'),
columnMenuSortAsc: t('Sort by ASC'),
columnMenuSortDesc: t('Sort by DESC'),
columnHeaderFiltersLabel: t('Show filters'),
columnHeaderSortIconLabel: t('Sort'),
booleanCellTrueLabel: t('yes'),
booleanCellFalseLabel: t('no'),
};
const robotObj = function (width: number, hide: boolean) {
return {
hide,
field: 'maker_nick',
headerName: t('Robot'),
width: width * fontSize,
renderCell: (params) => {
return (
<ListItemButton style={{ cursor: 'pointer', position: 'relative', left: '-1.3em' }}>
<ListItemAvatar>
<RobotAvatar
nickname={params.row.maker_nick}
style={{ width: '3.215em', height: '3.215em' }}
smooth={true}
orderType={params.row.type}
statusColor={statusBadgeColor(params.row.maker_status)}
tooltip={t(params.row.maker_status)}
/>
</ListItemAvatar>
<ListItemText primary={params.row.maker_nick} />
</ListItemButton>
);
},
};
};
const robotSmallObj = function (width: number, hide: boolean) {
return {
hide,
field: 'maker_nick',
headerName: t('Robot'),
width: width * fontSize,
renderCell: (params) => {
return (
<div style={{ position: 'relative', left: '-1.5em' }}>
<ListItemButton style={{ cursor: 'pointer' }}>
<RobotAvatar
nickname={params.row.maker_nick}
smooth={true}
style={{ width: '3.215em', height: '3.215em' }}
orderType={params.row.type}
statusColor={statusBadgeColor(params.row.maker_status)}
tooltip={t(params.row.maker_status)}
/>
</ListItemButton>
</div>
);
},
};
};
const typeObj = function (width: number, hide: boolean) {
return {
hide,
field: 'type',
headerName: t('Is'),
width: width * fontSize,
renderCell: (params) => (params.row.type ? t('Seller') : t('Buyer')),
};
};
const amountObj = function (width: number, hide: boolean) {
return {
hide,
field: 'amount',
headerName: t('Amount'),
type: 'number',
width: width * fontSize,
renderCell: (params) => {
return (
<div style={{ cursor: 'pointer' }}>
{amountToString(
params.row.amount,
params.row.has_range,
params.row.min_amount,
params.row.max_amount,
)}
</div>
);
},
};
};
const currencyObj = function (width: number, hide: boolean) {
return {
hide,
field: 'currency',
headerName: t('Currency'),
width: width * fontSize,
renderCell: (params) => {
const currencyCode = currencyDict[params.row.currency.toString()];
return (
<div
style={{
cursor: 'pointer',
display: 'flex',
alignItems: 'center',
flexWrap: 'wrap',
}}
>
{currencyCode + ' '}
<FlagWithProps code={currencyCode} />
</div>
);
},
};
};
const paymentObj = function (width: number, hide: boolean) {
return {
hide,
field: 'payment_method',
headerName: t('Payment Method'),
width: width * fontSize,
renderCell: (params) => {
return (
<div style={{ cursor: 'pointer' }}>
<PaymentText
othersText={t('Others')}
verbose={true}
size={1.7 * fontSize}
text={params.row.payment_method}
/>
</div>
);
},
};
};
const paymentSmallObj = function (width: number, hide: boolean) {
return {
hide,
field: 'payment_icons',
headerName: t('Pay'),
width: width * fontSize,
renderCell: (params) => {
return (
<div
style={{
position: 'relative',
left: '-4px',
cursor: 'pointer',
align: 'center',
}}
>
<PaymentText
othersText={t('Others')}
size={1.3 * fontSize}
text={params.row.payment_method}
/>
</div>
);
},
};
};
const priceObj = function (width: number, hide: boolean) {
return {
hide,
field: 'price',
headerName: t('Price'),
type: 'number',
width: width * fontSize,
renderCell: (params) => {
const currencyCode = currencyDict[params.row.currency.toString()];
return (
<div style={{ cursor: 'pointer' }}>{`${pn(params.row.price)} ${currencyCode}/BTC`}</div>
);
},
};
};
const premiumObj = function (width: number, hide: boolean) {
// coloring premium texts based on 4 params:
// Hardcoded: a sell order at 0% is an outstanding premium
// Hardcoded: a buy order at 10% is an outstanding premium
const sellStandardPremium = 10;
const buyOutstandingPremium = 10;
return {
hide,
field: 'premium',
headerName: t('Premium'),
type: 'number',
width: width * fontSize,
renderCell: (params) => {
let fontColor = `rgb(0,0,0)`;
if (params.row.type === 0) {
var premiumPoint = params.row.premium / buyOutstandingPremium;
premiumPoint = premiumPoint < 0 ? 0 : premiumPoint > 1 ? 1 : premiumPoint;
fontColor = premiumColor(
theme.palette.text.primary,
theme.palette.secondary.dark,
premiumPoint,
);
} else {
var premiumPoint = (sellStandardPremium - params.row.premium) / sellStandardPremium;
premiumPoint = premiumPoint < 0 ? 0 : premiumPoint > 1 ? 1 : premiumPoint;
fontColor = premiumColor(
theme.palette.text.primary,
theme.palette.primary.dark,
premiumPoint,
);
}
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>
);
},
};
};
const timerObj = function (width: number, hide: boolean) {
return {
hide,
field: 'escrow_duration',
headerName: t('Timer'),
type: 'number',
width: width * fontSize,
renderCell: (params) => {
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>;
},
};
};
const expiryObj = function (width: number, hide: boolean) {
return {
hide,
field: 'expires_at',
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());
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);
return (
<Box sx={{ position: 'relative', display: 'inline-flex', left: '0.3em' }}>
<CircularProgress
value={percent}
color={percent < 15 ? 'error' : percent < 30 ? 'warning' : 'success'}
thickness={0.35 * fontSize}
size={2.5 * fontSize}
variant='determinate'
/>
<Box
sx={{
top: 0,
left: 0,
bottom: 0,
right: 0,
position: 'absolute',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
}}
>
<Typography variant='caption' component='div' color='text.secondary'>
{hours > 0 ? `${hours}h` : `${minutes}m`}
</Typography>
</Box>
</Box>
);
},
};
};
const satoshisObj = function (width: number, hide: boolean) {
return {
hide,
field: 'satoshis_now',
headerName: t('Sats now'),
type: 'number',
width: width * fontSize,
renderCell: (params) => {
return (
<div style={{ cursor: 'pointer' }}>
{`${pn(Math.round(params.row.satoshis_now / 1000))}K`}
</div>
);
},
};
};
const idObj = function (width: number, hide: boolean) {
return {
hide,
field: 'id',
headerName: 'Order ID',
width: width * fontSize,
renderCell: (params) => {
return (
<div style={{ cursor: 'pointer' }}>
<Typography variant='caption' color='text.secondary'>
{`#${params.row.id}`}
</Typography>
</div>
);
},
};
};
const columnSpecs = {
amount: {
priority: 1,
order: 4,
normal: {
width: 6.5,
object: amountObj,
},
},
currency: {
priority: 2,
order: 5,
normal: {
width: 5.8,
object: currencyObj,
},
},
premium: {
priority: 3,
order: 11,
normal: {
width: 6,
object: premiumObj,
},
},
robot: {
priority: 4,
order: 1,
normal: {
width: 17.14,
object: robotObj,
},
small: {
width: 4.3,
object: robotSmallObj,
},
},
paymentMethod: {
priority: 5,
order: 6,
normal: {
width: 12.85,
object: paymentObj,
},
small: {
width: 5.8,
object: paymentSmallObj,
},
},
price: {
priority: 6,
order: 10,
normal: {
width: 10,
object: priceObj,
},
},
expires_at: {
priority: 7,
order: 7,
normal: {
width: 5.8,
object: expiryObj,
},
},
escrow_duration: {
priority: 8,
order: 8,
normal: {
width: 3.8,
object: timerObj,
},
},
satoshisNow: {
priority: 9,
order: 9,
normal: {
width: 6,
object: satoshisObj,
},
},
type: {
priority: 10,
order: 2,
normal: {
width: 4.3,
object: typeObj,
},
},
id: {
priority: 11,
order: 12,
normal: {
width: 4.8,
object: idObj,
},
},
};
const filteredColumns = function (maxWidth: number) {
const useSmall = maxWidth < 70;
const selectedColumns: object[] = [];
let width: number = 0;
for (const [key, value] of Object.entries(columnSpecs)) {
const colWidth = useSmall && value.small ? value.small.width : value.normal.width;
const colObject = useSmall && value.small ? value.small.object : value.normal.object;
if (width + colWidth < maxWidth || selectedColumns.length < 2) {
width = width + colWidth;
selectedColumns.push([colObject(colWidth, false), value.order]);
} else {
selectedColumns.push([colObject(colWidth, true), value.order]);
}
}
// sort columns by column.order value
selectedColumns.sort(function (first, second) {
return first[1] - second[1];
});
const columns = selectedColumns.map(function (item) {
return item[0];
});
return [columns, width * 0.875 + 0.15];
};
const [columns, width] = filteredColumns(maxWidth);
return (
<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),
)}
loading={loading}
columns={columns}
components={{
NoResultsOverlay: () => (
<Stack height='100%' alignItems='center' justifyContent='center'>
{t('Filter has no results')}
</Stack>
),
}}
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.
/>
</Paper>
);
};
export default BookTable;

View File

@ -30,6 +30,7 @@ import PaymentText from '../../PaymentText';
import getNivoScheme from '../NivoScheme';
import median from '../../../utils/match';
import { apiClient } from '../../../services/api/index';
import statusBadgeColor from '../../../utils/statusBadgeColor';
interface DepthChartProps {
bookLoading: boolean;
@ -38,7 +39,8 @@ interface DepthChartProps {
currency: number;
setAppState: (state: object) => void;
limits: LimitList;
compact?: boolean;
maxWidth: number;
maxHeight: number;
}
const DepthChart: React.FC<DepthChartProps> = ({
@ -48,7 +50,8 @@ const DepthChart: React.FC<DepthChartProps> = ({
currency,
setAppState,
limits,
compact,
maxWidth,
maxHeight,
}) => {
const { t } = useTranslation();
const history = useHistory();
@ -61,6 +64,9 @@ const DepthChart: React.FC<DepthChartProps> = ({
const [currencyCode, setCurrencyCode] = useState<number>(1);
const [center, setCenter] = useState<number>();
const height = maxHeight < 20 ? 20 : maxHeight;
const width = maxWidth < 20 ? 20 : maxWidth > 72.8 ? 72.8 : maxWidth;
useEffect(() => {
if (Object.keys(limits).length === 0) {
apiClient.get('/api/limits/').then((data) => {
@ -216,17 +222,6 @@ const DepthChart: React.FC<DepthChartProps> = ({
/>
);
const statusBadgeColor = (status: string) => {
if (status === 'Active') {
return 'success';
}
if (status === 'Seen recently') {
return 'warning';
}
return 'error';
};
const generateTooltip: React.FunctionComponent<PointTooltipProps> = (
pointTooltip: PointTooltipProps,
) => {
@ -293,94 +288,109 @@ const DepthChart: React.FC<DepthChartProps> = ({
history.push('/order/' + point.data?.order?.id);
};
return bookLoading || center == undefined || enrichedOrders.length < 1 ? (
<div style={{ display: 'flex', justifyContent: 'center', paddingTop: 200, height: 420 }}>
<CircularProgress />
</div>
) : (
<Grid container style={{ paddingTop: 15 }}>
<Grid
container
direction='row'
justifyContent='space-around'
alignItems='flex-start'
style={{ position: 'absolute' }}
>
<Grid
container
justifyContent='flex-start'
alignItems='flex-start'
style={{ paddingLeft: 20 }}
>
<Select variant='standard' value={xType} onChange={(e) => setXType(e.target.value)}>
<MenuItem value={'premium'}>
<div style={{ display: 'flex', alignItems: 'center', flexWrap: 'wrap' }}>
{t('Premium')}
</div>
</MenuItem>
<MenuItem value={'base_amount'}>
<div style={{ display: 'flex', alignItems: 'center', flexWrap: 'wrap' }}>
{t('Price')}
</div>
</MenuItem>
</Select>
</Grid>
</Grid>
<Grid container direction='row' justifyContent='center' alignItems='center'>
<Grid container justifyContent='center' alignItems='center'>
<Grid item>
<IconButton onClick={() => setXRange(xRange + rangeSteps)}>
<RemoveCircleOutline />
</IconButton>
return (
<Paper style={{ width: `${width}em`, maxHeight: `${height}em` }}>
<Paper variant='outlined'>
{bookLoading || center == undefined || enrichedOrders.length < 1 ? (
<div
style={{
display: 'flex',
justifyContent: 'center',
paddingTop: `${(height - 3) / 2 - 1}em`,
height: `${height - 3}em`,
}}
>
<CircularProgress />
</div>
) : (
<Grid container style={{ paddingTop: 15 }}>
<Grid
container
direction='row'
justifyContent='space-around'
alignItems='flex-start'
style={{ position: 'absolute' }}
>
<Grid
container
justifyContent='flex-start'
alignItems='flex-start'
style={{ paddingLeft: 20 }}
>
<Select variant='standard' value={xType} onChange={(e) => setXType(e.target.value)}>
<MenuItem value={'premium'}>
<div style={{ display: 'flex', alignItems: 'center', flexWrap: 'wrap' }}>
{t('Premium')}
</div>
</MenuItem>
<MenuItem value={'base_amount'}>
<div style={{ display: 'flex', alignItems: 'center', flexWrap: 'wrap' }}>
{t('Price')}
</div>
</MenuItem>
</Select>
</Grid>
</Grid>
<Grid container direction='row' justifyContent='center' alignItems='center'>
<Grid container justifyContent='center' alignItems='center'>
<Grid item>
<IconButton onClick={() => setXRange(xRange + rangeSteps)}>
<RemoveCircleOutline />
</IconButton>
</Grid>
<Grid item>
<Box justifyContent='center'>
{xType === 'base_amount'
? `${center} ${currencyDict[currencyCode]}`
: `${center}%`}
</Box>
</Grid>
<Grid item>
<IconButton onClick={() => setXRange(xRange - rangeSteps)} disabled={xRange <= 1}>
<AddCircleOutline />
</IconButton>
</Grid>
</Grid>
</Grid>
<Grid container style={{ height: `${height - 7}em`, padding: 15 }}>
<ResponsiveLine
data={series}
enableArea={true}
useMesh={true}
animate={false}
crosshairType='cross'
tooltip={generateTooltip}
onClick={handleOnClick}
axisRight={{
tickSize: 5,
format: formatAxisY,
}}
axisLeft={{
tickSize: 5,
format: formatAxisY,
}}
axisBottom={{
tickSize: 5,
tickRotation: xType === 'base_amount' && width < 40 ? 45 : 0,
format: formatAxisX,
}}
margin={{ left: 65, right: 60, bottom: width < 40 ? 36 : 25, top: 10 }}
xFormat={(value) => Number(value).toFixed(0)}
lineWidth={3}
theme={getNivoScheme(theme)}
colors={[theme.palette.secondary.main, theme.palette.primary.main]}
xScale={{
type: 'linear',
min: center - xRange,
max: center + xRange,
}}
layers={['axes', 'areas', 'crosshair', 'lines', centerLine, 'slices', 'mesh']}
/>
</Grid>
</Grid>
<Grid item>
<Box justifyContent='center'>
{xType === 'base_amount' ? `${center} ${currencyDict[currencyCode]}` : `${center}%`}
</Box>
</Grid>
<Grid item>
<IconButton onClick={() => setXRange(xRange - rangeSteps)} disabled={xRange <= 1}>
<AddCircleOutline />
</IconButton>
</Grid>
</Grid>
</Grid>
<Grid container style={{ height: 357, padding: 15 }}>
<ResponsiveLine
data={series}
enableArea={true}
useMesh={true}
animate={false}
crosshairType='cross'
tooltip={generateTooltip}
onClick={handleOnClick}
axisRight={{
tickSize: 5,
format: formatAxisY,
}}
axisLeft={{
tickSize: 5,
format: formatAxisY,
}}
axisBottom={{
tickSize: 5,
tickRotation: xType === 'base_amount' && compact ? 45 : 0,
format: formatAxisX,
}}
margin={{ left: 65, right: 60, bottom: compact ? 36 : 25, top: 10 }}
xFormat={(value) => Number(value).toFixed(0)}
lineWidth={3}
theme={getNivoScheme(theme)}
colors={[theme.palette.secondary.main, theme.palette.primary.main]}
xScale={{
type: 'linear',
min: center - xRange,
max: center + xRange,
}}
layers={['axes', 'areas', 'crosshair', 'lines', centerLine, 'slices', 'mesh']}
/>
</Grid>
</Grid>
)}
</Paper>
</Paper>
);
};

View File

@ -46,7 +46,7 @@ const UpdateClientDialog = ({
<Typography>
{t(
'The RoboSats coordinator is on version {{coordinatorVersion}}, but your client app is {{clientVersion}}. This version mismatch might lead to a bad user experience.',
{ coordinatorVersion: coordinatorVersion, clientVersion: clientVersion },
{ coordinatorVersion, clientVersion },
)}
</Typography>
@ -63,7 +63,7 @@ const UpdateClientDialog = ({
<ListItemText
secondary={t('Download RoboSats {{coordinatorVersion}} APK from Github releases', {
coordinatorVersion: coordinatorVersion,
coordinatorVersion,
})}
primary={t('On Android RoboSats app ')}
/>

View File

@ -31,6 +31,23 @@ export default class HomePage extends Component {
};
}
componentDidMount = () => {
if (typeof window !== undefined) {
this.setState({ windowWidth: window.innerWidth, windowHeight: window.innerHeight });
window.addEventListener('resize', this.onResize);
}
};
componentWillUnmount = () => {
if (typeof window !== undefined) {
window.removeEventListener('resize', this.onResize);
}
};
onResize = () => {
this.setState({ windowWidth: window.innerWidth, windowHeight: window.innerHeight });
};
setAppState = (newState) => {
this.setState(newState);
};
@ -106,7 +123,7 @@ export default class HomePage extends Component {
</div>
<div
className='bottomBar'
style={{ height: `${40 * fontSizeFactor}px`, width: window.innerWidth }}
style={{ height: `${40 * fontSizeFactor}px`, width: this.state.windowWidth }}
>
<BottomBar
redirectTo={this.redirectTo}

View File

@ -56,6 +56,7 @@ import { copyToClipboard } from '../utils/clipboard';
import { getWebln } from '../utils/webln';
import { apiClient } from '../services/api';
import RobotAvatar from './Robots/RobotAvatar';
import statusBadgeColor from '../utils/statusBadgeColor';
class OrderPage extends Component {
constructor(props) {
@ -666,19 +667,6 @@ class OrderPage extends Component {
return null;
};
// Colors for the status badges
statusBadgeColor(status) {
if (status === 'Active') {
return 'success';
}
if (status === 'Seen recently') {
return 'warning';
}
if (status === 'Inactive') {
return 'error';
}
}
orderBox = () => {
const { t } = this.props;
return (
@ -694,7 +682,7 @@ class OrderPage extends Component {
<ListItem>
<ListItemAvatar sx={{ width: 56, height: 56 }}>
<RobotAvatar
statusColor={this.statusBadgeColor(this.state.maker_status)}
statusColor={statusBadgeColor(this.state.maker_status)}
nickname={this.state.maker_nick}
tooltip={t(this.state.maker_status)}
orderType={this.state.type}
@ -726,7 +714,7 @@ class OrderPage extends Component {
<ListItemAvatar>
<RobotAvatar
avatarClass='smallAvatar'
statusColor={this.statusBadgeColor(this.state.taker_status)}
statusColor={statusBadgeColor(this.state.taker_status)}
nickname={this.state.taker_nick}
tooltip={t(this.state.taker_status)}
orderType={this.state.type === 0 ? 1 : 0}

View File

@ -4,7 +4,7 @@ import { Avatar, Badge, Tooltip } from '@mui/material';
import { useTranslation } from 'react-i18next';
import { SendReceiveIcon } from '../../Icons';
interface DepthChartProps {
interface Props {
nickname: string;
smooth?: boolean;
style?: object;
@ -15,7 +15,7 @@ interface DepthChartProps {
onLoad?: () => void;
}
const RobotAvatar: React.FC<DepthChartProps> = ({
const RobotAvatar: React.FC<Props> = ({
nickname,
orderType,
statusColor,

View File

@ -15,8 +15,8 @@ export const checkVer: (
const patchAvailable = !updateAvailable && patch > Number(semver[2]);
return {
updateAvailable: updateAvailable,
patchAvailable: patchAvailable,
updateAvailable,
patchAvailable,
coordinatorVersion: `v${major}.${minor}.${patch}`,
clientVersion: `v${semver[0]}.${semver[1]}.${semver[2]}`,
};

View File

@ -0,0 +1,14 @@
export default function hexToRgb(c) {
if (c.includes('rgb')) {
const vals = c.split('(')[1].split(')')[0];
return vals.split(',');
}
if (/^#([a-f0-9]{3}){1,2}$/.test(c)) {
if (c.length == 4) {
c = '#' + [c[1], c[1], c[2], c[2], c[3], c[3]].join('');
}
c = '0x' + c.substring(1);
return [(c >> 16) & 255, (c >> 8) & 255, c & 255];
}
return '';
}

View File

@ -0,0 +1,9 @@
export default function statusBadgeColor(status: string) {
if (status === 'Active') {
return 'success';
}
if (status === 'Seen recently') {
return 'warning';
}
return 'error';
}

View File

@ -169,11 +169,12 @@ input[type='number'] {
width: auto !important;
}
@media (max-width: 929px) {
@media (max-height: 725px) {
.appCenter:has(> div.MuiGrid-root:first-child, > div.MuiBox-root:first-child) {
overflow-y: scroll;
margin-top: 12px;
padding-bottom: 25px;
overflow-y: auto;
margin-top: 1em;
padding-bottom: 3em;
height: 100%;
width: 100%;
}
}