diff --git a/.vscode/settings.json b/.vscode/settings.json index f2b1e2a..43a153a 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -19,11 +19,13 @@ "arraybuffer", "authorised", "clientside", + "commonmeta", "dont", "firstseen", "jwalelset", "languagedetector", "localisation", + "metaname", "middot", "motd", "multiline", @@ -34,6 +36,7 @@ "singleline", "submenu", "testid", + "timeago", "totalin", "totalout", "tsdoc", diff --git a/craco.config.js b/craco.config.js index b3383f1..76218fe 100644 --- a/craco.config.js +++ b/craco.config.js @@ -1,4 +1,6 @@ const CracoLessPlugin = require("craco-less"); +const { BundleAnalyzerPlugin } = require("webpack-bundle-analyzer"); +const WebpackBar = require("webpackbar"); module.exports = { style: { @@ -32,5 +34,14 @@ // TODO: run this for production builds, and add a separate command for it. eslint: { enable: false - } + }, + + webpack: { + plugins: [ + new WebpackBar({ profile: true }), + ...(process.env.NODE_ENV === "development" || process.env.FORCE_ANALYZE + ? [new BundleAnalyzerPlugin({ openAnalyzer: false })] + : []), + ], + }, }; diff --git a/package.json b/package.json index aed7805..7477ebc 100644 --- a/package.json +++ b/package.json @@ -34,6 +34,7 @@ "react-i18next": "^11.8.6", "react-redux": "^7.2.2", "react-router-dom": "^5.2.0", + "react-timeago": "^5.2.0", "react-world-flags": "^1.4.0", "redux": "^4.0.5", "semver": "^7.3.4", @@ -46,6 +47,8 @@ "scripts": { "start": "craco start", "build": "craco build", + "optimise": "gzip -kr build/static", + "full-build": "npm run build; npm run optimise", "test": "craco test" }, "eslintConfig": { @@ -74,6 +77,7 @@ "@types/react-dom": "^17.0.0", "@types/react-redux": "^7.1.16", "@types/react-router-dom": "^5.1.7", + "@types/react-timeago": "^4.1.2", "@types/semver": "^7.3.4", "@types/uuid": "^8.3.0", "@types/webpack-env": "^1.16.0", @@ -89,7 +93,9 @@ "react-scripts": "4.0.2", "redux-devtools-extension": "^2.13.8", "typescript": "^4.1.5", - "utility-types": "^3.10.0" + "utility-types": "^3.10.0", + "webpack-bundle-analyzer": "^4.4.0", + "webpackbar": "^5.0.0-3" }, "stylelint": { "extends": "stylelint-config-recommended", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9c1b819..b47f8e6 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -18,6 +18,7 @@ react-i18next: 11.8.6_i18next@19.8.7+react@17.0.1 react-redux: 7.2.2_380dc38591053d98779d1f25fc7202b9 react-router-dom: 5.2.0_react@17.0.1 + react-timeago: 5.2.0_react@17.0.1 react-world-flags: 1.4.0_react@17.0.1 redux: 4.0.5 semver: 7.3.4 @@ -37,6 +38,7 @@ '@types/react-dom': 17.0.1 '@types/react-redux': 7.1.16 '@types/react-router-dom': 5.1.7 + '@types/react-timeago': 4.1.2 '@types/semver': 7.3.4 '@types/uuid': 8.3.0 '@types/webpack-env': 1.16.0 @@ -53,6 +55,8 @@ redux-devtools-extension: 2.13.8_redux@4.0.5 typescript: 4.1.5 utility-types: 3.10.0 + webpack-bundle-analyzer: 4.4.0 + webpackbar: 5.0.0-3 lockfileVersion: 5.2 packages: /@ant-design/colors/6.0.0: @@ -226,7 +230,7 @@ integrity: sha512-zYoZC1uvebBFmj1wFAlXwt35JLEgecefATtKp20xalwEK8vHAixLBXTGxNrVGEmTT+gzOThUgr8UEdgtalc1BQ== /@babel/helper-module-imports/7.12.13: dependencies: - '@babel/types': 7.12.13 + '@babel/types': 7.12.17 dev: true resolution: integrity: sha512-NGmfvRp9Rqxy0uHSSVP+SRIW1q31a7Ji10cLBcqSDUngGentY4FRiHOFZFE1CLU5eiL0oE8reH7Tg1y99TDM/g== @@ -1344,6 +1348,14 @@ dev: true resolution: integrity: sha512-oKrdZTld2im1z8bDwTOQvUbxKwE+854zc16qWZQlcTqMN00pWxHQ4ZeOq0yDMnisOpRykH2/5Qqcrk/OlbAjiQ== + /@babel/types/7.12.17: + dependencies: + '@babel/helper-validator-identifier': 7.12.11 + lodash: 4.17.21 + to-fast-properties: 2.0.0 + dev: true + resolution: + integrity: sha512-tNMDjcv/4DIcHxErTgwB9q2ZcYyN0sUfgGKUK/mm1FJK7Wz+KstoEekxrl/tBiNDgLK1HGi+sppj1An/1DR4fQ== /@bcoe/v8-coverage/0.2.3: dev: true resolution: @@ -1721,6 +1733,10 @@ optional: true resolution: integrity: sha512-br5Qwvh8D2OQqSXpd1g/xqXKnK0r+Jz6qVKBbWmpUcrbGOxUrf39V5oZ1876084CGn18uMdR5uvPqBv9UqtBjQ== + /@polka/url/1.0.0-next.11: + dev: true + resolution: + integrity: sha512-3NsZsJIA/22P3QUyrEDNA2D133H4j224twJrdipXN38dpnIOzAbUDtOwkcJ5pXmn75w7LSQDjA4tO9dm1XlqlA== /@rollup/plugin-node-resolve/7.1.3_rollup@1.32.1: dependencies: '@rollup/pluginutils': 3.1.0_rollup@1.32.1 @@ -2138,6 +2154,12 @@ dev: true resolution: integrity: sha512-ofHbZMlp0Y2baOHgsWBQ4K3AttxY61bDMkwTiBOkPg7U6C/3UwwB5WaIx28JmSVi/eX3uFEMRo61BV22fDQIvg== + /@types/react-timeago/4.1.2: + dependencies: + '@types/react': 17.0.2 + dev: true + resolution: + integrity: sha512-gkhU3rH7aZgeRybbm9ie9wHOM9i1I5YhUoto/uqY/DAbeRZuLU8ugl6E97jp65XCl9QTij32Vs7BAX2E/MqOAw== /@types/react/17.0.2: dependencies: '@types/prop-types': 15.7.3 @@ -2539,6 +2561,12 @@ node: '>=0.4.0' resolution: integrity: sha512-OPdCF6GsMIP+Az+aWfAAOEt2/+iVDKE7oy6lJ098aoe59oAmK76qV6Gw60SbZ8jHuG2wH058GF4pLFbYamYrVA== + /acorn-walk/8.0.2: + dev: true + engines: + node: '>=0.4.0' + resolution: + integrity: sha512-+bpA9MJsHdZ4bgfDcpk0ozQyhhVct7rzOmO0s1IIr0AGGgKBljss8n2zp11rRP2wid5VGeh04CgeKzgat5/25A== /acorn/6.4.2: dev: true engines: @@ -2553,6 +2581,13 @@ hasBin: true resolution: integrity: sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A== + /acorn/8.0.5: + dev: true + engines: + node: '>=0.4.0' + hasBin: true + resolution: + integrity: sha512-v+DieK/HJkJOpFBETDJioequtc3PfxsWMaxIdIwujtF7FEV/MAyDQLlm6/zPvr7Mix07mLh6ccVwIsloceodlg== /address/1.1.2: dev: true engines: @@ -3683,6 +3718,10 @@ dev: true resolution: integrity: sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ== + /ci-info/3.1.1: + dev: true + resolution: + integrity: sha512-kdRWLBIJwdsYJWYJFtAFFYxybguqeF91qpZaggjG5Nf8QKdizFG2hjqvaTXbxFIcYbSaD74KpAXv6BSm17DHEQ== /cipher-base/1.0.4: dependencies: inherits: 2.0.4 @@ -3829,6 +3868,12 @@ node: '>= 6' resolution: integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA== + /commander/6.2.1: + dev: true + engines: + node: '>= 6' + resolution: + integrity: sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA== /common-tags/1.8.0: dev: true engines: @@ -3900,6 +3945,10 @@ node: '>=0.8' resolution: integrity: sha512-e54B99q/OUoH64zYYRf3HBP5z24G38h5D3qXu23JGRoigpX5Ss4r9ZnDk3g0Z8uQC2x2lPaJ+UlWBc1ZWBWdLg== + /consola/2.15.3: + dev: true + resolution: + integrity: sha512-9vAdYbHj6x2fLKC4+oPH0kFzY/orMZyG2Aj+kNylHxKGJ/Ed4dpNyAQYwJOdqO4zdM7XpVHmyejQDcQHrnuXbw== /console-browserify/1.2.0: dev: true resolution: @@ -5549,6 +5598,14 @@ dev: true resolution: integrity: sha512-0btnI/H8f2pavGMN8w40mlSKOfTK2SVJmBfBeVIj3kNw0swwgzyRq0d5TJVOwodFmtvpPeWPN/MCcfuWF0Ezbw== + /figures/3.2.0: + dependencies: + escape-string-regexp: 1.0.5 + dev: true + engines: + node: '>=8' + resolution: + integrity: sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg== /file-entry-cache/6.0.0: dependencies: flat-cache: 3.0.4 @@ -6014,6 +6071,14 @@ node: '>=6' resolution: integrity: sha512-FNHi6mmoHvs1mxZAds4PpdCS6QG8B4C1krxJsMutgxl5t3+GlRTzzI3NEkifXx2pVsOvJdOGSmIgDhQ55FwdPA== + /gzip-size/6.0.0: + dependencies: + duplexer: 0.1.2 + dev: true + engines: + node: '>=10' + resolution: + integrity: sha512-ax7ZYomf6jqPTQ4+XCpUGyXKHk5WweS+e05MBO4/y3WJ5RkmPXNKvX+bx1behVILVwr6JSQvZAku021CHPXG3Q== /handle-thing/2.0.1: dev: true resolution: @@ -7861,6 +7926,10 @@ /lodash/4.17.20: resolution: integrity: sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA== + /lodash/4.17.21: + dev: true + resolution: + integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== /loglevel/1.7.1: dev: true engines: @@ -8073,6 +8142,13 @@ hasBin: true resolution: integrity: sha512-ft3WayFSFUVBuJj7BMLKAQcSlItKtfjsKDDsii3rqFDAZ7t11zRe8ASw/GlmivGwVUYtwkQrxiGGpL6gFvB0ag== + /mime/2.5.2: + dev: true + engines: + node: '>=4.0.0' + hasBin: true + resolution: + integrity: sha512-tqkh47FzKeCPD2PUiPB6pkbMzsCasjxAfC62/Wap5qrUWcb+sFasXUC5I3gYM5iBM8v/Qpn4UK0x+j0iHyFPDg== /mimic-fn/2.1.0: dev: true engines: @@ -8622,6 +8698,11 @@ node: '>=8' resolution: integrity: sha512-Pxv+fKRsd/Ozflgn2Gjev1HZveJJeKR6hKKmdaImJMuEZ6htAvCTbcMABJo+qevlAelTLCrEK3YTKZ9fVTcSPw== + /opener/1.5.2: + dev: true + hasBin: true + resolution: + integrity: sha512-ur5UIdyw5Y7yEj9wLzhqXiy6GZ3Mwx0yGI+5sMn2r0N0v3cKJvUmFH5yPP+WXh9e0xfyzyJX95D8l088DNFj7A== /opn/5.5.0: dependencies: is-wsl: 1.1.0 @@ -9816,6 +9897,12 @@ node: '>= 10' resolution: integrity: sha512-7AeGuCYNGmycyQbCqd/3PWH4eOoX/OiCa0uphp57NVTeAGdJGaAliecxwBDHYQCIvrW7aDBZCYeNTP/WX69mkg== + /pretty-time/1.1.0: + dev: true + engines: + node: '>=4' + resolution: + integrity: sha512-28iF6xPQrP8Oa6uxE6a1biz+lWeTOAPKggvjB8HAs6nVMKZwf5bG++632Dx614hIWgUPkgivRfG+a8uAXGTIbA== /process-nextick-args/2.0.1: dev: true resolution: @@ -10754,6 +10841,14 @@ optional: true resolution: integrity: sha512-okaWNaGDGtnXyM2CLMUl8gYZnAubgxEulC40FYjsxn5bbj+G/mDINdy24wHz4Vypb/LWtIe8rdBU78k/74v8Mw== + /react-timeago/5.2.0_react@17.0.1: + dependencies: + react: 17.0.1 + dev: false + peerDependencies: + react: ^16.0.0 + resolution: + integrity: sha512-wCEEDGQHMdFh/PLp+Hj5vk9ZoC4KjQ5u0u6+KrrY9rny5LqJ2gZvNNEAS4mhSZDV1i7JLgQI5VQTAux7f+vj2w== /react-world-flags/1.4.0_react@17.0.1: dependencies: react: 17.0.1 @@ -11580,6 +11675,16 @@ dev: true resolution: integrity: sha1-pNprY1/8zMoz9w0Xy5JZLeleVXo= + /sirv/1.0.11: + dependencies: + '@polka/url': 1.0.0-next.11 + mime: 2.5.2 + totalist: 1.1.0 + dev: true + engines: + node: '>= 10' + resolution: + integrity: sha512-SR36i3/LSWja7AJNRBz4fF/Xjpn7lQFI30tZ434dIy+bitLYSP+ZEenHg36i23V2SGEz+kqjksg0uOGZ5LPiqg== /sisteransi/1.0.5: dev: true resolution: @@ -11840,6 +11945,12 @@ node: '>= 0.6' resolution: integrity: sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow= + /std-env/2.3.0: + dependencies: + ci-info: 3.1.1 + dev: true + resolution: + integrity: sha512-4qT5B45+Kjef2Z6pE0BkskzsH0GO7GrND0wGlTM1ioUe3v0dGYx9ZJH0Aro/YyA8fqQ5EyIKDRjZojJYMFTflw== /stealthy-require/1.1.1: dev: true engines: @@ -12349,6 +12460,12 @@ node: '>=0.6' resolution: integrity: sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw== + /totalist/1.1.0: + dev: true + engines: + node: '>=6' + resolution: + integrity: sha512-gduQwd1rOdDMGxFG1gEvhV88Oirdo2p+KjoYFU7k2g+i7n6AFFbDQ5kMPUsW0pNbfQsB/cwXvT1i4Bue0s9g5g== /tough-cookie/2.5.0: dependencies: psl: 1.8.0 @@ -12850,6 +12967,23 @@ node: '>=10.4' resolution: integrity: sha512-qBIvFLGiBpLjfwmYAaHPXsn+ho5xZnGvyGvsarywGNc8VyQJUMHJ8OBKGGrPER0okBeMDaan4mNBlgBROxuI8w== + /webpack-bundle-analyzer/4.4.0: + dependencies: + acorn: 8.0.5 + acorn-walk: 8.0.2 + chalk: 4.1.0 + commander: 6.2.1 + gzip-size: 6.0.0 + lodash: 4.17.21 + opener: 1.5.2 + sirv: 1.0.11 + ws: 7.4.3 + dev: true + engines: + node: '>= 10.13.0' + hasBin: true + resolution: + integrity: sha512-9DhNa+aXpqdHk8LkLPTBU/dMfl84Y+WE2+KnfI6rSpNRNVKa0VGLjPd2pjFubDeqnWmulFggxmWBxhfJXZnR0g== /webpack-dev-middleware/3.7.3_webpack@4.44.2: dependencies: memory-fs: 0.4.1 @@ -12988,6 +13122,23 @@ optional: true resolution: integrity: sha512-6KJVGlCxYdISyurpQ0IPTklv+DULv05rs2hseIXer6D7KrUicRDLFb4IUM1S6LUAKypPM/nSiVSuv8jHu1m3/Q== + /webpackbar/5.0.0-3: + dependencies: + ansi-escapes: 4.3.1 + chalk: 4.1.0 + consola: 2.15.3 + figures: 3.2.0 + pretty-time: 1.1.0 + std-env: 2.3.0 + text-table: 0.2.0 + wrap-ansi: 7.0.0 + dev: true + engines: + node: '>=10' + peerDependencies: + webpack: 3 || 4 || 5 + resolution: + integrity: sha512-viW6KCYjMb0NPoDrw2jAmLXU2dEOhRrtku28KmOfeE1vxbfwCYuTbTaMhnkrCZLFAFyY9Q49Z/jzYO80Dw5b8g== /websocket-as-promised/2.0.1: dependencies: chnl: 1.2.0 @@ -13253,6 +13404,16 @@ node: '>=8' resolution: integrity: sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA== + /wrap-ansi/7.0.0: + dependencies: + ansi-styles: 4.3.0 + string-width: 4.2.0 + strip-ansi: 6.0.0 + dev: true + engines: + node: '>=10' + resolution: + integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== /wrappy/1.0.2: dev: true resolution: @@ -13387,6 +13548,7 @@ '@types/react-dom': ^17.0.0 '@types/react-redux': ^7.1.16 '@types/react-router-dom': ^5.1.7 + '@types/react-timeago': ^4.1.2 '@types/semver': ^7.3.4 '@types/uuid': ^8.3.0 '@types/webpack-env': ^1.16.0 @@ -13415,6 +13577,7 @@ react-refresh: ^0.9.0 react-router-dom: ^5.2.0 react-scripts: 4.0.2 + react-timeago: ^5.2.0 react-world-flags: ^1.4.0 redux: ^4.0.5 redux-devtools-extension: ^2.13.8 @@ -13425,4 +13588,6 @@ utility-types: ^3.10.0 uuid: ^8.3.2 web-vitals: ^1.1.0 + webpack-bundle-analyzer: ^4.4.0 + webpackbar: ^5.0.0-3 websocket-as-promised: ^2.0.1 diff --git a/public/locales/en.json b/public/locales/en.json index d4ab497..353e6ba 100644 --- a/public/locales/en.json +++ b/public/locales/en.json @@ -55,6 +55,13 @@ "copy": "Copy to clipboard", "copied": "Copied!", + "pageNotFound": { + "resultTitle": "Page not found", + "buttonGoBack": "Go back" + }, + + "contextualAddressUnknown": "Unknown", + "typeahead": { "emptyLabel": "No matches found.", "paginationText": "Display additional results..." @@ -199,6 +206,24 @@ "walletOverviewAddWallets": "Add wallets...", "transactionsCardTitle": "Transactions", + "transactionItemID": "Transaction ID: {{id}}", + "transactionItemFrom": "<0>From: <1 />", + "transactionItemTo": "<0>To: <1 />", + "transactionItemName": "<0>Name: <1 />", + "transactionItemARecord": "<0>A record: <1 />", + "transactionItemARecordRemoved": "(removed)", + "transactionTypes": { + "transferred": "Transferred", + "sent": "Sent", + "received": "Received", + "mined": "Mined", + "name_a_record": "Updated name", + "name_transferred": "Moved name", + "name_sent": "Sent name", + "name_received": "Received name", + "name_purchased": "Purchased name", + "unknown": "Unknown" + }, "blockValueCardTitle": "Block Value", "blockValueBaseValue": "Base value (<1>)", diff --git a/src/components/ContextualAddress.less b/src/components/ContextualAddress.less new file mode 100644 index 0000000..7b86b40 --- /dev/null +++ b/src/components/ContextualAddress.less @@ -0,0 +1,12 @@ +@import (reference) "../App.less"; + +.contextual-address { + .address-metaname, .address-name, .address-raw-metaname, .address-original, + .address-wallet, .address-address { + white-space: nowrap; + } + + .address-original { + opacity: 0.8; + } +} diff --git a/src/components/ContextualAddress.tsx b/src/components/ContextualAddress.tsx new file mode 100644 index 0000000..260941b --- /dev/null +++ b/src/components/ContextualAddress.tsx @@ -0,0 +1,93 @@ +import React from "react"; +import { Tooltip } from "antd"; + +import { useTranslation } from "react-i18next"; +import { Link } from "react-router-dom"; + +import { KristAddress } from "../krist/api/types"; +import { Wallet } from "../krist/wallets/Wallet"; +import { parseCommonMeta, CommonMeta } from "../utils/commonmeta"; + +import { KristName } from "./KristName"; + +import "./ContextualAddress.less"; + +interface Props { + address: KristAddress | string | null; + wallet?: Wallet; + metadata?: string; + source?: boolean; + hideNameAddress?: boolean; +} + +interface AddressMetanameProps { + address: string; + commonMeta: CommonMeta; + source: boolean; + hideNameAddress: boolean; +} + +export function AddressMetaname({ address, commonMeta, source, hideNameAddress }: AddressMetanameProps): JSX.Element { + const rawMetaname = (source ? commonMeta?.return : commonMeta?.recipient) || undefined; + const metaname = (source ? commonMeta?.returnMetaname : commonMeta?.metaname) || undefined; + const name = (source ? commonMeta?.returnName : commonMeta?.name) || undefined; + + // TODO: support custom suffixes + const nameWithoutSuffix = name ? name.replace(/\.kst$/, "") : undefined; + + return name + ? <> + {/* Display the name/metaname (e.g. foo@bar.kst) */} + {metaname && <>{metaname}@} + + + {/* Display the original address too */} + {!hideNameAddress && <> +   + + ({address}) + + + } + + : ( + // Display the raw metaname, but link to the owner address + + {rawMetaname} + + ); +} + +export function ContextualAddress({ address: origAddress, wallet, metadata, source, hideNameAddress }: Props): JSX.Element { + const { t } = useTranslation(); + + if (!origAddress) return ( + {t("contextualAddressUnknown")} + ); + + const address = typeof origAddress === "object" ? origAddress.address : origAddress; + const commonMeta = parseCommonMeta(metadata); + const hasMetaname = source ? !!commonMeta?.returnRecipient : !!commonMeta?.recipient; + + return + {commonMeta && hasMetaname + ? ( + // Display the metaname and link to the name if possible + + ) + : ( + // Display our wallet label if present, but link to the address + + {wallet && wallet.label + ? {wallet.label} + : {address}} + + ) + } + ; +} diff --git a/src/components/KristName.tsx b/src/components/KristName.tsx new file mode 100644 index 0000000..ac830c4 --- /dev/null +++ b/src/components/KristName.tsx @@ -0,0 +1,17 @@ +import React from "react"; + +import { Link } from "react-router-dom"; + +interface Props { + name: string; + className?: string; +} + +export function KristName({ name, className }: Props): JSX.Element { + return + + {/* TODO: support other name suffixes */} + {name}.kst + + ; +} diff --git a/src/components/SmallResult.tsx b/src/components/SmallResult.tsx new file mode 100644 index 0000000..690f801 --- /dev/null +++ b/src/components/SmallResult.tsx @@ -0,0 +1,59 @@ +/** This is ant-design's Result component, but without importing 54 kB of + * images that we don't even use */ + +import React from "react"; + +import CheckCircleFilled from "@ant-design/icons/CheckCircleFilled"; +import CloseCircleFilled from "@ant-design/icons/CloseCircleFilled"; +import ExclamationCircleFilled from "@ant-design/icons/ExclamationCircleFilled"; +import WarningFilled from "@ant-design/icons/WarningFilled"; + +export const IconMap = { + success: CheckCircleFilled, + error: CloseCircleFilled, + info: ExclamationCircleFilled, + warning: WarningFilled, +}; +export type ResultStatusType = keyof typeof IconMap; + +export interface ResultProps { + icon?: React.ReactNode; + status?: ResultStatusType; + title?: React.ReactNode; + subTitle?: React.ReactNode; + extra?: React.ReactNode; + className?: string; + style?: React.CSSProperties; +} + +/** + * Render icon if ExceptionStatus includes ,render svg image else render iconNode + */ +const renderIcon = ({ status, icon }: ResultProps) => { + const iconNode = React.createElement(IconMap[status as ResultStatusType],); + return
{icon || iconNode}
; +}; + +const renderExtra = ({ extra }: ResultProps) => + extra &&
{extra}
; + +export const SmallResult: React.FC = ({ + className: customizeClassName, + subTitle, + title, + style, + children, + status = "info", + icon, + extra, +}) => { + return ( +
+ {renderIcon({ status, icon })} +
{title}
+ {subTitle &&
{subTitle}
} + {renderExtra({ extra })} + {children &&
{children}
} +
+ ); +}; diff --git a/src/components/ws/WebsocketService.tsx b/src/components/ws/WebsocketService.tsx index c2807d4..a7271eb 100644 --- a/src/components/ws/WebsocketService.tsx +++ b/src/components/ws/WebsocketService.tsx @@ -147,7 +147,7 @@ const transaction = data.transaction as KristTransaction; debug("transaction [%s] from %s to %s", transaction.type, transaction.from || "null", transaction.to || "null"); - const fromWallet = findWalletByAddress(this.wallets, transaction.from); + const fromWallet = findWalletByAddress(this.wallets, transaction.from || undefined); const toWallet = findWalletByAddress(this.wallets, transaction.to); switch (transaction.type) { diff --git a/src/krist/api/types.ts b/src/krist/api/types.ts index db24302..da112e8 100644 --- a/src/krist/api/types.ts +++ b/src/krist/api/types.ts @@ -11,12 +11,12 @@ export type KristTransactionType = "unknown" | "mined" | "name_purchase" | "name_a_record" | "name_transfer" | "transfer"; export interface KristTransaction { id: number; - from: string; + from: string | null; to: string; value: number; time: string; - name: string; - metadata: string; + name?: string; + metadata?: string; type: KristTransactionType; } diff --git a/src/layout/AppRouter.tsx b/src/layout/AppRouter.tsx index 84701c5..15399df 100644 --- a/src/layout/AppRouter.tsx +++ b/src/layout/AppRouter.tsx @@ -9,6 +9,8 @@ import { CreditsPage } from "../pages/credits/CreditsPage"; +import { NotFoundPage } from "../pages/NotFoundPage"; + interface AppRoute { path: string; name: string; @@ -32,6 +34,6 @@ component && {component} ))} - Not found + ; } diff --git a/src/pages/NotFoundPage.less b/src/pages/NotFoundPage.less new file mode 100644 index 0000000..9b8785b --- /dev/null +++ b/src/pages/NotFoundPage.less @@ -0,0 +1,9 @@ +@import (reference) "../App.less"; + +.page-not-found { + display: flex; + align-items: center; + justify-content: center; + + height: calc(100vh - @layout-header-height); +} diff --git a/src/pages/NotFoundPage.tsx b/src/pages/NotFoundPage.tsx new file mode 100644 index 0000000..e054d8e --- /dev/null +++ b/src/pages/NotFoundPage.tsx @@ -0,0 +1,28 @@ +import React from "react"; +import { Button } from "antd"; +import { FrownOutlined } from "@ant-design/icons"; + +import { useHistory } from "react-router-dom"; +import { useTranslation } from "react-i18next"; + +import { SmallResult } from "../components/SmallResult"; + +import "./NotFoundPage.less"; + +export function NotFoundPage(): JSX.Element { + const { t } = useTranslation(); + const history = useHistory(); + + return
+ } + status="error" + title={t("pageNotFound.resultTitle")} + extra={( + + )} + /> +
; +} diff --git a/src/pages/dashboard/BlockValueCard.tsx b/src/pages/dashboard/BlockValueCard.tsx index dc9061c..04b6a8a 100644 --- a/src/pages/dashboard/BlockValueCard.tsx +++ b/src/pages/dashboard/BlockValueCard.tsx @@ -51,7 +51,7 @@ Decreases by in - {{ count: work.decrease.blocks }} + {{ count: work.decrease.blocks }} · @@ -60,7 +60,7 @@ {/* Reset */} Resets in - {{ count: work.decrease.reset }} + {{ count: work.decrease.reset }} } diff --git a/src/pages/dashboard/DashboardPage.less b/src/pages/dashboard/DashboardPage.less index 20d8f09..5fcd3f3 100644 --- a/src/pages/dashboard/DashboardPage.less +++ b/src/pages/dashboard/DashboardPage.less @@ -2,9 +2,12 @@ .dashboard-page { .dashboard-main-row { - margin-bottom: @margin-md; align-items: stretch; + & > .ant-col { + margin-bottom: @margin-md; + } + & > .ant-col > .ant-card { height: 100%; @@ -44,21 +47,9 @@ margin-bottom: @margin-sm; } - .dashboard-wallet-preview { - padding: @padding-sm @padding-md; - margin-bottom: @margin-xs; - - background: @kw-dark; - border-radius: @kw-big-card-border-radius; - - height: 60px; - justify-content: center; - - line-height: 1.5; - + .dashboard-wallet-item { .wallet-left { flex: 1; - height: 100%; display: flex; flex-direction: column; @@ -77,9 +68,82 @@ justify-content: center; align-items: center; } + } + } - &:last-child { - margin-bottom: 0; + .dashboard-card-transactions { + .dashboard-transaction-item { + .transaction-left { + display: flex; + flex-direction: column; + justify-content: center; + + .transaction-type { + a { + font-weight: bold; + color: @text-color-secondary; + } + + &-transferred a, &-name_transferred a { color: @kw-primary; } + &-sent a, &-name_sent a, &-name_purchased a { color: @kw-orange; } + &-received a, &-mined a, &-name_received a { color: @kw-green; } + &-name_a_record a { color: @kw-purple; } + + @media (max-width: @screen-xl) { + font-size: 90%; + } + } + + .transaction-time { + color: @text-color-secondary; + font-size: 90%; + + @media (max-width: @screen-xl) { + font-size: 85%; + } + } + } + + .transaction-middle { + flex: 1; + + display: flex; + flex-direction: column; + justify-content: center; + + .transaction-field { + font-weight: bold; + white-space: nowrap; + color: @text-color-secondary; + } + + .transaction-a-record-value { + font-family: monospace; + font-size: 90%; + color: @text-color-secondary; + } + + .transaction-a-record-removed { + font-style: italic; + font-size: 90%; + color: @text-color-secondary; + + white-space: nowrap; + text-overflow: ellipsis; + } + } + + .transaction-right { + flex: 0; + font-size: @font-size-lg; + + display: flex; + justify-content: center; + align-items: center; + + .transaction-name { + font-weight: bold; + } } } } @@ -103,6 +167,24 @@ } } + .dashboard-list-item { + padding: (@padding-sm / 2) @padding-md; + margin-bottom: @margin-xs; + + background: @kw-dark; + border-radius: @kw-big-card-border-radius; + + box-sizing: border-box; + min-height: 60px; + justify-content: center; + + line-height: 1.5; + + &:last-child { + margin-bottom: 0; + } + } + .dashboard-more a { display: block; width: 100%; diff --git a/src/pages/dashboard/DashboardPage.tsx b/src/pages/dashboard/DashboardPage.tsx index 6a3f9e9..803d3de 100644 --- a/src/pages/dashboard/DashboardPage.tsx +++ b/src/pages/dashboard/DashboardPage.tsx @@ -13,13 +13,13 @@ export function DashboardPage(): JSX.Element { return - - + + - - + + ; } diff --git a/src/pages/dashboard/TransactionItem.tsx b/src/pages/dashboard/TransactionItem.tsx new file mode 100644 index 0000000..fae03ae --- /dev/null +++ b/src/pages/dashboard/TransactionItem.tsx @@ -0,0 +1,160 @@ +import React from "react"; +import { Row, Col, Tooltip, Grid } from "antd"; + +import TimeAgo from "react-timeago"; + +import { useTranslation, Trans } from "react-i18next"; +import { Link } from "react-router-dom"; + +import { KristTransaction } from "../../krist/api/types"; +import { Wallet } from "../../krist/wallets/Wallet"; +import { KristValue } from "../../components/KristValue"; +import { KristName } from "../../components/KristName"; +import { ContextualAddress } from "../../components/ContextualAddress"; + +type InternalTxType = "transferred" | "sent" | "received" | "mined" | + "name_a_record" | "name_transferred" | "name_sent" | "name_received" | + "name_purchased" | "unknown"; +const TYPES_SHOW_VALUE = ["transferred", "sent", "received", "mined", "name_purchased"]; + +const MAX_A_LENGTH = 24; + +interface Props { + transaction: KristTransaction; + + /** [address]: Wallet */ + wallets: Record; +} + +function getTxType(tx: KristTransaction, from: Wallet | undefined, to: Wallet | undefined): InternalTxType { + switch (tx.type) { + case "transfer": + if (from && to) return "transferred"; + if (from) return "sent"; + if (to) return "received"; + return "transferred"; + + case "name_transfer": + if (from && to) return "name_transferred"; + if (from) return "name_sent"; + if (to) return "name_received"; + return "name_purchased"; + + case "name_a_record": return "name_a_record"; + case "name_purchase": return "name_purchased"; + + case "mined": return "mined"; + + default: return "unknown"; + } +} + +export function TransactionARecord({ metadata }: { metadata: string | undefined | null }): JSX.Element { + const { t } = useTranslation(); + + return metadata + ? + + {metadata.length > MAX_A_LENGTH + ? <>{metadata.substring(0, MAX_A_LENGTH)}… + : metadata} + + + : ( + + {t("dashboard.transactionItemARecordRemoved")} + + ); +} + +export function TransactionItem({ transaction: tx, wallets }: Props): JSX.Element { + const { t } = useTranslation(); + const bps = Grid.useBreakpoint(); + + const fromWallet = tx.from ? wallets[tx.from] : undefined; + const toWallet = tx.to ? wallets[tx.to] : undefined; + + const type = getTxType(tx, fromWallet, toWallet); + + const txTime = new Date(tx.time); + const isNew = (new Date().getTime() - txTime.getTime()) < 360000; + + const txLink = "/network/transactions/" + encodeURIComponent(tx.id); + + const hideNameAddress = !bps.xl; + + return + + {/* Transaction type and link to transaction */} + + + {t("dashboard.transactionTypes." + type)} + + + + {/* Transaction time */} + + + + + + + {/* Transaction name */} + {(type === "name_a_record" || type === "name_purchased") && ( + + Name: + + + )} + + {/* Transaction A record */} + {type === "name_a_record" && ( + + A record: + + + )} + + {/* Transaction to */} + {type !== "name_a_record" && ( + + To: + {type === "name_purchased" + ? + : } + + )} + + {/* Transaction from */} + {type !== "name_a_record" && type !== "name_purchased" && type !== "mined" && ( + + From: + + + )} + + + + {TYPES_SHOW_VALUE.includes(type) + ? ( + // Transaction value + + ) + : tx.type === "name_transfer" && ( + // TODO: Transaction name + + )} + + ; +} diff --git a/src/pages/dashboard/TransactionsCard.tsx b/src/pages/dashboard/TransactionsCard.tsx index 879a9ef..72ae8b0 100644 --- a/src/pages/dashboard/TransactionsCard.tsx +++ b/src/pages/dashboard/TransactionsCard.tsx @@ -1,12 +1,28 @@ import React from "react"; import { Card } from "antd"; +import { useSelector, shallowEqual } from "react-redux"; +import { RootState } from "../../store"; import { useTranslation } from "react-i18next"; +import { TransactionItem } from "./TransactionItem"; +import { KristTransaction } from "../../krist/api/types"; + +// TODO: remove this +import MOCK_DATA from "./transaction-mock-data.json"; + export function TransactionsCard(): JSX.Element { + const { wallets } = useSelector((s: RootState) => s.wallets, shallowEqual); const { t } = useTranslation(); - return + const walletAddressMap = Object.values(wallets) + .reduce((o, wallet) => ({ ...o, [wallet.address]: wallet }), {}); + return + {MOCK_DATA.map(t => )} ; } diff --git a/src/pages/dashboard/WalletItem.tsx b/src/pages/dashboard/WalletItem.tsx new file mode 100644 index 0000000..5b5e7ff --- /dev/null +++ b/src/pages/dashboard/WalletItem.tsx @@ -0,0 +1,19 @@ +import React from "react"; +import { Row, Col } from "antd"; + +import { Wallet } from "../../krist/wallets/Wallet"; + +import { KristValue } from "../../components/KristValue"; + +export function WalletItem({ wallet }: { wallet: Wallet }): JSX.Element { + return + + {wallet.label && {wallet.label}} + {wallet.address} + + + + + + ; +} diff --git a/src/pages/dashboard/WalletOverviewCard.tsx b/src/pages/dashboard/WalletOverviewCard.tsx index 8ae2bb0..77ab0f2 100644 --- a/src/pages/dashboard/WalletOverviewCard.tsx +++ b/src/pages/dashboard/WalletOverviewCard.tsx @@ -1,5 +1,5 @@ import React from "react"; -import { Card, Row, Col } from "antd"; +import { Card, Row, Col, Button } from "antd"; import { useSelector, shallowEqual } from "react-redux"; import { RootState } from "../../store"; @@ -10,22 +10,10 @@ import { KristValue } from "../../components/KristValue"; import { Statistic } from "./Statistic"; +import { WalletItem } from "./WalletItem"; import { keyedNullSort } from "../../utils"; -export function WalletPreview({ wallet }: { wallet: Wallet }): JSX.Element { - return - - {wallet.label && {wallet.label}} - {wallet.address} - - - - - - ; -} - export function WalletOverviewCard(): JSX.Element { const { wallets } = useSelector((s: RootState) => s.wallets, shallowEqual); const { t } = useTranslation(); @@ -45,14 +33,14 @@ return - + 0} />} /> - + - {top4Wallets.map(w => )} + {top4Wallets.map(w => )} {clonedWallets.length > 0 ? t("dashboard.walletOverviewSeeMore", { count: clonedWallets.length }) - : t("dashboard.walletOverviewAddWallets")} + : } ; diff --git a/src/pages/dashboard/transaction-mock-data.json b/src/pages/dashboard/transaction-mock-data.json new file mode 100644 index 0000000..b2293fe --- /dev/null +++ b/src/pages/dashboard/transaction-mock-data.json @@ -0,0 +1,92 @@ +[ + { + "id": 10, + "value": 20, + "from": "kreichdyes", + "to": "khugepoopy", + "type": "transfer", + "time": "2021-02-21T18:23:43+0000" + }, + { + "id": 9, + "value": 125, + "from": "khugepoopy", + "to": "kqxhx5yn9v", + "metadata": "lemmmy@sc.kst", + "type": "transfer", + "time": "2021-02-21T18:20:43+0000" + }, + { + "id": 8, + "value": 3090, + "from": "kqxhx5yn9v", + "to": "khugepoopy", + "metadata": "return=lemmmy@sc.kst;username=Lemmmy", + "type": "transfer", + "time": "2021-02-21T18:00:43+0000" + }, + { + "id": 7, + "value": 1, + "from": null, + "to": "khugepoopy", + "type": "mined", + "time": "2021-02-21T17:00:43+0000" + }, + { + "id": 6, + "value": 0, + "from": "khugepoopy", + "to": "a", + "name": "poopy", + "metadata": "", + "type": "name_a_record", + "time": "2021-02-20T17:00:43+0000" + }, + { + "id": 5, + "value": 0, + "from": "khugepoopy", + "to": "a", + "name": "poopy", + "metadata": "https://switchcraft.pw/a/pretty/long/url.txt", + "type": "name_a_record", + "time": "2021-02-20T17:00:43+0000" + }, + { + "id": 4, + "value": 0, + "from": "khugepoopy", + "to": "kreichdyes", + "name": "shaft", + "type": "name_transfer", + "time": "2021-02-19T17:00:43+0000" + }, + { + "id": 3, + "value": 0, + "from": "krazedrugz", + "to": "khugepoopy", + "name": "lem", + "type": "name_transfer", + "time": "2021-02-18T17:00:43+0000" + }, + { + "id": 2, + "value": 0, + "from": "khugepoopy", + "to": "krazedrugz", + "name": "lem", + "type": "name_transfer", + "time": "2021-02-17T17:00:43+0000" + }, + { + "id": 1, + "value": 500, + "from": "khugepoopy", + "to": "name", + "name": "lem", + "type": "name_purchase", + "time": "2021-01-01T17:00:43+0000" + } +] diff --git a/src/pages/settings/SettingsTranslations.tsx b/src/pages/settings/SettingsTranslations.tsx index 512ef25..f3326eb 100644 --- a/src/pages/settings/SettingsTranslations.tsx +++ b/src/pages/settings/SettingsTranslations.tsx @@ -1,5 +1,5 @@ import React, { useState } from "react"; -import { Table, Progress, Result, Typography, Tooltip, Button } from "antd"; +import { Table, Progress, Typography, Tooltip, Button } from "antd"; import { ExclamationCircleOutlined, FileExcelOutlined } from "@ant-design/icons"; import { useTranslation } from "react-i18next"; @@ -10,6 +10,7 @@ import { saveAs } from "file-saver"; import { Flag } from "../../components/Flag"; +import { SmallResult } from "../../components/SmallResult"; import { SettingsPageLayout } from "./SettingsPage"; const { Text } = Typography; @@ -104,7 +105,7 @@ } | undefined>(); const languages = getLanguages(); - if (!languages) return