Skip to content

Fix: Update routes page to use server-side pagination and filtering #7133

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
May 23, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion apps/dashboard/src/@/components/ui/CopyTextButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ export function CopyTextButton(props: {
variant={props.variant || "outline"}
aria-label={props.tooltip}
className={cn(
"flex h-auto w-auto gap-2 rounded-lg px-2.5 py-1.5 font-normal text-foreground",
"flex h-auto w-auto gap-2 rounded-lg px-1.5 py-0.5 font-normal text-foreground",
props.className,
)}
onClick={(e) => {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { ArrowUpRightIcon } from "lucide-react";
import type { Metadata } from "next";
import { getClientThirdwebClient } from "../../../../../@/constants/thirdweb-client.client";
import { UniversalBridgeEmbed } from "./components/client/UniversalBridgeEmbed";
Expand Down Expand Up @@ -35,6 +36,34 @@ export default async function RoutesPage({
src="/assets/login/background.svg"
className="-bottom-12 -right-12 pointer-events-none absolute lg:right-0 lg:bottom-0"
/>

<div className="absolute inset-x-0 bottom-24 z-20">
<div className="container mx-auto px-4">
<div className="relative overflow-hidden rounded-lg border-2 border-green-500/20 bg-gradient-to-br from-card/80 to-card/50 p-4 shadow-[inset_0_1px_2px_0_rgba(0,0,0,0.02)]">
<div className="absolute inset-0 bg-gradient-to-br from-green-500/5 to-transparent" />
<div className="relative flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between">
<div className="flex flex-col gap-1">
<h3 className="font-medium text-lg">
Get Started with Universal Bridge
</h3>
<p className="text-muted-foreground text-sm">
Simple, instant, and secure payments across any token and
chain.
</p>
</div>
<a
href="https://portal.thirdweb.com/pay"
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center gap-2 rounded-md bg-green-600 px-4 py-2 font-medium text-sm text-white transition-all hover:bg-green-600/90 hover:shadow-sm"
>
Learn More
<ArrowUpRightIcon className="size-4" />
</a>
</div>
</div>
</div>
</div>
</div>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ export const SearchInput: React.FC = () => {
<div className="group relative w-full">
<SearchIcon className="-translate-y-1/2 absolute top-[50%] left-3 size-4 text-muted-foreground" />
<Input
placeholder="Search with anything!"
placeholder="Search by token name or symbol"
className="h-10 rounded-lg bg-card py-2 pl-9 lg:min-w-[300px]"
defaultValue={searchParams?.get("query") || ""}
onChange={(e) => handleSearch(e.target.value)}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,20 +52,20 @@ export async function RouteListRow({
]);

return (
<TableRow linkBox className="hover:bg-accent/50">
<TableRow linkBox className="group transition-colors hover:bg-accent/50">
<TableCell>
<div className="flex flex-row items-center gap-4">
<div className="flex items-center gap-1">
<div className="flex flex-row items-center gap-3">
<div className="flex items-center gap-2">
{resolvedOriginTokenIconUri ? (
// For now we're using a normal img tag because the domain for these images is unknown
// eslint-disable-next-line @next/next/no-img-element
<img
src={resolvedOriginTokenIconUri}
alt={originTokenAddress}
className="size-6 rounded-full border border-muted-foreground"
className="size-7 rounded-full border border-border/50 shadow-sm transition-transform group-hover:scale-105"
/>
) : (
<div className="size-6 rounded-full bg-muted-foreground" />
<div className="size-7 rounded-full bg-muted-foreground/20" />
)}
{originTokenSymbol && (
<CopyTextButton
Expand All @@ -76,7 +76,7 @@ export async function RouteListRow({
: originTokenSymbol
}
tooltip="Copy Token Address"
className="relative z-10 text-base"
className="relative z-10 font-medium text-base"
variant="ghost"
copyIconPosition="right"
/>
Expand All @@ -85,22 +85,22 @@ export async function RouteListRow({
</div>
</TableCell>

<TableCell className="text-muted-foreground">
{originChain.name}
<TableCell className="text-muted-foreground/90">
<span className="font-medium">{originChain.name}</span>
</TableCell>

<TableCell>
<div className="flex flex-row items-center gap-4">
<div className="flex items-center gap-1">
<div className="flex flex-row items-center gap-3">
<div className="flex items-center gap-2">
{resolvedDestinationTokenIconUri ? (
// eslint-disable-next-line @next/next/no-img-element
<img
src={resolvedDestinationTokenIconUri}
alt={destinationTokenAddress}
className="size-6 rounded-full border border-muted-foreground"
className="size-7 rounded-full border border-border/50 shadow-sm transition-transform group-hover:scale-105"
/>
) : (
<div className="size-6 rounded-full bg-muted-foreground" />
<div className="size-7 rounded-full bg-muted-foreground/20" />
)}
{destinationTokenSymbol && (
<CopyTextButton
Expand All @@ -111,7 +111,7 @@ export async function RouteListRow({
: destinationTokenSymbol
}
tooltip="Copy Token Address"
className="relative z-10 text-base"
className="relative z-10 font-medium text-base"
variant="ghost"
copyIconPosition="right"
/>
Expand All @@ -120,8 +120,8 @@ export async function RouteListRow({
</div>
</TableCell>

<TableCell className="text-muted-foreground">
{destinationChain.name}
<TableCell className="text-muted-foreground/90">
<span className="font-medium">{destinationChain.name}</span>
</TableCell>
</TableRow>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import {
TableRow,
} from "@/components/ui/table";
import type { Address } from "thirdweb";
import { checksumAddress } from "thirdweb/utils";
import { getRoutes } from "../../../utils";
import { ChainlistPagination } from "../client/pagination";
import { RouteListCard } from "./routelist-card";
Expand All @@ -25,18 +24,15 @@ export type SearchParams = Partial<{

// 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;
originQuery?: string;
destinationQuery?: string;
originChainId?: number;
originTokenAddress?: Address;
destinationChainId?: number;
destinationTokenAddress?: Address;
originTextQuery?: string;
destinationTextQuery?: string;
}> = {};

if (params.type === "origin" || typeof params.type === "undefined") {
Expand All @@ -45,77 +41,28 @@ async function getRoutesToRender(params: SearchParams) {
} else if (Number.isInteger(Number(params.query))) {
filters.originChainId = Number(params.query);
} else if (params.query) {
filters.originTextQuery = params.query;
filters.originQuery = params.query;
}
} else if (params.type === "destination") {
if (params.query?.startsWith("0x")) {
filters.destinationTokenAddress = params.query as Address;
} else if (Number.isInteger(Number(params.query))) {
filters.destinationChainId = Number(params.query);
} else if (params.query) {
filters.destinationTextQuery = params.query;
filters.destinationQuery = params.query;
}
}
// Temporary, will update this after the /routes endpoint
let routes = await getRoutes({ limit: 500_000 });

const totalCount = routes.length;

if (filters.originChainId) {
routes = routes.filter(
(route) => route.originToken.chainId === filters.originChainId,
);
}
if (filters.originTokenAddress) {
const originTokenAddress = filters.originTokenAddress;
routes = routes.filter(
(route) =>
checksumAddress(route.originToken.address) ===
checksumAddress(originTokenAddress),
);
}
if (filters.destinationChainId) {
routes = routes.filter(
(route) => route.destinationToken.chainId === filters.destinationChainId,
);
}
if (filters.destinationTokenAddress) {
const destinationTokenAddress = filters.destinationTokenAddress;
routes = routes.filter(
(route) =>
checksumAddress(route.destinationToken.address) ===
checksumAddress(destinationTokenAddress),
);
}

if (filters.originTextQuery) {
const originTextQuery = filters.originTextQuery.toLowerCase();
routes = routes.filter((route) => {
return (
route.originToken.name.toLowerCase().includes(originTextQuery) ||
route.originToken.symbol.toLowerCase().includes(originTextQuery)
);
});
}

if (filters.destinationTextQuery) {
const destinationTextQuery = filters.destinationTextQuery.toLowerCase();
routes = routes.filter((route) => {
return (
route.destinationToken.name
.toLowerCase()
.includes(destinationTextQuery) ||
route.destinationToken.symbol
.toLowerCase()
.includes(destinationTextQuery)
);
});
}
const routes = await getRoutes({
limit: DEFAULT_PAGE_SIZE,
offset: DEFAULT_PAGE_SIZE * ((params.page || 1) - 1),
originQuery: filters.originQuery,
destinationQuery: filters.destinationQuery,
});

return {
routesToRender: routes,
totalCount,
filteredCount: routes.length,
routesToRender: routes.data,
totalCount: routes.meta.totalCount,
filteredCount: routes.meta.filteredCount,
};
}

Expand All @@ -128,10 +75,9 @@ export async function RoutesData(props: {
props.searchParams,
);

// pagination
const totalPages = Math.ceil(routesToRender.length / DEFAULT_PAGE_SIZE);
const totalPages = Math.ceil(filteredCount / DEFAULT_PAGE_SIZE);

const activePage = Number(props.searchParams.page || DEFAULT_PAGE);
const activePage = Number(props.searchParams.page || 1);
const pageSize = DEFAULT_PAGE_SIZE;
const startIndex = (activePage - 1) * pageSize;
const endIndex = startIndex + pageSize;
Expand All @@ -146,14 +92,22 @@ export async function RoutesData(props: {
<p className="text-2xl">No Results found</p>
</div>
) : props.activeView === "table" ? (
<TableContainer>
<TableContainer className="overflow-hidden rounded-xl border border-border/50 bg-card/50 shadow-sm transition-all">
<Table>
<TableHeader className="z-0">
<TableRow>
<TableHead>Origin Token</TableHead>
<TableHead>Origin Chain</TableHead>
<TableHead>Destination Token</TableHead>
<TableHead>Destination Chain</TableHead>
<TableRow className="border-border/50 border-b bg-muted/50">
<TableHead className="py-4 font-medium text-muted-foreground/80 text-xs uppercase tracking-wider">
Origin Token
</TableHead>
<TableHead className="py-4 font-medium text-muted-foreground/80 text-xs uppercase tracking-wider">
Origin Chain
</TableHead>
<TableHead className="py-4 font-medium text-muted-foreground/80 text-xs uppercase tracking-wider">
Destination Token
</TableHead>
<TableHead className="py-4 font-medium text-muted-foreground/80 text-xs uppercase tracking-wider">
Destination Chain
</TableHead>
</TableRow>
</TableHeader>
<TableBody>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { ArrowUpRightIcon } from "lucide-react";
import type { Metadata } from "next";
import { headers } from "next/headers";
import { getAuthToken } from "../../../api/lib/getAuthToken";
Expand Down Expand Up @@ -43,15 +44,15 @@ export default async function RoutesPage(props: {

return (
<section className="container mx-auto flex h-full flex-col px-4 py-10">
<header className="flex flex-col gap-4">
<div className="flex flex-col gap-4 lg:flex-row lg:items-center lg:justify-between">
<div className="flex flex-row items-center justify-between gap-4 lg:flex-col lg:justify-start">
<header className="flex flex-col gap-6">
<div className="flex flex-col gap-6 lg:flex-row lg:items-center lg:justify-between">
<div className="flex flex-col gap-2">
<h1 className="font-semibold text-4xl tracking-tighter lg:text-5xl">
Routes
</h1>
</div>
<div className="flex flex-row items-end gap-4 lg:flex-col ">
<div className="flex w-full flex-row gap-4">
<div className="flex flex-row items-end gap-4 lg:flex-col">
<div className="flex w-full flex-row items-center gap-4">
<SearchInput />
<QueryType activeType={activeType} />
<RouteListView activeView={activeView} />
Expand All @@ -60,6 +61,29 @@ export default async function RoutesPage(props: {
</div>
</header>
<div className="h-10" />
<div className="relative overflow-hidden rounded-lg border-2 border-green-500/20 bg-gradient-to-br from-card/80 to-card/50 p-4 shadow-[inset_0_1px_2px_0_rgba(0,0,0,0.02)]">
<div className="absolute inset-0 bg-gradient-to-br from-green-500/5 to-transparent" />
<div className="relative flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between">
<div className="flex flex-col gap-1">
<h3 className="font-medium text-lg">
Get Started with Universal Bridge
</h3>
<p className="text-muted-foreground text-sm">
Simple, instant, and secure payments across any token and chain.
</p>
</div>
<a
href="https://portal.thirdweb.com/pay"
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center gap-2 rounded-md bg-green-600 px-4 py-2 font-medium text-sm text-white transition-all hover:bg-green-600/90 hover:shadow-sm"
>
Learn More
<ArrowUpRightIcon className="size-4" />
</a>
</div>
</div>
<div className="h-10" />
<RoutesData
searchParams={searchParams}
activeView={activeView}
Expand Down
Loading
Loading