diff --git a/next.config.js b/next.config.js index 7a68294..d0f2ba4 100644 --- a/next.config.js +++ b/next.config.js @@ -39,12 +39,21 @@ module.exports = { }, ], }, + { + source: '/api/holdings/:kind', + headers: [ + { + key: "Cache-Control", + value: "public; max-age=30, stale-while-revalidate=360", + }, + ], + }, { source: '/api/:any', headers: [ { key: "Cache-Control", - value: "public; max-age=10, stale-while-revalidate=20", + value: "public; max-age=5, stale-while-revalidate=20", }, ], }, diff --git a/src/components/Amount.tsx b/src/components/Amount.tsx index 5f6fc42..8b0ccb6 100644 --- a/src/components/Amount.tsx +++ b/src/components/Amount.tsx @@ -1,6 +1,7 @@ import { css } from '@emotion/react' import { BreakPoints } from 'src/components/styles' import colors from './colors' +import { loadingStyle } from './loadingKeyframes' interface AmountProps { label: string units: number @@ -20,7 +21,7 @@ export default function Amount({iconSrc, label, units, gridArea, context, value, {iconSrc && }

{label}

- {display} + {display} ) @@ -36,7 +37,7 @@ interface DollarDisplayProps { export function DollarDisplay({value, loading, label, className}: DollarDisplayProps) { const displayValue = value && Math.round(value).toLocaleString() - return + return {loading ? " ": !!value && `$${displayValue}`} } @@ -46,11 +47,14 @@ const abbrCSS = css({ cursor: "help" }) -const notShowing = css({ - opacity: 0, - minHeight: "1.15em" +const amountLoadingStyle = css(loadingStyle, { + backgroundColor: colors.gray, + borderTopRightRadius: 6, + color: "transparent", + minHeight: "1.1em", }) + const labelCss = css({ textAlign: "left", alignItems: "flex-end", diff --git a/src/components/Holdings.tsx b/src/components/Holdings.tsx index ca67449..54bd5b6 100644 --- a/src/components/Holdings.tsx +++ b/src/components/Holdings.tsx @@ -1,36 +1,16 @@ import { css } from '@emotion/react' -import useSWR from "swr" import Amount, { DollarDisplay } from 'src/components/Amount' import Heading from 'src/components/Heading' import { BreakPoints } from 'src/components/styles' import PieChart,{ChartData} from 'src/components/PieChart' import { HoldingsApi} from "src/service/holdings" import Head from 'next/head' -import { Tokens} from "src/service/Data" -import { fetcher } from 'src/utils/fetcher' import { skipZeros } from 'src/utils/skipZeros' import { Updated } from 'src/components/Updated' import Section from 'src/components/Section' import { sumTotalHoldings } from './sumTotalHoldings' - -const initalToken = { - value: 0, - units: 0, - hasError: false, - token: "CELO" as Tokens, - updated: 0 -} - -const INITAL_DATA: HoldingsApi = { - celo: { - custody: initalToken, - unfrozen: initalToken, - frozen: initalToken - }, - otherAssets: [] -} - +import useHoldings from 'src/hooks/useHoldings' export function sumCeloTotal(holdings: HoldingsApi) { const { custody, frozen, unfrozen } = holdings.celo @@ -70,34 +50,36 @@ function findOldestValueUpdatedAt(data?: HoldingsApi): number { } export default function Holdings() { - const {data} = useSWR("/api/holdings", fetcher, {initialData: INITAL_DATA, revalidateOnMount: true}) + const {data} = useHoldings() const percentages = getPercents(data) - const isLoading = data.otherAssets.length === 0 - const celo = data.celo + const isLoadingCelo = data.celo.frozen.updated === 0 || data.celo.unfrozen.updated === 0 + const isLoadingOther = !data.otherAssets.findIndex((coin) => coin.updated === 0) const oldestUpdate = findOldestValueUpdatedAt(data) + const celo = data.celo return (
- + } > - + +
- - + + {data?.otherAssets?.filter(skipZeros)?.map(asset => ( - + ))}
- +
) } @@ -106,12 +88,14 @@ const rootStyle = css({ display: 'grid', gridColumnGap: 20, gridRowGap: 12, + gridAutoColumns: "1fr 1fr 1fr", gridTemplateAreas: `"celo celo celo" "frozen unfrozen unfrozen" "crypto crypto crypto" "btc eth dai" `, [BreakPoints.tablet]: { + gridAutoColumns: "1fr", gridTemplateAreas: `"celo" "frozen" "unfrozen" diff --git a/src/components/PieChart.tsx b/src/components/PieChart.tsx index 24a2497..82a9a75 100644 --- a/src/components/PieChart.tsx +++ b/src/components/PieChart.tsx @@ -1,6 +1,7 @@ import {Fragment} from "react" import { css } from '@emotion/react' import colors from 'src/components/colors' +import { loadingStyle } from './loadingKeyframes' export const INITAL_TARGET: ChartData[] = [ @@ -51,7 +52,7 @@ export default function PieChart({slices,label,showFinePrint, isLoading}: Props) *Crypto Assets with low volatility. Candidates are decentralised stablecoins e.g. DAI } -
+
{dataWithOffsets.map(({ percent, offset, token }) => { return ( diff --git a/src/components/Ratios.tsx b/src/components/Ratios.tsx index 3118433..578db4f 100644 --- a/src/components/Ratios.tsx +++ b/src/components/Ratios.tsx @@ -2,15 +2,15 @@ import { css } from '@emotion/react' import useSWR from "swr" import Amount from 'src/components/Amount' import { BreakPoints } from 'src/components/styles' -import { HoldingsApi } from "src/service/holdings" import StableValueTokensAPI from 'src/interfaces/stable-value-tokens' import { fetcher } from "src/utils/fetcher" import { sumLiquidHoldings } from './sumLiquidHoldings' import { sumTotalHoldings } from './sumTotalHoldings' +import useHoldings from 'src/hooks/useHoldings' export function Ratios() { const stables = useSWR("/api/stable-value-tokens", fetcher) - const holdings = useSWR("/api/holdings", fetcher) + const holdings = useHoldings() const isLoading = !holdings.data || !stables.data const outstanding = stables.data?.totalStableValueInUSD || 1 diff --git a/src/components/loadingKeyframes.tsx b/src/components/loadingKeyframes.tsx new file mode 100644 index 0000000..103dee2 --- /dev/null +++ b/src/components/loadingKeyframes.tsx @@ -0,0 +1,19 @@ +import { css, keyframes } from '@emotion/react' + +const loadingKeyframes = keyframes` + from { + opacity: 0.15 + } + + to {opacity: 0.40} +` +export const loadingStyle = css({ + opacity: 0, + animationDirection: "alternate-reverse", + animationDuration: '1.4s', + animationDelay: "20ms", + animationFillMode: "none", + animationIterationCount: 'infinite', + animationTimingFunction: "ease-in-out", + animationName: loadingKeyframes +}) diff --git a/src/hooks/useHoldings.ts b/src/hooks/useHoldings.ts new file mode 100644 index 0000000..84b6388 --- /dev/null +++ b/src/hooks/useHoldings.ts @@ -0,0 +1,42 @@ +import useSWR from "swr" +import { Tokens} from "src/service/Data" +import { fetcher } from 'src/utils/fetcher' +import { HoldingsApi} from "src/service/holdings" + +const initalToken = { + value: NaN, + units: NaN, + hasError: false, + token: "CELO" as Tokens, + updated: 0 +} + +const initalOtherToken = { + value: NaN, + units: NaN, + hasError: false, + token: "BTC", + updated: 0 +} as const + +const INITAL_DATA: HoldingsApi = { + celo: { + custody: initalToken, + unfrozen: initalToken, + frozen: initalToken + }, + otherAssets: [ + initalOtherToken, + {...initalOtherToken, token: "ETH" }, + {...initalOtherToken, token: "DAI"} + ] +} + + +export default function useHoldings(): {data: HoldingsApi, error: any} { + const celoHoldings = useSWR>("/api/holdings/celo", fetcher, {initialData: {celo: INITAL_DATA.celo}, revalidateOnMount: true}) + const otherHoldings = useSWR>("/api/holdings/other", fetcher, {initialData: {otherAssets: INITAL_DATA.otherAssets}, revalidateOnMount: true}) + const error = celoHoldings.error || otherHoldings.error + const data: HoldingsApi = {...celoHoldings.data, ...otherHoldings.data} + return {data, error} +} \ No newline at end of file diff --git a/src/pages/api/holdings/[kind].ts b/src/pages/api/holdings/[kind].ts new file mode 100644 index 0000000..c4c8c50 --- /dev/null +++ b/src/pages/api/holdings/[kind].ts @@ -0,0 +1,26 @@ +import { NextApiRequest, NextApiResponse } from 'next' +import getHoldings, {getHoldingsCelo, getHoldingsOther} from "src/service/holdings" + +export default async function(req: NextApiRequest, res: NextApiResponse) { + try { + if (req.method === 'GET') { + const start = Date.now() + if (req.query.kind === 'celo') { + const holdings = await getHoldingsCelo() + res.setHeader("Server-Timing", `ms;dur=${Date.now() - start}`) + res.json(holdings) + } else if (req.query.kind === 'other') { + const holdings = await getHoldingsOther() + res.setHeader("Server-Timing", `ms;dur=${Date.now() - start}`) + res.json(holdings) + } else { + res.status(404) + } + + } else { + res.status(405) + } + } catch (error) { + res.status(error.statusCode || 500).json({ message: error.message || 'unknownError' }) + } +} \ No newline at end of file diff --git a/src/pages/api/holdings.ts b/src/pages/api/holdings/index.ts similarity index 100% rename from src/pages/api/holdings.ts rename to src/pages/api/holdings/index.ts diff --git a/src/service/holdings.ts b/src/service/holdings.ts index 4cd0767..67e31d4 100644 --- a/src/service/holdings.ts +++ b/src/service/holdings.ts @@ -4,7 +4,7 @@ import { getFrozenBalance, getInCustodyBalance, getUnFrozenBalance } from 'src/p import * as etherscan from 'src/providers/Etherscan' import * as ethplorer from 'src/providers/Ethplorerer' import duel, { Duel, sumMerge } from './duel' -import getRates from "./rates" +import getRates, { celoPrice } from "./rates" import {refresh, getOrSave} from "src/service/cache" import { MINUTE } from "src/utils/TIME" import { TokenModel, Tokens } from './Data' @@ -92,47 +92,80 @@ export interface HoldingsApi { otherAssets: TokenModel[] } -export default async function getHoldings(): Promise { - const [rates, btcHeld, ethHeld, daiHeld, celoCustodied, frozen, unfrozen] = await Promise.all([ - getRates(), - btcBalance(), - ethBalance(), - daiBalance(), +export async function getHoldingsCelo() { + const [celoRate, celoCustodied, frozen, unfrozen] = await Promise.all([ + celoPrice(), celoCustodiedBalance(), celoFrozenBalance(), celoUnfrozenBalance(), ]) - const otherAssets: TokenModel[] = [ - toToken("BTC", btcHeld, rates.btc), - toToken("ETH", ethHeld, rates.eth), - toToken("DAI", daiHeld), - ] + return {celo: toCeloShape(frozen, celoRate, unfrozen, celoCustodied)} +} + +function toCeloShape(frozen: ProviderSource, celoRate: Duel, unfrozen: ProviderSource, celoCustodied: ProviderSource) { return { - celo: { frozen: { token: "CELO", units: frozen.value, - value: frozen.value * rates.celo.value, + value: frozen.value * celoRate.value, hasError: frozen.hasError, updated: frozen.time }, unfrozen: { token: "CELO", units: unfrozen.value, - value: unfrozen.value * rates.celo.value, + value: unfrozen.value * celoRate.value, hasError: unfrozen.hasError, updated: unfrozen.time }, custody: { token: "CELO", units: celoCustodied.value, - value: celoCustodied.value * rates.celo.value, + value: celoCustodied.value * celoRate.value, hasError: celoCustodied.hasError, updated: celoCustodied.time } - }, + } as const +} + +export async function getHoldingsOther() { + const [rates, btcHeld, ethHeld, daiHeld] = await Promise.all([ + getRates(), + btcBalance(), + ethBalance(), + daiBalance(), + ]) + + const otherAssets: TokenModel[] = [ + toToken("BTC", btcHeld, rates.btc), + toToken("ETH", ethHeld, rates.eth), + toToken("DAI", daiHeld), + ] + + return {otherAssets} +} + +export default async function getHoldings(): Promise { + const [rates, btcHeld, ethHeld, daiHeld, celoCustodied, frozen, unfrozen] = await Promise.all([ + getRates(), + btcBalance(), + ethBalance(), + daiBalance(), + celoCustodiedBalance(), + celoFrozenBalance(), + celoUnfrozenBalance(), + ]) + + const otherAssets: TokenModel[] = [ + toToken("BTC", btcHeld, rates.btc), + toToken("ETH", ethHeld, rates.eth), + toToken("DAI", daiHeld), + ] + + return { + celo: toCeloShape(frozen, rates.celo, unfrozen, celoCustodied), otherAssets } } diff --git a/src/service/rates.ts b/src/service/rates.ts index 5300803..19433e6 100644 --- a/src/service/rates.ts +++ b/src/service/rates.ts @@ -51,10 +51,10 @@ export async function celoPrice() { } export default async function rates() { - const [btc, eth, euro, celo] = await Promise.all([btcPrice(), ethPrice(), euroPrice(), celoPrice()]) + const [btc, eth, celo] = await Promise.all([btcPrice(), ethPrice(), celoPrice()]) return { - btc, eth, celo, euro + btc, eth, celo } } diff --git a/src/utils/skipZeros.tsx b/src/utils/skipZeros.tsx index a99f54f..21818bb 100644 --- a/src/utils/skipZeros.tsx +++ b/src/utils/skipZeros.tsx @@ -1,4 +1,4 @@ import { TokenModel } from "src/service/Data" -export const skipZeros = (token: TokenModel) => !isNaN(token.units) && token.value != 0 +export const skipZeros = (token: TokenModel) => token.value != 0