import React, { useEffect, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { useHistory } from 'react-router-dom'; import { Box, Grid, Dialog, Typography, Paper, Stack, ListItemButton, ListItemText, ListItemAvatar, useTheme, CircularProgress, LinearProgress, IconButton, } 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 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'; interface Props { loading: boolean; refreshing: boolean; clickRefresh: () => void; orders: Order[]; type: number; currency: number; maxWidth: number; maxHeight: number; fullWidth: number; fullHeight: number; defaultFullscreen: boolean; } const BookTable = ({ loading, refreshing, clickRefresh, orders, type, currency, maxWidth, maxHeight, fullWidth, fullHeight, defaultFullscreen, }: Props): JSX.Element => { const { t } = useTranslation(); const theme = useTheme(); const history = useHistory(); const [pageSize, setPageSize] = useState(0); const [fullscreen, setFullscreen] = useState(defaultFullscreen); // all sizes in 'em' const fontSize = theme.typography.fontSize; const verticalHeightFrame = 6.9075; const verticalHeightRow = 3.25; const defaultPageSize = Math.max( Math.floor( ((fullscreen ? fullHeight * 0.875 : 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:') }, 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 ( ); }, }; }; const robotSmallObj = function (width: number, hide: boolean) { return { hide, field: 'maker_nick', headerName: t('Robot'), width: width * fontSize, renderCell: (params) => { return (
); }, }; }; 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 (
{amountToString( params.row.amount, params.row.has_range, params.row.min_amount, params.row.max_amount, )}
); }, }; }; 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 (
{currencyCode + ' '}
); }, }; }; const paymentObj = function (width: number, hide: boolean) { return { hide, field: 'payment_method', headerName: t('Payment Method'), width: width * fontSize, renderCell: (params) => { return (
); }, }; }; const paymentSmallObj = function (width: number, hide: boolean) { return { hide, field: 'payment_icons', headerName: t('Pay'), width: width * fontSize, renderCell: (params) => { return (
); }, }; }; 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 (
{`${pn(params.row.price)} ${currencyCode}/BTC`}
); }, }; }; 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 (
{parseFloat(parseFloat(params.row.premium).toFixed(4)) + '%'}
); }, }; }; 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
{hours > 0 ? `${hours}h` : `${minutes}m`}
; }, }; }; 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 ( {hours > 0 ? `${hours}h` : `${minutes}m`} ); }, }; }; const satoshisObj = function (width: number, hide: boolean) { return { hide, field: 'satoshis_now', headerName: t('Sats now'), type: 'number', width: width * fontSize, renderCell: (params) => { return (
{params.row.satoshis_now > 1000000 ? `${pn(Math.round(params.row.satoshis_now / 10000) / 100)} M` : `${pn(Math.round(params.row.satoshis_now / 1000))} K`}
); }, }; }; const idObj = function (width: number, hide: boolean) { return { hide, field: 'id', headerName: 'Order ID', width: width * fontSize, renderCell: (params) => { return (
{`#${params.row.id}`}
); }, }; }; 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, }, }, 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, }, }, 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(fullscreen ? fullWidth : maxWidth); const gridComponents = { LoadingOverlay: LinearProgress, NoResultsOverlay: () => ( {t('Filter has no results')} ), Footer: () => ( setFullscreen(!fullscreen)}> {fullscreen ? : } ), }; if (!fullscreen) { return ( (order.type == type || type == null) && (order.currency == currency || currency == 0), )} loading={loading || refreshing} columns={columns} 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. /> ); } else { return ( (order.type == type || type == null) && (order.currency == currency || currency == 0), )} loading={loading || refreshing} columns={columns} 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. /> ); } }; export default BookTable;