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/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 && tokenListingOrderHash !== null) { + 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 new file mode 100644 index 00000000..3d59902b --- /dev/null +++ b/apps/arkmarket/src/app/token/[contractAddress]/[tokenId]/components/token-offers-table.tsx @@ -0,0 +1,86 @@ +import { getRoundedRemainingTime, shortAddress } from "@ark-market/ui"; +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 type { TokenMarketData } from "~/types"; +import TokenOffersTableAction from "./token-offers-table-action"; + +interface TokenOffersTableProps { + tokenOffers: TokenOffer[]; + tokenContractAdress: string; + tokenId: string; + owner: string; + tokenMarketData: TokenMarketData | null; +} + +export default function TokenOffersTable({ + tokenOffers, + owner, + tokenContractAdress, + tokenId, + tokenMarketData, +}: TokenOffersTableProps) { + return ( + + + + + Price + + + Floor difference + + From + + Expiration + + + Action + + + + + {tokenOffers.map((offer) => { + return ( + + + + + {/* TODO @YohanTz: Check how this one looks */} + {offer.floor_difference ?? "_"} + + {offer.source ? shortAddress(offer.source) : "_"} + + + In {getRoundedRemainingTime(offer.expire_at)} + + + + + + ); + })} + +
+ ); +} 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..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 @@ -1,6 +1,7 @@ "use client"; -import { useState } from "react"; +import { useMemo, useState } from "react"; +import { useInfiniteQuery } from "@tanstack/react-query"; import { ChevronDown, ChevronUp, Meh } from "lucide-react"; import type { PropsWithClassName } from "@ark-market/ui"; @@ -12,8 +13,41 @@ import { CollapsibleTrigger, } from "@ark-market/ui/collapsible"; -export default function TokenOffers({ className }: PropsWithClassName) { +import type { TokenMarketData } from "~/types"; +import { getTokenOffers } from "../queries/getTokenData"; +import TokenOffersTable from "./token-offers-table"; + +interface TokenOffersProps { + contractAddress: string; + tokenId: string; + owner: string; + tokenMarketData: TokenMarketData | null; +} + +export default function TokenOffers({ + className, + contractAddress, + owner, + tokenId, + tokenMarketData, +}: 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 +71,24 @@ 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]/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}

diff --git a/apps/arkmarket/src/app/token/[contractAddress]/[tokenId]/page.tsx b/apps/arkmarket/src/app/token/[contractAddress]/[tokenId]/page.tsx index 6f50a033..5f25c468 100644 --- a/apps/arkmarket/src/app/token/[contractAddress]/[tokenId]/page.tsx +++ b/apps/arkmarket/src/app/token/[contractAddress]/[tokenId]/page.tsx @@ -72,7 +72,13 @@ 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,