Skip to content
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
102 changes: 101 additions & 1 deletion apps/dashboard/src/@/components/blocks/NetworkSelectors.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
"use client";

import { useQuery } from "@tanstack/react-query";
import { useCallback, useMemo } from "react";
import type { ThirdwebClient } from "thirdweb";
import { Bridge, type ThirdwebClient } from "thirdweb";
import { MultiSelect } from "@/components/blocks/multi-select";
import { SelectWithSearch } from "@/components/blocks/select-with-search";
import { Badge } from "@/components/ui/badge";
Expand Down Expand Up @@ -296,3 +297,102 @@ export function SingleNetworkSelector(props: {
/>
);
}

export function BridgeNetworkSelector(props: {
chainId: number | undefined;
onChange: (chainId: number) => void;
className?: string;
popoverContentClassName?: string;
side?: "left" | "right" | "top" | "bottom";
align?: "center" | "start" | "end";
placeholder?: string;
client: ThirdwebClient;
}) {
const chainsQuery = useQuery({
queryKey: ["bridge-chains"],
queryFn: () => {
return Bridge.chains({ client: props.client });
},
refetchOnMount: false,
refetchOnWindowFocus: false,
});

const options = useMemo(() => {
return (chainsQuery.data || [])?.map((chain) => {
return {
label: cleanChainName(chain.name),
value: String(chain.chainId),
};
});
}, [chainsQuery.data]);

const searchFn = useCallback(
(option: Option, searchValue: string) => {
const chain = chainsQuery.data?.find(
(chain) => chain.chainId === Number(option.value),
);
if (!chain) {
return false;
}

if (Number.isInteger(Number.parseInt(searchValue))) {
return String(chain.chainId).startsWith(searchValue);
}
return chain.name.toLowerCase().includes(searchValue.toLowerCase());
},
[chainsQuery.data],
);

const renderOption = useCallback(
(option: Option) => {
const chain = chainsQuery.data?.find(
(chain) => chain.chainId === Number(option.value),
);
if (!chain) {
return option.label;
}

return (
<div className="flex justify-between gap-4">
<span className="flex grow gap-2 truncate text-left">
<ChainIconClient
className="size-5"
client={props.client}
src={chain.icon}
loading="lazy"
/>
{cleanChainName(chain.name)}
</span>
</div>
);
},
[chainsQuery.data, props.client],
);

const isLoadingChains = chainsQuery.isPending;

return (
<SelectWithSearch
align={props.align}
className={props.className}
closeOnSelect={true}
disabled={isLoadingChains}
onValueChange={(chainId) => {
props.onChange(Number(chainId));
}}
options={options}
overrideSearchFn={searchFn}
placeholder={
isLoadingChains
? "Loading Chains..."
: props.placeholder || "Select Chain"
}
popoverContentClassName={props.popoverContentClassName}
renderOption={renderOption}
searchPlaceholder="Search by Name or Chain ID"
showCheck={false}
side={props.side}
value={props.chainId?.toString()}
/>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,9 @@ export function PublicPageConnectButton(props: {
connectButton={{
className: props.connectButtonClassName,
}}
detailsButton={{
className: props.connectButtonClassName,
}}
connectModal={{
privacyPolicyUrl: "/privacy-policy",
showThirdwebBranding: false,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import Link from "next/link";
import { ToggleThemeButton } from "@/components/blocks/color-mode-toggle";
import { cn } from "@/lib/utils";
import { ThirdwebMiniLogo } from "../../../components/ThirdwebMiniLogo";
import { PublicPageConnectButton } from "../../(chain)/[chain_id]/[contractAddress]/public-pages/_components/PublicPageConnectButton";

export function PageHeader(props: { containerClassName?: string }) {
return (
<div className="border-b">
<header
className={cn(
"container flex max-w-7xl justify-between py-3",
props.containerClassName,
)}
>
<div className="flex items-center gap-4">
<Link className="flex items-center gap-2" href="/team">
<ThirdwebMiniLogo className="h-5" />
<span className="hidden font-bold text-2xl tracking-tight lg:block">
thirdweb
</span>
</Link>
</div>

<div className="flex items-center gap-5">
<Link
href="https://portal.thirdweb.com/bridge"
target="_blank"
className="text-sm text-muted-foreground hover:text-foreground"
>
Docs
</Link>
<ToggleThemeButton />
<PublicPageConnectButton connectButtonClassName="!rounded-full" />
</div>
</header>
</div>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
"use client";

import { useQuery } from "@tanstack/react-query";
import { cn } from "@workspace/ui/lib/utils";
import { ActivityIcon, TrendingUpIcon } from "lucide-react";
import { useState } from "react";
import { Bridge } from "thirdweb";
import { BridgeNetworkSelector } from "@/components/blocks/NetworkSelectors";
import { Button } from "@/components/ui/button";
import { getClientThirdwebClient } from "@/constants/thirdweb-client.client";
import { TokensTable } from "./tokens-table";

const client = getClientThirdwebClient();

const pageSize = 20;

export function TokenPage() {
const [page, setPage] = useState(1);
const [chainId, setChainId] = useState(1);
const [sortBy, setSortBy] = useState<"volume" | "market_cap">("volume");

const tokensQuery = useQuery({
queryKey: [
"tokens",
{
page,
chainId,
sortBy,
},
],
queryFn: () => {
return Bridge.tokens({
client: client,
chainId: chainId,
limit: pageSize,
offset: (page - 1) * pageSize,
sortBy,
});
},
refetchOnMount: false,
refetchOnWindowFocus: false,
});

return (
<div className="pb-20 pt-8">
<div className="container max-w-7xl">
<div className="mb-4 flex gap-3 flex-col lg:flex-row">
<BridgeNetworkSelector
client={client}
chainId={chainId}
onChange={setChainId}
className="rounded-full bg-card lg:w-fit min-w-[320px]"
popoverContentClassName="!w-[350px] rounded-xl overflow-hidden"
/>

<div className="flex gap-3">
<SortButton
label="Popular"
onClick={() => setSortBy("market_cap")}
isSelected={sortBy === "market_cap"}
icon={ActivityIcon}
/>
<SortButton
label="Trending"
onClick={() => setSortBy("volume")}
isSelected={sortBy === "volume"}
icon={TrendingUpIcon}
/>
</div>
</div>

<TokensTable
pageSize={pageSize}
tokens={tokensQuery.data ?? []}
isFetching={tokensQuery.isFetching}
pagination={{
onNext: () => setPage(page + 1),
onPrevious: () => setPage(page - 1),
nextDisabled: !!(
tokensQuery.data && tokensQuery.data.length < pageSize
),
previousDisabled: page === 1,
}}
/>
</div>
</div>
);
}

function SortButton(props: {
label: string;
onClick: () => void;
isSelected: boolean;
icon: React.FC<{ className?: string }>;
}) {
return (
<Button
variant="outline"
onClick={props.onClick}
className={cn(
"rounded-xl gap-2 bg-card",
props.isSelected && "bg-accent border-active-border",
)}
>
<props.icon className="size-3.5" />
{props.label}
</Button>
);
}
Loading
Loading