From 402fbc2b047f390e0cbac450b26cbcbf60fbabd2 Mon Sep 17 00:00:00 2001 From: Connor Prussin Date: Mon, 9 Jun 2025 15:33:15 -0700 Subject: [PATCH 1/4] feat(entropy-explorer): bind to live apis --- apps/entropy-explorer/package.json | 3 - .../src/components/Address/index.module.scss | 16 - .../src/components/Address/index.tsx | 28 +- .../src/components/Home/chain-select.tsx | 126 ----- .../src/components/Home/index.module.scss | 9 + .../src/components/Home/index.tsx | 116 ++++- .../Home/request-drawer.module.scss | 13 + .../src/components/Home/request-drawer.tsx | 89 +++- .../src/components/Home/results.module.scss | 3 - .../src/components/Home/results.tsx | 173 ++----- .../src/components/Home/search-bar.tsx | 37 -- ...odule.scss => search-controls.module.scss} | 7 - .../src/components/Home/search-controls.tsx | 303 +++++++++++ .../src/components/Home/status-select.tsx | 95 ---- .../src/components/Home/use-query.ts | 87 ---- .../src/components/Root/evm-provider.tsx | 21 - .../src/components/Root/index.tsx | 3 +- .../src/components/Timestamp/index.tsx | 16 +- .../src/entropy-deployments.ts | 471 ------------------ .../src/entropy-deployments.tsx | 421 ++++++++++++++++ apps/entropy-explorer/src/errors.ts | 37 +- apps/entropy-explorer/src/pages.ts | 3 + apps/entropy-explorer/src/requests.ts | 274 ++++++---- .../component-library/src/ErrorPage/index.tsx | 2 + .../component-library/src/Header/index.tsx | 18 +- .../src/Paginator/index.module.scss | 7 + .../component-library/src/Paginator/index.tsx | 19 + .../src/Select/index.module.scss | 10 + .../component-library/src/Select/index.tsx | 14 +- pnpm-lock.yaml | 51 +- 30 files changed, 1296 insertions(+), 1176 deletions(-) delete mode 100644 apps/entropy-explorer/src/components/Home/chain-select.tsx delete mode 100644 apps/entropy-explorer/src/components/Home/search-bar.tsx rename apps/entropy-explorer/src/components/Home/{chain-select.module.scss => search-controls.module.scss} (60%) create mode 100644 apps/entropy-explorer/src/components/Home/search-controls.tsx delete mode 100644 apps/entropy-explorer/src/components/Home/status-select.tsx delete mode 100644 apps/entropy-explorer/src/components/Home/use-query.ts delete mode 100644 apps/entropy-explorer/src/components/Root/evm-provider.tsx delete mode 100644 apps/entropy-explorer/src/entropy-deployments.ts create mode 100644 apps/entropy-explorer/src/entropy-deployments.tsx create mode 100644 apps/entropy-explorer/src/pages.ts diff --git a/apps/entropy-explorer/package.json b/apps/entropy-explorer/package.json index e358138090..7e6c6c3c19 100644 --- a/apps/entropy-explorer/package.json +++ b/apps/entropy-explorer/package.json @@ -23,15 +23,12 @@ "@phosphor-icons/react": "catalog:", "@pythnetwork/component-library": "workspace:*", "clsx": "catalog:", - "connectkit": "catalog:", "next": "catalog:", "nuqs": "catalog:", "react": "catalog:", "react-aria": "catalog:", "react-dom": "catalog:", "react-timeago": "catalog:", - "viem": "catalog:", - "wagmi": "catalog:", "zod": "catalog:" }, "devDependencies": { diff --git a/apps/entropy-explorer/src/components/Address/index.module.scss b/apps/entropy-explorer/src/components/Address/index.module.scss index 8c3aa684b1..adebc99a72 100644 --- a/apps/entropy-explorer/src/components/Address/index.module.scss +++ b/apps/entropy-explorer/src/components/Address/index.module.scss @@ -5,20 +5,4 @@ flex-flow: row nowrap; gap: theme.spacing(2); font-size: theme.font-size("sm"); - - .full { - display: none; - } - - &:not([data-always-truncate]) { - @include theme.breakpoint("xl") { - .truncated { - display: none; - } - - .full { - display: unset; - } - } - } } diff --git a/apps/entropy-explorer/src/components/Address/index.tsx b/apps/entropy-explorer/src/components/Address/index.tsx index e1005cf5f3..980419f066 100644 --- a/apps/entropy-explorer/src/components/Address/index.tsx +++ b/apps/entropy-explorer/src/components/Address/index.tsx @@ -9,24 +9,32 @@ import { truncate } from "../../truncate"; type Props = { value: string; chain: keyof typeof EntropyDeployments; - alwaysTruncate?: boolean | undefined; + isAccount?: boolean | undefined; }; -export const Address = ({ value, chain, alwaysTruncate }: Props) => { - const { explorer } = EntropyDeployments[chain]; +export const Account = (props: Omit) => ( +
+); + +export const Transaction = (props: Omit) => ( +
+); + +const Address = ({ value, chain, isAccount }: Props) => { + const { explorerTxTemplate, explorerAccountTemplate } = + EntropyDeployments[chain]; + const explorerTemplate = isAccount + ? explorerAccountTemplate + : explorerTxTemplate; const truncatedValue = useMemo(() => truncate(value), [value]); return ( -
+
- {truncatedValue} - {value} + {truncatedValue}
diff --git a/apps/entropy-explorer/src/components/Home/chain-select.tsx b/apps/entropy-explorer/src/components/Home/chain-select.tsx deleted file mode 100644 index 4d7216ee8e..0000000000 --- a/apps/entropy-explorer/src/components/Home/chain-select.tsx +++ /dev/null @@ -1,126 +0,0 @@ -"use client"; - -import type { Props as SelectProps } from "@pythnetwork/component-library/Select"; -import { Select } from "@pythnetwork/component-library/Select"; -import { ChainIcon } from "connectkit"; -import type { ComponentProps } from "react"; -import { Suspense, useCallback, useMemo } from "react"; -import { useCollator } from "react-aria"; -import * as viemChains from "viem/chains"; - -import styles from "./chain-select.module.scss"; -import { useQuery } from "./use-query"; -import { EntropyDeployments } from "../../entropy-deployments"; -import type { ConstrainedOmit } from "../../type-utils"; - -export const ChainSelect = ( - props: ComponentProps, -) => ( - - } - > - - -); - -type Deployment = - | ReturnType[number] - | { id: "all" }; - -const ResolvedChainSelect = ( - props: ConstrainedOmit< - SelectProps, - keyof typeof defaultProps | keyof ReturnType - >, -) => { - const resolvedProps = useResolvedProps(); - - return ; + +const useStatusSelect = () => { + const status = useSearchParam({ + paramName: "status", + parse: parseStatus, + defaultValue: "all", + serialize: serializeStatus, + }); + + return { + selectedKey: status.value, + onSelectionChange: status.onChange, + isPending: status.isTransitioning, + optionGroups: useMemo( + () => [ + { + name: "All", + options: [{ id: "all" as const }], + }, + { + name: "Statuses", + options: [ + { id: Status.Complete }, + { id: Status.Pending }, + { id: Status.CallbackError }, + ], + }, + ], + [], + ), + show: useCallback( + (status: { id: Status | "all" }) => + status.id === "all" ? ( + "All" + ) : ( + + ), + [], + ), + buttonLabel: + status.value === "all" ? ( + "Status" + ) : ( + + ), + }; +}; + +const parseStatus = (value: string) => { + switch (value) { + case StatusParams[Status.Pending]: { + return Status.Pending; + } + case StatusParams[Status.CallbackError]: { + return Status.CallbackError; + } + case StatusParams[Status.Complete]: { + return Status.Complete; + } + default: { + return "all"; + } + } +}; + +const serializeStatus = (value: ReturnType) => + value === "all" ? "" : StatusParams[value]; + +type Deployment = + | ReturnType[number] + | { id: "all" }; + +export const ChainSelect = ( + props: ConstrainedOmit< + SelectProps, + keyof ReturnType + >, +) => { + const chainSelectProps = useChainSelect(); + + return ; -}; - -const useResolvedProps = () => { - const { status, setStatus } = useQuery(); - const chains = useMemo( - () => [ - { - name: "All", - options: [{ id: "all" as const }], - }, - { - name: "Statuses", - options: [ - { id: Status.Complete }, - { id: Status.Pending }, - { id: Status.CallbackError }, - ], - }, - ], - [], - ); - - const showStatus = useCallback( - (status: (typeof chains)[number]["options"][number]) => - status.id === "all" ? ( - "All" - ) : ( - - ), - [], - ); - - return { - selectedKey: status ?? ("all" as const), - onSelectionChange: setStatus, - optionGroups: chains, - show: showStatus, - buttonLabel: - status === null ? ( - "Status" - ) : ( - - ), - }; -}; - -const defaultProps = { - label: "Status", - hideLabel: true, - defaultButtonLabel: "Status", - hideGroupLabel: true, -} as const; diff --git a/apps/entropy-explorer/src/components/Home/use-query.ts b/apps/entropy-explorer/src/components/Home/use-query.ts deleted file mode 100644 index f5d6144954..0000000000 --- a/apps/entropy-explorer/src/components/Home/use-query.ts +++ /dev/null @@ -1,87 +0,0 @@ -import { useLogger } from "@pythnetwork/component-library/useLogger"; -import { useQueryStates, parseAsString, parseAsStringEnum } from "nuqs"; -import { useCallback, useMemo } from "react"; - -import { EntropyDeployments } from "../../entropy-deployments"; -import { Status } from "../../requests"; - -const StatusParams = { - [Status.Pending]: "pending", - [Status.Complete]: "complete", - [Status.CallbackError]: "callback-error", -} as const; - -const queryParams = { - status: parseAsStringEnum<(typeof StatusParams)[Status]>( - Object.values(StatusParams), - ), - search: parseAsString.withDefault(""), - chain: parseAsStringEnum( - Object.keys(EntropyDeployments) as (keyof typeof EntropyDeployments)[], - ), -}; - -export const useQuery = () => { - const logger = useLogger(); - const [{ search, chain, status }, setQuery] = useQueryStates(queryParams); - - const updateQuery = useCallback( - (newQuery: Parameters[0]) => { - setQuery(newQuery).catch((error: unknown) => { - logger.error("Failed to update query", error); - }); - }, - [setQuery, logger], - ); - - const setSearch = useCallback( - (newSearch: string) => { - updateQuery({ search: newSearch }); - }, - [updateQuery], - ); - - const setChain = useCallback( - (newChain: keyof typeof EntropyDeployments | "all") => { - // eslint-disable-next-line unicorn/no-null - updateQuery({ chain: newChain === "all" ? null : newChain }); - }, - [updateQuery], - ); - - const setStatus = useCallback( - (newStatus: Status | "all") => { - updateQuery({ - // eslint-disable-next-line unicorn/no-null - status: newStatus === "all" ? null : StatusParams[newStatus], - }); - }, - [updateQuery], - ); - - return { - search, - chain, - status: useMemo(() => { - switch (status) { - case "pending": { - return Status.Pending; - } - case "callback-error": { - return Status.CallbackError; - } - case "complete": { - return Status.Complete; - } - // eslint-disable-next-line unicorn/no-null - case null: { - // eslint-disable-next-line unicorn/no-null - return null; - } - } - }, [status]), - setSearch, - setChain, - setStatus, - }; -}; diff --git a/apps/entropy-explorer/src/components/Root/evm-provider.tsx b/apps/entropy-explorer/src/components/Root/evm-provider.tsx deleted file mode 100644 index 1e6b56e636..0000000000 --- a/apps/entropy-explorer/src/components/Root/evm-provider.tsx +++ /dev/null @@ -1,21 +0,0 @@ -"use client"; - -import type { ReactNode } from "react"; -import { mainnet } from "viem/chains"; -import { WagmiProvider, createConfig, http } from "wagmi"; - -// We only use wagmi because we use connectkit to get chain icons, and -// connectkit blows up if there isn't a wagmi context initialized. However, the -// wagmi config isn't actually used when fetching chain icons. But wagmi -// requires at least one chain to create a config, so we'll just inject mainnet -// here to make everyone happy. -export const EvmProvider = ({ children }: { children: ReactNode }) => ( - - {children} - -); diff --git a/apps/entropy-explorer/src/components/Root/index.tsx b/apps/entropy-explorer/src/components/Root/index.tsx index 00ae4cdc39..35218a6128 100644 --- a/apps/entropy-explorer/src/components/Root/index.tsx +++ b/apps/entropy-explorer/src/components/Root/index.tsx @@ -2,7 +2,6 @@ import { AppShell } from "@pythnetwork/component-library/AppShell"; import { NuqsAdapter } from "nuqs/adapters/next/app"; import type { ReactNode } from "react"; -import { EvmProvider } from "./evm-provider"; import { ENABLE_ACCESSIBILITY_REPORTING, GOOGLE_ANALYTICS_ID, @@ -23,7 +22,7 @@ export const Root = ({ children }: Props) => ( label: "Entropy Docs", href: "https://docs.pyth.network/entropy", }} - providers={[EvmProvider, NuqsAdapter]} + providers={[NuqsAdapter]} > {children} diff --git a/apps/entropy-explorer/src/components/Timestamp/index.tsx b/apps/entropy-explorer/src/components/Timestamp/index.tsx index 44a1069a58..c5705dfa9e 100644 --- a/apps/entropy-explorer/src/components/Timestamp/index.tsx +++ b/apps/entropy-explorer/src/components/Timestamp/index.tsx @@ -1,11 +1,19 @@ import { Clock } from "@phosphor-icons/react/dist/ssr/Clock"; import { Button } from "@pythnetwork/component-library/unstyled/Button"; import { useState } from "react"; +import { useIsSSR } from "react-aria"; import TimeAgo from "react-timeago"; import styles from "./index.module.scss"; -export const Timestamp = ({ timestamp }: { timestamp: Date }) => { +export const Timestamp = ({ + timestamp, + now, +}: { + timestamp: Date; + now: Date; +}) => { + const isSSR = useIsSSR(); const [showRelative, setShowRelative] = useState(true); const month = timestamp.toLocaleString("default", { month: "long", @@ -16,6 +24,7 @@ export const Timestamp = ({ timestamp }: { timestamp: Date }) => { const hour = timestamp.getUTCHours().toString().padStart(2, "0"); const minute = timestamp.getUTCMinutes().toString().padStart(2, "0"); const seconds = timestamp.getUTCSeconds().toString().padStart(2, "0"); + return ( {extraCta} - + ); -const MobileMenuContents = () => ( +const MobileMenuContents = ({ mainCta }: { mainCta: Props["mainCta"] }) => (
diff --git a/packages/component-library/src/Paginator/index.module.scss b/packages/component-library/src/Paginator/index.module.scss index c6a174ec87..84e69d9177 100644 --- a/packages/component-library/src/Paginator/index.module.scss +++ b/packages/component-library/src/Paginator/index.module.scss @@ -37,7 +37,14 @@ .paginatorToolbar { display: flex; flex-flow: row nowrap; + align-items: center; gap: theme.spacing(1); + position: relative; + + .spinner { + position: absolute; + left: -#{theme.spacing(8)}; + } .selectedPage { cursor: text; diff --git a/packages/component-library/src/Paginator/index.tsx b/packages/component-library/src/Paginator/index.tsx index 94075803f4..b7724f9218 100644 --- a/packages/component-library/src/Paginator/index.tsx +++ b/packages/component-library/src/Paginator/index.tsx @@ -10,15 +10,18 @@ import type { Props as ButtonProps } from "../Button/index.jsx"; import { Button } from "../Button/index.jsx"; import buttonStyles from "../Button/index.module.scss"; import { Select } from "../Select/index.jsx"; +import { Spinner } from "../Spinner/index.jsx"; import { Toolbar } from "../unstyled/Toolbar/index.jsx"; type Props = { numPages: number; currentPage: number; onPageChange: (newPage: number) => void; + isPageTransitioning: boolean; pageSize: number; pageSizeOptions: number[]; onPageSizeChange: (newPageSize: number) => void; + isPageSizeTransitioning: boolean; mkPageLink?: ((page: number) => string) | undefined; className?: string | undefined; }; @@ -26,10 +29,12 @@ type Props = { export const Paginator = ({ numPages, currentPage, + isPageTransitioning, pageSize, pageSizeOptions, onPageChange, onPageSizeChange, + isPageSizeTransitioning, mkPageLink, className, }: Props) => ( @@ -38,6 +43,7 @@ export const Paginator = ({ pageSize={pageSize} pageSizeOptions={pageSizeOptions} onPageSizeChange={onPageSizeChange} + isPending={isPageSizeTransitioning} /> {numPages > 1 && ( )}
@@ -54,12 +61,14 @@ type PageSizeSelectProps = { pageSize: number; pageSizeOptions: number[]; onPageSizeChange: (newPageSize: number) => void; + isPending: boolean; }; const PageSizeSelect = ({ pageSize, onPageSizeChange, pageSizeOptions, + isPending, }: PageSizeSelectProps) => (