diff --git a/packages/next-common/components/dotusd/dailyStats.jsx b/packages/next-common/components/dotusd/dailyStats.jsx new file mode 100644 index 0000000000..d38145c45b --- /dev/null +++ b/packages/next-common/components/dotusd/dailyStats.jsx @@ -0,0 +1,190 @@ +import dayjs from "dayjs"; +import utc from "dayjs/plugin/utc"; +import { Line } from "react-chartjs-2"; +import "next-common/components/charts/globalConfig"; +import hoverLinePlugin from "next-common/components/charts/plugins/hoverLine"; +import { SecondaryCard } from "next-common/components/styled/containers/secondaryCard"; +import Loading from "next-common/components/loading"; +import NoData from "next-common/components/noData"; +import ValueDisplay from "next-common/components/valueDisplay"; +import { useThemeSetting } from "next-common/context/theme"; +import { toPrecision, toPrecisionNumber } from "next-common/utils"; +import { abbreviateBigNumber } from "next-common/utils/viewfuncs"; +import RecordsLink from "./recordsLink"; +import useHolderStats from "./useHolderStats"; +import useIssuanceStats from "./useIssuanceStats"; + +dayjs.extend(utc); + +// dotUSD is an Asset Hub asset with six decimals. +const DOT_USD_DECIMALS = 6; +const chartHeight = 240; + +function buildChartData(points, color, valueOf) { + return { + labels: points.map(({ date }) => dayjs.utc(date).format("MMM D")), + datasets: [ + { + data: points.map(valueOf), + borderColor: color, + backgroundColor: color, + borderWidth: 2, + // A long series would otherwise be covered by its own points. + pointRadius: points.length > 31 ? 0 : 2, + pointHoverRadius: 4, + pointBackgroundColor: color, + tension: 0.2, + }, + ], + }; +} + +function buildChartOptions( + points, + { textTertiary, neutral100, neutral300, neutral400, tooltipBg }, + { formatTick, formatTooltip, ticksPrecision }, +) { + return { + responsive: true, + maintainAspectRatio: false, + animation: false, + interaction: { mode: "index", intersect: false }, + scales: { + x: { + border: { display: false }, + grid: { display: false }, + ticks: { color: textTertiary, maxRotation: 0, maxTicksLimit: 7 }, + }, + y: { + border: { display: false }, + grid: { color: neutral300, drawTicks: false }, + ticks: { + color: textTertiary, + maxTicksLimit: 5, + precision: ticksPrecision, + callback: formatTick, + }, + }, + }, + plugins: { + legend: { display: false }, + hoverLine: { lineColor: neutral400, lineWidth: 1 }, + tooltip: { + displayColors: false, + backgroundColor: tooltipBg, + titleColor: neutral100, + bodyColor: neutral100, + callbacks: { + title: ([{ dataIndex }]) => + dayjs.utc(points[dataIndex].date).format("MMM D, YYYY"), + label: ({ parsed }) => formatTooltip(parsed.y), + }, + }, + }, + }; +} + +// One panel of the trend: the latest value, its description and the line of the metric. +function StatPanel({ + points, + isLoading, + colorKey, + valueOf, + value, + label, + action, + formatTick, + formatTooltip, + ticksPrecision, + ariaLabel, + emptyText, +}) { + const theme = useThemeSetting(); + + return ( + + {isLoading ? ( +
+ +
+ ) : !points.length ? ( + + ) : ( + <> +
+
{value}
+
+ {label} + {action} +
+
+
+ +
+ + )} +
+ ); +} + +// The two metrics are read from their own apis, so they load and fail independently. +export default function DotUsdStats() { + const holder = useHolderStats(); + const issuance = useIssuanceStats(); + const latestHolder = holder.items[holder.items.length - 1]?.holderCount; + const latestIssuance = + issuance.items[issuance.items.length - 1]?.totalIssuance; + + return ( +
+ + toPrecisionNumber(totalIssuance, DOT_USD_DECIMALS) + } + value={ + latestIssuance === undefined ? ( + "—" + ) : ( + + ) + } + label="Total issued dotUSD" + formatTick={(value) => abbreviateBigNumber(value, 2)} + formatTooltip={(value) => `${abbreviateBigNumber(value, 2)} dotUSD`} + ariaLabel="dotUSD total issuance since launch" + emptyText="No issuance data available." + /> + + holderCount} + value={latestHolder ?? "—"} + label="Accounts holding the asset" + action={} + formatTick={(value) => value} + ticksPrecision={0} + formatTooltip={(value) => `${value} accounts`} + ariaLabel="dotUSD holders since launch" + emptyText="No holder data available." + /> +
+ ); +} diff --git a/packages/next-common/components/dotusd/holdersTab.jsx b/packages/next-common/components/dotusd/holdersTab.jsx new file mode 100644 index 0000000000..a287a43b5d --- /dev/null +++ b/packages/next-common/components/dotusd/holdersTab.jsx @@ -0,0 +1,10 @@ +import { Section } from "./layout"; +import DotUsdHolders from "./holders"; + +export default function DotUsdHoldersTab() { + return ( +
+ +
+ ); +} diff --git a/packages/next-common/components/dotusd/index.jsx b/packages/next-common/components/dotusd/index.jsx index d0fd0c14c2..7f7c68c6af 100644 --- a/packages/next-common/components/dotusd/index.jsx +++ b/packages/next-common/components/dotusd/index.jsx @@ -4,7 +4,8 @@ import DotUsdAccount from "./account"; import DotUsdInstance from "./instance"; import DotUsdExternalAssets from "./externalAssets"; import DotUsdPool from "./pool"; -import DotUsdHolders from "./holders"; +import DotUsdStats from "./dailyStats"; +import DotUsdHoldersTab from "./holdersTab"; import DotUsdMintRecords from "./mintRecords"; import DotUsdRedeemRecords from "./redeemRecords"; @@ -37,10 +38,10 @@ function DotUsdOverview() {
- +
); @@ -49,6 +50,10 @@ function DotUsdOverview() { function DotUsdTabContent() { const { activeTab } = useDotUsdTabs(); + if (activeTab === "holders") { + return ; + } + if (activeTab === "mint") { return ; } diff --git a/packages/next-common/components/dotusd/recordsLink.jsx b/packages/next-common/components/dotusd/recordsLink.jsx new file mode 100644 index 0000000000..170954e44c --- /dev/null +++ b/packages/next-common/components/dotusd/recordsLink.jsx @@ -0,0 +1,21 @@ +import { ArrowExternalLink } from "@osn/icons/subsquare"; +import Tooltip from "next-common/components/tooltip"; +import { useDotUsdTabs } from "./tabs"; + +// Switches to the tab of the list the number is summed from. +export default function RecordsLink({ tab, tooltip }) { + const { setActiveTab } = useDotUsdTabs(); + + return ( + + + + ); +} diff --git a/packages/next-common/components/dotusd/summary.jsx b/packages/next-common/components/dotusd/summary.jsx index 3109678bad..07b1056a48 100644 --- a/packages/next-common/components/dotusd/summary.jsx +++ b/packages/next-common/components/dotusd/summary.jsx @@ -1,4 +1,3 @@ -import { ArrowExternalLink } from "@osn/icons/subsquare"; import BigNumber from "bignumber.js"; import { isNil } from "lodash-es"; import { toPrecision } from "next-common/utils"; @@ -10,7 +9,7 @@ import ValueDisplay from "next-common/components/valueDisplay"; import Tooltip from "next-common/components/tooltip"; import { useDotUsdContext } from "./context"; import usePsmStats from "./usePsmStats"; -import { useDotUsdTabs } from "./tabs"; +import RecordsLink from "./recordsLink"; function EmptyValue() { return —; @@ -57,24 +56,6 @@ function SignedAmountValue({ value, symbol, decimals }) { ); } -// Switches to the records list the flow number is summed from. -function RecordsLink({ tab, tooltip }) { - const { setActiveTab } = useDotUsdTabs(); - - return ( - - - - ); -} - function DebtUsageBar({ ratio, maxDebt }) { const percent = BigNumber.min(BigNumber.max(ratio, 0), 100).toNumber(); diff --git a/packages/next-common/components/dotusd/tabs.jsx b/packages/next-common/components/dotusd/tabs.jsx index eab7fec996..a55125dce7 100644 --- a/packages/next-common/components/dotusd/tabs.jsx +++ b/packages/next-common/components/dotusd/tabs.jsx @@ -2,6 +2,7 @@ import { createContext, useContext, useState } from "react"; export const DOT_USD_TABS = [ { value: "overview", label: "Overview" }, + { value: "holders", label: "Holders" }, { value: "mint", label: "Mint records" }, { value: "redeem", label: "Redeem records" }, ]; diff --git a/packages/next-common/components/dotusd/useDailyStats.js b/packages/next-common/components/dotusd/useDailyStats.js new file mode 100644 index 0000000000..c60de820a1 --- /dev/null +++ b/packages/next-common/components/dotusd/useDailyStats.js @@ -0,0 +1,50 @@ +import { useEffect, useState } from "react"; +import nextApi from "next-common/services/nextApi"; + +/** + * The daily snapshots of one dotUSD metric served by the backend, oldest first. `days` is + * forwarded to the api when set, and the api decides how much history it returns otherwise. + */ +export default function useDailyStats(path, days) { + const [state, setState] = useState({ items: [], isLoading: true }); + + useEffect(() => { + let isMounted = true; + // Keep the previous points on screen while the next response loads. + setState((previous) => ({ ...previous, isLoading: true })); + + const params = days === undefined ? {} : { days }; + + nextApi + .fetch(path, params) + .then(({ result, error }) => { + if (!isMounted) { + return; + } + + if (error) { + console.error(`Failed to fetch ${path}`, error); + setState({ items: [], isLoading: false }); + return; + } + + setState({ + items: Array.isArray(result) ? result : [], + isLoading: false, + }); + }) + .catch((error) => { + console.error(`Failed to fetch ${path}`, error); + + if (isMounted) { + setState({ items: [], isLoading: false }); + } + }); + + return () => { + isMounted = false; + }; + }, [path, days]); + + return state; +} diff --git a/packages/next-common/components/dotusd/useHolderStats.js b/packages/next-common/components/dotusd/useHolderStats.js new file mode 100644 index 0000000000..c4adad790c --- /dev/null +++ b/packages/next-common/components/dotusd/useHolderStats.js @@ -0,0 +1,6 @@ +import useDailyStats from "./useDailyStats"; + +// No days argument: the api returns the whole history since the launch. +export default function useHolderStats() { + return useDailyStats("dotusd/holder-stats"); +} diff --git a/packages/next-common/components/dotusd/useIssuanceStats.js b/packages/next-common/components/dotusd/useIssuanceStats.js new file mode 100644 index 0000000000..03e8ee9a57 --- /dev/null +++ b/packages/next-common/components/dotusd/useIssuanceStats.js @@ -0,0 +1,6 @@ +import useDailyStats from "./useDailyStats"; + +// No days argument: the api returns the whole history since the launch. +export default function useIssuanceStats() { + return useDailyStats("dotusd/issuance-stats"); +}