Commit 7229637c authored by Charles Bachmeier's avatar Charles Bachmeier Committed by GitHub

feat: [ListV2] Update price and duration dropdowns (#5925)

* add custom option and style market dropdown

* working dropdown for price

* hide dropdown on mobile

* update duration dropdown

* themed opacity hover

---------
Co-authored-by: default avatarCharles Bachmeier <charlie@genie.xyz>
parent f26b0953
import Column from 'components/Column'
import Row from 'components/Row'
import { DropDownOption } from 'nft/types'
import { Check } from 'react-feather'
import styled, { useTheme } from 'styled-components/macro'
import { ThemedText } from 'theme'
const DropdownWrapper = styled(Column)<{ $width: number }>`
gap: 4px;
background: ${({ theme }) => theme.backgroundSurface};
padding: 8px;
width: ${({ $width }) => $width}px;
border-radius: 12px;
box-shadow: ${({ theme }) => theme.deepShadow};
border: 1px solid ${({ theme }) => theme.backgroundOutline};
`
const DropdownRow = styled(Row)`
justify-content: space-between;
padding: 8px;
cursor: pointer;
border-radius: 12px;
&:hover {
background: ${({ theme }) => theme.backgroundInteractive};
}
`
interface DropdownArgs {
dropDownOptions: DropDownOption[]
width: number
}
export const Dropdown = ({ dropDownOptions, width }: DropdownArgs) => {
const theme = useTheme()
return (
<DropdownWrapper $width={width}>
{dropDownOptions.map((option) => (
<DropdownRow key={option.displayText} onClick={option.onClick}>
<ThemedText.BodyPrimary lineHeight="24px">{option.displayText}</ThemedText.BodyPrimary>
{option.isSelected && <Check height={20} width={20} color={theme.accentAction} />}
</DropdownRow>
))}
</DropdownWrapper>
)
}
...@@ -135,7 +135,7 @@ export const MarketplaceRow = ({ ...@@ -135,7 +135,7 @@ export const MarketplaceRow = ({
if (globalPriceMethod === SetPriceMethod.FLOOR_PRICE) { if (globalPriceMethod === SetPriceMethod.FLOOR_PRICE) {
setListPrice(asset?.floorPrice) setListPrice(asset?.floorPrice)
setGlobalPrice(asset.floorPrice) setGlobalPrice(asset.floorPrice)
} else if (globalPriceMethod === SetPriceMethod.PREV_LISTING) { } else if (globalPriceMethod === SetPriceMethod.LAST_PRICE) {
setListPrice(asset.lastPrice) setListPrice(asset.lastPrice)
setGlobalPrice(asset.lastPrice) setGlobalPrice(asset.lastPrice)
} else if (globalPriceMethod === SetPriceMethod.SAME_PRICE) } else if (globalPriceMethod === SetPriceMethod.SAME_PRICE)
......
import { Trans } from '@lingui/macro' import { Trans } from '@lingui/macro'
// eslint-disable-next-line no-restricted-imports // eslint-disable-next-line no-restricted-imports
import { t } from '@lingui/macro'
import Column from 'components/Column' import Column from 'components/Column'
import Row from 'components/Row' import Row from 'components/Row'
import { SortDropdown } from 'nft/components/common/SortDropdown' import { useOnClickOutside } from 'hooks/useOnClickOutside'
import { useSellAsset } from 'nft/hooks' import { useSellAsset } from 'nft/hooks'
import { DropDownOption, ListingMarket } from 'nft/types' import { DropDownOption, ListingMarket } from 'nft/types'
import { useMemo, useState } from 'react' import { useMemo, useReducer, useRef, useState } from 'react'
import { ChevronDown } from 'react-feather'
import styled, { css } from 'styled-components/macro' import styled, { css } from 'styled-components/macro'
import { BREAKPOINTS } from 'theme' import { BREAKPOINTS } from 'theme'
import { Dropdown } from './Dropdown'
import { NFTListRow } from './NFTListRow' import { NFTListRow } from './NFTListRow'
const TableHeader = styled.div` const TableHeader = styled.div`
...@@ -28,11 +29,7 @@ const TableHeader = styled.div` ...@@ -28,11 +29,7 @@ const TableHeader = styled.div`
` `
const NFTHeader = styled.div` const NFTHeader = styled.div`
flex: 2; flex: 1.5;
@media screen and (min-width: ${BREAKPOINTS.md}px) {
flex: 1.5;
}
` `
const PriceHeaders = styled(Row)` const PriceHeaders = styled(Row)`
...@@ -52,8 +49,52 @@ const PriceInfoHeader = styled.div` ...@@ -52,8 +49,52 @@ const PriceInfoHeader = styled.div`
} }
` `
const DropdownWrapper = styled.div` const DropdownAndHeaderWrapper = styled(Row)`
flex: 2; flex: 2;
gap: 4px;
`
const DropdownPromptContainer = styled(Column)`
position: relative;
@media screen and (max-width: ${BREAKPOINTS.sm}px) {
display: none;
}
`
const DropdownPrompt = styled(Row)`
gap: 4px;
background-color: ${({ theme }) => theme.backgroundInteractive};
cursor: pointer;
font-weight: 600;
font-size: 12px;
line-height: 16px;
border-radius: 4px;
padding: 2px 6px;
width: min-content;
white-space: nowrap;
color: ${({ theme }) => theme.textPrimary};
&:hover {
opacity: ${({ theme }) => theme.opacity.hover};
}
`
const DropdownChevron = styled(ChevronDown)<{ isOpen: boolean }>`
height: 16px;
width: 16px;
color: ${({ theme }) => theme.textSecondary};
transform: ${({ isOpen }) => isOpen && 'rotate(180deg)'};
transition: ${({
theme: {
transition: { duration, timing },
},
}) => `transform ${duration.fast} ${timing.ease}`};
`
const DropdownContainer = styled.div`
position: absolute;
top: 36px;
right: 0px;
` `
const FeeUserReceivesSharedStyles = css` const FeeUserReceivesSharedStyles = css`
...@@ -86,32 +127,74 @@ const RowDivider = styled.hr` ...@@ -86,32 +127,74 @@ const RowDivider = styled.hr`
export enum SetPriceMethod { export enum SetPriceMethod {
SAME_PRICE, SAME_PRICE,
FLOOR_PRICE, FLOOR_PRICE,
PREV_LISTING, LAST_PRICE,
CUSTOM,
} }
export const NFTListingsGrid = ({ selectedMarkets }: { selectedMarkets: ListingMarket[] }) => { export const NFTListingsGrid = ({ selectedMarkets }: { selectedMarkets: ListingMarket[] }) => {
const sellAssets = useSellAsset((state) => state.sellAssets) const sellAssets = useSellAsset((state) => state.sellAssets)
const [globalPriceMethod, setGlobalPriceMethod] = useState<SetPriceMethod>() const [globalPriceMethod, setGlobalPriceMethod] = useState(SetPriceMethod.CUSTOM)
const [globalPrice, setGlobalPrice] = useState<number>() const [globalPrice, setGlobalPrice] = useState<number>()
const [showDropdown, toggleShowDropdown] = useReducer((s) => !s, false)
const dropdownRef = useRef<HTMLDivElement>(null)
useOnClickOutside(dropdownRef, showDropdown ? toggleShowDropdown : undefined)
const priceDropdownOptions: DropDownOption[] = useMemo( const priceDropdownOptions: DropDownOption[] = useMemo(
() => [ () => [
{ {
displayText: 'Same price', displayText: 'Custom',
onClick: () => setGlobalPriceMethod(SetPriceMethod.SAME_PRICE), isSelected: globalPriceMethod === SetPriceMethod.CUSTOM,
onClick: () => {
setGlobalPriceMethod(SetPriceMethod.CUSTOM)
toggleShowDropdown()
},
}, },
{ {
displayText: 'Floor price', displayText: 'Floor price',
onClick: () => setGlobalPriceMethod(SetPriceMethod.FLOOR_PRICE), isSelected: globalPriceMethod === SetPriceMethod.FLOOR_PRICE,
onClick: () => {
setGlobalPriceMethod(SetPriceMethod.FLOOR_PRICE)
toggleShowDropdown()
},
}, },
{ {
displayText: 'Prev. listing', displayText: 'Last price',
onClick: () => setGlobalPriceMethod(SetPriceMethod.PREV_LISTING), isSelected: globalPriceMethod === SetPriceMethod.LAST_PRICE,
onClick: () => {
setGlobalPriceMethod(SetPriceMethod.LAST_PRICE)
toggleShowDropdown()
},
},
{
displayText: 'Same price',
isSelected: globalPriceMethod === SetPriceMethod.SAME_PRICE,
onClick: () => {
setGlobalPriceMethod(SetPriceMethod.SAME_PRICE)
toggleShowDropdown()
},
}, },
], ],
[] [globalPriceMethod]
) )
let prompt
switch (globalPriceMethod) {
case SetPriceMethod.CUSTOM:
prompt = <Trans>Custom</Trans>
break
case SetPriceMethod.FLOOR_PRICE:
prompt = <Trans>Floor price</Trans>
break
case SetPriceMethod.LAST_PRICE:
prompt = <Trans>Last Price</Trans>
break
case SetPriceMethod.SAME_PRICE:
prompt = <Trans>Same Price</Trans>
break
default:
break
}
return ( return (
<Column> <Column>
<TableHeader> <TableHeader>
...@@ -126,9 +209,19 @@ export const NFTListingsGrid = ({ selectedMarkets }: { selectedMarkets: ListingM ...@@ -126,9 +209,19 @@ export const NFTListingsGrid = ({ selectedMarkets }: { selectedMarkets: ListingM
<Trans>Last</Trans> <Trans>Last</Trans>
</PriceInfoHeader> </PriceInfoHeader>
<DropdownWrapper> <DropdownAndHeaderWrapper ref={dropdownRef}>
<SortDropdown dropDownOptions={priceDropdownOptions} mini miniPrompt={t`Set price by`} /> <Trans>Price</Trans>
</DropdownWrapper> <DropdownPromptContainer>
<DropdownPrompt onClick={toggleShowDropdown}>
{prompt} <DropdownChevron isOpen={showDropdown} />
</DropdownPrompt>
{showDropdown && (
<DropdownContainer>
<Dropdown dropDownOptions={priceDropdownOptions} width={200} />
</DropdownContainer>
)}
</DropdownPromptContainer>
</DropdownAndHeaderWrapper>
<FeeHeader> <FeeHeader>
<Trans>Fees</Trans> <Trans>Fees</Trans>
......
...@@ -79,10 +79,10 @@ const HeaderButtonWrap = styled(Row)` ...@@ -79,10 +79,10 @@ const HeaderButtonWrap = styled(Row)`
border-radius: 12px; border-radius: 12px;
width: 180px; width: 180px;
justify-content: space-between; justify-content: space-between;
background: ${({ theme }) => theme.backgroundModule}; background: ${({ theme }) => theme.backgroundInteractive};
cursor: pointer; cursor: pointer;
&:hover { &:hover {
background-color: ${({ theme }) => theme.backgroundInteractive}; opacity: ${({ theme }) => theme.opacity.hover};
} }
@media screen and (min-width: ${SMALL_MEDIA_BREAKPOINT}) { @media screen and (min-width: ${SMALL_MEDIA_BREAKPOINT}) {
width: 220px; width: 220px;
...@@ -124,7 +124,7 @@ const ModalWrapper = styled.div` ...@@ -124,7 +124,7 @@ const ModalWrapper = styled.div`
const DropdownWrapper = styled(Column)<{ isOpen: boolean }>` const DropdownWrapper = styled(Column)<{ isOpen: boolean }>`
padding: 16px 0px; padding: 16px 0px;
background-color: ${({ theme }) => theme.backgroundModule}; background-color: ${({ theme }) => theme.backgroundSurface};
display: ${({ isOpen }) => (isOpen ? 'flex' : 'none')}; display: ${({ isOpen }) => (isOpen ? 'flex' : 'none')};
position: absolute; position: absolute;
top: 52px; top: 52px;
......
import { Plural } from '@lingui/macro'
import { useOnClickOutside } from 'hooks/useOnClickOutside'
import ms from 'ms.macro' import ms from 'ms.macro'
import { SortDropdown } from 'nft/components/common/SortDropdown'
import { Column, Row } from 'nft/components/Flex' import { Column, Row } from 'nft/components/Flex'
import { NumericInput } from 'nft/components/layout/Input' import { NumericInput } from 'nft/components/layout/Input'
import { bodySmall, buttonTextMedium, caption } from 'nft/css/common.css' import { bodySmall, caption } from 'nft/css/common.css'
import { useSellAsset } from 'nft/hooks' import { useSellAsset } from 'nft/hooks'
import { DropDownOption } from 'nft/types' import { DropDownOption } from 'nft/types'
import { pluralize } from 'nft/utils/roundAndPluralize' import { useEffect, useMemo, useReducer, useRef, useState } from 'react'
import { useEffect, useMemo, useState } from 'react' import { AlertTriangle, ChevronDown } from 'react-feather'
import { AlertTriangle } from 'react-feather'
import styled from 'styled-components/macro' import styled from 'styled-components/macro'
import { ThemedText } from 'theme' import { Z_INDEX } from 'theme/zIndex'
import { Dropdown } from './Dropdown'
const ModalWrapper = styled(Column)` const ModalWrapper = styled(Column)`
gap: 4px; gap: 4px;
...@@ -25,17 +27,41 @@ const InputWrapper = styled(Row)<{ isInvalid: boolean }>` ...@@ -25,17 +27,41 @@ const InputWrapper = styled(Row)<{ isInvalid: boolean }>`
border-color: ${({ isInvalid, theme }) => (isInvalid ? theme.accentCritical : theme.backgroundOutline)}; border-color: ${({ isInvalid, theme }) => (isInvalid ? theme.accentCritical : theme.backgroundOutline)};
` `
const DropdownWrapper = styled(ThemedText.BodyPrimary)` const DropdownPrompt = styled(Row)`
gap: 4px;
background-color: ${({ theme }) => theme.backgroundInteractive};
cursor: pointer; cursor: pointer;
display: flex; font-weight: 600;
justify-content: flex-end; font-size: 12px;
height: min-content; line-height: 16px;
width: 80px; border-radius: 8px;
padding: 6px 4px 6px 8px;
width: min-content;
white-space: nowrap;
color: ${({ theme }) => theme.textPrimary};
&:hover { &:hover {
background-color: ${({ theme }) => theme.backgroundInteractive}; opacity: ${({ theme }) => theme.opacity.hover};
} }
border-radius: 12px; `
padding: 8px;
const DropdownChevron = styled(ChevronDown)<{ isOpen: boolean }>`
height: 16px;
width: 16px;
color: ${({ theme }) => theme.textSecondary};
transform: ${({ isOpen }) => isOpen && 'rotate(180deg)'};
transition: ${({
theme: {
transition: { duration, timing },
},
}) => `transform ${duration.fast} ${timing.ease}`};
`
const DropdownContainer = styled.div`
position: absolute;
top: 48px;
right: 0px;
z-index: ${Z_INDEX.dropdown};
` `
const ErrorMessage = styled(Row)` const ErrorMessage = styled(Row)`
...@@ -65,39 +91,73 @@ enum ErrorState { ...@@ -65,39 +91,73 @@ enum ErrorState {
export const SetDurationModal = () => { export const SetDurationModal = () => {
const [duration, setDuration] = useState(Duration.day) const [duration, setDuration] = useState(Duration.day)
const [displayDuration, setDisplayDuration] = useState(Duration.day)
const [amount, setAmount] = useState('7') const [amount, setAmount] = useState('7')
const [errorState, setErrorState] = useState(ErrorState.valid) const [errorState, setErrorState] = useState(ErrorState.valid)
const setGlobalExpiration = useSellAsset((state) => state.setGlobalExpiration) const setGlobalExpiration = useSellAsset((state) => state.setGlobalExpiration)
const [showDropdown, toggleShowDropdown] = useReducer((s) => !s, false)
const durationDropdownRef = useRef<HTMLDivElement>(null)
useOnClickOutside(durationDropdownRef, showDropdown ? toggleShowDropdown : undefined)
const setCustomExpiration = (event: React.ChangeEvent<HTMLInputElement>) => { const setCustomExpiration = (event: React.ChangeEvent<HTMLInputElement>) => {
setAmount(event.target.value.length ? event.target.value : '') setAmount(event.target.value.length ? event.target.value : '')
setDuration(displayDuration)
}
const selectDuration = (duration: Duration) => {
setDuration(duration)
setDisplayDuration(duration)
} }
const durationOptions: DropDownOption[] = useMemo( const durationOptions: DropDownOption[] = useMemo(
() => [ () => [
{ {
displayText: 'Hours', displayText: 'hours',
onClick: () => selectDuration(Duration.hour), isSelected: duration === Duration.hour,
onClick: () => {
setDuration(Duration.hour)
toggleShowDropdown()
},
}, },
{ {
displayText: 'Days', displayText: 'days',
onClick: () => selectDuration(Duration.day), isSelected: duration === Duration.day,
onClick: () => {
setDuration(Duration.day)
toggleShowDropdown()
},
}, },
{ {
displayText: 'Weeks', displayText: 'weeks',
onClick: () => selectDuration(Duration.week), isSelected: duration === Duration.week,
onClick: () => {
setDuration(Duration.week)
toggleShowDropdown()
},
}, },
{ {
displayText: 'Months', displayText: 'months',
onClick: () => selectDuration(Duration.month), isSelected: duration === Duration.month,
onClick: () => {
setDuration(Duration.month)
toggleShowDropdown()
},
}, },
], ],
[] [duration]
) )
let prompt
switch (duration) {
case Duration.hour:
prompt = <Plural value={amount} _1="hour" other="hours" />
break
case Duration.day:
prompt = <Plural value={amount} _1="day" other="days" />
break
case Duration.week:
prompt = <Plural value={amount} _1="week" other="weeks" />
break
case Duration.month:
prompt = <Plural value={amount} _1="month" other="months" />
break
default:
break
}
useEffect(() => { useEffect(() => {
const expiration = convertDurationToExpiration(parseFloat(amount), duration) const expiration = convertDurationToExpiration(parseFloat(amount), duration)
...@@ -108,7 +168,7 @@ export const SetDurationModal = () => { ...@@ -108,7 +168,7 @@ export const SetDurationModal = () => {
}, [amount, duration, setGlobalExpiration]) }, [amount, duration, setGlobalExpiration])
return ( return (
<ModalWrapper> <ModalWrapper ref={durationDropdownRef}>
<InputWrapper isInvalid={errorState !== ErrorState.valid}> <InputWrapper isInvalid={errorState !== ErrorState.valid}>
<NumericInput <NumericInput
as="input" as="input"
...@@ -124,15 +184,14 @@ export const SetDurationModal = () => { ...@@ -124,15 +184,14 @@ export const SetDurationModal = () => {
onChange={setCustomExpiration} onChange={setCustomExpiration}
flexShrink="0" flexShrink="0"
/> />
<DropdownWrapper className={buttonTextMedium}> <DropdownPrompt onClick={toggleShowDropdown}>
<SortDropdown {prompt} <DropdownChevron isOpen={showDropdown} />
dropDownOptions={durationOptions} </DropdownPrompt>
mini {showDropdown && (
miniPrompt={displayDuration + (displayDuration === duration ? pluralize(parseFloat(amount)) : 's')} <DropdownContainer>
left={38} <Dropdown dropDownOptions={durationOptions} width={125} />
top={38} </DropdownContainer>
/> )}
</DropdownWrapper>
</InputWrapper> </InputWrapper>
{errorState !== ErrorState.valid && ( {errorState !== ErrorState.valid && (
<ErrorMessage className={caption}> <ErrorMessage className={caption}>
......
...@@ -171,6 +171,7 @@ export interface DropDownOption { ...@@ -171,6 +171,7 @@ export interface DropDownOption {
reverseIndex?: number reverseIndex?: number
reverseOnClick?: () => void reverseOnClick?: () => void
sortBy?: SortBy sortBy?: SortBy
isSelected?: boolean
} }
export enum DetailsOrigin { export enum DetailsOrigin {
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment