Skip to content

Commit ec3b28c

Browse files
committed
feat(insights): add StatCard icons and Alerts, plus code cleanup
1 parent 1306817 commit ec3b28c

21 files changed

+676
-492
lines changed

apps/insights/src/components/PriceFeeds/asset-classes-drawer.tsx

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,14 @@ const AssetClassTable = ({
8080
count: <Badge style="outline">{count}</Badge>,
8181
},
8282
})),
83-
[numFeedsByAssetClass, collator, closeDrawer, pathname, updateAssetClass],
83+
[
84+
numFeedsByAssetClass,
85+
collator,
86+
closeDrawer,
87+
pathname,
88+
updateAssetClass,
89+
updateSearch,
90+
],
8491
);
8592
return (
8693
<Table
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
@use "@pythnetwork/component-library/theme";
2+
3+
.changePercent {
4+
font-size: theme.font-size("sm");
5+
transition: color 100ms linear;
6+
display: flex;
7+
flex-flow: row nowrap;
8+
gap: theme.spacing(1);
9+
align-items: center;
10+
11+
.caret {
12+
width: theme.spacing(3);
13+
height: theme.spacing(3);
14+
transition: transform 300ms linear;
15+
}
16+
17+
&[data-direction="up"] {
18+
color: theme.color("states", "success", "base");
19+
}
20+
21+
&[data-direction="down"] {
22+
color: theme.color("states", "error", "base");
23+
24+
.caret {
25+
transform: rotate3d(1, 0, 0, 180deg);
26+
}
27+
}
28+
}
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,14 @@
11
"use client";
22

33
import { CaretUp } from "@phosphor-icons/react/dist/ssr/CaretUp";
4-
import { Card } from "@pythnetwork/component-library/Card";
54
import { Skeleton } from "@pythnetwork/component-library/Skeleton";
6-
import { type ReactNode, useMemo } from "react";
5+
import { type ComponentProps, createContext, use } from "react";
76
import { useNumberFormatter } from "react-aria";
87
import { z } from "zod";
98

10-
import styles from "./featured-recently-added.module.scss";
9+
import styles from "./change-percent.module.scss";
1110
import { StateType, useData } from "../../use-data";
12-
import { LivePrice, useLivePrice } from "../LivePrices";
11+
import { useLivePrice } from "../LivePrices";
1312

1413
const ONE_SECOND_IN_MS = 1000;
1514
const ONE_MINUTE_IN_MS = 60 * ONE_SECOND_IN_MS;
@@ -18,87 +17,64 @@ const REFRESH_YESTERDAYS_PRICES_INTERVAL = ONE_HOUR_IN_MS;
1817

1918
const CHANGE_PERCENT_SKELETON_WIDTH = 15;
2019

21-
type Props = {
22-
recentlyAdded: RecentlyAddedPriceFeed[];
20+
type Props = Omit<ComponentProps<typeof YesterdaysPricesContext>, "value"> & {
21+
symbolsToFeedKeys: Record<string, string>;
2322
};
2423

25-
type RecentlyAddedPriceFeed = {
26-
id: string;
27-
symbol: string;
28-
priceFeedName: ReactNode;
29-
};
24+
const YesterdaysPricesContext = createContext<
25+
undefined | ReturnType<typeof useData<Map<string, number>>>
26+
>(undefined);
3027

31-
export const FeaturedRecentlyAdded = ({ recentlyAdded }: Props) => {
32-
const feedKeys = useMemo(
33-
() => recentlyAdded.map(({ id }) => id),
34-
[recentlyAdded],
35-
);
36-
const symbols = useMemo(
37-
() => recentlyAdded.map(({ symbol }) => symbol),
38-
[recentlyAdded],
39-
);
28+
export const YesterdaysPricesProvider = ({
29+
symbolsToFeedKeys,
30+
...props
31+
}: Props) => {
4032
const state = useData(
41-
["yesterdaysPrices", feedKeys],
42-
() => getYesterdaysPrices(symbols),
33+
["yesterdaysPrices", Object.values(symbolsToFeedKeys)],
34+
() => getYesterdaysPrices(symbolsToFeedKeys),
4335
{
4436
refreshInterval: REFRESH_YESTERDAYS_PRICES_INTERVAL,
4537
},
4638
);
4739

48-
return (
49-
<>
50-
{recentlyAdded.map(({ priceFeedName, id, symbol }, i) => (
51-
<Card
52-
key={i}
53-
href="#"
54-
title={priceFeedName}
55-
footer={
56-
<div className={styles.footer}>
57-
<LivePrice account={id} />
58-
<div className={styles.changePercent}>
59-
<ChangePercent
60-
yesterdaysPriceState={state}
61-
feedKey={id}
62-
symbol={symbol}
63-
/>
64-
</div>
65-
</div>
66-
}
67-
className={styles.recentlyAddedFeed ?? ""}
68-
variant="tertiary"
69-
/>
70-
))}
71-
</>
72-
);
40+
return <YesterdaysPricesContext value={state} {...props} />;
7341
};
7442

7543
const getYesterdaysPrices = async (
76-
symbols: string[],
77-
): Promise<Record<string, number>> => {
44+
symbolsToFeedKeys: Record<string, string>,
45+
): Promise<Map<string, number>> => {
7846
const url = new URL("/yesterdays-prices", window.location.origin);
79-
for (const symbol of symbols) {
47+
for (const symbol of Object.keys(symbolsToFeedKeys)) {
8048
url.searchParams.append("symbols", symbol);
8149
}
8250
const response = await fetch(url);
8351
const data: unknown = await response.json();
84-
return yesterdaysPricesSchema.parse(data);
52+
return new Map(
53+
Object.entries(yesterdaysPricesSchema.parse(data)).map(
54+
([symbol, value]) => [symbolsToFeedKeys[symbol] ?? "", value],
55+
),
56+
);
8557
};
8658

8759
const yesterdaysPricesSchema = z.record(z.string(), z.number());
8860

61+
const useYesterdaysPrices = () => {
62+
const state = use(YesterdaysPricesContext);
63+
64+
if (state) {
65+
return state;
66+
} else {
67+
throw new YesterdaysPricesNotInitializedError();
68+
}
69+
};
70+
8971
type ChangePercentProps = {
90-
yesterdaysPriceState: ReturnType<
91-
typeof useData<Awaited<ReturnType<typeof getYesterdaysPrices>>>
92-
>;
9372
feedKey: string;
94-
symbol: string;
9573
};
9674

97-
const ChangePercent = ({
98-
yesterdaysPriceState,
99-
feedKey,
100-
symbol,
101-
}: ChangePercentProps) => {
75+
export const ChangePercent = ({ feedKey }: ChangePercentProps) => {
76+
const yesterdaysPriceState = useYesterdaysPrices();
77+
10278
switch (yesterdaysPriceState.type) {
10379
case StateType.Error: {
10480
// eslint-disable-next-line unicorn/no-null
@@ -107,11 +83,16 @@ const ChangePercent = ({
10783

10884
case StateType.Loading:
10985
case StateType.NotLoaded: {
110-
return <Skeleton width={CHANGE_PERCENT_SKELETON_WIDTH} />;
86+
return (
87+
<Skeleton
88+
className={styles.changePercent}
89+
width={CHANGE_PERCENT_SKELETON_WIDTH}
90+
/>
91+
);
11192
}
11293

11394
case StateType.Loaded: {
114-
const yesterdaysPrice = yesterdaysPriceState.data[symbol];
95+
const yesterdaysPrice = yesterdaysPriceState.data.get(feedKey);
11596
// eslint-disable-next-line unicorn/no-null
11697
return yesterdaysPrice === undefined ? null : (
11798
<ChangePercentLoaded priorPrice={yesterdaysPrice} feedKey={feedKey} />
@@ -132,7 +113,10 @@ const ChangePercentLoaded = ({
132113
const currentPrice = useLivePrice(feedKey);
133114

134115
return currentPrice === undefined ? (
135-
<Skeleton width={CHANGE_PERCENT_SKELETON_WIDTH} />
116+
<Skeleton
117+
className={styles.changePercent}
118+
width={CHANGE_PERCENT_SKELETON_WIDTH}
119+
/>
136120
) : (
137121
<PriceDifference
138122
currentPrice={currentPrice.price}
@@ -154,7 +138,7 @@ const PriceDifference = ({
154138
const direction = getDirection(currentPrice, priorPrice);
155139

156140
return (
157-
<span data-direction={direction} className={styles.price}>
141+
<span data-direction={direction} className={styles.changePercent}>
158142
<CaretUp weight="fill" className={styles.caret} />
159143
{numberFormatter.format(
160144
(100 * Math.abs(currentPrice - priorPrice)) / currentPrice,
@@ -173,3 +157,12 @@ const getDirection = (currentPrice: number, priorPrice: number) => {
173157
return "flat";
174158
}
175159
};
160+
161+
class YesterdaysPricesNotInitializedError extends Error {
162+
constructor() {
163+
super(
164+
"This component must be contained within a <YesterdaysPricesProvider>",
165+
);
166+
this.name = "YesterdaysPricesNotInitializedError";
167+
}
168+
}

apps/insights/src/components/PriceFeeds/featured-recently-added.module.scss

Lines changed: 0 additions & 43 deletions
This file was deleted.

0 commit comments

Comments
 (0)