Reckless_Satoshi 741fba11d5 Add swappable baseurls (#316)
* Add swappable baseurls (network and coordinators)

* Add testnet bitcoin styles

* Small fixes

* Small fixes
2022-11-07 16:45:05 +00:00

802 lines
23 KiB
TypeScript

import React, { useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useHistory } from 'react-router-dom';
import {
Box,
Grid,
Dialog,
Typography,
Paper,
ListItemButton,
ListItemText,
ListItemAvatar,
useTheme,
CircularProgress,
LinearProgress,
IconButton,
Tooltip,
} from '@mui/material';
import { DataGrid, GridPagination } from '@mui/x-data-grid';
import currencyDict from '../../../static/assets/currencies.json';
import { Book, Favorites } from '../../models';
import { filterOrders, hexToRgb, statusBadgeColor, pn, amountToString } from '../../utils';
import BookControl from './BookControl';
import { FlagWithProps } from '../Icons';
import { PaymentStringAsIcons } from '../PaymentMethods';
import RobotAvatar from '../RobotAvatar';
// Icons
import { Fullscreen, FullscreenExit, Refresh } from '@mui/icons-material';
interface BookTableProps {
clickRefresh?: () => void;
book: Book;
fav?: Favorites;
maxWidth: number;
maxHeight: number;
fullWidth?: number;
fullHeight?: number;
elevation?: number;
defaultFullscreen?: boolean;
fillContainer?: boolean;
showControls?: boolean;
showFooter?: boolean;
showNoResults?: boolean;
onCurrencyChange?: (e: any) => void;
onTypeChange?: (mouseEvent: any, val: number) => void;
onOrderClicked?: (id: number) => void;
baseUrl: string;
}
const BookTable = ({
clickRefresh,
book,
fav = { currency: 1, type: 0 },
maxWidth = 100,
maxHeight = 70,
fullWidth = 100,
fullHeight = 70,
defaultFullscreen = false,
elevation = 6,
fillContainer = false,
showControls = true,
showFooter = true,
showNoResults = true,
onCurrencyChange,
onTypeChange,
onOrderClicked = () => null,
baseUrl,
}: BookTableProps): JSX.Element => {
const { t } = useTranslation();
const theme = useTheme();
const history = useHistory();
const [pageSize, setPageSize] = useState(0);
const [fullscreen, setFullscreen] = useState(defaultFullscreen);
const [paymentMethods, setPaymentMethods] = useState<string[]>([]);
// all sizes in 'em'
const fontSize = theme.typography.fontSize;
const verticalHeightFrame = 3.25 + (showControls ? 3.7 : 0) + (showFooter ? 2.35 : 0);
const verticalHeightRow = 3.25;
const defaultPageSize = Math.max(
Math.floor(
((fullscreen ? fullHeight * 0.9 : maxHeight) - verticalHeightFrame) / verticalHeightRow,
),
1,
);
const height = defaultPageSize * verticalHeightRow + verticalHeightFrame;
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:') },
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: any) => {
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}
flipHorizontally={true}
orderType={params.row.type}
statusColor={statusBadgeColor(params.row.maker_status)}
tooltip={t(params.row.maker_status)}
baseUrl={baseUrl}
/>
</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: any) => {
return (
<div style={{ position: 'relative', left: '-1.64em' }}>
<ListItemButton style={{ cursor: 'pointer' }}>
<RobotAvatar
nickname={params.row.maker_nick}
smooth={true}
flipHorizontally={true}
style={{ width: '3.215em', height: '3.215em' }}
orderType={params.row.type}
statusColor={statusBadgeColor(params.row.maker_status)}
tooltip={t(params.row.maker_status)}
baseUrl={baseUrl}
/>
</ListItemButton>
</div>
);
},
};
};
const typeObj = function (width: number, hide: boolean) {
return {
hide,
field: 'type',
headerName: t('Is'),
width: width * fontSize,
renderCell: (params: any) => (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: any) => {
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: any) => {
const currencyCode = currencyDict[params.row.currency.toString()];
return (
<div
style={{
cursor: 'pointer',
display: 'flex',
alignItems: 'center',
flexWrap: 'wrap',
}}
>
{currencyCode}
<div style={{ width: '0.3em' }} />
<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: any) => {
return (
<div style={{ cursor: 'pointer' }}>
<PaymentStringAsIcons
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: any) => {
return (
<div
style={{
position: 'relative',
left: '-4px',
cursor: 'pointer',
}}
>
<PaymentStringAsIcons
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: any) => {
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: 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;
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 (
<Tooltip
placement='left'
enterTouchDelay={0}
title={pn(params.row.price) + ' ' + currencyCode + '/BTC'}
>
<div style={{ cursor: 'pointer' }}>
<Typography variant='inherit' color={fontColor} sx={{ fontWeight }}>
{parseFloat(parseFloat(params.row.premium).toFixed(4)) + '%'}
</Typography>
</div>
</Tooltip>
);
},
};
};
const timerObj = function (width: number, hide: boolean) {
return {
hide,
field: 'escrow_duration',
headerName: t('Timer'),
type: 'number',
width: width * fontSize,
renderCell: (params: any) => {
const hours = Math.round(params.row.escrow_duration / 3600);
const minutes = Math.round((params.row.escrow_duration - hours * 3600) / 60);
return <div style={{ cursor: 'pointer' }}>{hours > 0 ? `${hours}h` : `${minutes}m`}</div>;
},
};
};
const expiryObj = function (width: number, hide: boolean) {
return {
hide,
field: 'expires_at',
headerName: t('Expiry'),
type: 'string',
width: width * fontSize,
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);
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: any) => {
return (
<div style={{ cursor: 'pointer' }}>
{params.row.satoshis_now > 1000000
? `${pn(Math.round(params.row.satoshis_now / 10000) / 100)} M`
: `${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: any) => {
return (
<div style={{ cursor: 'pointer' }}>
<Typography variant='caption' color='text.secondary'>
{`#${params.row.id}`}
</Typography>
</div>
);
},
};
};
const bondObj = function (width: number, hide: boolean) {
return {
hide,
field: 'bond_size',
headerName: t('Bond'),
type: 'number',
width: width * fontSize,
renderCell: (params: any) => {
return <div style={{ cursor: 'pointer' }}>{`${Number(params.row.bond_size)}%`}</div>;
},
};
};
const columnSpecs = {
amount: {
priority: 1,
order: 4,
normal: {
width: 6.5,
object: amountObj,
},
},
currency: {
priority: 2,
order: 5,
normal: {
width: 5.9,
object: currencyObj,
},
},
premium: {
priority: 3,
order: 11,
normal: {
width: 6,
object: premiumObj,
},
},
paymentMethod: {
priority: 4,
order: 6,
normal: {
width: 12.85,
object: paymentObj,
},
small: {
width: 4.4,
object: paymentSmallObj,
},
},
robot: {
priority: 5,
order: 1,
normal: {
width: 17.14,
object: robotObj,
},
small: {
width: 4.1,
object: robotSmallObj,
},
},
price: {
priority: 6,
order: 10,
normal: {
width: 10,
object: priceObj,
},
},
expires_at: {
priority: 7,
order: 7,
normal: {
width: 5,
object: expiryObj,
},
},
escrow_duration: {
priority: 8,
order: 8,
normal: {
width: 4.8,
object: timerObj,
},
},
satoshisNow: {
priority: 9,
order: 9,
normal: {
width: 6,
object: satoshisObj,
},
},
type: {
priority: 10,
order: 2,
normal: {
width: 4.3,
object: typeObj,
},
},
bond: {
priority: 11,
order: 10,
normal: {
width: 4.2,
object: bondObj,
},
},
id: {
priority: 12,
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(fullscreen ? fullWidth : maxWidth);
const Footer = function () {
return (
<Grid container alignItems='center' direction='row' justifyContent='space-between'>
<Grid item>
<Grid container alignItems='center' direction='row'>
<Grid item xs={6}>
<IconButton onClick={() => setFullscreen(!fullscreen)}>
{fullscreen ? <FullscreenExit /> : <Fullscreen />}
</IconButton>
</Grid>
<Grid item xs={6}>
<IconButton onClick={clickRefresh}>
<Refresh />
</IconButton>
</Grid>
</Grid>
</Grid>
<Grid item>
<GridPagination />
</Grid>
</Grid>
);
};
interface GridComponentProps {
LoadingOverlay: JSX.Element;
NoResultsOverlay?: JSX.Element;
NoRowsOverlay?: JSX.Element;
Footer?: JSX.Element;
Toolbar?: JSX.Element;
}
const NoResultsOverlay = function () {
return (
<Grid
container
direction='column'
justifyContent='center'
alignItems='center'
sx={{ width: '100%', height: '100%' }}
>
<Grid item>
<Typography align='center' component='h5' variant='h5'>
{fav.type == 0
? t('No orders found to sell BTC for {{currencyCode}}', {
currencyCode:
fav.currency == 0 ? t('ANY') : currencyDict[fav.currency.toString()],
})
: t('No orders found to buy BTC for {{currencyCode}}', {
currencyCode:
fav.currency == 0 ? t('ANY') : currencyDict[fav.currency.toString()],
})}
</Typography>
</Grid>
<Grid item>
<Typography align='center' color='primary' variant='h6'>
{t('Be the first one to create an order')}
</Typography>
</Grid>
</Grid>
);
};
const Controls = function () {
return (
<BookControl
width={width}
type={fav.type}
currency={fav.currency}
onCurrencyChange={onCurrencyChange}
onTypeChange={onTypeChange}
paymentMethod={paymentMethods}
setPaymentMethods={setPaymentMethods}
/>
);
};
const gridComponents = function () {
const components: GridComponentProps = {
LoadingOverlay: LinearProgress,
};
if (showNoResults) {
components.NoResultsOverlay = NoResultsOverlay;
components.NoRowsOverlay = NoResultsOverlay;
}
if (showFooter) {
components.Footer = Footer;
}
if (showControls) {
components.Toolbar = Controls;
}
return components;
};
if (!fullscreen) {
return (
<Paper
elevation={elevation}
style={
fillContainer
? { width: '100%', height: '100%' }
: { width: `${width}em`, height: `${height}em`, overflow: 'auto' }
}
>
<DataGrid
localeText={localeText}
rowHeight={3.714 * theme.typography.fontSize}
headerHeight={3.25 * theme.typography.fontSize}
rows={
showControls
? filterOrders({
orders: book.orders,
baseFilter: fav,
paymentMethods,
})
: book.orders
}
loading={book.loading}
columns={columns}
hideFooter={!showFooter}
components={gridComponents()}
pageSize={book.loading && book.orders.length == 0 ? 0 : pageSize}
rowsPerPageOptions={width < 22 ? [] : [0, pageSize, defaultPageSize * 2, 50, 100]}
onPageSizeChange={(newPageSize) => {
setPageSize(newPageSize);
setUseDefaultPageSize(false);
}}
onRowClick={(params: any) => onOrderClicked(params.row.id)}
/>
</Paper>
);
} else {
return (
<Dialog open={fullscreen} fullScreen={true}>
<Paper style={{ width: '100%', height: '100%', overflow: 'auto' }}>
<DataGrid
localeText={localeText}
rowHeight={3.714 * theme.typography.fontSize}
headerHeight={3.25 * theme.typography.fontSize}
rows={
showControls
? filterOrders({
orders: book.orders,
baseFilter: fav,
paymentMethods,
})
: book.orders
}
loading={book.loading}
columns={columns}
hideFooter={!showFooter}
components={gridComponents()}
pageSize={book.loading && book.orders.length == 0 ? 0 : pageSize}
rowsPerPageOptions={[0, pageSize, defaultPageSize * 2, 50, 100]}
onPageSizeChange={(newPageSize) => {
setPageSize(newPageSize);
setUseDefaultPageSize(false);
}}
onRowClick={(params: any) => onOrderClicked(params.row.id)}
/>
</Paper>
</Dialog>
);
}
};
export default BookTable;