"use client"; import { ChartLine } from "@phosphor-icons/react/dist/ssr/ChartLine"; import { useLogger } from "@pythnetwork/app-logger"; import { Badge } from "@pythnetwork/component-library/Badge"; import { Card } from "@pythnetwork/component-library/Card"; import { Paginator } from "@pythnetwork/component-library/Paginator"; import { SearchInput } from "@pythnetwork/component-library/SearchInput"; import { Select } from "@pythnetwork/component-library/Select"; import { type RowConfig, type SortDescriptor, Table, } from "@pythnetwork/component-library/Table"; import { useQueryState, parseAsString } from "nuqs"; import { Suspense, useCallback, useMemo } from "react"; import { useFilter, useCollator } from "react-aria"; import { usePriceFeeds } from "../../hooks/use-price-feeds"; import { useQueryParamFilterPagination } from "../../hooks/use-query-param-filter-pagination"; import { AssetClassTag } from "../AssetClassTag"; import { FeedKey } from "../FeedKey"; import { SKELETON_WIDTH, LivePrice, LiveConfidence, LiveValue, } from "../LivePrices"; import { NoResults } from "../NoResults"; import { PriceFeedTag } from "../PriceFeedTag"; import rootStyles from "../Root/index.module.scss"; type Props = { id: string; priceFeeds: PriceFeed[]; }; type PriceFeed = { symbol: string; exponent: number; numQuoters: number; }; export const PriceFeedsCard = ({ priceFeeds, ...props }: Props) => ( }> ); const ResolvedPriceFeedsCard = ({ priceFeeds, ...props }: Props) => { const logger = useLogger(); const collator = useCollator(); const filter = useFilter({ sensitivity: "base", usage: "search" }); const [assetClass, setAssetClass] = useQueryState( "assetClass", parseAsString.withDefault(""), ); const feeds = usePriceFeeds(); const priceFeedsWithContextInfo = useMemo( () => priceFeeds.map((feed) => { const contextFeed = feeds.get(feed.symbol); if (contextFeed) { return { ...feed, assetClass: contextFeed.assetClass, displaySymbol: contextFeed.displaySymbol, key: contextFeed.key, }; } else { throw new NoSuchFeedError(feed.symbol); } }), [feeds, priceFeeds], ); const feedsFilteredByAssetClass = useMemo( () => assetClass ? priceFeedsWithContextInfo.filter( (feed) => feed.assetClass === assetClass, ) : priceFeedsWithContextInfo, [assetClass, priceFeedsWithContextInfo], ); const { search, sortDescriptor, page, pageSize, updateSearch, updateSortDescriptor, updatePage, updatePageSize, paginatedItems, numResults, numPages, mkPageLink, } = useQueryParamFilterPagination( feedsFilteredByAssetClass, (priceFeed, search) => { const searchTokens = search .split(" ") .flatMap((item) => item.split(",")) .filter(Boolean); return searchTokens.some((token) => filter.contains(priceFeed.displaySymbol, token), ); }, (a, b, { column, direction }) => { const field = column === "assetClass" ? "assetClass" : "displaySymbol"; return ( (direction === "descending" ? -1 : 1) * collator.compare(a[field], b[field]) ); }, { defaultSort: "priceFeedName" }, ); const rows = useMemo( () => paginatedItems.map(({ symbol, exponent, numQuoters, key }) => ({ id: symbol, href: `/price-feeds/${encodeURIComponent(symbol)}`, data: { exponent: ( ), numPublishers: ( ), price: , confidenceInterval: , priceFeedName: , assetClass: , priceFeedId: , }, })), [paginatedItems], ); const updateAssetClass = useCallback( (newAssetClass: string) => { updatePage(1); setAssetClass(newAssetClass).catch((error: unknown) => { logger.error("Failed to update asset class", error); }); }, [updatePage, setAssetClass, logger], ); const assetClasses = useMemo( () => [ ...new Set(priceFeedsWithContextInfo.map((feed) => feed.assetClass)), ].sort((a, b) => collator.compare(a, b)), [priceFeedsWithContextInfo, collator], ); return ( ); }; type PriceFeedsCardContents = Pick & ( | { isLoading: true } | { isLoading?: false; numResults: number; search: string; sortDescriptor: SortDescriptor; onSortChange: (newSort: SortDescriptor) => void; assetClass: string; assetClasses: string[]; numPages: number; page: number; pageSize: number; onSearchChange: (newSearch: string) => void; onAssetClassChange: (newAssetClass: string) => void; onPageSizeChange: (newPageSize: number) => void; onPageChange: (newPage: number) => void; mkPageLink: (page: number) => string; rows: RowConfig< | "priceFeedName" | "assetClass" | "priceFeedId" | "price" | "confidenceInterval" | "exponent" | "numPublishers" >[]; } ); const PriceFeedsCardContents = ({ id, ...props }: PriceFeedsCardContents) => ( } title={ <> Price Feeds {!props.isLoading && ( {props.numResults} )} } toolbar={ <> label="Asset Class" size="sm" variant="outline" hideLabel {...(props.isLoading ? { isPending: true, options: [], buttonLabel: "Asset Class" } : { optionGroups: [ { name: "All", options: [""] }, { name: "Asset classes", options: props.assetClasses }, ], hideGroupLabel: true, show: (value) => (value === "" ? "All" : value), placement: "bottom end", buttonLabel: props.assetClass === "" ? "Asset Class" : props.assetClass, selectedKey: props.assetClass, onSelectionChange: props.onAssetClassChange, })} /> } {...(!props.isLoading && { footer: ( ), })} > , allowsSorting: true, }, { id: "assetClass", name: "ASSET CLASS", alignment: "left", width: 75, loadingSkeletonWidth: 20, allowsSorting: true, }, { id: "priceFeedId", name: "PRICE FEED ID", alignment: "left", width: 50, loadingSkeletonWidth: 30, }, { id: "price", name: "PRICE", alignment: "right", width: 40, loadingSkeletonWidth: SKELETON_WIDTH, }, { id: "confidenceInterval", name: "CONFIDENCE INTERVAL", alignment: "left", width: 50, loadingSkeletonWidth: SKELETON_WIDTH, }, { id: "exponent", name: "EXPONENT", alignment: "left", width: 8, }, { id: "numPublishers", name: "# PUBLISHERS", alignment: "left", width: 8, }, ]} {...(props.isLoading ? { isLoading: true, } : { rows: props.rows, sortDescriptor: props.sortDescriptor, onSortChange: props.onSortChange, emptyState: ( { props.onSearchChange(""); }} /> ), })} /> ); class NoSuchFeedError extends Error { constructor(symbol: string) { super(`No feed exists named ${symbol}`); this.name = "NoSuchFeedError"; } }