Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 9 additions & 9 deletions apps/insights/src/components/PriceFeed/price-feed-select.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,15 +36,15 @@ export const PriceFeedSelect = ({ children }: Props) => {
const filteredFeeds = useMemo(
() =>
search === ""
? feeds.entries()
: feeds
.entries()
.filter(
([, { displaySymbol, assetClass, key }]) =>
filter.contains(displaySymbol, search) ||
filter.contains(assetClass, search) ||
filter.contains(key[Cluster.Pythnet], search),
),
? // This is inefficient but Safari doesn't support `Iterator.filter`, see
// https://bugs.webkit.org/show_bug.cgi?id=248650
[...feeds.entries()]
: [...feeds.entries()].filter(
([, { displaySymbol, assetClass, key }]) =>
filter.contains(displaySymbol, search) ||
filter.contains(assetClass, search) ||
filter.contains(key[Cluster.Pythnet], search),
),
[feeds, search, filter],
);
const sortedFeeds = useMemo(
Expand Down
2 changes: 1 addition & 1 deletion apps/insights/src/components/Root/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ const getPublishersForSearchDialog = async (cluster: Cluster) => {
const knownPublisher = lookupPublisher(publisher.key);

return {
id: publisher.key,
publisherKey: publisher.key,
averageScore: publisher.averageScore,
cluster,
...(knownPublisher && {
Expand Down
221 changes: 130 additions & 91 deletions apps/insights/src/components/Root/search-dialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
ListBox,
ListBoxItem,
} from "@pythnetwork/component-library/unstyled/ListBox";
import { useRouter } from "next/navigation";
import {
type ReactNode,
useState,
Expand All @@ -23,7 +24,7 @@ import {
use,
useMemo,
} from "react";
import { useCollator, useFilter } from "react-aria";
import { RouterProvider, useCollator, useFilter } from "react-aria";

import styles from "./search-dialog.module.scss";
import { usePriceFeeds } from "../../hooks/use-price-feeds";
Expand All @@ -46,7 +47,7 @@ const SearchDialogOpenContext = createContext<
type Props = {
children: ReactNode;
publishers: ({
id: string;
publisherKey: string;
averageScore: number;
cluster: Cluster;
} & (
Expand All @@ -63,54 +64,85 @@ export const SearchDialogProvider = ({ children, publishers }: Props) => {
const filter = useFilter({ sensitivity: "base", usage: "search" });
const feeds = usePriceFeeds();

const close = useCallback(() => {
searchDialogState.close();
setTimeout(() => {
setSearch("");
setType("");
}, CLOSE_DURATION_IN_MS);
}, [searchDialogState, setSearch, setType]);
const close = useCallback(
() =>
new Promise<void>((resolve) => {
searchDialogState.close();
setTimeout(() => {
setSearch("");
setType("");
resolve();
}, CLOSE_DURATION_IN_MS);
}),
[searchDialogState, setSearch, setType],
);

const handleOpenChange = useCallback(
(isOpen: boolean) => {
if (!isOpen) {
close();
close().catch(() => {
/* no-op since this actually can't fail */
});
}
},
[close],
);

const router = useRouter();
const handleOpenItem = useCallback(
(href: string) => {
close()
.then(() => {
router.push(href);
})
.catch(() => {
/* no-op since this actually can't fail */
});
},
[close, router],
);

const results = useMemo(
() =>
[
...(type === ResultType.Publisher
? []
: feeds
.entries()
: // This is inefficient but Safari doesn't support `Iterator.filter`,
// see https://bugs.webkit.org/show_bug.cgi?id=248650
[...feeds.entries()]
.filter(([, { displaySymbol }]) =>
filter.contains(displaySymbol, search),
)
.map(([symbol, feed]) => ({
.map(([symbol, { assetClass, displaySymbol }]) => ({
type: ResultType.PriceFeed as const,
id: symbol,
...feed,
assetClass,
displaySymbol,
}))),
...(type === ResultType.PriceFeed
? []
: publishers
.filter(
(publisher) =>
filter.contains(publisher.id, search) ||
filter.contains(publisher.publisherKey, search) ||
(publisher.name && filter.contains(publisher.name, search)),
)
.map((publisher) => ({
type: ResultType.Publisher as const,
id: [
ClusterToName[publisher.cluster],
publisher.publisherKey,
].join(":"),
...publisher,
}))),
].sort((a, b) =>
collator.compare(
a.type === ResultType.PriceFeed ? a.displaySymbol : (a.name ?? a.id),
b.type === ResultType.PriceFeed ? b.displaySymbol : (b.name ?? b.id),
a.type === ResultType.PriceFeed
? a.displaySymbol
: (a.name ?? a.publisherKey),
b.type === ResultType.PriceFeed
? b.displaySymbol
: (b.name ?? b.publisherKey),
),
),
[feeds, publishers, collator, filter, search, type],
Expand Down Expand Up @@ -182,80 +214,87 @@ export const SearchDialogProvider = ({ children, publishers }: Props) => {
</Button>
</div>
<div className={styles.body}>
<Virtualizer layout={new ListLayout()}>
<ListBox
aria-label="Search"
items={results}
className={styles.listbox ?? ""}
// eslint-disable-next-line jsx-a11y/no-autofocus
autoFocus={false}
// @ts-expect-error looks like react-aria isn't exposing this
// property in the typescript types correctly...
shouldFocusOnHover
onAction={close}
emptyState={
<NoResults
query={search}
onClearSearch={() => {
setSearch("");
}}
/>
}
>
{(result) => (
<ListBoxItem
textValue={
result.type === ResultType.PriceFeed
? result.displaySymbol
: (result.name ?? result.id)
}
className={styles.item ?? ""}
href={`${result.type === ResultType.PriceFeed ? "/price-feeds" : `/publishers/${ClusterToName[result.cluster]}`}/${encodeURIComponent(result.id)}`}
data-is-first={result.id === results[0]?.id ? "" : undefined}
>
<div className={styles.itemType}>
<Badge
variant={
result.type === ResultType.PriceFeed
? "warning"
: "info"
}
style="filled"
size="xs"
>
{result.type === ResultType.PriceFeed
? "PRICE FEED"
: "PUBLISHER"}
</Badge>
</div>
{result.type === ResultType.PriceFeed ? (
<>
<PriceFeedTag
compact
symbol={result.id}
className={styles.itemTag}
/>
<AssetClassTag symbol={result.id} />
</>
) : (
<>
<PublisherTag
className={styles.itemTag}
compact
cluster={result.cluster}
publisherKey={result.id}
{...(result.name && {
name: result.name,
icon: result.icon,
})}
/>
<Score score={result.averageScore} />
</>
)}
</ListBoxItem>
)}
</ListBox>
</Virtualizer>
<RouterProvider navigate={handleOpenItem}>
<Virtualizer layout={new ListLayout()}>
<ListBox
aria-label="Search"
items={results}
className={styles.listbox ?? ""}
// eslint-disable-next-line jsx-a11y/no-autofocus
autoFocus={false}
// @ts-expect-error looks like react-aria isn't exposing this
// property in the typescript types correctly...
shouldFocusOnHover
emptyState={
<NoResults
query={search}
onClearSearch={() => {
setSearch("");
}}
/>
}
>
{(result) => (
<ListBoxItem
textValue={
result.type === ResultType.PriceFeed
? result.displaySymbol
: (result.name ?? result.publisherKey)
}
className={styles.item ?? ""}
href={
result.type === ResultType.PriceFeed
? `/price-feeds/${encodeURIComponent(result.id)}`
: `/publishers/${ClusterToName[result.cluster]}/${encodeURIComponent(result.publisherKey)}`
}
data-is-first={
result.id === results[0]?.id ? "" : undefined
}
>
<div className={styles.itemType}>
<Badge
variant={
result.type === ResultType.PriceFeed
? "warning"
: "info"
}
style="filled"
size="xs"
>
{result.type === ResultType.PriceFeed
? "PRICE FEED"
: "PUBLISHER"}
</Badge>
</div>
{result.type === ResultType.PriceFeed ? (
<>
<PriceFeedTag
compact
symbol={result.id}
className={styles.itemTag}
/>
<AssetClassTag symbol={result.id} />
</>
) : (
<>
<PublisherTag
className={styles.itemTag}
compact
cluster={result.cluster}
publisherKey={result.publisherKey}
{...(result.name && {
name: result.name,
icon: result.icon,
})}
/>
<Score score={result.averageScore} />
</>
)}
</ListBoxItem>
)}
</ListBox>
</Virtualizer>
</RouterProvider>
</div>
</ModalDialog>
</>
Expand Down
Loading