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` 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` 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 ( <> {defaultLayout ? (
Orderbook
Size ({baseCurrency})
Price ({quoteCurrency})
Size ({baseCurrency})
{orderbookData?.bids.map(({ price, size, sizePercent }) => ( ))}
{orderbookData?.asks.map(({ price, size, sizePercent }) => ( ))}
Spread
{orderbookData?.spread.toFixed(2)}
{orderbookData?.spreadPercentage.toFixed(2)}%
) : (
Orderbook
Size ({baseCurrency})
Price ({quoteCurrency})
{orderbookData?.asks.map(({ price, size, sizePercent }) => ( ))}
Spread
{orderbookData?.spread.toFixed(2)}
{orderbookData?.spreadPercentage.toFixed(2)}%
{orderbookData?.bids.map(({ price, size, sizePercent }) => ( ))} )} ) } const OrderbookRow = React.memo( ({ 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 (
{invert ? ( <>
{formattedPrice}
{formattedSize}
) : ( <>
{formattedSize}
{formattedPrice}
)}
) }, (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 (
previousMarkPrice ? `text-th-green` : markPrice < previousMarkPrice ? `text-th-red` : `text-th-fgd-1` }`} > {markPrice > previousMarkPrice && ( )} {markPrice < previousMarkPrice && ( )} {formattedMarkPrice || '----'}
) }, (prevProps, nextProps) => isEqual(prevProps, nextProps, ['markPrice']) )