From 1af5a453f177a11392fbb1f8328128bfb2c20384 Mon Sep 17 00:00:00 2001 From: Yohan Tancrez Date: Tue, 2 Jul 2024 20:26:23 +0200 Subject: [PATCH 1/8] feat: linked token offers to api --- .../components/token-offers-table.tsx | 75 +++++++++++++++++++ .../[tokenId]/components/token-offers.tsx | 61 ++++++++++++--- .../[contractAddress]/[tokenId]/page.tsx | 7 +- .../[tokenId]/queries/getTokenData.ts | 37 +++++++++ packages/ui/src/button.tsx | 6 +- packages/ui/src/index.ts | 2 +- packages/ui/src/price-tag.tsx | 2 +- 7 files changed, 171 insertions(+), 19 deletions(-) create mode 100644 apps/arkmarket/src/app/token/[contractAddress]/[tokenId]/components/token-offers-table.tsx diff --git a/apps/arkmarket/src/app/token/[contractAddress]/[tokenId]/components/token-offers-table.tsx b/apps/arkmarket/src/app/token/[contractAddress]/[tokenId]/components/token-offers-table.tsx new file mode 100644 index 00000000..5e75b0e7 --- /dev/null +++ b/apps/arkmarket/src/app/token/[contractAddress]/[tokenId]/components/token-offers-table.tsx @@ -0,0 +1,75 @@ +import { useAccount } from "@starknet-react/core"; +import { validateAndParseAddress } from "starknet"; + +import { shortAddress } from "@ark-market/ui"; +import { Button } from "@ark-market/ui/button"; +import { PriceTag } from "@ark-market/ui/price-tag"; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@ark-market/ui/table"; + +import type { TokenOffer } from "../queries/getTokenData"; +import ConnectWalletModal from "~/components/connect-wallet-modal"; + +interface TokenOffersTableProps { + tokenOffers: TokenOffer[]; + owner: string; +} + +export default function TokenOffersTable({ + tokenOffers, + owner, +}: TokenOffersTableProps) { + const { isConnected, address } = useAccount(); + + const isOwner = + address !== undefined && + validateAndParseAddress(address) === validateAndParseAddress(owner); + + const showActionHeader = !isConnected || isOwner; + + return ( + + + Price + Floor difference + From + Expiration + {showActionHeader && Action} + + + {tokenOffers.map((offer) => { + return ( + + + + + {/* TODO @YohanTz: Check how this one looks */} + {offer.floor_difference ?? "_"} + + {offer.source ? shortAddress(offer.source) : "_"} + + {offer.expire_at} + {showActionHeader && ( + + {!isConnected ? ( + + + + ) : ( + + )} + + )} + + ); + })} + +
+ ); +} diff --git a/apps/arkmarket/src/app/token/[contractAddress]/[tokenId]/components/token-offers.tsx b/apps/arkmarket/src/app/token/[contractAddress]/[tokenId]/components/token-offers.tsx index 75f68be4..fe98fc97 100644 --- a/apps/arkmarket/src/app/token/[contractAddress]/[tokenId]/components/token-offers.tsx +++ b/apps/arkmarket/src/app/token/[contractAddress]/[tokenId]/components/token-offers.tsx @@ -1,7 +1,10 @@ "use client"; -import { useState } from "react"; +import { useMemo, useState } from "react"; +import { useAccount } from "@starknet-react/core"; +import { useInfiniteQuery } from "@tanstack/react-query"; import { ChevronDown, ChevronUp, Meh } from "lucide-react"; +import { validateAndParseAddress } from "starknet"; import type { PropsWithClassName } from "@ark-market/ui"; import { cn } from "@ark-market/ui"; @@ -12,8 +15,38 @@ import { CollapsibleTrigger, } from "@ark-market/ui/collapsible"; -export default function TokenOffers({ className }: PropsWithClassName) { +import { getTokenOffers } from "../queries/getTokenData"; +import TokenOffersTable from "./token-offers-table"; + +interface TokenOffersProps { + contractAddress: string; + tokenId: string; + owner: string; +} + +export default function TokenOffers({ + className, + contractAddress, + owner, + tokenId, +}: PropsWithClassName) { const [open, setOpen] = useState(true); + + const { data: infiniteData } = useInfiniteQuery({ + queryKey: ["tokenOffers", contractAddress, tokenId], + refetchInterval: 10_000, + // getNextPageParam: (lastPage) => lastPage.next_page, + getNextPageParam: () => null, + initialPageParam: undefined, + queryFn: ({ pageParam }) => + getTokenOffers({ contractAddress, tokenId, page: pageParam }), + }); + + const tokenOffers = useMemo( + () => infiniteData?.pages.flatMap((page) => page?.data ?? []) ?? [], + [infiniteData], + ); + return (

Offers

- {0} + {tokenOffers.length}
@@ -37,14 +70,20 @@ export default function TokenOffers({ className }: PropsWithClassName) { -
- -

- No offers yet! -
- Make the first offers! -

-
+ {tokenOffers.length > 0 ? ( +
+ +
+ ) : ( +
+ +

+ No offers yet! +
+ Make the first offers! +

+
+ )}
); diff --git a/apps/arkmarket/src/app/token/[contractAddress]/[tokenId]/page.tsx b/apps/arkmarket/src/app/token/[contractAddress]/[tokenId]/page.tsx index 68c2795c..772dfd41 100644 --- a/apps/arkmarket/src/app/token/[contractAddress]/[tokenId]/page.tsx +++ b/apps/arkmarket/src/app/token/[contractAddress]/[tokenId]/page.tsx @@ -69,7 +69,12 @@ export default async function TokenPage({ className="-mx-5 lg:mx-0" /> - + ; } + +export interface TokenOffer { + expire_at: number; + floor_difference: string | null; + hash: string; + offer_id: number; + price: string; + source: string | null; +} +interface TokenOffersApiResponse { + data: TokenOffer[]; +} +interface GetTokenOffersParams { + contractAddress: string; + page?: number; + tokenId: string; +} +export async function getTokenOffers({ + contractAddress, + page, + tokenId, +}: GetTokenOffersParams) { + const queryParams = [`items_per_page=${10}`]; + if (page !== undefined) { + queryParams.push(`page=${page}`); + } + + const url = `${env.NEXT_PUBLIC_MARKETPLACE_API_URL}/tokens/${contractAddress}/0x534e5f4d41494e/${tokenId}/offers?${queryParams.join("&")}`; + + const response = await fetch(url); + if (!response.ok) { + console.error(url, response.status); + return undefined; + } + + return (await response.json()) as Promise; +} diff --git a/packages/ui/src/button.tsx b/packages/ui/src/button.tsx index 595def91..6da651d7 100644 --- a/packages/ui/src/button.tsx +++ b/packages/ui/src/button.tsx @@ -6,7 +6,7 @@ import { cva } from "class-variance-authority"; import { cn } from "@ark-market/ui"; const buttonVariants = cva( - "inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50", + "inline-flex items-center justify-center gap-2.5 whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50", { variants: { variant: { @@ -33,10 +33,6 @@ const buttonVariants = cva( "icon-xl": "h-12 w-12", "icon-sm": "h-8 w-8 rounded-xs", }, - - // size: { - // sm: "", - // }, }, defaultVariants: { variant: "default", diff --git a/packages/ui/src/index.ts b/packages/ui/src/index.ts index aaec5bf8..5ae7c926 100644 --- a/packages/ui/src/index.ts +++ b/packages/ui/src/index.ts @@ -94,7 +94,7 @@ export function formatNumber(value: number | bigint) { } // TODO: use viem formatEther instead -export function formatUnits(value: bigint | number, decimals: number) { +export function formatUnits(value: bigint | number | string, decimals: number) { let display = value.toString(); const negative = display.startsWith("-"); diff --git a/packages/ui/src/price-tag.tsx b/packages/ui/src/price-tag.tsx index 1f0d79da..55f87800 100644 --- a/packages/ui/src/price-tag.tsx +++ b/packages/ui/src/price-tag.tsx @@ -3,7 +3,7 @@ import { cn, formatUnits } from "."; import EthereumLogo2 from "./icons/ethereum-logo-2"; interface PriceTagProps { - price: number | bigint; + price: number | bigint | string; } export function PriceTag({ className, From 40f7ee9be1254c3f984ef50b5c80e95582258fbc Mon Sep 17 00:00:00 2001 From: Yohan Tancrez Date: Wed, 3 Jul 2024 01:33:42 +0200 Subject: [PATCH 2/8] fix: token offers table --- .../components/token-offers-table.tsx | 45 ++++++++++++++----- .../[tokenId]/components/token-offers.tsx | 4 +- 2 files changed, 35 insertions(+), 14 deletions(-) diff --git a/apps/arkmarket/src/app/token/[contractAddress]/[tokenId]/components/token-offers-table.tsx b/apps/arkmarket/src/app/token/[contractAddress]/[tokenId]/components/token-offers-table.tsx index 5e75b0e7..45054ebc 100644 --- a/apps/arkmarket/src/app/token/[contractAddress]/[tokenId]/components/token-offers-table.tsx +++ b/apps/arkmarket/src/app/token/[contractAddress]/[tokenId]/components/token-offers-table.tsx @@ -1,7 +1,7 @@ import { useAccount } from "@starknet-react/core"; import { validateAndParseAddress } from "starknet"; -import { shortAddress } from "@ark-market/ui"; +import { cn, shortAddress } from "@ark-market/ui"; import { Button } from "@ark-market/ui/button"; import { PriceTag } from "@ark-market/ui/price-tag"; import { @@ -18,7 +18,7 @@ import ConnectWalletModal from "~/components/connect-wallet-modal"; interface TokenOffersTableProps { tokenOffers: TokenOffer[]; - owner: string; + owner: string | null; } export default function TokenOffersTable({ @@ -29,23 +29,46 @@ export default function TokenOffersTable({ const isOwner = address !== undefined && - validateAndParseAddress(address) === validateAndParseAddress(owner); + validateAndParseAddress(address) === validateAndParseAddress(owner ?? ""); const showActionHeader = !isConnected || isOwner; return ( - +
- Price - Floor difference - From - Expiration - {showActionHeader && Action} + + + Price + + + Floor difference + + From + + Expiration + + {showActionHeader && ( + + Action + + )} + - + {tokenOffers.map((offer) => { return ( - + diff --git a/apps/arkmarket/src/app/token/[contractAddress]/[tokenId]/components/token-offers.tsx b/apps/arkmarket/src/app/token/[contractAddress]/[tokenId]/components/token-offers.tsx index fe98fc97..ea56a240 100644 --- a/apps/arkmarket/src/app/token/[contractAddress]/[tokenId]/components/token-offers.tsx +++ b/apps/arkmarket/src/app/token/[contractAddress]/[tokenId]/components/token-offers.tsx @@ -1,10 +1,8 @@ "use client"; import { useMemo, useState } from "react"; -import { useAccount } from "@starknet-react/core"; import { useInfiniteQuery } from "@tanstack/react-query"; import { ChevronDown, ChevronUp, Meh } from "lucide-react"; -import { validateAndParseAddress } from "starknet"; import type { PropsWithClassName } from "@ark-market/ui"; import { cn } from "@ark-market/ui"; @@ -21,7 +19,7 @@ import TokenOffersTable from "./token-offers-table"; interface TokenOffersProps { contractAddress: string; tokenId: string; - owner: string; + owner: string | null; } export default function TokenOffers({ From dab8abf0f168e6f07d48f57e9c5d080107e5f4bc Mon Sep 17 00:00:00 2001 From: Yohan Tancrez Date: Wed, 3 Jul 2024 16:49:05 +0200 Subject: [PATCH 3/8] feat: cancel offer --- .../[token_id]/components/cancel-offer.tsx | 21 ++++--- .../[token_id]/components/token-offers.tsx | 6 +- .../components/token-offers-table-action.tsx | 56 +++++++++++++++++ .../components/token-offers-table.tsx | 60 +++++++------------ .../[tokenId]/components/token-offers.tsx | 7 ++- 5 files changed, 99 insertions(+), 51 deletions(-) create mode 100644 apps/arkmarket/src/app/token/[contractAddress]/[tokenId]/components/token-offers-table-action.tsx diff --git a/apps/arkmarket/src/app/assets/[contract_address]/[token_id]/components/cancel-offer.tsx b/apps/arkmarket/src/app/assets/[contract_address]/[token_id]/components/cancel-offer.tsx index 4e57dab2..92292f01 100644 --- a/apps/arkmarket/src/app/assets/[contract_address]/[token_id]/components/cancel-offer.tsx +++ b/apps/arkmarket/src/app/assets/[contract_address]/[token_id]/components/cancel-offer.tsx @@ -8,14 +8,17 @@ import { useAccount } from "@starknet-react/core"; import { Button } from "@ark-market/ui/button"; import { toast } from "@ark-market/ui/toast"; -import type { Offer, Token } from "~/types"; - interface CancelOfferProps { - token: Token; - offer: Offer; + tokenContractAddress: string; + tokenId: string; + offerOrderHash: string; } -const CancelOffer: React.FC = ({ token, offer }) => { +const CancelOffer = ({ + offerOrderHash, + tokenContractAddress, + tokenId, +}: CancelOfferProps) => { const { account } = useAccount(); const { cancel, status } = useCancel(); @@ -32,9 +35,9 @@ const CancelOffer: React.FC = ({ token, offer }) => { await cancel({ starknetAccount: account, - tokenAddress: token.contract_address, - tokenId: BigInt(token.token_id), - orderHash: BigInt(offer.order_hash), + tokenAddress: tokenContractAddress, + tokenId: BigInt(tokenId), + orderHash: BigInt(offerOrderHash), }); }; @@ -45,7 +48,7 @@ const CancelOffer: React.FC = ({ token, offer }) => { const isLoading = ["loading", "cancelling"].includes(status); return ( - + + ); + } + + if (isOwner) { + return ; + } + + if ( + validateAndParseAddress(address) === + validateAndParseAddress(offerSourceAddress ?? "") + ) + return ( + + ); + + return null; +} diff --git a/apps/arkmarket/src/app/token/[contractAddress]/[tokenId]/components/token-offers-table.tsx b/apps/arkmarket/src/app/token/[contractAddress]/[tokenId]/components/token-offers-table.tsx index 45054ebc..98ff1cc8 100644 --- a/apps/arkmarket/src/app/token/[contractAddress]/[tokenId]/components/token-offers-table.tsx +++ b/apps/arkmarket/src/app/token/[contractAddress]/[tokenId]/components/token-offers-table.tsx @@ -1,8 +1,4 @@ -import { useAccount } from "@starknet-react/core"; -import { validateAndParseAddress } from "starknet"; - -import { cn, shortAddress } from "@ark-market/ui"; -import { Button } from "@ark-market/ui/button"; +import { shortAddress } from "@ark-market/ui"; import { PriceTag } from "@ark-market/ui/price-tag"; import { Table, @@ -14,34 +10,25 @@ import { } from "@ark-market/ui/table"; import type { TokenOffer } from "../queries/getTokenData"; -import ConnectWalletModal from "~/components/connect-wallet-modal"; +import TokenOffersTableAction from "./token-offers-table-action"; interface TokenOffersTableProps { tokenOffers: TokenOffer[]; + tokenContractAdress: string; + tokenId: string; owner: string | null; } export default function TokenOffersTable({ tokenOffers, owner, + tokenContractAdress, + tokenId, }: TokenOffersTableProps) { - const { isConnected, address } = useAccount(); - - const isOwner = - address !== undefined && - validateAndParseAddress(address) === validateAndParseAddress(owner ?? ""); - - const showActionHeader = !isConnected || isOwner; - return (
- + Price @@ -52,11 +39,9 @@ export default function TokenOffersTable({ Expiration - {showActionHeader && ( - - Action - - )} + + Action + @@ -64,10 +49,7 @@ export default function TokenOffersTable({ return ( @@ -78,17 +60,15 @@ export default function TokenOffersTable({ {offer.source ? shortAddress(offer.source) : "_"} {offer.expire_at} - {showActionHeader && ( - - {!isConnected ? ( - - - - ) : ( - - )} - - )} + + + ); })} diff --git a/apps/arkmarket/src/app/token/[contractAddress]/[tokenId]/components/token-offers.tsx b/apps/arkmarket/src/app/token/[contractAddress]/[tokenId]/components/token-offers.tsx index ea56a240..02ff88b5 100644 --- a/apps/arkmarket/src/app/token/[contractAddress]/[tokenId]/components/token-offers.tsx +++ b/apps/arkmarket/src/app/token/[contractAddress]/[tokenId]/components/token-offers.tsx @@ -70,7 +70,12 @@ export default function TokenOffers({ {tokenOffers.length > 0 ? (
- +
) : (
From 75defc62ed498f4273b28f7933790e208fad7abf Mon Sep 17 00:00:00 2001 From: Yohan Tancrez Date: Wed, 3 Jul 2024 16:59:47 +0200 Subject: [PATCH 4/8] feat: added timestamp date --- .../[tokenId]/components/token-offers-table.tsx | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/apps/arkmarket/src/app/token/[contractAddress]/[tokenId]/components/token-offers-table.tsx b/apps/arkmarket/src/app/token/[contractAddress]/[tokenId]/components/token-offers-table.tsx index 98ff1cc8..8e78bbfb 100644 --- a/apps/arkmarket/src/app/token/[contractAddress]/[tokenId]/components/token-offers-table.tsx +++ b/apps/arkmarket/src/app/token/[contractAddress]/[tokenId]/components/token-offers-table.tsx @@ -1,4 +1,6 @@ -import { shortAddress } from "@ark-market/ui"; +import moment from "moment"; + +import { getRoundedRemainingTime, shortAddress } from "@ark-market/ui"; import { PriceTag } from "@ark-market/ui/price-tag"; import { Table, @@ -46,6 +48,7 @@ export default function TokenOffersTable({ {tokenOffers.map((offer) => { + console.log(offer.expire_at); return ( {offer.source ? shortAddress(offer.source) : "_"} - {offer.expire_at} + + In {getRoundedRemainingTime(offer.expire_at)} + Date: Wed, 3 Jul 2024 19:46:02 +0200 Subject: [PATCH 5/8] feat: accept offer --- .../[token_id]/components/accept-offer.tsx | 44 +++++++++++-------- .../[token_id]/components/token-offers.tsx | 10 +++-- .../components/token-offers-table-action.tsx | 30 ++++++++++--- .../components/token-offers-table.tsx | 11 +++-- .../[tokenId]/components/token-offers.tsx | 6 ++- .../[contractAddress]/[tokenId]/page.tsx | 1 + .../[tokenId]/queries/getTokenData.ts | 2 +- 7 files changed, 70 insertions(+), 34 deletions(-) diff --git a/apps/arkmarket/src/app/assets/[contract_address]/[token_id]/components/accept-offer.tsx b/apps/arkmarket/src/app/assets/[contract_address]/[token_id]/components/accept-offer.tsx index c233c80f..7fce5932 100644 --- a/apps/arkmarket/src/app/assets/[contract_address]/[token_id]/components/accept-offer.tsx +++ b/apps/arkmarket/src/app/assets/[contract_address]/[token_id]/components/accept-offer.tsx @@ -12,30 +12,38 @@ import { useAccount } from "@starknet-react/core"; import { areAddressesEqual } from "@ark-market/ui"; import { Button } from "@ark-market/ui/button"; -import type { Offer, Token, TokenMarketData } from "~/types"; import { env } from "~/env"; interface AcceptOfferProps { - token: Token; - tokenMarketData: TokenMarketData; - offer: Offer; + offerOrderHash: string; + offerAmount: string; + + tokenOwner: string; + tokenContractAddress: string; + tokenId: string; + tokenIsListed: boolean; + tokenListingOrderHash: string; } const AcceptOffer: React.FC = ({ - token, - tokenMarketData, - offer, + offerAmount, + offerOrderHash, + tokenIsListed, + tokenListingOrderHash, + tokenContractAddress, + tokenId, + tokenOwner, }) => { const { address, account } = useAccount(); const { fulfillOffer, status } = useFulfillOffer(); const { fulfill: fulfillAuction, status: statusAuction } = useFulfillAuction(); const type = useOrderType({ - orderHash: BigInt(tokenMarketData.order_hash), + orderHash: BigInt(tokenListingOrderHash), }); const isAuction = type === "AUCTION"; - const isOwner = areAddressesEqual(token.owner, address); - const isListed = tokenMarketData.is_listed; + const isOwner = areAddressesEqual(tokenOwner, address); + const isListed = tokenIsListed; if (!account || !isOwner) { return null; @@ -46,19 +54,19 @@ const AcceptOffer: React.FC = ({ await fulfillAuction({ starknetAccount: account, brokerId: env.NEXT_PUBLIC_BROKER_ID, - tokenAddress: token.contract_address, - tokenId: token.token_id, - orderHash: tokenMarketData.order_hash, - relatedOrderHash: offer.order_hash, - startAmount: offer.offer_amount, + tokenAddress: tokenContractAddress, + tokenId, + orderHash: tokenListingOrderHash, + relatedOrderHash: offerOrderHash, + startAmount: offerAmount, }); } else { await fulfillOffer({ starknetAccount: account, brokerId: env.NEXT_PUBLIC_BROKER_ID, - tokenAddress: token.contract_address, - tokenId: token.token_id, - orderHash: offer.order_hash, + tokenAddress: tokenContractAddress, + tokenId, + orderHash: offerOrderHash, }); } }; diff --git a/apps/arkmarket/src/app/assets/[contract_address]/[token_id]/components/token-offers.tsx b/apps/arkmarket/src/app/assets/[contract_address]/[token_id]/components/token-offers.tsx index 08bb21e0..9db85a48 100644 --- a/apps/arkmarket/src/app/assets/[contract_address]/[token_id]/components/token-offers.tsx +++ b/apps/arkmarket/src/app/assets/[contract_address]/[token_id]/components/token-offers.tsx @@ -106,9 +106,13 @@ const TokenOffers: React.FC = ({
{isOwner && tokenMarketData && ( )} {areAddressesEqual(offer.offer_maker, address) && ( diff --git a/apps/arkmarket/src/app/token/[contractAddress]/[tokenId]/components/token-offers-table-action.tsx b/apps/arkmarket/src/app/token/[contractAddress]/[tokenId]/components/token-offers-table-action.tsx index c9b4e437..87b536d0 100644 --- a/apps/arkmarket/src/app/token/[contractAddress]/[tokenId]/components/token-offers-table-action.tsx +++ b/apps/arkmarket/src/app/token/[contractAddress]/[tokenId]/components/token-offers-table-action.tsx @@ -8,25 +8,31 @@ import CancelOffer from "~/app/assets/[contract_address]/[token_id]/components/c import ConnectWalletModal from "~/components/connect-wallet-modal"; interface TokenOffersTableActionProps { - owner: string | null; + owner: string; offerSourceAddress: string | null; offerOrderHash: string; tokenId: string; tokenContractAddress: string; + offerAmount: string; + tokenIsListed: boolean; + tokenListingOrderHash: string | null; } export default function TokenOffersTableAction({ - owner, - offerSourceAddress, + offerAmount, offerOrderHash, - tokenId, + offerSourceAddress, + owner, tokenContractAddress, + tokenId, + tokenIsListed, + tokenListingOrderHash, }: TokenOffersTableActionProps) { const { address } = useAccount(); const isOwner = address !== undefined && - validateAndParseAddress(address) === validateAndParseAddress(owner ?? ""); + validateAndParseAddress(address) === validateAndParseAddress(owner); if (!address) { return ( @@ -36,8 +42,18 @@ export default function TokenOffersTableAction({ ); } - if (isOwner) { - return ; + if (isOwner && tokenListingOrderHash !== null) { + return ( + + ); } if ( diff --git a/apps/arkmarket/src/app/token/[contractAddress]/[tokenId]/components/token-offers-table.tsx b/apps/arkmarket/src/app/token/[contractAddress]/[tokenId]/components/token-offers-table.tsx index 8e78bbfb..3d59902b 100644 --- a/apps/arkmarket/src/app/token/[contractAddress]/[tokenId]/components/token-offers-table.tsx +++ b/apps/arkmarket/src/app/token/[contractAddress]/[tokenId]/components/token-offers-table.tsx @@ -1,5 +1,3 @@ -import moment from "moment"; - import { getRoundedRemainingTime, shortAddress } from "@ark-market/ui"; import { PriceTag } from "@ark-market/ui/price-tag"; import { @@ -12,13 +10,15 @@ import { } from "@ark-market/ui/table"; import type { TokenOffer } from "../queries/getTokenData"; +import type { TokenMarketData } from "~/types"; import TokenOffersTableAction from "./token-offers-table-action"; interface TokenOffersTableProps { tokenOffers: TokenOffer[]; tokenContractAdress: string; tokenId: string; - owner: string | null; + owner: string; + tokenMarketData: TokenMarketData | null; } export default function TokenOffersTable({ @@ -26,6 +26,7 @@ export default function TokenOffersTable({ owner, tokenContractAdress, tokenId, + tokenMarketData, }: TokenOffersTableProps) { return (
@@ -48,7 +49,6 @@ export default function TokenOffersTable({ {tokenOffers.map((offer) => { - console.log(offer.expire_at); return ( diff --git a/apps/arkmarket/src/app/token/[contractAddress]/[tokenId]/components/token-offers.tsx b/apps/arkmarket/src/app/token/[contractAddress]/[tokenId]/components/token-offers.tsx index 02ff88b5..aaef157c 100644 --- a/apps/arkmarket/src/app/token/[contractAddress]/[tokenId]/components/token-offers.tsx +++ b/apps/arkmarket/src/app/token/[contractAddress]/[tokenId]/components/token-offers.tsx @@ -13,13 +13,15 @@ import { CollapsibleTrigger, } from "@ark-market/ui/collapsible"; +import type { TokenMarketData } from "~/types"; import { getTokenOffers } from "../queries/getTokenData"; import TokenOffersTable from "./token-offers-table"; interface TokenOffersProps { contractAddress: string; tokenId: string; - owner: string | null; + owner: string; + tokenMarketData: TokenMarketData | null; } export default function TokenOffers({ @@ -27,6 +29,7 @@ export default function TokenOffers({ contractAddress, owner, tokenId, + tokenMarketData, }: PropsWithClassName) { const [open, setOpen] = useState(true); @@ -75,6 +78,7 @@ export default function TokenOffers({ owner={owner} tokenContractAdress={contractAddress} tokenId={tokenId} + tokenMarketData={tokenMarketData} /> ) : ( diff --git a/apps/arkmarket/src/app/token/[contractAddress]/[tokenId]/page.tsx b/apps/arkmarket/src/app/token/[contractAddress]/[tokenId]/page.tsx index 772dfd41..4cebcd37 100644 --- a/apps/arkmarket/src/app/token/[contractAddress]/[tokenId]/page.tsx +++ b/apps/arkmarket/src/app/token/[contractAddress]/[tokenId]/page.tsx @@ -73,6 +73,7 @@ export default async function TokenPage({ className="-mx-5 lg:mx-0" contractAddress={contractAddress} tokenId={tokenId} + tokenMarketData={tokenMarketData} owner={tokenInfosInitialData.data.owner} /> Date: Thu, 4 Jul 2024 12:12:36 +0200 Subject: [PATCH 6/8] fix: spacings --- .../[tokenId]/components/token-offers.tsx | 16 +++++++--------- .../[tokenId]/components/token-stats.tsx | 19 +++++++------------ 2 files changed, 14 insertions(+), 21 deletions(-) diff --git a/apps/arkmarket/src/app/token/[contractAddress]/[tokenId]/components/token-offers.tsx b/apps/arkmarket/src/app/token/[contractAddress]/[tokenId]/components/token-offers.tsx index aaef157c..8424c5e2 100644 --- a/apps/arkmarket/src/app/token/[contractAddress]/[tokenId]/components/token-offers.tsx +++ b/apps/arkmarket/src/app/token/[contractAddress]/[tokenId]/components/token-offers.tsx @@ -72,15 +72,13 @@ export default function TokenOffers({ {tokenOffers.length > 0 ? ( -
- -
+ ) : (
diff --git a/apps/arkmarket/src/app/token/[contractAddress]/[tokenId]/components/token-stats.tsx b/apps/arkmarket/src/app/token/[contractAddress]/[tokenId]/components/token-stats.tsx index e3ddbd48..2afbfd1b 100644 --- a/apps/arkmarket/src/app/token/[contractAddress]/[tokenId]/components/token-stats.tsx +++ b/apps/arkmarket/src/app/token/[contractAddress]/[tokenId]/components/token-stats.tsx @@ -3,7 +3,7 @@ import { useMemo } from "react"; import type { PropsWithClassName } from "@ark-market/ui"; -import { cn, ellipsableStyles, formatNumber } from "@ark-market/ui"; +import { cn, ellipsableStyles, formatUnits } from "@ark-market/ui"; import EthereumLogo2 from "@ark-market/ui/icons/ethereum-logo-2"; import { Separator } from "@ark-market/ui/separator"; @@ -19,9 +19,6 @@ export default function TokenStats({ tokenInfos, }: PropsWithClassName) { const shortenedAddress = useMemo(() => { - if (tokenInfos.owner === null) { - return undefined; - } return `${tokenInfos.owner.slice(0, 7)}...${tokenInfos.owner.slice(-4)}`; }, [tokenInfos.owner]); @@ -59,7 +56,7 @@ export default function TokenStats({

- {formatNumber(BigInt(tokenInfos.top_offer ?? "0"))} ETH + {formatUnits(BigInt(tokenInfos.top_offer ?? "0"), 18)} ETH

@@ -68,12 +65,10 @@ export default function TokenStats({

Owner

- {tokenInfos.owner !== null && ( - - )} +

- {shortenedAddress ?? "_"} + {shortenedAddress}

From 51e264a64fd9c4cb2c9e003b86f4a3689c4847d1 Mon Sep 17 00:00:00 2001 From: Yohan Tancrez Date: Thu, 4 Jul 2024 12:13:10 +0200 Subject: [PATCH 7/8] fix: lint --- .../[contractAddress]/[tokenId]/components/token-about.tsx | 3 --- 1 file changed, 3 deletions(-) diff --git a/apps/arkmarket/src/app/token/[contractAddress]/[tokenId]/components/token-about.tsx b/apps/arkmarket/src/app/token/[contractAddress]/[tokenId]/components/token-about.tsx index 224a9354..7166e293 100644 --- a/apps/arkmarket/src/app/token/[contractAddress]/[tokenId]/components/token-about.tsx +++ b/apps/arkmarket/src/app/token/[contractAddress]/[tokenId]/components/token-about.tsx @@ -35,9 +35,6 @@ export default function TokenAbout({ }, [contractAddress]); const ownerShortenedAddress = useMemo(() => { - if (tokenInfos.owner === null) { - return undefined; - } return `${tokenInfos.owner.slice(0, 4)}...${tokenInfos.owner.slice(-4)}`; }, [tokenInfos.owner]); From 150f7d24934e88896c98217c801ba094a3503ac0 Mon Sep 17 00:00:00 2001 From: Paul Launay Date: Fri, 5 Jul 2024 10:02:16 +0200 Subject: [PATCH 8/8] feat(listing): user can create a listing (#42) --- .../components/collection-items-data.tsx | 17 +- .../components/collection.tsx | 1 - .../token-actions-accept-best-offer.tsx | 2 +- .../components/token-actions-buttons.tsx | 2 +- .../token-actions-create-listing.tsx | 261 ++++++++++-------- .../components/token-actions-empty.tsx | 2 +- .../components/token-actions-make-offer.tsx | 17 +- .../components/tokens-actions-buy-now.tsx | 2 +- packages/ui/src/form.tsx | 4 +- packages/ui/src/input.tsx | 2 +- packages/ui/src/label.tsx | 2 +- tooling/github/setup/action.yml | 2 +- 12 files changed, 177 insertions(+), 137 deletions(-) diff --git a/apps/arkmarket/src/app/collection/[collectionAddress]/components/collection-items-data.tsx b/apps/arkmarket/src/app/collection/[collectionAddress]/components/collection-items-data.tsx index 5eeab75d..909c51e4 100644 --- a/apps/arkmarket/src/app/collection/[collectionAddress]/components/collection-items-data.tsx +++ b/apps/arkmarket/src/app/collection/[collectionAddress]/components/collection-items-data.tsx @@ -4,7 +4,10 @@ import { useMemo } from "react"; import { useInfiniteQuery } from "@tanstack/react-query"; import type { ViewType } from "../../../../components/view-type-toggle-group"; -import type { CollectionTokensApiResponse, CollectionToken } from "../queries/getCollectionData"; +import type { + CollectionToken, + CollectionTokensApiResponse, +} from "../queries/getCollectionData"; import type { CollectionSortBy, CollectionSortDirection, @@ -37,9 +40,15 @@ export default function CollectionItemsData({ isFetchingNextPage, isLoading, } = useInfiniteQuery({ - queryKey: ["collectionTokens", sortDirection, sortBy, collectionAddress] as const, + queryKey: [ + "collectionTokens", + sortDirection, + sortBy, + collectionAddress, + ] as const, refetchInterval: false, - getNextPageParam: (lastPage: CollectionTokensApiResponse) => lastPage.next_page, + getNextPageParam: (lastPage: CollectionTokensApiResponse) => + lastPage.next_page, initialPageParam: undefined as number | undefined, queryFn: ({ pageParam }) => getCollectionTokens({ @@ -82,4 +91,4 @@ export default function CollectionItemsData({ )} ); -} \ No newline at end of file +} diff --git a/apps/arkmarket/src/app/collection/[collectionAddress]/components/collection.tsx b/apps/arkmarket/src/app/collection/[collectionAddress]/components/collection.tsx index f676e33e..83f86e4c 100644 --- a/apps/arkmarket/src/app/collection/[collectionAddress]/components/collection.tsx +++ b/apps/arkmarket/src/app/collection/[collectionAddress]/components/collection.tsx @@ -4,7 +4,6 @@ import { useState } from "react"; import { useQuery } from "@tanstack/react-query"; import { useQueryState } from "nuqs"; - import type { ViewType } from "~/components/view-type-toggle-group"; import { getCollectionInfos } from "../queries/getCollectionData"; import { diff --git a/apps/arkmarket/src/app/token/[contractAddress]/[tokenId]/components/token-actions-accept-best-offer.tsx b/apps/arkmarket/src/app/token/[contractAddress]/[tokenId]/components/token-actions-accept-best-offer.tsx index e863924e..a96ae0a0 100644 --- a/apps/arkmarket/src/app/token/[contractAddress]/[tokenId]/components/token-actions-accept-best-offer.tsx +++ b/apps/arkmarket/src/app/token/[contractAddress]/[tokenId]/components/token-actions-accept-best-offer.tsx @@ -75,7 +75,7 @@ export default function TokenActionsAcceptBestOffer({ )} Accept offer - + {formatEther(BigInt(tokenMarketData.top_bid.amount))} ETH ); diff --git a/apps/arkmarket/src/app/token/[contractAddress]/[tokenId]/components/token-actions-buttons.tsx b/apps/arkmarket/src/app/token/[contractAddress]/[tokenId]/components/token-actions-buttons.tsx index dc244c37..687be2b4 100644 --- a/apps/arkmarket/src/app/token/[contractAddress]/[tokenId]/components/token-actions-buttons.tsx +++ b/apps/arkmarket/src/app/token/[contractAddress]/[tokenId]/components/token-actions-buttons.tsx @@ -59,8 +59,8 @@ export default function TokenActionsButtons({ ) : ( )} diff --git a/apps/arkmarket/src/app/token/[contractAddress]/[tokenId]/components/token-actions-create-listing.tsx b/apps/arkmarket/src/app/token/[contractAddress]/[tokenId]/components/token-actions-create-listing.tsx index 2f5f0748..37f9a8ba 100644 --- a/apps/arkmarket/src/app/token/[contractAddress]/[tokenId]/components/token-actions-create-listing.tsx +++ b/apps/arkmarket/src/app/token/[contractAddress]/[tokenId]/components/token-actions-create-listing.tsx @@ -1,8 +1,9 @@ "use client"; -import { useState } from "react"; +import { useEffect, useState } from "react"; import { useCreateAuction, useCreateListing } from "@ark-project/react"; import { zodResolver } from "@hookform/resolvers/zod"; +import { ReloadIcon } from "@radix-ui/react-icons"; import { useAccount } from "@starknet-react/core"; import { List } from "lucide-react"; import moment from "moment"; @@ -25,8 +26,7 @@ import { FormLabel, FormMessage, } from "@ark-market/ui/form"; -import { Input } from "@ark-market/ui/input"; -import { RadioGroup, RadioGroupItem } from "@ark-market/ui/radio-group"; +import { NumericalInput } from "@ark-market/ui/numerical-input"; import { Select, SelectContent, @@ -34,51 +34,102 @@ import { SelectTrigger, SelectValue, } from "@ark-market/ui/select"; +import { toast } from "@ark-market/ui/toast"; -import type { Token, TokenMarketData } from "~/types"; -import TokenMedia from "~/app/assets/[contract_address]/[token_id]/components/token-media"; +import type { Collection, Token } from "~/types"; import { env } from "~/env"; +import formatAmount from "~/lib/formatAmount"; +import TokenActionsTokenOverview from "./token-actions-token-overview"; interface TokenActionsCreateListingProps { + collection: Collection; token: Token; - tokenMarketData?: TokenMarketData; } -const FIXED = "fixed"; -const AUCTION = "auction"; - -const formSchema = z.object({ - startAmount: z.string({ - invalid_type_error: "Please enter a valid amount", - }), - endAmount: z - .string({ - invalid_type_error: "Please enter a valid amount", - }) - .optional(), - duration: z.string(), - type: z.enum([FIXED, AUCTION]), -}); - export function TokenActionsCreateListing({ + collection, token, - // tokenMarketData, }: TokenActionsCreateListingProps) { const { account } = useAccount(); const [isOpen, setIsOpen] = useState(false); + const [isAuction, setIsAuction] = useState(false); const { createListing, status } = useCreateListing(); const { create: createAuction, status: auctionStatus } = useCreateAuction(); - const form = useForm>({ + const formSchema = z + .object({ + startAmount: z.string().refine( + (val) => { + const num = parseFloat(val); + return !isNaN(num) && num > 0; + }, + { + message: "Must be a valid amount", + }, + ), + endAmount: z + .string() + .refine( + (val) => { + const num = parseFloat(val); + + if (!isAuction) { + return true; + } + + return !isNaN(num); + }, + { + message: "Must be a valid amount", + }, + ) + .optional(), + duration: z.string(), + }) + .refine( + (data) => { + if (!isAuction) { + return true; + } + + if (data.endAmount !== undefined) { + const sa = parseFloat(data.startAmount); + const ea = parseFloat(data.endAmount); + return ea > sa; + } + return true; + }, + { + message: "Must be greater than start amount", + path: ["endAmount"], + }, + ); + + const form = useForm({ + mode: "all", resolver: zodResolver(formSchema), - mode: "onBlur", defaultValues: { - type: FIXED, - startAmount: "0.1", + startAmount: "", + endAmount: "", duration: "1", }, }); + useEffect(() => { + if (status === "error") { + setIsOpen(false); + toast.error("Your token listing failed."); + } else if (status === "success") { + setIsOpen(false); + toast.success("Your token is successfully listed."); + } + }, [status]); + + useEffect(() => { + setIsAuction(false); + form.reset(); + }, [form, isOpen]); + async function onSubmit(values: z.infer) { if (!account) { // TODO: Handle error with toast @@ -96,7 +147,7 @@ export function TokenActionsCreateListing({ }; try { - if (values.type === AUCTION) { + if (isAuction) { await createAuction({ starknetAccount: account, brokerId: env.NEXT_PUBLIC_BROKER_ID, @@ -116,28 +167,21 @@ export function TokenActionsCreateListing({ startAmount: processedValues.startAmount, }); } - - // queryClient.setQueryData( - // ["tokenMarketData", token.contract_address, token.token_id], - // { - // ...tokenMarketData, - // is_listed: true, - // type: values.type === AUCTION ? "AUCTION" , - // start_amount: processedValues.startAmount, - // end_amount: processedValues.endAmount, - // end_date: processedValues.endDate, - // }, - // ); } catch (error) { console.error("error: create listing failed", error); } } - const isAuction = form.getValues("type") === AUCTION; - // const duration = form.watch("duration"); - // const expiredAt = moment().add(duration, "hours").format("LLLL"); + const startAmount = form.watch("startAmount"); + const formattedStartAmount = formatAmount(startAmount); const isLoading = status === "loading" || auctionStatus === "loading"; + const isDisabled = + !form.formState.isValid || + form.formState.isSubmitting || + status === "loading" || + auctionStatus === "loading"; + return ( @@ -147,82 +191,69 @@ export function TokenActionsCreateListing({ - - {/* List for sale */} - -
+ +
List for sale
-
-
- -
-
-
Duo #{token.token_id}
-
Everai
-
-
-
- +
- ( - - Choose a type of sale - - - - - Fixed price - - The item is listed at the price you set. - - - - - - - - - - Sell to highest bidder - - - The item is listed for auction. - - - - - - - - - - - )} - /> + + Type of sale +
+ + +
+
+ ( Set starting price + - + - + {formattedStartAmount !== "-" && } )} /> @@ -234,7 +265,10 @@ export function TokenActionsCreateListing({ Set reserve price - + @@ -246,10 +280,7 @@ export function TokenActionsCreateListing({ name="duration" render={({ field }) => ( -
- Set expiration - {/*
Expires {expiredAt}
*/} -
+ Set expiration