diff --git a/package.json b/package.json index 013dd7e..69aef78 100644 --- a/package.json +++ b/package.json @@ -51,6 +51,7 @@ "build": "craco build", "optimise": "gzip -kr build/static", "full-build": "npm run clean; GENERATE_SOURCEMAP=false npm run build; npm run optimise", + "analyze-build": "npm run clean; FORCE_ANALYZE=true npm run build", "test": "craco test" }, "eslintConfig": { diff --git a/public/locales/en.json b/public/locales/en.json index 2fb3549..5903e46 100644 --- a/public/locales/en.json +++ b/public/locales/en.json @@ -111,12 +111,12 @@ "columnNames": "Names", "columnCategory": "Category", "columnFirstSeen": "First Seen", - "nameCount": "{{count}} name", - "nameCount_plural": "{{count}} names", + "nameCount": "{{count, number}} name", + "nameCount_plural": "{{count, number}} names", "firstSeen": "First seen {{date}}", - "walletCount": "{{count}} wallet", - "walletCount_plural": "{{count}} wallets", + "walletCount": "{{count, number}} wallet", + "walletCount_plural": "{{count, number}} wallets", "actionsEditTooltip": "Edit wallet", "actionsDelete": "Delete wallet", @@ -204,9 +204,9 @@ "walletOverviewCardTitle": "Wallets", "walletOverviewTotalBalance": "Total balance", "walletOverviewNames": "Names", - "walletOverviewNamesCount": "{{count}} name", - "walletOverviewNamesCount_plural": "{{count}} names", - "walletOverviewSeeMore": "See all {{count}}...", + "walletOverviewNamesCount": "{{count, number}} name", + "walletOverviewNamesCount_plural": "{{count, number}} names", + "walletOverviewSeeMore": "See all {{count, number}}...", "walletOverviewAddWallets": "Add wallets...", "transactionsCardTitle": "Transactions", @@ -228,15 +228,17 @@ "name_purchased": "Purchased name", "unknown": "Unknown" }, + "transactionsError": "There was an error fetching your transactions. See the console for details.", + "transactionsSeeMore": "See all {{count, number}}...", "blockValueCardTitle": "Block Value", "blockValueBaseValue": "Base value (<1>)", - "blockValueBaseValueNames": "{{count}} name", - "blockValueBaseValueNames_plural": "{{count}} names", - "blockValueNextDecrease": "Decreases by <1> in <3>{{count}} block", - "blockValueNextDecrease_plural": "Decreases by <1> in <3>{{count}} blocks", - "blockValueReset": "Resets in <1>{{count}} block", - "blockValueReset_plural": "Resets in <1>{{count}} blocks", + "blockValueBaseValueNames": "{{count, number}} name", + "blockValueBaseValueNames_plural": "{{count, number}} names", + "blockValueNextDecrease": "Decreases by <1> in <3>{{count, number}} block", + "blockValueNextDecrease_plural": "Decreases by <1> in <3>{{count, number}} blocks", + "blockValueReset": "Resets in <1>{{count, number}} block", + "blockValueReset_plural": "Resets in <1>{{count, number}} blocks", "blockDifficultyCardTitle": "Block Difficulty" }, diff --git a/src/krist/api/lookup.ts b/src/krist/api/lookup.ts index cc43517..8b9a434 100644 --- a/src/krist/api/lookup.ts +++ b/src/krist/api/lookup.ts @@ -1,7 +1,7 @@ // Copyright (c) 2020-2021 Drew Lemmy // This file is part of KristWeb 2 under GPL-3.0. // Full details: https://github.com/tmpim/KristWeb2/blob/master/LICENSE.txt -import { APIResponse, KristAddress } from "./types"; +import { APIResponse, KristAddress, KristTransaction } from "./types"; interface LookupAddressesResponse { found: number; @@ -10,9 +10,9 @@ } export interface KristAddressWithNames extends KristAddress { names?: number } -export type LookupResults = Record; +export type AddressLookupResults = Record; -export async function lookupAddresses(syncNode: string, addresses: string[], fetchNames?: boolean): Promise { +export async function lookupAddresses(syncNode: string, addresses: string[], fetchNames?: boolean): Promise { if (!addresses || addresses.length === 0) return {}; try { @@ -35,3 +35,41 @@ return {}; } + +interface LookupTransactionsOptions { + includeMined?: boolean; + limit?: number; + offset?: number; + orderBy?: "id" | "from" | "to" | "value" | "time"; + order?: "ASC" | "DESC"; +} + +export interface LookupTransactionsResponse { + count: number; + total: number; + transactions: KristTransaction[]; +} + +export async function lookupTransactions(syncNode: string, addresses: string[], opts: LookupTransactionsOptions): Promise { + if (!addresses || addresses.length === 0) return { count: 0, total: 0, transactions: [] }; + + const qs = new URLSearchParams(); + if (opts.includeMined) qs.append("includeMined", ""); + if (opts.limit) qs.append("limit", opts.limit.toString()); + if (opts.offset) qs.append("offset", opts.offset.toString()); + if (opts.orderBy) qs.append("orderBy", opts.orderBy); + if (opts.order) qs.append("order", opts.order); + + const res = await fetch( + syncNode + + "/lookup/transactions/" + + encodeURIComponent(addresses.join(",")) + + "?" + qs + ); + if (!res.ok || res.status !== 200) throw new Error(res.statusText); + + const data: APIResponse = await res.json(); + if (!data.ok || data.error) throw new Error(data.error); + + return data; +} diff --git a/src/pages/dashboard/BlockValueCard.tsx b/src/pages/dashboard/BlockValueCard.tsx index 555d91c..98d28d9 100644 --- a/src/pages/dashboard/BlockValueCard.tsx +++ b/src/pages/dashboard/BlockValueCard.tsx @@ -21,7 +21,7 @@ const hasNames = (work?.unpaid || 0) > 0; return - + {work && <> {/* Main block value */} .ant-col > .ant-card { + display: flex; + flex-direction: column; + height: 100%; border: none; @@ -76,6 +79,8 @@ .dashboard-card-transactions { .dashboard-transaction-item { + flex-flow: nowrap; + .transaction-left { display: flex; flex-direction: column; @@ -114,6 +119,8 @@ flex-direction: column; justify-content: center; + overflow: hidden; + .transaction-field { font-weight: bold; white-space: nowrap; @@ -149,6 +156,34 @@ } } } + + &.empty .ant-card-body { + height: 100%; + padding-top: 0 !important; + padding: 0; + + display: flex; + align-items: center; + justify-content: center; + + &::before, &::after { + content: none; + } + + .ant-empty-normal { + margin: 0; + } + + .ant-result { + padding: @padding-sm; + + .ant-result-icon { + margin-bottom: @margin-xs; + + .anticon { font-size: 48px; } + } + } + } } .dashboard-card-block-value { diff --git a/src/pages/dashboard/TransactionsCard.tsx b/src/pages/dashboard/TransactionsCard.tsx index 92e3f1e..d7e757e 100644 --- a/src/pages/dashboard/TransactionsCard.tsx +++ b/src/pages/dashboard/TransactionsCard.tsx @@ -1,31 +1,89 @@ // Copyright (c) 2020-2021 Drew Lemmy // This file is part of KristWeb 2 under GPL-3.0. // Full details: https://github.com/tmpim/KristWeb2/blob/master/LICENSE.txt -import React from "react"; -import { Card } from "antd"; +import React, { useState, useEffect, useMemo } from "react"; +import { Card, Skeleton, Empty, Row } from "antd"; import { useSelector, shallowEqual } from "react-redux"; import { RootState } from "../../store"; import { useTranslation } from "react-i18next"; +import { Link } from "react-router-dom"; import { TransactionItem } from "./TransactionItem"; -import { KristTransaction } from "../../krist/api/types"; +import { lookupTransactions, LookupTransactionsResponse } from "../../krist/api/lookup"; -// TODO: remove this -import MOCK_DATA from "./transaction-mock-data.json"; +import { SmallResult } from "../../components/SmallResult"; + +import { throttle } from "lodash-es"; + +import Debug from "debug"; +const debug = Debug("kristweb:transactions-card"); export function TransactionsCard(): JSX.Element { + const syncNode = useSelector((s: RootState) => s.node.syncNode); const { wallets } = useSelector((s: RootState) => s.wallets, shallowEqual); const { t } = useTranslation(); + const [res, setRes] = useState(); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(); + + async function _fetchTransactions(): Promise { + try { + debug("fetching transactions"); + + setRes(await lookupTransactions( + syncNode, + Object.values(wallets).map(w => w.address), + { includeMined: true, limit: 5, orderBy: "id", order: "DESC" } + )); + } catch (err) { + console.error(err); + setError(err); + } finally { + setLoading(false); + } + }; + + const fetchTransactions = useMemo(() => + throttle(_fetchTransactions, 300, { leading: false, trailing: true }), []); + + useEffect(() => { + if (!syncNode || !wallets) return; + fetchTransactions(); + }, [syncNode, wallets]); + const walletAddressMap = Object.values(wallets) .reduce((o, wallet) => ({ ...o, [wallet.address]: wallet }), {}); - return - {MOCK_DATA.map(t => )} + function cardContents(): JSX.Element { + return <> + {res && res.transactions.map(t => ( + + ))} + + + + {t("dashboard.transactionsSeeMore", { count: res?.total || 0 })} + + + ; + } + + const isEmpty = !loading && (error || !res || res.count == 0); + + return + + {error + ? + : (res && res.count > 0 + ? cardContents() + : + )} + ; } diff --git a/src/utils/i18n.ts b/src/utils/i18n.ts index aa26852..096631b 100644 --- a/src/utils/i18n.ts +++ b/src/utils/i18n.ts @@ -47,7 +47,15 @@ keySeparator: ".", interpolation: { - escapeValue: false // React already safes from XSS + escapeValue: false, // React already safes from XSS + + format(value, format) { + // Format numbers with commas + if (format === "number" && typeof value === "number") + return value.toLocaleString(); + + return value; + } }, backend: {