From 11bce9859629389162688ce8a90de412b07547cd Mon Sep 17 00:00:00 2001 From: Danh Date: Thu, 5 Oct 2023 16:19:55 +0700 Subject: [PATCH] create new select v --- src/components/SelectV2.tsx | 201 ++++++++++++++ .../MultipleChainSelectV2/PopoverBody.tsx | 256 ++++++++++++++++++ .../MultipleChainSelectV2/SelectButton.tsx | 98 +++++++ .../MultipleChainSelectV2/index.tsx | 127 +++++++++ .../components/TokenFilter/index.tsx | 38 +-- 5 files changed, 687 insertions(+), 33 deletions(-) create mode 100644 src/components/SelectV2.tsx create mode 100644 src/pages/MyEarnings/MultipleChainSelectV2/PopoverBody.tsx create mode 100644 src/pages/MyEarnings/MultipleChainSelectV2/SelectButton.tsx create mode 100644 src/pages/MyEarnings/MultipleChainSelectV2/index.tsx diff --git a/src/components/SelectV2.tsx b/src/components/SelectV2.tsx new file mode 100644 index 0000000000..60e8d4ae92 --- /dev/null +++ b/src/components/SelectV2.tsx @@ -0,0 +1,201 @@ +import { Portal } from '@reach/portal' +import { AnimatePresence, motion } from 'framer-motion' +import { CSSProperties, ReactNode, useCallback, useEffect, useRef, useState } from 'react' +import { usePopper } from 'react-popper' +import styled from 'styled-components' + +import useInterval from 'hooks/useInterval' +import { useOnClickOutside } from 'hooks/useOnClickOutside' + +import { DropdownArrowIcon } from './ArrowRotate' + +const SelectWrapper = styled.div` + cursor: pointer; + border-radius: 12px; + background: ${({ theme }) => theme.buttonBlack}; + display: flex; + justify-content: space-between; + align-items: center; + position: relative; + font-size: 12px; + color: ${({ theme }) => theme.subText}; + padding: 12px; + :hover { + filter: brightness(1.2); + z-index: 10; + } +` + +const SelectMenu = styled(motion.div)` + position: absolute; + top: 0px; + left: 0; + right: 0; + margin: auto; + border-radius: 16px; + filter: drop-shadow(0px 4px 12px rgba(0, 0, 0, 0.36)); + z-index: 2; + background: ${({ theme }) => theme.tabActive}; + padding: 10px 0px; + width: max-content; +` + +const Option = styled.div<{ $selected: boolean }>` + padding: 10px 18px; + cursor: pointer; + font-size: 12px; + color: ${({ theme }) => theme.subText}; + white-space: nowrap; + &:hover { + background-color: ${({ theme }) => theme.background}; + } + font-weight: ${({ $selected }) => ($selected ? '500' : 'unset')}; +` + +const SelectedWrap = styled.div` + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + flex: 1; +` +export type SelectOption = { value?: string | number; label: ReactNode; onSelect?: () => void } + +const getOptionValue = (option: SelectOption | undefined) => { + if (!option) return '' + return typeof option !== 'object' ? option : option.value ?? '' +} +const getOptionLabel = (option: SelectOption | undefined) => { + if (!option) return '' + return typeof option !== 'object' ? option : option.label || option.value +} + +// function isElementOverflowBottom(el: HTMLElement) { +// const rect = el.getBoundingClientRect() +// return rect.bottom >= (window.innerHeight || document?.documentElement?.clientHeight) +// } + +const defaultOffset: [number, number] = [0 /* skidding */, 2 /* distance */] +function Select({ + options = [], + activeRender, + optionRender, + style = {}, + menuStyle = {}, + optionStyle = {}, + onChange, + value: selectedValue, + className, + forceMenuPlacementTop = false, + arrowColor, + dropdownRender, +}: { + value?: string | number + className?: string + options: SelectOption[] + dropdownRender?: (menu: ReactNode) => ReactNode + activeRender?: (selectedItem: SelectOption | undefined) => ReactNode + optionRender?: (option: SelectOption | undefined) => ReactNode + style?: CSSProperties + menuStyle?: CSSProperties + optionStyle?: CSSProperties + onChange?: (value: any) => void + forceMenuPlacementTop?: boolean + arrowColor?: string +}) { + const [selected, setSelected] = useState(getOptionValue(options?.[0])) + const [showMenu, setShowMenu] = useState(false) + const [menuPlacementTop] = useState(forceMenuPlacementTop) + + useEffect(() => { + const findValue = options.find(item => getOptionValue(item) === selectedValue)?.value + setSelected(findValue || getOptionValue(options?.[0])) + }, [selectedValue, options]) + + useEffect(() => { + if (!refMenu?.current) return + // if (!menuPlacementTop) setForceMenuPlacementTop(showMenu && isElementOverflowBottom(refMenu.current)) + }, [showMenu, menuPlacementTop]) + const [referenceElement, setReferenceElement] = useState(null) + + useOnClickOutside(referenceElement as any, () => { + setShowMenu(false) + }) + const selectedInfo = options.find(item => getOptionValue(item) === selected) + const refMenu = useRef(null) + + const renderMenu = () => { + return options.map(item => { + const value = getOptionValue(item) + const onClick = (e: React.MouseEvent) => { + e.stopPropagation() + e.preventDefault() + setShowMenu(prev => !prev) + if (item.onSelect) item.onSelect?.() + else { + setSelected(value) + onChange?.(value) + } + } + return ( + + ) + }) + } + + const [popperElement, setPopperElement] = useState(null) + + const { styles, update } = usePopper(referenceElement, popperElement, { + placement: 'bottom', + strategy: 'fixed', + modifiers: [{ name: 'offset', options: { offset: defaultOffset } }], + }) + + const updateCallback = useCallback(() => { + update && update() + }, [update]) + + useInterval(updateCallback, showMenu ? 100 : null) + + return ( + { + setShowMenu(!showMenu) + }} + style={style} + className={className} + > + {activeRender ? activeRender(selectedInfo) : getOptionLabel(selectedInfo)} + + + {showMenu && ( + + + {dropdownRender ? dropdownRender(renderMenu()) : renderMenu()} + + + )} + + + ) +} + +export default styled(Select)`` diff --git a/src/pages/MyEarnings/MultipleChainSelectV2/PopoverBody.tsx b/src/pages/MyEarnings/MultipleChainSelectV2/PopoverBody.tsx new file mode 100644 index 0000000000..4a80e373d1 --- /dev/null +++ b/src/pages/MyEarnings/MultipleChainSelectV2/PopoverBody.tsx @@ -0,0 +1,256 @@ +import { Trans } from '@lingui/macro' +import { rgba } from 'polished' +import { MouseEventHandler, useEffect, useRef, useState } from 'react' +import { Box, Flex, Text } from 'rebass' +import styled from 'styled-components' + +import { ReactComponent as LogoKyber } from 'assets/svg/logo_kyber.svg' +import { ButtonPrimary } from 'components/Button' +import Checkbox from 'components/CheckBox' +import { MouseoverTooltip } from 'components/Tooltip' +import { NETWORKS_INFO } from 'constants/networks' +import useChainsConfig from 'hooks/useChainsConfig' +import useTheme from 'hooks/useTheme' + +import { MultipleChainSelectProps, StyledLogo } from '.' + +const ChainListWrapper = styled.div` + display: flex; + flex-direction: column; + gap: 12px; + max-height: 180px; + overflow: auto; + + /* width */ + ::-webkit-scrollbar { + display: unset; + width: 8px; + border-radius: 999px; + } + + /* Track */ + ::-webkit-scrollbar-track { + background: transparent; + border-radius: 999px; + } + + /* Handle */ + ::-webkit-scrollbar-thumb { + background: ${({ theme }) => rgba(theme.subText, 0.4)}; + border-radius: 999px; + } +` + +type ApplyButtonProps = { + disabled: boolean + onClick: MouseEventHandler + numOfChains: number +} + +export const ApplyButton: React.FC = ({ disabled, onClick, numOfChains }) => { + const theme = useTheme() + return ( + + + View Selected Chains + + {numOfChains ? String(numOfChains).padStart(2, '0') : 0} + + + + ) +} + +const PopoverBody: React.FC void }> = ({ + onClose, + comingSoonList = [], + chainIds, + selectedChainIds, + handleChangeChains, + onTracking, + menuStyle, +}) => { + const theme = useTheme() + const selectAllRef = useRef(null) + + const { activeChains } = useChainsConfig() + const selectedChains = selectedChainIds.filter(item => !comingSoonList.includes(item)) + + const [localSelectedChains, setLocalSelectedChains] = useState(() => selectedChains) + + const networkList = chainIds.filter( + item => !comingSoonList.includes(item) && activeChains.some(e => e.chainId === item), + ) + + const isAllSelected = localSelectedChains.length === networkList.length + + useEffect(() => { + setLocalSelectedChains(selectedChains) + // eslint-disable-next-line + }, [selectedChains.length]) + + useEffect(() => { + if (!selectAllRef.current) { + return + } + + const indeterminate = 0 < localSelectedChains.length && localSelectedChains.length < networkList.length + selectAllRef.current.indeterminate = indeterminate + }, [localSelectedChains, networkList.length]) + + const allNetworks = [...networkList, ...comingSoonList] + + const onChangeChain = () => { + if (isAllSelected) { + setLocalSelectedChains([]) + } else { + onTracking?.() + setLocalSelectedChains(networkList) + } + } + + return ( + + + + + + + + + + All Chains + + + + + {allNetworks.map((network, i) => { + const config = NETWORKS_INFO[network] + + const isComingSoon = comingSoonList.includes(network) + const isSelected = isComingSoon ? false : localSelectedChains.includes(network) + + const handleClick = () => { + if (isComingSoon) return + if (isSelected) { + setLocalSelectedChains(localSelectedChains.filter(chain => chain !== network)) + } else { + setLocalSelectedChains([...localSelectedChains, network]) + } + } + + return ( + + + + + + + + {config.name} + + + + ) + })} + + + + + { + handleChangeChains(localSelectedChains) + onClose() + }} + numOfChains={localSelectedChains.length} + /> + + ) +} + +export default PopoverBody diff --git a/src/pages/MyEarnings/MultipleChainSelectV2/SelectButton.tsx b/src/pages/MyEarnings/MultipleChainSelectV2/SelectButton.tsx new file mode 100644 index 0000000000..d96d28b34a --- /dev/null +++ b/src/pages/MyEarnings/MultipleChainSelectV2/SelectButton.tsx @@ -0,0 +1,98 @@ +import { Trans } from '@lingui/macro' +import { Flex } from 'rebass' +import styled from 'styled-components' + +import { ReactComponent as LogoKyber } from 'assets/svg/logo_kyber.svg' +import { NETWORKS_INFO } from 'constants/networks' +import useTheme from 'hooks/useTheme' + +import { MultipleChainSelectProps, StyledLogo } from '.' + +const ButtonBodyWrapper = styled.div` + display: flex; + align-items: center; + gap: 8px; +` + +const Label = styled.span<{ labelColor?: string }>` + font-weight: 500; + font-size: 14px; + line-height: 20px; + color: ${({ theme, labelColor }) => labelColor || theme.subText}; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +` +type Props = { + onClick: () => void +} & MultipleChainSelectProps +const SelectButton: React.FC = ({ + onClick, + selectedChainIds, + chainIds, + activeRender, + activeStyle, + labelColor, +}) => { + const theme = useTheme() + + const renderButtonBody = () => { + if (selectedChainIds.length === chainIds.length) { + return ( + + + + + ) + } + + if (selectedChainIds.length === 1) { + const config = NETWORKS_INFO[selectedChainIds[0]] + const iconSrc = theme.darkMode && config.iconDark ? config.iconDark : config.icon + + return ( + + + + + ) + } + + return ( + + {selectedChainIds.slice(0, 3).map(chainId => { + const config = NETWORKS_INFO[chainId] + const iconSrc = theme.darkMode && config.iconDark ? config.iconDark : config.icon + + return + })} + + {selectedChainIds.length > 3 && `+${selectedChainIds.length - 3}`} + + ) + } + + return ( + + {activeRender ? activeRender(renderButtonBody()) : renderButtonBody()} + + ) +} + +export default SelectButton diff --git a/src/pages/MyEarnings/MultipleChainSelectV2/index.tsx b/src/pages/MyEarnings/MultipleChainSelectV2/index.tsx new file mode 100644 index 0000000000..a098609eec --- /dev/null +++ b/src/pages/MyEarnings/MultipleChainSelectV2/index.tsx @@ -0,0 +1,127 @@ +import { ChainId } from '@kyberswap/ks-sdk-core' +import { ReactNode, useRef, useState } from 'react' +import { Box, Flex, Text } from 'rebass' +import styled, { CSSProperties } from 'styled-components' + +import Checkbox from 'components/CheckBox' +import Select from 'components/SelectV2' +import { MouseoverTooltip } from 'components/Tooltip' +import { NETWORKS_INFO } from 'constants/networks' +import { useOnClickOutside } from 'hooks/useOnClickOutside' +import useTheme from 'hooks/useTheme' + +import { ApplyButton } from './PopoverBody' +import SelectButton from './SelectButton' + +export const StyledLogo = styled.img` + width: 20px; + height: auto; +` + +export type MultipleChainSelectProps = { + className?: string + comingSoonList?: ChainId[] + chainIds: ChainId[] + selectedChainIds: ChainId[] + handleChangeChains: (v: ChainId[]) => void + onTracking?: () => void + menuStyle?: CSSProperties + style?: CSSProperties + activeStyle?: CSSProperties + labelColor?: string + activeRender?: (node: ReactNode) => ReactNode +} +const MultipleChainSelect: React.FC = ({ className, style, ...props }) => { + const [expanded, setExpanded] = useState(false) + const ref = useRef(null) + + const collapse = () => { + setExpanded(false) + } + + useOnClickOutside(ref, expanded ? collapse : undefined) + const { comingSoonList = [], selectedChainIds = [], handleChangeChains } = props + const options = props.chainIds.map(id => ({ value: id, label: id })) + const theme = useTheme() + const selectedChains = selectedChainIds.filter(item => !comingSoonList.includes(item)) + const [localSelectedChains, setLocalSelectedChains] = useState(() => selectedChains) + return ( +