Skip to content
Merged
Show file tree
Hide file tree
Changes from 12 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
8 changes: 6 additions & 2 deletions apps/dashboard/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand Down Expand Up @@ -97,4 +101,4 @@ REDIS_URL=""
ANALYTICS_SERVICE_URL=""

# Required for Nebula Chat
NEXT_PUBLIC_NEBULA_URL=""
NEXT_PUBLIC_NEBULA_URL=""
3 changes: 3 additions & 0 deletions apps/dashboard/src/@/constants/env.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
*/
Expand Down
Original file line number Diff line number Diff line change
@@ -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<ChainlistPaginationProps> = ({
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 (
<PaginationButtons
activePage={activePage}
totalPages={totalPages}
onPageClick={(page) => router.push(createPageURL(page))}
/>
);
};
Original file line number Diff line number Diff line change
@@ -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<HTMLInputElement>(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 (
<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 by token address or chain ID"
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)}
ref={inputRef}
/>
{searchParams?.has("query") && (
<Button
size="icon"
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"
variant="outline"
onClick={() => {
const params = new URLSearchParams(searchParams ?? undefined);
params.delete("query");
params.delete("page");
const url = cleanUrl(`${pathname}?${params.toString()}`);
router.replace(url);
}}
>
<XCircleIcon className="size-5" />
</Button>
)}
</div>
);
};
Original file line number Diff line number Diff line change
@@ -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<QueryTypeProps> = ({ 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 (
<div className="flex flex-row">
<ToolTipLabel label="Origin" contentClassName="w-full">
<Button
size="icon"
variant={activeType === "origin" ? "default" : "outline"}
onClick={() => {
router.replace(createPageURL("origin"));
}}
className="rounded-r-none"
>
<ArrowUpRightIcon strokeWidth={1} />
</Button>
</ToolTipLabel>
<ToolTipLabel label="Destination" contentClassName="w-full">
<Button
variant={activeType === "destination" ? "default" : "outline"}
size="icon"
onClick={() => {
router.replace(createPageURL("destination"));
}}
className="rounded-l-none"
>
<ArrowDownLeftIcon strokeWidth={1} />
</Button>
</ToolTipLabel>
</div>
);
};
Original file line number Diff line number Diff line change
@@ -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<RouteListViewProps> = ({ 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 (
<div className="flex flex-row">
<Button
size="icon"
variant={activeView === "table" ? "default" : "outline"}
onClick={() => {
router.replace(createPageURL("table"));
}}
className="rounded-r-none"
>
<ListIcon strokeWidth={1} />
</Button>
<Button
variant={activeView === "grid" ? "default" : "outline"}
size="icon"
onClick={() => {
router.replace(createPageURL("grid"));
}}
className="rounded-l-none"
>
<Grid2X2Icon strokeWidth={1} />
</Button>
</div>
);
};
Original file line number Diff line number Diff line change
@@ -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 (
<div className="relative h-full">
<Card className="h-full w-full transition-colors hover:border-active-border">
<CardHeader className="flex flex-row items-center justify-between p-4">
<div className="flex flex-row items-center gap-2">
{resolvedOriginTokenIconUri ? (
<img
src={resolvedOriginTokenIconUri}
alt={originTokenAddress}
className="size-8 rounded-full bg-white"
/>
) : (
<div className="size-8 rounded-full bg-white/10" />
)}
{resolvedDestinationTokenIconUri ? (
<img
src={resolvedDestinationTokenIconUri}
alt={destinationTokenAddress}
className="-translate-x-4 size-8 rounded-full bg-white ring-2 ring-card"
/>
) : (
<div className="-translate-x-4 size-8 rounded-full bg-muted-foreground ring-2 ring-card" />
)}
</div>
</CardHeader>

<CardContent className="px-4 pt-0 pb-4">
<table className="w-full">
<tbody className="text-sm [&_td>*]:min-h-[25px]">
<tr>
<th className="text-left font-normal text-base">
{originTokenName === "ETH"
? originChain.nativeCurrency.name
: originTokenName}
</th>
<td className="text-right text-base text-muted-foreground">
{originChain.name}
</td>
</tr>
<tr>
<th className="text-left font-normal text-base">
{destinationTokenName === "ETH"
? destinationChain.nativeCurrency.name
: destinationTokenName}
</th>
<td className="text-right text-base text-muted-foreground">
{destinationChain.name}
</td>
</tr>
</tbody>
</table>
</CardContent>
</Card>
</div>
);
}
Loading
Loading