"use client"; import { XCircle } from "@phosphor-icons/react/dist/ssr/XCircle"; import { Badge } from "@pythnetwork/component-library/Badge"; import { Button } from "@pythnetwork/component-library/Button"; import { ModalDialog } from "@pythnetwork/component-library/ModalDialog"; import { SearchInput } from "@pythnetwork/component-library/SearchInput"; import { SingleToggleGroup } from "@pythnetwork/component-library/SingleToggleGroup"; import { Virtualizer, ListLayout, } from "@pythnetwork/component-library/Virtualizer"; import { ListBox, ListBoxItem, } from "@pythnetwork/component-library/unstyled/ListBox"; import { type ReactNode, useState, useCallback, useEffect, createContext, use, useMemo, } from "react"; import { useCollator, useFilter } from "react-aria"; import styles from "./search-dialog.module.scss"; import { usePriceFeeds } from "../../hooks/use-price-feeds"; import { AssetClassTag } from "../AssetClassTag"; import { NoResults } from "../NoResults"; import { PriceFeedTag } from "../PriceFeedTag"; import { PublisherTag } from "../PublisherTag"; import { Score } from "../Score"; const CLOSE_DURATION_IN_SECONDS = 0.1; const CLOSE_DURATION_IN_MS = CLOSE_DURATION_IN_SECONDS * 1000; const INPUTS = new Set(["input", "select", "button", "textarea"]); const SearchDialogOpenContext = createContext< ReturnType | undefined >(undefined); type Props = { children: ReactNode; publishers: ({ id: string; averageScore: number; } & ( | { name: string; icon: ReactNode } | { name?: undefined; icon?: undefined } ))[]; }; export const SearchDialogProvider = ({ children, publishers }: Props) => { const searchDialogState = useSearchDialogStateContext(); const [search, setSearch] = useState(""); const [type, setType] = useState(""); const collator = useCollator(); 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 handleOpenChange = useCallback( (isOpen: boolean) => { if (!isOpen) { close(); } }, [close], ); const results = useMemo( () => [ ...(type === ResultType.Publisher ? [] : feeds .entries() .filter(([, { displaySymbol }]) => filter.contains(displaySymbol, search), ) .map(([symbol, feed]) => ({ type: ResultType.PriceFeed as const, id: symbol, ...feed, }))), ...(type === ResultType.PriceFeed ? [] : publishers .filter( (publisher) => filter.contains(publisher.id, search) || (publisher.name && filter.contains(publisher.name, search)), ) .map((publisher) => ({ type: ResultType.Publisher as const, ...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), ), ), [feeds, publishers, collator, filter, search, type], ); return ( <> {children}
{ setSearch(""); }} /> } > {(result) => (
{result.type === ResultType.PriceFeed ? "PRICE FEED" : "PUBLISHER"}
{result.type === ResultType.PriceFeed ? ( <> ) : ( <> )}
)}
); }; enum ResultType { PriceFeed, Publisher, } const useSearchDialogStateContext = () => { const [isOpen, setIsOpen] = useState(false); const toggleIsOpen = useCallback(() => { setIsOpen((value) => !value); }, [setIsOpen]); const close = useCallback(() => { setIsOpen(false); }, [setIsOpen]); const open = useCallback(() => { setIsOpen(true); }, [setIsOpen]); const handleKeyDown = useCallback( (event: KeyboardEvent) => { const activeElement = document.activeElement; const tagName = activeElement?.tagName.toLowerCase(); const isEditing = !tagName || INPUTS.has(tagName) || (activeElement !== null && "isContentEditable" in activeElement && activeElement.isContentEditable); const isSlash = event.key === "/"; // Meta key for mac, ctrl key for non-mac const isCtrlK = event.key === "k" && (event.metaKey || event.ctrlKey); if (!isEditing && (isSlash || isCtrlK)) { event.preventDefault(); toggleIsOpen(); } }, [toggleIsOpen], ); useEffect(() => { window.addEventListener("keydown", handleKeyDown); return () => { window.removeEventListener("keydown", handleKeyDown); }; }, [handleKeyDown]); return { isOpen, setIsOpen, toggleIsOpen, open, close, }; }; const useSearchDialogState = () => { const value = use(SearchDialogOpenContext); if (value) { return value; } else { throw new NotInitializedError(); } }; export const useToggleSearchDialog = () => useSearchDialogState().toggleIsOpen; class NotInitializedError extends Error { constructor() { super("This component must be contained within a "); this.name = "NotInitializedError"; } }