Skip to content

Commit 324a08e

Browse files
committed
[TOOL-4802] NFT Asset Page
1 parent 5f57300 commit 324a08e

File tree

27 files changed

+2387
-69
lines changed

27 files changed

+2387
-69
lines changed
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
"use client";
2+
import { Suspense } from "react";
3+
import { ClientOnly } from "../../components/ClientOnly/ClientOnly";
4+
import { useIsMobile } from "../hooks/use-mobile";
5+
6+
export function ResponsiveLayout(props: {
7+
desktop: React.ReactNode;
8+
mobile: React.ReactNode;
9+
fallback: React.ReactNode;
10+
debugMode?: boolean;
11+
}) {
12+
const isMobile = useIsMobile();
13+
14+
if (props.debugMode) {
15+
return props.fallback;
16+
}
17+
18+
return (
19+
<Suspense fallback={props.fallback}>
20+
<ClientOnly ssr={props.fallback}>
21+
{isMobile ? props.mobile : props.desktop}
22+
</ClientOnly>
23+
</Suspense>
24+
);
25+
}

apps/dashboard/src/@/components/blocks/wallet-address.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ export function WalletAddress(props: {
2424
className?: string;
2525
iconClassName?: string;
2626
client: ThirdwebClient;
27+
preventOpenOnFocus?: boolean;
2728
}) {
2829
// default back to zero address if no address provided
2930
const address = useMemo(() => props.address || ZERO_ADDRESS, [props.address]);
@@ -64,7 +65,10 @@ export function WalletAddress(props: {
6465

6566
return (
6667
<HoverCard>
67-
<HoverCardTrigger asChild>
68+
<HoverCardTrigger
69+
asChild
70+
tabIndex={props.preventOpenOnFocus ? -1 : undefined}
71+
>
6872
<Button
6973
onClick={(e) => e.stopPropagation()}
7074
variant="link"

apps/dashboard/src/@/components/pagination-buttons.tsx

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ export const PaginationButtons = (props: {
1919
activePage: number;
2020
totalPages: number;
2121
onPageClick: (page: number) => void;
22+
className?: string;
2223
}) => {
2324
const { activePage, totalPages, onPageClick: setPage } = props;
2425
const [inputHasError, setInputHasError] = useState(false);
@@ -40,11 +41,37 @@ export const PaginationButtons = (props: {
4041
}
4142
}
4243

44+
// if only two pages, show "prev" and "next"
45+
if (totalPages === 2) {
46+
return (
47+
<Pagination className={props.className}>
48+
<PaginationContent>
49+
<PaginationItem>
50+
<PaginationPrevious
51+
disabled={activePage === 1}
52+
onClick={() => {
53+
setPage(activePage - 1);
54+
}}
55+
/>
56+
</PaginationItem>
57+
<PaginationItem>
58+
<PaginationNext
59+
disabled={activePage === totalPages}
60+
onClick={() => {
61+
setPage(activePage + 1);
62+
}}
63+
/>
64+
</PaginationItem>
65+
</PaginationContent>
66+
</Pagination>
67+
);
68+
}
69+
4370
// just render all the page buttons directly
4471
if (totalPages <= 6) {
4572
const pages = [...Array(totalPages)].map((_, i) => i + 1);
4673
return (
47-
<Pagination>
74+
<Pagination className={props.className}>
4875
<PaginationContent>
4976
{pages.map((page) => (
5077
<PaginationItem key={page}>
@@ -64,7 +91,7 @@ export const PaginationButtons = (props: {
6491
}
6592

6693
return (
67-
<Pagination>
94+
<Pagination className={props.className}>
6895
<PaginationContent>
6996
<PaginationItem>
7097
<PaginationPrevious

apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/_utils/newPublicPage.ts

Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
import { resolveFunctionSelectors } from "lib/selectors";
22
import type { ThirdwebContract } from "thirdweb";
3-
import { isERC20 } from "thirdweb/extensions/erc20";
3+
import * as ERC721Ext from "thirdweb/extensions/erc721";
4+
import * as ERC1155Ext from "thirdweb/extensions/erc1155";
5+
import { supportedERCs } from "./detectedFeatures/supportedERCs";
46

5-
export type NewPublicPageType = "erc20";
7+
export type NewPublicPageType = "erc20" | "erc1155" | "erc721";
68

79
export async function shouldRenderNewPublicPage(
810
contract: ThirdwebContract,
@@ -16,9 +18,29 @@ export async function shouldRenderNewPublicPage(
1618
return false;
1719
}
1820

19-
const isERC20Contract = isERC20(functionSelectors);
21+
const _supportedERCs = supportedERCs(functionSelectors);
2022

21-
if (isERC20Contract) {
23+
// Edition Drop
24+
if (
25+
_supportedERCs.isERC1155 &&
26+
ERC1155Ext.isGetNFTsSupported(functionSelectors)
27+
) {
28+
return {
29+
type: "erc1155",
30+
};
31+
}
32+
33+
// NFT Drop
34+
if (
35+
_supportedERCs.isERC721 &&
36+
ERC721Ext.isGetNFTsSupported(functionSelectors)
37+
) {
38+
return {
39+
type: "erc721",
40+
};
41+
}
42+
43+
if (_supportedERCs.isERC20) {
2244
return {
2345
type: "erc20",
2446
};

apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/nfts/[tokenId]/shared-nfts-token-page.tsx

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ import { redirectToContractLandingPage } from "../../../../../../team/[team_slug
55
import { buildContractPagePath } from "../../_utils/contract-page-path";
66
import { getContractPageParamsInfo } from "../../_utils/getContractFromParams";
77
import { getContractPageMetadata } from "../../_utils/getContractPageMetadata";
8+
import { shouldRenderNewPublicPage } from "../../_utils/newPublicPage";
9+
import { NFTPublicPage } from "../../public-pages/nft/nft-page";
810
import { TokenIdPageClient } from "./TokenIdPage.client";
911
import { TokenIdPage } from "./token-id";
1012

@@ -40,6 +42,22 @@ export async function SharedNFTTokenPage(props: {
4042
);
4143
}
4244

45+
// public /nfts/[tokenId] page
46+
if (!props.projectMeta) {
47+
const meta = await shouldRenderNewPublicPage(info.serverContract);
48+
if (meta && (meta.type === "erc721" || meta.type === "erc1155")) {
49+
return (
50+
<NFTPublicPage
51+
serverContract={info.serverContract}
52+
clientContract={info.clientContract}
53+
chainMetadata={info.chainMetadata}
54+
tokenId={props.tokenId}
55+
type={meta.type}
56+
/>
57+
);
58+
}
59+
}
60+
4361
const { clientContract, serverContract, isLocalhostChain } = info;
4462
if (isLocalhostChain) {
4563
return (

apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/nfts/shared-nfts-page.tsx

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import type { ProjectMeta } from "../../../../../team/[team_slug]/[project_slug]
33
import { redirectToContractLandingPage } from "../../../../../team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/utils";
44
import { getContractPageParamsInfo } from "../_utils/getContractFromParams";
55
import { getContractPageMetadata } from "../_utils/getContractPageMetadata";
6+
import { shouldRenderNewPublicPage } from "../_utils/newPublicPage";
67
import { ContractNFTPage } from "./ContractNFTPage";
78
import { ContractNFTPageClient } from "./ContractNFTPage.client";
89

@@ -44,6 +45,18 @@ export async function SharedNFTPage(props: {
4445
});
4546
}
4647

48+
// public nft page doesn't have /nfts page
49+
if (!props.projectMeta) {
50+
const shouldHide = await shouldRenderNewPublicPage(info.serverContract);
51+
if (shouldHide) {
52+
redirectToContractLandingPage({
53+
contractAddress: props.contractAddress,
54+
chainIdOrSlug: props.chainIdOrSlug,
55+
projectMeta: props.projectMeta,
56+
});
57+
}
58+
}
59+
4760
return (
4861
<ContractNFTPage
4962
contract={clientContract}

apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/public-pages/_components/PageHeader.tsx

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,23 @@
11
import { ToggleThemeButton } from "@/components/color-mode-toggle";
22
import Link from "next/link";
3+
import { cn } from "../../../../../../../../@/lib/utils";
34
import { ThirdwebMiniLogo } from "../../../../../../components/ThirdwebMiniLogo";
45
import { PublicPageConnectButton } from "./PublicPageConnectButton";
56

6-
export function PageHeader() {
7+
export function PageHeader(props: {
8+
containerClassName?: string;
9+
}) {
710
return (
811
<div className="border-b border-dashed">
9-
<header className="container flex max-w-8xl justify-between py-3">
12+
<header
13+
className={cn(
14+
"container flex max-w-8xl justify-between py-3",
15+
props.containerClassName,
16+
)}
17+
>
1018
<div className="flex items-center gap-4">
1119
<Link href="/team" className="flex items-center gap-2">
12-
<ThirdwebMiniLogo className="h-6" />
20+
<ThirdwebMiniLogo className="h-5" />
1321
<span className="hidden font-bold text-2xl tracking-tight lg:block">
1422
thirdweb
1523
</span>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import { SkeletonContainer } from "@/components/ui/skeleton";
2+
3+
export function TokenPrice(props: {
4+
strikethrough: boolean;
5+
data:
6+
| {
7+
priceInTokens: number;
8+
symbol: string;
9+
}
10+
| undefined;
11+
}) {
12+
return (
13+
<SkeletonContainer
14+
skeletonData={"0.00 ETH"}
15+
loadedData={
16+
props.data
17+
? props.data.priceInTokens === 0
18+
? "FREE"
19+
: `${compactNumberFormatter.format(props.data.priceInTokens)} ${props.data.symbol}`
20+
: undefined
21+
}
22+
render={(v) => {
23+
if (props.strikethrough) {
24+
return (
25+
<s className="font-medium text-muted-foreground text-sm line-through decoration-muted-foreground/50">
26+
{v}
27+
</s>
28+
);
29+
}
30+
return <span className="font-medium text-foreground text-sm">{v}</span>;
31+
}}
32+
/>
33+
);
34+
}
35+
36+
const compactNumberFormatter = new Intl.NumberFormat("en-US", {
37+
notation: "compact",
38+
maximumFractionDigits: 10,
39+
});

apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/public-pages/erc20/_components/ContractHeader.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ export function ContractHeaderUI(props: {
4141
chainMetadata: ChainMetadata;
4242
clientContract: ThirdwebContract;
4343
socialUrls: object;
44+
imageClassName?: string;
4445
}) {
4546
const socialUrls = useMemo(() => {
4647
const socialUrlsValue: { name: string; href: string }[] = [];
@@ -67,7 +68,10 @@ export function ContractHeaderUI(props: {
6768
<div className="flex flex-col items-start gap-4 border-b border-dashed py-8 lg:flex-row lg:items-center">
6869
{props.image && (
6970
<Img
70-
className="size-20 shrink-0 rounded-full border bg-muted"
71+
className={cn(
72+
"size-20 shrink-0 rounded-full border bg-muted",
73+
props.imageClassName,
74+
)}
7175
src={
7276
props.image
7377
? resolveSchemeWithErrorHandler({

0 commit comments

Comments
 (0)