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/@/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/routelist-card.tsx b/apps/dashboard/src/app/(dashboard)/(bridge)/routes/components/server/routelist-card.tsx new file mode 100644 index 00000000000..7deca3f819a --- /dev/null +++ b/apps/dashboard/src/app/(dashboard)/(bridge)/routes/components/server/routelist-card.tsx @@ -0,0 +1,126 @@ +import { Card, CardContent, CardHeader } from "@/components/ui/card"; +import { getThirdwebClient } from "@/constants/thirdweb.server"; +import { resolveSchemeWithErrorHandler } from "@/lib/resolveSchemeWithErrorHandler"; +import { NATIVE_TOKEN_ADDRESS, defineChain, getContract } from "thirdweb"; +import { getChainMetadata } from "thirdweb/chains"; +import { name } from "thirdweb/extensions/common"; + +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, + originTokenName, + destinationChain, + destinationTokenName, + resolvedOriginTokenIconUri, + resolvedDestinationTokenIconUri, + ] = await Promise.all([ + // eslint-disable-next-line no-restricted-syntax + getChainMetadata(defineChain(originChainId)), + originTokenAddress.toLowerCase() === NATIVE_TOKEN_ADDRESS + ? "ETH" + : name({ + contract: getContract({ + address: originTokenAddress, + // eslint-disable-next-line no-restricted-syntax + chain: defineChain(originChainId), + client: getThirdwebClient(), + }), + }).catch(() => undefined), + // eslint-disable-next-line no-restricted-syntax + getChainMetadata(defineChain(destinationChainId)), + destinationTokenAddress.toLowerCase() === NATIVE_TOKEN_ADDRESS + ? "ETH" + : name({ + contract: getContract({ + address: destinationTokenAddress, + // eslint-disable-next-line no-restricted-syntax + chain: defineChain(destinationChainId), + client: getThirdwebClient(), + }), + }).catch(() => undefined), + originTokenIconUri + ? resolveSchemeWithErrorHandler({ + uri: originTokenIconUri, + client: getThirdwebClient(), + }) + : undefined, + destinationTokenIconUri + ? resolveSchemeWithErrorHandler({ + uri: destinationTokenIconUri, + client: getThirdwebClient(), + }) + : undefined, + ]); + + return ( +
+ + +
+ {resolvedOriginTokenIconUri ? ( + {originTokenAddress} + ) : ( +
+ )} + {resolvedDestinationTokenIconUri ? ( + {destinationTokenAddress} + ) : ( +
+ )} +
+ + + + + + + + + + + + + + +
+ {originTokenName === "ETH" + ? originChain.nativeCurrency.name + : originTokenName} + + {originChain.name} +
+ {destinationTokenName === "ETH" + ? destinationChain.nativeCurrency.name + : destinationTokenName} + + {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..b31c7f55758 --- /dev/null +++ b/apps/dashboard/src/app/(dashboard)/(bridge)/routes/components/server/routelist-row.tsx @@ -0,0 +1,144 @@ +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 { NATIVE_TOKEN_ADDRESS, getContract } from "thirdweb"; +import { defineChain, getChainMetadata } from "thirdweb/chains"; +import { symbol } from "thirdweb/extensions/common"; + +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, + originTokenSymbol, + destinationChain, + destinationTokenSymbol, + resolvedOriginTokenIconUri, + resolvedDestinationTokenIconUri, + ] = await Promise.all([ + // eslint-disable-next-line no-restricted-syntax + getChainMetadata(defineChain(originChainId)), + originTokenAddress.toLowerCase() === NATIVE_TOKEN_ADDRESS + ? "ETH" + : symbol({ + contract: getContract({ + address: originTokenAddress, + // eslint-disable-next-line no-restricted-syntax + chain: defineChain(originChainId), + client: getThirdwebClient(), + }), + }).catch(() => undefined), + // eslint-disable-next-line no-restricted-syntax + getChainMetadata(defineChain(destinationChainId)), + destinationTokenAddress.toLowerCase() === NATIVE_TOKEN_ADDRESS + ? "ETH" + : symbol({ + contract: getContract({ + address: destinationTokenAddress, + // eslint-disable-next-line no-restricted-syntax + chain: defineChain(destinationChainId), + client: getThirdwebClient(), + }), + }).catch(() => undefined), + 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} + ) : ( +
+ )} + {originTokenSymbol && ( + + )} +
+
+ + + + {originChain.name} + + + +
+
+ {resolvedDestinationTokenIconUri ? ( + {destinationTokenAddress} + ) : ( +
+ )} + {destinationTokenSymbol && ( + + )} +
+
+ + + + {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..1ae7b052cca --- /dev/null +++ b/apps/dashboard/src/app/(dashboard)/(bridge)/routes/components/server/routes-table.tsx @@ -0,0 +1,161 @@ +import { + Table, + TableBody, + TableContainer, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table"; +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<{ + 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; + } 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 00000000000..aceb6dba56b Binary files /dev/null and b/apps/dashboard/src/app/(dashboard)/(bridge)/routes/opengraph-image.png differ 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..40595f0c031 --- /dev/null +++ b/apps/dashboard/src/app/(dashboard)/(bridge)/types/route.ts @@ -0,0 +1,14 @@ +import type { Address } from "thirdweb"; + +export type Route = { + originToken: { + address: Address; + chainId: number; + iconUri: string; + }; + destinationToken: { + address: Address; + chainId: number; + iconUri: string; + }; +}; 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/src/components/icons/brand-icons/DiscordIcon.tsx b/apps/dashboard/src/components/icons/brand-icons/DiscordIcon.tsx deleted file mode 100644 index 05dae7581b1..00000000000 --- a/apps/dashboard/src/components/icons/brand-icons/DiscordIcon.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import type { SVGProps } from "react"; - -export const DiscordIcon = (props: SVGProps) => { - return ( - - Discord - - - ); -}; 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; }