Mp 2713 asset selector (#275)
* added dumb asset selector
* fix table layouts borrow and farm
* 🍱 added basic overlay, esc btn
* finish asset selector
* Update tailwind configf to include button styles
This commit is contained in:
parent
789a0d7b47
commit
39e745b210
@ -80,7 +80,7 @@ describe('<Button />', () => {
|
||||
})
|
||||
|
||||
it('should set correct values for progress indicator size', () => {
|
||||
const sizeValues = { small: 10, medium: 12, large: 18 }
|
||||
const sizeValues = { xs: 8, sm: 10, md: 12, lg: 18 }
|
||||
|
||||
Object.entries(sizeValues).forEach(([size, value]) => {
|
||||
const { getByTestId } = render(
|
||||
|
@ -48,7 +48,7 @@ function Content(props: Props) {
|
||||
if (props.type === 'active') {
|
||||
return (
|
||||
<Card
|
||||
className='h-fit w-full bg-white/5'
|
||||
className='mb-4 h-fit w-full bg-white/5'
|
||||
title={props.type === 'active' ? 'Borrowings' : 'Available to borrow'}
|
||||
>
|
||||
<BorrowTable data={assets} />
|
||||
|
43
src/components/Button/EscButton.tsx
Normal file
43
src/components/Button/EscButton.tsx
Normal file
@ -0,0 +1,43 @@
|
||||
import { useCallback, useEffect } from 'react'
|
||||
|
||||
import { Cross } from 'components/Icons'
|
||||
import Button from 'components/Button'
|
||||
import Text from 'components/Text'
|
||||
|
||||
interface Props {
|
||||
enableKeyPress?: boolean
|
||||
onClick: () => void
|
||||
}
|
||||
|
||||
export default function EscButton(props: Props) {
|
||||
const handleEscKey = useCallback(
|
||||
(event: KeyboardEvent) => {
|
||||
if (event.code === 'Escape') {
|
||||
props.onClick()
|
||||
}
|
||||
},
|
||||
[props],
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
if (props.enableKeyPress) {
|
||||
document.addEventListener('keydown', handleEscKey)
|
||||
}
|
||||
return () => {
|
||||
document.removeEventListener('keydown', handleEscKey)
|
||||
}
|
||||
}, [props.onClick, props.enableKeyPress, handleEscKey])
|
||||
|
||||
return (
|
||||
<Button
|
||||
onClick={props.onClick}
|
||||
leftIcon={<Cross />}
|
||||
iconClassName='w-3'
|
||||
color='tertiary'
|
||||
className='h-3'
|
||||
size='xs'
|
||||
>
|
||||
<Text size='2xs'>ESC</Text>
|
||||
</Button>
|
||||
)
|
||||
}
|
@ -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,
|
||||
}
|
||||
|
@ -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<HTMLButtonElement>) => 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 ? (
|
||||
<CircularProgress size={size === 'small' ? 10 : size === 'medium' ? 12 : 18} />
|
||||
<CircularProgress size={circularProgressSize[size]} />
|
||||
) : (
|
||||
<>
|
||||
{leftIcon && <span className={classNames(leftIconClassNames)}>{leftIcon}</span>}
|
||||
@ -119,7 +120,7 @@ const Button = React.forwardRef(function Button(
|
||||
{children && children}
|
||||
{rightIcon && <span className={classNames(rightIconClassNames)}>{rightIcon}</span>}
|
||||
{hasSubmenu && (
|
||||
<span data-testid='button-submenu-indicator' className='ml-2 inline-block w-2.5'>
|
||||
<span data-testid='button-submenu-indicator' className='ml-auto inline-block w-2.5'>
|
||||
<ChevronDown />
|
||||
</span>
|
||||
)}
|
||||
|
@ -42,6 +42,14 @@ function Content(props: Props) {
|
||||
|
||||
if (!vaultsToDisplay.length) return null
|
||||
|
||||
if (props.type === 'deposited') {
|
||||
return (
|
||||
<Card className='mb-4 h-fit w-full bg-white/5' title={'Deposited'}>
|
||||
<VaultTable data={vaultsToDisplay} />
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
return <VaultTable data={vaultsToDisplay} />
|
||||
}
|
||||
|
||||
|
9
src/components/Icons/StarFilled.svg
Normal file
9
src/components/Icons/StarFilled.svg
Normal file
@ -0,0 +1,9 @@
|
||||
<svg viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M7.52193 2.30302C7.67559 1.99173 7.75242 1.83609 7.85672 1.78636C7.94746 1.74309 8.05289 1.74309 8.14363 1.78636C8.24793 1.83609 8.32476 1.99173 8.47842 2.30302L9.9362 5.25634C9.98157 5.34824 10.0042 5.39419 10.0374 5.42986C10.0667 5.46145 10.1019 5.48705 10.141 5.50523C10.1852 5.52576 10.2359 5.53317 10.3373 5.548L13.5982 6.02462C13.9415 6.07481 14.1132 6.0999 14.1927 6.18377C14.2618 6.25674 14.2943 6.35701 14.2812 6.45666C14.266 6.5712 14.1417 6.69227 13.8931 6.9344L11.5345 9.23176C11.4609 9.30338 11.4242 9.33918 11.4004 9.38179C11.3794 9.41951 11.366 9.46096 11.3608 9.50382C11.3549 9.55223 11.3636 9.60281 11.3809 9.70397L11.9375 12.9489C11.9962 13.2911 12.0255 13.4623 11.9704 13.5638C11.9224 13.6522 11.8371 13.7141 11.7382 13.7325C11.6246 13.7535 11.4709 13.6727 11.1636 13.5111L8.24841 11.978C8.15758 11.9303 8.11217 11.9064 8.06432 11.897C8.02196 11.8887 7.97839 11.8887 7.93602 11.897C7.88818 11.9064 7.84276 11.9303 7.75193 11.978L4.83678 13.5111C4.52944 13.6727 4.37577 13.7535 4.26214 13.7325C4.16328 13.7141 4.07798 13.6522 4.02999 13.5638C3.97483 13.4623 4.00418 13.2911 4.06288 12.9489L4.61942 9.70397C4.63677 9.60281 4.64545 9.55223 4.63958 9.50382C4.63438 9.46096 4.6209 9.41951 4.5999 9.38179C4.57618 9.33918 4.53941 9.30337 4.46589 9.23176L2.1072 6.9344C1.8586 6.69227 1.73431 6.5712 1.71918 6.45666C1.70602 6.35701 1.73853 6.25674 1.80766 6.18377C1.88712 6.0999 2.05881 6.07481 2.40219 6.02462L5.66304 5.548C5.76445 5.53317 5.81515 5.52576 5.85931 5.50523C5.89841 5.48705 5.9336 5.46145 5.96295 5.42986C5.9961 5.39419 6.01878 5.34824 6.06415 5.25634L7.52193 2.30302Z" fill="url(#paint0_linear_1694_217638)"/>
|
||||
<defs>
|
||||
<linearGradient id="paint0_linear_1694_217638" x1="19.7824" y1="1.75391" x2="-3.19296" y2="1.7547" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#BA08BD" stop-opacity="0.764896"/>
|
||||
<stop offset="1" stop-color="#FFA0BB" stop-opacity="0.88641"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
After Width: | Height: | Size: 2.0 KiB |
3
src/components/Icons/StarOutlined.svg
Normal file
3
src/components/Icons/StarOutlined.svg
Normal file
@ -0,0 +1,3 @@
|
||||
<svg viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M7.52193 2.30302C7.67559 1.99173 7.75242 1.83609 7.85672 1.78636C7.94746 1.74309 8.05289 1.74309 8.14363 1.78636C8.24793 1.83609 8.32476 1.99173 8.47842 2.30302L9.9362 5.25634C9.98157 5.34824 10.0042 5.39419 10.0374 5.42986C10.0667 5.46145 10.1019 5.48705 10.141 5.50523C10.1852 5.52576 10.2359 5.53317 10.3373 5.548L13.5982 6.02462C13.9415 6.07481 14.1132 6.0999 14.1927 6.18377C14.2618 6.25674 14.2943 6.35701 14.2812 6.45666C14.266 6.5712 14.1417 6.69227 13.8931 6.9344L11.5345 9.23176C11.4609 9.30338 11.4242 9.33918 11.4004 9.38179C11.3794 9.41951 11.366 9.46096 11.3608 9.50382C11.3549 9.55223 11.3636 9.60281 11.3809 9.70397L11.9375 12.9489C11.9962 13.2911 12.0255 13.4623 11.9704 13.5638C11.9224 13.6522 11.8371 13.7141 11.7382 13.7325C11.6246 13.7535 11.4709 13.6727 11.1636 13.5111L8.24841 11.978C8.15758 11.9303 8.11217 11.9064 8.06432 11.897C8.02196 11.8887 7.97839 11.8887 7.93602 11.897C7.88818 11.9064 7.84276 11.9303 7.75193 11.978L4.83678 13.5111C4.52944 13.6727 4.37577 13.7535 4.26214 13.7325C4.16328 13.7141 4.07798 13.6522 4.02999 13.5638C3.97483 13.4623 4.00418 13.2911 4.06288 12.9489L4.61942 9.70397C4.63677 9.60281 4.64545 9.55223 4.63958 9.50382C4.63438 9.46096 4.6209 9.41951 4.5999 9.38179C4.57618 9.33918 4.53941 9.30337 4.46589 9.23176L2.1072 6.9344C1.8586 6.69227 1.73431 6.5712 1.71918 6.45666C1.70602 6.35701 1.73853 6.25674 1.80766 6.18377C1.88712 6.0999 2.05881 6.07481 2.40219 6.02462L5.66304 5.548C5.76445 5.53317 5.81515 5.52576 5.85931 5.50523C5.89841 5.48705 5.9336 5.46145 5.96295 5.42986C5.9961 5.39419 6.01878 5.34824 6.06415 5.25634L7.52193 2.30302Z" stroke="currentColor" stroke-opacity="0.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
After Width: | Height: | Size: 1.7 KiB |
3
src/components/Icons/SwapIcon.svg
Normal file
3
src/components/Icons/SwapIcon.svg
Normal file
@ -0,0 +1,3 @@
|
||||
<svg viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M13.3334 11.3333H2.66675M2.66675 11.3333L5.33341 8.66667M2.66675 11.3333L5.33341 14M2.66675 4.66667H13.3334M13.3334 4.66667L10.6667 2M13.3334 4.66667L10.6667 7.33333" stroke="currentColor" stroke-opacity="0.6" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
After Width: | Height: | Size: 348 B |
@ -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
|
||||
|
@ -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) {
|
||||
>
|
||||
<div className={classNames('flex justify-between', props.headerClassName)}>
|
||||
{props.header}
|
||||
{!props.hideCloseBtn && (
|
||||
<Button onClick={onClose} leftIcon={<Cross />} iconClassName='h-3 w-3' color='tertiary'>
|
||||
<Text size='sm'>ESC</Text>
|
||||
</Button>
|
||||
)}
|
||||
{!props.hideCloseBtn && <EscButton onClick={props.onClose} />}
|
||||
</div>
|
||||
<div className={classNames(props.contentClassName, 'flex-grow')}>
|
||||
{props.children ? props.children : props.content}
|
||||
|
@ -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<HTMLInputElement>) {
|
||||
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}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default forwardRef(SearchBar)
|
||||
|
@ -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 (
|
||||
<Button
|
||||
leftIcon={<AssetImage asset={props.asset} size={16} />}
|
||||
text={props.asset.symbol}
|
||||
color='tertiary'
|
||||
variant='transparent'
|
||||
className='w-full border border-white/20'
|
||||
size='md'
|
||||
hasSubmenu
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
59
src/components/Trade/TradeModule/AssetSelector/AssetItem.tsx
Normal file
59
src/components/Trade/TradeModule/AssetSelector/AssetItem.tsx
Normal file
@ -0,0 +1,59 @@
|
||||
import AssetImage from 'components/AssetImage'
|
||||
import DisplayCurrency from 'components/DisplayCurrency'
|
||||
import { StarFilled, StarOutlined } from 'components/Icons'
|
||||
import Text from 'components/Text'
|
||||
import { FAVORITE_ASSETS } from 'constants/localStore'
|
||||
import { BNCoin } from 'types/classes/BNCoin'
|
||||
import { BN } from 'utils/helpers'
|
||||
|
||||
interface Props {
|
||||
asset: Asset
|
||||
onSelectAsset: (asset: Asset) => void
|
||||
}
|
||||
|
||||
export default function AssetItem(props: Props) {
|
||||
const asset = props.asset
|
||||
|
||||
function handleToggleFavorite(event: React.MouseEvent<HTMLDivElement, MouseEvent>) {
|
||||
event.stopPropagation()
|
||||
const favoriteAssets: string[] = JSON.parse(localStorage.getItem(FAVORITE_ASSETS) || '[]')
|
||||
if (favoriteAssets) {
|
||||
if (favoriteAssets.includes(asset.denom)) {
|
||||
localStorage.setItem(
|
||||
FAVORITE_ASSETS,
|
||||
JSON.stringify(favoriteAssets.filter((item: string) => item !== asset.denom)),
|
||||
)
|
||||
} else {
|
||||
localStorage.setItem(FAVORITE_ASSETS, JSON.stringify([...favoriteAssets, asset.denom]))
|
||||
}
|
||||
window.dispatchEvent(new Event('storage'))
|
||||
}
|
||||
}
|
||||
return (
|
||||
<li className='border-b border-white/10 hover:bg-black/10'>
|
||||
<button
|
||||
onClick={() => props.onSelectAsset(asset)}
|
||||
className='flex w-full items-center justify-between gap-2 p-4'
|
||||
>
|
||||
<div className='flex items-center gap-2'>
|
||||
<div onClick={handleToggleFavorite}>
|
||||
{asset.isFavorite ? <StarFilled width={16} /> : <StarOutlined width={16} />}
|
||||
</div>
|
||||
<AssetImage asset={asset} size={24} />
|
||||
<Text size='sm' className='text-left'>
|
||||
{asset.name}
|
||||
</Text>
|
||||
<div className='rounded-sm bg-white/20 px-[6px] py-[2px]'>
|
||||
<Text size='xs'>{asset.symbol}</Text>
|
||||
</div>
|
||||
</div>
|
||||
<DisplayCurrency
|
||||
className='text-sm'
|
||||
coin={
|
||||
new BNCoin({ denom: asset.denom, amount: BN(1).shiftedBy(asset.decimals).toString() })
|
||||
}
|
||||
/>
|
||||
</button>
|
||||
</li>
|
||||
)
|
||||
}
|
43
src/components/Trade/TradeModule/AssetSelector/AssetList.tsx
Normal file
43
src/components/Trade/TradeModule/AssetSelector/AssetList.tsx
Normal file
@ -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 (
|
||||
<section>
|
||||
<button
|
||||
className='flex w-full items-center justify-between bg-black/20 p-4'
|
||||
onClick={props.toggleOpen}
|
||||
>
|
||||
<Text>{props.type === 'buy' ? 'Buy asset' : 'Sell asset'}</Text>
|
||||
<ChevronDown className={classNames(props.isOpen && '-rotate-180')} />
|
||||
</button>
|
||||
{props.isOpen &&
|
||||
(props.assets.length === 0 ? (
|
||||
<Text size='xs' className='p-4'>
|
||||
No available assets found
|
||||
</Text>
|
||||
) : (
|
||||
<ul>
|
||||
{props.assets.map((asset) => (
|
||||
<AssetItem
|
||||
key={`${props.type}-${asset.symbol}`}
|
||||
asset={asset}
|
||||
onSelectAsset={props.onChangeAsset}
|
||||
/>
|
||||
))}
|
||||
</ul>
|
||||
))}
|
||||
</section>
|
||||
)
|
||||
}
|
@ -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 (
|
||||
<Overlay className='w-full' show={props.state !== 'closed'} setShow={handleClose}>
|
||||
<div className='flex justify-between p-4'>
|
||||
<Text>Select asset</Text>
|
||||
<EscButton onClick={handleClose} enableKeyPress />
|
||||
</div>
|
||||
<Divider />
|
||||
<div className='p-4'>
|
||||
<SearchBar
|
||||
key={props.state}
|
||||
value={searchString}
|
||||
onChange={onChangeSearch}
|
||||
placeholder='Search for e.g. "ETH" or "Ethereum"'
|
||||
autofocus
|
||||
/>
|
||||
</div>
|
||||
<Divider />
|
||||
<AssetList
|
||||
type='buy'
|
||||
assets={buyAssets}
|
||||
isOpen={props.state === 'buy'}
|
||||
toggleOpen={handleToggle}
|
||||
onChangeAsset={onChangeBuyAsset}
|
||||
/>
|
||||
<AssetList
|
||||
type='sell'
|
||||
assets={sellAssets}
|
||||
isOpen={props.state === 'sell'}
|
||||
toggleOpen={handleToggle}
|
||||
onChangeAsset={onChangeSellAsset}
|
||||
/>
|
||||
</Overlay>
|
||||
)
|
||||
}
|
@ -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<OverlayState>('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 (
|
||||
<div className='grid-rows-auto relative grid grid-cols-[1fr_min-content_1fr] gap-y-2 bg-white/5 p-3'>
|
||||
<Text size='sm'>Buy</Text>
|
||||
<Text size='sm' className='col-start-3'>
|
||||
Sell
|
||||
</Text>
|
||||
<AssetButton onClick={() => setOverlayState('buy')} asset={buyAsset} />
|
||||
<button onClick={handleSwapAssets}>
|
||||
<SwapIcon className='mx-2 w-4 place-self-center' />
|
||||
</button>
|
||||
<AssetButton onClick={() => setOverlayState('sell')} asset={sellAsset} />
|
||||
<AssetOverlay
|
||||
state={overlayState}
|
||||
onChangeState={handleChangeState}
|
||||
buyAsset={buyAsset}
|
||||
sellAsset={sellAsset}
|
||||
onChangeBuyAsset={handleChangeBuyAsset}
|
||||
onChangeSellAsset={handleChangeSellAsset}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
@ -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 (
|
||||
<Card
|
||||
className='row-span-2 h-full w-full bg-white/5'
|
||||
title='Trade Module'
|
||||
contentClassName='px-4 py-6'
|
||||
<div
|
||||
className={classNames(
|
||||
'relative isolate max-w-full overflow-hidden rounded-base',
|
||||
'before:content-[" "] before:absolute before:inset-0 before:-z-1 before:rounded-base before:p-[1px] before:border-glas',
|
||||
'row-span-2 h-full',
|
||||
)}
|
||||
>
|
||||
<Suspense fallback={<Fallback />}>
|
||||
<Content />
|
||||
</Suspense>
|
||||
</Card>
|
||||
<AssetSelector />
|
||||
<Divider />
|
||||
</div>
|
||||
)
|
||||
}
|
@ -1,2 +1,3 @@
|
||||
export const DISPLAY_CURRENCY_KEY = 'displayCurrency'
|
||||
export const ENABLE_ANIMATIONS_KEY = 'enableAnimations'
|
||||
export const FAVORITE_ASSETS = 'favoriteAssets'
|
||||
|
33
src/hooks/useAssets.ts
Normal file
33
src/hooks/useAssets.ts
Normal file
@ -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<Asset[]>(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
|
||||
}
|
29
src/hooks/useFilteredAssets.ts
Normal file
29
src/hooks/useFilteredAssets.ts
Normal file
@ -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 }
|
||||
}
|
@ -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<boolean>(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]
|
||||
}
|
||||
|
@ -4,7 +4,7 @@ import TradingView from 'components/Trade/TradingView'
|
||||
|
||||
export default function TradePage() {
|
||||
return (
|
||||
<div className=' grid w-full grid-cols-[346px_auto] gap-4'>
|
||||
<div className='grid h-full w-full grid-cols-[346px_auto] gap-4'>
|
||||
<TradeModule />
|
||||
<TradingView />
|
||||
<OrderBook />
|
||||
|
1
src/types/interfaces/asset.d.ts
vendored
1
src/types/interfaces/asset.d.ts
vendored
@ -14,6 +14,7 @@ interface Asset {
|
||||
isMarket: boolean
|
||||
isDisplayCurrency?: boolean
|
||||
isStable?: boolean
|
||||
isFavorite?: boolean
|
||||
}
|
||||
|
||||
interface OtherAsset extends Omit<Asset, 'symbol'> {
|
||||
|
@ -157,6 +157,10 @@ module.exports = {
|
||||
},
|
||||
minHeight: {
|
||||
3: '12px',
|
||||
5: '20px',
|
||||
8: '32px',
|
||||
10: '40px',
|
||||
14: '56px',
|
||||
},
|
||||
maxWidth: {
|
||||
content: '1024px',
|
||||
|
Loading…
Reference in New Issue
Block a user