|
1 |
| -"use client"; |
| 1 | +import type { ComponentProps } from "react"; |
2 | 2 |
|
3 |
| -import { type ComponentProps, createContext, use } from "react"; |
4 |
| -import { useNumberFormatter } from "react-aria"; |
5 |
| -import { z } from "zod"; |
6 |
| - |
7 |
| -import { StateType, useData } from "../../use-data"; |
8 | 3 | import { ChangeValue } from "../ChangeValue";
|
9 |
| -import { useLivePrice } from "../LivePrices"; |
10 |
| - |
11 |
| -const ONE_SECOND_IN_MS = 1000; |
12 |
| -const ONE_MINUTE_IN_MS = 60 * ONE_SECOND_IN_MS; |
13 |
| -const ONE_HOUR_IN_MS = 60 * ONE_MINUTE_IN_MS; |
14 |
| -const REFRESH_YESTERDAYS_PRICES_INTERVAL = ONE_HOUR_IN_MS; |
15 |
| - |
16 |
| -type Props = Omit<ComponentProps<typeof YesterdaysPricesContext>, "value"> & { |
17 |
| - feeds: Record<string, string>; |
18 |
| -}; |
19 |
| - |
20 |
| -const YesterdaysPricesContext = createContext< |
21 |
| - undefined | ReturnType<typeof useData<Map<string, number>>> |
22 |
| ->(undefined); |
23 |
| - |
24 |
| -export const YesterdaysPricesProvider = ({ feeds, ...props }: Props) => { |
25 |
| - const state = useData( |
26 |
| - ["yesterdaysPrices", Object.keys(feeds)], |
27 |
| - () => getYesterdaysPrices(feeds), |
28 |
| - { |
29 |
| - refreshInterval: REFRESH_YESTERDAYS_PRICES_INTERVAL, |
30 |
| - }, |
31 |
| - ); |
32 |
| - |
33 |
| - return <YesterdaysPricesContext value={state} {...props} />; |
34 |
| -}; |
35 |
| - |
36 |
| -const getYesterdaysPrices = async ( |
37 |
| - feeds: Props["feeds"], |
38 |
| -): Promise<Map<string, number>> => { |
39 |
| - const url = new URL("/yesterdays-prices", window.location.origin); |
40 |
| - for (const symbol of Object.keys(feeds)) { |
41 |
| - url.searchParams.append("symbols", symbol); |
42 |
| - } |
43 |
| - const response = await fetch(url); |
44 |
| - const data = yesterdaysPricesSchema.parse(await response.json()); |
45 |
| - return new Map( |
46 |
| - Object.entries(data).map(([symbol, value]) => [feeds[symbol] ?? "", value]), |
47 |
| - ); |
48 |
| -}; |
49 |
| - |
50 |
| -const yesterdaysPricesSchema = z.record(z.string(), z.number()); |
51 |
| - |
52 |
| -const useYesterdaysPrices = () => { |
53 |
| - const state = use(YesterdaysPricesContext); |
54 |
| - |
55 |
| - if (state) { |
56 |
| - return state; |
57 |
| - } else { |
58 |
| - throw new YesterdaysPricesNotInitializedError(); |
59 |
| - } |
60 |
| -}; |
61 |
| - |
62 |
| -type ChangePercentProps = { |
63 |
| - className?: string | undefined; |
64 |
| - feedKey: string; |
65 |
| -}; |
66 |
| - |
67 |
| -export const ChangePercent = ({ feedKey, className }: ChangePercentProps) => { |
68 |
| - const yesterdaysPriceState = useYesterdaysPrices(); |
69 |
| - |
70 |
| - switch (yesterdaysPriceState.type) { |
71 |
| - case StateType.Error: |
72 |
| - case StateType.Loading: |
73 |
| - case StateType.NotLoaded: { |
74 |
| - return <ChangeValue className={className} isLoading />; |
75 |
| - } |
76 |
| - |
77 |
| - case StateType.Loaded: { |
78 |
| - const yesterdaysPrice = yesterdaysPriceState.data.get(feedKey); |
79 |
| - return yesterdaysPrice === undefined ? ( |
80 |
| - <ChangeValue className={className} isLoading /> |
81 |
| - ) : ( |
82 |
| - <ChangePercentLoaded |
83 |
| - className={className} |
84 |
| - priorPrice={yesterdaysPrice} |
85 |
| - feedKey={feedKey} |
86 |
| - /> |
87 |
| - ); |
88 |
| - } |
89 |
| - } |
90 |
| -}; |
| 4 | +import { FormattedNumber } from "../FormattedNumber"; |
91 | 5 |
|
92 |
| -type ChangePercentLoadedProps = { |
| 6 | +type PriceDifferenceProps = Omit< |
| 7 | + ComponentProps<typeof ChangeValue>, |
| 8 | + "children" | "direction" | "isLoading" |
| 9 | +> & { |
93 | 10 | className?: string | undefined;
|
94 |
| - priorPrice: number; |
95 |
| - feedKey: string; |
96 |
| -}; |
97 |
| - |
98 |
| -const ChangePercentLoaded = ({ |
99 |
| - className, |
100 |
| - priorPrice, |
101 |
| - feedKey, |
102 |
| -}: ChangePercentLoadedProps) => { |
103 |
| - const { current } = useLivePrice(feedKey); |
104 |
| - |
105 |
| - return current === undefined ? ( |
106 |
| - <ChangeValue className={className} isLoading /> |
107 |
| - ) : ( |
108 |
| - <PriceDifference |
109 |
| - className={className} |
110 |
| - currentPrice={current.aggregate.price} |
111 |
| - priorPrice={priorPrice} |
112 |
| - /> |
| 11 | +} & ( |
| 12 | + | { isLoading: true } |
| 13 | + | { |
| 14 | + isLoading?: false; |
| 15 | + currentValue: number; |
| 16 | + previousValue: number; |
| 17 | + } |
113 | 18 | );
|
114 |
| -}; |
115 |
| - |
116 |
| -type PriceDifferenceProps = { |
117 |
| - className?: string | undefined; |
118 |
| - currentPrice: number; |
119 |
| - priorPrice: number; |
120 |
| -}; |
121 | 19 |
|
122 |
| -const PriceDifference = ({ |
123 |
| - className, |
124 |
| - currentPrice, |
125 |
| - priorPrice, |
126 |
| -}: PriceDifferenceProps) => { |
127 |
| - const numberFormatter = useNumberFormatter({ maximumFractionDigits: 2 }); |
128 |
| - const direction = getDirection(currentPrice, priorPrice); |
129 |
| - |
130 |
| - return ( |
131 |
| - <ChangeValue direction={direction} className={className}> |
132 |
| - {numberFormatter.format( |
133 |
| - (100 * Math.abs(currentPrice - priorPrice)) / priorPrice, |
134 |
| - )} |
| 20 | +export const ChangePercent = ({ ...props }: PriceDifferenceProps) => |
| 21 | + props.isLoading ? ( |
| 22 | + <ChangeValue {...props} /> |
| 23 | + ) : ( |
| 24 | + <ChangeValue |
| 25 | + direction={getDirection(props.currentValue, props.previousValue)} |
| 26 | + {...props} |
| 27 | + > |
| 28 | + <FormattedNumber |
| 29 | + maximumFractionDigits={2} |
| 30 | + value={ |
| 31 | + (100 * Math.abs(props.currentValue - props.previousValue)) / |
| 32 | + props.previousValue |
| 33 | + } |
| 34 | + /> |
135 | 35 | %
|
136 | 36 | </ChangeValue>
|
137 | 37 | );
|
138 |
| -}; |
139 | 38 |
|
140 |
| -const getDirection = (currentPrice: number, priorPrice: number) => { |
141 |
| - if (currentPrice < priorPrice) { |
| 39 | +const getDirection = (currentValue: number, previousValue: number) => { |
| 40 | + if (currentValue < previousValue) { |
142 | 41 | return "down";
|
143 |
| - } else if (currentPrice > priorPrice) { |
| 42 | + } else if (currentValue > previousValue) { |
144 | 43 | return "up";
|
145 | 44 | } else {
|
146 | 45 | return "flat";
|
147 | 46 | }
|
148 | 47 | };
|
149 |
| - |
150 |
| -class YesterdaysPricesNotInitializedError extends Error { |
151 |
| - constructor() { |
152 |
| - super( |
153 |
| - "This component must be contained within a <YesterdaysPricesProvider>", |
154 |
| - ); |
155 |
| - this.name = "YesterdaysPricesNotInitializedError"; |
156 |
| - } |
157 |
| -} |
0 commit comments