Skip to content

Commit e1ae1ae

Browse files
committed
Optimize payload size for price feeds pages
1 parent c6a2eb9 commit e1ae1ae

File tree

15 files changed

+362
-259
lines changed

15 files changed

+362
-259
lines changed
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import { Badge } from "@pythnetwork/component-library/Badge";
2+
import type { ComponentProps } from "react";
3+
4+
import { usePriceFeeds } from "../../hooks/use-price-feeds";
5+
6+
type Props = ComponentProps<typeof Badge> & {
7+
symbol: string;
8+
};
9+
10+
export const AssetClassTag = ({ symbol }: Props) => {
11+
const feed = usePriceFeeds().get(symbol);
12+
13+
if (feed) {
14+
return (
15+
<Badge variant="neutral" style="outline" size="xs">
16+
{feed.assetClass.toUpperCase()}
17+
</Badge>
18+
);
19+
} else {
20+
throw new NoSuchFeedError(symbol);
21+
}
22+
};
23+
24+
class NoSuchFeedError extends Error {
25+
constructor(symbol: string) {
26+
super(`No feed exists named ${symbol}`);
27+
this.name = "NoSuchFeedError";
28+
}
29+
}

apps/insights/src/components/PriceFeed/layout.tsx

Lines changed: 4 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@ import type { ReactNode } from "react";
1313
import styles from "./layout.module.scss";
1414
import { PriceFeedSelect } from "./price-feed-select";
1515
import { ReferenceData } from "./reference-data";
16-
import { toHex } from "../../hex";
1716
import { Cluster, getFeeds } from "../../services/pyth";
1817
import { FeedKey } from "../FeedKey";
1918
import {
@@ -26,7 +25,6 @@ import {
2625
YesterdaysPricesProvider,
2726
PriceFeedChangePercent,
2827
} from "../PriceFeedChangePercent";
29-
import { PriceFeedIcon } from "../PriceFeedIcon";
3028
import { PriceFeedTag } from "../PriceFeedTag";
3129
import { TabPanel, TabRoot, Tabs } from "../Tabs";
3230

@@ -38,12 +36,12 @@ type Props = {
3836
};
3937

4038
export const PriceFeedLayout = async ({ children, params }: Props) => {
41-
const [{ slug }, fees] = await Promise.all([
39+
const [{ slug }, feeds] = await Promise.all([
4240
params,
4341
getFeeds(Cluster.Pythnet),
4442
]);
4543
const symbol = decodeURIComponent(slug);
46-
const feed = fees.find((item) => item.symbol === symbol);
44+
const feed = feeds.find((item) => item.symbol === symbol);
4745

4846
return feed ? (
4947
<div className={styles.priceFeedLayout}>
@@ -64,22 +62,8 @@ export const PriceFeedLayout = async ({ children, params }: Props) => {
6462
</div>
6563
</div>
6664
<div className={styles.headerRow}>
67-
<PriceFeedSelect
68-
feeds={fees
69-
.filter((feed) => feed.symbol !== symbol)
70-
.map((feed) => ({
71-
id: feed.symbol,
72-
key: toHex(feed.product.price_account),
73-
displaySymbol: feed.product.display_symbol,
74-
icon: <PriceFeedIcon symbol={feed.symbol} />,
75-
assetClass: feed.product.asset_type,
76-
}))}
77-
>
78-
<PriceFeedTag
79-
symbol={feed.product.display_symbol}
80-
description={feed.product.description}
81-
icon={<PriceFeedIcon symbol={feed.symbol} />}
82-
/>
65+
<PriceFeedSelect>
66+
<PriceFeedTag symbol={feed.symbol} />
8367
</PriceFeedSelect>
8468
<div className={styles.rightGroup}>
8569
<FeedKey

apps/insights/src/components/PriceFeed/price-feed-select.tsx

Lines changed: 30 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
"use client";
22

3-
import { Badge } from "@pythnetwork/component-library/Badge";
43
import { DropdownCaretDown } from "@pythnetwork/component-library/DropdownCaretDown";
54
import {
65
Virtualizer,
@@ -20,39 +19,43 @@ import { type ReactNode, useMemo, useState } from "react";
2019
import { useCollator, useFilter } from "react-aria";
2120

2221
import styles from "./price-feed-select.module.scss";
22+
import { usePriceFeeds } from "../../hooks/use-price-feeds";
23+
import { AssetClassTag } from "../AssetClassTag";
2324
import { PriceFeedTag } from "../PriceFeedTag";
2425

2526
type Props = {
2627
children: ReactNode;
27-
feeds: {
28-
id: string;
29-
key: string;
30-
displaySymbol: string;
31-
icon: ReactNode;
32-
assetClass: string;
33-
}[];
3428
};
3529

36-
export const PriceFeedSelect = ({ children, feeds }: Props) => {
30+
export const PriceFeedSelect = ({ children }: Props) => {
31+
const feeds = usePriceFeeds();
3732
const collator = useCollator();
3833
const filter = useFilter({ sensitivity: "base", usage: "search" });
3934
const [search, setSearch] = useState("");
40-
const sortedFeeds = useMemo(
41-
() =>
42-
feeds.sort((a, b) => collator.compare(a.displaySymbol, b.displaySymbol)),
43-
[feeds, collator],
44-
);
4535
const filteredFeeds = useMemo(
4636
() =>
4737
search === ""
48-
? sortedFeeds
49-
: sortedFeeds.filter(
50-
(feed) =>
51-
filter.contains(feed.displaySymbol, search) ||
52-
filter.contains(feed.assetClass, search) ||
53-
filter.contains(feed.key, search),
54-
),
55-
[sortedFeeds, search, filter],
38+
? feeds.entries()
39+
: feeds
40+
.entries()
41+
.filter(
42+
([, { displaySymbol, assetClass, key }]) =>
43+
filter.contains(displaySymbol, search) ||
44+
filter.contains(assetClass, search) ||
45+
filter.contains(key, search),
46+
),
47+
[feeds, search, filter],
48+
);
49+
const sortedFeeds = useMemo(
50+
() =>
51+
// eslint-disable-next-line unicorn/no-useless-spread
52+
[
53+
...filteredFeeds.map(([symbol, { displaySymbol }]) => ({
54+
id: symbol,
55+
displaySymbol,
56+
})),
57+
].toSorted((a, b) => collator.compare(a.displaySymbol, b.displaySymbol)),
58+
[filteredFeeds, collator],
5659
);
5760
return (
5861
<Select
@@ -80,22 +83,20 @@ export const PriceFeedSelect = ({ children, feeds }: Props) => {
8083
</SearchField>
8184
<Virtualizer layout={new ListLayout()}>
8285
<ListBox
83-
items={filteredFeeds}
86+
items={sortedFeeds}
8487
className={styles.listbox ?? ""}
8588
// eslint-disable-next-line jsx-a11y/no-autofocus
8689
autoFocus={false}
8790
>
88-
{({ assetClass, id, displaySymbol, icon }) => (
91+
{({ id, displaySymbol }) => (
8992
<ListBoxItem
9093
textValue={displaySymbol}
9194
className={styles.priceFeed ?? ""}
9295
href={`/price-feeds/${encodeURIComponent(id)}`}
93-
data-is-first={id === filteredFeeds[0]?.id ? "" : undefined}
96+
data-is-first={id === sortedFeeds[0]?.id ? "" : undefined}
9497
>
95-
<PriceFeedTag compact symbol={displaySymbol} icon={icon} />
96-
<Badge variant="neutral" style="outline" size="xs">
97-
{assetClass.toUpperCase()}
98-
</Badge>
98+
<PriceFeedTag compact symbol={id} />
99+
<AssetClassTag symbol={id} />
99100
</ListBoxItem>
100101
)}
101102
</ListBox>

apps/insights/src/components/PriceFeed/reference-data.tsx

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
"use client";
22

3-
import { Badge } from "@pythnetwork/component-library/Badge";
43
import { Table } from "@pythnetwork/component-library/Table";
54
import { useMemo } from "react";
65
import { useCollator } from "react-aria";
76

87
import styles from "./reference-data.module.scss";
8+
import { AssetClassTag } from "../AssetClassTag";
99
import { LiveValue } from "../LivePrices";
1010

1111
type Props = {
@@ -42,11 +42,7 @@ export const ReferenceData = ({ feed }: Props) => {
4242
() =>
4343
[
4444
...Object.entries({
45-
"Asset Type": (
46-
<Badge variant="neutral" style="outline" size="xs">
47-
{feed.assetClass.toUpperCase()}
48-
</Badge>
49-
),
45+
"Asset Type": <AssetClassTag symbol={feed.symbol} />,
5046
Base: feed.base,
5147
Description: feed.description,
5248
Symbol: feed.symbol,
Lines changed: 73 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1,49 +1,93 @@
1+
"use client";
2+
13
import { Skeleton } from "@pythnetwork/component-library/Skeleton";
24
import clsx from "clsx";
35
import { type ComponentProps, type ReactNode, Fragment } from "react";
46

57
import styles from "./index.module.scss";
8+
import { usePriceFeeds } from "../../hooks/use-price-feeds";
9+
import { omitKeys } from "../../omit-keys";
610

7-
type OwnProps =
8-
| { isLoading: true; compact?: boolean | undefined }
9-
| ({
11+
type OwnProps = { compact?: boolean | undefined } & (
12+
| { isLoading: true }
13+
| {
1014
isLoading?: false;
1115
symbol: string;
12-
icon: ReactNode;
13-
} & (
14-
| { compact: true }
15-
| {
16-
compact?: false;
17-
description: string;
18-
}
19-
));
16+
}
17+
);
2018

2119
type Props = Omit<ComponentProps<"div">, keyof OwnProps> & OwnProps;
2220

23-
export const PriceFeedTag = ({ className, ...props }: Props) => {
24-
// eslint-disable-next-line @typescript-eslint/no-unused-vars
25-
const { compact, ...propsWithoutCompact } = props;
21+
export const PriceFeedTag = (props: Props) => {
22+
return props.isLoading ? (
23+
<PriceFeedTagImpl {...props} />
24+
) : (
25+
<LoadedPriceFeedTag {...props} />
26+
);
27+
};
28+
29+
const LoadedPriceFeedTag = ({
30+
symbol,
31+
...props
32+
}: Props & { isLoading?: false }) => {
33+
const feed = usePriceFeeds().get(symbol);
34+
if (feed) {
35+
const [firstPart, ...rest] = feed.displaySymbol.split("/");
36+
return (
37+
<PriceFeedTagImpl
38+
description={feed.description}
39+
feedName={[firstPart ?? "", ...rest]}
40+
icon={feed.icon}
41+
{...props}
42+
/>
43+
);
44+
} else {
45+
throw new NoSuchFeedError(symbol);
46+
}
47+
};
48+
49+
type OwnImplProps = { compact?: boolean | undefined } & (
50+
| { isLoading: true }
51+
| {
52+
isLoading?: false;
53+
feedName: [string, ...string[]];
54+
icon: ReactNode;
55+
description: string;
56+
}
57+
);
58+
59+
type ImplProps = Omit<ComponentProps<"div">, keyof OwnImplProps> & OwnImplProps;
60+
61+
const PriceFeedTagImpl = ({ className, compact, ...props }: ImplProps) => {
2662
return (
2763
<div
2864
className={clsx(styles.priceFeedTag, className)}
29-
data-compact={props.compact ? "" : undefined}
65+
data-compact={compact ? "" : undefined}
3066
data-loading={props.isLoading ? "" : undefined}
31-
{...propsWithoutCompact}
67+
{...omitKeys(props, ["feedName", "icon", "description"])}
3268
>
3369
{props.isLoading ? (
3470
<Skeleton fill className={styles.icon} />
3571
) : (
3672
<div className={styles.icon}>{props.icon}</div>
3773
)}
3874
<div className={styles.nameAndDescription}>
39-
{props.isLoading ? (
40-
<div className={styles.name}>
75+
<div className={styles.name}>
76+
{props.isLoading ? (
4177
<Skeleton width={30} />
42-
</div>
43-
) : (
44-
<FeedName className={styles.name} symbol={props.symbol} />
45-
)}
46-
{!props.compact && (
78+
) : (
79+
<>
80+
<span className={styles.firstPart}>{props.feedName[0]}</span>
81+
{props.feedName.slice(1).map((part, i) => (
82+
<Fragment key={i}>
83+
<span className={styles.divider}>/</span>
84+
<span className={styles.part}>{part}</span>
85+
</Fragment>
86+
))}
87+
</>
88+
)}
89+
</div>
90+
{!compact && (
4791
<div className={styles.description}>
4892
{props.isLoading ? (
4993
<Skeleton width={50} />
@@ -57,22 +101,9 @@ export const PriceFeedTag = ({ className, ...props }: Props) => {
57101
);
58102
};
59103

60-
type OwnFeedNameProps = { symbol: string };
61-
type FeedNameProps = Omit<ComponentProps<"div">, keyof OwnFeedNameProps> &
62-
OwnFeedNameProps;
63-
64-
const FeedName = ({ symbol, className, ...props }: FeedNameProps) => {
65-
const [firstPart, ...parts] = symbol.split("/");
66-
67-
return (
68-
<div className={clsx(styles.priceFeedName, className)} {...props}>
69-
<span className={styles.firstPart}>{firstPart}</span>
70-
{parts.map((part, i) => (
71-
<Fragment key={i}>
72-
<span className={styles.divider}>/</span>
73-
<span className={styles.part}>{part}</span>
74-
</Fragment>
75-
))}
76-
</div>
77-
);
78-
};
104+
class NoSuchFeedError extends Error {
105+
constructor(symbol: string) {
106+
super(`No feed exists named ${symbol}`);
107+
this.name = "NoSuchFeedError";
108+
}
109+
}

0 commit comments

Comments
 (0)