diff --git a/packages/web/.env.example b/packages/web/.env.example index 063d63b02..331f23361 100644 --- a/packages/web/.env.example +++ b/packages/web/.env.example @@ -1 +1,6 @@ -NEXT_PUBLIC_API_URL="https://mock-api.gnoswap.io/v3/testnet" \ No newline at end of file +NEXT_PUBLIC_DEFAULT_CHAIN_ID="dev.gnoswap" +NEXT_PUBLIC_API_URL="https://dev-api.gnoswap.io/v3/testnet" +NEXT_PUBLIC_PACKAGE_POOL_PATH="gno.land/r/pool" +NEXT_PUBLIC_PACKAGE_POOL_ADDRESS="g1ee305k8yk0pjz443xpwtqdyep522f9g5r7d63w" +NEXT_PUBLIC_PACKAGE_POSITION_PATH="gno.land/r/position" +NEXT_PUBLIC_PACKAGE_POSITION_ADDRESS="g1htpxzv2dkplvzg50nd8fswrneaxmdpwn459thx" \ No newline at end of file diff --git a/packages/web/.storybook/main.js b/packages/web/.storybook/main.js index 247f34afc..f52e68e7f 100644 --- a/packages/web/.storybook/main.js +++ b/packages/web/.storybook/main.js @@ -21,6 +21,9 @@ module.exports = { }, }); return merge(config, { + experiments: { + topLevelAwait: true, + }, resolve: { plugins: [new TsconfigPathsPlugin()], }, diff --git a/packages/web/package.json b/packages/web/package.json index d698f996e..a61847728 100644 --- a/packages/web/package.json +++ b/packages/web/package.json @@ -19,6 +19,7 @@ "@emotion/styled": "11.10.6", "@floating-ui/react": "0.21.1", "@gnolang/gno-js-client": "1.1.6", + "@hookform/resolvers": "^3.3.2", "@tanstack/react-query": "4.26.1", "axios": "1.3.4", "bignumber.js": "9.1.1", @@ -27,10 +28,12 @@ "next": "13.2.3", "react": "18.2.0", "react-dom": "18.2.0", + "react-hook-form": "^7.47.0", "typescript": "4.9.5", "url-loader": "4.1.1", "utility-types": "3.10.0", - "ws": "8.14.2" + "ws": "8.14.2", + "yup": "^1.3.2" }, "devDependencies": { "@babel/core": "7.21.0", diff --git a/packages/web/public/gnos.svg b/packages/web/public/gnos.svg new file mode 100644 index 000000000..77cf26cee --- /dev/null +++ b/packages/web/public/gnos.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/packages/web/src/common/clients/wallet-client/adena/adena-client.ts b/packages/web/src/common/clients/wallet-client/adena/adena-client.ts index 32ac27689..b2db68f80 100644 --- a/packages/web/src/common/clients/wallet-client/adena/adena-client.ts +++ b/packages/web/src/common/clients/wallet-client/adena/adena-client.ts @@ -73,6 +73,9 @@ export class AdenaClient implements WalletClient { }; public static createAdenaClient() { + if (typeof window === "undefined" || typeof window.adena === "undefined") { + return null; + } return new AdenaClient(); } } diff --git a/packages/web/src/common/errors/common/common-error.ts b/packages/web/src/common/errors/common/common-error.ts index 7f2f2f8d0..65916b209 100644 --- a/packages/web/src/common/errors/common/common-error.ts +++ b/packages/web/src/common/errors/common/common-error.ts @@ -5,6 +5,14 @@ const ERROR_VALUE = { status: 400, type: "Failed to initialize Provider", }, + FAILED_INITIALIZE_GNO_PROVIDER: { + status: 401, + type: "Failed to initialize Gno Provider", + }, + FAILED_INITIALIZE_WALLET: { + status: 402, + type: "Failed to initialize Wallet", + }, }; type ErrorType = keyof typeof ERROR_VALUE; diff --git a/packages/web/src/common/errors/swap/swap-error.ts b/packages/web/src/common/errors/swap/swap-error.ts index b852bbe53..51d0e439a 100644 --- a/packages/web/src/common/errors/swap/swap-error.ts +++ b/packages/web/src/common/errors/swap/swap-error.ts @@ -1,21 +1,13 @@ import { BaseError } from "@common/errors"; const ERROR_VALUE = { - SYMBOL_TYPE_CHECK_ERROR: { - status: 1000, - type: "Token Symbol Data check Error", - }, AMOUNT_TYPE_CHECK_ERROR: { - status: 1001, - type: "Amount Data check Error", - }, - SWAP_RATE_LOOKUP_FAILED: { status: 4000, - type: "Swap rate lookup failed", + type: "Amount Data check Error", }, - EXPECTED_RESULT_LOOKUP_FAILED: { + NOT_FOUND_SWAP_POOL: { status: 4001, - type: "Expected result lookup failed", + type: "Swap Pool not found", }, SWAP_FAILED: { status: 4002, @@ -29,6 +21,14 @@ const ERROR_VALUE = { status: 4004, type: "Get slippage failed", }, + INVALID_PARAMS: { + status: 5000, + type: "Invalid request parameters", + }, + INSUFFICIENT_BALANCE: { + status: 5001, + type: "Insufficient Balance", + }, }; type ErrorType = keyof typeof ERROR_VALUE; diff --git a/packages/web/src/common/utils/data-check-util.ts b/packages/web/src/common/utils/data-check-util.ts index 6a2fe2591..3ef56b295 100644 --- a/packages/web/src/common/utils/data-check-util.ts +++ b/packages/web/src/common/utils/data-check-util.ts @@ -21,3 +21,8 @@ export const notNullNumberType = (v: any) => { export const notEmptyStringType = (v: string) => { return notNullStringType(v) && emptyStrCheckAfterTrim(v); }; + +export function isAmount(str: string) { + const regex = /^\d+(\.\d*)?$/; + return regex.test(str); +} diff --git a/packages/web/src/common/values/data-constant.ts b/packages/web/src/common/values/data-constant.ts index 55f13be1d..c976f1735 100644 --- a/packages/web/src/common/values/data-constant.ts +++ b/packages/web/src/common/values/data-constant.ts @@ -8,7 +8,7 @@ export type IncentivizedOptions = export type StatusOptions = "SUCCESS" | "PENDING" | "FAILED"; export type ActiveStatusOptions = "ACTIVE" | "IN_ACTIVE" | "NONE"; export type TokenTableSelectType = "NATIVE" | "GRC20" | "ALL"; -export type ExactTypeOption = "EXACT_IN" | "EXACT_OUT"; +export type SwapDirectionType = "EXACT_IN" | "EXACT_OUT"; export enum NotificationType { "Approve" = 0, "CreatePool" = 1, diff --git a/packages/web/src/common/values/storage-constant.ts b/packages/web/src/common/values/storage-constant.ts index bc97f5f8b..fd50bcfe6 100644 --- a/packages/web/src/common/values/storage-constant.ts +++ b/packages/web/src/common/values/storage-constant.ts @@ -4,4 +4,4 @@ export type StorageKeyType = | "language" | "search-token-logs"; -export type SessionStorageKeyType = "connectedWallet"; +export type SessionStorageKeyType = "connected-wallet"; diff --git a/packages/web/src/components/common/badge/Badge.tsx b/packages/web/src/components/common/badge/Badge.tsx index a61d81062..a3a7f95f4 100644 --- a/packages/web/src/components/common/badge/Badge.tsx +++ b/packages/web/src/components/common/badge/Badge.tsx @@ -5,6 +5,7 @@ interface BadgeProps { type: BADGE_TYPE; leftIcon?: React.ReactNode; text: string; + className?: string; } export const BADGE_TYPE = { @@ -15,9 +16,9 @@ export const BADGE_TYPE = { } as const; export type BADGE_TYPE = ValuesType; -const Badge: React.FC = ({ type, leftIcon, text }) => { +const Badge: React.FC = ({ type, leftIcon, text, className }) => { return ( - + {leftIcon &&
{leftIcon}
} {text}
diff --git a/packages/web/src/components/common/button/Button.styles.ts b/packages/web/src/components/common/button/Button.styles.ts index 2d276982c..d01c5ad6a 100644 --- a/packages/web/src/components/common/button/Button.styles.ts +++ b/packages/web/src/components/common/button/Button.styles.ts @@ -18,6 +18,7 @@ export interface ButtonStyleProps { radius?: CSSProperties["borderRadius"]; justify?: CSSProperties["justifyContent"]; padding?: CSSProperties["padding"]; + minWidth?: CSSProperties["minWidth"]; } export const ButtonWrapper = styled.button` @@ -31,6 +32,11 @@ export const ButtonWrapper = styled.button` if (fullWidth) return "100%"; return "auto"; }}; + min-width: ${({ minWidth }) => { + if (minWidth) + return typeof minWidth === "number" ? minWidth + "px" : minWidth; + return "auto"; + }}; height: ${({ height }) => { if (height) return typeof height === "number" ? height + "px" : height; return "auto"; diff --git a/packages/web/src/components/common/form-input/FormInput.styles.ts b/packages/web/src/components/common/form-input/FormInput.styles.ts new file mode 100644 index 000000000..18f06ab23 --- /dev/null +++ b/packages/web/src/components/common/form-input/FormInput.styles.ts @@ -0,0 +1,37 @@ +import { fonts } from "@constants/font.constant"; +import styled from "@emotion/styled"; +import { media } from "@styles/media"; +import mixins from "@styles/mixins"; + +export const FormInputWrapper = styled.div` + ${mixins.flexbox("column", "flex-start", "flex-start")}; + width: 100%; + gap: 6px; + .error-text { + color: ${({ theme }) => theme.color.red02}; + ${fonts.p4} + ${media.mobile} { + ${fonts.p6} + } + } +`; + +export const FormInputStyle = styled.input` + width: 100%; + border-radius: 8px; + padding: 16px; + background-color: ${({ theme }) => theme.color.backgroundOpacity2}; + color: ${({ theme }) => theme.color.text01}; + border: 1px solid ${({ theme }) => theme.color.border02}; + ${fonts.body9} + ${media.mobile} { + ${fonts.body11} + } + &::placeholder { + color: ${({ theme }) => theme.color.text04}; + ${fonts.body9} + ${media.mobile} { + ${fonts.body11} + } + } +`; diff --git a/packages/web/src/components/common/form-input/FormInput.tsx b/packages/web/src/components/common/form-input/FormInput.tsx new file mode 100644 index 000000000..d5a8dbe33 --- /dev/null +++ b/packages/web/src/components/common/form-input/FormInput.tsx @@ -0,0 +1,30 @@ +import React, { FC, memo } from "react"; +import { FormInputStyle, FormInputWrapper } from "./FormInput.styles"; + +interface ParentProps { + className?: string; +} + +type Props = { + placeholder?: string; + name: string; + type?: string; + errorText?: any; + parentProps?: ParentProps; +}; + +const FormInput: FC = React.forwardRef( + (props, ref) => { + const { errorText, parentProps } = props; + return ( + + + {errorText &&
{errorText}
} +
+ ); + }, +); + +FormInput.displayName = "FormInput"; + +export default memo(FormInput); diff --git a/packages/web/src/components/common/form-textarea/FormTextArea.styles.ts b/packages/web/src/components/common/form-textarea/FormTextArea.styles.ts new file mode 100644 index 000000000..35c819f76 --- /dev/null +++ b/packages/web/src/components/common/form-textarea/FormTextArea.styles.ts @@ -0,0 +1,34 @@ +import { fonts } from "@constants/font.constant"; +import styled from "@emotion/styled"; +import { media } from "@styles/media"; +import mixins from "@styles/mixins"; + +export const FormTextAreaWrapper = styled.div` + ${mixins.flexbox("column", "flex-start", "flex-start")}; + width: 100%; + gap: 6px; + .error-text { + color: ${({ theme }) => theme.color.red02}; + ${fonts.p4} + ${media.mobile} { + ${fonts.p6} + } + } +`; + +export const FormTextAreaStyle = styled.textarea` + width: 100%; + border-radius: 8px; + padding: 16px; + background-color: ${({ theme }) => theme.color.backgroundOpacity2}; + color: ${({ theme }) => theme.color.text01}; + border: 1px solid ${({ theme }) => theme.color.border02}; + resize: none; + &::placeholder { + color: ${({ theme }) => theme.color.text04}; + ${fonts.body9} + ${media.mobile} { + ${fonts.body11} + } + } +`; diff --git a/packages/web/src/components/common/form-textarea/FormTextArea.tsx b/packages/web/src/components/common/form-textarea/FormTextArea.tsx new file mode 100644 index 000000000..71cecf1d3 --- /dev/null +++ b/packages/web/src/components/common/form-textarea/FormTextArea.tsx @@ -0,0 +1,26 @@ +import React, { FC, memo } from "react"; +import { FormTextAreaStyle, FormTextAreaWrapper } from "./FormTextArea.styles"; + +type Props = { + errorText?: any; + placeholder: string; + name: string; + rows: number; +}; + +const TextArea: FC = React.forwardRef( + (props, ref) => { + const { errorText } = props; + + return ( + + + {errorText &&
{errorText}
} +
+ ); + }, +); + +TextArea.displayName = "TextArea"; + +export default memo(TextArea); diff --git a/packages/web/src/components/common/form/FormProvider.tsx b/packages/web/src/components/common/form/FormProvider.tsx new file mode 100644 index 000000000..14be1141f --- /dev/null +++ b/packages/web/src/components/common/form/FormProvider.tsx @@ -0,0 +1,27 @@ +import React from "react"; +import { + FormProvider as HookFormProvider, + UseFormReturn, + SubmitHandler, + FieldValues, +} from "react-hook-form"; + +interface Props { + methods: UseFormReturn; + onSubmit: SubmitHandler; + children?: React.ReactNode; +} + +const FormProvider = ({ + methods, + onSubmit, + children, +}: Props) => { + return ( + +
{children}
+
+ ); +}; + +export default FormProvider; diff --git a/packages/web/src/components/common/icons/IconFile.tsx b/packages/web/src/components/common/icons/IconFile.tsx new file mode 100644 index 000000000..3d5209d55 --- /dev/null +++ b/packages/web/src/components/common/icons/IconFile.tsx @@ -0,0 +1,18 @@ +const IconFile = ({ className }: { className?: string }) => ( + + + + ); + + export default IconFile; + \ No newline at end of file diff --git a/packages/web/src/components/common/icons/IconNewTab.tsx b/packages/web/src/components/common/icons/IconNewTab.tsx new file mode 100644 index 000000000..ff85a75a1 --- /dev/null +++ b/packages/web/src/components/common/icons/IconNewTab.tsx @@ -0,0 +1,19 @@ +const IconNewTab = ({ className }: { className?: string }) => ( + + + +); + +export default IconNewTab; diff --git a/packages/web/src/components/common/icons/IconOutlineClock.tsx b/packages/web/src/components/common/icons/IconOutlineClock.tsx new file mode 100644 index 000000000..b36c4715e --- /dev/null +++ b/packages/web/src/components/common/icons/IconOutlineClock.tsx @@ -0,0 +1,19 @@ +const IconOutlineClock = ({ className }: { className?: string }) => ( + + + +); + +export default IconOutlineClock; diff --git a/packages/web/src/components/common/icons/IconPass.tsx b/packages/web/src/components/common/icons/IconPass.tsx new file mode 100644 index 000000000..718e1b06c --- /dev/null +++ b/packages/web/src/components/common/icons/IconPass.tsx @@ -0,0 +1,17 @@ +const IconPass = ({ className }: { className?: string }) => ( + + + +); + +export default IconPass; diff --git a/packages/web/src/components/common/icons/IconStrokeArrowLeft.tsx b/packages/web/src/components/common/icons/IconStrokeArrowLeft.tsx index 3de73e928..36b922510 100644 --- a/packages/web/src/components/common/icons/IconStrokeArrowLeft.tsx +++ b/packages/web/src/components/common/icons/IconStrokeArrowLeft.tsx @@ -1,11 +1,12 @@ -const IconStrokeArrowLeft = ({ className }: { className?: string }) => ( +import { SVGProps } from "react"; +const IconStrokeArrowLeft = (props: SVGProps) => ( ( +import { SVGProps } from "react"; +const IconStrokeArrowRight = (props: SVGProps) => ( = args => ( ); const token = { - path: "1", - name: "HEX", - symbol: "HEX", - logoURI: - "https://raw.githubusercontent.com/Uniswap/assets/master/blockchains/ethereum/assets/0x2b591e99afE9f32eAA6214f7B7629768c40Eeb39/logo.png", + "chainId": "test3", + "address": "0x111111111117dC0aa78b770fA6A738034120C302", + "path": "gno.land/r/demo/1inch", + "name": "1inch", + "symbol": "1INCH", + "decimals": 6, + "logoURI": "https://assets.coingecko.com/coins/images/13469/thumb/1inch-token.png?1608803028", + "priceId": "1inch", + "createdAt": "1999-01-01T00:00:01Z" }; export const Default = Template.bind({}); Default.args = { - token0Input: { + tokenAInput: { token: token, amount: "121", usdValue: "$0.00", balance: "0", changeAmount: action("changeAmount"), - changeBalance: action("changeBalance"), }, - token1Input: { + tokenBInput: { token: token, amount: "121", usdValue: "$0.00", balance: "0", changeAmount: action("changeAmount"), - changeBalance: action("changeBalance"), }, - changeToken0: action("changeToken0"), - changeToken1: action("changeToken1"), + changeTokenA: action("changeTokenA"), + changeTokenB: action("changeTokenB"), }; diff --git a/packages/web/src/components/common/liquidity-enter-amounts/LiquidityEnterAmounts.tsx b/packages/web/src/components/common/liquidity-enter-amounts/LiquidityEnterAmounts.tsx index ad42f7ad7..f04ccc1d8 100644 --- a/packages/web/src/components/common/liquidity-enter-amounts/LiquidityEnterAmounts.tsx +++ b/packages/web/src/components/common/liquidity-enter-amounts/LiquidityEnterAmounts.tsx @@ -3,26 +3,26 @@ import IconAdd from "../icons/IconAdd"; import { LiquidityEnterAmountsWrapper } from "./LiquidityEnterAmounts.styles"; import TokenAmountInput from "../token-amount-input/TokenAmountInput"; import { TokenAmountInputModel } from "@hooks/token/use-token-amount-input"; -import { TokenInfo } from "@models/token/token-info"; +import { TokenModel } from "@models/token/token-model"; interface LiquidityEnterAmountsProps { - token0Input: TokenAmountInputModel; - token1Input: TokenAmountInputModel; - changeToken0: (token: TokenInfo) => void; - changeToken1: (token: TokenInfo) => void; + tokenAInput: TokenAmountInputModel; + tokenBInput: TokenAmountInputModel; + changeTokenA: (token: TokenModel) => void; + changeTokenB: (token: TokenModel) => void; } const LiquidityEnterAmounts: React.FC = ({ - token0Input, - token1Input, - changeToken0, - changeToken1, + tokenAInput, + tokenBInput, + changeTokenA, + changeTokenB, }) => { return ( - - + +
diff --git a/packages/web/src/components/common/modal/Modal.styles.ts b/packages/web/src/components/common/modal/Modal.styles.ts index 21c13b91e..452b276f9 100644 --- a/packages/web/src/components/common/modal/Modal.styles.ts +++ b/packages/web/src/components/common/modal/Modal.styles.ts @@ -22,8 +22,8 @@ export const ModalWrapper = styled.div` if (height) return typeof height === "number" ? height + "px" : height; return "auto"; }}; - padding: 24px; ${mixins.positionCenter()}; + position: fixed; z-index: ${Z_INDEX.modal}; background-color: ${({ theme }) => theme.color.background06}; border: 1px solid ${({ theme }) => theme.color.border02}; diff --git a/packages/web/src/components/common/pool-graph/PoolGraph.stories.tsx b/packages/web/src/components/common/pool-graph/PoolGraph.stories.tsx index b4c82a3e7..10969c413 100644 --- a/packages/web/src/components/common/pool-graph/PoolGraph.stories.tsx +++ b/packages/web/src/components/common/pool-graph/PoolGraph.stories.tsx @@ -1,4 +1,3 @@ -import { DUMMY_POOL_TICKS } from "@containers/earn-add-liquidity-container/earn-add-liquidity-dummy"; import PoolGraph, { type PoolGraphProps } from "./PoolGraph"; import { Meta, StoryObj } from "@storybook/react"; import { action } from "@storybook/addon-actions"; @@ -10,12 +9,9 @@ export default { export const Default: StoryObj = { args: { - ticks: DUMMY_POOL_TICKS, - currentTick: DUMMY_POOL_TICKS[20], + ticks: [], width: 400, height: 200, - minTick: DUMMY_POOL_TICKS[10], - maxTick: DUMMY_POOL_TICKS[30], onChangeMinTick: action("onChangeMinTick"), onChangeMaxTick: action("onChangeMaxTick"), }, diff --git a/packages/web/src/components/common/select-fee-tier/SelectFeeTier.stories.tsx b/packages/web/src/components/common/select-fee-tier/SelectFeeTier.stories.tsx index 5f10a9fa9..f1193c354 100644 --- a/packages/web/src/components/common/select-fee-tier/SelectFeeTier.stories.tsx +++ b/packages/web/src/components/common/select-fee-tier/SelectFeeTier.stories.tsx @@ -2,7 +2,6 @@ import React from "react"; import { ComponentStory, ComponentMeta } from "@storybook/react"; import SelectFeeTier from "./SelectFeeTier"; import { action } from "@storybook/addon-actions"; -import { DUMMY_FEE_TIERS } from "@containers/earn-add-liquidity-container/earn-add-liquidity-dummy"; export default { title: "common/AddLiquidity/SelectFeeTier", @@ -15,7 +14,6 @@ const Template: ComponentStory = args => ( export const Default = Template.bind({}); Default.args = { - feeTiers: DUMMY_FEE_TIERS, - feeRate: "0.01", + feeTiers: [], selectFeeTier: action("selectFeeTier"), }; diff --git a/packages/web/src/components/common/select-fee-tier/SelectFeeTier.tsx b/packages/web/src/components/common/select-fee-tier/SelectFeeTier.tsx index 3a04f184b..3fc24dcf7 100644 --- a/packages/web/src/components/common/select-fee-tier/SelectFeeTier.tsx +++ b/packages/web/src/components/common/select-fee-tier/SelectFeeTier.tsx @@ -1,65 +1,76 @@ import React, { useCallback, useMemo } from "react"; import { SelectFeeTierItemWrapper, SelectFeeTierWrapper } from "./SelectFeeTier.styles"; -import { AddLiquidityFeeTier } from "@containers/earn-add-liquidity-container/EarnAddLiquidityContainer"; -import { FEE_RATE_OPTION } from "@constants/option.constant"; +import { SwapFeeTierInfoMap, SwapFeeTierType } from "@constants/option.constant"; +import { PoolModel } from "@models/pool/pool-model"; +import BigNumber from "bignumber.js"; interface SelectFeeTierProps { - feeTiers: AddLiquidityFeeTier[]; - feeRate: string | undefined; - selectFeeTier: (feeRate: FEE_RATE_OPTION) => void; + feeTiers: SwapFeeTierType[]; + feeTier: SwapFeeTierType | null; + pools: PoolModel[], + selectFeeTier: (feeTier: SwapFeeTierType) => void; } const SelectFeeTier: React.FC = ({ feeTiers, - feeRate, + feeTier, + pools, selectFeeTier, }) => { - const onClickFeeTierItem = useCallback((feeRate: string) => { - const feeRateOption = Object.values(FEE_RATE_OPTION).find(option => option === feeRate); - if (feeRateOption) { - selectFeeTier(feeRateOption); - } + const onClickFeeTierItem = useCallback((feeTier: SwapFeeTierType) => { + selectFeeTier(feeTier); }, [selectFeeTier]); return ( - {feeTiers.map((feeTier, index) => ( + {feeTiers.map((item, index) => ( onClickFeeTierItem(feeTier.feeRate)} + selected={feeTier === item} + feeTier={item} + pools={pools} + onClick={() => onClickFeeTierItem(item)} /> ))} ); }; -interface SelectFeeTierItemProps extends AddLiquidityFeeTier { +interface SelectFeeTierItemProps { selected: boolean; + feeTier: SwapFeeTierType; + pools: PoolModel[]; onClick: () => void; } const SelectFeeTierItem: React.FC = ({ selected, - feeRate, - description, - range, + feeTier, + pools, onClick, }) => { const feeRateStr = useMemo(() => { - return `${feeRate}%`; - }, [feeRate]); + return SwapFeeTierInfoMap[feeTier].rateStr; + }, [feeTier]); const rangeStr = useMemo(() => { - if (range === "0") { + const pool = pools.find(pool => pool.fee === feeTier); + if (!pool || pool.bins.length < 2) { return "Not created"; } - return `${range}% select`; - }, [range]); + const sortedBins = pool.bins.sort((p1, p2) => p1.currentTick - p2.currentTick); + const fullTickRange = 1774545; + const currentTickGap = sortedBins[0].currentTick - sortedBins[sortedBins.length - 1].currentTick; + return BigNumber(currentTickGap) + .dividedBy(fullTickRange) + .multipliedBy(100) + .toFixed(); + }, [feeTier, pools]); + + const description = useMemo(() => { + return SwapFeeTierInfoMap[feeTier].description; + }, [feeTier]); return ( diff --git a/packages/web/src/components/common/select-pair-button/SelectPairButton.stories.tsx b/packages/web/src/components/common/select-pair-button/SelectPairButton.stories.tsx index 8743ca9f0..661a03cd3 100644 --- a/packages/web/src/components/common/select-pair-button/SelectPairButton.stories.tsx +++ b/packages/web/src/components/common/select-pair-button/SelectPairButton.stories.tsx @@ -15,11 +15,15 @@ const Template: ComponentStory = args => ( export const Selected = Template.bind({}); Selected.args = { token: { - path: Math.floor(Math.random() * 50 + 1).toString(), - name: "HEX", - symbol: "HEX", - logoURI: - "https://raw.githubusercontent.com/Uniswap/assets/master/blockchains/ethereum/assets/0x2b591e99afE9f32eAA6214f7B7629768c40Eeb39/logo.png", + chainId: "test3", + address: "0x111111111117dC0aa78b770fA6A738034120C302", + path: "gno.land/r/demo/1inch", + name: "1inch", + symbol: "1INCH", + decimals: 6, + logoURI: "https://assets.coingecko.com/coins/images/13469/thumb/1inch-token.png?1608803028", + priceId: "1inch", + createdAt: "1999-01-01T00:00:01Z" }, }; diff --git a/packages/web/src/components/common/select-pair-button/SelectPairButton.tsx b/packages/web/src/components/common/select-pair-button/SelectPairButton.tsx index 6c4f06662..daad47884 100644 --- a/packages/web/src/components/common/select-pair-button/SelectPairButton.tsx +++ b/packages/web/src/components/common/select-pair-button/SelectPairButton.tsx @@ -1,12 +1,12 @@ import React, { useCallback } from "react"; import IconStrokeArrowDown from "@components/common/icons/IconStrokeArrowDown"; import { wrapper } from "./SelectPairButton.styles"; -import { TokenInfo } from "@models/token/token-info"; import { useSelectTokenModal } from "@hooks/token/use-select-token-modal"; +import { TokenModel } from "@models/token/token-model"; interface SelectPairButtonProps { - token?: TokenInfo; - changeToken?: (token: TokenInfo) => void; + token: TokenModel | null; + changeToken?: (token: TokenModel) => void; disabled?: boolean; } diff --git a/packages/web/src/components/common/select-pair/SelectPair.stories.tsx b/packages/web/src/components/common/select-pair/SelectPair.stories.tsx index 5ec79c943..731d697f3 100644 --- a/packages/web/src/components/common/select-pair/SelectPair.stories.tsx +++ b/packages/web/src/components/common/select-pair/SelectPair.stories.tsx @@ -13,17 +13,28 @@ const Template: ComponentStory = args => ( ); const tokenA = { - path: "0", - name: "HEX", - symbol: "HEX", - logoURI: "", + chainId: "test3", + address: "0x111111111117dC0aa78b770fA6A738034120C302", + path: "gno.land/r/demo/1inch", + name: "1inch", + symbol: "1INCH", + decimals: 6, + logoURI: "https://assets.coingecko.com/coins/images/13469/thumb/1inch-token.png?1608803028", + priceId: "1inch", + createdAt: "1999-01-01T00:00:01Z" }; const tokenB = { - path: "1", - name: "USDCoin", - symbol: "USDC", - logoURI: "", + name: "Wrapped Ether", + address: "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2", + path: "gno.land/r/demo/weth", + symbol: "WETH", + decimals: 6, + chainId: "test3", + priceId: "weth", + createdAt: "1999-01-01T00:00:02Z", + isWrappedGasToken: true, + logoURI: "https://s2.coinmarketcap.com/static/img/coins/64x64/2396.png" }; export const Default = Template.bind({}); diff --git a/packages/web/src/components/common/select-pair/SelectPair.tsx b/packages/web/src/components/common/select-pair/SelectPair.tsx index 544cda1f0..e1f940429 100644 --- a/packages/web/src/components/common/select-pair/SelectPair.tsx +++ b/packages/web/src/components/common/select-pair/SelectPair.tsx @@ -1,28 +1,28 @@ import React from "react"; import SelectPairButton from "@components/common/select-pair-button/SelectPairButton"; import { SelectPairWrapper } from "./SelectPair.styles"; -import { TokenInfo } from "@models/token/token-info"; +import { TokenModel } from "@models/token/token-model"; interface SelectPairProps { - tokenA: TokenInfo | undefined; - tokenB: TokenInfo | undefined; - changeToken0: (token: TokenInfo) => void; - changeToken1: (token: TokenInfo) => void; + tokenA: TokenModel | null; + tokenB: TokenModel | null; + changeTokenA: (token: TokenModel) => void; + changeTokenB: (token: TokenModel) => void; disabled?: boolean; } const SelectPair: React.FC = ({ tokenA, tokenB, - changeToken0, - changeToken1, + changeTokenA, + changeTokenB, disabled, }) => { return ( - - + + ); }; diff --git a/packages/web/src/components/common/select-price-range/SelectPriceRange.stories.tsx b/packages/web/src/components/common/select-price-range/SelectPriceRange.stories.tsx index 6a2ec1f53..9ae6d48ad 100644 --- a/packages/web/src/components/common/select-price-range/SelectPriceRange.stories.tsx +++ b/packages/web/src/components/common/select-price-range/SelectPriceRange.stories.tsx @@ -1,9 +1,7 @@ import React from "react"; import { ComponentStory, ComponentMeta } from "@storybook/react"; - import SelectPriceRange from "./SelectPriceRange"; import { action } from "@storybook/addon-actions"; -import { DUMMY_PRICE_RANGE_MAP } from "@containers/earn-add-liquidity-container/earn-add-liquidity-dummy"; export default { title: "common/AddLiquidity/SelectPriceRange", @@ -16,7 +14,7 @@ const Template: ComponentStory = args => ( export const Default = Template.bind({}); Default.args = { - priceRangeMap: DUMMY_PRICE_RANGE_MAP, + priceRanges: [], priceRange: undefined, - selectPriceRange: action("selectPriceRange"), + changePriceRange: action("changePriceRange"), }; diff --git a/packages/web/src/components/common/select-price-range/SelectPriceRange.tsx b/packages/web/src/components/common/select-price-range/SelectPriceRange.tsx index 194006299..08d4f94c2 100644 --- a/packages/web/src/components/common/select-price-range/SelectPriceRange.tsx +++ b/packages/web/src/components/common/select-price-range/SelectPriceRange.tsx @@ -1,4 +1,4 @@ -import { PriceRangeTooltip, PriceRangeType } from "@constants/option.constant"; +import { PriceRangeTooltip } from "@constants/option.constant"; import React, { useCallback, useMemo } from "react"; import IconInfo from "@components/common/icons/IconInfo"; import IconStrokeArrowRight from "@components/common/icons/IconStrokeArrowRight"; @@ -7,30 +7,27 @@ import { SelectPriceRangeItemWrapper, SelectPriceRangeWrapper } from "./SelectPr import { AddLiquidityPriceRage } from "@containers/earn-add-liquidity-container/EarnAddLiquidityContainer"; interface SelectPriceRangeProps { - priceRangeMap: { [key in PriceRangeType]: AddLiquidityPriceRage | undefined }; - priceRange: PriceRangeType | undefined; - selectPriceRange: (priceRange: PriceRangeType) => void; + priceRanges: AddLiquidityPriceRage[]; + priceRange: AddLiquidityPriceRage | null; + changePriceRange: (priceRange: AddLiquidityPriceRage) => void; } -const PRICE_RANGE_ORDERS: PriceRangeType[] = ["Active", "Passive", "Custom"]; - const SelectPriceRange: React.FC = ({ - priceRangeMap, + priceRanges, priceRange, - selectPriceRange, + changePriceRange, }) => { return (
- {PRICE_RANGE_ORDERS.map((priceRangeType, index: number) => ( + {priceRanges.map((item, index) => ( ))}
@@ -40,37 +37,36 @@ const SelectPriceRange: React.FC = ({ interface SelectPriceRangeItemProps { selected: boolean; - priceRage: PriceRangeType; + priceRange: AddLiquidityPriceRage; tooltip: string | undefined; - apr: string | undefined; - selectPriceRange: (priceRange: PriceRangeType) => void; + changePriceRange: (priceRange: AddLiquidityPriceRage) => void; } export const SelectPriceRangeItem: React.FC = ({ selected, - priceRage, + priceRange, tooltip, - apr, - selectPriceRange, + changePriceRange, }) => { const aprStr = useMemo(() => { + const apr = priceRange.apr; if (apr) { return `${apr}%`; } - if (priceRage === "Custom") { + if (priceRange.type === "Custom") { return null; } return "-"; - }, [apr, priceRage]); + }, [priceRange]); const onClickItem = useCallback(() => { - selectPriceRange(priceRage); - }, [priceRage, selectPriceRange]); + changePriceRange(priceRange); + }, [priceRange, changePriceRange]); return ( - {priceRage} + {priceRange.type} {tooltip && (
{ keyword: "", defaultTokens: [], tokens: [], - tokenPrices: [], + tokenPrices: {}, changeKeyword: () => { return; }, changeToken: () => { return; }, close: () => { return; }, diff --git a/packages/web/src/components/common/select-token/SelectToken.styles.ts b/packages/web/src/components/common/select-token/SelectToken.styles.ts index 656fd2be2..c0403a74b 100644 --- a/packages/web/src/components/common/select-token/SelectToken.styles.ts +++ b/packages/web/src/components/common/select-token/SelectToken.styles.ts @@ -8,9 +8,6 @@ export const SelectTokenWrapper = styled.div` width: 460px; padding: 24px 0px 16px 0px; gap: 24px; - border-radius: 8px; - box-shadow: 10px 14px 48px 0px rgba(0, 0, 0, 0.12); - border: 1px solid ${({ theme }) => theme.color.border02}; background-color: ${({ theme }) => theme.color.background06}; ${media.mobile} { width: 328px; diff --git a/packages/web/src/components/common/select-token/SelectToken.tsx b/packages/web/src/components/common/select-token/SelectToken.tsx index 9139c8bed..8f66697d1 100644 --- a/packages/web/src/components/common/select-token/SelectToken.tsx +++ b/packages/web/src/components/common/select-token/SelectToken.tsx @@ -27,7 +27,7 @@ const SelectToken: React.FC = ({ const getTokenPrice = useCallback((token: TokenModel) => { const tokenPrice = tokenPrices[token.priceId]; - if (!tokenPrice) { + if (tokenPrice === null || Number.isNaN(tokenPrice)) { return "-"; } return BigNumber(tokenPrice).toFormat(); @@ -59,7 +59,7 @@ const SelectToken: React.FC = ({
diff --git a/packages/web/src/components/common/token-amount-input/TokenAmountInput.spec.tsx b/packages/web/src/components/common/token-amount-input/TokenAmountInput.spec.tsx index af0b1e9dc..d02b67481 100644 --- a/packages/web/src/components/common/token-amount-input/TokenAmountInput.spec.tsx +++ b/packages/web/src/components/common/token-amount-input/TokenAmountInput.spec.tsx @@ -4,10 +4,15 @@ import GnoswapThemeProvider from "@providers/gnoswap-theme-provider/GnoswapTheme import TokenAmountInput, { TokenAmountInputProps } from "./TokenAmountInput"; const token = { - path: "1", - name: "Gnoland", - symbol: "GNO.LAND", - logoURI: "https://raw.githubusercontent.com/Uniswap/assets/master/blockchains/ethereum/assets/0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2/logo.png", + chainId: "dev", + createdAt: "2023-10-10T08:48:46+09:00", + name: "Gnoswap", + address: "g1sqaft388ruvsseu97r04w4rr4szxkh4nn6xpax", + path: "gno.land/r/gnos", + decimals: 4, + symbol: "GNOS", + logoURI: "https://s2.coinmarketcap.com/static/img/coins/64x64/5994.png", + priceId: "gno.land/r/gnos" }; describe("TokenAmountInput Component", () => { @@ -18,7 +23,6 @@ describe("TokenAmountInput Component", () => { balance: "12,211", usdValue: "12.3", changable: true, - changeBalance: () => { return; }, changeAmount: () => { return; }, changeToken: () => { return; }, }; diff --git a/packages/web/src/components/common/token-amount-input/TokenAmountInput.stories.tsx b/packages/web/src/components/common/token-amount-input/TokenAmountInput.stories.tsx index e16b0e090..e230e47e8 100644 --- a/packages/web/src/components/common/token-amount-input/TokenAmountInput.stories.tsx +++ b/packages/web/src/components/common/token-amount-input/TokenAmountInput.stories.tsx @@ -8,10 +8,15 @@ export default { } as Meta; const token = { - path: "1", - name: "Gnoland", - symbol: "GNO.LAND", - logoURI: "https://raw.githubusercontent.com/Uniswap/assets/master/blockchains/ethereum/assets/0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2/logo.png", + chainId: "dev", + createdAt: "2023-10-10T08:48:46+09:00", + name: "Gnoswap", + address: "g1sqaft388ruvsseu97r04w4rr4szxkh4nn6xpax", + path: "gno.land/r/gnos", + decimals: 4, + symbol: "GNOS", + logoURI: "https://s2.coinmarketcap.com/static/img/coins/64x64/5994.png", + priceId: "gno.land/r/gnos" }; export const Default: StoryObj = { @@ -21,7 +26,6 @@ export const Default: StoryObj = { balance: "12,211", usdValue: "12.3", changable: true, - changeBalance: action("changeBalance"), changeAmount: action("changeAmount"), changeToken: action("changeToken"), }, 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 58d0e19bd..e43e17947 100644 --- a/packages/web/src/components/common/token-amount-input/TokenAmountInput.tsx +++ b/packages/web/src/components/common/token-amount-input/TokenAmountInput.tsx @@ -3,11 +3,11 @@ import { TokenAmountInputWrapper } from "./TokenAmountInput.styles"; import SelectPairButton from "../select-pair-button/SelectPairButton"; import BigNumber from "bignumber.js"; import { TokenAmountInputModel } from "@hooks/token/use-token-amount-input"; -import { TokenInfo } from "@models/token/token-info"; +import { TokenModel } from "@models/token/token-model"; export interface TokenAmountInputProps extends TokenAmountInputModel { changable?: boolean; - changeToken: (token: TokenInfo) => void; + changeToken: (token: TokenModel) => void; } const TokenAmountInput: React.FC = ({ @@ -37,7 +37,7 @@ const TokenAmountInput: React.FC = ({ type="number" value={amount} onChange={onChangeAmountInput} - placeholder={amount} + placeholder="0" />
{ diff --git a/packages/web/src/components/common/token-amount/TokenAmount.stories.tsx b/packages/web/src/components/common/token-amount/TokenAmount.stories.tsx index 9224b6870..88c16fc8e 100644 --- a/packages/web/src/components/common/token-amount/TokenAmount.stories.tsx +++ b/packages/web/src/components/common/token-amount/TokenAmount.stories.tsx @@ -7,10 +7,15 @@ export default { } as Meta; const token = { - path: "1", - name: "Gnoland", - symbol: "GNO.LAND", - logoURI: "", + chainId: "dev", + createdAt: "2023-10-10T08:48:46+09:00", + name: "Gnoswap", + address: "g1sqaft388ruvsseu97r04w4rr4szxkh4nn6xpax", + path: "gno.land/r/gnos", + decimals: 4, + symbol: "GNOS", + logoURI: "https://s2.coinmarketcap.com/static/img/coins/64x64/5994.png", + priceId: "gno.land/r/gnos" }; export const Default: StoryObj = { diff --git a/packages/web/src/components/common/token-amount/TokenAmount.tsx b/packages/web/src/components/common/token-amount/TokenAmount.tsx index 51b0d2fbd..5e85a8402 100644 --- a/packages/web/src/components/common/token-amount/TokenAmount.tsx +++ b/packages/web/src/components/common/token-amount/TokenAmount.tsx @@ -1,10 +1,10 @@ import React from "react"; import { TokenAmountWrapper } from "./TokenAmount.styles"; import SelectPairButton from "../select-pair-button/SelectPairButton"; -import { TokenInfo } from "@models/token/token-info"; +import { TokenModel } from "@models/token/token-model"; export interface TokenAmountProps { - token: TokenInfo; + token: TokenModel; usdPrice: string; amount: string; } diff --git a/packages/web/src/components/common/tooltip/FloatingTooltip.tsx b/packages/web/src/components/common/tooltip/FloatingTooltip.tsx new file mode 100644 index 000000000..2ccd6d9c7 --- /dev/null +++ b/packages/web/src/components/common/tooltip/FloatingTooltip.tsx @@ -0,0 +1,94 @@ +import { FloatingPortal, FloatingArrow } from "@floating-ui/react"; + +import type { ElementRef } from "react"; +import React, { cloneElement, forwardRef } from "react"; + +import { useMergedRef } from "@hooks/common/use-merged-ref"; +import { + FloatingPosition, + useFloatingTooltip, +} from "@hooks/common/use-floating-tooltip"; +import { FloatContent } from "./Tooltip.styles"; +import { Z_INDEX } from "@styles/zIndex"; +import { useTheme } from "@emotion/react"; + +interface TooltipProps { + offset?: number; + position: FloatingPosition; + content: React.ReactNode; + className?: string; + children?: any; +} + +const FloatingTooltip = forwardRef, TooltipProps>( + ({ children, content, className }, ref) => { + const { + handleMouseMove, + x, + y, + arrowRef, + context, + opened, + boundaryRef, + strategy, + floating, + setOpened, + } = useFloatingTooltip({ + offset: 20, + position: "top", + }); + const theme = useTheme(); + + const targetRef = useMergedRef(boundaryRef, (children as any).ref, ref); + + const onMouseEnter = (event: React.MouseEvent) => { + children.props.onMouseEnter?.(event); + handleMouseMove(event); + setOpened(true); + }; + + const onMouseLeave = (event: React.MouseEvent) => { + children.props.onMouseLeave?.(event); + setOpened(false); + }; + + return ( + <> + {cloneElement(children, { + ...children.props, + ref: targetRef, + onMouseEnter, + onMouseLeave, + })} + + +
+ + {content} +
+
+ + ); + }, +); + +FloatingTooltip.displayName = "FloatingTooltip"; + +export default FloatingTooltip; diff --git a/packages/web/src/components/common/tooltip/Tooltip.styles.ts b/packages/web/src/components/common/tooltip/Tooltip.styles.ts index 2a058b28f..d2a5d3ea4 100644 --- a/packages/web/src/components/common/tooltip/Tooltip.styles.ts +++ b/packages/web/src/components/common/tooltip/Tooltip.styles.ts @@ -1,4 +1,5 @@ import styled from "@emotion/styled"; +import { media } from "@styles/media"; export const Content = styled.div` color: ${({ theme }) => theme.color.text15}; @@ -8,4 +9,24 @@ export const Content = styled.div` box-sizing: border-box; width: max-content; max-width: calc(100vw - 10px); + box-shadow: 8px 8px 20px rgba(0, 0, 0, 0.2); + ${media.mobile} { + padding: 12px; + gap: 4px; + } +`; + +export const FloatContent = styled.div` + color: ${({ theme }) => theme.color.text02}; + background-color: ${({ theme }) => theme.color.background02}; + padding: 16px; + border-radius: 8px; + box-sizing: border-box; + width: max-content; + max-width: calc(100vw - 10px); + box-shadow: 8px 8px 20px rgba(0, 0, 0, 0.2); + ${media.mobile} { + padding: 12px; + gap: 4px; + } `; diff --git a/packages/web/src/components/common/tooltip/Tooltip.tsx b/packages/web/src/components/common/tooltip/Tooltip.tsx index 20c9c43b5..b2e195e28 100644 --- a/packages/web/src/components/common/tooltip/Tooltip.tsx +++ b/packages/web/src/components/common/tooltip/Tooltip.tsx @@ -70,6 +70,7 @@ interface TooltipProps { placement: Placement; FloatingContent: React.ReactNode; width?: CSSProperties["width"]; + floatClassName?: string; } const Tooltip: React.FC> = ({ @@ -77,6 +78,7 @@ const Tooltip: React.FC> = ({ placement, FloatingContent, width, + floatClassName, }) => { const { open, refs, strategy, x, y, context, arrowRef } = useTooltip({ placement, @@ -109,6 +111,7 @@ const Tooltip: React.FC> = ({ visibility: x == null ? "hidden" : "visible", zIndex: Z_INDEX.modalTooltip, }} + className={floatClassName} > ; - -export const Default: StoryObj = { - args: { - tokenA: token, - tokenB: token, - feeRate: "0.30%" - }, -}; \ No newline at end of file diff --git a/packages/web/src/components/earn-add/earn-add-confirm-amount-info/EarnAddConfirmAmountInfo.tsx b/packages/web/src/components/earn-add/earn-add-confirm-amount-info/EarnAddConfirmAmountInfo.tsx index 9b4b425d9..768bc6091 100644 --- a/packages/web/src/components/earn-add/earn-add-confirm-amount-info/EarnAddConfirmAmountInfo.tsx +++ b/packages/web/src/components/earn-add/earn-add-confirm-amount-info/EarnAddConfirmAmountInfo.tsx @@ -1,17 +1,17 @@ import React from "react"; import { EarnAddConfirmAmountInfoWrapper, EarnAddConfirmFeeInfoSection } from "./EarnAddConfirmAmountInfo.styles"; -import { TokenInfo } from "@models/token/token-info"; import TokenAmount from "@components/common/token-amount/TokenAmount"; import IconAdd from "@components/common/icons/IconAdd"; +import { TokenModel } from "@models/token/token-model"; export interface EarnAddConfirmAmountInfoProps { tokenA: { - info: TokenInfo; + info: TokenModel; amount: string; usdPrice: string; }; tokenB: { - info: TokenInfo; + info: TokenModel; amount: string; usdPrice: string; }; diff --git a/packages/web/src/components/earn-add/earn-add-confirm/EarnAddConfirm.stories.tsx b/packages/web/src/components/earn-add/earn-add-confirm/EarnAddConfirm.stories.tsx index 38d3317e7..23ef5753c 100644 --- a/packages/web/src/components/earn-add/earn-add-confirm/EarnAddConfirm.stories.tsx +++ b/packages/web/src/components/earn-add/earn-add-confirm/EarnAddConfirm.stories.tsx @@ -7,12 +7,18 @@ export default { component: EarnAddConfirm, } as Meta; + const tokenA = { info: { - path: "1", - name: "Gnoland", - symbol: "GNOT", - logoURI: "", + chainId: "test3", + address: "0x111111111117dC0aa78b770fA6A738034120C302", + path: "gno.land/r/demo/1inch", + name: "1inch", + symbol: "1INCH", + decimals: 6, + logoURI: "https://assets.coingecko.com/coins/images/13469/thumb/1inch-token.png?1608803028", + priceId: "1inch", + createdAt: "1999-01-01T00:00:01Z" }, amount: "12,211", usdPrice: "$12.3", @@ -20,10 +26,15 @@ const tokenA = { const tokenB = { info: { - path: "2", - name: "Ether", - symbol: "ETH", - logoURI: "", + chainId: "test3", + address: "0x111111111117dC0aa78b770fA6A738034120C302", + path: "gno.land/r/demo/1inch", + name: "1inch", + symbol: "1INCH", + decimals: 6, + logoURI: "https://assets.coingecko.com/coins/images/13469/thumb/1inch-token.png?1608803028", + priceId: "1inch", + createdAt: "1999-01-01T00:00:01Z" }, amount: "12,211", usdPrice: "$12.3", @@ -31,7 +42,11 @@ const tokenB = { const amountInfo = { tokenA: tokenA, + tokenAAmount: "123", + tokenAUSDPrice: "1234", tokenB: tokenB, + tokenBAmount: "123", + tokenBUSDPrice: "1234", feeRate: "0.30%" }; diff --git a/packages/web/src/components/earn-add/earn-add-confirm/EarnAddConfirm.styles.ts b/packages/web/src/components/earn-add/earn-add-confirm/EarnAddConfirm.styles.ts index 1dccc99db..707a8afa7 100644 --- a/packages/web/src/components/earn-add/earn-add-confirm/EarnAddConfirm.styles.ts +++ b/packages/web/src/components/earn-add/earn-add-confirm/EarnAddConfirm.styles.ts @@ -4,14 +4,10 @@ import styled from "@emotion/styled"; export const EarnAddConfirmWrapper = styled.div` display: flex; flex-direction: column; - width: 100%; - height: auto; + width: 460px; padding: 24px; + height: auto; gap: 16px; - border-radius: 8px; - border: 1px solid ${({ theme }) => theme.color.border02}; - background-color: ${({ theme }) => theme.color.background06}; - box-shadow: 10px 14px 60px 0px rgba(0, 0, 0, 0.40); .confirm-header { display: flex; diff --git a/packages/web/src/components/earn-add/earn-add-confirm/EarnAddConfirm.tsx b/packages/web/src/components/earn-add/earn-add-confirm/EarnAddConfirm.tsx index 2151292db..bad3ed9b9 100644 --- a/packages/web/src/components/earn-add/earn-add-confirm/EarnAddConfirm.tsx +++ b/packages/web/src/components/earn-add/earn-add-confirm/EarnAddConfirm.tsx @@ -2,20 +2,20 @@ import React from "react"; import { EarnAddConfirmWrapper } from "./EarnAddConfirm.styles"; import Button, { ButtonHierarchy } from "@components/common/button/Button"; import IconClose from "@components/common/icons/IconCancel"; -import { TokenInfo } from "@models/token/token-info"; import EarnAddConfirmAmountInfo from "../earn-add-confirm-amount-info/EarnAddConfirmAmountInfo"; import EarnAddConfirmPriceRangeInfo from "../earn-add-confirm-price-range-info/EarnAddConfirmPriceRangeInfo"; import EarnAddConfirmFeeInfo from "../earn-add-confirm-fee-info/EarnAddConfirmFeeInfo"; +import { TokenModel } from "@models/token/token-model"; export interface EarnAddConfirmProps { amountInfo: { tokenA: { - info: TokenInfo; + info: TokenModel; amount: string; usdPrice: string; }; tokenB: { - info: TokenInfo; + info: TokenModel; amount: string; usdPrice: string; }; @@ -31,7 +31,7 @@ export interface EarnAddConfirmProps { estimatedAPR: string; }; feeInfo: { - token: TokenInfo; + token: TokenModel; fee: string; }; confirm: () => void; diff --git a/packages/web/src/components/earn-add/earn-add-liquidity/EarnAddLiquidity.stories.tsx b/packages/web/src/components/earn-add/earn-add-liquidity/EarnAddLiquidity.stories.tsx index b6c95f792..94aa4d369 100644 --- a/packages/web/src/components/earn-add/earn-add-liquidity/EarnAddLiquidity.stories.tsx +++ b/packages/web/src/components/earn-add/earn-add-liquidity/EarnAddLiquidity.stories.tsx @@ -3,7 +3,6 @@ import { ComponentStory, ComponentMeta } from "@storybook/react"; import EarnAddLiquidity from "./EarnAddLiquidity"; import { action } from "@storybook/addon-actions"; -import { DUMMY_FEE_TIERS, DUMMY_POOL_TICKS, DUMMY_PRICE_RANGE_MAP } from "@containers/earn-add-liquidity-container/earn-add-liquidity-dummy"; export default { title: "earn-add/EarnAddLiquidity", @@ -14,16 +13,26 @@ const Template: ComponentStory = args => ( ); const tokenA = { - path: "1", - logoURI: "", - name: "Bitcoin", - symbol: "BTC", + chainId: "dev", + createdAt: "2023-10-10T08:48:46+09:00", + name: "Gnoswap", + address: "g1sqaft388ruvsseu97r04w4rr4szxkh4nn6xpax", + path: "gno.land/r/gnos", + decimals: 4, + symbol: "GNOS", + logoURI: "https://s2.coinmarketcap.com/static/img/coins/64x64/5994.png", + priceId: "gno.land/r/gnos" }; const tokenB = { - path: "2", - logoURI: "", - name: "Ethereum", - symbol: "ETH", + chainId: "dev", + createdAt: "2023-10-10T08:48:46+09:00", + name: "Gnoswap", + address: "g1sqaft388ruvsseu97r04w4rr4szxkh4nn6xpax", + path: "gno.land/r/gnos", + decimals: 4, + symbol: "GNOS", + logoURI: "https://s2.coinmarketcap.com/static/img/coins/64x64/5994.png", + priceId: "gno.land/r/gnos" }; export const Default = Template.bind({}); @@ -31,12 +40,9 @@ Default.args = { mode: "POOL", tokenA: tokenA, tokenB: tokenB, - feeTiers: DUMMY_FEE_TIERS, - feeRate: "0.01", + feeTiers: [], selectFeeTier: action("selectFeeTier"), - priceRangeMap: DUMMY_PRICE_RANGE_MAP, - priceRange: "Custom", - selectPriceRange: action("selectPriceRange"), - ticks: DUMMY_POOL_TICKS, - currentTick: DUMMY_POOL_TICKS[20], + priceRanges: [], + changePriceRange: action("selectPriceRange"), + ticks: [], }; diff --git a/packages/web/src/components/earn-add/earn-add-liquidity/EarnAddLiquidity.tsx b/packages/web/src/components/earn-add/earn-add-liquidity/EarnAddLiquidity.tsx index 05870b31d..9d96d02a5 100644 --- a/packages/web/src/components/earn-add/earn-add-liquidity/EarnAddLiquidity.tsx +++ b/packages/web/src/components/earn-add/earn-add-liquidity/EarnAddLiquidity.tsx @@ -1,96 +1,90 @@ import Button, { ButtonHierarchy } from "@components/common/button/Button"; import SelectFeeTier from "@components/common/select-fee-tier/SelectFeeTier"; -import SelectPriceRange from "@components/common/select-price-range/SelectPriceRange"; import React, { useCallback, useMemo, useState } from "react"; import { EarnAddLiquidityWrapper } from "./EarnAddLiquidity.styles"; -import { AddLiquidityType, FEE_RATE_OPTION, PriceRangeType } from "@constants/option.constant"; -import { AddLiquidityFeeTier, AddLiquidityPriceRage, PoolTick, PriceRangeSummary } from "@containers/earn-add-liquidity-container/EarnAddLiquidityContainer"; +import { AddLiquidityType, SwapFeeTierType, SwapFeeTierInfoMap, AddLiquiditySubmitType } from "@constants/option.constant"; +import { AddLiquidityPriceRage, PoolTick, PriceRangeSummary } from "@containers/earn-add-liquidity-container/EarnAddLiquidityContainer"; import LiquidityEnterAmounts from "@components/common/liquidity-enter-amounts/LiquidityEnterAmounts"; -import { TokenInfo } from "@models/token/token-info"; import SelectPair from "@components/common/select-pair/SelectPair"; import { TokenAmountInputModel } from "@hooks/token/use-token-amount-input"; import DoubleLogo from "@components/common/double-logo/DoubleLogo"; import IconSettings from "@components/common/icons/IconSettings"; import Badge, { BADGE_TYPE } from "@components/common/badge/Badge"; -import SelectPriceRangeCustom from "@components/common/select-price-range-custom/SelectPriceRangeCustom"; +import { PoolModel } from "@models/pool/pool-model"; +import SelectPriceRange from "@components/common/select-price-range/SelectPriceRange"; import SelectPriceRangeSummary from "@components/common/select-price-range-summary/SelectPriceRangeSummary"; +import { TokenModel } from "@models/token/token-model"; interface EarnAddLiquidityProps { mode: AddLiquidityType; - tokenA: TokenInfo | undefined; - tokenB: TokenInfo | undefined; - changeToken0: (token: TokenInfo) => void; - changeToken1: (token: TokenInfo) => void; - token0Input: TokenAmountInputModel; - token1Input: TokenAmountInputModel; - feeTiers: AddLiquidityFeeTier[]; - feeRate: FEE_RATE_OPTION | undefined; - selectFeeTier: (feeRate: FEE_RATE_OPTION) => void; - priceRangeMap: { [key in PriceRangeType]: AddLiquidityPriceRage | undefined }; - priceRange: PriceRangeType | undefined; + tokenA: TokenModel | null; + tokenB: TokenModel | null; + changeTokenA: (token: TokenModel) => void; + changeTokenB: (token: TokenModel) => void; + tokenAInput: TokenAmountInputModel; + tokenBInput: TokenAmountInputModel; + feeTiers: SwapFeeTierType[]; + feeTier: SwapFeeTierType | null; + pools: PoolModel[]; + selectFeeTier: (feeRate: SwapFeeTierType) => void; + priceRanges: AddLiquidityPriceRage[]; + priceRange: AddLiquidityPriceRage | null; + changePriceRange: (priceRange: AddLiquidityPriceRage) => void; priceRangeSummary: PriceRangeSummary; - selectPriceRange: (priceRange: PriceRangeType) => void; ticks: PoolTick[]; - currentTick?: PoolTick; + currentTick: PoolTick | null; + submitType: AddLiquiditySubmitType; + submit: () => void; } const EarnAddLiquidity: React.FC = ({ tokenA, tokenB, - changeToken0, - changeToken1, - token0Input, - token1Input, + changeTokenA, + changeTokenB, + tokenAInput, + tokenBInput, feeTiers, - feeRate, + feeTier, + pools, selectFeeTier, - priceRangeMap, + priceRanges, priceRange, priceRangeSummary, - selectPriceRange, - ticks, - currentTick, + changePriceRange, + submitType, + submit, }) => { const [openedSelectPair, setOpenedSelectPair] = useState(true); const [openedFeeTier, setOpenedFeeTier] = useState(true); const [openedPriceRange, setOpenedPriceRange] = useState(true); const existTokenPair = useMemo(() => { - return tokenA !== undefined && tokenB !== undefined; + return tokenA !== null && tokenB !== null; }, [tokenA, tokenB]); - const token0Logo = useMemo(() => { + const tokenALogo = useMemo(() => { return tokenA?.logoURI || ""; }, [tokenA]); - const token1Logo = useMemo(() => { + const tokenBLogo = useMemo(() => { return tokenB?.logoURI || ""; }, [tokenB]); const selectedFeeRate = useMemo(() => { - if (!feeRate) { + if (!feeTier) { return null; } - return `${feeRate}%`; - }, [feeRate]); + return SwapFeeTierInfoMap[feeTier].rateStr; + }, [feeTier]); const selectedPriceRange = useMemo(() => { if (!priceRange) { return null; } - return `${priceRange}`; + return `${priceRange.type}`; }, [priceRange]); - const selectableCustomPriceRange = useMemo(() => { - if (priceRange !== "Custom") { - return false; - } - if (!tokenA || !tokenB) { - return false; - } - return true; - }, [priceRange, tokenA, tokenB]); - const toggleSelectPair = useCallback(() => { setOpenedSelectPair(!openedSelectPair); }, [openedSelectPair]); @@ -103,6 +97,37 @@ const EarnAddLiquidity: React.FC = ({ setOpenedPriceRange(!openedPriceRange); }, [openedPriceRange]); + const activatedSubmit = useMemo(() => { + switch (submitType) { + case "CREATE_POOL": + case "ADD_LIQUIDITY": + case "CONNECT_WALLET": + return true; + default: + return false; + } + }, [submitType]); + + const submitButtonStr = useMemo(() => { + switch (submitType) { + case "CREATE_POOL": + return "Create Pool"; + case "ADD_LIQUIDITY": + return "Add Liquidity"; + case "CONNECT_WALLET": + return "Connect Wallet"; + case "INVALID_PAIR": + return "Invalid Pair"; + case "INSUFFICIENT_BALANCE": + return "Insufficient Balance"; + case "INVALID_RANGE": + return "Invalid Range"; + case "ENTER_AMOUNT": + default: + return "Enter Amount"; + } + }, [submitType]); + return (

Add Liquidity

@@ -112,8 +137,8 @@ const EarnAddLiquidity: React.FC = ({
1. Select Pair
{existTokenPair && ( )} @@ -122,8 +147,8 @@ const EarnAddLiquidity: React.FC = ({ )} @@ -142,7 +167,8 @@ const EarnAddLiquidity: React.FC = ({ {openedFeeTier && ( )} @@ -161,18 +187,9 @@ const EarnAddLiquidity: React.FC = ({ {openedPriceRange && ( - )} - - {selectableCustomPriceRange && ( - )} @@ -187,18 +204,18 @@ const EarnAddLiquidity: React.FC = ({
@@ -50,15 +57,15 @@ const IncentivizedPoolCard: React.FC = ({ {POOL_CONTENT_TITLE.FEE}
- {item.volume24h} - {item.fees24h} + {pool.volume24h} + {pool.fees24h}
- {`1 ${item.name[0]}`} + {`1 ${pool.tokenA.symbol}`} - {`${item.currentTick} ${item.name[1]}`} + {`${pool.tickInfo.currentTick} ${pool.tokenB.symbol}`}
= [ - { - logo: [ - "https://picsum.photos/id/313/36/36", - "https://picsum.photos/id/218/36/36", - ], - name: ["GNOS", "GNOT"], - fee: "0.5% Fee", - liquidity: "$524.98M", - apr: "108.12%", - volume24h: "$200.12M", - fees24h: "$10.50k", - currentTick: "1.1", - }, - { - logo: [ - "https://picsum.photos/id/13/36/36", - "https://picsum.photos/id/28/36/36", - ], - name: ["GNOS", "GNOT"], - fee: "0.5% Fee", - liquidity: "$524.98M", - apr: "108.12%", - volume24h: "$200.12M", - fees24h: "$10.50k", - currentTick: "1.1", - }, - { - logo: [ - "https://picsum.photos/id/158/36/36", - "https://picsum.photos/id/268/36/36", - ], - name: ["GNOS", "GNOT"], - fee: "0.5% Fee", - liquidity: "$524.98M", - apr: "108.12%", - volume24h: "$200.12M", - fees24h: "$10.50k", - currentTick: "1.1", - }, - { - logo: [ - "https://picsum.photos/id/103/36/36", - "https://picsum.photos/id/2/36/36", - ], - name: ["GNOS", "GNOT"], - fee: "0.5% Fee", - liquidity: "$524.98M", - apr: "108.12%", - volume24h: "$200.12M", - fees24h: "$10.50k", - currentTick: "1.1", - }, - { - logo: [ - "https://picsum.photos/id/3/36/36", - "https://picsum.photos/id/8/36/36", - ], - name: ["GNOS", "GNOT"], - fee: "0.5% Fee", - liquidity: "$524.98M", - apr: "108.12%", - volume24h: "$200.12M", - fees24h: "$10.50k", - currentTick: "1.1", - }, - { - logo: [ - "https://picsum.photos/id/19/36/36", - "https://picsum.photos/id/58/36/36", - ], - name: ["GNOS", "GNOT"], - fee: "0.5% Fee", - liquidity: "$524.98M", - apr: "108.12%", - volume24h: "$200.12M", - fees24h: "$10.50k", - currentTick: "1.1", - }, - { - logo: [ - "https://picsum.photos/id/178/36/36", - "https://picsum.photos/id/278/36/36", - ], - name: ["GNOS", "GNOT"], - fee: "0.5% Fee", - liquidity: "$524.98M", - apr: "108.12%", - volume24h: "$200.12M", - fees24h: "$10.50k", - currentTick: "1.1", - }, - { - logo: [ - "https://picsum.photos/id/139/36/36", - "https://picsum.photos/id/63/36/36", - ], - name: ["GNOS", "GNOT"], - fee: "0.5% Fee", - liquidity: "$524.98M", - apr: "108.12%", - volume24h: "$200.12M", - fees24h: "$10.50k", - currentTick: "1.1", - }, -]; diff --git a/packages/web/src/components/earn/pool-info/PoolInfo.stories.tsx b/packages/web/src/components/earn/pool-info/PoolInfo.stories.tsx index aa40c509b..f8a273a27 100644 --- a/packages/web/src/components/earn/pool-info/PoolInfo.stories.tsx +++ b/packages/web/src/components/earn/pool-info/PoolInfo.stories.tsx @@ -3,8 +3,60 @@ import { ComponentStory, ComponentMeta } from "@storybook/react"; import PoolInfo from "./PoolInfo"; import { css, Theme } from "@emotion/react"; -import { dummyPoolList } from "@containers/pool-list-container/PoolListContainer"; import { action } from "@storybook/addon-actions"; +import { PoolCardInfo } from "@models/pool/info/pool-card-info"; + +const pool: PoolCardInfo = { + poolId: "bar_foo_500", + tokenA: { + chainId: "dev", + createdAt: "2023-10-12T06:56:10+09:00", + name: "Bar", + address: "g1w8wqgrp08cqhtupzx98n4jtm8kqy7vadfmmyd0", + path: "gno.land/r/bar", + decimals: 4, + symbol: "BAR", + logoURI: "https://s2.coinmarketcap.com/static/img/coins/64x64/5994.png", + priceId: "gno.land/r/bar" + }, + tokenB: { + chainId: "dev", + createdAt: "2023-10-12T06:56:08+09:00", + name: "Foo", + address: "g1evezrh92xaucffmtgsaa3rvmz5s8kedffsg469", + path: "gno.land/r/foo", + decimals: 4, + symbol: "FOO", + logoURI: "https://s2.coinmarketcap.com/static/img/coins/64x64/5994.png", + priceId: "gno.land/r/foo" + }, + feeTier: "FEE_500", + liquidity: "$1,182,797", + apr: "0.12%", + volume24h: "$1,182,797", + fees24h: "$131.937491", + rewards: [ + { + token: { + chainId: "dev", + createdAt: "2023-10-12T06:56:12+09:00", + name: "Gnoswap", + address: "g1sqaft388ruvsseu97r04w4rr4szxkh4nn6xpax", + path: "gno.land/r/gnos", + decimals: 4, + symbol: "GNOS", + logoURI: "/gnos.svg", + priceId: "gno.land/r/gnos" + }, + amount: 10 + } + ], + incentiveType: "Incentivized", + tickInfo: { + currentTick: 1.498590, + ticks: [] + } +}; export default { title: "earn/PoolList/PoolInfo", @@ -13,7 +65,7 @@ export default { const Template: ComponentStory = args => (
- +
); diff --git a/packages/web/src/components/earn/pool-info/PoolInfo.tsx b/packages/web/src/components/earn/pool-info/PoolInfo.tsx index 5ba832b6c..2820ab288 100644 --- a/packages/web/src/components/earn/pool-info/PoolInfo.tsx +++ b/packages/web/src/components/earn/pool-info/PoolInfo.tsx @@ -3,39 +3,51 @@ import BarGraph from "@components/common/bar-graph/BarGraph"; import DoubleLogo from "@components/common/double-logo/DoubleLogo"; import { POOL_TD_WIDTH } from "@constants/skeleton.constant"; -import { type Pool } from "@containers/pool-list-container/PoolListContainer"; -import React from "react"; +import React, { useMemo } from "react"; import { PoolInfoWrapper, TableColumn } from "./PoolInfo.styles"; +import { PoolListInfo } from "@models/pool/info/pool-list-info"; +import { SwapFeeTierInfoMap } from "@constants/option.constant"; interface PoolInfoProps { - pool: Pool; - routeItem: (id: number) => void; + pool: PoolListInfo; + routeItem: (id: string) => void; } const PoolInfo: React.FC = ({ pool, routeItem }) => { const { poolId, - tokenPair, - feeRate, + tokenA, + tokenB, + feeTier, liquidity, apr, volume24h, fees24h, rewards, - incentiveType, tickInfo } = pool; + + const rewardImage = useMemo(() => { + if (rewards.length === 0) { + return <>-; + } + if (rewards.length === 1) { + return icon reward; + } + return ; + }, [rewards]); + return ( routeItem(Math.floor(Math.random() * 100 + 1))} + onClick={() => routeItem(poolId)} > - {`${tokenPair.tokenA.symbol}/${tokenPair.tokenB.symbol}`} - {feeRate} + {`${tokenA.symbol}/${tokenB.symbol}`} + {SwapFeeTierInfoMap[feeTier].rateStr} {liquidity} @@ -50,7 +62,7 @@ const PoolInfo: React.FC = ({ pool, routeItem }) => { {apr} - + {rewardImage}
diff --git a/packages/web/src/components/earn/pool-list-table/PoolListTable.stories.tsx b/packages/web/src/components/earn/pool-list-table/PoolListTable.stories.tsx index b628e23eb..023a5c730 100644 --- a/packages/web/src/components/earn/pool-list-table/PoolListTable.stories.tsx +++ b/packages/web/src/components/earn/pool-list-table/PoolListTable.stories.tsx @@ -2,8 +2,60 @@ import React from "react"; import { ComponentStory, ComponentMeta } from "@storybook/react"; import PoolListTable from "./PoolListTable"; -import { dummyPoolList } from "@containers/pool-list-container/PoolListContainer"; import { action } from "@storybook/addon-actions"; +import { PoolCardInfo } from "@models/pool/info/pool-card-info"; + +const pool: PoolCardInfo = { + poolId: "bar_foo_500", + tokenA: { + chainId: "dev", + createdAt: "2023-10-12T06:56:10+09:00", + name: "Bar", + address: "g1w8wqgrp08cqhtupzx98n4jtm8kqy7vadfmmyd0", + path: "gno.land/r/bar", + decimals: 4, + symbol: "BAR", + logoURI: "https://s2.coinmarketcap.com/static/img/coins/64x64/5994.png", + priceId: "gno.land/r/bar" + }, + tokenB: { + chainId: "dev", + createdAt: "2023-10-12T06:56:08+09:00", + name: "Foo", + address: "g1evezrh92xaucffmtgsaa3rvmz5s8kedffsg469", + path: "gno.land/r/foo", + decimals: 4, + symbol: "FOO", + logoURI: "https://s2.coinmarketcap.com/static/img/coins/64x64/5994.png", + priceId: "gno.land/r/foo" + }, + feeTier: "FEE_500", + liquidity: "$1,182,797", + apr: "0.12%", + volume24h: "$1,182,797", + fees24h: "$131.937491", + rewards: [ + { + token: { + chainId: "dev", + createdAt: "2023-10-12T06:56:12+09:00", + name: "Gnoswap", + address: "g1sqaft388ruvsseu97r04w4rr4szxkh4nn6xpax", + path: "gno.land/r/gnos", + decimals: 4, + symbol: "GNOS", + logoURI: "/gnos.svg", + priceId: "gno.land/r/gnos" + }, + amount: 10 + } + ], + incentiveType: "Incentivized", + tickInfo: { + currentTick: 1.498590, + ticks: [] + } +}; export default { title: "earn/PoolList/PoolListTable", @@ -16,7 +68,7 @@ const Template: ComponentStory = args => ( export const Default = Template.bind({}); Default.args = { - pools: dummyPoolList, + pools: [pool], isFetched: true, routeItem: action("routeItem"), sortOption: undefined, @@ -34,8 +86,8 @@ Skeleton.args = { isSortOption: () => true, }; -export const NotFount = Template.bind({}); -NotFount.args = { +export const NotFound = Template.bind({}); +NotFound.args = { pools: [], isFetched: true, routeItem: action("routeItem"), diff --git a/packages/web/src/components/earn/pool-list-table/PoolListTable.tsx b/packages/web/src/components/earn/pool-list-table/PoolListTable.tsx index b72838633..fc7323e89 100644 --- a/packages/web/src/components/earn/pool-list-table/PoolListTable.tsx +++ b/packages/web/src/components/earn/pool-list-table/PoolListTable.tsx @@ -1,6 +1,5 @@ import React, { useCallback } from "react"; import { - Pool, PoolSortOption, TABLE_HEAD, } from "@containers/pool-list-container/PoolListContainer"; @@ -11,14 +10,15 @@ import TableSkeleton from "@components/common/table-skeleton/TableSkeleton"; import { POOL_INFO, POOL_TD_WIDTH } from "@constants/skeleton.constant"; import IconTriangleArrowUp from "@components/common/icons/IconTriangleArrowUp"; import IconTriangleArrowDown from "@components/common/icons/IconTriangleArrowDown"; +import { PoolListInfo } from "@models/pool/info/pool-list-info"; interface PoolListTableProps { - pools: Pool[]; + pools: PoolListInfo[]; isFetched: boolean; sortOption: PoolSortOption | undefined; sort: (head: TABLE_HEAD) => void; isSortOption: (head: TABLE_HEAD) => boolean; - routeItem: (id: number) => void; + routeItem: (id: string) => void; } const PoolListTable: React.FC = ({ diff --git a/packages/web/src/components/earn/pool-list/PoolList.stories.tsx b/packages/web/src/components/earn/pool-list/PoolList.stories.tsx index c51cb4ca2..f0eaf1c0a 100644 --- a/packages/web/src/components/earn/pool-list/PoolList.stories.tsx +++ b/packages/web/src/components/earn/pool-list/PoolList.stories.tsx @@ -5,7 +5,6 @@ import { action } from "@storybook/addon-actions"; import PoolList from "./PoolList"; import { POOL_TYPE, - dummyPoolList, } from "@containers/pool-list-container/PoolListContainer"; import { DEVICE_TYPE } from "@styles/media"; @@ -20,7 +19,7 @@ const Template: ComponentStory = args => ( export const Default = Template.bind({}); Default.args = { - pools: dummyPoolList, + pools: [], poolType: POOL_TYPE.ALL, changePoolType: action("changePoolType"), search: action("search"), diff --git a/packages/web/src/components/earn/pool-list/PoolList.tsx b/packages/web/src/components/earn/pool-list/PoolList.tsx index 38d29ba2a..82dcda3d1 100644 --- a/packages/web/src/components/earn/pool-list/PoolList.tsx +++ b/packages/web/src/components/earn/pool-list/PoolList.tsx @@ -11,12 +11,12 @@ import PoolListHeader from "@components/earn/pool-list-header/PoolListHeader"; import PoolListTable from "@components/earn/pool-list-table/PoolListTable"; import Pagination from "@components/common/pagination/Pagination"; import { PoolListWrapper } from "./PoolList.styles"; -import { DeviceSize, DEVICE_TYPE } from "@styles/media"; +import { DEVICE_TYPE } from "@styles/media"; +import { PoolListInfo } from "@models/pool/info/pool-list-info"; interface TokenListProps { - pools: Pool[]; + pools: PoolListInfo[]; isFetched: boolean; - error: Error | null; poolType?: POOL_TYPE; changePoolType: (newType: string) => void; search: (e: React.ChangeEvent) => void; @@ -30,13 +30,12 @@ interface TokenListProps { searchIcon: boolean; onTogleSearch: () => void; breakpoint: DEVICE_TYPE; - routeItem: (id: number) => void; + routeItem: (id: string) => void; } const PoolList: React.FC = ({ pools, isFetched, - error, poolType = POOL_TYPE.ALL, changePoolType, search, diff --git a/packages/web/src/components/governance/create-proposal-modal/CreateProposalModal.spec.tsx b/packages/web/src/components/governance/create-proposal-modal/CreateProposalModal.spec.tsx new file mode 100644 index 000000000..dc2c6b0db --- /dev/null +++ b/packages/web/src/components/governance/create-proposal-modal/CreateProposalModal.spec.tsx @@ -0,0 +1,22 @@ +import { render } from "@testing-library/react"; +import { Provider as JotaiProvider } from "jotai"; +import GnoswapThemeProvider from "@providers/gnoswap-theme-provider/GnoswapThemeProvider"; +import CreateProposalModal from "./CreateProposalModal"; +import { DEVICE_TYPE } from "@styles/media"; + +describe("CreateProposalModal Component", () => { + it("CreateProposalModal render", () => { + const mockProps = { + breakpoint: DEVICE_TYPE.WEB, + setIsShowCreateProposal: () => null, + }; + + render( + + + + + , + ); + }); +}); diff --git a/packages/web/src/components/governance/create-proposal-modal/CreateProposalModal.stories.tsx b/packages/web/src/components/governance/create-proposal-modal/CreateProposalModal.stories.tsx new file mode 100644 index 000000000..95ce5027f --- /dev/null +++ b/packages/web/src/components/governance/create-proposal-modal/CreateProposalModal.stories.tsx @@ -0,0 +1,20 @@ +import React from "react"; +import { ComponentStory, ComponentMeta } from "@storybook/react"; +import CreateProposalModal from "./CreateProposalModal"; +import { action } from "@storybook/addon-actions"; +import { DEVICE_TYPE } from "@styles/media"; + +export default { + title: "governance/CreateProposalModal", + component: CreateProposalModal, +} as ComponentMeta; + +const Template: ComponentStory = args => ( + +); + +export const Default = Template.bind({}); +Default.args = { + breakpoint: DEVICE_TYPE.WEB, + setIsShowCreateProposal: action("setIsShowCreateProposal"), +}; diff --git a/packages/web/src/components/governance/create-proposal-modal/CreateProposalModal.styles.ts b/packages/web/src/components/governance/create-proposal-modal/CreateProposalModal.styles.ts new file mode 100644 index 000000000..652c3352c --- /dev/null +++ b/packages/web/src/components/governance/create-proposal-modal/CreateProposalModal.styles.ts @@ -0,0 +1,212 @@ +import { fonts } from "@constants/font.constant"; +import styled from "@emotion/styled"; +import { media } from "@styles/media"; +import mixins from "@styles/mixins"; +import { Z_INDEX } from "@styles/zIndex"; +export const CreateProposalModalBackground = styled.div` + position: fixed; + overflow: hidden; + top: 0px; + bottom: 0px; + left: 0px; + right: 0px; + width: 100%; + height: 100lvh; + background: rgba(10, 14, 23, 0.7); + z-index: ${Z_INDEX.modalOverlay}; +`; + +export const CreateProposalModalWrapper = styled.div` + ${mixins.flexbox("column", "flex-start", "flex-start")}; + position: absolute; + overflow: hidden; + width: 700px; + border-radius: 8px; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + box-shadow: 10px 14px 60px 0px rgba(0, 0, 0, 0.4); + background-color: ${({ theme }) => theme.color.background06}; + .btn-submit { + border-top-left-radius: 0; + border-top-right-radius: 0; + height: 57px; + } + ${media.mobile} { + width: 328px; + .btn-submit { + height: 41px; + border-top-left-radius: 0; + border-top-right-radius: 0; + } + } + .modal-body { + border: 1px solid ${({ theme }) => theme.color.border02}; + ${mixins.flexbox("column", "flex-start", "flex-start")}; + width: 100%; + padding: 24px 24px 5px 24px; + overflow: scroll; + gap: 16px; + max-height: calc(100lvh - 100px); + .header { + ${mixins.flexbox("row", "center", "space-between")}; + width: 100%; + > h6 { + ${fonts.h6} + color: ${({ theme }) => theme.color.text02}; + } + .close-wrap { + ${mixins.flexbox("row", "center", "center")}; + cursor: pointer; + width: 24px; + height: 24px; + .close-icon { + width: 24px; + height: 24px; + * { + fill: ${({ theme }) => theme.color.icon01}; + } + } + } + ${media.mobile} { + > h6 { + ${fonts.body9} + } + } + } + ${media.mobile} { + padding: 12px; + } + } +`; + +export const BoxItem = styled.div` + ${mixins.flexbox("column", "flex-start", "flex-start")}; + width: 100%; + padding: 16px; + gap: 16px; + border-radius: 8px; + background-color: ${({ theme }) => theme.color.backgroundOpacity2}; + border: 1px solid ${({ theme }) => theme.color.border02}; + position: relative; + .multiple-variable { + width: 100%; + gap: 12px; + ${mixins.flexbox("row", "flex-start", "flex-start")}; + div:first-of-type { + ${mixins.flexbox("row", "flex-start", "flex-start")}; + gap: 8px; + > div { + ${mixins.flexbox("column", "flex-start", "flex-start")}; + } + ${media.mobile} { + width: 100%; + ${mixins.flexbox("column", "flex-start", "flex-start")}; + } + } + gap: 8px; + } + .box-label { + ${fonts.body12}; + color: ${({ theme }) => theme.color.text03}; + } + .type-tab { + ${mixins.flexbox("row", "center", "center")}; + width: 100%; + gap: 8px; + ${fonts.body11}; + > div { + cursor: pointer; + flex: 1; + border: 1px solid ${({ theme }) => theme.color.border02}; + padding: 12px 8px 12px 8px; + border-radius: 8px; + text-align: center; + color: ${({ theme }) => theme.color.text02}; + &:hover { + background-color: ${({ theme }) => theme.color.background11}; + border: 1px solid ${({ theme }) => theme.color.border03}; + } + } + .active-type-tab { + background-color: ${({ theme }) => theme.color.background11}; + border: 1px solid ${({ theme }) => theme.color.border03}; + } + } + ${media.mobile} { + gap: 12px; + padding: 12px; + .type-tab { + ${mixins.flexbox("column", "flex-start", "flex-start")}; + > div { + width: 100%; + } + } + } + .deposit { + ${mixins.flexbox("row", "center", "space-between")}; + border: 1px solid ${({ theme }) => theme.color.border02}; + border-radius: 8px; + padding: 16px; + width: 100%; + > span { + ${fonts.body7}; + color: ${({ theme }) => theme.color.text02}; + } + } + .deposit-currency { + ${mixins.flexbox("row", "center", "space-between")}; + gap: 8px; + span { + color: ${({ theme }) => theme.color.text01}; + ${fonts.body9}; + } + img { + height: 24px; + width: 24px; + } + } + .suffix-wrapper { + position: relative; + width: 100%; + input { + ${fonts.body1}; + padding: 16px 110px 16px 24px; + &::placeholder { + ${fonts.body1}; + color: ${({ theme }) => theme.color.text01}; + } + ${media.mobile} { + padding: 16px 110px 16px 16px; + ${fonts.body5}; + &::placeholder { + ${fonts.body5}; + } + } + } + } + .suffix-currency { + position: absolute; + right: 24px; + top: 21px; + background-color: ${({ theme }) => theme.color.background02}; + border-radius: 36px; + padding: 4px 12px 4px 6px; + ${media.mobile} { + top: 18px; + right: 16px; + } + } +`; + +export const IconButton = styled.div` + ${mixins.flexbox("column", "center", "center")}; + height: 53px; + min-width: 53px; + border-radius: 8px; + background-color: ${({ theme }) => theme.color.background05}; + cursor: pointer; + * { + fill: ${({ theme }) => theme.color.icon05}; + } +`; diff --git a/packages/web/src/components/governance/create-proposal-modal/CreateProposalModal.tsx b/packages/web/src/components/governance/create-proposal-modal/CreateProposalModal.tsx new file mode 100644 index 000000000..7ecabbe53 --- /dev/null +++ b/packages/web/src/components/governance/create-proposal-modal/CreateProposalModal.tsx @@ -0,0 +1,332 @@ +import Button, { ButtonHierarchy } from "@components/common/button/Button"; +import FormInput from "@components/common/form-input/FormInput"; +import FormTextArea from "@components/common/form-textarea/FormTextArea"; +import FormProvider from "@components/common/form/FormProvider"; +import IconAdd from "@components/common/icons/IconAdd"; +import IconClose from "@components/common/icons/IconCancel"; +import IconRemove from "@components/common/icons/IconRemove"; +import { yupResolver } from "@hookform/resolvers/yup"; +import { DEVICE_TYPE } from "@styles/media"; +import { + getCreateProposalChangeParameterValidation, + getCreateProposalCommunityPoolSpendValidation, + getCreateProposalValidation, +} from "@utils/create-proposal-validation"; +import { isEmptyObject } from "@utils/validation-utils"; +import React, { + useMemo, + useState, + useRef, + useEffect, + Dispatch, + SetStateAction, +} from "react"; +import { useFieldArray, useForm } from "react-hook-form"; +import { + BoxItem, + CreateProposalModalBackground, + CreateProposalModalWrapper, + IconButton, +} from "./CreateProposalModal.styles"; + +interface Props { + breakpoint: DEVICE_TYPE; + setIsShowCreateProposal: Dispatch>; +} + +interface BoxContentProps { + label: string; + children?: React.ReactNode; +} + +interface Variable { + subspace: string; + key: string; + value: string; +} + +interface FormValues { + title: string; + description: string; + amount: number; + recipientAddress: string; + variable: Variable[]; +} + +const ProposalOption = [ + "Text Proposal", + "Community Pool Spend", + "Parameter Change", +]; + +const TOKEN = { + urlIcon: + "https://raw.githubusercontent.com/Uniswap/assets/master/blockchains/ethereum/assets/0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48/logo.png", + currency: "GNOS", + value: -500, +}; +const BoxContent: React.FC = ({ + label, + children, + ...props +}) => { + return ( + + + {children} + + ); +}; + +const CreateProposalModal: React.FC = ({ + breakpoint, + setIsShowCreateProposal, +}) => { + const [type, setType] = useState(ProposalOption[0]); + + const modalRef = useRef(null); + + const handleResize = () => { + if (typeof window !== "undefined" && modalRef.current) { + const height = modalRef.current.getBoundingClientRect().height; + if (height >= window?.innerHeight) { + modalRef.current.style.top = "0"; + modalRef.current.style.transform = "translateX(-50%)"; + } else { + modalRef.current.style.top = "50%"; + modalRef.current.style.transform = "translate(-50%, -50%)"; + } + } + }; + + useEffect(() => { + const closeModal = (e: MouseEvent) => { + if (modalRef.current && modalRef.current.contains(e.target as Node)) { + return; + } else { + e.stopPropagation(); + setIsShowCreateProposal(true); + } + }; + window.addEventListener("click", closeModal, true); + return () => { + window.removeEventListener("click", closeModal, true); + }; + }, [modalRef, setIsShowCreateProposal]); + + useEffect(() => { + handleResize(); + window.addEventListener("resize", handleResize); + return () => { + window.removeEventListener("resize", handleResize); + }; + }, [modalRef]); + + const validationProps: any = useMemo(() => { + if (type === ProposalOption[1]) { + return getCreateProposalCommunityPoolSpendValidation(); + } + if (type === ProposalOption[2]) { + return getCreateProposalChangeParameterValidation(); + } + return getCreateProposalValidation(); + }, [type]); + + const methods = useForm({ + mode: "onChange", + resolver: yupResolver(validationProps), + defaultValues: { + title: "", + description: "", + recipientAddress: "", + variable: [ + { + subspace: "", + key: "", + value: "", + }, + { + subspace: "", + key: "", + value: "", + }, + ], + }, + }); + const { + register, + formState: { errors, isDirty, isValid }, + control, + } = methods; + + const { fields, append, remove } = useFieldArray({ + control, + name: "variable", + }); + + const handleClickFormFieldArray = (index: number) => { + if (index === fields.length - 1) { + append({ + subspace: "", + key: "", + value: "", + }); + } else { + remove(index); + } + }; + + const isDisableSubmit = useMemo(() => { + return !isEmptyObject(errors) || !isDirty || !isValid; + }, [isDirty, isValid, errors]); + + return ( + + {}}> + +
+
+
Create Proposal
+
setIsShowCreateProposal(false)} + > + +
+
+ +
+ {ProposalOption.map((item, index) => ( +
setType(ProposalOption[index])} + > + {ProposalOption[index]} +
+ ))} +
+
+ + + + + {type === ProposalOption[1] && ( + + +
+ +
+ token logo + {TOKEN.currency} +
+
+
+ )} + {type === ProposalOption[2] && ( + + {fields.map((item, index) => ( +
+
+ + + + +
+ handleClickFormFieldArray(index)} + > + {index === fields.length - 1 ? ( + + ) : ( + + )} + +
+ ))} +
+ )} + +
+
+
+ token logo + {TOKEN.currency} +
+
+ {TOKEN.value} +
+
+
+
+ +); + +export default ProposalHeader; diff --git a/packages/web/src/components/swap/swap-button-tooltip/SwapButtonTooltip.spec.tsx b/packages/web/src/components/governance/proposals-list/ProposalList.spec.tsx similarity index 54% rename from packages/web/src/components/swap/swap-button-tooltip/SwapButtonTooltip.spec.tsx rename to packages/web/src/components/governance/proposals-list/ProposalList.spec.tsx index 4e887acae..66c413533 100644 --- a/packages/web/src/components/swap/swap-button-tooltip/SwapButtonTooltip.spec.tsx +++ b/packages/web/src/components/governance/proposals-list/ProposalList.spec.tsx @@ -1,19 +1,21 @@ import { render } from "@testing-library/react"; import { Provider as JotaiProvider } from "jotai"; import GnoswapThemeProvider from "@providers/gnoswap-theme-provider/GnoswapThemeProvider"; -import SwapButtonTooltip from "./SwapButtonTooltip"; -import { dummySwapGasInfo } from "@containers/swap-container/SwapContainer"; +import ProposalList from "./ProposalList"; -describe("SwapButtonTooltip Component", () => { - it("SwapButtonTooltip render", () => { +describe("ProposalList Component", () => { + it("ProposalList render", () => { const mockProps = { - swapGasInfo: dummySwapGasInfo, + isShowCancelled: false, + toggleShowCancelled: () => null, + proposalList: [], + isConnected: false, }; render( - + , ); diff --git a/packages/web/src/components/governance/proposals-list/ProposalList.stories.tsx b/packages/web/src/components/governance/proposals-list/ProposalList.stories.tsx new file mode 100644 index 000000000..ac0c03239 --- /dev/null +++ b/packages/web/src/components/governance/proposals-list/ProposalList.stories.tsx @@ -0,0 +1,22 @@ +import React from "react"; +import { ComponentStory, ComponentMeta } from "@storybook/react"; +import ProposalList from "./ProposalList"; +import { action } from "@storybook/addon-actions"; +import { createDummyProposalItem } from "@containers/proposal-list-container/ProposalListContainer"; + +export default { + title: "governance/ProposalList", + component: ProposalList, +} as ComponentMeta; + +const Template: ComponentStory = args => ( + +); + +export const Default = Template.bind({}); +Default.args = { + isShowCancelled: true, + toggleShowCancelled: action("toggleShowCancelled"), + proposalList: [createDummyProposalItem(), createDummyProposalItem()], + isConnected: false, +}; diff --git a/packages/web/src/components/governance/proposals-list/ProposalList.styles.ts b/packages/web/src/components/governance/proposals-list/ProposalList.styles.ts new file mode 100644 index 000000000..ffa5f51e6 --- /dev/null +++ b/packages/web/src/components/governance/proposals-list/ProposalList.styles.ts @@ -0,0 +1,8 @@ +import styled from "@emotion/styled"; +import mixins from "@styles/mixins"; + +export const ProposalListWrapper = styled.div` + ${mixins.flexbox("column", "flex-start", "flex-start")}; + width: 100%; + gap: 16px; +`; diff --git a/packages/web/src/components/governance/proposals-list/ProposalList.tsx b/packages/web/src/components/governance/proposals-list/ProposalList.tsx new file mode 100644 index 000000000..fca674868 --- /dev/null +++ b/packages/web/src/components/governance/proposals-list/ProposalList.tsx @@ -0,0 +1,66 @@ +import { ProposalDetailProps } from "@containers/proposal-list-container/ProposalListContainer"; +import { DEVICE_TYPE } from "@styles/media"; +import ProposalDetail from "../proposal-detail/ProposalDetail"; +import ProposalHeader from "../proposal-header/ProposalHeader"; +import ViewProposalModal from "../view-proposal-modal/ViewProposalModal"; +import { ProposalListWrapper } from "./ProposalList.styles"; +import { Dispatch, SetStateAction } from "react"; +import CreateProposalModal from "../create-proposal-modal/CreateProposalModal"; +interface ProposalListProps { + proposalList: ProposalDetailProps[]; + isShowCancelled: boolean; + toggleShowCancelled: () => void; + isShowProposalModal: boolean; + breakpoint: DEVICE_TYPE; + proposalDetail: ProposalDetailProps; + setIsShowProposalModal: Dispatch>; + onClickProposalDetail: (id: string) => void; + isShowCreateProposal: boolean; + setIsShowCreateProposal: Dispatch>; + isConnected: boolean; +} + +const ProposalList: React.FC = ({ + proposalList, + toggleShowCancelled, + isShowCancelled, + isShowProposalModal, + breakpoint, + proposalDetail, + setIsShowProposalModal, + onClickProposalDetail, + isShowCreateProposal, + setIsShowCreateProposal, + isConnected, +}) => ( + + + {proposalList.map((proposalDetail: ProposalDetailProps) => ( + + ))} + {isShowProposalModal && ( + + )} + {isShowCreateProposal && ( + + )} + +); + +export default ProposalList; diff --git a/packages/web/src/components/governance/view-proposal-modal/ViewProposalModal.spec.tsx b/packages/web/src/components/governance/view-proposal-modal/ViewProposalModal.spec.tsx new file mode 100644 index 000000000..d518894f2 --- /dev/null +++ b/packages/web/src/components/governance/view-proposal-modal/ViewProposalModal.spec.tsx @@ -0,0 +1,39 @@ +import { render } from "@testing-library/react"; +import { Provider as JotaiProvider } from "jotai"; +import GnoswapThemeProvider from "@providers/gnoswap-theme-provider/GnoswapThemeProvider"; +import ViewProposalModal from "./ViewProposalModal"; +import { DEVICE_TYPE } from "@styles/media"; + +describe("ViewProposalModal Component", () => { + it("ViewProposalModal render", () => { + const mockProps = { + breakpoint: DEVICE_TYPE.WEB, + proposalDetail: { + id: "1", + title: "#7 Proposal Title", + label: "Community Pool Spend", + status: "ACTIVE", + timeEnd: "2023-08-01, 12:00:00 UTC+9", + abstainOfQuorum: 30, + noOfQuorum: 20, + yesOfQuorum: 50, + currentValue: 200000, + maxValue: 400000, + icon: "https://raw.githubusercontent.com/Uniswap/assets/master/blockchains/ethereum/assets/0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48/logo.png", + description: + "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Phasellus nisiorci, ultrices sit amet mi eget, efficitur elementum tellus. Integeraugue purus, rutrum eu pretium sit amet, varius in quam.Lorem ipsumdolor sit amet, consectetur adipiscing elit. Phasellus nisi orci,ultrices sit amet mi eget, efficitur elementum tellus. Integer auguepurus, rutrum eu pretium sit amet, varius in quam.Lorem ipsum dolor sitamet, consectetur adipiscing elit. Phasellus nisi orci, ultrices sitamet mi eget, efficitur elementum tellus. Integer augue purus, rutrum eupretium sit amet, varius in quam.Lorem ipsum dolor sit amet, consecteturadipiscing elit. Phasellus nisi orci, ultrices sit amet mi eget,efficitur elementum tellus. Integer augue purus, rutrum eu pretium sitamet, varius in quam.Lorem ipsum dolor sit amet, consectetur adipiscingelit. Phasellus nisi orci, ultrices sit amet mi eget, efficiturelementum tellus. Integer augue purus, rutrum eu pretium sit amet,varius in quam.Lorem ipsum dolor sit amet, consectetur adipiscing elit.Phasellus nisi orci, ultrices sit amet mi eget, efficitur elementumtellus. Integer augue purus, rutrum eu pretium sit amet, varius inquam.Lorem ipsum dolor sit amet, consectetur adipiscing elit. Phasellusnisi orci, ultrices sit amet mi eget, efficitur elementum tellus.Integer augue purus, rutrum eu pretium sit amet, varius in quam.Loremipsum dolor sit amet, consectetur adipiscing elit. Phasellus nisi orci,ultrices sit amet mi eget, efficitur elementum tellus. Integer auguepurus, rutrum eu pretium sit amet, varius in quam.Lorem ipsum dolor sitamet, consectetur adipiscing elit. Phasellus nisi orci, ultrices sitamet mi eget, efficitur elementum tellus. Integer augue purus, rutrum eupretium sit amet, varius in quam.Lorem ipsum dolor sit amet, consecteturadipiscing elit. Phasellus nisi orci, ultrices sit amet mi eget,efficitur elementum tellus. Integer augue purus, rutrum eu pretium sitamet, varius in quam.Lorem ipsum dolor sit amet, consectetur adipiscingelit. Phasellus nisi orci, ultrices sit amet mi eget, efficiturelementum tellus. Integer augue purus, rutrum eu pretium sit amet,varius in quam.Lorem ipsum dolor sit amet, consectetur adipiscing elit.Phasellus nisi orci, ultrices sit amet mi eget, efficitur elementumtellus. Integer augue purus, rutrum eu pretium sit amet, varius inquam.Lorem ipsum dolor sit amet, consectetur adipiscing elit. Phasellusnisi orci, ultrices sit amet mi eget, efficitur elementum tellus.Integer augue purus, rutrum eu pretium sit amet, varius in quam.Loremipsum dolor sit amet, consectetur adipiscing elit. Phasellus nisi orci,ultrices sit amet mi eget, efficitur elementum tellus. Integer auguepurus, rutrum eu pretium sit amet, varius in quam.Lorem ipsum dolor sitamet, consectetur adipiscing elit. Phasellus nisi orci, ultrices sitamet mi eget, efficitur elementum tellus. Integer augue purus, rutrum eupretium sit amet, varius in quam.", + votingPower: 14245, + currency: "USDC", + }, + setIsShowProposalModal: () => null, + }; + + render( + + + + + , + ); + }); +}); diff --git a/packages/web/src/components/governance/view-proposal-modal/ViewProposalModal.stories.tsx b/packages/web/src/components/governance/view-proposal-modal/ViewProposalModal.stories.tsx new file mode 100644 index 000000000..ef819117e --- /dev/null +++ b/packages/web/src/components/governance/view-proposal-modal/ViewProposalModal.stories.tsx @@ -0,0 +1,38 @@ +import React from "react"; +import { ComponentStory, ComponentMeta } from "@storybook/react"; +import ViewProposalModal from "./ViewProposalModal"; +import { DEVICE_TYPE } from "@styles/media"; +import { action } from "@storybook/addon-actions"; + +export default { + title: "governance/ViewProposalModal", + component: ViewProposalModal, +} as ComponentMeta; + +const Template: ComponentStory = args => ( + +); + +export const Default = Template.bind({}); +Default.args = { + breakpoint: DEVICE_TYPE.WEB, + proposalDetail: { + id: "1", + title: "#7 Proposal Title", + label: "Community Pool Spend", + status: "ACTIVE", + timeEnd: "2023-08-01, 12:00:00 UTC+9", + abstainOfQuorum: 30, + noOfQuorum: 20, + yesOfQuorum: 50, + currentValue: 200000, + maxValue: 400000, + icon: "https://raw.githubusercontent.com/Uniswap/assets/master/blockchains/ethereum/assets/0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48/logo.png", + description: + "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Phasellus nisiorci, ultrices sit amet mi eget, efficitur elementum tellus. Integeraugue purus, rutrum eu pretium sit amet, varius in quam.Lorem ipsumdolor sit amet, consectetur adipiscing elit. Phasellus nisi orci,ultrices sit amet mi eget, efficitur elementum tellus. Integer auguepurus, rutrum eu pretium sit amet, varius in quam.Lorem ipsum dolor sitamet, consectetur adipiscing elit. Phasellus nisi orci, ultrices sitamet mi eget, efficitur elementum tellus. Integer augue purus, rutrum eupretium sit amet, varius in quam.Lorem ipsum dolor sit amet, consecteturadipiscing elit. Phasellus nisi orci, ultrices sit amet mi eget,efficitur elementum tellus. Integer augue purus, rutrum eu pretium sitamet, varius in quam.Lorem ipsum dolor sit amet, consectetur adipiscingelit. Phasellus nisi orci, ultrices sit amet mi eget, efficiturelementum tellus. Integer augue purus, rutrum eu pretium sit amet,varius in quam.Lorem ipsum dolor sit amet, consectetur adipiscing elit.Phasellus nisi orci, ultrices sit amet mi eget, efficitur elementumtellus. Integer augue purus, rutrum eu pretium sit amet, varius inquam.Lorem ipsum dolor sit amet, consectetur adipiscing elit. Phasellusnisi orci, ultrices sit amet mi eget, efficitur elementum tellus.Integer augue purus, rutrum eu pretium sit amet, varius in quam.Loremipsum dolor sit amet, consectetur adipiscing elit. Phasellus nisi orci,ultrices sit amet mi eget, efficitur elementum tellus. Integer auguepurus, rutrum eu pretium sit amet, varius in quam.Lorem ipsum dolor sitamet, consectetur adipiscing elit. Phasellus nisi orci, ultrices sitamet mi eget, efficitur elementum tellus. Integer augue purus, rutrum eupretium sit amet, varius in quam.Lorem ipsum dolor sit amet, consecteturadipiscing elit. Phasellus nisi orci, ultrices sit amet mi eget,efficitur elementum tellus. Integer augue purus, rutrum eu pretium sitamet, varius in quam.Lorem ipsum dolor sit amet, consectetur adipiscingelit. Phasellus nisi orci, ultrices sit amet mi eget, efficiturelementum tellus. Integer augue purus, rutrum eu pretium sit amet,varius in quam.Lorem ipsum dolor sit amet, consectetur adipiscing elit.Phasellus nisi orci, ultrices sit amet mi eget, efficitur elementumtellus. Integer augue purus, rutrum eu pretium sit amet, varius inquam.Lorem ipsum dolor sit amet, consectetur adipiscing elit. Phasellusnisi orci, ultrices sit amet mi eget, efficitur elementum tellus.Integer augue purus, rutrum eu pretium sit amet, varius in quam.Loremipsum dolor sit amet, consectetur adipiscing elit. Phasellus nisi orci,ultrices sit amet mi eget, efficitur elementum tellus. Integer auguepurus, rutrum eu pretium sit amet, varius in quam.Lorem ipsum dolor sitamet, consectetur adipiscing elit. Phasellus nisi orci, ultrices sitamet mi eget, efficitur elementum tellus. Integer augue purus, rutrum eupretium sit amet, varius in quam.", + votingPower: 14245, + currency: "USDC", + typeVote: "", + }, + setIsShowProposalModal: action("setIsShowProposalModal"), +}; diff --git a/packages/web/src/components/governance/view-proposal-modal/ViewProposalModal.styles.ts b/packages/web/src/components/governance/view-proposal-modal/ViewProposalModal.styles.ts new file mode 100644 index 000000000..9f708f00a --- /dev/null +++ b/packages/web/src/components/governance/view-proposal-modal/ViewProposalModal.styles.ts @@ -0,0 +1,390 @@ +import mixins from "@styles/mixins"; +import { fonts } from "@constants/font.constant"; +import styled from "@emotion/styled"; +import { Z_INDEX } from "@styles/zIndex"; +import { media } from "@styles/media"; + +export const ViewProposalModalBackground = styled.div` + position: fixed; + overflow: scroll; + top: 0px; + bottom: 0px; + left: 0px; + right: 0px; + width: 100%; + height: 100lvh; + background: rgba(10, 14, 23, 0.7); + z-index: ${Z_INDEX.modalOverlay}; +`; + +export const ViewProposalModalWrapper = styled.div` + position: absolute; + overflow: hidden; + width: 700px; + border-radius: 8px; + padding: 24px 0px; + gap: 16px; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + box-shadow: 10px 14px 60px 0px rgba(0, 0, 0, 0.4); + border: 1px solid ${({ theme }) => theme.color.border02}; + background-color: ${({ theme }) => theme.color.background06}; + ${media.mobile} { + width: 328px; + height: 610px; + padding: 16px 12px 0px 12px; + transform: translate(-50%, -50%); + } + .modal-body { + ${mixins.flexbox("column", "flex-start", "flex-start")}; + width: 100%; + padding: 0px 24px; + gap: 16px; + ${media.mobile} { + padding: 0px; + } + } +`; + +export const ModalHeaderWrapper = styled.div` + ${mixins.flexbox("column", "flex-start", "flex-start")}; + gap: 8px; + width: 100%; + ${media.mobile} { + gap: 12px; + } + .header { + width: 100%; + ${mixins.flexbox("row", "center", "space-between")}; + .title { + color: ${({ theme }) => theme.color.text02}; + ${mixins.flexbox("row", "center", "space-between")}; + gap: 12px; + ${fonts.h6} + ${media.mobile} { + ${fonts.body9} + } + } + .badge-label { + color: ${({ theme }) => theme.color.text12}; + } + .close-wrap { + ${mixins.flexbox("row", "center", "center")}; + cursor: pointer; + width: 24px; + height: 24px; + .close-icon { + width: 24px; + height: 24px; + * { + fill: ${({ theme }) => theme.color.icon01}; + } + } + } + } + .active-wrapper { + gap: 12px; + ${mixins.flexbox("row", "center", "center")}; + ${media.mobile} { + margin-top: 4px; + ${mixins.flexbox("column", "flex-start", "flex-start")}; + gap: 8px; + } + .status { + ${fonts.p4}; + ${mixins.flexbox("row", "center", "center")}; + ${media.mobile} { + ${mixins.flexbox("row", "flex-start", "flex-start")}; + } + gap: 6px; + .status-icon { + width: 16px; + height: 16px; + } + .success-icon * { + fill: ${({ theme }) => theme.color.green02}; + } + .failed-icon * { + fill: ${({ theme }) => theme.color.red02}; + } + .passed-icon * { + fill: ${({ theme }) => theme.color.point}; + } + .cancelled-icon * { + fill: ${({ theme }) => theme.color.icon03}; + } + } + + .success { + color: ${({ theme }) => theme.color.green02}; + } + .failed { + color: ${({ theme }) => theme.color.red02}; + } + .passed { + color: ${({ theme }) => theme.color.point}; + } + .cancelled { + color: ${({ theme }) => theme.color.icon03}; + } + .time { + color: ${({ theme }) => theme.color.text03}; + br { + display: none; + } + ${media.mobile} { + br { + display: block; + } + } + * { + fill: ${({ theme }) => theme.color.text10}; + } + } + } +`; + +export const ModalQuorum = styled.div` + ${mixins.flexbox("column", "flex-start", "flex-start")}; + width: 100%; + border: 1px solid ${({ theme }) => theme.color.border02}; + padding: 16px; + gap: 8px; + border-radius: 8px; + ${media.mobile} { + padding: 12px; + gap: 10px; + } + .quorum-header { + width: 100%; + ${mixins.flexbox("row", "center", "space-between")}; + ${fonts.body12}; + ${media.mobile} { + ${fonts.p4}; + } + span { + color: ${({ theme }) => theme.color.text04}; + } + .progress-value { + ${mixins.flexbox("row", "center", "center")}; + flex-wrap: wrap; + gap: 4px; + color: ${({ theme }) => theme.color.text04}; + ${fonts.body12}; + span { + color: ${({ theme }) => theme.color.text10}; + } + } + ${media.mobile} { + gap: 8px; + .progress-value { + ${fonts.p6}; + } + } + } +`; + +export const ProgressWrapper = styled.div` + ${mixins.flexbox("row", "center", "space-between")}; + width: 100%; +`; + +export interface progressBarProps { + rateWidth?: string; + noOfQuorumWidth?: string; + abstainOfQuorumWidth?: string; +} + +export const ProgressBar = styled.div` + width: 100%; + ${mixins.flexbox("column", "flex-start", "center")}; + min-width: calc(100% - 200px); + height: 14px; + border-radius: 99px; + background-color: ${({ theme }) => theme.color.background01}; + position: relative; + .progress-bar-rate { + position: absolute; + top: 0; + left: 0; + height: 100%; + } + .progress-bar-yes-of-quorum { + z-index: 3; + width: ${({ rateWidth }) => { + return rateWidth ? rateWidth : "0%"; + }}; + height: 100%; + border-radius: 8px; + background-color: ${({ theme }) => theme.color.point}; + } + .progress-bar-no-of-quorum { + z-index: 2; + width: ${({ noOfQuorumWidth }) => { + return noOfQuorumWidth ? noOfQuorumWidth : "0%"; + }}; + height: 100%; + border-radius: 8px; + background-color: ${({ theme }) => theme.color.text08}; + } + .progress-bar-abstain { + z-index: 1; + width: ${({ abstainOfQuorumWidth }) => { + return abstainOfQuorumWidth ? abstainOfQuorumWidth : "0%"; + }}; + height: 100%; + border-radius: 8px; + background-color: ${({ theme }) => theme.color.background12}; + } + ${media.mobile} { + height: 8px; + } +`; + +export const BoxQuorumWrapper = styled.div` + ${mixins.flexbox("row", "center", "center")}; + width: 100%; + gap: 2px; + .box-quorum { + ${mixins.flexbox("column", "center", "center")}; + gap: 8px; + cursor: pointer; + position: relative; + padding: 15px 8px; + border-radius: 8px; + flex: 1; + background-color: ${({ theme }) => theme.color.backgroundOpacity2}; + border: 1px solid ${({ theme }) => theme.color.border02}; + span { + color: ${({ theme }) => theme.color.text10}; + ${fonts.body12} + } + > div { + ${fonts.body4} + color: ${({ theme }) => theme.color.text02}; + } + .badge { + > span { + color: ${({ theme }) => theme.color.text12}; + } + position: absolute; + top: 8px; + right: 12px; + } + ${media.mobile} { + ${mixins.flexbox("column", "flex-start", "flex-start")}; + padding: 12px; + > div { + ${fonts.body11} + } + } + &:hover { + background-color: ${({ theme }) => theme.color.background05Hover}; + border: 1px solid ${({ theme }) => theme.color.border03}; + } + } + .active-quorum { + background-color: ${({ theme }) => theme.color.background05Hover}; + border: 1px solid ${({ theme }) => theme.color.border03}; + } +`; + +export const VotingPowerWrapper = styled.div` + ${mixins.flexbox("row", "center", "space-between")}; + background-color: ${({ theme }) => theme.color.backgroundOpacity2}; + border: 1px solid ${({ theme }) => theme.color.border02}; + padding: 16px; + width: 100%; + border-radius: 8px; + > span { + ${fonts.body12} + color: ${({ theme }) => theme.color.text10}; + } + > div { + ${mixins.flexbox("row", "center", "center")}; + gap: 16px; + color: ${({ theme }) => theme.color.text01}; + } + .power-value { + ${fonts.body3} + } + .power-currency { + ${mixins.flexbox("row", "center", "center")}; + background-color: ${({ theme }) => theme.color.background11}; + border-radius: 36px; + padding: 4px 12px 4px 6px; + gap: 8px; + img { + height: 24px; + width: 24px; + } + span { + ${fonts.body9} + } + } + ${media.mobile} { + padding: 12px; + > div { + gap: 8px; + } + .power-value { + ${fonts.body11} + } + .power-currency { + background-color: transparent; + padding: 0; + > span { + display: none; + } + } + } +`; + +export const ProposalContentWrapper = styled.div` + ${mixins.flexbox("column", "flex-start", "flex-start")}; + width: 100%; + border: 1px solid ${({ theme }) => theme.color.border02}; + height: 335px; + overflow: scroll; + padding: 24px; + gap: 12px; + border-radius: 8px; + &::-webkit-scrollbar { + width: 8px; + display: block; + height: 0; + } + + &::-webkit-scrollbar-track { + background: transparent; + padding: 0; + height: 0; + display: none; + } + + &::-webkit-scrollbar-thumb { + background-color: ${({ theme }) => theme.color.background12}; + border-radius: 8px; + padding: 0; + } + + .title { + ${fonts.body7} + color: ${({ theme }) => theme.color.text01}; + ${media.mobile} { + ${fonts.body11} + } + } + .content { + color: ${({ theme }) => theme.color.text04}; + ${fonts.body12} + ${media.mobile} { + ${fonts.p2} + } + } + ${media.mobile} { + padding: 12px 4px 12px 12px; + gap: 8px; + } +`; diff --git a/packages/web/src/components/governance/view-proposal-modal/ViewProposalModal.tsx b/packages/web/src/components/governance/view-proposal-modal/ViewProposalModal.tsx new file mode 100644 index 000000000..c892cc477 --- /dev/null +++ b/packages/web/src/components/governance/view-proposal-modal/ViewProposalModal.tsx @@ -0,0 +1,332 @@ +import Badge, { BADGE_TYPE } from "@components/common/badge/Badge"; +import Button, { ButtonHierarchy } from "@components/common/button/Button"; +import IconClose from "@components/common/icons/IconCancel"; +import IconCheck from "@components/common/icons/IconCheck"; +import IconCircleInCancel from "@components/common/icons/IconCircleInCancel"; +import IconCircleInCheck from "@components/common/icons/IconCircleInCheck"; +import IconInfo from "@components/common/icons/IconInfo"; +import IconOutlineClock from "@components/common/icons/IconOutlineClock"; +import IconPass from "@components/common/icons/IconPass"; +import FloatingTooltip from "@components/common/tooltip/FloatingTooltip"; +import { ProposalDetailProps } from "@containers/proposal-list-container/ProposalListContainer"; +import { DEVICE_TYPE } from "@styles/media"; +import React, { + Dispatch, + SetStateAction, + useCallback, + useEffect, + useMemo, + useRef, + useState, +} from "react"; +import { + BoxQuorumWrapper, + ModalHeaderWrapper, + ModalQuorum, + ProgressBar, + ProgressWrapper, + ProposalContentWrapper, + ViewProposalModalBackground, + ViewProposalModalWrapper, + VotingPowerWrapper, +} from "./ViewProposalModal.styles"; +import dayjs from "dayjs"; +import relative from "dayjs/plugin/relativeTime"; + +dayjs.extend(relative); + +interface Props { + breakpoint: DEVICE_TYPE; + proposalDetail: ProposalDetailProps; + setIsShowProposalModal: Dispatch>; +} + +type OptionVote = "YES" | "NO" | "ABSTAIN" | ""; + +const MAPPING_STATUS: Record = { + ACTIVE: ( +
+ Active +
+ ), + REJECTED: ( +
+ Reject +
+ ), + CANCELLED: ( +
+ Cancelled +
+ ), + PASSED: ( +
+ Passed +
+ ), +}; + +const BoxQuorum = ({ + breakpoint, + proposalDetail, + optionVote, + setOptionVote, +}: { + breakpoint?: DEVICE_TYPE; + proposalDetail: ProposalDetailProps; + optionVote: OptionVote; + setOptionVote: Dispatch>; +}) => { + const showBadge = useMemo(() => { + return breakpoint !== DEVICE_TYPE.MOBILE ? ( + + ) : ( +
+ +
+ ); + }, [, breakpoint]); + return ( + +
setOptionVote("YES")} + > + Yes +
{proposalDetail.currentValue.toLocaleString()}
+ {optionVote === "YES" && showBadge} +
+
setOptionVote("NO")} + > + No +
+ {( + proposalDetail.currentValue + + (proposalDetail.maxValue * proposalDetail.noOfQuorum) / 100 + ).toLocaleString()} +
+ {optionVote === "NO" && showBadge} +
+
setOptionVote("ABSTAIN")} + > + Abstain +
{proposalDetail.maxValue.toLocaleString()}
+ {optionVote === "ABSTAIN" && showBadge} +
+
+ ); +}; + +const VotingPower = ({ + proposalDetail, +}: { + proposalDetail: ProposalDetailProps; +}) => { + return ( + + Your Voting Power +
+
+ {proposalDetail.votingPower.toLocaleString()} +
+
+ token logo + {proposalDetail.currency} +
+
+
+ ); +}; + +const ProposalContent = ({ + proposalDetail, +}: { + proposalDetail: ProposalDetailProps; +}) => { + return ( + +
{proposalDetail.title}
+
{proposalDetail.description}
+
+ ); +}; + +const ViewProposalModal: React.FC = ({ + breakpoint, + proposalDetail, + setIsShowProposalModal, +}) => { + const [optionVote, setOptionVote] = useState(""); + + const modalRef = useRef(null); + + const handleResize = () => { + if (typeof window !== "undefined" && modalRef.current) { + const height = modalRef.current.getBoundingClientRect().height; + if (height >= window?.innerHeight) { + modalRef.current.style.top = "0"; + modalRef.current.style.transform = "translateX(-50%)"; + } else { + modalRef.current.style.top = "50%"; + modalRef.current.style.transform = "translate(-50%, -50%)"; + } + } + }; + + useEffect(() => { + const closeModal = (e: MouseEvent) => { + if (modalRef.current && modalRef.current.contains(e.target as Node)) { + return; + } else { + e.stopPropagation(); + setIsShowProposalModal(true); + } + }; + window.addEventListener("click", closeModal, true); + return () => { + window.removeEventListener("click", closeModal, true); + }; + }, [modalRef, setIsShowProposalModal]); + + useEffect(() => { + handleResize(); + window.addEventListener("resize", handleResize); + return () => { + window.removeEventListener("resize", handleResize); + }; + }, [modalRef]); + + const handleSelectVoting = useCallback(() => {}, [optionVote]); + + if (!proposalDetail) return null; + + return ( + + +
+ +
+
+ {proposalDetail.title} + {breakpoint !== DEVICE_TYPE.MOBILE && ( + + )} +
+
setIsShowProposalModal(false)} + > + +
+
+ {breakpoint === DEVICE_TYPE.MOBILE && ( + + )} +
+ {MAPPING_STATUS[proposalDetail.status]} +
+ {" "} + {`Voting ${ + proposalDetail.status === "ACTIVE" ? "Ends in" : "Ended1" + } ${dayjs(proposalDetail.timeEnd).fromNow()} `} +
+ {proposalDetail.timeEnd} +
+
+
+ +
+ Quorum +
+ {proposalDetail.currentValue.toLocaleString()}/ +
{proposalDetail.maxValue.toLocaleString()}
+
+
+ + + +
+ + +
+ + +
+ + + + + + + {proposalDetail.status === "ACTIVE" && ( +
+ + + ); +}; + +export default ViewProposalModal; diff --git a/packages/web/src/components/home/card-list/CardList.tsx b/packages/web/src/components/home/card-list/CardList.tsx index 12313c877..ff38847e5 100644 --- a/packages/web/src/components/home/card-list/CardList.tsx +++ b/packages/web/src/components/home/card-list/CardList.tsx @@ -4,6 +4,7 @@ import { CardListWrapper, ListItem } from "./CardList.styles"; import DoubleLogo from "@components/common/double-logo/DoubleLogo"; import { CardListPoolInfo, CardListTokenInfo } from "@models/common/card-list-item-info"; import { useCallback, useMemo } from "react"; +import { SwapFeeTierInfoMap } from "@constants/option.constant"; interface CardListProps { list: Array; @@ -57,7 +58,10 @@ const CardListPoolItem: React.FC = ({ index, item, onClic const poolFeeRate = useMemo(() => { const pool = item.pool; - return `${pool.fee}%`; + const feeRate = + Object.values(SwapFeeTierInfoMap).find(model => `${model.fee}` === pool.fee) + ?.rateStr || "-"; + return feeRate; }, [item]); const pairLogo = useMemo(() => { diff --git a/packages/web/src/components/home/home-swap/HomeSwap.stories.tsx b/packages/web/src/components/home/home-swap/HomeSwap.stories.tsx index 5ace746b0..e5045e708 100644 --- a/packages/web/src/components/home/home-swap/HomeSwap.stories.tsx +++ b/packages/web/src/components/home/home-swap/HomeSwap.stories.tsx @@ -15,29 +15,39 @@ const Template: ComponentStory = args => ( export const Default = Template.bind({}); Default.args = { - from: { - token: { - path: "USDCoin", - name: "USDCoin", - symbol: "USDC", - logoURI: - "https://raw.githubusercontent.com/Uniswap/assets/master/blockchains/ethereum/assets/0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48/logo.png", + swapTokenInfo: { + tokenA: { + chainId: "dev", + createdAt: "2023-10-17T05:58:00+09:00", + name: "Foo", + address: "g1evezrh92xaucffmtgsaa3rvmz5s8kedffsg469", + path: "gno.land/r/foo", + decimals: 4, + symbol: "FOO", + logoURI: "https://s2.coinmarketcap.com/static/img/coins/64x64/5994.png", + priceId: "gno.land/r/foo" }, - amount: "121", - price: "$0.00", - balance: "0", - }, - to: { - token: { - path: "HEX", - name: "HEX", - symbol: "HEX", - logoURI: - "https://raw.githubusercontent.com/Uniswap/assets/master/blockchains/ethereum/assets/0x2b591e99afE9f32eAA6214f7B7629768c40Eeb39/logo.png", + tokenAAmount: "0", + tokenABalance: "0", + tokenAUSD: 0, + tokenAUSDStr: "0", + tokenB: { + chainId: "dev", + createdAt: "2023-10-17T05:58:00+09:00", + name: "Foo", + address: "g1evezrh92xaucffmtgsaa3rvmz5s8kedffsg469", + path: "gno.land/r/foo", + decimals: 4, + symbol: "FOO", + logoURI: "https://s2.coinmarketcap.com/static/img/coins/64x64/5994.png", + priceId: "gno.land/r/foo" }, - amount: "5000", - price: "$0.00", - balance: "0", + tokenBAmount: "0", + tokenBBalance: "0", + tokenBUSD: 0, + tokenBUSDStr: "0", + direction: "EXACT_IN", + slippage: 0, }, swapNow: action("swapNow"), }; diff --git a/packages/web/src/components/home/home-swap/HomeSwap.tsx b/packages/web/src/components/home/home-swap/HomeSwap.tsx index 990aa3f69..28dcb3f58 100644 --- a/packages/web/src/components/home/home-swap/HomeSwap.tsx +++ b/packages/web/src/components/home/home-swap/HomeSwap.tsx @@ -1,17 +1,15 @@ -import React, { useCallback, useState } from "react"; +import React, { useCallback } from "react"; import { wrapper } from "./HomeSwap.styles"; import IconSettings from "@components/common/icons/IconSettings"; import Button, { ButtonHierarchy } from "@components/common/button/Button"; import SelectPairButton from "@components/common/select-pair-button/SelectPairButton"; import IconSwapArrowDown from "@components/common/icons/IconSwapArrowDown"; -import { DeviceSize } from "@styles/media"; -import { SwapTokenModel } from "@models/swap/swap-token-model"; +import { SwapTokenInfo } from "@models/swap/swap-token-info"; +import { useWindowSize } from "@hooks/common/use-window-size"; interface HomeSwapProps { - from: SwapTokenModel; - to: SwapTokenModel; + swapTokenInfo: SwapTokenInfo; swapNow: () => void; - windowSize: number; } function isAmount(str: string) { @@ -20,13 +18,10 @@ function isAmount(str: string) { } const HomeSwap: React.FC = ({ - from, - to, + swapTokenInfo, swapNow, - windowSize, }) => { - const [fromAmount, setFromAmount] = useState(from.amount); - const [toAmount, setToAmount] = useState(to.amount); + const { breakpoint } = useWindowSize(); const onChangeFromAmount = useCallback( (e: React.ChangeEvent) => { @@ -34,7 +29,6 @@ const HomeSwap: React.FC = ({ if (value !== "" && !isAmount(value)) return; - setFromAmount(value); // TODO // - mapT0AmountToT0Price // - mapT0AmpuntT1Amount @@ -49,7 +43,6 @@ const HomeSwap: React.FC = ({ if (value !== "" && !isAmount(value)) return; - setToAmount(value); // TODO // - mapT1AmountToT1Price // - mapT1AmpuntT0Amount @@ -62,7 +55,7 @@ const HomeSwap: React.FC = ({ swapNow(); }, [swapNow]); - return windowSize > DeviceSize.mobile ? ( + return breakpoint === "web" ? (
Swap @@ -75,34 +68,34 @@ const HomeSwap: React.FC = ({
- +
- {from.price} - Balance : {from.balance} + {swapTokenInfo.tokenAUSDStr} + Balance : {swapTokenInfo.tokenABalance}
- +
- {to.price} - Balance : {to.balance} + {swapTokenInfo.tokenBUSDStr} + Balance : {swapTokenInfo.tokenBBalance}
diff --git a/packages/web/src/components/incentivize/pool-incentivize-select-pool-item/PoolIncentivizeSelectPoolItem.stories.tsx b/packages/web/src/components/incentivize/pool-incentivize-select-pool-item/PoolIncentivizeSelectPoolItem.stories.tsx index f9ee0eaa2..721f38681 100644 --- a/packages/web/src/components/incentivize/pool-incentivize-select-pool-item/PoolIncentivizeSelectPoolItem.stories.tsx +++ b/packages/web/src/components/incentivize/pool-incentivize-select-pool-item/PoolIncentivizeSelectPoolItem.stories.tsx @@ -1,8 +1,8 @@ -import { toPoolSelectItemInfo } from "@models/pool/info/pool-select-item-info"; import PoolIncentivizeSelectPoolItem, { type PoolIncentivizeSelectPoolItemProps } from "./PoolIncentivizeSelectPoolItem"; import { Meta, StoryObj } from "@storybook/react"; import React from "react"; import { PoolRepositoryMock } from "@repositories/pool"; +import { PoolMapper } from "@models/pool/mapper/pool-mapper"; const poolRepository = new PoolRepositoryMock(); const pools = (await poolRepository.getPools()).pools; @@ -21,7 +21,7 @@ export default { } as Meta; -const poolSelectItem = toPoolSelectItemInfo(pools[0]); +const poolSelectItem = PoolMapper.toPoolSelectItemInfo(pools[0]); export const Default: StoryObj = { args: { diff --git a/packages/web/src/components/incentivize/pool-incentivize-select-pool-item/PoolIncentivizeSelectPoolItem.tsx b/packages/web/src/components/incentivize/pool-incentivize-select-pool-item/PoolIncentivizeSelectPoolItem.tsx index d99d8c665..36ba9705c 100644 --- a/packages/web/src/components/incentivize/pool-incentivize-select-pool-item/PoolIncentivizeSelectPoolItem.tsx +++ b/packages/web/src/components/incentivize/pool-incentivize-select-pool-item/PoolIncentivizeSelectPoolItem.tsx @@ -41,8 +41,7 @@ const PoolIncentivizeSelectPoolItem: React.FC { diff --git a/packages/web/src/components/incentivize/pool-incentivize-select-pool/PoolIncentivizeSelectPool.stories.tsx b/packages/web/src/components/incentivize/pool-incentivize-select-pool/PoolIncentivizeSelectPool.stories.tsx index 935295d2c..23d839f90 100644 --- a/packages/web/src/components/incentivize/pool-incentivize-select-pool/PoolIncentivizeSelectPool.stories.tsx +++ b/packages/web/src/components/incentivize/pool-incentivize-select-pool/PoolIncentivizeSelectPool.stories.tsx @@ -2,10 +2,11 @@ import PoolIncentivizeSelectPool from "./PoolIncentivizeSelectPool"; import { ComponentStory, Meta } from "@storybook/react"; import { useCallback, useState } from "react"; import { PoolRepositoryMock } from "@repositories/pool"; -import { PoolSelectItemInfo, toPoolSelectItemInfo } from "@models/pool/info/pool-select-item-info"; +import { PoolSelectItemInfo } from "@models/pool/info/pool-select-item-info"; +import { PoolMapper } from "@models/pool/mapper/pool-mapper"; const poolRepository = new PoolRepositoryMock(); -const pools = (await poolRepository.getPools()).pools.map(toPoolSelectItemInfo); +const pools = (await poolRepository.getPools()).pools.map(PoolMapper.toPoolSelectItemInfo); export default { title: "incentivize/PoolIncentivizeSelectPool", diff --git a/packages/web/src/components/incentivize/pool-incentivize/PoolIncentivize.tsx b/packages/web/src/components/incentivize/pool-incentivize/PoolIncentivize.tsx index 86b374e90..c4629c4f3 100644 --- a/packages/web/src/components/incentivize/pool-incentivize/PoolIncentivize.tsx +++ b/packages/web/src/components/incentivize/pool-incentivize/PoolIncentivize.tsx @@ -8,8 +8,9 @@ import { PoolIncentivizeWrapper } from "./PoolIncentivize.styles"; import PoolIncentivizeSelectPool from "../pool-incentivize-select-pool/PoolIncentivizeSelectPool"; import { PoolModel } from "@models/pool/pool-model"; import { TokenBalanceInfo } from "@models/token/token-balance-info"; -import { PoolSelectItemInfo, toPoolSelectItemInfo } from "@models/pool/info/pool-select-item-info"; +import { PoolSelectItemInfo } from "@models/pool/info/pool-select-item-info"; import { PoolDetailModel } from "@models/pool/pool-detail-model"; +import { PoolMapper } from "@models/pool/mapper/pool-mapper"; export interface DistributionPeriodDate { year: number; @@ -54,11 +55,11 @@ const PoolIncentivize: React.FC = ({ }) => { const selectedItem = useMemo((): PoolSelectItemInfo | null => { - return selectedPool ? toPoolSelectItemInfo(selectedPool) : null; + return selectedPool ? PoolMapper.toPoolSelectItemInfo(selectedPool) : null; }, [selectedPool]); const poolSelectItems = useMemo((): PoolSelectItemInfo[] => { - return pools.map(toPoolSelectItemInfo); + return pools.map(PoolMapper.toPoolSelectItemInfo); }, [pools]); return ( diff --git a/packages/web/src/components/incentivize/set-reward-amount/SetRewardAmount.tsx b/packages/web/src/components/incentivize/set-reward-amount/SetRewardAmount.tsx index 93eb40749..012a11063 100644 --- a/packages/web/src/components/incentivize/set-reward-amount/SetRewardAmount.tsx +++ b/packages/web/src/components/incentivize/set-reward-amount/SetRewardAmount.tsx @@ -39,7 +39,7 @@ const SetRewardAmount: React.FC = ({ className="amount" value={amount} onChange={onChangeAmount} - placeholder={amount === "" ? "0" : ""} + placeholder="0" />
diff --git a/packages/web/src/components/remove/remove-liquidity-select-list-item/RemoveLiquiditySelectListItem.stories.tsx b/packages/web/src/components/remove/remove-liquidity-select-list-item/RemoveLiquiditySelectListItem.stories.tsx deleted file mode 100644 index d6270c065..000000000 --- a/packages/web/src/components/remove/remove-liquidity-select-list-item/RemoveLiquiditySelectListItem.stories.tsx +++ /dev/null @@ -1,29 +0,0 @@ -import React, { useState } from "react"; -import { ComponentStory, ComponentMeta } from "@storybook/react"; -import RemoveLiquiditySelectListItem from "./RemoveLiquiditySelectListItem"; -import { action } from "@storybook/addon-actions"; -import LPPositionData from "@repositories/position/mock/positions.json"; - -export default { - title: "remove liquidity/RemoveLiquiditySelectListItem", - component: RemoveLiquiditySelectListItem, -} as ComponentMeta; - - -const Template: ComponentStory = args => { - const [selected, setSelected] = useState(false); - return ( - setSelected(!selected)} - /> - ); -}; - -export const Default = Template.bind({}); -Default.args = { - lpPosition: LPPositionData.stakedPositions[0], - selected: false, - select: action("select"), -}; diff --git a/packages/web/src/components/remove/remove-liquidity-select-list/RemoveLiquiditySelectList.stories.tsx b/packages/web/src/components/remove/remove-liquidity-select-list/RemoveLiquiditySelectList.stories.tsx deleted file mode 100644 index c9918a7f3..000000000 --- a/packages/web/src/components/remove/remove-liquidity-select-list/RemoveLiquiditySelectList.stories.tsx +++ /dev/null @@ -1,53 +0,0 @@ -import React, { useCallback, useMemo, useState } from "react"; -import { ComponentStory, ComponentMeta } from "@storybook/react"; -import RemoveLiquiditySelectList from "./RemoveLiquiditySelectList"; -import LPPositionData from "@repositories/position/mock/positions.json"; - -export default { - title: "remove lpPosition/RemoveLiquiditySelectList", - component: RemoveLiquiditySelectList, -} as ComponentMeta; - -const Template: ComponentStory = args => { - const [selectedIds, setSelectedIds] = useState([]); - - const unstakedLiquidities = useMemo(() => { - return args.lpPositions; - }, [args.lpPositions]); - - const selectedAll = useMemo(() => { - return unstakedLiquidities.length === selectedIds.length; - }, [selectedIds.length, unstakedLiquidities.length]); - - const selectAll = useCallback(() => { - if (selectedAll) { - setSelectedIds([]); - return; - } - const selectedIds = unstakedLiquidities.map(lpPosition => lpPosition.lpRewardId); - setSelectedIds(selectedIds); - }, [selectedAll, unstakedLiquidities]); - - const select = useCallback((id: string) => { - if (selectedIds.includes(id)) { - setSelectedIds(selectedIds.filter((selectedId => selectedId !== id))); - return; - } - setSelectedIds([...selectedIds, id]); - }, [selectedIds]); - - return ( - - ); -}; - -export const Default = Template.bind({}); -Default.args = { - lpPositions: LPPositionData.stakedPositions, -}; diff --git a/packages/web/src/components/remove/remove-liquidity-select-result/RemoveLiquiditySelectResult.stories.tsx b/packages/web/src/components/remove/remove-liquidity-select-result/RemoveLiquiditySelectResult.stories.tsx deleted file mode 100644 index ecbb575ca..000000000 --- a/packages/web/src/components/remove/remove-liquidity-select-result/RemoveLiquiditySelectResult.stories.tsx +++ /dev/null @@ -1,19 +0,0 @@ -import React from "react"; -import { ComponentStory, ComponentMeta } from "@storybook/react"; -import RemoveLiquiditySelectResult from "./RemoveLiquiditySelectResult"; -import PositionData from "@repositories/position/mock/positions.json"; - - -export default { - title: "remove liquidity/RemoveLiquiditySelectResult", - component: RemoveLiquiditySelectResult, -} as ComponentMeta; - -const Template: ComponentStory = args => ( - -); - -export const Default = Template.bind({}); -Default.args = { - selectedLiquidities: PositionData.stakedPositions, -}; diff --git a/packages/web/src/components/remove/remove-liquidity/RemoveLiquidity.stories.tsx b/packages/web/src/components/remove/remove-liquidity/RemoveLiquidity.stories.tsx deleted file mode 100644 index 2c486b739..000000000 --- a/packages/web/src/components/remove/remove-liquidity/RemoveLiquidity.stories.tsx +++ /dev/null @@ -1,52 +0,0 @@ -import React, { useCallback, useMemo, useState } from "react"; -import { ComponentStory, ComponentMeta } from "@storybook/react"; -import RemoveLiquidity from "./RemoveLiquidity"; -import LPPositionData from "@repositories/position/mock/positions.json"; - -export default { - title: "remove position/RemoveLiquidity", - component: RemoveLiquidity, -} as ComponentMeta; - -const Template: ComponentStory = (args) => { - const [selectedIds, setSelectedIds] = useState([]); - - const unstakedLiquidities = useMemo(() => { - return args.lpPositions; - }, [args.lpPositions]); - - const selectedAll = useMemo(() => { - return unstakedLiquidities.length === selectedIds.length; - }, [selectedIds.length, unstakedLiquidities.length]); - - const selectAll = useCallback(() => { - if (selectedAll) { - setSelectedIds([]); - return; - } - const selectedIds = unstakedLiquidities.map(position => position.lpRewardId); - setSelectedIds(selectedIds); - }, [selectedAll, unstakedLiquidities]); - - const select = useCallback((id: string) => { - if (selectedIds.includes(id)) { - setSelectedIds(selectedIds.filter((selectedId => selectedId !== id))); - return; - } - setSelectedIds([...selectedIds, id]); - }, [selectedIds]); - - return ( - - ); -}; - -export const Default = Template.bind({}); -Default.args = { - lpPositions: LPPositionData.stakedPositions -}; diff --git a/packages/web/src/components/swap/confirm-swap-modal/ConfirmSwapModal.spec.tsx b/packages/web/src/components/swap/confirm-swap-modal/ConfirmSwapModal.spec.tsx deleted file mode 100644 index f6c01788b..000000000 --- a/packages/web/src/components/swap/confirm-swap-modal/ConfirmSwapModal.spec.tsx +++ /dev/null @@ -1,49 +0,0 @@ -import ConfirmSwapModal from "./ConfirmSwapModal"; -import { render } from "@testing-library/react"; -import { Provider as JotaiProvider } from "jotai"; -import GnoswapThemeProvider from "@providers/gnoswap-theme-provider/GnoswapThemeProvider"; -import { dummySwapGasInfo } from "@containers/swap-container/SwapContainer"; -import { DEVICE_TYPE } from "@styles/media"; - -describe("ConfirmSwapModal Component", () => { - it("should render", () => { - const mockProps = { - onConfirmModal: () => null, - submitSwap: () => null, - tolerance: "", - submit: false, - isFetching: false, - from: { - token: "USDCoin", - symbol: "USDC", - amount: "121", - price: "$0.00", - gnosExchangePrice: "1250", - usdExchangePrice: "($1541.55)", - balance: "0", - logoURI: - "https://raw.githubusercontent.com/Uniswap/assets/master/blockchains/ethereum/assets/0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48/logo.png", - }, - to: { - token: "HEX", - symbol: "HEX", - amount: "5000", - price: "$0.00", - gnosExchangePrice: "1250", - usdExchangePrice: "($1541.55)", - balance: "0", - logoURI: - "https://raw.githubusercontent.com/Uniswap/assets/master/blockchains/ethereum/assets/0x2b591e99afE9f32eAA6214f7B7629768c40Eeb39/logo.png", - }, - swapGasInfo: dummySwapGasInfo, - breakpoint: DEVICE_TYPE.WEB, - }; - render( - - - - - , - ); - }); -}); diff --git a/packages/web/src/components/swap/confirm-swap-modal/ConfirmSwapModal.stories.tsx b/packages/web/src/components/swap/confirm-swap-modal/ConfirmSwapModal.stories.tsx index fa5c2cdc4..b33acd19c 100644 --- a/packages/web/src/components/swap/confirm-swap-modal/ConfirmSwapModal.stories.tsx +++ b/packages/web/src/components/swap/confirm-swap-modal/ConfirmSwapModal.stories.tsx @@ -4,8 +4,81 @@ import { css } from "@emotion/react"; import { action } from "@storybook/addon-actions"; import ConfirmSwapModal from "./ConfirmSwapModal"; -import { dummySwapGasInfo } from "@containers/swap-container/SwapContainer"; -import { DEVICE_TYPE } from "@styles/media"; +import { SwapSummaryInfo } from "@models/swap/swap-summary-info"; +import { SwapTokenInfo } from "@models/swap/swap-token-info"; + +const swapTokenInfo: SwapTokenInfo = { + tokenA: { + chainId: "test3", + address: "0x111111111117dC0aa78b770fA6A738034120C302", + path: "gno.land/r/demo/1inch", + name: "1inch", + symbol: "1INCH", + decimals: 6, + logoURI: "https://assets.coingecko.com/coins/images/13469/thumb/1inch-token.png?1608803028", + priceId: "1inch", + createdAt: "1999-01-01T00:00:01Z" + }, + tokenAAmount: "", + tokenABalance: "", + tokenAUSD: 0, + tokenAUSDStr: "0", + tokenB: { + chainId: "test3", + address: "0x111111111117dC0aa78b770fA6A738034120C302", + path: "gno.land/r/demo/1inch", + name: "1inch", + symbol: "1INCH", + decimals: 6, + logoURI: "https://assets.coingecko.com/coins/images/13469/thumb/1inch-token.png?1608803028", + priceId: "1inch", + createdAt: "1999-01-01T00:00:01Z" + }, + tokenBAmount: "", + tokenBBalance: "", + tokenBUSD: 0, + tokenBUSDStr: "0", + direction: "EXACT_IN", + slippage: 10 +}; + +const swapSummaryInfo: SwapSummaryInfo = { + tokenA: { + chainId: "test3", + address: "0x111111111117dC0aa78b770fA6A738034120C302", + path: "gno.land/r/demo/1inch", + name: "1inch", + symbol: "1INCH", + decimals: 6, + logoURI: "https://assets.coingecko.com/coins/images/13469/thumb/1inch-token.png?1608803028", + priceId: "1inch", + createdAt: "1999-01-01T00:00:01Z" + }, + tokenB: { + chainId: "test3", + address: "0x111111111117dC0aa78b770fA6A738034120C302", + path: "gno.land/r/demo/1inch", + name: "1inch", + symbol: "1INCH", + decimals: 6, + logoURI: "https://assets.coingecko.com/coins/images/13469/thumb/1inch-token.png?1608803028", + priceId: "1inch", + createdAt: "1999-01-01T00:00:01Z" + }, + swapDirection: "EXACT_IN", + swapRate: 1.14, + swapRateUSD: 1.14, + priceImpact: 0.3, + guaranteedAmount: { + amount: 45124, + currency: "GNOT" + }, + gasFee: { + amount: 0.000001, + currency: "GNOT" + }, + gasFeeUSD: 0.1 +}; export default { title: "swap/ConfirmSwapModal", @@ -22,35 +95,12 @@ const Template: ComponentStory = args => ( export const Default = Template.bind({}); Default.args = { - onConfirmModal: action("onConfirmModal"), - submitSwap: action("submitSwap"), - swapGasInfo: dummySwapGasInfo, - breakpoint: DEVICE_TYPE.WEB, - tolerance: "5", - submit: false, - isFetching: true, - from: { - token: "USDCoin", - symbol: "USDC", - amount: "121", - price: "$0.00", - gnosExchangePrice: "1250", - usdExchangePrice: "($1541.55)", - balance: "0", - logoURI: - "https://raw.githubusercontent.com/Uniswap/assets/master/blockchains/ethereum/assets/0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48/logo.png", - }, - to: { - token: "HEX", - symbol: "HEX", - amount: "5000", - price: "$0.00", - gnosExchangePrice: "1250", - usdExchangePrice: "($1541.55)", - balance: "0", - logoURI: - "https://raw.githubusercontent.com/Uniswap/assets/master/blockchains/ethereum/assets/0x2b591e99afE9f32eAA6214f7B7629768c40Eeb39/logo.png", - }, + submitted: false, + swapTokenInfo, + swapSummaryInfo, + swapResult: null, + swap: action("swap"), + close: action("close"), }; const wrapper = () => css` diff --git a/packages/web/src/components/swap/confirm-swap-modal/ConfirmSwapModal.styles.ts b/packages/web/src/components/swap/confirm-swap-modal/ConfirmSwapModal.styles.ts index c0477ae02..9b2f41a43 100644 --- a/packages/web/src/components/swap/confirm-swap-modal/ConfirmSwapModal.styles.ts +++ b/packages/web/src/components/swap/confirm-swap-modal/ConfirmSwapModal.styles.ts @@ -171,7 +171,7 @@ export const ConfirmModal = styled.div` border-radius: 8px; background: ${({ theme }) => theme.color.background01}; border: 1px solid ${({ theme }) => theme.color.border02}; - .ocin-info { + .coin-info { ${mixins.flexbox("row", "center", "flex-start")}; gap: 4px; .gnos-price { diff --git a/packages/web/src/components/swap/confirm-swap-modal/ConfirmSwapModal.tsx b/packages/web/src/components/swap/confirm-swap-modal/ConfirmSwapModal.tsx index 49c40a210..fa6c7a923 100644 --- a/packages/web/src/components/swap/confirm-swap-modal/ConfirmSwapModal.tsx +++ b/packages/web/src/components/swap/confirm-swap-modal/ConfirmSwapModal.tsx @@ -1,65 +1,94 @@ -import React, { useEffect, useRef } from "react"; +import React, { useMemo, useRef } from "react"; import { ConfirmSwapModalBackground, ConfirmModal, SwapDivider, } from "./ConfirmSwapModal.styles"; -import { - SwapData, - SwapGasInfo, -} from "@containers/swap-container/SwapContainer"; -import { TokenInfo } from "../swap-card/SwapCard"; import IconClose from "@components/common/icons/IconCancel"; import IconSwapArrowDown from "@components/common/icons/IconSwapArrowDown"; import IconInfo from "@components/common/icons/IconInfo"; import Button, { ButtonHierarchy } from "@components/common/button/Button"; -import { DEVICE_TYPE } from "@styles/media"; import IconSuccess from "@components/common/icons/IconSuccess"; import IconOpenLink from "@components/common/icons/IconOpenLink"; import IconFailed from "@components/common/icons/IconFailed"; import LoadingSpinner from "@components/common/loading-spinner/LoadingSpinner"; +import { SwapTokenInfo } from "@models/swap/swap-token-info"; +import { SwapSummaryInfo, swapDirectionToGuaranteedType } from "@models/swap/swap-summary-info"; +import { SwapResultInfo } from "@models/swap/swap-result-info"; +import useModalCloseEvent from "@hooks/common/use-modal-close-event"; +import { numberToUSD, toNumberFormat } from "@utils/number-utils"; +import { numberToFormat } from "@utils/string-utils"; +import BigNumber from "bignumber.js"; interface ConfirmSwapModalProps { - onConfirmModal: () => void; - submitSwap: (event: React.MouseEvent) => void; - from: TokenInfo; - to: TokenInfo; - swapGasInfo: SwapGasInfo; - breakpoint: DEVICE_TYPE; - tolerance: string; - submit: boolean; - isFetching: boolean; - swapResult: SwapData | null; + submitted: boolean; + swapTokenInfo: SwapTokenInfo; + swapSummaryInfo: SwapSummaryInfo; + swapResult: SwapResultInfo | null; + + swap: (event: React.MouseEvent) => void; + close: () => void; } const ConfirmSwapModal: React.FC = ({ - onConfirmModal, - submitSwap, - from, - to, - swapGasInfo, - breakpoint, - tolerance, - submit, - isFetching, + submitted, + swapTokenInfo, + swapSummaryInfo, swapResult, + swap, + close, }) => { const menuRef = useRef(null); - useEffect(() => { - const closeMenu = (e: MouseEvent) => { - if (menuRef.current && menuRef.current.contains(e.target as Node)) { - return; - } else { - e.stopPropagation(); - onConfirmModal(); - } - }; - window.addEventListener("click", closeMenu, true); - return () => { - window.removeEventListener("click", closeMenu, true); - }; - }, [menuRef, onConfirmModal]); + useModalCloseEvent(menuRef, close); + + const tokenAAmountStr = useMemo(() => { + return BigNumber(swapTokenInfo.tokenAAmount).toFormat(); + }, [swapTokenInfo.tokenAAmount]); + + const tokenBAmountStr = useMemo(() => { + return BigNumber(swapTokenInfo.tokenBAmount).toFormat(); + }, [swapTokenInfo.tokenBAmount]); + + const swapRateDescription = useMemo(() => { + const { tokenA, tokenB, swapRate } = swapSummaryInfo; + return `1 ${tokenA.symbol} = ${numberToFormat(swapRate)} ${tokenB.symbol}`; + }, [swapSummaryInfo]); + + const swapRateUSDStr = useMemo(() => { + const swapRateStr = numberToUSD(swapSummaryInfo.swapRateUSD); + return `(${swapRateStr})`; + }, [swapSummaryInfo.swapRateUSD]); + + const priceImpactStr = useMemo(() => { + const priceImpact = swapSummaryInfo.priceImpact; + return `${priceImpact}%`; + }, [swapSummaryInfo.priceImpact]); + + const slippageStr = useMemo(() => { + const slippage = swapTokenInfo.slippage; + return `${slippage}%`; + }, [swapTokenInfo.slippage]); + + const guaranteedTypeStr = useMemo(() => { + const swapDirection = swapSummaryInfo.swapDirection; + return swapDirectionToGuaranteedType(swapDirection); + }, [swapSummaryInfo.swapDirection]); + + const guaranteedStr = useMemo(() => { + const { amount, currency } = swapSummaryInfo.guaranteedAmount; + return `${toNumberFormat(amount)} ${currency}`; + }, [swapSummaryInfo.guaranteedAmount]); + + const gasFeeStr = useMemo(() => { + const { amount, currency } = swapSummaryInfo.gasFee; + return `${toNumberFormat(amount)} ${currency}`; + }, [swapSummaryInfo.gasFee]); + + const gasFeeUSDStr = useMemo(() => { + const gasFeeUSD = swapSummaryInfo.gasFeeUSD; + return `$${toNumberFormat(gasFeeUSD)}`; + }, [swapSummaryInfo.gasFeeUSD]); return ( @@ -67,107 +96,33 @@ const ConfirmSwapModal: React.FC = ({
Confirm Swap -
+
- {submit ? ( - <> - {isFetching && ( - <> -
- -
-
- Waiting for Confirmation - - Swapping 0.1 GNOS for 0.12 GNOT - -
- Confirm this transaction in your wallet -
-
- - )} - {!isFetching && swapResult?.success && ( - <> -
- -
-
- Transaction Submitted -
- View Transaction -
{ - window.open(swapResult?.transaction, "_blank"); - }} - > - -
-
-
-
-
- - )} - {!isFetching && !swapResult?.success && ( - <> -
- -
-
- Transaction Rejected -
- - Your transaction has been rejected. Please try again. - -
-
-
-
- - )} - + {submitted ? ( + ) : ( <>
- {from.amount} + {tokenAAmountStr}
logo - {from.symbol} + {swapSummaryInfo.tokenA.symbol}
- {from.price} + {swapTokenInfo.tokenAUSDStr}
@@ -177,30 +132,29 @@ const ConfirmSwapModal: React.FC = ({
- {to.amount} + {tokenBAmountStr}
logo - {to.symbol} + {swapSummaryInfo.tokenB.symbol}
- {to.price} + {swapTokenInfo.tokenBUSDStr}
-
+
- {from.amount} {from.symbol} = {from.gnosExchangePrice}{" "} - GNOS + {swapRateDescription} - {from.usdExchangePrice} + {swapRateUSDStr}
@@ -208,26 +162,26 @@ const ConfirmSwapModal: React.FC = ({
Price Impact - {swapGasInfo.priceImpact} + {priceImpactStr}
Max. Slippage - {tolerance}% + {slippageStr}
- Min. Received + {guaranteedTypeStr} - {swapGasInfo.minReceived} + {guaranteedStr}
Gas Fee - {swapGasInfo.gasFee} GNOT{" "} + {gasFeeStr} - ({swapGasInfo.usdExchangeGasFee}) + ({gasFeeUSDStr})
@@ -238,11 +192,11 @@ const ConfirmSwapModal: React.FC = ({ text="Confrim Swap" style={{ fullWidth: true, - height: breakpoint === DEVICE_TYPE.MOBILE ? 41 : 57, + height: 57, fontType: "body7", hierarchy: ButtonHierarchy.Primary, }} - onClick={submitSwap} + onClick={swap} />
@@ -253,4 +207,98 @@ const ConfirmSwapModal: React.FC = ({ ); }; +interface ConfirmSwapResultProps { + swapResult: SwapResultInfo | null; + close: () => void; +} + +const ConfirmSwapResult: React.FC = ({ + swapResult, + close, +}) => { + + if (swapResult === null) { + return ( + <> +
+ +
+
+ Waiting for Confirmation + + Swapping 0.1 GNOS for 0.12 GNOT + +
+ Confirm this transaction in your wallet +
+
+ + ); + } + + if (swapResult.success) { + return ( + <> +
+ +
+
+ Transaction Submitted +
+ View Transaction +
{ + window.open(swapResult?.hash, "_blank"); + }} + > + +
+
+
+
+
+ + ); + } + + return ( + <> +
+ +
+
+ Transaction Rejected +
+ + Your transaction has been rejected. Please try again. + +
+
+
+
+ + ); +}; + export default ConfirmSwapModal; diff --git a/packages/web/src/components/swap/select-token-modal/SelectTokenModal.spec.tsx b/packages/web/src/components/swap/select-token-modal/SelectTokenModal.spec.tsx deleted file mode 100644 index 6226d8326..000000000 --- a/packages/web/src/components/swap/select-token-modal/SelectTokenModal.spec.tsx +++ /dev/null @@ -1,24 +0,0 @@ -import SearchMenuModal from "./SelectTokenModal"; -import { render } from "@testing-library/react"; -import { Provider as JotaiProvider } from "jotai"; -import GnoswapThemeProvider from "@providers/gnoswap-theme-provider/GnoswapThemeProvider"; -import { coinList } from "@containers/swap-container/SwapContainer"; - -describe("SearchMenuModal Component", () => { - it("should render", () => { - const mockProps = { - search: () => null, - keyword: "", - onSelectTokenModal: () => null, - coinList: coinList(), - changeToken: () => null, - }; - render( - - - - - , - ); - }); -}); diff --git a/packages/web/src/components/swap/select-token-modal/SelectTokenModal.stories.tsx b/packages/web/src/components/swap/select-token-modal/SelectTokenModal.stories.tsx deleted file mode 100644 index 56b20dceb..000000000 --- a/packages/web/src/components/swap/select-token-modal/SelectTokenModal.stories.tsx +++ /dev/null @@ -1,41 +0,0 @@ -import React from "react"; -import { ComponentStory, ComponentMeta } from "@storybook/react"; -import { css } from "@emotion/react"; -import { action } from "@storybook/addon-actions"; - -import SelectTokenModal from "./SelectTokenModal"; -import { coinList } from "@containers/swap-container/SwapContainer"; - -export default { - title: "swap/SelectTokenModal", - component: SelectTokenModal, -} as ComponentMeta; - -const Template: ComponentStory = args => ( -
-
- -
-
-); - -export const Default = Template.bind({}); -Default.args = { - search: action("search"), - onSelectTokenModal: action("onClick"), - keyword: "", - coinList: coinList(), - changeToken: action("changeToken"), -}; - -const wrapper = () => css` - display: flex; - width: 100%; - align-items: center; - justify-content: center; - margin-top: 50px; -`; - -const contentWrap = () => css` - width: 500px; -`; diff --git a/packages/web/src/components/swap/select-token-modal/SelectTokenModal.styles.ts b/packages/web/src/components/swap/select-token-modal/SelectTokenModal.styles.ts deleted file mode 100644 index ae0b2bd45..000000000 --- a/packages/web/src/components/swap/select-token-modal/SelectTokenModal.styles.ts +++ /dev/null @@ -1,209 +0,0 @@ -import mixins from "@styles/mixins"; -import { fonts } from "@constants/font.constant"; -import styled from "@emotion/styled"; -import { Z_INDEX } from "@styles/zIndex"; -import { media } from "@styles/media"; - -export const SearchModalBackground = styled.div` - position: fixed; - top: 0px; - bottom: 0px; - left: 0px; - right: 0px; - width: 100%; - height: 100%; - background: rgba(10, 14, 23, 0.7); - z-index: ${Z_INDEX.modalOverlay}; -`; - -export const SearchModal = styled.div` - ${mixins.flexbox("column", "flex-start", "flex-start")}; - position: absolute; - width: 460px; - padding: 24px 0px 16px 0px; - gap: 24px; - top: calc(40vh - 230px); - left: calc(50vw - 230px); - border-radius: 8px; - box-shadow: 10px 14px 48px 0px rgba(0, 0, 0, 0.12); - border: 1px solid ${({ theme }) => theme.color.border02}; - background-color: ${({ theme }) => theme.color.background06}; - ${media.mobile} { - width: 328px; - top: calc(40vh - 164px); - left: calc(50vw - 164px); - padding: 16px 0px; - } - - .modal-body { - ${mixins.flexbox("column", "flex-start", "flex-start")}; - width: 100%; - padding: 0px 24px; - gap: 24px; - ${media.mobile} { - padding: 0px 12px; - gap: 16px; - } - .modal-header { - ${mixins.flexbox("row", "center", "space-between")}; - width: 100%; - span { - color: ${({ theme }) => theme.color.text02}; - ${fonts.body7} - font-weight: 600; - ${media.mobile} { - ${fonts.body9} - } - } - - .close-wrap { - ${mixins.flexbox("row", "center", "center")}; - cursor: pointer; - width: 24px; - height: 24px; - .close-icon { - width: 24px; - height: 24px; - * { - fill: ${({ theme }) => theme.color.icon01}; - } - } - } - } - .search-wrap { - ${mixins.flexbox("row", "center", "space-between")}; - width: 100%; - padding: 12px 16px; - border-radius: 8px; - ${fonts.body9} - ${media.mobile} { - padding: 8px 12px; - ${fonts.body11} - } - - &:focus-within { - background-color: ${({ theme }) => theme.color.background13}; - border: 1px solid ${({ theme }) => theme.color.border03}; - color: ${({ theme }) => theme.color.text01}; - .search-icon * { - fill: ${({ theme }) => theme.color.icon03}; - } - } - - &:not(:focus-within, .empty-status) { - border: 1px solid ${({ theme }) => theme.color.border11}; - color: ${({ theme }) => theme.color.text01}; - .search-icon * { - fill: ${({ theme }) => theme.color.icon05}; - } - } - - &:not(:focus-within).empty-status { - color: ${({ theme }) => theme.color.text17}; - border: 1px solid ${({ theme }) => theme.color.border02}; - .search-icon * { - fill: ${({ theme }) => theme.color.icon08}; - } - } - } - .coin-select { - display: grid; - width: 100%; - grid-template-rows: auto; - col-gap: 8px; - row-gap: 8px; - grid-template-columns: repeat(4, 1fr); - - ${media.mobile} { - grid-template-columns: repeat(3, 1fr); - } - .coin-button { - ${mixins.flexbox("row", "center", "flex-start")}; - margin: 0 auto; - padding: 6px 12px 6px 6px; - gap: 8px; - border-radius: 36px; - border: 1px solid ${({ theme }) => theme.color.border02}; - background-color: ${({ theme }) => theme.color.background02}; - &:hover { - background-color: ${({ theme }) => theme.color.background09}; - } - cursor: pointer; - span { - color: ${({ theme }) => theme.color.text01}; - ${fonts.body9} - ${media.mobile} { - ${fonts.body11} - } - } - .coin-logo { - width: 24px; - height: 24px; - } - } - } - } - - .list-wrap { - ${mixins.flexbox("column", "flex-start", "flex-start")}; - width: 100%; - gap: 4px; - height: 292px; - ${media.mobile} { - height: 248px; - } - overflow-y: auto; - - .list { - ${mixins.flexbox("row", "center", "space-between")}; - width: 100%; - padding: 16px 24px; - gap: 8px; - ${media.mobile} { - padding: 12px; - } - &:hover { - background-color: ${({ theme }) => theme.color.background09}; - } - cursor: pointer; - .coin-logo { - width: 24px; - height: 24px; - } - .coin-info { - ${mixins.flexbox("row", "center", "flex-start")}; - gap: 8px; - .coin-name { - color: ${({ theme }) => theme.color.text02}; - ${fonts.body8} - ${media.mobile} { - ${fonts.body12} - } - } - .coin-symbol { - color: ${({ theme }) => theme.color.text04}; - ${fonts.body8} - ${media.mobile} { - ${fonts.body12} - } - } - } - .coin-balance { - color: ${({ theme }) => theme.color.text02}; - ${fonts.body7} - ${media.mobile} { - ${fonts.body11} - } - } - } - } -`; - -export const InputStyle = styled.input` - width: 100%; - height: 100%; - margin-right: 16px; - &::placeholder { - color: ${({ theme }) => theme.color.text04}; - } -`; diff --git a/packages/web/src/components/swap/select-token-modal/SelectTokenModal.tsx b/packages/web/src/components/swap/select-token-modal/SelectTokenModal.tsx deleted file mode 100644 index 6c7af3d29..000000000 --- a/packages/web/src/components/swap/select-token-modal/SelectTokenModal.tsx +++ /dev/null @@ -1,101 +0,0 @@ -import React, { useEffect, useRef } from "react"; -import { - SearchModalBackground, - SearchModal, - InputStyle, -} from "./SelectTokenModal.styles"; -import IconSearch from "@components/common/icons/IconSearch"; -import IconClose from "@components/common/icons/IconCancel"; -import { tokenInfo } from "@containers/swap-container/SwapContainer"; - -interface SelectTokenModalProps { - onSelectTokenModal: () => void; - search: (e: React.ChangeEvent) => void; - keyword: string; - coinList: tokenInfo[]; - changeToken: (token: tokenInfo, type: string) => void; -} - -const SelectTokenModal: React.FC = ({ - onSelectTokenModal, - search, - keyword, - coinList, - changeToken, -}) => { - const menuRef = useRef(null); - - useEffect(() => { - const closeMenu = (e: MouseEvent) => { - if (menuRef.current && menuRef.current.contains(e.target as Node)) { - return; - } else { - e.stopPropagation(); - onSelectTokenModal(); - } - }; - window.addEventListener("click", closeMenu, true); - return () => { - window.removeEventListener("click", closeMenu, true); - }; - }, [menuRef, onSelectTokenModal]); - - return ( - - -
-
- Select a token -
- -
-
-
- - -
-
- {coinList.map((data, idx) => ( -
{ - changeToken(data, "from"); - onSelectTokenModal(); - }} - > - logo - {data.symbol} -
- ))} -
-
-
- {coinList.map((data, idx) => ( -
{ - changeToken(data, "from"); - onSelectTokenModal(); - }} - > -
- logo - {data.name} - {data.symbol} -
- {data.balance} -
- ))} -
-
-
- ); -}; - -export default SelectTokenModal; diff --git a/packages/web/src/components/swap/setting-menu-modal/SettingMenuModal.spec.tsx b/packages/web/src/components/swap/setting-menu-modal/SettingMenuModal.spec.tsx index 795026856..5df45dc3a 100644 --- a/packages/web/src/components/swap/setting-menu-modal/SettingMenuModal.spec.tsx +++ b/packages/web/src/components/swap/setting-menu-modal/SettingMenuModal.spec.tsx @@ -6,10 +6,9 @@ import GnoswapThemeProvider from "@providers/gnoswap-theme-provider/GnoswapTheme describe("SettingMenuModal Component", () => { it("should render", () => { const mockProps = { - onSettingMenu: () => null, - tolerance: "", - changeTolerance: () => null, - resetTolerance: () => null, + slippage: 0, + changeSlippage: () => null, + close: () => null, }; render( diff --git a/packages/web/src/components/swap/setting-menu-modal/SettingMenuModal.stories.tsx b/packages/web/src/components/swap/setting-menu-modal/SettingMenuModal.stories.tsx index 799fe98bd..901f8c6da 100644 --- a/packages/web/src/components/swap/setting-menu-modal/SettingMenuModal.stories.tsx +++ b/packages/web/src/components/swap/setting-menu-modal/SettingMenuModal.stories.tsx @@ -1,6 +1,6 @@ import React from "react"; import { ComponentStory, ComponentMeta } from "@storybook/react"; -import { css, Theme } from "@emotion/react"; +import { css } from "@emotion/react"; import { action } from "@storybook/addon-actions"; import SettingMenuModal from "./SettingMenuModal"; @@ -19,10 +19,9 @@ const Template: ComponentStory = args => ( export const Default = Template.bind({}); Default.args = { - onSettingMenu: action("onSettingMenu"), - changeTolerance: action("changeTolerance"), - resetTolerance: action("resetTolerance"), - tolerance: "", + slippage: 0, + changeSlippage: action("changeSlippage"), + close: action("close"), }; const wrapper = () => css` diff --git a/packages/web/src/components/swap/setting-menu-modal/SettingMenuModal.tsx b/packages/web/src/components/swap/setting-menu-modal/SettingMenuModal.tsx index 6a5c82f56..133399a4d 100644 --- a/packages/web/src/components/swap/setting-menu-modal/SettingMenuModal.tsx +++ b/packages/web/src/components/swap/setting-menu-modal/SettingMenuModal.tsx @@ -2,26 +2,27 @@ import Button from "@components/common/button/Button"; import IconClose from "@components/common/icons/IconCancel"; import IconInfo from "@components/common/icons/IconInfo"; import Tooltip from "@components/common/tooltip/Tooltip"; -import React, { useEffect, useRef } from "react"; +import React, { useCallback, useRef } from "react"; import { ModalTooltipWrap, SettingMenuModalWrapper, } from "./SettingMenuModal.styles"; +import useModalCloseEvent from "@hooks/common/use-modal-close-event"; interface SettingMenuModalProps { - onSettingMenu: () => void; - tolerance: string; - changeTolerance: (e: React.ChangeEvent) => void; - resetTolerance: () => void; + slippage: number; + changeSlippage: (value: string) => void; + close: () => void; } const SettingMenuModal: React.FC = ({ - onSettingMenu, - tolerance, - changeTolerance, - resetTolerance, + slippage, + changeSlippage, + close, }) => { const settingMenuRef = useRef(null); + useModalCloseEvent(settingMenuRef, close); + const TooltipFloatingContent = (
@@ -34,30 +35,21 @@ const SettingMenuModal: React.FC = ({ ); - useEffect(() => { - const closeMenu = (e: MouseEvent) => { - if ( - settingMenuRef.current && - settingMenuRef.current.contains(e.target as Node) - ) { - return; - } else { - e.stopPropagation(); - onSettingMenu(); - } - }; - window.addEventListener("click", closeMenu, true); - return () => { - window.removeEventListener("click", closeMenu, true); - }; - }, [settingMenuRef, onSettingMenu]); + const onChangeSlippage = useCallback((event: React.ChangeEvent) => { + const value = event.target.value; + changeSlippage(value); + }, [changeSlippage]); + + const onClickReset = useCallback(() => { + changeSlippage("10"); + }, [changeSlippage]); return (
Settings -
+
@@ -78,14 +70,14 @@ const SettingMenuModal: React.FC = ({ fontType: "p1", textColor: "text20", }} - onClick={resetTolerance} + onClick={onClickReset} />
%
diff --git a/packages/web/src/components/swap/swap-button-tooltip/SwapButtonTooltip.stories.tsx b/packages/web/src/components/swap/swap-button-tooltip/SwapButtonTooltip.stories.tsx index 3873a866a..1f50ac6b5 100644 --- a/packages/web/src/components/swap/swap-button-tooltip/SwapButtonTooltip.stories.tsx +++ b/packages/web/src/components/swap/swap-button-tooltip/SwapButtonTooltip.stories.tsx @@ -2,7 +2,45 @@ import React from "react"; import { ComponentStory, ComponentMeta } from "@storybook/react"; import SwapButtonTooltip from "./SwapButtonTooltip"; import { css } from "@emotion/react"; -import { dummySwapGasInfo } from "@containers/swap-container/SwapContainer"; +import { SwapSummaryInfo } from "@models/swap/swap-summary-info"; + +const swapSummaryInfo: SwapSummaryInfo = { + tokenA: { + chainId: "test3", + address: "0x111111111117dC0aa78b770fA6A738034120C302", + path: "gno.land/r/demo/1inch", + name: "1inch", + symbol: "1INCH", + decimals: 6, + logoURI: "https://assets.coingecko.com/coins/images/13469/thumb/1inch-token.png?1608803028", + priceId: "1inch", + createdAt: "1999-01-01T00:00:01Z" + }, + tokenB: { + chainId: "test3", + address: "0x111111111117dC0aa78b770fA6A738034120C302", + path: "gno.land/r/demo/1inch", + name: "1inch", + symbol: "1INCH", + decimals: 6, + logoURI: "https://assets.coingecko.com/coins/images/13469/thumb/1inch-token.png?1608803028", + priceId: "1inch", + createdAt: "1999-01-01T00:00:01Z" + }, + swapDirection: "EXACT_IN", + swapRate: 1.14, + swapRateUSD: 1.14, + priceImpact: 0.3, + guaranteedAmount: { + amount: 45124, + currency: "GNOT" + }, + gasFee: { + amount: 0.000001, + currency: "GNOT" + }, + gasFeeUSD: 0.1 +}; export default { title: "swap/SwapButtonTooltip", @@ -19,7 +57,7 @@ const Template: ComponentStory = args => ( export const Default = Template.bind({}); Default.args = { - swapGasInfo: dummySwapGasInfo, + swapSummaryInfo, }; const wrapper = () => css` diff --git a/packages/web/src/components/swap/swap-button-tooltip/SwapButtonTooltip.styles.ts b/packages/web/src/components/swap/swap-button-tooltip/SwapButtonTooltip.styles.ts index 68f75a0d5..d397ed3ce 100644 --- a/packages/web/src/components/swap/swap-button-tooltip/SwapButtonTooltip.styles.ts +++ b/packages/web/src/components/swap/swap-button-tooltip/SwapButtonTooltip.styles.ts @@ -5,7 +5,7 @@ import { fonts } from "@constants/font.constant"; export const SwapButtonTooltipWrap = styled.div` ${mixins.flexbox("column", "flex-start", "flex-start")}; width: 300px; - height: 123px + height: 123px; padding: 16px; gap: 8px; ${fonts.body12}; diff --git a/packages/web/src/components/swap/swap-button-tooltip/SwapButtonTooltip.tsx b/packages/web/src/components/swap/swap-button-tooltip/SwapButtonTooltip.tsx index 12c24336d..355e67446 100644 --- a/packages/web/src/components/swap/swap-button-tooltip/SwapButtonTooltip.tsx +++ b/packages/web/src/components/swap/swap-button-tooltip/SwapButtonTooltip.tsx @@ -1,32 +1,55 @@ -import React from "react"; +import React, { useMemo } from "react"; import { IconWrap, SwapButtonTooltipWrap } from "./SwapButtonTooltip.styles"; -import { SwapGasInfo } from "@containers/swap-container/SwapContainer"; import Tooltip from "@components/common/tooltip/Tooltip"; import IconInfo from "@components/common/icons/IconInfo"; +import { SwapSummaryInfo, swapDirectionToGuaranteedType } from "@models/swap/swap-summary-info"; +import { toNumberFormat } from "@utils/number-utils"; interface WalletBalanceDetailInfoProps { - swapGasInfo: SwapGasInfo; + swapSummaryInfo: SwapSummaryInfo; } const SwapButtonTooltip: React.FC = ({ - swapGasInfo, + swapSummaryInfo, }) => { - const TooltipFloatingContent = ( - -
- Price Impact - {swapGasInfo.gasFee} -
-
- Min. Received - {swapGasInfo.minReceived} -
-
- Gas Fee - {swapGasInfo.gasFee} -
-
- ); + const priceImpactStr = useMemo(() => { + const priceImpact = swapSummaryInfo.priceImpact; + return `${priceImpact}%`; + }, [swapSummaryInfo.priceImpact]); + + const guaranteedTypeStr = useMemo(() => { + const swapDirection = swapSummaryInfo.swapDirection; + return swapDirectionToGuaranteedType(swapDirection); + }, [swapSummaryInfo.swapDirection]); + + const guaranteedStr = useMemo(() => { + const { amount, currency } = swapSummaryInfo.guaranteedAmount; + return `${toNumberFormat(amount)} ${currency}`; + }, [swapSummaryInfo.guaranteedAmount]); + + const gasFeeStr = useMemo(() => { + const { amount, currency } = swapSummaryInfo.gasFee; + return `${toNumberFormat(amount)} ${currency}`; + }, [swapSummaryInfo.gasFee]); + + const TooltipFloatingContent = useMemo(() => { + return ( + +
+ Price Impact + {priceImpactStr} +
+
+ {guaranteedTypeStr} + {guaranteedStr} +
+
+ Gas Fee + {gasFeeStr} +
+
+ ); + }, [gasFeeStr, guaranteedStr, guaranteedTypeStr, priceImpactStr]); return ( diff --git a/packages/web/src/components/swap/swap-card-auto-router/SwapCardAutoRouter.spec.tsx b/packages/web/src/components/swap/swap-card-auto-router/SwapCardAutoRouter.spec.tsx index b489b5ed3..169e189af 100644 --- a/packages/web/src/components/swap/swap-card-auto-router/SwapCardAutoRouter.spec.tsx +++ b/packages/web/src/components/swap/swap-card-auto-router/SwapCardAutoRouter.spec.tsx @@ -2,36 +2,11 @@ import { render } from "@testing-library/react"; import { Provider as JotaiProvider } from "jotai"; import GnoswapThemeProvider from "@providers/gnoswap-theme-provider/GnoswapThemeProvider"; import SwapCardAutoRouter from "./SwapCardAutoRouter"; -import { - dummyAutoRouterInfo, -} from "@containers/swap-container/SwapContainer"; describe("SwapCard Component", () => { it("SwapCard render", () => { const mockProps = { - autoRouterInfo: dummyAutoRouterInfo, - from: { - token: "USDCoin", - symbol: "USDC", - amount: "121", - price: "$0.00", - gnosExchangePrice: "1250", - usdExchangePrice: "($1541.55)", - balance: "0", - logoURI: - "https://raw.githubusercontent.com/Uniswap/assets/master/blockchains/ethereum/assets/0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48/logo.png", - }, - to: { - token: "HEX", - symbol: "HEX", - amount: "5000", - price: "$0.00", - gnosExchangePrice: "1250", - usdExchangePrice: "($1541.55)", - balance: "0", - logoURI: - "https://raw.githubusercontent.com/Uniswap/assets/master/blockchains/ethereum/assets/0x2b591e99afE9f32eAA6214f7B7629768c40Eeb39/logo.png", - }, + swapRouteInfos: [] }; render( diff --git a/packages/web/src/components/swap/swap-card-auto-router/SwapCardAutoRouter.stories.tsx b/packages/web/src/components/swap/swap-card-auto-router/SwapCardAutoRouter.stories.tsx index 8259a1198..8c35893fd 100644 --- a/packages/web/src/components/swap/swap-card-auto-router/SwapCardAutoRouter.stories.tsx +++ b/packages/web/src/components/swap/swap-card-auto-router/SwapCardAutoRouter.stories.tsx @@ -1,13 +1,44 @@ import React from "react"; import { ComponentStory, ComponentMeta } from "@storybook/react"; import SwapCardAutoRouter from "./SwapCardAutoRouter"; -import { css, Theme } from "@emotion/react"; -import { - dummyAutoRouterInfo, - dummySwapGasInfo, -} from "@containers/swap-container/SwapContainer"; -import { action } from "@storybook/addon-actions"; -import { DEVICE_TYPE } from "@styles/media"; +import { css } from "@emotion/react"; +import { SwapRouteInfo } from "@models/swap/swap-route-info"; +import PoolData from "@repositories/pool/mock/pools.json"; + +const pools = PoolData.pools; + +const swapRouteInfos: SwapRouteInfo[] = [{ + from: { + chainId: "test3", + address: "0x111111111117dC0aa78b770fA6A738034120C302", + path: "gno.land/r/demo/1inch", + name: "1inch", + symbol: "1INCH", + decimals: 6, + logoURI: "https://assets.coingecko.com/coins/images/13469/thumb/1inch-token.png?1608803028", + priceId: "1inch", + createdAt: "1999-01-01T00:00:01Z" + }, + to: { + chainId: "test3", + address: "0x111111111117dC0aa78b770fA6A738034120C302", + path: "gno.land/r/demo/1inch", + name: "1inch", + symbol: "1INCH", + decimals: 6, + logoURI: "https://assets.coingecko.com/coins/images/13469/thumb/1inch-token.png?1608803028", + priceId: "1inch", + createdAt: "1999-01-01T00:00:01Z" + }, + gasFee: { + amount: 0.000001, + currency: "GNOT" + }, + gasFeeUSD: 0.1, + pools, + version: "V1", + weight: 100, +}]; export default { title: "swap/SwapCardAutoRouter", @@ -24,32 +55,10 @@ const Template: ComponentStory = args => ( export const Default = Template.bind({}); Default.args = { - autoRouterInfo: dummyAutoRouterInfo, - from: { - token: "USDCoin", - symbol: "USDC", - amount: "121", - price: "$0.00", - gnosExchangePrice: "1250", - usdExchangePrice: "($1541.55)", - balance: "0", - logoURI: - "https://raw.githubusercontent.com/Uniswap/assets/master/blockchains/ethereum/assets/0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48/logo.png", - }, - to: { - token: "HEX", - symbol: "HEX", - amount: "5000", - price: "$0.00", - gnosExchangePrice: "1250", - usdExchangePrice: "($1541.55)", - balance: "0", - logoURI: - "https://raw.githubusercontent.com/Uniswap/assets/master/blockchains/ethereum/assets/0x2b591e99afE9f32eAA6214f7B7629768c40Eeb39/logo.png", - }, + swapRouteInfos, }; -const wrapper = (theme: Theme) => css` +const wrapper = () => css` display: flex; width: 100%; align-items: center; @@ -57,6 +66,6 @@ const wrapper = (theme: Theme) => css` margin-top: 50px; `; -const contentWrap = (theme: Theme) => css` +const contentWrap = () => css` width: 500px; `; diff --git a/packages/web/src/components/swap/swap-card-auto-router/SwapCardAutoRouter.tsx b/packages/web/src/components/swap/swap-card-auto-router/SwapCardAutoRouter.tsx index b832b610b..40dd7ef17 100644 --- a/packages/web/src/components/swap/swap-card-auto-router/SwapCardAutoRouter.tsx +++ b/packages/web/src/components/swap/swap-card-auto-router/SwapCardAutoRouter.tsx @@ -1,110 +1,64 @@ -import React from "react"; +import React, { useMemo } from "react"; import { AutoRouterWrapper, DotLine } from "./SwapCardAutoRouter.styles"; -import { TokenInfo } from "../swap-card/SwapCard"; -import { AutoRouterInfo } from "@containers/swap-container/SwapContainer"; -import IconLogoPrimary from "@components/common/icons/IconLogoPrimary"; +import { SwapRouteInfo } from "@models/swap/swap-route-info"; +import DoubleLogo from "@components/common/double-logo/DoubleLogo"; interface ContentProps { - to: TokenInfo; - from: TokenInfo; - autoRouterInfo: AutoRouterInfo; + swapRouteInfos: SwapRouteInfo[]; } const SwapCardAutoRouter: React.FC = ({ - to, - from, - autoRouterInfo, + swapRouteInfos, }) => { + const bestGasFee = useMemo(() => { + const totalGasFee = swapRouteInfos.reduce((prev, current) => prev + current.gasFeeUSD, 0); + return `$${totalGasFee}`; + }, [swapRouteInfos]); + return ( -
- token logo -
-
V1
- {autoRouterInfo.v1fee[0]} -
- -
-
-
- pair-logo -
-
- pair-logo -
-
-

{autoRouterInfo.v1fee[1]}

-
- -
-
-
- pair-logo -
-
- -
-
-

{autoRouterInfo.v1fee[2]}

-
- -
- -
-
-
- token logo -
-
V1
- {autoRouterInfo.v2fee[0]} -
- -
-
-
- pair-logo -
-
- -
-
-

{autoRouterInfo.v2fee[1]}

-
- -
- -
-
-
- token logo -
-
V1
- {autoRouterInfo.v3fee[0]} -
- -
-
-
- pair-logo -
-
- -
-
-

{autoRouterInfo.v3fee[1]}

-
- -
- -
-
+ {swapRouteInfos.map((swapRouteInfo, index) => ( + + ))}

- Best price route costs ~$0.58 in gas. This route optimizes your total - output by considering split routes, multiple hops, and the gas cost of - each step. + {`Best price route costs ~${bestGasFee} in gas. This route optimizes your total output by considering split routes, multiple hops, and the gas cost of each step.`}

); }; +interface SwapCardAutoRouterItemProps { + swapRouteInfo: SwapRouteInfo; +} + +const SwapCardAutoRouterItem: React.FC = ({ + swapRouteInfo, +}) => { + const weightStr = useMemo(() => { + return `${swapRouteInfo.weight}%`; + }, [swapRouteInfo.weight]); + + return ( +
+ token logo +
+
{swapRouteInfo.version}
+ {weightStr} +
+ + {swapRouteInfo.pools.map((pool, index) => ( + <> +
+ +

{pool.fee}

+
+ + + ))} + token logo +
+ ); +}; + + export default SwapCardAutoRouter; diff --git a/packages/web/src/components/swap/swap-card-content-detail/SwapCardContentDetail.spec.tsx b/packages/web/src/components/swap/swap-card-content-detail/SwapCardContentDetail.spec.tsx index f2961d10d..d00e5e6f8 100644 --- a/packages/web/src/components/swap/swap-card-content-detail/SwapCardContentDetail.spec.tsx +++ b/packages/web/src/components/swap/swap-card-content-detail/SwapCardContentDetail.spec.tsx @@ -2,44 +2,51 @@ import { render } from "@testing-library/react"; import { Provider as JotaiProvider } from "jotai"; import GnoswapThemeProvider from "@providers/gnoswap-theme-provider/GnoswapThemeProvider"; import SwapCardContentDetail from "./SwapCardContentDetail"; -import { - dummyAutoRouterInfo, - dummySwapGasInfo, -} from "@containers/swap-container/SwapContainer"; -import { DEVICE_TYPE } from "@styles/media"; +import { SwapSummaryInfo } from "@models/swap/swap-summary-info"; + +const swapSummaryInfo: SwapSummaryInfo = { + tokenA: { + chainId: "test3", + address: "0x111111111117dC0aa78b770fA6A738034120C302", + path: "gno.land/r/demo/1inch", + name: "1inch", + symbol: "1INCH", + decimals: 6, + logoURI: "https://assets.coingecko.com/coins/images/13469/thumb/1inch-token.png?1608803028", + priceId: "1inch", + createdAt: "1999-01-01T00:00:01Z" + }, + tokenB: { + chainId: "test3", + address: "0x111111111117dC0aa78b770fA6A738034120C302", + path: "gno.land/r/demo/1inch", + name: "1inch", + symbol: "1INCH", + decimals: 6, + logoURI: "https://assets.coingecko.com/coins/images/13469/thumb/1inch-token.png?1608803028", + priceId: "1inch", + createdAt: "1999-01-01T00:00:01Z" + }, + swapDirection: "EXACT_IN", + swapRate: 1.14, + swapRateUSD: 1.14, + priceImpact: 0.3, + guaranteedAmount: { + amount: 45124, + currency: "GNOT" + }, + gasFee: { + amount: 0.000001, + currency: "GNOT" + }, + gasFeeUSD: 0.1 +}; describe("SwapCardContentDetail Component", () => { it("SwapCardContentDetail render", () => { const mockProps = { - autoRouter: false, - showAutoRouter: () => null, - swapGasInfo: dummySwapGasInfo, - swapInfo: true, - showSwapInfo: () => null, - autoRouterInfo: dummyAutoRouterInfo, - breakpoint: DEVICE_TYPE.WEB, - from: { - token: "USDCoin", - symbol: "USDC", - amount: "121", - price: "$0.00", - gnosExchangePrice: "1250", - usdExchangePrice: "($1541.55)", - balance: "0", - logoURI: - "https://raw.githubusercontent.com/Uniswap/assets/master/blockchains/ethereum/assets/0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48/logo.png", - }, - to: { - token: "HEX", - symbol: "HEX", - amount: "5000", - price: "$0.00", - gnosExchangePrice: "1250", - usdExchangePrice: "($1541.55)", - balance: "0", - logoURI: - "https://raw.githubusercontent.com/Uniswap/assets/master/blockchains/ethereum/assets/0x2b591e99afE9f32eAA6214f7B7629768c40Eeb39/logo.png", - }, + swapSummaryInfo, + swapRouteInfos: [], }; render( diff --git a/packages/web/src/components/swap/swap-card-content-detail/SwapCardContentDetail.stories.tsx b/packages/web/src/components/swap/swap-card-content-detail/SwapCardContentDetail.stories.tsx index 34d516fcb..ebe04b1c7 100644 --- a/packages/web/src/components/swap/swap-card-content-detail/SwapCardContentDetail.stories.tsx +++ b/packages/web/src/components/swap/swap-card-content-detail/SwapCardContentDetail.stories.tsx @@ -2,12 +2,82 @@ import React from "react"; import { ComponentStory, ComponentMeta } from "@storybook/react"; import SwapCardContentDetail from "./SwapCardContentDetail"; import { css } from "@emotion/react"; -import { - dummyAutoRouterInfo, - dummySwapGasInfo, -} from "@containers/swap-container/SwapContainer"; -import { action } from "@storybook/addon-actions"; -import { DEVICE_TYPE } from "@styles/media"; +import { SwapRouteInfo } from "@models/swap/swap-route-info"; +import PoolData from "@repositories/pool/mock/pools.json"; +import { SwapSummaryInfo } from "@models/swap/swap-summary-info"; + +const pools = PoolData.pools; + +const swapSummaryInfo: SwapSummaryInfo = { + tokenA: { + chainId: "test3", + address: "0x111111111117dC0aa78b770fA6A738034120C302", + path: "gno.land/r/demo/1inch", + name: "1inch", + symbol: "1INCH", + decimals: 6, + logoURI: "https://assets.coingecko.com/coins/images/13469/thumb/1inch-token.png?1608803028", + priceId: "1inch", + createdAt: "1999-01-01T00:00:01Z" + }, + tokenB: { + chainId: "test3", + address: "0x111111111117dC0aa78b770fA6A738034120C302", + path: "gno.land/r/demo/1inch", + name: "1inch", + symbol: "1INCH", + decimals: 6, + logoURI: "https://assets.coingecko.com/coins/images/13469/thumb/1inch-token.png?1608803028", + priceId: "1inch", + createdAt: "1999-01-01T00:00:01Z" + }, + swapDirection: "EXACT_IN", + swapRate: 1.14, + swapRateUSD: 1.14, + priceImpact: 0.3, + guaranteedAmount: { + amount: 45124, + currency: "GNOT" + }, + gasFee: { + amount: 0.000001, + currency: "GNOT" + }, + gasFeeUSD: 0.1 +}; + +const swapRouteInfos: SwapRouteInfo[] = [{ + from: { + chainId: "test3", + address: "0x111111111117dC0aa78b770fA6A738034120C302", + path: "gno.land/r/demo/1inch", + name: "1inch", + symbol: "1INCH", + decimals: 6, + logoURI: "https://assets.coingecko.com/coins/images/13469/thumb/1inch-token.png?1608803028", + priceId: "1inch", + createdAt: "1999-01-01T00:00:01Z" + }, + to: { + chainId: "test3", + address: "0x111111111117dC0aa78b770fA6A738034120C302", + path: "gno.land/r/demo/1inch", + name: "1inch", + symbol: "1INCH", + decimals: 6, + logoURI: "https://assets.coingecko.com/coins/images/13469/thumb/1inch-token.png?1608803028", + priceId: "1inch", + createdAt: "1999-01-01T00:00:01Z" + }, + gasFee: { + amount: 0.000001, + currency: "GNOT" + }, + gasFeeUSD: 0.1, + pools, + version: "V1", + weight: 100, +}]; export default { title: "swap/SwapCardContentDetail", @@ -24,35 +94,8 @@ const Template: ComponentStory = args => ( export const Default = Template.bind({}); Default.args = { - autoRouter: true, - showAutoRouter: action("onClick"), - swapGasInfo: dummySwapGasInfo, - swapInfo: true, - showSwapInfo: action("onClick"), - autoRouterInfo: dummyAutoRouterInfo, - breakpoint: DEVICE_TYPE.WEB, - from: { - token: "USDCoin", - symbol: "USDC", - amount: "121", - price: "$0.00", - gnosExchangePrice: "1250", - usdExchangePrice: "($1541.55)", - balance: "0", - logoURI: - "https://raw.githubusercontent.com/Uniswap/assets/master/blockchains/ethereum/assets/0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48/logo.png", - }, - to: { - token: "HEX", - symbol: "HEX", - amount: "5000", - price: "$0.00", - gnosExchangePrice: "1250", - usdExchangePrice: "($1541.55)", - balance: "0", - logoURI: - "https://raw.githubusercontent.com/Uniswap/assets/master/blockchains/ethereum/assets/0x2b591e99afE9f32eAA6214f7B7629768c40Eeb39/logo.png", - }, + swapSummaryInfo, + swapRouteInfos, }; const wrapper = () => css` diff --git a/packages/web/src/components/swap/swap-card-content-detail/SwapCardContentDetail.styles.ts b/packages/web/src/components/swap/swap-card-content-detail/SwapCardContentDetail.styles.ts index f25bd7faf..70219e0b3 100644 --- a/packages/web/src/components/swap/swap-card-content-detail/SwapCardContentDetail.styles.ts +++ b/packages/web/src/components/swap/swap-card-content-detail/SwapCardContentDetail.styles.ts @@ -3,15 +3,15 @@ import styled from "@emotion/styled"; import { media } from "@styles/media"; import mixins from "@styles/mixins"; -interface exchangeProps { - swapInfo: boolean; +interface WrapperProps { + opened: boolean; } -export const DetailWrapper = styled.div` +export const DetailWrapper = styled.div` ${mixins.flexbox("column", "flex-start", "flex-start")}; width: 100%; - border-radius: ${({ swapInfo }) => { - return swapInfo ? "8px 8px 0px 0px" : "8px"; + border-radius: ${({ opened }) => { + return opened ? "8px 8px 0px 0px" : "8px"; }}; background: ${({ theme }) => theme.color.background01}; border: 1px solid ${({ theme }) => theme.color.border02}; @@ -68,11 +68,11 @@ export const DetailWrapper = styled.div` } `; -export const FeelWrapper = styled.div` +export const FeelWrapper = styled.div` ${mixins.flexbox("column", "flex-start", "flex-start")}; width: 100%; - border-radius: ${({ swapInfo }) => { - return swapInfo ? "0px 0px 8px 8px" : "8px"; + border-radius: ${({ opened }) => { + return opened ? "0px 0px 8px 8px" : "8px"; }}; background: ${({ theme }) => theme.color.background01}; border-left: 1px solid ${({ theme }) => theme.color.border02}; diff --git a/packages/web/src/components/swap/swap-card-content-detail/SwapCardContentDetail.tsx b/packages/web/src/components/swap/swap-card-content-detail/SwapCardContentDetail.tsx index 50af30f1d..9f0aa130b 100644 --- a/packages/web/src/components/swap/swap-card-content-detail/SwapCardContentDetail.tsx +++ b/packages/web/src/components/swap/swap-card-content-detail/SwapCardContentDetail.tsx @@ -1,88 +1,97 @@ -import React from "react"; +import React, { useCallback, useMemo, useState } from "react"; import { DetailWrapper, FeelWrapper } from "./SwapCardContentDetail.styles"; -import { TokenInfo } from "../swap-card/SwapCard"; import IconNote from "@components/common/icons/IconNote"; import IconStrokeArrowDown from "@components/common/icons/IconStrokeArrowDown"; import IconStrokeArrowUp from "@components/common/icons/IconStrokeArrowUp"; import SwapCardFeeInfo from "../swap-card-fee-info/SwapCardFeeInfo"; import SwapCardAutoRouter from "../swap-card-auto-router/SwapCardAutoRouter"; -import { - AutoRouterInfo, - SwapGasInfo, -} from "@containers/swap-container/SwapContainer"; import SwapButtonTooltip from "../swap-button-tooltip/SwapButtonTooltip"; import { DEVICE_TYPE } from "@styles/media"; +import { SwapSummaryInfo } from "@models/swap/swap-summary-info"; +import { SwapRouteInfo } from "@models/swap/swap-route-info"; +import { numberToFormat } from "@utils/string-utils"; +import { useWindowSize } from "@hooks/common/use-window-size"; interface ContentProps { - to: TokenInfo; - from: TokenInfo; - swapInfo: boolean; - showSwapInfo: () => void; - autoRouter: boolean; - showAutoRouter: () => void; - swapGasInfo: SwapGasInfo; - autoRouterInfo: AutoRouterInfo; - breakpoint: DEVICE_TYPE; + swapSummaryInfo: SwapSummaryInfo; + swapRouteInfos: SwapRouteInfo[]; } const SwapCardContentDetail: React.FC = ({ - to, - from, - swapInfo, - showSwapInfo, - autoRouter, - showAutoRouter, - swapGasInfo, - autoRouterInfo, - breakpoint, + swapSummaryInfo, + swapRouteInfos, }) => { + const { breakpoint } = useWindowSize(); + const [openedDetailInfo, setOpenedDetailInfo] = useState(false); + const [openedRouteInfo, setOpenedRouteInfo] = useState(false); + + const swapRateDescription = useMemo(() => { + const { tokenA, tokenB, swapRate } = swapSummaryInfo; + return `1 ${tokenA.symbol} = ${numberToFormat(swapRate)} ${tokenB.symbol}`; + }, [swapSummaryInfo]); + + const swapRateUSD = useMemo(() => { + const swapRateUSD = swapSummaryInfo.swapRateUSD; + return numberToFormat(swapRateUSD); + }, [swapSummaryInfo.swapRateUSD]); + + const gasFeeUSDStr = useMemo(() => { + const gasFeeUSD = swapSummaryInfo.gasFeeUSD; + return `$${gasFeeUSD}`; + }, [swapSummaryInfo.gasFeeUSD]); + + const toggleDetailInfo = useCallback(() => { + setOpenedDetailInfo(!openedDetailInfo); + }, [openedDetailInfo]); + + const toggleRouteInfo = useCallback(() => { + setOpenedRouteInfo(!openedRouteInfo); + }, [openedRouteInfo]); + return ( <> - +
- - - {from.amount} {from.symbol} = {from.gnosExchangePrice} GNOS - + + {swapRateDescription} {breakpoint !== DEVICE_TYPE.MOBILE && ( - {from.usdExchangePrice} + {`($${swapRateUSD})`} )}
- {swapGasInfo.usdExchangeGasFee} - {swapInfo ? ( + {gasFeeUSDStr} + {openedDetailInfo ? ( ) : ( )}
- {swapInfo && ( - + + {openedDetailInfo && ( +
- {swapInfo && ( + {openedDetailInfo && ( )} - {autoRouter && ( + {openedRouteInfo && ( )}
diff --git a/packages/web/src/components/swap/swap-card-content/SwapCardContent.spec.tsx b/packages/web/src/components/swap/swap-card-content/SwapCardContent.spec.tsx index 1304f9556..0ef267cff 100644 --- a/packages/web/src/components/swap/swap-card-content/SwapCardContent.spec.tsx +++ b/packages/web/src/components/swap/swap-card-content/SwapCardContent.spec.tsx @@ -2,52 +2,55 @@ import { render } from "@testing-library/react"; import { Provider as JotaiProvider } from "jotai"; import GnoswapThemeProvider from "@providers/gnoswap-theme-provider/GnoswapThemeProvider"; import SwapCardContent from "./SwapCardContent"; -import { - coinList, - dummyAutoRouterInfo, - dummySwapGasInfo, -} from "@containers/swap-container/SwapContainer"; -import { DEVICE_TYPE } from "@styles/media"; +import { SwapTokenInfo } from "@models/swap/swap-token-info"; + +const swapTokenInfo: SwapTokenInfo = { + tokenA: { + chainId: "test3", + address: "0x111111111117dC0aa78b770fA6A738034120C302", + path: "gno.land/r/demo/1inch", + name: "1inch", + symbol: "1INCH", + decimals: 6, + logoURI: "https://assets.coingecko.com/coins/images/13469/thumb/1inch-token.png?1608803028", + priceId: "1inch", + createdAt: "1999-01-01T00:00:01Z" + }, + tokenAAmount: "", + tokenABalance: "", + tokenAUSD: 0, + tokenAUSDStr: "0", + tokenB: { + chainId: "test3", + address: "0x111111111117dC0aa78b770fA6A738034120C302", + path: "gno.land/r/demo/1inch", + name: "1inch", + symbol: "1INCH", + decimals: 6, + logoURI: "https://assets.coingecko.com/coins/images/13469/thumb/1inch-token.png?1608803028", + priceId: "1inch", + createdAt: "1999-01-01T00:00:01Z" + }, + tokenBAmount: "", + tokenBBalance: "", + tokenBUSD: 0, + tokenBUSDStr: "0", + direction: "EXACT_IN", + slippage: 10 +}; describe("SwapCardContent Component", () => { it("SwapCardContent render", () => { const mockProps = { - autoRouter: false, - showAutoRouter: () => null, - swapGasInfo: dummySwapGasInfo, - swapInfo: true, - showSwapInfo: () => null, - autoRouterInfo: dummyAutoRouterInfo, - tokenModal: true, - onSelectTokenModal: () => null, - search: () => null, - keyword: "", - coinList: coinList(), - changeToken: () => null, - selectToken: () => null, - breakpoint: DEVICE_TYPE.WEB, - from: { - token: "USDCoin", - symbol: "USDC", - amount: "121", - price: "$0.00", - gnosExchangePrice: "1250", - usdExchangePrice: "($1541.55)", - balance: "0", - logoURI: - "https://raw.githubusercontent.com/Uniswap/assets/master/blockchains/ethereum/assets/0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48/logo.png", - }, - to: { - token: "HEX", - symbol: "HEX", - amount: "5000", - price: "$0.00", - gnosExchangePrice: "1250", - usdExchangePrice: "($1541.55)", - balance: "0", - logoURI: - "https://raw.githubusercontent.com/Uniswap/assets/master/blockchains/ethereum/assets/0x2b591e99afE9f32eAA6214f7B7629768c40Eeb39/logo.png", - }, + swapTokenInfo, + swapSummaryInfo: null, + swapRouteInfos: [], + changeTokenA: () => null, + changeTokenAAmount: () => null, + changeTokenB: () => null, + changeTokenBAmount: () => null, + changeSlippage: () => null, + switchSwapDirection: () => null }; render( diff --git a/packages/web/src/components/swap/swap-card-content/SwapCardContent.stories.tsx b/packages/web/src/components/swap/swap-card-content/SwapCardContent.stories.tsx index 73c5994b8..8628a5851 100644 --- a/packages/web/src/components/swap/swap-card-content/SwapCardContent.stories.tsx +++ b/packages/web/src/components/swap/swap-card-content/SwapCardContent.stories.tsx @@ -1,14 +1,44 @@ import React from "react"; import { ComponentStory, ComponentMeta } from "@storybook/react"; import SwapCardContent from "./SwapCardContent"; -import { css, Theme } from "@emotion/react"; -import { - coinList, - dummyAutoRouterInfo, - dummySwapGasInfo, -} from "@containers/swap-container/SwapContainer"; +import { css } from "@emotion/react"; +import { SwapTokenInfo } from "@models/swap/swap-token-info"; import { action } from "@storybook/addon-actions"; -import { DEVICE_TYPE } from "@styles/media"; + +const swapTokenInfo: SwapTokenInfo = { + tokenA: { + chainId: "test3", + address: "0x111111111117dC0aa78b770fA6A738034120C302", + path: "gno.land/r/demo/1inch", + name: "1inch", + symbol: "1INCH", + decimals: 6, + logoURI: "https://assets.coingecko.com/coins/images/13469/thumb/1inch-token.png?1608803028", + priceId: "1inch", + createdAt: "1999-01-01T00:00:01Z" + }, + tokenAAmount: "", + tokenABalance: "", + tokenAUSD: 0, + tokenAUSDStr: "0", + tokenB: { + chainId: "test3", + address: "0x111111111117dC0aa78b770fA6A738034120C302", + path: "gno.land/r/demo/1inch", + name: "1inch", + symbol: "1INCH", + decimals: 6, + logoURI: "https://assets.coingecko.com/coins/images/13469/thumb/1inch-token.png?1608803028", + priceId: "1inch", + createdAt: "1999-01-01T00:00:01Z" + }, + tokenBAmount: "", + tokenBBalance: "", + tokenBUSD: 0, + tokenBUSDStr: "0", + direction: "EXACT_IN", + slippage: 10 +}; export default { title: "swap/SwapCardContent", @@ -25,45 +55,16 @@ const Template: ComponentStory = args => ( export const Default = Template.bind({}); Default.args = { - autoRouter: true, - showAutoRouter: action("onClick"), - swapGasInfo: dummySwapGasInfo, - swapInfo: true, - showSwapInfo: action("onClick"), - autoRouterInfo: dummyAutoRouterInfo, - search: action("search"), - tokenModal: true, - onSelectTokenModal: action("onClick"), - keyword: "", - coinList: coinList(), - changeToken: action("changeToken"), - selectToken: action("selectToken"), - breakpoint: DEVICE_TYPE.WEB, - from: { - token: "USDCoin", - symbol: "USDC", - amount: "121", - price: "$0.00", - gnosExchangePrice: "1250", - usdExchangePrice: "($1541.55)", - balance: "0", - logoURI: - "https://raw.githubusercontent.com/Uniswap/assets/master/blockchains/ethereum/assets/0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48/logo.png", - }, - to: { - token: "HEX", - symbol: "HEX", - amount: "5000", - price: "$0.00", - gnosExchangePrice: "1250", - usdExchangePrice: "($1541.55)", - balance: "0", - logoURI: - "https://raw.githubusercontent.com/Uniswap/assets/master/blockchains/ethereum/assets/0x2b591e99afE9f32eAA6214f7B7629768c40Eeb39/logo.png", - }, + swapTokenInfo, + swapSummaryInfo: null, + swapRouteInfos: [], + changeTokenA: action("changeTokenA"), + changeTokenAAmount: action("changeTokenAAmount"), + changeTokenB: action("changeTokenB"), + changeTokenBAmount: action("changeTokenBAmount"), }; -const wrapper = (theme: Theme) => css` +const wrapper = () => css` display: flex; width: 100%; align-items: center; @@ -71,6 +72,6 @@ const wrapper = (theme: Theme) => css` margin-top: 50px; `; -const contentWrap = (theme: Theme) => css` +const contentWrap = () => css` width: 500px; `; diff --git a/packages/web/src/components/swap/swap-card-content/SwapCardContent.styles.ts b/packages/web/src/components/swap/swap-card-content/SwapCardContent.styles.ts index eeb813d69..1f83ba095 100644 --- a/packages/web/src/components/swap/swap-card-content/SwapCardContent.styles.ts +++ b/packages/web/src/components/swap/swap-card-content/SwapCardContent.styles.ts @@ -40,6 +40,11 @@ export const ContentWrapper = styled.div` color: ${({ theme }) => theme.color.text01}; } + .token-selector { + display: block; + height: 32px; + } + .amount-info { ${mixins.flexbox("row", "center", "space-between")}; width: 100%; diff --git a/packages/web/src/components/swap/swap-card-content/SwapCardContent.tsx b/packages/web/src/components/swap/swap-card-content/SwapCardContent.tsx index c73284e47..ebe86d565 100644 --- a/packages/web/src/components/swap/swap-card-content/SwapCardContent.tsx +++ b/packages/web/src/components/swap/swap-card-content/SwapCardContent.tsx @@ -1,162 +1,106 @@ -import React, { useCallback, useState } from "react"; -import { ContentWrapper, SelectPairButton } from "./SwapCardContent.styles"; -import { TokenInfo } from "../swap-card/SwapCard"; +import React, { useCallback } from "react"; +import { ContentWrapper } from "./SwapCardContent.styles"; import IconSwapArrowDown from "@components/common/icons/IconSwapArrowDown"; import SwapCardContentDetail from "../swap-card-content-detail/SwapCardContentDetail"; -import { - AutoRouterInfo, - tokenInfo, - SwapGasInfo, -} from "@containers/swap-container/SwapContainer"; -import SelectTokenModal from "../select-token-modal/SelectTokenModal"; -import { DEVICE_TYPE } from "@styles/media"; -import IconStrokeArrowDown from "@components/common/icons/IconStrokeArrowDown"; +import { SwapTokenInfo } from "@models/swap/swap-token-info"; +import { SwapSummaryInfo } from "@models/swap/swap-summary-info"; +import { SwapRouteInfo } from "@models/swap/swap-route-info"; +import { TokenModel } from "@models/token/token-model"; +import { isAmount } from "@common/utils/data-check-util"; +import SelectPairButton from "@components/common/select-pair-button/SelectPairButton"; interface ContentProps { - to: TokenInfo; - from: TokenInfo; - swapInfo: boolean; - showSwapInfo: () => void; - autoRouter: boolean; - showAutoRouter: () => void; - swapGasInfo: SwapGasInfo; - autoRouterInfo: AutoRouterInfo; - tokenModal: boolean; - onSelectTokenModal: () => void; - search: (e: React.ChangeEvent) => void; - keyword: string; - coinList: tokenInfo[]; - changeToken: (token: tokenInfo, type: string) => void; - selectToken: (e: string) => void; - breakpoint: DEVICE_TYPE; -} - -function isAmount(str: string) { - const regex = /^\d+(\.\d*)?$/; - return regex.test(str); + swapTokenInfo: SwapTokenInfo; + swapSummaryInfo: SwapSummaryInfo | null; + swapRouteInfos: SwapRouteInfo[]; + changeTokenA: (token: TokenModel) => void; + changeTokenAAmount: (value: string) => void; + changeTokenB: (token: TokenModel) => void; + changeTokenBAmount: (value: string) => void; + switchSwapDirection: () => void; } const SwapCardContent: React.FC = ({ - to, - from, - swapInfo, - showSwapInfo, - autoRouter, - showAutoRouter, - swapGasInfo, - autoRouterInfo, - tokenModal, - onSelectTokenModal, - search, - keyword, - coinList, - changeToken, - selectToken, - breakpoint, + swapTokenInfo, + swapSummaryInfo, + swapRouteInfos, + changeTokenA, + changeTokenAAmount, + changeTokenB, + changeTokenBAmount, + switchSwapDirection, }) => { - const [fromAmount, setFromAmount] = useState(from.amount); - const [toAmount, setToAmount] = useState(to.amount); - const onChangeFromAmount = useCallback( + const tokenA = swapTokenInfo.tokenA; + const tokenB = swapTokenInfo.tokenB; + + const onChangeTokenAAmount = useCallback( (e: React.ChangeEvent) => { const value = e.target.value; if (value !== "" && !isAmount(value)) return; - setFromAmount(value); + changeTokenAAmount(value); }, - [], + [changeTokenAAmount], ); - const onChangeToAmount = useCallback( + const onChangeTokenBAmount = useCallback( (e: React.ChangeEvent) => { const value = e.target.value; if (value !== "" && !isAmount(value)) return; - setToAmount(value); + changeTokenBAmount(value); }, - [], + [changeTokenBAmount], ); return ( - <> - {tokenModal && ( - - )} - -
-
- - { - selectToken("from"); - onSelectTokenModal(); - }} - > - token logo - {from.symbol} - - -
-
- {from.price} - Balance : {from.balance} -
-
-
- -
+ +
+
+ +
+
-
-
- - { - selectToken("to"); - onSelectTokenModal(); - }} - > - token logo - {to.symbol} - - +
+ {swapTokenInfo.tokenAUSDStr} + Balance : {swapTokenInfo.tokenABalance} +
+
+
+
-
- {to.price} - Balance : {to.balance} +
+
+
+
+ +
+
+
+ {swapTokenInfo.tokenBUSDStr} + Balance : {swapTokenInfo.tokenBBalance} +
+
+ + {swapSummaryInfo && ( - - + )} + ); }; diff --git a/packages/web/src/components/swap/swap-card-fee-info/SwapCardFeeInfo.spec.tsx b/packages/web/src/components/swap/swap-card-fee-info/SwapCardFeeInfo.spec.tsx index 6cc16ead2..eacf76ed3 100644 --- a/packages/web/src/components/swap/swap-card-fee-info/SwapCardFeeInfo.spec.tsx +++ b/packages/web/src/components/swap/swap-card-fee-info/SwapCardFeeInfo.spec.tsx @@ -2,14 +2,52 @@ import { render } from "@testing-library/react"; import { Provider as JotaiProvider } from "jotai"; import GnoswapThemeProvider from "@providers/gnoswap-theme-provider/GnoswapThemeProvider"; import SwapCardFeeInfo from "./SwapCardFeeInfo"; -import { dummySwapGasInfo } from "@containers/swap-container/SwapContainer"; +import { SwapSummaryInfo } from "@models/swap/swap-summary-info"; + +const swapSummaryInfo: SwapSummaryInfo = { + tokenA: { + chainId: "test3", + address: "0x111111111117dC0aa78b770fA6A738034120C302", + path: "gno.land/r/demo/1inch", + name: "1inch", + symbol: "1INCH", + decimals: 6, + logoURI: "https://assets.coingecko.com/coins/images/13469/thumb/1inch-token.png?1608803028", + priceId: "1inch", + createdAt: "1999-01-01T00:00:01Z" + }, + tokenB: { + chainId: "test3", + address: "0x111111111117dC0aa78b770fA6A738034120C302", + path: "gno.land/r/demo/1inch", + name: "1inch", + symbol: "1INCH", + decimals: 6, + logoURI: "https://assets.coingecko.com/coins/images/13469/thumb/1inch-token.png?1608803028", + priceId: "1inch", + createdAt: "1999-01-01T00:00:01Z" + }, + swapDirection: "EXACT_IN", + swapRate: 1.14, + swapRateUSD: 1.14, + priceImpact: 0.3, + guaranteedAmount: { + amount: 45124, + currency: "GNOT" + }, + gasFee: { + amount: 0.000001, + currency: "GNOT" + }, + gasFeeUSD: 0.1 +}; describe("SwapCardFeeInfo Component", () => { it("SwapCardFeeInfo render", () => { const mockProps = { - autoRouter: false, - showAutoRouter: () => null, - swapGasInfo: dummySwapGasInfo, + openedRouteInfo: false, + toggleRouteInfo: () => null, + swapSummaryInfo, }; render( diff --git a/packages/web/src/components/swap/swap-card-fee-info/SwapCardFeeInfo.stories.tsx b/packages/web/src/components/swap/swap-card-fee-info/SwapCardFeeInfo.stories.tsx index 47e6ee7b6..b8c6e63c1 100644 --- a/packages/web/src/components/swap/swap-card-fee-info/SwapCardFeeInfo.stories.tsx +++ b/packages/web/src/components/swap/swap-card-fee-info/SwapCardFeeInfo.stories.tsx @@ -1,9 +1,47 @@ import React from "react"; import { ComponentStory, ComponentMeta } from "@storybook/react"; import SwapCardFeeInfo from "./SwapCardFeeInfo"; -import { css, Theme } from "@emotion/react"; -import { dummySwapGasInfo } from "@containers/swap-container/SwapContainer"; +import { css } from "@emotion/react"; import { action } from "@storybook/addon-actions"; +import { SwapSummaryInfo } from "@models/swap/swap-summary-info"; + +const swapSummaryInfo: SwapSummaryInfo = { + tokenA: { + chainId: "test3", + address: "0x111111111117dC0aa78b770fA6A738034120C302", + path: "gno.land/r/demo/1inch", + name: "1inch", + symbol: "1INCH", + decimals: 6, + logoURI: "https://assets.coingecko.com/coins/images/13469/thumb/1inch-token.png?1608803028", + priceId: "1inch", + createdAt: "1999-01-01T00:00:01Z" + }, + tokenB: { + chainId: "test3", + address: "0x111111111117dC0aa78b770fA6A738034120C302", + path: "gno.land/r/demo/1inch", + name: "1inch", + symbol: "1INCH", + decimals: 6, + logoURI: "https://assets.coingecko.com/coins/images/13469/thumb/1inch-token.png?1608803028", + priceId: "1inch", + createdAt: "1999-01-01T00:00:01Z" + }, + swapDirection: "EXACT_IN", + swapRate: 1.14, + swapRateUSD: 1.14, + priceImpact: 0.3, + guaranteedAmount: { + amount: 45124, + currency: "GNOT" + }, + gasFee: { + amount: 0.000001, + currency: "GNOT" + }, + gasFeeUSD: 0.1 +}; export default { title: "swap/SwapCardFeeInfo", @@ -20,9 +58,9 @@ const Template: ComponentStory = args => ( export const Default = Template.bind({}); Default.args = { - autoRouter: true, - showAutoRouter: action("onClick"), - swapGasInfo: dummySwapGasInfo, + openedRouteInfo: true, + toggleRouteInfo: action("toggleRouteInfo"), + swapSummaryInfo }; const wrapper = () => css` diff --git a/packages/web/src/components/swap/swap-card-fee-info/SwapCardFeeInfo.tsx b/packages/web/src/components/swap/swap-card-fee-info/SwapCardFeeInfo.tsx index 429211c37..c976e6032 100644 --- a/packages/web/src/components/swap/swap-card-fee-info/SwapCardFeeInfo.tsx +++ b/packages/web/src/components/swap/swap-card-fee-info/SwapCardFeeInfo.tsx @@ -1,37 +1,64 @@ -import React from "react"; +import React, { useMemo } from "react"; import { FeeWrapper, SwapDivider } from "./SwapCardFeeInfo.styles"; import IconStrokeArrowDown from "@components/common/icons/IconStrokeArrowDown"; import IconStrokeArrowUp from "@components/common/icons/IconStrokeArrowUp"; import IconRouter from "@components/common/icons/IconRouter"; -import { SwapGasInfo } from "@containers/swap-container/SwapContainer"; +import { SwapSummaryInfo, swapDirectionToGuaranteedType } from "@models/swap/swap-summary-info"; +import { toNumberFormat } from "@utils/number-utils"; interface ContentProps { - autoRouter: boolean; - showAutoRouter: () => void; - swapGasInfo: SwapGasInfo; + openedRouteInfo: boolean; + toggleRouteInfo: () => void; + swapSummaryInfo: SwapSummaryInfo; } const SwapCardFeeInfo: React.FC = ({ - autoRouter, - showAutoRouter, - swapGasInfo, + openedRouteInfo, + toggleRouteInfo, + swapSummaryInfo, }) => { + + const priceImpactStr = useMemo(() => { + const priceImpact = swapSummaryInfo.priceImpact; + return `${priceImpact}%`; + }, [swapSummaryInfo.priceImpact]); + + const guaranteedTypeStr = useMemo(() => { + const swapDirection = swapSummaryInfo.swapDirection; + return swapDirectionToGuaranteedType(swapDirection); + }, [swapSummaryInfo.swapDirection]); + + const guaranteedStr = useMemo(() => { + const { amount, currency } = swapSummaryInfo.guaranteedAmount; + return `${toNumberFormat(amount)} ${currency}`; + }, [swapSummaryInfo.guaranteedAmount]); + + const gasFeeStr = useMemo(() => { + const { amount, currency } = swapSummaryInfo.gasFee; + return `${toNumberFormat(amount)} ${currency}`; + }, [swapSummaryInfo.gasFee]); + + const gasFeeUSDStr = useMemo(() => { + const gasFeeUSD = swapSummaryInfo.gasFeeUSD; + return `$${toNumberFormat(gasFeeUSD)}`; + }, [swapSummaryInfo.gasFeeUSD]); + return (
Price Impact - {swapGasInfo.priceImpact} + {priceImpactStr}
- Min. Received - {swapGasInfo.minReceived} + {guaranteedTypeStr} + {guaranteedStr}
Gas Fee - {swapGasInfo.gasFee} GNOT{" "} - ({swapGasInfo.usdExchangeGasFee}) + {gasFeeStr} + {`(${gasFeeUSDStr})`}
@@ -40,14 +67,10 @@ const SwapCardFeeInfo: React.FC = ({

Auto Router

- {autoRouter ? ( - - ) : ( - - )} + + {openedRouteInfo ? + : + }
); diff --git a/packages/web/src/components/swap/swap-card-header/SwapCardHeader.spec.tsx b/packages/web/src/components/swap/swap-card-header/SwapCardHeader.spec.tsx index 4eb0e198f..4bc7f28ea 100644 --- a/packages/web/src/components/swap/swap-card-header/SwapCardHeader.spec.tsx +++ b/packages/web/src/components/swap/swap-card-header/SwapCardHeader.spec.tsx @@ -6,13 +6,10 @@ import SwapCardHeader from "./SwapCardHeader"; describe("SwapCardHeader Component", () => { it("SwapCardHeader render", () => { const mockProps = { - settingMenuToggle: true, - onSettingMenu: () => null, - tolerance: "", - changeTolerance: () => null, - resetTolerance: () => null, - handleCopyClipBoard: () => null, - copied: true, + copied: false, + copyURL: () => null, + slippage: 0, + changeSlippage: () => null, }; render( diff --git a/packages/web/src/components/swap/swap-card-header/SwapCardHeader.stories.tsx b/packages/web/src/components/swap/swap-card-header/SwapCardHeader.stories.tsx index c98949aaf..819765365 100644 --- a/packages/web/src/components/swap/swap-card-header/SwapCardHeader.stories.tsx +++ b/packages/web/src/components/swap/swap-card-header/SwapCardHeader.stories.tsx @@ -19,13 +19,10 @@ const Template: ComponentStory = args => ( export const Default = Template.bind({}); Default.args = { - settingMenuToggle: true, - onSettingMenu: action("onSettingMenu"), - tolerance: "", - changeTolerance: action("changeTolerance"), - resetTolerance: action("resetTolerance"), - handleCopyClipBoard: action("handleCopyClipBoard"), - copied: true, + copied: false, + copyURL: action("copyURL"), + slippage: 0, + changeSlippage: action("changeSlippage"), }; const wrapper = () => css` diff --git a/packages/web/src/components/swap/swap-card-header/SwapCardHeader.tsx b/packages/web/src/components/swap/swap-card-header/SwapCardHeader.tsx index 9ff2e6fd9..9b89f5020 100644 --- a/packages/web/src/components/swap/swap-card-header/SwapCardHeader.tsx +++ b/packages/web/src/components/swap/swap-card-header/SwapCardHeader.tsx @@ -1,7 +1,7 @@ import IconLink from "@components/common/icons/IconLink"; import IconPolygon from "@components/common/icons/IconPolygon"; import IconSettings from "@components/common/icons/IconSettings"; -import React from "react"; +import React, { useCallback, useState } from "react"; import SettingMenuModal from "../setting-menu-modal/SettingMenuModal"; import { CopyTooltip, @@ -9,30 +9,34 @@ import { SwapCardHeaderWrapper, } from "./SwapCardHeader.styles"; interface SwapCardHeaderProps { - settingMenuToggle: boolean; - onSettingMenu: () => void; - tolerance: string; - changeTolerance: (e: React.ChangeEvent) => void; - resetTolerance: () => void; - handleCopyClipBoard: (text: string) => void; copied: boolean; + copyURL: () => void; + slippage: number; + changeSlippage: (value: string) => void; } const SwapCardHeader: React.FC = ({ - settingMenuToggle, - onSettingMenu, - tolerance, - changeTolerance, - resetTolerance, - handleCopyClipBoard, copied, + copyURL, + slippage, + changeSlippage, }) => { + const [openedSetting, setOpenedSetting] = useState(false); + + const openSetting = useCallback(() => { + setOpenedSetting(true); + }, []); + + const closeSetting = useCallback(() => { + setOpenedSetting(false); + }, []); + return (

Swap

handleCopyClipBoard("Copy Completed.")} + onClick={copyURL} > <> @@ -47,15 +51,14 @@ const SwapCardHeader: React.FC = ({
-
+
- {settingMenuToggle && ( + {openedSetting && ( )} diff --git a/packages/web/src/components/swap/swap-card/SwapCard.spec.tsx b/packages/web/src/components/swap/swap-card/SwapCard.spec.tsx deleted file mode 100644 index 15186c1f4..000000000 --- a/packages/web/src/components/swap/swap-card/SwapCard.spec.tsx +++ /dev/null @@ -1,76 +0,0 @@ -import { render } from "@testing-library/react"; -import { Provider as JotaiProvider } from "jotai"; -import GnoswapThemeProvider from "@providers/gnoswap-theme-provider/GnoswapThemeProvider"; -import SwapCard from "./SwapCard"; -import { - coinList, - dummyAutoRouterInfo, - dummySwapGasInfo, -} from "@containers/swap-container/SwapContainer"; -import { DEVICE_TYPE } from "@styles/media"; - -describe("SwapCard Component", () => { - it("SwapCard render", () => { - const mockProps = { - search: () => null, - keyword: "", - gnosAmount: "1", - isConnected: true, - autoRouter: false, - showAutoRouter: () => null, - swapGasInfo: dummySwapGasInfo, - swapInfo: true, - showSwapInfo: () => null, - autoRouterInfo: dummyAutoRouterInfo, - settingMenuToggle: true, - onSettingMenu: () => null, - tolerance: "", - changeTolerance: () => null, - tokenModal: true, - onSelectTokenModal: () => null, - swapOpen: true, - onConfirmModal: () => null, - coinList: coinList(), - changeToken: () => null, - selectToken: () => null, - submitSwap: () => null, - resetTolerance: () => null, - breakpoint: DEVICE_TYPE.WEB, - handleCopyClipBoard: () => null, - submit: false, - isFetching: true, - copied: true, - swapResult: { success: true, transaction: "https//:naver.com" }, - from: { - token: "USDCoin", - symbol: "USDC", - amount: "121", - price: "$0.00", - gnosExchangePrice: "1250", - usdExchangePrice: "($1541.55)", - balance: "0", - logoURI: - "https://raw.githubusercontent.com/Uniswap/assets/master/blockchains/ethereum/assets/0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48/logo.png", - }, - to: { - token: "HEX", - symbol: "HEX", - amount: "5000", - price: "$0.00", - gnosExchangePrice: "1250", - usdExchangePrice: "($1541.55)", - balance: "0", - logoURI: - "https://raw.githubusercontent.com/Uniswap/assets/master/blockchains/ethereum/assets/0x2b591e99afE9f32eAA6214f7B7629768c40Eeb39/logo.png", - }, - }; - - render( - - - - - , - ); - }); -}); diff --git a/packages/web/src/components/swap/swap-card/SwapCard.stories.tsx b/packages/web/src/components/swap/swap-card/SwapCard.stories.tsx index b33274a92..e847831b2 100644 --- a/packages/web/src/components/swap/swap-card/SwapCard.stories.tsx +++ b/packages/web/src/components/swap/swap-card/SwapCard.stories.tsx @@ -2,13 +2,82 @@ import React from "react"; import { ComponentStory, ComponentMeta } from "@storybook/react"; import SwapCard from "./SwapCard"; import { css } from "@emotion/react"; -import { - coinList, - dummyAutoRouterInfo, - dummySwapGasInfo, -} from "@containers/swap-container/SwapContainer"; -import { action } from "@storybook/addon-actions"; -import { DEVICE_TYPE } from "@styles/media"; +import { SwapSummaryInfo } from "@models/swap/swap-summary-info"; +import PoolData from "@repositories/pool/mock/pools.json"; +import { SwapRouteInfo } from "@models/swap/swap-route-info"; + +const pools = PoolData.pools; + +const swapSummaryInfo: SwapSummaryInfo = { + tokenA: { + chainId: "test3", + address: "0x111111111117dC0aa78b770fA6A738034120C302", + path: "gno.land/r/demo/1inch", + name: "1inch", + symbol: "1INCH", + decimals: 6, + logoURI: "https://assets.coingecko.com/coins/images/13469/thumb/1inch-token.png?1608803028", + priceId: "1inch", + createdAt: "1999-01-01T00:00:01Z" + }, + tokenB: { + chainId: "test3", + address: "0x111111111117dC0aa78b770fA6A738034120C302", + path: "gno.land/r/demo/1inch", + name: "1inch", + symbol: "1INCH", + decimals: 6, + logoURI: "https://assets.coingecko.com/coins/images/13469/thumb/1inch-token.png?1608803028", + priceId: "1inch", + createdAt: "1999-01-01T00:00:01Z" + }, + swapDirection: "EXACT_IN", + swapRate: 1.14, + swapRateUSD: 1.14, + priceImpact: 0.3, + guaranteedAmount: { + amount: 45124, + currency: "GNOT" + }, + gasFee: { + amount: 0.000001, + currency: "GNOT" + }, + gasFeeUSD: 0.1 +}; + +const swapRouteInfos: SwapRouteInfo[] = [{ + from: { + chainId: "test3", + address: "0x111111111117dC0aa78b770fA6A738034120C302", + path: "gno.land/r/demo/1inch", + name: "1inch", + symbol: "1INCH", + decimals: 6, + logoURI: "https://assets.coingecko.com/coins/images/13469/thumb/1inch-token.png?1608803028", + priceId: "1inch", + createdAt: "1999-01-01T00:00:01Z" + }, + to: { + chainId: "test3", + address: "0x111111111117dC0aa78b770fA6A738034120C302", + path: "gno.land/r/demo/1inch", + name: "1inch", + symbol: "1INCH", + decimals: 6, + logoURI: "https://assets.coingecko.com/coins/images/13469/thumb/1inch-token.png?1608803028", + priceId: "1inch", + createdAt: "1999-01-01T00:00:01Z" + }, + gasFee: { + amount: 0.000001, + currency: "GNOT" + }, + gasFeeUSD: 0.1, + pools, + version: "V1", + weight: 100, +}]; export default { title: "swap/SwapCard", @@ -25,57 +94,9 @@ const Template: ComponentStory = args => ( export const Default = Template.bind({}); Default.args = { - search: action("search"), - keyword: "", - gnosAmount: "1500", - isConnected: true, - autoRouter: true, - showAutoRouter: action("onClick"), - swapGasInfo: dummySwapGasInfo, - swapInfo: true, - showSwapInfo: action("onClick"), - autoRouterInfo: dummyAutoRouterInfo, - settingMenuToggle: true, - onSettingMenu: action("onSettingMenu"), - tolerance: "", - changeTolerance: action("changeTolerance"), - tokenModal: true, - onSelectTokenModal: action("onClick"), - swapOpen: true, - onConfirmModal: action("onClick"), - submitSwap: action("submitSwap"), - coinList: coinList(), - changeToken: action("changeToken"), - selectToken: action("selectToken"), - resetTolerance: action("resetTolerance"), - handleCopyClipBoard: action("handleCopyClipBoard"), - breakpoint: DEVICE_TYPE.WEB, - submit: false, - copied: true, - isFetching: true, - swapResult: { success: true, transaction: "https//:naver.com" }, - from: { - token: "USDCoin", - symbol: "USDC", - amount: "121", - price: "$0.00", - gnosExchangePrice: "1250", - usdExchangePrice: "($1541.55)", - balance: "0", - logoURI: - "https://raw.githubusercontent.com/Uniswap/assets/master/blockchains/ethereum/assets/0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48/logo.png", - }, - to: { - token: "HEX", - symbol: "HEX", - amount: "5000", - price: "$0.00", - gnosExchangePrice: "1250", - usdExchangePrice: "($1541.55)", - balance: "0", - logoURI: - "https://raw.githubusercontent.com/Uniswap/assets/master/blockchains/ethereum/assets/0x2b591e99afE9f32eAA6214f7B7629768c40Eeb39/logo.png", - }, + swapSummaryInfo, + swapRouteInfos, + swapButtonText: "Swap" }; const wrapper = () => css` diff --git a/packages/web/src/components/swap/swap-card/SwapCard.tsx b/packages/web/src/components/swap/swap-card/SwapCard.tsx index b60736a8a..410c12f23 100644 --- a/packages/web/src/components/swap/swap-card/SwapCard.tsx +++ b/packages/web/src/components/swap/swap-card/SwapCard.tsx @@ -3,178 +3,160 @@ import Button, { ButtonHierarchy } from "@components/common/button/Button"; import { SwapCardWrapper } from "./SwapCard.styles"; import SwapCardHeader from "../swap-card-header/SwapCardHeader"; import SwapCardContent from "../swap-card-content/SwapCardContent"; -import { - AutoRouterInfo, - tokenInfo, - SwapData, - SwapGasInfo, -} from "@containers/swap-container/SwapContainer"; import ConfirmSwapModal from "../confirm-swap-modal/ConfirmSwapModal"; -import { DEVICE_TYPE } from "@styles/media"; - -export interface TokenInfo { - token: string; - symbol: string; - amount: string; - price: string; - gnosExchangePrice: string; - usdExchangePrice: string; - balance: string; - logoURI: string; -} +import { TokenModel } from "@models/token/token-model"; +import { SwapTokenInfo } from "@models/swap/swap-token-info"; +import { SwapSummaryInfo } from "@models/swap/swap-summary-info"; +import { SwapRouteInfo } from "@models/swap/swap-route-info"; +import { SwapResultInfo } from "@models/swap/swap-result-info"; +import { FontsKey } from "@constants/font.constant"; interface SwapCardProps { - search: (e: React.ChangeEvent) => void; - keyword: string; - isConnected: boolean; - from: TokenInfo; - to: TokenInfo; - gnosAmount: string; - swapInfo: boolean; - showSwapInfo: () => void; - autoRouter: boolean; - showAutoRouter: () => void; - swapGasInfo: SwapGasInfo; - autoRouterInfo: AutoRouterInfo; - settingMenuToggle: boolean; - onSettingMenu: () => void; - tolerance: string; - changeTolerance: (e: React.ChangeEvent) => void; - tokenModal: boolean; - onSelectTokenModal: () => void; - swapOpen: boolean; - onConfirmModal: () => void; - coinList: tokenInfo[]; - changeToken: (token: tokenInfo, type: string) => void; - selectToken: (e: string) => void; - submitSwap: (event: React.MouseEvent) => void; - breakpoint: DEVICE_TYPE; - submit: boolean; - isFetching: boolean; - swapResult: SwapData | null; - resetTolerance: () => void; - handleCopyClipBoard: (text: string) => void; + connectedWallet: boolean; copied: boolean; + swapTokenInfo: SwapTokenInfo; + swapSummaryInfo: SwapSummaryInfo | null; + swapRouteInfos: SwapRouteInfo[]; + isAvailSwap: boolean; + swapButtonText: string; + submitted: boolean; + swapResult: SwapResultInfo | null; + openedConfirmModal: boolean; + + changeTokenA: (token: TokenModel) => void; + changeTokenAAmount: (value: string) => void; + changeTokenB: (token: TokenModel) => void; + changeTokenBAmount: (value: string) => void; + changeSlippage: (value: string) => void; + + switchSwapDirection: () => void; + openConfirmModal: () => void; + openConnectWallet: () => void; + closeModal: () => void; + copyURL: () => void; + swap: () => void; } const SwapCard: React.FC = ({ - search, - keyword, - from, - to, - gnosAmount, - isConnected, - swapInfo, - showSwapInfo, - autoRouter, - showAutoRouter, - swapGasInfo, - autoRouterInfo, - settingMenuToggle, - onSettingMenu, - tolerance, - changeTolerance, - tokenModal, - onSelectTokenModal, - swapOpen, - onConfirmModal, - coinList, - changeToken, - selectToken, - submitSwap, - breakpoint, - submit, - isFetching, - swapResult, - resetTolerance, - handleCopyClipBoard, + connectedWallet, copied, + swapTokenInfo, + swapSummaryInfo, + swapRouteInfos, + isAvailSwap, + swapButtonText, + submitted, + swapResult, + openedConfirmModal, + changeTokenA, + changeTokenAAmount, + changeTokenB, + changeTokenBAmount, + changeSlippage, + switchSwapDirection, + openConfirmModal, + openConnectWallet, + closeModal, + copyURL, + swap, }) => { + return ( <>
- {isConnected ? ( - Number(gnosAmount) - Number(from.gnosExchangePrice) > 0 ? ( -
- {swapOpen && ( + + {openedConfirmModal && swapSummaryInfo && ( )} ); }; +interface SwapButtonProps { + connectedWallet: boolean; + isAvailSwap: boolean; + text: string; + openConfirmModal: () => void; + openConnectWallet: () => void; +} + +const SwapButton: React.FC = ({ + connectedWallet, + isAvailSwap, + text, + openConfirmModal, + openConnectWallet, +}) => { + + const defaultStyle = { + fullWidth: true, + fontType: "body7" as FontsKey, + hierarchy: ButtonHierarchy.Primary, + height: 56, + }; + + if (!connectedWallet) { + return ( +