Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add limit order view UI #8020

Merged
merged 17 commits into from
Oct 29, 2024
Merged
Show file tree
Hide file tree
Changes from 14 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions src/assets/translations/en/main.json
Original file line number Diff line number Diff line change
Expand Up @@ -2690,5 +2690,19 @@
"noActiveProposals": "No active proposals available",
"noClosedProposals": "No closed proposals available"
}
},
"limitOrders": {
"listTitle": "Limit Orders",
"limitPrice": "Limit Price",
"expiresIn": "Expires In",
"filled": "Filled",
"openOrders": "Open Orders",
"orderHistory": "Order History",
"status": {
"open": "Open",
"filled": "Filled",
"cancelled": "Cancelled",
"expired": "Expired"
}
}
}
4 changes: 2 additions & 2 deletions src/components/AssetHeader/WatchAssetButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,9 @@ export const WatchAssetButton: React.FC<WatchAssetButtonProps> = ({ assetId, ...
alignItems='center'
minWidth='auto'
borderRadius='full'
bg='var(--chakra-colors-background-button-secondary-base)'
bg='background.button.secondary.base'
// eslint-disable-next-line react-memo/require-usememo
_hover={{ bg: 'var(--chakra-colors-background-button-secondary-hover)' }}
_hover={{ bg: 'background.button.secondary.hover' }}
0xApotheosis marked this conversation as resolved.
Show resolved Hide resolved
p={2}
ml={2}
aria-label='favorite asset'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ export const LimitOrder = ({ isCompact, tradeInputRef, onChangeTab }: LimitOrder
return (
<MemoryRouter initialEntries={LimitOrderRouteEntries} initialIndex={0}>
<Switch location={location}>
<Box flex={1} width='full' maxWidth='500px'>
<Box flex={1} width='full'>
<Route
key={LimitOrderRoutePaths.Input}
path={LimitOrderRoutePaths.Input}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import type { CardProps } from '@chakra-ui/react'
import {
Card,
CardBody,
CardHeader,
Tab,
TabList,
TabPanel,
TabPanels,
Tabs,
} from '@chakra-ui/react'
import { foxAssetId } from '@shapeshiftoss/caip'
import type { FC } from 'react'
import { usdcAssetId } from 'test/mocks/accounts'
import { Text } from 'components/Text'

import { LimitOrderCard } from './components/LimitOrderCard'
import { LimitOrderStatus } from './types'

type LimitOrderListProps = {
isLoading: boolean
} & CardProps

const textColorBaseProps = {
color: 'text.base',
}

export const LimitOrderList: FC<LimitOrderListProps> = ({ isLoading, ...cardProps }) => {
0xApotheosis marked this conversation as resolved.
Show resolved Hide resolved
// FIXME: Use real data
const MockOpenOrderCard = () => (
<LimitOrderCard
id='1'
sellAmount={7000000}
buyAmount={159517.575}
buyAssetId={usdcAssetId}
sellAssetId={foxAssetId}
expiry={7}
filledDecimalPercentage={0.0888}
status={LimitOrderStatus.Open}
/>
)

const MockHistoryOrderCard = () => (
<LimitOrderCard
id='2'
sellAmount={5000000}
buyAmount={120000.0}
buyAssetId={usdcAssetId}
sellAssetId={foxAssetId}
expiry={0}
filledDecimalPercentage={1.0}
status={LimitOrderStatus.Filled}
/>
)

return (
<Card {...cardProps}>
<CardHeader px={6} pt={4}>
<Tabs variant='unstyled'>
<TabList gap={4}>
<Tab
p={0}
fontSize='md'
fontWeight='bold'
color='text.subtle'
_selected={textColorBaseProps}
>
<Text translation='limitOrders.openOrders' />
</Tab>
<Tab
p={0}
fontSize='md'
fontWeight='bold'
color='text.subtle'
_selected={textColorBaseProps}
>
<Text translation='limitOrders.orderHistory' />
</Tab>
</TabList>

<TabPanels>
<TabPanel px={0}>
<CardBody px={0} overflowY='auto' flex='1 1 auto'>
{Array.from({ length: 3 }).map((_, index) => (
<MockOpenOrderCard key={index} />
))}
</CardBody>
</TabPanel>

<TabPanel px={0}>
<CardBody px={0} overflowY='auto' flex='1 1 auto'>
{Array.from({ length: 2 }).map((_, index) => (
<MockHistoryOrderCard key={index} />
))}
</CardBody>
</TabPanel>
</TabPanels>
</Tabs>
</CardHeader>
</Card>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import type { CardProps } from '@chakra-ui/react'

import { HorizontalCollapse } from '../../TradeInput/components/HorizontalCollapse'
import { LimitOrderList } from '../LimitOrderList'

export type CollapsibleLimitOrderListProps = {
isOpen: boolean
width: string | number
height: string | number
isLoading: boolean
ml: CardProps['ml']
}

export const CollapsibleLimitOrderList: React.FC<CollapsibleLimitOrderListProps> = ({
isOpen,
width,
height,
isLoading,
ml,
}) => {
return (
<HorizontalCollapse isOpen={isOpen} width={width} height={height}>
<LimitOrderList isLoading={isLoading} ml={ml} height={height} />
</HorizontalCollapse>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
import { Box, Button, Center, Flex, Progress, Tag } from '@chakra-ui/react'
import type { AssetId } from '@shapeshiftoss/caip'
import { ethAssetId } from '@shapeshiftoss/caip'
import { type FC, useCallback, useMemo } from 'react'
import { useTranslate } from 'react-polyglot'
import { Amount } from 'components/Amount/Amount'
import { AssetIconWithBadge } from 'components/AssetIconWithBadge'
import { SwapBoldIcon } from 'components/Icons/SwapBold'
import { RawText, Text } from 'components/Text'
import { selectAssetById } from 'state/slices/selectors'
import { useAppSelector } from 'state/store'

import { LimitOrderStatus } from '../types'

export interface LimitOrderCardProps {
id: string
buyAmount: number
sellAmount: number
buyAssetId: AssetId
sellAssetId: AssetId
expiry: number
filledDecimalPercentage: number
status: LimitOrderStatus
}

const buttonBgHover = {
bg: 'var(--chakra-colors-background-button-secondary-hover)',
0xApotheosis marked this conversation as resolved.
Show resolved Hide resolved
}

export const LimitOrderCard: FC<LimitOrderCardProps> = ({
id,
buyAmount,
sellAmount,
buyAssetId,
sellAssetId,
expiry,
filledDecimalPercentage,
status,
}) => {
const translate = useTranslate()

const buyAsset = useAppSelector(state => selectAssetById(state, buyAssetId))
const sellAsset = useAppSelector(state => selectAssetById(state, sellAssetId))

const handleCancel = useCallback(() => {
console.log(`Cancel limit order ${id}`)
}, [id])

const formattedPercentage = (filledDecimalPercentage * 100).toFixed(2)
const limitPrice = (buyAmount / sellAmount).toFixed(6)

const tagColorScheme = useMemo(() => {
switch (status) {
case LimitOrderStatus.Open:
return 'blue'
case LimitOrderStatus.Filled:
return 'green'
case LimitOrderStatus.Cancelled:
return 'red'
case LimitOrderStatus.Expired:
return 'yellow'
default:
return 'gray'
}
}, [status])

const barColorScheme = useMemo(() => {
switch (status) {
case LimitOrderStatus.Open:
case LimitOrderStatus.Filled:
return 'green'
case LimitOrderStatus.Cancelled:
return 'red'
case LimitOrderStatus.Expired:
return 'yellow'
default:
return 'gray'
}
}, [status])

if (!buyAsset || !sellAsset) return null

return (
<Box
key={id}
borderRadius='2xl'
p={4}
width='100%'
border='1px solid'
borderColor='whiteAlpha.100'
mb={4}
>
<Flex direction='column' gap={4}>
{/* Asset amounts row */}
<Flex justify='space-between' align='flex-start'>
<Flex>
<AssetIconWithBadge size='lg' assetId={ethAssetId}>
<Center borderRadius='full' boxSize='100%' bg='purple.500'>
<SwapBoldIcon boxSize='100%' />
</Center>
</AssetIconWithBadge>
<Flex direction='column' align='flex-start' ml={4}>
<Amount.Crypto
value={sellAmount.toString()}
symbol={sellAsset.symbol}
color='gray.500'
fontSize='xl'
/>
<Amount.Crypto value={buyAmount.toString()} symbol={buyAsset.symbol} fontSize='xl' />
</Flex>
</Flex>
<Tag colorScheme={tagColorScheme}>{translate(`limitOrders.status.${status}`)}</Tag>
</Flex>

{/* Price row */}
<Flex justify='space-between' align='center'>
<Text color='gray.500' translation='limitOrders.limitPrice' />
<Flex justify='flex-end'>
<RawText mr={1}>{`1 ${sellAsset.symbol} =`}</RawText>
<Amount.Crypto value={limitPrice} symbol={buyAsset.symbol} />
</Flex>
</Flex>

{/* Expiry row */}
<Flex justify='space-between' align='center'>
<Text color='gray.500' translation='limitOrders.expiresIn' />
<RawText>{`${expiry} days`}</RawText>
woodenfurniture marked this conversation as resolved.
Show resolved Hide resolved
</Flex>

{/* Filled row */}
<Flex justify='space-between' align='center'>
<Text color='gray.500' translation='limitOrders.filled' />
<Flex align='center' gap={4} width='60%'>
<Progress
value={filledDecimalPercentage * 100}
width='100%'
borderRadius='full'
colorScheme={barColorScheme}
/>
<RawText>{`${formattedPercentage}%`}</RawText>
</Flex>
</Flex>

{/* Cancel button */}
{status === LimitOrderStatus.Open && (
<Button
colorScheme='red'
width='100%'
mt={2}
color='red.500'
onClick={handleCancel}
bg='var(--chakra-colors-background-button-secondary-base)'
0xApotheosis marked this conversation as resolved.
Show resolved Hide resolved
_hover={buttonBgHover}
>
<Text translation='common.cancel' />
</Button>
)}
</Flex>
</Box>
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import { SharedTradeInput } from '../../SharedTradeInput/SharedTradeInput'
import { SharedTradeInputBody } from '../../SharedTradeInput/SharedTradeInputBody'
import { SharedTradeInputFooter } from '../../SharedTradeInput/SharedTradeInputFooter/SharedTradeInputFooter'
import { LimitOrderRoutePaths } from '../types'
import { CollapsibleLimitOrderList } from './CollapsibleLimitOrderList'
import { LimitOrderBuyAsset } from './LimitOrderBuyAsset'
import { LimitOrderConfig } from './LimitOrderConfig'

Expand All @@ -41,9 +42,6 @@ type LimitOrderInputProps = {
onChangeTab: (newTab: TradeInputTab) => void
}

// TODO: Implement me
const CollapsibleLimitOrderList = () => <></>

export const LimitOrderInput = ({
isCompact,
tradeInputRef,
Expand Down Expand Up @@ -276,7 +274,7 @@ export const LimitOrderInput = ({
<SharedTradeInput
bodyContent={bodyContent}
footerContent={footerContent}
hasUserEnteredAmount={hasUserEnteredAmount}
shouldOpenSideComponent={true}
headerRightContent={headerRightContent}
isCompact={isCompact}
isLoading={isLoading}
Expand Down
7 changes: 7 additions & 0 deletions src/components/MultiHopTrade/components/LimitOrder/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,10 @@ export enum LimitOrderRoutePaths {
Confirm = '/trade/limit-order/confirm',
Status = '/trade/limit-order/status',
}

export enum LimitOrderStatus {
Open = 'open',
Filled = 'filled',
Cancelled = 'cancelled',
Expired = 'expired',
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import { useSharedHeight } from '../TradeInput/hooks/useSharedHeight'
type SharedTradeInputProps = {
bodyContent: JSX.Element
footerContent: JSX.Element
hasUserEnteredAmount: boolean
0xApotheosis marked this conversation as resolved.
Show resolved Hide resolved
shouldOpenSideComponent: boolean
headerRightContent: JSX.Element
isCompact: boolean | undefined
isLoading: boolean
Expand All @@ -23,7 +23,7 @@ type SharedTradeInputProps = {

export const SharedTradeInput: React.FC<SharedTradeInputProps> = ({
bodyContent,
hasUserEnteredAmount,
shouldOpenSideComponent,
headerRightContent,
isCompact,
isLoading,
Expand Down Expand Up @@ -64,7 +64,7 @@ export const SharedTradeInput: React.FC<SharedTradeInputProps> = ({
<WithLazyMount
shouldUse={!isCompact && !isSmallerThanXl}
component={sideComponent}
isOpen={!isCompact && !isSmallerThanXl && hasUserEnteredAmount}
isOpen={!isCompact && !isSmallerThanXl && shouldOpenSideComponent}
isLoading={isLoading}
width={tradeInputRef.current?.offsetWidth ?? 'full'}
height={totalHeight ?? 'full'}
Expand Down
Loading
Loading