Dashboard: Delete Marketplace v2 listing page (#3975)

- [x] Marketplace v2 is very outdated
- [x] Marketplace v3 is out for over 2 years
- [x] After the _incident_, people should be on v3 now
- [x] This also helps remove more sdk v4 code

<!-- start pr-codex -->

---

## PR-Codex overview
The focus of this PR is to refactor and clean up the contract UI listings components.

### Detailed summary
- Removed unused components from listings page
- Updated route configuration for listings
- Refactored ListingStats component
- Updated imports and dependencies

> ✨ Ask PR-Codex anything about this PR by commenting with `/codex {your question}`

<!-- end pr-codex -->
This commit is contained in:
kien-ngo
2024-08-06 20:49:05 +00:00
parent ab5d6bcfe2
commit c05a94d985
7 changed files with 2 additions and 664 deletions
@@ -38,11 +38,6 @@ const LazyContractEnglishAuctionsPage = dynamic(() =>
({ ContractEnglishAuctionsPage }) => ContractEnglishAuctionsPage,
),
);
const LazyContractListingsPage = dynamic(() =>
import("../tabs/listings/page").then(
({ ContractListingsPage }) => ContractListingsPage,
),
);
const LazyContractSplitPage = dynamic(() =>
import("../tabs/split/page").then(
({ ContractSplitPage }) => ContractSplitPage,
@@ -196,16 +191,6 @@ export function useContractRouteConfig(
}),
component: LazyContractEnglishAuctionsPage,
},
{
title: "Listings",
path: "listings",
isEnabled: contractTypeQuery.isLoading
? "loading"
: contractTypeQuery.data === "marketplace"
? "enabled"
: "disabled",
component: LazyContractListingsPage,
},
{
title: "Balances",
path: "split",
@@ -1,76 +0,0 @@
import { useEVMContractInfo } from "@3rdweb-sdk/react";
import { Stack } from "@chakra-ui/react";
import { useCancelListing } from "@thirdweb-dev/react";
import type { ListingType, Marketplace } from "@thirdweb-dev/sdk";
import { TransactionButton } from "components/buttons/TransactionButton";
import { useTrack } from "hooks/analytics/useTrack";
import { useTxNotifications } from "hooks/useTxNotifications";
interface CancelTabProps {
contract: Marketplace;
listingId: string;
listingType: ListingType;
}
export const CancelTab: React.FC<CancelTabProps> = ({
contract,
listingId,
listingType,
}) => {
const trackEvent = useTrack();
const network = useEVMContractInfo()?.chain;
const cancelListing = useCancelListing(contract);
const { onSuccess, onError } = useTxNotifications(
"Listing cancelled",
"Error cancelling listing",
);
return (
<Stack pt={3} gap={3}>
{/* maybe some text? */}
<TransactionButton
transactionCount={1}
isLoading={cancelListing.isLoading}
onClick={() => {
trackEvent({
category: "marketplace",
action: "cancel-listing",
label: "attempt",
});
cancelListing.mutate(
{
id: listingId,
type: listingType,
},
{
onSuccess: () => {
trackEvent({
category: "marketplace",
action: "cancel-listing",
label: "success",
network,
});
onSuccess();
},
onError: (error) => {
trackEvent({
category: "marketplace",
action: "cancel-listing",
label: "error",
network,
error,
});
onError(error);
},
},
);
}}
colorScheme="primary"
alignSelf="flex-end"
>
Cancel Listing
</TransactionButton>
</Stack>
);
};
@@ -354,20 +354,6 @@ export const CreateListingsForm: React.FC<NFTMintForm> = ({
</Stack>
) : null}
</FormControl>
{/* Just don't support Marketplace v2 anymore */}
{/* <FormControl isDisabled={noNfts}>
<Heading as={FormLabel} size="label.lg">
Listing Type
</Heading>
<Select {...form.register("listingType")}>
<option value="direct">Direct</option>
<option value="auction">Auction</option>
</Select>
<FormHelperText>
The type of listing you want to create, either an auction or direct
listing.
</FormHelperText>
</FormControl> */}
<FormControl isRequired isDisabled={noNfts}>
<Heading as={FormLabel} size="label.lg">
Listing Currency
@@ -1,189 +0,0 @@
import {
Flex,
GridItem,
SimpleGrid,
Tab,
TabList,
TabPanel,
TabPanels,
Tabs,
usePrevious,
} from "@chakra-ui/react";
import type {
AuctionListing,
DirectListing,
Marketplace,
} from "@thirdweb-dev/sdk";
import { BigNumber } from "ethers";
import { useMemo } from "react";
import { useActiveAccount } from "thirdweb/react";
import { Badge, Card, CodeBlock, Drawer, Heading, Text } from "tw-components";
import { AddressCopyButton } from "tw-components/AddressCopyButton";
import { NFTMediaWithEmptyState } from "tw-components/nft-media";
import { CancelTab } from "./cancel-tab";
interface NFTDrawerProps {
contract: Marketplace;
isOpen: boolean;
onClose: () => void;
data: AuctionListing | DirectListing | null;
}
export const ListingDrawer: React.FC<NFTDrawerProps> = ({
isOpen,
onClose,
data,
contract,
}) => {
const address = useActiveAccount()?.address;
const prevData = usePrevious(data);
const renderData = data || prevData;
const isOwner = address === renderData?.sellerAddress;
const tokenId = renderData?.asset.id.toString() || "";
const tabs = useMemo(() => {
if (!renderData) {
return [];
}
const t = [
{
title: "Details",
isDisabled: false,
children: () => (
<Flex flexDir="column" gap={4}>
<Card as={Flex} flexDir="column" gap={3}>
<SimpleGrid rowGap={3} columns={12} placeItems="center left">
<GridItem colSpan={3}>
<Heading size="label.md">Token ID</Heading>
</GridItem>
<GridItem colSpan={9}>
<AddressCopyButton
size="xs"
address={tokenId}
title="Token ID"
/>
</GridItem>
<GridItem colSpan={3}>
<Heading size="label.md">Seller</Heading>
</GridItem>
<GridItem colSpan={9}>
<AddressCopyButton
size="xs"
address={renderData.sellerAddress}
/>
</GridItem>
<GridItem colSpan={3}>
<Heading size="label.md">Listing Type</Heading>
</GridItem>
<GridItem colSpan={9}>
<Badge
size="label.sm"
variant="subtle"
textTransform="capitalize"
>
{renderData?.type === 0 ? "Direct Listing" : "Auction"}
</Badge>
</GridItem>
<GridItem colSpan={3}>
<Heading size="label.md">Quantity</Heading>
</GridItem>
<GridItem colSpan={9}>
<Text fontFamily="mono" size="body.md">
{BigNumber.from(renderData.quantity || "0").toString()}
</Text>
</GridItem>
</SimpleGrid>
</Card>
{data?.asset.attributes || data?.asset.properties ? (
<Card as={Flex} flexDir="column" gap={4}>
<Heading size="label.md">Attributes</Heading>
<CodeBlock
code={
JSON.stringify(
data?.asset.attributes || data?.asset.properties,
null,
2,
) || ""
}
language="json"
canCopy={false}
wrap={false}
overflow="auto"
/>
</Card>
) : null}
</Flex>
),
},
{
title: "Cancel Listing",
isDisabled: !isOwner,
children: () => (
<CancelTab
contract={contract}
listingId={renderData.id}
listingType={renderData.type}
/>
),
},
];
return t;
}, [contract, isOwner, renderData, tokenId, data?.asset]);
if (!renderData) {
return null;
}
return (
<Drawer
allowPinchZoom
preserveScrollBarGap
size="xl"
onClose={onClose}
isOpen={isOpen}
>
<Flex py={6} px={2} flexDir="column" gap={6}>
<Flex gap={6}>
<NFTMediaWithEmptyState
metadata={renderData.asset}
requireInteraction
width="150px"
height="150px"
/>
<Flex flexDir="column" gap={2} w="70%">
<Heading size="title.lg">{renderData.asset.name}</Heading>
<Text size="label.md" noOfLines={6}>
{renderData.asset.description}
</Text>
</Flex>
</Flex>
<Tabs isLazy lazyBehavior="keepMounted" colorScheme="gray">
<TabList
px={0}
borderBottomColor="borderColor"
borderBottomWidth="1px"
overflowX={{ base: "auto", md: "inherit" }}
>
{tabs.map((tab) => (
<Tab key={tab.title} gap={2} isDisabled={tab.isDisabled}>
{tab.title}
</Tab>
))}
</TabList>
<TabPanels px={0} py={2}>
{tabs.map((tab) => {
return (
<TabPanel key={tab.title} px={0}>
{tab.children()}
</TabPanel>
);
})}
</TabPanels>
</Tabs>
</Flex>
</Drawer>
);
};
@@ -1,42 +1,13 @@
import {
SimpleGrid,
Skeleton,
Stack,
Stat,
StatLabel,
StatNumber,
} from "@chakra-ui/react";
import { Skeleton, Stack, Stat, StatLabel, StatNumber } from "@chakra-ui/react";
import {
useDirectListingsCount,
useEnglishAuctionsCount,
useListingsCount,
} from "@thirdweb-dev/react";
import type { Marketplace, MarketplaceV3 } from "@thirdweb-dev/sdk";
import type { MarketplaceV3 } from "@thirdweb-dev/sdk";
import { BigNumber } from "ethers";
import { useMemo } from "react";
import { Card } from "tw-components";
interface ListingStatsProps {
contract?: Marketplace;
}
export const ListingStats: React.FC<ListingStatsProps> = ({ contract }) => {
const listingsQuery = useListingsCount(contract);
return (
<SimpleGrid columns={3} spacing={{ base: 3, md: 6 }}>
<Card as={Stat}>
<StatLabel mb={{ base: 1, md: 0 }}>Total Listings</StatLabel>
<Skeleton isLoaded={!contract || listingsQuery.isSuccess}>
<StatNumber>
{BigNumber.from(listingsQuery?.data || 0).toString()}
</StatNumber>
</Skeleton>
</Card>
</SimpleGrid>
);
};
interface ListingStatsV3Props {
contract?: MarketplaceV3;
features: string[];
@@ -1,297 +0,0 @@
import {
ButtonGroup,
Center,
Flex,
Icon,
IconButton,
Select,
Skeleton,
Spinner,
Table,
TableContainer,
Tbody,
Td,
Th,
Thead,
Tr,
usePrevious,
} from "@chakra-ui/react";
import {
useActiveListings,
useListings,
useListingsCount,
} from "@thirdweb-dev/react";
import type {
AuctionListing,
DirectListing,
Marketplace,
} from "@thirdweb-dev/sdk";
import { MediaCell } from "components/contract-pages/table/table-columns/cells/media-cell";
import { BigNumber } from "ethers";
import { useEffect, useMemo, useState } from "react";
import {
MdFirstPage,
MdLastPage,
MdNavigateBefore,
MdNavigateNext,
} from "react-icons/md";
import { type Cell, type Column, usePagination, useTable } from "react-table";
import { Button, Text } from "tw-components";
import { AddressCopyButton } from "tw-components/AddressCopyButton";
import { ListingDrawer } from "./listing-drawer";
type ListingMetadata = AuctionListing | DirectListing;
const tableColumns: Column<ListingMetadata>[] = [
{
Header: "Listing Id",
accessor: (row) => row.id.toString(),
},
{
Header: "Media",
accessor: (row) => row.asset,
// biome-ignore lint/suspicious/noExplicitAny: FIXME
Cell: (cell: any) => <MediaCell cell={cell} />,
},
{
Header: "Name",
accessor: (row) => row.asset?.name,
},
{
Header: "Seller",
accessor: (row) => row.sellerAddress,
Cell: ({ cell }: { cell: Cell<ListingMetadata, string> }) => (
<AddressCopyButton variant="outline" address={cell.value} />
),
},
{
Header: "Price",
accessor: (row) => row.buyoutCurrencyValuePerToken,
// biome-ignore lint/suspicious/noExplicitAny: FIXME
Cell: ({ cell }: { cell: Cell<ListingMetadata, any> }) => {
return (
<Text size="label.md" whiteSpace="nowrap">
{cell.value.displayValue} {cell.value.symbol}
</Text>
);
},
},
{
Header: "Type",
// 0 = Direct, 1 = Auction
accessor: (row) => (row.type === 0 ? "Direct Listing" : "Auction"),
},
];
interface ListingsTableProps {
contract: Marketplace;
}
const DEFAULT_QUERY_STATE = { count: 50, start: 0 };
export const ListingsTable: React.FC<ListingsTableProps> = ({ contract }) => {
const [queryParams, setQueryParams] = useState(DEFAULT_QUERY_STATE);
const getAllQueryResult = useListings(contract, queryParams);
const getActiveQueryResult = useActiveListings(contract, queryParams);
const totalCountQuery = useListingsCount(contract);
const [listingsToShow, setListingsToShow_] = useState<"all" | "active">(
"all",
);
const setListingsToShow = (value: "all" | "active") => {
setQueryParams(DEFAULT_QUERY_STATE);
setListingsToShow_(value);
};
const prevData = usePrevious(
listingsToShow === "all"
? getAllQueryResult?.data
: getActiveQueryResult?.data,
);
const renderData = useMemo(() => {
if (listingsToShow === "all") {
return getAllQueryResult?.data || prevData;
}
return getActiveQueryResult?.data || prevData;
}, [getAllQueryResult, getActiveQueryResult, listingsToShow, prevData]);
const {
getTableProps,
getTableBodyProps,
headerGroups,
prepareRow,
page,
canPreviousPage,
canNextPage,
pageCount,
gotoPage,
nextPage,
previousPage,
setPageSize,
state: { pageIndex, pageSize },
} = useTable(
{
columns: tableColumns,
data: renderData || [],
initialState: {
pageSize: queryParams.count,
pageIndex: 0,
},
manualPagination: true,
pageCount: Math.max(
Math.ceil(
BigNumber.from(totalCountQuery.data || 0).toNumber() /
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,
);
// 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({ start: pageIndex * pageSize, count: pageSize });
}, [pageIndex, pageSize]);
const [tokenRow, setTokenRow] = useState<ListingMetadata | null>(null);
return (
<Flex gap={4} direction="column">
<ButtonGroup size="sm" variant="outline" isAttached>
<Button
onClick={() => setListingsToShow("all")}
variant={listingsToShow === "all" ? "solid" : "outline"}
>
All
</Button>
<Button
onClick={() => setListingsToShow("active")}
variant={listingsToShow === "active" ? "solid" : "outline"}
>
Active
</Button>
</ButtonGroup>
<TableContainer>
{((listingsToShow === "all" && getAllQueryResult.isFetching) ||
(listingsToShow === "active" && getActiveQueryResult.isFetching)) && (
<Spinner
color="primary"
size="xs"
position="absolute"
top={2}
right={4}
/>
)}
<ListingDrawer
contract={contract}
data={tokenRow}
isOpen={!!tokenRow}
onClose={() => setTokenRow(null)}
/>
<Table {...getTableProps()}>
<Thead>
{headerGroups.map((headerGroup, headerGroupIndex) => (
// biome-ignore lint/suspicious/noArrayIndexKey: FIXME
<Tr {...headerGroup.getHeaderGroupProps()} key={headerGroupIndex}>
{headerGroup.headers.map((column, columnIndex) => (
<Th
{...column.getHeaderProps()}
border="none"
// biome-ignore lint/suspicious/noArrayIndexKey: FIXME
key={columnIndex}
>
<Text as="label" size="label.sm" color="faded">
{column.render("Header")}
</Text>
</Th>
))}
<Th border="none" />
</Tr>
))}
</Thead>
<Tbody {...getTableBodyProps()}>
{page.map((row, rowIndex) => {
prepareRow(row);
return (
// biome-ignore lint/suspicious/noArrayIndexKey: FIXME
<Tr {...row.getRowProps()} key={rowIndex}>
{row.cells.map((cell, cellIndex) => {
return (
<Td
{...cell.getCellProps()}
borderColor="borderColor"
// biome-ignore lint/suspicious/noArrayIndexKey: FIXME
key={cellIndex}
>
{cell.render("Cell")}
</Td>
);
})}
</Tr>
);
})}
</Tbody>
</Table>
</TableContainer>
<Center w="100%">
<Flex gap={2} direction="row" align="center">
<IconButton
isDisabled={!canPreviousPage || totalCountQuery.isLoading}
aria-label="first page"
icon={<Icon as={MdFirstPage} />}
onClick={() => gotoPage(0)}
/>
<IconButton
isDisabled={!canPreviousPage || totalCountQuery.isLoading}
aria-label="previous page"
icon={<Icon as={MdNavigateBefore} />}
onClick={() => previousPage()}
/>
<Text whiteSpace="nowrap">
Page <strong>{pageIndex + 1}</strong> of{" "}
<Skeleton
as="span"
display="inline"
isLoaded={totalCountQuery.isSuccess}
>
<strong>{pageCount}</strong>
</Skeleton>
</Text>
<IconButton
isDisabled={!canNextPage || totalCountQuery.isLoading}
aria-label="next page"
icon={<Icon as={MdNavigateNext} />}
onClick={() => nextPage()}
/>
<IconButton
isDisabled={!canNextPage || totalCountQuery.isLoading}
aria-label="last page"
icon={<Icon as={MdLastPage} />}
onClick={() => gotoPage(pageCount - 1)}
/>
<Select
onChange={(e) => {
setPageSize(Number.parseInt(e.target.value as string, 10));
}}
value={pageSize}
isDisabled={totalCountQuery.isLoading}
>
<option value="25">25</option>
<option value="50">50</option>
<option value="100">100</option>
<option value="250">250</option>
<option value="500">500</option>
</Select>
</Flex>
</Center>
</Flex>
);
};
@@ -1,42 +0,0 @@
import { Flex } from "@chakra-ui/react";
import { useContract } from "@thirdweb-dev/react";
import { Heading } from "tw-components";
import { CreateListingButton } from "../shared-components/list-button";
import { ListingStats } from "./components/listing-stats";
import { ListingsTable } from "./components/table";
interface ListingsPageProps {
contractAddress?: string;
}
export const ContractListingsPage: React.FC<ListingsPageProps> = ({
contractAddress,
}) => {
const contractQuery = useContract(contractAddress, "marketplace");
if (contractQuery.isLoading) {
// TODO build a skeleton for this
return <div>Loading...</div>;
}
if (!contractQuery?.contract) {
return null;
}
return (
<Flex direction="column" gap={6}>
<Flex direction="row" justify="space-between" align="center">
<Heading size="title.sm">Contract Listings</Heading>
<Flex gap={4}>
<CreateListingButton
contractAddress={contractQuery.contract.getAddress()}
chainId={contractQuery.contract.chainId}
createText="Create Listing"
/>
</Flex>
</Flex>
<ListingStats contract={contractQuery.contract} />
<ListingsTable contract={contractQuery.contract} />
</Flex>
);
};