mirror of
https://github.com/RoboSats/robosats.git
synced 2024-12-14 03:16:24 +00:00
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:
parent
e613cd9ffa
commit
458f5ac4e7
278
frontend/package-lock.json
generated
278
frontend/package-lock.json
generated
@ -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",
|
||||
|
@ -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",
|
||||
|
@ -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>
|
||||
);
|
||||
|
@ -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 = () => {
|
||||
|
346
frontend/src/components/Charts/DepthChart/index.tsx
Normal file
346
frontend/src/components/Charts/DepthChart/index.tsx
Normal 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
|
61
frontend/src/components/Charts/NivoScheme/index.ts
Normal file
61
frontend/src/components/Charts/NivoScheme/index.ts
Normal 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
|
@ -26,6 +26,8 @@ export default class HomePage extends Component {
|
||||
lastOrderId: null,
|
||||
earnedRewards: 0,
|
||||
referralCode:'',
|
||||
lastDayPremium: 0,
|
||||
limits: {}
|
||||
}
|
||||
}
|
||||
|
||||
|
61
frontend/src/components/Robots/RobotAvatar/index.tsx
Normal file
61
frontend/src/components/Robots/RobotAvatar/index.tsx
Normal 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
|
13
frontend/src/models/Limit.model.ts
Normal file
13
frontend/src/models/Limit.model.ts
Normal 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
|
24
frontend/src/models/Order.model.ts
Normal file
24
frontend/src/models/Order.model.ts
Normal 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
|
7
frontend/src/utils/match.ts
Normal file
7
frontend/src/utils/match.ts
Normal 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
|
@ -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);
|
||||
});
|
||||
});
|
||||
})
|
||||
|
@ -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
|
||||
|
@ -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",
|
||||
|
@ -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",
|
||||
|
@ -164,6 +164,8 @@
|
||||
"Show filters":"Показать фильтры",
|
||||
"yes":"да",
|
||||
"no":"нет",
|
||||
"Depth chart": "Схемами глубин",
|
||||
"Chart": "Схемами",
|
||||
|
||||
|
||||
"BOTTOM BAR AND MISC - BottomBar.js":"Bottom Bar user profile and miscellaneous dialogs",
|
||||
|
Loading…
Reference in New Issue
Block a user