diff --git a/src/components/Footer/Footer.tsx b/src/components/Footer/Footer.tsx index 596d65be3f..27f185f8b7 100644 --- a/src/components/Footer/Footer.tsx +++ b/src/components/Footer/Footer.tsx @@ -16,9 +16,13 @@ import { TrackingLink } from "components/TrackingLink/TrackingLink"; import { userAnalytics } from "lib/userAnalytics"; import { LandingPageFooterMenuEvent } from "lib/userAnalytics/types"; -type Props = { showRedirectModal?: (to: string) => void; redirectPopupTimestamp?: number }; +type Props = { + showRedirectModal?: (to: string) => void; + redirectPopupTimestamp?: number; + isMobileTradePage?: boolean; +}; -export default function Footer({ showRedirectModal, redirectPopupTimestamp }: Props) { +export default function Footer({ showRedirectModal, redirectPopupTimestamp, isMobileTradePage }: Props) { const isHome = isHomeSite(); const [isUserFeedbackModalVisible, setIsUserFeedbackModalVisible] = useState(false); @@ -33,10 +37,14 @@ export default function Footer({ showRedirectModal, redirectPopupTimestamp }: Pr return ( <>
) { + const curtainRef = useRef(null); + const isPointerDownRef = useRef(false); + const isDraggingRef = useRef(false); + const currentRelativeY = useRef(0); + const prevScreenY = useRef(0); + const prevScreenX = useRef(0); + const prevTime = useRef(0); + const currentVelocity = useRef(0); + const isDirectionLocked = useRef(false); + const startX = useRef(0); + const startY = useRef(0); + + const [isOpen, setIsOpen] = useState(false); + + const handleAnimate = useCallback((newIsOpen: boolean) => { + if (!curtainRef.current) return; + + const oldTransition = curtainRef.current.style.transition; + const animation = curtainRef.current.animate( + { + transform: `translateY(${newIsOpen ? `calc(-100% + ${HEADER_HEIGHT}px)` : 0})`, + }, + { + duration: 150, + easing: "ease-out", + fill: "both", + } + ); + animation.addEventListener("finish", () => { + animation.commitStyles(); + animation.cancel(); + if (curtainRef.current) { + curtainRef.current.style.transition = oldTransition; + } + }); + }, []); + + const headerClick = useCallback(() => { + setIsOpen(true); + handleAnimate(true); + }, [setIsOpen, handleAnimate]); + + const handleToggle = useCallback(() => { + const newIsOpen = !isOpen; + setIsOpen(newIsOpen); + handleAnimate(newIsOpen); + }, [isOpen, handleAnimate]); + + const handleClose = useCallback(() => { + setIsOpen(false); + handleAnimate(false); + }, [setIsOpen, handleAnimate]); + + const handlePointerDown = useCallback((e: React.PointerEvent) => { + e.stopPropagation(); + + if (!curtainRef.current) { + return; + } + + isPointerDownRef.current = true; + isDirectionLocked.current = false; + isDraggingRef.current = false; + startX.current = e.screenX; + startY.current = e.screenY; + + const transformString = curtainRef.current.style.transform; + const transform = new DOMMatrix(transformString); + currentRelativeY.current = transform.f; + prevScreenY.current = e.screenY; + prevScreenX.current = e.screenX; + }, []); + + const handlePointerMove = useCallback((e: React.PointerEvent) => { + if (!isPointerDownRef.current) return; + + const offsetX = e.screenX - startX.current; + const offsetY = e.screenY - startY.current; + + if (!isDirectionLocked.current) { + const isVertical = Math.abs(offsetY) > Math.abs(offsetX) * DIRECTION_THRESHOLD; + if (Math.abs(offsetX) > MOVEMENT_THRESHOLD || Math.abs(offsetY) > MOVEMENT_THRESHOLD) { + isDirectionLocked.current = true; + isDraggingRef.current = isVertical; + if (!isVertical) return; + } + } + + if (!isDraggingRef.current || !curtainRef.current) return; + + const deltaY = e.screenY - prevScreenY.current; + + curtainRef.current.style.willChange = "transform"; + + const time = e.timeStamp - prevTime.current; + const velocity = deltaY / time; + + let newY = currentRelativeY.current + deltaY; + + if (newY > 0) { + newY = 0; + } else if (newY < -curtainRef.current.clientHeight + HEADER_HEIGHT) { + newY = -curtainRef.current.clientHeight + HEADER_HEIGHT; + } + + curtainRef.current.style.transform = `translateY(${newY}px)`; + + currentRelativeY.current = newY; + prevTime.current = e.timeStamp; + currentVelocity.current = velocity; + + prevScreenX.current = e.screenX; + prevScreenY.current = e.screenY; + }, []); + + const handlePointerUp = useCallback(() => { + isPointerDownRef.current = false; + + if (!isDraggingRef.current || !curtainRef.current) { + return; + } + + isDraggingRef.current = false; + curtainRef.current.style.willChange = ""; + + const targetY = + currentRelativeY.current + + (Math.sign(currentVelocity.current) * currentVelocity.current ** 2) / (2 * DECELERATION); + + if (curtainRef.current) { + const isOpen = curtainRef.current.clientHeight / 2 < -targetY; + setIsOpen(isOpen); + handleAnimate(isOpen); + } + }, [handleAnimate]); + + return ( + <> +
+ +
+
+
+ {header} +
+ +
+ +
+
{children}
+
+
+
+ + ); +} diff --git a/src/components/Synthetics/TradeBox/TradeBox.scss b/src/components/Synthetics/TradeBox/TradeBox.scss index f999cb0563..19586f97a5 100644 --- a/src/components/Synthetics/TradeBox/TradeBox.scss +++ b/src/components/Synthetics/TradeBox/TradeBox.scss @@ -3,21 +3,6 @@ padding: 1.5rem; } -.SwapBox-option-tabs.Tab.Tab__block .Tab-option { - padding: 1.05rem; -} - -.SwapBox-option-tabs.Tab.Tab__block .Tab-option-icon { - margin-top: -0.155rem; - transform: scale(0.75); - vertical-align: middle; - margin-right: 0.8rem; -} - -.SwapBox-asset-options-tabs { - margin: 1.5rem 0; -} - .SwapBox-info-section { margin-bottom: 0.8rem; } diff --git a/src/components/Synthetics/TradeBox/TradeBox.tsx b/src/components/Synthetics/TradeBox/TradeBox.tsx index bfa74ca9f8..17f9d5162c 100644 --- a/src/components/Synthetics/TradeBox/TradeBox.tsx +++ b/src/components/Synthetics/TradeBox/TradeBox.tsx @@ -3,7 +3,6 @@ import { useConnectModal } from "@rainbow-me/rainbowkit"; import cx from "classnames"; import { ChangeEvent, KeyboardEvent, ReactNode, useCallback, useEffect, useMemo, useRef } from "react"; import { IoMdSwap } from "react-icons/io"; -import { useHistory } from "react-router-dom"; import { useKey, useLatest, usePrevious } from "react-use"; import { getBridgingOptionsForToken } from "config/bridging"; @@ -62,7 +61,6 @@ import { } from "domain/synthetics/positions"; import { convertToUsd } from "domain/synthetics/tokens"; import { - TradeType, applySlippageToPrice, getIncreasePositionAmounts, getNextPositionValuesForIncreaseTrade, @@ -97,13 +95,13 @@ import { EMPTY_ARRAY, getByKey } from "lib/objects"; import { sleep } from "lib/sleep"; import { mustNeverExist } from "lib/types"; import { useCursorInside } from "lib/useCursorInside"; +import { usePendingTxns } from "lib/usePendingTxns"; import useIsMetamaskMobile from "lib/wallets/useIsMetamaskMobile"; import useWallet from "lib/wallets/useWallet"; import TokenIcon from "components/TokenIcon/TokenIcon"; import { ExecutionPriceRow } from "../ExecutionPriceRow"; import { NetworkFeeRow } from "../NetworkFeeRow/NetworkFeeRow"; -import { SwapCard } from "../SwapCard/SwapCard"; import { TradeFeesRow } from "../TradeFeesRow/TradeFeesRow"; import { MarketPoolSelectorRow } from "./MarketPoolSelectorRow"; import { CollateralSelectorRow } from "./TradeBoxRows/CollateralSelectorRow"; @@ -143,15 +141,12 @@ import { TradeBoxOneClickTrading } from "./TradeBoxRows/OneClickTrading"; import { selectChartHeaderInfo } from "context/SyntheticsStateContext/selectors/chartSelectors"; import { MissedCoinsPlace } from "domain/synthetics/userFeedback"; import { sendTradeBoxInteractionStartedEvent, sendUserAnalyticsConnectWalletClickEvent } from "lib/userAnalytics"; -import { tradeModeLabels, tradeTypeClassNames, tradeTypeIcons, tradeTypeLabels } from "./tradeboxConstants"; +import { tradeModeLabels, tradeTypeLabels } from "./tradeboxConstants"; import "./TradeBox.scss"; -export type Props = { - setPendingTxns: (txns: any) => void; -}; - -export function TradeBox(p: Props) { +export function TradeBox({ isInCurtain }: { isInCurtain?: boolean }) { + const [, setPendingTxns] = usePendingTxns(); const localizedTradeModeLabels = useLocalizedMap(tradeModeLabels); const localizedTradeTypeLabels = useLocalizedMap(tradeTypeLabels); @@ -161,10 +156,8 @@ export function TradeBox(p: Props) { const isCursorInside = useCursorInside(formRef); const allowedSlippage = useSelector(selectTradeboxAllowedSlippage); - const { setPendingTxns } = p; const { openConnectModal } = useConnectModal(); - const history = useHistory(); const { swapTokens, infoTokens, sortedLongAndShortTokens, sortedAllMarkets } = avaialbleTokenOptions; const tokensData = useTokensData(); const marketsInfoData = useMarketsInfoData(); @@ -195,7 +188,6 @@ export function TradeBox(p: Props) { setToTokenInputValue: setToTokenInputValueRaw, setCollateralAddress: onSelectCollateralAddress, setFromTokenAddress: onSelectFromTokenAddress, - setTradeType: onSelectTradeType, setTradeMode: onSelectTradeMode, stage, setStage, @@ -813,13 +805,6 @@ export function TradeBox(p: Props) { toTokenInputValue, ]); - function onTradeTypeChange(type: TradeType) { - onSelectTradeType(type); - if (tradeType !== type) { - history.push(`/trade/${type.toLowerCase()}`); - } - } - const { onSubmitWrapOrUnwrap, onSubmitSwap, onSubmitIncreaseOrder, onSubmitDecreaseOrder } = useTradeboxTransactions({ setPendingTxns, }); @@ -1421,100 +1406,84 @@ export function TradeBox(p: Props) { return ( <> -
-
- - - -
- {(isSwap || isIncrease) && renderTokenInputs()} - {isTrigger && renderDecreaseSizeInput()} - - {isSwap && isLimit && renderTriggerRatioInput()} - {isPosition && (isLimit || isTrigger) && renderTriggerPriceInput()} - - - - {maxAutoCancelOrdersWarning} - {isSwap && isLimit && ( - - - The execution price will constantly vary based on fees and price impact to guarantee that you - receive the minimum receive amount. - - + + + {(isSwap || isIncrease) && renderTokenInputs()} + {isTrigger && renderDecreaseSizeInput()} + + {isSwap && isLimit && renderTriggerRatioInput()} + {isPosition && (isLimit || isTrigger) && renderTriggerPriceInput()} + + + + {maxAutoCancelOrdersWarning} + {isSwap && isLimit && ( + + + The execution price will constantly vary based on fees and price impact to guarantee that you receive + the minimum receive amount. + + + )} + + + {isPosition && renderPositionControls()} + + + + + + + + + + {isIncrease && renderIncreaseOrderInfo()} + {isTrigger && renderTriggerOrderInfo()} + + + + + + + + + + + + {isTrigger && selectedPosition && decreaseAmounts?.receiveUsd !== undefined && ( + + - - {isPosition && renderPositionControls()} - - - - - - - - - - {isIncrease && renderIncreaseOrderInfo()} - {isTrigger && renderTriggerOrderInfo()} - - - - - - - - - - - - {isTrigger && selectedPosition && decreaseAmounts?.receiveUsd !== undefined && ( - - - - )} - - {isSwap && ( - - - - )} + /> + + )} - {tradeboxWarningRows && {tradeboxWarningRows}} - {triggerConsentRows && {triggerConsentRows}} - -
{button}
- -
-
+ {isSwap && ( + + + + )} - {isSwap && } + {tradeboxWarningRows && {tradeboxWarningRows}} + {triggerConsentRows && {triggerConsentRows}} + +
+
{button}
+ ); } diff --git a/src/components/Synthetics/TradeBox/TradeBoxHeaderTabs.tsx b/src/components/Synthetics/TradeBox/TradeBoxHeaderTabs.tsx new file mode 100644 index 0000000000..f710660512 --- /dev/null +++ b/src/components/Synthetics/TradeBox/TradeBoxHeaderTabs.tsx @@ -0,0 +1,57 @@ +import { useCallback } from "react"; +import { useHistory } from "react-router-dom"; + +import { selectTradeboxState } from "context/SyntheticsStateContext/selectors/tradeboxSelectors"; +import { useSelector } from "context/SyntheticsStateContext/utils"; +import { TradeType } from "domain/synthetics/trade"; +import { useLocalizedMap } from "lib/i18n"; + +import Tab from "components/Tab/Tab"; + +import { mobileTradeTypeClassNames, tradeTypeClassNames, tradeTypeIcons, tradeTypeLabels } from "./tradeboxConstants"; +import { SwipeTabs } from "components/Tab/SwipeTabs"; + +const OPTIONS = Object.values(TradeType); + +export function TradeBoxHeaderTabs({ isInCurtain }: { isInCurtain?: boolean }) { + const localizedTradeTypeLabels = useLocalizedMap(tradeTypeLabels); + const history = useHistory(); + const { setTradeType: onSelectTradeType, tradeType } = useSelector(selectTradeboxState); + + const onTradeTypeChange = useCallback( + (type: TradeType) => { + onSelectTradeType(type); + if (tradeType !== type) { + history.push(`/trade/${type.toLowerCase()}`); + } + }, + [history, onSelectTradeType, tradeType] + ); + + if (!isInCurtain) { + return ( + + ); + } + + return ( + + ); +} diff --git a/src/components/Synthetics/TradeBox/TradeBoxResponsiveContainer.tsx b/src/components/Synthetics/TradeBox/TradeBoxResponsiveContainer.tsx new file mode 100644 index 0000000000..ba01b4c05f --- /dev/null +++ b/src/components/Synthetics/TradeBox/TradeBoxResponsiveContainer.tsx @@ -0,0 +1,24 @@ +import { useMedia } from "react-use"; + +import { Curtain } from "./Curtain"; +import { TradeBox } from "./TradeBox"; +import { TradeBoxHeaderTabs } from "./TradeBoxHeaderTabs"; + +export function TradeBoxResponsiveContainer() { + const isMobile = useMedia("(max-width: 1100px)"); + + if (!isMobile) { + return ( +
+ + +
+ ); + } + + return ( + } dataQa="tradebox"> + + + ); +} diff --git a/src/components/Synthetics/TradeBox/tradeboxConstants.tsx b/src/components/Synthetics/TradeBox/tradeboxConstants.tsx index 8e413c854f..460f1fdce5 100644 --- a/src/components/Synthetics/TradeBox/tradeboxConstants.tsx +++ b/src/components/Synthetics/TradeBox/tradeboxConstants.tsx @@ -42,3 +42,9 @@ export const tradeTypeClassNames = { regular: "border-b border-b-[transparent]", }, }; + +export const mobileTradeTypeClassNames = { + [TradeType.Long]: "!bg-[#1F3445] border-b border-b-green-500", + [TradeType.Short]: "!bg-[#392A46] border-b border-b-red-500", + [TradeType.Swap]: "!bg-[#252B57] border-b border-b-blue-300", +}; diff --git a/src/components/Tab/SwipeTabs.tsx b/src/components/Tab/SwipeTabs.tsx new file mode 100644 index 0000000000..70240bcb3d --- /dev/null +++ b/src/components/Tab/SwipeTabs.tsx @@ -0,0 +1,246 @@ +import cx from "classnames"; +import { MotionProps, MotionStyle, PanInfo, motion, useSpring } from "framer-motion"; +import { ReactNode, useCallback, useEffect, useMemo, useRef, useState } from "react"; + +type Props = { + options: (string | number)[]; + option: string | number | undefined; + onChange?: (option: any) => void; + optionLabels?: Record | string[]; + optionClassnames?: Record; + icons?: Record; + qa?: string; +}; + +const MAGNETIC_SNAP_WEIGHT = 0.8; +const SWIPE_SENSITIVITY = 1.5; +const DIRECTION_THRESHOLD = 2; +const MOVEMENT_THRESHOLD = 10; + +function getTransformTemplate({ x }: Parameters>[0]) { + // Make a custom string to avoid translateZ passed by Framer Motion default + // It causes stacking context issues in iOS + return `translateX(${x})`; +} + +export function SwipeTabs({ options, option, onChange, optionLabels, icons, qa, optionClassnames }: Props) { + const containerRef = useRef(null); + const optionsRefs = useRef>({}); + const bgRef = useRef(null); + const [visuallyActiveOption, setVisuallyActiveOption] = useState(option); + const offsetRef = useRef(0); + const x = useSpring(0, { stiffness: 1000, damping: 60, bounce: 0 }); + const isDragging = useRef(false); + const isDirectionLocked = useRef(false); + + const onClick = useCallback( + (opt: string | number) => { + onChange?.(opt); + + const optElem = optionsRefs.current[options.indexOf(opt)]; + if (!optElem) return; + + const optRect = optElem.getBoundingClientRect(); + const containerRect = containerRef.current?.getBoundingClientRect(); + if (!containerRect) return; + + x.set(optRect.x - containerRect.x); + + if (bgRef.current) { + bgRef.current.style.width = `${optRect.width}px`; + } + setVisuallyActiveOption(opt); + }, + [onChange, options, x] + ); + + const getNearestOption = useCallback( + ( + atX: number, + params: { withHalfWidth: boolean; isAbsolute: boolean } = { withHalfWidth: true, isAbsolute: false } + ) => { + const containerRect = containerRef.current?.getBoundingClientRect(); + const bgRect = bgRef.current?.getBoundingClientRect(); + + if (!containerRect || !bgRect) return {}; + + const xDiff = atX + (params.withHalfWidth ? bgRect.width / 2 : 0) - (params.isAbsolute ? containerRect.x : 0); + + let acc = 0; + for (let i = 0; i < options.length; i++) { + const optionElem = optionsRefs.current[i]; + if (!optionElem) continue; + + acc += optionElem.clientWidth; + + if (acc > xDiff || i === options.length - 1) { + return { + option: options[i], + elem: optionElem, + width: optionElem.clientWidth, + offset: acc - optionElem.clientWidth, + }; + } + } + + return {}; + }, + [options] + ); + + const handlePanStart = useCallback(() => { + isDragging.current = false; + isDirectionLocked.current = false; + offsetRef.current = x.get(); + }, [x]); + + const handlePan = useCallback( + (event: PointerEvent, info: PanInfo) => { + if (!isDirectionLocked.current) { + const isHorizontal = Math.abs(info.offset.x) > Math.abs(info.offset.y) * DIRECTION_THRESHOLD; + if (Math.abs(info.offset.x) > MOVEMENT_THRESHOLD || Math.abs(info.offset.y) > MOVEMENT_THRESHOLD) { + isDirectionLocked.current = true; + isDragging.current = isHorizontal; + if (!isHorizontal) return; + } + } + + if (!isDragging.current) return; + + let newX = offsetRef.current + info.delta.x * SWIPE_SENSITIVITY; + offsetRef.current = newX; + + const { + option: nearestOption, + width, + offset, + } = getNearestOption(newX, { + withHalfWidth: true, + isAbsolute: false, + }); + + x.set((offset ?? 0) * MAGNETIC_SNAP_WEIGHT + newX * (1 - MAGNETIC_SNAP_WEIGHT)); + if (nearestOption) { + setVisuallyActiveOption(nearestOption); + + bgRef.current!.style.width = `${width}px`; + } + }, + [getNearestOption, x] + ); + + const handlePanEnd = useCallback(() => { + const { offset: targetOffset, option: nearestOption } = getNearestOption(offsetRef.current, { + withHalfWidth: true, + isAbsolute: false, + }); + + if (targetOffset !== undefined) { + isDragging.current = false; + + x.set(targetOffset); + + if (nearestOption !== option) { + setVisuallyActiveOption(nearestOption); + onChange?.(nearestOption); + } + } + }, [getNearestOption, onChange, option, x]); + + useEffect(() => { + if (!option) return; + + const optElem = optionsRefs.current[options.indexOf(option)]; + if (!optElem) return; + + const optRect = optElem.getBoundingClientRect(); + const containerRect = containerRef.current?.getBoundingClientRect(); + if (!containerRect) return; + + x.set(optRect.x - containerRect.x); + + if (bgRef.current) { + bgRef.current.style.width = `${optRect.width}px`; + } + setVisuallyActiveOption(option); + }, [option, options, x]); + + const bgStyle = useMemo( + (): MotionStyle => ({ + x, + }), + [x] + ); + + useEffect( + function handleResize() { + if (!bgRef.current) return; + + const handler = () => { + if (!option) return; + + const optElem = optionsRefs.current[options.indexOf(option)]; + if (!optElem) return; + + const containerRect = containerRef.current?.getBoundingClientRect(); + if (!containerRect) return; + + const optRect = optElem.getBoundingClientRect(); + + x.set(optRect.x - containerRect.x); + if (bgRef.current) { + bgRef.current.style.width = `${optRect.width}px`; + } + }; + + window.addEventListener("resize", handler); + + return () => { + window.removeEventListener("resize", handler); + }; + }, + [option, options, x] + ); + + return ( + + + + {options.map((opt, index) => { + const label = optionLabels && optionLabels[opt] ? optionLabels[opt] : opt; + const isActive = opt === visuallyActiveOption; + + return ( +
onClick(opt)} + key={opt} + data-qa={`${qa}-tab-${opt}`} + ref={(el) => (optionsRefs.current[index] = el)} + > + {icons && icons[opt] && {icons[opt]}} + {label} +
+ ); + })} +
+ ); +} diff --git a/src/components/Tab/Tab.css b/src/components/Tab/Tab.css index a6475e961f..d02f2ad4d3 100644 --- a/src/components/Tab/Tab.css +++ b/src/components/Tab/Tab.css @@ -21,6 +21,10 @@ padding-right: 1.5rem; } +.Tab.Tab__block.Tab__l .Tab-option { + padding: 10.5px; +} + .Tab.Tab__block .Tab-option:not(.disabled):not(.active):hover { color: var(--color-white); background: var(--color-cold-blue-700); diff --git a/src/components/Tab/Tab.tsx b/src/components/Tab/Tab.tsx index 5399b57c19..b4538ab80e 100644 --- a/src/components/Tab/Tab.tsx +++ b/src/components/Tab/Tab.tsx @@ -8,6 +8,7 @@ type Props = { option: string | number | undefined; setOption?: (option: any) => void; onChange?: (option: any) => void; + size?: "l" | "m"; type?: "block" | "inline"; className?: string; optionLabels?: Record | string[]; @@ -23,7 +24,18 @@ type Props = { }; export default function Tab(props: Props) { - const { options, option, setOption, onChange, type = "block", className, optionLabels, icons, qa } = props; + const { + options, + option, + setOption, + onChange, + type = "block", + className, + optionLabels, + icons, + qa, + size = "m", + } = props; const onClick = (opt) => { if (setOption) { setOption(opt); @@ -34,7 +46,7 @@ export default function Tab(props: Props) { }; return ( -
+
{options.map((opt) => { const className = props.optionClassnames && props.optionClassnames[opt]; const label = optionLabels && optionLabels[opt] ? optionLabels[opt] : opt; diff --git a/src/img/swap.svg b/src/img/swap.svg index d514628e78..69132cbddd 100644 --- a/src/img/swap.svg +++ b/src/img/swap.svg @@ -1 +1,5 @@ - + + + \ No newline at end of file diff --git a/src/pages/Exchange/Exchange.css b/src/pages/Exchange/Exchange.css index 8519cf3be4..933e1edf69 100644 --- a/src/pages/Exchange/Exchange.css +++ b/src/pages/Exchange/Exchange.css @@ -694,13 +694,6 @@ table.Position-list .Exchange-list-item-active { margin-top: 1.5rem; } -.Exchange-swap-option-tabs.Tab.Tab__block .Tab-option-icon { - margin-top: -0.155rem; - transform: scale(0.75); - vertical-align: middle; - margin-right: 0.8rem; -} - .Exchange-swap-section-top { display: grid; grid-template-columns: auto auto; diff --git a/src/pages/SyntheticsPage/SyntheticsPage.tsx b/src/pages/SyntheticsPage/SyntheticsPage.tsx index 2c0a1ed0af..034cce9b10 100644 --- a/src/pages/SyntheticsPage/SyntheticsPage.tsx +++ b/src/pages/SyntheticsPage/SyntheticsPage.tsx @@ -2,13 +2,13 @@ import { Plural, t, Trans } from "@lingui/macro"; import cx from "classnames"; import uniq from "lodash/uniq"; import { startTransition, useCallback, useEffect, useMemo, useState } from "react"; +import { useMedia } from "react-use"; import type { MarketFilterLongShortItemData } from "components/Synthetics/TableMarketFilter/MarketFilterLongShort"; import { getSyntheticsListSectionKey } from "config/localStorage"; import { useSettings } from "context/SettingsContext/SettingsContextProvider"; import { useSubaccount, useSubaccountCancelOrdersDetailsMessage } from "context/SubaccountContext/SubaccountContext"; -import { useCalcSelector } from "context/SyntheticsStateContext/SyntheticsStateContextProvider"; -import { useClosingPositionKeyState } from "context/SyntheticsStateContext/hooks/globalsHooks"; +import { useClosingPositionKeyState, useTokensData } from "context/SyntheticsStateContext/hooks/globalsHooks"; import { useCancellingOrdersKeysState } from "context/SyntheticsStateContext/hooks/orderEditorHooks"; import { useOrderErrorsCount } from "context/SyntheticsStateContext/hooks/orderHooks"; import { selectChartToken } from "context/SyntheticsStateContext/selectors/chartSelectors"; @@ -16,44 +16,47 @@ import { selectClaimablesCount } from "context/SyntheticsStateContext/selectors/ import { selectChainId, selectPositionsInfoData } from "context/SyntheticsStateContext/selectors/globalSelectors"; import { selectOrdersCount } from "context/SyntheticsStateContext/selectors/orderSelectors"; import { + selectTradeboxMaxLiquidityPath, selectTradeboxSetActivePosition, + selectTradeboxState, selectTradeboxTradeFlags, } from "context/SyntheticsStateContext/selectors/tradeboxSelectors"; +import { useCalcSelector } from "context/SyntheticsStateContext/SyntheticsStateContextProvider"; import { useSelector } from "context/SyntheticsStateContext/utils"; import { getMarketIndexName, getMarketPoolName } from "domain/synthetics/markets"; import { cancelOrdersTxn } from "domain/synthetics/orders/cancelOrdersTxn"; import type { OrderType } from "domain/synthetics/orders/types"; +import { useSetOrdersAutoCancelByQueryParams } from "domain/synthetics/orders/useSetOrdersAutoCancelByQueryParams"; import { TradeMode } from "domain/synthetics/trade"; import { useTradeParamsProcessor } from "domain/synthetics/trade/useTradeParamsProcessor"; +import { useInterviewNotification } from "domain/synthetics/userFeedback/useInterviewNotification"; import { getMidPrice } from "domain/tokens"; import { useChainId } from "lib/chains"; import { helperToast } from "lib/helperToast"; import { getPageTitle } from "lib/legacy"; import { useLocalStorageSerializeKey } from "lib/localStorage"; +import { useMeasureComponentMountTime } from "lib/metrics/useMeasureComponentMountTime"; import { formatUsdPrice } from "lib/numbers"; import { EMPTY_ARRAY, getByKey } from "lib/objects"; import { usePendingTxns } from "lib/usePendingTxns"; import { useEthersSigner } from "lib/wallets/useEthersSigner"; import useWallet from "lib/wallets/useWallet"; +import { getTokenVisualMultiplier } from "sdk/configs/tokens"; -import { NpsModal } from "components/NpsModal/NpsModal"; import Checkbox from "components/Checkbox/Checkbox"; import Footer from "components/Footer/Footer"; import { InterviewModal } from "components/InterviewModal/InterviewModal"; +import { NpsModal } from "components/NpsModal/NpsModal"; import { Claims } from "components/Synthetics/Claims/Claims"; import { OrderList } from "components/Synthetics/OrderList/OrderList"; import { PositionEditor } from "components/Synthetics/PositionEditor/PositionEditor"; import { PositionList } from "components/Synthetics/PositionList/PositionList"; import { PositionSeller } from "components/Synthetics/PositionSeller/PositionSeller"; -import { TVChart } from "components/Synthetics/TVChart/TVChart"; -import { TradeBox } from "components/Synthetics/TradeBox/TradeBox"; +import { SwapCard } from "components/Synthetics/SwapCard/SwapCard"; +import { TradeBoxResponsiveContainer } from "components/Synthetics/TradeBox/TradeBoxResponsiveContainer"; import { TradeHistory } from "components/Synthetics/TradeHistory/TradeHistory"; +import { TVChart } from "components/Synthetics/TVChart/TVChart"; import Tab from "components/Tab/Tab"; -import { useInterviewNotification } from "domain/synthetics/userFeedback/useInterviewNotification"; -import { useMedia } from "react-use"; -import { useMeasureComponentMountTime } from "lib/metrics"; -import { useSetOrdersAutoCancelByQueryParams } from "domain/synthetics/orders/useSetOrdersAutoCancelByQueryParams"; -import { getTokenVisualMultiplier } from "sdk/configs/tokens"; export type Props = { openSettings: () => void; @@ -115,6 +118,12 @@ export function SyntheticsPage(p: Props) { setOrderTypesFilter, } = useOrdersControl(); + const { maxLiquidity: swapOutLiquidity } = useSelector(selectTradeboxMaxLiquidityPath); + const tokensData = useTokensData(); + const { fromTokenAddress, toTokenAddress } = useSelector(selectTradeboxState); + const fromToken = getByKey(tokensData, fromTokenAddress); + const toToken = getByKey(tokensData, toTokenAddress); + const [selectedPositionOrderKey, setSelectedPositionOrderKey] = useState(); const handlePositionListOrdersClick = useCallback( @@ -238,8 +247,12 @@ export function SyntheticsPage(p: Props) { useMeasureComponentMountTime({ metricType: "syntheticsPage", onlyForLocation: "#/trade" }); return ( -
-
+
+
{!isMobile && ( @@ -306,10 +319,17 @@ export function SyntheticsPage(p: Props) { )}
-
-
- -
+
+ + {isSwap && ( +
+ +
+ )}
{isMobile && ( @@ -352,14 +372,11 @@ export function SyntheticsPage(p: Props) {
)}
- - - -
); }