Skip to content

Commit aeaeecb

Browse files
committed
feat: routes page
1 parent 514c1a8 commit aeaeecb

File tree

20 files changed

+772
-9
lines changed

20 files changed

+772
-9
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/@/components/ui/CopyTextButton.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import { Button } from "./button";
77
import { ToolTipLabel } from "./tooltip";
88

99
export function CopyTextButton(props: {
10-
textToShow: string;
10+
textToShow: string | React.ReactNode;
1111
textToCopy: string;
1212
tooltip: string | undefined;
1313
className?: string;

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: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import { Button } from "@/components/ui/button";
2+
import { PlusIcon } from "lucide-react";
3+
import Link from "next/link";
4+
5+
export function AddYourTokenButton(props: { className?: string }) {
6+
return (
7+
<Button asChild variant="default" className={props.className}>
8+
<Link
9+
href="https://share.hsforms.com/1XDi-ieM9Rl6oIkn7ynK6Lgea58c"
10+
target="_blank"
11+
className="flex items-center gap-2"
12+
>
13+
<PlusIcon className="size-4" />
14+
Add your token
15+
</Link>
16+
</Button>
17+
);
18+
}
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
import { Card, CardContent, CardHeader } from "@/components/ui/card";
2+
import { getThirdwebClient } from "@/constants/thirdweb.server";
3+
import { resolveSchemeWithErrorHandler } from "@/lib/resolveSchemeWithErrorHandler";
4+
import { defineChain } from "thirdweb";
5+
import { getChainMetadata } from "thirdweb/chains";
6+
import { TokenName, TokenProvider } from "thirdweb/react";
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+
destinationChain,
28+
resolvedOriginTokenIconUri,
29+
resolvedDestinationTokenIconUri,
30+
] = await Promise.all([
31+
// eslint-disable-next-line no-restricted-syntax
32+
getChainMetadata(defineChain(originChainId)),
33+
// eslint-disable-next-line no-restricted-syntax
34+
getChainMetadata(defineChain(destinationChainId)),
35+
originTokenIconUri
36+
? resolveSchemeWithErrorHandler({
37+
uri: originTokenIconUri,
38+
client: getThirdwebClient(),
39+
})
40+
: undefined,
41+
destinationTokenIconUri
42+
? resolveSchemeWithErrorHandler({
43+
uri: destinationTokenIconUri,
44+
client: getThirdwebClient(),
45+
})
46+
: undefined,
47+
]);
48+
49+
return (
50+
<div className="relative h-full">
51+
<Card className="h-full w-full transition-colors hover:border-active-border">
52+
<CardHeader className="flex flex-row items-center justify-between p-4">
53+
<div className="flex flex-row items-center gap-2">
54+
{resolvedOriginTokenIconUri ? (
55+
<img
56+
src={resolvedOriginTokenIconUri}
57+
alt={originTokenAddress}
58+
className="size-8 rounded-full bg-white"
59+
/>
60+
) : (
61+
<div className="size-8 rounded-full bg-white/10" />
62+
)}
63+
{resolvedDestinationTokenIconUri ? (
64+
<img
65+
src={resolvedDestinationTokenIconUri}
66+
alt={destinationTokenAddress}
67+
className="-translate-x-4 size-8 rounded-full bg-white ring-2 ring-card"
68+
/>
69+
) : (
70+
<div className="-translate-x-4 size-8 rounded-full bg-muted-foreground ring-2 ring-card" />
71+
)}
72+
</div>
73+
</CardHeader>
74+
75+
<CardContent className="px-4 pt-0 pb-4">
76+
{/* table of `chain id` `native token` `managed support`, header row on left value row on right */}
77+
<table className="w-full">
78+
<tbody className="text-sm [&_td>*]:min-h-[25px]">
79+
<tr>
80+
<th className="text-left font-normal text-base">
81+
<TokenProvider
82+
address={originTokenAddress}
83+
// eslint-disable-next-line no-restricted-syntax
84+
chain={defineChain(originChainId)}
85+
client={getThirdwebClient()}
86+
>
87+
<TokenName />
88+
</TokenProvider>
89+
</th>
90+
<td className="text-right text-base text-muted-foreground">
91+
{originChain.name}
92+
</td>
93+
</tr>
94+
<tr>
95+
<th className="text-left font-normal text-base">
96+
<TokenProvider
97+
address={destinationTokenAddress}
98+
// eslint-disable-next-line no-restricted-syntax
99+
chain={defineChain(destinationChainId)}
100+
client={getThirdwebClient()}
101+
>
102+
<TokenName />
103+
</TokenProvider>
104+
</th>
105+
<td className="text-right text-base text-muted-foreground">
106+
{destinationChain.name}
107+
</td>
108+
</tr>
109+
</tbody>
110+
</table>
111+
</CardContent>
112+
</Card>
113+
</div>
114+
);
115+
}

0 commit comments

Comments
 (0)