Explorer: introduce Coingecko price cards on mint pages (#18653)

* feat: add coingecko prices, market caps, volume to token account

* feat: add updated time to price widget

* feat: add loading state to coingecko and break front page price data into widgets

* fix: prevent flicker on refresh
This commit is contained in:
Josh
2021-07-20 09:43:17 -07:00
committed by GitHub
parent c8442fd476
commit 8549c19f1a
5 changed files with 254 additions and 186 deletions

View File

@ -11,7 +11,7 @@ import { TableCardBody } from "components/common/TableCardBody";
import { Address } from "components/common/Address";
import { UnknownAccountCard } from "./UnknownAccountCard";
import { Cluster, useCluster } from "providers/cluster";
import { normalizeTokenAmount } from "utils";
import { abbreviatedNumber, normalizeTokenAmount } from "utils";
import { addressLabel } from "utils/tx";
import { reportError } from "utils/sentry";
import { useTokenRegistry } from "providers/mints/token-registry";
@ -19,6 +19,7 @@ import { BigNumber } from "bignumber.js";
import { Copyable } from "components/common/Copyable";
import { CoingeckoStatus, useCoinGecko } from "utils/coingecko";
import { displayTimestampWithoutDate } from "utils/date";
import { LoadingCard } from "components/common/LoadingCard";
const getEthAddress = (link?: string) => {
let address = "";
@ -95,6 +96,71 @@ function MintAccountCard({
}
return (
<>
{tokenInfo?.extensions?.coingeckoId &&
coinInfo?.status === CoingeckoStatus.Loading && (
<LoadingCard message="Loading token price data" />
)}
{tokenPriceInfo && (
<div className="row">
<div className="col-12 col-lg-4 col-xl">
<div className="card">
<div className="card-body">
<h4>
Price{" "}
<span className="ml-2 badge badge-primary rank">
Rank #{tokenPriceInfo.market_cap_rank}
</span>
</h4>
<h1 className="mb-0">
${tokenPriceInfo.price.toFixed(2)}{" "}
{tokenPriceInfo.price_change_percentage_24h > 0 && (
<small className="change-positive">
&uarr;{" "}
{tokenPriceInfo.price_change_percentage_24h.toFixed(2)}%
</small>
)}
{tokenPriceInfo.price_change_percentage_24h < 0 && (
<small className="change-negative">
&darr;{" "}
{tokenPriceInfo.price_change_percentage_24h.toFixed(2)}%
</small>
)}
{tokenPriceInfo.price_change_percentage_24h === 0 && (
<small>0%</small>
)}
</h1>
</div>
</div>
</div>
<div className="col-12 col-lg-4 col-xl">
<div className="card">
<div className="card-body">
<h4>24 Hour Volume</h4>
<h1 className="mb-0">
${abbreviatedNumber(tokenPriceInfo.volume_24)}
</h1>
</div>
</div>
</div>
<div className="col-12 col-lg-4 col-xl">
<div className="card">
<div className="card-body">
<h4>Market Cap</h4>
<h1 className="mb-0">
${abbreviatedNumber(tokenPriceInfo.market_cap)}
</h1>
<p className="updated-time text-muted">
Updated at{" "}
{displayTimestampWithoutDate(
tokenPriceInfo.last_updated.getTime()
)}
</p>
</div>
</div>
</div>
</div>
)}
<div className="card">
<div className="card-header">
<h3 className="card-header-title mb-0 d-flex align-items-center">
@ -105,7 +171,6 @@ function MintAccountCard({
Refresh
</button>
</div>
<TableCardBody>
<tr>
<td>Address</td>
@ -126,17 +191,6 @@ function MintAccountCard({
)}
</td>
</tr>
{tokenPriceInfo?.price && (
<tr>
<td>Current Price</td>
<td className="text-lg-right">
$
{tokenPriceInfo.price.toLocaleString("en-US", {
minimumFractionDigits: 2,
})}
</td>
</tr>
)}
{tokenInfo?.extensions?.website && (
<tr>
<td>Website</td>
@ -211,13 +265,8 @@ function MintAccountCard({
</tr>
)}
</TableCardBody>
{tokenPriceInfo && (
<p className="updated-time text-muted mr-4">
Price updated at{" "}
{displayTimestampWithoutDate(tokenPriceInfo.last_updated.getTime())}
</p>
)}
</div>
</>
);
}

View File

@ -7,7 +7,7 @@ import {
usePerformanceInfo,
useStatsProvider,
} from "providers/stats/solanaClusterStats";
import { lamportsToSol, slotsToHumanString } from "utils";
import { abbreviatedNumber, lamportsToSol, slotsToHumanString } from "utils";
import { ClusterStatus, useCluster } from "providers/cluster";
import { TpsCard } from "components/TpsCard";
import { displayTimestampWithoutDate, displayTimestampUtc } from "utils/date";
@ -82,7 +82,7 @@ function StakingComponent() {
}
if (supply === Status.Idle || supply === Status.Connecting || !coinInfo) {
return <LoadingCard />;
return <LoadingCard message="Loading supply and price data" />;
} else if (typeof supply === "string") {
return <ErrorCard text={supply} retry={fetchData} />;
}
@ -105,10 +105,10 @@ function StakingComponent() {
}
return (
<div className="card staking-card">
<div className="row staking-card">
<div className="col-12 col-lg-4 col-xl">
<div className="card">
<div className="card-body">
<div className="d-flex flex-md-row flex-column">
<div className="p-2 flex-fill">
<h4>Circulating Supply</h4>
<h1>
<em>{displayLamports(supply.circulating)}</em> /{" "}
@ -118,8 +118,11 @@ function StakingComponent() {
<em>{circulatingPercentage}%</em> is circulating
</h5>
</div>
<hr className="hidden-sm-up" />
<div className="p-2 flex-fill">
</div>
</div>
<div className="col-12 col-lg-4 col-xl">
<div className="card">
<div className="card-body">
<h4>Active Stake</h4>
{activeStake && (
<h1>
@ -133,9 +136,13 @@ function StakingComponent() {
</h5>
)}
</div>
<hr className="hidden-sm-up" />
</div>
</div>
<div className="col-12 col-lg-4 col-xl">
<div className="card">
<div className="card-body">
{solanaInfo && (
<div className="p-2 flex-fill">
<>
<h4>
Price{" "}
<span className="ml-2 badge badge-primary rank">
@ -146,12 +153,14 @@ function StakingComponent() {
<em>${solanaInfo.price.toFixed(2)}</em>{" "}
{solanaInfo.price_change_percentage_24h > 0 && (
<small className="change-positive">
&uarr; {solanaInfo.price_change_percentage_24h.toFixed(2)}%
&uarr; {solanaInfo.price_change_percentage_24h.toFixed(2)}
%
</small>
)}
{solanaInfo.price_change_percentage_24h < 0 && (
<small className="change-negative">
&darr; {solanaInfo.price_change_percentage_24h.toFixed(2)}%
&darr; {solanaInfo.price_change_percentage_24h.toFixed(2)}
%
</small>
)}
{solanaInfo.price_change_percentage_24h === 0 && (
@ -162,37 +171,30 @@ function StakingComponent() {
24h Vol: <em>${abbreviatedNumber(solanaInfo.volume_24)}</em>{" "}
MCap: <em>${abbreviatedNumber(solanaInfo.market_cap)}</em>
</h5>
</div>
</>
)}
{coinInfo.status === CoingeckoStatus.FetchFailed && (
<div className="p-2 flex-fill">
<>
<h4>Price</h4>
<h1>
<em>$--.--</em>
</h1>
<h5>Error fetching the latest price information</h5>
</div>
</>
)}
</div>
{solanaInfo && (
<p className="updated-time text-muted mb-0">
<p className="updated-time text-muted">
Updated at{" "}
{displayTimestampWithoutDate(solanaInfo.last_updated.getTime())}
</p>
)}
</div>
</div>
</div>
</div>
);
}
const abbreviatedNumber = (value: number, fixed = 1) => {
if (value < 1e3) return value;
if (value >= 1e3 && value < 1e6) return +(value / 1e3).toFixed(fixed) + "K";
if (value >= 1e6 && value < 1e9) return +(value / 1e6).toFixed(fixed) + "M";
if (value >= 1e9 && value < 1e12) return +(value / 1e9).toFixed(fixed) + "B";
if (value >= 1e12) return +(value / 1e12).toFixed(fixed) + "T";
};
function displayLamports(value: number) {
return abbreviatedNumber(lamportsToSol(value));
}

View File

@ -379,8 +379,11 @@ pre.json-wrap {
}
p.updated-time {
position: absolute;
font-size: 0.66rem;
text-align: right;
margin: .375rem;
right: 0;
bottom: 0;
}
.change-positive {

View File

@ -9,6 +9,7 @@ const CoinGeckoClient = new CoinGecko();
export enum CoingeckoStatus {
Success,
FetchFailed,
Loading,
}
export interface CoinInfo {
@ -49,7 +50,12 @@ export function useCoinGecko(coinId?: string): CoinGeckoResult | undefined {
React.useEffect(() => {
let interval: NodeJS.Timeout | undefined;
if (coinId) {
const getCoinInfo = () => {
const getCoinInfo = (refresh = false) => {
if (!refresh) {
setCoinInfo({
status: CoingeckoStatus.Loading,
});
}
CoinGeckoClient.coins
.fetch(coinId)
.then((info: CoinInfoResult) => {
@ -75,7 +81,7 @@ export function useCoinGecko(coinId?: string): CoinGeckoResult | undefined {
getCoinInfo();
interval = setInterval(() => {
getCoinInfo();
getCoinInfo(true);
}, PRICE_REFRESH);
}
return () => {

View File

@ -125,3 +125,11 @@ export function camelToTitleCase(str: string): string {
const result = str.replace(/([A-Z])/g, " $1");
return result.charAt(0).toUpperCase() + result.slice(1);
}
export function abbreviatedNumber(value: number, fixed = 1) {
if (value < 1e3) return value;
if (value >= 1e3 && value < 1e6) return +(value / 1e3).toFixed(fixed) + "K";
if (value >= 1e6 && value < 1e9) return +(value / 1e6).toFixed(fixed) + "M";
if (value >= 1e9 && value < 1e12) return +(value / 1e9).toFixed(fixed) + "B";
if (value >= 1e12) return +(value / 1e12).toFixed(fixed) + "T";
}