From fc7985470a58268ad8d47def528f59a1a397a6b8 Mon Sep 17 00:00:00 2001 From: leifu Date: Thu, 23 Feb 2023 16:42:20 +0200 Subject: [PATCH 1/7] fix: move helper icon to label (#2042) --- .../dashboard/Stake/TradingRewardsTab.tsx | 125 ++++++++++-------- 1 file changed, 68 insertions(+), 57 deletions(-) diff --git a/sections/dashboard/Stake/TradingRewardsTab.tsx b/sections/dashboard/Stake/TradingRewardsTab.tsx index e3e103ab8c..f23640c69e 100644 --- a/sections/dashboard/Stake/TradingRewardsTab.tsx +++ b/sections/dashboard/Stake/TradingRewardsTab.tsx @@ -113,47 +113,50 @@ const TradingRewardsTab: FC = memo( - - - - {t('dashboard.stake.tabs.trading-rewards.future-fee-paid', { - EpochPeriod: period, - })} - - - {formatDollars(futuresFeePaid, { minDecimals: 2 })} - - - -
- {t('dashboard.stake.tabs.trading-rewards.fees-paid', { EpochPeriod: period })} - - {formatDollars(totalFuturesFeePaid, { minDecimals: 2 })} -
- {showEstimatedValue ? ( - <> + {t('dashboard.stake.tabs.trading-rewards.future-fee-paid', { + EpochPeriod: period, + })} - {t('dashboard.stake.tabs.trading-rewards.estimated-rewards')} - - - {truncateNumbers(wei(estimatedReward), 4)} - - + + + {formatDollars(futuresFeePaid, { minDecimals: 2 })} + +
+ + {t('dashboard.stake.tabs.trading-rewards.fees-paid', { EpochPeriod: period })} + + {formatDollars(totalFuturesFeePaid, { minDecimals: 2 })} +
+ {showEstimatedValue ? ( + <> +
+ + {t('dashboard.stake.tabs.trading-rewards.estimated-rewards')} + <CustomStyledTooltip + preset="bottom" + width="260px" + height="auto" + content={t('dashboard.stake.tabs.trading-rewards.estimated-info')} + > + <WithCursor cursor="help"> + <SpacedHelpIcon /> + </WithCursor> + </CustomStyledTooltip> + + + {truncateNumbers(wei(estimatedReward), 4)} + +
{t('dashboard.stake.tabs.trading-rewards.estimated-reward-share', { @@ -180,39 +183,46 @@ const TradingRewardsTab: FC<TradingRewardProps> = memo( <MobileOnlyView> <CardGridContainer> <CardGrid> - <CustomStyledTooltip - width="260px" - height="auto" - left="15px !important" - content={t('dashboard.stake.tabs.trading-rewards.trading-rewards-tooltip')} - > - <WithCursor cursor="help"> - <Title>{t('dashboard.stake.tabs.trading-rewards.future-fee-paid-mobile')} - - {formatDollars(futuresFeePaid, { minDecimals: 2 })} - - - -
- {t('dashboard.stake.tabs.trading-rewards.fees-paid-mobile')} - {formatDollars(totalFuturesFeePaid, { minDecimals: 2 })} -
- {showEstimatedValue ? ( - <> + + {t('dashboard.stake.tabs.trading-rewards.future-fee-paid-mobile')} <CustomStyledTooltip - width="260px" + width="200px" height="auto" - right="0px !important" - content={t('dashboard.stake.tabs.trading-rewards.estimated-info')} + left="-100px !important" + top="-70px !important" + content={t('dashboard.stake.tabs.trading-rewards.trading-rewards-tooltip')} > <WithCursor cursor="help"> - <Title>{t('dashboard.stake.tabs.trading-rewards.estimated-rewards')} - - {truncateNumbers(wei(estimatedReward), 4)} - + + + {formatDollars(futuresFeePaid, { minDecimals: 2 })} +
+
+ {t('dashboard.stake.tabs.trading-rewards.fees-paid-mobile')} + {formatDollars(totalFuturesFeePaid, { minDecimals: 2 })} +
+ {showEstimatedValue ? ( + <> +
+ + {t('dashboard.stake.tabs.trading-rewards.estimated-rewards')} + <CustomStyledTooltip + width="260px" + height="auto" + top="-120px !important" + right="-60px !important" + content={t('dashboard.stake.tabs.trading-rewards.estimated-info')} + > + <WithCursor cursor="help"> + <SpacedHelpIcon /> + </WithCursor> + </CustomStyledTooltip> + + {truncateNumbers(wei(estimatedReward), 4)} +
{t('dashboard.stake.tabs.trading-rewards.estimated-reward-share-mobile', { @@ -243,6 +253,7 @@ const TradingRewardsTab: FC<TradingRewardProps> = memo( const CustomStyledTooltip = styled(Tooltip)` padding: 10px; + white-space: normal; ${media.lessThan('md')` width: 310px; `} From baf0462f6e0f4cacddb998df1a85295ee3a49aca Mon Sep 17 00:00:00 2001 From: Oluwakorede Fashokun <koredefashokun@gmail.com> Date: Fri, 24 Feb 2023 10:52:31 -0500 Subject: [PATCH 2/7] chore: component refactor (#1949) * All pushed changes copied * Button things * Centralize * Init Pill and new number input * Some input changes * More input things * Get rid of CustomInput * Get rid of CustomNumericInput * Switch input style changes * Remove box-shadow * Inputs and perf * Make homepage button large * Fix more input things * Init futures stories and fix final input issue * Colors * New position buttons * More position button cleanup * Selectors * Remove unused styles * Split PositionCard into multiple components * Finish PositionCard * Refactor InfoBox * More changes * Finish badges * Finishing * Stories finally * Add title to PositionCard story * AccountStats * Pill stuff * Start adding positive/negative colors * Enrich documentation * Remove Redux from Storybook * Cursor pointer * Some fixes * Remove redundant PositionCard code * Get rid of DefaultCell * Add isSubItem prop * Fix fonts issue * Reorder and mono * Use NumericValue in PositionCard * Set bold TabButton * Add side to CurrencyPrice * Switch to colored * Add default Body story * Fix isolated margin fee box * Fix translation text in FeeInfoBox * Add mono to Tooltip component * Fix ProfitCalculator styling * Remove unused import and rename TotalFeeRow * Cleanup * Adjust SVG values * Update suggestedDecimals function * Fix dark theme position buttons * Last changes * fix lint warnings --------- Co-authored-by: platschi <platschi@posteo.org> --- components/Badge/Badge.tsx | 43 +- components/Button/Button.tsx | 180 +++-- components/Button/TabButton.tsx | 136 ++-- .../Currency/CurrencyPrice/CurrencyPrice.tsx | 20 +- components/ErrorView/ErrorView.tsx | 2 +- components/InfoBox/InfoBox.tsx | 171 ++--- components/InfoBox/index.ts | 2 +- components/Input/CustomInput.tsx | 117 ---- components/Input/CustomNumericInput.tsx | 83 --- components/Input/InputBalanceLabel.tsx | 6 +- components/Input/NumericInput.tsx | 181 +++-- components/Input/SearchInput.tsx | 7 - components/Pill.tsx | 46 ++ components/README.md | 30 + .../SegmentedControl/SegmentedControl.tsx | 47 +- components/StakeCard/StakeCard.tsx | 2 +- components/Table/Search.tsx | 28 +- components/Table/Table.tsx | 305 ++++---- components/Text/Body.tsx | 106 ++- components/Text/Display.tsx | 14 + components/Text/Heading.tsx | 63 +- components/Text/NumberLabel.tsx | 12 +- components/Text/NumericValue.tsx | 39 ++ components/Text/index.ts | 3 +- components/Tooltip/BaseTooltip.tsx | 22 +- components/Tooltip/ErrorTooltip.ts | 14 +- components/Tooltip/TimerTooltip.tsx | 12 +- components/Tooltip/Tooltip.tsx | 5 +- pages/dashboard/earn.tsx | 1 - sdk/README.md | 1 + sections/README.md | 17 + sections/app/AcknowledgementModal.tsx | 6 +- .../FuturesHistoryTable.tsx | 6 +- .../FuturesMarketsTable.tsx | 14 +- .../FuturesPositionsTable.tsx | 13 +- sections/dashboard/History/History.tsx | 2 +- sections/dashboard/Markets/Markets.tsx | 19 +- .../PortfolioChart/PortfolioChart.tsx | 2 +- .../SpotHistoryTable/SpotHistoryTable.tsx | 3 +- .../Stake/InputCards/RedeemInputCard.tsx | 2 +- sections/dashboard/Stake/StakingPortfolio.tsx | 3 - sections/dashboard/Stake/StakingTab.tsx | 2 +- .../dashboard/Stake/TradingRewardsTab.tsx | 9 +- .../dashboard/Stake/VestConfirmationModal.tsx | 21 +- .../SynthBalancesTable/SynthBalancesTable.tsx | 9 +- sections/earn/StakeGrid.tsx | 2 +- sections/exchange/BasicSwap.tsx | 2 +- .../FooterCard/SettleTransactionsCard.tsx | 2 +- .../exchange/FooterCard/TradeSummaryCard.tsx | 28 +- sections/exchange/MobileSwap/SwapButton.tsx | 4 +- sections/exchange/MobileSwap/SwapInfoBox.tsx | 142 ++-- .../CurrencyCard/CurrencyCardInput.tsx | 28 +- .../CurrencyCard/MobileCurrencyCard.tsx | 5 - sections/exchange/message.ts | 8 +- .../CrossMarginOnboard/CrossMarginOnboard.tsx | 15 - sections/futures/FeeInfoBox/FeeInfoBox.tsx | 456 ++++++------ sections/futures/FeeInfoBox/index.ts | 2 +- .../futures/LeverageInput/LeverageInput.tsx | 46 +- .../futures/MarketDetails/MarketDetail.tsx | 67 +- .../futures/MarketDetails/MarketDetails.tsx | 184 +++-- .../futures/MarketDetails/useGetMarketData.ts | 149 ---- .../futures/MarketInfoBox/MarketInfoBox.tsx | 150 ++-- .../MobileTrade/UserTabs/TransfersTab.tsx | 21 +- .../OrderPriceInput/OrderPriceInput.tsx | 4 +- sections/futures/OrderSizing/OrderSizing.tsx | 61 +- .../PositionButtons/PositionButtons.tsx | 90 +-- .../PositionCard/ClosePositionModal.tsx | 2 +- .../futures/PositionCard/PositionCard.tsx | 660 ++++++++---------- .../ProfitCalculator/LabelWithInput.tsx | 55 +- .../ProfitCalculator/ProfitCalculator.tsx | 12 +- .../ProfitCalculator/ProfitDetails.tsx | 141 ++-- .../ProfitCalculator/StatWithContainer.tsx | 25 +- .../futures/ShareModal/ShareModalButton.tsx | 6 +- .../Trade/DelayedOrderConfirmationModal.tsx | 10 +- sections/futures/Trade/MarketsDropdown.tsx | 76 +- .../futures/Trade/MarketsDropdownOption.tsx | 10 +- .../Trade/MarketsDropdownSingleValue.tsx | 9 +- sections/futures/Trade/OrderWarning.tsx | 36 - .../futures/Trade/TradeIsolatedMargin.tsx | 46 +- sections/futures/Trade/TradePanelHeader.tsx | 83 ++- .../Trade/TransferIsolatedMarginModal.tsx | 4 +- .../TradeCrossMargin/CrossMarginInfoBox.tsx | 469 ++++++------- .../DepositWithdrawCrossMargin.tsx | 4 +- .../EditCrossMarginLeverageModal.tsx | 21 +- .../ManageKeeperBalanceModal.tsx | 4 +- .../TradeCrossMargin/TradeCrossMargin.tsx | 4 +- sections/futures/Trades/Trades.tsx | 26 +- sections/futures/TradingHistory/SkewInfo.tsx | 57 +- .../TradingHistory/TradesHistoryTable.tsx | 29 +- sections/futures/Transfers/Transfers.tsx | 20 +- sections/futures/UserInfo/UserInfo.tsx | 37 +- sections/homepage/Hero.tsx | 4 +- sections/homepage/ShortList.tsx | 9 +- sections/homepage/TradeNow.tsx | 35 +- sections/homepage/text.ts | 2 +- sections/leaderboard/Leaderboard.tsx | 2 +- sections/leaderboard/TraderHistory.tsx | 5 +- .../Header/MobileUserMenu/MobileUserMenu.tsx | 2 +- .../MobileUserMenu/MobileWalletButton.tsx | 4 +- .../AppLayout/Header/NetworksSwitcher.tsx | 2 +- .../Layout/AppLayout/Header/WalletActions.tsx | 2 +- .../Layout/AppLayout/Header/WalletButtons.tsx | 6 +- sections/shared/Layout/HomeLayout/Footer.tsx | 11 +- sections/shared/Layout/HomeLayout/Header.tsx | 6 +- .../shared/components/CompetitionBanner.tsx | 7 +- sections/shared/components/FeeCostSummary.tsx | 10 +- sections/shared/components/GasPriceSelect.tsx | 23 +- .../shared/components/PriceImpactSummary.tsx | 10 +- .../shared/modals/SelectCurrencyModal.tsx | 4 +- sections/stats/TimeframeSwitcher.tsx | 4 +- state/futures/selectors.ts | 131 +++- stories/AccountStats.stories.tsx | 17 + stories/Badge.stories.tsx | 30 + stories/Button.stories.tsx | 16 +- stories/FeeInfoBox.stories.tsx | 49 ++ stories/FuturesTabs.stories.tsx | 12 + stories/InfoBox.stories.tsx | 50 +- stories/Input.stories.tsx | 7 +- stories/MarketStats.stories.tsx | 35 + stories/OrderInput.stories.tsx | 27 + stories/OrderType.stories.tsx | 24 + stories/Pill.stories.tsx | 30 + stories/PositionCard.stories.tsx | 65 ++ stories/PositionType.stories.tsx | 9 + stories/Text.stories.tsx | 23 +- styles/common.tsx | 14 +- styles/theme/colors/common.ts | 88 ++- styles/theme/colors/dark.ts | 116 +++ styles/theme/colors/light.ts | 116 +++ utils/formatters/number.ts | 2 +- 130 files changed, 3289 insertions(+), 2858 deletions(-) delete mode 100644 components/Input/CustomInput.tsx delete mode 100644 components/Input/CustomNumericInput.tsx delete mode 100644 components/Input/SearchInput.tsx create mode 100644 components/Pill.tsx create mode 100644 components/Text/Display.tsx create mode 100644 components/Text/NumericValue.tsx create mode 100644 sections/README.md delete mode 100644 sections/futures/MarketDetails/useGetMarketData.ts delete mode 100644 sections/futures/Trade/OrderWarning.tsx create mode 100644 stories/AccountStats.stories.tsx create mode 100644 stories/Badge.stories.tsx create mode 100644 stories/FeeInfoBox.stories.tsx create mode 100644 stories/FuturesTabs.stories.tsx create mode 100644 stories/MarketStats.stories.tsx create mode 100644 stories/OrderInput.stories.tsx create mode 100644 stories/OrderType.stories.tsx create mode 100644 stories/Pill.stories.tsx create mode 100644 stories/PositionCard.stories.tsx create mode 100644 stories/PositionType.stories.tsx diff --git a/components/Badge/Badge.tsx b/components/Badge/Badge.tsx index ac167f4a60..f70f0bfe68 100644 --- a/components/Badge/Badge.tsx +++ b/components/Badge/Badge.tsx @@ -1,21 +1,44 @@ -import styled from 'styled-components'; +import { FC } from 'react'; +import styled, { css } from 'styled-components'; -const Badge = styled.span<{ color: 'yellow' | 'red' | 'gray' }>` +type BadgeProps = { + color?: 'yellow' | 'red' | 'gray'; + size?: 'small' | 'regular'; + dark?: boolean; +}; + +const Badge: FC<BadgeProps> = ({ color = 'yellow', size = 'regular', dark, ...props }) => { + return <BaseBadge $color={color} $dark={dark} $size={size} {...props} />; +}; + +const BaseBadge = styled.span<{ + $color: 'yellow' | 'red' | 'gray'; + $dark?: boolean; + $size: 'small' | 'regular'; +}>` text-transform: uppercase; - padding: 1.6px 3px 1px 3px; text-align: center; - font-family: ${(props) => props.theme.fonts.black}; - color: ${(props) => props.theme.colors.selectedTheme.badge[props.color].text}; - background: ${(props) => props.theme.colors.selectedTheme.badge[props.color].background}; + ${(props) => css` + padding: 2px 6px; + padding: ${props.$size === 'small' ? '2px 4px' : '2px 6px'}; + font-size: ${props.$size === 'small' ? 10 : 12}px; + font-family: ${props.theme.fonts.black}; + color: ${props.theme.colors.selectedTheme.newTheme.badge[props.$color].text}; + background: ${props.theme.colors.selectedTheme.newTheme.badge[props.$color].background}; + ${props.$dark && + css` + color: ${props.theme.colors.selectedTheme.newTheme.badge[props.$color].dark.text}; + background: ${props.theme.colors.selectedTheme.newTheme.badge[props.$color].dark.background}; + border: 1px solid ${props.theme.colors.selectedTheme.newTheme.badge[props.$color].dark.border}; + `} + `} border-radius: 100px; - margin-left: 5px; line-height: unset; - font-size: 10px; font-variant: all-small-caps; opacity: 1; user-select: none; + display: flex; + align-items: center; `; -Badge.displayName = 'Badge'; - export default Badge; diff --git a/components/Button/Button.tsx b/components/Button/Button.tsx index f7df6df35b..d259f6cb69 100644 --- a/components/Button/Button.tsx +++ b/components/Button/Button.tsx @@ -1,5 +1,8 @@ +import { FC, ReactNode, memo } from 'react'; import styled, { css } from 'styled-components'; +import { ButtonLoader } from 'components/Loader/Loader'; + // TODO: Clean up these styles export type ButtonVariant = | 'primary' @@ -12,16 +15,17 @@ export type ButtonVariant = | 'select' | 'yellow'; -type ButtonProps = { - size?: 'xs' | 'sm' | 'md' | 'lg' | 'xl'; - variant?: ButtonVariant; +type BaseButtonProps = { + $size: 'small' | 'medium' | 'large'; + $variant: ButtonVariant; isActive?: boolean; isRounded?: boolean; - mono?: boolean; fullWidth?: boolean; noOutline?: boolean; textColor?: 'yellow'; textTransform?: 'none' | 'uppercase' | 'capitalize' | 'lowercase'; + $active?: boolean; + $mono?: boolean; }; export const border = css` @@ -47,20 +51,41 @@ export const border = css` } `; -const Button = styled.button<ButtonProps>` +const sizeMap = { + small: { + paddingVertical: 8, + paddingHorizontal: 16, + height: 40, + fontSize: 13, + }, + medium: { + paddingVertical: 14, + paddingHorizontal: 26, + height: 47, + fontSize: 15, + }, + large: { + paddingVertical: 16, + paddingHorizontal: 36, + height: 55, + fontSize: 16, + }, +} as const; + +const BaseButton = styled.button<BaseButtonProps>` + display: flex; + justify-content: center; + align-items: center; + height: auto; cursor: pointer; position: relative; border-radius: ${(props) => (props.isRounded ? '50px' : '8px')}; - padding: 0 14px; box-sizing: border-box; - text-transform: ${(props) => props.textTransform || 'capitalize'}; + text-transform: ${(props) => props.textTransform ?? 'capitalize'}; outline: none; white-space: nowrap; - font-size: 17px; - color: ${(props) => - (props.textColor && props.theme.colors.selectedTheme.button.text[props.textColor]) || - props.theme.colors.selectedTheme.button.text.primary}; + color: ${(props) => props.theme.colors.selectedTheme.button.text[props.textColor ?? 'primary']}; transition: all 0.1s ease-in-out; ${border} &:hover { @@ -68,104 +93,64 @@ const Button = styled.button<ButtonProps>` } ${(props) => - props.variant === 'primary' && + props.$variant === 'primary' && css` background: ${props.theme.colors.selectedTheme.button.primary.background}; text-shadow: ${props.theme.colors.selectedTheme.button.primary.textShadow}; &:hover { background: ${props.theme.colors.selectedTheme.button.primary.hover}; } - `}; + `} ${(props) => - (props.noOutline || props.variant === 'flat') && + (props.noOutline || props.$variant === 'flat') && css` - background: ${(props) => props.theme.colors.selectedTheme.button.fill}; - border: ${(props) => props.theme.colors.selectedTheme.border}; + background: ${props.theme.colors.selectedTheme.button.fill}; + border: ${props.theme.colors.selectedTheme.border}; box-shadow: none; &:hover { - background: ${(props) => props.theme.colors.selectedTheme.button.fillHover}; + background: ${props.theme.colors.selectedTheme.button.fillHover}; } &::before { display: none; } - `}; + `} ${(props) => - props.variant === 'yellow' && + props.$variant === 'yellow' && css` - background: ${(props) => props.theme.colors.selectedTheme.button.yellow.fill}; - border: 1px solid ${(props) => props.theme.colors.selectedTheme.button.yellow.border}; - color: ${(props) => props.theme.colors.selectedTheme.button.yellow.text}; + background: ${props.theme.colors.selectedTheme.button.yellow.fill}; + border: 1px solid ${props.theme.colors.selectedTheme.button.yellow.border}; + color: ${props.theme.colors.selectedTheme.button.yellow.text}; box-shadow: none; &:hover { - background: ${(props) => props.theme.colors.selectedTheme.button.yellow.fillHover}; + background: ${props.theme.colors.selectedTheme.button.yellow.fillHover}; } &::before { display: none; } - `}; + `} - ${(props) => - props.mono - ? css` - font-family: ${props.theme.fonts.mono}; - ` - : css` - font-family: ${props.theme.fonts.bold}; - `}; + font-family: ${(props) => props.theme.fonts[props.$mono ? 'mono' : 'bold']}; ${(props) => - props.variant === 'secondary' && + props.$variant === 'secondary' && css` color: ${props.theme.colors.selectedTheme.button.secondary.text}; - `}; + `} ${(props) => - props.variant === 'danger' && + props.$variant === 'danger' && css` color: ${props.theme.colors.selectedTheme.red}; - `}; - - ${(props) => - props.size === 'xs' && - css` - height: 22px; - min-width: 50px; - font-size: 11px; - `}; - - ${(props) => - props.size === 'sm' && - css` - height: 41px; - min-width: 157px; - font-size: 15px; - `}; - - ${(props) => - props.size === 'md' && - css` - height: 50px; - min-width: 200px; - `}; + `} - ${(props) => - props.size === 'lg' && - css` - height: 70px; - min-width: 260px; - font-size: 19px; - `}; - - ${(props) => - props.size === 'xl' && - css` - height: 80px; - min-width: 360px; - font-size: 21px; - `}; + ${(props) => css` + height: ${sizeMap[props.$size].height}px; + padding: ${sizeMap[props.$size].paddingVertical}px ${sizeMap[props.$size].paddingHorizontal}px; + font-size: ${sizeMap[props.$size].fontSize}px; + `} ${(props) => props.fullWidth && @@ -186,4 +171,51 @@ const Button = styled.button<ButtonProps>` } `; +type ButtonProps = { + loading?: boolean; + active?: boolean; + mono?: boolean; + className?: string; + left?: ReactNode; + right?: ReactNode; + size?: 'small' | 'medium' | 'large'; + variant?: ButtonVariant; + fullWidth?: boolean; + noOutline?: boolean; + textColor?: 'yellow'; + textTransform?: 'none' | 'uppercase' | 'capitalize' | 'lowercase'; + style?: React.CSSProperties; + disabled?: boolean; + onClick?: React.MouseEventHandler<HTMLButtonElement> | undefined; + isRounded?: boolean; +}; + +const Button: FC<ButtonProps> = memo( + ({ + loading, + children, + mono, + left, + right, + active = true, + size = 'medium', + variant = 'flat', + ...props + }) => { + return ( + <BaseButton $active={active} $mono={mono} $size={size} $variant={variant} {...props}> + {loading ? ( + <ButtonLoader /> + ) : ( + <> + {left} + <>{children}</> + {right} + </> + )} + </BaseButton> + ); + } +); + export default Button; diff --git a/components/Button/TabButton.tsx b/components/Button/TabButton.tsx index d73b8a5096..88e3dfa8e9 100644 --- a/components/Button/TabButton.tsx +++ b/components/Button/TabButton.tsx @@ -1,12 +1,14 @@ import React, { ReactNode } from 'react'; import styled, { css } from 'styled-components'; +import { Body } from 'components/Text'; + import Button from './Button'; export type TabButtonProps = { title: string; detail?: string; - badge?: number; + badgeCount?: number; icon?: any; active?: boolean; titleIcon?: ReactNode; @@ -19,111 +21,107 @@ export type TabButtonProps = { }; const TabButton: React.FC<TabButtonProps> = React.memo( - ({ title, detail, badge, icon, titleIcon, ...props }) => ( - <StyledButton noOutline {...props}> + ({ title, detail, badgeCount, icon, titleIcon, vertical, nofill, ...props }) => ( + <StyledButton $vertical={vertical} $nofill={nofill} noOutline {...props}> {!!icon && <div>{icon}</div>} <div> <div className="title-container"> {titleIcon} - <p className="title">{title}</p> - {!!badge && <div className="badge">{badge}</div>} + <Body className="title" weight="bold"> + {title} + </Body> + {!!badgeCount && <div className="badge">{badgeCount}</div>} </div> - {detail && <p className="detail">{detail}</p>} + {detail && ( + <Body className="detail" mono weight="bold"> + {detail} + </Body> + )} </div> </StyledButton> ) ); -const StyledButton = styled(Button)<{ - active?: boolean; - vertical?: boolean; - nofill?: boolean; +const StyledButton = styled(Button).attrs({ size: 'small' })<{ + $vertical?: boolean; + $nofill?: boolean; }>` height: initial; display: flex; align-items: center; - border-radius: ${(props) => (props.isRounded ? '100px' : '8px')}; padding-top: 10px; padding-bottom: 10px; justify-content: center; - background-color: ${(props) => - props.active - ? props.theme.colors.selectedTheme.tab.background.active - : props.theme.colors.selectedTheme.tab.background.inactive}; + p { - margin: 0; - font-size: 13px; text-align: left; } + .title-container { display: flex; flex-direction: row; align-items: center; } - .title { - text-align: center; - color: ${(props) => - props.active - ? props.theme.colors.selectedTheme.button.text.primary - : props.theme.colors.selectedTheme.gray}; - } - .detail { - color: ${(props) => - props.active ? props.theme.colors.selectedTheme.gold : props.theme.colors.selectedTheme.gray}; - margin-top: 4px; - font-size: 18px; - font-family: ${(props) => props.theme.fonts.monoBold}; - } + ${(props) => css` + flex-direction: ${props.$vertical ? 'column' : 'row'}; + border-radius: ${props.isRounded ? '100px' : '8px'}; + background-color: ${props.theme.colors.selectedTheme.tab.background[ + props.active ? 'active' : 'inactive' + ]}; - .badge { - height: 16px; - width: fit-content; - min-width: 16px; - padding-left: 4px; - padding-right: 4px; - margin-left: 7px; - font-size: 13px; - color: ${(props) => props.theme.colors.selectedTheme.black}; - background-color: ${(props) => props.theme.colors.selectedTheme.button.tab.badge.background}; - border-radius: 4px; - } + .title { + text-align: center; + color: ${props.active + ? props.theme.colors.selectedTheme.button.text.primary + : props.theme.colors.selectedTheme.gray}; + } - svg { - margin-right: ${(props) => (props.vertical ? '0' : '7px')}; - path { - ${(props) => - css` - ${props.nofill ? 'stroke' : 'fill'}: ${props.active - ? props.theme.colors.selectedTheme.button.text.primary - : props.theme.colors.selectedTheme.gray}; - `} + .detail { + color: ${props.theme.colors.selectedTheme[props.active ? 'gold' : 'gray']}; + margin-top: 4px; + font-size: 18px; } - } - &:disabled { - background-color: transparent; - p { - color: ${(props) => props.theme.colors.selectedTheme.button.tab.disabled.text}; + .badge { + height: 16px; + width: fit-content; + min-width: 16px; + padding-left: 4px; + padding-right: 4px; + margin-left: 7px; + font-size: 13px; + color: ${props.theme.colors.selectedTheme.black}; + background-color: ${props.theme.colors.selectedTheme.button.tab.badge.background}; + border-radius: 4px; } + svg { + margin-right: ${props.$vertical ? '0' : '7px'}; path { - fill: ${(props) => props.theme.colors.selectedTheme.button.tab.disabled.text}; + ${props.$nofill ? 'stroke' : 'fill'}: ${props.active + ? props.theme.colors.selectedTheme.button.text.primary + : props.theme.colors.selectedTheme.gray}; } } - .badge { - display: none; - } - } + &:disabled { + background-color: transparent; + p { + color: ${props.theme.colors.selectedTheme.button.tab.disabled.text}; + } + svg { + path { + fill: ${props.theme.colors.selectedTheme.button.tab.disabled.text}; + } + } - ${(props) => - props.vertical && - css` - display: flex; - flex-direction: ${props.vertical ? 'column' : 'row'}; - align-items: center; - `} + .badge { + display: none; + } + } + `} `; + export default TabButton; diff --git a/components/Currency/CurrencyPrice/CurrencyPrice.tsx b/components/Currency/CurrencyPrice/CurrencyPrice.tsx index 0f89a2a8a7..589361a39f 100644 --- a/components/Currency/CurrencyPrice/CurrencyPrice.tsx +++ b/components/Currency/CurrencyPrice/CurrencyPrice.tsx @@ -1,24 +1,22 @@ -import Wei, { wei } from '@synthetixio/wei'; -import { ethers } from 'ethers'; +import { wei, WeiSource } from '@synthetixio/wei'; import React, { FC, memo } from 'react'; -import styled from 'styled-components'; +import styled, { css } from 'styled-components'; import ChangePercent from 'components/ChangePercent'; import { ContainerRowMixin } from 'components/layout/grid'; import { CurrencyKey } from 'constants/currency'; import { formatCurrency, FormatCurrencyOptions } from 'utils/formatters/number'; -type WeiSource = Wei | number | string | ethers.BigNumber; - type CurrencyPriceProps = { currencyKey: CurrencyKey; showCurrencyKey?: boolean; price: WeiSource; sign?: string; change?: number; - conversionRate?: WeiSource | null; + conversionRate?: WeiSource; formatOptions?: FormatCurrencyOptions; truncate?: boolean; + side?: 'positive' | 'negative'; }; export const CurrencyPrice: FC<CurrencyPriceProps> = memo( @@ -30,6 +28,7 @@ export const CurrencyPrice: FC<CurrencyPriceProps> = memo( conversionRate, showCurrencyKey, formatOptions, + side, truncate = false, ...rest }) => { @@ -41,7 +40,7 @@ export const CurrencyPrice: FC<CurrencyPriceProps> = memo( } return ( - <Container {...rest}> + <Container $side={side} {...rest}> <span className="price"> {formatCurrency( currencyKey, @@ -59,10 +58,15 @@ export const CurrencyPrice: FC<CurrencyPriceProps> = memo( } ); -const Container = styled.span` +const Container = styled.span<{ $side?: 'positive' | 'negative' }>` ${ContainerRowMixin}; font-family: ${(props) => props.theme.fonts.mono}; color: ${(props) => props.theme.colors.selectedTheme.button.text.primary}; + ${(props) => + !!props.$side && + css` + color: ${props.theme.colors.selectedTheme.newTheme.text.number[props.$side]}; + `} `; export default CurrencyPrice; diff --git a/components/ErrorView/ErrorView.tsx b/components/ErrorView/ErrorView.tsx index ca63c5b710..fe911d2180 100644 --- a/components/ErrorView/ErrorView.tsx +++ b/components/ErrorView/ErrorView.tsx @@ -45,7 +45,7 @@ export const ErrorView: FC<ErrorProps> = memo( {retryButton && ( <> <Spacer height={10} /> - <Button variant="danger" size="xs" onClick={retryButton.onClick}> + <Button variant="danger" size="small" onClick={retryButton.onClick}> {retryButton.label} </Button> </> diff --git a/components/InfoBox/InfoBox.tsx b/components/InfoBox/InfoBox.tsx index ffb3944ba3..a182c92618 100644 --- a/components/InfoBox/InfoBox.tsx +++ b/components/InfoBox/InfoBox.tsx @@ -1,121 +1,75 @@ -import { memo, FC, useState, useCallback } from 'react'; +import { memo, FC } from 'react'; import styled, { css } from 'styled-components'; import CaretDownIcon from 'assets/svg/app/caret-down-gray.svg'; -import * as Text from 'components/Text'; +import { Body } from 'components/Text'; import { NO_VALUE } from 'constants/placeholder'; -export type DetailedInfo = { - value: string | React.ReactNode; +type InfoBoxRowProps = { + title: string; + value: React.ReactNode; keyNode?: React.ReactNode; valueNode?: React.ReactNode; - color?: 'green' | 'red' | 'gold' | undefined; spaceBeneath?: boolean; compactBox?: boolean; - expandable?: boolean; - subItems?: Record<string, DetailedInfo>; -}; - -type InfoBoxProps = { - details: Record<string, DetailedInfo | null | undefined>; - style?: React.CSSProperties; - className?: string; + color?: 'green' | 'red' | 'gold' | undefined; disabled?: boolean; dataTestId?: string; -}; - -const InfoBox: FC<InfoBoxProps> = memo(({ details, disabled, dataTestId, ...props }) => { - const [expandedRows, setExpandedRows] = useState<Set<string>>(new Set()); - - const onToggleExpand = useCallback( - (key: string) => { - expandedRows.has(key) ? expandedRows.delete(key) : expandedRows.add(key); - setExpandedRows(new Set([...expandedRows])); - }, - [expandedRows] - ); - - return ( - <InfoBoxContainer {...props}> - {Object.entries(details).map(([key, value], index) => ( - <> - <InfoBoxValue - key={key} - title={key} - value={value} - disabled={disabled} - dataTestId={`${dataTestId}-${index}`} - expandable={!!value?.subItems} - expanded={expandedRows.has(key)} - onToggleExpand={onToggleExpand} - /> - {value?.subItems && expandedRows.has(key) - ? Object.entries(value.subItems).map(([key, value], index) => ( - <InfoBoxValue - key={key} - title={key} - value={value} - disabled={disabled} - dataTestId={`${dataTestId}-${index}`} - isSubtItem - /> - )) - : null} - </> - ))} - </InfoBoxContainer> - ); -}); - -type InfoBoxValueProps = { - title: string; - value?: DetailedInfo | null; - disabled?: boolean; - dataTestId: string; expandable?: boolean; expanded?: boolean; - isSubtItem?: boolean; + isSubItem?: boolean; onToggleExpand?: (key: string) => void; }; -const InfoBoxValue: FC<InfoBoxValueProps> = memo( - ({ title, value, disabled, dataTestId, expandable, expanded, isSubtItem, onToggleExpand }) => { - if (!value) return null; - - return ( - <> - {value.compactBox ? ( - value.keyNode - ) : ( - <Row - isSubtItem={isSubtItem} - onClick={expandable ? () => onToggleExpand?.(title) : undefined} +export const InfoBoxRow: FC<InfoBoxRowProps> = memo( + ({ + title, + value, + keyNode, + compactBox, + disabled, + dataTestId, + expandable, + expanded, + isSubItem, + onToggleExpand, + children, + color, + valueNode, + spaceBeneath, + }) => ( + <> + {compactBox ? ( + keyNode + ) : ( + <Row + $isSubItem={isSubItem} + onClick={expandable ? () => onToggleExpand?.(title) : undefined} + > + <InfoBoxKey> + {title}: {keyNode} {expandable ? expanded ? <HideIcon /> : <ExpandIcon /> : null} + </InfoBoxKey> + <ValueText + $isSubItem={isSubItem} + data-testid={dataTestId} + $disabled={disabled} + $color={color} > - <InfoBoxKey> - {title}: {value.keyNode}{' '} - {expandable ? expanded ? <HideIcon /> : <ExpandIcon /> : null} - </InfoBoxKey> - <ValueText - isSubtItem={isSubtItem} - data-testid={dataTestId} - $disabled={disabled} - $color={value.color} - > - {disabled ? NO_VALUE : value.value} - {value.valueNode} - </ValueText> - </Row> - )} - {value?.spaceBeneath && <br />} - </> - ); - } + {disabled ? NO_VALUE : value} + {valueNode} + </ValueText> + </Row> + )} + {spaceBeneath && <br />} + {expandable && expanded && children} + </> + ) ); -const Row = styled.div<{ onClick?: (title: string) => void; isSubtItem?: boolean }>` +const Row = styled.div<{ $isSubItem?: boolean }>` cursor: ${(props) => (props.onClick ? 'pointer' : 'default')}; - padding-left: ${(props) => (props.isSubtItem ? '10px' : '0')}; - border-left: ${(props) => (props.isSubtItem ? props.theme.colors.selectedTheme.border : '0')}; + padding-left: ${(props) => (props.$isSubItem ? '10px' : '0')}; + border-left: ${(props) => (props.$isSubItem ? props.theme.colors.selectedTheme.border : '0')}; border-width: 2px; display: flex; justify-content: space-between; @@ -125,7 +79,7 @@ const Row = styled.div<{ onClick?: (title: string) => void; isSubtItem?: boolean } `; -const InfoBoxContainer = styled.div` +export const InfoBoxContainer = styled.div` border: ${(props) => props.theme.colors.selectedTheme.border}; border-radius: 10px; padding: 14px; @@ -133,23 +87,18 @@ const InfoBoxContainer = styled.div` width: 100%; `; -const InfoBoxKey = styled(Text.Body)` +const InfoBoxKey = styled(Body)` color: ${(props) => props.theme.colors.selectedTheme.text.label}; - font-size: 13px; text-transform: capitalize; `; -const ValueText = styled(Text.Body)<{ +const ValueText = styled(Body).attrs({ mono: true })<{ $disabled?: boolean; - $color?: DetailedInfo['color']; - isSubtItem?: boolean; + $color?: InfoBoxRowProps['color']; + $isSubItem?: boolean; }>` - color: ${(props) => - props.isSubtItem - ? props.theme.colors.selectedTheme.text.label - : props.theme.colors.selectedTheme.text.value}; - font-family: ${(props) => props.theme.fonts.mono}; - font-size: 13px; + color: ${(props) => props.theme.colors.selectedTheme.text[props.$isSubItem ? 'label' : 'value']}; + cursor: default; ${(props) => props.$color === 'red' && @@ -184,5 +133,3 @@ const HideIcon = styled(ExpandIcon)` transform: rotate(180deg); margin-bottom: -4px; `; - -export default InfoBox; diff --git a/components/InfoBox/index.ts b/components/InfoBox/index.ts index 60640b6a0c..0b7f721929 100644 --- a/components/InfoBox/index.ts +++ b/components/InfoBox/index.ts @@ -1 +1 @@ -export { default } from './InfoBox'; +export { InfoBoxContainer, InfoBoxRow } from './InfoBox'; diff --git a/components/Input/CustomInput.tsx b/components/Input/CustomInput.tsx deleted file mode 100644 index 87b10043dd..0000000000 --- a/components/Input/CustomInput.tsx +++ /dev/null @@ -1,117 +0,0 @@ -import { memo, FC, useCallback } from 'react'; -import styled from 'styled-components'; - -type CustomInputProps = { - placeholder?: string; - value?: string | number; - onChange: (e: React.ChangeEvent<HTMLInputElement>, value: string) => void; - right: React.ReactNode; - left?: React.ReactNode; - style?: React.CSSProperties; - className?: string; - disabled?: boolean; - id?: string; - defaultValue?: any; - dataTestId?: string; - textAlign?: string; - invalid?: boolean; -}; - -const INVALID_CHARS = ['-', '+', 'e']; - -const CustomInput: FC<CustomInputProps> = memo( - ({ - value, - placeholder, - onChange, - right, - left, - disabled, - id, - defaultValue, - dataTestId, - textAlign = 'left', - ...props - }) => { - const handleChange = useCallback( - (e: React.ChangeEvent<HTMLInputElement>) => { - const standardizedNum = e.target.value.replace(/,/g, '.').replace(/[e+-]/gi, ''); - if (isNaN(Number(standardizedNum))) return; - onChange(e, standardizedNum); - }, - [onChange] - ); - - return ( - <CustomInputContainer textAlign={textAlign} {...props}> - {typeof left === 'string' ? <span>{left}</span> : left} - <input - data-testid={dataTestId} - disabled={disabled} - placeholder={placeholder} - value={value} - type="text" - inputMode="decimal" - onChange={handleChange} - onKeyDown={(e) => { - if (INVALID_CHARS.includes(e.key)) { - e.preventDefault(); - } - }} - id={id} - defaultValue={defaultValue} - /> - {typeof right === 'string' ? <span>{right}</span> : right} - </CustomInputContainer> - ); - } -); - -const CustomInputContainer = styled.div<{ textAlign: string; invalid?: boolean }>` - display: flex; - align-items: center; - justify-content: space-between; - box-sizing: border-box; - height: 46px; - background: ${(props) => props.theme.colors.selectedTheme.input.secondary.background}; - box-shadow: ${(props) => props.theme.colors.selectedTheme.input.shadow}; - border: ${(props) => props.theme.colors.selectedTheme.border}; - border-color: ${(props) => - props.invalid ? props.theme.colors.selectedTheme.red : props.theme.colors.selectedTheme.border}; - - border-radius: 10px; - padding: 0 10px; - - input { - display: flex; - flex: 1; - margin-right: 4px; - font-family: ${(props) => props.theme.fonts.mono}; - font-size: 18px; - line-height: 22px; - background-color: transparent; - border: none; - text-align: ${(props) => props.textAlign || 'left'}; - color: ${(props) => - props.invalid - ? props.theme.colors.selectedTheme.red - : props.theme.colors.selectedTheme.button.text.primary}; - width: 100%; - - &:focus { - outline: none; - } - - ::placeholder { - color: ${(props) => props.theme.colors.selectedTheme.input.placeholder}; - } - } - - span { - font-family: ${(props) => props.theme.fonts.mono}; - font-size: 16px; - color: ${(props) => props.theme.colors.selectedTheme.input.placeholder}; - } -`; - -export default CustomInput; diff --git a/components/Input/CustomNumericInput.tsx b/components/Input/CustomNumericInput.tsx deleted file mode 100644 index d1018a0301..0000000000 --- a/components/Input/CustomNumericInput.tsx +++ /dev/null @@ -1,83 +0,0 @@ -import { ChangeEvent, FC, memo } from 'react'; -import styled from 'styled-components'; - -import Input from './Input'; - -type CustomNumericInputProps = { - value: string; - placeholder?: string; - suffix: string; - onChange: (e: ChangeEvent<HTMLInputElement>, value: string) => void; - className?: string; - defaultValue?: any; - maxValue?: number; - disabled?: boolean; - id?: string; -}; - -const CustomNumericInput: FC<CustomNumericInputProps> = memo( - ({ - value, - placeholder, - suffix, - onChange, - className, - defaultValue, - maxValue, - disabled, - id, - ...rest - }) => { - const handleOnChange = (e: ChangeEvent<HTMLInputElement>) => { - const { value } = e.target; - const standardizedNum = value - .replace(/[^0-9.,]/g, '') - .replace(/,/g, '.') - .substring(0, 4); - if (isNaN(Number(standardizedNum))) return; - const max = maxValue || 0; - const valueIsAboveMax = max !== 0 && Number(standardizedNum) > max; - if (!valueIsAboveMax) { - onChange(e, standardizedNum); - } - }; - - return ( - <InputWrapper $length={value.length} $suffix={suffix}> - <StyledInput - type="text" - inputMode="decimal" - value={value} - placeholder={placeholder ? `${placeholder} ${suffix}` : suffix} - onChange={handleOnChange} - className={className} - defaultValue={defaultValue} - disabled={disabled} - id={id} - {...rest} - /> - </InputWrapper> - ); - } -); - -export const InputWrapper = styled.div<{ $length: number; $suffix: string }>` - position: relative; - overflow: hidden; - ::after { - position: absolute; - top: calc(25%); - left: calc(${(props) => props.$length} * 1ch + 1.3ch)); - content: var(${(props) => (props.$length === 0 ? '' : props.$suffix)}); - font-family: ${(props) => props.theme.fonts.mono}; - font-size: 18px; - color: ${(props) => props.theme.colors.selectedTheme.input.placeholder}; - } -`; - -export const StyledInput = styled(Input)` - font-family: ${(props) => props.theme.fonts.mono}; - text-overflow: ellipsis; -`; - -export default CustomNumericInput; diff --git a/components/Input/InputBalanceLabel.tsx b/components/Input/InputBalanceLabel.tsx index 7acbccb5a0..2bed200b54 100644 --- a/components/Input/InputBalanceLabel.tsx +++ b/components/Input/InputBalanceLabel.tsx @@ -3,6 +3,7 @@ import { useTranslation } from 'react-i18next'; import styled from 'styled-components'; import { FlexDivRowCentered } from 'components/layout/flex'; +import { Body } from 'components/Text'; import { formatCurrency } from 'utils/formatters/number'; type Props = { @@ -34,12 +35,9 @@ export default function InputBalanceLabel({ balance, currencyKey, onSetAmount }: export const BalanceContainer = styled(FlexDivRowCentered)` margin-bottom: 8px; - p { - margin: 0; - } `; -export const BalanceText = styled.p` +export const BalanceText = styled(Body)` color: ${(props) => props.theme.colors.selectedTheme.gray}; span { color: ${(props) => props.theme.colors.selectedTheme.button.text.primary}; diff --git a/components/Input/NumericInput.tsx b/components/Input/NumericInput.tsx index 6444b64167..fe9c86bf0d 100644 --- a/components/Input/NumericInput.tsx +++ b/components/Input/NumericInput.tsx @@ -1,55 +1,156 @@ -import React, { ChangeEvent, FC, memo, useCallback } from 'react'; +import { FC, memo, useCallback } from 'react'; import styled, { css } from 'styled-components'; -import Input from './Input'; +import Spacer from 'components/Spacer'; type NumericInputProps = Omit<React.InputHTMLAttributes<HTMLInputElement>, 'onChange'> & { - value: string | number; - placeholder?: string; - onChange: (e: ChangeEvent<HTMLInputElement>, value: string) => void; - className?: string; - defaultValue?: any; - disabled?: boolean; - id?: string; + value: string; + onChange: (e: React.ChangeEvent<HTMLInputElement>, value: string) => void; + left?: React.ReactNode; + right?: React.ReactNode; + dataTestId?: string; + invalid?: boolean; bold?: boolean; + textAlign?: string; + suffix?: string; + max?: number; }; const INVALID_CHARS = ['-', '+', 'e']; -const NumericInput: FC<NumericInputProps> = memo(({ onChange, bold, ...props }) => { - const handleOnChange = useCallback( - (e: ChangeEvent<HTMLInputElement>) => { - const standardizedNum = e.target.value.replace(/,/g, '.').replace(/[e+-]/gi, ''); - if (isNaN(Number(standardizedNum))) return; - onChange(e, standardizedNum); - }, - [onChange] - ); - - return ( - <StyledInput - type="text" - inputMode="decimal" - onChange={handleOnChange} - onKeyDown={(e) => { - if (INVALID_CHARS.includes(e.key)) { - e.preventDefault(); +const isInvalid = (key: string) => INVALID_CHARS.includes(key); + +const NumericInput: FC<NumericInputProps> = memo( + ({ + value, + onChange, + left, + right, + dataTestId, + invalid, + bold, + textAlign, + max = 0, + className, + ...props + }) => { + const handleChange = useCallback( + (e: React.ChangeEvent<HTMLInputElement>) => { + const standardizedNum = e.target.value + .replace(/[^0-9.,]/g, '') + .replace(/,/g, '.') + .substring(0, 4); + // TODO: make regex only accept valid numbers, so we don't need to check again. + if (isNaN(Number(standardizedNum))) return; + const valueIsAboveMax = max !== 0 && Number(standardizedNum) > max; + if (!valueIsAboveMax) { + onChange(e, standardizedNum); } - }} - $bold={bold} - {...props} - /> - ); -}); - -export const StyledInput = styled(Input)<{ $bold?: boolean }>` - font-family: ${(props) => props.theme.fonts.mono}; + }, + [onChange, max] + ); + + return ( + <InputContainer + $invalid={invalid} + $bold={bold} + $textAlign={textAlign} + $length={value.length} + className={className} + > + {left && ( + <> + {left} + <Spacer width={4} /> + </> + )} + <input + data-testid={dataTestId} + value={value} + type="text" + inputMode="decimal" + onChange={handleChange} + onKeyDown={(e) => { + if (isInvalid(e.key)) { + e.preventDefault(); + } + }} + {...props} + /> + {right && ( + <> + <Spacer width={4} /> + {right} + </> + )} + </InputContainer> + ); + } +); + +const InputContainer = styled.div<{ + $invalid?: boolean; + $bold?: boolean; + $textAlign?: string; + $suffix?: string; + $length: number; +}>` + display: flex; + align-items: center; + justify-content: space-between; + background: ${(props) => props.theme.colors.selectedTheme.input.secondary.background}; + box-shadow: ${(props) => props.theme.colors.selectedTheme.input.shadow}; + border: ${(props) => props.theme.colors.selectedTheme.border}; + border-radius: 10px; + padding: 0 10px; + height: 46px; + box-sizing: border-box; + + & > input { + display: flex; + flex: 1; + font-family: ${(props) => (props.$bold ? props.theme.fonts.monoBold : props.theme.fonts.mono)}; + font-size: 18px; + line-height: 22px; + padding: 0; + background-color: transparent; + border: none; + text-overflow: ellipsis; + min-width: 0px; + width: 100%; + color: ${(props) => + props.$invalid + ? props.theme.colors.selectedTheme.red + : props.theme.colors.selectedTheme.button.text.primary}; + + ${(props) => + props.$textAlign && + css` + text-align: ${props.$textAlign}; + `} + + &:focus { + outline: none; + } + + ::placeholder { + color: ${(props) => props.theme.colors.selectedTheme.input.placeholder}; + } + } + ${(props) => - props.$bold && + props.$suffix && css` - font-family: ${props.theme.fonts.monoBold}; - `} - text-overflow: ellipsis; + ::after { + position: absolute; + top: calc(25%); + left: calc(${props.$length} * 1ch + 1.3ch)); + content: var(${props.$length === 0 ? '""' : props.$suffix}); + font-family: ${props.theme.fonts.mono}; + font-size: 18px; + color: ${props.theme.colors.selectedTheme.input.placeholder}; + } + `} `; export default NumericInput; diff --git a/components/Input/SearchInput.tsx b/components/Input/SearchInput.tsx deleted file mode 100644 index aae17f061e..0000000000 --- a/components/Input/SearchInput.tsx +++ /dev/null @@ -1,7 +0,0 @@ -import styled from 'styled-components'; - -import Input from './Input'; - -export const TextInput = styled(Input).attrs({ type: 'search' })``; - -export default TextInput; diff --git a/components/Pill.tsx b/components/Pill.tsx new file mode 100644 index 0000000000..a2549490dc --- /dev/null +++ b/components/Pill.tsx @@ -0,0 +1,46 @@ +import { FC, memo } from 'react'; +import styled, { css } from 'styled-components'; + +type PillProps = React.ButtonHTMLAttributes<HTMLButtonElement> & { + size?: 'small' | 'large'; + color?: 'yellow' | 'gray' | 'red'; + outline?: boolean; +}; + +const Pill: FC<PillProps> = memo(({ size = 'small', color = 'yellow', outline, ...props }) => { + return <BasePill $size={size} $color={color} $outline={outline} {...props} />; +}); + +const BasePill = styled.button<{ + $size: 'small' | 'large'; + $color: 'yellow' | 'gray' | 'red'; + $outline?: boolean; +}>` + ${(props) => css` + padding: ${props.$size === 'small' ? '5px' : '8px'}; + font-size: ${props.$size === 'small' ? 10 : 12}px; + font-family: ${props.theme.fonts.black}; + background: ${props.theme.colors.selectedTheme.newTheme.pill[props.$color].background}; + color: ${props.theme.colors.selectedTheme.newTheme.pill[props.$color].text}; + border: 1px solid ${props.theme.colors.selectedTheme.newTheme.pill[props.$color].border}; + border-radius: 50px; + cursor: pointer; + + ${props.$outline && + css` + background: ${props.theme.colors.selectedTheme.newTheme.pill[props.$color].outline + .background}; + color: ${props.theme.colors.selectedTheme.newTheme.pill[props.$color].outline.text}; + border: 1px solid + ${props.theme.colors.selectedTheme.newTheme.pill[props.$color].outline.border}; + `} + + &:hover { + background: ${props.theme.colors.selectedTheme.newTheme.pill[props.$color].hover.background}; + color: ${props.theme.colors.selectedTheme.newTheme.pill[props.$color].hover.text}; + border: 1px solid ${props.theme.colors.selectedTheme.newTheme.pill[props.$color].hover.border}; + } + `} +`; + +export default Pill; diff --git a/components/README.md b/components/README.md index 90bfef15f2..73496f99a1 100644 --- a/components/README.md +++ b/components/README.md @@ -1,3 +1,33 @@ # Kwenta Components +This folder contains all the base components that are used across the Kwenta interface. We try to ensure that they remain in sync will our latest designs. Components here have a number of props that cover every use case that is consistent with our design system. + +Most of the components in this folder are viewable in our [Storybook](). + ## Folder Structure + +We try to ensure that folders are as flat as (reasonbly) possible. For this, reason, most individual components can be found under `{component-name}.tsx`. However in the future, some domain specific components may be found in subfolders like `mobile`, `futures` or `exchange`. The components are only expected to be used in the context of the folders in which they are contained. + +## Checklist + +This list tracks features needed to complete the component refactor, as well as any component-related issues noticed with the current UI. This will help us get closer to a standard UI. + +- [ ] Create primary, secondary and tertiary colors for both body text and headings. Make sure that designs conform to them and add them to the theme file. +- [ ] Create component that takes in a wei value and returns the number with either a red, green or neutral color, depending on whether the value is positive, negative, zero or overridden. +- [x] Make the `InfoBox` (and similar components) more composable, rather than prop-driven, to ensure that render performance does not suffer. +- [ ] Complete Button component: Align completely with designs and figure out way to get rid of standalone `TabButton` component +- [ ] Decide whether or not it makes sense to completely decouple futures components from data requirements to make them Storybook compatible +- [ ] Finally crack down on styled components in the frontend. We should reduce the number of styled components by at least 60% to consider the refactor a success. +- [ ] Enforce uniform spacing between UI components. Consider adding a margin prop to most UI components (will also help get rid of most styled components). +- [ ] Make theme files contain everything that pertains to component variants and their different styles. +- [ ] Upgrade `react-table` to `@tanstack/react-table`. It's more recent and has better typings. We'll have to do a little bit of work though, to deal with breaking changes between the versions and make sure we improve on the render performance of the `Table` component. +- [ ] Finish implementing the `Text.Display` component. +- [ ] Do a pass through of all text-related components in the codebase, and make sure they use the new components. Also, add new props for use cases that aren't covered with the current implementation. +- [ ] Consider making `value` prop on `NumericValue` optional. +- [ ] Completely restructure the `ProfitCalculator` component. +- [ ] Add reusable `Label` component under `Text`. It should also support tooltip descriptions. + +## Guidelines + +- [ ] Avoid using `Styled` as a prefix to the names of styled components. Instead, prefer names that describe the function of the component. This makes it easier to understand the component's function at first glance. It also makes it easier to refactor or replace the component when necessary. +- [ ] Avoid using absolute positioning unless necessary. This helps avoid z-index conflicts and makes it easier for components to be adapted to multiple screen sizes. diff --git a/components/SegmentedControl/SegmentedControl.tsx b/components/SegmentedControl/SegmentedControl.tsx index 8e261a9926..fa5f5f9820 100644 --- a/components/SegmentedControl/SegmentedControl.tsx +++ b/components/SegmentedControl/SegmentedControl.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { FC, memo } from 'react'; import styled from 'styled-components'; type StyleType = 'tab' | 'check' | 'button'; @@ -13,31 +13,26 @@ interface SegmentedControlProps { onChange(index: number): void; } -function SegmentedControl({ - values, - selectedIndex, - suffix, - onChange, - styleType = 'tab', - ...props -}: SegmentedControlProps) { - return ( - <SegmentedControlContainer $length={values.length} styleType={styleType} {...props}> - {values.map((value, index) => ( - <SegmentedControlOption - styleType={styleType} - key={value} - isSelected={selectedIndex === index} - onClick={() => onChange(index)} - > - {styleType === 'check' && <CheckBox selected={selectedIndex === index} />} - {value} - {suffix} - </SegmentedControlOption> - ))} - </SegmentedControlContainer> - ); -} +const SegmentedControl: FC<SegmentedControlProps> = memo( + ({ values, selectedIndex, suffix, onChange, styleType = 'tab', ...props }) => { + return ( + <SegmentedControlContainer $length={values.length} styleType={styleType} {...props}> + {values.map((value, index) => ( + <SegmentedControlOption + styleType={styleType} + key={value} + isSelected={selectedIndex === index} + onClick={() => onChange(index)} + > + {styleType === 'check' && <CheckBox selected={selectedIndex === index} />} + {value} + {suffix} + </SegmentedControlOption> + ))} + </SegmentedControlContainer> + ); + } +); const SegmentedControlContainer = styled.div<{ $length: number; styleType: StyleType }>` ${(props) => diff --git a/components/StakeCard/StakeCard.tsx b/components/StakeCard/StakeCard.tsx index 6aad7e75a3..b3bee1f9fe 100644 --- a/components/StakeCard/StakeCard.tsx +++ b/components/StakeCard/StakeCard.tsx @@ -124,7 +124,7 @@ const StakeCard: FC<StakeCardProps> = memo( </StakeInputHeader> <NumericInput value={amount} onChange={handleChange} bold /> </StakeInputContainer> - <Button fullWidth variant="flat" size="sm" disabled={isDisabled} onClick={handleSubmit}> + <Button fullWidth variant="flat" size="small" disabled={isDisabled} onClick={handleSubmit}> {!isApproved ? t('dashboard.stake.tabs.stake-table.approve') : activeTab === 0 diff --git a/components/Table/Search.tsx b/components/Table/Search.tsx index 10c01fa85d..5367ba734b 100644 --- a/components/Table/Search.tsx +++ b/components/Table/Search.tsx @@ -1,24 +1,28 @@ -import React, { ChangeEvent } from 'react'; +import { memo, ChangeEvent, FC, useCallback } from 'react'; import styled from 'styled-components'; import SearchIconPath from 'assets/svg/app/search.svg'; -import SearchInput from 'components/Input/SearchInput'; +import Input from 'components/Input/Input'; import media from 'styles/media'; -type Props = { +type SearchProps = { value: string | undefined; - onChange: (text: string) => any; + onChange: (text: string) => void; disabled: boolean; }; -export default function Search({ value, onChange, disabled }: Props) { - const handleOnChange = (event: ChangeEvent<HTMLInputElement>) => { - onChange(event.target.value); - }; +const Search: FC<SearchProps> = memo(({ value, onChange, disabled }) => { + const handleOnChange = useCallback( + (event: ChangeEvent<HTMLInputElement>) => { + onChange(event.target.value); + }, + [onChange] + ); + return ( <SearchBar> <StyledSvg /> - <StyledSearchInput + <SearchInput value={value} onChange={handleOnChange} placeholder="Search..." @@ -26,14 +30,14 @@ export default function Search({ value, onChange, disabled }: Props) { /> </SearchBar> ); -} +}); const StyledSvg = styled(SearchIconPath)` position: absolute; left: 12px; `; -const StyledSearchInput = styled(SearchInput)` +const SearchInput = styled(Input).attrs({ type: 'search' })` position: relative; height: 100%; text-indent: 16px; @@ -52,3 +56,5 @@ const SearchBar = styled.div` display: flex; align-items: center; `; + +export default Search; diff --git a/components/Table/Table.tsx b/components/Table/Table.tsx index 586f468790..853d65707a 100644 --- a/components/Table/Table.tsx +++ b/components/Table/Table.tsx @@ -1,12 +1,12 @@ -import React, { FC, useMemo, DependencyList, useEffect, useRef } from 'react'; +import React, { FC, useMemo, DependencyList, useEffect, useRef, memo } from 'react'; import { useTable, useFlexLayout, useSortBy, Column, Row, usePagination } from 'react-table'; import type { TableInstance, UsePaginationInstanceProps, UsePaginationState } from 'react-table'; import styled, { css } from 'styled-components'; import SortDownIcon from 'assets/svg/app/caret-down.svg'; import SortUpIcon from 'assets/svg/app/caret-up.svg'; -import Spinner from 'assets/svg/app/loader.svg'; import { GridDivCenteredRow } from 'components/layout/grid'; +import Loader from 'components/Loader'; import { Body } from 'components/Text'; import Pagination from './Pagination'; @@ -70,171 +70,168 @@ type TableProps = { compactPagination?: boolean; }; -export const Table: FC<TableProps> = ({ - columns = [], - columnsDeps = [], - data = [], - options = {}, - noResultsMessage = null, - onTableRowClick = undefined, - palette = 'primary', - isLoading = false, - className, - showPagination = false, - pageSize = null, - hiddenColumns = [], - hideHeaders, - highlightRowsOnHover, - showShortList, - sortBy = [], - lastRef = null, - compactPagination = false, -}) => { - const memoizedColumns = useMemo( - () => columns, - // eslint-disable-next-line react-hooks/exhaustive-deps - columnsDeps - ); +export const Table: FC<TableProps> = memo( + ({ + columns = [], + columnsDeps = [], + data = [], + options = {}, + noResultsMessage = null, + onTableRowClick = undefined, + palette = 'primary', + isLoading = false, + className, + showPagination = false, + pageSize = null, + hiddenColumns = [], + hideHeaders, + highlightRowsOnHover, + showShortList, + sortBy = [], + lastRef = null, + compactPagination = false, + }) => { + const memoizedColumns = useMemo( + () => columns, + // eslint-disable-next-line react-hooks/exhaustive-deps + columnsDeps + ); - const { - getTableProps, - getTableBodyProps, - headerGroups, - page, - prepareRow, - canPreviousPage, - canNextPage, - pageCount, - gotoPage, - nextPage, - previousPage, - state: { pageIndex }, - setHiddenColumns, - } = useTable( - { - columns: memoizedColumns, - data, - initialState: { - pageSize: showPagination - ? pageSize + const { + getTableProps, + getTableBodyProps, + headerGroups, + page, + prepareRow, + canPreviousPage, + canNextPage, + pageCount, + gotoPage, + nextPage, + previousPage, + state: { pageIndex }, + setHiddenColumns, + } = useTable( + { + columns: memoizedColumns, + data, + initialState: { + pageSize: showPagination ? pageSize - : MAX_PAGE_ROWS - : showShortList - ? pageSize ?? 5 - : MAX_TOTAL_ROWS, - hiddenColumns: hiddenColumns, - sortBy: sortBy, + ? pageSize + : MAX_PAGE_ROWS + : showShortList + ? pageSize ?? 5 + : MAX_TOTAL_ROWS, + hiddenColumns: hiddenColumns, + sortBy: sortBy, + }, + autoResetPage: false, + autoResetSortBy: false, + ...options, }, - autoResetPage: false, - autoResetSortBy: false, - ...options, - }, - useSortBy, - usePagination, - useFlexLayout - ) as TableWithPagination<object>; + useSortBy, + usePagination, + useFlexLayout + ) as TableWithPagination<object>; - useEffect(() => { - setHiddenColumns(hiddenColumns); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); + useEffect(() => { + setHiddenColumns(hiddenColumns); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); - // reset to the first page - // this fires when filters are applied that change the data - // if a filter is applied that reduces the data size below max pages for that filter, reset to the first page - useEffect(() => { - if (pageIndex > pageCount) { - gotoPage(0); - } - }, [pageIndex, pageCount, gotoPage]); + // reset to the first page + // this fires when filters are applied that change the data + // if a filter is applied that reduces the data size below max pages for that filter, reset to the first page + useEffect(() => { + if (pageIndex > pageCount) { + gotoPage(0); + } + }, [pageIndex, pageCount, gotoPage]); - const defaultRef = useRef(null); + const defaultRef = useRef(null); - return ( - <> - <TableContainer> - <ReactTable {...getTableProps()} palette={palette} className={className}> - {headerGroups.map((headerGroup) => ( - <div className="table-row" {...headerGroup.getHeaderGroupProps()}> - {headerGroup.headers.map((column: any) => ( - <TableCellHead - hideHeaders={hideHeaders} - {...column.getHeaderProps( - column.sortable ? column.getSortByToggleProps() : undefined - )} - > - {column.render('Header')} - {column.sortable && ( - <SortIconContainer> - {column.isSorted ? ( - column.isSortedDesc ? ( - <StyledSortDownIcon /> + return ( + <> + <TableContainer> + <ReactTable {...getTableProps()} palette={palette} className={className}> + {headerGroups.map((headerGroup) => ( + <div className="table-row" {...headerGroup.getHeaderGroupProps()}> + {headerGroup.headers.map((column: any) => ( + <TableCellHead + hideHeaders={hideHeaders} + {...column.getHeaderProps( + column.sortable ? column.getSortByToggleProps() : undefined + )} + > + {column.render('Header')} + {column.sortable && ( + <SortIconContainer> + {column.isSorted ? ( + column.isSortedDesc ? ( + <StyledSortDownIcon /> + ) : ( + <StyledSortUpIcon /> + ) ) : ( - <StyledSortUpIcon /> - ) - ) : ( - <> - <StyledSortUpIcon /> - <StyledSortDownIcon /> - </> - )} - </SortIconContainer> - )} - </TableCellHead> - ))} - </div> - ))} - {isLoading ? ( - <StyledSpinner /> - ) : ( - page.length > 0 && ( - <TableBody className="table-body" {...getTableBodyProps()}> - {page.map((row: Row, idx: number) => { - prepareRow(row); - const props = row.getRowProps(); - const localRef = lastRef && idx === page.length - 1 ? lastRef : defaultRef; - const handleClick = onTableRowClick ? () => onTableRowClick(row) : undefined; - return ( - <TableBodyRow - localRef={localRef} - highlightRowsOnHover={highlightRowsOnHover} - row={row} - onClick={handleClick} - {...props} - /> - ); - })} - </TableBody> - ) - )} - {!!noResultsMessage && !isLoading && data.length === 0 && noResultsMessage} - </ReactTable> - </TableContainer> - {showPagination && !showShortList && data.length > (pageSize ?? MAX_PAGE_ROWS) ? ( - <Pagination - compact={compactPagination} - pageIndex={pageIndex} - pageCount={pageCount} - canNextPage={canNextPage} - canPreviousPage={canPreviousPage} - setPage={gotoPage} - previousPage={previousPage} - nextPage={nextPage} - /> - ) : undefined} - </> - ); -}; + <> + <StyledSortUpIcon /> + <StyledSortDownIcon /> + </> + )} + </SortIconContainer> + )} + </TableCellHead> + ))} + </div> + ))} + {isLoading ? ( + <Loader /> + ) : ( + page.length > 0 && ( + <TableBody className="table-body" {...getTableBodyProps()}> + {page.map((row: Row, idx: number) => { + prepareRow(row); + const props = row.getRowProps(); + const localRef = lastRef && idx === page.length - 1 ? lastRef : defaultRef; + const handleClick = onTableRowClick ? () => onTableRowClick(row) : undefined; + return ( + <TableBodyRow + localRef={localRef} + highlightRowsOnHover={highlightRowsOnHover} + row={row} + onClick={handleClick} + {...props} + /> + ); + })} + </TableBody> + ) + )} + {!!noResultsMessage && !isLoading && data.length === 0 && noResultsMessage} + </ReactTable> + </TableContainer> + {showPagination && !showShortList && data.length > (pageSize ?? MAX_PAGE_ROWS) ? ( + <Pagination + compact={compactPagination} + pageIndex={pageIndex} + pageCount={pageCount} + canNextPage={canNextPage} + canPreviousPage={canPreviousPage} + setPage={gotoPage} + previousPage={previousPage} + nextPage={nextPage} + /> + ) : undefined} + </> + ); + } +); const TableContainer = styled.div` overflow-x: auto; `; -const StyledSpinner = styled(Spinner)` - display: block; - margin: 30px auto; -`; - const TableBody = styled.div` overflow-y: auto; overflow-x: hidden; diff --git a/components/Text/Body.tsx b/components/Text/Body.tsx index ac68a9311b..85aa83b205 100644 --- a/components/Text/Body.tsx +++ b/components/Text/Body.tsx @@ -1,85 +1,73 @@ -import { memo } from 'react'; +import { ComponentType, memo } from 'react'; import styled, { css } from 'styled-components'; -type BodyProps = { - size?: 'small' | 'medium' | 'large'; - variant?: 'regular' | 'bold'; +export type BodyProps = React.HTMLAttributes<HTMLParagraphElement> & { + size?: 'xsmall' | 'small' | 'medium' | 'large'; + weight?: 'regular' | 'bold' | 'black'; + color?: 'primary' | 'secondary' | 'tertiary'; className?: string; fontSize?: number; mono?: boolean; - color?: 'title' | 'value' | 'body'; + capitalized?: boolean; inline?: boolean; + as?: keyof JSX.IntrinsicElements | ComponentType<any>; }; const Body: React.FC<BodyProps> = memo( - ({ size = 'small', variant = 'regular', fontSize, mono, inline, ...props }) => { - return ( - <StyledBody - $size={size} - $variant={variant} - $fontSize={fontSize} - $mono={mono} - $inline={inline} - {...props} - /> - ); - } + ({ + size = 'medium', + weight = 'regular', + color = 'primary', + fontSize, + mono, + capitalized, + inline, + ...props + }) => ( + <StyledBody + $size={size} + $weight={weight} + $fontSize={fontSize} + $mono={mono} + $capitalized={capitalized} + $inline={inline} + $color={color} + {...props} + /> + ) ); +const sizeMap = { xsmall: 10, small: 12, medium: 13, large: 15 } as const; + +const getFontFamily = (weight: NonNullable<BodyProps['weight']>, mono?: boolean) => { + return mono ? (weight !== 'regular' ? 'monoBold' : 'mono') : weight; +}; + const StyledBody = styled.p<{ - $size?: BodyProps['size']; - $variant?: BodyProps['variant']; + $size: NonNullable<BodyProps['size']>; + $weight: NonNullable<BodyProps['weight']>; + $color: NonNullable<BodyProps['color']>; $fontSize?: number; $mono?: boolean; + $capitalized?: boolean; $inline?: boolean; }>` - line-height: 1.4; + line-height: 1.2; margin: 0; - color: ${(props) => props.theme.colors.selectedTheme.text.value}; - - ${(props) => - props.$size === 'small' && - css` - font-size: 13px; - `}; - - ${(props) => - props.$size === 'medium' && - css` - font-size: 15px; - `}; - - ${(props) => - props.$size === 'large' && - css` - font-size: 18px; - `}; - - ${(props) => - props.$variant === 'bold' && - css` - font-family: ${props.theme.fonts.bold}; - `} - - ${(props) => - props.$mono && - css` - font-family: ${props.$variant === 'bold' - ? props.theme.fonts.monoBold - : props.theme.fonts.mono}; - `} - ${(props) => - props.$fontSize && + ${(props) => css` + color: ${props.theme.colors.selectedTheme.newTheme.text[props.$color]}; + font-size: ${props.$fontSize ?? sizeMap[props.$size]}px; + font-family: ${props.theme.fonts[getFontFamily(props.$weight, props.$mono)]}; + ${props.$capitalized && css` - font-size: ${props.$fontSize}px; + font-variant: all-small-caps; `} - - ${(props) => - props.$inline && + ${props.$inline && css` display: inline; `} + `} `; export default Body; diff --git a/components/Text/Display.tsx b/components/Text/Display.tsx new file mode 100644 index 0000000000..efae7e8399 --- /dev/null +++ b/components/Text/Display.tsx @@ -0,0 +1,14 @@ +import { FC } from 'react'; +import styled from 'styled-components'; + +type DisplayProps = {}; + +const Display: FC<DisplayProps> = ({ ...props }) => { + return <StyledDisplay {...props} />; +}; + +const StyledDisplay = styled.p` + line-height: 1.2; +`; + +export default Display; diff --git a/components/Text/Heading.tsx b/components/Text/Heading.tsx index 764f1904d2..8252cef5d1 100644 --- a/components/Text/Heading.tsx +++ b/components/Text/Heading.tsx @@ -1,61 +1,28 @@ -import { memo, useMemo } from 'react'; +import { memo } from 'react'; import styled, { css } from 'styled-components'; type HeadingProps = { - variant?: 'h1' | 'h2' | 'h3' | 'h4' | 'h5'; - className?: string; + variant?: 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6'; fontSize?: number; + className?: string; }; const Heading: React.FC<HeadingProps> = memo(({ variant = 'h1', fontSize, ...props }) => { - const StyledHeading = useMemo(() => headingMap[variant], [variant]); - - return <StyledHeading $fontSize={fontSize} {...props} />; + return <StyledHeading as={variant} $fontSize={fontSize} $variant={variant} {...props} />; }); -const commonStyles = css<{ $fontSize?: number }>` - line-height: 1.4; - letter-spacing: 0.2px; - margin: 0; - color: ${(props) => props.theme.colors.common.primaryWhite}; - ${(props) => - props.$fontSize && - css` - font-size: ${props.$fontSize}px; - `} -`; - -const StyledH1 = styled.h1` - font-size: 64px; - ${commonStyles} -`; - -const StyledH2 = styled.h2` - font-size: 48px; - ${commonStyles} -`; - -const StyledH3 = styled.h3` - font-size: 32px; - ${commonStyles} -`; - -const StyledH4 = styled.h4` - font-size: 20px; - ${commonStyles} -`; +const sizes = { h1: 30, h2: 26, h3: 23, h4: 21, h5: 19, h6: 16 } as const; -const StyledH5 = styled.h5` - font-size: 16px; - ${commonStyles} +const StyledHeading = styled.h1<{ + $fontSize?: number; + $variant: NonNullable<HeadingProps['variant']>; +}>` + line-height: 1.2; + margin: 0; + ${(props) => css` + color: ${props.theme.colors.common.primaryWhite}; + font-size: ${props.$fontSize ?? sizes[props.$variant]}px; + `} `; -const headingMap = { - h1: StyledH1, - h2: StyledH2, - h3: StyledH3, - h4: StyledH4, - h5: StyledH5, -}; - export default Heading; diff --git a/components/Text/NumberLabel.tsx b/components/Text/NumberLabel.tsx index 1e427dc14b..526f8de8d3 100644 --- a/components/Text/NumberLabel.tsx +++ b/components/Text/NumberLabel.tsx @@ -1,8 +1,8 @@ -import styled, { css } from 'styled-components'; +import styled from 'styled-components'; type Contrast = 'strong' | 'mild'; -const sharedStyles = css<{ fontWeight?: string; contrast?: Contrast }>` +export const NumberDiv = styled.div<{ fontWeight?: string; contrast?: Contrast }>` font-family: ${(props) => props.theme.fonts.mono}; font-weight: ${(props) => props.fontWeight || 'regular'}; color: ${({ contrast, theme }) => { @@ -12,11 +12,3 @@ const sharedStyles = css<{ fontWeight?: string; contrast?: Contrast }>` : theme.colors.selectedTheme.text.label; }}; `; - -export const NumberDiv = styled.div<{ fontWeight?: string; contrast?: Contrast }>` - ${sharedStyles} -`; - -export const NumberSpan = styled.span<{ fontWeight?: string; contrast?: Contrast }>` - ${sharedStyles} -`; diff --git a/components/Text/NumericValue.tsx b/components/Text/NumericValue.tsx new file mode 100644 index 0000000000..223d74f831 --- /dev/null +++ b/components/Text/NumericValue.tsx @@ -0,0 +1,39 @@ +import { wei, WeiSource } from '@synthetixio/wei'; +import { FC, memo, useMemo } from 'react'; +import styled from 'styled-components'; + +import Body, { BodyProps } from './Body'; + +type NumericValueProps = BodyProps & { + value: WeiSource; + preview?: boolean; + colored?: boolean; +}; + +const NumericValue: FC<NumericValueProps> = memo(({ value, preview, colored, ...props }) => { + const color = useMemo(() => { + if (preview) { + return 'preview'; + } else if (colored) { + if (wei(value).gt(0)) { + return 'positive'; + } else if (wei(value).lt(0)) { + return 'negative'; + } + } else { + return 'neutral'; + } + }, [preview, colored, value]); + + return ( + <NumberBody mono $color={color} {...props}> + {props.children ?? value.toString()} + </NumberBody> + ); +}); + +const NumberBody = styled(Body)<{ $color: 'positive' | 'negative' | 'neutral' | 'preview' }>` + color: ${(props) => props.theme.colors.selectedTheme.newTheme.text.number[props.$color]}; +`; + +export default NumericValue; diff --git a/components/Text/index.ts b/components/Text/index.ts index 52d795e1c2..4a7a578e59 100644 --- a/components/Text/index.ts +++ b/components/Text/index.ts @@ -1,5 +1,6 @@ import { default as Body } from './Body'; import { default as Heading } from './Heading'; import { default as LogoText } from './LogoText'; +import { default as NumericValue } from './NumericValue'; -export { Body, Heading, LogoText }; +export { Body, Heading, LogoText, NumericValue }; diff --git a/components/Tooltip/BaseTooltip.tsx b/components/Tooltip/BaseTooltip.tsx index eeb287385f..5b49656f78 100644 --- a/components/Tooltip/BaseTooltip.tsx +++ b/components/Tooltip/BaseTooltip.tsx @@ -17,24 +17,24 @@ interface BaseTooltipProps { export const BaseTooltip = styled.div<BaseTooltipProps>` width: max-content; - max-width: ${(props) => props.width || '472.5px'}; - background: ${(props) => props.theme.colors.selectedTheme.button.fill}; - border: ${(props) => props.theme.colors.selectedTheme.border}; box-sizing: border-box; border-radius: 8px; padding: 10px; margin: 0; - position: ${(props) => props.position || 'absolute'}; - top: ${(props) => props.top}; - bottom: ${(props) => props.bottom}; - left: ${(props) => props.left}; - right: ${(props) => props.right}; z-index: 2; + ${(props) => css` + max-width: ${props.width ?? '472.5px'}; + background: ${props.theme.colors.selectedTheme.button.fill}; + border: ${props.theme.colors.selectedTheme.border}; + position: ${props.position ?? 'absolute'}; + top: ${props.top}; + bottom: ${props.bottom}; + left: ${props.left}; + right: ${props.right}; + `} + p, span { - margin: 0; - font-size: 13px; - font-family: ${(props) => props.theme.fonts.regular}; color: ${(props) => props.theme.colors.selectedTheme.button.text.primary}; } diff --git a/components/Tooltip/ErrorTooltip.ts b/components/Tooltip/ErrorTooltip.ts index b372a0db46..8b0912981a 100644 --- a/components/Tooltip/ErrorTooltip.ts +++ b/components/Tooltip/ErrorTooltip.ts @@ -1,14 +1,16 @@ -import styled from 'styled-components'; +import styled, { css } from 'styled-components'; import StyledTooltip from './Tooltip'; const ErrorTooltip = styled(StyledTooltip)` font-size: 12px; - background-color: ${(props) => props.theme.colors.red}; - color: ${(props) => props.theme.colors.selectedTheme.button.text.primary}; - .tippy-arrow { - color: ${(props) => props.theme.colors.red}; - } + ${(props) => css` + background-color: ${props.theme.colors.red}; + color: ${props.theme.colors.selectedTheme.button.text.primary}; + .tippy-arrow { + color: ${props.theme.colors.red}; + } + `} `; export default ErrorTooltip; diff --git a/components/Tooltip/TimerTooltip.tsx b/components/Tooltip/TimerTooltip.tsx index 02d3f708c5..2ee12d9f98 100644 --- a/components/Tooltip/TimerTooltip.tsx +++ b/components/Tooltip/TimerTooltip.tsx @@ -30,7 +30,7 @@ const TimerTooltip: FC<TooltipProps> = (props) => { const [position, setPosition] = useState({}); const myRef = useRef<HTMLDivElement>(null); - const setFixedPosition = () => { + const setFixedPosition = useCallback(() => { const isFirefox = /firefox/i.test(navigator.userAgent); if (myRef.current !== null) { const { left, bottom, top } = myRef.current.getBoundingClientRect(); @@ -40,18 +40,18 @@ const TimerTooltip: FC<TooltipProps> = (props) => { setPosition({ left: `${left}px`, top: `${bottom + 20}px` }); } } - }; + }, []); - const openToolTip = () => { + const openToolTip = useCallback(() => { setActiveMouse(true); if (props.position === 'fixed') { setFixedPosition(); } - }; + }, [setFixedPosition, props.position]); - const closeToolTip = () => { + const closeToolTip = useCallback(() => { setActiveMouse(false); - }; + }, []); const startTimeDate = props.startTimeDate; diff --git a/components/Tooltip/Tooltip.tsx b/components/Tooltip/Tooltip.tsx index 0644b4f31c..4615412545 100644 --- a/components/Tooltip/Tooltip.tsx +++ b/components/Tooltip/Tooltip.tsx @@ -1,5 +1,7 @@ import { useState, useRef, memo, FC, useCallback } from 'react'; +import { Body } from 'components/Text'; + import { BaseTooltip, ToolTipWrapper } from './BaseTooltip'; // Import this tooltip to a new component and customize @@ -17,6 +19,7 @@ type TooltipProps = { style?: React.CSSProperties; position?: string; visible?: boolean; + mono?: boolean; }; const Tooltip: FC<TooltipProps> = memo((props) => { @@ -54,7 +57,7 @@ const Tooltip: FC<TooltipProps> = memo((props) => { {props.children} {activeMouse && isVisible && ( <BaseTooltip {...position} {...props} style={props.style}> - <p>{props.content}</p> + <Body mono={props.mono}>{props.content}</Body> </BaseTooltip> )} </ToolTipWrapper> diff --git a/pages/dashboard/earn.tsx b/pages/dashboard/earn.tsx index f3c7e582d2..89abc38e85 100644 --- a/pages/dashboard/earn.tsx +++ b/pages/dashboard/earn.tsx @@ -68,7 +68,6 @@ const EarnContent = styled(MainContent)` `; const StyledBody = styled(Text.Body).attrs({ size: 'large' })` - font-size: 15px; color: ${(props) => props.theme.colors.selectedTheme.gray}; `; diff --git a/sdk/README.md b/sdk/README.md index 4003d47efa..fb1638ce5b 100644 --- a/sdk/README.md +++ b/sdk/README.md @@ -56,6 +56,7 @@ The following tasks are expected to be completed before the SDK can be considere - [ ] Make all sdk number params consistent, e.g. use Wei everywhere insreads of BigNumber or string - [ ] Remove Duplicated types. - [ ] Create a standard way of passing in numeric values (particularly amounts) to the SDK. Weigh pros and cons of (`Wei`, `ethers.BigNumber` and `string`). +- [ ] Create interfaces for all the services, so we don't have to colocate method types and logic, especially when the types are verbose. ## Exchange diff --git a/sections/README.md b/sections/README.md new file mode 100644 index 0000000000..35519860b8 --- /dev/null +++ b/sections/README.md @@ -0,0 +1,17 @@ +# Kwenta Sections + +## Folder Structure + +- earn +- exchange +- dashboard +- futures +- leaderboard + +## Guidelines + +We are currently working on making this folder easier to navigate and contribute to. For this reason, there are a number of guidelines we are following closely. Please note that contributions will not be accepted unless they follow them closely as well: + +- Don't use a folder, unless absolutely necessary. Prefer `{FeatureName}.tsx` over `{FeatureName}/index.tsx`. Only use the latter when there are other components necessary to make `{FeatureName}` complete, but do not classify as base components or standalone sections. +- Don't create a styled component unless absolutely necessary. We're currently revamping the component library, to make it more feature-rich. This will enable us to create new features without having to create very specific components. +- Any instance of `eslint-disable-next-line react-hooks/exhaustive-deps` should be treated as a bug. diff --git a/sections/app/AcknowledgementModal.tsx b/sections/app/AcknowledgementModal.tsx index 0f8651986c..d9ab2845d2 100644 --- a/sections/app/AcknowledgementModal.tsx +++ b/sections/app/AcknowledgementModal.tsx @@ -4,6 +4,7 @@ import styled from 'styled-components'; import BaseModal from 'components/BaseModal'; import Button from 'components/Button'; +import { Body } from 'components/Text'; import ROUTES from 'constants/routes'; import Connector from 'containers/Connector'; import localStore from 'utils/localStore'; @@ -60,7 +61,7 @@ export default function AcknowledgementModal() { <br /> <br /> </BodyText> - <Button variant="flat" size="md" onClick={onAccept}> + <Button variant="flat" size="medium" onClick={onAccept}> Accept & Continue </Button> </StyledBaseModal> @@ -73,7 +74,6 @@ const StyledBaseModal = styled(BaseModal)` } `; -const BodyText = styled.div` - font-size: 13px; +const BodyText = styled(Body)` color: ${(props) => props.theme.colors.selectedTheme.text.body}; `; diff --git a/sections/dashboard/FuturesHistoryTable/FuturesHistoryTable.tsx b/sections/dashboard/FuturesHistoryTable/FuturesHistoryTable.tsx index c5c497659f..e2ed3cbcf6 100644 --- a/sections/dashboard/FuturesHistoryTable/FuturesHistoryTable.tsx +++ b/sections/dashboard/FuturesHistoryTable/FuturesHistoryTable.tsx @@ -11,6 +11,7 @@ import Currency from 'components/Currency'; import { DesktopOnlyView, MobileOrTabletView } from 'components/Media'; import FuturesIcon from 'components/Nav/FuturesIcon'; import Table, { TableNoResults } from 'components/Table'; +import { Body } from 'components/Text'; import { ETH_UNIT } from 'constants/network'; import { NO_VALUE } from 'constants/placeholder'; import ROUTES from 'constants/routes'; @@ -38,7 +39,7 @@ import { getDisplayAsset, getMarketName, MarketKeyByAsset } from 'utils/futures' import TimeDisplay from '../../futures/Trades/TimeDisplay'; const conditionalRender = <T,>(prop: T, children: ReactElement) => - _.isNil(prop) ? <p>{NO_VALUE}</p> : children; + _.isNil(prop) ? <Body>{NO_VALUE}</Body> : children; const FuturesHistoryTable: FC = () => { const [selectedTrade, setSelectedTrade] = useState<FuturesTrade>(); @@ -357,8 +358,7 @@ const MobileStyledCurrencyIcon = styled(Currency.Icon)` const TableContainer = styled.div` margin-top: 16px; - margin-bottom: '40px'; - font-family: ${(props) => props.theme.fonts.regular}; + margin-bottom: 40px; .paused { color: ${(props) => props.theme.colors.common.secondaryGray}; } diff --git a/sections/dashboard/FuturesMarketsTable/FuturesMarketsTable.tsx b/sections/dashboard/FuturesMarketsTable/FuturesMarketsTable.tsx index d5d0550c25..03596d72d9 100644 --- a/sections/dashboard/FuturesMarketsTable/FuturesMarketsTable.tsx +++ b/sections/dashboard/FuturesMarketsTable/FuturesMarketsTable.tsx @@ -202,17 +202,19 @@ const FuturesMarketsTable: FC = () => { Cell: (cellProps: CellProps<typeof data[number]>) => { return ( <OpenInterestContainer> - <StyledLongPrice + <Currency.Price currencyKey="sUSD" price={cellProps.row.original.longInterest} sign="$" truncate + side="positive" /> - <StyledShortPrice + <Currency.Price currencyKey="sUSD" price={cellProps.row.original.shortInterest} sign="$" truncate + side="negative" /> </OpenInterestContainer> ); @@ -389,14 +391,6 @@ const FuturesMarketsTable: FC = () => { ); }; -const StyledLongPrice = styled(Currency.Price)` - color: ${(props) => props.theme.colors.selectedTheme.green}; -`; - -const StyledShortPrice = styled(Currency.Price)` - color: ${(props) => props.theme.colors.selectedTheme.red}; -`; - const StyledCurrencyIcon = styled(Currency.Icon)` width: 30px; height: 30px; diff --git a/sections/dashboard/FuturesPositionsTable/FuturesPositionsTable.tsx b/sections/dashboard/FuturesPositionsTable/FuturesPositionsTable.tsx index b9fbeba9fc..ecca7cbbea 100644 --- a/sections/dashboard/FuturesPositionsTable/FuturesPositionsTable.tsx +++ b/sections/dashboard/FuturesPositionsTable/FuturesPositionsTable.tsx @@ -12,6 +12,7 @@ import ChangePercent from 'components/ChangePercent'; import Currency from 'components/Currency'; import { DesktopOnlyView, MobileOrTabletView } from 'components/Media'; import Table, { TableNoResults } from 'components/Table'; +import { Body } from 'components/Text'; import { EXTERNAL_LINKS } from 'constants/links'; import { NO_VALUE } from 'constants/placeholder'; import ROUTES from 'constants/routes'; @@ -47,7 +48,7 @@ const LegacyLink = () => { <Button fullWidth variant="flat" - size="sm" + size="small" noOutline={true} textTransform="none" onClick={() => window.open(EXTERNAL_LINKS.Trade.V1, '_blank', 'noopener noreferrer')} @@ -212,9 +213,7 @@ const FuturesPositionsTable: FC<FuturesPositionTableProps> = ({ accessor: 'leverage', Cell: (cellProps: CellProps<any>) => { return ( - <DefaultCell> - {formatNumber(cellProps.row.original.position.leverage ?? 0)}x - </DefaultCell> + <Body>{formatNumber(cellProps.row.original.position.leverage ?? 0)}x</Body> ); }, width: 90, @@ -253,7 +252,7 @@ const FuturesPositionsTable: FC<FuturesPositionTableProps> = ({ suggestDecimals: true, }; return cellProps.row.original.avgEntryPrice === undefined ? ( - <DefaultCell>{NO_VALUE}</DefaultCell> + <Body>{NO_VALUE}</Body> ) : ( <Currency.Price currencyKey="sUSD" @@ -364,10 +363,6 @@ const StyledValue = styled.div` grid-row: 2; `; -const DefaultCell = styled.p` - color: ${(props) => props.theme.colors.selectedTheme.button.text.primary}; -`; - const TableHeader = styled.div` color: ${(props) => props.theme.colors.selectedTheme.gray}; `; diff --git a/sections/dashboard/History/History.tsx b/sections/dashboard/History/History.tsx index 64ab677f44..f864a96193 100644 --- a/sections/dashboard/History/History.tsx +++ b/sections/dashboard/History/History.tsx @@ -69,7 +69,7 @@ const History: FC = () => { ); }; -const TabButtonsContainer = styled.div<{ mobile?: boolean }>` +export const TabButtonsContainer = styled.div<{ mobile?: boolean }>` display: flex; margin-top: 16px; margin-bottom: 16px; diff --git a/sections/dashboard/Markets/Markets.tsx b/sections/dashboard/Markets/Markets.tsx index 5f7d32f098..1c28b7d64b 100644 --- a/sections/dashboard/Markets/Markets.tsx +++ b/sections/dashboard/Markets/Markets.tsx @@ -1,12 +1,12 @@ import { FC, useState, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; -import styled from 'styled-components'; import TabButton from 'components/Button/TabButton'; import { DesktopOnlyView } from 'components/Media'; import { TabPanel } from 'components/Tab'; import FuturesMarketsTable from '../FuturesMarketsTable'; +import { TabButtonsContainer } from '../History/History'; export enum MarketsTab { FUTURES = 'futures', @@ -49,21 +49,4 @@ const Markets: FC = () => { ); }; -const TabButtonsContainer = styled.div<{ mobile?: boolean }>` - display: flex; - margin-top: 16px; - margin-bottom: 16px; - - margin-left: ${(props) => (props.mobile ? '16px' : '0')}; - - & > button { - height: 38px; - font-size: 13px; - - &:not(:last-of-type) { - margin-right: 14px; - } - } -`; - export default Markets; diff --git a/sections/dashboard/PortfolioChart/PortfolioChart.tsx b/sections/dashboard/PortfolioChart/PortfolioChart.tsx index 63c84d151b..4841310f87 100644 --- a/sections/dashboard/PortfolioChart/PortfolioChart.tsx +++ b/sections/dashboard/PortfolioChart/PortfolioChart.tsx @@ -46,7 +46,7 @@ const Chart = styled.div` height: 200px; `; -const PortfolioTitle = styled(Text.Body).attrs({ variant: 'bold' })` +const PortfolioTitle = styled(Text.Body).attrs({ weight: 'bold' })` color: ${(props) => props.theme.colors.selectedTheme.gray}; font-size: 16px; margin: 26px 0 10px 26px; diff --git a/sections/dashboard/SpotHistoryTable/SpotHistoryTable.tsx b/sections/dashboard/SpotHistoryTable/SpotHistoryTable.tsx index c58d559b20..1d3db724ab 100644 --- a/sections/dashboard/SpotHistoryTable/SpotHistoryTable.tsx +++ b/sections/dashboard/SpotHistoryTable/SpotHistoryTable.tsx @@ -8,6 +8,7 @@ import styled from 'styled-components'; import LinkIcon from 'assets/svg/app/link.svg'; import Currency from 'components/Currency'; import Table, { TableNoResults } from 'components/Table'; +import { Body } from 'components/Text'; import { CurrencyKey } from 'constants/currency'; import { NO_VALUE } from 'constants/placeholder'; import ROUTES from 'constants/routes'; @@ -28,7 +29,7 @@ type WalletTradesExchangeResult = Omit<SynthTradesExchangeResult, 'timestamp'> & }; const conditionalRender = <T,>(prop: T, children: ReactElement) => - !prop ? <p>{NO_VALUE}</p> : children; + !prop ? <Body>{NO_VALUE}</Body> : children; const SpotHistoryTable: FC = () => { const { t } = useTranslation(); diff --git a/sections/dashboard/Stake/InputCards/RedeemInputCard.tsx b/sections/dashboard/Stake/InputCards/RedeemInputCard.tsx index 8611849f4a..b35d7eb00c 100644 --- a/sections/dashboard/Stake/InputCards/RedeemInputCard.tsx +++ b/sections/dashboard/Stake/InputCards/RedeemInputCard.tsx @@ -69,7 +69,7 @@ const RedeemInputCard: FC<RedeemInputCardProps> = ({ inputLabel, isVKwenta }) => </StyledFlexDivRowCentered> </StakeInputHeader> </div> - <Button fullWidth variant="flat" size="sm" disabled={balance.eq(0)} onClick={submitRedeem}> + <Button fullWidth variant="flat" size="small" disabled={balance.eq(0)} onClick={submitRedeem}> {t(buttonTranslationKey)} </Button> </StakingInputCardContainer> diff --git a/sections/dashboard/Stake/StakingPortfolio.tsx b/sections/dashboard/Stake/StakingPortfolio.tsx index 226a1ae559..8328e39148 100644 --- a/sections/dashboard/Stake/StakingPortfolio.tsx +++ b/sections/dashboard/Stake/StakingPortfolio.tsx @@ -112,9 +112,6 @@ const StakingPortfolio: FC<StakingPortfolioProps> = ({ setCurrentTab }) => { }; const StyledTabButton = styled(TabButton)` - p { - font-size: 12px; - } margin-bottom: 9px; `; diff --git a/sections/dashboard/Stake/StakingTab.tsx b/sections/dashboard/Stake/StakingTab.tsx index 69ac19c944..8e61a832cb 100644 --- a/sections/dashboard/Stake/StakingTab.tsx +++ b/sections/dashboard/Stake/StakingTab.tsx @@ -40,7 +40,7 @@ const StakingTab = () => { <Button fullWidth variant="flat" - size="sm" + size="small" disabled={!getReward || claimableBalance.eq(0)} onClick={handleGetReward} > diff --git a/sections/dashboard/Stake/TradingRewardsTab.tsx b/sections/dashboard/Stake/TradingRewardsTab.tsx index f23640c69e..77ab4f6381 100644 --- a/sections/dashboard/Stake/TradingRewardsTab.tsx +++ b/sections/dashboard/Stake/TradingRewardsTab.tsx @@ -102,7 +102,7 @@ const TradingRewardsTab: FC<TradingRewardProps> = memo( <Button fullWidth variant="flat" - size="sm" + size="small" onClick={handleClaim} disabled={claimDisabled} > @@ -171,7 +171,7 @@ const TradingRewardsTab: FC<TradingRewardProps> = memo( <Button fullWidth variant="flat" - size="sm" + size="small" onClick={() => window.open(EXTERNAL_LINKS.Docs.TradingRewardsV2, '_blank', 'noopener noreferrer') } @@ -237,7 +237,7 @@ const TradingRewardsTab: FC<TradingRewardProps> = memo( <Button fullWidth variant="flat" - size="sm" + size="small" onClick={() => window.open(EXTERNAL_LINKS.Docs.TradingRewardsV2, '_blank', 'noopener noreferrer') } @@ -270,10 +270,9 @@ const CardGridContainer = styled(StakingCard)` height: 240px; `; -const Value = styled(Body).attrs({ variant: 'bold', mono: true })` +const Value = styled(Body).attrs({ weight: 'bold', mono: true })` color: ${(props) => props.theme.colors.selectedTheme.yellow}; font-size: 26px; - /*margin-top: 5px;*/ line-height: initial; `; diff --git a/sections/dashboard/Stake/VestConfirmationModal.tsx b/sections/dashboard/Stake/VestConfirmationModal.tsx index 42c63e122e..0657baf0c2 100644 --- a/sections/dashboard/Stake/VestConfirmationModal.tsx +++ b/sections/dashboard/Stake/VestConfirmationModal.tsx @@ -1,12 +1,13 @@ import Wei from '@synthetixio/wei'; import React from 'react'; import { Trans, useTranslation } from 'react-i18next'; -import styled from 'styled-components'; +import styled, { css } from 'styled-components'; import BaseModal from 'components/BaseModal'; import Button from 'components/Button'; import { FlexDivRowCentered } from 'components/layout/flex'; import Spacer from 'components/Spacer'; +import { Body } from 'components/Text'; import { EXTERNAL_LINKS } from 'constants/links'; import { ExternalLink } from 'styles/common'; import { truncateNumbers } from 'utils/formatters/number'; @@ -43,7 +44,7 @@ const VestConfirmationModal: React.FC<Props> = ({ onDismiss, totalFee, handleVes <Spacer height={5} /> <BalanceContainer> - <BalanceText $gold> + <BalanceText> <Trans i18nKey="dashboard.stake.tabs.escrow.modal.confirm-text" values={{ totalFee: truncateNumbers(totalFee, 4) }} @@ -75,17 +76,15 @@ const StyledBaseModal = styled(BaseModal)` const BalanceContainer = styled(FlexDivRowCentered)` margin-bottom: 8px; - p { - margin: 0; - } `; -const BalanceText = styled.p<{ $gold?: boolean }>` - color: ${(props) => - props.$gold ? props.theme.colors.selectedTheme.yellow : props.theme.colors.selectedTheme.gray}; - span { - color: ${(props) => props.theme.colors.selectedTheme.button.text.primary}; - } +const BalanceText = styled(Body)` + ${(props) => css` + color: ${props.theme.colors.selectedTheme.yellow}; + span { + color: ${props.theme.colors.selectedTheme.button.text.primary}; + } + `} `; const VestConfirmButton = styled(Button)` diff --git a/sections/dashboard/SynthBalancesTable/SynthBalancesTable.tsx b/sections/dashboard/SynthBalancesTable/SynthBalancesTable.tsx index 5880fcca3d..58b2c49580 100644 --- a/sections/dashboard/SynthBalancesTable/SynthBalancesTable.tsx +++ b/sections/dashboard/SynthBalancesTable/SynthBalancesTable.tsx @@ -10,6 +10,7 @@ import ChangePercent from 'components/ChangePercent'; import Currency from 'components/Currency'; import { MobileHiddenView, MobileOnlyView } from 'components/Media'; import Table, { TableNoResults } from 'components/Table'; +import { Body } from 'components/Text'; import { NO_VALUE } from 'constants/placeholder'; import Connector from 'containers/Connector'; import { getDisplayAsset } from 'sdk/utils/futures'; @@ -30,7 +31,7 @@ type Cell = { }; const conditionalRender = <T,>(prop: T, children: ReactElement) => - !prop ? <DefaultCell>{NO_VALUE}</DefaultCell> : children; + !prop ? <Body>{NO_VALUE}</Body> : children; type SynthBalancesTableProps = { exchangeTokens: { @@ -117,7 +118,7 @@ const SynthBalancesTable: FC<SynthBalancesTableProps> = ({ exchangeTokens }) => return conditionalRender<Cell['balance']>( cellProps.row.original.balance, <AmountCol> - <p>{formatNumber(cellProps.row.original.balance ?? 0)}</p> + <Body mono>{formatNumber(cellProps.row.original.balance ?? 0)}</Body> </AmountCol> ); }, @@ -327,10 +328,6 @@ const StyledValue = styled.div` grid-row: 2; `; -const DefaultCell = styled.p` - color: ${(props) => props.theme.colors.selectedTheme.button.text.primary}; -`; - const StyledText = styled.div` display: flex; align-items: center; diff --git a/sections/earn/StakeGrid.tsx b/sections/earn/StakeGrid.tsx index f83509bae3..6781910596 100644 --- a/sections/earn/StakeGrid.tsx +++ b/sections/earn/StakeGrid.tsx @@ -38,7 +38,7 @@ const StakeGrid = () => { <Button fullWidth variant="flat" - size="sm" + size="small" style={{ marginTop: 10 }} disabled={earnedRewards.lte(0)} onClick={handleClaim} diff --git a/sections/exchange/BasicSwap.tsx b/sections/exchange/BasicSwap.tsx index 3b1c45f1a9..d6ed275ea6 100644 --- a/sections/exchange/BasicSwap.tsx +++ b/sections/exchange/BasicSwap.tsx @@ -29,7 +29,7 @@ const BasicSwap: FC = memo(() => { export default BasicSwap; -const ExchangeTitle = styled(Text.Body).attrs({ variant: 'bold' })` +const ExchangeTitle = styled(Text.Body).attrs({ weight: 'bold' })` color: ${(props) => props.theme.colors.selectedTheme.button.text.primary}; font-size: 30px; margin-bottom: 1.5em; diff --git a/sections/exchange/FooterCard/SettleTransactionsCard.tsx b/sections/exchange/FooterCard/SettleTransactionsCard.tsx index 4b712e12d7..a685e0a57f 100644 --- a/sections/exchange/FooterCard/SettleTransactionsCard.tsx +++ b/sections/exchange/FooterCard/SettleTransactionsCard.tsx @@ -88,7 +88,7 @@ const SettleTransactionsCard: FC = memo(() => { variant="primary" disabled={!!settlementDisabledReason} onClick={handleSettle} - size="lg" + size="large" data-testid="settle" > {settlementDisabledReason ?? t('exchange.summary-info.button.settle')} diff --git a/sections/exchange/FooterCard/TradeSummaryCard.tsx b/sections/exchange/FooterCard/TradeSummaryCard.tsx index 42325348fa..34a5a8f501 100644 --- a/sections/exchange/FooterCard/TradeSummaryCard.tsx +++ b/sections/exchange/FooterCard/TradeSummaryCard.tsx @@ -1,4 +1,3 @@ -import useSynthetixQueries from '@synthetixio/queries'; import { FC, useMemo, memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; import styled from 'styled-components'; @@ -17,12 +16,9 @@ import TxApproveModal from 'sections/shared/modals/TxApproveModal'; import TxConfirmationModal from 'sections/shared/modals/TxConfirmationModal'; import { submitApprove, submitExchange } from 'state/exchange/actions'; import { - selectFeeCostWei, selectIsApproved, selectShowFee, - selectSlippagePercentWei, selectSubmissionDisabledReason, - selectTransactionFeeWei, } from 'state/exchange/selectors'; import { useAppDispatch, useAppSelector } from 'state/hooks'; import { secondsToTime } from 'utils/formatters/date'; @@ -66,31 +62,26 @@ const TradeSummaryCard: FC = memo(() => { ); }); -const SummaryItemsWrapper = () => { - const { useEthGasPriceQuery } = useSynthetixQueries(); - const ethGasPriceQuery = useEthGasPriceQuery(); - const gasPrices = useMemo(() => ethGasPriceQuery?.data, [ethGasPriceQuery.data]); - const transactionFee = useAppSelector(selectTransactionFeeWei); - const feeCost = useAppSelector(selectFeeCostWei); +const SummaryItemsWrapper = memo(() => { const showFee = useAppSelector(selectShowFee); - const slippagePercent = useAppSelector(selectSlippagePercentWei); return ( <SummaryItems> - <GasPriceSelect gasPrices={gasPrices} transactionFee={transactionFee} /> - <PriceImpactSummary slippagePercent={slippagePercent} /> + <GasPriceSelect /> + <PriceImpactSummary /> {showFee && ( <> <FeeRateSummaryItem /> - <FeeCostSummaryItem feeCost={feeCost} /> + <FeeCostSummaryItem /> </> )} </SummaryItems> ); -}; +}); -const SubmissionButton = ({ onSubmit, isApproved }: any) => { +const SubmissionButton = ({ onSubmit }: any) => { const { t } = useTranslation(); + const isApproved = useAppSelector(selectIsApproved); const submissionDisabledReason = useAppSelector(selectSubmissionDisabledReason); const isSubmissionDisabled = useMemo(() => submissionDisabledReason != null, [ @@ -101,7 +92,7 @@ const SubmissionButton = ({ onSubmit, isApproved }: any) => { <Button disabled={isSubmissionDisabled} onClick={onSubmit} - size="lg" + size="large" data-testid="submit-order" fullWidth > @@ -121,7 +112,6 @@ type TradeErrorTooltipProps = { const TradeErrorTooltip: FC<TradeErrorTooltipProps> = memo(({ onSubmit }) => { const { t } = useTranslation(); - const isApproved = useAppSelector(selectIsApproved); const { feeReclaimPeriod, quoteCurrencyKey } = useAppSelector(({ exchange }) => ({ feeReclaimPeriod: exchange.feeReclaimPeriod, quoteCurrencyKey: exchange.quoteCurrencyKey, @@ -141,7 +131,7 @@ const TradeErrorTooltip: FC<TradeErrorTooltipProps> = memo(({ onSubmit }) => { } > <span> - <SubmissionButton onSubmit={onSubmit} isApproved={isApproved} /> + <SubmissionButton onSubmit={onSubmit} /> </span> </ErrorTooltip> ); diff --git a/sections/exchange/MobileSwap/SwapButton.tsx b/sections/exchange/MobileSwap/SwapButton.tsx index 14eec771bc..2c44a49411 100644 --- a/sections/exchange/MobileSwap/SwapButton.tsx +++ b/sections/exchange/MobileSwap/SwapButton.tsx @@ -33,7 +33,7 @@ const SwapButton: FC = () => { <Button disabled={!!submissionDisabledReason} onClick={handleSubmit} - size="md" + size="medium" data-testid="submit-order" fullWidth > @@ -44,7 +44,7 @@ const SwapButton: FC = () => { : t('exchange.summary-info.button.submit-order')} </Button> ) : ( - <Button onClick={connectWallet} size="md" fullWidth noOutline> + <Button onClick={connectWallet} size="medium" fullWidth noOutline> {t('common.wallet.connect-wallet')} </Button> ); diff --git a/sections/exchange/MobileSwap/SwapInfoBox.tsx b/sections/exchange/MobileSwap/SwapInfoBox.tsx index d1a1d0acd9..374101bd43 100644 --- a/sections/exchange/MobileSwap/SwapInfoBox.tsx +++ b/sections/exchange/MobileSwap/SwapInfoBox.tsx @@ -5,7 +5,7 @@ import { useTranslation } from 'react-i18next'; import styled from 'styled-components'; import TimerIcon from 'assets/svg/app/timer.svg'; -import InfoBox from 'components/InfoBox'; +import { InfoBoxContainer, InfoBoxRow } from 'components/InfoBox'; import Tooltip from 'components/Tooltip/Tooltip'; import { NO_VALUE } from 'constants/placeholder'; import { parseGasPriceObject } from 'hooks/useGas'; @@ -20,21 +20,81 @@ import { import { useAppSelector } from 'state/hooks'; import { formatDollars, formatNumber, formatPercent, zeroBN } from 'utils/formatters/number'; -const SwapInfoBox: React.FC = () => { +const PriceImpactRow = () => { + const { t } = useTranslation(); + const slippagePercent = useAppSelector(selectSlippagePercentWei); + + return ( + <InfoBoxRow + title={t('exchange.currency-card.price-impact')} + value={slippagePercent.lt(0) ? formatPercent(slippagePercent) : NO_VALUE} + /> + ); +}; + +const FeeCostRow = () => { + const { t } = useTranslation(); + const feeCost = useAppSelector(selectFeeCostWei); + + return ( + <InfoBoxRow + title={t('common.summary.fee-cost')} + value={!!feeCost ? formatDollars(feeCost, { suggestDecimals: true }) : NO_VALUE} + /> + ); +}; + +const FeeRow = () => { const { t } = useTranslation(); - const gasSpeed = useAppSelector(selectGasSpeed); - const customGasPrice = useAppSelector(selectGasPrice); - const isL2 = useIsL2(); - const isMainnet = useIsL1(); const { exchangeFeeRate, baseFeeRate } = useAppSelector(({ exchange }) => ({ exchangeFeeRate: exchange.exchangeFeeRate, baseFeeRate: exchange.baseFeeRate, })); + return ( + <InfoBoxRow + title={t('exchange.summary-info.fee')} + value="" + valueNode={ + <div style={{ display: 'flex' }}> + {formatPercent(baseFeeRate ?? zeroBN)} + {exchangeFeeRate != null && baseFeeRate != null ? ( + wei(exchangeFeeRate) + .sub(baseFeeRate ?? 0) + .gt(0) ? ( + <> + {' + '} + <Tooltip + height="auto" + preset="bottom" + width="300px" + content="This transaction will incur an additional dynamic fee due to market volatility." + style={{ padding: 10, textTransform: 'none' }} + > + <StyledDynamicFee> + {formatPercent(wei(exchangeFeeRate).sub(baseFeeRate), { minDecimals: 2 })} + </StyledDynamicFee> + <StyledTimerIcon /> + </Tooltip> + </> + ) : null + ) : null} + </div> + } + /> + ); +}; + +const GasPriceRow = () => { + const { t } = useTranslation(); + const gasSpeed = useAppSelector(selectGasSpeed); + const customGasPrice = useAppSelector(selectGasPrice); + const isL2 = useIsL2(); + const isMainnet = useIsL1(); + const { useEthGasPriceQuery } = useSynthetixQueries(); const transactionFee = useAppSelector(selectTransactionFeeWei); - const feeCost = useAppSelector(selectFeeCostWei); const ethGasPriceQuery = useEthGasPriceQuery(); @@ -42,7 +102,7 @@ const SwapInfoBox: React.FC = () => { ethGasPriceQuery.data, ]); - const hasCustomGasPrice = customGasPrice !== ''; + const hasCustomGasPrice = !!customGasPrice; const gasPrice = gasPrices ? parseGasPriceObject(gasPrices[gasSpeed]) : null; const formattedTransactionFee = React.useMemo(() => { @@ -55,60 +115,30 @@ const SwapInfoBox: React.FC = () => { minDecimals: 2, })} Gwei`; - const slippagePercent = useAppSelector(selectSlippagePercentWei); - return ( - <StyledInfoBox - details={{ - [isMainnet + <InfoBoxRow + title={ + isMainnet ? t('common.summary.gas-prices.max-fee') - : t('common.summary.gas-prices.gas-price')]: { - value: gasPrice != null ? gasPriceItem : NO_VALUE, - }, - [t('exchange.currency-card.price-impact')]: { - value: slippagePercent?.lt(0) ? formatPercent(slippagePercent) : NO_VALUE, - }, - [t('exchange.summary-info.fee')]: { - value: '', - valueNode: ( - <div style={{ display: 'flex' }}> - {formatPercent(baseFeeRate ?? zeroBN)} - {exchangeFeeRate != null && baseFeeRate != null ? ( - wei(exchangeFeeRate) - .sub(baseFeeRate ?? 0) - .gt(0) ? ( - <> - {' + '} - <Tooltip - height="auto" - preset="bottom" - width="300px" - content="This transaction will incur an additional dynamic fee due to market volatility." - style={{ padding: 10, textTransform: 'none' }} - > - <StyledDynamicFee> - {formatPercent(wei(exchangeFeeRate).sub(baseFeeRate), { minDecimals: 2 })} - </StyledDynamicFee> - <StyledTimerIcon /> - </Tooltip> - </> - ) : null - ) : null} - </div> - ), - }, - [t('common.summary.fee-cost')]: { - value: - feeCost != null - ? formatDollars(feeCost, { minDecimals: feeCost.lt(0.01) ? 4 : 2 }) - : NO_VALUE, - }, - }} + : t('common.summary.gas-prices.gas-price') + } + value={gasPrice != null ? gasPriceItem : NO_VALUE} /> ); }; -const StyledInfoBox = styled(InfoBox)` +const SwapInfoBox: React.FC = () => { + return ( + <SwapInfoBoxContainer> + <GasPriceRow /> + <PriceImpactRow /> + <FeeRow /> + <FeeCostRow /> + </SwapInfoBoxContainer> + ); +}; + +const SwapInfoBoxContainer = styled(InfoBoxContainer)` margin-bottom: 15px; `; diff --git a/sections/exchange/TradeCard/CurrencyCard/CurrencyCardInput.tsx b/sections/exchange/TradeCard/CurrencyCard/CurrencyCardInput.tsx index f94ceaff28..a92a9a9a47 100644 --- a/sections/exchange/TradeCard/CurrencyCard/CurrencyCardInput.tsx +++ b/sections/exchange/TradeCard/CurrencyCard/CurrencyCardInput.tsx @@ -122,25 +122,28 @@ const InputLabel = styled.div` margin-left: 16px; `; -const MaxButton = styled(Button)` +const MaxButton = styled(Button).attrs({ mono: true })` width: 40px; height: 21px; font-size: 11px; - padding: 0px 10px; - margin: 10px 15px 0px 0px; - font-family: ${(props) => props.theme.fonts.mono}; + padding: 0 10px; + margin-left: 15px; `; const CurrencyAmount = styled(NumericInput)` - margin-top: 10px; - padding: 10px 16px; - font-size: 16px; border: 0; + padding: 0; height: 30px; - font-size: 30px; - line-height: 2.25em; - letter-spacing: -1px; background: transparent; + box-shadow: none; + + input { + font-size: 30px; + line-height: 2.25em; + letter-spacing: -1px; + height: 30px; + width: 100%; + } `; const CurrencyAmountContainer = styled.div<{ disableInput?: boolean }>` @@ -148,8 +151,9 @@ const CurrencyAmountContainer = styled.div<{ disableInput?: boolean }>` border: ${(props) => props.theme.colors.selectedTheme.border}; box-sizing: border-box; border-radius: 8px; - height: 84px; + min-height: 84px; width: 290px; + padding: 13px 16px; position: relative; ${(props) => @@ -165,7 +169,7 @@ const StyledLoader = styled(Loader)` const CurrencyAmountValue = styled.div` ${numericValueCSS}; - padding: 8px 8px 2px 16px; + padding: 8px 8px 2px 0; font-size: 14px; line-height: 1.25em; width: 150px; diff --git a/sections/exchange/TradeCard/CurrencyCard/MobileCurrencyCard.tsx b/sections/exchange/TradeCard/CurrencyCard/MobileCurrencyCard.tsx index f9d274dc39..85cfa17d22 100644 --- a/sections/exchange/TradeCard/CurrencyCard/MobileCurrencyCard.tsx +++ b/sections/exchange/TradeCard/CurrencyCard/MobileCurrencyCard.tsx @@ -139,13 +139,8 @@ const SwapTextInput = styled(NumericInput)` background-color: transparent; border: none; color: ${(props) => props.theme.colors.selectedTheme.text.value}; - font-size: 18px; margin-bottom: 10px; height: initial; - - &:focus { - outline: none; - } `; const SwapCurrencyPrice = styled.div` diff --git a/sections/exchange/message.ts b/sections/exchange/message.ts index 6dc77385ff..fea8c7775f 100644 --- a/sections/exchange/message.ts +++ b/sections/exchange/message.ts @@ -35,12 +35,8 @@ export const Message = styled.div` `; export const MessageButton = styled(Button).attrs({ - size: 'lg', + size: 'large', noOutline: true, isRounded: true, fullWidth: true, -})` - font-size: 17px; - height: 55px; - width: 100%; -`; +})``; diff --git a/sections/futures/CrossMarginOnboard/CrossMarginOnboard.tsx b/sections/futures/CrossMarginOnboard/CrossMarginOnboard.tsx index b4022016ba..f602aacd2a 100644 --- a/sections/futures/CrossMarginOnboard/CrossMarginOnboard.tsx +++ b/sections/futures/CrossMarginOnboard/CrossMarginOnboard.tsx @@ -11,7 +11,6 @@ import ErrorView from 'components/ErrorView'; import { notifyError } from 'components/ErrorView/ErrorNotifier'; import InputBalanceLabel from 'components/Input/InputBalanceLabel'; import NumericInput from 'components/Input/NumericInput'; -import { FlexDivRowCentered } from 'components/layout/flex'; import Loader from 'components/Loader'; import ProgressSteps from 'components/ProgressSteps'; import { MIN_MARGIN_AMOUNT } from 'constants/futures'; @@ -282,20 +281,6 @@ const Intro = styled.div` margin-bottom: 30px; `; -export const BalanceContainer = styled(FlexDivRowCentered)` - margin-bottom: 8px; - p { - margin: 0; - } -`; - -export const BalanceText = styled.p` - color: ${(props) => props.theme.colors.selectedTheme.gray}; - span { - color: ${(props) => props.theme.colors.selectedTheme.button.text.primary}; - } -`; - const Complete = styled.div` padding: 40px; text-align: center; diff --git a/sections/futures/FeeInfoBox/FeeInfoBox.tsx b/sections/futures/FeeInfoBox/FeeInfoBox.tsx index 945e010467..3e8366df7b 100644 --- a/sections/futures/FeeInfoBox/FeeInfoBox.tsx +++ b/sections/futures/FeeInfoBox/FeeInfoBox.tsx @@ -1,5 +1,5 @@ import router from 'next/router'; -import React, { useMemo, useState } from 'react'; +import React, { memo, useCallback, useMemo, useReducer } from 'react'; import { Trans, useTranslation } from 'react-i18next'; import styled from 'styled-components'; @@ -7,7 +7,8 @@ import EligibleIcon from 'assets/svg/app/eligible.svg'; import LinkArrowIcon from 'assets/svg/app/link-arrow.svg'; import NotEligibleIcon from 'assets/svg/app/not-eligible.svg'; import HelpIcon from 'assets/svg/app/question-mark.svg'; -import InfoBox, { DetailedInfo } from 'components/InfoBox/InfoBox'; +import Badge from 'components/Badge'; +import { InfoBoxContainer, InfoBoxRow } from 'components/InfoBox'; import { FlexDivRow, FlexDivRowCentered } from 'components/layout/flex'; import { Body } from 'components/Text'; import Tooltip from 'components/Tooltip/Tooltip'; @@ -17,49 +18,109 @@ import Connector from 'containers/Connector'; import { selectCrossMarginSettings, selectCrossMarginTradeFees, - selectFuturesType, selectIsolatedMarginFee, selectMarketInfo, + selectOrderFee, selectOrderType, selectTradePreview, - selectTradeSizeInputs, } from 'state/futures/selectors'; import { useAppSelector } from 'state/hooks'; import { selectStakedEscrowedKwentaBalance, selectStakedKwentaBalance, } from 'state/staking/selectors'; -import { computeOrderFee } from 'utils/costCalculations'; import { formatCurrency, formatDollars, formatPercent, zeroBN } from 'utils/formatters/number'; -const FeeInfoBox: React.FC = () => { +const MarketCostTooltip = memo(() => { const { t } = useTranslation(); - const { walletAddress } = Connector.useContainer(); + + return ( + <Tooltip + height="auto" + preset="top" + width="300px" + content={t('futures.market.trade.fees.tooltip')} + style={{ textTransform: 'none' }} + > + <StyledHelpIcon /> + </Tooltip> + ); +}); + +const ExecutionFeeTooltip = memo(() => { + const { t } = useTranslation(); + + return ( + <Tooltip + height="auto" + preset="top" + width="300px" + content={t('futures.market.trade.fees.keeper-tooltip')} + style={{ textTransform: 'none' }} + > + <StyledHelpIcon /> + </Tooltip> + ); +}); + +export const CrossMarginFeeInfoBox = memo(() => { const orderType = useAppSelector(selectOrderType); - const stakedEscrowedKwentaBalance = useAppSelector(selectStakedEscrowedKwentaBalance); - const stakedKwentaBalance = useAppSelector(selectStakedKwentaBalance); - const crossMarginFees = useAppSelector(selectCrossMarginTradeFees); - const isolatedMarginFee = useAppSelector(selectIsolatedMarginFee); - const { susdSizeDelta } = useAppSelector(selectTradeSizeInputs); - const accountType = useAppSelector(selectFuturesType); - const { tradeFee: crossMarginTradeFeeRate, limitOrderFee, stopOrderFee } = useAppSelector( - selectCrossMarginSettings + + return ( + <FeeInfoBoxContainer> + <ProtocolFeeRow /> + <LimitStopFeeRow /> + <CrossMarginFeeRow /> + <CrossMarginTotalFeeRow /> + {(orderType === 'limit' || orderType === 'stop_market') && <KeeperDepositRow />} + </FeeInfoBoxContainer> ); - const marketInfo = useAppSelector(selectMarketInfo); - const tradePreview = useAppSelector(selectTradePreview); +}); + +export const IsolatedMarginFeeInfoBox = memo(() => { + const orderType = useAppSelector(selectOrderType); + return ( + <FeeInfoBoxContainer> + {orderType === 'delayed' || orderType === 'delayed_offchain' ? ( + <> + <TotalFeesRow /> + <TradingRewardRow /> + </> + ) : ( + <FeeRow /> + )} + </FeeInfoBoxContainer> + ); +}); - const [feesExpanded, setFeesExpanded] = useState(false); +const FeeRow = memo(() => { + const isolatedMarginFee = useAppSelector(selectIsolatedMarginFee); - const commitDeposit = useMemo(() => tradePreview?.fee ?? zeroBN, [tradePreview?.fee]); + return ( + <InfoBoxRow + title="Fee" + value={formatDollars(isolatedMarginFee, { suggestDecimals: true })} + keyNode={<MarketCostTooltip />} + /> + ); +}); - const totalDeposit = useMemo(() => { - return commitDeposit.add(marketInfo?.keeperDeposit ?? zeroBN); - }, [commitDeposit, marketInfo?.keeperDeposit]); +const ProtocolFeeRow = memo(() => { + const crossMarginFees = useAppSelector(selectCrossMarginTradeFees); - const { makerFee, takerFee } = useMemo( - () => computeOrderFee(marketInfo, susdSizeDelta, orderType), - [marketInfo, susdSizeDelta, orderType] + return ( + <InfoBoxRow + title="Protocol Fee" + value={formatDollars(crossMarginFees.staticFee, { suggestDecimals: true })} + keyNode={<MarketCostTooltip />} + /> ); +}); + +const LimitStopFeeRow = memo(() => { + const crossMarginFees = useAppSelector(selectCrossMarginTradeFees); + const orderType = useAppSelector(selectOrderType); + const { limitOrderFee, stopOrderFee } = useAppSelector(selectCrossMarginSettings); const orderFeeRate = useMemo( () => @@ -67,175 +128,163 @@ const FeeInfoBox: React.FC = () => { [orderType, stopOrderFee, limitOrderFee] ); - const marketCostTooltip = useMemo( - () => ( - <Tooltip - height={'auto'} - preset="top" - width="300px" - content={t('futures.market.trade.fees.tooltip')} - style={{ textTransform: 'none' }} - > - <StyledHelpIcon /> - </Tooltip> - ), - [t] + return ( + <InfoBoxRow + dataTestId="limit-stop" + title="Limit / Stop Fee" + {...(crossMarginFees.limitStopOrderFee.gt(0) && orderFeeRate + ? { + value: formatDollars(crossMarginFees.limitStopOrderFee, { suggestDecimals: true }), + keyNode: formatPercent(orderFeeRate), + } + : { value: '' })} + /> ); +}); + +const CrossMarginFeeRow = memo(() => { + const crossMarginFees = useAppSelector(selectCrossMarginTradeFees); + const { tradeFee: crossMarginTradeFeeRate } = useAppSelector(selectCrossMarginSettings); - const executionFeeTooltip = useMemo( - () => ( - <Tooltip - height={'auto'} - preset="top" - width="300px" - content={t('futures.market.trade.fees.keeper-tooltip')} - style={{ textTransform: 'none' }} - > - <StyledHelpIcon /> - </Tooltip> - ), - [t] + return ( + <InfoBoxRow + title="Cross Margin Fee" + value={formatDollars(crossMarginFees.crossMarginFee, { suggestDecimals: true })} + keyNode={formatPercent(crossMarginTradeFeeRate)} + /> ); +}); + +const TradingRewardRow = memo(() => { + const { t } = useTranslation(); + const { walletAddress } = Connector.useContainer(); + const stakedEscrowedKwentaBalance = useAppSelector(selectStakedEscrowedKwentaBalance); + const stakedKwentaBalance = useAppSelector(selectStakedKwentaBalance); const isRewardEligible = useMemo( () => !!walletAddress && stakedKwentaBalance.add(stakedEscrowedKwentaBalance).gt(0), [walletAddress, stakedKwentaBalance, stakedEscrowedKwentaBalance] ); - const feesInfo = useMemo<Record<string, DetailedInfo | null | undefined>>(() => { - const crossMarginFeeInfo = { - 'Protocol Fee': { - value: formatDollars(crossMarginFees.staticFee, { - minDecimals: crossMarginFees.staticFee.lt(0.01) ? 4 : 2, - }), - keyNode: marketCostTooltip, - }, - 'Limit / Stop Fee': - crossMarginFees.limitStopOrderFee.gt(0) && orderFeeRate - ? { - value: formatDollars(crossMarginFees.limitStopOrderFee, { - minDecimals: crossMarginFees.limitStopOrderFee.lt(0.01) ? 4 : 2, - }), - keyNode: formatPercent(orderFeeRate), - } - : null, - 'Cross Margin Fee': { - value: formatDollars(crossMarginFees.crossMarginFee, { - minDecimals: crossMarginFees.crossMarginFee.lt(0.01) ? 4 : 2, - }), - keyNode: formatPercent(crossMarginTradeFeeRate), - }, - 'Total Fee': { - value: formatDollars(crossMarginFees.total, { - minDecimals: crossMarginFees.total.lt(0.01) ? 4 : 2, - }), - }, - }; - if (orderType === 'limit' || orderType === 'stop_market') { - return { - ...crossMarginFeeInfo, - 'Keeper Deposit': { - value: !!marketInfo?.keeperDeposit - ? formatCurrency('ETH', crossMarginFees.keeperEthDeposit, { currencyKey: 'ETH' }) - : NO_VALUE, - }, - }; - } - if (orderType === 'delayed' || orderType === 'delayed_offchain') { - return { - 'Total Fees': { - expandable: true, - value: formatDollars(totalDeposit), - subItems: { - 'Execution Fee': { - value: !!marketInfo?.keeperDeposit - ? formatDollars(marketInfo.keeperDeposit) - : NO_VALUE, - keyNode: executionFeeTooltip, - }, - [`Est. Trade Fee (${formatPercent(makerFee ?? zeroBN)} / ${formatPercent( - takerFee ?? zeroBN - )})`]: { - value: !!commitDeposit - ? formatDollars(commitDeposit, { minDecimals: commitDeposit.lt(0.01) ? 4 : 2 }) - : NO_VALUE, - keyNode: marketCostTooltip, - }, - }, - onClick: () => setFeesExpanded(!feesExpanded), - }, - 'Trading Reward': { - value: '', - compactBox: true, - spaceBeneath: false, - keyNode: ( - <CompactBox - $isEligible={isRewardEligible} - onClick={() => router.push(ROUTES.Dashboard.Stake)} - > - <FlexDivRow style={{ marginBottom: '5px' }}> - <div>{t('dashboard.stake.tabs.trading-rewards.trading-reward')}</div> - {isRewardEligible ? ( - <div className="badge badge-yellow"> - {t('dashboard.stake.tabs.trading-rewards.eligible')} - <EligibleIcon style={{ paddingLeft: '2px' }} /> - </div> - ) : ( - <div className="badge badge-red"> - {t('dashboard.stake.tabs.trading-rewards.not-eligible')} - <NotEligibleIcon style={{ paddingLeft: '2px' }} /> - </div> - )} - </FlexDivRow> - <FlexDivRowCentered> - <RewardCopy> - <Trans - i18nKey={`dashboard.stake.tabs.trading-rewards.stake-to-${ - isRewardEligible ? 'earn' : 'start' - }`} - components={[<Body variant="bold" inline={true} />]} - /> - </RewardCopy> - <StyledLinkArrowIcon /> - </FlexDivRowCentered> - </CompactBox> - ), - }, - }; - } - return accountType === 'isolated_margin' - ? { - Fee: { - value: formatDollars(isolatedMarginFee, { - minDecimals: isolatedMarginFee.lt(0.01) ? 4 : 2, - }), - keyNode: marketCostTooltip, - }, - } - : crossMarginFeeInfo; - }, [ - t, - isRewardEligible, - orderType, - totalDeposit, - crossMarginTradeFeeRate, - isolatedMarginFee, - crossMarginFees, - orderFeeRate, - commitDeposit, - accountType, - marketInfo?.keeperDeposit, - marketCostTooltip, - executionFeeTooltip, - feesExpanded, - makerFee, - takerFee, - ]); - - return <StyledInfoBox details={feesInfo} />; -}; - -const StyledInfoBox = styled(InfoBox)` + const goToStaking = useCallback(() => { + router.push(ROUTES.Dashboard.Stake); + }, []); + + return ( + <InfoBoxRow + title="Trading Reward" + compactBox + value="" + keyNode={ + <CompactBox $isEligible={isRewardEligible} onClick={goToStaking}> + <FlexDivRow style={{ marginBottom: '5px' }}> + <div>{t('dashboard.stake.tabs.trading-rewards.trading-reward')}</div> + <Badge color={isRewardEligible ? 'yellow' : 'red'}> + {t(`dashboard.stake.tabs.trading-rewards.${isRewardEligible ? '' : 'not-'}eligible`)} + {isRewardEligible ? ( + <EligibleIcon viewBox="0 0 8 8" style={{ paddingLeft: '2px' }} /> + ) : ( + <NotEligibleIcon width="12" height="12" viewBox="-1.5 -0.5 9 9" /> + )} + </Badge> + </FlexDivRow> + <FlexDivRowCentered> + <Body color="secondary"> + <Trans + i18nKey={`dashboard.stake.tabs.trading-rewards.stake-to-${ + isRewardEligible ? 'earn' : 'start' + }`} + components={[<Body weight="bold" inline />]} + /> + </Body> + <StyledLinkArrowIcon /> + </FlexDivRowCentered> + </CompactBox> + } + /> + ); +}); + +const CrossMarginTotalFeeRow = memo(() => { + const crossMarginFees = useAppSelector(selectCrossMarginTradeFees); + return ( + <InfoBoxRow + title="Total Fee" + value={formatDollars(crossMarginFees.total, { + minDecimals: crossMarginFees.total.lt(0.01) ? 4 : 2, + })} + /> + ); +}); + +const KeeperDepositRow = memo(() => { + const marketInfo = useAppSelector(selectMarketInfo); + const crossMarginFees = useAppSelector(selectCrossMarginTradeFees); + + return ( + <InfoBoxRow + title="Keeper Deposit" + value={ + !!marketInfo?.keeperDeposit + ? formatCurrency('ETH', crossMarginFees.keeperEthDeposit, { currencyKey: 'ETH' }) + : NO_VALUE + } + /> + ); +}); + +const TotalFeesRow = memo(() => { + const [expanded, toggleExpanded] = useReducer((s) => !s, false); + const tradePreview = useAppSelector(selectTradePreview); + const commitDeposit = useMemo(() => tradePreview?.fee ?? zeroBN, [tradePreview?.fee]); + const marketInfo = useAppSelector(selectMarketInfo); + const totalDeposit = useMemo(() => { + return commitDeposit.add(marketInfo?.keeperDeposit ?? zeroBN); + }, [commitDeposit, marketInfo?.keeperDeposit]); + + return ( + <InfoBoxRow + title="Total Fees" + value={formatDollars(totalDeposit)} + expandable + expanded={expanded} + onToggleExpand={toggleExpanded} + > + <ExecutionFeeRow /> + <EstimatedTradeFeeRow /> + </InfoBoxRow> + ); +}); + +const ExecutionFeeRow = memo(() => { + const marketInfo = useAppSelector(selectMarketInfo); + + return ( + <InfoBoxRow + title="Execution Fee" + value={!!marketInfo?.keeperDeposit ? formatDollars(marketInfo.keeperDeposit) : NO_VALUE} + keyNode={<ExecutionFeeTooltip />} + isSubItem + /> + ); +}); + +const EstimatedTradeFeeRow = memo(() => { + const { takerFee, makerFee } = useAppSelector(selectOrderFee); + const tradePreview = useAppSelector(selectTradePreview); + const commitDeposit = useMemo(() => tradePreview?.fee ?? zeroBN, [tradePreview?.fee]); + + return ( + <InfoBoxRow + title={`Est. Trade Fee (${formatPercent(makerFee)} / ${formatPercent(takerFee)})`} + value={!!commitDeposit ? formatDollars(commitDeposit, { suggestDecimals: true }) : NO_VALUE} + keyNode={<MarketCostTooltip />} + isSubItem + /> + ); +}); + +const FeeInfoBoxContainer = styled(InfoBoxContainer)` margin-bottom: 16px; `; @@ -248,44 +297,15 @@ const StyledLinkArrowIcon = styled(LinkArrowIcon)` fill: ${(props) => props.theme.colors.selectedTheme.text.label}; `; -const RewardCopy = styled(Body)` - color: ${(props) => props.theme.colors.selectedTheme.text.label}; -`; - const CompactBox = styled.div<{ $isEligible: boolean }>` - color: ${(props) => props.theme.colors.selectedTheme.text.value}; font-size: 13px; padding-left: 10px; cursor: pointer; margin-top: 10px; - .badge { - font-family: ${(props) => props.theme.fonts.black}; - padding: 1px 5px; - border-radius: 100px; - font-variant: all-small-caps; - } - - .badge-red { - color: ${(props) => props.theme.colors.selectedTheme.badge.red.text}; - background: ${(props) => props.theme.colors.selectedTheme.badge.red.background}; - min-width: 100px; - } - - .badge-yellow { - color: ${(props) => props.theme.colors.selectedTheme.badge.yellow.text}; - background: ${(props) => props.theme.colors.selectedTheme.badge.yellow.background}; - min-width: 70px; - } - - ${(props) => - `border-left: 2px solid - ${ - props.$isEligible - ? props.theme.colors.selectedTheme.badge.yellow.background - : props.theme.colors.selectedTheme.badge.red.background - }; + ${(props) => ` + color: ${props.theme.colors.selectedTheme.text.value}; + border-left: 2px solid + ${props.theme.colors.selectedTheme.badge[props.$isEligible ? 'yellow' : 'red'].background}; `} `; - -export default FeeInfoBox; diff --git a/sections/futures/FeeInfoBox/index.ts b/sections/futures/FeeInfoBox/index.ts index 3745949314..11fee7d0de 100644 --- a/sections/futures/FeeInfoBox/index.ts +++ b/sections/futures/FeeInfoBox/index.ts @@ -1 +1 @@ -export { default } from './FeeInfoBox'; +export { IsolatedMarginFeeInfoBox, CrossMarginFeeInfoBox } from './FeeInfoBox'; diff --git a/sections/futures/LeverageInput/LeverageInput.tsx b/sections/futures/LeverageInput/LeverageInput.tsx index 50e15f6be2..a24d5a74a5 100644 --- a/sections/futures/LeverageInput/LeverageInput.tsx +++ b/sections/futures/LeverageInput/LeverageInput.tsx @@ -1,11 +1,11 @@ import { wei } from '@synthetixio/wei'; -import { FC, memo, useCallback, useMemo, useState } from 'react'; +import { Dispatch, FC, memo, SetStateAction, useCallback, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; import styled from 'styled-components'; import Button from 'components/Button'; -import CustomNumericInput from 'components/Input/CustomNumericInput'; import InputTitle from 'components/Input/InputTitle'; +import NumericInput from 'components/Input/NumericInput'; import { FlexDivCol, FlexDivRow } from 'components/layout/flex'; import { DEFAULT_FIAT_DECIMALS } from 'constants/defaults'; import { editIsolatedMarginSize } from 'state/futures/actions'; @@ -22,6 +22,17 @@ import { floorNumber, truncateNumbers, zeroBN } from 'utils/formatters/number'; import LeverageSlider from '../LeverageSlider'; +const ModeButton: FC<{ + mode: 'slider' | 'input'; + setMode: Dispatch<SetStateAction<'slider' | 'input'>>; +}> = ({ mode, setMode }) => { + const toggleMode = useCallback(() => { + setMode((m) => (m === 'slider' ? 'input' : 'slider')); + }, [setMode]); + + return <TextButton onClick={toggleMode}>{mode === 'slider' ? 'Manual' : 'Slider'}</TextButton>; +}; + const LeverageInput: FC = memo(() => { const { t } = useTranslation(); const dispatch = useAppDispatch(); @@ -36,28 +47,16 @@ const LeverageInput: FC = memo(() => { (newLeverage: string) => { const remainingMargin = position?.remainingMargin ?? zeroBN; const newTradeSize = - marketPrice.eq(0) || remainingMargin.eq(0) + newLeverage === '' || marketPrice.eq(0) || remainingMargin.eq(0) ? '' - : wei(Number(newLeverage)).mul(remainingMargin).div(marketPrice).toString(); - const floored = floorNumber(Number(newTradeSize), 4); + : wei(newLeverage).mul(remainingMargin).div(marketPrice).toString(); + const floored = floorNumber(newTradeSize, 4); dispatch(editIsolatedMarginSize(String(floored), 'native')); dispatch(setIsolatedMarginLeverageInput(newLeverage)); }, [position?.remainingMargin, marketPrice, dispatch] ); - const modeButton = useMemo(() => { - return ( - <TextButton - onClick={() => { - setMode(mode === 'slider' ? 'input' : 'slider'); - }} - > - {mode === 'slider' ? 'Manual' : 'Slider'} - </TextButton> - ); - }, [mode]); - const isDisabled = useMemo(() => { return position?.remainingMargin.lte(0) || maxLeverage.lte(0); }, [position, maxLeverage]); @@ -79,7 +78,7 @@ const LeverageInput: FC = memo(() => { {t('futures.market.trade.input.leverage.title')}  โ€” <span>  Up to {truncateMaxLeverage}x</span> </LeverageTitle> - {modeButton} + <ModeButton mode={mode} setMode={setMode} /> </LeverageRow> {mode === 'slider' ? ( @@ -96,12 +95,12 @@ const LeverageInput: FC = memo(() => { </SliderRow> ) : ( <LeverageInputContainer> - <StyledInput + <NumericInput data-testid="leverage-input" value={leverageInput} placeholder="1" suffix="x" - maxValue={maxLeverage.toNumber()} + max={maxLeverage.toNumber()} onChange={(_, newValue) => { onLeverageChange(newValue); }} @@ -115,7 +114,7 @@ const LeverageInput: FC = memo(() => { onClick={() => { onLeverageChange(l); }} - disabled={maxLeverage.lt(Number(l)) || marketInfo?.isSuspended} + disabled={maxLeverage.lt(l) || marketInfo?.isSuspended} > {l}x </LeverageButton> @@ -170,9 +169,4 @@ const TextButton = styled.button` cursor: pointer; `; -const StyledInput = styled(CustomNumericInput)` - font-family: ${(props) => props.theme.fonts.mono}; - text-overflow: ellipsis; -`; - export default LeverageInput; diff --git a/sections/futures/MarketDetails/MarketDetail.tsx b/sections/futures/MarketDetails/MarketDetail.tsx index 400dfc314a..66c533116a 100644 --- a/sections/futures/MarketDetails/MarketDetail.tsx +++ b/sections/futures/MarketDetails/MarketDetail.tsx @@ -1,7 +1,8 @@ -import { ReactElement, memo, FC } from 'react'; +import { ReactElement, memo, FC, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; import styled from 'styled-components'; +import { Body } from 'components/Text'; import Tooltip from 'components/Tooltip/Tooltip'; import { selectMarketInfo } from 'state/futures/selectors'; import { useAppSelector } from 'state/hooks'; @@ -9,51 +10,41 @@ import { useAppSelector } from 'state/hooks'; import { isMarketDataKey, marketDataKeyMap } from './utils'; type MarketDetailProps = { - mobile: boolean; - marketKey: string; + mobile?: boolean; + dataKey: string; color?: string; value: string | ReactElement; }; -const MarketDetail: FC<MarketDetailProps> = memo(({ mobile, marketKey, color, value }) => { +const MarketDetail: FC<MarketDetailProps> = memo(({ mobile, dataKey, color, value }) => { const { t } = useTranslation(); const marketInfo = useAppSelector(selectMarketInfo); - const pausedClass = marketInfo?.isSuspended ? 'paused' : ''; - const children = ( - <WithCursor cursor="help" key={marketKey}> - <div key={marketKey}> - <p className="heading">{marketKey}</p> - <span className={`value ${color || ''} ${pausedClass}`}>{value}</span> - </div> - </WithCursor> - ); - if (marketKey === marketInfo?.marketName) { - return ( - <MarketDetailsTooltip - key={marketKey} - mobile={mobile} - content={t(`exchange.market-details-card.tooltips.market-key`)} - > - {children} - </MarketDetailsTooltip> - ); - } - - if (isMarketDataKey(marketKey)) { - return ( - <MarketDetailsTooltip - key={marketKey} - mobile={mobile} - content={t(`exchange.market-details-card.tooltips.${marketDataKeyMap[marketKey]}`)} - > - {children} - </MarketDetailsTooltip> - ); - } - - return children; + const contentSuffix = useMemo(() => { + if (dataKey === marketInfo?.marketName) { + return 'market-key'; + } else if (isMarketDataKey(dataKey)) { + return marketDataKeyMap[dataKey]; + } else { + return ''; + } + }, [dataKey, marketInfo]); + + return ( + <MarketDetailsTooltip + key={dataKey} + mobile={mobile} + content={t(`exchange.market-details-card.tooltips.${contentSuffix}`)} + > + <WithCursor cursor="help"> + <Body className="heading">{dataKey}</Body> + <Body as="span" mono className={`value ${color || ''} ${pausedClass}`}> + {value} + </Body> + </WithCursor> + </MarketDetailsTooltip> + ); }); export default MarketDetail; diff --git a/sections/futures/MarketDetails/MarketDetails.tsx b/sections/futures/MarketDetails/MarketDetails.tsx index 08a0ff0683..bb7d0b899f 100644 --- a/sections/futures/MarketDetails/MarketDetails.tsx +++ b/sections/futures/MarketDetails/MarketDetails.tsx @@ -1,20 +1,31 @@ -import React from 'react'; +import React, { memo } from 'react'; +import { useTranslation } from 'react-i18next'; import styled, { css } from 'styled-components'; +import { getColorFromPriceInfo } from 'components/ColoredPrice/ColoredPrice'; import { FlexDivCentered } from 'components/layout/flex'; +import { NO_VALUE } from 'constants/placeholder'; +import { + selectMarketAsset, + selectMarketInfo, + selectMarketPriceInfo, + selectSkewAdjustedPriceInfo, +} from 'state/futures/selectors'; +import { useAppSelector } from 'state/hooks'; +import { selectPreviousDayPrices } from 'state/prices/selectors'; import media from 'styles/media'; +import { formatDollars, formatPercent, zeroBN } from 'utils/formatters/number'; +import { getDisplayAsset } from 'utils/futures'; import MarketsDropdown from '../Trade/MarketsDropdown'; import MarketDetail from './MarketDetail'; -import useGetMarketData from './useGetMarketData'; +import { MarketDataKey } from './utils'; type MarketDetailsProps = { mobile?: boolean; }; const MarketDetails: React.FC<MarketDetailsProps> = ({ mobile }) => { - const marketData = useGetMarketData(mobile); - return ( <FlexDivCentered> {!mobile && ( @@ -24,21 +35,119 @@ const MarketDetails: React.FC<MarketDetailsProps> = ({ mobile }) => { )} <MarketDetailsContainer mobile={mobile}> - {Object.entries(marketData).map(([marketKey, data]) => ( - <MarketDetail - color={data.color} - value={data.value} - key={marketKey} - marketKey={marketKey} - mobile={Boolean(mobile)} - /> - ))} + <MarketPriceDetail /> + <IndexPriceDetail /> + <DailyChangeDetail /> + <HourlyFundingDetail /> + <OpenInterestLongDetail /> + <OpenInterestShortDetail /> </MarketDetailsContainer> </FlexDivCentered> ); }; -const MarketDetailsContainer = styled.div<{ mobile?: boolean }>` +const MarketPriceDetail = memo(() => { + const markPrice = useAppSelector(selectSkewAdjustedPriceInfo); + + return ( + <MarketDetail + color={getColorFromPriceInfo(markPrice)} + value={markPrice ? formatDollars(markPrice.price) : NO_VALUE} + dataKey={MarketDataKey.marketPrice} + /> + ); +}); + +const IndexPriceDetail = memo(() => { + const indexPrice = useAppSelector(selectMarketPriceInfo); + + return ( + <MarketDetail + dataKey={MarketDataKey.indexPrice} + value={indexPrice ? formatDollars(indexPrice.price) : NO_VALUE} + /> + ); +}); + +const DailyChangeDetail = memo(() => { + const indexPrice = useAppSelector(selectMarketPriceInfo); + const indexPriceWei = indexPrice?.price ?? zeroBN; + const pastRates = useAppSelector(selectPreviousDayPrices); + const marketAsset = useAppSelector(selectMarketAsset); + const pastPrice = pastRates.find((price) => price.synth === getDisplayAsset(marketAsset)); + + return ( + <MarketDetail + dataKey={MarketDataKey.dailyChange} + value={ + indexPriceWei.gt(0) && pastPrice?.rate + ? formatPercent(indexPriceWei.sub(pastPrice.rate).div(indexPriceWei) ?? zeroBN) + : NO_VALUE + } + color={ + pastPrice?.rate + ? indexPriceWei.sub(pastPrice.rate).gt(zeroBN) + ? 'green' + : indexPriceWei.sub(pastPrice.rate).lt(zeroBN) + ? 'red' + : '' + : undefined + } + /> + ); +}); + +const HourlyFundingDetail = memo(() => { + const { t } = useTranslation(); + const marketInfo = useAppSelector(selectMarketInfo); + const fundingValue = marketInfo?.currentFundingRate; + + return ( + <MarketDetail + dataKey={t('futures.market.info.hourly-funding')} + value={fundingValue ? formatPercent(fundingValue ?? zeroBN, { minDecimals: 6 }) : NO_VALUE} + color={fundingValue?.gt(zeroBN) ? 'green' : fundingValue?.lt(zeroBN) ? 'red' : undefined} + /> + ); +}); + +const OpenInterestLongDetail = memo(() => { + const marketInfo = useAppSelector(selectMarketInfo); + const oiCap = marketInfo?.marketLimit + ? formatDollars(marketInfo?.marketLimit, { truncate: true }) + : null; + + return ( + <MarketDetail + dataKey={MarketDataKey.openInterestLong} + value={ + marketInfo?.openInterest.longUSD + ? `${formatDollars(marketInfo?.openInterest.longUSD, { truncate: true })} / ${oiCap}` + : NO_VALUE + } + /> + ); +}); + +const OpenInterestShortDetail = memo(() => { + const marketInfo = useAppSelector(selectMarketInfo); + const oiCap = marketInfo?.marketLimit + ? formatDollars(marketInfo?.marketLimit, { truncate: true }) + : null; + + return ( + <MarketDetail + dataKey={MarketDataKey.openInterestShort} + value={ + marketInfo?.openInterest.shortUSD + ? `${formatDollars(marketInfo?.openInterest.shortUSD, { truncate: true })} / ${oiCap}` + : NO_VALUE + } + /> + ); +}); + +export const MarketDetailsContainer = styled.div<{ mobile?: boolean }>` flex: 1; gap: 26px; height: 55px; @@ -52,7 +161,6 @@ const MarketDetailsContainer = styled.div<{ mobile?: boolean }>` justify-content: space-between; align-items: start; - border: ${(props) => props.theme.colors.selectedTheme.border}; border-radius: 10px; box-sizing: border-box; @@ -62,38 +170,32 @@ const MarketDetailsContainer = styled.div<{ mobile?: boolean }>` } `} - p, - span { - margin: 0; - text-align: left; - } + ${(props) => css` + border: ${props.theme.colors.selectedTheme.border}; - .heading, - .value { - white-space: nowrap; - } + .heading { + color: ${props.theme.colors.selectedTheme.text.label}; + } - .heading { - font-size: 13px; - color: ${(props) => props.theme.colors.selectedTheme.text.label}; - } + .value { + color: ${props.theme.colors.selectedTheme.text.value}; + } - .value { - font-family: ${(props) => props.theme.fonts.mono}; - font-size: 13px; - color: ${(props) => props.theme.colors.selectedTheme.text.value}; - } + .green { + color: ${props.theme.colors.selectedTheme.green}; + } - .green { - color: ${(props) => props.theme.colors.selectedTheme.green}; - } + .red { + color: ${props.theme.colors.selectedTheme.red}; + } - .red { - color: ${(props) => props.theme.colors.selectedTheme.red}; - } + .paused { + color: ${props.theme.colors.selectedTheme.gray}; + } + `} - .paused { - color: ${(props) => props.theme.colors.selectedTheme.gray}; + .heading, .value { + white-space: nowrap; } ${(props) => diff --git a/sections/futures/MarketDetails/useGetMarketData.ts b/sections/futures/MarketDetails/useGetMarketData.ts deleted file mode 100644 index 3bf2caa222..0000000000 --- a/sections/futures/MarketDetails/useGetMarketData.ts +++ /dev/null @@ -1,149 +0,0 @@ -import { useMemo } from 'react'; -import { useTranslation } from 'react-i18next'; - -import { getColorFromPriceInfo } from 'components/ColoredPrice/ColoredPrice'; -import { DEFAULT_CRYPTO_DECIMALS } from 'constants/defaults'; -import { NO_VALUE } from 'constants/placeholder'; -import useSelectedPriceCurrency from 'hooks/useSelectedPriceCurrency'; -import { getDisplayAsset } from 'sdk/utils/futures'; -import { - selectMarketAsset, - selectMarketInfo, - selectMarketKey, - selectMarketPriceInfo, - selectSkewAdjustedPriceInfo, -} from 'state/futures/selectors'; -import { useAppSelector } from 'state/hooks'; -import { selectPreviousDayPrices } from 'state/prices/selectors'; -import { isFiatCurrency } from 'utils/currencies'; -import { formatDollars, formatPercent, zeroBN } from 'utils/formatters/number'; -import { isDecimalFour } from 'utils/futures'; - -import { MarketDataKey } from './utils'; - -type MarketData = Record<string, { value: string | JSX.Element; color?: string }>; - -const useGetMarketData = (mobile?: boolean) => { - const { t } = useTranslation(); - - const marketAsset = useAppSelector(selectMarketAsset); - const marketKey = useAppSelector(selectMarketKey); - const marketInfo = useAppSelector(selectMarketInfo); - - const pastRates = useAppSelector(selectPreviousDayPrices); - const markPrice = useAppSelector(selectSkewAdjustedPriceInfo); - const indexPrice = useAppSelector(selectMarketPriceInfo); - - const { selectedPriceCurrency } = useSelectedPriceCurrency(); - - const minDecimals = - isFiatCurrency(selectedPriceCurrency.name) && isDecimalFour(marketKey) - ? DEFAULT_CRYPTO_DECIMALS - : undefined; - - const pastPrice = pastRates.find((price) => price.synth === getDisplayAsset(marketAsset)); - - const data: MarketData = useMemo(() => { - const fundingValue = marketInfo?.currentFundingRate; - - const oiCap = marketInfo?.marketLimit - ? formatDollars(marketInfo?.marketLimit, { truncate: true }) - : null; - - const longOi = marketInfo?.openInterest.longUSD - ? `${formatDollars(marketInfo?.openInterest.longUSD, { truncate: true })} / ${oiCap}` - : NO_VALUE; - const shortOi = marketInfo?.openInterest.shortUSD - ? `${formatDollars(marketInfo?.openInterest.shortUSD, { truncate: true })} / ${oiCap}` - : NO_VALUE; - - const indexPriceWei = indexPrice?.price ?? zeroBN; - - if (mobile) { - return { - [MarketDataKey.marketPrice]: { - value: markPrice ? formatDollars(markPrice.price, { suggestDecimals: true }) : NO_VALUE, - color: getColorFromPriceInfo(markPrice), - }, - [MarketDataKey.indexPrice]: { - value: indexPrice ? formatDollars(indexPrice.price, { suggestDecimals: true }) : NO_VALUE, - }, - [MarketDataKey.dailyChange]: { - value: - indexPriceWei.gt(0) && pastPrice?.rate - ? formatPercent(indexPriceWei.sub(pastPrice.rate).div(indexPriceWei) ?? zeroBN) - : NO_VALUE, - color: pastPrice?.rate - ? indexPriceWei.sub(pastPrice.rate).gt(zeroBN) - ? 'green' - : indexPriceWei.sub(pastPrice.rate).lt(zeroBN) - ? 'red' - : '' - : undefined, - }, - [t('futures.market.info.hourly-funding')]: { - value: fundingValue - ? formatPercent(fundingValue ?? zeroBN, { minDecimals: 6 }) - : NO_VALUE, - color: fundingValue?.gt(zeroBN) ? 'green' : fundingValue?.lt(zeroBN) ? 'red' : undefined, - }, - [MarketDataKey.openInterestLong]: { - value: longOi, - }, - [MarketDataKey.openInterestShort]: { - value: shortOi, - }, - }; - } else { - return { - [MarketDataKey.marketPrice]: { - value: markPrice ? formatDollars(markPrice.price, { suggestDecimals: true }) : NO_VALUE, - color: getColorFromPriceInfo(markPrice), - }, - [MarketDataKey.indexPrice]: { - value: indexPrice ? formatDollars(indexPrice.price, { suggestDecimals: true }) : NO_VALUE, - }, - [MarketDataKey.dailyChange]: { - value: - indexPriceWei.gt(0) && pastPrice?.rate - ? formatPercent(indexPriceWei.sub(pastPrice.rate).div(indexPriceWei) ?? zeroBN) - : NO_VALUE, - color: pastPrice?.rate - ? indexPriceWei.sub(pastPrice.rate).gt(zeroBN) - ? 'green' - : indexPriceWei.sub(pastPrice.rate).lt(zeroBN) - ? 'red' - : '' - : undefined, - }, - [t('futures.market.info.hourly-funding')]: { - value: fundingValue - ? formatPercent(fundingValue ?? zeroBN, { minDecimals: 6 }) - : NO_VALUE, - color: fundingValue?.gt(zeroBN) ? 'green' : fundingValue?.lt(zeroBN) ? 'red' : undefined, - }, - [MarketDataKey.openInterestLong]: { - value: longOi, - }, - [MarketDataKey.openInterestShort]: { - value: shortOi, - }, - }; - } - - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [ - marketAsset, - markPrice, - marketInfo, - selectedPriceCurrency.name, - pastPrice?.rate, - minDecimals, - indexPrice, - t, - ]); - - return data; -}; - -export default useGetMarketData; diff --git a/sections/futures/MarketInfoBox/MarketInfoBox.tsx b/sections/futures/MarketInfoBox/MarketInfoBox.tsx index c6a7790297..a88a369dea 100644 --- a/sections/futures/MarketInfoBox/MarketInfoBox.tsx +++ b/sections/futures/MarketInfoBox/MarketInfoBox.tsx @@ -1,104 +1,92 @@ -import { wei } from '@synthetixio/wei'; -import React, { useMemo } from 'react'; +import React, { memo } from 'react'; import styled from 'styled-components'; -import InfoBox from 'components/InfoBox'; +import { InfoBoxContainer, InfoBoxRow } from 'components/InfoBox'; import PreviewArrow from 'components/PreviewArrow'; -import { PositionSide } from 'sdk/types/futures'; import { selectAvailableMargin, - selectMarketInfo, - selectMaxLeverage, - selectPosition, - selectPreviewAvailableMargin, + selectBuyingPower, + selectMarginUsage, + selectMarketSuspended, + selectPreviewTradeData, selectTradePreview, } from 'state/futures/selectors'; import { useAppSelector } from 'state/hooks'; -import { formatDollars, formatPercent, zeroBN } from 'utils/formatters/number'; +import { formatDollars, formatPercent } from 'utils/formatters/number'; -const MarketInfoBox: React.FC = () => { - const potentialTrade = useAppSelector(selectTradePreview); - - const marketInfo = useAppSelector(selectMarketInfo); - const position = useAppSelector(selectPosition); - const maxLeverage = useAppSelector(selectMaxLeverage); +const AvailableMarginRow = memo(() => { const availableMargin = useAppSelector(selectAvailableMargin); - const previewAvailableMargin = useAppSelector(selectPreviewAvailableMargin); - - const totalMargin = position?.remainingMargin ?? zeroBN; - const buyingPower = totalMargin.gt(zeroBN) ? totalMargin.mul(maxLeverage ?? zeroBN) : zeroBN; + const potentialTrade = useAppSelector(selectTradePreview); + const previewTradeData = useAppSelector(selectPreviewTradeData); + const marketSuspended = useAppSelector(selectMarketSuspended); - const marginUsage = useMemo( - () => - availableMargin.gt(zeroBN) - ? totalMargin.sub(availableMargin).div(totalMargin) - : totalMargin.gt(zeroBN) - ? wei(1) - : zeroBN, - [availableMargin, totalMargin] + return ( + <InfoBoxRow + title="Available Margin" + value={formatDollars(availableMargin, { currencyKey: undefined })} + valueNode={ + <PreviewArrow showPreview={previewTradeData.showPreview && !potentialTrade?.showStatus}> + {formatDollars(previewTradeData?.availableMargin)} + </PreviewArrow> + } + disabled={marketSuspended} + /> ); +}); - const previewTradeData = useMemo(() => { - const potentialMarginUsage = potentialTrade?.margin.gt(0) - ? potentialTrade!.margin.sub(previewAvailableMargin).div(potentialTrade!.margin).abs() ?? - zeroBN - : zeroBN; - - const maxPositionSize = - !!potentialTrade && !!marketInfo - ? potentialTrade.margin - .mul(marketInfo.maxLeverage) - .mul(potentialTrade.side === PositionSide.LONG ? 1 : -1) - : null; +const BuyingPowerRow = memo(() => { + const potentialTrade = useAppSelector(selectTradePreview); + const previewTradeData = useAppSelector(selectPreviewTradeData); + const buyingPower = useAppSelector(selectBuyingPower); + const marketSuspended = useAppSelector(selectMarketSuspended); - const potentialBuyingPower = !!maxPositionSize - ? maxPositionSize.sub(potentialTrade?.notionalValue).abs() - : zeroBN; + return ( + <InfoBoxRow + title="Buying Power" + value={formatDollars(buyingPower)} + valueNode={ + previewTradeData?.buyingPower && ( + <PreviewArrow showPreview={previewTradeData.showPreview && !potentialTrade?.showStatus}> + {formatDollars(previewTradeData?.buyingPower)} + </PreviewArrow> + ) + } + disabled={marketSuspended} + /> + ); +}); - return { - showPreview: !!potentialTrade && potentialTrade.sizeDelta.abs().gt(0), - totalMargin: potentialTrade?.margin || zeroBN, - availableMargin: previewAvailableMargin.gt(0) ? previewAvailableMargin : zeroBN, - buyingPower: potentialBuyingPower.gt(0) ? potentialBuyingPower : zeroBN, - marginUsage: potentialMarginUsage.gt(1) ? wei(1) : potentialMarginUsage, - }; - }, [potentialTrade, previewAvailableMargin, marketInfo]); +const MarginUsageRow = memo(() => { + const previewTradeData = useAppSelector(selectPreviewTradeData); + const potentialTrade = useAppSelector(selectTradePreview); + const marginUsage = useAppSelector(selectMarginUsage); + const marketSuspended = useAppSelector(selectMarketSuspended); return ( - <StyledInfoBox - dataTestId="market-info-box" - details={{ - 'Available Margin': { - value: formatDollars(availableMargin, { currencyKey: undefined }), - valueNode: ( - <PreviewArrow showPreview={previewTradeData.showPreview && !potentialTrade?.showStatus}> - {formatDollars(previewTradeData?.availableMargin)} - </PreviewArrow> - ), - }, - 'Buying Power': { - value: formatDollars(buyingPower, { currencyKey: undefined }), - valueNode: previewTradeData?.buyingPower && ( - <PreviewArrow showPreview={previewTradeData.showPreview && !potentialTrade?.showStatus}> - {formatDollars(previewTradeData?.buyingPower)} - </PreviewArrow> - ), - }, - 'Margin Usage': { - value: formatPercent(marginUsage), - valueNode: ( - <PreviewArrow showPreview={previewTradeData.showPreview && !potentialTrade?.showStatus}> - {formatPercent(previewTradeData?.marginUsage)} - </PreviewArrow> - ), - }, - }} - disabled={marketInfo?.isSuspended} + <InfoBoxRow + title="Margin Usage" + value={formatPercent(marginUsage)} + valueNode={ + <PreviewArrow showPreview={previewTradeData.showPreview && !potentialTrade?.showStatus}> + {formatPercent(previewTradeData?.marginUsage)} + </PreviewArrow> + } + disabled={marketSuspended} /> ); -}; +}); + +const MarketInfoBox: React.FC = memo(() => { + return ( + <MarketInfoBoxContainer> + <AvailableMarginRow /> + <BuyingPowerRow /> + <MarginUsageRow /> + </MarketInfoBoxContainer> + ); +}); -const StyledInfoBox = styled(InfoBox)` +const MarketInfoBoxContainer = styled(InfoBoxContainer)` margin-bottom: 16px; .value { diff --git a/sections/futures/MobileTrade/UserTabs/TransfersTab.tsx b/sections/futures/MobileTrade/UserTabs/TransfersTab.tsx index ae2dd5677d..628af852dc 100644 --- a/sections/futures/MobileTrade/UserTabs/TransfersTab.tsx +++ b/sections/futures/MobileTrade/UserTabs/TransfersTab.tsx @@ -4,6 +4,7 @@ import styled from 'styled-components'; import ColoredPrice from 'components/ColoredPrice'; import Table, { TableHeader, TableNoResults } from 'components/Table'; +import { Body } from 'components/Text'; import { SectionHeader, SectionTitle } from 'sections/futures/mobile'; import { selectMarginTransfers, selectQueryStatuses } from 'state/futures/selectors'; import { useAppSelector } from 'state/hooks'; @@ -35,7 +36,7 @@ const TransfersTab: React.FC = () => { { Header: <TableHeader>{t('futures.market.user.transfers.table.action')}</TableHeader>, accessor: 'action', - Cell: (cellProps: any) => <StyledActionCell>{cellProps.value}</StyledActionCell>, + Cell: (cellProps: any) => <ActionCell>{cellProps.value}</ActionCell>, width: 50, }, { @@ -65,9 +66,7 @@ const TransfersTab: React.FC = () => { { Header: <TableHeader>{t('futures.market.user.transfers.table.date')}</TableHeader>, accessor: 'timestamp', - Cell: (cellProps: any) => ( - <DefaultCell>{timePresentation(cellProps.value, t)}</DefaultCell> - ), + Cell: (cellProps: any) => <Body>{timePresentation(cellProps.value, t)}</Body>, width: 50, }, ]} @@ -77,7 +76,7 @@ const TransfersTab: React.FC = () => { noResultsMessage={ marginTransfers?.length === 0 ? ( <TableNoResults> - <StyledTitle>{t('futures.market.user.transfers.table.no-results')}</StyledTitle> + <Body size="large">{t('futures.market.user.transfers.table.no-results')}</Body> </TableNoResults> ) : undefined } @@ -88,18 +87,8 @@ const TransfersTab: React.FC = () => { ); }; -const DefaultCell = styled.p` - color: ${(props) => props.theme.colors.selectedTheme.button.text.primary}; -`; - -const StyledActionCell = styled(DefaultCell)` +const ActionCell = styled(Body)` text-transform: capitalize; `; -const StyledTitle = styled.p` - color: ${(props) => props.theme.colors.selectedTheme.button.text.primary}; - font-size: 16px; - margin: 0; -`; - export default TransfersTab; diff --git a/sections/futures/OrderPriceInput/OrderPriceInput.tsx b/sections/futures/OrderPriceInput/OrderPriceInput.tsx index 57aa495629..f128979152 100644 --- a/sections/futures/OrderPriceInput/OrderPriceInput.tsx +++ b/sections/futures/OrderPriceInput/OrderPriceInput.tsx @@ -4,8 +4,8 @@ import { ChangeEvent, useCallback, useEffect, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; import styled from 'styled-components'; -import CustomInput from 'components/Input/CustomInput'; import InputTitle from 'components/Input/InputTitle'; +import NumericInput from 'components/Input/NumericInput'; import SegmentedControl from 'components/SegmentedControl'; import Tooltip from 'components/Tooltip/Tooltip'; import { FuturesOrderType } from 'sdk/types/futures'; @@ -97,7 +97,7 @@ export default function OrderPriceInput({ </> )} </StyledInputTitle> - <CustomInput + <NumericInput invalid={!!minMaxLabelString} dataTestId="order-price-input" disabled={isDisabled} diff --git a/sections/futures/OrderSizing/OrderSizing.tsx b/sections/futures/OrderSizing/OrderSizing.tsx index 49187942b9..9b761538df 100644 --- a/sections/futures/OrderSizing/OrderSizing.tsx +++ b/sections/futures/OrderSizing/OrderSizing.tsx @@ -1,10 +1,10 @@ import { wei } from '@synthetixio/wei'; -import React, { ChangeEvent, useMemo, memo } from 'react'; +import React, { useMemo, memo, useCallback } from 'react'; import styled from 'styled-components'; import SwitchAssetArrows from 'assets/svg/futures/switch-arrows.svg'; -import CustomInput from 'components/Input/CustomInput'; import InputTitle from 'components/Input/InputTitle'; +import NumericInput from 'components/Input/NumericInput'; import { FlexDivRow } from 'components/layout/flex'; import { editTradeSizeInput } from 'state/futures/actions'; import { setSelectedInputDenomination } from 'state/futures/reducer'; @@ -32,6 +32,23 @@ type OrderSizingProps = { disabled?: boolean; }; +const DenominationToggle = memo(() => { + const assetInputType = useAppSelector(selectSelectedInputDenomination); + const dispatch = useAppDispatch(); + const marketAsset = useAppSelector(selectMarketAsset); + + const toggleDenomination = useCallback(() => { + dispatch(setSelectedInputDenomination(assetInputType === 'usd' ? 'native' : 'usd')); + }, [dispatch, assetInputType]); + + return ( + <InputButton onClick={toggleDenomination}> + {assetInputType === 'usd' ? 'sUSD' : getDisplayAsset(marketAsset)}{' '} + <span>{<SwitchAssetArrows />}</span> + </InputButton> + ); +}); + const OrderSizing: React.FC<OrderSizingProps> = memo(({ disabled, isMobile }) => { const dispatch = useAppDispatch(); @@ -47,36 +64,41 @@ const OrderSizing: React.FC<OrderSizingProps> = memo(({ disabled, isMobile }) => const assetInputType = useAppSelector(selectSelectedInputDenomination); const maxUsdInputAmount = useAppSelector(selectMaxUsdInputAmount); - const marketAsset = useAppSelector(selectMarketAsset); - const tradePrice = useMemo(() => (orderPrice ? wei(orderPrice) : marketAssetRate), [ orderPrice, marketAssetRate, ]); + const maxNativeValue = useMemo( () => (!isZero(tradePrice) ? maxUsdInputAmount.div(tradePrice) : zeroBN), [tradePrice, maxUsdInputAmount] ); - const onSizeChange = (value: string, assetType: 'native' | 'usd') => { - dispatch(editTradeSizeInput(value, assetType)); - }; + const onSizeChange = useCallback( + (value: string, assetType: 'native' | 'usd') => { + dispatch(editTradeSizeInput(value, assetType)); + }, + [dispatch] + ); - const handleSetMax = () => { + const handleSetMax = useCallback(() => { if (assetInputType === 'usd') { onSizeChange(String(floorNumber(maxUsdInputAmount)), 'usd'); } else { onSizeChange(String(floorNumber(maxNativeValue)), 'native'); } - }; + }, [onSizeChange, assetInputType, maxUsdInputAmount, maxNativeValue]); const handleSetPositionSize = () => { onSizeChange(position?.position?.size.toString() ?? '0', 'native'); }; - const onChangeValue = (_: ChangeEvent<HTMLInputElement>, v: string) => { - dispatch(editTradeSizeInput(v, assetInputType)); - }; + const onChangeValue = useCallback( + (_, v: string) => { + dispatch(editTradeSizeInput(v, assetInputType)); + }, + [dispatch, assetInputType] + ); const isDisabled = useMemo(() => { const remaining = @@ -114,20 +136,11 @@ const OrderSizing: React.FC<OrderSizingProps> = memo(({ disabled, isMobile }) => </InputHelpers> </OrderSizingRow> - <CustomInput + <NumericInput invalid={invalid} dataTestId={'set-order-size-amount-susd' + (isMobile ? '-mobile' : '-desktop')} disabled={isDisabled} - right={ - <InputButton - onClick={() => - dispatch(setSelectedInputDenomination(assetInputType === 'usd' ? 'native' : 'usd')) - } - > - {assetInputType === 'usd' ? 'sUSD' : getDisplayAsset(marketAsset)}{' '} - <span>{<SwitchAssetArrows />}</span> - </InputButton> - } + right={<DenominationToggle />} value={assetInputType === 'usd' ? susdSizeString : nativeSizeString} placeholder="0.00" onChange={onChangeValue} @@ -162,7 +175,7 @@ const MaxButton = styled.button` cursor: pointer; `; -const InputButton = styled.button` +export const InputButton = styled.button` height: 22px; padding: 3px 2px 4px 10px; border: none; diff --git a/sections/futures/PositionButtons/PositionButtons.tsx b/sections/futures/PositionButtons/PositionButtons.tsx index 7899d132ab..2fb126631e 100644 --- a/sections/futures/PositionButtons/PositionButtons.tsx +++ b/sections/futures/PositionButtons/PositionButtons.tsx @@ -17,26 +17,24 @@ const PositionButtons: FC<PositionButtonsProps> = memo(({ selected, onSelect }) return ( <PositionButtonsContainer> - <StyledPositionButton + <PositionButton data-testid="position-side-long-button" - fullWidth $position={PositionSide.LONG} $isActive={selected === 'long'} disabled={marketInfo?.isSuspended} onClick={() => onSelect(PositionSide.LONG)} > <span>Long</span> - </StyledPositionButton> - <StyledPositionButton + </PositionButton> + <PositionButton data-testid="position-side-short-button" - fullWidth $position={PositionSide.SHORT} $isActive={selected === 'short'} disabled={marketInfo?.isSuspended} onClick={() => onSelect(PositionSide.SHORT)} > <span>Short</span> - </StyledPositionButton> + </PositionButton> </PositionButtonsContainer> ); }); @@ -54,9 +52,12 @@ const PositionButtonsContainer = styled.div` margin-top: 8px; `; -const StyledPositionButton = styled(Button)<PositionButtonProps>` - font-size: 14px; - height: 40px; +const PositionButton = styled(Button).attrs({ fullWidth: true })<PositionButtonProps>` + font-size: 16px; + height: 57px; + font-variant: all-small-caps; + text-transform: uppercase; + border-radius: 8px; &:active { transform: scale(0.96); @@ -66,58 +67,33 @@ const StyledPositionButton = styled(Button)<PositionButtonProps>` position: relative; } - ${(props) => - props.$position === PositionSide.LONG && - css` - color: ${props.theme.colors.selectedTheme.green}; - ${props.$isActive && - css` - border: 1px solid ${props.theme.colors.selectedTheme.green}; - border-radius: 8px; - background: linear-gradient( - 180deg, - rgba(127, 212, 130, 0.15) 0%, - rgba(71, 122, 73, 0.05) 100% - ); - box-shadow: rgb(127 212 130 / 50%) 0px 0 3px; + ${(props) => css` + font-family: ${props.theme.fonts.bold}; + color: ${props.theme.colors.selectedTheme.newTheme.button.position[props.$position].color}; + background: ${props.theme.colors.selectedTheme.newTheme.button.position.background}; - &::before { - display: none; - } - &:hover { - background: linear-gradient( - 180deg, - rgba(127, 212, 130, 0.15) 0%, - rgba(71, 122, 73, 0.05) 100% - ); - } - `}; - `}; + &:hover { + background: ${props.theme.colors.selectedTheme.newTheme.button.position.hover.background}; + } + `} ${(props) => - props.$position === PositionSide.SHORT && + props.$isActive && css` - color: ${props.theme.colors.selectedTheme.red}; - ${props.$isActive && - css` - border: 1px solid rgba(239, 104, 104, 0.7); - border-radius: 8px; - background: linear-gradient( - 180deg, - rgba(239, 104, 104, 0.15) 0%, - rgba(116, 56, 56, 0.05) 100% - ); - box-shadow: rgb(239 104 104 / 50%) 0px 0 3px; - &::before { - display: none; - } - &:hover { - background: linear-gradient( - 180deg, - rgba(239, 104, 104, 0.15) 0%, - rgba(116, 56, 56, 0.05) 100% - ); - `}; + &::before { + display: none; + } + + border: 1px solid + ${props.theme.colors.selectedTheme.newTheme.button.position[props.$position].active.border}; + background: ${props.theme.colors.selectedTheme.newTheme.button.position[props.$position] + .active.background}; + color: ${props.theme.colors.selectedTheme.newTheme.button.position[props.$position].active + .color}; + &:hover { + background: ${props.theme.colors.selectedTheme.newTheme.button.position[props.$position] + .active.background}; + } `}; `; diff --git a/sections/futures/PositionCard/ClosePositionModal.tsx b/sections/futures/PositionCard/ClosePositionModal.tsx index 714742791c..c05b4a6a33 100644 --- a/sections/futures/PositionCard/ClosePositionModal.tsx +++ b/sections/futures/PositionCard/ClosePositionModal.tsx @@ -109,7 +109,7 @@ function ClosePositionModal({ <StyledButton data-testid="trade-close-position-confirm-order-button" variant="flat" - size="lg" + size="large" onClick={onClosePosition} disabled={!!error || disabled || isClosing} > diff --git a/sections/futures/PositionCard/PositionCard.tsx b/sections/futures/PositionCard/PositionCard.tsx index 75c67513bc..16c5048e6f 100644 --- a/sections/futures/PositionCard/PositionCard.tsx +++ b/sections/futures/PositionCard/PositionCard.tsx @@ -1,19 +1,14 @@ -import Wei from '@synthetixio/wei'; -import React from 'react'; +import { memo } from 'react'; import { useTranslation } from 'react-i18next'; import styled, { css } from 'styled-components'; import ColoredPrice from 'components/ColoredPrice'; import { FlexDivCentered, FlexDivCol } from 'components/layout/flex'; import PreviewArrow from 'components/PreviewArrow'; -import { Body } from 'components/Text'; +import { Body, NumericValue } from 'components/Text'; import Tooltip from 'components/Tooltip/Tooltip'; -import { DEFAULT_CRYPTO_DECIMALS } from 'constants/defaults'; import { NO_VALUE } from 'constants/placeholder'; -import Connector from 'containers/Connector'; -import useAverageEntryPrice from 'hooks/useAverageEntryPrice'; import useFuturesMarketClosed from 'hooks/useFuturesMarketClosed'; -import useSelectedPriceCurrency from 'hooks/useSelectedPriceCurrency'; import { PositionSide } from 'sdk/types/futures'; import { setOpenModal } from 'state/app/reducer'; import { selectOpenModal } from 'state/app/selectors'; @@ -21,259 +16,25 @@ import { selectMarketAsset, selectMarketKey, selectPosition, - selectTradePreview, - selectFuturesType, selectSkewAdjustedPrice, selectMarketPriceInfo, selectSelectedMarketPositionHistory, + selectPreviewData, + selectFuturesType, } from 'state/futures/selectors'; import { useAppDispatch, useAppSelector } from 'state/hooks'; import { PillButtonDiv } from 'styles/common'; import media from 'styles/media'; -import { isFiatCurrency } from 'utils/currencies'; import { formatDollars, formatPercent, zeroBN } from 'utils/formatters/number'; import { formatNumber } from 'utils/formatters/number'; -import { getMarketName, getSynthDescription, isDecimalFour, MarketKeyByAsset } from 'utils/futures'; +import { getMarketName } from 'utils/futures'; import EditLeverageModal from '../TradeCrossMargin/EditCrossMarginLeverageModal'; -type PositionCardProps = { - dashboard?: boolean; -}; - -type PositionData = { - marketShortName: string; - marketLongName: string; - marketPrice: JSX.Element; - positionSide: JSX.Element; - positionSize: string | React.ReactNode; - leverage: string | React.ReactNode; - liquidationPrice: string | JSX.Element; - pnl: string | Wei | JSX.Element; - realizedPnl: Wei; - pnlText: string | null | JSX.Element; - realizedPnlText: string | JSX.Element; - netFunding: Wei; - netFundingText: string | null | JSX.Element; - fees: string | JSX.Element; - avgEntryPrice: string | JSX.Element; -}; - -type PositionPreviewData = { - fillPrice: Wei; - sizeIsNotZero: boolean; - positionSide: string; - positionSize: Wei; - leverage: Wei; - liquidationPrice: Wei; - avgEntryPrice: Wei; - notionalValue: Wei; - showStatus: boolean; -}; - -const PositionCard: React.FC<PositionCardProps> = () => { - const { t } = useTranslation(); - const { synthsMap } = Connector.useContainer(); - const { selectedPriceCurrency } = useSelectedPriceCurrency(); - const dispatch = useAppDispatch(); - - const futuresAccountType = useAppSelector(selectFuturesType); - const position = useAppSelector(selectPosition); - const marketAsset = useAppSelector(selectMarketAsset); +const PositionCard = memo(() => { const marketKey = useAppSelector(selectMarketKey); - const marketPrice = useAppSelector(selectSkewAdjustedPrice); - const previewTradeData = useAppSelector(selectTradePreview); - const thisPositionHistory = useAppSelector(selectSelectedMarketPositionHistory); const openModal = useAppSelector(selectOpenModal); const { isFuturesMarketClosed } = useFuturesMarketClosed(marketKey); - const marketPriceInfo = useAppSelector(selectMarketPriceInfo); - - const positionDetails = position?.position ?? null; - - const minDecimals = - isFiatCurrency(selectedPriceCurrency.name) && isDecimalFour(marketKey) - ? DEFAULT_CRYPTO_DECIMALS - : undefined; - - const modifiedAverage = useAverageEntryPrice(thisPositionHistory); - - const previewData: PositionPreviewData = React.useMemo(() => { - if (positionDetails === null || previewTradeData === null) { - return {} as PositionPreviewData; - } - - const size: Wei = previewTradeData?.size; - const newSide = size?.gt(zeroBN) ? PositionSide.LONG : PositionSide.SHORT; - - return { - fillPrice: previewTradeData.price, - sizeIsNotZero: size && !size?.eq(0), - positionSide: newSide, - positionSize: size?.abs(), - notionalValue: previewTradeData.notionalValue, - leverage: previewTradeData.margin.gt(0) - ? previewTradeData.notionalValue.div(previewTradeData.margin).abs() - : zeroBN, - liquidationPrice: previewTradeData.liqPrice, - avgEntryPrice: modifiedAverage || zeroBN, - showStatus: previewTradeData.showStatus, - }; - }, [positionDetails, previewTradeData, modifiedAverage]); - - const data: PositionData = React.useMemo(() => { - const pnl = positionDetails?.pnl ?? zeroBN; - const pnlPct = positionDetails?.pnlPct ?? zeroBN; - const realizedPnl = - thisPositionHistory?.pnl - .add(thisPositionHistory?.netFunding) - .sub(thisPositionHistory?.feesPaid) ?? zeroBN; - const realizedPnlPct = realizedPnl.abs().gt(0) - ? realizedPnl.div(thisPositionHistory?.initialMargin.add(thisPositionHistory?.totalDeposits)) - : zeroBN; - const netFunding = - positionDetails?.accruedFunding.add(thisPositionHistory?.netFunding ?? zeroBN) ?? zeroBN; - - return { - currencyIconKey: MarketKeyByAsset[marketAsset], - marketShortName: marketAsset ? getMarketName(marketAsset) : 'Select a market', - marketLongName: getSynthDescription(marketAsset, synthsMap, t), - marketPrice: ( - <> - {formatDollars(marketPrice, { minDecimals, suggestDecimals: true })} - { - <PreviewArrow showPreview={previewData.sizeIsNotZero && !previewData.showStatus}> - {formatDollars(previewData.fillPrice ?? zeroBN, { - minDecimals, - suggestDecimals: true, - })} - </PreviewArrow> - } - </> - ), - positionSide: positionDetails ? ( - <PositionValue - side={positionDetails.side === 'long' ? PositionSide.LONG : PositionSide.SHORT} - > - {positionDetails.side === 'long' ? PositionSide.LONG : PositionSide.SHORT} - {previewData.positionSide !== positionDetails.side && ( - <PreviewArrow - showPreview={ - previewData.sizeIsNotZero && - previewData.positionSide !== positionDetails.side && - !previewData.showStatus - } - > - <PositionValue side={previewData.positionSide as PositionSide}> - {previewData.positionSide} - </PositionValue> - </PreviewArrow> - )} - </PositionValue> - ) : ( - <StyledValue>{NO_VALUE}</StyledValue> - ), - positionSize: positionDetails ? ( - <> - {`${formatNumber(positionDetails.size ?? 0, { - minDecimals: positionDetails.size.abs().lt(0.01) ? 4 : 2, - })} (${formatDollars(positionDetails.notionalValue?.abs() ?? zeroBN, { - minDecimals: positionDetails.notionalValue?.abs()?.lt(0.01) ? 4 : 2, - })})`} - <PreviewArrow - showPreview={ - previewData.positionSize && previewData.sizeIsNotZero && !previewData.showStatus - } - > - {`${formatNumber(previewData.positionSize ?? 0, { - minDecimals: 2, - })} (${formatDollars(previewData.notionalValue?.abs() ?? zeroBN, { - minDecimals: 2, - })})`} - </PreviewArrow> - </> - ) : ( - NO_VALUE - ), - leverage: positionDetails ? ( - <> - {formatNumber(positionDetails?.leverage ?? zeroBN) + 'x'} - { - <PreviewArrow showPreview={previewData.sizeIsNotZero && !previewData.showStatus}> - {formatNumber(previewData?.leverage ?? zeroBN) + 'x'} - </PreviewArrow> - } - </> - ) : ( - NO_VALUE - ), - liquidationPrice: positionDetails ? ( - <> - {formatDollars(positionDetails?.liquidationPrice ?? zeroBN, { - minDecimals, - suggestDecimals: true, - })} - { - <PreviewArrow showPreview={previewData.sizeIsNotZero && !previewData.showStatus}> - {formatDollars(previewData?.liquidationPrice ?? zeroBN, { - minDecimals, - suggestDecimals: true, - })} - </PreviewArrow> - } - </> - ) : ( - NO_VALUE - ), - pnl: pnl ?? NO_VALUE, - realizedPnl: realizedPnl, - pnlText: - positionDetails && pnl - ? `${formatDollars(pnl, { - minDecimals: pnl.abs().lt(0.01) ? 4 : 2, - })} (${formatPercent(pnlPct)})` - : NO_VALUE, - realizedPnlText: - thisPositionHistory && realizedPnl - ? `${formatDollars(realizedPnl, { - minDecimals: realizedPnl.abs().lt(0.01) ? 4 : 2, - })} (${formatPercent(realizedPnlPct)})` - : NO_VALUE, - netFunding: netFunding, - netFundingText: netFunding - ? `${formatDollars(netFunding, { - minDecimals: netFunding.abs().lt(0.01) ? 4 : 2, - })}` - : null, - fees: positionDetails ? formatDollars(thisPositionHistory?.feesPaid ?? zeroBN) : NO_VALUE, - avgEntryPrice: positionDetails ? ( - <> - {formatDollars(thisPositionHistory?.entryPrice ?? zeroBN, { - minDecimals, - suggestDecimals: true, - })} - { - <PreviewArrow showPreview={previewData.sizeIsNotZero && !previewData.showStatus}> - {formatDollars(previewData.avgEntryPrice ?? zeroBN, { - minDecimals, - suggestDecimals: true, - })} - </PreviewArrow> - } - </> - ) : ( - NO_VALUE - ), - }; - }, [ - positionDetails, - thisPositionHistory, - marketPrice, - marketAsset, - synthsMap, - t, - previewData, - minDecimals, - ]); return ( <> @@ -281,120 +42,313 @@ const PositionCard: React.FC<PositionCardProps> = () => { <Container id={isFuturesMarketClosed ? 'closed' : undefined}> <DataCol> - <InfoRow> - <Subtitle>{data.marketShortName}</Subtitle> - <ColoredPrice priceInfo={marketPriceInfo}>{data.marketPrice}</ColoredPrice> - </InfoRow> - <InfoRow> - <PositionCardTooltip content={t('futures.market.position-card.tooltips.position-side')}> - <SubtitleWithCursor> - {t('futures.market.position-card.position-side')} - </SubtitleWithCursor> - </PositionCardTooltip> - <div data-testid="position-card-side-value">{data.positionSide}</div> - </InfoRow> - <InfoRow> - <PositionCardTooltip content={t('futures.market.position-card.tooltips.position-size')}> - <SubtitleWithCursor> - {t('futures.market.position-card.position-size')} - </SubtitleWithCursor> - </PositionCardTooltip> - <StyledValue>{data.positionSize}</StyledValue> - </InfoRow> + <MarketNameRow /> + <PositionSideRow /> + <PositionSizeRow /> </DataCol> <DataColDivider /> <DataCol> - <InfoRow> - <PositionCardTooltip content={t('futures.market.position-card.tooltips.net-funding')}> - <SubtitleWithCursor> - {t('futures.market.position-card.net-funding')} - </SubtitleWithCursor> - </PositionCardTooltip> - {positionDetails ? ( - <StyledValue - className={ - data.netFunding > zeroBN ? 'green' : data.netFunding < zeroBN ? 'red' : '' - } - > - {data.netFundingText} - </StyledValue> - ) : ( - <StyledValue>{NO_VALUE}</StyledValue> - )} - </InfoRow> - <InfoRow> - <PositionCardTooltip content={t('futures.market.position-card.tooltips.u-pnl')}> - <SubtitleWithCursor>{t('futures.market.position-card.u-pnl')}</SubtitleWithCursor> - </PositionCardTooltip> - {positionDetails ? ( - <StyledValue className={data.pnl > zeroBN ? 'green' : data.pnl < zeroBN ? 'red' : ''}> - {data.pnlText} - </StyledValue> - ) : ( - <StyledValue>{NO_VALUE}</StyledValue> - )} - </InfoRow> - <InfoRow> - <PositionCardTooltip content={t('futures.market.position-card.tooltips.r-pnl')}> - <SubtitleWithCursor>{t('futures.market.position-card.r-pnl')}</SubtitleWithCursor> - </PositionCardTooltip> - {positionDetails ? ( - <StyledValue - className={ - data.realizedPnl > zeroBN ? 'green' : data.realizedPnl < zeroBN ? 'red' : '' - } - > - {data.realizedPnlText} - </StyledValue> - ) : ( - <StyledValue>{NO_VALUE}</StyledValue> - )} - </InfoRow> + <NetFundingRow /> + <UnrealizedPNLRow /> + <RealizedPNLRow /> </DataCol> <DataColDivider /> <DataCol> - <InfoRow> - <PositionCardTooltip content={t('futures.market.position-card.tooltips.leverage')}> - <SubtitleWithCursor>{t('futures.market.position-card.leverage')}</SubtitleWithCursor> - </PositionCardTooltip> - <FlexDivCentered> - <StyledValue data-testid="position-card-leverage-value">{data.leverage}</StyledValue> - {position?.position && futuresAccountType === 'cross_margin' && ( - <PillButtonDiv onClick={() => dispatch(setOpenModal('futures_cross_leverage'))}> - Edit - </PillButtonDiv> - )} - </FlexDivCentered> - </InfoRow> - <InfoRow> - <PositionCardTooltip - content={t('futures.market.position-card.tooltips.liquidation-price')} - > - <SubtitleWithCursor> - {t('futures.market.position-card.liquidation-price')} - </SubtitleWithCursor> - </PositionCardTooltip> - <StyledValue>{data.liquidationPrice}</StyledValue> - </InfoRow> - <InfoRow> - <PositionCardTooltip - content={t('futures.market.position-card.tooltips.avg-entry-price')} - > - <SubtitleWithCursor> - {t('futures.market.position-card.avg-entry-price')} - </SubtitleWithCursor> - </PositionCardTooltip> - <StyledValue>{data.avgEntryPrice}</StyledValue> - </InfoRow> + <LeverageRow /> + <LiquidationPriceRow /> + <AverageEntryPriceRow /> </DataCol> </Container> </> ); -}; +}); + +const MarketNameRow = memo(() => { + const marketPriceInfo = useAppSelector(selectMarketPriceInfo); + const marketAsset = useAppSelector(selectMarketAsset); + const marketPrice = useAppSelector(selectSkewAdjustedPrice); + const marketShortName = getMarketName(marketAsset); + const previewData = useAppSelector(selectPreviewData); + + return ( + <InfoRow> + <Subtitle>{marketShortName}</Subtitle> + <ColoredPrice priceInfo={marketPriceInfo}> + {formatDollars(marketPrice, { suggestDecimals: true })} + <PreviewArrow showPreview={previewData.sizeIsNotZero && !previewData.showStatus}> + {formatDollars(previewData.fillPrice ?? zeroBN, { suggestDecimals: true })} + </PreviewArrow> + </ColoredPrice> + </InfoRow> + ); +}); + +const PositionSideRow = memo(() => { + const { t } = useTranslation(); + const position = useAppSelector(selectPosition); + const previewData = useAppSelector(selectPreviewData); + const positionDetails = position?.position; + + return ( + <InfoRow> + <PositionCardTooltip content={t('futures.market.position-card.tooltips.position-side')}> + <SubtitleWithCursor>{t('futures.market.position-card.position-side')}</SubtitleWithCursor> + </PositionCardTooltip> + <div data-testid="position-card-side-value"> + {positionDetails ? ( + <PositionValue side={positionDetails.side}> + {positionDetails.side} + {previewData.positionSide !== positionDetails.side && ( + <PreviewArrow + showPreview={ + previewData.sizeIsNotZero && + previewData.positionSide !== positionDetails.side && + !previewData.showStatus + } + > + <PositionValue side={previewData.positionSide as PositionSide}> + {previewData.positionSide} + </PositionValue> + </PreviewArrow> + )} + </PositionValue> + ) : ( + <StyledValue>{NO_VALUE}</StyledValue> + )} + </div> + </InfoRow> + ); +}); + +const PositionSizeRow = memo(() => { + const { t } = useTranslation(); + const position = useAppSelector(selectPosition); + const previewData = useAppSelector(selectPreviewData); + const positionDetails = position?.position; + + return ( + <InfoRow> + <PositionCardTooltip content={t('futures.market.position-card.tooltips.position-size')}> + <SubtitleWithCursor>{t('futures.market.position-card.position-size')}</SubtitleWithCursor> + </PositionCardTooltip> + <StyledValue> + {positionDetails ? ( + <> + {`${formatNumber(positionDetails.size ?? 0, { + suggestDecimals: true, + })} (${formatDollars(positionDetails.notionalValue?.abs() ?? zeroBN, { + suggestDecimals: true, + })})`} + <PreviewArrow + showPreview={ + previewData.positionSize && previewData.sizeIsNotZero && !previewData.showStatus + } + > + {`${formatNumber(previewData.positionSize ?? 0, { + minDecimals: 2, + })} (${formatDollars(previewData.notionalValue?.abs() ?? zeroBN, { + minDecimals: 2, + })})`} + </PreviewArrow> + </> + ) : ( + NO_VALUE + )} + </StyledValue> + </InfoRow> + ); +}); + +const NetFundingRow = memo(() => { + const { t } = useTranslation(); + const position = useAppSelector(selectPosition); + const positionDetails = position?.position; + const positionHistory = useAppSelector(selectSelectedMarketPositionHistory); + + const netFunding = + positionDetails?.accruedFunding.add(positionHistory?.netFunding ?? zeroBN) ?? zeroBN; + + const netFundingText = netFunding + ? `${formatDollars(netFunding, { + minDecimals: netFunding.abs().lt(0.01) ? 4 : 2, + })}` + : null; + + return ( + <InfoRow> + <PositionCardTooltip content={t('futures.market.position-card.tooltips.net-funding')}> + <SubtitleWithCursor>{t('futures.market.position-card.net-funding')}</SubtitleWithCursor> + </PositionCardTooltip> + {positionDetails ? ( + <NumericValue colored value={netFunding}> + {netFundingText} + </NumericValue> + ) : ( + <StyledValue>{NO_VALUE}</StyledValue> + )} + </InfoRow> + ); +}); + +const UnrealizedPNLRow = memo(() => { + const { t } = useTranslation(); + const position = useAppSelector(selectPosition); + const positionDetails = position?.position; + + const pnl = positionDetails?.pnl ?? zeroBN; + const pnlPct = positionDetails?.pnlPct ?? zeroBN; + const pnlText = + positionDetails && pnl + ? `${formatDollars(pnl, { + minDecimals: pnl.abs().lt(0.01) ? 4 : 2, + })} (${formatPercent(pnlPct)})` + : NO_VALUE; + + return ( + <InfoRow> + <PositionCardTooltip content={t('futures.market.position-card.tooltips.u-pnl')}> + <SubtitleWithCursor>{t('futures.market.position-card.u-pnl')}</SubtitleWithCursor> + </PositionCardTooltip> + {positionDetails ? ( + <NumericValue colored value={pnl}> + {pnlText} + </NumericValue> + ) : ( + <StyledValue>{NO_VALUE}</StyledValue> + )} + </InfoRow> + ); +}); + +const RealizedPNLRow = memo(() => { + const { t } = useTranslation(); + const positionHistory = useAppSelector(selectSelectedMarketPositionHistory); + const position = useAppSelector(selectPosition); + const positionDetails = position?.position; + const realizedPnl = + positionHistory?.pnl.add(positionHistory?.netFunding).sub(positionHistory?.feesPaid) ?? zeroBN; + const realizedPnlPct = realizedPnl.abs().gt(0) + ? realizedPnl.div(positionHistory?.initialMargin.add(positionHistory?.totalDeposits)) + : zeroBN; + const realizedPnlText = + positionHistory && realizedPnl + ? `${formatDollars(realizedPnl, { + minDecimals: realizedPnl.abs().lt(0.01) ? 4 : 2, + })} (${formatPercent(realizedPnlPct)})` + : NO_VALUE; + + return ( + <InfoRow> + <PositionCardTooltip content={t('futures.market.position-card.tooltips.r-pnl')}> + <SubtitleWithCursor>{t('futures.market.position-card.r-pnl')}</SubtitleWithCursor> + </PositionCardTooltip> + {positionDetails ? ( + <NumericValue colored value={realizedPnl}> + {realizedPnlText} + </NumericValue> + ) : ( + <StyledValue>{NO_VALUE}</StyledValue> + )} + </InfoRow> + ); +}); + +const LeverageRow = memo(() => { + const { t } = useTranslation(); + const futuresAccountType = useAppSelector(selectFuturesType); + const dispatch = useAppDispatch(); + const position = useAppSelector(selectPosition); + const previewData = useAppSelector(selectPreviewData); + const positionDetails = position?.position; + + return ( + <InfoRow> + <PositionCardTooltip content={t('futures.market.position-card.tooltips.leverage')}> + <SubtitleWithCursor>{t('futures.market.position-card.leverage')}</SubtitleWithCursor> + </PositionCardTooltip> + <FlexDivCentered> + <StyledValue data-testid="position-card-leverage-value"> + {positionDetails ? ( + <> + {formatNumber(positionDetails?.leverage ?? zeroBN) + 'x'} + <PreviewArrow showPreview={previewData.sizeIsNotZero && !previewData.showStatus}> + {formatNumber(previewData?.leverage ?? zeroBN) + 'x'} + </PreviewArrow> + </> + ) : ( + NO_VALUE + )} + </StyledValue> + {position?.position && futuresAccountType === 'cross_margin' && ( + <PillButtonDiv onClick={() => dispatch(setOpenModal('futures_cross_leverage'))}> + Edit + </PillButtonDiv> + )} + </FlexDivCentered> + </InfoRow> + ); +}); + +const LiquidationPriceRow = memo(() => { + const { t } = useTranslation(); + const position = useAppSelector(selectPosition); + const previewData = useAppSelector(selectPreviewData); + const positionDetails = position?.position; + + return ( + <InfoRow> + <PositionCardTooltip content={t('futures.market.position-card.tooltips.liquidation-price')}> + <SubtitleWithCursor> + {t('futures.market.position-card.liquidation-price')} + </SubtitleWithCursor> + </PositionCardTooltip> + <StyledValue> + {positionDetails ? ( + <> + {formatDollars(positionDetails?.liquidationPrice ?? zeroBN, { suggestDecimals: true })} + <PreviewArrow showPreview={previewData.sizeIsNotZero && !previewData.showStatus}> + {formatDollars(previewData?.liquidationPrice ?? zeroBN, { suggestDecimals: true })} + </PreviewArrow> + </> + ) : ( + NO_VALUE + )} + </StyledValue> + </InfoRow> + ); +}); + +const AverageEntryPriceRow = memo(() => { + const { t } = useTranslation(); + const position = useAppSelector(selectPosition); + const previewData = useAppSelector(selectPreviewData); + const positionDetails = position?.position; + const positionHistory = useAppSelector(selectSelectedMarketPositionHistory); + + return ( + <InfoRow> + <PositionCardTooltip content={t('futures.market.position-card.tooltips.avg-entry-price')}> + <SubtitleWithCursor>{t('futures.market.position-card.avg-entry-price')}</SubtitleWithCursor> + </PositionCardTooltip> + <StyledValue> + {positionDetails ? ( + <> + {formatDollars(positionHistory?.entryPrice ?? zeroBN, { suggestDecimals: true })} + <PreviewArrow showPreview={previewData.sizeIsNotZero && !previewData.showStatus}> + {formatDollars(previewData.avgEntryPrice ?? zeroBN, { suggestDecimals: true })} + </PreviewArrow> + </> + ) : ( + NO_VALUE + )} + </StyledValue> + </InfoRow> + ); +}); export default PositionCard; -const Container = styled.div` +export const Container = styled.div` display: grid; grid-template-columns: 1fr 30px 1fr 30px 1fr; background-color: transparent; @@ -410,11 +364,11 @@ const Container = styled.div` `} `; -const DataCol = styled(FlexDivCol)` +export const DataCol = styled(FlexDivCol)` justify-content: space-between; `; -const DataColDivider = styled.div` +export const DataColDivider = styled.div` width: 1px; background-color: #2b2a2a; margin: 0 15px; @@ -426,7 +380,7 @@ const DataColDivider = styled.div` `} `; -const InfoRow = styled.div` +export const InfoRow = styled.div` display: flex; justify-content: space-between; line-height: 16px; @@ -444,8 +398,7 @@ const InfoRow = styled.div` } `; -const Subtitle = styled(Body)` - font-size: 13px; +export const Subtitle = styled(Body)` color: ${(props) => props.theme.colors.selectedTheme.text.label}; text-transform: capitalize; `; @@ -459,8 +412,7 @@ const PositionCardTooltip = styled(Tooltip).attrs({ preset: 'fixed', height: 'au padding: 10px; `; -const StyledValue = styled(Body).attrs({ mono: true })` - font-size: 13px; +export const StyledValue = styled(Body).attrs({ mono: true })` color: ${(props) => props.theme.colors.selectedTheme.button.text.primary}; text-align: end; ${Container}#closed & { @@ -468,7 +420,7 @@ const StyledValue = styled(Body).attrs({ mono: true })` } `; -const PositionValue = styled.span<{ side?: PositionSide }>` +export const PositionValue = styled.span<{ side?: PositionSide }>` font-family: ${(props) => props.theme.fonts.bold}; font-size: 13px; text-transform: uppercase; diff --git a/sections/futures/ProfitCalculator/LabelWithInput.tsx b/sections/futures/ProfitCalculator/LabelWithInput.tsx index 4ba011d17b..ad21778bd2 100644 --- a/sections/futures/ProfitCalculator/LabelWithInput.tsx +++ b/sections/futures/ProfitCalculator/LabelWithInput.tsx @@ -1,7 +1,8 @@ -import React from 'react'; +import React, { memo } from 'react'; import styled from 'styled-components'; -import CustomInput from 'components/Input/CustomInput'; +import NumericInput from 'components/Input/NumericInput'; +import { Body } from 'components/Text'; type LabelWithInputProps = { disabled?: boolean; @@ -14,41 +15,27 @@ type LabelWithInputProps = { defaultValue?: any; }; -const LabelWithInput: React.FC<LabelWithInputProps> = ({ - id, - right, - value, - onChange, - disabled, - labelText, - placeholder, - defaultValue, -}) => ( - <> - <LabelText>{labelText}</LabelText> - <CustomInput - id={id} - right={right} - value={value} - disabled={disabled} - onChange={onChange} - placeholder={placeholder} - defaultValue={defaultValue} - style={{ display: '', flex: '', width: '100%' }} - /> - </> +const LabelWithInput: React.FC<LabelWithInputProps> = memo( + ({ id, right, value, onChange, disabled, labelText, placeholder, defaultValue }) => ( + <> + <LabelText>{labelText}</LabelText> + <NumericInput + id={id} + right={right} + value={value} + disabled={disabled} + onChange={onChange} + placeholder={placeholder} + defaultValue={defaultValue} + style={{ display: '', flex: '', width: '100%' }} + /> + </> + ) ); -const LabelText = styled.p` - width: 100.91px; +const LabelText = styled(Body).attrs({ size: 'small' })` height: 12px; - left: 479px; - top: 329px; - margin-left: 12.1px; - font-size: 12px; - line-height: 12px; - - color: ${(props) => props.theme.colors.selectedTheme.button.text.primary}; + margin: 8px 12px; `; export default LabelWithInput; diff --git a/sections/futures/ProfitCalculator/ProfitCalculator.tsx b/sections/futures/ProfitCalculator/ProfitCalculator.tsx index 9f6d1cdf9c..4efede72eb 100644 --- a/sections/futures/ProfitCalculator/ProfitCalculator.tsx +++ b/sections/futures/ProfitCalculator/ProfitCalculator.tsx @@ -1,12 +1,12 @@ import { wei } from '@synthetixio/wei'; -import { useCallback, useEffect, useState, FC } from 'react'; +import { useCallback, useEffect, useState, FC, memo } from 'react'; import { useTranslation } from 'react-i18next'; import styled from 'styled-components'; import BaseModal from 'components/BaseModal'; -import { FuturesMarketAsset, PositionSide } from 'sdk/types/futures'; +import { PositionSide } from 'sdk/types/futures'; import PositionButtons from 'sections/futures/PositionButtons'; -import { selectMarketPrice } from 'state/futures/selectors'; +import { selectMarketAsset, selectMarketPrice } from 'state/futures/selectors'; import { useAppSelector } from 'state/hooks'; import { getMarketName } from 'utils/futures'; @@ -15,11 +15,11 @@ import PnLs from './PnLs'; import ProfitDetails from './ProfitDetails'; type ProfitCalculatorProps = { - marketAsset: FuturesMarketAsset; setOpenProfitCalcModal(val: boolean): void; }; -const ProfitCalculator: FC<ProfitCalculatorProps> = ({ marketAsset, setOpenProfitCalcModal }) => { +const ProfitCalculator: FC<ProfitCalculatorProps> = memo(({ setOpenProfitCalcModal }) => { + const marketAsset = useAppSelector(selectMarketAsset); const marketName = getMarketName(marketAsset); const marketAsset__RemovedSChar = marketAsset[0] === 's' ? marketAsset.slice(1) : marketAsset; const { t } = useTranslation(); @@ -226,7 +226,7 @@ const ProfitCalculator: FC<ProfitCalculatorProps> = ({ marketAsset, setOpenProfi </StyledBaseModal> </> ); -}; +}); const StyledBaseModal = styled(BaseModal)` [data-reach-dialog-content] { diff --git a/sections/futures/ProfitCalculator/ProfitDetails.tsx b/sections/futures/ProfitCalculator/ProfitDetails.tsx index 79741207b0..b5353b799c 100644 --- a/sections/futures/ProfitCalculator/ProfitDetails.tsx +++ b/sections/futures/ProfitCalculator/ProfitDetails.tsx @@ -1,8 +1,9 @@ import { wei } from '@synthetixio/wei'; import React from 'react'; import { useTranslation } from 'react-i18next'; -import styled from 'styled-components'; +import styled, { css } from 'styled-components'; +import { Body } from 'components/Text'; import { PositionSide } from 'sdk/types/futures'; function textColor(props: any) { @@ -34,93 +35,101 @@ const ProfitDetails: React.FC<ProfitDetailsProps> = ({ return ( <> - <StyledProfitDetails> + <ProfitDetailsContainer> {/* ENTRY ORDER */} - <RowName> - <RowText className="row-name"> + <ProfitDetailsRow> + <RowText> {t('futures.modals.profit-calculator.profit-details.row-name.entry-price')} </RowText> - </RowName> - <Details style={{ justifySelf: 'right' }}> - <RowText className={leverageSide}>{`${entryOrderDetails}`}</RowText> - <RowText style={{ marginLeft: '2px' }}>{`,`}</RowText> - <RowText style={{ marginLeft: '10px' }}> - {t('futures.modals.profit-calculator.profit-details.details.market')} - </RowText> - </Details> + <Details> + <RowText className={leverageSide}>{`${entryOrderDetails}`}</RowText> + <RowText style={{ marginLeft: '2px' }}>{`,`}</RowText> + <RowText style={{ marginLeft: '10px' }}> + {t('futures.modals.profit-calculator.profit-details.details.market')} + </RowText> + </Details> + </ProfitDetailsRow> {/* TAKE PROFIT */} - <RowName> - <RowText className="row-name"> + <ProfitDetailsRow> + <RowText> {t('futures.modals.profit-calculator.profit-details.row-name.take-profit')} </RowText> - </RowName> - <Details style={{ justifySelf: 'right' }}> - <RowText>{t('futures.modals.profit-calculator.profit-details.details.sell')}</RowText> - <RowText style={{ marginRight: '10px', marginLeft: '10px' }} className="gray-font-color"> - {t('futures.modals.profit-calculator.profit-details.details.at')} - </RowText> - <RowText>{exitPrice !== '' ? '$' + wei(exitPrice).toNumber().toFixed(2) : ''}</RowText> - </Details> + <Details> + <RowText>{t('futures.modals.profit-calculator.profit-details.details.sell')}</RowText> + <RowText + style={{ marginRight: '10px', marginLeft: '10px' }} + className="gray-font-color" + > + {t('futures.modals.profit-calculator.profit-details.details.at')} + </RowText> + <RowText>{exitPrice !== '' ? '$' + wei(exitPrice).toNumber().toFixed(2) : ''}</RowText> + </Details> + </ProfitDetailsRow> {/* STOP LOSS */} - <RowName> - <RowText className="row-name"> + <ProfitDetailsRow> + <RowText> {t('futures.modals.profit-calculator.profit-details.row-name.stop-loss')} </RowText> - </RowName> - <Details style={{ justifySelf: 'right' }}> - <RowText>{t('futures.modals.profit-calculator.profit-details.details.sell')}</RowText> - <RowText style={{ marginRight: '10px', marginLeft: '10px' }} className="gray-font-color"> - {t('futures.modals.profit-calculator.profit-details.details.at')} - </RowText> - <RowText>{stopLoss !== '' ? '$' + wei(stopLoss).toNumber().toFixed(2) : ''}</RowText> - </Details> + <Details> + <RowText>{t('futures.modals.profit-calculator.profit-details.details.sell')}</RowText> + <RowText + style={{ marginRight: '10px', marginLeft: '10px' }} + className="gray-font-color" + > + {t('futures.modals.profit-calculator.profit-details.details.at')} + </RowText> + <RowText>{stopLoss !== '' ? '$' + wei(stopLoss).toNumber().toFixed(2) : ''}</RowText> + </Details> + </ProfitDetailsRow> {/* SIZE */} - <RowName> - <RowText className="row-name"> - {t('futures.modals.profit-calculator.profit-details.row-name.size')} - </RowText> - </RowName> - <Details style={{ justifySelf: 'right' }}> - <RowText style={{ marginRight: '10px' }}> - {marketAssetPositionSize !== '' - ? wei(marketAssetPositionSize).toNumber().toFixed(2) - : ''} - </RowText> - <RowText className="gray-font-color">{marketName}</RowText> - </Details> - </StyledProfitDetails> + <ProfitDetailsRow> + <RowText>{t('futures.modals.profit-calculator.profit-details.row-name.size')}</RowText> + <Details> + <RowText style={{ marginRight: '10px' }}> + {marketAssetPositionSize !== '' + ? wei(marketAssetPositionSize).toNumber().toFixed(2) + : ''} + </RowText> + <RowText className="gray-font-color">{marketName}</RowText> + </Details> + </ProfitDetailsRow> + </ProfitDetailsContainer> </> ); }; -const RowName = styled.div` - margin-left: 15px; -`; - -const Details = styled.div` - margin-right: 15px; -`; +const Details = styled.div``; -const RowText = styled.p` +const RowText = styled(Body).attrs({ size: 'small' })` display: inline-block; - color: ${(props) => textColor(props)}; - - font-size: 12px; - line-height: 10px; - text-align: ${(props) => (props.className === 'row-name' ? 'left' : 'right')}; + ${(props) => css` + color: ${textColor(props)}; + text-align: ${props.className === 'row-name' ? 'left' : 'right'}; + `} `; -const StyledProfitDetails = styled.div` - display: grid; - grid-gap: 0rem; - grid-template-columns: repeat(2, 1fr); - - border: ${(props) => props.theme.colors.selectedTheme.border}; +const ProfitDetailsContainer = styled.div` + margin-top: 20px; + padding: 18px 20px; box-sizing: border-box; + border: ${(props) => props.theme.colors.selectedTheme.border}; border-radius: 6px; +`; - margin-top: 20px; +const ProfitDetailsRow = styled.div` + display: flex; + justify-content: space-between; + align-items: center; + + &:not(:first-of-type) { + padding-top: 12px; + } + + &:not(:last-of-type) { + padding-bottom: 12px; + border-bottom: ${(props) => props.theme.colors.selectedTheme.border}; + } `; export default ProfitDetails; diff --git a/sections/futures/ProfitCalculator/StatWithContainer.tsx b/sections/futures/ProfitCalculator/StatWithContainer.tsx index 12dfef5b9a..3500eacff2 100644 --- a/sections/futures/ProfitCalculator/StatWithContainer.tsx +++ b/sections/futures/ProfitCalculator/StatWithContainer.tsx @@ -1,5 +1,7 @@ import styled from 'styled-components'; +import { Body } from 'components/Text'; + type StatWithContainerProps = { label: string; stat: any; @@ -16,32 +18,25 @@ function pnlText(type: number, stat: any) { export const StatWithContainer: React.FC<StatWithContainerProps> = ({ label, stat, type }) => { return ( - <> - <StatContainer> - <StatLabel>{label}</StatLabel> - <Stat colorNum={type}>{pnlText(type, stat)}</Stat> - </StatContainer> - </> + <StatContainer> + <StatLabel>{label}</StatLabel> + <Stat colorNum={type}>{pnlText(type, stat)}</Stat> + </StatContainer> ); }; -const Stat = styled.div<{ colorNum: any }>` - font-size: 15px; - line-height: 15px; - margin: -7.5px 0px 0px 12px; +const Stat = styled(Body).attrs({ size: 'large' })<{ colorNum: any }>` color: ${(props) => textColor(props)}; `; -const StatLabel = styled.p` - font-size: 13px; - line-height: 12px; +const StatLabel = styled(Body)` color: ${(props) => props.theme.colors.selectedTheme.button.text.primary}; - margin-left: 12px; + margin-top: 4px; `; const StatContainer = styled.div` width: auto; - height: 59px; + padding: 15px; border: ${(props) => props.theme.colors.selectedTheme.border}; box-sizing: border-box; border-radius: 6px; diff --git a/sections/futures/ShareModal/ShareModalButton.tsx b/sections/futures/ShareModal/ShareModalButton.tsx index 801a1f6b7a..3430a1a0f7 100644 --- a/sections/futures/ShareModal/ShareModalButton.tsx +++ b/sections/futures/ShareModal/ShareModalButton.tsx @@ -81,17 +81,17 @@ const ShareModalButton: FC<ShareModalButtonProps> = ({ position }) => { <> <DesktopOnlyView> <ButtonContainer> - <Button variant="primary" onClick={handleDownloadImage} size="sm" disabled={false}> + <Button variant="primary" onClick={handleDownloadImage} size="small" disabled={false}> {t('futures.modals.share.buttons.download')} </Button> </ButtonContainer> </DesktopOnlyView> <MobileOrTabletView> <ButtonContainer> - <Button variant="secondary" onClick={handleDownloadImage} size="sm" disabled={false}> + <Button variant="secondary" onClick={handleDownloadImage} size="small" disabled={false}> {t('futures.modals.share.buttons.download')} </Button> - <Button variant="primary" onClick={handleTweet} size="sm" disabled={false}> + <Button variant="primary" onClick={handleTweet} size="small" disabled={false}> <InnerButtonContainer> <span>{t('futures.modals.share.buttons.twitter')}</span> <StyledTwitterIcon /> diff --git a/sections/futures/Trade/DelayedOrderConfirmationModal.tsx b/sections/futures/Trade/DelayedOrderConfirmationModal.tsx index a46f4a3dbe..b41fa74fb2 100644 --- a/sections/futures/Trade/DelayedOrderConfirmationModal.tsx +++ b/sections/futures/Trade/DelayedOrderConfirmationModal.tsx @@ -10,7 +10,6 @@ import { FlexDivCentered } from 'components/layout/flex'; import { ButtonLoader } from 'components/Loader/Loader'; import { DesktopOnlyView, MobileOrTabletView } from 'components/Media'; import Tooltip from 'components/Tooltip/Tooltip'; -import useSelectedPriceCurrency from 'hooks/useSelectedPriceCurrency'; import { PositionSide } from 'sdk/types/futures'; import { getDisplayAsset, OrderNameByType } from 'sdk/utils/futures'; import { setOpenModal } from 'state/app/reducer'; @@ -45,7 +44,6 @@ import { MobileConfirmTradeButton } from './TradeConfirmationModal'; const DelayedOrderConfirmationModal: FC = () => { const { t } = useTranslation(); const isDisclaimerDisplayed = useAppSelector(selectNextPriceDisclaimer); - const { selectedPriceCurrency } = useSelectedPriceCurrency(); const dispatch = useAppDispatch(); const { nativeSizeDelta } = useAppSelector(selectTradeSizeInputs); @@ -126,16 +124,14 @@ const DelayedOrderConfirmationModal: FC = () => { }, { label: t('futures.market.user.position.modal.fee-estimated'), - value: formatCurrency(selectedPriceCurrency.name, potentialTradeDetails?.fee ?? zeroBN, { + value: formatDollars(potentialTradeDetails?.fee ?? zeroBN, { minDecimals: 2, - sign: selectedPriceCurrency.sign, }), }, { label: t('futures.market.user.position.modal.keeper-deposit'), - value: formatCurrency(selectedPriceCurrency.name, marketInfo?.keeperDeposit ?? zeroBN, { + value: formatDollars(marketInfo?.keeperDeposit ?? zeroBN, { minDecimals: 2, - sign: selectedPriceCurrency.sign, }), }, { @@ -154,8 +150,6 @@ const DelayedOrderConfirmationModal: FC = () => { totalDeposit, marketInfo?.keeperDeposit, marketInfo?.settings.offchainDelayedOrderMinAge, - selectedPriceCurrency.name, - selectedPriceCurrency.sign, ] ); diff --git a/sections/futures/Trade/MarketsDropdown.tsx b/sections/futures/Trade/MarketsDropdown.tsx index 723fcbd9ae..15be5fd4c0 100644 --- a/sections/futures/Trade/MarketsDropdown.tsx +++ b/sections/futures/Trade/MarketsDropdown.tsx @@ -5,11 +5,9 @@ import { useTranslation } from 'react-i18next'; import styled, { css } from 'styled-components'; import Select from 'components/Select'; -import { DEFAULT_CRYPTO_DECIMALS } from 'constants/defaults'; import ROUTES from 'constants/routes'; import Connector from 'containers/Connector'; import useFuturesMarketClosed, { FuturesClosureReason } from 'hooks/useFuturesMarketClosed'; -import useSelectedPriceCurrency from 'hooks/useSelectedPriceCurrency'; import { FuturesMarketAsset, FuturesMarketKey } from 'sdk/types/futures'; import { getDisplayAsset } from 'sdk/utils/futures'; import { @@ -22,8 +20,8 @@ import { import { useAppSelector } from 'state/hooks'; import { selectPreviousDayPrices } from 'state/prices/selectors'; import { FetchStatus } from 'state/types'; -import { formatCurrency, formatPercent, zeroBN } from 'utils/formatters/number'; -import { getMarketName, getSynthDescription, isDecimalFour, MarketKeyByAsset } from 'utils/futures'; +import { formatDollars, formatPercent, zeroBN } from 'utils/formatters/number'; +import { getMarketName, getSynthDescription, MarketKeyByAsset } from 'utils/futures'; import MarketsDropdownIndicator, { DropdownLoadingIndicator } from './MarketsDropdownIndicator'; import MarketsDropdownOption from './MarketsDropdownOption'; @@ -74,7 +72,6 @@ const MarketsDropdown: React.FC<MarketsDropdownProps> = ({ mobile }) => { MarketKeyByAsset[marketAsset] ); - const { selectedPriceCurrency } = useSelectedPriceCurrency(); const router = useRouter(); const { synthsMap } = Connector.useContainer(); const { t } = useTranslation(); @@ -94,47 +91,28 @@ const MarketsDropdown: React.FC<MarketsDropdownProps> = ({ mobile }) => { const selectedBasePriceRate = getBasePriceRate(marketAsset); const selectedPastPrice = getPastPrice(marketAsset); - const getMinDecimals = React.useCallback( - (asset: string) => (isDecimalFour(asset) ? DEFAULT_CRYPTO_DECIMALS : undefined), - [] - ); - const options = React.useMemo(() => { - return ( - futuresMarkets?.map((market) => { - const pastPrice = getPastPrice(market.asset); - const basePriceRate = getBasePriceRate(market.asset); - - return assetToCurrencyOption({ - asset: market.asset, - key: market.marketKey, - description: getSynthDescription(market.asset, synthsMap, t), - price: formatCurrency(selectedPriceCurrency.name, basePriceRate, { - sign: '$', - minDecimals: getMinDecimals(market.asset), - suggestDecimals: true, - }), - change: formatPercent( - basePriceRate && pastPrice?.rate - ? wei(basePriceRate).sub(pastPrice?.rate).div(basePriceRate) - : zeroBN - ), - negativeChange: - basePriceRate && pastPrice?.rate ? wei(basePriceRate).lt(pastPrice?.rate) : false, - isMarketClosed: market.isSuspended, - closureReason: market.marketClosureReason, - }); - }) ?? [] - ); - }, [ - futuresMarkets, - selectedPriceCurrency.name, - synthsMap, - t, - getBasePriceRate, - getPastPrice, - getMinDecimals, - ]); + return futuresMarkets.map((market) => { + const pastPrice = getPastPrice(market.asset); + const basePriceRate = getBasePriceRate(market.asset); + + return assetToCurrencyOption({ + asset: market.asset, + key: market.marketKey, + description: getSynthDescription(market.asset, synthsMap, t), + price: formatDollars(basePriceRate, { suggestDecimals: true }), + change: formatPercent( + basePriceRate && pastPrice?.rate + ? wei(basePriceRate).sub(pastPrice?.rate).div(basePriceRate) + : zeroBN + ), + negativeChange: + basePriceRate && pastPrice?.rate ? wei(basePriceRate).lt(pastPrice?.rate) : false, + isMarketClosed: market.isSuspended, + closureReason: market.marketClosureReason, + }); + }); + }, [futuresMarkets, synthsMap, t, getBasePriceRate, getPastPrice]); const isFetching = !futuresMarkets.length && marketsQueryStatus.status === FetchStatus.Loading; @@ -144,7 +122,7 @@ const MarketsDropdown: React.FC<MarketsDropdownProps> = ({ mobile }) => { maxMenuHeight={Math.max(window.innerHeight - (mobile ? 135 : 250), 300)} instanceId={`markets-dropdown-${marketAsset}`} controlHeight={55} - menuWidth={'100%'} + menuWidth="100%" onChange={(x) => { // Types are not perfect from react-select, this should always be true (just helping typescript) if (x && 'value' in x) { @@ -156,11 +134,7 @@ const MarketsDropdown: React.FC<MarketsDropdownProps> = ({ mobile }) => { key: MarketKeyByAsset[marketAsset], description: getSynthDescription(marketAsset, synthsMap, t), price: mobile - ? formatCurrency(selectedPriceCurrency.name, selectedBasePriceRate, { - sign: '$', - minDecimals: getMinDecimals(marketAsset), - suggestDecimals: true, - }) + ? formatDollars(selectedBasePriceRate, { suggestDecimals: true }) : undefined, change: mobile ? formatPercent( diff --git a/sections/futures/Trade/MarketsDropdownOption.tsx b/sections/futures/Trade/MarketsDropdownOption.tsx index 98a9a25a8f..a1ceeb3156 100644 --- a/sections/futures/Trade/MarketsDropdownOption.tsx +++ b/sections/futures/Trade/MarketsDropdownOption.tsx @@ -1,3 +1,4 @@ +import { memo } from 'react'; import { components, OptionProps } from 'react-select'; import styled, { css } from 'styled-components'; @@ -9,7 +10,7 @@ import { Body } from 'components/Text'; import { MarketsCurrencyOption } from './MarketsDropdown'; import { CurrencyLabel, SingleValueContainer } from './MarketsDropdownSingleValue'; -const MarketsDropdownOption: React.FC<OptionProps<MarketsCurrencyOption>> = (props) => ( +const MarketsDropdownOption: React.FC<OptionProps<MarketsCurrencyOption>> = memo((props) => ( <components.Option {...props}> <OptionDetailsContainer $isSelected={props.isSelected}> <CurrencyMeta $isSelected={props.isSelected}> @@ -36,12 +37,13 @@ const MarketsDropdownOption: React.FC<OptionProps<MarketsCurrencyOption>> = (pro </div> </OptionDetailsContainer> </components.Option> -); +)); const StyledCurrencyLabel = styled(CurrencyLabel)` color: ${(props) => props.theme.colors.selectedTheme.white}; font-size: 13px; `; + const CurrencyMeta = styled(FlexDivCentered)<{ $isSelected: boolean }>` gap: 7px; width: 125px; @@ -65,10 +67,6 @@ const OptionDetailsContainer = styled(SingleValueContainer)<{ $isSelected: boole justify-content: space-between; gap: 5px; - p { - margin: 0; - } - .price { font-family: ${(props) => props.theme.fonts.mono}; color: ${(props) => props.theme.colors.selectedTheme.gray}; diff --git a/sections/futures/Trade/MarketsDropdownSingleValue.tsx b/sections/futures/Trade/MarketsDropdownSingleValue.tsx index ee638b1d1a..86c58c23c7 100644 --- a/sections/futures/Trade/MarketsDropdownSingleValue.tsx +++ b/sections/futures/Trade/MarketsDropdownSingleValue.tsx @@ -5,6 +5,7 @@ import styled from 'styled-components'; import MarketBadge from 'components/Badge/MarketBadge'; import CurrencyIcon from 'components/Currency/CurrencyIcon'; import { FlexDivCentered } from 'components/layout/flex'; +import { Body } from 'components/Text'; import { MarketKeyByAsset } from 'utils/futures'; import { MarketsCurrencyOption } from './MarketsDropdown'; @@ -22,13 +23,13 @@ const MarketsDropdownSingleValue: React.FC<SingleValueProps<MarketsCurrencyOptio futuresClosureReason={props.data.closureReason} /> </CurrencyLabel> - <p className="name">{props.data.description}</p> + <Body className="name">{props.data.description}</Body> </div> <div style={{ marginRight: 15 }}> - <p className="price">{props.data.price}</p> - <p className={props.data.negativeChange ? `change red` : 'change green'}> + <Body className="price">{props.data.price}</Body> + <Body className={props.data.negativeChange ? `change red` : 'change green'}> {props.data.change} - </p> + </Body> </div> </SingleValueContainer> </SingleValueWrapper> diff --git a/sections/futures/Trade/OrderWarning.tsx b/sections/futures/Trade/OrderWarning.tsx deleted file mode 100644 index 0e731550a1..0000000000 --- a/sections/futures/Trade/OrderWarning.tsx +++ /dev/null @@ -1,36 +0,0 @@ -import React from 'react'; -import { useTranslation } from 'react-i18next'; -import styled from 'styled-components'; - -import { selectOrderType } from 'state/futures/selectors'; -import { useAppSelector } from 'state/hooks'; - -const OrderWarning: React.FC = () => { - const { t } = useTranslation(); - const orderType = useAppSelector(selectOrderType); - - return ( - <Container> - <p className="description"> - {orderType === 'delayed_offchain' - ? t('futures.market.trade.delayed-order.description') - : t('futures.market.trade.market-order.description')} - </p> - </Container> - ); -}; - -const Container = styled.div` - margin-bottom: 16px; - - .description { - color: ${(props) => props.theme.colors.selectedTheme.gray}; - margin: 0 8px; - - a { - color: ${(props) => props.theme.colors.selectedTheme.button.text.primary}; - } - } -`; - -export default OrderWarning; diff --git a/sections/futures/Trade/TradeIsolatedMargin.tsx b/sections/futures/Trade/TradeIsolatedMargin.tsx index 31e67849e2..be00eec059 100644 --- a/sections/futures/Trade/TradeIsolatedMargin.tsx +++ b/sections/futures/Trade/TradeIsolatedMargin.tsx @@ -1,6 +1,9 @@ +import { memo, useCallback } from 'react'; + import Error from 'components/ErrorView'; import SegmentedControl from 'components/SegmentedControl'; import { DEFAULT_DELAYED_LEVERAGE_CAP, ISOLATED_MARGIN_ORDER_TYPES } from 'constants/futures'; +import { PositionSide } from 'sdk/types/futures'; import { setOpenModal } from 'state/app/reducer'; import { changeLeverageSide } from 'state/futures/actions'; import { setOrderType } from 'state/futures/reducer'; @@ -8,7 +11,7 @@ import { selectLeverageSide, selectOrderType, selectPosition } from 'state/futur import { useAppDispatch, useAppSelector } from 'state/hooks'; import { selectPricesConnectionError } from 'state/prices/selectors'; -import FeeInfoBox from '../FeeInfoBox'; +import { IsolatedMarginFeeInfoBox } from '../FeeInfoBox/FeeInfoBox'; import LeverageInput from '../LeverageInput'; import MarketInfoBox from '../MarketInfoBox'; import OrderSizing from '../OrderSizing'; @@ -20,7 +23,7 @@ type Props = { isMobile?: boolean; }; -const TradeIsolatedMargin = ({ isMobile }: Props) => { +const TradeIsolatedMargin = memo(({ isMobile }: Props) => { const dispatch = useAppDispatch(); const leverageSide = useAppSelector(selectLeverageSide); @@ -29,12 +32,27 @@ const TradeIsolatedMargin = ({ isMobile }: Props) => { const orderType = useAppSelector(selectOrderType); const pricesConnectionError = useAppSelector(selectPricesConnectionError); + const openDepositModal = useCallback(() => { + dispatch(setOpenModal('futures_isolated_transfer')); + }, [dispatch]); + + const handleChangeSide = useCallback( + (side: PositionSide) => { + dispatch(changeLeverageSide(side)); + }, + [dispatch] + ); + + const changeOrderType = useCallback( + (oType: number) => { + dispatch(setOrderType(oType === 1 ? 'market' : 'delayed_offchain')); + }, + [dispatch] + ); + return ( <div> - <TradePanelHeader - onManageBalance={() => dispatch(setOpenModal('futures_isolated_transfer'))} - accountType={'isolated_margin'} - /> + <TradePanelHeader onManageBalance={openDepositModal} accountType="isolated_margin" /> {pricesConnectionError && ( <Error message="Failed to connect to price feed. Please try disabling any ad blockers and refresh." /> )} @@ -46,19 +64,11 @@ const TradeIsolatedMargin = ({ isMobile }: Props) => { styleType="check" values={ISOLATED_MARGIN_ORDER_TYPES} selectedIndex={ISOLATED_MARGIN_ORDER_TYPES.indexOf(orderType)} - onChange={(oType: number) => { - const newOrderType = oType === 1 ? 'market' : 'delayed_offchain'; - dispatch(setOrderType(newOrderType)); - }} + onChange={changeOrderType} /> )} - <PositionButtons - selected={leverageSide} - onSelect={(side) => { - dispatch(changeLeverageSide(side)); - }} - /> + <PositionButtons selected={leverageSide} onSelect={handleChangeSide} /> <OrderSizing /> @@ -66,9 +76,9 @@ const TradeIsolatedMargin = ({ isMobile }: Props) => { <ManagePosition /> - <FeeInfoBox /> + <IsolatedMarginFeeInfoBox /> </div> ); -}; +}); export default TradeIsolatedMargin; diff --git a/sections/futures/Trade/TradePanelHeader.tsx b/sections/futures/Trade/TradePanelHeader.tsx index 3a191ed48b..1b8fde605c 100644 --- a/sections/futures/Trade/TradePanelHeader.tsx +++ b/sections/futures/Trade/TradePanelHeader.tsx @@ -1,6 +1,7 @@ import { useConnectModal } from '@rainbow-me/rainbowkit'; +import { FC, memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; -import styled, { useTheme } from 'styled-components'; +import styled from 'styled-components'; import HelpIcon from 'assets/svg/app/question-mark.svg'; import SwitchAssetArrows from 'assets/svg/futures/deposit-withdraw-arrows.svg'; @@ -12,6 +13,7 @@ import { FuturesAccountType } from 'queries/futures/subgraph'; import { setOpenModal } from 'state/app/reducer'; import { selectCrossMarginBalanceInfo, + selectFuturesType, selectHasRemainingMargin, selectPosition, } from 'state/futures/selectors'; @@ -25,12 +27,41 @@ type Props = { onManageBalance: () => void; }; -export default function TradePanelHeader({ accountType, onManageBalance }: Props) { +const ConnectDepositButton = memo(() => { const { t } = useTranslation(); - const dispatch = useAppDispatch(); - const theme = useTheme(); const { openConnectModal } = useConnectModal(); + return ( + <DepositButton onClick={openConnectModal}> + <ButtonContent>{t('common.wallet.connect-wallet')}</ButtonContent> + </DepositButton> + ); +}); + +const DepositMarginButton = memo(() => { + const dispatch = useAppDispatch(); + const accountType = useAppSelector(selectFuturesType); + + const handleOpenModal = useCallback(() => { + dispatch( + setOpenModal( + accountType === 'isolated_margin' ? 'futures_isolated_transfer' : 'futures_cross_deposit' + ) + ); + }, [dispatch, accountType]); + + return ( + <DepositButton onClick={handleOpenModal}> + <ButtonContent> + Deposit Margin <SwitchAssetArrows /> + </ButtonContent> + </DepositButton> + ); +}); + +const TradePanelHeader: FC<Props> = memo(({ accountType, onManageBalance }) => { + const { t } = useTranslation(); + const position = useAppSelector(selectPosition); const { freeMargin } = useAppSelector(selectCrossMarginBalanceInfo); const hasMargin = useAppSelector(selectHasRemainingMargin); @@ -40,32 +71,11 @@ export default function TradePanelHeader({ accountType, onManageBalance }: Props accountType === 'isolated_margin' ? position?.remainingMargin ?? zeroBN : freeMargin; if (!wallet) { - return ( - <DepositButton variant="yellow" onClick={openConnectModal}> - <ButtonContent>{t('common.wallet.connect-wallet')}</ButtonContent> - </DepositButton> - ); + return <ConnectDepositButton />; } if (!hasMargin) { - return ( - <DepositButton - variant="yellow" - onClick={() => - dispatch( - setOpenModal( - accountType === 'isolated_margin' - ? 'futures_isolated_transfer' - : 'futures_cross_deposit' - ) - ) - } - > - <ButtonContent> - Deposit Margin <SwitchAssetArrows fill={theme.colors.selectedTheme.button.yellow.text} /> - </ButtonContent> - </DepositButton> - ); + return <DepositMarginButton />; } return ( @@ -73,9 +83,9 @@ export default function TradePanelHeader({ accountType, onManageBalance }: Props <Title> <StyledFuturesIcon type={accountType} /> {t( - accountType === 'cross_margin' - ? 'futures.market.trade.cross-margin.title' - : 'futures.market.trade.isolated-margin.title' + `futures.market.trade.${ + accountType === 'cross_margin' ? 'cross' : 'isolated' + }-margin.title` )} {accountType === 'cross_margin' && ( <FAQLink onClick={() => window.open(EXTERNAL_LINKS.Docs.CrossMarginFaq)}> @@ -84,7 +94,7 @@ export default function TradePanelHeader({ accountType, onManageBalance }: Props )} - {formatDollars(balance ?? zeroBN)} + {formatDollars(balance)} @@ -93,7 +103,7 @@ export default function TradePanelHeader({ accountType, onManageBalance }: Props ); -} +}); const StyledPillButtonSpan = styled(PillButtonSpan)` display: flex; @@ -104,10 +114,13 @@ const StyledPillButtonSpan = styled(PillButtonSpan)` margin-left: 0; `; -const DepositButton = styled(Button)` +const DepositButton = styled(Button).attrs({ fullWidth: true, variant: 'yellow' })` height: 55px; - width: 100%; margin-bottom: 16px; + + svg { + fill: ${(props) => props.theme.colors.selectedTheme.button.yellow.text}; + } `; const Container = styled(BorderedPanel)` @@ -157,3 +170,5 @@ const ButtonContent = styled.div` justify-content: center; gap: 10px; `; + +export default TradePanelHeader; diff --git a/sections/futures/Trade/TransferIsolatedMarginModal.tsx b/sections/futures/Trade/TransferIsolatedMarginModal.tsx index d0cfd9fa53..bcebc5f473 100644 --- a/sections/futures/Trade/TransferIsolatedMarginModal.tsx +++ b/sections/futures/Trade/TransferIsolatedMarginModal.tsx @@ -10,7 +10,7 @@ import BaseModal from 'components/BaseModal'; import Button from 'components/Button'; import { CardHeader } from 'components/Card'; import Error from 'components/ErrorView'; -import CustomInput from 'components/Input/CustomInput'; +import NumericInput from 'components/Input/NumericInput'; import { FlexDivRowCentered } from 'components/layout/flex'; import SegmentedControl from 'components/SegmentedControl'; import Spacer from 'components/Spacer'; @@ -157,7 +157,7 @@ const TransferIsolatedMarginModal: React.FC = ({ onDismiss, defaultTab }) {formatDollars(susdBal)} sUSD - marketInfo?.minInitialMargin ?? zeroBN, [ - marketInfo?.minInitialMargin, - ]); - const previewTotalMargin = useMemo(() => { - const remainingMargin = crossMarginFreeMargin.sub(marginDelta); - return remainingMargin.add(potentialTrade?.margin || zeroBN); - }, [crossMarginFreeMargin, marginDelta, potentialTrade?.margin]); - - const getPotentialAvailableMargin = useCallback( - (previewTrade: FuturesPotentialTradeDetails | null, marketMaxLeverage: Wei | undefined) => { - let inaccessible; - - if (!marketMaxLeverage) return zeroBN; - - inaccessible = previewTrade?.notionalValue.div(marketMaxLeverage).abs() ?? zeroBN; - - // If the user has a position open, we'll enforce a min initial margin requirement. - if (inaccessible.gt(0)) { - if (inaccessible.lt(minInitialMargin)) { - inaccessible = minInitialMargin; - } - } - - // check if available margin will be less than 0 - return previewTotalMargin.sub(inaccessible).gt(0) - ? previewTotalMargin.sub(inaccessible).abs() - : zeroBN; - }, - [previewTotalMargin, minInitialMargin] - ); - - const previewAvailableMargin = React.useMemo(() => { - const potentialAvailableMargin = getPotentialAvailableMargin( - potentialTrade, - marketInfo?.maxLeverage - ); - return potentialAvailableMargin; - }, [potentialTrade, marketInfo?.maxLeverage, getPotentialAvailableMargin]); - - const potentialMarginUsage = useMemo(() => { - if (!potentialTrade) return zeroBN; - const notionalValue = potentialTrade.notionalValue.abs(); - const maxSize = totalMargin.mul(potentialTrade.leverage); - return maxSize.gt(0) ? notionalValue.div(maxSize) : zeroBN; - }, [potentialTrade, totalMargin]); - - const previewTradeData = React.useMemo(() => { - const size = wei(nativeSize || zeroBN); - - return { - showPreview: - ((orderType === 'market' || orderType === 'delayed' || orderType === 'delayed_offchain') && - (!size.eq(0) || !marginDelta.eq(0))) || - ((orderType === 'limit' || orderType === 'stop_market') && !!orderPrice && !size.eq(0)), - totalMargin: potentialTrade?.margin.sub(crossMarginFee) || zeroBN, - freeAccountMargin: crossMarginFreeMargin.sub(marginDelta), - availableMargin: previewAvailableMargin.gt(0) ? previewAvailableMargin : zeroBN, - size: potentialTrade?.size || zeroBN, - leverage: potentialTrade?.margin.gt(0) - ? potentialTrade.notionalValue.div(potentialTrade.margin).abs() - : zeroBN, - marginUsage: potentialMarginUsage.gt(1) ? wei(1) : potentialMarginUsage, - }; - }, [ - nativeSize, - marginDelta, - crossMarginFee, - orderType, - orderPrice, - potentialTrade?.margin, - previewAvailableMargin, - potentialTrade?.notionalValue, - potentialTrade?.size, - crossMarginFreeMargin, - potentialMarginUsage, - ]); - - const showPreview = previewTradeData.showPreview && !potentialTrade?.showStatus; - - const isLoading = previewStatus.status === FetchStatus.Loading; - return ( - <> - - {formatDollars(previewTradeData.freeAccountMargin)} - - ), - } - : null, - 'Market Margin': { - value: formatDollars(position?.remainingMargin || 0), - valueNode: ( - - {formatDollars(previewTradeData.totalMargin)} - - ), - }, - 'Margin Usage': { - value: formatPercent(marginUsage), - valueNode: ( - - {formatPercent(previewTradeData?.marginUsage)} - - ), - }, - 'Account ETH Balance': !editingLeverage - ? { - value: formatCurrency('ETH', keeperEthBal, { currencyKey: 'ETH' }), - valueNode: ( - <> - {keeperEthBal.gt(0) && ( - dispatch(setOpenModal('futures_withdraw_keeper_balance'))} - > - - - )} - - ), - } - : null, - Leverage: { - value: ( - <> - {formatNumber( - editingLeverage - ? position?.position?.leverage ?? selectedLeverage - : selectedLeverage, - { - maxDecimals: 2, - } - )} - x - {!editingLeverage && ( - dispatch(setOpenModal('futures_edit_input_leverage'))} - > - Edit - - )} - - ), - valueNode: ( - - {formatNumber(previewTradeData.leverage || 0) + 'x'} - - ), - }, - }} - disabled={marketInfo?.isSuspended} - /> - - {openModal === 'futures_edit_input_leverage' && } - {openModal === 'futures_withdraw_keeper_balance' && ( - - )} - - ); + return
; + // const dispatch = useAppDispatch(); + + // const position = useAppSelector(selectPosition); + // const marketInfo = useAppSelector(selectMarketInfo); + // const { nativeSize } = useAppSelector(selectTradeSizeInputs); + // const potentialTrade = useAppSelector(selectTradePreview); + // const marginDelta = useAppSelector(selectCrossMarginMarginDelta); + // const { freeMargin: crossMarginFreeMargin, keeperEthBal } = useAppSelector( + // selectCrossMarginBalanceInfo + // ); + // const previewStatus = useAppSelector(selectTradePreviewStatus); + // const orderType = useAppSelector(selectOrderType); + // const orderPrice = useAppSelector(selectCrossMarginOrderPrice); + // const openModal = useAppSelector(selectOpenModal); + // const selectedLeverage = useAppSelector(selectCrossMarginSelectedLeverage); + + // const { crossMarginFee } = useAppSelector(selectCrossMarginTradeFees); + + // const totalMargin = position?.remainingMargin.add(crossMarginFreeMargin) ?? zeroBN; + // const remainingMargin = position?.remainingMargin ?? zeroBN; + + // const marginUsage = totalMargin.gt(zeroBN) ? remainingMargin.div(totalMargin) : zeroBN; + // const minInitialMargin = useMemo(() => marketInfo?.minInitialMargin ?? zeroBN, [ + // marketInfo?.minInitialMargin, + // ]); + // const previewTotalMargin = useMemo(() => { + // const remainingMargin = crossMarginFreeMargin.sub(marginDelta); + // return remainingMargin.add(potentialTrade?.margin || zeroBN); + // }, [crossMarginFreeMargin, marginDelta, potentialTrade?.margin]); + + // const getPotentialAvailableMargin = useCallback( + // (previewTrade: FuturesPotentialTradeDetails | null, marketMaxLeverage: Wei | undefined) => { + // let inaccessible; + + // if (!marketMaxLeverage) return zeroBN; + + // inaccessible = previewTrade?.notionalValue.div(marketMaxLeverage).abs() ?? zeroBN; + + // // If the user has a position open, we'll enforce a min initial margin requirement. + // if (inaccessible.gt(0)) { + // if (inaccessible.lt(minInitialMargin)) { + // inaccessible = minInitialMargin; + // } + // } + + // // check if available margin will be less than 0 + // return previewTotalMargin.sub(inaccessible).gt(0) + // ? previewTotalMargin.sub(inaccessible).abs() + // : zeroBN; + // }, + // [previewTotalMargin, minInitialMargin] + // ); + + // const previewAvailableMargin = React.useMemo(() => { + // const potentialAvailableMargin = getPotentialAvailableMargin( + // potentialTrade, + // marketInfo?.maxLeverage + // ); + // return potentialAvailableMargin; + // }, [potentialTrade, marketInfo?.maxLeverage, getPotentialAvailableMargin]); + + // const potentialMarginUsage = useMemo(() => { + // if (!potentialTrade) return zeroBN; + // const notionalValue = potentialTrade.notionalValue.abs(); + // const maxSize = totalMargin.mul(potentialTrade.leverage); + // return maxSize.gt(0) ? notionalValue.div(maxSize) : zeroBN; + // }, [potentialTrade, totalMargin]); + + // const previewTradeData = React.useMemo(() => { + // const size = wei(nativeSize || zeroBN); + + // return { + // showPreview: + // ((orderType === 'market' || orderType === 'delayed' || orderType === 'delayed_offchain') && + // (!size.eq(0) || !marginDelta.eq(0))) || + // ((orderType === 'limit' || orderType === 'stop_market') && !!orderPrice && !size.eq(0)), + // totalMargin: potentialTrade?.margin.sub(crossMarginFee) || zeroBN, + // freeAccountMargin: crossMarginFreeMargin.sub(marginDelta), + // availableMargin: previewAvailableMargin.gt(0) ? previewAvailableMargin : zeroBN, + // size: potentialTrade?.size || zeroBN, + // leverage: potentialTrade?.margin.gt(0) + // ? potentialTrade.notionalValue.div(potentialTrade.margin).abs() + // : zeroBN, + // marginUsage: potentialMarginUsage.gt(1) ? wei(1) : potentialMarginUsage, + // }; + // }, [ + // nativeSize, + // marginDelta, + // crossMarginFee, + // orderType, + // orderPrice, + // potentialTrade?.margin, + // previewAvailableMargin, + // potentialTrade?.notionalValue, + // potentialTrade?.size, + // crossMarginFreeMargin, + // potentialMarginUsage, + // ]); + + // const showPreview = previewTradeData.showPreview && !potentialTrade?.showStatus; + + // const isLoading = previewStatus.status === FetchStatus.Loading; + // return ( + // <> + // + // {formatDollars(previewTradeData.freeAccountMargin)} + // + // ), + // } + // : null, + // 'Market Margin': { + // value: formatDollars(position?.remainingMargin || 0), + // valueNode: ( + // + // {formatDollars(previewTradeData.totalMargin)} + // + // ), + // }, + // 'Margin Usage': { + // value: formatPercent(marginUsage), + // valueNode: ( + // + // {formatPercent(previewTradeData?.marginUsage)} + // + // ), + // }, + // 'Account ETH Balance': !editingLeverage + // ? { + // value: formatCurrency('ETH', keeperEthBal, { currencyKey: 'ETH' }), + // valueNode: ( + // <> + // {keeperEthBal.gt(0) && ( + // dispatch(setOpenModal('futures_withdraw_keeper_balance'))} + // > + // + // + // )} + // + // ), + // } + // : null, + // Leverage: { + // value: ( + // <> + // {formatNumber( + // editingLeverage + // ? position?.position?.leverage ?? selectedLeverage + // : selectedLeverage, + // { + // maxDecimals: 2, + // } + // )} + // x + // {!editingLeverage && ( + // dispatch(setOpenModal('futures_edit_input_leverage'))} + // > + // Edit + // + // )} + // + // ), + // valueNode: ( + // + // {formatNumber(previewTradeData.leverage || 0) + 'x'} + // + // ), + // }, + // }} + // disabled={marketInfo?.isSuspended} + // /> + + // {openModal === 'futures_edit_input_leverage' && } + // {openModal === 'futures_withdraw_keeper_balance' && ( + // + // )} + // + // ); } -const StyledInfoBox = styled(InfoBox)` - margin-bottom: 16px; +// const StyledInfoBox = styled(InfoBoxContainer)` +// margin-bottom: 16px; - .value { - font-family: ${(props) => props.theme.fonts.regular}; - } -`; +// .value { +// font-family: ${(props) => props.theme.fonts.regular}; +// } +// `; -export default React.memo(MarginInfoBox); +export default memo(MarginInfoBox); diff --git a/sections/futures/TradeCrossMargin/DepositWithdrawCrossMargin.tsx b/sections/futures/TradeCrossMargin/DepositWithdrawCrossMargin.tsx index 4136b3a391..36cd513b65 100644 --- a/sections/futures/TradeCrossMargin/DepositWithdrawCrossMargin.tsx +++ b/sections/futures/TradeCrossMargin/DepositWithdrawCrossMargin.tsx @@ -6,7 +6,7 @@ import styled from 'styled-components'; import BaseModal from 'components/BaseModal'; import Button from 'components/Button'; import ErrorView from 'components/ErrorView'; -import CustomInput from 'components/Input/CustomInput'; +import NumericInput from 'components/Input/NumericInput'; import { FlexDivRowCentered } from 'components/layout/flex'; import Loader from 'components/Loader'; import SegmentedControl from 'components/SegmentedControl'; @@ -204,6 +204,6 @@ const StyledSegmentedControl = styled(SegmentedControl)` margin-bottom: 16px; `; -const InputContainer = styled(CustomInput)` +const InputContainer = styled(NumericInput)` margin-bottom: 10px; `; diff --git a/sections/futures/TradeCrossMargin/EditCrossMarginLeverageModal.tsx b/sections/futures/TradeCrossMargin/EditCrossMarginLeverageModal.tsx index 17c444d89c..f2ca7cb2f4 100644 --- a/sections/futures/TradeCrossMargin/EditCrossMarginLeverageModal.tsx +++ b/sections/futures/TradeCrossMargin/EditCrossMarginLeverageModal.tsx @@ -7,11 +7,12 @@ import styled from 'styled-components'; import BaseModal from 'components/BaseModal'; import Button from 'components/Button'; import ErrorView from 'components/ErrorView'; -import CustomInput from 'components/Input/CustomInput'; +import NumericInput from 'components/Input/NumericInput'; import { FlexDivRow, FlexDivRowCentered } from 'components/layout/flex'; import Loader from 'components/Loader'; import Spacer from 'components/Spacer'; -import { NumberSpan } from 'components/Text/NumberLabel'; +import { Body } from 'components/Text'; +import { NumberDiv } from 'components/Text/NumberLabel'; import { ORDER_PREVIEW_ERRORS_I18N, previewErrorI18n } from 'queries/futures/constants'; import { setOpenModal } from 'state/app/reducer'; import { @@ -37,7 +38,7 @@ import { useAppSelector, useAppDispatch } from 'state/hooks'; import { isUserDeniedError } from 'utils/formatters/error'; import { formatDollars, zeroBN } from 'utils/formatters/number'; -import FeeInfoBox from '../FeeInfoBox'; +import { CrossMarginFeeInfoBox } from '../FeeInfoBox'; import LeverageSlider from '../LeverageSlider'; import MarginInfoBox from './CrossMarginInfoBox'; @@ -67,7 +68,7 @@ export default function EditLeverageModal({ editMode }: DepositMarginModalProps) const submitting = useAppSelector(selectSubmittingFuturesTx); const maxLeverage = useAppSelector(selectMaxLeverage); - const [leverage, setLeverage] = useState( + const [leverage, setLeverage] = useState( editMode === 'existing_position' && position?.position ? Number(position.position.leverage.toNumber().toFixed(2)) : Number(Number(selectedLeverage).toFixed(2)) @@ -186,7 +187,10 @@ export default function EditLeverageModal({ editMode }: DepositMarginModalProps) )} @@ -195,7 +199,7 @@ export default function EditLeverageModal({ editMode }: DepositMarginModalProps) <> - {tradeFees.total.gt(0) && } + {tradeFees.total.gt(0) && } )} @@ -250,8 +254,7 @@ const MaxPosContainer = styled(FlexDivRowCentered)` } `; -const Label = styled.p` - font-size: 13px; +const Label = styled(Body)` color: ${(props) => props.theme.colors.selectedTheme.gray}; `; @@ -270,7 +273,7 @@ const MaxButton = styled.div` cursor: pointer; `; -const InputContainer = styled(CustomInput)` +const InputContainer = styled(NumericInput)` margin-bottom: 15px; `; diff --git a/sections/futures/TradeCrossMargin/ManageKeeperBalanceModal.tsx b/sections/futures/TradeCrossMargin/ManageKeeperBalanceModal.tsx index 9df6f08cc5..d34eb9d915 100644 --- a/sections/futures/TradeCrossMargin/ManageKeeperBalanceModal.tsx +++ b/sections/futures/TradeCrossMargin/ManageKeeperBalanceModal.tsx @@ -5,7 +5,7 @@ import styled from 'styled-components'; import ErrorView from 'components/ErrorView'; import { notifyError } from 'components/ErrorView/ErrorNotifier'; -import CustomInput from 'components/Input/CustomInput'; +import NumericInput from 'components/Input/NumericInput'; import Loader from 'components/Loader'; import SegmentedControl from 'components/SegmentedControl'; import Spacer from 'components/Spacer'; @@ -118,7 +118,7 @@ export default function ManageKeeperBalanceModal({ defaultType }: Props) { - - + {openTransferModal && ( = ({ history, isLoading, isLoaded, marketAsset }) => { +const Trades: React.FC = memo(() => { const { t } = useTranslation(); const { switchToL2 } = useNetworkSwitcher(); + const marketAsset = useAppSelector(selectMarketAsset); + const history = useAppSelector(selectUsersTradesForMarket); + const { trades } = useAppSelector(selectQueryStatuses); + + const isLoading = !history.length && trades.status === FetchStatus.Loading; + const isLoaded = trades.status === FetchStatus.Success; const isL2 = useIsL2(); @@ -145,7 +151,7 @@ const Trades: React.FC = ({ history, isLoading, isLoaded, marketAss /> ); -}; +}); export default Trades; diff --git a/sections/futures/TradingHistory/SkewInfo.tsx b/sections/futures/TradingHistory/SkewInfo.tsx index a4b07b218b..dfdd87669c 100644 --- a/sections/futures/TradingHistory/SkewInfo.tsx +++ b/sections/futures/TradingHistory/SkewInfo.tsx @@ -1,13 +1,12 @@ -/* eslint-disable no-console */ import React from 'react'; import { useMemo } from 'react'; import { useTranslation } from 'react-i18next'; import styled from 'styled-components'; +import { Body } from 'components/Text'; import Tooltip from 'components/Tooltip/Tooltip'; import { selectMarketAsset, selectMarketInfo } from 'state/futures/selectors'; import { useAppSelector } from 'state/hooks'; -import { CapitalizedText, NumericValue } from 'styles/common'; import { formatCurrency, formatPercent } from 'utils/formatters/number'; import OpenInterestBar from './OpenInterestBar'; @@ -49,34 +48,17 @@ const SkewInfo: React.FC = () => { return ( - + {formatPercent(data.short, { minDecimals: 0 })} - + {t('futures.market.history.skew-label')} - + {formatPercent(data.long, { minDecimals: 0 })} @@ -93,19 +75,11 @@ const WithCursor = styled.div<{ cursor: 'help' }>` cursor: ${(props) => props.cursor}; `; -const SkewTooltip = styled(Tooltip)<{ isNumber?: boolean }>` +const SkewTooltip = styled(Tooltip).attrs({ width: '310px', height: 'auto' })` left: -30px; z-index: 2; padding: 10px; color: red; - - p, - span { - font-size: 13px; - font-family: ${(props) => - props.isNumber ? props.theme.fonts.mono : props.theme.fonts.regular}; - color: ${(props) => props.theme.colors.selectedTheme.button.text.primary}; - } `; const SkewContainer = styled.div` @@ -126,21 +100,8 @@ const SkewContainer = styled.div` p, span { - margin: 0; text-align: left; } - - .heading { - font-size: 13px; - color: ${(props) => props.theme.colors.selectedTheme.gray}; - width: 292px; - } - - .value { - font-family: ${(props) => props.theme.fonts.mono}; - font-size: 13px; - color: ${(props) => props.theme.colors.selectedTheme.button.text.primary}; - } `; const SkewHeader = styled.div` @@ -150,15 +111,13 @@ const SkewHeader = styled.div` margin-bottom: 5px; `; -const SkewLabel = styled(CapitalizedText)` +const SkewLabel = styled(Body).attrs({ as: 'span' })` + text-transform: capitalize; text-align: center; color: ${(props) => props.theme.colors.selectedTheme.gray}; - font-size: 13px; `; -const SkewValue = styled(NumericValue)` +const SkewValue = styled(Body).attrs({ mono: true })` text-align: center; color: ${(props) => props.theme.colors.selectedTheme.button.text.primary}; - font-size: 13px; - font-family: ${(props) => props.theme.fonts.mono}; `; diff --git a/sections/futures/TradingHistory/TradesHistoryTable.tsx b/sections/futures/TradingHistory/TradesHistoryTable.tsx index 1cd19f47cb..8c983cd517 100644 --- a/sections/futures/TradingHistory/TradesHistoryTable.tsx +++ b/sections/futures/TradingHistory/TradesHistoryTable.tsx @@ -4,13 +4,13 @@ import { CellProps } from 'react-table'; import styled, { css } from 'styled-components'; import Table, { TableHeader } from 'components/Table'; +import { Body } from 'components/Text'; import { NO_VALUE } from 'constants/placeholder'; import { blockExplorer } from 'containers/Connector/Connector'; import useGetFuturesTrades from 'queries/futures/useGetFuturesTrades'; import { FuturesTrade } from 'sdk/types/futures'; import { selectMarketKey } from 'state/futures/selectors'; import { useAppSelector } from 'state/hooks'; -import { NumericValue } from 'styles/common'; import { formatNumber } from 'utils/formatters/number'; type TradesHistoryTableProps = { @@ -216,42 +216,25 @@ const TableAlignment = css` `; const StyledTable = styled(Table)<{ mobile?: boolean }>` - border: 0px; + border: none; ${(props) => - !props.mobile && css` - height: 695px; + height: ${props.mobile ? 242 : 695}px; `} - ${(props) => - props.mobile && - css` - height: 242px; - `} - - .table-row { - ${TableAlignment} - } - .table-body-row { + .table-row, .table-body-row { ${TableAlignment} padding: 0; } - - .table-body-row { - padding: 0; - } `; -const PriceValue = styled(NumericValue)` - font-size: 13px; +const PriceValue = styled(Body).attrs({ mono: true })` color: ${(props) => props.theme.colors.selectedTheme.button.text.primary}; padding-left: 5px; `; -const TimeValue = styled.p` - font-size: 13px; - font-family: ${(props) => props.theme.fonts.regular}; +const TimeValue = styled(Body)` color: ${(props) => props.theme.colors.selectedTheme.button.text.primary}; text-decoration: underline; `; diff --git a/sections/futures/Transfers/Transfers.tsx b/sections/futures/Transfers/Transfers.tsx index 9c06ed4324..f9eb41e52f 100644 --- a/sections/futures/Transfers/Transfers.tsx +++ b/sections/futures/Transfers/Transfers.tsx @@ -4,6 +4,7 @@ import styled from 'styled-components'; import ColoredPrice from 'components/ColoredPrice'; import Table, { TableHeader, TableNoResults } from 'components/Table'; +import { Body } from 'components/Text'; import { blockExplorer } from 'containers/Connector/Connector'; import useIsL2 from 'hooks/useIsL2'; import useNetworkSwitcher from 'hooks/useNetworkSwitcher'; @@ -36,7 +37,7 @@ const Transfers: FC = () => { { Header: {t('futures.market.user.transfers.table.action')}, accessor: 'action', - Cell: (cellProps: any) => {cellProps.value}, + Cell: (cellProps) => {cellProps.value}, width: 50, }, { @@ -66,9 +67,7 @@ const Transfers: FC = () => { { Header: {t('futures.market.user.transfers.table.date')}, accessor: 'timestamp', - Cell: (cellProps: any) => ( - {timePresentation(cellProps.value, t)} - ), + Cell: (cellProps: any) => {timePresentation(cellProps.value, t)}, width: 50, }, { @@ -76,11 +75,11 @@ const Transfers: FC = () => { accessor: 'txHash', Cell: (cellProps: any) => { return ( - + {truncateAddress(cellProps.value)} - + ); }, width: 50, @@ -109,18 +108,13 @@ const Transfers: FC = () => { export default Transfers; -const DefaultCell = styled.p` - color: ${(props) => props.theme.colors.selectedTheme.button.text.primary}; -`; - -const StyledActionCell = styled(DefaultCell)` +const ActionCell = styled(Body)` text-transform: capitalize; `; -const StyledTitle = styled.p` +const StyledTitle = styled(Body)` color: ${(props) => props.theme.colors.selectedTheme.button.text.primary}; font-size: 16px; - margin: 0; `; const StyledExternalLink = styled(ExternalLink)` diff --git a/sections/futures/UserInfo/UserInfo.tsx b/sections/futures/UserInfo/UserInfo.tsx index 30ca8c1152..7eaa4d3076 100644 --- a/sections/futures/UserInfo/UserInfo.tsx +++ b/sections/futures/UserInfo/UserInfo.tsx @@ -1,5 +1,5 @@ import { useRouter } from 'next/router'; -import React, { useMemo, useState, useCallback, useEffect } from 'react'; +import React, { useMemo, useState, useCallback, useEffect, memo } from 'react'; import styled from 'styled-components'; import CalculatorIcon from 'assets/svg/futures/calculator-icon.svg'; @@ -18,11 +18,8 @@ import { selectMarketAsset, selectOpenOrders, selectPosition, - selectQueryStatuses, - selectUsersTradesForMarket, } from 'state/futures/selectors'; import { useAppSelector, useFetchAction, useAppDispatch } from 'state/hooks'; -import { FetchStatus } from 'state/types'; import { selectWallet } from 'state/wallet/selectors'; import PositionCard from '../PositionCard'; @@ -43,18 +40,16 @@ enum FuturesTab { const FutureTabs = Object.values(FuturesTab); -const UserInfo: React.FC = () => { +const UserInfo: React.FC = memo(() => { const router = useRouter(); const dispatch = useAppDispatch(); const marketAsset = useAppSelector(selectMarketAsset); const position = useAppSelector(selectPosition); const walletAddress = useAppSelector(selectWallet); - const { trades: tradesQuery } = useAppSelector(selectQueryStatuses); const openOrders = useAppSelector(selectOpenOrders); const accountType = useAppSelector(selectFuturesType); - const trades = useAppSelector(selectUsersTradesForMarket); useFetchAction(fetchTradesForSelectedMarket, { dependencies: [walletAddress, accountType, marketAsset, position?.position?.size.toString()], @@ -78,12 +73,12 @@ const UserInfo: React.FC = () => { const activeTab = tabQuery ?? FuturesTab.POSITION; const handleOpenProfitCalc = useCallback(() => { - setOpenProfitCalcModal(!openProfitCalcModal); - }, [openProfitCalcModal]); + setOpenProfitCalcModal((s) => !s); + }, []); const handleOpenShareModal = useCallback(() => { - setShowShareModal(!showShareModal); - }, [showShareModal]); + setShowShareModal((s) => !s); + }, []); const refetchTrades = useCallback(() => { dispatch(fetchTradesForSelectedMarket); @@ -145,7 +140,7 @@ const UserInfo: React.FC = () => { ); useEffect(() => { - setHasOpenPosition(!!position && !!position.position); + setHasOpenPosition(!!position?.position); }, [position]); return ( @@ -156,7 +151,7 @@ const UserInfo: React.FC = () => { { - + - {openProfitCalcModal && ( - - )} + {openProfitCalcModal && } {showShareModal && } ); -}; +}); const TabButtonsContainer = styled.div` display: grid; diff --git a/sections/homepage/Hero.tsx b/sections/homepage/Hero.tsx index 7d1f59bd4c..810b6c065e 100644 --- a/sections/homepage/Hero.tsx +++ b/sections/homepage/Hero.tsx @@ -30,7 +30,7 @@ const Hero = () => { - @@ -54,7 +54,7 @@ const Emphasis = styled.b` color: ${(props) => props.theme.colors.common.primaryWhite}; `; -const Header = styled(Text.Body).attrs({ variant: 'bold', mono: true })` +const Header = styled(Text.Body).attrs({ weight: 'bold', mono: true })` max-width: 636px; font-size: 80px; line-height: 85%; diff --git a/sections/homepage/ShortList.tsx b/sections/homepage/ShortList.tsx index bdf2dd7254..75fda74d22 100644 --- a/sections/homepage/ShortList.tsx +++ b/sections/homepage/ShortList.tsx @@ -10,6 +10,7 @@ import Currency from 'components/Currency'; import { FlexDivColCentered, FlexDivRow } from 'components/layout/flex'; import Loader from 'components/Loader'; import Table, { TableHeader } from 'components/Table'; +import { Body } from 'components/Text'; import ROUTES from 'constants/routes'; import useENS from 'hooks/useENS'; import useGetFuturesCumulativeStats from 'queries/futures/useGetFuturesCumulativeStats'; @@ -116,7 +117,7 @@ const ShortList = () => { ), accessor: 'totalTrades', Cell: (cellProps: CellProps) => ( - {cellProps.row.original.totalTrades} + {cellProps.row.original.totalTrades} ), width: 100, }, @@ -126,7 +127,7 @@ const ShortList = () => { ), accessor: 'liquidations', Cell: (cellProps: CellProps) => ( - {cellProps.row.original.liquidations} + {cellProps.row.original.liquidations} ), width: 100, }, @@ -324,10 +325,6 @@ const Medal = styled.span` font-size: 15px; `; -const DefaultCell = styled.p` - font-size: 15px; -`; - const ColorCodedPrice = styled(Currency.Price)` align-items: right; color: ${(props) => diff --git a/sections/homepage/TradeNow.tsx b/sections/homepage/TradeNow.tsx index 33dd8e1d15..21d14fd4f9 100644 --- a/sections/homepage/TradeNow.tsx +++ b/sections/homepage/TradeNow.tsx @@ -14,29 +14,30 @@ import media from 'styles/media'; const TradeNow = () => { const { t } = useTranslation(); - const title = ( - - {t('homepage.tradenow.title')} - {t('homepage.tradenow.description')} - {t('homepage.tradenow.categories')} - - - - - - - ); - return ( - {title} + + + {t('homepage.tradenow.title')} + {t('homepage.tradenow.description')} + {t('homepage.tradenow.categories')} + + + + + + + ); }; const TransparentCard = styled.div` + display: flex; + flex-direction: column; + align-items: center; padding: 140px 303px; box-sizing: border-box; text-align: center; @@ -83,7 +84,7 @@ const GrayDescription = styled(Text.Body)` `; const CTAContainer = styled.div` - margin: 50px 0px 0px 0; + margin-top: 50px; z-index: 1; `; diff --git a/sections/homepage/text.ts b/sections/homepage/text.ts index 7aef0b1b1a..46048edff8 100644 --- a/sections/homepage/text.ts +++ b/sections/homepage/text.ts @@ -2,7 +2,7 @@ import styled from 'styled-components'; import { Body } from 'components/Text'; -export const Title = styled(Body).attrs({ variant: 'bold' })` +export const Title = styled(Body).attrs({ weight: 'bold' })` font-size: 16px; text-align: left; color: ${(props) => props.theme.colors.selectedTheme.button.text.primary}; diff --git a/sections/leaderboard/Leaderboard.tsx b/sections/leaderboard/Leaderboard.tsx index 65034537b0..f9c68fcba0 100644 --- a/sections/leaderboard/Leaderboard.tsx +++ b/sections/leaderboard/Leaderboard.tsx @@ -89,7 +89,7 @@ const Leaderboard: FC = ({ compact, mobile }) => { }, [compRound, trader, urlPath, dispatch]); const onChangeSearch = (text: string) => { - setSearchInput(text?.toLowerCase()); + setSearchInput(text.toLowerCase()); if (isAddress(text)) { setSearchTerm(getAddress(text)); diff --git a/sections/leaderboard/TraderHistory.tsx b/sections/leaderboard/TraderHistory.tsx index 0fa57cf774..6e750dd831 100644 --- a/sections/leaderboard/TraderHistory.tsx +++ b/sections/leaderboard/TraderHistory.tsx @@ -11,6 +11,7 @@ import { FlexDiv } from 'components/layout/flex'; import { DesktopOnlyView, MobileOrTabletView } from 'components/Media'; import FuturesIcon from 'components/Nav/FuturesIcon'; import Table, { TableHeader } from 'components/Table'; +import { Body } from 'components/Text'; import ROUTES from 'constants/routes'; import { FuturesPositionHistory } from 'sdk/types/futures'; import TimeDisplay from 'sections/futures/Trades/TimeDisplay'; @@ -332,9 +333,7 @@ const CurrencyInfo = styled(FlexDiv)` align-items: center; `; -const StyledSubtitle = styled.div` - font-family: ${(props) => props.theme.fonts.regular}; - font-size: 13px; +const StyledSubtitle = styled(Body)` color: ${(props) => props.theme.colors.selectedTheme.button.text.primary}; text-transform: capitalize; `; diff --git a/sections/shared/Layout/AppLayout/Header/MobileUserMenu/MobileUserMenu.tsx b/sections/shared/Layout/AppLayout/Header/MobileUserMenu/MobileUserMenu.tsx index 925e96ec07..54119ce4a2 100644 --- a/sections/shared/Layout/AppLayout/Header/MobileUserMenu/MobileUserMenu.tsx +++ b/sections/shared/Layout/AppLayout/Header/MobileUserMenu/MobileUserMenu.tsx @@ -54,7 +54,7 @@ const MobileUserMenu: FC = () => { {window.location.pathname === ROUTES.Home.Root ? ( - + ) : ( diff --git a/sections/shared/Layout/AppLayout/Header/MobileUserMenu/MobileWalletButton.tsx b/sections/shared/Layout/AppLayout/Header/MobileUserMenu/MobileWalletButton.tsx index 527ff61aa3..3d297a592d 100644 --- a/sections/shared/Layout/AppLayout/Header/MobileUserMenu/MobileWalletButton.tsx +++ b/sections/shared/Layout/AppLayout/Header/MobileUserMenu/MobileWalletButton.tsx @@ -22,7 +22,7 @@ const MobileConnectButton = () => { return ( { return ( { return !isL2 ? ( - + {activeChain && {networkIcon(activeChain.name)}} {activeChain?.name} diff --git a/sections/shared/Layout/AppLayout/Header/WalletActions.tsx b/sections/shared/Layout/AppLayout/Header/WalletActions.tsx index 65ffc17cea..85c655ae02 100644 --- a/sections/shared/Layout/AppLayout/Header/WalletActions.tsx +++ b/sections/shared/Layout/AppLayout/Header/WalletActions.tsx @@ -28,7 +28,7 @@ export const WalletActions: FC = ({ isMobile }) => { return ( { const walletIsNotConnected = ( <> { const walletIsConnectedButNotSupported = ( <> - + {t('homepage.l2.cta-buttons.switch-networks')} - + {t('common.wallet.unsupported-network')} diff --git a/sections/shared/Layout/HomeLayout/Footer.tsx b/sections/shared/Layout/HomeLayout/Footer.tsx index 3afb81867d..99c326de2f 100644 --- a/sections/shared/Layout/HomeLayout/Footer.tsx +++ b/sections/shared/Layout/HomeLayout/Footer.tsx @@ -7,6 +7,7 @@ import DiscordLogo from 'assets/svg/social/discord.svg'; import MirrorLogo from 'assets/svg/social/mirror.svg'; import { FlexDivCentered } from 'components/layout/flex'; import PoweredBySynthetix from 'components/PoweredBySynthetix'; +import { Body } from 'components/Text'; import { EXTERNAL_LINKS } from 'constants/links'; import { GridContainer } from 'sections/homepage/section'; import { ExternalLink } from 'styles/common'; @@ -120,7 +121,7 @@ const Footer = memo(() => { {title} {links.map(({ key, title, link }) => ( -

{title}

+ {title}
))} @@ -137,7 +138,13 @@ const Footer = memo(() => { const StyledLink = styled.a` cursor: pointer; - color: ${(props) => props.theme.colors.common.primaryWhite}; + p { + line-height: 1.5; + margin: 18px 0; + ${media.lessThan('sm')` + font-size: 15px; + `}; + } `; const CopyRight = styled.div` diff --git a/sections/shared/Layout/HomeLayout/Header.tsx b/sections/shared/Layout/HomeLayout/Header.tsx index ede21237f2..17b2a2ab32 100644 --- a/sections/shared/Layout/HomeLayout/Header.tsx +++ b/sections/shared/Layout/HomeLayout/Header.tsx @@ -12,6 +12,7 @@ import Button from 'components/Button'; import { FlexDivRow, FlexDivRowCentered } from 'components/layout/flex'; import { GridDivCenteredCol } from 'components/layout/grid'; import { MobileHiddenView, MobileOnlyView } from 'components/Media'; +import { Body } from 'components/Text'; import { DEFAULT_FUTURES_MARGIN_TYPE } from 'constants/defaults'; import { EXTERNAL_LINKS } from 'constants/links'; import ROUTES from 'constants/routes'; @@ -124,7 +125,7 @@ const Header = memo(() => { - @@ -179,8 +180,7 @@ const StyledMenu = styled.div` } `; -const StyledMenuItem = styled.p` - font-family: ${(props) => props.theme.fonts.bold}; +const StyledMenuItem = styled(Body).attrs({ weight: 'bold' })` cursor: pointer; width: 100%; font-size: 15px; diff --git a/sections/shared/components/CompetitionBanner.tsx b/sections/shared/components/CompetitionBanner.tsx index fb96c4c19f..01c686b9e6 100644 --- a/sections/shared/components/CompetitionBanner.tsx +++ b/sections/shared/components/CompetitionBanner.tsx @@ -3,7 +3,7 @@ import { useTranslation } from 'react-i18next'; import styled from 'styled-components'; import CompetitionBannerBg from 'assets/svg/app/competition-banner-bg.svg'; -import * as Text from 'components/Text'; +import { Body } from 'components/Text'; import { COMPETITION_DATES, COMPETITION_ENABLED } from 'constants/competition'; import { EXTERNAL_LINKS } from 'constants/links'; import { ExternalLink } from 'styles/common'; @@ -56,12 +56,9 @@ const BannerContainer = styled.div<{ compact?: boolean; hideBanner?: boolean }>` gap: 10px; `; -const CompetitionPeriod = styled(Text.Body).attrs({ mono: true, variant: 'bold' })` +const CompetitionPeriod = styled(Body).attrs({ mono: true, weight: 'bold' })` font-style: normal; - font-size: 13px; - line-height: 10px; color: ${(props) => props.theme.colors.selectedTheme.gray}; - margin: 0; `; const CTA = styled(ExternalLink)` diff --git a/sections/shared/components/FeeCostSummary.tsx b/sections/shared/components/FeeCostSummary.tsx index f44f05b272..6e54fd1362 100644 --- a/sections/shared/components/FeeCostSummary.tsx +++ b/sections/shared/components/FeeCostSummary.tsx @@ -1,17 +1,15 @@ -import Wei from '@synthetixio/wei'; import { FC, memo } from 'react'; import { useTranslation } from 'react-i18next'; import { NO_VALUE } from 'constants/placeholder'; import { SummaryItem, SummaryItemValue, SummaryItemLabel } from 'sections/exchange/summary'; +import { selectFeeCostWei } from 'state/exchange/selectors'; +import { useAppSelector } from 'state/hooks'; import { formatDollars } from 'utils/formatters/number'; -type FeeRateSummaryItemProps = { - feeCost?: Wei; -}; - -const FeeCostSummary: FC = memo(({ feeCost, ...rest }) => { +const FeeCostSummary: FC = memo(({ ...rest }) => { const { t } = useTranslation(); + const feeCost = useAppSelector(selectFeeCostWei); return ( diff --git a/sections/shared/components/GasPriceSelect.tsx b/sections/shared/components/GasPriceSelect.tsx index ca5b9fe0cf..f63620b42e 100644 --- a/sections/shared/components/GasPriceSelect.tsx +++ b/sections/shared/components/GasPriceSelect.tsx @@ -1,5 +1,4 @@ -import { GasPrices } from '@synthetixio/queries'; -import Wei from '@synthetixio/wei'; +import useSynthetixQueries from '@synthetixio/queries'; import { FC, useMemo, memo } from 'react'; import { useTranslation } from 'react-i18next'; @@ -9,30 +8,28 @@ import useIsL1 from 'hooks/useIsL1'; import useIsL2 from 'hooks/useIsL2'; import { SummaryItem, SummaryItemValue, SummaryItemLabel } from 'sections/exchange/summary'; import { selectGasPrice, selectGasSpeed } from 'state/app/selectors'; +import { selectTransactionFeeWei } from 'state/exchange/selectors'; import { useAppSelector } from 'state/hooks'; import { formatNumber, formatDollars } from 'utils/formatters/number'; type GasPriceSelectProps = { - gasPrices: GasPrices | undefined; - transactionFee?: Wei | number | null; className?: string; }; -type GasPriceItemProps = { - gasPrices: GasPrices | undefined; - transactionFee?: Wei | number | null; -}; - -const GasPriceItem: FC = memo(({ gasPrices, transactionFee }) => { +const GasPriceItem: FC = memo(() => { const gasSpeed = useAppSelector(selectGasSpeed); const customGasPrice = useAppSelector(selectGasPrice); const isL2 = useIsL2(); + const { useEthGasPriceQuery } = useSynthetixQueries(); + const ethGasPriceQuery = useEthGasPriceQuery(); + const gasPrices = useMemo(() => ethGasPriceQuery?.data, [ethGasPriceQuery.data]); + const transactionFee = useAppSelector(selectTransactionFeeWei); const formattedTransactionFee = useMemo(() => { return transactionFee ? formatDollars(transactionFee, { maxDecimals: 1 }) : NO_VALUE; }, [transactionFee]); - const hasCustomGasPrice = customGasPrice !== ''; + const hasCustomGasPrice = !!customGasPrice; const gasPrice = gasPrices ? parseGasPriceObject(gasPrices[gasSpeed]) : null; if (!gasPrice) return <>{NO_VALUE}; @@ -48,7 +45,7 @@ const GasPriceItem: FC = memo(({ gasPrices, transactionFee }) ); }); -const GasPriceSelect: FC = memo(({ gasPrices, transactionFee, ...rest }) => { +const GasPriceSelect: FC = memo(({ ...rest }) => { const { t } = useTranslation(); const isMainnet = useIsL1(); @@ -60,7 +57,7 @@ const GasPriceSelect: FC = memo(({ gasPrices, transactionFe : t('common.summary.gas-prices.gas-price')} - + ); diff --git a/sections/shared/components/PriceImpactSummary.tsx b/sections/shared/components/PriceImpactSummary.tsx index 14e9c4b1d3..61cd7fb98e 100644 --- a/sections/shared/components/PriceImpactSummary.tsx +++ b/sections/shared/components/PriceImpactSummary.tsx @@ -1,17 +1,15 @@ -import Wei from '@synthetixio/wei'; import { FC, memo } from 'react'; import { useTranslation } from 'react-i18next'; import { NO_VALUE } from 'constants/placeholder'; import { SummaryItem, SummaryItemValue, SummaryItemLabel } from 'sections/exchange/summary'; +import { selectSlippagePercentWei } from 'state/exchange/selectors'; +import { useAppSelector } from 'state/hooks'; import { formatPercent } from 'utils/formatters/number'; -type PriceImpactProps = { - slippagePercent: Wei; -}; - -const PriceImpactSummary: FC = memo(({ slippagePercent }) => { +const PriceImpactSummary: FC = memo(() => { const { t } = useTranslation(); + const slippagePercent = useAppSelector(selectSlippagePercentWei); return ( diff --git a/sections/shared/modals/SelectCurrencyModal.tsx b/sections/shared/modals/SelectCurrencyModal.tsx index 9a7b2ecad0..5232ae16f9 100644 --- a/sections/shared/modals/SelectCurrencyModal.tsx +++ b/sections/shared/modals/SelectCurrencyModal.tsx @@ -6,7 +6,7 @@ import { useTranslation } from 'react-i18next'; import InfiniteScroll from 'react-infinite-scroll-component'; import styled from 'styled-components'; -import SearchInput from 'components/Input/SearchInput'; +import Input from 'components/Input/Input'; import { FlexDivCentered } from 'components/layout/flex'; import { RowsHeader, CenteredModal } from 'components/layout/modals'; import Loader from 'components/Loader'; @@ -265,7 +265,7 @@ const SearchContainer = styled.div` margin: 0 16px 12px 16px; `; -const AssetSearchInput = styled(SearchInput)` +const AssetSearchInput = styled(Input).attrs({ type: 'search' })` font-size: 16px; height: 40px; font-family: ${(props) => props.theme.fonts.regular}; diff --git a/sections/stats/TimeframeSwitcher.tsx b/sections/stats/TimeframeSwitcher.tsx index 4bdd5dddc7..bc90659745 100644 --- a/sections/stats/TimeframeSwitcher.tsx +++ b/sections/stats/TimeframeSwitcher.tsx @@ -18,7 +18,7 @@ const StyledBtn = styled(Button)` height: 24px; border: 0.669444px solid rgba(255, 255, 255, 0.1); border-color: ${(props) => - props.isActive ? props.theme.colors.common.primaryGold : 'rgba(255, 255, 255, 0.1)'}; + props.active ? props.theme.colors.common.primaryGold : 'rgba(255, 255, 255, 0.1)'}; border-radius: 7px; box-shadow: 0px 1.33889px 1.33889px rgba(0, 0, 0, 0.25), inset 0px 0px 13.3889px rgba(255, 255, 255, 0.03); @@ -49,7 +49,7 @@ export const TimeframeSwitcher: FC = () => { dispatch(setSelectedTimeframe(timeframe))} - isActive={selectedTimeframe === timeframe} + active={selectedTimeframe === timeframe} > {timeframe} diff --git a/state/futures/selectors.ts b/state/futures/selectors.ts index 15bf540e55..6d63487d2a 100644 --- a/state/futures/selectors.ts +++ b/state/futures/selectors.ts @@ -1,5 +1,5 @@ import { createSelector } from '@reduxjs/toolkit'; -import { wei } from '@synthetixio/wei'; +import Wei, { wei } from '@synthetixio/wei'; import { formatBytes32String } from 'ethers/lib/utils'; import { DEFAULT_LEVERAGE, DEFAULT_NP_LEVERAGE_ADJUSTMENT } from 'constants/defaults'; @@ -11,7 +11,7 @@ import { accountType, deserializeWeiObject } from 'state/helpers'; import { selectOffchainPricesInfo, selectPrices } from 'state/prices/selectors'; import { RootState } from 'state/store'; import { selectNetwork, selectWallet } from 'state/wallet/selectors'; -import { sameSide } from 'utils/costCalculations'; +import { computeOrderFee, sameSide } from 'utils/costCalculations'; import { getKnownError } from 'utils/formatters/error'; import { zeroBN } from 'utils/formatters/number'; import { @@ -846,6 +846,15 @@ export const selectHasRemainingMargin = createSelector( } ); +export const selectOrderFee = createSelector( + selectMarketInfo, + selectTradeSizeInputs, + selectOrderType, + (marketInfo, { susdSizeDelta }, orderType) => { + return computeOrderFee(marketInfo, susdSizeDelta, orderType); + } +); + export const selectMaxUsdInputAmount = createSelector( selectFuturesType, selectPosition, @@ -884,3 +893,121 @@ export const selectPreviewAvailableMargin = createSelector( : zeroBN; } ); + +export const selectAverageEntryPrice = createSelector( + selectTradePreview, + selectSelectedMarketPositionHistory, + (tradePreview, positionHistory) => { + if (positionHistory && tradePreview) { + const { avgEntryPrice, side, size } = positionHistory; + const currentSize = side === PositionSide.SHORT ? size.neg() : size; + + // If the trade switched sides (long -> short or short -> long), use oracle price + if (currentSize.mul(tradePreview.size).lt(0)) return tradePreview.price; + + // If the trade reduced position size on the same side, average entry remains the same + if (tradePreview.size.abs().lt(size)) return avgEntryPrice; + + // If the trade increased position size on the same side, calculate new average + const existingValue = avgEntryPrice.mul(size); + const newValue = tradePreview.price.mul(tradePreview.sizeDelta.abs()); + const totalValue = existingValue.add(newValue); + return totalValue.div(tradePreview.size.abs()); + } + return null; + } +); + +type PositionPreviewData = { + fillPrice: Wei; + sizeIsNotZero: boolean; + positionSide: string; + positionSize: Wei; + leverage: Wei; + liquidationPrice: Wei; + avgEntryPrice: Wei; + notionalValue: Wei; + showStatus: boolean; +}; + +export const selectPreviewData = createSelector( + selectTradePreview, + selectPosition, + selectAverageEntryPrice, + (tradePreview, position, modifiedAverage) => { + if (!position?.position || tradePreview === null) { + return {} as PositionPreviewData; + } + + return { + fillPrice: tradePreview.price, + sizeIsNotZero: tradePreview.size && !tradePreview.size?.eq(0), + positionSide: tradePreview.size?.gt(0) ? PositionSide.LONG : PositionSide.SHORT, + positionSize: tradePreview.size?.abs(), + notionalValue: tradePreview.notionalValue, + leverage: tradePreview.margin.gt(0) + ? tradePreview.notionalValue.div(tradePreview.margin).abs() + : zeroBN, + liquidationPrice: tradePreview.liqPrice, + avgEntryPrice: modifiedAverage || zeroBN, + showStatus: tradePreview.showStatus, + } as PositionPreviewData; + } +); + +export const selectPreviewTradeData = createSelector( + selectTradePreview, + selectPreviewAvailableMargin, + selectMarketInfo, + (tradePreview, previewAvailableMargin, marketInfo) => { + const potentialMarginUsage = tradePreview?.margin.gt(0) + ? tradePreview!.margin.sub(previewAvailableMargin).div(tradePreview!.margin).abs() ?? zeroBN + : zeroBN; + + const maxPositionSize = + !!tradePreview && !!marketInfo + ? tradePreview.margin + .mul(marketInfo.maxLeverage) + .mul(tradePreview.side === PositionSide.LONG ? 1 : -1) + : null; + + const potentialBuyingPower = !!maxPositionSize + ? maxPositionSize.sub(tradePreview?.notionalValue).abs() + : zeroBN; + + return { + showPreview: !!tradePreview && tradePreview.sizeDelta.abs().gt(0), + totalMargin: tradePreview?.margin || zeroBN, + availableMargin: previewAvailableMargin.gt(0) ? previewAvailableMargin : zeroBN, + buyingPower: potentialBuyingPower.gt(0) ? potentialBuyingPower : zeroBN, + marginUsage: potentialMarginUsage.gt(1) ? wei(1) : potentialMarginUsage, + }; + } +); + +export const selectBuyingPower = createSelector( + selectPosition, + selectMaxLeverage, + (position, maxLeverage) => { + const totalMargin = position?.remainingMargin ?? zeroBN; + return totalMargin.gt(zeroBN) ? totalMargin.mul(maxLeverage ?? zeroBN) : zeroBN; + } +); + +export const selectMarginUsage = createSelector( + selectAvailableMargin, + selectPosition, + (availableMargin, position) => { + const totalMargin = position?.remainingMargin ?? zeroBN; + return availableMargin.gt(zeroBN) + ? totalMargin.sub(availableMargin).div(totalMargin) + : totalMargin.gt(zeroBN) + ? wei(1) + : zeroBN; + } +); + +export const selectMarketSuspended = createSelector( + selectMarketInfo, + (marketInfo) => marketInfo?.isSuspended +); diff --git a/stories/AccountStats.stories.tsx b/stories/AccountStats.stories.tsx new file mode 100644 index 0000000000..1de01afd9d --- /dev/null +++ b/stories/AccountStats.stories.tsx @@ -0,0 +1,17 @@ +import { InfoBoxContainer, InfoBoxRow } from 'components/InfoBox'; + +export default { + title: 'Futures/AccountStats', +}; + +export const Default = () => { + return ( +
+ + + + + +
+ ); +}; diff --git a/stories/Badge.stories.tsx b/stories/Badge.stories.tsx new file mode 100644 index 0000000000..a446d9fadd --- /dev/null +++ b/stories/Badge.stories.tsx @@ -0,0 +1,30 @@ +import { ComponentMeta, ComponentStory } from '@storybook/react'; + +import Badge from 'components/Badge'; + +export default { + title: 'Components/Badge', + component: Badge, +} as ComponentMeta; + +const Template: ComponentStory = (args) => Badge; + +export const Default = Template.bind({}); + +export const Dark = Template.bind({}); + +Dark.args = { + dark: true, +}; + +export const Gray = Template.bind({}); + +Gray.args = { + color: 'gray', +}; + +export const Red = Template.bind({}); + +Red.args = { + color: 'red', +}; diff --git a/stories/Button.stories.tsx b/stories/Button.stories.tsx index 6c028d046c..2abdd7e422 100644 --- a/stories/Button.stories.tsx +++ b/stories/Button.stories.tsx @@ -26,20 +26,20 @@ const Template: ComponentStory = (args) =>