mango-ui-v2/components/Orderbook.tsx

422 lines
13 KiB
TypeScript

import React, { useRef, useEffect, useState } from 'react'
import styled from '@emotion/styled'
import { css, keyframes } from '@emotion/react'
import useInterval from '../hooks/useInterval'
import usePrevious from '../hooks/usePrevious'
import { isEqual, getDecimalCount } from '../utils/'
import {
ArrowUpIcon,
ArrowDownIcon,
SwitchHorizontalIcon,
} from '@heroicons/react/solid'
import useMarkPrice from '../hooks/useMarkPrice'
import useOrderbook from '../hooks/useOrderbook'
import useMarket from '../hooks/useMarket'
import { ElementTitle } from './styles'
import useMangoStore from '../stores/useMangoStore'
import Tooltip from './Tooltip'
import FloatingElement from './FloatingElement'
const Line = styled.div<any>`
text-align: ${(props) => (props.invert ? 'left' : 'right')};
height: 100%;
filter: opacity(40%);
${(props) => props['data-width'] && `width: ${props['data-width']};`}
`
const fadeIn = keyframes`
from {
opacity: 0;
}
to {
opacity: 1;
}
`
const FlipCard = styled.div`
background-color: transparent;
height: 100%;
perspective: 1000px;
`
const FlipCardInner = styled.div<any>`
position: relative;
width: 100%;
height: 100%;
text-align: center;
transition: transform 0.8s ease-out;
transform-style: preserve-3d;
transform: ${({ flip }) => (flip ? 'rotateY(0deg)' : 'rotateY(180deg)')};
`
const FlipCardFront = styled.div`
position: absolute;
width: 100%;
height: 100%;
`
const FlipCardBack = styled.div`
position: absolute;
width: 100%;
height: 100%;
transform: rotateY(180deg);
`
const StyledFloatingElement = styled(FloatingElement)`
animation: ${css`
${fadeIn} 1s linear
`};
`
const getCumulativeOrderbookSide = (
orders,
totalSize,
depth,
backwards = false
) => {
let cumulative = orders
.slice(0, depth)
.reduce((cumulative, [price, size], i) => {
const cumulativeSize = (cumulative[i - 1]?.cumulativeSize || 0) + size
cumulative.push({
price,
size,
cumulativeSize,
sizePercent: Math.round((cumulativeSize / (totalSize || 1)) * 100),
})
return cumulative
}, [])
if (backwards) {
cumulative = cumulative.reverse()
}
return cumulative
}
export default function Orderbook({ depth = 8 }) {
const markPrice = useMarkPrice()
const [orderbook] = useOrderbook()
const { baseCurrency, quoteCurrency } = useMarket()
const currentOrderbookData = useRef(null)
const lastOrderbookData = useRef(null)
const [orderbookData, setOrderbookData] = useState(null)
const [defaultLayout, setDefaultLayout] = useState(true)
useInterval(() => {
if (
!currentOrderbookData.current ||
JSON.stringify(currentOrderbookData.current) !==
JSON.stringify(lastOrderbookData.current)
) {
const bids = orderbook?.bids || []
const asks = orderbook?.asks || []
const sum = (total, [, size], index) =>
index < depth ? total + size : total
const totalSize = bids.reduce(sum, 0) + asks.reduce(sum, 0)
const bidsToDisplay = getCumulativeOrderbookSide(
bids,
totalSize,
depth,
false
)
const asksToDisplay = defaultLayout
? getCumulativeOrderbookSide(asks, totalSize, depth, false)
: getCumulativeOrderbookSide(asks, totalSize, depth, true)
currentOrderbookData.current = {
bids: orderbook?.bids,
asks: orderbook?.asks,
}
if (bidsToDisplay[0] && asksToDisplay[0]) {
const bid = bidsToDisplay[0].price
const ask = defaultLayout
? asksToDisplay[0].price
: asksToDisplay[7].price
const spread = ask - bid
const spreadPercentage = (spread / ask) * 100
setOrderbookData({
bids: bidsToDisplay,
asks: asksToDisplay,
spread: spread,
spreadPercentage: spreadPercentage,
})
}
}
}, 250)
useEffect(() => {
lastOrderbookData.current = {
bids: orderbook?.bids,
asks: orderbook?.asks,
}
}, [orderbook])
const handleLayoutChange = () => {
setDefaultLayout(!defaultLayout)
setOrderbookData((prevState) => ({
...orderbookData,
asks: prevState.asks.reverse(),
}))
}
return (
<>
<FlipCard>
<FlipCardInner flip={defaultLayout}>
{defaultLayout ? (
<FlipCardFront>
<StyledFloatingElement>
<div className="flex items-center justify-between pb-2.5">
<div className="w-8 h-8" />
<ElementTitle noMarignBottom>Orderbook</ElementTitle>
<div className="flex relative">
<Tooltip content={'Switch Layout'} className="text-xs py-1">
<button
onClick={handleLayoutChange}
className="flex items-center justify-center rounded-full bg-th-bkg-3 w-8 h-8 hover:text-th-primary focus:outline-none"
>
<SwitchHorizontalIcon className="w-5 h-5" />
</button>
</Tooltip>
</div>
</div>
<MarkPriceComponent markPrice={markPrice} />
<div
className={`text-th-fgd-4 flex justify-between mb-2 text-xs`}
>
<div className={`text-left`}>Size ({baseCurrency})</div>
<div className={`text-center`}>Price ({quoteCurrency})</div>
<div className={`text-right`}>Size ({baseCurrency})</div>
</div>
<div className="flex">
<div className="w-1/2">
{orderbookData?.bids.map(({ price, size, sizePercent }) => (
<OrderbookRow
key={price + ''}
price={price}
size={size}
side="buy"
sizePercent={sizePercent}
/>
))}
</div>
<div className="w-1/2">
{orderbookData?.asks.map(({ price, size, sizePercent }) => (
<OrderbookRow
invert
key={price + ''}
price={price}
size={size}
side="sell"
sizePercent={sizePercent}
/>
))}
</div>
</div>
<div className="flex justify-between bg-th-bkg-1 p-2 mt-4 rounded-md text-xs">
<div className="text-th-fgd-3">Spread</div>
<div className="text-th-fgd-1">
{orderbookData?.spread.toFixed(2)}
</div>
<div className="text-th-fgd-1">
{orderbookData?.spreadPercentage.toFixed(2)}%
</div>
</div>
</StyledFloatingElement>
</FlipCardFront>
) : (
<FlipCardBack>
<StyledFloatingElement>
<div className="flex items-center justify-between pb-2.5">
<div className="w-8 h-8" />
<ElementTitle noMarignBottom>Orderbook</ElementTitle>
<div className="flex relative">
<Tooltip content={'Switch Layout'} className="text-xs py-1">
<button
onClick={handleLayoutChange}
className="flex items-center justify-center rounded-full bg-th-bkg-3 w-8 h-8 hover:text-th-primary focus:outline-none"
>
<SwitchHorizontalIcon className="w-5 h-5" />
</button>
</Tooltip>
</div>
</div>
<MarkPriceComponent markPrice={markPrice} />
<div className={`text-th-fgd-4 flex justify-between mb-2`}>
<div className={`text-left text-xs`}>
Size ({baseCurrency})
</div>
<div className={`text-right text-xs`}>
Price ({quoteCurrency})
</div>
</div>
{orderbookData?.asks.map(({ price, size, sizePercent }) => (
<OrderbookRow
key={price + ''}
price={price}
size={size}
side="sell"
sizePercent={sizePercent}
/>
))}
<div className="flex justify-between bg-th-bkg-1 p-2 my-2 rounded-md text-xs">
<div className="text-th-fgd-3">Spread</div>
<div className="text-th-fgd-1">
{orderbookData?.spread.toFixed(2)}
</div>
<div className="text-th-fgd-1">
{orderbookData?.spreadPercentage.toFixed(2)}%
</div>
</div>
{orderbookData?.bids.map(({ price, size, sizePercent }) => (
<OrderbookRow
key={price + ''}
price={price}
size={size}
side="buy"
sizePercent={sizePercent}
/>
))}
</StyledFloatingElement>
</FlipCardBack>
)}
</FlipCardInner>
</FlipCard>
</>
)
}
const OrderbookRow = React.memo<any>(
({ side, price, size, sizePercent, invert }) => {
const element = useRef(null)
const { market } = useMarket()
const setMangoStore = useMangoStore((s) => s.set)
useEffect(() => {
!element.current?.classList.contains('flash') &&
element.current?.classList.add('flash')
const id = setTimeout(
() =>
element.current?.classList.contains('flash') &&
element.current?.classList.remove('flash'),
250
)
return () => clearTimeout(id)
}, [price, size])
const formattedSize =
market?.minOrderSize && !isNaN(size)
? Number(size).toFixed(getDecimalCount(market.minOrderSize) + 1)
: size
const formattedPrice =
market?.tickSize && !isNaN(price)
? Number(price).toFixed(getDecimalCount(market.tickSize) + 1)
: price
const handlePriceClick = () => {
setMangoStore((state) => {
state.tradeForm.price = price
})
}
const handleSizeClick = () => {
setMangoStore((state) => {
state.tradeForm.baseSize = size
})
}
return (
<div className={`flex text-sm leading-7 justify-between`} ref={element}>
{invert ? (
<>
<div className={`text-left relative flex-1`}>
<Line
invert
data-width={sizePercent + '%'}
side={side}
className={`absolute inset-y-0 left-0 ${
side === 'buy' ? `bg-th-green` : `bg-th-red`
}`}
/>
<div
onClick={handlePriceClick}
className="z-30 relative text-th-fgd-1 px-1"
>
{formattedPrice}
</div>
</div>
<div className={`text-right`} onClick={handleSizeClick}>
{formattedSize}
</div>
</>
) : (
<>
<div
className={`text-left flex-1 text-th-fgd-1`}
onClick={handleSizeClick}
>
{formattedSize}
</div>
<div className={`text-right relative flex-1`}>
<Line
className={`absolute inset-y-0 right-0 ${
side === 'buy' ? `bg-th-green` : `bg-th-red`
}`}
data-width={sizePercent + '%'}
side={side}
/>
<div
className={`z-30 relative text-th-fgd-1 px-1`}
onClick={handlePriceClick}
>
{formattedPrice}
</div>
</div>
</>
)}
</div>
)
},
(prevProps, nextProps) =>
isEqual(prevProps, nextProps, ['price', 'size', 'sizePercent'])
)
const MarkPriceComponent = React.memo<{ markPrice: number }>(
({ markPrice }) => {
const { market } = useMarket()
const previousMarkPrice: number = usePrevious(markPrice)
const formattedMarkPrice =
markPrice &&
market?.tickSize &&
markPrice.toFixed(getDecimalCount(market.tickSize))
return (
<div
className={`flex justify-center items-center font-bold text-lg pb-4 ${
markPrice > previousMarkPrice
? `text-th-green`
: markPrice < previousMarkPrice
? `text-th-red`
: `text-th-fgd-1`
}`}
>
{markPrice > previousMarkPrice && (
<ArrowUpIcon className={`h-5 w-5 mr-1 text-th-green`} />
)}
{markPrice < previousMarkPrice && (
<ArrowDownIcon className={`h-5 w-5 mr-1 text-th-red`} />
)}
{formattedMarkPrice || '----'}
</div>
)
},
(prevProps, nextProps) => isEqual(prevProps, nextProps, ['markPrice'])
)