From 9c60af0fa333be8e20af6893b2a6517c216be4c9 Mon Sep 17 00:00:00 2001 From: Henry Palacios Date: Mon, 30 May 2022 21:58:19 -0300 Subject: [PATCH] Adding surplus column on tx table (#99) * Adding surplus column on tx table * Use popper tooltip with click instead hover * Fixing onClick on tooltip * Creating BaseIconTooltip * Avoid use tooltip on mobile view * Fix mobile view wrap surplus * Use OrderSurplusTooltipDisplay on OrderUserTable * Hover instead Click, no cast return, amount of smart formatting * Fix row spacing, improve type naming --- src/components/Tooltip.tsx | 96 +++++++++++----- .../orders/OrderSurplusDisplay/index.tsx | 80 +++++++++++--- .../OrderSurplusTooltipStyledByRow.tsx | 25 +++++ .../orders/OrdersUserDetailsTable/index.tsx | 10 +- .../transaction/TransactionTable/index.tsx | 103 ++---------------- .../transaction/TransactionTable/styled.ts | 89 +++++++++++++++ src/hooks/useGetMatchingScreenSize.tsx | 14 +++ src/hooks/useMediaBreakPoint.tsx | 8 ++ src/utils/mediaQueries.ts | 12 +- 9 files changed, 299 insertions(+), 138 deletions(-) create mode 100644 src/components/orders/OrdersUserDetailsTable/OrderSurplusTooltipStyledByRow.tsx create mode 100644 src/components/transaction/TransactionTable/styled.ts create mode 100644 src/hooks/useGetMatchingScreenSize.tsx create mode 100644 src/hooks/useMediaBreakPoint.tsx diff --git a/src/components/Tooltip.tsx b/src/components/Tooltip.tsx index 016ec76f8..c9424f9c7 100644 --- a/src/components/Tooltip.tsx +++ b/src/components/Tooltip.tsx @@ -13,7 +13,7 @@ import Portal from 'components/Portal' // hooks import { usePopperOnClick, usePopperDefault, TOOLTIP_OFFSET } from 'hooks/usePopper' -const QuestionIcon = styled(SVG)` +const CustomSvgIcon = styled(SVG)` width: 1.4rem; height: 1.4rem; fill: ${({ theme }): string => theme.grey}; @@ -200,31 +200,29 @@ export const LongTooltipContainer = styled.div` line-height: 1.4; ` -interface HelpTooltipProps { - tooltip: ReactNode - placement?: Placement - offset?: number -} - -const HelperSpan = styled.span` - cursor: pointer; - transition: color 0.1s; - display: flex; - padding: 0 0.6rem 0 0; - - :hover { - color: #748a47; - } -` - -export const HelpTooltipContainer = styled(LongTooltipContainer)` - font-size: 1.6rem; - font-family: monospace; - padding: 1em; - color: black; -` - -export const HelpTooltip: React.FC = ({ tooltip, placement = 'top', offset }) => { +type RequireAtLeastOne = Pick> & + { + [K in Keys]-?: Required> & Partial>> + }[Keys] + +type BaseTooltipsProps = RequireAtLeastOne< + { + tooltip: ReactNode + placement?: Placement + offset?: number + sourceIconSvg?: string + targetContent?: ReactNode + }, + 'sourceIconSvg' | 'targetContent' +> + +export const BaseIconTooltipOnClick: React.FC = ({ + tooltip, + placement = 'top', + offset, + sourceIconSvg, + targetContent, +}) => { const { targetProps: { ref, onClick }, tooltipProps, @@ -241,9 +239,53 @@ export const HelpTooltip: React.FC = ({ tooltip, placement = ' return ( <> - + {sourceIconSvg ? : targetContent} {tooltip} ) } + +export const BaseIconTooltipOnHover: React.FC = ({ + tooltip, + placement = 'top', + offset, + sourceIconSvg, + targetContent, +}) => { + const { tooltipProps, targetProps } = usePopperDefault(placement, offset) + + return ( + <> + {sourceIconSvg ? : targetContent} + {tooltip} + + ) +} + +type HelpTooltipProps = Omit + +const HelperSpan = styled.span` + cursor: pointer; + transition: color 0.1s; + display: flex; + padding: 0 0.6rem 0 0; + + .default-icon-tooltip { + color: ${({ theme }): string => theme.grey}; + :hover { + color: ${({ theme }): string => theme.white}; + } + } +` + +export const HelpTooltipContainer = styled(LongTooltipContainer)` + font-size: 1.6rem; + font-family: monospace; + padding: 1em; + color: black; +` + +export const HelpTooltip: React.FC = ({ tooltip, placement = 'top', offset }) => { + return +} diff --git a/src/components/orders/OrderSurplusDisplay/index.tsx b/src/components/orders/OrderSurplusDisplay/index.tsx index e42f45eca..754cd4c99 100644 --- a/src/components/orders/OrderSurplusDisplay/index.tsx +++ b/src/components/orders/OrderSurplusDisplay/index.tsx @@ -1,13 +1,23 @@ import React from 'react' -import styled from 'styled-components' +import styled, { useTheme } from 'styled-components' import { Order } from 'api/operator' -import { formatSmart, formatSmartMaxPrecision, safeTokenName } from 'utils' +import { + formatSmart, + formatSmartMaxPrecision, + safeTokenName, + formattingAmountPrecision, + FormatAmountPrecision, +} from 'utils' import { LOW_PRECISION_DECIMALS, PERCENTAGE_PRECISION } from 'apps/explorer/const' +import { BaseIconTooltipOnHover } from 'components/Tooltip' +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' +import { faArrowAltCircleUp as faIcon } from '@fortawesome/free-regular-svg-icons' const Wrapper = styled.div` + display: flex; & > * { margin-right: 0.25rem; } @@ -26,12 +36,11 @@ const Surplus = styled.span` // opacity: 0.5; // ` -export type Props = { order: Order } +export type Props = { order: Order; amountSmartFormatting?: boolean } & React.HTMLAttributes +type SurplusText = { amount: string; percentage: string; formattedSmartAmount: string } -export function OrderSurplusDisplay(props: Props): JSX.Element | null { - const { - order: { kind, buyToken, sellToken, surplusAmount, surplusPercentage }, - } = props +function useGetSurplus(order: Order): SurplusText | null { + const { kind, buyToken, sellToken, surplusAmount, surplusPercentage } = order const surplusToken = kind === 'buy' ? sellToken : buyToken @@ -47,7 +56,13 @@ export function OrderSurplusDisplay(props: Props): JSX.Element | null { precision: PERCENTAGE_PRECISION, decimals: LOW_PRECISION_DECIMALS, }) - const formattedSurplusAmount = formatSmartMaxPrecision(surplusAmount, surplusToken) + const formattedSurplusAmountMaxPrecision = formatSmartMaxPrecision(surplusAmount, surplusToken) + const formattedSurplusAmount = formattingAmountPrecision( + surplusAmount, + surplusToken, + FormatAmountPrecision.highPrecision, + ) + const tokenSymbol = safeTokenName(surplusToken) // const formattedUsdAmount = formatSmart({ // amount: usdAmount, @@ -55,13 +70,52 @@ export function OrderSurplusDisplay(props: Props): JSX.Element | null { // decimals: LOW_PRECISION_DECIMALS, // }) + return { + amount: `${formattedSurplusAmountMaxPrecision} ${tokenSymbol}`, + formattedSmartAmount: `${formattedSurplusAmount} ${tokenSymbol}`, + percentage: `+${formattedSurplusPercentage}%`, + } +} + +export function OrderSurplusDisplay(props: Props): JSX.Element | null { + const surplus = useGetSurplus(props.order) + + if (!surplus) return null + return ( - - +{formattedSurplusPercentage}% - - {formattedSurplusAmount} {tokenSymbol} - + + {surplus.percentage} + {props.amountSmartFormatting ? surplus.formattedSmartAmount : surplus.amount} {/* (~${formattedUsdAmount}) */} ) } + +const IconWrapper = styled(FontAwesomeIcon)` + padding: 0.6rem; + margin: -0.6rem 0 -0.6rem -0.6rem; + box-sizing: content-box; + + :hover { + cursor: pointer; + } +` + +export function OrderSurplusTooltipDisplay({ order }: Props): JSX.Element | null { + const surplus = useGetSurplus(order) + const theme = useTheme() + + if (!surplus) return null + + return ( + + + {surplus.percentage} + + } + /> + ) +} diff --git a/src/components/orders/OrdersUserDetailsTable/OrderSurplusTooltipStyledByRow.tsx b/src/components/orders/OrdersUserDetailsTable/OrderSurplusTooltipStyledByRow.tsx new file mode 100644 index 000000000..7be10282c --- /dev/null +++ b/src/components/orders/OrdersUserDetailsTable/OrderSurplusTooltipStyledByRow.tsx @@ -0,0 +1,25 @@ +import React from 'react' +import styled from 'styled-components' + +import { media } from 'theme/styles/media' +import { Order } from 'api/operator' +import { useMediaBreakpoint } from 'hooks/useMediaBreakPoint' +import { OrderSurplusTooltipDisplay, OrderSurplusDisplay } from '../OrderSurplusDisplay' + +export const OrderSurplusDisplayStyled = styled(OrderSurplusDisplay)` + ${media.mobile} { + flex-direction: column; + } +` +/** + * Displays surplus amount inside tooltip when display mode has little space to display + */ +export function OrderSurplusDisplayStyledByRow({ order }: { order: Order }): JSX.Element { + const isDesktop = useMediaBreakpoint(['xl', 'lg']) + + if (isDesktop) { + return + } + + return +} diff --git a/src/components/orders/OrdersUserDetailsTable/index.tsx b/src/components/orders/OrdersUserDetailsTable/index.tsx index 01b0608fe..fd69a4c36 100644 --- a/src/components/orders/OrdersUserDetailsTable/index.tsx +++ b/src/components/orders/OrdersUserDetailsTable/index.tsx @@ -22,11 +22,12 @@ import TradeOrderType from 'components/common/TradeOrderType' import { LinkWithPrefixNetwork } from 'components/common/LinkWithPrefixNetwork' import { TextWithTooltip } from 'apps/explorer/components/common/TextWithTooltip' import Spinner from 'components/common/Spinner' +import { OrderSurplusDisplayStyledByRow } from './OrderSurplusTooltipStyledByRow' const Wrapper = styled(StyledUserDetailsTable)` > thead > tr, > tbody > tr { - grid-template-columns: 12rem 7rem repeat(2, minmax(16rem, 1.5fr)) repeat(2, minmax(18rem, 2fr)) 1fr; + grid-template-columns: 12rem 5.5rem repeat(2, minmax(16rem, 1.5fr)) minmax(18rem, 2fr) 9rem minmax(21.6rem, 2fr) 1.18fr; } tr > td { span.span-inside-tooltip { @@ -210,6 +211,12 @@ const RowOrder: React.FC = ({ order, isPriceInverted }) => { {renderSpinnerWhenNoValue(limitPriceSettled) || limitPriceSettled} + + Surplus + + + + Created @@ -267,6 +274,7 @@ const OrdersUserDetailsTable: React.FC = (props) => { Limit price + Surplus Created Status diff --git a/src/components/transaction/TransactionTable/index.tsx b/src/components/transaction/TransactionTable/index.tsx index ccf350aa8..cf1dc3af8 100644 --- a/src/components/transaction/TransactionTable/index.tsx +++ b/src/components/transaction/TransactionTable/index.tsx @@ -1,5 +1,4 @@ import React, { useEffect, useState } from 'react' -import styled from 'styled-components' import { faExchangeAlt, faSpinner } from '@fortawesome/free-solid-svg-icons' import { Order } from 'api/operator' @@ -9,105 +8,18 @@ import { RowWithCopyButton } from 'components/common/RowWithCopyButton' import { getOrderLimitPrice, formatCalculatedPriceToDisplay, formattedAmount, FormatAmountPrecision } from 'utils' import { getShortOrderId } from 'utils/operator' import { HelpTooltip } from 'components/Tooltip' -import StyledUserDetailsTable, { - StyledUserDetailsTableProps, - EmptyItemWrapper, -} from '../../common/StyledUserDetailsTable' +import { StyledUserDetailsTableProps, EmptyItemWrapper } from '../../common/StyledUserDetailsTable' import Icon from 'components/Icon' import TradeOrderType from 'components/common/TradeOrderType' import { LinkWithPrefixNetwork } from 'components/common/LinkWithPrefixNetwork' import { StatusLabel } from 'components/orders/StatusLabel' -import { media } from 'theme/styles/media' import { TextWithTooltip } from 'apps/explorer/components/common/TextWithTooltip' import { TokenDisplay } from 'components/common/TokenDisplay' import { useNetworkId } from 'state/network' import { safeTokenName } from '@gnosis.pm/dex-js' import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' - -const Wrapper = styled(StyledUserDetailsTable)` - > thead > tr, - > tbody > tr { - grid-template-columns: 12rem 7rem repeat(2, minmax(16rem, 1.5fr)) repeat(2, minmax(18rem, 2fr)) 1fr; - } - tr > td { - span.span-inside-tooltip { - display: flex; - flex-direction: row; - flex-wrap: wrap; - img { - padding: 0; - } - } - } - ${media.desktopMediumDown} { - > thead > tr { - display: none; - } - > tbody > tr { - grid-template-columns: none; - border: 0.1rem solid ${({ theme }): string => theme.tableRowBorder}; - box-shadow: 0px 4px 12px ${({ theme }): string => theme.boxShadow}; - border-radius: 6px; - margin-top: 16px; - padding: 12px; - &:hover { - background: none; - backdrop-filter: none; - } - } - tr > td { - display: flex; - flex: 1; - width: 100%; - justify-content: space-between; - margin: 0; - margin-bottom: 18px; - min-height: 32px; - span.span-inside-tooltip { - align-items: flex-end; - flex-direction: column; - img { - margin-left: 0; - } - } - } - .header-value { - flex-wrap: wrap; - text-align: end; - } - .span-copybtn-wrap { - display: flex; - flex-wrap: nowrap; - span { - display: flex; - align-items: center; - } - .copy-text { - margin-left: 5px; - } - } - } - overflow: auto; -` - -const HeaderTitle = styled.span` - display: none; - ${media.desktopMediumDown} { - font-weight: 600; - align-items: center; - display: flex; - margin-right: 3rem; - svg { - margin-left: 5px; - } - } -` -const HeaderValue = styled.span` - ${media.desktopMediumDown} { - flex-wrap: wrap; - text-align: end; - } -` +import { HeaderTitle, HeaderValue, WrapperUserDetailsTable } from './styled' +import { OrderSurplusDisplayStyledByRow } from 'components/orders/OrdersUserDetailsTable/OrderSurplusTooltipStyledByRow' function getLimitPrice(order: Order, isPriceInverted: boolean): string { if (!order.buyToken || !order.sellToken) return '-' @@ -213,6 +125,12 @@ const RowTransaction: React.FC = ({ order, isPriceInverted, invertLimi {renderSpinnerWhenNoValue(limitPriceSettled) || limitPriceSettled} + + Surplus + + + + Created @@ -269,7 +187,7 @@ const TransactionTable: React.FC = (props) => { } return ( - @@ -282,6 +200,7 @@ const TransactionTable: React.FC = (props) => { Limit price + Surplus Created Status diff --git a/src/components/transaction/TransactionTable/styled.ts b/src/components/transaction/TransactionTable/styled.ts new file mode 100644 index 000000000..71a012303 --- /dev/null +++ b/src/components/transaction/TransactionTable/styled.ts @@ -0,0 +1,89 @@ +import styled from 'styled-components' + +import { media } from 'theme/styles/media' +import StyledUserDetailsTable from '../../common/StyledUserDetailsTable' + +export const WrapperUserDetailsTable = styled(StyledUserDetailsTable)` + > thead > tr, + > tbody > tr { + grid-template-columns: 12rem 5.5rem repeat(2, minmax(16rem, 1.5fr)) minmax(18rem, 2fr) 9rem minmax(21.6rem, 2fr) 1fr; + } + tr > td { + span.span-inside-tooltip { + display: flex; + flex-direction: row; + flex-wrap: wrap; + img { + padding: 0; + } + } + } + ${media.desktopMediumDown} { + > thead > tr { + display: none; + } + > tbody > tr { + grid-template-columns: none; + border: 0.1rem solid ${({ theme }): string => theme.tableRowBorder}; + box-shadow: 0px 4px 12px ${({ theme }): string => theme.boxShadow}; + border-radius: 6px; + margin-top: 16px; + padding: 12px; + &:hover { + background: none; + backdrop-filter: none; + } + } + tr > td { + display: flex; + flex: 1; + width: 100%; + justify-content: space-between; + margin: 0; + margin-bottom: 18px; + min-height: 32px; + span.span-inside-tooltip { + align-items: flex-end; + flex-direction: column; + img { + margin-left: 0; + } + } + } + .header-value { + flex-wrap: wrap; + text-align: end; + } + .span-copybtn-wrap { + display: flex; + flex-wrap: nowrap; + span { + display: flex; + align-items: center; + } + .copy-text { + margin-left: 5px; + } + } + } + overflow: auto; +` + +export const HeaderTitle = styled.span` + display: none; + ${media.desktopMediumDown} { + font-weight: 600; + align-items: center; + display: flex; + margin-right: 3rem; + svg { + margin-left: 5px; + } + } +` +export const HeaderValue = styled.span` + ${media.desktopMediumDown} { + flex-wrap: wrap; + text-align: end; + } +` diff --git a/src/hooks/useGetMatchingScreenSize.tsx b/src/hooks/useGetMatchingScreenSize.tsx new file mode 100644 index 000000000..6ffc3b4c9 --- /dev/null +++ b/src/hooks/useGetMatchingScreenSize.tsx @@ -0,0 +1,14 @@ +import { useEffect, useState } from 'react' + +import { getMatchingScreenSize, subscribeToScreenSizeChange, Breakpoints } from 'utils/mediaQueries' + +export function useGetMatchingScreenSize(): Breakpoints { + const [resolution, setResolution] = useState(getMatchingScreenSize()) + + useEffect(() => { + const mediaQuery = subscribeToScreenSizeChange(() => setResolution(getMatchingScreenSize())) + return (): void => mediaQuery() + }, []) + + return resolution +} diff --git a/src/hooks/useMediaBreakPoint.tsx b/src/hooks/useMediaBreakPoint.tsx new file mode 100644 index 000000000..b4fb3a04b --- /dev/null +++ b/src/hooks/useMediaBreakPoint.tsx @@ -0,0 +1,8 @@ +import { useGetMatchingScreenSize } from 'hooks/useGetMatchingScreenSize' +import { Breakpoints } from 'utils/mediaQueries' + +export function useMediaBreakpoint(breakpoints: Breakpoints[]): boolean { + const resolution = useGetMatchingScreenSize() + + return breakpoints.includes(resolution) +} diff --git a/src/utils/mediaQueries.ts b/src/utils/mediaQueries.ts index 3a4888efa..6fafb5273 100644 --- a/src/utils/mediaQueries.ts +++ b/src/utils/mediaQueries.ts @@ -1,6 +1,8 @@ import { Command } from 'types' -export const MEDIA_QUERY_MATCHES = [ +export type Breakpoints = 'xl' | 'lg' | 'md' | 'sm' | 'xs' + +export const MEDIA_QUERY_MATCHES: Array<{ name: Breakpoints; query: string }> = [ // must be in descending order for .find to match from largest to smallest // as sm will also match for xl and lg, for example { @@ -22,9 +24,9 @@ export const MEDIA_QUERY_MATCHES = [ // anything smaller -- xs ] -const DEFAULT_QUERY_NAME = 'xs' +const DEFAULT_QUERY_NAME: Breakpoints = 'xs' -export const getMatchingScreenSize = (): string => +export const getMatchingScreenSize = (): Breakpoints => MEDIA_QUERY_MATCHES.find(({ query }) => window.matchMedia(query).matches)?.name || DEFAULT_QUERY_NAME export const MEDIA_QUERIES = MEDIA_QUERY_MATCHES.map(({ query }) => query) @@ -33,7 +35,7 @@ export const MEDIA_QUERY_NAMES = MEDIA_QUERY_MATCHES.map(({ name }) => name).con export const subscribeToScreenSizeChange = (callback: (event: MediaQueryListEvent) => void): Command => { const mediaQueryLists = MEDIA_QUERIES.map((query) => window.matchMedia(query)) - mediaQueryLists.forEach((mql) => mql.addListener(callback)) + mediaQueryLists.forEach((mql) => mql.addEventListener('change', callback)) - return (): void => mediaQueryLists.forEach((mql) => mql.removeListener(callback)) + return (): void => mediaQueryLists.forEach((mql) => mql.removeEventListener('change', callback)) }