chore: dash and settings

This commit is contained in:
apotdevin 2021-06-17 11:42:40 +02:00
parent beb7da43ed
commit d5ba08cf67
No known key found for this signature in database
GPG key ID: 4403F1DFBE779457
55 changed files with 3390 additions and 220 deletions

View file

@ -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',
},
},
});

359
package-lock.json generated
View file

@ -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",

View file

@ -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",

View file

@ -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 }) => {

View file

@ -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 <ClosedChannels />;
default:
return <Channels />;
return (
<Card mobileCardPadding={'0'} mobileNoBackground={true}>
<Channels />
</Card>
);
}
};

35
pages/dashboard.tsx Normal file
View file

@ -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 = () => <LoadingCard noCard={true} loadingHeight={'90vh'} />;
const Dashboard = dynamic(() => import('src/views/dashboard'), {
ssr: false,
loading: LoadingComp,
});
const Wrapped = () => {
return (
<SimpleWrapper>
<S.wrapper>
<Dashboard />
</S.wrapper>
</SimpleWrapper>
);
};
export default Wrapped;
export async function getServerSideProps(context: NextPageContext) {
return await getProps(context);
}

View file

@ -38,7 +38,9 @@ const LeaderboardView = () => {
return (
<>
<SupportBar />
<Card>
<SupportBar />
</Card>
{renderBoard()}
</>
);

View file

@ -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 = () => <LoadingCard noCard={true} loadingHeight={'30vh'} />;
const Dashboard = dynamic(() => import('src/views/settings/DashPanel'), {
ssr: false,
loading: LoadingComp,
});
const Wrapped = () => (
<GridWrapper noNavigation={true}>
<Dashboard />
</GridWrapper>
);
export default Wrapped;
export async function getServerSideProps(context: NextPageContext) {
return await getProps(context);
}

View file

@ -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 (
<>
<InterfaceSettings />
<DashboardSettings />
<PrivacySettings />
<ChatSettings />
<DangerView />

View file

@ -61,20 +61,15 @@ const TransactionsView = () => {
const [open, setOpen] = useState<boolean>(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 <LoadingCard title={'Transactions'} />;
return (
<>
<FlowBox />
<LoadingCard title={'Transactions'} />
</>
);
}
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 (
<>

View file

@ -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<any>();
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<string>({
domain: data.map(getDate),
padding: 0.2,
});
const barScale = scaleBand<string>({
domain: keys,
padding: 0.1,
});
const yScale = scaleLinear<number>({
domain: [
0,
Math.max(...data.map(d => Math.max(...keys.map(key => Number(d[key]))))),
],
});
const colorScale = scaleOrdinal<string, string>({
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 (
<div style={{ position: 'relative' }}>
<svg width={width} height={height}>
<Group top={margin.top} left={margin.left}>
<BarGroup
data={data}
keys={keys}
height={yMax}
x0={getDate}
x0Scale={xScale}
x1Scale={barScale}
yScale={yScale}
color={colorScale}
>
{barGroups =>
barGroups.map(barGroup => (
<Group
key={`bar-group-${barGroup.index}-${barGroup.x0}`}
left={barGroup.x0}
>
{barGroup.bars.map(bar => (
<rect
key={`bar-group-bar-${barGroup.index}-${bar.index}-${bar.value}-${bar.key}`}
x={bar.x}
y={bar.y}
width={bar.width}
height={Math.abs(bar.height)}
fill={bar.color}
onMouseOver={(e: any) => {
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);
}}
/>
))}
</Group>
))
}
</BarGroup>
</Group>
<AxisLeft
numTicks={amountOfYTicks(height)}
left={width - margin.left}
top={margin.bottom}
hideZero={true}
scale={yScale}
stroke={axisColor}
tickStroke={axisColor}
tickLabelProps={() => ({
fill: axisColor,
fontSize: 11,
textAnchor: 'end',
dy: '0.33em',
dx: '-0.33em',
})}
/>
<AxisBottom
numTicks={amountOfXTicks(width)}
top={yMax + margin.top}
tickFormat={formatDate}
scale={xScale}
stroke={axisColor}
tickStroke={axisColor}
tickLabelProps={() => ({
fill: axisColor,
fontSize: 11,
textAnchor: 'middle',
})}
/>
</svg>
{tooltipOpen && tooltipData ? (
<TooltipWithBounds
top={tooltipTop}
left={tooltipLeft}
style={tooltipStyles}
>
<div style={{ color: colorScale(tooltipData.key) }}>
<strong>{tooltipData.key}</strong>
</div>
{priceLabel ? (
<Price amount={tooltipData.value} />
) : (
tooltipData.value
)}
</TooltipWithBounds>
) : null}
</div>
);
};
export const BarChart = (props: BarChartProps) => (
<ParentSize>
{parent => <Chart width={parent.width} height={parent.height} {...props} />}
</ParentSize>
);

View file

@ -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<any>();
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<string>({
domain: data.map(getLabel),
});
const barScale = scaleBand<string>({
domain: keys,
padding: 0.1,
});
const xScale = scaleLinear<number>({
domain: [0, maxValue + 0.1 * maxValue],
});
const colorScale = scaleOrdinal<string, string>({
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 (
<div style={{ position: 'relative' }}>
<svg width={width} height={height}>
<Group top={margin.top} left={margin.left}>
<BarGroupHorizontal
data={data}
keys={keys}
width={xMax}
y0={getLabel}
y0Scale={yScale}
y1Scale={barScale}
xScale={xScale}
color={colorScale}
>
{barGroups =>
barGroups.map(barGroup => (
<Group
key={`bar-group-${barGroup.index}-${barGroup.y0}`}
top={barGroup.y0}
>
{barGroup.bars.map(bar => (
<Bar
key={`bar-group-bar-${barGroup.index}-${bar.index}-${bar.value}-${bar.key}`}
x={bar.x}
y={bar.y}
width={bar.width < 10 ? 10 : bar.width}
height={Math.abs(bar.height)}
fill={bar.color}
onMouseOver={(e: any) => {
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);
}}
/>
))}
</Group>
))
}
</BarGroupHorizontal>
</Group>
<AxisLeft
left={width - margin.left}
top={margin.bottom}
hideZero={true}
scale={yScale}
stroke={axisColor}
tickStroke={axisColor}
tickLabelProps={() => ({
fill: axisColor,
fontSize: 11,
textAnchor: 'end',
dy: '0.33em',
dx: '-0.33em',
})}
/>
</svg>
{tooltipOpen && tooltipData ? (
<TooltipWithBounds
top={tooltipTop}
left={tooltipLeft}
style={tooltipStyles}
>
<div style={{ color: colorScale(tooltipData.key) }}>
<strong>{tooltipData.key}</strong>
</div>
{priceLabel ? (
<Price amount={tooltipData.value} />
) : (
tooltipData.value
)}
</TooltipWithBounds>
) : null}
</div>
);
};
export const HorizontalBarChart = (props: BarChartProps) => (
<ParentSize>
{parent => <Chart width={parent.width} height={parent.height} {...props} />}
</ParentSize>
);

View file

@ -46,3 +46,12 @@ export const GridWrapper: React.FC<GridProps> = ({
</Container>
</Section>
);
export const SimpleWrapper: React.FC<GridProps> = ({ children }) => (
<Section fixedWidth={false} padding={'16px'}>
<BitcoinPrice />
<BitcoinFees />
<StatusCheck />
{children}
</Section>
);

View file

@ -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;

View file

@ -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}
/>
</StyledWrapper>
);
};
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 (
<StyledWrapper maxWidth={maxWidth} fullWidth={true}>
<StyledSmallSelect
isMulti={isMulti}
classNamePrefix={'Select'}
options={options}
onChange={handleChange}
value={value || null}
isClearable={isClearable}
/>
</StyledWrapper>
);

View file

@ -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 }) => (
<PriceProvider>
<ChatProvider>
<RebalanceProvider>{children}</RebalanceProvider>
</ChatProvider>
</PriceProvider>
<DashProvider>
<PriceProvider>
<ChatProvider>
<RebalanceProvider>{children}</RebalanceProvider>
</ChatProvider>
</PriceProvider>
</DashProvider>
);

View file

@ -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<State | undefined>(undefined);
export const DispatchContext = createContext<Dispatch | undefined>(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 (
<DispatchContext.Provider value={dispatch}>
<StateContext.Provider value={state}>{children}</StateContext.Provider>
</DispatchContext.Provider>
);
};
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 };

View file

@ -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<boolean>(false);
const [connected, setCanConnect] = useState<boolean>(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 };
};

View file

@ -0,0 +1,40 @@
import { RefObject, useState, useEffect, useCallback } from 'react';
import useEventListener from './UseEventListener';
interface Size {
width: number;
height: number;
}
function useElementSize<T extends HTMLElement = HTMLDivElement>(
elementRef: RefObject<T>
): Size {
const [size, setSize] = useState<Size>({
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;

View file

@ -0,0 +1,39 @@
import { useRef, useEffect, RefObject } from 'react';
function useEventListener<T extends HTMLElement = HTMLDivElement>(
eventName: string,
handler: (event: Event) => void,
element?: RefObject<T>
) {
// 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;

View file

@ -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;

View file

@ -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<NavProps>`
`;
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 = () => (
<ButtonSection isOpen={sidebar}>
{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)}
</ButtonSection>
);
const renderBurger = () => (
<BurgerRow>
{renderBurgerNav('Home', HOME, Home)}
{renderBurgerNav('Dashboard', DASHBOARD, Grid)}
{renderBurgerNav('Peers', PEERS, Users)}
{renderBurgerNav('Channels', CHANNEL, Cpu)}
{renderBurgerNav('Rebalance', REBALANCE, Repeat)}

View file

@ -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],
};

View file

@ -29,7 +29,7 @@ const S = {
};
export const ChannelBosScore: FC<{ score?: BosScore | null }> = ({ score }) => {
const connected = useBaseConnect();
const { connected } = useBaseConnect();
if (!connected) return null;

View file

@ -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 (
<Card mobileCardPadding={'0'} mobileNoBackground={true}>
<>
{getChannels().map((channel, index) => (
<ChannelCard
channelInfo={channel as ChannelType}
@ -182,6 +181,6 @@ export const Channels: React.FC = () => {
biggestRateFee={Math.max(Math.min(biggestRateFee, 10000), 2000)}
/>
))}
</Card>
</>
);
};

View file

@ -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>('layouts', {});
const [availableWidgets] = useLocalStorage<StoredWidget[]>(
'dashboardWidgets',
[]
);
const props = {
isBounded: true,
};
const handleChange = (_: any, layouts: any) => {
setLayouts(layouts);
};
const widgets = getWidgets(availableWidgets, width, [{ id: 28 }]);
if (!widgets.length) {
return (
<S.fill>
<SubTitle>No Widgets Enabled!</SubTitle>
<Link href={'settings/dashboard'}>
<ColorButton arrow={true}>Settings</ColorButton>
</Link>
</S.fill>
);
}
const renderContent = () => {
if (width === 0) {
return <LoadingCard noCard={true} loadingHeight={'90vh'} />;
}
return (
<>
<S.styles>
<ResponsiveGridLayout
{...props}
className="layout"
layouts={layouts}
rowHeight={28}
width={width}
margin={defaultGrid.margin}
breakpoints={defaultGrid.breakpoints}
cols={defaultGrid.columns}
onLayoutChange={handleChange}
>
{widgets.map(w => (
<S.card widgetColor={w.color} key={w.id} data-grid={w.default}>
<w.component />
</S.card>
))}
</ResponsiveGridLayout>
</S.styles>
<Modal
isOpen={!!modalType}
closeCallback={() => dispatch({ type: 'openModal', modalType: '' })}
>
<DashboardModal />
</Modal>
</>
);
};
return <S.gridWrapper ref={wrapperRef}>{renderContent()}</S.gridWrapper>;
};
export default Dashboard;

View file

@ -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 (
<PayCard
setOpen={() => dispatch({ type: 'openModal', modalType: '' })}
/>
);
case 'createInvoice':
return <CreateInvoiceCard color={'#FFD300'} />;
case 'sendChain':
return (
<SendOnChainCard
setOpen={() => dispatch({ type: 'openModal', modalType: '' })}
/>
);
case 'receiveChain':
return <ReceiveOnChainCard />;
case 'openChannel':
return (
<OpenChannel
setOpenCard={() => dispatch({ type: 'openModal', modalType: '' })}
/>
);
case 'donate':
return <SupportBar />;
case 'signMessage':
return <SignMessage />;
default:
return null;
}
};
return renderModal();
};

View file

@ -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 (
<S.wrapper>
<Table alignCenter={true} tableColumns={columns} tableData={data} />
</S.wrapper>
);
};

View file

@ -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;
};

View file

@ -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 (
<S.wrapper>
<S.pending>Total Balance</S.pending>
<S.total>
<Price amount={total} />
</S.total>
{pending > 0 ? (
<S.pending>
<Price amount={pending} />
</S.pending>
) : null}
</S.wrapper>
);
};
export const ChannelBalance = () => {
const { channelBalance, channelPending } = useNodeInfo();
return (
<S.wrapper>
<S.pending>Channel Balance</S.pending>
<S.smallTotal>
<Price amount={channelBalance} />
</S.smallTotal>
{channelPending > 0 ? (
<S.pending>
<Price amount={channelPending} />
</S.pending>
) : null}
</S.wrapper>
);
};
export const ChainBalance = () => {
const { chainBalance, chainPending } = useNodeInfo();
return (
<S.wrapper>
<S.pending>Chain Balance</S.pending>
<S.smallTotal>
<Price amount={chainBalance} />
</S.smallTotal>
{chainPending > 0 ? (
<S.pending>
<Price amount={chainPending} />
</S.pending>
) : null}
</S.wrapper>
);
};

View file

@ -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 (
<S.wrapper>
<Channels />
</S.wrapper>
);
};

View file

@ -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: <S.nowrap>{getDateDif(f.created_at)}</S.nowrap>,
amount: (
<S.nowrap>
<Price amount={f.tokens} />
</S.nowrap>
),
fee: (
<S.nowrap>
<Price amount={f.fee} />
</S.nowrap>
),
incoming: <ChannelAlias id={f.incoming_channel} />,
outgoing: <ChannelAlias id={f.outgoing_channel} />,
},
];
}, [] as any);
return (
<S.wrapper>
<S.title>Forwards</S.title>
<S.table>
<Table tableColumns={columns} tableData={tableData} />
</S.table>
</S.wrapper>
);
};

View file

@ -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 = () => (
<S.row>
<S.title>Forwards</S.title>
<SmallSelectWithValue
callback={e => setDays((e[0] || options[1]) as any)}
options={options}
value={days}
isClearable={false}
maxWidth={'60px'}
/>
<SmallSelectWithValue
callback={e => setType((e[0] || typeOptions[1]) as any)}
options={typeOptions}
value={type}
isClearable={false}
maxWidth={'90px'}
/>
</S.row>
);
if (loading) {
return (
<S.wrapper>
<Header />
<S.contentWrapper>
<LoadingCard noCard={true} />
</S.contentWrapper>
</S.wrapper>
);
}
if (!data?.getForwards.length) {
return (
<S.wrapper>
<Header />
<S.contentWrapper>No forwards for this period.</S.contentWrapper>
</S.wrapper>
);
}
const forwards = getByTime(data.getForwards, days.value);
return (
<S.wrapper>
<Header />
<S.content>
<BarChart
priceLabel={type.value !== 'amount'}
data={forwards.map(f => ({
Forward: f[type.value] || 0,
date: f.date,
}))}
colorRange={[chartColors.purple]}
/>
</S.content>
</S.wrapper>
);
};

View file

@ -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 (
<S.wrapper>
<S.title>{alias}</S.title>
</S.wrapper>
);
};
export const BalanceWidget = () => {
const { data } = useGetLiquidReportQuery({ errorPolicy: 'ignore' });
if (!data?.getChannelReport) {
return (
<S.wrapper>
<S.title>-</S.title>
</S.wrapper>
);
}
const { local, remote } = data.getChannelReport;
const balance = Math.round(((local || 0) / (remote || 1)) * 100);
return (
<S.wrapper>
<S.title>{`${balance}%`}</S.title>
</S.wrapper>
);
};

View file

@ -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 = () => (
<S.row>
<S.title>Invoices</S.title>
<SmallSelectWithValue
callback={e => setDays((e[0] || options[1]) as any)}
options={options}
value={days}
isClearable={false}
maxWidth={'60px'}
/>
<SmallSelectWithValue
callback={e => setType((e[0] || typeOptions[1]) as any)}
options={typeOptions}
value={type}
isClearable={false}
maxWidth={'90px'}
/>
</S.row>
);
if (loading) {
return (
<S.wrapper>
<Header />
<S.contentWrapper>
<LoadingCard noCard={true} />
</S.contentWrapper>
</S.wrapper>
);
}
if (!resume.length) {
return (
<S.wrapper>
<Header />
<S.contentWrapper>No invoices for this period.</S.contentWrapper>
</S.wrapper>
);
}
return (
<S.wrapper>
<Header />
<S.content>
<BarChart
priceLabel={type.value !== 'amount'}
data={invoicesByDate.map(f => {
return {
Invoices: f?.[type.value] || 0,
date: f.date,
};
})}
colorRange={[chartColors.orange2]}
/>
</S.content>
</S.wrapper>
);
};

View file

@ -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 (
<S.wrapper>
<S.title>Liquidity</S.title>
<S.contentWrapper>
<LoadingCard noCard={true} />
</S.contentWrapper>
</S.wrapper>
);
}
if (!data?.getChannelReport) {
return (
<S.wrapper>
<S.title>Liquidity</S.title>
<S.contentWrapper>No invoices for this period.</S.contentWrapper>
</S.wrapper>
);
}
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 (
<S.wrapper>
<HorizontalBarChart
priceLabel={true}
data={liquidity}
colorRange={[chartColors.green]}
/>
</S.wrapper>
);
};

View file

@ -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 (
<ColorButton
fullWidth={true}
onClick={() => dispatch({ type: 'openModal', modalType: 'payInvoice' })}
>
Pay Invoice
</ColorButton>
);
};
export const CreateInvoice = () => {
const dispatch = useDashDispatch();
return (
<ColorButton
fullWidth={true}
onClick={() =>
dispatch({ type: 'openModal', modalType: 'createInvoice' })
}
>
Create Invoice
</ColorButton>
);
};
export const SendOnChain = () => {
const dispatch = useDashDispatch();
return (
<ColorButton
fullWidth={true}
onClick={() => dispatch({ type: 'openModal', modalType: 'sendChain' })}
>
Send Bitcoin
</ColorButton>
);
};
export const ReceiveOnChain = () => {
const dispatch = useDashDispatch();
return (
<ColorButton
fullWidth={true}
onClick={() => dispatch({ type: 'openModal', modalType: 'receiveChain' })}
>
Receive Bitcoin
</ColorButton>
);
};
export const OpenChannel = () => {
const dispatch = useDashDispatch();
return (
<ColorButton
fullWidth={true}
onClick={() => dispatch({ type: 'openModal', modalType: 'openChannel' })}
>
Open Channel
</ColorButton>
);
};

View file

@ -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 = () => (
<S.row>
<S.title>Payments</S.title>
<SmallSelectWithValue
callback={e => setDays((e[0] || options[1]) as any)}
options={options}
value={days}
isClearable={false}
maxWidth={'60px'}
/>
<SmallSelectWithValue
callback={e => setType((e[0] || typeOptions[1]) as any)}
options={typeOptions}
value={type}
isClearable={false}
maxWidth={'90px'}
/>
</S.row>
);
if (loading) {
return (
<S.wrapper>
<Header />
<S.contentWrapper>
<LoadingCard noCard={true} />
</S.contentWrapper>
</S.wrapper>
);
}
if (!resume.length) {
return (
<S.wrapper>
<Header />
<S.contentWrapper>No payments for this period.</S.contentWrapper>
</S.wrapper>
);
}
return (
<S.wrapper>
<Header />
<S.content>
<BarChart
priceLabel={type.value !== 'amount'}
data={paymentsByDate.map(f => {
return {
Payments: f?.[type.value] || 0,
date: f.date,
};
})}
colorRange={[chartColors.darkyellow]}
/>
</S.content>
</S.wrapper>
);
};

View file

@ -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: <ArrowDown size={14} color={chartColors.green} />,
value: (
<S.nowrap>
<Price amount={c.received} />
</S.nowrap>
),
date: <S.nowrap>{getDateDif(c.confirmed_at)}</S.nowrap>,
info: <S.nowrap>{c.description || c.description_hash}</S.nowrap>,
},
];
}
if (c.__typename === 'PaymentType') {
if (!c.is_confirmed) return p;
return [
...p,
{
type: <ArrowUp size={14} color={chartColors.red} />,
value: (
<S.nowrap>
<Price amount={c.tokens} />
</S.nowrap>
),
date: <S.nowrap>{getDateDif(c.created_at)}</S.nowrap>,
info: (
<S.nowrap>
{c.destination_node
? `Payment to ${c.destination_node.node.alias}`
: `Payment to ${shorten(c.destination)}`}
</S.nowrap>
),
},
];
}
}, [] as any);
return (
<S.wrapper>
<S.title>Transactions</S.title>
<S.table>
<Table tableColumns={columns} tableData={normalized} />
</S.table>
</S.wrapper>
);
};

View file

@ -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 = () => (
<S.row>
<S.title>Transactions</S.title>
<SmallSelectWithValue
callback={e => setDays((e[0] || options[1]) as any)}
options={options}
value={days}
isClearable={false}
maxWidth={'60px'}
/>
<SmallSelectWithValue
callback={e => setType((e[0] || typeOptions[1]) as any)}
options={typeOptions}
value={type}
isClearable={false}
maxWidth={'90px'}
/>
</S.row>
);
if (loading) {
return (
<S.wrapper>
<Header />
<S.contentWrapper>
<LoadingCard noCard={true} />
</S.contentWrapper>
</S.wrapper>
);
}
if (!resume.length) {
return (
<S.wrapper>
<Header />
<S.contentWrapper>No transactions for this period.</S.contentWrapper>
</S.wrapper>
);
}
return (
<S.wrapper>
<Header />
<S.content>
<BarChart
priceLabel={type.value !== 'amount'}
data={invoicesByDate.map(f => {
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]}
/>
</S.content>
</S.wrapper>
);
};

View file

@ -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 (
<S.wrapper>
<Link href={'/settings/dashboard'}>
<ColorButton fullWidth={true}>Dash Settings</ColorButton>
</Link>
</S.wrapper>
);
};
export const ForwardsViewLink = () => {
return (
<S.wrapper>
<Link href={'/forwards'}>
<ColorButton fullWidth={true}>Forwards</ColorButton>
</Link>
</S.wrapper>
);
};
export const TransactionsViewLink = () => {
return (
<S.wrapper>
<Link href={'/transactions'}>
<ColorButton fullWidth={true}>Transactions</ColorButton>
</Link>
</S.wrapper>
);
};
export const ChannelViewLink = () => {
return (
<S.wrapper>
<Link href={'/channels'}>
<ColorButton fullWidth={true}>Channels</ColorButton>
</Link>
</S.wrapper>
);
};
export const RebalanceViewLink = () => {
return (
<S.wrapper>
<Link href={'/rebalance'}>
<ColorButton fullWidth={true}>Rebalance</ColorButton>
</Link>
</S.wrapper>
);
};

View file

@ -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 (
<S.wrapper>
<SingleButton
selected={theme === 'light'}
onClick={() => handleDispatch('light')}
>
<Sun size={16} />
</SingleButton>
<SingleButton
selected={theme === 'dark'}
onClick={() => handleDispatch('dark')}
>
<Moon size={16} />
</SingleButton>
<SingleButton
selected={theme === 'night'}
onClick={() => handleDispatch('night')}
>
<Star size={16} />
</SingleButton>
</S.wrapper>
);
};
export const CurrencySetting = () => {
const { currency } = useConfigState();
const dispatch = useConfigDispatch();
const handleDispatch = (currency: string) =>
dispatch({ type: 'change', currency });
return (
<S.wrapper>
<SingleButton
selected={currency === 'sat'}
onClick={() => handleDispatch('sat')}
>
Sat
</SingleButton>
<SingleButton
selected={currency === 'btc'}
onClick={() => handleDispatch('btc')}
>
Btc
</SingleButton>
<SingleButton
selected={currency === 'fiat'}
onClick={() => handleDispatch('fiat')}
>
Fiat
</SingleButton>
</S.wrapper>
);
};

View file

@ -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 (
<S.contentWrapper>
Fetching fiat prices is disabled. Enable it in the settings.
</S.contentWrapper>
);
}
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 (
<S.wrapper>
<S.row>
<div>{getValue(firstAmount, first.value)}</div>
<Input
fullWidth={true}
placeholder={'Amount'}
value={firstAmount}
type={'number'}
onChange={e => {
const value = Number(e.target.value);
setFirstAmount(value);
setSecondAmount(convert(first.value, second.value, value));
}}
/>
<SelectWithValue
callback={e => {
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'}
/>
<div>{getValue(secondAmount, second.value)}</div>
<Input
fullWidth={true}
placeholder={'Amount'}
value={secondAmount}
type={'number'}
onChange={e => {
const value = Number(e.target.value);
setSecondAmount(value);
setFirstAmount(convert(second.value, first.value, value));
}}
/>
<SelectWithValue
callback={e => {
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'}
/>
</S.row>
</S.wrapper>
);
};

View file

@ -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 (
<S.wrapper>
<ColorButton
fullWidth={true}
onClick={() => dispatch({ type: 'openModal', modalType: 'donate' })}
>
<S.row>
<Heart size={18} />
<S.title>Donate</S.title>
</S.row>
</ColorButton>
</S.wrapper>
);
};

View file

@ -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 (
<S.wrapper>
<ColorButton
fullWidth={true}
onClick={() =>
dispatch({ type: 'openModal', modalType: 'signMessage' })
}
>
Sign Message
</ColorButton>
</S.wrapper>
);
};

View file

@ -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 },
},
];

View file

@ -50,12 +50,8 @@ const sectionColor = '#FFD300';
export const AccountInfo = () => {
const [state, setState] = useState<string>('none');
const {
chainBalance,
chainPending,
channelBalance,
channelPending,
} = useNodeInfo();
const { chainBalance, chainPending, channelBalance, channelPending } =
useNodeInfo();
const renderContent = () => {
switch (state) {

View file

@ -80,7 +80,11 @@ export const QuickActions = () => {
const renderContent = () => {
switch (openCard) {
case 'support':
return <SupportBar />;
return (
<Card>
<SupportBar />
</Card>
);
case 'decode':
return <DecodeCard />;
case 'ln_url':

View file

@ -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 (
<QuickCard onClick={callback}>
<Heart size={24} />

View file

@ -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<string>('');
const [id, idSet] = React.useState<string>('');
const connected = useBaseConnect();
const { connected } = useBaseConnect();
const [withPoints, setWithPoints] = React.useState<boolean>(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 (
<div style={{ textAlign: 'center' }}>
<SubTitle>Unable to connect to donation server.</SubTitle>
<Sub4Title>
Please check back later.Thanks for wanting to donate
<Emoji symbol={'❤️'} label={'heart'} />
</Sub4Title>
</div>
);
const handleReset = () => {
modalOpenSet(false);
@ -102,56 +104,54 @@ export const SupportBar = () => {
return (
<>
<Card>
<div style={{ textAlign: 'center' }}>
<SubTitle>This project is completely free and open-source.</SubTitle>
<Sub4Title>
If you have enjoyed it, please consider supporting ThunderHub with
some sats <Emoji symbol={'❤️'} label={'heart'} />
</Sub4Title>
</div>
<Separation />
<InputWithDeco
title={'Amount'}
value={amount}
amount={amount}
inputType={'number'}
inputCallback={value => amountSet(Number(value))}
/>
<Separation />
<InputWithDeco title={'With Points'} noInput={true}>
<MultiButton>
{renderButton(() => setWithPoints(true), 'Yes', withPoints)}
{renderButton(() => setWithPoints(false), 'No', !withPoints)}
</MultiButton>
</InputWithDeco>
{withPoints && (
<>
<StyledText>
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.
</StyledText>
<Warning>
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.
</Warning>
</>
)}
<Separation />
<ColorButton
onClick={() => getInvoice({ variables: { amount } })}
loading={loading}
disabled={amount <= 0 || loading}
fullWidth={true}
withMargin={'8px 0 0 0'}
>
Send
</ColorButton>
</Card>
<div style={{ textAlign: 'center' }}>
<SubTitle>This project is completely free and open-source.</SubTitle>
<Sub4Title>
If you have enjoyed it, please consider supporting ThunderHub with
some sats <Emoji symbol={'❤️'} label={'heart'} />
</Sub4Title>
</div>
<Separation />
<InputWithDeco
title={'Amount'}
value={amount}
amount={amount}
inputType={'number'}
inputCallback={value => amountSet(Number(value))}
/>
<Separation />
<InputWithDeco title={'With Points'} noInput={true}>
<MultiButton>
{renderButton(() => setWithPoints(true), 'Yes', withPoints)}
{renderButton(() => setWithPoints(false), 'No', !withPoints)}
</MultiButton>
</InputWithDeco>
{withPoints && (
<>
<StyledText>
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.
</StyledText>
<Warning>
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.
</Warning>
</>
)}
<Separation />
<ColorButton
onClick={() => getInvoice({ variables: { amount } })}
loading={loading}
disabled={amount <= 0 || loading}
fullWidth={true}
withMargin={'8px 0 0 0'}
>
Send
</ColorButton>
<Modal isOpen={modalOpen} closeCallback={handleReset}>
<Pay predefinedRequest={invoice} payCallback={handlePaidReset} />
</Modal>

View file

@ -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>('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 (
<Card>
{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 (
<Fragment key={key + index + subIndex}>
<S.subTitle>{subKey ? `${key} - ${subKey}` : key}</S.subTitle>
{subWidgets.map(w => (
<Fragment key={w.id}>
<WidgetRow
widget={w}
handleAdd={handleAdd}
handleDelete={handleDelete}
/>
</Fragment>
))}
</Fragment>
);
});
})}
<Link href={'/dashboard'}>
<ColorButton withMargin={'16px 0 0'} width={'100%'} arrow={true}>
To Dashboard
</ColorButton>
</Link>
<ColorButton
withMargin={'8px 0 0'}
width={'100%'}
onClick={() => {
setLayouts({});
setAvailableWidgets([]);
}}
>
Reset Widgets
</ColorButton>
</Card>
);
};
export default DashPanel;

View file

@ -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 (
<CardWithTitle>
<SubTitle>Dashboard</SubTitle>
<Card>
<SettingsLine>
<Sub4Title>Widgets</Sub4Title>
<ColorButton arrow={true} onClick={() => push('/settings/dashboard')}>
Change
</ColorButton>
</SettingsLine>
</Card>
</CardWithTitle>
);
};

View file

@ -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<WidgetRowParams> = ({
widget,
handleAdd,
handleDelete,
}) => (
<S.line>
<DarkSubTitle>{widget.name}</DarkSubTitle>
<MultiButton>
<SingleButton
selected={widget.active}
onClick={() => handleAdd(widget.id)}
>
Show
</SingleButton>
<SingleButton
selected={!widget.active}
onClick={() => handleDelete(widget.id)}
>
Hide
</SingleButton>
</MultiButton>
</S.line>
);

View file

@ -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 = () => {
<SubTitle>Messages</SubTitle>
<Card>
<VerifyMessage />
<SignMessage />
<SignMessageCard />
</Card>
</CardWithTitle>
);

View file

@ -16,7 +16,6 @@ import { NoWrap } from './Messages';
export const SignMessage = () => {
const [message, setMessage] = useState<string>('');
const [isPasting, setIsPasting] = useState<boolean>(false);
const [signed, setSigned] = useState<string>('');
const [signMessage, { data, loading }] = useSignMessageLazyQuery({
@ -49,40 +48,51 @@ export const SignMessage = () => {
>
Sign
</ColorButton>
<Separation />
</>
);
const renderMessage = () => (
<Column>
<WrapRequest>{signed}</WrapRequest>
<CopyToClipboard
text={signed}
onCopy={() => toast.success('Signature Copied')}
>
<ColorButton>
<Copy size={18} />
Copy
</ColorButton>
</CopyToClipboard>
</Column>
<>
<Separation />
<Column>
<WrapRequest>{signed}</WrapRequest>
<CopyToClipboard
text={signed}
onCopy={() => toast.success('Signature Copied')}
>
<ColorButton>
<Copy size={18} />
Copy
</ColorButton>
</CopyToClipboard>
</Column>
</>
);
return (
<>
{renderInput()}
{signed !== '' && renderMessage()}
</>
);
};
export const SignMessageCard = () => {
const [isPasting, setIsPasting] = useState<boolean>(false);
return (
<>
<SingleLine>
<DarkSubTitle>Sign Message</DarkSubTitle>
<ColorButton
withMargin={'4px 0'}
disabled={loading}
arrow={!isPasting}
onClick={() => setIsPasting(prev => !prev)}
>
{isPasting ? <X size={18} /> : 'Sign'}
</ColorButton>
</SingleLine>
{isPasting && renderInput()}
{signed !== '' && isPasting && renderMessage()}
{isPasting && <SignMessage />}
</>
);
};