From 5039e836919e340a90382478a5f2016d34e550ec Mon Sep 17 00:00:00 2001 From: yjin <124853691+tfrg@users.noreply.github.com> Date: Tue, 7 Jan 2025 18:16:07 +0900 Subject: [PATCH] [GSW-2033] swap renewal (#598) * feat: [GSW-2033] Swap Renewal (TokenPriceChart LineGraph) * feat: [GSW-2033] Swap Renewal (TokenPriceChart LineGraph) * feat: [GSW-2033] Swap Renewal (TokenPriceChart LineGraph) * fix: [GSW-2033] Token Price Chart * feat: [GSW-2033] display default token when no tokens are selected * feat: [GSW-2033] add mouseOut handler for chart reset * feat: [GSW-2033] i18n * fix: [GSW-2033] Change chart time notation * feat: [GSW-2033] Improve Swap Chart Gradient position & loading * feat: [GSW-2033] Remove the top and bottom coordinate (price range) lines * fix: [GSW-2033] Improve mobile UI * feat: [GSW-2033] TokenPair Transaction list UI * fix: Not fetching when typing after the maximum number of decimal places * fix: [GSW-2033] skip loading state for messages ending with period * fix: [GSW-2033] Improve UI * fix: [GSW-2033] Improve UI * fix: [GSW-2033] Response layout * fix: [GSW-2033] Improve layout * fix: [GSW-2033] Improve UI * fix: [GSW-2033] Improve UI * fix: [GSW-2033] Improve UI * feat: [GSW-2033] Amount Input UI renewal * feat: [GSW-2033] Amount Input UI renewal in TokenDetail * feat: [GSW-2033] Amount Input UI renewal in EarnAddPosition * feat: [GSW-2033] Amount Input UI renewal in Wallet Send * feat: [GSW-2033] Amount Input UI renewal in Launchpad * fix: [GSW-2033] path-link-icon color * refactor: [GSW-2033] Extract this nested ternary * fix: [GSW-2033] Improve AmountInput UI * Revert "refactor: [GSW-2033] Extract this nested ternary" This reverts commit 632104e010695656d14d7df91c625482f96fb1db. * fix: [GSW-2033] UI * refactor: [GSW-2033] SonarQube issue * fix: [GSW-2033] Improved when loading is not visible * fix: [GSW-2033] TokenSwapChart Mobile Hover UI * fix: [GSW-2033] Improve UI * feat: [GSW-2033] refetching UI * feat: [GSW-2033] Improve User Typing state * fix: [GSW-2033] MaxButton size * fix: [GSW-2033] TokenPath Responsive UI * fix: [GSW-2033] TokenSelect Modal UI Issue * fix: [GSW-2033] TokenPath UI * feat: [GSW-2033] Improve UI * fix: [GSW-2033] Mobile Chart Hover * fix: [GSW-2033] Native TokenDetail Link * fix: [GSW-2033] Native TokenDetail Link * fix: [GSW-2033] TokenPath LinkIcon UI * refactor: [GSW-2033] SonarQube issue * refactor: [GSW-2033] SonarQube issue * fix: [GSW-2033] useElementWidthList * fix: [GSW-2033] useElementWidthList * refactor: [GSW-2033] refactor: Improve readability of graph hover logic with early return * refactor: [GSW-2033] apply PR review feedback --- .../src/components/common/divider/divider.tsx | 1 + .../SearchMenuModal.styles.ts | 28 ++-- .../search-menu-modal/SearchMenuModal.tsx | 112 ++++++--------- .../components/common/icons/IconWallet.tsx | 32 +++++ .../common/line-graph/LineGraph.styles.ts | 10 +- .../common/line-graph/LineGraph.tsx | 82 ++++++++--- .../common/select-token/SelectToken.tsx | 21 +-- .../TokenAmountInput.styles.ts | 24 +++- .../token-amount-input/TokenAmountInput.tsx | 21 ++- .../token-info-cell/TokenInfoCell.styles.ts | 15 +- .../common/token-info-cell/TokenInfoCell.tsx | 55 ++------ packages/web/src/constants/graph.constant.ts | 7 + .../web/src/constants/skeleton.constant.ts | 4 + .../SelectTokenContainer.tsx | 4 +- .../hooks/common/use-element-width-list.tsx | 37 +++++ .../src/hooks/common/use-element-width.tsx | 26 ++++ .../src/hooks/swap/data/use-swap-handler.tsx | 61 ++++++--- packages/web/src/hooks/swap/data/use-swap.tsx | 59 ++++---- .../launchpad-detail/LaunchpadDetail.tsx | 3 + .../LaunchpadParticipate.styles.ts | 25 +++- .../LaunchpadParticipate.tsx | 37 +++-- .../LaunchpadParticipateContainer.tsx | 4 +- .../LiquidityEnterAmounts.tsx | 2 + packages/web/src/layouts/swap/Swap.tsx | 12 +- .../web/src/layouts/swap/SwapLayout.styles.ts | 5 +- packages/web/src/layouts/swap/SwapLayout.tsx | 13 +- .../swap/components/swap-card/SwapCard.tsx | 6 + .../SwapCardContent.styles.ts | 26 +++- .../swap-card-content/SwapCardContent.tsx | 62 ++++++--- .../swap-info-chart/SwapInfoChart.styles.ts | 14 ++ .../swap-info-chart/SwapInfoChart.tsx | 29 ++++ .../swap-token-info/SwapTokenChart.styles.ts | 42 ++++++ .../swap-token-info/SwapTokenChart.tsx | 89 ++++++++++++ .../swap-token-info/SwapTokenHeader.styles.ts | 113 ++++++++++++++++ .../swap-token-info/SwapTokenHeader.tsx | 128 ++++++++++++++++++ .../swap-token-info/SwapTokenInfo.styles.ts | 9 ++ .../swap-token-info/SwapTokenInfo.tsx | 84 ++++++++++++ .../SwapInfoTransactionList.styles.ts | 14 ++ .../SwapInfoTransactionList.tsx | 20 +++ .../SwapInfoTransactionListTable.styles.ts | 122 +++++++++++++++++ .../SwapInfoTransactionListTable.tsx | 111 +++++++++++++++ .../swap-container/SwapContainer.tsx | 4 + .../SwapInfoChartContainer.tsx | 9 ++ .../SwapInfoTransactionListContainer.tsx | 22 +++ .../token-chart-graph/TokenChartGraph.tsx | 3 + .../components/token-swap/TokenSwap.styles.ts | 24 +++- .../components/token-swap/TokenSwap.tsx | 44 +++--- .../asset-send-modal/AssetSendModal.styles.ts | 25 +++- .../asset-send-modal/AssetSendModal.tsx | 31 +++-- .../src/react-query/router/use-get-routes.ts | 2 +- 50 files changed, 1437 insertions(+), 296 deletions(-) create mode 100644 packages/web/src/components/common/icons/IconWallet.tsx create mode 100644 packages/web/src/hooks/common/use-element-width-list.tsx create mode 100644 packages/web/src/hooks/common/use-element-width.tsx create mode 100644 packages/web/src/layouts/swap/components/swap-info-chart/SwapInfoChart.styles.ts create mode 100644 packages/web/src/layouts/swap/components/swap-info-chart/SwapInfoChart.tsx create mode 100644 packages/web/src/layouts/swap/components/swap-info-chart/swap-token-info/SwapTokenChart.styles.ts create mode 100644 packages/web/src/layouts/swap/components/swap-info-chart/swap-token-info/SwapTokenChart.tsx create mode 100644 packages/web/src/layouts/swap/components/swap-info-chart/swap-token-info/SwapTokenHeader.styles.ts create mode 100644 packages/web/src/layouts/swap/components/swap-info-chart/swap-token-info/SwapTokenHeader.tsx create mode 100644 packages/web/src/layouts/swap/components/swap-info-chart/swap-token-info/SwapTokenInfo.styles.ts create mode 100644 packages/web/src/layouts/swap/components/swap-info-chart/swap-token-info/SwapTokenInfo.tsx create mode 100644 packages/web/src/layouts/swap/components/swap-info-transaction-list/SwapInfoTransactionList.styles.ts create mode 100644 packages/web/src/layouts/swap/components/swap-info-transaction-list/SwapInfoTransactionList.tsx create mode 100644 packages/web/src/layouts/swap/components/swap-info-transaction-list/swap-info-transaction-list-table/SwapInfoTransactionListTable.styles.ts create mode 100644 packages/web/src/layouts/swap/components/swap-info-transaction-list/swap-info-transaction-list-table/SwapInfoTransactionListTable.tsx create mode 100644 packages/web/src/layouts/swap/containers/swap-info-chart-container/SwapInfoChartContainer.tsx create mode 100644 packages/web/src/layouts/swap/containers/swap-info-transaction-list-container/SwapInfoTransactionListContainer.tsx diff --git a/packages/web/src/components/common/divider/divider.tsx b/packages/web/src/components/common/divider/divider.tsx index 4c9d9f2a1..a4f65405d 100644 --- a/packages/web/src/components/common/divider/divider.tsx +++ b/packages/web/src/components/common/divider/divider.tsx @@ -3,4 +3,5 @@ import styled from "@emotion/styled"; export const Divider = styled.div` width: 100%; border-top: 1px solid ${({ theme }) => theme.color.border02}; + margin: 6px 0; `; diff --git a/packages/web/src/components/common/header/search-menu-modal/SearchMenuModal.styles.ts b/packages/web/src/components/common/header/search-menu-modal/SearchMenuModal.styles.ts index aff27e734..6395be378 100644 --- a/packages/web/src/components/common/header/search-menu-modal/SearchMenuModal.styles.ts +++ b/packages/web/src/components/common/header/search-menu-modal/SearchMenuModal.styles.ts @@ -149,6 +149,7 @@ export const ModalContainer = styled.div` overflow: hidden; text-overflow: ellipsis; white-space: nowrap; + direction: rtl; } padding: 1.5px 4px; ${mixins.flexbox("row", "center", "flex-start")}; @@ -267,28 +268,39 @@ export const Overlay = styled.div` `; interface Props { + containerWidth: number; maxWidth: number; tokenNameWidthList: number; } export const TokenInfoWrapper = styled.div` overflow-x: hidden; - max-width: ${({ maxWidth }) => { - return `calc(460px - 150px - ${maxWidth}px)`; + max-width: ${({ containerWidth, maxWidth }) => { + return `calc(${containerWidth}px - 150px - ${maxWidth}px)`; }}; + width: 100%; .token-path { - max-width: ${({ tokenNameWidthList, maxWidth }) => { - return `calc(460px - 158px - ${maxWidth}px - ${tokenNameWidthList}px)`; + max-width: ${({ containerWidth, tokenNameWidthList, maxWidth }) => { + return `calc(${containerWidth}px - 158px - ${maxWidth}px - ${tokenNameWidthList}px)`; }}; } ${media.mobile} { - max-width: ${({ maxWidth }) => { - return `calc(328px - 96px - ${maxWidth}px)`; + max-width: ${({ containerWidth, maxWidth }) => { + return `calc(${containerWidth}px - 96px - ${maxWidth}px)`; }}; .token-path { - max-width: ${({ tokenNameWidthList, maxWidth }) => { - return `calc(328px - 104px - ${maxWidth}px - ${tokenNameWidthList}px)`; + max-width: ${({ containerWidth, tokenNameWidthList, maxWidth }) => { + return `calc(${containerWidth}px - 104px - ${maxWidth}px - ${tokenNameWidthList}px)`; }}; } } + + .token-path { + .path { + overflow: hidden; + text-overflow: ellipsis; + direction: rtl; + white-space: nowrap; + } + } `; diff --git a/packages/web/src/components/common/header/search-menu-modal/SearchMenuModal.tsx b/packages/web/src/components/common/header/search-menu-modal/SearchMenuModal.tsx index 274c974a6..ee7e269e0 100644 --- a/packages/web/src/components/common/header/search-menu-modal/SearchMenuModal.tsx +++ b/packages/web/src/components/common/header/search-menu-modal/SearchMenuModal.tsx @@ -1,5 +1,5 @@ import { useAtom } from "jotai"; -import React, { useCallback, useEffect, useMemo, useRef, useState } from "react"; +import React, { useCallback, useMemo, useRef } from "react"; import { useTranslation } from "react-i18next"; import Badge, { BADGE_TYPE } from "@components/common/badge/Badge"; @@ -25,6 +25,8 @@ import { SearchWrapper, TokenInfoWrapper, } from "./SearchMenuModal.styles"; +import useElementWidth from "@hooks/common/use-element-width"; +import useElementWidthList from "@hooks/common/use-element-width-list"; interface NegativeStatusType { status: MATH_NEGATIVE_TYPE; @@ -77,23 +79,51 @@ const SearchMenuModal: React.FC = ({ const { t } = useTranslation(); const { getGnoscanUrl, getTokenUrl } = useGnoscanUrl(); - const [, setRecentsData] = useAtom(TokenState.recents); - const [widthListPopular, setWidthListPopular] = useState(popularTokens.map(() => 0)); - const [widthListRecent, setWidthListRecent] = useState(recents.map(() => 0)); - const [tokenNameRecentWidthList, setTokenNameRecentWidthList] = useState(recents.map(() => 0)); - const [tokenNamePopularWidthList, setTokenNamePopularWidthList] = useState(popularTokens.map(() => 0)); + const menuRef = useRef(null); + + const popularTokenKey = useMemo(() => popularTokens.map(token => token.path).join(","), [popularTokens]); + + const recentKey = useMemo(() => recents.map(token => token.path).join(","), [recents]); + + const containerRef = useRef(null); const tokenNamePopularRef = useRef(popularTokens.map(() => React.createRef())); const tokenNameRecentsRef = useRef(recents.map(() => React.createRef())); const recentPriceRef = useRef(recents.map(() => React.createRef())); const popularPriceRef = useRef(popularTokens.map(() => React.createRef())); - const menuRef = useRef(null); + const widthListPopular = useElementWidthList(popularTokens, popularPriceRef.current, [popularPriceRef]); + const widthListRecent = useElementWidthList(recents, recentPriceRef.current, [ + recentPriceRef, + popularTokenKey, + keyword, + ]); + const tokenNameRecentWidthList = useElementWidthList(recents, tokenNameRecentsRef.current, [ + tokenNameRecentsRef, + recentKey, + keyword, + ]); + const tokenNamePopularWidthList = useElementWidthList(popularTokens, tokenNamePopularRef.current, [ + tokenNamePopularRef, + keyword, + popularTokenKey, + ]); - const popularTokenKey = useMemo(() => popularTokens.map(token => token.path).join(","), [popularTokens]); + const containerWidth = useElementWidth(containerRef, [ + tokens, + recents, + popularPriceRef, + recentPriceRef, + popularTokenKey, + keyword, + popularTokens, + tokenNameRecentsRef, + recentKey, + tokenNamePopularRef, + popularTokenKey, + ]); - const recentKey = useMemo(() => recents.map(token => token.path).join(","), [recents]); const onClickItem = (item: Token) => { const current = recents.length > 0 ? [item, recents[0]] : [item]; @@ -136,71 +166,15 @@ const SearchMenuModal: React.FC = ({ [getGnoscanUrl, getTokenUrl], ); - useEffect(() => { - const widthValues: number[] = []; - popularPriceRef.current.forEach(ref => { - if (ref.current) { - const width = ref.current.getBoundingClientRect().width; - widthValues.push(width); - } - }); - setWidthListPopular(widthValues); - }, [popularPriceRef]); - - useEffect(() => { - const widthValues: number[] = []; - recentPriceRef.current.forEach(ref => { - if (ref.current) { - const width = ref.current.getBoundingClientRect().width; - widthValues.push(width); - } - }); - setWidthListRecent(widthValues); - }, [recentPriceRef, popularTokenKey, keyword]); - - useEffect(() => { - const widthValues: number[] = []; - tokenNameRecentsRef.current.forEach(ref => { - if (ref.current) { - const width = ref.current.getBoundingClientRect().width; - widthValues.push(width); - } - }); - setTokenNameRecentWidthList(widthValues); - }, [tokenNameRecentsRef, recentKey, keyword]); - - useEffect(() => { - const temp: number[] = []; - tokenNamePopularRef.current.forEach(ref => { - if (ref.current) { - const width = ref.current.getBoundingClientRect().width; - temp.push(width); - } - }); - setTokenNamePopularWidthList(temp); - }, [tokenNamePopularRef, keyword, popularTokenKey]); - const length = useMemo(() => { return breakpoint === DEVICE_TYPE.MOBILE ? 15 : 25; }, [breakpoint]); const getTokenPathDisplay = useCallback( (path: string, isNative?: boolean) => { - const path_ = path; - if (isNative) return STATIC_TEXT.NATIVE_COIN; - const tokenPathArr = path_?.split("/") ?? []; - - if (tokenPathArr?.length <= 0) return path_; - - const replacedPath = path_.replace("gno.land", ""); - - if (replacedPath.length >= length) { - return "..." + replacedPath.slice(replacedPath.length - length, replacedPath.length - 1); - } - - return path_.replace("gno.land", "..."); + return path; }, [length], ); @@ -221,7 +195,7 @@ const SearchMenuModal: React.FC = ({ - +
    {popularTokens.length === 0 && mostLiquidity.length === 0 && isFetched && (
    {t("common:noDataFound")}
    @@ -244,6 +218,7 @@ const SearchMenuModal: React.FC = ({ /> @@ -259,7 +234,7 @@ const SearchMenuModal: React.FC = ({ onClickPath(e, item.token.path) } > -
    {getTokenPathDisplay(item.token.path, item.isNative)}
    +
    {getTokenPathDisplay(item.token.path, item.isNative)}
    @@ -322,6 +297,7 @@ const SearchMenuModal: React.FC = ({ /> diff --git a/packages/web/src/components/common/icons/IconWallet.tsx b/packages/web/src/components/common/icons/IconWallet.tsx new file mode 100644 index 000000000..bbaeca099 --- /dev/null +++ b/packages/web/src/components/common/icons/IconWallet.tsx @@ -0,0 +1,32 @@ +import { useTheme } from "@emotion/react"; + +const IconWallet = () => { + const theme = useTheme(); + return ( + + + + + + ); +}; + +export default IconWallet; diff --git a/packages/web/src/components/common/line-graph/LineGraph.styles.ts b/packages/web/src/components/common/line-graph/LineGraph.styles.ts index f5412f005..9f4c68897 100644 --- a/packages/web/src/components/common/line-graph/LineGraph.styles.ts +++ b/packages/web/src/components/common/line-graph/LineGraph.styles.ts @@ -2,27 +2,27 @@ import { fonts } from "@constants/font.constant"; import styled from "@emotion/styled"; import { media } from "@styles/media"; -export const LineGraphWrapper = styled.div` +export const LineGraphWrapper = styled.div<{ forcedHeight?: number }>` position: relative; display: flex; flex-direction: column; width: 100%; - height: 321px; + height: ${({ forcedHeight }) => (forcedHeight ? `${forcedHeight}px` : "321px")}; overflow: visible; ${media.mobile} { - height: 252px; + height: ${({ forcedHeight }) => (forcedHeight ? `${forcedHeight}px` : "252px")}; } & svg { display: flex; flex-direction: column; width: 100%; - height: 321px; + height: ${({ forcedHeight }) => (forcedHeight ? `${forcedHeight}px` : "321px")}; overflow: visible; .center-line { transform: translateY(50%) !important; } ${media.mobile} { - height: 252px; + height: ${({ forcedHeight }) => (forcedHeight ? `${forcedHeight}px` : "252px")}; } .y-axis-number { ${fonts.p6} diff --git a/packages/web/src/components/common/line-graph/LineGraph.tsx b/packages/web/src/components/common/line-graph/LineGraph.tsx index 6f65b0a6a..2af38c5b7 100644 --- a/packages/web/src/components/common/line-graph/LineGraph.tsx +++ b/packages/web/src/components/common/line-graph/LineGraph.tsx @@ -82,6 +82,7 @@ export interface LineGraphProps { smooth?: boolean; width?: number; height?: number; + forcedHeight?: number; point?: boolean; firstPointColor?: string; typeOfChart?: string; @@ -89,9 +90,10 @@ export interface LineGraphProps { centerLineColor?: string; showBaseLine?: boolean; showBaseLineLabels?: boolean; + showPriceRangeLine?: boolean; renderBottom?: (baseLineNumberWidth: number) => React.ReactElement; isShowTooltip?: boolean; - onMouseMove?: (LineGraphData?: LineGraphData) => void; + onMouseMove?: (LineGraphData?: LineGraphData, dateDisplay?: { date: string; time: string; value?: string }) => void; onMouseOut?: (active: boolean) => void; baseLineMap?: [boolean, boolean, boolean, boolean]; baseLineLabelsPosition?: "left" | "right"; @@ -100,6 +102,7 @@ export interface LineGraphProps { baseLineLabelsStyle?: React.CSSProperties; displayLastDayAsNow?: boolean; popupYValueFormatter?: (value: string) => string; + hasNoLabel?: boolean; } export interface LineGraphRef { @@ -148,6 +151,7 @@ const LineGraph: React.FC = ({ smooth, width = VIEWPORT_DEFAULT_WIDTH, height = VIEWPORT_DEFAULT_HEIGHT, + forcedHeight, point, customData = { height: 0, locationTooltip: 0 }, showBaseLine = false, @@ -156,6 +160,7 @@ const LineGraph: React.FC = ({ onMouseMove: onLineGraphMouseMove, onMouseOut: onLineGraphMouseOut, showBaseLineLabels = false, + showPriceRangeLine = true, baseLineMap = [true, true, true, true], baseLineLabelsPosition = "left", baseLineLabelsTransform, @@ -163,6 +168,7 @@ const LineGraph: React.FC = ({ firstPointColor, displayLastDayAsNow = false, popupYValueFormatter, + hasNoLabel = false, }: LineGraphProps) => { const COMPONENT_ID = (Math.random() * 100000).toString(); const [activated, setActivated] = useState(false); @@ -509,6 +515,7 @@ const LineGraph: React.FC = ({ } return points[0]; }, [points]); + const locationTooltipPosition = useMemo(() => { if ((chartPoint?.y || 0) > customHeight + height - 25) { if (width < (currentPoint?.x || 0) + locationTooltip) { @@ -527,33 +534,42 @@ const LineGraph: React.FC = ({ const onTouchStart = (event: React.MouseEvent | React.TouchEvent) => { event.preventDefault(); + setActivated(true); onMouseMove(event); }; const areaPath = useMemo(() => { if (!points || points.length === 0 || points.some(point => point === undefined)) { - return undefined; // Or render some fallback UI + return undefined; } - // Start at the first point of the line chart - let path = `M ${points[0].x},${points[0].y}`; - - // Draw the line chart path - for (let i = 1; i < points.length; i++) { - path += smooth ? bezierCommand(points[i], i, points) : ` L ${points[i].x},${points[i].y}`; + // Start at the first point's x coordinate at firstPoint.y level + let path = `M ${points[0].x},${firstPoint.y}`; + + // Draw the main line chart path + for (let i = 0; i < points.length; i++) { + const point = points[i]; + // Plots the actual curve only when the current point is above firstPoint.y + if (point.y <= firstPoint.y) { + let pathSegment: string; + if (i === 0) { + pathSegment = ` L ${point.x},${point.y}`; + } else { + pathSegment = smooth ? bezierCommand(point, i, points) : ` L ${point.x},${point.y}`; + } + path += pathSegment; + } else { + // Move at the level of firstPoint.y if it is below firstPoint.y + path += ` L ${point.x},${firstPoint.y}`; + } } - // Draw a line straight down to the bottom of the chart - path += ` L ${points[points.length - 1].x},${height}`; - - // Draw a line straight across to the bottom left corner - path += ` L ${points[0].x},${height}`; - - // Close the path by connecting back to the start point - path += "Z"; + // Close the path by drawing back to firstPoint.y level + path += ` L ${points[points.length - 1].x},${firstPoint.y}`; + path += " Z"; return path; - }, [height, points, smooth]); + }, [points, smooth, firstPoint.y]); const isLightTheme = theme.themeKey === "light"; @@ -571,6 +587,27 @@ const LineGraph: React.FC = ({ return parseTimeTVL(datas[currentPointIndex]?.time); }, [currentPointIndex, datas, displayLastDayAsNow]); + useEffect(() => { + if (currentPointIndex < 0) { + onLineGraphMouseMove?.(undefined, undefined); + return; + } + + const currentDate = + displayLastDayAsNow && datas.length - 1 === currentPointIndex + ? parseTimeTVL(getLocalizeTime(new Date().toString())) + : parseTimeTVL(datas[currentPointIndex]?.time); + + const formattedValue = popupYValueFormatter + ? popupYValueFormatter(datas[currentPointIndex]?.value) + : formatPrice(datas[currentPointIndex]?.value); + + onLineGraphMouseMove?.(datas[currentPointIndex], { + ...currentDate, + value: formattedValue, + }); + }, [currentPointIndex, datas, displayLastDayAsNow, popupYValueFormatter]); + return ( = ({ }} onTouchMove={onTouchMove} onTouchStart={onTouchStart} + forcedHeight={forcedHeight} > = ({ className="first-line" /> )} - {isFocus() && currentPoint && ( + {isFocus() && currentPoint && showPriceRangeLine && ( = ({ /> )} {isFocus() && currentPoint && ( - + )} } diff --git a/packages/web/src/components/common/select-token/SelectToken.tsx b/packages/web/src/components/common/select-token/SelectToken.tsx index 2f49980aa..70907e3da 100644 --- a/packages/web/src/components/common/select-token/SelectToken.tsx +++ b/packages/web/src/components/common/select-token/SelectToken.tsx @@ -24,7 +24,6 @@ export interface SelectTokenProps { changeToken: (token: TokenModel) => void; close: () => void; themeKey: "dark" | "light"; - modalRef?: React.RefObject; breakpoint: DEVICE_TYPE; recents: TokenModel[]; isSwitchNetwork: boolean; @@ -39,7 +38,6 @@ const SelectToken: React.FC = ({ changeToken, close, themeKey, - modalRef, breakpoint, recents = [], isSwitchNetwork, @@ -51,7 +49,6 @@ const SelectToken: React.FC = ({ const tokenNameRef = useRef(tokens.map(() => React.createRef())); const [widthList, setWidthList] = useState(tokens.map(() => 0)); const [tokenNameWidthList, setTokenNameWidthList] = useState(tokens.map(() => 0)); - const [positionTop, setPositionTop] = useState(0); const [, setRecentsData] = useAtom(TokenState.selectRecents); const { getGnoscanUrl, getTokenUrl } = useGnoscanUrl(); @@ -98,22 +95,6 @@ const SelectToken: React.FC = ({ [changeKeyword], ); - useEffect(() => { - const getPositionTop = () => { - const element = myElementRef.current; - if (element) { - const rect = element.getBoundingClientRect(); - const temp = Math.max(positionTop, rect.top); - if (modalRef && modalRef.current) { - modalRef.current.style.top = `${temp}px`; - modalRef.current.style.transform = "translate(-50%, 0)"; - } - setPositionTop(temp); - } - }; - getPositionTop(); - }, [positionTop]); - useEffect(() => { const temp: number[] = []; priceRefs.current.forEach(ref => { @@ -123,7 +104,7 @@ const SelectToken: React.FC = ({ } }); setWidthList(temp); - }, [priceRefs]); + }, [priceRefs, tokens]); useEffect(() => { const temp: number[] = []; diff --git a/packages/web/src/components/common/token-amount-input/TokenAmountInput.styles.ts b/packages/web/src/components/common/token-amount-input/TokenAmountInput.styles.ts index e13954b11..4b2f53a6c 100644 --- a/packages/web/src/components/common/token-amount-input/TokenAmountInput.styles.ts +++ b/packages/web/src/components/common/token-amount-input/TokenAmountInput.styles.ts @@ -33,6 +33,29 @@ export const TokenAmountInputWrapper = styled.div` .info { ${mixins.flexbox("row", "center", "space-between")}; width: 100%; + .balance-wrapper { + display: flex; + align-items: center; + justify-content: center; + gap: 4px; + .balance-max-button { + box-sizing: content-box; + display: flex; + justify-content: center; + align-items: center; + padding: 1px 6px; + height: 14px; + border-radius: 36px; + background: rgba(0, 89, 255, 0.2); + font-size: 11px; + font-weight: 500; + color: #007aff; + cursor: pointer; + &:hover { + background: ${({ theme }) => (theme.themeKey === "dark" ? "rgba(0, 89, 255, 0.1)" : "rgba(0, 89, 255, 0.3)")}; + } + } + } } .amount-text { @@ -52,7 +75,6 @@ export const TokenAmountInputWrapper = styled.div` .balance-text { ${fonts.p2}; color: ${({ theme }) => theme.color.text04}; - cursor: pointer; } .price-text { flex-shrink: 0; diff --git a/packages/web/src/components/common/token-amount-input/TokenAmountInput.tsx b/packages/web/src/components/common/token-amount-input/TokenAmountInput.tsx index b7b8f8a2a..e995fcda4 100644 --- a/packages/web/src/components/common/token-amount-input/TokenAmountInput.tsx +++ b/packages/web/src/components/common/token-amount-input/TokenAmountInput.tsx @@ -9,12 +9,14 @@ import { DEFAULT_CONTRACT_USE_FEE, DEFAULT_GAS_FEE } from "@common/values"; import { makeDisplayTokenAmount } from "@utils/token-utils"; import { formatOtherPrice } from "@utils/new-number-utils"; import { useTranslation } from "react-i18next"; +import IconWallet from "../icons/IconWallet"; export interface TokenAmountInputProps extends TokenAmountInputModel { changable?: boolean; changeToken: (token: TokenModel) => void; connected: boolean; style?: React.CSSProperties; + isVisibleMaxButton?: boolean; } const TokenAmountInput: React.FC = ({ @@ -27,6 +29,7 @@ const TokenAmountInput: React.FC = ({ connected, amount, style, + isVisibleMaxButton = true, }) => { const { t } = useTranslation(); @@ -63,6 +66,12 @@ const TokenAmountInput: React.FC = ({ } }, [connected, balance, token, changeAmount]); + const hasTokenBalance = useMemo(() => { + if (!connected || balance === "0") return false; + + return true; + }, [connected, balance]); + const balanceADisplay = useMemo(() => { if (!connected || balance === "0") { return "-"; @@ -101,9 +110,15 @@ const TokenAmountInput: React.FC = ({
    {usdValue} - - {t("business:balance")}: {balanceADisplay} - +
    + {connected && } + {balanceADisplay} + {isVisibleMaxButton && hasTokenBalance && ( + + )} +
    ); diff --git a/packages/web/src/components/common/token-info-cell/TokenInfoCell.styles.ts b/packages/web/src/components/common/token-info-cell/TokenInfoCell.styles.ts index e173d61fe..4a5a800ef 100644 --- a/packages/web/src/components/common/token-info-cell/TokenInfoCell.styles.ts +++ b/packages/web/src/components/common/token-info-cell/TokenInfoCell.styles.ts @@ -2,7 +2,12 @@ import { fonts } from "@constants/font.constant"; import styled from "@emotion/styled"; import mixins from "@styles/mixins"; -export const TokenInfoCellWrapper = styled.div` +interface Props { + containerWidth: number; + tokenNameWidth: number; +} + +export const TokenInfoCellWrapper = styled.div` ${mixins.flexbox("row", "flex-start", "flex-start")}; color: ${({ theme }) => theme.color.text01}; gap: 2px; @@ -34,6 +39,13 @@ export const TokenInfoCellWrapper = styled.div` } .token-link { + max-width: ${({ containerWidth, tokenNameWidth }) => `${containerWidth - tokenNameWidth - 44}px`}; + span { + overflow: hidden; + text-overflow: ellipsis; + direction: rtl; + white-space: nowrap; + } &:hover { color: ${({ theme }) => theme.color.text03}; .path-link-icon { @@ -52,6 +64,7 @@ export const TokenInfoCellWrapper = styled.div` white-space: nowrap; .path-link-icon { + flex-shrink: 0; width: 10px; height: 10px; fill: ${({ theme }) => theme.color.text04}; diff --git a/packages/web/src/components/common/token-info-cell/TokenInfoCell.tsx b/packages/web/src/components/common/token-info-cell/TokenInfoCell.tsx index 869e1e6a5..4ae0c4e96 100644 --- a/packages/web/src/components/common/token-info-cell/TokenInfoCell.tsx +++ b/packages/web/src/components/common/token-info-cell/TokenInfoCell.tsx @@ -1,5 +1,5 @@ import { useTheme } from "@emotion/react"; -import { useCallback, useEffect, useMemo, useState } from "react"; +import { useCallback, useMemo, useRef } from "react"; import { useTranslation } from "react-i18next"; import IconOpenLink from "@components/common/icons/IconOpenLink"; @@ -9,6 +9,7 @@ import { DEVICE_TYPE } from "@styles/media"; import { STATIC_TEXT } from "@common/values"; import { TokenInfoCellWrapper } from "./TokenInfoCell.styles"; +import useElementWidth from "@hooks/common/use-element-width"; export interface TokenInfoCellProps { token: { @@ -21,58 +22,24 @@ export interface TokenInfoCellProps { breakpoint?: DEVICE_TYPE; } -const DETERMIN_SHORT_SIZE_WEB = 160 as const; -const DETERMIN_SHORT_SIZE_TABLET = 60 as const; -const DETERMIN_SHORT_SIZE_MOBILE = 70 as const; - function TokenInfoCell({ token, breakpoint, isNative }: TokenInfoCellProps) { const { name, path, symbol, logoURI } = token; const { t } = useTranslation(); const theme = useTheme(); const { getGnoscanUrl, getTokenUrl } = useGnoscanUrl(); - const [shortenPath, setShortenPath] = useState(false); const elementId = useMemo(() => `${token.path}`, [token.path]); - useEffect(() => { - const element = document.getElementById(elementId); - - if ((element?.clientWidth || 0) > DETERMIN_SHORT_SIZE_MOBILE && breakpoint === DEVICE_TYPE.MOBILE) { - setShortenPath(true); - return; - } - - if ( - (element?.clientWidth || 0) > DETERMIN_SHORT_SIZE_TABLET && - (breakpoint === DEVICE_TYPE.TABLET || breakpoint === DEVICE_TYPE.TABLET_M || breakpoint === DEVICE_TYPE.TABLET_S) - ) { - setShortenPath(true); - return; - } + const containerRef = useRef(null); + const containerWidth = useElementWidth(containerRef, [token]); - // breakpoint === DEVICE_TYPE.WEB || breakpoint === DEVICE_TYPE.MEDIUM_WEB - if ((element?.clientWidth || 0) > DETERMIN_SHORT_SIZE_WEB) { - setShortenPath(true); - return; - } - - setShortenPath(false); - }, [elementId, breakpoint]); - - const length = useMemo(() => { - return breakpoint === DEVICE_TYPE.MOBILE ? 10 : 15; - }, [breakpoint]); + const tokenNameRef = useRef(null); + const tokenNameWidth = useElementWidth(tokenNameRef, [token]); const tokenPathDisplay = useMemo(() => { - if (shortenPath) return ""; if (isNative) return STATIC_TEXT.NATIVE_COIN; - let replacedPath = path.replace("gno.land", ""); - - if (replacedPath.length > length) { - replacedPath = replacedPath.slice(replacedPath.length - length); - } - return "...".concat(replacedPath); - }, [isNative, length, path, shortenPath, t]); + return path; + }, [isNative, path, t]); const onClickPath = useCallback( (e: React.MouseEvent) => { @@ -87,15 +54,15 @@ function TokenInfoCell({ token, breakpoint, isNative }: TokenInfoCellProps) { ); return ( - +
    - + {name}
    - {tokenPathDisplay} + {tokenPathDisplay}
    diff --git a/packages/web/src/constants/graph.constant.ts b/packages/web/src/constants/graph.constant.ts index 282833886..6d651771c 100644 --- a/packages/web/src/constants/graph.constant.ts +++ b/packages/web/src/constants/graph.constant.ts @@ -1 +1,8 @@ export const ZOOL_VALUES = [40, 80, 160, 320, 640, 1280, 2560, 5120] as const; + +export const SWAP_TOKEN_CHART_COLORS = { + GRADIENT: { + START: "rgba(25, 46, 162, 0.5)", + END: "rgba(25, 46, 162, 0.02)", + }, +}; diff --git a/packages/web/src/constants/skeleton.constant.ts b/packages/web/src/constants/skeleton.constant.ts index 35ea7e5fd..b4e4aeb1d 100644 --- a/packages/web/src/constants/skeleton.constant.ts +++ b/packages/web/src/constants/skeleton.constant.ts @@ -252,6 +252,10 @@ export const TOKEN_SEARCH_WIDTH = [400]; export const TOKEN_TD_WIDTH = [56, 199, 105, 85, 85, 85, 140, 140, 138, 201, 124]; export const MOBILE_TOKEN_TD_WIDTH = [160, 160]; +export const TRANSACTION_TD_WIDTH = [80, 100, 202]; +export const MOBILE_TRANSACTION_TD_WIDTH = [80, 216]; +export const TABLET_TRANSACTION_TD_WIDTH = [80, 150, 238]; + export const LEADERBOARD_TD_WIDTH = [120, 400, 200, 200, 200, 240]; export const MOBILE_LEADERBOARD_TD_WIDTH = [50, 150, 128]; export const TABLET_LEADERBOARD_TD_WIDTH = [120, 300, 170, 170, 170, 170]; diff --git a/packages/web/src/containers/select-token-container/SelectTokenContainer.tsx b/packages/web/src/containers/select-token-container/SelectTokenContainer.tsx index 83e49967e..930e73221 100644 --- a/packages/web/src/containers/select-token-container/SelectTokenContainer.tsx +++ b/packages/web/src/containers/select-token-container/SelectTokenContainer.tsx @@ -17,7 +17,6 @@ import { parseJson } from "@utils/common"; interface SelectTokenContainerProps { changeToken?: (token: TokenModel) => void; callback?: (value: boolean) => void; - modalRef?: React.RefObject; } export interface SortedProps extends TokenModel { @@ -75,7 +74,7 @@ const handleSort = (list: SortedProps[]) => { return [...rs, ...valueOfBalance, ...amountOfBalance, ...alphabest]; }; -const SelectTokenContainer: React.FC = ({ changeToken, callback, modalRef }) => { +const SelectTokenContainer: React.FC = ({ changeToken, callback }) => { const { breakpoint } = useWindowSize(); const { tokens, balances, tokenPrices, displayBalanceMap } = useTokenData(); const [keyword, setKeyword] = useState(""); @@ -169,7 +168,6 @@ const SelectTokenContainer: React.FC = ({ changeToken changeToken={selectToken} close={close} themeKey={themeKey} - modalRef={modalRef} breakpoint={breakpoint} recents={recents} isSwitchNetwork={isSwitchNetwork} diff --git a/packages/web/src/hooks/common/use-element-width-list.tsx b/packages/web/src/hooks/common/use-element-width-list.tsx new file mode 100644 index 000000000..872653d2a --- /dev/null +++ b/packages/web/src/hooks/common/use-element-width-list.tsx @@ -0,0 +1,37 @@ +import React from "react"; + +/** + * + * Custom hooks to manage width values for each element in a given array of items. + * + * @template T - Generic parameters that define the item's type + * @param {T[]} items - Array of items to track width + * @param {React.RefObject[]} refs - A ref array of elements to measure + * @param {React.DependencyList} deps - Array of dependencies to trigger width measurements + * @returns {number[]} + * - Return an array of width values for each item + * - widthList: array containing the width value of each item (initialized to 0) + * + * @example + * const items = [target1Ref, target2Ref, target3Ref]; + * const widthList = useElementWidtahList(items, refs, []); + * widthList Initial values: [0, 0, 0] + * + */ +function useElementWidthList( + items: T[], + refs: React.RefObject[], + dependencies: React.DependencyList = [], +): number[] { + const [widthList, setWidthList] = React.useState(items.map(() => 0)); + + React.useEffect(() => { + const widthValues = refs.map(ref => (ref.current ? ref.current.getBoundingClientRect().width : 0)); + + setWidthList(widthValues); + }, dependencies); + + return widthList; +} + +export default useElementWidthList; diff --git a/packages/web/src/hooks/common/use-element-width.tsx b/packages/web/src/hooks/common/use-element-width.tsx new file mode 100644 index 000000000..89923fa95 --- /dev/null +++ b/packages/web/src/hooks/common/use-element-width.tsx @@ -0,0 +1,26 @@ +import React from "react"; + +function useElementWidth(ref: React.RefObject, dependencies: React.DependencyList = []) { + const [width, setWidth] = React.useState(0); + + React.useEffect(() => { + const updateWidth = () => { + if (ref.current) { + const width = ref.current.getBoundingClientRect().width; + setWidth(width); + } + }; + + updateWidth(); + + window.addEventListener("resize", updateWidth); + + return () => { + window.removeEventListener("resize", updateWidth); + }; + }, dependencies); + + return width; +} + +export default useElementWidth; diff --git a/packages/web/src/hooks/swap/data/use-swap-handler.tsx b/packages/web/src/hooks/swap/data/use-swap-handler.tsx index d4231894d..cfe476f7a 100644 --- a/packages/web/src/hooks/swap/data/use-swap-handler.tsx +++ b/packages/web/src/hooks/swap/data/use-swap-handler.tsx @@ -104,6 +104,13 @@ function compareAmountFn(amountA: string | number | bigint, amountB: string | nu function handleAmount(changed: string, token: TokenModel | null) { let value = changed; const decimals = token?.decimals || 0; + + // Check if input exceeds decimal places + if (changed.includes(".") && changed.split(".")[1].length > decimals) { + // Signal invalid input + return { isValid: false, value: changed }; + } + if (!value || BigNumber(value).isZero()) { value = changed; } else { @@ -117,7 +124,7 @@ function handleAmount(changed: string, token: TokenModel | null) { } } - return value; + return { isValid: true, value }; } export const useSwapHandler = () => { @@ -170,6 +177,8 @@ export const useSwapHandler = () => { updateSwapAmount, resetSwapAmount, isTyping, + isRefetching, + handleResetEstimatedLiquidity, } = useSwap({ tokenA, tokenB, @@ -592,18 +601,25 @@ export const useSwapHandler = () => { const changeTokenAAmount = useCallback( (changed: string, none?: boolean) => { - const value = handleAmount(changed, tokenA); + const result = handleAmount(changed, tokenA); + + // If invalid decimal places, don't update or trigger loading + if (!result.isValid) { + setIsLoading(false); + return; + } + if (tokenA && tokenB) { - updateSwapAmount(value); + updateSwapAmount(result.value); } if (isSameToken) { - setTokenAAmount(value); - setTokenBAmount(value); + setTokenAAmount(result.value); + setTokenBAmount(result.value); setSwapValue(prev => ({ ...prev, - tokenAAmount: value, - tokenBAmount: value, + tokenAAmount: result.value, + tokenBAmount: result.value, type: "EXACT_IN", })); return; @@ -613,10 +629,10 @@ export const useSwapHandler = () => { setIsLoading(false); return; } - if (!matchInputNumber(value)) { + if (!matchInputNumber(result.value)) { return; } - if (!!Number(value) && tokenB?.symbol) { + if (!!Number(result.value) && tokenB?.symbol) { setIsLoading(true); } else { setTokenBAmount("0"); @@ -626,7 +642,7 @@ export const useSwapHandler = () => { ...prev, type: "EXACT_IN", })); - setTokenAAmount(value); + setTokenAAmount(result.value); }, [isSameToken, setSwapValue, tokenA, tokenB?.symbol], ); @@ -641,15 +657,20 @@ export const useSwapHandler = () => { const changeTokenBAmount = useCallback( (changed: string, none?: boolean) => { - const value = handleAmount(changed, tokenB); + const result = handleAmount(changed, tokenB); + + if (!result.isValid) { + setIsLoading(false); + return; + } if (isSameToken) { - setTokenAAmount(value); - setTokenBAmount(value); + setTokenAAmount(result.value); + setTokenBAmount(result.value); setSwapValue(prev => ({ ...prev, - tokenAAmount: value, - tokenBAmount: value, + tokenAAmount: result.value, + tokenBAmount: result.value, type: "EXACT_IN", })); return; @@ -660,11 +681,11 @@ export const useSwapHandler = () => { return; } - if (!matchInputNumber(value)) { + if (!matchInputNumber(result.value)) { return; } - if (!!Number(value) && tokenA?.symbol) { + if (!!Number(result.value) && tokenA?.symbol) { setIsLoading(true); } else { setTokenAAmount("0"); @@ -674,8 +695,8 @@ export const useSwapHandler = () => { ...prev, type: "EXACT_OUT", })); - updateSwapAmount(value); - setTokenBAmount(value); + updateSwapAmount(result.value); + setTokenBAmount(result.value); }, [isSameToken, tokenA, tokenB], ); @@ -1110,6 +1131,7 @@ export const useSwapHandler = () => { isSwitchNetwork, switchNetwork, isLoading: swapState === "LOADING" || isTyping, + isRefetching, setSwapValue, tokenA, tokenB, @@ -1120,5 +1142,6 @@ export const useSwapHandler = () => { setTokenAAmount, priceImpactStatus, isSameToken, + handleResetEstimatedLiquidity, }; }; diff --git a/packages/web/src/hooks/swap/data/use-swap.tsx b/packages/web/src/hooks/swap/data/use-swap.tsx index ad4adee92..c40b4cc0e 100644 --- a/packages/web/src/hooks/swap/data/use-swap.tsx +++ b/packages/web/src/hooks/swap/data/use-swap.tsx @@ -81,6 +81,7 @@ export const useSwap = ({ tokenA, tokenB, direction, slippage, swapFee = 15 }: U const { data: estimatedSwapResult, isLoading: isEstimatedSwapLoading, + isRefetching, error, } = useGetRoutes( { @@ -165,35 +166,34 @@ export const useSwap = ({ tokenA, tokenB, direction, slippage, swapFee = 15 }: U return 0; }, [direction, estimatedAmount, slippage, tokenA]); - const updateSwapAmount = useCallback( - (amount: string) => { - if (!amount) { - setSwapAmount(null); - setIsTyping(false); - return; - } + const updateSwapAmount = (amount: string) => { + if (!amount) { + setSwapAmount(null); + setIsTyping(false); + return; + } - let newAmount = 0; - if (BigNumber(amount).isZero()) { - newAmount = 0; - } - newAmount = BigNumber(amount).toNumber(); + const processedAmount = amount.endsWith(".") ? amount.slice(0, -1) : amount; + const newAmount = BigNumber(processedAmount).isZero() ? 0 : BigNumber(processedAmount).toNumber(); - setSwapAmount(newAmount); - if (tokenA && tokenB) { + setSwapAmount(prevAmount => { + const hasValueChanged = prevAmount !== newAmount; + + if (hasValueChanged) { setIsTyping(true); - } - if (typingTimeoutRef.current) { - clearTimeout(typingTimeoutRef.current); + if (typingTimeoutRef.current) { + clearTimeout(typingTimeoutRef.current); + } + + typingTimeoutRef.current = setTimeout(() => { + setIsTyping(false); + }, SWAP_AMOUNT_DEBOUNCE_TIME_MS + 100); } - typingTimeoutRef.current = setTimeout(() => { - setIsTyping(false); - }, SWAP_AMOUNT_DEBOUNCE_TIME_MS + 100); - }, - [tokenA, tokenB], - ); + return hasValueChanged ? newAmount : prevAmount; + }); + }; useEffect(() => { if (debouncedSwapAmount !== null) { @@ -283,6 +283,17 @@ export const useSwap = ({ tokenA, tokenB, direction, slippage, swapFee = 15 }: U } }, [estimatedLiquidityMax]); + const handleResetEstimatedLiquidity = () => { + setEstimatedLiquidityMax(null); + }; + /** + * Reset estimatedLiquidityMax when tokens change + * This prevents stale liquidity max values from persisting across different token pairs + */ + useEffect(() => { + handleResetEstimatedLiquidity(); + }, [tokenA, tokenB]); + return { isSameToken, tokenAmountLimit, @@ -295,6 +306,8 @@ export const useSwap = ({ tokenA, tokenB, direction, slippage, swapFee = 15 }: U updateSwapAmount, isEstimatedSwapLoading, isTyping, + isRefetching, + handleResetEstimatedLiquidity, resetSwapAmount: () => { setSwapAmount(0); setIsTyping(false); diff --git a/packages/web/src/layouts/launchpad/launchpad-detail/LaunchpadDetail.tsx b/packages/web/src/layouts/launchpad/launchpad-detail/LaunchpadDetail.tsx index e9f7c20f1..4db82524e 100644 --- a/packages/web/src/layouts/launchpad/launchpad-detail/LaunchpadDetail.tsx +++ b/packages/web/src/layouts/launchpad/launchpad-detail/LaunchpadDetail.tsx @@ -23,6 +23,7 @@ import LaunchpadDetailClickHereContainer from "./containers/launchpad-detail-cli import Footer from "@components/common/footer/Footer"; import { useWindowSize } from "@hooks/common/use-window-size"; import { PAGE_PATH } from "@constants/page.constant"; +import { useAddress } from "@hooks/common/use-address"; export interface ProjectSummaryDataModel { totalAllocation: number; @@ -59,6 +60,7 @@ export interface ProjectRewardInfoModel { const LaunchpadDetail: React.FC = () => { const { t } = useTranslation(); + const { connected } = useAddress(); const [selectPoolId, setSelectPoolId] = useAtom(LaunchpadState.selectLaunchpadPool); const [, setDepositConditions] = useAtom(LaunchpadState.depositConditions); @@ -262,6 +264,7 @@ const LaunchpadDetail: React.FC = () => { aboutProject={} participate={ (theme.themeKey === "dark" ? "rgba(0, 89, 255, 0.1)" : "rgba(0, 89, 255, 0.3)")}; + } + } + } } .participate-info-wrapper { ${mixins.flexbox("column", "center", "center")}; diff --git a/packages/web/src/layouts/launchpad/launchpad-detail/components/launchpad-participate/LaunchpadParticipate.tsx b/packages/web/src/layouts/launchpad/launchpad-detail/components/launchpad-participate/LaunchpadParticipate.tsx index 7c8e1c77f..e6f049d8a 100644 --- a/packages/web/src/layouts/launchpad/launchpad-detail/components/launchpad-participate/LaunchpadParticipate.tsx +++ b/packages/web/src/layouts/launchpad/launchpad-detail/components/launchpad-participate/LaunchpadParticipate.tsx @@ -27,6 +27,7 @@ import DepositConditionsTooltip from "@components/common/launchpad-tooltip/depos import LaunchpadTooltip from "../common/launchpad-tooltip/LaunchpadTooltip"; import { pulseSkeletonStyle } from "@constants/skeleton.constant"; import LaunchpadDepositModal from "@components/common/launchpad-modal/launchpad-deposit-modal/LaunchpadDepositModal"; +import IconWallet from "@components/common/icons/IconWallet"; const DEFAULT_DEPOSIT_TOKEN = GNS_TOKEN; @@ -92,10 +93,15 @@ const LaunchpadParticipate: React.FC = ({ [setParticipateAmount, status], ); - const currentGnsBalance = React.useMemo( - () => displayBalanceMap?.[DEFAULT_DEPOSIT_TOKEN?.path ?? ""] ?? null, - [displayBalanceMap], - ); + const currentGnsBalance = React.useMemo(() => { + if (!isWalletConnected) return null; + return displayBalanceMap?.[DEFAULT_DEPOSIT_TOKEN?.path ?? ""] ?? null; + }, [displayBalanceMap, isWalletConnected]); + + const hasTokenBalance = React.useMemo(() => { + return !!currentGnsBalance; + }, [currentGnsBalance]); + const estimatePrice = React.useMemo( () => DEFAULT_DEPOSIT_TOKEN?.wrappedPath && @@ -207,14 +213,21 @@ const LaunchpadParticipate: React.FC = ({
    {estimatePrice} - - {t("Launchpad:participate.balance")}: {currentGnsBalance ? toNumberFormat(currentGnsBalance, 2) : "-"} - +
    + {isWalletConnected && } + + {currentGnsBalance ? toNumberFormat(currentGnsBalance, 2) : "-"} + + {hasTokenBalance && ( + + )} +
    diff --git a/packages/web/src/layouts/launchpad/launchpad-detail/containers/launchpad-participate-container/LaunchpadParticipateContainer.tsx b/packages/web/src/layouts/launchpad/launchpad-detail/containers/launchpad-participate-container/LaunchpadParticipateContainer.tsx index 3a935ddcf..9abb62ddb 100644 --- a/packages/web/src/layouts/launchpad/launchpad-detail/containers/launchpad-participate-container/LaunchpadParticipateContainer.tsx +++ b/packages/web/src/layouts/launchpad/launchpad-detail/containers/launchpad-participate-container/LaunchpadParticipateContainer.tsx @@ -2,13 +2,13 @@ import React from "react"; import useCustomRouter from "@hooks/common/use-custom-router"; import { useLaunchpadHandler } from "@hooks/launchpad/data/use-launchpad-handler"; -import { useWallet } from "@hooks/wallet/data/use-wallet"; import { LaunchpadPoolModel } from "@models/launchpad"; import { ProjectRewardInfoModel } from "../../LaunchpadDetail"; import LaunchpadParticipate from "../../components/launchpad-participate/LaunchpadParticipate"; interface LaunchpadParticipateContainerProps { + connected: boolean; poolInfo?: LaunchpadPoolModel; rewardInfo: ProjectRewardInfoModel; status: string; @@ -18,6 +18,7 @@ interface LaunchpadParticipateContainerProps { } const LaunchpadParticipateContainer: React.FC = ({ + connected, poolInfo, rewardInfo, status, @@ -27,7 +28,6 @@ const LaunchpadParticipateContainer: React.FC = ({ connected={connected} changeToken={changeTokenA} changeAmount={changeTokenAAmount} + isVisibleMaxButton={true} /> = ({ connected={connected} changeToken={changeTokenB} changeAmount={changeTokenBAmount} + isVisibleMaxButton={true} />
    diff --git a/packages/web/src/layouts/swap/Swap.tsx b/packages/web/src/layouts/swap/Swap.tsx index 0804e29a0..1302ad523 100644 --- a/packages/web/src/layouts/swap/Swap.tsx +++ b/packages/web/src/layouts/swap/Swap.tsx @@ -1,19 +1,19 @@ import React from "react"; -import Footer from "@components/common/footer/Footer"; import HeaderContainer from "@containers/header-container/HeaderContainer"; - -import SwapContainer from "./containers/swap-container/SwapContainer"; -import SwapLiquidityContainer from "./containers/swap-liquidity-container/SwapLiquidityContainer"; - import SwapLayout from "./SwapLayout"; +import SwapContainer from "./containers/swap-container/SwapContainer"; +import SwapInfoChartContainer from "./containers/swap-info-chart-container/SwapInfoChartContainer"; +import SwapInfoTransactionListContainer from "./containers/swap-info-transaction-list-container/SwapInfoTransactionListContainer"; +import Footer from "@components/common/footer/Footer"; const Swap: React.FC = () => { return ( } swap={} - liquidity={} + chart={} + info={} footer={
    } /> ); diff --git a/packages/web/src/layouts/swap/SwapLayout.styles.ts b/packages/web/src/layouts/swap/SwapLayout.styles.ts index 98178b692..19517b46c 100644 --- a/packages/web/src/layouts/swap/SwapLayout.styles.ts +++ b/packages/web/src/layouts/swap/SwapLayout.styles.ts @@ -85,9 +85,12 @@ export const SwapLayoutWrapper = styled.div` } } - .liquidity { + .data { max-width: 414px; width: 100%; + display: flex; + flex-direction: column; + gap: 16px; ${media.tablet} { max-width: 500px; } diff --git a/packages/web/src/layouts/swap/SwapLayout.tsx b/packages/web/src/layouts/swap/SwapLayout.tsx index fdd72fee0..6aa07ca92 100644 --- a/packages/web/src/layouts/swap/SwapLayout.tsx +++ b/packages/web/src/layouts/swap/SwapLayout.tsx @@ -1,19 +1,17 @@ import React from "react"; import { SwapLayoutWrapper } from "./SwapLayout.styles"; -import { useAtom } from "jotai"; -import { SwapState } from "@states/index"; import { useTranslation } from "react-i18next"; interface SwapLayoutProps { header: React.ReactNode; swap: React.ReactNode; - liquidity: React.ReactNode; + chart: React.ReactNode; + info: React.ReactNode; footer: React.ReactNode; } -const SwapLayout: React.FC = ({ header, swap, liquidity, footer }) => { +const SwapLayout: React.FC = ({ header, swap, chart, info, footer }) => { const { t } = useTranslation(); - const [swapValue] = useAtom(SwapState.swap); return ( @@ -23,7 +21,10 @@ const SwapLayout: React.FC = ({ header, swap, liquidity, footer
    {t("Swap:header")}
    {swap}
    -
    {swapValue.tokenA && swapValue.tokenB && liquidity}
    +
    + {chart} + {info} +
    diff --git a/packages/web/src/layouts/swap/components/swap-card/SwapCard.tsx b/packages/web/src/layouts/swap/components/swap-card/SwapCard.tsx index b9ab4a9fe..5a2339b0e 100644 --- a/packages/web/src/layouts/swap/components/swap-card/SwapCard.tsx +++ b/packages/web/src/layouts/swap/components/swap-card/SwapCard.tsx @@ -32,12 +32,14 @@ interface SwapCardProps { isSwitchNetwork: boolean; isLoading: boolean; isSameToken: boolean; + isRefetching: boolean; changeTokenA: (token: TokenModel) => void; changeTokenAAmount: (value: string, none?: boolean) => void; changeTokenB: (token: TokenModel) => void; changeTokenBAmount: (value: string, none?: boolean) => void; changeSlippage: (value: number) => void; + resetEstimatedLiquidity: () => void; switchSwapDirection: () => void; openConfirmModal: () => void; @@ -63,6 +65,7 @@ const SwapCard: React.FC = ({ changeTokenB, changeTokenBAmount, changeSlippage, + resetEstimatedLiquidity, switchSwapDirection, openConfirmModal, openConnectWallet, @@ -74,6 +77,7 @@ const SwapCard: React.FC = ({ setSwapRateAction, priceImpactStatus, isSameToken, + isRefetching, }) => { const theme = useTheme(); const { t } = useTranslation(); @@ -107,6 +111,8 @@ const SwapCard: React.FC = ({ isSwitchNetwork={isSwitchNetwork} priceImpactStatus={priceImpactStatus} isSameToken={isSameToken} + isRefetching={isRefetching} + resetEstimatedLiquidity={resetEstimatedLiquidity} /> {shouldShowPriceImpactWarning && ( diff --git a/packages/web/src/layouts/swap/components/swap-card/swap-card-content/SwapCardContent.styles.ts b/packages/web/src/layouts/swap/components/swap-card/swap-card-content/SwapCardContent.styles.ts index 057dcd518..8c1abf5ee 100644 --- a/packages/web/src/layouts/swap/components/swap-card/swap-card-content/SwapCardContent.styles.ts +++ b/packages/web/src/layouts/swap/components/swap-card/swap-card-content/SwapCardContent.styles.ts @@ -34,6 +34,7 @@ export const ContentWrapper = styled.div` align-self: stretch; color: ${({ theme }) => theme.color.text02}; } + .text-opacity { opacity: 0.5; } @@ -70,6 +71,30 @@ export const ContentWrapper = styled.div` ${mixins.flexbox("row", "center", "space-between")}; width: 100%; align-self: stretch; + .balance-wrapper { + display: flex; + align-items: center; + justify-content: center; + gap: 4px; + .balance-max-button { + box-sizing: content-box; + display: flex; + justify-content: center; + align-items: center; + padding: 1px 6px; + height: 14px; + border-radius: 36px; + background: rgba(0, 89, 255, 0.2); + font-size: 11px; + font-weight: 500; + color: #007aff; + cursor: pointer; + &:hover { + background: ${({ theme }) => (theme.themeKey === "dark" ? "rgba(0, 89, 255, 0.1)" : "rgba(0, 89, 255, 0.3)")}; + } + } + } + .price-text, .balance-text { flex-shrink: 0; @@ -90,7 +115,6 @@ export const ContentWrapper = styled.div` max-width: 100%; } .balance-text-disabled { - cursor: pointer; } } diff --git a/packages/web/src/layouts/swap/components/swap-card/swap-card-content/SwapCardContent.tsx b/packages/web/src/layouts/swap/components/swap-card/swap-card-content/SwapCardContent.tsx index bc309773a..c60ffd61c 100644 --- a/packages/web/src/layouts/swap/components/swap-card/swap-card-content/SwapCardContent.tsx +++ b/packages/web/src/layouts/swap/components/swap-card/swap-card-content/SwapCardContent.tsx @@ -1,6 +1,5 @@ import BigNumber from "bignumber.js"; import React, { useCallback, useMemo } from "react"; -import { useTranslation } from "react-i18next"; import { isAmount } from "@common/utils/data-check-util"; import IconSwapArrowDown from "@components/common/icons/IconSwapArrowDown"; @@ -20,6 +19,8 @@ import { PriceInfoWrapper, SwapDetailSectionWrapper, } from "./SwapCardContent.styles"; +import IconWallet from "@components/common/icons/IconWallet"; +import { useTranslation } from "react-i18next"; interface ContentProps { swapTokenInfo: SwapTokenInfo; @@ -30,12 +31,14 @@ interface ContentProps { changeTokenB: (token: TokenModel) => void; changeTokenBAmount: (value: string, none?: boolean) => void; switchSwapDirection: () => void; + resetEstimatedLiquidity: () => void; connectedWallet: boolean; isLoading: boolean; setSwapRateAction: (type: "ATOB" | "BTOA") => void; isSwitchNetwork: boolean; priceImpactStatus: PriceImpactStatus; isSameToken: boolean; + isRefetching: boolean; } const SwapCardContent: React.FC = ({ @@ -52,8 +55,11 @@ const SwapCardContent: React.FC = ({ setSwapRateAction, priceImpactStatus, isSameToken, + resetEstimatedLiquidity, + isRefetching, }) => { const { t } = useTranslation(); + const theme = useTheme(); const tokenA = swapTokenInfo.tokenA; const tokenB = swapTokenInfo.tokenB; @@ -87,18 +93,12 @@ const SwapCardContent: React.FC = ({ const handleAutoFillTokenA = useCallback(() => { if (connectedWallet) { + resetEstimatedLiquidity(); const formatValue = parseFloat(swapTokenInfo.tokenABalance.replace(/,/g, "")).toString(); changeTokenAAmount(formatValue); } }, [changeTokenAAmount, connectedWallet, swapTokenInfo]); - const handleAutoFillTokenB = useCallback(() => { - if (connectedWallet) { - const formatValue = parseFloat(swapTokenInfo.tokenBBalance.replace(/,/g, "")).toString(); - changeTokenBAmount(formatValue); - } - }, [changeTokenBAmount, connectedWallet, swapTokenInfo]); - const isShowInfoSection = useMemo(() => { return ( !!(swapSummaryInfo && !!Number(swapTokenInfo.tokenAAmount) && !!Number(swapTokenInfo.tokenBAmount)) || isLoading @@ -121,6 +121,15 @@ const SwapCardContent: React.FC = ({ return swapTokenInfo.tokenBAmount; }, [swapTokenInfo.tokenBAmount, tokenB?.decimals]); + /** + * Ensure tokenABalance is a valid value (not empty (“-”) or zero) + * Note: Consider using includes when you have more than 3 comparisons + * return !(["-", "0", "undefined"].includes(swapTokenInfo.tokenABalance)); + */ + const hasTokenABalance = useMemo(() => { + return swapTokenInfo.tokenABalance !== "-" && swapTokenInfo.tokenABalance !== "0"; + }, [swapTokenInfo.tokenABalance]); + const showPriceImpact = useMemo( () => !isLoading && !!swapSummaryInfo?.priceImpact && swapRouteInfos.length > 0, [isLoading, swapRouteInfos.length, swapSummaryInfo?.priceImpact], @@ -132,7 +141,11 @@ const SwapCardContent: React.FC = ({
    = ({ {swapTokenInfo.tokenAUSDStr} - - {t("business:balance")}: {swapTokenInfo.tokenABalance} - +
    + {connectedWallet && } + + {swapTokenInfo.tokenABalance} + + {hasTokenABalance && ( + + )} +
    @@ -164,7 +182,7 @@ const SwapCardContent: React.FC = ({
    = ({ )} - - {t("business:balance")}: {swapTokenInfo.tokenBBalance} - +
    + {connectedWallet && } + + {swapTokenInfo.tokenBBalance} + +
    {!isSameToken && ( diff --git a/packages/web/src/layouts/swap/components/swap-info-chart/SwapInfoChart.styles.ts b/packages/web/src/layouts/swap/components/swap-info-chart/SwapInfoChart.styles.ts new file mode 100644 index 000000000..adf592d46 --- /dev/null +++ b/packages/web/src/layouts/swap/components/swap-info-chart/SwapInfoChart.styles.ts @@ -0,0 +1,14 @@ +import styled from "@emotion/styled"; + +export const SwapInfoChartWrapper = styled.div` + width: 100%; + height: 100%; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + + padding: 6px 16px; + border-radius: 8px; + border: 1px solid ${({ theme }) => theme.color.border02}; +`; diff --git a/packages/web/src/layouts/swap/components/swap-info-chart/SwapInfoChart.tsx b/packages/web/src/layouts/swap/components/swap-info-chart/SwapInfoChart.tsx new file mode 100644 index 000000000..619ed0817 --- /dev/null +++ b/packages/web/src/layouts/swap/components/swap-info-chart/SwapInfoChart.tsx @@ -0,0 +1,29 @@ +import React from "react"; +import { useAtomValue } from "jotai"; + +import { SwapState } from "@states/index"; + +import { SwapInfoChartWrapper } from "./SwapInfoChart.styles"; +import SwapTokenInfo from "./swap-token-info/SwapTokenInfo"; +import { Divider } from "@components/common/divider/divider"; +import { GNOT_TOKEN_DEFAULT } from "@common/values/token-constant"; + +const SwapInfoChart = () => { + const swapValue = useAtomValue(SwapState.swap); + const { tokenA, tokenB } = swapValue; + const DEFAULT_TOKEN = GNOT_TOKEN_DEFAULT; + + const hasNoTokens = !tokenA && !tokenB; + const isTokenPairSelected = tokenA && tokenB; + + return ( + + {hasNoTokens && } + {tokenA && } + {isTokenPairSelected && } + {tokenB && } + + ); +}; + +export default SwapInfoChart; diff --git a/packages/web/src/layouts/swap/components/swap-info-chart/swap-token-info/SwapTokenChart.styles.ts b/packages/web/src/layouts/swap/components/swap-info-chart/swap-token-info/SwapTokenChart.styles.ts new file mode 100644 index 000000000..5ecfd6724 --- /dev/null +++ b/packages/web/src/layouts/swap/components/swap-info-chart/swap-token-info/SwapTokenChart.styles.ts @@ -0,0 +1,42 @@ +import styled from "@emotion/styled"; +import mixins from "@styles/mixins"; +import { fonts } from "@constants/font.constant"; + +export const SwapTokenChartWrapper = styled.div` + display: flex; + align-items: center; + justify-content: center; + width: 100%; + height: 50px; +`; + +export const ChartNotFound = styled.div` + display: flex; + justify-content: center; + align-items: center; + width: 100%; + height: 50px; + border-radius: 8px; + color: ${({ theme }) => theme.color.text04}; + font-size: 14px; +`; + +export const LoadingWrapper = styled.div` + ${mixins.flexbox("row", "flex-start", "center")} + width: 100%; + background-color: ${({ theme }) => theme.color.background01}; + border-radius: 8px; + > span { + margin-top: 6px; + color: ${({ theme }) => theme.color.text04} ${fonts.body11}; + } + > div { + width: 36px; + height: 36px; + &::before { + background-color: ${({ theme }) => theme.color.background01}; + width: 26px; + height: 26px; + } + } +`; diff --git a/packages/web/src/layouts/swap/components/swap-info-chart/swap-token-info/SwapTokenChart.tsx b/packages/web/src/layouts/swap/components/swap-info-chart/swap-token-info/SwapTokenChart.tsx new file mode 100644 index 000000000..2ff2db331 --- /dev/null +++ b/packages/web/src/layouts/swap/components/swap-info-chart/swap-token-info/SwapTokenChart.tsx @@ -0,0 +1,89 @@ +import React from "react"; + +import { IPriceResponse } from "@repositories/token"; +import { getLocalizeTime } from "@utils/chart"; + +import LineGraph, { LineGraphData } from "@components/common/line-graph/LineGraph"; +import { ChartNotFound, LoadingWrapper, SwapTokenChartWrapper } from "./SwapTokenChart.styles"; +import LoadingSpinner from "@components/common/loading-spinner/LoadingSpinner"; +import { SWAP_TOKEN_CHART_COLORS } from "@constants/graph.constant"; + +interface SwapTokenChartProps { + data: IPriceResponse[]; + isLoading: boolean; + isFetched: boolean; + isChartHovered: boolean; + onMouseMove: (data?: LineGraphData) => void; + onMouseOut: () => void; + onMouseHover: () => void; + onMouseLeave: () => void; +} + +const SwapTokenChart = ({ + data = [], + isLoading, + isFetched, + isChartHovered, + onMouseMove, + onMouseOut, + onMouseHover, + onMouseLeave, +}: SwapTokenChartProps) => { + const hasData = isFetched && data && data.length > 0; + const isNoData = isFetched && !isLoading && !data; + + const chartData = React.useMemo(() => { + if (!hasData) return []; + return data + .map(item => { + return { + value: item.price, + time: getLocalizeTime(item.time), + }; + }) + .reverse(); + }, [hasData, data]); + + const handleMouseMove = React.useCallback( + (data?: LineGraphData) => { + onMouseMove(data); + }, + [onMouseMove], + ); + + const handleMouseOut = React.useCallback(() => { + if (!isChartHovered) { + onMouseOut(); + } + }, [onMouseOut, isChartHovered]); + + return ( + + {isLoading && ( + + + + )} + {isNoData && No price history} + {hasData && ( + + )} + + ); +}; + +export default SwapTokenChart; diff --git a/packages/web/src/layouts/swap/components/swap-info-chart/swap-token-info/SwapTokenHeader.styles.ts b/packages/web/src/layouts/swap/components/swap-info-chart/swap-token-info/SwapTokenHeader.styles.ts new file mode 100644 index 000000000..2436ba87b --- /dev/null +++ b/packages/web/src/layouts/swap/components/swap-info-chart/swap-token-info/SwapTokenHeader.styles.ts @@ -0,0 +1,113 @@ +import styled from "@emotion/styled"; + +interface Props { + containerWidth: number; + priceWidth: number; + tokenNameWidth: number; +} + +export const SwapTokenHeaderWrapper = styled.div` + width: 100%; + display: flex; + align-items: center; + justify-content: space-between; + .left { + display: flex; + justify-content: flex-start; + align-items: flex-start; + gap: 8px; + max-width: ${({ containerWidth, priceWidth }) => `calc(${containerWidth}px - 10px - ${priceWidth}px)`}; + width: 100%; + .token-title { + display: flex; + flex-direction: column; + justify-content: center; + align-items: flex-start; + gap: 2px; + font-weight: 500; + .name { + display: flex; + align-items: center; + gap: 8px; + + color: ${({ theme }) => theme.color.text02}; + font-size: 18px; + .token-name { + cursor: pointer; + &:hover { + color: ${({ theme }) => theme.color.text07}; + } + } + .link { + max-width: ${({ containerWidth, tokenNameWidth, priceWidth }) => + `calc(${containerWidth}px - 58px - ${tokenNameWidth}px - ${priceWidth}px)`}; + width: 100%; + display: flex; + align-items: center; + gap: 6px; + color: ${({ theme }) => theme.color.text04}; + font-size: 10px; + font-weight: 400; + padding: 2px 4px; + border-radius: 4px; + background-color: ${({ theme }) => (theme.themeKey === "dark" ? "#0D121C" : "rgba(224, 232, 244, 0.40)")}; + span { + overflow: hidden; + text-overflow: ellipsis; + direction: rtl; + white-space: nowrap; + } + .path-link-icon { + width: 10px; + height: 10px; + flex-shrink: 0; + } + &:hover { + color: ${({ theme }) => theme.color.text03}; + .path-link-icon { + path { + fill: ${({ theme }) => theme.color.text03}; + } + } + } + } + } + .symbol { + color: #596782; + font-size: 14px; + } + } + } + .right { + display: flex; + justify-content: flex-end; + align-items: center; + gap: 8px; + .token-price { + position: relative; + display: flex; + flex-direction: column; + justify-content: center; + align-items: flex-end; + gap: 2px; + .price { + color: ${({ theme }) => theme.color.text02}; + font-size: 18px; + font-weight: 500; + } + .blank { + min-height: 17px; + font-size: 14px; + } + .date { + position: absolute; + min-width: 100px; + text-align: end; + top: 22px; + color: #596782; + font-size: 14px; + font-weight: 400; + } + } + } +`; diff --git a/packages/web/src/layouts/swap/components/swap-info-chart/swap-token-info/SwapTokenHeader.tsx b/packages/web/src/layouts/swap/components/swap-info-chart/swap-token-info/SwapTokenHeader.tsx new file mode 100644 index 000000000..f6ac5d356 --- /dev/null +++ b/packages/web/src/layouts/swap/components/swap-info-chart/swap-token-info/SwapTokenHeader.tsx @@ -0,0 +1,128 @@ +import React from "react"; +import dayjs from "dayjs"; +import { useTranslation } from "react-i18next"; + +import { formatPrice } from "@utils/new-number-utils"; +import { useTheme } from "@emotion/react"; +import { useGnoscanUrl } from "@hooks/common/use-gnoscan-url"; +import { LineGraphData } from "@components/common/line-graph/LineGraph"; +import { GNOT_TOKEN } from "@common/values/token-constant"; +import useElementWidth from "@hooks/common/use-element-width"; + +import MissingLogo from "@components/common/missing-logo/MissingLogo"; +import { SwapTokenHeaderWrapper } from "./SwapTokenHeader.styles"; +import IconOpenLink from "@components/common/icons/IconOpenLink"; +import { nullish } from "@utils/nullish-utils"; +import { STATIC_TEXT } from "@common/values"; +import useCustomRouter from "@hooks/common/use-custom-router"; + +interface TokenInfo { + name: string; + symbol: string; + logoURI: string; + path: string | undefined; + isNative: boolean; +} + +interface SwapTokenHeaderProps { + tokenInfo: TokenInfo; + currentPrice: string | undefined; + chartData?: LineGraphData; + containerWidth: number; +} + +const SwapTokenHeader = ({ tokenInfo, currentPrice, chartData, containerWidth }: SwapTokenHeaderProps) => { + const router = useCustomRouter(); + const elementId = React.useMemo(() => `${tokenInfo.name}`, [tokenInfo.name]); + + const priceRef = React.useRef(null); + const tokenNameRef = React.useRef(null); + + const priceWidth = useElementWidth(priceRef, [tokenInfo]); + const tokenNameWidth = useElementWidth(tokenNameRef, [tokenInfo]); + + const theme = useTheme(); + const { t } = useTranslation(); + + const { getGnoscanUrl, getTokenUrl } = useGnoscanUrl(); + + const displayPrice = React.useMemo(() => { + const price = nullish.handleFalsy(chartData?.value, currentPrice); + return `${formatPrice(price, { lessThan1Significant: 2 })}`; + }, [chartData, currentPrice]); + + const displayDate = React.useMemo(() => { + if (!chartData) return t("common:day.today"); + + const timeFormat = "MMM D, HH:mm"; + const today = dayjs().format(timeFormat); + const chartDate = dayjs(chartData.time).format(timeFormat); + + return chartDate === today ? t("common:day.today") : chartDate; + }, [chartData, t]); + + const displayTokenPath = React.useMemo(() => { + if (tokenInfo.isNative) { + return STATIC_TEXT.NATIVE_COIN; + } + return tokenInfo.path; + }, [tokenInfo]); + + const onClickTokenName = React.useCallback(() => { + if (!tokenInfo.path) return; + if (tokenInfo.isNative) { + router.movePageWithTokenPath("TOKEN", GNOT_TOKEN.path); + return; + } + router.movePageWithTokenPath("TOKEN", tokenInfo.path); + }, [tokenInfo.path, router]); + + const onClickPath = React.useCallback( + (e: React.MouseEvent) => { + e.stopPropagation(); + if (tokenInfo.isNative) { + window.open(getGnoscanUrl(), "_blank", "noopener,noreferrer"); + } else { + window.open(getTokenUrl(nullish.handleFalsy(tokenInfo.path, "")), "_blank", "noopener,noreferrer"); + } + }, + [tokenInfo], + ); + + return ( + +
    + +
    +
    + + +
    +
    {tokenInfo.symbol}
    +
    +
    +
    +
    +
    + {displayPrice} +
    +
    +
    {displayDate}
    +
    +
    + + ); +}; + +export default SwapTokenHeader; diff --git a/packages/web/src/layouts/swap/components/swap-info-chart/swap-token-info/SwapTokenInfo.styles.ts b/packages/web/src/layouts/swap/components/swap-info-chart/swap-token-info/SwapTokenInfo.styles.ts new file mode 100644 index 000000000..1ef8f2152 --- /dev/null +++ b/packages/web/src/layouts/swap/components/swap-info-chart/swap-token-info/SwapTokenInfo.styles.ts @@ -0,0 +1,9 @@ +import styled from "@emotion/styled"; + +export const SwapTokenInfoWrapper = styled.div` + width: 100%; + display: flex; + flex-direction: column; + gap: 32px; + padding: 10px 0; +`; diff --git a/packages/web/src/layouts/swap/components/swap-info-chart/swap-token-info/SwapTokenInfo.tsx b/packages/web/src/layouts/swap/components/swap-info-chart/swap-token-info/SwapTokenInfo.tsx new file mode 100644 index 000000000..658997d9a --- /dev/null +++ b/packages/web/src/layouts/swap/components/swap-info-chart/swap-token-info/SwapTokenInfo.tsx @@ -0,0 +1,84 @@ +import React from "react"; + +import { TokenModel } from "@models/token/token-model"; +import { LineGraphData } from "@components/common/line-graph/LineGraph"; + +import { SwapTokenInfoWrapper } from "./SwapTokenInfo.styles"; +import SwapTokenHeader from "./SwapTokenHeader"; +import { useGetTokenDetails, useGetTokenPrices } from "@query/token"; +import SwapTokenChart from "./SwapTokenChart"; +import useElementWidth from "@hooks/common/use-element-width"; + +interface SwapTokenInfoProps { + token: TokenModel; +} + +const SwapTokenInfo = ({ token }: SwapTokenInfoProps) => { + const [chartData, setChartData] = React.useState(); + const [isChartHovered, setIsChartHovered] = React.useState(false); + + const containerRef = React.useRef(null); + const containerWidth = useElementWidth(containerRef); + + const tokenData = React.useMemo( + () => ({ + name: token.name, + symbol: token.symbol, + logoURI: token.logoURI, + path: token.type === "native" ? token.wrappedPath : token.path, + isNative: token.type === "native", + }), + [token], + ); + + const { data: { usd: currentPrice } = {} } = useGetTokenPrices(tokenData.path as string, { + enabled: !!tokenData.path, + }); + + const { + data: { prices7d = [] } = {}, + isLoading, + isFetched, + } = useGetTokenDetails(tokenData.path as string, { + enabled: !!tokenData.path, + }); + + const handleMouseMove = React.useCallback( + (data?: LineGraphData) => { + setChartData(data); + }, + [tokenData.path], + ); + + const handleMouseOut = React.useCallback(() => { + handleMouseMove(undefined); + }, [tokenData.path]); + + // @dev If the selected token changes, reset the chart data. + React.useEffect(() => { + handleMouseOut(); + }, [tokenData, handleMouseOut]); + + return ( + + + setIsChartHovered(true)} + onMouseLeave={() => setIsChartHovered(false)} + /> + + ); +}; + +export default SwapTokenInfo; diff --git a/packages/web/src/layouts/swap/components/swap-info-transaction-list/SwapInfoTransactionList.styles.ts b/packages/web/src/layouts/swap/components/swap-info-transaction-list/SwapInfoTransactionList.styles.ts new file mode 100644 index 000000000..aa72344b4 --- /dev/null +++ b/packages/web/src/layouts/swap/components/swap-info-transaction-list/SwapInfoTransactionList.styles.ts @@ -0,0 +1,14 @@ +import styled from "@emotion/styled"; + +export const SwapInfoTransactionListWrapper = styled.div` + width: 100%; + height: 100%; + display: flex; + flex-direction: column; + align-items: center; + justify-content: flex-start; + + padding: 16px 16px 0 16px; + border-radius: 8px; + border: 1px solid ${({ theme }) => theme.color.border02}; +`; diff --git a/packages/web/src/layouts/swap/components/swap-info-transaction-list/SwapInfoTransactionList.tsx b/packages/web/src/layouts/swap/components/swap-info-transaction-list/SwapInfoTransactionList.tsx new file mode 100644 index 000000000..d268390d2 --- /dev/null +++ b/packages/web/src/layouts/swap/components/swap-info-transaction-list/SwapInfoTransactionList.tsx @@ -0,0 +1,20 @@ +import React from "react"; + +import { SwapInfoTransactionListWrapper } from "./SwapInfoTransactionList.styles"; + +import SwapInfoTransactionListTable from "./swap-info-transaction-list-table/SwapInfoTransactionListTable"; +import { DEVICE_TYPE } from "@styles/media"; + +interface SwapInfoTransactionListProps { + breakpoint: DEVICE_TYPE; +} + +const SwapInfoTransactionList = ({ breakpoint }: SwapInfoTransactionListProps) => { + return ( + + + + ); +}; + +export default SwapInfoTransactionList; diff --git a/packages/web/src/layouts/swap/components/swap-info-transaction-list/swap-info-transaction-list-table/SwapInfoTransactionListTable.styles.ts b/packages/web/src/layouts/swap/components/swap-info-transaction-list/swap-info-transaction-list-table/SwapInfoTransactionListTable.styles.ts new file mode 100644 index 000000000..be59a9167 --- /dev/null +++ b/packages/web/src/layouts/swap/components/swap-info-transaction-list/swap-info-transaction-list-table/SwapInfoTransactionListTable.styles.ts @@ -0,0 +1,122 @@ +import styled from "@emotion/styled"; + +import { fonts } from "@constants/font.constant"; + +export const TransactionListTableHeader = styled.div` + width: 100%; + display: flex; + align-items: center; + justify-content: center; + ${fonts.body12}; + color: ${({ theme }) => theme.color.text04}; +`; + +export const TableHeader = styled.div<{ tdWidth: number }>` + width: ${({ tdWidth }) => `${tdWidth}px`}; + height: 100%; + + display: flex; + align-items: center; + justify-content: flex-end; + &.left { + flex-shrink: 0; + justify-content: flex-start; + } + + @media screen and (max-width: 768px) { + &.left { + width: auto; + flex: 1; + } + &:last-child { + width: auto; + flex: 2; + } + } +`; + +export const TransactionListTableList = styled.div` + display: flex; + flex-direction: column; + align-items: center; + justify-content: flex-start; + width: 100%; + padding-bottom: 4px; +`; + +export const TransactionListTableRow = styled.div` + display: flex; + align-items: center; + justify-content: flex-start; +`; + +export const TransactionListTableRowWrapper = styled.div` + display: flex; + align-items: center; + justify-content: flex-start; + width: 100%; + padding: 12px 0; +`; + +export const TableColumn = styled.div<{ tdWidth: number }>` + display: flex; + align-items: center; + justify-content: flex-end; + gap: 4px; + width: ${({ tdWidth }) => `${tdWidth}px`}; + height: 100%; + + ${fonts.body12}; + color: ${({ theme }) => theme.color.text01}; + &.left { + flex-shrink: 0; + justify-content: flex-start; + } + + .path-link-icon { + path { + fill: ${({ theme }) => theme.color.text04}; + } + &:hover { + path { + fill: ${({ theme }) => theme.color.text03}; + } + } + } + + @media screen and (max-width: 768px) { + &.left { + width: auto; + flex: 1; + } + &:last-child { + width: auto; + flex: 2; + } + } +`; + +export const TokenPairWrapper = styled.div` + display: flex; + align-items: center; + gap: 4px; + + .token-amount { + display: flex; + align-items: center; + gap: 4px; + + span { + font-size: 14px; + color: ${({ theme }) => theme.color.text01}; + } + } + + .arrow { + width: 14px; + height: 14px; + * { + fill: ${({ theme }) => (theme.themeKey === "dark" ? "#596782" : "#90a2c0")}; + } + } +`; diff --git a/packages/web/src/layouts/swap/components/swap-info-transaction-list/swap-info-transaction-list-table/SwapInfoTransactionListTable.tsx b/packages/web/src/layouts/swap/components/swap-info-transaction-list/swap-info-transaction-list-table/SwapInfoTransactionListTable.tsx new file mode 100644 index 000000000..751224532 --- /dev/null +++ b/packages/web/src/layouts/swap/components/swap-info-transaction-list/swap-info-transaction-list-table/SwapInfoTransactionListTable.tsx @@ -0,0 +1,111 @@ +import React from "react"; +import { cx } from "@emotion/css"; + +import { + MOBILE_TABLE_HEAD, + TABLE_HEAD, +} from "@layouts/swap/containers/swap-info-transaction-list-container/SwapInfoTransactionListContainer"; +import { + TRANSACTION_TD_WIDTH, + TABLET_TRANSACTION_TD_WIDTH, + MOBILE_TRANSACTION_TD_WIDTH, +} from "@constants/skeleton.constant"; + +import { + TableHeader, + TableColumn, + TransactionListTableHeader, + TransactionListTableList, + TransactionListTableRowWrapper, + TokenPairWrapper, +} from "./SwapInfoTransactionListTable.styles"; +import IconOpenLink from "@components/common/icons/IconOpenLink"; +import IconRightArrow from "@components/common/icons/IconRightArrow"; +import MissingLogo from "@components/common/missing-logo/MissingLogo"; +import { GNOT_TOKEN_DEFAULT } from "@common/values/token-constant"; +import DateTimeTooltip from "@components/common/date-time-tooltip/DateTimeTooltip"; +import { DEVICE_TYPE } from "@styles/media"; + +interface SwapInfoTransactionListTableProps { + breakpoint: DEVICE_TYPE; +} + +const getTableWidths = (breakpoint: DEVICE_TYPE) => { + if (breakpoint === DEVICE_TYPE.MOBILE) { + return MOBILE_TRANSACTION_TD_WIDTH; + } + if (breakpoint === DEVICE_TYPE.TABLET || breakpoint === DEVICE_TYPE.TABLET_M || breakpoint === DEVICE_TYPE.TABLET_S) { + return TABLET_TRANSACTION_TD_WIDTH; + } + return TRANSACTION_TD_WIDTH; +}; + +const SwapInfoTransactionListTable = ({ breakpoint }: SwapInfoTransactionListTableProps) => { + const getTableHeaders = React.useCallback(() => { + if (breakpoint === DEVICE_TYPE.MOBILE) { + return MOBILE_TABLE_HEAD; + } + + return TABLE_HEAD; + }, [breakpoint]); + + return ( + <> + + {Object.values(getTableHeaders()).map((head, idx) => { + return ( + + {head} + + ); + })} + + + + {[...Array(5)].map((_, index) => ( + // temporarily use index as key for development phase only + + ))} + + + ); +}; + +const TransactionListTableRow = ({ breakpoint }: { breakpoint: DEVICE_TYPE }) => { + const today = new Date(); + const widths = getTableWidths(breakpoint); + const isMobile = breakpoint === DEVICE_TYPE.MOBILE; + + return ( + + + + 1s ago + + + + {!isMobile && $12.05} + + +
    + 152.15 + +
    + +
    + 5.15K + +
    +
    +
    +
    + ); +}; + +export default SwapInfoTransactionListTable; diff --git a/packages/web/src/layouts/swap/containers/swap-container/SwapContainer.tsx b/packages/web/src/layouts/swap/containers/swap-container/SwapContainer.tsx index 17eb2144c..a298c60bd 100644 --- a/packages/web/src/layouts/swap/containers/swap-container/SwapContainer.tsx +++ b/packages/web/src/layouts/swap/containers/swap-container/SwapContainer.tsx @@ -40,11 +40,13 @@ const SwapContainer: React.FC = () => { isSwitchNetwork, switchNetwork, isLoading, + isRefetching, setSwapValue, setSwapRateAction, priceImpactStatus, setTokenAAmount, isSameToken, + handleResetEstimatedLiquidity, } = useSwapHandler(); useEffect(() => { @@ -118,7 +120,9 @@ const SwapContainer: React.FC = () => { isLoading={isLoading} setSwapRateAction={setSwapRateAction} priceImpactStatus={priceImpactStatus} + resetEstimatedLiquidity={handleResetEstimatedLiquidity} isSameToken={isSameToken} + isRefetching={isRefetching} /> ); }; diff --git a/packages/web/src/layouts/swap/containers/swap-info-chart-container/SwapInfoChartContainer.tsx b/packages/web/src/layouts/swap/containers/swap-info-chart-container/SwapInfoChartContainer.tsx new file mode 100644 index 000000000..e799930ed --- /dev/null +++ b/packages/web/src/layouts/swap/containers/swap-info-chart-container/SwapInfoChartContainer.tsx @@ -0,0 +1,9 @@ +import React from "react"; + +import SwapInfoChart from "@layouts/swap/components/swap-info-chart/SwapInfoChart"; + +const SwapInfoChartContainer = () => { + return ; +}; + +export default SwapInfoChartContainer; diff --git a/packages/web/src/layouts/swap/containers/swap-info-transaction-list-container/SwapInfoTransactionListContainer.tsx b/packages/web/src/layouts/swap/containers/swap-info-transaction-list-container/SwapInfoTransactionListContainer.tsx new file mode 100644 index 000000000..910721e5f --- /dev/null +++ b/packages/web/src/layouts/swap/containers/swap-info-transaction-list-container/SwapInfoTransactionListContainer.tsx @@ -0,0 +1,22 @@ +import React from "react"; + +import SwapInfoTransactionList from "@layouts/swap/components/swap-info-transaction-list/SwapInfoTransactionList"; +import { useWindowSize } from "@hooks/common/use-window-size"; + +export const TABLE_HEAD = { + TIME: "Time", + VALUE: "Value", + Swap: "Swap", +}; + +export const MOBILE_TABLE_HEAD = { + TIME: "Time", + Swap: "Swap", +}; + +const SwapInfoTransactionListContainer = () => { + const { breakpoint } = useWindowSize(); + return ; +}; + +export default SwapInfoTransactionListContainer; diff --git a/packages/web/src/layouts/token-detail/components/token-chart/token-chart-graph/TokenChartGraph.tsx b/packages/web/src/layouts/token-detail/components/token-chart/token-chart-graph/TokenChartGraph.tsx index 4229e23d1..58f5e0773 100644 --- a/packages/web/src/layouts/token-detail/components/token-chart/token-chart-graph/TokenChartGraph.tsx +++ b/packages/web/src/layouts/token-detail/components/token-chart/token-chart-graph/TokenChartGraph.tsx @@ -7,6 +7,7 @@ import { ComponentSize } from "@hooks/common/use-component-size"; import { DEVICE_TYPE } from "@styles/media"; import { TokenChartGraphWrapper, TokenChartGraphXLabel, YAxisLabelWrapper } from "./TokenChartGraph.styles"; +import { SWAP_TOKEN_CHART_COLORS } from "@constants/graph.constant"; export interface TokenChartGraphProps { datas: { @@ -182,6 +183,8 @@ const TokenChartGraph: React.FC = ({ width={size?.width || 0} height={(size?.height || 0) - (breakpoint !== DEVICE_TYPE.MOBILE ? 40 : 30) - customData.height} color="#192EA2" + gradientStartColor={SWAP_TOKEN_CHART_COLORS.GRADIENT.START} + gradientEndColor={SWAP_TOKEN_CHART_COLORS.GRADIENT.END} strokeWidth={1} datas={datas.map(data => ({ value: data.amount.value, diff --git a/packages/web/src/layouts/token-detail/components/token-swap/TokenSwap.styles.ts b/packages/web/src/layouts/token-detail/components/token-swap/TokenSwap.styles.ts index 9d555d977..d94de299b 100644 --- a/packages/web/src/layouts/token-detail/components/token-swap/TokenSwap.styles.ts +++ b/packages/web/src/layouts/token-detail/components/token-swap/TokenSwap.styles.ts @@ -92,6 +92,29 @@ export const wrapper = (theme: Theme) => css` .info { ${mixins.flexbox("row", "center", "space-between")}; width: 100%; + .balance-wrapper { + display: flex; + align-items: center; + justify-content: center; + gap: 4px; + .balance-max-button { + box-sizing: content-box; + display: flex; + justify-content: center; + align-items: center; + padding: 1px 6px; + height: 14px; + border-radius: 36px; + background: rgba(0, 89, 255, 0.2); + font-size: 11px; + font-weight: 500; + color: #007aff; + cursor: pointer; + &:hover { + background: ${theme.themeKey === "dark" ? "rgba(0, 89, 255, 0.1)" : "rgba(0, 89, 255, 0.3)"}; + } + } + } } .text-opacity { opacity: 0.5; @@ -121,7 +144,6 @@ export const wrapper = (theme: Theme) => css` overflow: hidden; } .balance-text-disabled { - cursor: pointer; } .token { diff --git a/packages/web/src/layouts/token-detail/components/token-swap/TokenSwap.tsx b/packages/web/src/layouts/token-detail/components/token-swap/TokenSwap.tsx index 6780b63ac..029c03d48 100644 --- a/packages/web/src/layouts/token-detail/components/token-swap/TokenSwap.tsx +++ b/packages/web/src/layouts/token-detail/components/token-swap/TokenSwap.tsx @@ -16,6 +16,7 @@ import { TokenModel } from "@models/token/token-model"; import { DataTokenInfo } from "@models/token/token-swap-model"; import { CopyTooltip, wrapper } from "./TokenSwap.styles"; +import IconWallet from "@components/common/icons/IconWallet"; export interface TokenSwapProps { isSwitchNetwork: boolean; @@ -108,12 +109,14 @@ const TokenSwap: React.FC = ({ } }, [changeTokenAAmount, connected, dataTokenInfo]); - const handleAutoFillTokenB = useCallback(() => { - if (connected) { - const formatValue = parseFloat(dataTokenInfo.tokenBBalance.replace(/,/g, "")).toString(); - changeTokenBAmount(formatValue); - } - }, [changeTokenBAmount, connected, dataTokenInfo]); + /** + * Ensure tokenABalance is a valid value (not empty (“-”) or zero) + * Note: Consider using includes when you have more than 3 comparisons + * return !(["-", "0", "undefined"].includes(swapTokenInfo.tokenABalance)); + */ + const hasTokenABalance = useMemo(() => { + return swapTokenInfo.tokenABalance !== "-" && swapTokenInfo.tokenABalance !== "0"; + }, [swapTokenInfo.tokenABalance]); const onClickConfirm = useCallback(() => { if (!connected || isSwitchNetwork) { @@ -167,12 +170,17 @@ const TokenSwap: React.FC = ({ {dataTokenInfo.tokenAUSDStr} - - {t("business:balance")}: {dataTokenInfo.tokenABalance} - +
    + {connected && } + + {dataTokenInfo.tokenABalance} + + {hasTokenABalance && ( + + )} +
    @@ -193,12 +201,12 @@ const TokenSwap: React.FC = ({ {dataTokenInfo.tokenBUSDStr} - - {t("business:balance")}: {dataTokenInfo.tokenBBalance} - +
    + {connected && } + + {dataTokenInfo.tokenBBalance} + +
    diff --git a/packages/web/src/layouts/wallet/components/asset-send-modal/AssetSendModal.styles.ts b/packages/web/src/layouts/wallet/components/asset-send-modal/AssetSendModal.styles.ts index f8badd8ba..b3fd534fb 100644 --- a/packages/web/src/layouts/wallet/components/asset-send-modal/AssetSendModal.styles.ts +++ b/packages/web/src/layouts/wallet/components/asset-send-modal/AssetSendModal.styles.ts @@ -231,6 +231,29 @@ export const AssetSendContent = styled.div` .info { ${mixins.flexbox("row", "center", "space-between")}; width: 100%; + .balance-wrapper { + display: flex; + align-items: center; + justify-content: center; + gap: 4px; + .balance-max-button { + box-sizing: content-box; + display: flex; + justify-content: center; + align-items: center; + padding: 1px 6px; + height: 14px; + border-radius: 36px; + background: rgba(0, 89, 255, 0.2); + font-size: 11px; + font-weight: 500; + color: #007aff; + cursor: pointer; + &:hover { + background: ${({ theme }) => (theme.themeKey === "dark" ? "rgba(0, 89, 255, 0.1)" : "rgba(0, 89, 255, 0.3)")}; + } + } + } } .amount-text { @@ -263,7 +286,7 @@ export const AssetSendContent = styled.div` } .balance-text { - cursor: pointer; + /* cursor: pointer; */ } `; diff --git a/packages/web/src/layouts/wallet/components/asset-send-modal/AssetSendModal.tsx b/packages/web/src/layouts/wallet/components/asset-send-modal/AssetSendModal.tsx index 865f5eea7..dcdcb6b3a 100644 --- a/packages/web/src/layouts/wallet/components/asset-send-modal/AssetSendModal.tsx +++ b/packages/web/src/layouts/wallet/components/asset-send-modal/AssetSendModal.tsx @@ -30,6 +30,7 @@ import { AssetSendTooltipContent, AssetSendWarningContentWrapper, } from "./AssetSendModal.styles"; +import IconWallet from "@components/common/icons/IconWallet"; const DEFAULT_WITHDRAW_GNOT = GNOT_TOKEN; @@ -112,6 +113,10 @@ const AssetSendModal: React.FC = ({ [displayBalanceMap, withdrawInfo?.path], ); + const hasTokenBalance = useMemo(() => { + return !!currentAvailableBalance; + }, [currentAvailableBalance]); + const isDisabledWithdraw = !Number(amount ?? 0) || !address || @@ -208,16 +213,22 @@ const AssetSendModal: React.FC = ({
    {estimatePrice} - {`${t( - "common:action.balance", - )}: ${ - currentAvailableBalance - ? formatPrice(currentAvailableBalance, { - isKMB: false, - usd: false, - }) - : "-" - }`} +
    + {connected && } + {` ${ + currentAvailableBalance + ? formatPrice(currentAvailableBalance, { + isKMB: false, + usd: false, + }) + : "-" + }`} + {hasTokenBalance && ( + + )} +
    diff --git a/packages/web/src/react-query/router/use-get-routes.ts b/packages/web/src/react-query/router/use-get-routes.ts index 10168dcde..9deeb6d43 100644 --- a/packages/web/src/react-query/router/use-get-routes.ts +++ b/packages/web/src/react-query/router/use-get-routes.ts @@ -67,7 +67,7 @@ export const useGetRoutes = ( return result; }, - retry: false, + retry: 1, refetchInterval: REFETCH_INTERVAL, staleTime: STALE_TIME, enabled: !!request?.inputToken?.path && !!request?.outputToken?.path,