Browse Source

Make nft widget collapsible (#4140)

* make nft widget collapsible

* add chevron and pill

* introduce StatusContainer

* introduce NftWidgetContent, improve widget performance

* nft history adjustments

* show not collapsed if it's clicked from nft tile

* fix circular dependency

* fixes after merge

* cr fixes
Bartosz Dryl 1 year ago
parent
commit
98d92c4986

+ 1 - 0
packages/atlas/src/components/_nft/NftTile/NftTile.tsx

@@ -76,6 +76,7 @@ export const NftTile: FC<NftTileProps> = ({
       <VideoThumbnail
         type="video"
         videoHref={thumbnail?.videoHref}
+        linkState={{ shouldCollapse: false }}
         onMouseEnter={() => setHovered(true)}
         onMouseLeave={() => setHovered(false)}
         loading={loading}

+ 5 - 46
packages/atlas/src/components/_nft/NftWidget/NftHistory.styles.ts

@@ -1,19 +1,15 @@
 import styled from '@emotion/styled'
 
-import { Button } from '@/components/_buttons/Button'
 import { cVar, sizes } from '@/styles'
 
 import { SizeProps, sizeObj } from './NftWidget.styles'
 
-type OpenProps = { 'data-open': boolean }
-
-export const NftHistoryHeader = styled.div<SizeProps & OpenProps>`
+export const NftHistoryHeader = styled.div<SizeProps>`
   display: grid;
   grid-template-columns: 1fr auto;
   align-items: center;
   padding: ${sizes(6)};
   user-select: none;
-  cursor: pointer;
 
   &[data-size=${sizeObj.small}] {
     padding: ${sizes(4)};
@@ -24,58 +20,21 @@ export const NftHistoryHeader = styled.div<SizeProps & OpenProps>`
   }
 `
 
-export const StyledChevronButton = styled(Button)<OpenProps>`
-  transform: rotate(0);
-  transform-origin: center;
-  transition: ${cVar('animationTransitionFast')};
-
-  &[data-open='true'] {
-    transform: rotate(180deg);
-  }
-`
-
-type FadingBlockProps = { width: number; 'data-bottom'?: boolean } & SizeProps
-export const FadingBlock = styled.div<FadingBlockProps>`
-  height: ${sizes(6)};
-  background: linear-gradient(0deg, rgb(11 12 15 / 0) 0%, ${cVar('colorCoreNeutral900')} 100%);
-  position: absolute;
-  width: ${({ width }) => width}px;
-  z-index: 1;
-
-  &[data-size=${sizeObj.small}] {
-    height: ${sizes(4)};
-  }
-
-  &[data-bottom] {
-    transform: rotate(180deg);
-    bottom: 0;
-  }
-`
-
-type HistoryPanelProps = SizeProps & OpenProps
+type HistoryPanelProps = SizeProps
 export const HistoryPanel = styled.div<HistoryPanelProps>`
   background-color: ${cVar('colorBackgroundMuted')};
   position: relative;
   display: grid;
   gap: ${sizes(6)};
-  max-height: 280px;
-  padding: ${sizes(6)};
+  padding: ${sizes(2)} ${sizes(6)} ${sizes(6)} ${sizes(6)};
   overflow: hidden auto;
-  transition: transform ${cVar('animationTransitionFast')};
-  will-change: transform;
+  max-height: 376px;
 
   &[data-size=${sizeObj.small}] {
+    max-height: 400px;
     gap: ${sizes(4)};
     padding: ${sizes(4)};
   }
-
-  transform: translateY(-100%);
-  z-index: -1;
-
-  &[data-open='true'] {
-    transform: translateY(0);
-    z-index: unset;
-  }
 `
 
 export const HistoryItemContainer = styled.div<SizeProps>`

+ 9 - 20
packages/atlas/src/components/_nft/NftWidget/NftHistory.tsx

@@ -3,54 +3,43 @@ import { FC } from 'react'
 import { useNavigate } from 'react-router'
 
 import { BasicMembershipFieldsFragment } from '@/api/queries/__generated__/fragments.generated'
-import { SvgActionChevronB } from '@/assets/icons'
 import { Avatar } from '@/components/Avatar'
 import { JoyTokenIcon } from '@/components/JoyTokenIcon'
 import { NumberFormat } from '@/components/NumberFormat'
 import { Text } from '@/components/Text'
 import { absoluteRoutes } from '@/config/routes'
-import { useToggle } from '@/hooks/useToggle'
 import { getMemberAvatar } from '@/providers/assets/assets.helpers'
 import { useTokenPrice } from '@/providers/joystream/joystream.hooks'
 import { formatDateTime } from '@/utils/time'
 
 import {
   CopyContainer,
-  FadingBlock,
   HistoryItemContainer,
   HistoryPanel,
   HistoryPanelContainer,
   JoyPlusIcon,
   NftHistoryHeader,
-  StyledChevronButton,
   TextContainer,
   ValueContainer,
 } from './NftHistory.styles'
 import { OwnerHandle, Size } from './NftWidget.styles'
 
 type NftHistoryProps = { size: Size; width: number; historyItems: NftHistoryEntry[] }
-export const NftHistory: FC<NftHistoryProps> = ({ size, width, historyItems }) => {
-  const [isOpen, toggleIsOpen] = useToggle()
-
+export const NftHistory: FC<NftHistoryProps> = ({ size, historyItems }) => {
   return (
     <>
-      <NftHistoryHeader data-open={isOpen} data-size={size} onClick={toggleIsOpen}>
+      <NftHistoryHeader data-size={size}>
         <Text as="h3" variant={size === 'small' ? 'h300' : 'h400'}>
           History
         </Text>
-        <StyledChevronButton data-open={isOpen} variant="tertiary" icon={<SvgActionChevronB />} />
       </NftHistoryHeader>
-      {isOpen && (
-        <HistoryPanelContainer>
-          <FadingBlock data-size={size} width={width} />
-          <HistoryPanel data-size={size} data-open={isOpen}>
-            {historyItems.map((props, index) => (
-              <HistoryItem key={index} {...props} size={size} />
-            ))}
-          </HistoryPanel>
-          <FadingBlock data-size={size} width={width} data-bottom />
-        </HistoryPanelContainer>
-      )}
+      <HistoryPanelContainer>
+        <HistoryPanel data-size={size}>
+          {historyItems.map((props, index) => (
+            <HistoryItem key={index} {...props} size={size} />
+          ))}
+        </HistoryPanel>
+      </HistoryPanelContainer>
     </>
   )
 }

+ 46 - 48
packages/atlas/src/components/_nft/NftWidget/NftWidget.styles.ts

@@ -1,6 +1,8 @@
+import isPropValid from '@emotion/is-prop-valid'
 import styled from '@emotion/styled'
 import { Link } from 'react-router-dom'
 
+import { SvgActionChevronT } from '@/assets/icons'
 import { Avatar } from '@/components/Avatar'
 import { Text } from '@/components/Text'
 import { cVar, sizes } from '@/styles'
@@ -13,13 +15,22 @@ export const Container = styled.div`
   background-color: ${cVar('colorBackgroundMuted')};
   min-width: 0;
 `
+export const CollapsibleWrapper = styled.div<{ collapsed: boolean }>`
+  display: grid;
+  grid-template-rows: ${({ collapsed }) => (collapsed ? '0fr' : '1fr')};
+  transition: grid-template-rows ${cVar('animationTransitionMedium')};
+`
+
+export const CollapsibleElement = styled.div`
+  overflow: hidden;
+`
 
 export const Content = styled.div<SizeProps>`
   display: grid;
   gap: ${sizes(6)};
   grid-template-columns: 1fr 1fr;
   justify-content: space-between;
-  box-shadow: ${cVar('effectDividersTop')}, ${cVar('effectDividersBottom')};
+  box-shadow: ${cVar('effectDividersBottom')};
   padding: ${sizes(6)};
 
   &[data-size=${sizeObj.small}] {
@@ -31,10 +42,11 @@ export const Content = styled.div<SizeProps>`
 
 export const NftOwnerContainer = styled.div<SizeProps>`
   display: grid;
+  box-shadow: ${cVar('effectDividersBottom')};
   gap: ${sizes(1)} ${sizes(6)};
   grid-template:
-    'avatar owner-label' auto
-    'avatar owner' auto / auto 1fr;
+    'avatar owner-label collapsible-button' auto
+    'avatar owner collapsible-button' auto / min-content auto auto;
   align-items: center;
   padding: ${sizes(6)};
 
@@ -44,62 +56,48 @@ export const NftOwnerContainer = styled.div<SizeProps>`
   }
 `
 
-export const OwnerAvatar = styled(Avatar)`
-  grid-area: avatar;
+export const StatusContainer = styled.div`
+  background-color: ${cVar('colorBackground')};
+  padding: ${sizes(1)} ${sizes(6)};
+  display: flex;
+  gap: ${sizes(2)};
+  align-items: center;
 `
 
-export const OwnerLabel = styled(Text)`
-  grid-area: owner-label;
+export const StatusMark = styled.div`
+  width: 11px;
+  height: 11px;
+  background-color: ${cVar('colorTextSuccess')};
+  border-radius: 12px;
+  border: 1px solid ${cVar('colorBackgroundElevatedAlpha')};
 `
 
-export const OwnerHandle = styled(Link)`
-  grid-area: owner;
-  justify-content: start;
-  text-decoration: none;
+export const StyledSvgActionChevronT = styled(SvgActionChevronT, {
+  shouldForwardProp: isPropValid,
+})<{ isCollapsed: boolean }>`
+  transform: rotate(${({ isCollapsed }) => (isCollapsed ? '-180deg' : '0deg')});
+  transition: transform ${cVar('animationTransitionMedium')};
 `
 
-export const ButtonGrid = styled.div<SizeProps & { 'data-two-columns'?: boolean }>`
-  display: grid;
-  gap: ${sizes(4)};
-
-  &[data-size=${sizeObj.small}] {
-    gap: ${sizes(2)};
-  }
+export const OwnerAvatar = styled(Avatar)`
+  grid-area: avatar;
+`
 
-  &[data-two-columns='true'] {
-    grid-template-columns: 1fr 1fr;
-  }
+export const OwnerLabel = styled(Text)`
+  grid-area: owner-label;
 `
 
-export const TopBidderTokenContainer = styled.div<SizeProps>`
+export const CollapsibleButtonWrapper = styled.div`
+  grid-area: collapsible-button;
+  justify-self: end;
   display: flex;
+  gap: ${sizes(3)};
   align-items: center;
-  position: relative;
-  left: -4px;
-  z-index: 10;
-
-  &::before {
-    display: inline-block;
-    position: absolute;
-    content: '';
-    width: 28px;
-    height: 28px;
-    background: ${cVar('colorBackgroundMuted')};
-    border-radius: 100%;
-    left: -2px;
-    top: -2px;
-  }
-
-  &[data-size=${sizeObj.small}] {
-    &::before {
-      width: 21px;
-      height: 21px;
-      left: -2.5px;
-      top: 1.5px;
-    }
-  }
 `
 
-export const TopBidderContainer = styled.div`
-  display: flex;
+export const OwnerHandle = styled(Link)`
+  grid-area: owner;
+  justify-content: start;
+  justify-self: start;
+  text-decoration: none;
 `

+ 70 - 540
packages/atlas/src/components/_nft/NftWidget/NftWidget.tsx

@@ -1,29 +1,19 @@
 import BN from 'bn.js'
-import { differenceInSeconds } from 'date-fns'
-import { FC, memo } from 'react'
+import { FC, useEffect, useState } from 'react'
+import { useLocation } from 'react-router'
 import useResizeObserver from 'use-resize-observer'
 
-import { BasicBidFieldsFragment, FullBidFieldsFragment } from '@/api/queries/__generated__/fragments.generated'
-import { SvgAlertsInformative24 } from '@/assets/icons'
-import { Avatar } from '@/components/Avatar'
-import { Banner } from '@/components/Banner'
-import { JoyTokenIcon } from '@/components/JoyTokenIcon'
-import { GridItem } from '@/components/LayoutGrid'
-import { NumberFormat } from '@/components/NumberFormat'
+import { FullBidFieldsFragment } from '@/api/queries/__generated__/fragments.generated'
 import { Text } from '@/components/Text'
 import { Button } from '@/components/_buttons/Button'
 import { absoluteRoutes } from '@/config/routes'
-import { useDeepMemo } from '@/hooks/useDeepMemo'
-import { useMsTimestamp } from '@/hooks/useMsTimestamp'
-import { EnglishTimerState } from '@/hooks/useNftState'
 import { NftSaleType } from '@/joystream-lib/types'
-import { useTokenPrice } from '@/providers/joystream/joystream.hooks'
-import { formatDateTime, formatDurationShort, formatTime } from '@/utils/time'
 
 import { NftHistory, NftHistoryEntry } from './NftHistory'
-import { NftInfoItem, NftTimerItem } from './NftInfoItem'
 import {
-  ButtonGrid,
+  CollapsibleButtonWrapper,
+  CollapsibleElement,
+  CollapsibleWrapper,
   Container,
   Content,
   NftOwnerContainer,
@@ -31,67 +21,34 @@ import {
   OwnerHandle,
   OwnerLabel,
   Size,
-  TopBidderContainer,
-  TopBidderTokenContainer,
+  StatusContainer,
+  StatusMark,
+  StyledSvgActionChevronT,
 } from './NftWidget.styles'
-
-export type Auction = {
-  status: 'auction'
-  type: 'open' | 'english'
-  startingPrice: BN
-  buyNowPrice: BN | undefined
-  topBid: BasicBidFieldsFragment | undefined
-  topBidAmount: BN | undefined
-  topBidderHandle: string | undefined
-  topBidderAvatarUris: string[] | null | undefined
-  isUserTopBidder: boolean | undefined
-  userBidAmount: BN | undefined
-  userBidUnlockDate: Date | undefined
-  canWithdrawBid: boolean | undefined
-  canChangeBid: boolean | undefined
-  hasTimersLoaded: boolean | undefined
-  englishTimerState: EnglishTimerState | undefined
-  auctionPlannedEndDate: Date | undefined
-  startsAtDate: Date | undefined
-  plannedEndAtBlock: number | null | undefined
-  startsAtBlock: number | null | undefined
-  auctionBeginsInDays: number
-  auctionBeginsInSeconds: number
-  isUserWhitelisted: boolean | undefined
-}
+import { NftWidgetStatus } from './NftWidget.types'
+import { NftWidgetContent } from './NftWidgetContent'
 
 export type NftWidgetProps = {
   ownerHandle: string | null | undefined
   ownerAvatarUrls: string[] | null | undefined
   creatorId?: string
-  isOwner: boolean | undefined
-  needsSettling: boolean | undefined
-  bidFromPreviousAuction: FullBidFieldsFragment | undefined
   saleType: NftSaleType | null
-  nftStatus?:
-    | {
-        status: 'idle'
-        lastSalePrice: BN | undefined
-        lastSaleDate: Date | undefined
-      }
-    | {
-        status: 'buy-now'
-        buyNowPrice: BN
-      }
-    | Auction
-    | undefined
+  isOwnedByChannel?: boolean
+  nftStatus: NftWidgetStatus | undefined
   nftHistory: NftHistoryEntry[]
+  isOwner: boolean | undefined
+  needsSettling: boolean | undefined
+  onNftPutOnSale?: () => void
+  onNftAcceptBid?: () => void
   onNftPurchase?: () => void
+  onWithdrawBid?: (bid?: BN, createdAt?: Date) => void
+  bidFromPreviousAuction: FullBidFieldsFragment | undefined
   onNftSettlement?: () => void
   onNftBuyNow?: () => void
-  onNftPutOnSale?: () => void
-  onNftAcceptBid?: () => void
   onNftCancelSale?: () => void
   onNftChangePrice?: () => void
-  onWithdrawBid?: (bid?: BN, createdAt?: Date) => void
   userBidCreatedAt?: Date
   userBidAmount?: BN
-  isOwnedByChannel?: boolean
 }
 
 const SMALL_VARIANT_MAXIMUM_SIZE = 416
@@ -99,11 +56,12 @@ const SMALL_VARIANT_MAXIMUM_SIZE = 416
 export const NftWidget: FC<NftWidgetProps> = ({
   ownerHandle,
   creatorId,
-  isOwner,
   nftStatus,
   nftHistory,
-  needsSettling,
+  isOwnedByChannel,
   ownerAvatarUrls,
+  isOwner,
+  needsSettling,
   onNftPutOnSale,
   onNftAcceptBid,
   onWithdrawBid,
@@ -115,487 +73,21 @@ export const NftWidget: FC<NftWidgetProps> = ({
   onNftBuyNow,
   userBidCreatedAt,
   userBidAmount,
-  isOwnedByChannel,
 }) => {
-  const timestamp = useMsTimestamp()
   const { ref, width = SMALL_VARIANT_MAXIMUM_SIZE + 1 } = useResizeObserver({
     box: 'border-box',
   })
 
-  const size: Size = width > SMALL_VARIANT_MAXIMUM_SIZE ? 'medium' : 'small'
-  const { convertHapiToUSD, isLoadingPrice } = useTokenPrice()
-
-  const content = useDeepMemo(() => {
-    if (!nftStatus) {
-      return
-    }
-    const contentTextVariant = size === 'small' ? 'h400' : 'h600'
-    const buttonSize = size === 'small' ? 'medium' : 'large'
-    const buttonColumnSpan = size === 'small' ? 1 : 2
-    const timerColumnSpan = size === 'small' ? 1 : 2
-
-    const BuyNow = memo(({ buyNowPrice }: { buyNowPrice?: BN }) => {
-      const buyNowPriceInUsd = buyNowPrice && convertHapiToUSD(buyNowPrice)
-      return buyNowPrice?.gtn(0) ? (
-        <NftInfoItem
-          size={size}
-          label="Buy now"
-          disableSecondary={buyNowPriceInUsd === null}
-          content={
-            <>
-              <JoyTokenIcon size={size === 'small' ? 16 : 24} variant="silver" />
-              <NumberFormat as="span" value={buyNowPrice} format="short" variant={contentTextVariant} />
-            </>
-          }
-          secondaryText={
-            buyNowPriceInUsd && <NumberFormat as="span" color="colorText" format="dollar" value={buyNowPriceInUsd} />
-          }
-        />
-      ) : null
-    })
-    BuyNow.displayName = 'BuyNow'
-    const InfoBanner = ({ title, description }: { title: string; description: string }) => (
-      <GridItem colSpan={buttonColumnSpan}>
-        <Banner icon={<SvgAlertsInformative24 />} {...{ title, description }} />
-      </GridItem>
-    )
+  const location = useLocation()
+  const [isCollapsed, setIsCollapsed] = useState(true)
 
-    const WithdrawBidFromPreviousAuction = ({ secondary }: { secondary?: boolean }) =>
-      bidFromPreviousAuction ? (
-        <>
-          <GridItem colSpan={buttonColumnSpan}>
-            <Button
-              variant={secondary ? 'secondary' : undefined}
-              fullWidth
-              size={buttonSize}
-              onClick={() =>
-                onWithdrawBid?.(new BN(bidFromPreviousAuction.amount), new Date(bidFromPreviousAuction.createdAt))
-              }
-            >
-              Withdraw last bid
-            </Button>
-            <Text as="p" margin={{ top: 2 }} variant="t100" color="colorText" align="center">
-              You bid{' '}
-              <NumberFormat
-                as="span"
-                value={new BN(bidFromPreviousAuction?.amount)}
-                format="short"
-                variant="t100"
-                color="colorText"
-                withToken
-              />{' '}
-              on {formatDateTime(new Date(bidFromPreviousAuction.createdAt))}
-            </Text>
-          </GridItem>
-        </>
-      ) : null
+  const shouldCollapse = location.state?.shouldCollapse === undefined ? true : location.state?.shouldCollapse
 
-    const BidPlacingInfoText = () => (
-      <Text as="p" variant="t100" color="colorText" align="center">
-        Placing a bid will withdraw your last bid
-      </Text>
-    )
+  useEffect(() => {
+    setIsCollapsed(shouldCollapse)
+  }, [shouldCollapse])
 
-    switch (nftStatus.status) {
-      case 'idle':
-        return (
-          <>
-            {nftStatus.lastSalePrice ? (
-              <NftInfoItem
-                size={size}
-                label="Last price"
-                content={
-                  <>
-                    <JoyTokenIcon size={size === 'small' ? 16 : 24} variant="silver" />
-                    <NumberFormat
-                      as="span"
-                      value={nftStatus.lastSalePrice}
-                      format="short"
-                      variant={contentTextVariant}
-                      color="colorText"
-                    />
-                  </>
-                }
-                secondaryText={nftStatus.lastSaleDate && formatDateTime(nftStatus.lastSaleDate)}
-              />
-            ) : (
-              <NftInfoItem
-                size={size}
-                label="status"
-                content={
-                  <Text as="span" variant={contentTextVariant} color="colorText">
-                    Not for sale
-                  </Text>
-                }
-              />
-            )}
-            {bidFromPreviousAuction && (
-              <>
-                <InfoBanner
-                  title="Withdraw your bid"
-                  description="You placed a bid in a previous auction that you can now withdraw to claim back your money."
-                />
-                <WithdrawBidFromPreviousAuction />
-              </>
-            )}
-            {isOwner && (
-              <GridItem colSpan={buttonColumnSpan}>
-                <Button fullWidth variant="secondary" size={buttonSize} onClick={onNftPutOnSale}>
-                  Start sale of this NFT
-                </Button>
-              </GridItem>
-            )}
-          </>
-        )
-      case 'buy-now':
-        return (
-          <>
-            <BuyNow buyNowPrice={nftStatus.buyNowPrice} />
-
-            <GridItem colSpan={buttonColumnSpan}>
-              <ButtonGrid data-size={size}>
-                {isOwner ? (
-                  <>
-                    <Button fullWidth variant="secondary" size={buttonSize} onClick={onNftChangePrice}>
-                      Change price
-                    </Button>
-                    <Button fullWidth variant="destructive" size={buttonSize} onClick={onNftCancelSale}>
-                      Remove from sale
-                    </Button>
-                  </>
-                ) : (
-                  <GridItem colSpan={buttonColumnSpan}>
-                    <Button fullWidth size={buttonSize} onClick={onNftPurchase}>
-                      Buy now
-                    </Button>
-                  </GridItem>
-                )}
-                {bidFromPreviousAuction && (
-                  <>
-                    <InfoBanner
-                      title="Withdraw your bid"
-                      description="You placed a bid in a previous auction that you can now withdraw to claim back your money."
-                    />
-                    <WithdrawBidFromPreviousAuction secondary />
-                  </>
-                )}
-              </ButtonGrid>
-            </GridItem>
-          </>
-        )
-      case 'auction': {
-        const getInfoBannerProps = () => {
-          const hasBids = !nftStatus.topBid?.isCanceled && nftStatus.topBidAmount?.gtn(0)
-          if (nftStatus.type === 'open' && bidFromPreviousAuction) {
-            return {
-              title: 'Withdraw your bid to participate',
-              description:
-                'You placed a bid in a previous auction that you can now withdraw to be able to participate in this auction.',
-            }
-          }
-
-          if (nftStatus.englishTimerState === 'expired' && isOwner && !hasBids) {
-            return {
-              title: 'Auction ended',
-              description: 'This auction has ended and no one placed a bid. You can now remove this NFT from sale.',
-            }
-          }
-          if (nftStatus.englishTimerState === 'expired' && !bidFromPreviousAuction && !hasBids && !isOwner) {
-            return {
-              title: 'Auction ended',
-              description:
-                "This auction has ended and no one placed a bid. We're waiting for the NFT owner to remove this NFT from sale.",
-            }
-          }
-          if (
-            nftStatus.englishTimerState === 'expired' &&
-            !bidFromPreviousAuction &&
-            hasBids &&
-            !isOwner &&
-            !nftStatus.isUserTopBidder
-          ) {
-            return {
-              title: 'Auction ended',
-              description:
-                'We are waiting for this auction to be settled by the auction winner or the current NFT owner.',
-            }
-          }
-          if (nftStatus.englishTimerState === 'expired' && bidFromPreviousAuction) {
-            return {
-              title: 'Withdraw your bid',
-              description: 'You placed a bid in a previous auction that you can now withdraw.',
-            }
-          }
-
-          if (nftStatus.englishTimerState === 'running' && bidFromPreviousAuction) {
-            return {
-              title: 'Withdraw your bid to participate',
-              description:
-                'You placed a bid in a previous auction that you can now withdraw to be able to participate in this auction.',
-            }
-          }
-
-          if (nftStatus.englishTimerState === 'upcoming' && bidFromPreviousAuction) {
-            return {
-              title: 'Withdraw your bid to participate',
-              description:
-                'You placed a bid in a previous auction that you can now withdraw to be able to participate in this upcoming auction.',
-            }
-          }
-
-          if (nftStatus.isUserWhitelisted === false) {
-            return {
-              title: "You're not on the whitelist",
-              description: `This sale is available only to members whitelisted by ${ownerHandle}.`,
-            }
-          }
-
-          return null
-        }
-        const infoBannerProps = getInfoBannerProps()
-
-        const infoTextNode = !!nftStatus.userBidAmount?.gtn(0) && nftStatus.userBidUnlockDate && (
-          <GridItem colSpan={buttonColumnSpan}>
-            {nftStatus.type === 'english' ? (
-              <BidPlacingInfoText />
-            ) : (
-              <Text as="p" variant="t100" color="colorText" align="center">
-                {nftStatus.canWithdrawBid ? `Your last bid: ` : `Your last bid (`}
-                <NumberFormat as="span" value={nftStatus.userBidAmount} format="short" withToken />
-                {nftStatus.canWithdrawBid
-                  ? ''
-                  : `) becomes withdrawable on ${formatDateTime(nftStatus.userBidUnlockDate)}`}
-              </Text>
-            )}
-          </GridItem>
-        )
-
-        const topBidAmountInUsd = nftStatus.topBidAmount && convertHapiToUSD(nftStatus.topBidAmount)
-        const startingPriceInUsd = convertHapiToUSD(nftStatus.startingPrice)
-
-        return (
-          <>
-            {nftStatus.topBidAmount?.gtn(0) && !nftStatus.topBid?.isCanceled ? (
-              <NftInfoItem
-                size={size}
-                label="Top bid"
-                content={
-                  <>
-                    <TopBidderContainer>
-                      <Avatar assetUrls={nftStatus.topBidderAvatarUris} size={24} />
-                      <TopBidderTokenContainer data-size={size}>
-                        <JoyTokenIcon size={size === 'small' ? 16 : 24} variant="silver" />
-                      </TopBidderTokenContainer>
-                    </TopBidderContainer>
-                    <NumberFormat
-                      as="span"
-                      format="short"
-                      value={nftStatus.topBidAmount}
-                      variant={contentTextVariant}
-                    />
-                  </>
-                }
-                secondaryText={
-                  !isLoadingPrice && nftStatus.topBidderHandle ? (
-                    <>
-                      {topBidAmountInUsd ? (
-                        <NumberFormat as="span" color="colorText" format="dollar" value={topBidAmountInUsd} />
-                      ) : null}{' '}
-                      from{' '}
-                      <OwnerHandle to={absoluteRoutes.viewer.member(nftStatus.topBidderHandle)}>
-                        <Text as="span" variant="t100">
-                          {nftStatus.isUserTopBidder ? 'you' : nftStatus.topBidderHandle}
-                        </Text>
-                      </OwnerHandle>
-                    </>
-                  ) : null
-                }
-              />
-            ) : (
-              <NftInfoItem
-                size={size}
-                label="Starting Price"
-                content={
-                  <>
-                    <JoyTokenIcon size={size === 'small' ? 16 : 24} variant="silver" />
-                    <NumberFormat
-                      as="span"
-                      format="short"
-                      value={nftStatus.startingPrice}
-                      variant={contentTextVariant}
-                    />
-                  </>
-                }
-                disableSecondary={startingPriceInUsd === null}
-                secondaryText={
-                  startingPriceInUsd && (
-                    <NumberFormat as="span" color="colorText" format="dollar" value={startingPriceInUsd ?? 0} />
-                  )
-                }
-              />
-            )}
-            <BuyNow buyNowPrice={nftStatus.buyNowPrice} />
-
-            {nftStatus.englishTimerState === 'expired' && (
-              <GridItem colSpan={timerColumnSpan}>
-                <NftInfoItem
-                  size={size}
-                  label="Auction ended on"
-                  loading={!nftStatus.auctionPlannedEndDate}
-                  content={
-                    nftStatus.auctionPlannedEndDate && (
-                      <Text as="span" variant={contentTextVariant} color="colorText">
-                        {formatDateTime(nftStatus.auctionPlannedEndDate)}
-                      </Text>
-                    )
-                  }
-                />
-              </GridItem>
-            )}
-            {nftStatus.englishTimerState === 'running' && nftStatus?.auctionPlannedEndDate && (
-              <GridItem colSpan={timerColumnSpan}>
-                <NftTimerItem size={size} time={nftStatus.auctionPlannedEndDate} />
-              </GridItem>
-            )}
-            {nftStatus.startsAtBlock && nftStatus.auctionBeginsInSeconds >= 0 && (
-              <GridItem colSpan={timerColumnSpan}>
-                <NftInfoItem
-                  size={size}
-                  label="Auction begins on"
-                  loading={!nftStatus.startsAtDate}
-                  content={
-                    nftStatus.startsAtDate && (
-                      <Text as="span" variant={contentTextVariant} color="colorText">
-                        {nftStatus.auctionBeginsInDays > 1 && formatDateTime(nftStatus.startsAtDate)}
-                        {nftStatus.auctionBeginsInDays === 1 && `Tomorrow at ${formatTime(nftStatus.startsAtDate)}`}
-                        {nftStatus.auctionBeginsInDays < 1 &&
-                          formatDurationShort(differenceInSeconds(nftStatus.startsAtDate, timestamp))}
-                      </Text>
-                    )
-                  }
-                />
-              </GridItem>
-            )}
-
-            {nftStatus.hasTimersLoaded && infoBannerProps && <InfoBanner {...infoBannerProps} />}
-
-            {nftStatus.hasTimersLoaded && needsSettling && (nftStatus.isUserTopBidder || isOwner) && (
-              <GridItem colSpan={buttonColumnSpan}>
-                <Button fullWidth size={buttonSize} onClick={onNftSettlement}>
-                  Settle auction
-                </Button>
-              </GridItem>
-            )}
-
-            {nftStatus.hasTimersLoaded && bidFromPreviousAuction && <WithdrawBidFromPreviousAuction />}
-
-            {nftStatus.hasTimersLoaded &&
-              !needsSettling &&
-              !bidFromPreviousAuction &&
-              (isOwner
-                ? (nftStatus.type === 'open' ||
-                    // english auction with no bids
-                    !nftStatus.topBidAmount ||
-                    nftStatus.topBid?.isCanceled) && (
-                    <GridItem colSpan={buttonColumnSpan}>
-                      <ButtonGrid data-size={size}>
-                        {nftStatus.type === 'open' && nftStatus.topBid && !nftStatus.topBid?.isCanceled && (
-                          <Button fullWidth size={buttonSize} onClick={onNftAcceptBid}>
-                            Review and accept bid
-                          </Button>
-                        )}
-                        <Button
-                          fullWidth
-                          onClick={onNftCancelSale}
-                          variant={
-                            nftStatus.type === 'open' && !nftStatus.topBid?.isCanceled
-                              ? 'destructive-secondary'
-                              : 'destructive'
-                          }
-                          size={buttonSize}
-                        >
-                          Remove from sale
-                        </Button>
-                      </ButtonGrid>
-                    </GridItem>
-                  )
-                : nftStatus.englishTimerState === 'running' &&
-                  nftStatus.isUserWhitelisted !== false &&
-                  (nftStatus.buyNowPrice?.gtn(0) ? (
-                    <GridItem colSpan={buttonColumnSpan}>
-                      <ButtonGrid data-size={size} data-two-columns={size === 'medium'}>
-                        <Button fullWidth variant="secondary" size={buttonSize} onClick={onNftPurchase}>
-                          {nftStatus.canChangeBid ? 'Change bid' : 'Place bid'}
-                        </Button>
-                        <Button fullWidth size={buttonSize} onClick={onNftBuyNow}>
-                          Buy now
-                        </Button>
-                        {/* second row button */}
-                        {nftStatus.canWithdrawBid && (
-                          <GridItem colSpan={buttonColumnSpan}>
-                            <Button
-                              fullWidth
-                              size={buttonSize}
-                              variant="destructive-secondary"
-                              onClick={() => onWithdrawBid?.(userBidAmount, userBidCreatedAt)}
-                            >
-                              Withdraw bid
-                            </Button>
-                          </GridItem>
-                        )}
-
-                        {infoTextNode}
-                      </ButtonGrid>
-                    </GridItem>
-                  ) : (
-                    <GridItem colSpan={buttonColumnSpan}>
-                      <ButtonGrid data-size={size}>
-                        <GridItem colSpan={buttonColumnSpan}>
-                          <Button fullWidth size={buttonSize} onClick={onNftPurchase}>
-                            {nftStatus.canChangeBid ? 'Change bid' : 'Place bid'}
-                          </Button>
-                        </GridItem>
-                        {nftStatus.canWithdrawBid && (
-                          <GridItem colSpan={buttonColumnSpan}>
-                            <Button
-                              fullWidth
-                              size={buttonSize}
-                              variant="destructive-secondary"
-                              onClick={() => onWithdrawBid?.(userBidAmount, userBidCreatedAt)}
-                            >
-                              Withdraw bid
-                            </Button>
-                          </GridItem>
-                        )}
-                        {infoTextNode}
-                      </ButtonGrid>
-                    </GridItem>
-                  )))}
-          </>
-        )
-      }
-    }
-  }, [
-    nftStatus,
-    size,
-    convertHapiToUSD,
-    bidFromPreviousAuction,
-    onWithdrawBid,
-    isOwner,
-    onNftPutOnSale,
-    onNftChangePrice,
-    onNftCancelSale,
-    onNftPurchase,
-    isLoadingPrice,
-    timestamp,
-    needsSettling,
-    onNftSettlement,
-    onNftAcceptBid,
-    onNftBuyNow,
-    ownerHandle,
-    userBidAmount,
-    userBidCreatedAt,
-  ])
+  const size: Size = width > SMALL_VARIANT_MAXIMUM_SIZE ? 'medium' : 'small'
 
   if (!nftStatus) return null
 
@@ -619,10 +111,48 @@ export const NftWidget: FC<NftWidgetProps> = ({
             {ownerHandle}
           </Text>
         </OwnerHandle>
+        <CollapsibleButtonWrapper>
+          <Button
+            icon={<StyledSvgActionChevronT isCollapsed={isCollapsed} />}
+            variant="tertiary"
+            size="small"
+            onClick={() => setIsCollapsed((isCollapsed) => !isCollapsed)}
+          />
+        </CollapsibleButtonWrapper>
       </NftOwnerContainer>
-      <Content data-size={size}>{content}</Content>
-
-      <NftHistory size={size} width={width} historyItems={nftHistory} />
+      {nftStatus.status !== 'idle' && isCollapsed && (
+        <StatusContainer>
+          <StatusMark />
+          <Text variant="t100" as="p">
+            Purchasable
+          </Text>
+        </StatusContainer>
+      )}
+      <CollapsibleWrapper collapsed={isCollapsed}>
+        <CollapsibleElement>
+          <Content data-size={size}>
+            <NftWidgetContent
+              ownerHandle={ownerHandle}
+              size={size}
+              nftStatus={nftStatus}
+              isOwner={isOwner}
+              needsSettling={needsSettling}
+              onNftPutOnSale={onNftPutOnSale}
+              onNftAcceptBid={onNftAcceptBid}
+              onWithdrawBid={onWithdrawBid}
+              bidFromPreviousAuction={bidFromPreviousAuction}
+              onNftCancelSale={onNftCancelSale}
+              onNftChangePrice={onNftChangePrice}
+              onNftPurchase={onNftPurchase}
+              onNftSettlement={onNftSettlement}
+              onNftBuyNow={onNftBuyNow}
+              userBidCreatedAt={userBidCreatedAt}
+              userBidAmount={userBidAmount}
+            />
+          </Content>
+          <NftHistory size={size} width={width} historyItems={nftHistory} />
+        </CollapsibleElement>
+      </CollapsibleWrapper>
     </Container>
   )
 }

+ 41 - 0
packages/atlas/src/components/_nft/NftWidget/NftWidget.types.ts

@@ -0,0 +1,41 @@
+import BN from 'bn.js'
+
+import { BasicBidFieldsFragment } from '@/api/queries/__generated__/fragments.generated'
+import { EnglishTimerState } from '@/hooks/useNftState'
+
+export type Auction = {
+  status: 'auction'
+  type: 'open' | 'english'
+  startingPrice: BN
+  buyNowPrice: BN | undefined
+  topBid: BasicBidFieldsFragment | undefined
+  topBidAmount: BN | undefined
+  topBidderHandle: string | undefined
+  topBidderAvatarUris: string[] | null | undefined
+  isUserTopBidder: boolean | undefined
+  userBidAmount: BN | undefined
+  userBidUnlockDate: Date | undefined
+  canWithdrawBid: boolean | undefined
+  canChangeBid: boolean | undefined
+  hasTimersLoaded: boolean | undefined
+  englishTimerState: EnglishTimerState | undefined
+  auctionPlannedEndDate: Date | undefined
+  startsAtDate: Date | undefined
+  plannedEndAtBlock: number | null | undefined
+  startsAtBlock: number | null | undefined
+  auctionBeginsInDays: number
+  auctionBeginsInSeconds: number
+  isUserWhitelisted: boolean | undefined
+}
+
+export type NftWidgetStatus =
+  | {
+      status: 'idle'
+      lastSalePrice: BN | undefined
+      lastSaleDate: Date | undefined
+    }
+  | {
+      status: 'buy-now'
+      buyNowPrice: BN
+    }
+  | Auction

+ 54 - 0
packages/atlas/src/components/_nft/NftWidget/NftWidgetContent.styles.ts

@@ -0,0 +1,54 @@
+import styled from '@emotion/styled'
+
+import { cVar, sizes, zIndex } from '@/styles'
+
+export const sizeObj = { small: 'small', medium: 'medium' } as const
+export type Size = keyof typeof sizeObj
+
+export type SizeProps = { 'data-size': keyof typeof sizeObj }
+
+export const ButtonGrid = styled.div<SizeProps & { 'data-two-columns'?: boolean }>`
+  display: grid;
+  gap: ${sizes(4)};
+
+  &[data-size=${sizeObj.small}] {
+    gap: ${sizes(2)};
+  }
+
+  &[data-two-columns='true'] {
+    grid-template-columns: 1fr 1fr;
+  }
+`
+
+export const TopBidderContainer = styled.div`
+  display: flex;
+`
+
+export const TopBidderTokenContainer = styled.div<SizeProps>`
+  display: flex;
+  align-items: center;
+  position: relative;
+  left: -4px;
+  z-index: ${zIndex.overlay};
+
+  &::before {
+    display: inline-block;
+    position: absolute;
+    content: '';
+    width: 28px;
+    height: 28px;
+    background: ${cVar('colorBackgroundMuted')};
+    border-radius: 100%;
+    left: -2px;
+    top: -2px;
+  }
+
+  &[data-size=${sizeObj.small}] {
+    &::before {
+      width: 21px;
+      height: 21px;
+      left: -2.5px;
+      top: 1.5px;
+    }
+  }
+`

+ 554 - 0
packages/atlas/src/components/_nft/NftWidget/NftWidgetContent.tsx

@@ -0,0 +1,554 @@
+import BN from 'bn.js'
+import { differenceInSeconds } from 'date-fns'
+import { FC, memo } from 'react'
+
+import { FullBidFieldsFragment } from '@/api/queries/__generated__/fragments.generated'
+import { SvgAlertsInformative24 } from '@/assets/icons'
+import { Avatar } from '@/components/Avatar'
+import { Banner } from '@/components/Banner'
+import { JoyTokenIcon } from '@/components/JoyTokenIcon'
+import { GridItem } from '@/components/LayoutGrid'
+import { NumberFormat } from '@/components/NumberFormat'
+import { Text } from '@/components/Text'
+import { Button } from '@/components/_buttons/Button'
+import { absoluteRoutes } from '@/config/routes'
+import { useMsTimestamp } from '@/hooks/useMsTimestamp'
+import { useTokenPrice } from '@/providers/joystream/joystream.hooks'
+import { formatDateTime, formatDurationShort, formatTime } from '@/utils/time'
+
+import { NftInfoItem, NftTimerItem } from './NftInfoItem'
+import { OwnerHandle } from './NftWidget.styles'
+import { NftWidgetStatus } from './NftWidget.types'
+import { ButtonGrid, TopBidderContainer, TopBidderTokenContainer } from './NftWidgetContent.styles'
+
+type Size = 'small' | 'medium'
+
+type NftWidgetContentProps = {
+  nftStatus: NftWidgetStatus | undefined
+  ownerHandle: string | null | undefined
+  isOwner: boolean | undefined
+  needsSettling: boolean | undefined
+  onNftPutOnSale?: () => void
+  onNftAcceptBid?: () => void
+  onNftPurchase?: () => void
+  onWithdrawBid?: (bid?: BN, createdAt?: Date) => void
+  bidFromPreviousAuction: FullBidFieldsFragment | undefined
+  onNftSettlement?: () => void
+  onNftBuyNow?: () => void
+  onNftCancelSale?: () => void
+  onNftChangePrice?: () => void
+  userBidCreatedAt?: Date
+  userBidAmount?: BN
+  size: Size
+}
+
+export const NftWidgetContent: FC<NftWidgetContentProps> = memo(
+  ({
+    nftStatus,
+    isOwner,
+    ownerHandle,
+    needsSettling,
+    onNftPutOnSale,
+    onNftAcceptBid,
+    onNftPurchase,
+    onWithdrawBid,
+    bidFromPreviousAuction,
+    onNftSettlement,
+    onNftBuyNow,
+    onNftCancelSale,
+    onNftChangePrice,
+    userBidCreatedAt,
+    size,
+    userBidAmount,
+  }) => {
+    const { convertHapiToUSD, isLoadingPrice } = useTokenPrice()
+    const shouldIgnoreTimestamp = nftStatus?.status === 'auction' && !nftStatus.startsAtDate
+    const timestamp = useMsTimestamp({
+      shouldStop: shouldIgnoreTimestamp,
+    })
+    if (!nftStatus) {
+      return null
+    }
+
+    const contentTextVariant = size === 'small' ? 'h400' : 'h600'
+    const buttonSize = size === 'small' ? 'medium' : 'large'
+    const buttonColumnSpan = size === 'small' ? 1 : 2
+    const timerColumnSpan = size === 'small' ? 1 : 2
+
+    switch (nftStatus.status) {
+      case 'idle':
+        return (
+          <>
+            {nftStatus.lastSalePrice ? (
+              <NftInfoItem
+                size={size}
+                label="Last price"
+                content={
+                  <>
+                    <JoyTokenIcon size={size === 'small' ? 16 : 24} variant="silver" />
+                    <NumberFormat
+                      as="span"
+                      value={nftStatus.lastSalePrice}
+                      format="short"
+                      variant={contentTextVariant}
+                      color="colorText"
+                    />
+                  </>
+                }
+                secondaryText={nftStatus.lastSaleDate && formatDateTime(nftStatus.lastSaleDate)}
+              />
+            ) : (
+              <NftInfoItem
+                size={size}
+                label="status"
+                content={
+                  <Text as="span" variant={contentTextVariant} color="colorText">
+                    Not for sale
+                  </Text>
+                }
+              />
+            )}
+            {bidFromPreviousAuction && (
+              <>
+                <InfoBanner
+                  size={size}
+                  title="Withdraw your bid"
+                  description="You placed a bid in a previous auction that you can now withdraw to claim back your money."
+                />
+                <WithdrawBidFromPreviousAuction size={size} bidFromPreviousAuction={bidFromPreviousAuction} />
+              </>
+            )}
+            {isOwner && (
+              <GridItem colSpan={buttonColumnSpan}>
+                <Button fullWidth variant="secondary" size={buttonSize} onClick={onNftPutOnSale}>
+                  Start sale of this NFT
+                </Button>
+              </GridItem>
+            )}
+          </>
+        )
+      case 'buy-now':
+        return (
+          <>
+            <BuyNow buyNowPrice={nftStatus.buyNowPrice} size={size} />
+
+            <GridItem colSpan={buttonColumnSpan}>
+              <ButtonGrid data-size={size}>
+                {isOwner ? (
+                  <>
+                    <Button fullWidth variant="secondary" size={buttonSize} onClick={onNftChangePrice}>
+                      Change price
+                    </Button>
+                    <Button fullWidth variant="destructive" size={buttonSize} onClick={onNftCancelSale}>
+                      Remove from sale
+                    </Button>
+                  </>
+                ) : (
+                  <GridItem colSpan={buttonColumnSpan}>
+                    <Button fullWidth size={buttonSize} onClick={onNftPurchase}>
+                      Buy now
+                    </Button>
+                  </GridItem>
+                )}
+                {bidFromPreviousAuction && (
+                  <>
+                    <InfoBanner
+                      size={size}
+                      title="Withdraw your bid"
+                      description="You placed a bid in a previous auction that you can now withdraw to claim back your money."
+                    />
+                    <WithdrawBidFromPreviousAuction
+                      secondary
+                      size={size}
+                      bidFromPreviousAuction={bidFromPreviousAuction}
+                    />
+                  </>
+                )}
+              </ButtonGrid>
+            </GridItem>
+          </>
+        )
+      case 'auction': {
+        const getInfoBannerProps = () => {
+          const hasBids = !nftStatus.topBid?.isCanceled && nftStatus.topBidAmount?.gtn(0)
+          if (nftStatus.type === 'open' && bidFromPreviousAuction) {
+            return {
+              title: 'Withdraw your bid to participate',
+              description:
+                'You placed a bid in a previous auction that you can now withdraw to be able to participate in this auction.',
+            }
+          }
+
+          if (nftStatus.englishTimerState === 'expired' && isOwner && !hasBids) {
+            return {
+              title: 'Auction ended',
+              description: 'This auction has ended and no one placed a bid. You can now remove this NFT from sale.',
+            }
+          }
+          if (nftStatus.englishTimerState === 'expired' && !bidFromPreviousAuction && !hasBids && !isOwner) {
+            return {
+              title: 'Auction ended',
+              description:
+                "This auction has ended and no one placed a bid. We're waiting for the NFT owner to remove this NFT from sale.",
+            }
+          }
+          if (
+            nftStatus.englishTimerState === 'expired' &&
+            !bidFromPreviousAuction &&
+            hasBids &&
+            !isOwner &&
+            !nftStatus.isUserTopBidder
+          ) {
+            return {
+              title: 'Auction ended',
+              description:
+                'We are waiting for this auction to be settled by the auction winner or the current NFT owner.',
+            }
+          }
+          if (nftStatus.englishTimerState === 'expired' && bidFromPreviousAuction) {
+            return {
+              title: 'Withdraw your bid',
+              description: 'You placed a bid in a previous auction that you can now withdraw.',
+            }
+          }
+
+          if (nftStatus.englishTimerState === 'running' && bidFromPreviousAuction) {
+            return {
+              title: 'Withdraw your bid to participate',
+              description:
+                'You placed a bid in a previous auction that you can now withdraw to be able to participate in this auction.',
+            }
+          }
+
+          if (nftStatus.englishTimerState === 'upcoming' && bidFromPreviousAuction) {
+            return {
+              title: 'Withdraw your bid to participate',
+              description:
+                'You placed a bid in a previous auction that you can now withdraw to be able to participate in this upcoming auction.',
+            }
+          }
+
+          if (nftStatus.isUserWhitelisted === false) {
+            return {
+              title: "You're not on the whitelist",
+              description: `This sale is available only to members whitelisted by ${ownerHandle}.`,
+            }
+          }
+
+          return null
+        }
+        const infoBannerProps = getInfoBannerProps()
+
+        const infoTextNode = !!nftStatus.userBidAmount?.gtn(0) && nftStatus.userBidUnlockDate && (
+          <GridItem colSpan={buttonColumnSpan}>
+            {nftStatus.type === 'english' ? (
+              <BidPlacingInfoText />
+            ) : (
+              <Text as="p" variant="t100" color="colorText" align="center">
+                {nftStatus.canWithdrawBid ? `Your last bid: ` : `Your last bid (`}
+                <NumberFormat as="span" value={nftStatus.userBidAmount} format="short" withToken />
+                {nftStatus.canWithdrawBid
+                  ? ''
+                  : `) becomes withdrawable on ${formatDateTime(nftStatus.userBidUnlockDate)}`}
+              </Text>
+            )}
+          </GridItem>
+        )
+
+        const topBidAmountInUsd = nftStatus.topBidAmount && convertHapiToUSD(nftStatus.topBidAmount)
+        const startingPriceInUsd = convertHapiToUSD(nftStatus.startingPrice)
+
+        return (
+          <>
+            {nftStatus.topBidAmount?.gtn(0) && !nftStatus.topBid?.isCanceled ? (
+              <NftInfoItem
+                size={size}
+                label="Top bid"
+                content={
+                  <>
+                    <TopBidderContainer>
+                      <Avatar assetUrls={nftStatus.topBidderAvatarUris} size={24} />
+                      <TopBidderTokenContainer data-size={size}>
+                        <JoyTokenIcon size={size === 'small' ? 16 : 24} variant="silver" />
+                      </TopBidderTokenContainer>
+                    </TopBidderContainer>
+                    <NumberFormat
+                      as="span"
+                      format="short"
+                      value={nftStatus.topBidAmount}
+                      variant={contentTextVariant}
+                    />
+                  </>
+                }
+                secondaryText={
+                  !isLoadingPrice && nftStatus.topBidderHandle ? (
+                    <>
+                      {topBidAmountInUsd ? (
+                        <NumberFormat as="span" color="colorText" format="dollar" value={topBidAmountInUsd} />
+                      ) : null}{' '}
+                      from{' '}
+                      <OwnerHandle to={absoluteRoutes.viewer.member(nftStatus.topBidderHandle)}>
+                        <Text as="span" variant="t100">
+                          {nftStatus.isUserTopBidder ? 'you' : nftStatus.topBidderHandle}
+                        </Text>
+                      </OwnerHandle>
+                    </>
+                  ) : null
+                }
+              />
+            ) : (
+              <NftInfoItem
+                size={size}
+                label="Starting Price"
+                content={
+                  <>
+                    <JoyTokenIcon size={size === 'small' ? 16 : 24} variant="silver" />
+                    <NumberFormat
+                      as="span"
+                      format="short"
+                      value={nftStatus.startingPrice}
+                      variant={contentTextVariant}
+                    />
+                  </>
+                }
+                disableSecondary={startingPriceInUsd === null}
+                secondaryText={
+                  startingPriceInUsd && (
+                    <NumberFormat as="span" color="colorText" format="dollar" value={startingPriceInUsd ?? 0} />
+                  )
+                }
+              />
+            )}
+            <BuyNow buyNowPrice={nftStatus.buyNowPrice} size={size} />
+
+            {nftStatus.englishTimerState === 'expired' && (
+              <GridItem colSpan={timerColumnSpan}>
+                <NftInfoItem
+                  size={size}
+                  label="Auction ended on"
+                  loading={!nftStatus.auctionPlannedEndDate}
+                  content={
+                    nftStatus.auctionPlannedEndDate && (
+                      <Text as="span" variant={contentTextVariant} color="colorText">
+                        {formatDateTime(nftStatus.auctionPlannedEndDate)}
+                      </Text>
+                    )
+                  }
+                />
+              </GridItem>
+            )}
+            {nftStatus.englishTimerState === 'running' && nftStatus?.auctionPlannedEndDate && (
+              <GridItem colSpan={timerColumnSpan}>
+                <NftTimerItem size={size} time={nftStatus.auctionPlannedEndDate} />
+              </GridItem>
+            )}
+            {nftStatus.startsAtBlock && nftStatus.auctionBeginsInSeconds >= 0 && (
+              <GridItem colSpan={timerColumnSpan}>
+                <NftInfoItem
+                  size={size}
+                  label="Auction begins on"
+                  loading={!nftStatus.startsAtDate}
+                  content={
+                    nftStatus.startsAtDate && (
+                      <Text as="span" variant={contentTextVariant} color="colorText">
+                        {nftStatus.auctionBeginsInDays > 1 && formatDateTime(nftStatus.startsAtDate)}
+                        {nftStatus.auctionBeginsInDays === 1 && `Tomorrow at ${formatTime(nftStatus.startsAtDate)}`}
+                        {nftStatus.auctionBeginsInDays < 1 &&
+                          formatDurationShort(differenceInSeconds(nftStatus.startsAtDate, timestamp))}
+                      </Text>
+                    )
+                  }
+                />
+              </GridItem>
+            )}
+
+            {nftStatus.hasTimersLoaded && infoBannerProps && <InfoBanner size={size} {...infoBannerProps} />}
+
+            {nftStatus.hasTimersLoaded && needsSettling && (nftStatus.isUserTopBidder || isOwner) && (
+              <GridItem colSpan={buttonColumnSpan}>
+                <Button fullWidth size={buttonSize} onClick={onNftSettlement}>
+                  Settle auction
+                </Button>
+              </GridItem>
+            )}
+
+            {nftStatus.hasTimersLoaded && bidFromPreviousAuction && (
+              <WithdrawBidFromPreviousAuction size={size} bidFromPreviousAuction={bidFromPreviousAuction} />
+            )}
+
+            {nftStatus.hasTimersLoaded &&
+              !needsSettling &&
+              !bidFromPreviousAuction &&
+              (isOwner
+                ? (nftStatus.type === 'open' ||
+                    // english auction with no bids
+                    !nftStatus.topBidAmount ||
+                    nftStatus.topBid?.isCanceled) && (
+                    <GridItem colSpan={buttonColumnSpan}>
+                      <ButtonGrid data-size={size}>
+                        {nftStatus.type === 'open' && nftStatus.topBid && !nftStatus.topBid?.isCanceled && (
+                          <Button fullWidth size={buttonSize} onClick={onNftAcceptBid}>
+                            Review and accept bid
+                          </Button>
+                        )}
+                        <Button
+                          fullWidth
+                          onClick={onNftCancelSale}
+                          variant={
+                            nftStatus.type === 'open' && !nftStatus.topBid?.isCanceled
+                              ? 'destructive-secondary'
+                              : 'destructive'
+                          }
+                          size={buttonSize}
+                        >
+                          Remove from sale
+                        </Button>
+                      </ButtonGrid>
+                    </GridItem>
+                  )
+                : nftStatus.englishTimerState === 'running' &&
+                  nftStatus.isUserWhitelisted !== false &&
+                  (nftStatus.buyNowPrice?.gtn(0) ? (
+                    <GridItem colSpan={buttonColumnSpan}>
+                      <ButtonGrid data-size={size} data-two-columns={size === 'medium'}>
+                        <Button fullWidth variant="secondary" size={buttonSize} onClick={onNftPurchase}>
+                          {nftStatus.canChangeBid ? 'Change bid' : 'Place bid'}
+                        </Button>
+                        <Button fullWidth size={buttonSize} onClick={onNftBuyNow}>
+                          Buy now
+                        </Button>
+                        {/* second row button */}
+                        {nftStatus.canWithdrawBid && (
+                          <GridItem colSpan={buttonColumnSpan}>
+                            <Button
+                              fullWidth
+                              size={buttonSize}
+                              variant="destructive-secondary"
+                              onClick={() => onWithdrawBid?.(userBidAmount, userBidCreatedAt)}
+                            >
+                              Withdraw bid
+                            </Button>
+                          </GridItem>
+                        )}
+
+                        {infoTextNode}
+                      </ButtonGrid>
+                    </GridItem>
+                  ) : (
+                    <GridItem colSpan={buttonColumnSpan}>
+                      <ButtonGrid data-size={size}>
+                        <GridItem colSpan={buttonColumnSpan}>
+                          <Button fullWidth size={buttonSize} onClick={onNftPurchase}>
+                            {nftStatus.canChangeBid ? 'Change bid' : 'Place bid'}
+                          </Button>
+                        </GridItem>
+                        {nftStatus.canWithdrawBid && (
+                          <GridItem colSpan={buttonColumnSpan}>
+                            <Button
+                              fullWidth
+                              size={buttonSize}
+                              variant="destructive-secondary"
+                              onClick={() => onWithdrawBid?.(userBidAmount, userBidCreatedAt)}
+                            >
+                              Withdraw bid
+                            </Button>
+                          </GridItem>
+                        )}
+                        {infoTextNode}
+                      </ButtonGrid>
+                    </GridItem>
+                  )))}
+          </>
+        )
+      }
+    }
+  }
+)
+
+NftWidgetContent.displayName = 'NftWidgetContent'
+
+const BidPlacingInfoText = () => (
+  <Text as="p" variant="t100" color="colorText" align="center">
+    Placing a bid will withdraw your last bid
+  </Text>
+)
+
+export const BuyNow = memo(({ buyNowPrice, size }: { buyNowPrice?: BN; size: Size }) => {
+  const { convertHapiToUSD } = useTokenPrice()
+  const buyNowPriceInUsd = buyNowPrice && convertHapiToUSD(buyNowPrice)
+
+  const contentTextVariant = size === 'small' ? 'h400' : 'h600'
+  return buyNowPrice?.gtn(0) ? (
+    <NftInfoItem
+      size={size}
+      label="Buy now"
+      disableSecondary={buyNowPriceInUsd === null}
+      content={
+        <>
+          <JoyTokenIcon size={size === 'small' ? 16 : 24} variant="silver" />
+          <NumberFormat as="span" value={buyNowPrice} format="short" variant={contentTextVariant} />
+        </>
+      }
+      secondaryText={
+        buyNowPriceInUsd && <NumberFormat as="span" color="colorText" format="dollar" value={buyNowPriceInUsd} />
+      }
+    />
+  ) : null
+})
+BuyNow.displayName = 'BuyNow'
+
+const InfoBanner = ({ title, description, size }: { title: string; description: string; size: Size }) => {
+  const buttonColumnSpan = size === 'small' ? 1 : 2
+  return (
+    <GridItem colSpan={buttonColumnSpan}>
+      <Banner icon={<SvgAlertsInformative24 />} {...{ title, description }} />
+    </GridItem>
+  )
+}
+
+const WithdrawBidFromPreviousAuction = memo(
+  ({
+    secondary,
+    bidFromPreviousAuction,
+    size,
+    onWithdrawBid,
+  }: {
+    size: Size
+    secondary?: boolean
+    bidFromPreviousAuction: FullBidFieldsFragment | undefined
+    onWithdrawBid?: (bid?: BN, createdAt?: Date) => void
+  }) => {
+    const buttonColumnSpan = size === 'small' ? 1 : 2
+    const buttonSize = size === 'small' ? 'medium' : 'large'
+    return bidFromPreviousAuction ? (
+      <>
+        <GridItem colSpan={buttonColumnSpan}>
+          <Button
+            variant={secondary ? 'secondary' : undefined}
+            fullWidth
+            size={buttonSize}
+            onClick={() =>
+              onWithdrawBid?.(new BN(bidFromPreviousAuction.amount), new Date(bidFromPreviousAuction.createdAt))
+            }
+          >
+            Withdraw last bid
+          </Button>
+          <Text as="p" margin={{ top: 2 }} variant="t100" color="colorText" align="center">
+            You bid{' '}
+            <NumberFormat
+              as="span"
+              value={new BN(bidFromPreviousAuction?.amount)}
+              format="short"
+              variant="t100"
+              color="colorText"
+              withToken
+            />{' '}
+            on {formatDateTime(new Date(bidFromPreviousAuction.createdAt))}
+          </Text>
+        </GridItem>
+      </>
+    ) : null
+  }
+)
+
+WithdrawBidFromPreviousAuction.displayName = 'WithdrawBidFromPreviousAuction'