From d5ba08cf67746fa87ecd5250f38f0185f4731561 Mon Sep 17 00:00:00 2001 From: apotdevin Date: Thu, 17 Jun 2021 11:42:40 +0200 Subject: [PATCH] chore: dash and settings --- config/client.tsx | 24 -- package-lock.json | 359 +++++++++++++++--- package.json | 5 + pages/_app.tsx | 5 +- pages/channels.tsx | 7 +- pages/dashboard.tsx | 35 ++ pages/leaderboard.tsx | 4 +- pages/settings/dashboard.tsx | 25 ++ pages/{settings.tsx => settings/index.tsx} | 12 +- pages/transactions.tsx | 26 +- src/components/chart/BarChart.tsx | 232 +++++++++++ src/components/chart/HorizontalBarChart.tsx | 189 +++++++++ src/components/gridWrapper/GridWrapper.tsx | 9 + src/components/section/Section.tsx | 2 +- src/components/select/index.tsx | 71 +++- src/context/ContextProvider.tsx | 13 +- src/context/DashContext.tsx | 54 +++ src/hooks/UseBaseConnect.tsx | 4 +- src/hooks/UseElementSize.tsx | 40 ++ src/hooks/UseEventListener.tsx | 39 ++ src/layouts/header/Header.tsx | 2 +- src/layouts/navigation/Navigation.tsx | 8 +- src/utils/gridConstants.ts | 11 + .../channels/channels/ChannelBosScore.tsx | 2 +- src/views/channels/channels/Channels.tsx | 5 +- src/views/dashboard/index.tsx | 126 ++++++ src/views/dashboard/modal/index.tsx | 48 +++ .../dashboard/widgets/external/mempool.tsx | 40 ++ src/views/dashboard/widgets/helpers.tsx | 185 +++++++++ .../dashboard/widgets/lightning/balances.tsx | 84 ++++ .../dashboard/widgets/lightning/channels.tsx | 18 + .../dashboard/widgets/lightning/forwards.tsx | 72 ++++ .../widgets/lightning/forwardsGraph.tsx | 123 ++++++ .../dashboard/widgets/lightning/info.tsx | 50 +++ .../widgets/lightning/invoiceGraph.tsx | 140 +++++++ .../widgets/lightning/liquidityGraph.tsx | 71 ++++ .../dashboard/widgets/lightning/modal.tsx | 69 ++++ .../widgets/lightning/paymentGraph.tsx | 136 +++++++ .../widgets/lightning/transactions.tsx | 91 +++++ .../widgets/lightning/transactionsGraph.tsx | 147 +++++++ src/views/dashboard/widgets/link/index.tsx | 60 +++ .../dashboard/widgets/settings/index.tsx | 76 ++++ src/views/dashboard/widgets/util/Convert.tsx | 159 ++++++++ .../dashboard/widgets/util/DonateWidget.tsx | 38 ++ src/views/dashboard/widgets/util/Sign.tsx | 27 ++ src/views/dashboard/widgets/widgetList.tsx | 301 +++++++++++++++ src/views/home/account/AccountInfo.tsx | 8 +- src/views/home/quickActions/QuickActions.tsx | 6 +- .../home/quickActions/donate/DonateCard.tsx | 5 - .../quickActions/donate/DonateContent.tsx | 124 +++--- src/views/settings/DashPanel.tsx | 101 +++++ src/views/settings/Dashboard.tsx | 27 ++ src/views/settings/WidgetRow.tsx | 47 +++ src/views/tools/messages/Messages.tsx | 4 +- src/views/tools/messages/SignMessage.tsx | 44 ++- 55 files changed, 3390 insertions(+), 220 deletions(-) create mode 100644 pages/dashboard.tsx create mode 100644 pages/settings/dashboard.tsx rename pages/{settings.tsx => settings/index.tsx} (64%) create mode 100644 src/components/chart/BarChart.tsx create mode 100644 src/components/chart/HorizontalBarChart.tsx create mode 100644 src/context/DashContext.tsx create mode 100644 src/hooks/UseElementSize.tsx create mode 100644 src/hooks/UseEventListener.tsx create mode 100644 src/utils/gridConstants.ts create mode 100644 src/views/dashboard/index.tsx create mode 100644 src/views/dashboard/modal/index.tsx create mode 100644 src/views/dashboard/widgets/external/mempool.tsx create mode 100644 src/views/dashboard/widgets/helpers.tsx create mode 100644 src/views/dashboard/widgets/lightning/balances.tsx create mode 100644 src/views/dashboard/widgets/lightning/channels.tsx create mode 100644 src/views/dashboard/widgets/lightning/forwards.tsx create mode 100644 src/views/dashboard/widgets/lightning/forwardsGraph.tsx create mode 100644 src/views/dashboard/widgets/lightning/info.tsx create mode 100644 src/views/dashboard/widgets/lightning/invoiceGraph.tsx create mode 100644 src/views/dashboard/widgets/lightning/liquidityGraph.tsx create mode 100644 src/views/dashboard/widgets/lightning/modal.tsx create mode 100644 src/views/dashboard/widgets/lightning/paymentGraph.tsx create mode 100644 src/views/dashboard/widgets/lightning/transactions.tsx create mode 100644 src/views/dashboard/widgets/lightning/transactionsGraph.tsx create mode 100644 src/views/dashboard/widgets/link/index.tsx create mode 100644 src/views/dashboard/widgets/settings/index.tsx create mode 100644 src/views/dashboard/widgets/util/Convert.tsx create mode 100644 src/views/dashboard/widgets/util/DonateWidget.tsx create mode 100644 src/views/dashboard/widgets/util/Sign.tsx create mode 100644 src/views/dashboard/widgets/widgetList.tsx create mode 100644 src/views/settings/DashPanel.tsx create mode 100644 src/views/settings/Dashboard.tsx create mode 100644 src/views/settings/WidgetRow.tsx diff --git a/config/client.tsx b/config/client.tsx index f80a2cb8..61dce71e 100644 --- a/config/client.tsx +++ b/config/client.tsx @@ -43,35 +43,11 @@ function createApolloClient(context?: ResolverContext) { ssrMode: typeof window === 'undefined', link: createIsomorphLink(context), cache: new InMemoryCache({ - typePolicies: { - Query: { - fields: { - getResume: { - keyArgs: [], - merge(existing, incoming) { - if (!existing) return incoming; - - const current = existing?.resume ? [...existing.resume] : []; - const newIncoming = incoming?.resume - ? [...incoming.resume] - : []; - - return { - ...existing, - offset: incoming.offset, - resume: [...current, ...newIncoming], - }; - }, - }, - }, - }, - }, ...possibleTypes, }), defaultOptions: { query: { fetchPolicy: 'cache-first', - // errorPolicy: 'all', }, }, }); diff --git a/package-lock.json b/package-lock.json index c1301ba5..5763ed9f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5,12 +5,13 @@ "requires": true, "packages": { "": { - "version": "0.12.17", + "version": "0.12.19", "license": "MIT", "dependencies": { "@apollo/client": "^3.3.19", "@emotion/babel-plugin": "^11.3.0", "@next/bundle-analyzer": "^10.2.3", + "@visx/axis": "^1.12.0", "@visx/chord": "^1.7.0", "@visx/curve": "^1.7.0", "@visx/event": "^1.7.0", @@ -30,6 +31,7 @@ "cookie": "^0.4.1", "crypto-js": "^4.0.0", "d3-array": "^2.12.1", + "d3-time-format": "^3.0.0", "date-fns": "^2.22.1", "graphql": "^15.5.0", "graphql-iso-date": "^3.6.1", @@ -53,6 +55,7 @@ "react-copy-to-clipboard": "^5.0.3", "react-dom": "^17.0.2", "react-feather": "^2.0.9", + "react-grid-layout": "^1.2.5", "react-intersection-observer": "^8.32.0", "react-qr-reader": "^2.2.1", "react-select": "^4.3.1", @@ -92,6 +95,7 @@ "@types/cookie": "^0.4.0", "@types/crypto-js": "^4.0.1", "@types/d3-array": "^2.12.1", + "@types/d3-time-format": "^3.0.0", "@types/graphql-iso-date": "^3.4.0", "@types/js-cookie": "^2.2.6", "@types/js-yaml": "^4.0.1", @@ -106,6 +110,7 @@ "@types/qrcode.react": "^1.0.1", "@types/react": "^17.0.8", "@types/react-copy-to-clipboard": "^5.0.0", + "@types/react-grid-layout": "^1.1.1", "@types/react-qr-reader": "^2.1.3", "@types/react-select": "^4.0.15", "@types/react-slider": "^1.1.2", @@ -363,14 +368,12 @@ "node_modules/@babel/compat-data": { "version": "7.14.4", "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.14.4.tgz", - "integrity": "sha512-i2wXrWQNkH6JplJQGn3Rd2I4Pij8GdHkXwHMxm+zV5YG/Jci+bCNrWZEWC4o+umiDkRrRs4dVzH3X4GP7vyjQQ==", - "dev": true + "integrity": "sha512-i2wXrWQNkH6JplJQGn3Rd2I4Pij8GdHkXwHMxm+zV5YG/Jci+bCNrWZEWC4o+umiDkRrRs4dVzH3X4GP7vyjQQ==" }, "node_modules/@babel/core": { "version": "7.14.3", "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.14.3.tgz", "integrity": "sha512-jB5AmTKOCSJIZ72sd78ECEhuPiDMKlQdDI/4QRI6lzYATx5SSogS1oQA2AoPecRCknm30gHi2l+QVvNUu3wZAg==", - "dev": true, "dependencies": { "@babel/code-frame": "^7.12.13", "@babel/generator": "^7.14.3", @@ -428,7 +431,6 @@ "version": "7.14.4", "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.14.4.tgz", "integrity": "sha512-JgdzOYZ/qGaKTVkn5qEDV/SXAh8KcyUVkCoSWGN8T3bwrgd6m+/dJa2kVGi6RJYJgEYPBdZ84BZp9dUjNWkBaA==", - "dev": true, "dependencies": { "@babel/compat-data": "^7.14.4", "@babel/helper-validator-option": "^7.12.17", @@ -510,7 +512,6 @@ "version": "7.13.12", "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.13.12.tgz", "integrity": "sha512-48ql1CLL59aKbU94Y88Xgb2VFy7a95ykGRbJJaaVv+LX5U8wFpLfiGXJJGUozsmA1oEh/o5Bp60Voq7ACyA/Sw==", - "dev": true, "dependencies": { "@babel/types": "^7.13.12" } @@ -527,7 +528,6 @@ "version": "7.14.2", "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.14.2.tgz", "integrity": "sha512-OznJUda/soKXv0XhpvzGWDnml4Qnwp16GN+D/kZIdLsWoHj05kyu8Rm5kXmMef+rVJZ0+4pSGLkeixdqNUATDA==", - "dev": true, "dependencies": { "@babel/helper-module-imports": "^7.13.12", "@babel/helper-replace-supers": "^7.13.12", @@ -543,7 +543,6 @@ "version": "7.12.13", "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.12.13.tgz", "integrity": "sha512-BdWQhoVJkp6nVjB7nkFWcn43dkprYauqtk++Py2eaf/GRDFm5BxRqEIZCiHlZUGAVmtwKcsVL1dC68WmzeFmiA==", - "dev": true, "dependencies": { "@babel/types": "^7.12.13" } @@ -568,7 +567,6 @@ "version": "7.14.4", "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.14.4.tgz", "integrity": "sha512-zZ7uHCWlxfEAAOVDYQpEf/uyi1dmeC7fX4nCf2iz9drnCwi1zvwXL3HwWWNXUQEJ1k23yVn3VbddiI9iJEXaTQ==", - "dev": true, "dependencies": { "@babel/helper-member-expression-to-functions": "^7.13.12", "@babel/helper-optimise-call-expression": "^7.12.13", @@ -580,7 +578,6 @@ "version": "7.13.12", "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.13.12.tgz", "integrity": "sha512-7FEjbrx5SL9cWvXioDbnlYTppcZGuCY6ow3/D5vMggb2Ywgu4dMrpTJX0JdQAIcRRUElOIxF3yEooa9gUb9ZbA==", - "dev": true, "dependencies": { "@babel/types": "^7.13.12" } @@ -610,8 +607,7 @@ "node_modules/@babel/helper-validator-option": { "version": "7.12.17", "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.12.17.tgz", - "integrity": "sha512-TopkMDmLzq8ngChwRlyjR6raKD6gMSae4JdYDB8bByKreQgG0RBTuKe9LRxW3wFtUnjxOPRKBDwEH6Mg5KeDfw==", - "dev": true + "integrity": "sha512-TopkMDmLzq8ngChwRlyjR6raKD6gMSae4JdYDB8bByKreQgG0RBTuKe9LRxW3wFtUnjxOPRKBDwEH6Mg5KeDfw==" }, "node_modules/@babel/helper-wrap-function": { "version": "7.13.0", @@ -629,7 +625,6 @@ "version": "7.14.0", "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.14.0.tgz", "integrity": "sha512-+ufuXprtQ1D1iZTO/K9+EBRn+qPWMJjZSw/S0KlFrxCw4tkrzv9grgpDHkY9MeQTjTY8i2sp7Jep8DfU6tN9Mg==", - "dev": true, "dependencies": { "@babel/template": "^7.12.13", "@babel/traverse": "^7.14.0", @@ -5084,6 +5079,12 @@ "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-2.1.0.tgz", "integrity": "sha512-qVCiT93utxN0cawScyQuNx8H82vBvZXSClZfgOu3l3dRRlRO6FjKEZlaPgXG9XUFjIAOsA4kAJY101vobHeJLQ==" }, + "node_modules/@types/d3-time-format": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/d3-time-format/-/d3-time-format-3.0.0.tgz", + "integrity": "sha512-UpLg1mn/8PLyjr+J/JwdQJM/GzysMvv2CS8y+WYAL5K0+wbvXv/pPSLEfdNaprCZsGcXTxPsFMy8QtkYv9ueew==", + "dev": true + }, "node_modules/@types/express": { "version": "4.17.12", "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.12.tgz", @@ -5421,6 +5422,15 @@ "@types/react": "*" } }, + "node_modules/@types/react-grid-layout": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@types/react-grid-layout/-/react-grid-layout-1.1.1.tgz", + "integrity": "sha512-bvPkITzwGGOZKjp01nVSgPrdfGm/uTa5t8Odd8vQRXJsLj7uZLZXSXgWr+TiXBAkUsmHPxhsyswXQCiFeDuZnQ==", + "dev": true, + "dependencies": { + "@types/react": "*" + } + }, "node_modules/@types/react-qr-reader": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/@types/react-qr-reader/-/react-qr-reader-2.1.3.tgz", @@ -5808,6 +5818,25 @@ "url": "https://opencollective.com/typescript-eslint" } }, + "node_modules/@visx/axis": { + "version": "1.12.0", + "resolved": "https://registry.npmjs.org/@visx/axis/-/axis-1.12.0.tgz", + "integrity": "sha512-g867/6/TTHEjzVoIbnYjdYOiNGC3vETn7AZG9tT13KUy9AJE1gwm91pVoi+USHd+R3AvwdbgaQti88FavamoWA==", + "dependencies": { + "@types/classnames": "^2.2.9", + "@types/react": "*", + "@visx/group": "1.7.0", + "@visx/point": "1.7.0", + "@visx/scale": "1.11.1", + "@visx/shape": "1.12.0", + "@visx/text": "1.10.0", + "classnames": "^2.2.5", + "prop-types": "^15.6.0" + }, + "peerDependencies": { + "react": "^16.3.0-0" + } + }, "node_modules/@visx/bounds": { "version": "1.7.0", "resolved": "https://registry.npmjs.org/@visx/bounds/-/bounds-1.7.0.tgz", @@ -5904,9 +5933,9 @@ } }, "node_modules/@visx/shape": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@visx/shape/-/shape-1.11.1.tgz", - "integrity": "sha512-k5VF3+VeG0nNPycDyAdJywOI8tTglHWkZDL9ZTM1o4dLXbytwtWxEcfRTmDHyynYTca+WBsY7dapeGuvrmw6Hw==", + "version": "1.12.0", + "resolved": "https://registry.npmjs.org/@visx/shape/-/shape-1.12.0.tgz", + "integrity": "sha512-BiY5nXrpg/CdY2Vd/oIaEsQoYjL7Nb+7IGsRCT5DVNelulgnwU1P13SqdVvs4FUtp/WYy97djQuIrR/6zCHdyw==", "dependencies": { "@types/classnames": "^2.2.9", "@types/d3-path": "^1.0.8", @@ -5926,6 +5955,23 @@ "react": "^16.3.0-0" } }, + "node_modules/@visx/text": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@visx/text/-/text-1.10.0.tgz", + "integrity": "sha512-2y56LxbbSHAlu8XP0cARB9JlhPx0HlQyIC4iKUzj36+iJaBiw9wUwLb9RbT/rY715V+6VzU7WCNCxoa4lDr9Sg==", + "dependencies": { + "@types/classnames": "^2.2.9", + "@types/lodash": "^4.14.160", + "@types/react": "*", + "classnames": "^2.2.5", + "lodash": "^4.17.20", + "prop-types": "^15.7.2", + "reduce-css-calc": "^1.3.0" + }, + "peerDependencies": { + "react": "^16.3.0-0" + } + }, "node_modules/@visx/tooltip": { "version": "1.7.2", "resolved": "https://registry.npmjs.org/@visx/tooltip/-/tooltip-1.7.2.tgz", @@ -7298,8 +7344,7 @@ "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" }, "node_modules/balanceofsatoshis": { "version": "8.0.14", @@ -12003,7 +12048,6 @@ "version": "1.0.0-beta.2", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", - "dev": true, "engines": { "node": ">=6.9.0" } @@ -16677,7 +16721,6 @@ "version": "2.2.0", "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.0.tgz", "integrity": "sha512-f+8cldu7X/y7RAJurMEJmdoKXGB/X550w2Nr3tTbezL6RwEE/iMcm+tZnXeoZtKuOq6ft8+CqzEkrIgx1fPoQA==", - "dev": true, "dependencies": { "minimist": "^1.2.5" }, @@ -18436,6 +18479,11 @@ "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", "integrity": "sha1-bC4XHbKiV82WgC/UOwGyDV9YcPY=" }, + "node_modules/lodash.isequal": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz", + "integrity": "sha1-QVxEePK8wwEgwizhDtMib30+GOA=" + }, "node_modules/lodash.isinteger": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", @@ -18865,6 +18913,11 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/math-expression-evaluator": { + "version": "1.3.7", + "resolved": "https://registry.npmjs.org/math-expression-evaluator/-/math-expression-evaluator-1.3.7.tgz", + "integrity": "sha512-nrbaifCl42w37hYd6oRLvoymFK42tWB+WQTMFtksDGQMi5GvlJwnz/CsS30FFAISFLtX+A0csJ0xLiuuyyec7w==" + }, "node_modules/md5.js": { "version": "1.3.5", "resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.5.tgz", @@ -21602,6 +21655,15 @@ "react": "17.0.2" } }, + "node_modules/react-draggable": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/react-draggable/-/react-draggable-4.4.3.tgz", + "integrity": "sha512-jV4TE59MBuWm7gb6Ns3Q1mxX8Azffb7oTtDtBgFkxRvhDp38YAARmRplrj0+XGkhOJB5XziArX+4HUUABtyZ0w==", + "dependencies": { + "classnames": "^2.2.5", + "prop-types": "^15.6.0" + } + }, "node_modules/react-fast-compare": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-2.0.4.tgz", @@ -21618,6 +21680,22 @@ "react": "^16.8.6 || ^17" } }, + "node_modules/react-grid-layout": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/react-grid-layout/-/react-grid-layout-1.2.5.tgz", + "integrity": "sha512-P/NNWAExTX/zEq+RUh6hrIG67UBicDNCOOg9LZe8BAtSdYtCnCGgVmWBS+sIbM0C8RJIiyGsFHh5dIfCddhS/w==", + "dependencies": { + "classnames": "2.3.1", + "lodash.isequal": "^4.0.0", + "prop-types": "^15.0.0", + "react-draggable": "^4.0.0", + "react-resizable": "^3.0.1" + }, + "peerDependencies": { + "react": ">= 16.3.0", + "react-dom": ">= 16.3.0" + } + }, "node_modules/react-input-autosize": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/react-input-autosize/-/react-input-autosize-3.0.0.tgz", @@ -21664,6 +21742,18 @@ "node": ">=0.10.0" } }, + "node_modules/react-resizable": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/react-resizable/-/react-resizable-3.0.4.tgz", + "integrity": "sha512-StnwmiESiamNzdRHbSSvA65b0ZQJ7eVQpPusrSmcpyGKzC0gojhtO62xxH6YOBmepk9dQTBi9yxidL3W4s3EBA==", + "dependencies": { + "prop-types": "15.x", + "react-draggable": "^4.0.3" + }, + "peerDependencies": { + "react": ">= 16.3" + } + }, "node_modules/react-select": { "version": "4.3.1", "resolved": "https://registry.npmjs.org/react-select/-/react-select-4.3.1.tgz", @@ -21982,6 +22072,29 @@ "node": ">=8" } }, + "node_modules/reduce-css-calc": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/reduce-css-calc/-/reduce-css-calc-1.3.0.tgz", + "integrity": "sha1-dHyRTgSWFKTJz7umKYca0dKSdxY=", + "dependencies": { + "balanced-match": "^0.4.2", + "math-expression-evaluator": "^1.2.14", + "reduce-function-call": "^1.0.1" + } + }, + "node_modules/reduce-css-calc/node_modules/balanced-match": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-0.4.2.tgz", + "integrity": "sha1-yz8+PHMtwPAe5wtAPzAuYddwmDg=" + }, + "node_modules/reduce-function-call": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/reduce-function-call/-/reduce-function-call-1.0.3.tgz", + "integrity": "sha512-Hl/tuV2VDgWgCSEeWMLwxLZqX7OK59eU1guxXsRKTAyeYimivsKdtcV4fu3r710tpG5GmDKDhQ0HSZLExnNmyQ==", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, "node_modules/regenerate": { "version": "1.4.2", "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz", @@ -26632,14 +26745,12 @@ "@babel/compat-data": { "version": "7.14.4", "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.14.4.tgz", - "integrity": "sha512-i2wXrWQNkH6JplJQGn3Rd2I4Pij8GdHkXwHMxm+zV5YG/Jci+bCNrWZEWC4o+umiDkRrRs4dVzH3X4GP7vyjQQ==", - "dev": true + "integrity": "sha512-i2wXrWQNkH6JplJQGn3Rd2I4Pij8GdHkXwHMxm+zV5YG/Jci+bCNrWZEWC4o+umiDkRrRs4dVzH3X4GP7vyjQQ==" }, "@babel/core": { "version": "7.14.3", "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.14.3.tgz", "integrity": "sha512-jB5AmTKOCSJIZ72sd78ECEhuPiDMKlQdDI/4QRI6lzYATx5SSogS1oQA2AoPecRCknm30gHi2l+QVvNUu3wZAg==", - "dev": true, "requires": { "@babel/code-frame": "^7.12.13", "@babel/generator": "^7.14.3", @@ -26690,7 +26801,6 @@ "version": "7.14.4", "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.14.4.tgz", "integrity": "sha512-JgdzOYZ/qGaKTVkn5qEDV/SXAh8KcyUVkCoSWGN8T3bwrgd6m+/dJa2kVGi6RJYJgEYPBdZ84BZp9dUjNWkBaA==", - "dev": true, "requires": { "@babel/compat-data": "^7.14.4", "@babel/helper-validator-option": "^7.12.17", @@ -26763,7 +26873,6 @@ "version": "7.13.12", "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.13.12.tgz", "integrity": "sha512-48ql1CLL59aKbU94Y88Xgb2VFy7a95ykGRbJJaaVv+LX5U8wFpLfiGXJJGUozsmA1oEh/o5Bp60Voq7ACyA/Sw==", - "dev": true, "requires": { "@babel/types": "^7.13.12" } @@ -26780,7 +26889,6 @@ "version": "7.14.2", "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.14.2.tgz", "integrity": "sha512-OznJUda/soKXv0XhpvzGWDnml4Qnwp16GN+D/kZIdLsWoHj05kyu8Rm5kXmMef+rVJZ0+4pSGLkeixdqNUATDA==", - "dev": true, "requires": { "@babel/helper-module-imports": "^7.13.12", "@babel/helper-replace-supers": "^7.13.12", @@ -26796,7 +26904,6 @@ "version": "7.12.13", "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.12.13.tgz", "integrity": "sha512-BdWQhoVJkp6nVjB7nkFWcn43dkprYauqtk++Py2eaf/GRDFm5BxRqEIZCiHlZUGAVmtwKcsVL1dC68WmzeFmiA==", - "dev": true, "requires": { "@babel/types": "^7.12.13" } @@ -26821,7 +26928,6 @@ "version": "7.14.4", "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.14.4.tgz", "integrity": "sha512-zZ7uHCWlxfEAAOVDYQpEf/uyi1dmeC7fX4nCf2iz9drnCwi1zvwXL3HwWWNXUQEJ1k23yVn3VbddiI9iJEXaTQ==", - "dev": true, "requires": { "@babel/helper-member-expression-to-functions": "^7.13.12", "@babel/helper-optimise-call-expression": "^7.12.13", @@ -26833,7 +26939,6 @@ "version": "7.13.12", "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.13.12.tgz", "integrity": "sha512-7FEjbrx5SL9cWvXioDbnlYTppcZGuCY6ow3/D5vMggb2Ywgu4dMrpTJX0JdQAIcRRUElOIxF3yEooa9gUb9ZbA==", - "dev": true, "requires": { "@babel/types": "^7.13.12" } @@ -26863,8 +26968,7 @@ "@babel/helper-validator-option": { "version": "7.12.17", "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.12.17.tgz", - "integrity": "sha512-TopkMDmLzq8ngChwRlyjR6raKD6gMSae4JdYDB8bByKreQgG0RBTuKe9LRxW3wFtUnjxOPRKBDwEH6Mg5KeDfw==", - "dev": true + "integrity": "sha512-TopkMDmLzq8ngChwRlyjR6raKD6gMSae4JdYDB8bByKreQgG0RBTuKe9LRxW3wFtUnjxOPRKBDwEH6Mg5KeDfw==" }, "@babel/helper-wrap-function": { "version": "7.13.0", @@ -26882,7 +26986,6 @@ "version": "7.14.0", "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.14.0.tgz", "integrity": "sha512-+ufuXprtQ1D1iZTO/K9+EBRn+qPWMJjZSw/S0KlFrxCw4tkrzv9grgpDHkY9MeQTjTY8i2sp7Jep8DfU6tN9Mg==", - "dev": true, "requires": { "@babel/template": "^7.12.13", "@babel/traverse": "^7.14.0", @@ -29187,7 +29290,8 @@ "@graphql-typed-document-node/core": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/@graphql-typed-document-node/core/-/core-3.1.0.tgz", - "integrity": "sha512-wYn6r8zVZyQJ6rQaALBEln5B1pzxb9shV5Ef97kTvn6yVGrqyXVnDqnU24MXnFubR+rZjBY9NWuxX3FB2sTsjg==" + "integrity": "sha512-wYn6r8zVZyQJ6rQaALBEln5B1pzxb9shV5Ef97kTvn6yVGrqyXVnDqnU24MXnFubR+rZjBY9NWuxX3FB2sTsjg==", + "requires": {} }, "@grpc/grpc-js": { "version": "1.2.12", @@ -29948,7 +30052,8 @@ "@next/react-refresh-utils": { "version": "10.2.3", "resolved": "https://registry.npmjs.org/@next/react-refresh-utils/-/react-refresh-utils-10.2.3.tgz", - "integrity": "sha512-qtBF56vPC6d6a8p7LYd0iRjW89fhY80kAIzmj+VonvIGjK/nymBjcFUhbKiMFqlhsarCksnhwX+Zmn95Dw9qvA==" + "integrity": "sha512-qtBF56vPC6d6a8p7LYd0iRjW89fhY80kAIzmj+VonvIGjK/nymBjcFUhbKiMFqlhsarCksnhwX+Zmn95Dw9qvA==", + "requires": {} }, "@nodelib/fs.scandir": { "version": "2.1.4", @@ -30507,6 +30612,12 @@ "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-2.1.0.tgz", "integrity": "sha512-qVCiT93utxN0cawScyQuNx8H82vBvZXSClZfgOu3l3dRRlRO6FjKEZlaPgXG9XUFjIAOsA4kAJY101vobHeJLQ==" }, + "@types/d3-time-format": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/d3-time-format/-/d3-time-format-3.0.0.tgz", + "integrity": "sha512-UpLg1mn/8PLyjr+J/JwdQJM/GzysMvv2CS8y+WYAL5K0+wbvXv/pPSLEfdNaprCZsGcXTxPsFMy8QtkYv9ueew==", + "dev": true + }, "@types/express": { "version": "4.17.12", "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.12.tgz", @@ -30843,6 +30954,15 @@ "@types/react": "*" } }, + "@types/react-grid-layout": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@types/react-grid-layout/-/react-grid-layout-1.1.1.tgz", + "integrity": "sha512-bvPkITzwGGOZKjp01nVSgPrdfGm/uTa5t8Odd8vQRXJsLj7uZLZXSXgWr+TiXBAkUsmHPxhsyswXQCiFeDuZnQ==", + "dev": true, + "requires": { + "@types/react": "*" + } + }, "@types/react-qr-reader": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/@types/react-qr-reader/-/react-qr-reader-2.1.3.tgz", @@ -31147,6 +31267,22 @@ "eslint-visitor-keys": "^2.0.0" } }, + "@visx/axis": { + "version": "1.12.0", + "resolved": "https://registry.npmjs.org/@visx/axis/-/axis-1.12.0.tgz", + "integrity": "sha512-g867/6/TTHEjzVoIbnYjdYOiNGC3vETn7AZG9tT13KUy9AJE1gwm91pVoi+USHd+R3AvwdbgaQti88FavamoWA==", + "requires": { + "@types/classnames": "^2.2.9", + "@types/react": "*", + "@visx/group": "1.7.0", + "@visx/point": "1.7.0", + "@visx/scale": "1.11.1", + "@visx/shape": "1.12.0", + "@visx/text": "1.10.0", + "classnames": "^2.2.5", + "prop-types": "^15.6.0" + } + }, "@visx/bounds": { "version": "1.7.0", "resolved": "https://registry.npmjs.org/@visx/bounds/-/bounds-1.7.0.tgz", @@ -31230,9 +31366,9 @@ } }, "@visx/shape": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@visx/shape/-/shape-1.11.1.tgz", - "integrity": "sha512-k5VF3+VeG0nNPycDyAdJywOI8tTglHWkZDL9ZTM1o4dLXbytwtWxEcfRTmDHyynYTca+WBsY7dapeGuvrmw6Hw==", + "version": "1.12.0", + "resolved": "https://registry.npmjs.org/@visx/shape/-/shape-1.12.0.tgz", + "integrity": "sha512-BiY5nXrpg/CdY2Vd/oIaEsQoYjL7Nb+7IGsRCT5DVNelulgnwU1P13SqdVvs4FUtp/WYy97djQuIrR/6zCHdyw==", "requires": { "@types/classnames": "^2.2.9", "@types/d3-path": "^1.0.8", @@ -31249,6 +31385,20 @@ "prop-types": "^15.5.10" } }, + "@visx/text": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@visx/text/-/text-1.10.0.tgz", + "integrity": "sha512-2y56LxbbSHAlu8XP0cARB9JlhPx0HlQyIC4iKUzj36+iJaBiw9wUwLb9RbT/rY715V+6VzU7WCNCxoa4lDr9Sg==", + "requires": { + "@types/classnames": "^2.2.9", + "@types/lodash": "^4.14.160", + "@types/react": "*", + "classnames": "^2.2.5", + "lodash": "^4.17.20", + "prop-types": "^15.7.2", + "reduce-css-calc": "^1.3.0" + } + }, "@visx/tooltip": { "version": "1.7.2", "resolved": "https://registry.npmjs.org/@visx/tooltip/-/tooltip-1.7.2.tgz", @@ -31359,7 +31509,8 @@ "version": "5.3.1", "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.1.tgz", "integrity": "sha512-K0Ptm/47OKfQRpNQ2J/oIN/3QYiK6FwW+eJbILhsdxh2WTLdl+30o8aGdTbm5JbffpFFAg/g+zi1E+jvJha5ng==", - "dev": true + "dev": true, + "requires": {} }, "acorn-walk": { "version": "7.2.0", @@ -31414,7 +31565,8 @@ "version": "3.5.2", "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", - "dev": true + "dev": true, + "requires": {} }, "anser": { "version": "1.4.9", @@ -31645,7 +31797,8 @@ "apollo-server-errors": { "version": "2.5.0", "resolved": "https://registry.npmjs.org/apollo-server-errors/-/apollo-server-errors-2.5.0.tgz", - "integrity": "sha512-lO5oTjgiC3vlVg2RKr3RiXIIQ5pGXBFxYGGUkKDhTud3jMIhs+gel8L8zsEjKaKxkjHhCQAA/bcEfYiKkGQIvA==" + "integrity": "sha512-lO5oTjgiC3vlVg2RKr3RiXIIQ5pGXBFxYGGUkKDhTud3jMIhs+gel8L8zsEjKaKxkjHhCQAA/bcEfYiKkGQIvA==", + "requires": {} }, "apollo-server-express": { "version": "2.25.0", @@ -32356,8 +32509,7 @@ "balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" }, "balanceofsatoshis": { "version": "8.0.14", @@ -35276,7 +35428,8 @@ "version": "8.3.0", "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-8.3.0.tgz", "integrity": "sha512-BgZuLUSeKzvlL/VUjx/Yb787VQ26RU3gGjA3iiFvdsp/2bMfVIWUVP7tjxtjS0e+HP409cPlPvNkQloz8C91ew==", - "dev": true + "dev": true, + "requires": {} }, "eslint-import-resolver-node": { "version": "0.3.4", @@ -35506,7 +35659,8 @@ "version": "4.2.0", "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.2.0.tgz", "integrity": "sha512-623WEiZJqxR7VdxFCKLI6d6LLpwJkGPYKODnkH3D7WpOG5KM8yWueBd8TLsNAetEJNF5iJmolaAKO3F8yzyVBQ==", - "dev": true + "dev": true, + "requires": {} }, "eslint-scope": { "version": "5.1.1", @@ -36148,8 +36302,7 @@ "gensync": { "version": "1.0.0-beta.2", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", - "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", - "dev": true + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==" }, "get-caller-file": { "version": "2.0.5", @@ -36693,7 +36846,8 @@ "ws": { "version": "7.4.4", "resolved": "https://registry.npmjs.org/ws/-/ws-7.4.4.tgz", - "integrity": "sha512-Qm8k8ojNQIMx7S+Zp8u/uHOx7Qazv3Yv4q68MiWWWOJhiwG5W3x7iqmRtJo8xxrciZUY4vRxUTJCKuRnF28ZZw==" + "integrity": "sha512-Qm8k8ojNQIMx7S+Zp8u/uHOx7Qazv3Yv4q68MiWWWOJhiwG5W3x7iqmRtJo8xxrciZUY4vRxUTJCKuRnF28ZZw==", + "requires": {} } } }, @@ -36791,7 +36945,8 @@ "graphql-iso-date": { "version": "3.6.1", "resolved": "https://registry.npmjs.org/graphql-iso-date/-/graphql-iso-date-3.6.1.tgz", - "integrity": "sha512-AwFGIuYMJQXOEAgRlJlFL4H1ncFM8n8XmoVDTNypNOZyQ8LFDG2ppMFlsS862BSTCDcSUfHp8PD3/uJhv7t59Q==" + "integrity": "sha512-AwFGIuYMJQXOEAgRlJlFL4H1ncFM8n8XmoVDTNypNOZyQ8LFDG2ppMFlsS862BSTCDcSUfHp8PD3/uJhv7t59Q==", + "requires": {} }, "graphql-middleware": { "version": "6.0.10", @@ -36901,7 +37056,8 @@ "version": "4.7.0", "resolved": "https://registry.npmjs.org/graphql-ws/-/graphql-ws-4.7.0.tgz", "integrity": "sha512-Md8SsmC9ZlsogFPd3Ot8HbIAAqsHh8Xoq7j4AmcIat1Bh6k91tjVyQvA0Au1/BolXSYq+RDvib6rATU2Hcf1Xw==", - "dev": true + "dev": true, + "requires": {} }, "gtoken": { "version": "5.2.1", @@ -37861,7 +38017,8 @@ "version": "4.0.1", "resolved": "https://registry.npmjs.org/isomorphic-ws/-/isomorphic-ws-4.0.1.tgz", "integrity": "sha512-BhBvN2MBpWTaSHdWRb/bwdZJ1WaehQ2L1KngkCkfLUGF0mAWAT1sQUQacEmQ0jXkFw/czDXPNQSL5u2/Krsz1w==", - "dev": true + "dev": true, + "requires": {} }, "isstream": { "version": "0.1.2", @@ -38839,7 +38996,8 @@ "version": "1.2.2", "resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.2.tgz", "integrity": "sha512-olV41bKSMm8BdnuMsewT4jqlZ8+3TCARAXjZGT9jcoSnrfUnRCqnMoF9XEeoWjbzObpqF9dRhHQj0Xb9QdF6/w==", - "dev": true + "dev": true, + "requires": {} }, "jest-regex-util": { "version": "27.0.1", @@ -39682,7 +39840,6 @@ "version": "2.2.0", "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.0.tgz", "integrity": "sha512-f+8cldu7X/y7RAJurMEJmdoKXGB/X550w2Nr3tTbezL6RwEE/iMcm+tZnXeoZtKuOq6ft8+CqzEkrIgx1fPoQA==", - "dev": true, "requires": { "minimist": "^1.2.5" } @@ -40568,7 +40725,8 @@ "ws": { "version": "7.4.4", "resolved": "https://registry.npmjs.org/ws/-/ws-7.4.4.tgz", - "integrity": "sha512-Qm8k8ojNQIMx7S+Zp8u/uHOx7Qazv3Yv4q68MiWWWOJhiwG5W3x7iqmRtJo8xxrciZUY4vRxUTJCKuRnF28ZZw==" + "integrity": "sha512-Qm8k8ojNQIMx7S+Zp8u/uHOx7Qazv3Yv4q68MiWWWOJhiwG5W3x7iqmRtJo8xxrciZUY4vRxUTJCKuRnF28ZZw==", + "requires": {} } } }, @@ -40616,7 +40774,8 @@ "ws": { "version": "7.4.6", "resolved": "https://registry.npmjs.org/ws/-/ws-7.4.6.tgz", - "integrity": "sha512-YmhHDO4MzaDLB+M9ym/mDA5z0naX8j7SIlT8f8z+I0VtzsRbekxEutHSme7NPS2qE8StCYQNUnfWdXta/Yu85A==" + "integrity": "sha512-YmhHDO4MzaDLB+M9ym/mDA5z0naX8j7SIlT8f8z+I0VtzsRbekxEutHSme7NPS2qE8StCYQNUnfWdXta/Yu85A==", + "requires": {} } } }, @@ -40967,7 +41126,8 @@ "ws": { "version": "7.4.4", "resolved": "https://registry.npmjs.org/ws/-/ws-7.4.4.tgz", - "integrity": "sha512-Qm8k8ojNQIMx7S+Zp8u/uHOx7Qazv3Yv4q68MiWWWOJhiwG5W3x7iqmRtJo8xxrciZUY4vRxUTJCKuRnF28ZZw==" + "integrity": "sha512-Qm8k8ojNQIMx7S+Zp8u/uHOx7Qazv3Yv4q68MiWWWOJhiwG5W3x7iqmRtJo8xxrciZUY4vRxUTJCKuRnF28ZZw==", + "requires": {} } } }, @@ -41082,6 +41242,11 @@ "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", "integrity": "sha1-bC4XHbKiV82WgC/UOwGyDV9YcPY=" }, + "lodash.isequal": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz", + "integrity": "sha1-QVxEePK8wwEgwizhDtMib30+GOA=" + }, "lodash.isinteger": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", @@ -41428,6 +41593,11 @@ "integrity": "sha512-+WA2/1sPmDj1dlvvJmB5G6JKfY9dpn7EVBUL06+y6PoljPkh+6V1QihwxNkbcGxCRjt2b0F9K0taiCuo7MbdFQ==", "dev": true }, + "math-expression-evaluator": { + "version": "1.3.7", + "resolved": "https://registry.npmjs.org/math-expression-evaluator/-/math-expression-evaluator-1.3.7.tgz", + "integrity": "sha512-nrbaifCl42w37hYd6oRLvoymFK42tWB+WQTMFtksDGQMi5GvlJwnz/CsS30FFAISFLtX+A0csJ0xLiuuyyec7w==" + }, "md5.js": { "version": "1.3.5", "resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.5.tgz", @@ -41606,7 +41776,8 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/meros/-/meros-1.1.4.tgz", "integrity": "sha512-E9ZXfK9iQfG9s73ars9qvvvbSIkJZF5yOo9j4tcwM5tN8mUKfj/EKN5PzOr3ZH0y5wL7dLAHw3RVEfpQV9Q7VQ==", - "dev": true + "dev": true, + "requires": {} }, "methods": { "version": "1.1.2", @@ -43242,7 +43413,8 @@ "ws": { "version": "7.4.4", "resolved": "https://registry.npmjs.org/ws/-/ws-7.4.4.tgz", - "integrity": "sha512-Qm8k8ojNQIMx7S+Zp8u/uHOx7Qazv3Yv4q68MiWWWOJhiwG5W3x7iqmRtJo8xxrciZUY4vRxUTJCKuRnF28ZZw==" + "integrity": "sha512-Qm8k8ojNQIMx7S+Zp8u/uHOx7Qazv3Yv4q68MiWWWOJhiwG5W3x7iqmRtJo8xxrciZUY4vRxUTJCKuRnF28ZZw==", + "requires": {} } } }, @@ -43544,7 +43716,8 @@ "react-circular-progressbar": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/react-circular-progressbar/-/react-circular-progressbar-2.0.4.tgz", - "integrity": "sha512-OfX0ThSxRYEVGaQSt0DlXfyl5w4DbXHsXetyeivmoQrh9xA9bzVPHNf8aAhOIiwiaxX2WYWpLDB3gcpsDJ9oww==" + "integrity": "sha512-OfX0ThSxRYEVGaQSt0DlXfyl5w4DbXHsXetyeivmoQrh9xA9bzVPHNf8aAhOIiwiaxX2WYWpLDB3gcpsDJ9oww==", + "requires": {} }, "react-copy-to-clipboard": { "version": "5.0.3", @@ -43565,6 +43738,15 @@ "scheduler": "^0.20.2" } }, + "react-draggable": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/react-draggable/-/react-draggable-4.4.3.tgz", + "integrity": "sha512-jV4TE59MBuWm7gb6Ns3Q1mxX8Azffb7oTtDtBgFkxRvhDp38YAARmRplrj0+XGkhOJB5XziArX+4HUUABtyZ0w==", + "requires": { + "classnames": "^2.2.5", + "prop-types": "^15.6.0" + } + }, "react-fast-compare": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-2.0.4.tgz", @@ -43578,6 +43760,18 @@ "prop-types": "^15.7.2" } }, + "react-grid-layout": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/react-grid-layout/-/react-grid-layout-1.2.5.tgz", + "integrity": "sha512-P/NNWAExTX/zEq+RUh6hrIG67UBicDNCOOg9LZe8BAtSdYtCnCGgVmWBS+sIbM0C8RJIiyGsFHh5dIfCddhS/w==", + "requires": { + "classnames": "2.3.1", + "lodash.isequal": "^4.0.0", + "prop-types": "^15.0.0", + "react-draggable": "^4.0.0", + "react-resizable": "^3.0.1" + } + }, "react-input-autosize": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/react-input-autosize/-/react-input-autosize-3.0.0.tgz", @@ -43589,7 +43783,8 @@ "react-intersection-observer": { "version": "8.32.0", "resolved": "https://registry.npmjs.org/react-intersection-observer/-/react-intersection-observer-8.32.0.tgz", - "integrity": "sha512-RlC6FvS3MFShxTn4FHAy904bVjX5Nn4/eTjUkurW0fHK+M/fyQdXuyCy9+L7yjA+YMGogzzSJNc7M4UtfSKvtw==" + "integrity": "sha512-RlC6FvS3MFShxTn4FHAy904bVjX5Nn4/eTjUkurW0fHK+M/fyQdXuyCy9+L7yjA+YMGogzzSJNc7M4UtfSKvtw==", + "requires": {} }, "react-is": { "version": "16.13.1", @@ -43611,6 +43806,15 @@ "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.8.3.tgz", "integrity": "sha512-X8jZHc7nCMjaCqoU+V2I0cOhNW+QMBwSUkeXnTi8IPe6zaRWfn60ZzvFDZqWPfmSJfjub7dDW1SP0jaHWLu/hg==" }, + "react-resizable": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/react-resizable/-/react-resizable-3.0.4.tgz", + "integrity": "sha512-StnwmiESiamNzdRHbSSvA65b0ZQJ7eVQpPusrSmcpyGKzC0gojhtO62xxH6YOBmepk9dQTBi9yxidL3W4s3EBA==", + "requires": { + "prop-types": "15.x", + "react-draggable": "^4.0.3" + } + }, "react-select": { "version": "4.3.1", "resolved": "https://registry.npmjs.org/react-select/-/react-select-4.3.1.tgz", @@ -43628,7 +43832,8 @@ "react-slider": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/react-slider/-/react-slider-1.1.4.tgz", - "integrity": "sha512-lL/MvzFcDue0ztdJItwLqas2lOy8Gg46eCDGJc4cJGldThmBHcHfGQePgBgyY1SEN95OwsWAakd3SuI8RyixDQ==" + "integrity": "sha512-lL/MvzFcDue0ztdJItwLqas2lOy8Gg46eCDGJc4cJGldThmBHcHfGQePgBgyY1SEN95OwsWAakd3SuI8RyixDQ==", + "requires": {} }, "react-spinners": { "version": "0.11.0", @@ -43654,7 +43859,8 @@ "react-table": { "version": "7.7.0", "resolved": "https://registry.npmjs.org/react-table/-/react-table-7.7.0.tgz", - "integrity": "sha512-jBlj70iBwOTvvImsU9t01LjFjy4sXEtclBovl3mTiqjz23Reu0DKnRza4zlLtOPACx6j2/7MrQIthIK1Wi+LIA==" + "integrity": "sha512-jBlj70iBwOTvvImsU9t01LjFjy4sXEtclBovl3mTiqjz23Reu0DKnRza4zlLtOPACx6j2/7MrQIthIK1Wi+LIA==", + "requires": {} }, "react-toastify": { "version": "7.0.4", @@ -43851,6 +44057,31 @@ } } }, + "reduce-css-calc": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/reduce-css-calc/-/reduce-css-calc-1.3.0.tgz", + "integrity": "sha1-dHyRTgSWFKTJz7umKYca0dKSdxY=", + "requires": { + "balanced-match": "^0.4.2", + "math-expression-evaluator": "^1.2.14", + "reduce-function-call": "^1.0.1" + }, + "dependencies": { + "balanced-match": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-0.4.2.tgz", + "integrity": "sha1-yz8+PHMtwPAe5wtAPzAuYddwmDg=" + } + } + }, + "reduce-function-call": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/reduce-function-call/-/reduce-function-call-1.0.3.tgz", + "integrity": "sha512-Hl/tuV2VDgWgCSEeWMLwxLZqX7OK59eU1guxXsRKTAyeYimivsKdtcV4fu3r710tpG5GmDKDhQ0HSZLExnNmyQ==", + "requires": { + "balanced-match": "^1.0.0" + } + }, "regenerate": { "version": "1.4.2", "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz", @@ -45297,7 +45528,8 @@ "stylis-rule-sheet": { "version": "0.0.10", "resolved": "https://registry.npmjs.org/stylis-rule-sheet/-/stylis-rule-sheet-0.0.10.tgz", - "integrity": "sha512-nTbZoaqoBnmK+ptANthb10ZRZOGC+EmTLLUxeYIuHNkEKcmKgXX1XWKkUBT2Ac4es3NybooPe0SmvKdhKJZAuw==" + "integrity": "sha512-nTbZoaqoBnmK+ptANthb10ZRZOGC+EmTLLUxeYIuHNkEKcmKgXX1XWKkUBT2Ac4es3NybooPe0SmvKdhKJZAuw==", + "requires": {} }, "subscriptions-transport-ws": { "version": "0.9.18", @@ -47349,7 +47581,8 @@ "ws": { "version": "7.4.5", "resolved": "https://registry.npmjs.org/ws/-/ws-7.4.5.tgz", - "integrity": "sha512-xzyu3hFvomRfXKH8vOFMU3OguG6oOvhXMo3xsGy3xWExqaM2dxBbVxuD99O7m3ZUFMvvscsZDqxfgMaRr/Nr1g==" + "integrity": "sha512-xzyu3hFvomRfXKH8vOFMU3OguG6oOvhXMo3xsGy3xWExqaM2dxBbVxuD99O7m3ZUFMvvscsZDqxfgMaRr/Nr1g==", + "requires": {} }, "xdg-basedir": { "version": "4.0.0", diff --git a/package.json b/package.json index 952fcc4e..03458694 100644 --- a/package.json +++ b/package.json @@ -37,6 +37,7 @@ "@apollo/client": "^3.3.19", "@emotion/babel-plugin": "^11.3.0", "@next/bundle-analyzer": "^10.2.3", + "@visx/axis": "^1.12.0", "@visx/chord": "^1.7.0", "@visx/curve": "^1.7.0", "@visx/event": "^1.7.0", @@ -56,6 +57,7 @@ "cookie": "^0.4.1", "crypto-js": "^4.0.0", "d3-array": "^2.12.1", + "d3-time-format": "^3.0.0", "date-fns": "^2.22.1", "graphql": "^15.5.0", "graphql-iso-date": "^3.6.1", @@ -79,6 +81,7 @@ "react-copy-to-clipboard": "^5.0.3", "react-dom": "^17.0.2", "react-feather": "^2.0.9", + "react-grid-layout": "^1.2.5", "react-intersection-observer": "^8.32.0", "react-qr-reader": "^2.2.1", "react-select": "^4.3.1", @@ -118,6 +121,7 @@ "@types/cookie": "^0.4.0", "@types/crypto-js": "^4.0.1", "@types/d3-array": "^2.12.1", + "@types/d3-time-format": "^3.0.0", "@types/graphql-iso-date": "^3.4.0", "@types/js-cookie": "^2.2.6", "@types/js-yaml": "^4.0.1", @@ -132,6 +136,7 @@ "@types/qrcode.react": "^1.0.1", "@types/react": "^17.0.8", "@types/react-copy-to-clipboard": "^5.0.0", + "@types/react-grid-layout": "^1.1.1", "@types/react-qr-reader": "^2.1.3", "@types/react-select": "^4.0.15", "@types/react-slider": "^1.1.2", diff --git a/pages/_app.tsx b/pages/_app.tsx index 1e6d8b69..0249ae55 100644 --- a/pages/_app.tsx +++ b/pages/_app.tsx @@ -12,8 +12,11 @@ import { useConfigState, ConfigProvider } from '../src/context/ConfigContext'; import { GlobalStyles } from '../src/styles/GlobalStyle'; import { Header } from '../src/layouts/header/Header'; import { Footer } from '../src/layouts/footer/Footer'; -import 'react-toastify/dist/ReactToastify.min.css'; import { PageWrapper, HeaderBodyWrapper } from '../src/layouts/Layout.styled'; + +import 'react-toastify/dist/ReactToastify.min.css'; +import 'react-grid-layout/css/styles.css'; +import 'react-resizable/css/styles.css'; import 'react-circular-progressbar/dist/styles.css'; const Wrapper: React.FC = ({ children }) => { diff --git a/pages/channels.tsx b/pages/channels.tsx index 686bc16d..05aba5fd 100644 --- a/pages/channels.tsx +++ b/pages/channels.tsx @@ -14,6 +14,7 @@ import { CardWithTitle, SubTitle, SmallButton, + Card, } from '../src/components/generic/Styled'; import { mediaWidths } from '../src/styles/Themes'; @@ -66,7 +67,11 @@ const ChannelView = () => { case 3: return ; default: - return ; + return ( + + + + ); } }; diff --git a/pages/dashboard.tsx b/pages/dashboard.tsx new file mode 100644 index 00000000..12062370 --- /dev/null +++ b/pages/dashboard.tsx @@ -0,0 +1,35 @@ +import { NextPageContext } from 'next'; +import { getProps } from 'src/utils/ssr'; +import dynamic from 'next/dynamic'; +import { LoadingCard } from 'src/components/loading/LoadingCard'; +import { SimpleWrapper } from 'src/components/gridWrapper/GridWrapper'; +import styled from 'styled-components'; + +const S = { + wrapper: styled.div` + position: relative; + `, +}; + +const LoadingComp = () => ; + +const Dashboard = dynamic(() => import('src/views/dashboard'), { + ssr: false, + loading: LoadingComp, +}); + +const Wrapped = () => { + return ( + + + + + + ); +}; + +export default Wrapped; + +export async function getServerSideProps(context: NextPageContext) { + return await getProps(context); +} diff --git a/pages/leaderboard.tsx b/pages/leaderboard.tsx index 19b16146..66435ed1 100644 --- a/pages/leaderboard.tsx +++ b/pages/leaderboard.tsx @@ -38,7 +38,9 @@ const LeaderboardView = () => { return ( <> - + + + {renderBoard()} ); diff --git a/pages/settings/dashboard.tsx b/pages/settings/dashboard.tsx new file mode 100644 index 00000000..1dfbcea2 --- /dev/null +++ b/pages/settings/dashboard.tsx @@ -0,0 +1,25 @@ +import React from 'react'; +import { GridWrapper } from 'src/components/gridWrapper/GridWrapper'; +import { NextPageContext } from 'next'; +import { getProps } from 'src/utils/ssr'; +import dynamic from 'next/dynamic'; +import { LoadingCard } from 'src/components/loading/LoadingCard'; + +const LoadingComp = () => ; + +const Dashboard = dynamic(() => import('src/views/settings/DashPanel'), { + ssr: false, + loading: LoadingComp, +}); + +const Wrapped = () => ( + + + +); + +export default Wrapped; + +export async function getServerSideProps(context: NextPageContext) { + return await getProps(context); +} diff --git a/pages/settings.tsx b/pages/settings/index.tsx similarity index 64% rename from pages/settings.tsx rename to pages/settings/index.tsx index 00829753..854c0432 100644 --- a/pages/settings.tsx +++ b/pages/settings/index.tsx @@ -3,11 +3,12 @@ import styled from 'styled-components'; import { GridWrapper } from 'src/components/gridWrapper/GridWrapper'; import { NextPageContext } from 'next'; import { getProps } from 'src/utils/ssr'; -import { SingleLine } from '../src/components/generic/Styled'; -import { InterfaceSettings } from '../src/views/settings/Interface'; -import { DangerView } from '../src/views/settings/Danger'; -import { ChatSettings } from '../src/views/settings/Chat'; -import { PrivacySettings } from '../src/views/settings/Privacy'; +import { SingleLine } from '../../src/components/generic/Styled'; +import { InterfaceSettings } from '../../src/views/settings/Interface'; +import { DangerView } from '../../src/views/settings/Danger'; +import { ChatSettings } from '../../src/views/settings/Chat'; +import { PrivacySettings } from '../../src/views/settings/Privacy'; +import { DashboardSettings } from 'src/views/settings/Dashboard'; export const ButtonRow = styled.div` width: auto; @@ -22,6 +23,7 @@ const SettingsView = () => { return ( <> + diff --git a/pages/transactions.tsx b/pages/transactions.tsx index 3cdf612e..0f7f11cb 100644 --- a/pages/transactions.tsx +++ b/pages/transactions.tsx @@ -61,20 +61,15 @@ const TransactionsView = () => { const [open, setOpen] = useState(false); const [offset, setOffset] = useState(0); + const [limit, setLimit] = useState(7); const { publicKey } = useNodeInfo(); const [settings] = useLocalStorage('transactionSettings', defaultSettings); - const { - data, - fetchMore, - startPolling, - stopPolling, - networkStatus, - } = useGetResumeQuery({ + const { data, startPolling, stopPolling, networkStatus } = useGetResumeQuery({ ssr: false, - variables: { offset: 0 }, + variables: { offset: 0, limit }, notifyOnNetworkStatusChange: true, onError: error => toast.error(getErrorContent(error)), }); @@ -85,9 +80,8 @@ const TransactionsView = () => { const loadingOrRefetching = isLoading || isRefetching; useEffect(() => { - if (!isLoading && data?.getResume?.offset) { - setOffset(data.getResume.offset); - } + if (isLoading || !data?.getResume?.offset) return; + setOffset(data.getResume.offset); }, [data, isLoading]); useEffect(() => { @@ -95,7 +89,12 @@ const TransactionsView = () => { }, [stopPolling]); if (isLoading || !data || !data.getResume) { - return ; + return ( + <> + + + + ); } const beforeDate = subDays(new Date(), offset); @@ -135,8 +134,7 @@ const TransactionsView = () => { return [...p, c]; }, [] as ResumeTransactions); - const handleClick = (limit: number) => - fetchMore({ variables: { offset, limit } }); + const handleClick = (limit: number) => setLimit(offset + limit); return ( <> diff --git a/src/components/chart/BarChart.tsx b/src/components/chart/BarChart.tsx new file mode 100644 index 00000000..52121bbc --- /dev/null +++ b/src/components/chart/BarChart.tsx @@ -0,0 +1,232 @@ +import { Group } from '@visx/group'; +import { BarGroup } from '@visx/shape'; +import { AxisBottom, AxisLeft } from '@visx/axis'; +import { scaleBand, scaleLinear, scaleOrdinal } from '@visx/scale'; +import { timeParse, timeFormat } from 'd3-time-format'; +import { ParentSize } from '@visx/responsive'; +import { chartColors } from 'src/styles/Themes'; +import { ThemeContext } from 'styled-components'; +import { useContext } from 'react'; +import { TooltipWithBounds, defaultStyles, useTooltip } from '@visx/tooltip'; +import { Price } from '../price/Price'; +import { localPoint } from '@visx/event'; + +type BarGroupProps = { + width: number; + height: number; +} & BarChartProps; + +type BarChartProps = { + data: any[]; + margin?: { top: number; right: number; bottom: number; left: number }; + events?: boolean; + colorRange?: string[]; + priceLabel?: boolean; +}; + +const defaultMargin = { top: 40, right: 0, bottom: 40, left: 0 }; +const defaultColorRange = [ + chartColors.green, + chartColors.orange, + chartColors.lightblue, +]; + +const amountOfYTicks = (height: number) => { + switch (true) { + case height < 300: + return 3; + case height < 400: + return 6; + default: + return 10; + } +}; + +const amountOfXTicks = (width: number) => { + switch (true) { + case width < 300: + return 2; + case width < 400: + return 6; + default: + return 10; + } +}; + +const parseDate = timeParse('%Y-%m-%d'); +const format = timeFormat('%b %d'); + +const formatDate = (date: string) => format(parseDate(date) as Date); + +const tooltipStyles = { + ...defaultStyles, + minWidth: 60, + backgroundColor: 'rgba(0,0,0,0.9)', + color: 'white', +}; + +const Chart = ({ + width, + height, + margin = defaultMargin, + data = [], + colorRange = defaultColorRange, + priceLabel, +}: BarGroupProps) => { + const { + tooltipData, + tooltipLeft, + tooltipTop, + tooltipOpen, + showTooltip, + hideTooltip, + } = useTooltip(); + + const themeContext = useContext(ThemeContext); + + const axisColor = themeContext.mode === 'light' ? 'black' : 'white'; + + const keys = Object.keys(data[0]).filter(d => d !== 'date'); + + let tooltipTimeout: number; + + const getDate = (d: any) => d.date; + + const xScale = scaleBand({ + domain: data.map(getDate), + padding: 0.2, + }); + + const barScale = scaleBand({ + domain: keys, + padding: 0.1, + }); + + const yScale = scaleLinear({ + domain: [ + 0, + Math.max(...data.map(d => Math.max(...keys.map(key => Number(d[key]))))), + ], + }); + + const colorScale = scaleOrdinal({ + domain: keys, + range: colorRange, + }); + + // bounds + const xMax = width - margin.left - margin.right; + const yMax = height - margin.top - margin.bottom; + + // update scale output dimensions + xScale.rangeRound([0, xMax]); + yScale.rangeRound([yMax, 0]); + barScale.rangeRound([0, xScale.bandwidth()]); + + return ( +
+ + + + {barGroups => + barGroups.map(barGroup => ( + + {barGroup.bars.map(bar => ( + { + if (tooltipTimeout) clearTimeout(tooltipTimeout); + + const coords = localPoint(e.target.ownerSVGElement, e); + if (!coords) return; + + showTooltip({ + tooltipLeft: coords.x, + tooltipTop: coords.y, + tooltipData: bar, + }); + }} + onMouseLeave={() => { + tooltipTimeout = window.setTimeout(() => { + hideTooltip(); + }, 300); + }} + /> + ))} + + )) + } + + + ({ + fill: axisColor, + fontSize: 11, + textAnchor: 'end', + dy: '0.33em', + dx: '-0.33em', + })} + /> + ({ + fill: axisColor, + fontSize: 11, + textAnchor: 'middle', + })} + /> + + {tooltipOpen && tooltipData ? ( + +
+ {tooltipData.key} +
+ {priceLabel ? ( + + ) : ( + tooltipData.value + )} +
+ ) : null} +
+ ); +}; + +export const BarChart = (props: BarChartProps) => ( + + {parent => } + +); diff --git a/src/components/chart/HorizontalBarChart.tsx b/src/components/chart/HorizontalBarChart.tsx new file mode 100644 index 00000000..7cd824f6 --- /dev/null +++ b/src/components/chart/HorizontalBarChart.tsx @@ -0,0 +1,189 @@ +import { Group } from '@visx/group'; +import { BarGroupHorizontal, Bar } from '@visx/shape'; +import { AxisLeft } from '@visx/axis'; +import { scaleBand, scaleLinear, scaleOrdinal } from '@visx/scale'; +import { ParentSize } from '@visx/responsive'; +import { chartColors } from 'src/styles/Themes'; +import { ThemeContext } from 'styled-components'; +import { useContext } from 'react'; +import { TooltipWithBounds, defaultStyles, useTooltip } from '@visx/tooltip'; +import { Price } from '../price/Price'; +import { localPoint } from '@visx/event'; + +type BarGroupProps = { + width: number; + height: number; +} & BarChartProps; + +type BarChartProps = { + data: any[]; + margin?: { top: number; right: number; bottom: number; left: number }; + events?: boolean; + colorRange?: string[]; + priceLabel?: boolean; +}; + +const defaultMargin = { top: 40, right: 0, bottom: 40, left: 0 }; +const defaultColorRange = [ + chartColors.green, + chartColors.orange, + chartColors.lightblue, +]; + +const tooltipStyles = { + ...defaultStyles, + minWidth: 60, + backgroundColor: 'rgba(0,0,0,0.9)', + color: 'white', +}; + +const Chart = ({ + width, + height, + margin = defaultMargin, + data = [], + colorRange = defaultColorRange, + priceLabel, +}: BarGroupProps) => { + const { + tooltipData, + tooltipLeft, + tooltipTop, + tooltipOpen, + showTooltip, + hideTooltip, + } = useTooltip(); + + const themeContext = useContext(ThemeContext); + const axisColor = themeContext.mode === 'light' ? 'black' : 'white'; + + const keys = Object.keys(data[0]).filter(d => d !== 'label'); + + const maxValue = Math.max( + ...data.map(d => Math.max(...keys.map(key => Number(d[key])))) + ); + + let tooltipTimeout: number; + + const getLabel = (d: any) => d.label; + + const yScale = scaleBand({ + domain: data.map(getLabel), + }); + + const barScale = scaleBand({ + domain: keys, + padding: 0.1, + }); + + const xScale = scaleLinear({ + domain: [0, maxValue + 0.1 * maxValue], + }); + + const colorScale = scaleOrdinal({ + domain: keys, + range: colorRange, + }); + + // bounds + const xMax = width - margin.left - margin.right; + const yMax = height - margin.top - margin.bottom; + + // update scale output dimensions + xScale.rangeRound([0, xMax]); + yScale.rangeRound([yMax, 0]); + barScale.rangeRound([0, yScale.bandwidth()]); + + return ( +
+ + + + {barGroups => + barGroups.map(barGroup => ( + + {barGroup.bars.map(bar => ( + { + if (tooltipTimeout) clearTimeout(tooltipTimeout); + + const coords = localPoint(e.target.ownerSVGElement, e); + if (!coords) return; + + showTooltip({ + tooltipLeft: coords.x, + tooltipTop: coords.y, + tooltipData: bar, + }); + }} + onMouseLeave={() => { + tooltipTimeout = window.setTimeout(() => { + hideTooltip(); + }, 300); + }} + /> + ))} + + )) + } + + + ({ + fill: axisColor, + fontSize: 11, + textAnchor: 'end', + dy: '0.33em', + dx: '-0.33em', + })} + /> + + {tooltipOpen && tooltipData ? ( + +
+ {tooltipData.key} +
+ {priceLabel ? ( + + ) : ( + tooltipData.value + )} +
+ ) : null} +
+ ); +}; + +export const HorizontalBarChart = (props: BarChartProps) => ( + + {parent => } + +); diff --git a/src/components/gridWrapper/GridWrapper.tsx b/src/components/gridWrapper/GridWrapper.tsx index 9407f24b..125ed86f 100644 --- a/src/components/gridWrapper/GridWrapper.tsx +++ b/src/components/gridWrapper/GridWrapper.tsx @@ -46,3 +46,12 @@ export const GridWrapper: React.FC = ({ ); + +export const SimpleWrapper: React.FC = ({ children }) => ( +
+ + + + {children} +
+); diff --git a/src/components/section/Section.tsx b/src/components/section/Section.tsx index e1afa167..b9467473 100644 --- a/src/components/section/Section.tsx +++ b/src/components/section/Section.tsx @@ -32,7 +32,7 @@ const FullWidth = styled.div` const FixedWidth = styled.div` max-width: 1000px; - margin: 0 auto 0 auto; + margin: 0 auto 0; @media (max-width: 1035px) { padding: 0 16px; diff --git a/src/components/select/index.tsx b/src/components/select/index.tsx index a907acd7..95ed2a64 100644 --- a/src/components/select/index.tsx +++ b/src/components/select/index.tsx @@ -52,9 +52,43 @@ const StyledSelect = styled(ReactSelect)` } `; +const StyledSmallSelect = styled(ReactSelect)` + & .Select__control { + cursor: pointer; + background-color: transparent; + border: none; + font-size: 12px; + + & .Select__control--is-focused { + border: 1px solid ${themeColors.blue2}; + } + + & .Select__single-value { + color: ${textColor}; + } + + & .Select__dropdown-indicator { + padding: 0 0 0 4px; + } + } + + & .Select__menu { + font-size: 14px; + color: black; + + & .Select__option { + cursor: pointer; + } + + & .Select__option--is-selected { + background-color: ${themeColors.blue2}; + } + } +`; + export type ValueProp = { - value: string; - label: string; + value: string | number; + label: string | number; }; type SelectProps = { @@ -94,6 +128,7 @@ type SelectWithValueProps = { value: ValueProp | undefined; isMulti?: boolean; maxWidth?: string; + isClearable?: boolean; callback: (value: ValueProp[]) => void; }; @@ -103,6 +138,7 @@ export const SelectWithValue = ({ maxWidth, callback, value, + isClearable = true, }: SelectWithValueProps) => { const handleChange = (value: ValueProp | ValueProp[]) => { if (Array.isArray(value)) { @@ -119,7 +155,36 @@ export const SelectWithValue = ({ options={options} onChange={handleChange} value={value || null} - isClearable={true} + isClearable={isClearable} + /> + + ); +}; + +export const SmallSelectWithValue = ({ + isMulti, + options, + maxWidth, + callback, + value, + isClearable = true, +}: SelectWithValueProps) => { + const handleChange = (value: ValueProp | ValueProp[]) => { + if (Array.isArray(value)) { + callback(value); + } else { + callback([value]); + } + }; + return ( + + ); diff --git a/src/context/ContextProvider.tsx b/src/context/ContextProvider.tsx index 390f8923..44c31068 100644 --- a/src/context/ContextProvider.tsx +++ b/src/context/ContextProvider.tsx @@ -2,11 +2,14 @@ import React from 'react'; import { PriceProvider } from './PriceContext'; import { ChatProvider } from './ChatContext'; import { RebalanceProvider } from './RebalanceContext'; +import { DashProvider } from './DashContext'; export const ContextProvider: React.FC = ({ children }) => ( - - - {children} - - + + + + {children} + + + ); diff --git a/src/context/DashContext.tsx b/src/context/DashContext.tsx new file mode 100644 index 00000000..348fad4f --- /dev/null +++ b/src/context/DashContext.tsx @@ -0,0 +1,54 @@ +import React, { createContext, useContext, useReducer } from 'react'; + +type State = { + modalType: string; +}; + +type ActionType = { + type: 'openModal'; + modalType: string; +}; + +type Dispatch = (action: ActionType) => void; + +export const StateContext = createContext(undefined); +export const DispatchContext = createContext(undefined); + +const stateReducer = (state: State, action: ActionType): State => { + switch (action.type) { + case 'openModal': + return { ...state, modalType: action.modalType }; + default: + return state; + } +}; + +const DashProvider: React.FC = ({ children }) => { + const [state, dispatch] = useReducer(stateReducer, { + modalType: '', + }); + + return ( + + {children} + + ); +}; + +const useDashState = () => { + const context = useContext(StateContext); + if (context === undefined) { + throw new Error('useDashState must be used within a DashProvider'); + } + return context; +}; + +const useDashDispatch = () => { + const context = useContext(DispatchContext); + if (context === undefined) { + throw new Error('useDashDispatch must be used within a DashProvider'); + } + return context; +}; + +export { DashProvider, useDashState, useDashDispatch }; diff --git a/src/hooks/UseBaseConnect.tsx b/src/hooks/UseBaseConnect.tsx index 30a00635..b8c5bc8e 100644 --- a/src/hooks/UseBaseConnect.tsx +++ b/src/hooks/UseBaseConnect.tsx @@ -2,7 +2,7 @@ import { useState, useEffect } from 'react'; import { useGetBaseCanConnectQuery } from 'src/graphql/queries/__generated__/getBaseCanConnect.generated'; export const useBaseConnect = () => { - const [canConnect, setCanConnect] = useState(false); + const [connected, setCanConnect] = useState(false); const { loading, error, data } = useGetBaseCanConnectQuery({ ssr: false, @@ -14,5 +14,5 @@ export const useBaseConnect = () => { setCanConnect(true); }, [loading, data, error]); - return canConnect; + return { connected, loading }; }; diff --git a/src/hooks/UseElementSize.tsx b/src/hooks/UseElementSize.tsx new file mode 100644 index 00000000..602bd00d --- /dev/null +++ b/src/hooks/UseElementSize.tsx @@ -0,0 +1,40 @@ +import { RefObject, useState, useEffect, useCallback } from 'react'; + +import useEventListener from './UseEventListener'; + +interface Size { + width: number; + height: number; +} + +function useElementSize( + elementRef: RefObject +): Size { + const [size, setSize] = useState({ + width: 0, + height: 0, + }); + + // Prevent too many rendering using useCallback + const updateSize = useCallback(() => { + const node = elementRef?.current; + if (node) { + setSize({ + width: node.offsetWidth || 0, + height: node.offsetHeight || 0, + }); + } + }, [elementRef]); + + // Initial size on mount + useEffect(() => { + updateSize(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + useEventListener('resize', updateSize); + + return size; +} + +export default useElementSize; diff --git a/src/hooks/UseEventListener.tsx b/src/hooks/UseEventListener.tsx new file mode 100644 index 00000000..1ba42acb --- /dev/null +++ b/src/hooks/UseEventListener.tsx @@ -0,0 +1,39 @@ +import { useRef, useEffect, RefObject } from 'react'; + +function useEventListener( + eventName: string, + handler: (event: Event) => void, + element?: RefObject +) { + // Create a ref that stores handler + const savedHandler = useRef<(event: Event) => void>(); + + useEffect(() => { + // Define the listening target + const targetElement: T | Window = element?.current || window; + if (!(targetElement && targetElement.addEventListener)) { + return; + } + + // Update saved handler if necessary + if (savedHandler.current !== handler) { + savedHandler.current = handler; + } + + // Create event listener that calls handler function stored in ref + const eventListener = (event: Event) => { + // eslint-disable-next-line no-extra-boolean-cast + if (!!savedHandler?.current) { + savedHandler.current(event); + } + }; + + targetElement.addEventListener(eventName, eventListener); + + return () => { + targetElement.removeEventListener(eventName, eventListener); + }; + }, [eventName, element, handler]); +} + +export default useEventListener; diff --git a/src/layouts/header/Header.tsx b/src/layouts/header/Header.tsx index 6f66eed7..472cbe4d 100644 --- a/src/layouts/header/Header.tsx +++ b/src/layouts/header/Header.tsx @@ -43,7 +43,7 @@ export const Header = () => { const [open, setOpen] = useState(false); const { lnMarketsAuth } = useConfigState(); - const connected = useBaseConnect(); + const { connected } = useBaseConnect(); const isRoot = pathname === MAIN || pathname === SSO; diff --git a/src/layouts/navigation/Navigation.tsx b/src/layouts/navigation/Navigation.tsx index a3a108c7..c0400a62 100644 --- a/src/layouts/navigation/Navigation.tsx +++ b/src/layouts/navigation/Navigation.tsx @@ -17,6 +17,7 @@ import { Heart, Shuffle, Aperture, + Grid, } from 'react-feather'; import { useRouter } from 'next/router'; import { useBaseConnect } from 'src/hooks/UseBaseConnect'; @@ -116,6 +117,7 @@ const BurgerNav = styled.a` `; const HOME = '/'; +const DASHBOARD = '/dashboard'; const PEERS = '/peers'; const CHANNEL = '/channels'; const REBALANCE = '/rebalance'; @@ -140,7 +142,7 @@ export const Navigation = ({ isBurger, setOpen }: NavigationProps) => { const { pathname } = useRouter(); const { sidebar } = useConfigState(); - const connected = useBaseConnect(); + const { connected } = useBaseConnect(); const isRoot = pathname === '/login' || pathname === '/sso'; @@ -173,6 +175,7 @@ export const Navigation = ({ isBurger, setOpen }: NavigationProps) => { const renderLinks = () => ( {renderNavButton('Home', HOME, Home, sidebar)} + {renderNavButton('Dashboard', DASHBOARD, Grid, sidebar)} {renderNavButton('Peers', PEERS, Users, sidebar)} {renderNavButton('Channels', CHANNEL, Cpu, sidebar)} {renderNavButton('Rebalance', REBALANCE, Repeat, sidebar)} @@ -183,13 +186,14 @@ export const Navigation = ({ isBurger, setOpen }: NavigationProps) => { {renderNavButton('Tools', TOOLS, Shield, sidebar)} {renderNavButton('Swap', SWAP, Shuffle, sidebar)} {renderNavButton('Stats', STATS, BarChart2, sidebar)} - {connected && renderNavButton('Scores', SCORES, Aperture)} + {connected && renderNavButton('Scores', SCORES, Aperture, sidebar)} ); const renderBurger = () => ( {renderBurgerNav('Home', HOME, Home)} + {renderBurgerNav('Dashboard', DASHBOARD, Grid)} {renderBurgerNav('Peers', PEERS, Users)} {renderBurgerNav('Channels', CHANNEL, Cpu)} {renderBurgerNav('Rebalance', REBALANCE, Repeat)} diff --git a/src/utils/gridConstants.ts b/src/utils/gridConstants.ts new file mode 100644 index 00000000..4e9e0f0c --- /dev/null +++ b/src/utils/gridConstants.ts @@ -0,0 +1,11 @@ +export const defaultGrid = { + breakpoints: { + lg: 1200, + md: 996, + sm: 768, + xs: 480, + xxs: 0, + }, + columns: { lg: 24, md: 16, sm: 12, xs: 4, xxs: 2 }, + margin: [4, 4] as [number, number], +}; diff --git a/src/views/channels/channels/ChannelBosScore.tsx b/src/views/channels/channels/ChannelBosScore.tsx index 536c2028..33c2e072 100644 --- a/src/views/channels/channels/ChannelBosScore.tsx +++ b/src/views/channels/channels/ChannelBosScore.tsx @@ -29,7 +29,7 @@ const S = { }; export const ChannelBosScore: FC<{ score?: BosScore | null }> = ({ score }) => { - const connected = useBaseConnect(); + const { connected } = useBaseConnect(); if (!connected) return null; diff --git a/src/views/channels/channels/Channels.tsx b/src/views/channels/channels/Channels.tsx index ae08f474..6f2ab077 100644 --- a/src/views/channels/channels/Channels.tsx +++ b/src/views/channels/channels/Channels.tsx @@ -7,7 +7,6 @@ import { getPercent } from 'src/utils/helpers'; import { ChannelType } from 'src/graphql/types'; import { useRebalanceState } from 'src/context/RebalanceContext'; import { useRouter } from 'next/router'; -import { Card } from '../../../components/generic/Styled'; import { getErrorContent } from '../../../utils/error'; import { LoadingCard } from '../../../components/loading/LoadingCard'; import { ChannelCard } from './ChannelCard'; @@ -167,7 +166,7 @@ export const Channels: React.FC = () => { }; return ( - + <> {getChannels().map((channel, index) => ( { biggestRateFee={Math.max(Math.min(biggestRateFee, 10000), 2000)} /> ))} - + ); }; diff --git a/src/views/dashboard/index.tsx b/src/views/dashboard/index.tsx new file mode 100644 index 00000000..589ade6e --- /dev/null +++ b/src/views/dashboard/index.tsx @@ -0,0 +1,126 @@ +import { Layouts, Responsive as ResponsiveGridLayout } from 'react-grid-layout'; +import styled, { css } from 'styled-components'; +import { defaultGrid } from 'src/utils/gridConstants'; +import { useLocalStorage } from 'src/hooks/UseLocalStorage'; +import { LoadingCard } from 'src/components/loading/LoadingCard'; +import { useRef } from 'react'; +import useElementSize from 'src/hooks/UseElementSize'; +import { getWidgets } from './widgets/helpers'; +import { Card, SubTitle } from 'src/components/generic/Styled'; +import { textColor } from 'src/styles/Themes'; +import { Link } from 'src/components/link/Link'; +import { ColorButton } from 'src/components/buttons/colorButton/ColorButton'; +import { useDashDispatch, useDashState } from 'src/context/DashContext'; +import Modal from 'src/components/modal/ReactModal'; +import { DashboardModal } from './modal'; + +const S = { + styles: styled.div` + .react-resizable-handle::after { + border-bottom: 2px solid ${textColor}; + border-right: 2px solid ${textColor}; + } + `, + card: styled(Card)<{ widgetColor?: string }>` + display: flex; + justify-content: center; + align-items: center; + + border-radius: 4px; + padding: 8px; + ${({ widgetColor }) => + css` + border-top: 2px solid #${widgetColor}; + `} + `, + gridWrapper: styled.div` + width: 100%; + `, + fill: styled.div` + height: 80vh; + width: 100%; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + `, +}; + +export type StoredWidget = { + id: number; +}; + +const Dashboard = () => { + const wrapperRef = useRef(null); + + const { width } = useElementSize(wrapperRef); + + const { modalType } = useDashState(); + const dispatch = useDashDispatch(); + + const [layouts, setLayouts] = useLocalStorage('layouts', {}); + const [availableWidgets] = useLocalStorage( + 'dashboardWidgets', + [] + ); + + const props = { + isBounded: true, + }; + + const handleChange = (_: any, layouts: any) => { + setLayouts(layouts); + }; + + const widgets = getWidgets(availableWidgets, width, [{ id: 28 }]); + + if (!widgets.length) { + return ( + + No Widgets Enabled! + + Settings + + + ); + } + + const renderContent = () => { + if (width === 0) { + return ; + } + return ( + <> + + + {widgets.map(w => ( + + + + ))} + + + dispatch({ type: 'openModal', modalType: '' })} + > + + + + ); + }; + + return {renderContent()}; +}; + +export default Dashboard; diff --git a/src/views/dashboard/modal/index.tsx b/src/views/dashboard/modal/index.tsx new file mode 100644 index 00000000..edde00d2 --- /dev/null +++ b/src/views/dashboard/modal/index.tsx @@ -0,0 +1,48 @@ +import { useDashDispatch, useDashState } from 'src/context/DashContext'; +import { CreateInvoiceCard } from 'src/views/home/account/createInvoice/CreateInvoice'; +import { PayCard } from 'src/views/home/account/pay/Payment'; +import { ReceiveOnChainCard } from 'src/views/home/account/receiveOnChain/ReceiveOnChain'; +import { SendOnChainCard } from 'src/views/home/account/sendOnChain/SendOnChain'; +import { SupportBar } from 'src/views/home/quickActions/donate/DonateContent'; +import { OpenChannel } from 'src/views/home/quickActions/openChannel'; +import { SignMessage } from 'src/views/tools/messages/SignMessage'; + +export const DashboardModal = () => { + const { modalType } = useDashState(); + const dispatch = useDashDispatch(); + + const renderModal = () => { + switch (modalType) { + case 'payInvoice': + return ( + dispatch({ type: 'openModal', modalType: '' })} + /> + ); + case 'createInvoice': + return ; + case 'sendChain': + return ( + dispatch({ type: 'openModal', modalType: '' })} + /> + ); + case 'receiveChain': + return ; + case 'openChannel': + return ( + dispatch({ type: 'openModal', modalType: '' })} + /> + ); + case 'donate': + return ; + case 'signMessage': + return ; + default: + return null; + } + }; + + return renderModal(); +}; diff --git a/src/views/dashboard/widgets/external/mempool.tsx b/src/views/dashboard/widgets/external/mempool.tsx new file mode 100644 index 00000000..8b22424b --- /dev/null +++ b/src/views/dashboard/widgets/external/mempool.tsx @@ -0,0 +1,40 @@ +import { Table } from 'src/components/table'; +import { useBitcoinFees } from 'src/hooks/UseBitcoinFees'; +import styled from 'styled-components'; + +const S = { + wrapper: styled.div` + width: 100%; + overflow: auto; + `, +}; + +export const MempoolWidget = () => { + const { fast, halfHour, hour, minimum, dontShow } = useBitcoinFees(); + + if (dontShow) { + return null; + } + + const columns = [ + { Header: 'Fastest', accessor: 'fast' }, + { Header: 'Half Hour', accessor: 'halfHour' }, + { Header: 'Hour', accessor: 'hour' }, + { Header: 'Minimum', accessor: 'minimum' }, + ]; + + const data = [ + { + fast: `${fast} sat/vB`, + halfHour: `${halfHour} sat/vB`, + hour: `${hour} sat/vB`, + minimum: `${minimum} sat/vB`, + }, + ]; + + return ( + + + + ); +}; diff --git a/src/views/dashboard/widgets/helpers.tsx b/src/views/dashboard/widgets/helpers.tsx new file mode 100644 index 00000000..d7ada273 --- /dev/null +++ b/src/views/dashboard/widgets/helpers.tsx @@ -0,0 +1,185 @@ +import { + differenceInDays, + differenceInHours, + subDays, + subHours, +} from 'date-fns'; +import groupBy from 'lodash.groupby'; +import { GetForwardsQuery } from 'src/graphql/queries/__generated__/getForwards.generated'; +import { Transaction } from 'src/graphql/types'; +import { defaultGrid } from 'src/utils/gridConstants'; +import { StoredWidget } from '..'; +import { widgetList, WidgetProps } from './widgetList'; + +const getColumns = (width: number): number => { + const { lg, md, sm, xs } = defaultGrid.breakpoints; + + if (width >= lg) { + return defaultGrid.columns.lg; + } + if (width >= md) { + return defaultGrid.columns.md; + } + if (width >= sm) { + return defaultGrid.columns.sm; + } + if (width >= xs) { + return defaultGrid.columns.xs; + } + return defaultGrid.columns.xxs; +}; + +export type EnrichedWidgetProps = { + nodeId?: string; + nodeAlias?: string; + color?: string; +} & WidgetProps; + +export const getWidgets = ( + widgets: StoredWidget[], + width: number, + extra: StoredWidget[] +): EnrichedWidgetProps[] => { + if (!widgets?.length) return []; + + const columns = getColumns(width); + + const normalized = [...widgets, ...extra].reduce((p, c, index) => { + const current = widgetList.find(w => w.id === c.id); + + if (!current) { + return p; + } + + return [ + ...p, + { + ...current, + default: { + ...current.default, + x: (current.default.w * index) % columns, + }, + }, + ]; + }, [] as EnrichedWidgetProps[]); + + return normalized; +}; + +type ArrayType = GetForwardsQuery['getForwards'] | Transaction[]; + +export const getByTime = (array: ArrayType, time: number): any[] => { + if (!array?.length) return []; + + const transactions: any[] = []; + const isDay = time <= 1; + + const today = new Date(); + + array.forEach((transaction: ArrayType[0]) => { + if (!transaction) return; + if (transaction.__typename === 'InvoiceType') { + if (!transaction.is_confirmed || !transaction.confirmed_at) return; + + const difference = isDay + ? 24 - differenceInHours(today, new Date(transaction.confirmed_at)) + : time - differenceInDays(today, new Date(transaction.confirmed_at)); + + transactions.push({ + difference, + date: new Date(transaction.confirmed_at).toISOString(), + tokens: Number(transaction.tokens), + }); + } else if (transaction.__typename === 'PaymentType') { + if (!transaction.is_confirmed) return; + + const difference = isDay + ? 24 - differenceInHours(today, new Date(transaction.created_at)) + : time - differenceInDays(today, new Date(transaction.created_at)); + + transactions.push({ + difference, + date: new Date(transaction.created_at).toISOString(), + tokens: Number(transaction.tokens), + }); + } else if (transaction.__typename === 'Forward') { + const difference = isDay + ? 24 - differenceInHours(today, new Date(transaction.created_at)) + : time - differenceInDays(today, new Date(transaction.created_at)); + + transactions.push({ + difference, + date: transaction.created_at, + tokens: Number(transaction.tokens), + fee: Number(transaction.fee), + }); + } + }); + + if (!transactions?.length) return []; + + const grouped = groupBy(transactions, 'difference'); + + const final: any[] = []; + const differences = Array.from( + { length: isDay ? 25 : time + 1 }, + (_, i) => i + ); + + differences.forEach(key => { + const group = grouped[key]; + if (!group) { + final.push({ + tokens: 0, + amount: 0, + fee: 0, + date: isDay + ? subHours(today, 24 - Number(key)) + .toISOString() + .slice(0, 10) + : subDays(today, time - Number(key)) + .toISOString() + .slice(0, 10), + }); + return; + } + + const reduced = group.reduce( + (total, transaction) => { + return { + tokens: total.tokens + transaction.tokens, + fee: total.fee + transaction.fee || 0, + amount: total.amount + 1, + date: total.date + ? total.date + : isDay + ? subHours(today, 24 - Number(key)) + .toISOString() + .slice(0, 10) + : subDays(today, time - Number(key)) + .toISOString() + .slice(0, 10), + }; + }, + { + tokens: 0, + fee: 0, + amount: 0, + date: '', + } + ); + + final.push(reduced); + }); + + // final.push({ tokens: 1, amount: 0, date: new Date().toString() }); + // final.push({ + // tokens: 1, + // amount: 0, + // date: isDay + // ? subHours(new Date(), 25).toString() + // : subDays(new Date(), time + 1).toString(), + // }); + + return final; +}; diff --git a/src/views/dashboard/widgets/lightning/balances.tsx b/src/views/dashboard/widgets/lightning/balances.tsx new file mode 100644 index 00000000..56e74116 --- /dev/null +++ b/src/views/dashboard/widgets/lightning/balances.tsx @@ -0,0 +1,84 @@ +import { Price } from 'src/components/price/Price'; +import { useNodeInfo } from 'src/hooks/UseNodeInfo'; +import { unSelectedNavButton } from 'src/styles/Themes'; +import styled from 'styled-components'; + +const S = { + wrapper: styled.div` + overflow: auto; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + width: 100%; + height: 100%; + `, + total: styled.h2` + margin: 0; + `, + smallTotal: styled.h3` + margin: 0; + `, + pending: styled.div` + color: ${unSelectedNavButton}; + font-size: 14px; + `, +}; + +export const TotalBalance = () => { + const { chainBalance, chainPending, channelBalance, channelPending } = + useNodeInfo(); + + const total = chainBalance + channelBalance; + const pending = chainPending + channelPending; + + return ( + + Total Balance + + + + {pending > 0 ? ( + + + + ) : null} + + ); +}; + +export const ChannelBalance = () => { + const { channelBalance, channelPending } = useNodeInfo(); + + return ( + + Channel Balance + + + + {channelPending > 0 ? ( + + + + ) : null} + + ); +}; + +export const ChainBalance = () => { + const { chainBalance, chainPending } = useNodeInfo(); + + return ( + + Chain Balance + + + + {chainPending > 0 ? ( + + + + ) : null} + + ); +}; diff --git a/src/views/dashboard/widgets/lightning/channels.tsx b/src/views/dashboard/widgets/lightning/channels.tsx new file mode 100644 index 00000000..944979ce --- /dev/null +++ b/src/views/dashboard/widgets/lightning/channels.tsx @@ -0,0 +1,18 @@ +import { Channels } from 'src/views/channels/channels/Channels'; +import styled from 'styled-components'; + +const S = { + wrapper: styled.div` + height: 100%; + width: 100%; + overflow: auto; + `, +}; + +export const ChannelListWidget = () => { + return ( + + + + ); +}; diff --git a/src/views/dashboard/widgets/lightning/forwards.tsx b/src/views/dashboard/widgets/lightning/forwards.tsx new file mode 100644 index 00000000..9cb839ff --- /dev/null +++ b/src/views/dashboard/widgets/lightning/forwards.tsx @@ -0,0 +1,72 @@ +import { getDateDif } from 'src/components/generic/helpers'; +import { Price } from 'src/components/price/Price'; +import { Table } from 'src/components/table'; +import { useGetForwardsQuery } from 'src/graphql/queries/__generated__/getForwards.generated'; +import { ChannelAlias } from 'src/views/home/reports/forwardReport/ChannelAlias'; +import styled from 'styled-components'; + +const S = { + wrapper: styled.div` + width: 100%; + height: 100%; + `, + table: styled.div` + width: 100%; + height: calc(100% - 40px); + overflow: auto; + `, + title: styled.h4` + font-weight: 900; + width: 100%; + text-align: center; + margin: 8px 0; + `, + nowrap: styled.div` + white-space: nowrap; + `, +}; + +export const ForwardListWidget = () => { + const { data } = useGetForwardsQuery({ variables: { days: 7 } }); + + const forwards = data?.getForwards || []; + + const columns = [ + { Header: 'Date', accessor: 'date' }, + { Header: 'Amount', accessor: 'amount' }, + { Header: 'Fee', accessor: 'fee' }, + { Header: 'Incoming', accessor: 'incoming' }, + { Header: 'Outgoing', accessor: 'outgoing' }, + ]; + + const tableData = forwards.reduce((p, f) => { + if (!f) return p; + return [ + ...p, + { + date: {getDateDif(f.created_at)}, + amount: ( + + + + ), + fee: ( + + + + ), + incoming: , + outgoing: , + }, + ]; + }, [] as any); + + return ( + + Forwards + +
+ + + ); +}; diff --git a/src/views/dashboard/widgets/lightning/forwardsGraph.tsx b/src/views/dashboard/widgets/lightning/forwardsGraph.tsx new file mode 100644 index 00000000..cb5c853c --- /dev/null +++ b/src/views/dashboard/widgets/lightning/forwardsGraph.tsx @@ -0,0 +1,123 @@ +import { useState } from 'react'; +import { BarChart } from 'src/components/chart/BarChart'; +import { LoadingCard } from 'src/components/loading/LoadingCard'; +import { SmallSelectWithValue } from 'src/components/select'; +import { useGetForwardsQuery } from 'src/graphql/queries/__generated__/getForwards.generated'; +import { chartColors } from 'src/styles/Themes'; +import styled from 'styled-components'; +import { getByTime } from '../helpers'; + +const S = { + row: styled.div` + display: grid; + grid-template-columns: 1fr 60px 90px; + `, + wrapper: styled.div` + width: 100%; + height: 100%; + `, + contentWrapper: styled.div` + width: 100%; + height: 100%; + display: flex; + justify-content: center; + align-items: center; + `, + content: styled.div` + width: 100%; + padding: 0 16px; + height: calc(100% - 40px); + overflow: auto; + `, + title: styled.h4` + font-weight: 900; + margin: 8px 0; + `, + nowrap: styled.div` + white-space: nowrap; + `, +}; + +const options = [ + { label: '1D', value: 1 }, + { label: '7D', value: 7 }, + { label: '1M', value: 30 }, + { label: '2M', value: 60 }, + { label: '6M', value: 180 }, + { label: '1Y', value: 360 }, +]; + +const typeOptions = [ + { label: 'Amount', value: 'amount' }, + { label: 'Tokens', value: 'tokens' }, + { label: 'Fees', value: 'fee' }, +]; + +export const ForwardsGraph = () => { + const [days, setDays] = useState(options[1]); + const [type, setType] = useState(typeOptions[0]); + + const { data, loading } = useGetForwardsQuery({ + ssr: false, + variables: { days: days.value }, + errorPolicy: 'ignore', + }); + + const Header = () => ( + + Forwards + setDays((e[0] || options[1]) as any)} + options={options} + value={days} + isClearable={false} + maxWidth={'60px'} + /> + setType((e[0] || typeOptions[1]) as any)} + options={typeOptions} + value={type} + isClearable={false} + maxWidth={'90px'} + /> + + ); + + if (loading) { + return ( + +
+ + + + + ); + } + + if (!data?.getForwards.length) { + return ( + +
+ No forwards for this period. + + ); + } + + const forwards = getByTime(data.getForwards, days.value); + + return ( + +
+ + ({ + Forward: f[type.value] || 0, + date: f.date, + }))} + colorRange={[chartColors.purple]} + /> + + + ); +}; diff --git a/src/views/dashboard/widgets/lightning/info.tsx b/src/views/dashboard/widgets/lightning/info.tsx new file mode 100644 index 00000000..641ae59b --- /dev/null +++ b/src/views/dashboard/widgets/lightning/info.tsx @@ -0,0 +1,50 @@ +import { useGetLiquidReportQuery } from 'src/graphql/queries/__generated__/getChannelReport.generated'; +import { useNodeInfo } from 'src/hooks/UseNodeInfo'; +import styled from 'styled-components'; + +const S = { + wrapper: styled.div` + overflow: auto; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + width: 100%; + height: 100%; + `, + title: styled.h2` + margin: 0; + `, +}; + +export const AliasWidget = () => { + const { alias } = useNodeInfo(); + + return ( + + {alias} + + ); +}; + +export const BalanceWidget = () => { + const { data } = useGetLiquidReportQuery({ errorPolicy: 'ignore' }); + + if (!data?.getChannelReport) { + return ( + + - + + ); + } + + const { local, remote } = data.getChannelReport; + + const balance = Math.round(((local || 0) / (remote || 1)) * 100); + + return ( + + {`${balance}%`} + + ); +}; diff --git a/src/views/dashboard/widgets/lightning/invoiceGraph.tsx b/src/views/dashboard/widgets/lightning/invoiceGraph.tsx new file mode 100644 index 00000000..10775364 --- /dev/null +++ b/src/views/dashboard/widgets/lightning/invoiceGraph.tsx @@ -0,0 +1,140 @@ +import { useMemo } from 'react'; +import { useState } from 'react'; +import { BarChart } from 'src/components/chart/BarChart'; +import { LoadingCard } from 'src/components/loading/LoadingCard'; +import { SmallSelectWithValue } from 'src/components/select'; +import { + GetResumeQuery, + useGetResumeQuery, +} from 'src/graphql/queries/__generated__/getResume.generated'; +import { InvoiceType } from 'src/graphql/types'; +import { chartColors } from 'src/styles/Themes'; +import styled from 'styled-components'; +import { getByTime } from '../helpers'; + +const S = { + row: styled.div` + display: grid; + grid-template-columns: 1fr 60px 90px; + `, + wrapper: styled.div` + width: 100%; + height: 100%; + `, + contentWrapper: styled.div` + width: 100%; + height: 100%; + display: flex; + justify-content: center; + align-items: center; + `, + content: styled.div` + width: 100%; + padding: 0 16px; + height: calc(100% - 40px); + overflow: auto; + `, + title: styled.h4` + font-weight: 900; + margin: 8px 0; + `, + nowrap: styled.div` + white-space: nowrap; + `, +}; + +const options = [ + { label: '1D', value: 1 }, + { label: '7D', value: 7 }, + { label: '1M', value: 30 }, + { label: '2M', value: 60 }, +]; + +const typeOptions = [ + { label: 'Amount', value: 'amount' }, + { label: 'Tokens', value: 'tokens' }, +]; + +export const InvoicesGraph = () => { + const [days, setDays] = useState(options[1]); + const [type, setType] = useState(typeOptions[0]); + + const { data, loading } = useGetResumeQuery({ + variables: { limit: days.value }, + errorPolicy: 'ignore', + }); + + const resume: GetResumeQuery['getResume']['resume'] = + data?.getResume.resume || []; + + const invoicesByDate = useMemo(() => { + const invoices = resume.reduce((p, c) => { + if (!c) return p; + if (c.__typename === 'InvoiceType') { + if (!c.is_confirmed) return p; + return [...p, c]; + } + return p; + }, [] as InvoiceType[]); + + return getByTime(invoices, days.value); + }, [resume]); + + const Header = () => ( + + Invoices + setDays((e[0] || options[1]) as any)} + options={options} + value={days} + isClearable={false} + maxWidth={'60px'} + /> + setType((e[0] || typeOptions[1]) as any)} + options={typeOptions} + value={type} + isClearable={false} + maxWidth={'90px'} + /> + + ); + + if (loading) { + return ( + +
+ + + + + ); + } + + if (!resume.length) { + return ( + +
+ No invoices for this period. + + ); + } + + return ( + +
+ + { + return { + Invoices: f?.[type.value] || 0, + date: f.date, + }; + })} + colorRange={[chartColors.orange2]} + /> + + + ); +}; diff --git a/src/views/dashboard/widgets/lightning/liquidityGraph.tsx b/src/views/dashboard/widgets/lightning/liquidityGraph.tsx new file mode 100644 index 00000000..94f08671 --- /dev/null +++ b/src/views/dashboard/widgets/lightning/liquidityGraph.tsx @@ -0,0 +1,71 @@ +import { HorizontalBarChart } from 'src/components/chart/HorizontalBarChart'; +import { LoadingCard } from 'src/components/loading/LoadingCard'; +import { useGetLiquidReportQuery } from 'src/graphql/queries/__generated__/getChannelReport.generated'; +import { chartColors } from 'src/styles/Themes'; +import styled from 'styled-components'; + +const S = { + row: styled.div` + display: grid; + grid-template-columns: 1fr 60px 90px; + `, + wrapper: styled.div` + width: 100%; + height: 100%; + `, + contentWrapper: styled.div` + width: 100%; + height: 100%; + display: flex; + justify-content: center; + align-items: center; + `, + title: styled.h4` + font-weight: 900; + margin: 8px 0; + `, +}; + +export const LiquidityGraph = () => { + const { data, loading } = useGetLiquidReportQuery({ errorPolicy: 'ignore' }); + + if (loading) { + return ( + + Liquidity + + + + + ); + } + + if (!data?.getChannelReport) { + return ( + + Liquidity + No invoices for this period. + + ); + } + + const { local, remote, maxIn, maxOut, commit } = data.getChannelReport; + + const liquidity = [ + { label: 'Total Commit', Value: commit }, + { label: 'Max Outgoing', Value: maxOut }, + { label: 'Max Incoming', Value: maxIn }, + { label: 'Local Balance', Value: local }, + { label: 'Remote Balance', Value: remote }, + ]; + + return ( + + + + ); +}; diff --git a/src/views/dashboard/widgets/lightning/modal.tsx b/src/views/dashboard/widgets/lightning/modal.tsx new file mode 100644 index 00000000..9c8b3398 --- /dev/null +++ b/src/views/dashboard/widgets/lightning/modal.tsx @@ -0,0 +1,69 @@ +import { ColorButton } from 'src/components/buttons/colorButton/ColorButton'; +import { useDashDispatch } from 'src/context/DashContext'; + +export const PayInvoice = () => { + const dispatch = useDashDispatch(); + + return ( + dispatch({ type: 'openModal', modalType: 'payInvoice' })} + > + Pay Invoice + + ); +}; + +export const CreateInvoice = () => { + const dispatch = useDashDispatch(); + + return ( + + dispatch({ type: 'openModal', modalType: 'createInvoice' }) + } + > + Create Invoice + + ); +}; + +export const SendOnChain = () => { + const dispatch = useDashDispatch(); + + return ( + dispatch({ type: 'openModal', modalType: 'sendChain' })} + > + Send Bitcoin + + ); +}; + +export const ReceiveOnChain = () => { + const dispatch = useDashDispatch(); + + return ( + dispatch({ type: 'openModal', modalType: 'receiveChain' })} + > + Receive Bitcoin + + ); +}; + +export const OpenChannel = () => { + const dispatch = useDashDispatch(); + + return ( + dispatch({ type: 'openModal', modalType: 'openChannel' })} + > + Open Channel + + ); +}; diff --git a/src/views/dashboard/widgets/lightning/paymentGraph.tsx b/src/views/dashboard/widgets/lightning/paymentGraph.tsx new file mode 100644 index 00000000..1825445e --- /dev/null +++ b/src/views/dashboard/widgets/lightning/paymentGraph.tsx @@ -0,0 +1,136 @@ +import { useMemo } from 'react'; +import { useState } from 'react'; +import { BarChart } from 'src/components/chart/BarChart'; +import { LoadingCard } from 'src/components/loading/LoadingCard'; +import { SmallSelectWithValue } from 'src/components/select'; +import { useGetResumeQuery } from 'src/graphql/queries/__generated__/getResume.generated'; +import { PaymentType } from 'src/graphql/types'; +import { chartColors } from 'src/styles/Themes'; +import styled from 'styled-components'; +import { getByTime } from '../helpers'; + +const S = { + row: styled.div` + display: grid; + grid-template-columns: 1fr 60px 90px; + `, + wrapper: styled.div` + width: 100%; + height: 100%; + `, + contentWrapper: styled.div` + width: 100%; + height: 100%; + display: flex; + justify-content: center; + align-items: center; + `, + content: styled.div` + width: 100%; + padding: 0 16px; + height: calc(100% - 40px); + overflow: auto; + `, + title: styled.h4` + font-weight: 900; + margin: 8px 0; + `, + nowrap: styled.div` + white-space: nowrap; + `, +}; + +const options = [ + { label: '1D', value: 1 }, + { label: '7D', value: 7 }, + { label: '1M', value: 30 }, + { label: '2M', value: 60 }, +]; + +const typeOptions = [ + { label: 'Amount', value: 'amount' }, + { label: 'Tokens', value: 'tokens' }, +]; + +export const PaymentsGraph = () => { + const [days, setDays] = useState(options[1]); + const [type, setType] = useState(typeOptions[0]); + + const { data, loading } = useGetResumeQuery({ + variables: { limit: days.value }, + errorPolicy: 'ignore', + }); + + const resume = data?.getResume.resume || []; + + const paymentsByDate = useMemo(() => { + const payments = resume.reduce((p, c) => { + if (!c) return p; + if (c.__typename === 'PaymentType') { + if (!c.is_confirmed) return p; + return [...p, c]; + } + return p; + }, [] as PaymentType[]); + + return getByTime(payments, days.value); + }, [resume]); + + const Header = () => ( + + Payments + setDays((e[0] || options[1]) as any)} + options={options} + value={days} + isClearable={false} + maxWidth={'60px'} + /> + setType((e[0] || typeOptions[1]) as any)} + options={typeOptions} + value={type} + isClearable={false} + maxWidth={'90px'} + /> + + ); + + if (loading) { + return ( + +
+ + + + + ); + } + + if (!resume.length) { + return ( + +
+ No payments for this period. + + ); + } + + return ( + +
+ + { + return { + Payments: f?.[type.value] || 0, + date: f.date, + }; + })} + colorRange={[chartColors.darkyellow]} + /> + + + ); +}; diff --git a/src/views/dashboard/widgets/lightning/transactions.tsx b/src/views/dashboard/widgets/lightning/transactions.tsx new file mode 100644 index 00000000..1f18e70c --- /dev/null +++ b/src/views/dashboard/widgets/lightning/transactions.tsx @@ -0,0 +1,91 @@ +import { ArrowDown, ArrowUp } from 'react-feather'; +import { getDateDif, shorten } from 'src/components/generic/helpers'; +import { Price } from 'src/components/price/Price'; +import { Table } from 'src/components/table'; +import { useGetResumeQuery } from 'src/graphql/queries/__generated__/getResume.generated'; +import { chartColors } from 'src/styles/Themes'; +import styled from 'styled-components'; + +const S = { + wrapper: styled.div` + width: 100%; + height: 100%; + `, + table: styled.div` + width: 100%; + height: calc(100% - 40px); + overflow: auto; + `, + title: styled.h4` + font-weight: 900; + width: 100%; + text-align: center; + margin: 8px 0; + `, + nowrap: styled.div` + white-space: nowrap; + `, +}; + +export const TransactionsWidget = () => { + const { data } = useGetResumeQuery(); + const transactions = data?.getResume.resume || []; + + const columns = [ + { Header: 'Date', accessor: 'date' }, + { Header: 'Type', accessor: 'type' }, + { Header: 'Amount', accessor: 'value' }, + { Header: 'Info', accessor: 'info' }, + ]; + + const normalized = transactions.reduce((p, c) => { + if (!c) return p; + if (c.__typename === 'InvoiceType') { + if (!c.is_confirmed) return p; + return [ + ...p, + { + type: , + value: ( + + + + ), + date: {getDateDif(c.confirmed_at)}, + info: {c.description || c.description_hash}, + }, + ]; + } + if (c.__typename === 'PaymentType') { + if (!c.is_confirmed) return p; + return [ + ...p, + { + type: , + value: ( + + + + ), + date: {getDateDif(c.created_at)}, + info: ( + + {c.destination_node + ? `Payment to ${c.destination_node.node.alias}` + : `Payment to ${shorten(c.destination)}`} + + ), + }, + ]; + } + }, [] as any); + + return ( + + Transactions + +
+ + + ); +}; diff --git a/src/views/dashboard/widgets/lightning/transactionsGraph.tsx b/src/views/dashboard/widgets/lightning/transactionsGraph.tsx new file mode 100644 index 00000000..b920b65d --- /dev/null +++ b/src/views/dashboard/widgets/lightning/transactionsGraph.tsx @@ -0,0 +1,147 @@ +import { useMemo } from 'react'; +import { useState } from 'react'; +import { BarChart } from 'src/components/chart/BarChart'; +import { LoadingCard } from 'src/components/loading/LoadingCard'; +import { SmallSelectWithValue } from 'src/components/select'; +import { useGetResumeQuery } from 'src/graphql/queries/__generated__/getResume.generated'; +import { InvoiceType, PaymentType } from 'src/graphql/types'; +import { chartColors } from 'src/styles/Themes'; +import styled from 'styled-components'; +import { getByTime } from '../helpers'; + +const S = { + row: styled.div` + display: grid; + grid-template-columns: 1fr 60px 90px; + `, + wrapper: styled.div` + width: 100%; + height: 100%; + `, + contentWrapper: styled.div` + width: 100%; + height: 100%; + display: flex; + justify-content: center; + align-items: center; + `, + content: styled.div` + width: 100%; + padding: 0 16px; + height: calc(100% - 40px); + overflow: auto; + `, + title: styled.h4` + font-weight: 900; + margin: 8px 0; + `, + nowrap: styled.div` + white-space: nowrap; + `, +}; + +const options = [ + { label: '1D', value: 1 }, + { label: '7D', value: 7 }, + { label: '1M', value: 30 }, + { label: '2M', value: 60 }, +]; + +const typeOptions = [ + { label: 'Amount', value: 'amount' }, + { label: 'Tokens', value: 'tokens' }, +]; + +export const TransactionsGraph = () => { + const [days, setDays] = useState(options[1]); + const [type, setType] = useState(typeOptions[0]); + + const { data, loading } = useGetResumeQuery({ + variables: { limit: days.value }, + errorPolicy: 'ignore', + }); + + const resume = data?.getResume.resume || []; + + const { invoicesByDate, paymentsByDate } = useMemo(() => { + const invoices: InvoiceType[] = []; + const payments: PaymentType[] = []; + + resume.forEach(t => { + if (!t) return; + if (t.__typename === 'InvoiceType') { + if (!t.is_confirmed) return; + invoices.push(t); + } + if (t.__typename === 'PaymentType') { + if (!t.is_confirmed) return; + payments.push(t); + } + }); + + const invoicesByDate = getByTime(invoices, days.value); + const paymentsByDate = getByTime(payments, days.value); + + return { invoicesByDate, paymentsByDate }; + }, [resume]); + + const Header = () => ( + + Transactions + setDays((e[0] || options[1]) as any)} + options={options} + value={days} + isClearable={false} + maxWidth={'60px'} + /> + setType((e[0] || typeOptions[1]) as any)} + options={typeOptions} + value={type} + isClearable={false} + maxWidth={'90px'} + /> + + ); + + if (loading) { + return ( + +
+ + + + + ); + } + + if (!resume.length) { + return ( + +
+ No transactions for this period. + + ); + } + + return ( + +
+ + { + const payment = paymentsByDate.find(p => p.date === f.date); + return { + Invoices: f?.[type.value] || 0, + Payments: payment?.[type.value] || 0, + date: f.date, + }; + })} + colorRange={[chartColors.orange2, chartColors.darkyellow]} + /> + + + ); +}; diff --git a/src/views/dashboard/widgets/link/index.tsx b/src/views/dashboard/widgets/link/index.tsx new file mode 100644 index 00000000..1956a5c2 --- /dev/null +++ b/src/views/dashboard/widgets/link/index.tsx @@ -0,0 +1,60 @@ +import { ColorButton } from 'src/components/buttons/colorButton/ColorButton'; +import { Link } from 'src/components/link/Link'; +import styled from 'styled-components'; + +const S = { + wrapper: styled.div` + width: 100%; + overflow: hidden; + `, +}; + +export const DashSettingsLink = () => { + return ( + + + Dash Settings + + + ); +}; + +export const ForwardsViewLink = () => { + return ( + + + Forwards + + + ); +}; + +export const TransactionsViewLink = () => { + return ( + + + Transactions + + + ); +}; + +export const ChannelViewLink = () => { + return ( + + + Channels + + + ); +}; + +export const RebalanceViewLink = () => { + return ( + + + Rebalance + + + ); +}; diff --git a/src/views/dashboard/widgets/settings/index.tsx b/src/views/dashboard/widgets/settings/index.tsx new file mode 100644 index 00000000..fc7f9781 --- /dev/null +++ b/src/views/dashboard/widgets/settings/index.tsx @@ -0,0 +1,76 @@ +import { Star, Sun, Moon } from 'react-feather'; +import { SingleButton } from 'src/components/buttons/multiButton/MultiButton'; +import { useConfigDispatch, useConfigState } from 'src/context/ConfigContext'; +import styled from 'styled-components'; + +const S = { + wrapper: styled.div` + overflow: auto; + width: 100%; + height: 100%; + display: flex; + flex-wrap: wrap; + `, +}; + +export const ThemeSetting = () => { + const { theme } = useConfigState(); + const dispatch = useConfigDispatch(); + + const handleDispatch = (theme: string) => + dispatch({ type: 'themeChange', theme }); + + return ( + + handleDispatch('light')} + > + + + handleDispatch('dark')} + > + + + handleDispatch('night')} + > + + + + ); +}; + +export const CurrencySetting = () => { + const { currency } = useConfigState(); + const dispatch = useConfigDispatch(); + + const handleDispatch = (currency: string) => + dispatch({ type: 'change', currency }); + + return ( + + handleDispatch('sat')} + > + Sat + + handleDispatch('btc')} + > + Btc + + handleDispatch('fiat')} + > + Fiat + + + ); +}; diff --git a/src/views/dashboard/widgets/util/Convert.tsx b/src/views/dashboard/widgets/util/Convert.tsx new file mode 100644 index 00000000..26bf8f68 --- /dev/null +++ b/src/views/dashboard/widgets/util/Convert.tsx @@ -0,0 +1,159 @@ +import numeral from 'numeral'; +import { useState } from 'react'; +import { Input } from 'src/components/input'; +import { SelectWithValue } from 'src/components/select'; +import { usePriceState } from 'src/context/PriceContext'; +import styled from 'styled-components'; + +const S = { + row: styled.div` + margin: 8px 0; + display: grid; + grid-gap: 8px; + grid-template-columns: 2fr 4fr 100px; + align-items: center; + `, + wrapper: styled.div` + width: 100%; + height: 100%; + display: flex; + flex-direction: column; + justify-content: center; + padding: 0 4px; + `, + contentWrapper: styled.div` + width: 100%; + height: 100%; + display: flex; + justify-content: center; + align-items: center; + `, + content: styled.div` + width: 100%; + padding: 0 16px; + height: calc(100% - 40px); + overflow: auto; + `, + title: styled.h4` + font-weight: 900; + margin: 8px 0; + `, + nowrap: styled.div` + white-space: nowrap; + `, +}; + +export const ConvertWidget = () => { + const { prices, dontShow } = usePriceState(); + + const [firstAmount, setFirstAmount] = useState(1); + const [secondAmount, setSecondAmount] = useState(100000000); + + const available = Object.keys(prices || {}); + const options = [ + { value: 'btc', label: 'BTC' }, + { value: 'sat', label: 'SAT' }, + ...available.map(c => ({ value: c, label: c })), + ]; + + const [first, setFirst] = useState(options[0]); + const [second, setSecond] = useState(options[1]); + + if (dontShow) { + return ( + + Fetching fiat prices is disabled. Enable it in the settings. + + ); + } + + const getPrice = (currency: string) => { + switch (currency) { + case 'btc': + return 1; + case 'sat': + return 100000000; + default: + return prices?.[currency]?.last || 0; + } + }; + + const convert = (from: string, to: string, value: number) => { + const fromPrice = getPrice(from); + const toPrice = getPrice(to); + + if (from === to) { + return value; + } + + if (from === 'btc') { + return fromPrice * toPrice * value; + } + + return (toPrice / fromPrice) * value; + }; + + const getValue = (value: number, current: string) => { + switch (current) { + case 'btc': + return numeral(value).format('0,0.[00000000]'); + case 'sat': + return numeral(value).format('0,0'); + default: + return numeral(value).format('0,0.[00]'); + } + }; + + return ( + + +
{getValue(firstAmount, first.value)}
+ { + const value = Number(e.target.value); + setFirstAmount(value); + setSecondAmount(convert(first.value, second.value, value)); + }} + /> + { + const value = (e[0] || options[0]) as any; + setFirst(value); + setFirstAmount(convert(second.value, value.value, secondAmount)); + }} + options={options} + value={first} + isClearable={false} + maxWidth={'100px'} + /> +
{getValue(secondAmount, second.value)}
+ { + const value = Number(e.target.value); + setSecondAmount(value); + setFirstAmount(convert(second.value, first.value, value)); + }} + /> + { + const value = (e[0] || options[1]) as any; + setSecond(value); + setSecondAmount(convert(first.value, value.value, firstAmount)); + }} + options={options} + value={second} + isClearable={false} + maxWidth={'100px'} + /> +
+
+ ); +}; diff --git a/src/views/dashboard/widgets/util/DonateWidget.tsx b/src/views/dashboard/widgets/util/DonateWidget.tsx new file mode 100644 index 00000000..e45950cc --- /dev/null +++ b/src/views/dashboard/widgets/util/DonateWidget.tsx @@ -0,0 +1,38 @@ +import { Heart } from 'react-feather'; +import { ColorButton } from 'src/components/buttons/colorButton/ColorButton'; +import { useDashDispatch } from 'src/context/DashContext'; +import styled from 'styled-components'; + +const S = { + wrapper: styled.div` + height: 100%; + width: 100%; + `, + title: styled.div` + font-size: 14px; + margin-left: 4px; + `, + row: styled.div` + display: flex; + justify-content: space-around; + align-items: center; + `, +}; + +export const DonateWidget = () => { + const dispatch = useDashDispatch(); + + return ( + + dispatch({ type: 'openModal', modalType: 'donate' })} + > + + + Donate + + + + ); +}; diff --git a/src/views/dashboard/widgets/util/Sign.tsx b/src/views/dashboard/widgets/util/Sign.tsx new file mode 100644 index 00000000..ded22e81 --- /dev/null +++ b/src/views/dashboard/widgets/util/Sign.tsx @@ -0,0 +1,27 @@ +import { ColorButton } from 'src/components/buttons/colorButton/ColorButton'; +import { useDashDispatch } from 'src/context/DashContext'; +import styled from 'styled-components'; + +const S = { + wrapper: styled.div` + height: 100%; + width: 100%; + `, +}; + +export const SignWidget = () => { + const dispatch = useDashDispatch(); + + return ( + + + dispatch({ type: 'openModal', modalType: 'signMessage' }) + } + > + Sign Message + + + ); +}; diff --git a/src/views/dashboard/widgets/widgetList.tsx b/src/views/dashboard/widgets/widgetList.tsx new file mode 100644 index 00000000..52dcf3be --- /dev/null +++ b/src/views/dashboard/widgets/widgetList.tsx @@ -0,0 +1,301 @@ +import { FC } from 'react'; +import { MempoolWidget } from './external/mempool'; +import { + ChainBalance, + ChannelBalance, + TotalBalance, +} from './lightning/balances'; +import { ChannelListWidget } from './lightning/channels'; +import { ForwardListWidget } from './lightning/forwards'; +import { ForwardsGraph } from './lightning/forwardsGraph'; +import { AliasWidget, BalanceWidget } from './lightning/info'; +import { InvoicesGraph } from './lightning/invoiceGraph'; +import { LiquidityGraph } from './lightning/liquidityGraph'; +import { + CreateInvoice, + OpenChannel, + PayInvoice, + ReceiveOnChain, + SendOnChain, +} from './lightning/modal'; +import { PaymentsGraph } from './lightning/paymentGraph'; +import { TransactionsWidget } from './lightning/transactions'; +import { TransactionsGraph } from './lightning/transactionsGraph'; +import { + ChannelViewLink, + DashSettingsLink, + ForwardsViewLink, + RebalanceViewLink, + TransactionsViewLink, +} from './link'; +import { CurrencySetting, ThemeSetting } from './settings'; +import { ConvertWidget } from './util/Convert'; +import { DonateWidget } from './util/DonateWidget'; +import { SignWidget } from './util/Sign'; + +export const widgetDefaults = { + width: 4, + height: 8, +}; + +export type WidgetProps = { + id: number; + name: string; + group: string; + subgroup: string; + hidden?: boolean; + component: FC; + default: { + x: number; + y: number; + w: number; + h: number; + minW?: number; + minH?: number; + maxW?: number; + maxH?: number; + }; +}; + +const defaultProps = { + x: 0, + y: Infinity, + w: widgetDefaults.width, + h: widgetDefaults.height, +}; + +export const widgetList: WidgetProps[] = [ + { + id: 1, + name: 'Theme', + group: 'Settings', + subgroup: '', + component: ThemeSetting, + default: { ...defaultProps, w: 2, h: 2 }, + }, + { + id: 2, + name: 'Currency', + group: 'Settings', + subgroup: '', + component: CurrencySetting, + default: { ...defaultProps, w: 2, h: 2 }, + }, + { + id: 3, + name: 'Mempool Fees', + group: 'External', + subgroup: '', + component: MempoolWidget, + default: { ...defaultProps, w: 4, h: 3 }, + }, + { + id: 4, + name: 'Total Balance', + group: 'Lightning', + subgroup: 'Info', + component: TotalBalance, + default: { ...defaultProps, w: 2, h: 3 }, + }, + { + id: 5, + name: 'Channel Balance', + group: 'Lightning', + subgroup: 'Info', + component: ChannelBalance, + default: { ...defaultProps, w: 2, h: 3 }, + }, + { + id: 6, + name: 'Chain Balance', + group: 'Lightning', + subgroup: 'Info', + component: ChainBalance, + default: { ...defaultProps, w: 2, h: 3 }, + }, + { + id: 7, + name: 'Alias', + group: 'Lightning', + subgroup: 'Info', + component: AliasWidget, + default: { ...defaultProps, w: 2, h: 2 }, + }, + { + id: 8, + name: 'Transactions', + group: 'Lightning', + subgroup: 'Table', + component: TransactionsWidget, + default: { ...defaultProps, w: 5, h: 16, minW: 3 }, + }, + { + id: 9, + name: 'Forwards', + group: 'Lightning', + subgroup: 'Table', + component: ForwardListWidget, + default: { ...defaultProps, w: 5, h: 16, minW: 3 }, + }, + { + id: 10, + name: 'Forwards', + group: 'Lightning', + subgroup: 'Graph', + component: ForwardsGraph, + default: { ...defaultProps, w: 8, h: 16, minW: 5, minH: 8 }, + }, + { + id: 11, + name: 'Transactions', + group: 'Lightning', + subgroup: 'Graph', + component: TransactionsGraph, + default: { ...defaultProps, w: 8, h: 16, minW: 5, minH: 8 }, + }, + { + id: 12, + name: 'Invoices', + group: 'Lightning', + subgroup: 'Graph', + component: InvoicesGraph, + default: { ...defaultProps, w: 8, h: 16, minW: 5, minH: 8 }, + }, + { + id: 13, + name: 'Payments', + group: 'Lightning', + subgroup: 'Graph', + component: PaymentsGraph, + default: { ...defaultProps, w: 8, h: 16, minW: 5, minH: 8 }, + }, + { + id: 14, + name: 'Pay Invoice', + group: 'Lightning', + subgroup: 'Action', + component: PayInvoice, + default: { ...defaultProps, w: 2, h: 2, minH: 2, maxH: 2 }, + }, + { + id: 15, + name: 'Create Invoice', + group: 'Lightning', + subgroup: 'Action', + component: CreateInvoice, + default: { ...defaultProps, w: 2, h: 2, minH: 2, maxH: 2 }, + }, + { + id: 16, + name: 'Send Bitcoin', + group: 'Lightning', + subgroup: 'Action', + component: SendOnChain, + default: { ...defaultProps, w: 2, h: 2, minH: 2, maxH: 2 }, + }, + { + id: 17, + name: 'Receive Bitcoin', + group: 'Lightning', + subgroup: 'Action', + component: ReceiveOnChain, + default: { ...defaultProps, w: 2, h: 2, minH: 2, maxH: 2 }, + }, + { + id: 18, + name: 'Dashboard Settings', + group: 'Link', + subgroup: '', + component: DashSettingsLink, + default: { ...defaultProps, w: 2, h: 2 }, + }, + { + id: 19, + name: 'Forwards View', + group: 'Link', + subgroup: '', + component: ForwardsViewLink, + default: { ...defaultProps, w: 2, h: 2 }, + }, + { + id: 20, + name: 'Transactions View', + group: 'Link', + subgroup: '', + component: TransactionsViewLink, + default: { ...defaultProps, w: 2, h: 2 }, + }, + { + id: 21, + name: 'Channel View', + group: 'Link', + subgroup: '', + component: ChannelViewLink, + default: { ...defaultProps, w: 2, h: 2 }, + }, + { + id: 22, + name: 'Rebalance View', + group: 'Link', + subgroup: '', + component: RebalanceViewLink, + default: { ...defaultProps, w: 2, h: 2 }, + }, + { + id: 23, + name: 'Open Channel', + group: 'Lightning', + subgroup: 'Action', + component: OpenChannel, + default: { ...defaultProps, w: 2, h: 2, minH: 2, maxH: 2 }, + }, + { + id: 24, + name: 'Convert', + group: 'Utils', + subgroup: '', + component: ConvertWidget, + default: { ...defaultProps, w: 4, h: 4, minW: 3, minH: 4, maxH: 4 }, + }, + { + id: 25, + name: 'Liquidity', + group: 'Lightning', + subgroup: 'Graph', + component: LiquidityGraph, + default: { ...defaultProps, w: 8, h: 8, minW: 5, minH: 6 }, + }, + { + id: 26, + name: 'Balance', + group: 'Lightning', + subgroup: 'Info', + component: BalanceWidget, + default: { ...defaultProps, w: 1, h: 2 }, + }, + { + id: 27, + name: 'Channels', + group: 'Lightning', + subgroup: 'Table', + component: ChannelListWidget, + default: { ...defaultProps, w: 9, h: 16, minW: 9 }, + }, + { + id: 28, + name: 'Donate', + group: 'Utils', + subgroup: '', + hidden: true, + component: DonateWidget, + default: { ...defaultProps, w: 2, h: 2, minH: 2, maxH: 2 }, + }, + { + id: 29, + name: 'Sign Message', + group: 'Utils', + subgroup: '', + component: SignWidget, + default: { ...defaultProps, w: 2, h: 2, minH: 2, maxH: 2 }, + }, +]; diff --git a/src/views/home/account/AccountInfo.tsx b/src/views/home/account/AccountInfo.tsx index 58510512..09db367a 100644 --- a/src/views/home/account/AccountInfo.tsx +++ b/src/views/home/account/AccountInfo.tsx @@ -50,12 +50,8 @@ const sectionColor = '#FFD300'; export const AccountInfo = () => { const [state, setState] = useState('none'); - const { - chainBalance, - chainPending, - channelBalance, - channelPending, - } = useNodeInfo(); + const { chainBalance, chainPending, channelBalance, channelPending } = + useNodeInfo(); const renderContent = () => { switch (state) { diff --git a/src/views/home/quickActions/QuickActions.tsx b/src/views/home/quickActions/QuickActions.tsx index 0eabebe3..ff88151b 100644 --- a/src/views/home/quickActions/QuickActions.tsx +++ b/src/views/home/quickActions/QuickActions.tsx @@ -80,7 +80,11 @@ export const QuickActions = () => { const renderContent = () => { switch (openCard) { case 'support': - return ; + return ( + + + + ); case 'decode': return ; case 'ln_url': diff --git a/src/views/home/quickActions/donate/DonateCard.tsx b/src/views/home/quickActions/donate/DonateCard.tsx index e7a4a512..546470b5 100644 --- a/src/views/home/quickActions/donate/DonateCard.tsx +++ b/src/views/home/quickActions/donate/DonateCard.tsx @@ -8,7 +8,6 @@ import { unSelectedNavButton, mediaWidths, } from 'src/styles/Themes'; -import { useBaseConnect } from 'src/hooks/UseBaseConnect'; const QuickTitle = styled.div` font-size: 14px; @@ -54,10 +53,6 @@ type SupportCardProps = { }; export const SupportCard = ({ callback }: SupportCardProps) => { - const connected = useBaseConnect(); - - if (!connected) return null; - return ( diff --git a/src/views/home/quickActions/donate/DonateContent.tsx b/src/views/home/quickActions/donate/DonateContent.tsx index b26be5a9..952e6e85 100644 --- a/src/views/home/quickActions/donate/DonateContent.tsx +++ b/src/views/home/quickActions/donate/DonateContent.tsx @@ -1,10 +1,5 @@ import * as React from 'react'; -import { - Card, - SubTitle, - Separation, - Sub4Title, -} from 'src/components/generic/Styled'; +import { SubTitle, Separation, Sub4Title } from 'src/components/generic/Styled'; import { InputWithDeco } from 'src/components/input/InputWithDeco'; import { ColorButton } from 'src/components/buttons/colorButton/ColorButton'; import Modal from 'src/components/modal/ReactModal'; @@ -42,16 +37,14 @@ export const SupportBar = () => { const [invoice, invoiceSet] = React.useState(''); const [id, idSet] = React.useState(''); - const connected = useBaseConnect(); + const { connected } = useBaseConnect(); const [withPoints, setWithPoints] = React.useState(false); const [getInvoice, { data, loading }] = useCreateBaseInvoiceMutation(); - const [ - createPoints, - { data: pointsData, called, loading: pointsLoading }, - ] = useCreateThunderPointsMutation({ refetchQueries: ['GetBasePoints'] }); + const [createPoints, { data: pointsData, called, loading: pointsLoading }] = + useCreateThunderPointsMutation({ refetchQueries: ['GetBasePoints'] }); const { data: info } = useGetCanConnectInfoQuery({ ssr: false }); React.useEffect(() => { @@ -73,7 +66,16 @@ export const SupportBar = () => { } }, [pointsData, pointsLoading, called]); - if (!connected) return null; + if (!connected) + return ( +
+ Unable to connect to donation server. + + Please check back later.Thanks for wanting to donate + + +
+ ); const handleReset = () => { modalOpenSet(false); @@ -102,56 +104,54 @@ export const SupportBar = () => { return ( <> - -
- This project is completely free and open-source. - - If you have enjoyed it, please consider supporting ThunderHub with - some sats - -
- - amountSet(Number(value))} - /> - - - - {renderButton(() => setWithPoints(true), 'Yes', withPoints)} - {renderButton(() => setWithPoints(false), 'No', !withPoints)} - - - {withPoints && ( - <> - - This means your node will appear in the ThunderHub donation - leaderboard. If you want to remain anonymous, do not enable this - option. Your node alias and public key will be stored if you - enable it. - - - Due to the increasing price of Bitcoin, to incentivize development - and to give everyone an opportunity to be in the top of the - leaderboard, points have a half life of 6 months. This means that - every 6 months they are halved. - - - )} - - getInvoice({ variables: { amount } })} - loading={loading} - disabled={amount <= 0 || loading} - fullWidth={true} - withMargin={'8px 0 0 0'} - > - Send - -
+
+ This project is completely free and open-source. + + If you have enjoyed it, please consider supporting ThunderHub with + some sats + +
+ + amountSet(Number(value))} + /> + + + + {renderButton(() => setWithPoints(true), 'Yes', withPoints)} + {renderButton(() => setWithPoints(false), 'No', !withPoints)} + + + {withPoints && ( + <> + + This means your node will appear in the ThunderHub donation + leaderboard. If you want to remain anonymous, do not enable this + option. Your node alias and public key will be stored if you enable + it. + + + Due to the increasing price of Bitcoin, to incentivize development + and to give everyone an opportunity to be in the top of the + leaderboard, points have a half life of 6 months. This means that + every 6 months they are halved. + + + )} + + getInvoice({ variables: { amount } })} + loading={loading} + disabled={amount <= 0 || loading} + fullWidth={true} + withMargin={'8px 0 0 0'} + > + Send + diff --git a/src/views/settings/DashPanel.tsx b/src/views/settings/DashPanel.tsx new file mode 100644 index 00000000..3aa40f70 --- /dev/null +++ b/src/views/settings/DashPanel.tsx @@ -0,0 +1,101 @@ +import { groupBy } from 'lodash'; +import { Fragment } from 'react'; +import { Layouts } from 'react-grid-layout'; +import { ColorButton } from 'src/components/buttons/colorButton/ColorButton'; +import { Card, SubTitle } from 'src/components/generic/Styled'; +import { Link } from 'src/components/link/Link'; +import { useLocalStorage } from 'src/hooks/UseLocalStorage'; +import styled from 'styled-components'; +import { StoredWidget } from '../dashboard'; +import { widgetList } from '../dashboard/widgets/widgetList'; +import { WidgetRow } from './WidgetRow'; + +const S = { + subTitle: styled(SubTitle)` + margin: 32px 0 8px; + `, +}; + +export type NormalizedWidgets = { + id: number; + name: string; + group: string; + active: boolean; +}; + +const DashPanel = () => { + const [, setLayouts] = useLocalStorage('layouts', {}); + const [availableWidgets, setAvailableWidgets] = useLocalStorage< + StoredWidget[] + >('dashboardWidgets', []); + + const normalizedList: NormalizedWidgets[] = widgetList.reduce((p, w) => { + if (w.hidden) return p; + + const active = + availableWidgets.findIndex((a: StoredWidget) => { + return a.id === w.id; + }) >= 0; + + return [...p, { ...w, active }]; + }, [] as NormalizedWidgets[]); + + const handleAdd = (id: number) => { + const filtered = availableWidgets.filter(a => a.id !== id); + setAvailableWidgets([...filtered, { id }]); + }; + + const handleDelete = (id: number) => { + const filtered = availableWidgets.filter(a => a.id !== id); + setAvailableWidgets(filtered); + }; + + const grouped = groupBy(normalizedList, 'group'); + const keys = Object.keys(grouped); + + return ( + + {keys.map((key, index) => { + const widgets = grouped[key]; + const subGrouped = groupBy(widgets, 'subgroup'); + const subKeys = Object.keys(subGrouped); + + return subKeys.map((subKey, subIndex) => { + const subWidgets = subGrouped[subKey]; + + return ( + + {subKey ? `${key} - ${subKey}` : key} + {subWidgets.map(w => ( + + + + ))} + + ); + }); + })} + + + To Dashboard + + + { + setLayouts({}); + setAvailableWidgets([]); + }} + > + Reset Widgets + + + ); +}; + +export default DashPanel; diff --git a/src/views/settings/Dashboard.tsx b/src/views/settings/Dashboard.tsx new file mode 100644 index 00000000..0c73594d --- /dev/null +++ b/src/views/settings/Dashboard.tsx @@ -0,0 +1,27 @@ +import { useRouter } from 'next/router'; +import { SettingsLine } from 'pages/settings'; +import { ColorButton } from 'src/components/buttons/colorButton/ColorButton'; +import { + Card, + CardWithTitle, + Sub4Title, + SubTitle, +} from 'src/components/generic/Styled'; + +export const DashboardSettings = () => { + const { push } = useRouter(); + + return ( + + Dashboard + + + Widgets + push('/settings/dashboard')}> + Change + + + + + ); +}; diff --git a/src/views/settings/WidgetRow.tsx b/src/views/settings/WidgetRow.tsx new file mode 100644 index 00000000..ce0e5a65 --- /dev/null +++ b/src/views/settings/WidgetRow.tsx @@ -0,0 +1,47 @@ +import { FC } from 'react'; +import { + MultiButton, + SingleButton, +} from 'src/components/buttons/multiButton/MultiButton'; +import { DarkSubTitle } from 'src/components/generic/Styled'; +import styled from 'styled-components'; +import { NormalizedWidgets } from './DashPanel'; + +const S = { + line: styled.div` + margin-bottom: 8px; + display: flex; + justify-content: space-between; + align-items: center; + `, +}; + +type WidgetRowParams = { + widget: NormalizedWidgets; + handleAdd: (id: number) => void; + handleDelete: (id: number) => void; +}; + +export const WidgetRow: FC = ({ + widget, + handleAdd, + handleDelete, +}) => ( + + {widget.name} + + handleAdd(widget.id)} + > + Show + + handleDelete(widget.id)} + > + Hide + + + +); diff --git a/src/views/tools/messages/Messages.tsx b/src/views/tools/messages/Messages.tsx index 33498776..35eb73d2 100644 --- a/src/views/tools/messages/Messages.tsx +++ b/src/views/tools/messages/Messages.tsx @@ -5,7 +5,7 @@ import { SubTitle, Card, } from '../../../components/generic/Styled'; -import { SignMessage } from './SignMessage'; +import { SignMessageCard } from './SignMessage'; import { VerifyMessage } from './VerifyMessage'; export const NoWrap = styled.div` @@ -19,7 +19,7 @@ export const MessagesView = () => { Messages - + ); diff --git a/src/views/tools/messages/SignMessage.tsx b/src/views/tools/messages/SignMessage.tsx index 22f6b9ba..6c34c2ce 100644 --- a/src/views/tools/messages/SignMessage.tsx +++ b/src/views/tools/messages/SignMessage.tsx @@ -16,7 +16,6 @@ import { NoWrap } from './Messages'; export const SignMessage = () => { const [message, setMessage] = useState(''); - const [isPasting, setIsPasting] = useState(false); const [signed, setSigned] = useState(''); const [signMessage, { data, loading }] = useSignMessageLazyQuery({ @@ -49,40 +48,51 @@ export const SignMessage = () => { > Sign - ); const renderMessage = () => ( - - {signed} - toast.success('Signature Copied')} - > - - - Copy - - - + <> + + + {signed} + toast.success('Signature Copied')} + > + + + Copy + + + + ); + return ( + <> + {renderInput()} + {signed !== '' && renderMessage()} + + ); +}; + +export const SignMessageCard = () => { + const [isPasting, setIsPasting] = useState(false); + return ( <> Sign Message setIsPasting(prev => !prev)} > {isPasting ? : 'Sign'} - {isPasting && renderInput()} - {signed !== '' && isPasting && renderMessage()} + {isPasting && } ); };