From 39e745b210c9e6d65a93029f91d77ef450ef8ff9 Mon Sep 17 00:00:00 2001 From: Bob van der Helm <34470358+bobthebuidlr@users.noreply.github.com> Date: Mon, 3 Jul 2023 09:39:34 +0200 Subject: [PATCH] Mp 2713 asset selector (#275) * added dumb asset selector * fix table layouts borrow and farm * :bento: added basic overlay, esc btn * finish asset selector * Update tailwind configf to include button styles --- __tests__/components/Button/Button.test.tsx | 2 +- src/components/Borrow/Borrowings.tsx | 2 +- src/components/Button/EscButton.tsx | 43 ++++++++++ src/components/Button/constants.ts | 28 ++++-- src/components/Button/index.tsx | 9 +- src/components/Earn/vault/Vaults.tsx | 8 ++ src/components/Icons/StarFilled.svg | 9 ++ src/components/Icons/StarOutlined.svg | 3 + src/components/Icons/SwapIcon.svg | 3 + src/components/Icons/index.ts | 3 + src/components/Modal.tsx | 11 +-- src/components/SearchBar.tsx | 8 +- .../TradeModule/AssetSelector/AssetButton.tsx | 22 +++++ .../TradeModule/AssetSelector/AssetItem.tsx | 59 +++++++++++++ .../TradeModule/AssetSelector/AssetList.tsx | 43 ++++++++++ .../AssetSelector/AssetOverlay.tsx | 86 +++++++++++++++++++ .../AssetSelector/AssetSelector.tsx | 67 +++++++++++++++ .../index.tsx} | 23 ++--- src/constants/localStore.ts | 1 + src/hooks/useAssets.ts | 33 +++++++ src/hooks/useFilteredAssets.ts | 29 +++++++ src/hooks/useToggle.tsx | 8 +- src/pages/TradePage.tsx | 2 +- src/types/interfaces/asset.d.ts | 1 + tailwind.config.js | 4 + 25 files changed, 467 insertions(+), 40 deletions(-) create mode 100644 src/components/Button/EscButton.tsx create mode 100644 src/components/Icons/StarFilled.svg create mode 100644 src/components/Icons/StarOutlined.svg create mode 100644 src/components/Icons/SwapIcon.svg create mode 100644 src/components/Trade/TradeModule/AssetSelector/AssetButton.tsx create mode 100644 src/components/Trade/TradeModule/AssetSelector/AssetItem.tsx create mode 100644 src/components/Trade/TradeModule/AssetSelector/AssetList.tsx create mode 100644 src/components/Trade/TradeModule/AssetSelector/AssetOverlay.tsx create mode 100644 src/components/Trade/TradeModule/AssetSelector/AssetSelector.tsx rename src/components/Trade/{TradeModule.tsx => TradeModule/index.tsx} (59%) create mode 100644 src/hooks/useAssets.ts create mode 100644 src/hooks/useFilteredAssets.ts diff --git a/__tests__/components/Button/Button.test.tsx b/__tests__/components/Button/Button.test.tsx index b0408243..7e52b677 100644 --- a/__tests__/components/Button/Button.test.tsx +++ b/__tests__/components/Button/Button.test.tsx @@ -80,7 +80,7 @@ describe(' + ) +} diff --git a/src/components/Button/constants.ts b/src/components/Button/constants.ts index 753df760..4f25ff57 100644 --- a/src/components/Button/constants.ts +++ b/src/components/Button/constants.ts @@ -31,21 +31,24 @@ export const buttonTransparentColorClasses = { } export const buttonRoundSizeClasses = { - small: 'h-[32px] w-[32px]', - medium: 'h-[40px] w-[40px]', - large: 'h-[56px] w-[56px]', + xs: 'h-5 w-5', + sm: 'h-8 w-8', + md: 'h-10 w-10', + lg: 'h-14 w-14', } export const buttonSizeClasses = { - small: 'text-sm', - medium: 'text-base', - large: 'text-lg', + xs: 'text-xs', + sm: 'text-sm', + md: 'text-base', + lg: 'text-lg', } export const buttonPaddingClasses = { - small: 'px-4 py-1.5 min-h-[32px]', - medium: 'px-4 py-2 min-h-[40px]', - large: 'px-4 py-2.5 min-h-[56px]', + xs: 'px-1.5 py-0.5 min-h-5', + sm: 'px-4 py-1.5 min-h-8', + md: 'px-4 py-2 min-h-10', + lg: 'px-4 py-2.5 min-h-14', } export const buttonVariantClasses = { @@ -53,3 +56,10 @@ export const buttonVariantClasses = { transparent: 'rounded-sm bg-transparent p-0 transition duration-200 ease-in', round: 'rounded-full p-0', } + +export const circularProgressSize = { + xs: 8, + sm: 10, + md: 12, + lg: 18, +} diff --git a/src/components/Button/index.tsx b/src/components/Button/index.tsx index ee4efda1..ed4affcd 100644 --- a/src/components/Button/index.tsx +++ b/src/components/Button/index.tsx @@ -11,6 +11,7 @@ import { buttonSizeClasses, buttonTransparentColorClasses, buttonVariantClasses, + circularProgressSize, focusClasses, } from 'components/Button/constants' import { glowElement } from 'components/Button/utils' @@ -24,7 +25,7 @@ interface Props { disabled?: boolean id?: string showProgressIndicator?: boolean - size?: 'small' | 'medium' | 'large' + size?: 'xs' | 'sm' | 'md' | 'lg' text?: string | ReactNode variant?: 'solid' | 'transparent' | 'round' onClick?: (e: React.MouseEvent) => void @@ -45,7 +46,7 @@ const Button = React.forwardRef(function Button( disabled, id = '', showProgressIndicator, - size = 'small', + size = 'sm', text, variant = 'solid', onClick, @@ -111,7 +112,7 @@ const Button = React.forwardRef(function Button( tabIndex={tabIndex} > {showProgressIndicator ? ( - + ) : ( <> {leftIcon && {leftIcon}} @@ -119,7 +120,7 @@ const Button = React.forwardRef(function Button( {children && children} {rightIcon && {rightIcon}} {hasSubmenu && ( - + )} diff --git a/src/components/Earn/vault/Vaults.tsx b/src/components/Earn/vault/Vaults.tsx index 496e2be2..5a476a32 100644 --- a/src/components/Earn/vault/Vaults.tsx +++ b/src/components/Earn/vault/Vaults.tsx @@ -42,6 +42,14 @@ function Content(props: Props) { if (!vaultsToDisplay.length) return null + if (props.type === 'deposited') { + return ( + + + + ) + } + return } diff --git a/src/components/Icons/StarFilled.svg b/src/components/Icons/StarFilled.svg new file mode 100644 index 00000000..b548bbfc --- /dev/null +++ b/src/components/Icons/StarFilled.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/src/components/Icons/StarOutlined.svg b/src/components/Icons/StarOutlined.svg new file mode 100644 index 00000000..ba632907 --- /dev/null +++ b/src/components/Icons/StarOutlined.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/components/Icons/SwapIcon.svg b/src/components/Icons/SwapIcon.svg new file mode 100644 index 00000000..2d2cce1c --- /dev/null +++ b/src/components/Icons/SwapIcon.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/components/Icons/index.ts b/src/components/Icons/index.ts index 591adf49..dae48f31 100644 --- a/src/components/Icons/index.ts +++ b/src/components/Icons/index.ts @@ -33,7 +33,10 @@ export { default as Shield } from 'components/Icons/Shield.svg' export { default as SortAsc } from 'components/Icons/SortAsc.svg' export { default as SortDesc } from 'components/Icons/SortDesc.svg' export { default as SortNone } from 'components/Icons/SortNone.svg' +export { default as StarFilled } from 'components/Icons/StarFilled.svg' +export { default as StarOutlined } from 'components/Icons/StarOutlined.svg' export { default as Subtract } from 'components/Icons/Subtract.svg' +export { default as SwapIcon } from 'components/Icons/SwapIcon.svg' export { default as TrashBin } from 'components/Icons/TrashBin.svg' export { default as Wallet } from 'components/Icons/Wallet.svg' // @endindex diff --git a/src/components/Modal.tsx b/src/components/Modal.tsx index 44963ce5..46742eb6 100644 --- a/src/components/Modal.tsx +++ b/src/components/Modal.tsx @@ -1,10 +1,9 @@ import classNames from 'classnames' import { ReactNode, useEffect, useRef } from 'react' -import Button from 'components/Button' import Card from 'components/Card' -import { Cross } from 'components/Icons' -import Text from 'components/Text' + +import EscButton from './Button/EscButton' interface Props { header: string | ReactNode @@ -63,11 +62,7 @@ export default function Modal(props: Props) { >
{props.header} - {!props.hideCloseBtn && ( - - )} + {!props.hideCloseBtn && }
{props.children ? props.children : props.content} diff --git a/src/components/SearchBar.tsx b/src/components/SearchBar.tsx index 5ad619d3..afff4d54 100644 --- a/src/components/SearchBar.tsx +++ b/src/components/SearchBar.tsx @@ -1,15 +1,16 @@ import classNames from 'classnames' -import { ChangeEvent } from 'react' +import { ChangeEvent, forwardRef } from 'react' import { Search } from 'components/Icons' interface Props { value: string placeholder: string + autofocus?: boolean onChange: (value: string) => void } -export default function SearchBar(props: Props) { +const SearchBar = (props: Props) => { function onChange(event: ChangeEvent) { props.onChange(event.target.value) } @@ -28,7 +29,10 @@ export default function SearchBar(props: Props) { className='h-full w-full bg-transparent text-xs placeholder-white/30 outline-none' placeholder={props.placeholder} onChange={(event) => onChange(event)} + autoFocus={props.autofocus} />
) } + +export default forwardRef(SearchBar) diff --git a/src/components/Trade/TradeModule/AssetSelector/AssetButton.tsx b/src/components/Trade/TradeModule/AssetSelector/AssetButton.tsx new file mode 100644 index 00000000..22531fab --- /dev/null +++ b/src/components/Trade/TradeModule/AssetSelector/AssetButton.tsx @@ -0,0 +1,22 @@ +import AssetImage from 'components/AssetImage' +import Button from 'components/Button' + +interface Props { + asset: Asset + onClick: () => void +} + +export default function AssetButton(props: Props) { + return ( + + + ) +} diff --git a/src/components/Trade/TradeModule/AssetSelector/AssetList.tsx b/src/components/Trade/TradeModule/AssetSelector/AssetList.tsx new file mode 100644 index 00000000..e03c6f03 --- /dev/null +++ b/src/components/Trade/TradeModule/AssetSelector/AssetList.tsx @@ -0,0 +1,43 @@ +import classNames from 'classnames' + +import { ChevronDown } from 'components/Icons' +import Text from 'components/Text' +import AssetItem from 'components/Trade/TradeModule/AssetSelector/AssetItem' + +interface Props { + type: 'buy' | 'sell' + assets: Asset[] + isOpen: boolean + toggleOpen: () => void + onChangeAsset: (asset: Asset) => void +} + +export default function AssetList(props: Props) { + return ( +
+ + {props.isOpen && + (props.assets.length === 0 ? ( + + No available assets found + + ) : ( +
    + {props.assets.map((asset) => ( + + ))} +
+ ))} +
+ ) +} diff --git a/src/components/Trade/TradeModule/AssetSelector/AssetOverlay.tsx b/src/components/Trade/TradeModule/AssetSelector/AssetOverlay.tsx new file mode 100644 index 00000000..3fef650d --- /dev/null +++ b/src/components/Trade/TradeModule/AssetSelector/AssetOverlay.tsx @@ -0,0 +1,86 @@ +import { useCallback, useMemo, useRef } from 'react' + +import EscButton from 'components/Button/EscButton' +import Divider from 'components/Divider' +import Overlay from 'components/Overlay' +import SearchBar from 'components/SearchBar' +import Text from 'components/Text' +import AssetList from 'components/Trade/TradeModule/AssetSelector/AssetList' +import useFilteredAssets from 'hooks/useFilteredAssets' + +export type OverlayState = 'buy' | 'sell' | 'closed' + +interface Props { + state: OverlayState + buyAsset: Asset + sellAsset: Asset + onChangeBuyAsset: (asset: Asset) => void + onChangeSellAsset: (asset: Asset) => void + onChangeState: (state: OverlayState) => void +} + +export default function AssetOverlay(props: Props) { + const { assets, searchString, onChangeSearch } = useFilteredAssets() + + const handleClose = useCallback(() => props.onChangeState('closed'), [props]) + + const handleToggle = useCallback( + () => props.onChangeState(props.state === 'buy' ? 'sell' : 'buy'), + [props], + ) + + const buyAssets = useMemo( + () => assets.filter((asset) => asset.denom !== props.sellAsset.denom), + [assets, props.sellAsset], + ) + + const sellAssets = useMemo( + () => assets.filter((asset) => asset.denom !== props.buyAsset.denom), + [assets, props.buyAsset], + ) + + function onChangeBuyAsset(asset: Asset) { + props.onChangeBuyAsset(asset) + props.onChangeState('sell') + onChangeSearch('') + } + + function onChangeSellAsset(asset: Asset) { + props.onChangeSellAsset(asset) + onChangeSearch('') + } + + return ( + +
+ Select asset + +
+ +
+ +
+ + + +
+ ) +} diff --git a/src/components/Trade/TradeModule/AssetSelector/AssetSelector.tsx b/src/components/Trade/TradeModule/AssetSelector/AssetSelector.tsx new file mode 100644 index 00000000..5fb29d81 --- /dev/null +++ b/src/components/Trade/TradeModule/AssetSelector/AssetSelector.tsx @@ -0,0 +1,67 @@ +import { useCallback, useMemo, useState } from 'react' + +import { SwapIcon } from 'components/Icons' +import Text from 'components/Text' +import { ASSETS } from 'constants/assets' +import AssetButton from 'components/Trade/TradeModule/AssetSelector/AssetButton' +import AssetOverlay, { OverlayState } from 'components/Trade/TradeModule/AssetSelector/AssetOverlay' + +export default function AssetSelector() { + const [overlayState, setOverlayState] = useState('closed') + const [buyAsset, setBuyAsset] = useState(ASSETS[0]) + const [sellAsset, setSellAsset] = useState(ASSETS[1]) + + function handleSwapAssets() { + setBuyAsset(sellAsset) + setSellAsset(buyAsset) + } + + const handleChangeBuyAsset = useCallback( + (asset: Asset) => { + setBuyAsset(asset) + setOverlayState('sell') + }, + [setBuyAsset], + ) + + const handleChangeSellAsset = useCallback( + (asset: Asset) => { + setSellAsset(asset) + setOverlayState('closed') + }, + [setSellAsset], + ) + const handleChangeState = useCallback( + (state: OverlayState) => { + setOverlayState(state) + }, + [setOverlayState], + ) + + const buyAssets = useMemo( + () => ASSETS.filter((asset) => asset.denom !== sellAsset.denom), + [sellAsset], + ) + + return ( +
+ Buy + + Sell + + setOverlayState('buy')} asset={buyAsset} /> + + setOverlayState('sell')} asset={sellAsset} /> + +
+ ) +} diff --git a/src/components/Trade/TradeModule.tsx b/src/components/Trade/TradeModule/index.tsx similarity index 59% rename from src/components/Trade/TradeModule.tsx rename to src/components/Trade/TradeModule/index.tsx index b4b69818..d3e85edb 100644 --- a/src/components/Trade/TradeModule.tsx +++ b/src/components/Trade/TradeModule/index.tsx @@ -1,9 +1,11 @@ -import { Suspense } from 'react' import { useParams } from 'react-router-dom' +import classNames from 'classnames' -import Card from 'components/Card' import Loading from 'components/Loading' import Text from 'components/Text' +import Divider from 'components/Divider' + +import AssetSelector from './AssetSelector/AssetSelector' function Content() { const params = useParams() @@ -24,14 +26,15 @@ function Fallback() { export default function TradeModule() { return ( - - }> - - - + + + ) } diff --git a/src/constants/localStore.ts b/src/constants/localStore.ts index 47bbe975..4705f93a 100644 --- a/src/constants/localStore.ts +++ b/src/constants/localStore.ts @@ -1,2 +1,3 @@ export const DISPLAY_CURRENCY_KEY = 'displayCurrency' export const ENABLE_ANIMATIONS_KEY = 'enableAnimations' +export const FAVORITE_ASSETS = 'favoriteAssets' diff --git a/src/hooks/useAssets.ts b/src/hooks/useAssets.ts new file mode 100644 index 00000000..8ca8dc20 --- /dev/null +++ b/src/hooks/useAssets.ts @@ -0,0 +1,33 @@ +import { useCallback, useEffect, useState } from 'react' + +import { ASSETS } from 'constants/assets' +import { FAVORITE_ASSETS } from 'constants/localStore' + +export default function useAssets() { + const [assets, setAssets] = useState(ASSETS) + + const getFavoriteAssets = useCallback(() => { + const favoriteAssets = JSON.parse(localStorage.getItem(FAVORITE_ASSETS) || '[]') + const assets = ASSETS.map((asset) => ({ + ...asset, + isFavorite: favoriteAssets.includes(asset.denom), + })).sort((a, b) => { + if (a.isFavorite && !b.isFavorite) return -1 + if (!a.isFavorite && b.isFavorite) return 1 + return 0 + }) + + setAssets(assets) + }, []) + + useEffect(() => { + getFavoriteAssets() + window.addEventListener('storage', getFavoriteAssets) + + return () => { + window.removeEventListener('storage', getFavoriteAssets) + } + }, [getFavoriteAssets]) + + return assets +} diff --git a/src/hooks/useFilteredAssets.ts b/src/hooks/useFilteredAssets.ts new file mode 100644 index 00000000..2bf1ec18 --- /dev/null +++ b/src/hooks/useFilteredAssets.ts @@ -0,0 +1,29 @@ +import { useCallback, useMemo, useState } from 'react' + +import useAssets from 'hooks/useAssets' + +export default function useFilteredAssets() { + const [searchString, setSearchString] = useState('') + + const allAssets = useAssets() + + const assets = useMemo( + () => + allAssets.filter( + (asset) => + asset.denom.toLocaleLowerCase().includes(searchString.toLowerCase()) || + asset.symbol.toLocaleLowerCase().includes(searchString.toLowerCase()) || + asset.name.toLocaleLowerCase().includes(searchString.toLowerCase()), + ), + [searchString, allAssets], + ) + + const onChangeSearch = useCallback( + (string: string) => { + setSearchString(string) + }, + [setSearchString], + ) + + return { assets, searchString, onChangeSearch } +} diff --git a/src/hooks/useToggle.tsx b/src/hooks/useToggle.tsx index ff7197ed..57923d21 100644 --- a/src/hooks/useToggle.tsx +++ b/src/hooks/useToggle.tsx @@ -1,14 +1,14 @@ -import { useState } from 'react' +import { useCallback, useState } from 'react' export default function useToggle( defaultValue?: boolean, ): [boolean, (isToggled?: boolean) => void] { const [toggle, setToggle] = useState(defaultValue ?? false) - function handleToggle(isToggled?: boolean) { + const handleToggle = useCallback((isToggled?: boolean) => { if (isToggled !== undefined) return setToggle(isToggled) - return setToggle(!toggle) - } + return setToggle((isToggled) => !isToggled) + }, []) return [toggle, handleToggle] } diff --git a/src/pages/TradePage.tsx b/src/pages/TradePage.tsx index 939e9653..d3ba4a12 100644 --- a/src/pages/TradePage.tsx +++ b/src/pages/TradePage.tsx @@ -4,7 +4,7 @@ import TradingView from 'components/Trade/TradingView' export default function TradePage() { return ( -
+
diff --git a/src/types/interfaces/asset.d.ts b/src/types/interfaces/asset.d.ts index def93ee0..a20c7229 100644 --- a/src/types/interfaces/asset.d.ts +++ b/src/types/interfaces/asset.d.ts @@ -14,6 +14,7 @@ interface Asset { isMarket: boolean isDisplayCurrency?: boolean isStable?: boolean + isFavorite?: boolean } interface OtherAsset extends Omit { diff --git a/tailwind.config.js b/tailwind.config.js index ccf1ea56..005ad064 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -157,6 +157,10 @@ module.exports = { }, minHeight: { 3: '12px', + 5: '20px', + 8: '32px', + 10: '40px', + 14: '56px', }, maxWidth: { content: '1024px',