From 425225d23d0618f0e49d7268adeedcba091bd289 Mon Sep 17 00:00:00 2001 From: KoalaSat Date: Tue, 30 Aug 2022 20:49:16 +0200 Subject: [PATCH] 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> --- frontend/package-lock.json | 278 ++++++++++++- frontend/package.json | 2 + frontend/src/components/BookPage.js | 372 +++++++++--------- frontend/src/components/BottomBar.js | 3 +- .../components/Charts/DepthChart/index.tsx | 346 ++++++++++++++++ .../src/components/Charts/NivoScheme/index.ts | 61 +++ frontend/src/components/HomePage.js | 2 + .../components/Robots/RobotAvatar/index.tsx | 61 +++ frontend/src/models/Limit.model.ts | 13 + frontend/src/models/Order.model.ts | 24 ++ frontend/src/utils/match.ts | 7 + frontend/src/utils/prettyNumbers.test.ts | 21 +- frontend/src/utils/prettyNumbers.ts | 12 + frontend/static/locales/en.json | 2 + frontend/static/locales/es.json | 2 + frontend/static/locales/ru.json | 2 + 16 files changed, 1026 insertions(+), 182 deletions(-) create mode 100644 frontend/src/components/Charts/DepthChart/index.tsx create mode 100644 frontend/src/components/Charts/NivoScheme/index.ts create mode 100644 frontend/src/components/Robots/RobotAvatar/index.tsx create mode 100644 frontend/src/models/Limit.model.ts create mode 100644 frontend/src/models/Order.model.ts create mode 100644 frontend/src/utils/match.ts diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 28336b5b..cca92326 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -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", diff --git a/frontend/package.json b/frontend/package.json index 23d832f0..2d2f6c4a 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -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", diff --git a/frontend/src/components/BookPage.js b/frontend/src/components/BookPage.js index 5a72dded..add0e31d 100644 --- a/frontend/src/components/BookPage.js +++ b/frontend/src/components/BookPage.js @@ -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 { 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 ( - - - - - {params.row.type == t("Buyer") ? : }}> -
- {params.row.robot} -
-
-
-
-
- -
- ); - } }, - { field: 'type', headerName: t("Is"), width: 60 }, + { + field: 'maker_nick', headerName: t("Robot"), width: 240, + renderCell: (params) => { + return ( + + + + + + + ); + } + }, + { 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 ( -
{this.amountToString(params.row.amount,params.row.has_range, params.row.min_amount, params.row.max_amount)}
- )}}, + renderCell: (params) => { + return ( +
+ {amountToString(params.row.amount,params.row.has_range, params.row.min_amount, params.row.max_amount)} +
+ ) + } + }, { field: 'currency', headerName: t("Currency"), width: 100, - renderCell: (params) => {return ( -
- {params.row.currency+" "} - -
- ) - }}, + renderCell: (params) => { + const currencyCode = this.getCurrencyCode(params.row.currency) + return ( +
+ {currencyCode + " "} + +
+ ) + } + }, { field: 'payment_method', headerName: t("Payment Method"), width: 180 , - renderCell: (params) => {return ( -
- )} }, + renderCell: (params) => { + return ( +
+ )} + }, { field: 'price', headerName: t("Price"), type: 'number', width: 140, - renderCell: (params) => {return ( -
{pn(params.row.price) + " " +params.row.currency+ "/BTC" }
- )} }, + renderCell: (params) => {return ( +
{pn(params.row.price) + " " + params.row.currency + "/BTC" }
+ )} + }, { field: 'premium', headerName: t("Premium"), type: 'number', width: 100, renderCell: (params) => {return (
{parseFloat(parseFloat(params.row.premium).toFixed(4))+"%" }
- )} }, - ]} + )} + }]} 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 ( -
- - - {params.row.type == t("Buyer") ? : }
}> -
- {params.row.robot} -
- - - - - ); - } }, - { field: 'type', headerName: t("Is"), width: 60, hide:'true'}, + { field: 'maker_nick', headerName: t("Robot"), width: 64, + renderCell: (params) => { + return ( +
+ +
+ ) + } + }, { field: 'amount', headerName: t("Amount"), type: 'number', width: 84, - renderCell: (params) => {return ( - -
{this.amountToString(params.row.amount,params.row.has_range, params.row.min_amount, params.row.max_amount)}
-
- )} }, + renderCell: (params) => {return ( + +
+ {amountToString(params.row.amount, params.row.has_range, params.row.min_amount, params.row.max_amount)} +
+
+ )} + }, { field: 'currency', headerName: t("Currency"), width: 85, - renderCell: (params) => {return ( - // -
- {params.row.currency+" "} - -
- //
- )} }, + renderCell: (params) => { + const currencyCode = this.getCurrencyCode(params.row.currency) + return ( +
+ {currencyCode + " "} + +
+ ) + } + }, { field: 'payment_method', headerName: t("Payment Method"), width: 180, hide:'true'}, { field: 'payment_icons', headerName: t("Pay"), width: 75 , - renderCell: (params) => {return ( -
- )} }, + renderCell: (params) => {return ( +
+ )} + }, { field: 'price', headerName: t("Price"), type: 'number', width: 140, hide:'true', - renderCell: (params) => {return ( -
{pn(params.row.price) + " " +params.row.currency+ "/BTC" }
- )} }, + renderCell: (params) => {return ( +
{pn(params.row.price) + " " + params.row.currency+ "/BTC" }
+ )} + }, { field: 'premium', headerName: t("Premium"), type: 'number', width: 85, renderCell: (params) => {return ( - +
{parseFloat(parseFloat(params.row.premium).toFixed(4))+"%" }
- )} }, - ]} + )} + }]} 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 { ) } + + mainView = () => { + if (this.props.bookNotFound) { return this.NoOrdersFound() } + + const components = this.state.view == 'depth' ? [ + , + + ] : [ + this.bookListTableDesktop(), + this.bookListTablePhone() + ] + + return ( + <> + {/* Desktop */} + + +
+ {components[0]} +
+
+
+ {/* Smartphone */} + + +
+ {components[1]} +
+
+
+ + ) + } + + 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 ( this.setState({loading: true}) & this.getOrderDetails(2, 0)}> - + @@ -460,49 +491,34 @@ class BookPage extends Component { - { this.props.bookNotFound ? "" : + { this.props.bookNotFound ? <> : + + + {this.getTitle()} + + + } - - {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") - ) - } - + {this.mainView()} - } - - { this.props.bookNotFound ? - this.NoOrdersFound() - : - {/* Desktop Book */} - - - {this.bookListTableDesktop()} - - - - {/* Smartphone Book */} - - - {this.bookListTablePhone()} - - - - } - - { !this.props.bookNotFound ? - - : null - } - + + { !this.props.bookNotFound ? + <> + + + + : null + } + + ); diff --git a/frontend/src/components/BottomBar.js b/frontend/src/components/BottomBar.js index 00edf222..01ccc126 100644 --- a/frontend/src/components/BottomBar.js +++ b/frontend/src/components/BottomBar.js @@ -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 = () => { diff --git a/frontend/src/components/Charts/DepthChart/index.tsx b/frontend/src/components/Charts/DepthChart/index.tsx new file mode 100644 index 00000000..6a56261c --- /dev/null +++ b/frontend/src/components/Charts/DepthChart/index.tsx @@ -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 = ({ + bookLoading, orders, lastDayPremium, currency, setAppState, limits, compact +}) => { + const { t } = useTranslation() + const history = useHistory() + const theme = useTheme() + const [enrichedOrders, setEnrichedOrders] = useState([]) + const [series, setSeries] = useState([]) + const [xRange, setXRange] = useState(8) + const [xType, setXType] = useState("premium") + const [currencyCode, setCurrencyCode] = useState(1) + const [center, setCenter] = useState(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) => ( + + ) + + const generateTooltip: React.FunctionComponent = (pointTooltip: PointTooltipProps) => { + const order: Order = pointTooltip.point.data.order + return order ? ( + + + + + + + + + + + {order.maker_nick} + + + + + {amountToString(order.amount, order.has_range, order.min_amount, order.max_amount)} + {' '} + {currencyDict[order.currency]} + + + + + + + + + + + ) : <> + } + + 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 ? ( +
+ +
+ ) : ( + + + + + + + + + + setXRange(xRange + rangeSteps)}> + + + + + + {xType === 'base_amount' ? `${center} ${currencyDict[currencyCode]}` : `${center}%`} + + + + setXRange(xRange - rangeSteps)} disabled={xRange <= 1}> + + + + + + + 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']} + /> + + + ) +} + +export default DepthChart diff --git a/frontend/src/components/Charts/NivoScheme/index.ts b/frontend/src/components/Charts/NivoScheme/index.ts new file mode 100644 index 00000000..4c34a2eb --- /dev/null +++ b/frontend/src/components/Charts/NivoScheme/index.ts @@ -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 diff --git a/frontend/src/components/HomePage.js b/frontend/src/components/HomePage.js index 55205e1b..e3fae635 100644 --- a/frontend/src/components/HomePage.js +++ b/frontend/src/components/HomePage.js @@ -26,6 +26,8 @@ export default class HomePage extends Component { lastOrderId: null, earnedRewards: 0, referralCode:'', + lastDayPremium: 0, + limits: {} } } diff --git a/frontend/src/components/Robots/RobotAvatar/index.tsx b/frontend/src/components/Robots/RobotAvatar/index.tsx new file mode 100644 index 00000000..c8520308 --- /dev/null +++ b/frontend/src/components/Robots/RobotAvatar/index.tsx @@ -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 = ({ order }) => { + const { t } = useTranslation() + + const avatarSrc: string = window.location.origin +'/static/assets/avatars/' + order?.maker_nick + '.png' + + const statusBadge = ( +
+ {order?.type === 0 ? + : + } +
+ ) + + 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 ? ( + + + +
+ {order.maker_nick} +
+
+
+
+ ) : <> +} + +export default RobotAvatar diff --git a/frontend/src/models/Limit.model.ts b/frontend/src/models/Limit.model.ts new file mode 100644 index 00000000..41712013 --- /dev/null +++ b/frontend/src/models/Limit.model.ts @@ -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 diff --git a/frontend/src/models/Order.model.ts b/frontend/src/models/Order.model.ts new file mode 100644 index 00000000..1e47a2e2 --- /dev/null +++ b/frontend/src/models/Order.model.ts @@ -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 diff --git a/frontend/src/utils/match.ts b/frontend/src/utils/match.ts new file mode 100644 index 00000000..ed8d325b --- /dev/null +++ b/frontend/src/utils/match.ts @@ -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 diff --git a/frontend/src/utils/prettyNumbers.test.ts b/frontend/src/utils/prettyNumbers.test.ts index ef4dbae5..b9382460 100644 --- a/frontend/src/utils/prettyNumbers.test.ts +++ b/frontend/src/utils/prettyNumbers.test.ts @@ -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: "NaN"}, + {input: undefined, output: "NaN"}, + {input: ["", false, 50, 150] , output: "0"}, + {input: ["100.00", false, 50, 150] , output: "100"}, + {input: ["100.00", true, undefined, undefined] , output: "NaN-NaN"}, + {input: ["100.00", true, undefined, 150] , output: "NaN-150"}, + {input: ["100.00", true, 50, undefined] , output: "50-NaN"}, + {input: ["100.00", true, 50, 150] , output: "50-150"}, + ].forEach((it) => { + const params: any[] = it.input || [] + const response = amountToString(params[0],params[1],params[2],params[3]); + expect(response).toBe(it.output); + }); + }); +}) diff --git a/frontend/src/utils/prettyNumbers.ts b/frontend/src/utils/prettyNumbers.ts index 17f20a5c..86d0f968 100644 --- a/frontend/src/utils/prettyNumbers.ts +++ b/frontend/src/utils/prettyNumbers.ts @@ -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 diff --git a/frontend/static/locales/en.json b/frontend/static/locales/en.json index b038f568..667721db 100644 --- a/frontend/static/locales/en.json +++ b/frontend/static/locales/en.json @@ -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", diff --git a/frontend/static/locales/es.json b/frontend/static/locales/es.json index dde31970..b9a6a2ce 100644 --- a/frontend/static/locales/es.json +++ b/frontend/static/locales/es.json @@ -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", diff --git a/frontend/static/locales/ru.json b/frontend/static/locales/ru.json index cd287755..cc2c2ca3 100644 --- a/frontend/static/locales/ru.json +++ b/frontend/static/locales/ru.json @@ -164,6 +164,8 @@ "Show filters":"Показать фильтры", "yes":"да", "no":"нет", + "Depth chart": "Схемами глубин", + "Chart": "Схемами", "BOTTOM BAR AND MISC - BottomBar.js":"Bottom Bar user profile and miscellaneous dialogs",