+
+
-
-
- {renderData.asset.metadata.name}
+
+
+ {data.asset.metadata.name}
-
- {renderData.asset.metadata.name}
-
-
+ {data.asset.metadata.description && (
+
+ {data.asset.metadata.description}
+
+ )}
+
-
-
-
-
- Asset contract address
-
-
+
+
+ {/* NFT contract address */}
+
+ NFT contract address
+
-
-
- Token ID
-
-
+
+
+
+ {/* Token ID */}
+
+ Token ID
+
-
-
- Seller
-
-
+
+
+
+ {/* Seller */}
+
+ Seller
+
-
-
- Listing ID
-
-
+
+
+
+ {/* Listing ID */}
+
+ Listing ID
+
-
-
- Type
-
- {renderData.asset.type}
-
- Status
-
-
+
+
+
+ {/* Type */}
+
+ Type
+ {data.asset.type}
+
+
+ {/* Status */}
+
+ Status
+
- {LISTING_STATUS[renderData.status]}
+ {LISTING_STATUS[data.status]}
-
-
- Quantity
-
-
- {(renderData.quantity || 0n).toString()}{" "}
+
+
+
+ {/* Quantity */}
+
+ Quantity
+
+ {(data.quantity || 0n).toString()}{" "}
{/* For listings that are completed, the `quantity` would be `0`
- So we show this text to make it clear */}
- {LISTING_STATUS[renderData.status] === "Completed"
+ So we show this text to make it clear */}
+ {LISTING_STATUS[data.status] === "Completed"
? "(Sold out)"
: ""}
-
+
+
+
+ {/* Price */}
+ {data.type === "direct-listing" && (
+
+ Price
+
+ {data.currencyValuePerToken.displayValue}{" "}
+ {data.currencyValuePerToken.symbol}
+
+
+ )}
+
+
+
+ {data?.asset.metadata.properties ? (
+
+ ) : null}
- {renderData.type === "direct-listing" && (
- <>
-
- Price
-
-
- {renderData.currencyValuePerToken.displayValue}{" "}
- {renderData.currencyValuePerToken.symbol}
-
- >
- )}
-
-
- {data?.asset.metadata.properties ? (
-
- Attributes
-
-
- ) : null}
-
{isOwner && (
)}
- {!isOwner && renderData.status === "ACTIVE" && (
+ {!isOwner && data.status === "ACTIVE" && (
{
toast.error("Failed to buy listing", {
description: error.message,
@@ -184,10 +191,18 @@ export const ListingDrawer: React.FC = ({
}}
quantity={1n}
>
- Buy Listing
+ Buy NFT
)}
);
-};
+}
+
+function StyledTableHead({ children }: { children: React.ReactNode }) {
+ return
{children};
+}
+
+function StyledTableCell({ children }: { children: React.ReactNode }) {
+ return
{children};
+}
diff --git a/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/(marketplace)/components/marketplace-table.tsx b/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/(marketplace)/components/marketplace-table.tsx
index 800aee0badd..8efac373528 100644
--- a/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/(marketplace)/components/marketplace-table.tsx
+++ b/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/(marketplace)/components/marketplace-table.tsx
@@ -1,49 +1,29 @@
-// biome-ignore-all lint/nursery/noNestedComponentDefinitions: TODO
-/** biome-ignore-all lint/a11y/useSemanticElements: FIXME */
-
-import {
- IconButton,
- Select,
- Skeleton,
- Spinner,
- Table,
- TableContainer,
- Tbody,
- Td,
- Th,
- Thead,
- Tr,
- usePrevious,
-} from "@chakra-ui/react";
import type { UseQueryResult } from "@tanstack/react-query";
-import {
- ChevronFirstIcon,
- ChevronLastIcon,
- ChevronLeftIcon,
- ChevronRightIcon,
- MoveRightIcon,
-} from "lucide-react";
-import {
- type Dispatch,
- type SetStateAction,
- useEffect,
- useMemo,
- useState,
-} from "react";
-import { type Cell, type Column, usePagination, useTable } from "react-table";
+import { type Dispatch, type SetStateAction, useMemo, useState } from "react";
import type { ThirdwebContract } from "thirdweb";
import type {
DirectListing,
EnglishAuction,
} from "thirdweb/extensions/marketplace";
import { min } from "thirdweb/utils";
+import { PaginationButtons } from "@/components/blocks/pagination-buttons";
import { WalletAddress } from "@/components/blocks/wallet-address";
import { MediaCell } from "@/components/contracts/media-cell";
import { Button } from "@/components/ui/button";
+import { Skeleton } from "@/components/ui/skeleton";
+import {
+ Table,
+ TableBody,
+ TableCell,
+ TableContainer,
+ TableHead,
+ TableHeader,
+ TableRow,
+} from "@/components/ui/table";
import { ListingDrawer } from "./listing-drawer";
import { LISTING_STATUS } from "./types";
-interface MarketplaceTableProps {
+export function MarketplaceTable(props: {
contract: ThirdwebContract;
getAllQueryResult: UseQueryResult
;
getValidQueryResult: UseQueryResult;
@@ -59,279 +39,217 @@ interface MarketplaceTableProps {
}>
>;
isLoggedIn: boolean;
-}
-
-const DEFAULT_QUERY_STATE = { count: 50, start: 0 };
+ title: string;
+ cta: React.ReactNode;
+}) {
+ const {
+ contract,
+ getAllQueryResult,
+ getValidQueryResult,
+ totalCountQuery,
+ queryParams,
+ setQueryParams,
+ isLoggedIn,
+ cta,
+ title,
+ } = props;
-export const MarketplaceTable: React.FC = ({
- contract,
- getAllQueryResult,
- getValidQueryResult,
- totalCountQuery,
- queryParams,
- setQueryParams,
- isLoggedIn,
-}) => {
const [listingsToShow, setListingsToShow_] = useState<"all" | "valid">("all");
const setListingsToShow = (value: "all" | "valid") => {
- setQueryParams(DEFAULT_QUERY_STATE);
+ setQueryParams({ count: 50, start: 0 });
setListingsToShow_(value);
};
- const prevData = usePrevious(
- listingsToShow === "all"
- ? getAllQueryResult?.data
- : getValidQueryResult?.data,
- );
-
const renderData = useMemo(() => {
if (listingsToShow === "all") {
- return getAllQueryResult?.data || prevData;
+ return getAllQueryResult?.data;
}
- return getValidQueryResult?.data || prevData;
- }, [getAllQueryResult, getValidQueryResult, listingsToShow, prevData]);
+ return getValidQueryResult?.data;
+ }, [getAllQueryResult, getValidQueryResult, listingsToShow]);
- const tableColumns: Column[] = useMemo(() => {
- return [
- {
- accessor: (row) => row.id.toString(),
- Header: "Listing Id",
- },
- {
- accessor: (row) => row.asset.metadata,
- // biome-ignore lint/suspicious/noExplicitAny: FIXME
- Cell: (cell: any) => ,
- Header: "Media",
- },
- {
- accessor: (row) => row.asset.metadata.name ?? "N/A",
- Header: "Name",
- },
- {
- accessor: (row) => row.creatorAddress,
- // biome-ignore lint/suspicious/noExplicitAny: FIXME
- Cell: ({ cell }: { cell: Cell }) => (
-
- ),
- Header: "Creator",
- },
- {
- accessor: (row) =>
- (row as DirectListing)?.currencyValuePerToken ||
- (row as EnglishAuction)?.buyoutCurrencyValue,
- // biome-ignore lint/suspicious/noExplicitAny: FIXME
- Cell: ({ cell }: { cell: Cell }) => {
- return (
-
- {cell.value.displayValue} {cell.value.symbol}
-
- );
- },
- Header: "Price",
- },
- {
- accessor: (row) => LISTING_STATUS[row.status],
- Header: "Status",
- },
- ];
- }, [contract.client]);
-
- const {
- getTableProps,
- getTableBodyProps,
- headerGroups,
- prepareRow,
- page,
- canPreviousPage,
- canNextPage,
- pageCount,
- gotoPage,
- nextPage,
- previousPage,
- setPageSize,
- state: { pageIndex, pageSize },
- } = useTable(
- {
- columns: tableColumns,
- // biome-ignore lint/suspicious/noExplicitAny: FIXME
- data: (renderData as any) || [],
- initialState: {
- pageIndex: 0,
- pageSize: queryParams.count,
- },
- manualPagination: true,
- pageCount: Math.max(
- Math.ceil(
- Number(
- // To avoid overflow issue
- min(totalCountQuery.data || 0n, BigInt(Number.MAX_SAFE_INTEGER)),
- ) / queryParams.count,
- ),
- 1,
- ),
- },
- // FIXME: re-work tables and pagination with @tanstack/table@latest - which (I believe) does not need this workaround anymore
- // eslint-disable-next-line react-compiler/react-compiler
- usePagination,
+ const pageSize = queryParams.count;
+ const currentPage = Math.floor(queryParams.start / pageSize) + 1; // PaginationButtons uses 1-based indexing
+ const totalCount = Number(
+ min(totalCountQuery.data || 0n, BigInt(Number.MAX_SAFE_INTEGER)),
);
+ const totalPages = Math.max(Math.ceil(totalCount / pageSize), 1);
+ const showPagination = totalPages > 1;
- // FIXME: re-work tables and pagination with @tanstack/table@latest - which (I believe) does not need this workaround anymore
- // eslint-disable-next-line no-restricted-syntax
- useEffect(() => {
- setQueryParams({ count: pageSize, start: pageIndex * pageSize });
- }, [pageIndex, pageSize, setQueryParams]);
+ // Pagination handler for PaginationButtons
+ const handlePageChange = (page: number) => {
+ setQueryParams({ count: pageSize, start: (page - 1) * pageSize });
+ };
- const [tokenRow, setTokenRow] = useState<
+ const [isDrawerOpen, setIsDrawerOpen] = useState(false);
+
+ const [selectedToken, setSelectedToken] = useState<
DirectListing | EnglishAuction | null
>(null);
+ const isFetching =
+ (listingsToShow === "all" && getAllQueryResult.isFetching) ||
+ (listingsToShow === "valid" && getValidQueryResult.isFetching);
+
return (
-
-
-
-
+
+
+
{title}
+
+
+
+
+
+ {cta}
+
-
- {((listingsToShow === "all" && getAllQueryResult.isFetching) ||
- (listingsToShow === "valid" && getValidQueryResult.isFetching)) && (
-
+
+
+
+
+ {renderData?.length === 0 ? (
+
+ ) : (
+
+
+
+
+ Listing Id
+
+
+ Media
+
+
+ Name
+
+
+ Creator
+
+
+ Price
+
+
+ Status
+
+
+
+
+ {isFetching &&
+ Array.from({ length: 5 }).map((_, index) => (
+ // biome-ignore lint/suspicious/noArrayIndexKey: ok
+
+ ))}
+
+ {!isFetching &&
+ renderData?.map((row, _rowIndex) => (
+ {
+ setSelectedToken(row);
+ setIsDrawerOpen(true);
+ }}
+ >
+ {row.id.toString()}
+
+
+
+ {row.asset.metadata.name ?? "N/A"}
+
+
+
+
+
+ {(row as DirectListing)?.currencyValuePerToken
+ ?.displayValue ||
+ (row as EnglishAuction)?.buyoutCurrencyValue
+ ?.displayValue ||
+ "N/A"}{" "}
+ {(row as DirectListing)?.currencyValuePerToken
+ ?.symbol ||
+ (row as EnglishAuction)?.buyoutCurrencyValue
+ ?.symbol ||
+ ""}
+
+
+ {LISTING_STATUS[row.status]}
+
+ ))}
+
+
)}
+
+
+ {showPagination && (
+
+ )}
+
+ {selectedToken && (
setTokenRow(null)}
+ isOpen={isDrawerOpen}
+ onClose={() => setIsDrawerOpen(false)}
/>
-
-
- {headerGroups.map((headerGroup, headerGroupIndex) => (
- // biome-ignore lint/suspicious/noArrayIndexKey: FIXME
-
- {headerGroup.headers.map((column, columnIndex) => (
-
-
- {column.render("Header")}
-
- |
- ))}
- {/* // Need to add an empty header for the drawer button */}
- |
-
- ))}
-
-
- {page.map((row, rowIndex) => {
- prepareRow(row);
- return (
- setTokenRow(row.original)}
- role="group"
- style={{ cursor: "pointer" }}
- >
- {row.cells.map((cell, cellIndex) => (
-
- {cell.render("Cell")}
- |
- ))}
-
-
- |
-
- );
- })}
-
-
-
-
-
-
}
- isDisabled={!canPreviousPage || totalCountQuery.isPending}
- onClick={() => gotoPage(0)}
- />
-
}
- isDisabled={!canPreviousPage || totalCountQuery.isPending}
- onClick={() => previousPage()}
- />
-
- Page {pageIndex + 1} of{" "}
-
- {pageCount}
-
-
-
}
- isDisabled={!canNextPage || totalCountQuery.isPending}
- onClick={() => nextPage()}
- />
-
}
- isDisabled={!canNextPage || totalCountQuery.isPending}
- onClick={() => gotoPage(pageCount - 1)}
- />
-
-
-
-
+ )}
);
-};
+}
+
+function SkeletonRow() {
+ return (
+
+ {/* listing id */}
+
+
+
+ {/* media */}
+
+
+
+ {/* name */}
+
+
+
+ {/* creator */}
+
+
+
+ {/* price */}
+
+
+
+ {/* status */}
+
+
+
+
+ );
+}
diff --git a/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/(marketplace)/direct-listings/ContractDirectListingsPage.tsx b/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/(marketplace)/direct-listings/ContractDirectListingsPage.tsx
index db5f9e1d015..9bcad03b1cb 100644
--- a/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/(marketplace)/direct-listings/ContractDirectListingsPage.tsx
+++ b/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/(marketplace)/direct-listings/ContractDirectListingsPage.tsx
@@ -4,33 +4,24 @@ import type { ThirdwebContract } from "thirdweb";
import { CreateListingButton } from "../components/list-button";
import { DirectListingsTable } from "./components/table";
-interface ContractDirectListingsPageProps {
+export function ContractDirectListingsPage(props: {
contract: ThirdwebContract;
isLoggedIn: boolean;
isInsightSupported: boolean;
-}
-
-export const ContractDirectListingsPage: React.FC<
- ContractDirectListingsPageProps
-> = ({ contract, isLoggedIn, isInsightSupported }) => {
+}) {
return (
-
-
-
- Direct Listings
-
-
-
-
-
-
-
-
+
+ }
+ />
);
-};
+}
diff --git a/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/(marketplace)/direct-listings/components/table.tsx b/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/(marketplace)/direct-listings/components/table.tsx
index 4ccbaadbd0d..3df767cb9c5 100644
--- a/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/(marketplace)/direct-listings/components/table.tsx
+++ b/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/(marketplace)/direct-listings/components/table.tsx
@@ -10,39 +10,38 @@ import {
import { useReadContract } from "thirdweb/react";
import { MarketplaceTable } from "../../components/marketplace-table";
-interface DirectListingsTableProps {
+export function DirectListingsTable(props: {
contract: ThirdwebContract;
isLoggedIn: boolean;
-}
-
-const DEFAULT_QUERY_STATE = { count: 50, start: 0 };
-
-export const DirectListingsTable: React.FC
= ({
- contract,
- isLoggedIn,
-}) => {
- const [queryParams, setQueryParams] = useState(DEFAULT_QUERY_STATE);
+ cta: React.ReactNode;
+}) {
+ const [queryParams, setQueryParams] = useState({ count: 50, start: 0 });
const getAllQueryResult = useReadContract(getAllListings, {
- contract,
+ contract: props.contract,
count: BigInt(queryParams.count),
start: queryParams.start,
});
+
const getValidQueryResult = useReadContract(getAllValidListings, {
- contract,
+ contract: props.contract,
count: BigInt(queryParams.count),
start: queryParams.start,
});
- const totalCountQuery = useReadContract(totalListings, { contract });
+ const totalCountQuery = useReadContract(totalListings, {
+ contract: props.contract,
+ });
return (
);
-};
+}
diff --git a/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/(marketplace)/english-auctions/ContractEnglishAuctionsPage.tsx b/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/(marketplace)/english-auctions/ContractEnglishAuctionsPage.tsx
index 805ab5c34c3..a98fe99b053 100644
--- a/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/(marketplace)/english-auctions/ContractEnglishAuctionsPage.tsx
+++ b/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/(marketplace)/english-auctions/ContractEnglishAuctionsPage.tsx
@@ -4,33 +4,24 @@ import type { ThirdwebContract } from "thirdweb";
import { CreateListingButton } from "../components/list-button";
import { EnglishAuctionsTable } from "./components/table";
-interface ContractEnglishAuctionsProps {
+export function ContractEnglishAuctionsPage(props: {
contract: ThirdwebContract;
isLoggedIn: boolean;
isInsightSupported: boolean;
-}
-
-export const ContractEnglishAuctionsPage: React.FC<
- ContractEnglishAuctionsProps
-> = ({ contract, isLoggedIn, isInsightSupported }) => {
+}) {
return (
-
-
-
- English Auctions
-
-
-
-
-
-
-
-
+
+ }
+ />
);
-};
+}
diff --git a/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/(marketplace)/english-auctions/components/table.tsx b/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/(marketplace)/english-auctions/components/table.tsx
index 2d20b7360fd..c3e91ba0a8b 100644
--- a/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/(marketplace)/english-auctions/components/table.tsx
+++ b/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/(marketplace)/english-auctions/components/table.tsx
@@ -10,39 +10,37 @@ import {
import { useReadContract } from "thirdweb/react";
import { MarketplaceTable } from "../../components/marketplace-table";
-interface EnglishAuctionsTableProps {
+export function EnglishAuctionsTable(props: {
contract: ThirdwebContract;
isLoggedIn: boolean;
-}
-
-const DEFAULT_QUERY_STATE = { count: 50, start: 0 };
-
-export const EnglishAuctionsTable: React.FC = ({
- contract,
- isLoggedIn,
-}) => {
- const [queryParams, setQueryParams] = useState(DEFAULT_QUERY_STATE);
+ cta: React.ReactNode;
+}) {
+ const [queryParams, setQueryParams] = useState({ count: 50, start: 0 });
const getAllQueryResult = useReadContract(getAllAuctions, {
- contract,
+ contract: props.contract,
count: BigInt(queryParams.count),
start: queryParams.start,
});
const getValidQueryResult = useReadContract(getAllValidAuctions, {
- contract,
+ contract: props.contract,
count: BigInt(queryParams.count),
start: queryParams.start,
});
- const totalCountQuery = useReadContract(totalAuctions, { contract });
+ const totalCountQuery = useReadContract(totalAuctions, {
+ contract: props.contract,
+ });
return (
);
-};
+}