From 10a3be710fa7575f76f03b5ec93d87327aa11b19 Mon Sep 17 00:00:00 2001 From: Leandro Boscariol Date: Wed, 24 Jun 2020 09:54:04 -0700 Subject: [PATCH] 1029/styling take 2 (#1096) * Adding breathing room to trades table * 1029/styling take 3 mobile opt (#1098) * extrapolate common CardWidget into new component * add correct data labels to TradeRow * Added CardWidgetWrapper to TradesWidget * address der uebernit, ja wohl * Extracting InnerTradesWidget component for reusability * Fitting `Fills` tab into TradeWidget * Combine limit price and fill price columns * Combine amount and received columns * Adding some space when using double headers on same column * Added some padding to the left of Fills table * Renamed columns Amount=>Sold and Received=>Bought * Move Fills tab to second position * 1029.2/david style take (#1112) * CardTable CSS 1. fix issue with left - right alignment 2. pass left alignment as default, right as custom to Balances 3. min-height and no padding for forced equal header height 4. css cleanup (formatting, lint) 5. less padding trades header * Card default css 1. format css for readability 2. better padding * add isTab prop to set smaller columns dimensions * fixed lowBalance format * 1102/filter fills trades (#1116) * moved common fns to utils 1. filter fns to utils/filter 2. computeMarketProp to utils/display * move DetailedPendingOrder into ExchangeAPI 1. create Market interface which is extended by DetailedAuctionElement, DetailedPendingOrder + Trades * moved types and imported, moved fns out of OrdersWidget into utils * derive market prop in useTrades for later filtering 1. use in TradesWidget * fixed moved type * Filter /Orders by market (#1119) * conputeMarketProp: always return string * filter orders by market * remove Market type * make filterTradesAndOrderFnFactory and new filter fns 1. filterTradesFn 2. filterOrdersFn 3. pass to useDataFilter 4. Orders: make work for both different filters * remove computeMarketProp * computeMarketProp accepts args to create inverse market 1. allow inverse markets to be searched on demand * empty string separator * reverse market computation (#1127) * reverse market computation * Trades: invert price (#1128) * CardRowDrawer classname 1. dont use styled component as class 2. fix padding * Update src/components/Layout/Card/Card.tsx Co-authored-by: Velenir * make CardTable/Widget fit better 1. Balances page: less cluttering, better wrapping format 2. make wrap/unwrap buttons thinner * Orders: better responsive design Co-authored-by: Velenir Co-authored-by: Leandro Boscariol Co-authored-by: David Co-authored-by: Velenir --- src/api/exchange/ExchangeApi.ts | 4 + src/components/DepositWidget/Styled.tsx | 2 +- src/components/DepositWidget/index.tsx | 172 +++------------- src/components/Layout/Card/Card.tsx | 186 ++++++++++++++++-- .../OrdersWidget/OrderRow.styled.ts | 1 - .../OrdersWidget/OrdersWidget.styled.ts | 4 + src/components/OrdersWidget/index.tsx | 124 ++++++++---- src/components/TradesWidget/TradeRow.tsx | 54 ++--- src/components/TradesWidget/index.tsx | 139 ++++++++----- src/components/WrapEtherBtn.tsx | 9 +- src/hooks/useOrders.ts | 5 +- src/hooks/usePendingOrders.ts | 6 +- src/utils/display.tsx | 31 +++ src/utils/filter.ts | 33 ++++ 14 files changed, 484 insertions(+), 286 deletions(-) create mode 100644 src/utils/filter.ts diff --git a/src/api/exchange/ExchangeApi.ts b/src/api/exchange/ExchangeApi.ts index 263d91b2f..b74b1801d 100644 --- a/src/api/exchange/ExchangeApi.ts +++ b/src/api/exchange/ExchangeApi.ts @@ -105,6 +105,10 @@ export interface ExchangeApi extends DepositApi { cancelOrders(params: CancelOrdersParams): Promise } +export interface DetailedPendingOrder extends DetailedAuctionElement { + txHash?: string +} + export interface DetailedAuctionElement extends AuctionElement { buyToken: TokenDetails | null sellToken: TokenDetails | null diff --git a/src/components/DepositWidget/Styled.tsx b/src/components/DepositWidget/Styled.tsx index 1393cf91d..1826c3b60 100644 --- a/src/components/DepositWidget/Styled.tsx +++ b/src/components/DepositWidget/Styled.tsx @@ -40,7 +40,7 @@ export const TokenRow = styled.tr` border-radius: 2.4rem; height: 2.4rem; width: 2.4rem; - margin: 0 0 0 1rem; + margin-left: 0.5rem; display: flex; align-items: center; justify-content: center; diff --git a/src/components/DepositWidget/index.tsx b/src/components/DepositWidget/index.tsx index 5b89a6cce..9eeb97975 100644 --- a/src/components/DepositWidget/index.tsx +++ b/src/components/DepositWidget/index.tsx @@ -7,13 +7,10 @@ import BN from 'bn.js' import { logDebug, getToken } from 'utils' import { ZERO, MEDIA } from 'const' import { TokenBalanceDetails } from 'types' -import { LocalTokensState } from 'reducers-actions/localTokens' -import { TokenLocalState } from 'reducers-actions' // Components -import { CardTable } from 'components/Layout/Card' +import { CardTable, CardWidgetWrapper } from 'components/Layout/Card' import ErrorMsg from 'components/ErrorMsg' -import Widget from 'components/Layout/Widget' import FilterTools from 'components/FilterTools' // DepositWidget: subcomponents @@ -30,154 +27,43 @@ import useGlobalState from 'hooks/useGlobalState' import { useEthBalances } from 'hooks/useEthBalance' import useDataFilter from 'hooks/useDataFilter' +// Reducer/Actions +import { LocalTokensState } from 'reducers-actions/localTokens' +import { TokenLocalState } from 'reducers-actions' + interface WithdrawState { amount: BN tokenAddress: string } -const BalancesWidget = styled(Widget)` - display: flex; - flex-flow: column nowrap; - width: auto; - padding: 0 0 2.4rem; - min-width: 85rem; - max-width: 140rem; - background: var(--color-background-pageWrapper); - box-shadow: 0 -1rem 4rem 0 rgba(0, 0, 0, 0.05), rgba(0, 0, 0, 0.02) 0 0.276726rem 0.221381rem 0, - rgba(0, 0, 0, 0.027) 0 0.666501rem 0.532008rem 0, rgba(0, 0, 0, 0.035) 0 1.25216rem 1.0172rem 0, - rgba(0, 0, 0, 0.043) 0 2.23363rem 1.7869rem 0, rgba(0, 0, 0, 0.05) 0 4.17776rem 3.34221rem 0, - rgba(0, 0, 0, 0.07) 0 10rem 8rem 0; - border-radius: 0.6rem; - margin: 0 auto; - min-height: 54rem; - font-size: 1.6rem; - line-height: 1; - justify-content: flex-start; - - @media ${MEDIA.tablet} { - min-width: 100vw; - min-width: calc(100vw - 4.8rem); - width: 100%; - max-width: 100%; - } - - @media ${MEDIA.mobile} { - max-width: 100%; - min-width: initial; - width: 100%; - - > div { - flex-flow: row wrap; +const BalancesWidget = styled(CardWidgetWrapper)` + ${CardTable} { + > thead, + > tbody { + > tr:not(.cardRowDrawer) { + > td, + > th { + justify-content: flex-end; + text-align: right; + } } } - - ${CardTable}.balancesOverview { - display: flex; - flex-flow: column nowrap; - width: auto; - order: 2; - } - - ${CardTable}.balancesOverview > tbody { - font-size: 1.3rem; - line-height: 1; - - @media ${MEDIA.mobile} { - display: flex; - flex-flow: column wrap; - width: 100%; - } - } - - ${CardTable}.balancesOverview > thead { - background: var(--color-background); - border-radius: 0.6rem; - - @media ${MEDIA.mobile} { - display: none; - } - } - - ${CardTable}.balancesOverview > thead > tr:not([class^="Card__CardRowDrawer"]), - ${CardTable}.balancesOverview > tbody > tr:not([class^="Card__CardRowDrawer"]) { - grid-template-columns: repeat(auto-fit, minmax(5rem, 1fr)); - text-align: right; - padding: 0.8rem; - margin: 0; - justify-content: flex-start; - - @media ${MEDIA.mobile} { - padding: 1.6rem 0.8rem; - display: table; - flex-flow: column wrap; - width: 100%; - border-bottom: 0.2rem solid rgba(159, 180, 201, 0.5); - } - } - - ${CardTable}.balancesOverview > thead > tr:not([class^="Card__CardRowDrawer"]) > th { - font-size: 1.1rem; - color: var(--color-text-primary); - letter-spacing: 0; - text-align: right; - padding: 0.8rem; - text-transform: uppercase; - - &:first-of-type { - text-align: left; - } - } - - ${CardTable}.balancesOverview > tbody > tr:not([class^="Card__CardRowDrawer"]) > td { - display: flex; - flex-flow: row wrap; - align-items: center; - padding: 0 0.5rem; - text-align: right; - justify-content: flex-end; - word-break: break-all; - white-space: normal; - @media ${MEDIA.mobile} { - width: 100%; - border-bottom: 0.1rem solid rgba(0, 0, 0, 0.14); - padding: 1rem 0.5rem; - flex-flow: row nowrap; - - &:last-of-type { - border: 0; + > tbody > tr:not(.cardRowDrawer) > td { + &[data-label='Token'] { + font-family: var(--font-default); + letter-spacing: 0; + line-height: 1.2; + flex-flow: row nowrap; } - } - - &:first-of-type { - text-align: left; - justify-content: flex-start; - } - - &[data-label='Token'] { - font-family: var(--font-default); - letter-spacing: 0; - line-height: 1.2; - flex-flow: row nowrap; - } - &[data-label='Token'] > div > b { - display: block; - color: var(--color-text-primary); - } + &[data-label='Token'] > div { + word-break: break-word; - &::before { - @media ${MEDIA.mobile} { - content: attr(data-label); - margin-right: auto; - font-weight: var(--font-weight-bold); - text-transform: uppercase; - font-size: 1rem; - font-family: var(--font-default); - letter-spacing: 0; - white-space: nowrap; - padding: 0 0.5rem 0 0; - color: var(--color-text-primary); + > b { + display: block; + color: var(--color-text-primary); + } } } } @@ -342,7 +228,7 @@ const BalancesDisplay: React.FC = ({ const { modalProps, toggleModal } = useManageTokens() return ( - + = ({ {error ? ( ) : ( - + Token diff --git a/src/components/Layout/Card/Card.tsx b/src/components/Layout/Card/Card.tsx index bdcbc9424..0d3633413 100644 --- a/src/components/Layout/Card/Card.tsx +++ b/src/components/Layout/Card/Card.tsx @@ -1,5 +1,7 @@ import React from 'react' import styled from 'styled-components' + +import Widget from '../Widget' import { MEDIA } from 'const' const CardRowDrawer = styled.tr` @@ -100,7 +102,7 @@ export const CardDrawer = React.forwardRef ref, ) { return ( - + × {children} @@ -116,6 +118,7 @@ export const CardTable = styled.table<{ $rows?: string $gap?: string $rowSeparation?: string + $padding?: string $align?: string $justify?: string @@ -143,7 +146,7 @@ export const CardTable = styled.table<{ } > thead, tbody { - > tr:not(${CardRowDrawer}) { + > tr:not(.cardRowDrawer) { position: relative; display: grid; grid-template-columns: ${({ $columns }): string => $columns || `repeat(auto-fit, minmax(3rem, 1fr))`}; @@ -156,11 +159,15 @@ export const CardTable = styled.table<{ border-bottom: .1rem solid rgba(159,180,201,0.50); border-radius: 0; + min-height: 4rem; + // How much separation between ROWS margin: ${({ $rowSeparation = '1rem' }): string => `${$rowSeparation} 0`}; text-align: center; transition: all 0.2s ease-in-out; + padding: ${({ $padding = '0' }): string => `${$padding}`}; + &:hover { background: var(--color-background-row-hover); } @@ -177,18 +184,15 @@ export const CardTable = styled.table<{ // Separation between CELLS > th, > td { + display: flex; + align-items: center; + text-overflow: ellipsis; overflow: hidden; text-align: left; } } - } - - .lowBalance { - color: #B27800; - display: block; - > img {margin: 0 0 0 .25rem;} - } + } // Table Header > thead { @@ -199,9 +203,9 @@ export const CardTable = styled.table<{ > th { color: inherit; - line-height: 1; + line-height: 1.2; font-size: 1.1rem; - padding: 1.3rem 0; + height: 4rem; &.sortable { cursor: pointer; @@ -231,12 +235,29 @@ export const CardTable = styled.table<{ } tbody { - > tr:not(${CardRowDrawer}) { + > tr:not(.cardRowDrawer) { > td { &.cardOpener { display: none; } + + // td.status + &.status { + flex-flow: column; + align-items: flex-start; + + > .lowBalance { + color: #B27800; + display: flex; + margin: 0.2rem 0; + font-size: smaller; + + > img { + margin: 0 0 0.2rem 0.45rem; + } + } + } } } @@ -245,3 +266,144 @@ export const CardTable = styled.table<{ // Top level custom CSS ${({ $webCSS }): string | undefined => $webCSS} ` + +export const CardWidgetWrapper = styled(Widget)<{ $columns?: string }>` + display: flex; + flex-flow: column nowrap; + justify-content: flex-start; + + width: auto; + margin: 0 auto; + padding: 0 0 2.4rem; + min-height: 54rem; + min-width: 85rem; + max-width: 140rem; + + background: var(--color-background-pageWrapper); + box-shadow: 0 -1rem 4rem 0 rgba(0, 0, 0, 0.05), rgba(0, 0, 0, 0.02) 0 0.276726rem 0.221381rem 0, + rgba(0, 0, 0, 0.027) 0 0.666501rem 0.532008rem 0, rgba(0, 0, 0, 0.035) 0 1.25216rem 1.0172rem 0, + rgba(0, 0, 0, 0.043) 0 2.23363rem 1.7869rem 0, rgba(0, 0, 0, 0.05) 0 4.17776rem 3.34221rem 0, + rgba(0, 0, 0, 0.07) 0 10rem 8rem 0; + + border-radius: 0.6rem; + font-size: 1.6rem; + line-height: 1; + + @media ${MEDIA.tablet} { + min-width: 100vw; + min-width: calc(100vw - 4.8rem); + width: 100%; + max-width: 100%; + } + + @media ${MEDIA.mobile} { + max-width: 100%; + min-width: initial; + width: 100%; + + > div { + flex-flow: row wrap; + } + } + + ${CardTable} { + display: flex; + flex-flow: column nowrap; + width: auto; + + ///////////////////// + // TABLE HEADERS + ///////////////////// + > thead { + background: var(--color-background); + border-radius: 0.6rem; + + @media ${MEDIA.mobile} { + display: none; + } + + > tr:not(.cardRowDrawer) > th { + font-size: 1.1rem; + color: var(--color-text-primary); + letter-spacing: 0; + text-transform: uppercase; + } + } + + ///////////////////// + // TABLE BODY + ///////////////////// + > tbody { + font-size: 1.3rem; + line-height: 1; + + @media ${MEDIA.mobile} { + display: flex; + flex-flow: column wrap; + width: 100%; + } + + > tr:not(.cardRowDrawer) > td { + display: flex; + flex-flow: row wrap; + align-items: center; + word-break: break-all; + white-space: normal; + + @media ${MEDIA.mobile} { + width: 100%; + border-bottom: 0.1rem solid rgba(0, 0, 0, 0.14); + padding: 1rem 0.5rem; + flex-flow: row nowrap; + + &:last-of-type { + border: 0; + } + } + + &::before { + @media ${MEDIA.mobile} { + content: attr(data-label); + margin-right: auto; + font-weight: var(--font-weight-bold); + text-transform: uppercase; + font-size: 1rem; + font-family: var(--font-default); + letter-spacing: 0; + white-space: nowrap; + padding: 0 0.5rem 0 0; + color: var(--color-text-primary); + } + } + } + } + + ///////////////////// + // ALL TABLE ROWS + ///////////////////// + > thead > tr:not(.cardRowDrawer), + > tbody > tr:not(.cardRowDrawer) { + ${({ $columns }): string => ($columns ? `grid-template-columns: ${$columns}` : '')}; + text-align: left; + padding: 0.8rem 1.6rem; + margin: 0; + justify-content: flex-end; + + @media ${MEDIA.mobile} { + padding: 1.6rem 0.8rem; + display: table; + flex-flow: column wrap; + width: 100%; + border-bottom: 0.2rem solid rgba(159, 180, 201, 0.5); + } + + > td, + > th { + &:first-of-type { + text-align: left; + justify-content: flex-start; + } + } + } + } +` diff --git a/src/components/OrdersWidget/OrderRow.styled.ts b/src/components/OrdersWidget/OrderRow.styled.ts index 2e91f587f..c5fcc2e3c 100644 --- a/src/components/OrdersWidget/OrderRow.styled.ts +++ b/src/components/OrdersWidget/OrderRow.styled.ts @@ -2,7 +2,6 @@ import styled from 'styled-components' export const OrderRowWrapper = styled.tr<{ $color?: string; $open?: boolean }>` color: ${({ $color = '' }): string => $color}; - min-height: 4rem; &.pending { background: rgba(33, 141, 255, 0.1); diff --git a/src/components/OrdersWidget/OrdersWidget.styled.ts b/src/components/OrdersWidget/OrdersWidget.styled.ts index 6d0189385..0b5aa3b3e 100644 --- a/src/components/OrdersWidget/OrdersWidget.styled.ts +++ b/src/components/OrdersWidget/OrdersWidget.styled.ts @@ -19,6 +19,10 @@ export const OrdersWrapper = styled.div` max-width: 140rem; /* ====================================================================== */ + @media ${MEDIA.tabletSmall} { + min-width: ${MEDIA.smallScreen}; + } + @media ${MEDIA.mobile} { max-width: 100%; min-width: initial; diff --git a/src/components/OrdersWidget/index.tsx b/src/components/OrdersWidget/index.tsx index 2ea5a8708..f5007a0c3 100644 --- a/src/components/OrdersWidget/index.tsx +++ b/src/components/OrdersWidget/index.tsx @@ -9,20 +9,23 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' // Const and utils import { isOrderActive, isPendingOrderActive } from 'utils' import { DEFAULT_ORDERS_SORTABLE_TOPIC } from 'const' +import { filterTradesFn, filterOrdersFn } from 'utils/filter' // Hooks import { useOrders } from 'hooks/useOrders' +import { useTrades } from 'hooks/useTrades' import useSafeState from 'hooks/useSafeState' import useDataFilter from 'hooks/useDataFilter' import useSortByTopic from 'hooks/useSortByTopic' import { useWalletConnection } from 'hooks/useWalletConnection' // Api -import { DetailedAuctionElement } from 'api/exchange/ExchangeApi' +import { DetailedAuctionElement, DetailedPendingOrder, Trade } from 'api/exchange/ExchangeApi' // Components import { ConnectWalletBanner } from 'components/ConnectWalletBanner' import { CardTable } from 'components/Layout/Card' +import { InnerTradesWidget } from 'components/TradesWidget' import FilterTools from 'components/FilterTools' // OrderWidget @@ -30,11 +33,7 @@ import { useDeleteOrders } from 'components/OrdersWidget/useDeleteOrders' import OrderRow from 'components/OrdersWidget/OrderRow' import { OrdersWrapper, ButtonWithIcon, OrdersForm } from 'components/OrdersWidget/OrdersWidget.styled' -// Types/misc -import { DetailedPendingOrder } from 'hooks/usePendingOrders' -import { TokenDetails } from 'types' - -type OrderTabs = 'active' | 'liquidity' | 'closed' +type OrderTabs = 'active' | 'liquidity' | 'closed' | 'fills' interface ShowOrdersButtonProps { type: OrderTabs @@ -49,7 +48,7 @@ const ShowOrdersButton: React.FC = ({ type, isActive, cou ) -type FilteredOrdersStateKeys = OrderTabs +type FilteredOrdersStateKeys = Exclude type FilteredOrdersState = { [key in FilteredOrdersStateKeys]: { orders: DetailedAuctionElement[] @@ -87,25 +86,6 @@ function classifyOrders( }) } -function checkTokenAgainstSearch(token: TokenDetails | null, searchText: string): boolean { - if (!token) return false - return ( - token?.symbol?.toLowerCase().includes(searchText) || - token?.name?.toLowerCase().includes(searchText) || - token?.address.toLowerCase().includes(searchText) - ) -} - -const filterOrdersFn = (searchTxt: string) => ({ id, buyToken, sellToken }: DetailedAuctionElement): boolean | null => { - if (searchTxt === '') return null - - return ( - !!id.includes(searchTxt) || - checkTokenAgainstSearch(buyToken, searchTxt) || - checkTokenAgainstSearch(sellToken, searchTxt) - ) -} - const compareFnFactory = (topic: TopicNames, asc: boolean) => ( lhs: DetailedAuctionElement, rhs: DetailedAuctionElement, @@ -130,12 +110,15 @@ const OrdersWidget: React.FC = ({ isWidget = false }) => { const [classifiedOrders, setClassifiedOrders] = useSafeState(emptyState()) const [selectedTab, setSelectedTab] = useSafeState('active') + // Subscribe to trade events + const trades = useTrades() + // syntactic sugar const { displayedOrders, displayedPendingOrders, markedForDeletion } = useMemo( () => ({ - displayedOrders: classifiedOrders[selectedTab].orders, - displayedPendingOrders: classifiedOrders[selectedTab].pendingOrders, - markedForDeletion: classifiedOrders[selectedTab].markedForDeletion, + displayedOrders: selectedTab === 'fills' ? [] : classifiedOrders[selectedTab].orders, + displayedPendingOrders: selectedTab === 'fills' ? [] : classifiedOrders[selectedTab].pendingOrders, + markedForDeletion: selectedTab === 'fills' ? new Set() : classifiedOrders[selectedTab].markedForDeletion, }), [classifiedOrders, selectedTab], ) @@ -172,6 +155,7 @@ const OrdersWidget: React.FC = ({ isWidget = false }) => { const ordersCount = displayedOrders.length + displayedPendingOrders.length const noOrders = allOrders.length === 0 + const noTrades = trades.length === 0 const overBalanceOrders = useMemo( () => @@ -224,7 +208,9 @@ const OrdersWidget: React.FC = ({ isWidget = false }) => { // ========================================= const toggleMarkForDeletionFactory = useCallback( - (orderId: string, selectedTab: OrderTabs): (() => void) => (): void => + (orderId: string, selectedTab: OrderTabs): (() => void) => (): void => { + if (selectedTab === 'fills') return + setClassifiedOrders(curr => { const state = emptyState() @@ -239,12 +225,15 @@ const OrdersWidget: React.FC = ({ isWidget = false }) => { state[selectedTab].markedForDeletion = newSet return state - }), + }) + }, [setClassifiedOrders], ) const toggleSelectAll = useCallback( - ({ currentTarget: { checked } }: React.SyntheticEvent) => + ({ currentTarget: { checked } }: React.SyntheticEvent) => { + if (selectedTab === 'fills') return + setClassifiedOrders(curr => { const state = emptyState() @@ -256,7 +245,8 @@ const OrdersWidget: React.FC = ({ isWidget = false }) => { : new Set() return state - }), + }) + }, [classifiedOrders, selectedTab, setClassifiedOrders], ) @@ -266,6 +256,8 @@ const OrdersWidget: React.FC = ({ isWidget = false }) => { async (event: React.SyntheticEvent): Promise => { event.preventDefault() + if (selectedTab === 'fills') return + const success = await deleteOrders(Array.from(markedForDeletion)) if (success) { @@ -295,27 +287,60 @@ const OrdersWidget: React.FC = ({ isWidget = false }) => { [deleteOrders, forceOrdersRefresh, markedForDeletion, selectedTab, setClassifiedOrders], ) + const { + filteredData: filteredTrades, + search: tradesSearch, + handlers: { handleSearch: handleTradesSearch }, + } = useDataFilter({ + data: trades, + filterFnFactory: filterTradesFn, + }) + + const { handleTabSpecificSearch, tabSpecficSearch, tabSpecificResultName, tabSpecificDataLength } = useMemo( + () => ({ + handleTabSpecificSearch: (e: React.ChangeEvent): void => + selectedTab === 'fills' ? handleTradesSearch(e) : handleBothOrderTypeSearch(e), + tabSpecficSearch: selectedTab === 'fills' ? tradesSearch : search, + tabSpecificResultName: selectedTab === 'fills' ? 'trades' : 'orders', + tabSpecificDataLength: + selectedTab === 'fills' + ? filteredTrades.length + : displayedPendingOrders.length + filteredAndSortedOrders.length, + }), + [ + selectedTab, + tradesSearch, + search, + filteredTrades.length, + displayedPendingOrders.length, + filteredAndSortedOrders.length, + handleTradesSearch, + handleBothOrderTypeSearch, + ], + ) + return ( {!isConnected ? ( ) : ( - noOrders && ( + noOrders && + noTrades && (

It appears you haven't placed any order yet.
Create one!

) )} - {!noOrders && networkId && ( + {(!noOrders || !noTrades) && networkId && (
{/* implement later when better data concerning order state and can be saved to global state */} + {/* ORDERS TABS: ACTIVE/FILLS/LIQUIDITY/CLOSED */}
= ({ isWidget = false }) => { count={classifiedOrders.active.orders.length + classifiedOrders.active.pendingOrders.length} onClick={setSelectedTabFactory('active')} /> + = ({ isWidget = false }) => { />
+ {/* DELETE ORDERS ROW */}
@@ -353,11 +386,18 @@ const OrdersWidget: React.FC = ({ isWidget = false }) => { {['active', 'liquidity'].includes(selectedTab) ? 'Cancel' : 'Delete'} {markedForDeletion.size} orders
- {ordersCount > 0 ? ( + {/* FILLS AKA TRADES */} + {selectedTab === 'fills' ? ( +
+ +
+ ) : ordersCount > 0 ? ( + // ACTIVE / LIQUIDITY / CLOSED ORDERS
diff --git a/src/components/TradesWidget/TradeRow.tsx b/src/components/TradesWidget/TradeRow.tsx index a6ff1492f..806368031 100644 --- a/src/components/TradesWidget/TradeRow.tsx +++ b/src/components/TradesWidget/TradeRow.tsx @@ -1,10 +1,8 @@ import React, { useMemo } from 'react' import styled from 'styled-components' import BigNumber from 'bignumber.js' -import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' -import { faUndo } from '@fortawesome/free-solid-svg-icons' -import { formatPrice, formatSmart, formatAmountFull, DEFAULT_PRECISION } from '@gnosis.pm/dex-js' +import { formatPrice, formatSmart, formatAmountFull, invertPrice, DEFAULT_PRECISION } from '@gnosis.pm/dex-js' import { Trade, TradeType } from 'api/exchange/ExchangeApi' @@ -86,6 +84,10 @@ export const TradeRow: React.FC = params => { } = trade const buyTokenDecimals = buyToken.decimals || DEFAULT_PRECISION const sellTokenDecimals = sellToken.decimals || DEFAULT_PRECISION + // Calculate the inverse price - just make sure Limit Price is defined and isn't ZERO + // don't want none of that divide by zero and destroy the world stuff + const invertedLimitPrice = limitPrice && !limitPrice.isZero() && invertPrice(limitPrice) + const invertedFillPrice = invertPrice(fillPrice) const typeColumnTitle = useMemo(() => { switch (type) { @@ -115,38 +117,38 @@ export const TradeRow: React.FC = params => { {formatDateFromBatchId(batchId, { strict: true })} - + {displayTokenSymbolOrLink(buyToken)}/{displayTokenSymbolOrLink(sellToken)} - - {limitPrice ? formatPrice(limitPrice) : 'N/A'} + + {invertedLimitPrice ? formatPrice(invertedLimitPrice) : 'N/A'} +
+ {formatPrice(invertedFillPrice)} - - {formatPrice(fillPrice)} - - + {formatSmart({ amount: sellAmount, precision: sellTokenDecimals })} {displayTokenSymbolOrLink(sellToken)} - - +
{formatSmart({ amount: buyAmount, precision: buyTokenDecimals })} {displayTokenSymbolOrLink(buyToken)} {type} - - {/* TODO: remove icon and filter out reverted trades */} - {' '} - {trade.revertId && ( - - )} + + ) diff --git a/src/components/TradesWidget/index.tsx b/src/components/TradesWidget/index.tsx index efe05b926..0fde8c964 100644 --- a/src/components/TradesWidget/index.tsx +++ b/src/components/TradesWidget/index.tsx @@ -6,25 +6,33 @@ import styled from 'styled-components' import { formatPrice, TokenDetails, formatAmount } from '@gnosis.pm/dex-js' -import { ContentPage } from 'components/Layout/PageWrapper' -import { CardTable } from 'components/Layout/Card' +import FilterTools from 'components/FilterTools' +import { CardTable, CardWidgetWrapper } from 'components/Layout/Card' import { ConnectWalletBanner } from 'components/ConnectWalletBanner' import { FileDownloaderLink } from 'components/FileDownloaderLink' +import { TradeRow } from 'components/TradesWidget/TradeRow' import { useWalletConnection } from 'hooks/useWalletConnection' import { useTrades } from 'hooks/useTrades' +import useDataFilter from 'hooks/useDataFilter' import { Trade } from 'api/exchange/ExchangeApi' import { toCsv, CsvColumns } from 'utils/csv' +import { filterTradesFn } from 'utils/filter' -import { TradeRow } from 'components/TradesWidget/TradeRow' import { getNetworkFromId, isTradeSettled, isTradeReverted } from 'utils' const CsvButtonContainer = styled.div` display: flex; justify-content: space-around; align-items: center; + width: 100%; +` + +const SplitHeaderTitle = styled.div` + display: flex; + flex-flow: column; ` function symbolOrAddress(token: TokenDetails): string { @@ -60,14 +68,14 @@ function csvTransformer(trade: Trade): CsvColumns { SellTokenAddress: sellToken.address, LimitPrice: limitPrice ? formatPrice({ price: limitPrice, decimals: 8 }) : 'N/A', FillPrice: formatPrice({ price: fillPrice, decimals: 8 }), - Amount: formatAmount({ + Sold: formatAmount({ amount: sellAmount, precision: sellToken.decimals as number, decimals: sellToken.decimals, thousandSeparator: false, isLocaleAware: false, }), - Received: formatAmount({ + Bought: formatAmount({ amount: buyAmount, precision: buyToken.decimals as number, decimals: sellToken.decimals, @@ -84,9 +92,15 @@ function csvTransformer(trade: Trade): CsvColumns { const CSV_FILE_OPTIONS = { type: 'text/csv;charset=utf-8;' } -const Trades: React.FC = () => { - const { networkId, userAddress, isConnected } = useWalletConnection() - const trades = useTrades() +interface InnerTradesWidgetProps { + trades: Trade[] + isTab?: boolean +} + +export const InnerTradesWidget: React.FC = props => { + const { isTab, trades } = props + + const { networkId, userAddress } = useWalletConnection() const filteredTrades = useMemo( () => trades.filter(trade => trade && isTradeSettled(trade) && !isTradeReverted(trade)), @@ -107,47 +121,80 @@ const Trades: React.FC = () => { [networkId, userAddress], ) + return ( + + + + Date + Market + + + Limit Price / + Fill Price + + + + + Sold / + Bought + + + Type + + + Tx + + {trades.length > 0 && ( + + + + )} + + + + + + {filteredTrades.map(trade => ( + + ))} + + + ) +} + +export const TradesWidget: React.FC = () => { + const { isConnected } = useWalletConnection() + const trades = useTrades() + + const { + filteredData, + search, + handlers: { handleSearch }, + } = useDataFilter({ + data: trades, + filterFnFactory: filterTradesFn, + }) + return !isConnected ? ( ) : ( - - - - - Date - Market - - Limit
- Price - - - Fill
- Price - - Amount - Received - Type - - - Tx - - {trades.length > 0 && ( - - - - )} - - - - - - {filteredTrades.map(trade => ( - - ))} - -
-
+ + + + ) } -export default Trades +export default TradesWidget diff --git a/src/components/WrapEtherBtn.tsx b/src/components/WrapEtherBtn.tsx index 68189ff9d..510efacd4 100644 --- a/src/components/WrapEtherBtn.tsx +++ b/src/components/WrapEtherBtn.tsx @@ -354,14 +354,7 @@ const WrapUnwrapEtherBtn: React.FC = (props: WrapUnwrap return ( <> - + {loading && } {label || title} diff --git a/src/hooks/useOrders.ts b/src/hooks/useOrders.ts index b32329468..2d800370d 100644 --- a/src/hooks/useOrders.ts +++ b/src/hooks/useOrders.ts @@ -9,12 +9,12 @@ import { overwriteOrders, updateOffset, updateOrders } from 'reducers-actions/or import useSafeState from './useSafeState' import useGlobalState from './useGlobalState' import { useWalletConnection } from './useWalletConnection' -import usePendingOrders, { DetailedPendingOrder } from './usePendingOrders' +import usePendingOrders from './usePendingOrders' import { useCheckWhenTimeRemainingInBatch } from './useTimeRemainingInBatch' // Constants/Types import { REFRESH_WHEN_SECONDS_LEFT } from 'const' -import { DetailedAuctionElement } from 'api/exchange/ExchangeApi' +import { DetailedAuctionElement, DetailedPendingOrder } from 'api/exchange/ExchangeApi' interface Result { orders: DetailedAuctionElement[] @@ -70,6 +70,7 @@ export function useOrders(): Result { getTokenFromExchangeById({ tokenId: order.sellTokenId, networkId }), getTokenFromExchangeById({ tokenId: order.buyTokenId, networkId }), ]) + return { ...order, sellToken, diff --git a/src/hooks/usePendingOrders.ts b/src/hooks/usePendingOrders.ts index 6eef9374a..342b1adb4 100644 --- a/src/hooks/usePendingOrders.ts +++ b/src/hooks/usePendingOrders.ts @@ -9,7 +9,7 @@ import { useWalletConnection } from './useWalletConnection' import { removePendingOrdersAction } from 'reducers-actions/pendingOrders' // Constants/Types/Misc. import { EMPTY_ARRAY } from 'const' -import { DetailedAuctionElement, AuctionElement } from 'api/exchange/ExchangeApi' +import { AuctionElement, DetailedPendingOrder } from 'api/exchange/ExchangeApi' import { getTokenFromExchangeById } from 'services' async function getDetailedPendingOrders({ @@ -37,10 +37,6 @@ async function getDetailedPendingOrders({ return setFn(detailedOrders) } -export interface DetailedPendingOrder extends DetailedAuctionElement { - txHash?: string -} - function usePendingOrders(): DetailedPendingOrder[] { const { blockNumber, userAddress, networkId } = useWalletConnection() diff --git a/src/utils/display.tsx b/src/utils/display.tsx index 1029106f1..9f0c78877 100644 --- a/src/utils/display.tsx +++ b/src/utils/display.tsx @@ -9,3 +9,34 @@ export function displayTokenSymbolOrLink(token: TokenDetails): React.ReactNode | } return displayName } + +/** + * computeMarketProp + * @description returns array of potentially accepted market names by:: BUYTOKEN SELLTOKEN + * @param { sellToken, buyToken, acceptedSeparators: string[] } + */ +export function computeMarketProp({ + sellToken, + buyToken, + acceptedSeparators = ['-', '/', ''], + inverseMarket = false, +}: { + sellToken: TokenDetails + buyToken: TokenDetails + acceptedSeparators?: string[] + inverseMarket?: boolean +}): string[] { + const buyTokenFormatted = safeTokenName(buyToken).toLowerCase() + const sellTokenFormatted = safeTokenName(sellToken).toLowerCase() + + // BUYTOKEN/SELLTOKEN + const marketList = acceptedSeparators.map(sep => `${buyTokenFormatted}${sep}${sellTokenFormatted}`) + + if (inverseMarket) { + // SELLTOKEN/BUYTOKEN + const inverseMarketList = acceptedSeparators.map(sep => `${sellTokenFormatted}${sep}${buyTokenFormatted}`) + return marketList.concat(inverseMarketList) + } + + return marketList +} diff --git a/src/utils/filter.ts b/src/utils/filter.ts new file mode 100644 index 000000000..87e3d3ce4 --- /dev/null +++ b/src/utils/filter.ts @@ -0,0 +1,33 @@ +import { TokenDetails } from 'types' +import { DetailedAuctionElement, Trade } from 'api/exchange/ExchangeApi' +import { computeMarketProp } from './display' + +export function checkTokenAgainstSearch(token: TokenDetails | null, searchText: string): boolean { + if (!token) return false + return ( + token?.symbol?.toLowerCase().includes(searchText) || + token?.name?.toLowerCase().includes(searchText) || + token?.address.toLowerCase().includes(searchText) + ) +} + +const filterTradesAndOrdersFnFactory = < + T extends { id: string; buyToken: TokenDetails | null; sellToken: TokenDetails | null } +>( + includeInverseMarket?: boolean, +) => (searchTxt: string) => ({ id, buyToken, sellToken }: T): boolean | null => { + if (searchTxt === '') return null + + const market = + sellToken && buyToken && computeMarketProp({ sellToken, buyToken, inverseMarket: includeInverseMarket }) + + return ( + !!id.includes(searchTxt) || + (market && !!market.includes(searchTxt)) || + checkTokenAgainstSearch(buyToken, searchTxt) || + checkTokenAgainstSearch(sellToken, searchTxt) + ) +} + +export const filterOrdersFn = filterTradesAndOrdersFnFactory(true) +export const filterTradesFn = filterTradesAndOrdersFnFactory()