Add book depth chart (#219)

* Amount X Axis, Avatars and refactor

* Theme and performance improvements

* Remove duplicated tooltips

* Code Review

* Marker Theme color

* Missing end lines

Signed-off-by: KoalaSat <111684255+KoalaSat@users.noreply.github.com>
This commit is contained in:
KoalaSat 2022-08-30 20:49:16 +02:00 committed by GitHub
parent e613cd9ffa
commit 458f5ac4e7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 1026 additions and 182 deletions

View File

@ -2810,6 +2810,133 @@
"reselect": "^4.1.5"
}
},
"@nivo/annotations": {
"version": "0.79.1",
"resolved": "https://registry.npmjs.org/@nivo/annotations/-/annotations-0.79.1.tgz",
"integrity": "sha512-lYso9Luu0maSDtIufwvyVt2+Wue7R9Fh3CIjuRDmNR72UjAgAVEcCar27Fy865UXGsj2hRJZ7KY/1s6kT3gu/w==",
"requires": {
"@nivo/colors": "0.79.1",
"@react-spring/web": "9.3.1",
"lodash": "^4.17.21"
}
},
"@nivo/axes": {
"version": "0.79.0",
"resolved": "https://registry.npmjs.org/@nivo/axes/-/axes-0.79.0.tgz",
"integrity": "sha512-EhSeCPxtWEuxqnifeyF/pIJEzL7pRM3rfygL+MpfT5ypu5NcXYRGQo/Bw0Vh+GF1ML+tNAE0rRvCu2jgLSdVNQ==",
"requires": {
"@nivo/scales": "0.79.0",
"@react-spring/web": "9.3.1",
"d3-format": "^1.4.4",
"d3-time-format": "^3.0.0"
},
"dependencies": {
"d3-format": {
"version": "1.4.5",
"resolved": "https://registry.npmjs.org/d3-format/-/d3-format-1.4.5.tgz",
"integrity": "sha512-J0piedu6Z8iB6TbIGfZgDzfXxUFN3qQRMofy2oPdXzQibYGqPB/9iMcxr/TGalU+2RsyDO+U4f33id8tbnSRMQ=="
}
}
},
"@nivo/colors": {
"version": "0.79.1",
"resolved": "https://registry.npmjs.org/@nivo/colors/-/colors-0.79.1.tgz",
"integrity": "sha512-45huBmz46OoQtfqzHrnqDJ9msebOBX84fTijyOBi8mn8iTDOK2xWgzT7cCYP3hKE58IclkibkzVyWCeJ+rUlqg==",
"requires": {
"d3-color": "^2.0.0",
"d3-scale": "^3.2.3",
"d3-scale-chromatic": "^2.0.0",
"lodash": "^4.17.21"
}
},
"@nivo/core": {
"version": "0.79.0",
"resolved": "https://registry.npmjs.org/@nivo/core/-/core-0.79.0.tgz",
"integrity": "sha512-e1iGodmGuXkF+QWAjhHVFc+lUnfBoUwaWqVcBXBfebzNc50tTJrTTMHyQczjgOIfTc8gEu23lAY4mVZCDKscig==",
"requires": {
"@nivo/recompose": "0.79.0",
"@react-spring/web": "9.3.1",
"d3-color": "^2.0.0",
"d3-format": "^1.4.4",
"d3-interpolate": "^2.0.1",
"d3-scale": "^3.2.3",
"d3-scale-chromatic": "^2.0.0",
"d3-shape": "^1.3.5",
"d3-time-format": "^3.0.0",
"lodash": "^4.17.21"
},
"dependencies": {
"d3-format": {
"version": "1.4.5",
"resolved": "https://registry.npmjs.org/d3-format/-/d3-format-1.4.5.tgz",
"integrity": "sha512-J0piedu6Z8iB6TbIGfZgDzfXxUFN3qQRMofy2oPdXzQibYGqPB/9iMcxr/TGalU+2RsyDO+U4f33id8tbnSRMQ=="
}
}
},
"@nivo/legends": {
"version": "0.79.1",
"resolved": "https://registry.npmjs.org/@nivo/legends/-/legends-0.79.1.tgz",
"integrity": "sha512-AoabiLherOAk3/HR/N791fONxNdwNk/gCTJC/6BKUo2nX+JngEYm3nVFmTC1R6RdjwJTeCb9Vtuc4MHA+mcgig=="
},
"@nivo/line": {
"version": "0.79.1",
"resolved": "https://registry.npmjs.org/@nivo/line/-/line-0.79.1.tgz",
"integrity": "sha512-V+2wY5TGpWiWBcb2LDtNsO79Ix93QtSq1HAdEIsjYtwFT/ekoCUA/OorIjRVUVzyf27vjjlbhmNNKrqIsYQR1Q==",
"requires": {
"@nivo/annotations": "0.79.1",
"@nivo/axes": "0.79.0",
"@nivo/colors": "0.79.1",
"@nivo/legends": "0.79.1",
"@nivo/scales": "0.79.0",
"@nivo/tooltip": "0.79.0",
"@nivo/voronoi": "0.79.0",
"@react-spring/web": "9.3.1",
"d3-shape": "^1.3.5"
}
},
"@nivo/recompose": {
"version": "0.79.0",
"resolved": "https://registry.npmjs.org/@nivo/recompose/-/recompose-0.79.0.tgz",
"integrity": "sha512-2GFnOHfA2jzTOA5mdKMwJ6myCRGoXQQbQvFFQ7B/+hnHfU/yrOVpiGt6TPAn3qReC4dyDYrzy1hr9UeQh677ig==",
"requires": {
"react-lifecycles-compat": "^3.0.4"
}
},
"@nivo/scales": {
"version": "0.79.0",
"resolved": "https://registry.npmjs.org/@nivo/scales/-/scales-0.79.0.tgz",
"integrity": "sha512-5fAt5Wejp8yzAk6qmA3KU+celCxNYrrBhfvOi2ECDG8KQi+orbDnrO6qjVF6+ebfOn9az8ZVukcSeGA5HceiMg==",
"requires": {
"d3-scale": "^3.2.3",
"d3-time": "^1.0.11",
"d3-time-format": "^3.0.0",
"lodash": "^4.17.21"
},
"dependencies": {
"d3-time": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/d3-time/-/d3-time-1.1.0.tgz",
"integrity": "sha512-Xh0isrZ5rPYYdqhAVk8VLnMEidhz5aP7htAADH6MfzgmmicPkTo8LhkLxci61/lCB7n7UmE3bN0leRt+qvkLxA=="
}
}
},
"@nivo/tooltip": {
"version": "0.79.0",
"resolved": "https://registry.npmjs.org/@nivo/tooltip/-/tooltip-0.79.0.tgz",
"integrity": "sha512-hsJsvhDVR9P/QqIEDIttaA6aslR3tU9So1s/k2jMdppL7J9ZH/IrVx9TbIP7jDKmnU5AMIP5uSstXj9JiKLhQA==",
"requires": {
"@react-spring/web": "9.3.1"
}
},
"@nivo/voronoi": {
"version": "0.79.0",
"resolved": "https://registry.npmjs.org/@nivo/voronoi/-/voronoi-0.79.0.tgz",
"integrity": "sha512-0MrY33MBjLPQsgtf6PU+NUeQVib0g5fR9UBWsbO3YdkgDhXNnbXZ4FZlMAznoDSOxQ/efAuP7jWfnemFCpSwUg==",
"requires": {
"d3-delaunay": "^5.3.0",
"d3-scale": "^3.2.3"
}
},
"@nodelib/fs.scandir": {
"version": "2.1.5",
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
@ -2841,6 +2968,55 @@
"resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.5.tgz",
"integrity": "sha512-9X2obfABZuDVLCgPK9aX0a/x4jaOEweTTWE2+9sr0Qqqevj2Uv5XorvusThmc9XGYpS9yI+fhh8RTafBtGposw=="
},
"@react-spring/animated": {
"version": "9.3.2",
"resolved": "https://registry.npmjs.org/@react-spring/animated/-/animated-9.3.2.tgz",
"integrity": "sha512-pBvKydRHbTzuyaeHtxGIOvnskZxGo/S5/YK1rtYm88b9NQZuZa95Rgd3O0muFL+99nvBMBL8cvQGD0UJmsqQsg==",
"requires": {
"@react-spring/shared": "~9.3.0",
"@react-spring/types": "~9.3.0"
}
},
"@react-spring/core": {
"version": "9.3.2",
"resolved": "https://registry.npmjs.org/@react-spring/core/-/core-9.3.2.tgz",
"integrity": "sha512-kMRjkgdQ6LJ0lmb/wQlONpghaMT83UxglXHJC6m9kZS/GKVmN//TYMEK85xN1rC5Gg+BmjG61DtLCSkkLDTfNw==",
"requires": {
"@react-spring/animated": "~9.3.0",
"@react-spring/shared": "~9.3.0",
"@react-spring/types": "~9.3.0"
}
},
"@react-spring/rafz": {
"version": "9.3.2",
"resolved": "https://registry.npmjs.org/@react-spring/rafz/-/rafz-9.3.2.tgz",
"integrity": "sha512-YtqNnAYp5bl6NdnDOD5TcYS40VJmB+Civ4LPtcWuRPKDAOa/XAf3nep48r0wPTmkK936mpX8aIm7h+luW59u5A=="
},
"@react-spring/shared": {
"version": "9.3.2",
"resolved": "https://registry.npmjs.org/@react-spring/shared/-/shared-9.3.2.tgz",
"integrity": "sha512-ypGQQ8w7mWnrELLon4h6mBCBxdd8j1pgLzmHXLpTC/f4ya2wdP+0WIKBWXJymIf+5NiTsXgSJra5SnHP5FBY+A==",
"requires": {
"@react-spring/rafz": "~9.3.0",
"@react-spring/types": "~9.3.0"
}
},
"@react-spring/types": {
"version": "9.3.2",
"resolved": "https://registry.npmjs.org/@react-spring/types/-/types-9.3.2.tgz",
"integrity": "sha512-u+IK9z9Re4hjNkBYKebZr7xVDYTai2RNBsI4UPL/k0B6lCNSwuqWIXfKZUDVlMOeZHtDqayJn4xz6HcSkTj3FQ=="
},
"@react-spring/web": {
"version": "9.3.1",
"resolved": "https://registry.npmjs.org/@react-spring/web/-/web-9.3.1.tgz",
"integrity": "sha512-sisZIgFGva/Z+xKWPSfXpukF0AP3kR9ALTxlHL87fVotMUCJX5vtH/YlVcywToEFwTHKt3MpI5Wy2M+vgVEeaw==",
"requires": {
"@react-spring/animated": "~9.3.0",
"@react-spring/core": "~9.3.0",
"@react-spring/shared": "~9.3.0",
"@react-spring/types": "~9.3.0"
}
},
"@sinonjs/commons": {
"version": "1.8.3",
"resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-1.8.3.tgz",
@ -4185,6 +4361,90 @@
"type": "^1.0.1"
}
},
"d3-array": {
"version": "2.12.1",
"resolved": "https://registry.npmjs.org/d3-array/-/d3-array-2.12.1.tgz",
"integrity": "sha512-B0ErZK/66mHtEsR1TkPEEkwdy+WDesimkM5gpZr5Dsg54BiTA5RXtYW5qTLIAcekaS9xfZrzBLF/OAkB3Qn1YQ==",
"requires": {
"internmap": "^1.0.0"
}
},
"d3-color": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/d3-color/-/d3-color-2.0.0.tgz",
"integrity": "sha512-SPXi0TSKPD4g9tw0NMZFnR95XVgUZiBH+uUTqQuDu1OsE2zomHU7ho0FISciaPvosimixwHFl3WHLGabv6dDgQ=="
},
"d3-delaunay": {
"version": "5.3.0",
"resolved": "https://registry.npmjs.org/d3-delaunay/-/d3-delaunay-5.3.0.tgz",
"integrity": "sha512-amALSrOllWVLaHTnDLHwMIiz0d1bBu9gZXd1FiLfXf8sHcX9jrcj81TVZOqD4UX7MgBZZ07c8GxzEgBpJqc74w==",
"requires": {
"delaunator": "4"
}
},
"d3-format": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/d3-format/-/d3-format-2.0.0.tgz",
"integrity": "sha512-Ab3S6XuE/Q+flY96HXT0jOXcM4EAClYFnRGY5zsjRGNy6qCYrQsMffs7cV5Q9xejb35zxW5hf/guKw34kvIKsA=="
},
"d3-interpolate": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-2.0.1.tgz",
"integrity": "sha512-c5UhwwTs/yybcmTpAVqwSFl6vrQ8JZJoT5F7xNFK9pymv5C0Ymcc9/LIJHtYIggg/yS9YHw8i8O8tgb9pupjeQ==",
"requires": {
"d3-color": "1 - 2"
}
},
"d3-path": {
"version": "1.0.9",
"resolved": "https://registry.npmjs.org/d3-path/-/d3-path-1.0.9.tgz",
"integrity": "sha512-VLaYcn81dtHVTjEHd8B+pbe9yHWpXKZUC87PzoFmsFrJqgFwDe/qxfp5MlfsfM1V5E/iVt0MmEbWQ7FVIXh/bg=="
},
"d3-scale": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-3.3.0.tgz",
"integrity": "sha512-1JGp44NQCt5d1g+Yy+GeOnZP7xHo0ii8zsQp6PGzd+C1/dl0KGsp9A7Mxwp+1D1o4unbTTxVdU/ZOIEBoeZPbQ==",
"requires": {
"d3-array": "^2.3.0",
"d3-format": "1 - 2",
"d3-interpolate": "1.2.0 - 2",
"d3-time": "^2.1.1",
"d3-time-format": "2 - 3"
}
},
"d3-scale-chromatic": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/d3-scale-chromatic/-/d3-scale-chromatic-2.0.0.tgz",
"integrity": "sha512-LLqy7dJSL8yDy7NRmf6xSlsFZ6zYvJ4BcWFE4zBrOPnQERv9zj24ohnXKRbyi9YHnYV+HN1oEO3iFK971/gkzA==",
"requires": {
"d3-color": "1 - 2",
"d3-interpolate": "1 - 2"
}
},
"d3-shape": {
"version": "1.3.7",
"resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-1.3.7.tgz",
"integrity": "sha512-EUkvKjqPFUAZyOlhY5gzCxCeI0Aep04LwIRpsZ/mLFelJiUfnK56jo5JMDSE7yyP2kLSb6LtF+S5chMk7uqPqw==",
"requires": {
"d3-path": "1"
}
},
"d3-time": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/d3-time/-/d3-time-2.1.1.tgz",
"integrity": "sha512-/eIQe/eR4kCQwq7yxi7z4c6qEXf2IYGcjoWB5OOQy4Tq9Uv39/947qlDcN2TLkiTzQWzvnsuYPB9TrWaNfipKQ==",
"requires": {
"d3-array": "2"
}
},
"d3-time-format": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-3.0.0.tgz",
"integrity": "sha512-UXJh6EKsHBTjopVqZBhFysQcoXSv/5yLONZvkQ5Kk3qbwiUYkdX17Xa1PT6U1ZWXGGfB1ey5L8dKMlFq2DO0Ag==",
"requires": {
"d3-time": "1 - 2"
}
},
"data-urls": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/data-urls/-/data-urls-2.0.0.tgz",
@ -4264,6 +4524,11 @@
"object-keys": "^1.0.12"
}
},
"delaunator": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/delaunator/-/delaunator-4.0.1.tgz",
"integrity": "sha512-WNPWi1IRKZfCt/qIDMfERkDp93+iZEmOxN2yy4Jg+Xhv8SLk2UTqqbe1sfiipn0and9QrE914/ihdx82Y/Giag=="
},
"delayed-stream": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
@ -5402,6 +5667,11 @@
"side-channel": "^1.0.4"
}
},
"internmap": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/internmap/-/internmap-1.0.1.tgz",
"integrity": "sha512-lDB5YccMydFBtasVtxnZ3MRBHuaoE8GKsppq+EchKL2U4nK/DmEpPHNH8MZe5HkMtpSiTSOZwfN0tzYjO/lJEw=="
},
"interpret": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/interpret/-/interpret-2.2.0.tgz",
@ -7885,8 +8155,7 @@
"lodash": {
"version": "4.17.21",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
"dev": true
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
},
"lodash.debounce": {
"version": "4.0.8",
@ -8426,6 +8695,11 @@
"resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz",
"integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w=="
},
"react-lifecycles-compat": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz",
"integrity": "sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA=="
},
"react-qr-code": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/react-qr-code/-/react-qr-code-2.0.3.tgz",

View File

@ -46,6 +46,8 @@
"@mui/material": "^5.9.0",
"@mui/system": "^5.9.0",
"@mui/x-data-grid": "^5.2.2",
"@nivo/core": "^0.79.0",
"@nivo/line": "^0.79.1",
"country-flag-icons": "^1.4.25",
"date-fns": "^2.28.0",
"i18next": "^21.6.14",

View File

@ -1,25 +1,27 @@
import React, { Component } from "react";
import { withTranslation } from "react-i18next";
import { Badge, Tooltip, Stack, Paper, Button, FormControlLabel, Checkbox, RadioGroup, ListItemButton, Typography, Grid, Select, MenuItem, FormControl, FormHelperText, ListItemText, ListItemAvatar, IconButton, CircularProgress} from "@mui/material";
import { Badge, Tooltip, Stack, Paper, Button, FormControlLabel, Checkbox, RadioGroup, 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 Image from 'material-ui-image'
import FlagWithProps from './FlagWithProps'
import { pn } from "../utils/prettyNumbers";
import { pn, amountToString } from "../utils/prettyNumbers";
import PaymentText from './PaymentText'
import DepthChart from './Charts/DepthChart'
import RobotAvatar from './Robots/RobotAvatar'
// Icons
import RefreshIcon from '@mui/icons-material/Refresh';
import { SendReceiveIcon, BuySatsCheckedIcon, BuySatsIcon, SellSatsCheckedIcon, SellSatsIcon} from "./Icons";
import { BarChart, FormatListBulleted, Refresh } from '@mui/icons-material';
import { BuySatsCheckedIcon, BuySatsIcon, SellSatsCheckedIcon, SellSatsIcon} from "./Icons";
class BookPage extends Component {
constructor(props) {
super(props);
this.state = {
pageSize: 6,
view: 'list'
};
}
@ -67,14 +69,6 @@ class BookPage extends Component {
if(status=='Inactive'){return('error')}
}
amountToString = (amount,has_range,min_amount,max_amount) => {
if (has_range){
return pn(parseFloat(Number(min_amount).toPrecision(4)))+'-'+pn(parseFloat(Number(max_amount).toPrecision(4)))
}else{
return pn(parseFloat(Number(amount).toPrecision(4)))
}
}
dataGridLocaleText=()=> {
const { t } = this.props;
return {
@ -135,77 +129,65 @@ class BookPage extends Component {
<DataGrid
localeText={this.dataGridLocaleText()}
rows={
this.props.bookOrders.filter(order => order.type == this.props.type || this.props.type == 2)
.filter(order => order.currency == this.props.currency || this.props.currency == 0)
.map((order) =>
({id: order.id,
avatar: window.location.origin +'/static/assets/avatars/' + order.maker_nick + '.png',
robot: order.maker_nick,
robot_status: order.maker_status,
type: order.type ? t("Seller"): t("Buyer"),
amount: order.amount,
has_range: order.has_range,
min_amount: order.min_amount,
max_amount: order.max_amount,
currency: this.getCurrencyCode(order.currency),
payment_method: order.payment_method,
price: order.price,
premium: order.premium,
})
)}
this.props.bookOrders
.filter(order =>
(order.type == this.props.type || this.props.type == 2) &&
(order.currency == this.props.currency || this.props.currency == 0)
)
}
loading={this.props.bookLoading}
columns={[
// { field: 'id', headerName: 'ID', width: 40 },
{ field: 'robot', headerName: t("Robot"), width: 240,
renderCell: (params) => {return (
<ListItemButton style={{ cursor: "pointer" }}>
<ListItemAvatar>
<Tooltip placement="right" enterTouchDelay={0} title={t(params.row.robot_status)}>
<Badge variant="dot" overlap="circular" badgeContent="" color={this.statusBadgeColor(params.row.robot_status)}>
<Badge overlap="circular" anchorOrigin={{horizontal: 'right', vertical: 'bottom'}} badgeContent={<div style={{position:"relative", left:"6px", top:"1px"}}> {params.row.type == t("Buyer") ? <SendReceiveIcon sx={{transform: "scaleX(-1)",height:"18px",width:"18px"}} color="secondary"/> : <SendReceiveIcon sx={{height:"20px",width:"20px"}} color="primary"/>}</div>}>
<div style={{ width: 45, height: 45 }}>
<Image className='bookAvatar'
disableError={true}
disableSpinner={true}
color='null'
alt={params.row.robot}
src={params.row.avatar}
/>
</div>
</Badge>
</Badge>
</Tooltip>
</ListItemAvatar>
<ListItemText primary={params.row.robot}/>
</ListItemButton>
);
} },
{ field: 'type', headerName: t("Is"), width: 60 },
{
field: 'maker_nick', headerName: t("Robot"), width: 240,
renderCell: (params) => {
return (
<ListItemButton style={{ cursor: "pointer" }}>
<ListItemAvatar>
<RobotAvatar order={params.row} />
</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" }}>{this.amountToString(params.row.amount,params.row.has_range, params.row.min_amount, params.row.max_amount)}</div>
)}},
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) => {return (
<div style={{ cursor: "pointer", display:'flex',alignItems:'center', flexWrap:'wrap'}}>
{params.row.currency+" "}
<FlagWithProps code={params.row.currency} />
</div>
)
}},
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>
)} },
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) => {return (
<div style={{ cursor: "pointer" }}>{pn(params.row.price) + " " +params.row.currency+ "/BTC" }</div>
)} },
renderCell: (params) => {return (
<div style={{ cursor: "pointer" }}>{pn(params.row.price) + " " + params.row.currency + "/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: () => (
@ -237,80 +219,60 @@ class BookPage extends Component {
localeText={this.dataGridLocaleText()}
loading={this.props.bookLoading}
rows={
this.props.bookOrders.filter(order => order.type == this.props.type || this.props.type == 2)
.filter(order => order.currency == this.props.currency || this.props.currency == 0)
.map((order) =>
({id: order.id,
avatar: window.location.origin +'/static/assets/avatars/' + order.maker_nick + '.png',
robot: order.maker_nick,
robot_status: order.maker_status,
type: order.type ? t("Seller"): t("Buyer"),
amount: order.amount,
has_range: order.has_range,
min_amount: order.min_amount,
max_amount: order.max_amount,
currency: this.getCurrencyCode(order.currency),
payment_method: order.payment_method,
price: order.price,
premium: order.premium,
})
)}
this.props.bookOrders.filter(order =>
(order.type == this.props.type || this.props.type == 2) &&
(order.currency == this.props.currency || this.props.currency == 0)
)
}
columns={[
// { field: 'id', headerName: 'ID', width: 40 },
{ field: 'robot', headerName: t("Robot"), width: 64,
renderCell: (params) => {return (
<div style={{ position: "relative", left: "-5px" }}>
<Tooltip placement="right" enterTouchDelay={0} title={params.row.robot+" ("+t(params.row.robot_status)+")"}>
<Badge variant="dot" overlap="circular" badgeContent="" color={this.statusBadgeColor(params.row.robot_status)}>
<Badge overlap="circular" anchorOrigin={{horizontal: 'right', vertical: 'bottom'}} badgeContent={<div style={{position:"relative", left:"6px", top:"1px"}}> {params.row.type == t("Buyer") ? <SendReceiveIcon sx={{transform: "scaleX(-1)",height:"18px",width:"18px"}} color="secondary"/> : <SendReceiveIcon sx={{height:"20px",width:"20px"}} color="primary"/>}</div>}>
<div style={{ width: 45, height: 45 }}>
<Image className='bookAvatar'
disableError={true}
disableSpinner={true}
color={null}
alt={params.row.robot}
src={params.row.avatar}
/>
</div>
</Badge>
</Badge>
</Tooltip>
</div>
);
} },
{ field: 'type', headerName: t("Is"), width: 60, hide:'true'},
{ field: 'maker_nick', headerName: t("Robot"), width: 64,
renderCell: (params) => {
return (
<div style={{ position: "relative", left: "-5px" }}>
<RobotAvatar order={params.row} />
</div>
)
}
},
{ field: 'amount', headerName: t("Amount"), type: 'number', width: 84,
renderCell: (params) => {return (
<Tooltip placement="right" enterTouchDelay={0} title={t(params.row.type)}>
<div style={{ cursor: "pointer" }}>{this.amountToString(params.row.amount,params.row.has_range, params.row.min_amount, params.row.max_amount)}</div>
</Tooltip>
)} },
renderCell: (params) => {return (
<Tooltip placement="right" enterTouchDelay={0} title={t(params.row.type)}>
<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) => {return (
// <Tooltip placement="left" enterTouchDelay={0} title={params.row.payment_method}>
<div style={{ cursor: "pointer", display:'flex',alignItems:'center', flexWrap:'wrap'}}>
{params.row.currency+" "}
<FlagWithProps code={params.row.currency} />
</div>
// </Tooltip>
)} },
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>
)} },
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>
)} },
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" }>
<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: () => (
@ -354,6 +316,10 @@ class BookPage extends Component {
this.handleTypeChange(buyChecked, sellChecked);
}
handleClickView=()=>{
this.setState({ view: this.state.view == 'depth' ? 'list' : 'depth' })
}
handleClickSell=(e)=>{
var buyChecked = this.props.buyChecked
var sellChecked = e.target.checked
@ -386,13 +352,78 @@ class BookPage extends Component {
</Grid>
)
}
mainView = () => {
if (this.props.bookNotFound) { 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: 500, overflow: 'auto'}}>
<div style={{ height: 422, width:'100%'}}>
{components[0]}
</div>
</Paper>
</MediaQuery>
{/* Smartphone */}
<MediaQuery maxWidth={929}>
<Paper elevation={0} style={{width: 395, maxHeight: 450, overflow: 'auto'}}>
<div style={{ height: 422, width:'100%'}}>
{components[1]}
</div>
</Paper>
</MediaQuery>
</>
)
}
getTitle = () => {
const { t } = this.props;
if (this.state.view == 'list') {
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")
}
}
render() {
const { t } = this.props;
return (
<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)}>
<RefreshIcon/>
<Refresh/>
</IconButton>
<Grid item xs={6} align="right">
@ -460,49 +491,34 @@ class BookPage extends Component {
</Select>
</FormControl>
</Grid>
{ this.props.bookNotFound ? "" :
{ 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.props.type == 0 ?
t("You are SELLING BTC for {{currencyCode}}",{currencyCode:this.props.bookCurrencyCode})
:
(this.props.type == 1 ?
t("You are BUYING BTC for {{currencyCode}}",{currencyCode:this.props.bookCurrencyCode})
:
t("You are looking at all")
)
}
</Typography>
{this.mainView()}
</Grid>
}
{ this.props.bookNotFound ?
this.NoOrdersFound()
:
<Grid item xs={12} align="center">
{/* Desktop Book */}
<MediaQuery minWidth={930}>
<Paper elevation={0} style={{width: 925, maxHeight: 500, overflow: 'auto'}}>
{this.bookListTableDesktop()}
</Paper>
</MediaQuery>
{/* Smartphone Book */}
<MediaQuery maxWidth={929}>
<Paper elevation={0} style={{width: 395, maxHeight: 450, overflow: 'auto'}}>
{this.bookListTablePhone()}
</Paper>
</MediaQuery>
</Grid>
}
<Grid item xs={12} align="center">
{ !this.props.bookNotFound ?
<Button variant="contained" color='primary' to='/make/' component={Link}>{t("Make Order")}</Button>
: null
}
<Button color="secondary" variant="contained" to="/" component={Link}>
{t("Back")}
</Button>
<ButtonGroup variant="contained" aria-label="outlined primary button group">
{ !this.props.bookNotFound ?
<>
<Button variant="contained" color='primary' to='/make/' component={Link}>{t("Make Order")}</Button>
<Button color='inherit' style={{color: '#111111'}} onClick={this.handleClickView}>
{ this.state.view == 'depth' ?
<><FormatListBulleted /> {t("List")}</> :
<><BarChart /> {t("Chart")}</>
}
</Button>
</>
: null
}
<Button color="secondary" variant="contained" to="/" component={Link}>
{t("Back")}
</Button>
</ButtonGroup>
</Grid>
</Grid>
);

View File

@ -70,7 +70,8 @@ class BottomBar extends Component {
activeOrderId: data.active_order_id ? data.active_order_id : null,
lastOrderId: data.last_order_id ? data.last_order_id : null,
referralCode: data.referral_code,
earnedRewards: data.earned_rewards,}));
earnedRewards: data.earned_rewards,
lastDayPremium: data.last_day_nonkyc_btc_premium}));
}
handleClickOpenStatsForNerds = () => {

View File

@ -0,0 +1,346 @@
import React, { useEffect, useState } from "react"
import { ResponsiveLine, Serie, Datum, PointTooltipProps, PointMouseHandler, Point, CustomLayer } from '@nivo/line'
import { Box, CircularProgress, Grid, IconButton, MenuItem, Paper, Select, useTheme } from "@mui/material"
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 RobotAvatar from '../../Robots/RobotAvatar'
import { amountToString } from "../../../utils/prettyNumbers";
import currencyDict from '../../../../static/assets/currencies.json';
import PaymentText from "../../PaymentText";
import getNivoScheme from "../NivoScheme"
import median from "../../../utils/match";
interface DepthChartProps {
bookLoading: boolean
orders: Order[]
lastDayPremium: number | undefined
currency: number
setAppState: (state: object) => void
limits: LimitList
compact?: boolean
}
const DepthChart: React.FC<DepthChartProps> = ({
bookLoading, orders, lastDayPremium, currency, setAppState, limits, compact
}) => {
const { t } = useTranslation()
const history = useHistory()
const theme = useTheme()
const [enrichedOrders, setEnrichedOrders] = useState<Order[]>([])
const [series, setSeries] = useState<Serie[]>([])
const [xRange, setXRange] = useState<number>(8)
const [xType, setXType] = useState<string>("premium")
const [currencyCode, setCurrencyCode] = useState<number>(1)
const [center, setCenter] = useState<number>(0)
useEffect(() => {
if (Object.keys(limits).length === 0) {
fetch('/api/limits/')
.then((response) => response.json())
.then((data) => {
setAppState({ limits: data })
})
}
}, [])
useEffect(() => {
setCurrencyCode(currency === 0 ? 1 : currency)
}, [currency])
useEffect(() => {
if (Object.keys(limits).length > 0) {
const enriched = orders.map((order) => {
// We need to transform all currencies to the same base (ex. USD), we don't have the exchange rate
// for EUR -> USD, but we know the rate of both to BTC, so we get advantage of it and apply a
// simple rule of three
order.base_amount = (order.price * limits[currencyCode].price) / limits[order.currency].price
return order
})
setEnrichedOrders(enriched)
}
}, [limits, orders, currencyCode])
useEffect(() => {
if (enrichedOrders.length > 0) {
generateSeries()
}
}, [enrichedOrders, xRange])
useEffect(() => {
if (xType === 'base_amount') {
const prices: number[] = enrichedOrders.map((order) => order?.base_amount || 0)
setCenter(~~median(prices))
setXRange(1500)
} else if (lastDayPremium) {
setCenter(lastDayPremium)
setXRange(8)
}
}, [enrichedOrders, xType, lastDayPremium, currencyCode])
const calculateBtc = (order: Order): number => {
const amount = parseInt(order.amount) || order.max_amount
return amount / order.price
}
const generateSeries:() => void = () => {
let sortedOrders: Order[] = xType === 'base_amount' ?
enrichedOrders.sort((order1, order2) => (order1?.base_amount || 0) - (order2?.base_amount || 0) )
: enrichedOrders.sort((order1, order2) => order1.premium - order2.premium )
const sortedBuyOrders: Order[] = sortedOrders.filter((order) => order.type == 0).reverse()
const sortedSellOrders: Order[] = sortedOrders.filter((order) => order.type == 1)
const buySerie: Datum[] = generateSerie(sortedBuyOrders)
const sellSerie: Datum[] = generateSerie(sortedSellOrders)
const maxX: number = center + xRange
const minX: number = center - xRange
setSeries([
{
id: "buy",
data: closeSerie(buySerie, maxX, minX)
},
{
id: "sell",
data: closeSerie(sellSerie, minX, maxX)
}
])
}
const generateSerie = (orders: Order[]): Datum[] => {
if (!center) { return [] }
let sumOrders: number = 0
let serie: Datum[] = []
orders.forEach((order) => {
const lastSumOrders = sumOrders
sumOrders += calculateBtc(order)
const datum: Datum[] = [
{ // Vertical Line
x: xType === 'base_amount' ? order.base_amount : order.premium,
y: lastSumOrders
},
{ // Order Point
x: xType === 'base_amount' ? order.base_amount : order.premium,
y: sumOrders,
order: order
}
]
serie = [...serie, ...datum]
})
const inlineSerie = serie.filter((datum: Datum) => {
return (Number(datum.x) > center - xRange) &&
(Number(datum.x) < center + xRange)
})
return inlineSerie
}
const closeSerie = (serie: Datum[], limitBottom: number, limitTop: number): Datum[] =>{
if (serie.length == 0) { return [] }
// If the bottom is not 0, exdens the horizontal bottom line
if (serie[0].y !== 0) {
const startingPoint: Datum = {
x: limitBottom,
y: serie[0].y
}
serie.unshift(startingPoint)
}
// exdens the horizontal top line
const endingPoint: Datum = {
x: limitTop,
y: serie[serie.length - 1].y
}
return [...serie, endingPoint]
}
const centerLine: CustomLayer = (props) => (
<path
key="center-line"
d={props.lineGenerator([
{
y: 0,
x: props.xScale(center)
},
{
y: props.innerHeight,
x: props.xScale(center)
},
])}
fill="none"
stroke={getNivoScheme(theme).markers?.lineColor}
strokeWidth={getNivoScheme(theme).markers?.lineStrokeWidth}
/>
)
const generateTooltip: React.FunctionComponent<PointTooltipProps> = (pointTooltip: PointTooltipProps) => {
const order: Order = pointTooltip.point.data.order
return order ? (
<Paper elevation={12} style={{ padding: 10, width: 250 }}>
<Grid container justifyContent="space-between">
<Grid item xs={3}>
<Grid
container
justifyContent="center"
alignItems="center"
>
<RobotAvatar order={order} />
</Grid>
</Grid>
<Grid item xs={8}>
<Grid
container
direction="column"
justifyContent="center"
alignItems="flex-start"
>
<Box>
{order.maker_nick}
</Box>
<Box>
<Grid
container
direction="column"
justifyContent="flex-start"
alignItems="flex-start"
>
<Grid item xs={12}>
{amountToString(order.amount, order.has_range, order.min_amount, order.max_amount)}
{' '}
{currencyDict[order.currency]}
</Grid>
<Grid item xs={12}>
<PaymentText
othersText={t("Others")}
verbose={true}
size={20}
text={order.payment_method}
/>
</Grid>
</Grid>
</Box>
</Grid>
</Grid>
</Grid>
</Paper>
) : <></>
}
const formatAxisX = (value: number): string => {
if (xType === 'base_amount') {
return value.toString()
}
return `${value}%`
}
const formatAxisY = (value: number): string => `${value}BTC`
const rangeSteps = xType === 'base_amount' ? 200 : 0.5
const handleOnClick: PointMouseHandler = (point: Point) => {
history.push('/order/' + point.data?.order?.id);
}
return bookLoading || !center || 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
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: 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>
)
}
export default DepthChart

View File

@ -0,0 +1,61 @@
import { light } from "@mui/material/styles/createPalette"
import { palette } from "@mui/system"
import { Theme as NivoTheme } from "@nivo/core"
import { Theme as MuiTheme } from './createTheme'
export const getNivoScheme: (theme: MuiTheme) => NivoTheme = (theme) => {
const lightMode = {
markers: {
lineColor: "rgb(0, 0, 0)",
lineStrokeWidth: 1
},
axis: {
ticks: {
line: {
strokeWidth: "1",
stroke: "rgb(0, 0, 0)"
}
},
domain: {
line: {
strokeWidth: "1",
stroke: "rgb(0, 0, 0)"
}
}
}
}
const darkMode = {
markers: {
lineColor: "rgb(255, 255, 255)",
lineStrokeWidth: 1
},
axis: {
ticks: {
text: {
fill: "rgb(255, 255, 255)"
},
line: {
strokeWidth: "1",
stroke: "rgb(255, 255, 255)"
}
},
domain: {
line: {
strokeWidth: "1",
stroke: "rgb(255, 255, 255)"
}
}
},
crosshair: {
line: {
strokeWidth: 1,
stroke: "rgb(255, 255, 255)"
}
}
}
return theme.palette.mode === 'dark' ? darkMode : lightMode
}
export default getNivoScheme

View File

@ -26,6 +26,8 @@ export default class HomePage extends Component {
lastOrderId: null,
earnedRewards: 0,
referralCode:'',
lastDayPremium: 0,
limits: {}
}
}

View File

@ -0,0 +1,61 @@
import React from "react"
import { Badge, Tooltip } from "@mui/material";
import Image from 'material-ui-image'
import Order from "../../../models/Order.model"
import { useTranslation } from "react-i18next";
import { SendReceiveIcon } from "../../Icons";
interface DepthChartProps {
order: Order
}
const RobotAvatar: React.FC<DepthChartProps> = ({ order }) => {
const { t } = useTranslation()
const avatarSrc: string = window.location.origin +'/static/assets/avatars/' + order?.maker_nick + '.png'
const statusBadge = (
<div style={{position:"relative", left:"6px", top:"1px"}}>
{order?.type === 0 ?
<SendReceiveIcon sx={{transform: "scaleX(-1)",height:"18px",width:"18px"}} color="secondary"/> :
<SendReceiveIcon sx={{height:"20px",width:"20px"}} color="primary"/>}
</div>
)
const statusBadgeColor = () => {
if(!order){ return }
if(order.maker_status ==='Active'){ return("success") }
if(order.maker_status ==='Seen recently'){ return("warning") }
if(order.maker_status ==='Inactive'){ return('error') }
}
return order ? (
<Tooltip placement="right" enterTouchDelay={0} title={t(order.maker_status) || ""}>
<Badge
variant="dot"
overlap="circular"
badgeContent=""
color={statusBadgeColor()}
>
<Badge
overlap="circular"
anchorOrigin={{horizontal: 'right', vertical: 'bottom'}}
badgeContent={statusBadge}
>
<div style={{ width: 45, height: 45 }}>
<Image className='bookAvatar'
disableError={true}
disableSpinner={true}
color='null'
alt={order.maker_nick}
src={avatarSrc}
/>
</div>
</Badge>
</Badge>
</Tooltip>
) : <></>
}
export default RobotAvatar

View File

@ -0,0 +1,13 @@
export interface Limit {
code: string,
price: number,
min_amount: number,
max_amount: number,
max_bondless_amount: number
}
export interface LimitList {
[currencyCode: string]: Limit
}
export default Limit

View File

@ -0,0 +1,24 @@
export interface Order {
id: number,
created_at: Date,
expires_at: Date,
type: number,
currency: number,
amount: string,
base_amount?: number,
has_range: boolean,
min_amount: number,
max_amount: number,
payment_method: string,
is_explicit: false,
premium: number,
satoshis: number,
bondless_taker: boolean,
maker: number,
escrow_duration: number,
maker_nick: string,
price: number,
maker_status: "Active" | "Seen recently" | "Inactive"
}
export default Order

View File

@ -0,0 +1,7 @@
export const median = (arr: number[]) => {
const mid = Math.floor(arr.length / 2),
nums = [...arr].sort((a, b) => a - b);
return arr.length % 2 !== 0 ? nums[mid] : (nums[mid - 1] + nums[mid]) / 2;
};
export default median

View File

@ -1,4 +1,4 @@
import { pn } from "./prettyNumbers";
import { pn, amountToString } from "./prettyNumbers";
describe("prettyNumbers", () => {
test("pn()", () => {
@ -24,3 +24,22 @@ describe("prettyNumbers", () => {
});
});
})
describe("amountToString", () => {
test("pn()", () => {
[
{input: null, output: undefined},
{input: undefined, output: undefined},
{input: ["", false, 50, 150] , output: "0"},
{input: ["100.00", false, 50, 150] , output: "0"},
{input: ["100.00", true, undefined, undefined] , output: "-"},
{input: ["100.00", true, undefined, 150] , output: "-150"},
{input: ["100.00", true, 50, undefined] , output: "0-"},
{input: ["100.00", true, 50, 150] , output: "0-150"},
].forEach((it) => {
const params: any[] = it.input || []
const response = amountToString(params[0],params[1],params[2],params[3]);
expect(response).toBe(it.output);
});
});
})

View File

@ -9,3 +9,15 @@ export const pn = (value?: number | null): string | undefined => {
return parts.join(".");
};
export const amountToString: (amount: string, has_range: boolean , min_amount: number, max_amount: number) => string =
(amount, has_range, min_amount, max_amount) => {
if (has_range){
return pn(parseFloat(Number(min_amount).toPrecision(4))) +
'-' +
pn(parseFloat(Number(max_amount).toPrecision(4)))
}
return pn(parseFloat(Number(amount).toPrecision(4))) || ""
}
export default pn

View File

@ -166,6 +166,8 @@
"Show filters":"Show filters",
"yes":"yes",
"no":"no",
"Depth chart": "Depth chart",
"Chart": "Chart",
"BOTTOM BAR AND MISC - BottomBar.js":"Bottom Bar user profile and miscellaneous dialogs",

View File

@ -167,6 +167,8 @@
"Show filters":"Mostrar filtros",
"yes":"si",
"no":"no",
"Depth chart": "Gráfico de profundidad",
"Chart": "Gráfico",
"BOTTOM BAR AND MISC - BottomBar.js":"Bottom Bar user profile and miscellaneous dialogs",
"Stats For Nerds":"Estadísticas para nerds",

View File

@ -164,6 +164,8 @@
"Show filters":"Показать фильтры",
"yes":"да",
"no":"нет",
"Depth chart": "Схемами глубин",
"Chart": "Схемами",
"BOTTOM BAR AND MISC - BottomBar.js":"Bottom Bar user profile and miscellaneous dialogs",