From aeaeecb9ec4dddd7cd4826704f929af62e978470 Mon Sep 17 00:00:00 2001 From: gregfromstl Date: Fri, 21 Feb 2025 20:06:47 -0800 Subject: [PATCH 01/12] feat: routes page --- apps/dashboard/.env.example | 8 +- .../src/@/components/ui/CopyTextButton.tsx | 2 +- apps/dashboard/src/@/constants/env.ts | 3 + .../routes/components/client/pagination.tsx | 37 +++++ .../routes/components/client/search.tsx | 74 +++++++++ .../routes/components/client/type.tsx | 55 +++++++ .../routes/components/client/view.tsx | 50 ++++++ .../components/server/add-token-button.tsx | 18 ++ .../components/server/routelist-card.tsx | 115 +++++++++++++ .../components/server/routelist-row.tsx | 123 ++++++++++++++ .../routes/components/server/routes-table.tsx | 154 ++++++++++++++++++ .../(bridge)/routes/opengraph-image.png | Bin 0 -> 13648 bytes .../app/(dashboard)/(bridge)/routes/page.tsx | 70 ++++++++ .../app/(dashboard)/(bridge)/types/route.ts | 8 + .../src/app/(dashboard)/(bridge)/utils.ts | 52 ++++++ apps/dashboard/tests/chain-page.spec.ts | 2 +- apps/dashboard/tests/contract-page.spec.ts | 2 +- apps/dashboard/tests/homepage.spec.ts | 2 +- apps/dashboard/tests/publisher-page.spec.ts | 2 +- apps/dashboard/tests/setup.ts | 4 +- 20 files changed, 772 insertions(+), 9 deletions(-) create mode 100644 apps/dashboard/src/app/(dashboard)/(bridge)/routes/components/client/pagination.tsx create mode 100644 apps/dashboard/src/app/(dashboard)/(bridge)/routes/components/client/search.tsx create mode 100644 apps/dashboard/src/app/(dashboard)/(bridge)/routes/components/client/type.tsx create mode 100644 apps/dashboard/src/app/(dashboard)/(bridge)/routes/components/client/view.tsx create mode 100644 apps/dashboard/src/app/(dashboard)/(bridge)/routes/components/server/add-token-button.tsx create mode 100644 apps/dashboard/src/app/(dashboard)/(bridge)/routes/components/server/routelist-card.tsx create mode 100644 apps/dashboard/src/app/(dashboard)/(bridge)/routes/components/server/routelist-row.tsx create mode 100644 apps/dashboard/src/app/(dashboard)/(bridge)/routes/components/server/routes-table.tsx create mode 100644 apps/dashboard/src/app/(dashboard)/(bridge)/routes/opengraph-image.png create mode 100644 apps/dashboard/src/app/(dashboard)/(bridge)/routes/page.tsx create mode 100644 apps/dashboard/src/app/(dashboard)/(bridge)/types/route.ts create mode 100644 apps/dashboard/src/app/(dashboard)/(bridge)/utils.ts diff --git a/apps/dashboard/.env.example b/apps/dashboard/.env.example index 27bf2a4453f..2b945e30c70 100644 --- a/apps/dashboard/.env.example +++ b/apps/dashboard/.env.example @@ -6,10 +6,14 @@ # not required to build, defaults to prod NEXT_PUBLIC_THIRDWEB_DOMAIN="localhost:3000" -# API host. For local development, please use "https://api.thirdweb-preview.com" +# API host. For local development, please use "https://api.thirdweb-dev.com" # otherwise: "https://api.thirdweb.com" NEXT_PUBLIC_THIRDWEB_API_HOST="https://api.thirdweb-dev.com" +# Bridge API. For local development, please use "https://bridge.thirdweb-dev.com" +# otherwise: "https://bridge.thirdweb.com" +NEXT_PUBLIC_THIRDWEB_BRIDGE_HOST="https://bridge.thirdweb-dev.com" + # Paper API host NEXT_PUBLIC_THIRDWEB_EWS_API_HOST="https://ews.thirdweb-dev.com" @@ -97,4 +101,4 @@ REDIS_URL="" ANALYTICS_SERVICE_URL="" # Required for Nebula Chat -NEXT_PUBLIC_NEBULA_URL="" \ No newline at end of file +NEXT_PUBLIC_NEBULA_URL="" diff --git a/apps/dashboard/src/@/components/ui/CopyTextButton.tsx b/apps/dashboard/src/@/components/ui/CopyTextButton.tsx index b1a9c14d7fb..3f3272e773d 100644 --- a/apps/dashboard/src/@/components/ui/CopyTextButton.tsx +++ b/apps/dashboard/src/@/components/ui/CopyTextButton.tsx @@ -7,7 +7,7 @@ import { Button } from "./button"; import { ToolTipLabel } from "./tooltip"; export function CopyTextButton(props: { - textToShow: string; + textToShow: string | React.ReactNode; textToCopy: string; tooltip: string | undefined; className?: string; diff --git a/apps/dashboard/src/@/constants/env.ts b/apps/dashboard/src/@/constants/env.ts index e6e6ce636bd..8796d24c28a 100644 --- a/apps/dashboard/src/@/constants/env.ts +++ b/apps/dashboard/src/@/constants/env.ts @@ -23,6 +23,9 @@ export const DASHBOARD_STORAGE_URL = export const API_SERVER_URL = process.env.NEXT_PUBLIC_THIRDWEB_API_HOST || "https://api.thirdweb.com"; +export const BRIDGE_URL = + process.env.NEXT_PUBLIC_THIRDWEB_BRIDGE_HOST || "https://bridge.thirdweb.com"; + /** * Faucet stuff */ diff --git a/apps/dashboard/src/app/(dashboard)/(bridge)/routes/components/client/pagination.tsx b/apps/dashboard/src/app/(dashboard)/(bridge)/routes/components/client/pagination.tsx new file mode 100644 index 00000000000..2da9d3bede2 --- /dev/null +++ b/apps/dashboard/src/app/(dashboard)/(bridge)/routes/components/client/pagination.tsx @@ -0,0 +1,37 @@ +"use client"; + +import { PaginationButtons } from "@/components/pagination-buttons"; +import { useDashboardRouter } from "@/lib/DashboardRouter"; +import { usePathname, useSearchParams } from "next/navigation"; +import { useCallback } from "react"; + +type ChainlistPaginationProps = { + totalPages: number; + activePage: number; +}; + +export const ChainlistPagination: React.FC = ({ + activePage, + totalPages, +}) => { + const pathname = usePathname(); + const searchParams = useSearchParams(); + const router = useDashboardRouter(); + + const createPageURL = useCallback( + (pageNumber: number) => { + const params = new URLSearchParams(searchParams || undefined); + params.set("page", pageNumber.toString()); + return `${pathname}?${params.toString()}`; + }, + [pathname, searchParams], + ); + + return ( + router.push(createPageURL(page))} + /> + ); +}; diff --git a/apps/dashboard/src/app/(dashboard)/(bridge)/routes/components/client/search.tsx b/apps/dashboard/src/app/(dashboard)/(bridge)/routes/components/client/search.tsx new file mode 100644 index 00000000000..f6076300164 --- /dev/null +++ b/apps/dashboard/src/app/(dashboard)/(bridge)/routes/components/client/search.tsx @@ -0,0 +1,74 @@ +"use client"; + +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { useDashboardRouter } from "@/lib/DashboardRouter"; +import { SearchIcon, XCircleIcon } from "lucide-react"; +import { usePathname, useSearchParams } from "next/navigation"; +import { useEffect, useRef } from "react"; +import { useDebouncedCallback } from "use-debounce"; + +function cleanUrl(url: string) { + if (url.endsWith("?")) { + return url.slice(0, -1); + } + return url; +} + +export const SearchInput: React.FC = () => { + const router = useDashboardRouter(); + const pathname = usePathname(); + const searchParams = useSearchParams(); + + const inputRef = useRef(null); + + // eslint-disable-next-line no-restricted-syntax + useEffect(() => { + // reset the input if the query param is removed + if (inputRef.current?.value && !searchParams?.get("query")) { + inputRef.current.value = ""; + } + }, [searchParams]); + + const handleSearch = useDebouncedCallback((term: string) => { + const params = new URLSearchParams(searchParams ?? undefined); + if (term) { + params.set("query", term); + } else { + params.delete("query"); + } + // always delete the page number when searching + params.delete("page"); + const url = cleanUrl(`${pathname}?${params.toString()}`); + router.replace(url); + }, 300); + + return ( +
+ + handleSearch(e.target.value)} + ref={inputRef} + /> + {searchParams?.has("query") && ( + + )} +
+ ); +}; diff --git a/apps/dashboard/src/app/(dashboard)/(bridge)/routes/components/client/type.tsx b/apps/dashboard/src/app/(dashboard)/(bridge)/routes/components/client/type.tsx new file mode 100644 index 00000000000..450e1641b91 --- /dev/null +++ b/apps/dashboard/src/app/(dashboard)/(bridge)/routes/components/client/type.tsx @@ -0,0 +1,55 @@ +"use client"; + +import { Button } from "@/components/ui/button"; +import { ToolTipLabel } from "@/components/ui/tooltip"; +import { useDashboardRouter } from "@/lib/DashboardRouter"; +import { ArrowDownLeftIcon, ArrowUpRightIcon } from "lucide-react"; +import { usePathname, useSearchParams } from "next/navigation"; +import { useCallback } from "react"; + +type QueryTypeProps = { + activeType: "origin" | "destination"; +}; + +export const QueryType: React.FC = ({ activeType }) => { + const pathname = usePathname(); + const searchParams = useSearchParams(); + const router = useDashboardRouter(); + + const createPageURL = useCallback( + (type: "origin" | "destination") => { + const params = new URLSearchParams(searchParams || undefined); + params.set("type", type); + return `${pathname}?${params.toString()}`; + }, + [pathname, searchParams], + ); + return ( +
+ + + + + + +
+ ); +}; diff --git a/apps/dashboard/src/app/(dashboard)/(bridge)/routes/components/client/view.tsx b/apps/dashboard/src/app/(dashboard)/(bridge)/routes/components/client/view.tsx new file mode 100644 index 00000000000..faf526cb5b0 --- /dev/null +++ b/apps/dashboard/src/app/(dashboard)/(bridge)/routes/components/client/view.tsx @@ -0,0 +1,50 @@ +"use client"; + +import { Button } from "@/components/ui/button"; +import { useDashboardRouter } from "@/lib/DashboardRouter"; +import { Grid2X2Icon, ListIcon } from "lucide-react"; +import { usePathname, useSearchParams } from "next/navigation"; +import { useCallback } from "react"; + +type RouteListViewProps = { + activeView: "grid" | "table"; +}; + +export const RouteListView: React.FC = ({ activeView }) => { + const pathname = usePathname(); + const searchParams = useSearchParams(); + const router = useDashboardRouter(); + + const createPageURL = useCallback( + (view: "grid" | "table") => { + const params = new URLSearchParams(searchParams || undefined); + params.set("view", view); + return `${pathname}?${params.toString()}`; + }, + [pathname, searchParams], + ); + return ( +
+ + +
+ ); +}; diff --git a/apps/dashboard/src/app/(dashboard)/(bridge)/routes/components/server/add-token-button.tsx b/apps/dashboard/src/app/(dashboard)/(bridge)/routes/components/server/add-token-button.tsx new file mode 100644 index 00000000000..ff80fc82033 --- /dev/null +++ b/apps/dashboard/src/app/(dashboard)/(bridge)/routes/components/server/add-token-button.tsx @@ -0,0 +1,18 @@ +import { Button } from "@/components/ui/button"; +import { PlusIcon } from "lucide-react"; +import Link from "next/link"; + +export function AddYourTokenButton(props: { className?: string }) { + return ( + + ); +} diff --git a/apps/dashboard/src/app/(dashboard)/(bridge)/routes/components/server/routelist-card.tsx b/apps/dashboard/src/app/(dashboard)/(bridge)/routes/components/server/routelist-card.tsx new file mode 100644 index 00000000000..6f73119bd19 --- /dev/null +++ b/apps/dashboard/src/app/(dashboard)/(bridge)/routes/components/server/routelist-card.tsx @@ -0,0 +1,115 @@ +import { Card, CardContent, CardHeader } from "@/components/ui/card"; +import { getThirdwebClient } from "@/constants/thirdweb.server"; +import { resolveSchemeWithErrorHandler } from "@/lib/resolveSchemeWithErrorHandler"; +import { defineChain } from "thirdweb"; +import { getChainMetadata } from "thirdweb/chains"; +import { TokenName, TokenProvider } from "thirdweb/react"; + +type RouteListCardProps = { + originChainId: number; + originTokenAddress: string; + originTokenIconUri: string | null; + destinationChainId: number; + destinationTokenAddress: string; + destinationTokenIconUri: string | null; +}; + +export async function RouteListCard({ + originChainId, + originTokenAddress, + originTokenIconUri, + destinationChainId, + destinationTokenAddress, + destinationTokenIconUri, +}: RouteListCardProps) { + const [ + originChain, + destinationChain, + resolvedOriginTokenIconUri, + resolvedDestinationTokenIconUri, + ] = await Promise.all([ + // eslint-disable-next-line no-restricted-syntax + getChainMetadata(defineChain(originChainId)), + // eslint-disable-next-line no-restricted-syntax + getChainMetadata(defineChain(destinationChainId)), + originTokenIconUri + ? resolveSchemeWithErrorHandler({ + uri: originTokenIconUri, + client: getThirdwebClient(), + }) + : undefined, + destinationTokenIconUri + ? resolveSchemeWithErrorHandler({ + uri: destinationTokenIconUri, + client: getThirdwebClient(), + }) + : undefined, + ]); + + return ( +
+ + +
+ {resolvedOriginTokenIconUri ? ( + {originTokenAddress} + ) : ( +
+ )} + {resolvedDestinationTokenIconUri ? ( + {destinationTokenAddress} + ) : ( +
+ )} +
+ + + + {/* table of `chain id` `native token` `managed support`, header row on left value row on right */} + + + + + + + + + + + +
+ + + + + {originChain.name} +
+ + + + + {destinationChain.name} +
+
+ +
+ ); +} diff --git a/apps/dashboard/src/app/(dashboard)/(bridge)/routes/components/server/routelist-row.tsx b/apps/dashboard/src/app/(dashboard)/(bridge)/routes/components/server/routelist-row.tsx new file mode 100644 index 00000000000..fd4133f24ae --- /dev/null +++ b/apps/dashboard/src/app/(dashboard)/(bridge)/routes/components/server/routelist-row.tsx @@ -0,0 +1,123 @@ +import { CopyTextButton } from "@/components/ui/CopyTextButton"; +import { TableCell, TableRow } from "@/components/ui/table"; +import { getThirdwebClient } from "@/constants/thirdweb.server"; +import { resolveSchemeWithErrorHandler } from "@/lib/resolveSchemeWithErrorHandler"; +import { defineChain, getChainMetadata } from "thirdweb/chains"; +import { TokenProvider, TokenSymbol } from "thirdweb/react"; + +type RouteListRowProps = { + originChainId: number; + originTokenAddress: string; + originTokenIconUri: string | null; + destinationChainId: number; + destinationTokenAddress: string; + destinationTokenIconUri: string | null; +}; + +export async function RouteListRow({ + originChainId, + originTokenAddress, + originTokenIconUri, + destinationChainId, + destinationTokenAddress, + destinationTokenIconUri, +}: RouteListRowProps) { + const [ + originChain, + destinationChain, + resolvedOriginTokenIconUri, + resolvedDestinationTokenIconUri, + ] = await Promise.all([ + // eslint-disable-next-line no-restricted-syntax + getChainMetadata(defineChain(originChainId)), + // eslint-disable-next-line no-restricted-syntax + getChainMetadata(defineChain(destinationChainId)), + originTokenIconUri + ? resolveSchemeWithErrorHandler({ + uri: originTokenIconUri, + client: getThirdwebClient(), + }) + : undefined, + destinationTokenIconUri + ? resolveSchemeWithErrorHandler({ + uri: destinationTokenIconUri, + client: getThirdwebClient(), + }) + : undefined, + ]); + + return ( + + + +
+
+ {resolvedOriginTokenIconUri ? ( + // For now we're using a normal img tag because the domain for these images is unknown + {originTokenAddress} + ) : ( +
+ )} + } + tooltip="Copy Token Address" + className="relative z-10 text-base" + variant="ghost" + copyIconPosition="right" + /> +
+
+ + + + + {originChain.name} + + + + +
+
+ {resolvedDestinationTokenIconUri ? ( + {destinationTokenAddress} + ) : ( +
+ )} + } + tooltip="Copy Token Address" + className="relative z-10 text-base" + variant="ghost" + copyIconPosition="right" + /> +
+
+ + + + + {destinationChain.name} + + + ); +} diff --git a/apps/dashboard/src/app/(dashboard)/(bridge)/routes/components/server/routes-table.tsx b/apps/dashboard/src/app/(dashboard)/(bridge)/routes/components/server/routes-table.tsx new file mode 100644 index 00000000000..ef535365203 --- /dev/null +++ b/apps/dashboard/src/app/(dashboard)/(bridge)/routes/components/server/routes-table.tsx @@ -0,0 +1,154 @@ +import { + Table, + TableBody, + TableContainer, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table"; +import type { Route } from "app/(dashboard)/(bridge)/types/route"; +import type { Address } from "thirdweb"; +import { getRoutes } from "../../../utils"; +import { ChainlistPagination } from "../client/pagination"; +import { RouteListCard } from "../server/routelist-card"; +import { RouteListRow } from "../server/routelist-row"; + +export type SearchParams = Partial<{ + includeDeprecated: boolean; + query: string; + page: number; + type: "origin" | "destination"; + // maybe later we'll have a page size param? + // pageSize: number; + view: "table" | "grid"; +}>; + +// 120 is divisible by 2, 3, and 4 so card layout looks nice +const DEFAULT_PAGE_SIZE = 120; +const DEFAULT_PAGE = 1; + +async function getRoutesToRender(params: SearchParams) { + const filters: Partial = {}; + if (params.type === "origin" || typeof params.type === "undefined") { + if (params.query?.startsWith("0x")) { + filters.originTokenAddress = params.query as Address; + } else if (params.query) { + filters.originChainId = Number(params.query); + } + } else if (params.type === "destination") { + if (params.query?.startsWith("0x")) { + filters.destinationTokenAddress = params.query as Address; + } else if (params.query) { + filters.destinationChainId = Number(params.query); + } + } + // Temporary, will update this after the /routes endpoint + filters.limit = 10_000; + + const routes = await getRoutes(filters); + + const totalCount = routes.length; + + return { + routesToRender: routes, + totalCount, + filteredCount: routes.length, + }; +} + +export async function RoutesData(props: { + searchParams: SearchParams; + activeView: "table" | "grid"; + isLoggedIn: boolean; +}) { + const { routesToRender, totalCount, filteredCount } = await getRoutesToRender( + props.searchParams, + ); + + // pagination + const totalPages = Math.ceil(routesToRender.length / DEFAULT_PAGE_SIZE); + + const activePage = Number(props.searchParams.page || DEFAULT_PAGE); + const pageSize = DEFAULT_PAGE_SIZE; + const startIndex = (activePage - 1) * pageSize; + const endIndex = startIndex + pageSize; + const paginatedRoutes = routesToRender.slice(startIndex, endIndex); + + return ( + <> +
+ {/* empty state */} + {paginatedRoutes.length === 0 ? ( +
+

No Results found

+
+ ) : props.activeView === "table" ? ( + + + + + Origin Token + Origin Chain + Destination Token + Destination Chain + + + + {paginatedRoutes.map((route) => ( + + ))} + +
+
+ ) : ( +
    + {paginatedRoutes.map((route) => ( +
  • + +
  • + ))} +
+ )} +
+
+ {totalPages > 1 && ( + + )} +
+

+ Showing{" "} + {paginatedRoutes.length}{" "} + out of{" "} + {filteredCount !== totalCount ? ( + <> + {filteredCount}{" "} + routes that match filters. (Total:{" "} + {totalCount}) + + ) : ( + <> + {totalCount} routes. + + )} +

+ + ); +} diff --git a/apps/dashboard/src/app/(dashboard)/(bridge)/routes/opengraph-image.png b/apps/dashboard/src/app/(dashboard)/(bridge)/routes/opengraph-image.png new file mode 100644 index 0000000000000000000000000000000000000000..aceb6dba56bf2104c2093fc39ea7ec50eb9bf65d GIT binary patch literal 13648 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 literal 0 HcmV?d00001 diff --git a/apps/dashboard/src/app/(dashboard)/(bridge)/routes/page.tsx b/apps/dashboard/src/app/(dashboard)/(bridge)/routes/page.tsx new file mode 100644 index 00000000000..cc18d49a74b --- /dev/null +++ b/apps/dashboard/src/app/(dashboard)/(bridge)/routes/page.tsx @@ -0,0 +1,70 @@ +import type { Metadata } from "next"; +import { headers } from "next/headers"; +import { getAuthToken } from "../../../api/lib/getAuthToken"; +import { SearchInput } from "./components/client/search"; +import { QueryType } from "./components/client/type"; +import { RouteListView } from "./components/client/view"; +import { + RoutesData, + type SearchParams, +} from "./components/server/routes-table"; + +const title = "Routes: Swap, Bridge, and On-Ramp"; +const description = + "A list of token routes for swapping, bridging, and on-ramping between EVM chains with thirdweb."; + +export const metadata: Metadata = { + title, + description, + openGraph: { + title, + description, + }, +}; + +export default async function ChainListPage(props: { + searchParams: Promise; +}) { + const authToken = await getAuthToken(); + const headersList = await headers(); + const viewportWithHint = Number( + headersList.get("Sec-Ch-Viewport-Width") || 0, + ); + const searchParams = await props.searchParams; + + const activeType = searchParams.type ?? "origin"; + + // default is driven by viewport hint + const activeView = searchParams.view + ? searchParams.view + : viewportWithHint > 1000 + ? "table" + : "grid"; + + return ( +
+
+
+
+

+ Routes +

+
+
+
+ + + +
+
+
+
+
+ +
+ ); +} diff --git a/apps/dashboard/src/app/(dashboard)/(bridge)/types/route.ts b/apps/dashboard/src/app/(dashboard)/(bridge)/types/route.ts new file mode 100644 index 00000000000..5504b73c54c --- /dev/null +++ b/apps/dashboard/src/app/(dashboard)/(bridge)/types/route.ts @@ -0,0 +1,8 @@ +import type { Address } from "thirdweb"; + +export type Route = { + originChainId: number; + originTokenAddress: Address; + destinationChainId: number; + destinationTokenAddress: Address; +}; diff --git a/apps/dashboard/src/app/(dashboard)/(bridge)/utils.ts b/apps/dashboard/src/app/(dashboard)/(bridge)/utils.ts new file mode 100644 index 00000000000..bfc5d64a040 --- /dev/null +++ b/apps/dashboard/src/app/(dashboard)/(bridge)/utils.ts @@ -0,0 +1,52 @@ +import "server-only"; + +import { BRIDGE_URL, DASHBOARD_THIRDWEB_CLIENT_ID } from "@/constants/env"; +import type { Address } from "thirdweb"; +import type { Route } from "./types/route"; + +export async function getRoutes({ + limit, + offset, + originChainId, + originTokenAddress, + destinationChainId, + destinationTokenAddress, +}: { + limit?: number; + offset?: number; + originChainId?: number; + originTokenAddress?: Address; + destinationChainId?: number; + destinationTokenAddress?: Address; +} = {}) { + const url = new URL(`${BRIDGE_URL}/v1/routes`); + if (limit) { + url.searchParams.set("limit", limit.toString()); + } + if (offset) { + url.searchParams.set("offset", offset.toString()); + } + if (originChainId) { + url.searchParams.set("originChainId", originChainId.toString()); + } + if (originTokenAddress) { + url.searchParams.set("originTokenAddress", originTokenAddress); + } + if (destinationChainId) { + url.searchParams.set("destinationChainId", destinationChainId.toString()); + } + if (destinationTokenAddress) { + url.searchParams.set("destinationTokenAddress", destinationTokenAddress); + } + const routesResponse = await fetch(url, { + headers: { "x-client-id": DASHBOARD_THIRDWEB_CLIENT_ID }, + next: { revalidate: 60 * 60 }, + }); + + if (!routesResponse.ok) { + throw new Error("Failed to fetch routes"); + } + const routes: { data: Route[] } = await routesResponse.json(); + + return routes.data; +} diff --git a/apps/dashboard/tests/chain-page.spec.ts b/apps/dashboard/tests/chain-page.spec.ts index f4b57170d38..912dcaa4178 100644 --- a/apps/dashboard/tests/chain-page.spec.ts +++ b/apps/dashboard/tests/chain-page.spec.ts @@ -1,4 +1,4 @@ -import { test, expect } from "@playwright/test"; +import { expect, test } from "@playwright/test"; import { getBaseURL } from "./setup"; test.beforeEach(async ({ page, baseURL }) => { diff --git a/apps/dashboard/tests/contract-page.spec.ts b/apps/dashboard/tests/contract-page.spec.ts index 51fb22e0fb3..4c3fa2ceac4 100644 --- a/apps/dashboard/tests/contract-page.spec.ts +++ b/apps/dashboard/tests/contract-page.spec.ts @@ -1,4 +1,4 @@ -import { test, expect } from "@playwright/test"; +import { expect, test } from "@playwright/test"; import { getBaseURL, waitForPageLoad } from "./setup"; test.beforeEach(async ({ page, baseURL }) => { diff --git a/apps/dashboard/tests/homepage.spec.ts b/apps/dashboard/tests/homepage.spec.ts index 96674ba680a..40ab02dceeb 100644 --- a/apps/dashboard/tests/homepage.spec.ts +++ b/apps/dashboard/tests/homepage.spec.ts @@ -1,4 +1,4 @@ -import { test, expect } from "@playwright/test"; +import { expect, test } from "@playwright/test"; import { getBaseURL } from "./setup"; test.beforeEach(async ({ page, baseURL }) => { diff --git a/apps/dashboard/tests/publisher-page.spec.ts b/apps/dashboard/tests/publisher-page.spec.ts index 20a381d1708..1619bedb4dc 100644 --- a/apps/dashboard/tests/publisher-page.spec.ts +++ b/apps/dashboard/tests/publisher-page.spec.ts @@ -1,4 +1,4 @@ -import { test, expect } from "@playwright/test"; +import { expect, test } from "@playwright/test"; import { getBaseURL } from "./setup"; test.beforeEach(async ({ page, baseURL }) => { diff --git a/apps/dashboard/tests/setup.ts b/apps/dashboard/tests/setup.ts index 22d1ce86395..5742902a7b0 100644 --- a/apps/dashboard/tests/setup.ts +++ b/apps/dashboard/tests/setup.ts @@ -1,6 +1,6 @@ -import { Page } from "@playwright/test"; +import type { Page } from "@playwright/test"; -export function getBaseURL(url: string | void) { +export function getBaseURL(url: string | undefined) { if (process.env.ENVIRONMENT_URL) { url = process.env.ENVIRONMENT_URL; } From 3845e5e3cd9ee9799f44188fb5224c9114072bcb Mon Sep 17 00:00:00 2001 From: gregfromstl Date: Fri, 21 Feb 2025 20:22:55 -0800 Subject: [PATCH 02/12] fix: token backgrounds --- .../components/server/routelist-row.tsx | 20 +++++----- .../routes/components/server/routes-table.tsx | 39 +++++++++++-------- 2 files changed, 33 insertions(+), 26 deletions(-) diff --git a/apps/dashboard/src/app/(dashboard)/(bridge)/routes/components/server/routelist-row.tsx b/apps/dashboard/src/app/(dashboard)/(bridge)/routes/components/server/routelist-row.tsx index fd4133f24ae..7b3c7135406 100644 --- a/apps/dashboard/src/app/(dashboard)/(bridge)/routes/components/server/routelist-row.tsx +++ b/apps/dashboard/src/app/(dashboard)/(bridge)/routes/components/server/routelist-row.tsx @@ -34,15 +34,15 @@ export async function RouteListRow({ getChainMetadata(defineChain(destinationChainId)), originTokenIconUri ? resolveSchemeWithErrorHandler({ - uri: originTokenIconUri, - client: getThirdwebClient(), - }) + uri: originTokenIconUri, + client: getThirdwebClient(), + }) : undefined, destinationTokenIconUri ? resolveSchemeWithErrorHandler({ - uri: destinationTokenIconUri, - client: getThirdwebClient(), - }) + uri: destinationTokenIconUri, + client: getThirdwebClient(), + }) : undefined, ]); @@ -62,10 +62,10 @@ export async function RouteListRow({ {originTokenAddress} ) : ( -
+
)} ) : ( -
+
)} = {}; + const filters: Partial<{ + limit: number; + offset: number; + originChainId?: number; + originTokenAddress?: Address; + destinationChainId?: number; + destinationTokenAddress?: Address; + }> = {}; + if (params.type === "origin" || typeof params.type === "undefined") { if (params.query?.startsWith("0x")) { filters.originTokenAddress = params.query as Address; @@ -96,13 +103,13 @@ export async function RoutesData(props: { {paginatedRoutes.map((route) => ( ))} @@ -112,16 +119,16 @@ export async function RoutesData(props: {