Skip to content

Commit 150a90c

Browse files
authored
Feature: Routes page (#6318)
1 parent 454fc6a commit 150a90c

File tree

19 files changed

+798
-26
lines changed

19 files changed

+798
-26
lines changed

apps/dashboard/.env.example

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,14 @@
66
# not required to build, defaults to prod
77
NEXT_PUBLIC_THIRDWEB_DOMAIN="localhost:3000"
88

9-
# API host. For local development, please use "https://api.thirdweb-preview.com"
9+
# API host. For local development, please use "https://api.thirdweb-dev.com"
1010
# otherwise: "https://api.thirdweb.com"
1111
NEXT_PUBLIC_THIRDWEB_API_HOST="https://api.thirdweb-dev.com"
1212

13+
# Bridge API. For local development, please use "https://bridge.thirdweb-dev.com"
14+
# otherwise: "https://bridge.thirdweb.com"
15+
NEXT_PUBLIC_THIRDWEB_BRIDGE_HOST="https://bridge.thirdweb-dev.com"
16+
1317
# Paper API host
1418
NEXT_PUBLIC_THIRDWEB_EWS_API_HOST="https://ews.thirdweb-dev.com"
1519

@@ -97,4 +101,4 @@ REDIS_URL=""
97101
ANALYTICS_SERVICE_URL=""
98102

99103
# Required for Nebula Chat
100-
NEXT_PUBLIC_NEBULA_URL=""
104+
NEXT_PUBLIC_NEBULA_URL=""

apps/dashboard/src/@/constants/env.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,9 @@ export const DASHBOARD_STORAGE_URL =
2323
export const API_SERVER_URL =
2424
process.env.NEXT_PUBLIC_THIRDWEB_API_HOST || "https://api.thirdweb.com";
2525

26+
export const BRIDGE_URL =
27+
process.env.NEXT_PUBLIC_THIRDWEB_BRIDGE_HOST || "https://bridge.thirdweb.com";
28+
2629
/**
2730
* Faucet stuff
2831
*/
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
"use client";
2+
3+
import { PaginationButtons } from "@/components/pagination-buttons";
4+
import { useDashboardRouter } from "@/lib/DashboardRouter";
5+
import { usePathname, useSearchParams } from "next/navigation";
6+
import { useCallback } from "react";
7+
8+
type ChainlistPaginationProps = {
9+
totalPages: number;
10+
activePage: number;
11+
};
12+
13+
export const ChainlistPagination: React.FC<ChainlistPaginationProps> = ({
14+
activePage,
15+
totalPages,
16+
}) => {
17+
const pathname = usePathname();
18+
const searchParams = useSearchParams();
19+
const router = useDashboardRouter();
20+
21+
const createPageURL = useCallback(
22+
(pageNumber: number) => {
23+
const params = new URLSearchParams(searchParams || undefined);
24+
params.set("page", pageNumber.toString());
25+
return `${pathname}?${params.toString()}`;
26+
},
27+
[pathname, searchParams],
28+
);
29+
30+
return (
31+
<PaginationButtons
32+
activePage={activePage}
33+
totalPages={totalPages}
34+
onPageClick={(page) => router.push(createPageURL(page))}
35+
/>
36+
);
37+
};
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
"use client";
2+
3+
import { Button } from "@/components/ui/button";
4+
import { Input } from "@/components/ui/input";
5+
import { useDashboardRouter } from "@/lib/DashboardRouter";
6+
import { SearchIcon, XCircleIcon } from "lucide-react";
7+
import { usePathname, useSearchParams } from "next/navigation";
8+
import { useEffect, useRef } from "react";
9+
import { useDebouncedCallback } from "use-debounce";
10+
11+
function cleanUrl(url: string) {
12+
if (url.endsWith("?")) {
13+
return url.slice(0, -1);
14+
}
15+
return url;
16+
}
17+
18+
export const SearchInput: React.FC = () => {
19+
const router = useDashboardRouter();
20+
const pathname = usePathname();
21+
const searchParams = useSearchParams();
22+
23+
const inputRef = useRef<HTMLInputElement>(null);
24+
25+
// eslint-disable-next-line no-restricted-syntax
26+
useEffect(() => {
27+
// reset the input if the query param is removed
28+
if (inputRef.current?.value && !searchParams?.get("query")) {
29+
inputRef.current.value = "";
30+
}
31+
}, [searchParams]);
32+
33+
const handleSearch = useDebouncedCallback((term: string) => {
34+
const params = new URLSearchParams(searchParams ?? undefined);
35+
if (term) {
36+
params.set("query", term);
37+
} else {
38+
params.delete("query");
39+
}
40+
// always delete the page number when searching
41+
params.delete("page");
42+
const url = cleanUrl(`${pathname}?${params.toString()}`);
43+
router.replace(url);
44+
}, 300);
45+
46+
return (
47+
<div className="group relative w-full">
48+
<SearchIcon className="-translate-y-1/2 absolute top-[50%] left-3 size-4 text-muted-foreground" />
49+
<Input
50+
placeholder="Search by token address or chain ID"
51+
className="h-10 rounded-lg bg-card py-2 pl-9 lg:min-w-[300px]"
52+
defaultValue={searchParams?.get("query") || ""}
53+
onChange={(e) => handleSearch(e.target.value)}
54+
ref={inputRef}
55+
/>
56+
{searchParams?.has("query") && (
57+
<Button
58+
size="icon"
59+
className="-translate-y-1/2 absolute top-[50%] right-0 bg-background text-muted-foreground opacity-0 transition duration-300 ease-in-out group-hover:opacity-100"
60+
variant="outline"
61+
onClick={() => {
62+
const params = new URLSearchParams(searchParams ?? undefined);
63+
params.delete("query");
64+
params.delete("page");
65+
const url = cleanUrl(`${pathname}?${params.toString()}`);
66+
router.replace(url);
67+
}}
68+
>
69+
<XCircleIcon className="size-5" />
70+
</Button>
71+
)}
72+
</div>
73+
);
74+
};
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
"use client";
2+
3+
import { Button } from "@/components/ui/button";
4+
import { ToolTipLabel } from "@/components/ui/tooltip";
5+
import { useDashboardRouter } from "@/lib/DashboardRouter";
6+
import { ArrowDownLeftIcon, ArrowUpRightIcon } from "lucide-react";
7+
import { usePathname, useSearchParams } from "next/navigation";
8+
import { useCallback } from "react";
9+
10+
type QueryTypeProps = {
11+
activeType: "origin" | "destination";
12+
};
13+
14+
export const QueryType: React.FC<QueryTypeProps> = ({ activeType }) => {
15+
const pathname = usePathname();
16+
const searchParams = useSearchParams();
17+
const router = useDashboardRouter();
18+
19+
const createPageURL = useCallback(
20+
(type: "origin" | "destination") => {
21+
const params = new URLSearchParams(searchParams || undefined);
22+
params.set("type", type);
23+
return `${pathname}?${params.toString()}`;
24+
},
25+
[pathname, searchParams],
26+
);
27+
return (
28+
<div className="flex flex-row">
29+
<ToolTipLabel label="Origin" contentClassName="w-full">
30+
<Button
31+
size="icon"
32+
variant={activeType === "origin" ? "default" : "outline"}
33+
onClick={() => {
34+
router.replace(createPageURL("origin"));
35+
}}
36+
className="rounded-r-none"
37+
>
38+
<ArrowUpRightIcon strokeWidth={1} />
39+
</Button>
40+
</ToolTipLabel>
41+
<ToolTipLabel label="Destination" contentClassName="w-full">
42+
<Button
43+
variant={activeType === "destination" ? "default" : "outline"}
44+
size="icon"
45+
onClick={() => {
46+
router.replace(createPageURL("destination"));
47+
}}
48+
className="rounded-l-none"
49+
>
50+
<ArrowDownLeftIcon strokeWidth={1} />
51+
</Button>
52+
</ToolTipLabel>
53+
</div>
54+
);
55+
};
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
"use client";
2+
3+
import { Button } from "@/components/ui/button";
4+
import { useDashboardRouter } from "@/lib/DashboardRouter";
5+
import { Grid2X2Icon, ListIcon } from "lucide-react";
6+
import { usePathname, useSearchParams } from "next/navigation";
7+
import { useCallback } from "react";
8+
9+
type RouteListViewProps = {
10+
activeView: "grid" | "table";
11+
};
12+
13+
export const RouteListView: React.FC<RouteListViewProps> = ({ activeView }) => {
14+
const pathname = usePathname();
15+
const searchParams = useSearchParams();
16+
const router = useDashboardRouter();
17+
18+
const createPageURL = useCallback(
19+
(view: "grid" | "table") => {
20+
const params = new URLSearchParams(searchParams || undefined);
21+
params.set("view", view);
22+
return `${pathname}?${params.toString()}`;
23+
},
24+
[pathname, searchParams],
25+
);
26+
return (
27+
<div className="flex flex-row">
28+
<Button
29+
size="icon"
30+
variant={activeView === "table" ? "default" : "outline"}
31+
onClick={() => {
32+
router.replace(createPageURL("table"));
33+
}}
34+
className="rounded-r-none"
35+
>
36+
<ListIcon strokeWidth={1} />
37+
</Button>
38+
<Button
39+
variant={activeView === "grid" ? "default" : "outline"}
40+
size="icon"
41+
onClick={() => {
42+
router.replace(createPageURL("grid"));
43+
}}
44+
className="rounded-l-none"
45+
>
46+
<Grid2X2Icon strokeWidth={1} />
47+
</Button>
48+
</div>
49+
);
50+
};
Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
import { Card, CardContent, CardHeader } from "@/components/ui/card";
2+
import { getThirdwebClient } from "@/constants/thirdweb.server";
3+
import { resolveSchemeWithErrorHandler } from "@/lib/resolveSchemeWithErrorHandler";
4+
import { NATIVE_TOKEN_ADDRESS, defineChain, getContract } from "thirdweb";
5+
import { getChainMetadata } from "thirdweb/chains";
6+
import { name } from "thirdweb/extensions/common";
7+
8+
type RouteListCardProps = {
9+
originChainId: number;
10+
originTokenAddress: string;
11+
originTokenIconUri: string | null;
12+
destinationChainId: number;
13+
destinationTokenAddress: string;
14+
destinationTokenIconUri: string | null;
15+
};
16+
17+
export async function RouteListCard({
18+
originChainId,
19+
originTokenAddress,
20+
originTokenIconUri,
21+
destinationChainId,
22+
destinationTokenAddress,
23+
destinationTokenIconUri,
24+
}: RouteListCardProps) {
25+
const [
26+
originChain,
27+
originTokenName,
28+
destinationChain,
29+
destinationTokenName,
30+
resolvedOriginTokenIconUri,
31+
resolvedDestinationTokenIconUri,
32+
] = await Promise.all([
33+
// eslint-disable-next-line no-restricted-syntax
34+
getChainMetadata(defineChain(originChainId)),
35+
originTokenAddress.toLowerCase() === NATIVE_TOKEN_ADDRESS
36+
? "ETH"
37+
: name({
38+
contract: getContract({
39+
address: originTokenAddress,
40+
// eslint-disable-next-line no-restricted-syntax
41+
chain: defineChain(originChainId),
42+
client: getThirdwebClient(),
43+
}),
44+
}).catch(() => undefined),
45+
// eslint-disable-next-line no-restricted-syntax
46+
getChainMetadata(defineChain(destinationChainId)),
47+
destinationTokenAddress.toLowerCase() === NATIVE_TOKEN_ADDRESS
48+
? "ETH"
49+
: name({
50+
contract: getContract({
51+
address: destinationTokenAddress,
52+
// eslint-disable-next-line no-restricted-syntax
53+
chain: defineChain(destinationChainId),
54+
client: getThirdwebClient(),
55+
}),
56+
}).catch(() => undefined),
57+
originTokenIconUri
58+
? resolveSchemeWithErrorHandler({
59+
uri: originTokenIconUri,
60+
client: getThirdwebClient(),
61+
})
62+
: undefined,
63+
destinationTokenIconUri
64+
? resolveSchemeWithErrorHandler({
65+
uri: destinationTokenIconUri,
66+
client: getThirdwebClient(),
67+
})
68+
: undefined,
69+
]);
70+
71+
return (
72+
<div className="relative h-full">
73+
<Card className="h-full w-full transition-colors hover:border-active-border">
74+
<CardHeader className="flex flex-row items-center justify-between p-4">
75+
<div className="flex flex-row items-center gap-2">
76+
{resolvedOriginTokenIconUri ? (
77+
<img
78+
src={resolvedOriginTokenIconUri}
79+
alt={originTokenAddress}
80+
className="size-8 rounded-full bg-white"
81+
/>
82+
) : (
83+
<div className="size-8 rounded-full bg-white/10" />
84+
)}
85+
{resolvedDestinationTokenIconUri ? (
86+
<img
87+
src={resolvedDestinationTokenIconUri}
88+
alt={destinationTokenAddress}
89+
className="-translate-x-4 size-8 rounded-full bg-white ring-2 ring-card"
90+
/>
91+
) : (
92+
<div className="-translate-x-4 size-8 rounded-full bg-muted-foreground ring-2 ring-card" />
93+
)}
94+
</div>
95+
</CardHeader>
96+
97+
<CardContent className="px-4 pt-0 pb-4">
98+
<table className="w-full">
99+
<tbody className="text-sm [&_td>*]:min-h-[25px]">
100+
<tr>
101+
<th className="text-left font-normal text-base">
102+
{originTokenName === "ETH"
103+
? originChain.nativeCurrency.name
104+
: originTokenName}
105+
</th>
106+
<td className="text-right text-base text-muted-foreground">
107+
{originChain.name}
108+
</td>
109+
</tr>
110+
<tr>
111+
<th className="text-left font-normal text-base">
112+
{destinationTokenName === "ETH"
113+
? destinationChain.nativeCurrency.name
114+
: destinationTokenName}
115+
</th>
116+
<td className="text-right text-base text-muted-foreground">
117+
{destinationChain.name}
118+
</td>
119+
</tr>
120+
</tbody>
121+
</table>
122+
</CardContent>
123+
</Card>
124+
</div>
125+
);
126+
}

0 commit comments

Comments
 (0)