|
|
@@ -14,6 +14,7 @@ import {
|
|
|
ListBox,
|
|
|
ListBoxItem,
|
|
|
} from "@pythnetwork/component-library/unstyled/ListBox";
|
|
|
+import { useRouter } from "next/navigation";
|
|
|
import {
|
|
|
type ReactNode,
|
|
|
useState,
|
|
|
@@ -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";
|
|
|
@@ -46,7 +47,7 @@ const SearchDialogOpenContext = createContext<
|
|
|
type Props = {
|
|
|
children: ReactNode;
|
|
|
publishers: ({
|
|
|
- id: string;
|
|
|
+ publisherKey: string;
|
|
|
averageScore: number;
|
|
|
cluster: Cluster;
|
|
|
} & (
|
|
|
@@ -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],
|
|
|
@@ -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>
|
|
|
</>
|