diff --git a/packages/app/assets/svg/app/caret-down-color-slim.svg b/packages/app/assets/svg/app/caret-down-color-slim.svg new file mode 100644 index 0000000000..c0b87ecdf7 --- /dev/null +++ b/packages/app/assets/svg/app/caret-down-color-slim.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/packages/app/package.json b/packages/app/package.json index 232fa1d606..c7734c4542 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -19,8 +19,6 @@ "@eth-optimism/contracts": "0.5.37", "@ethersproject/bignumber": "^5.7.0", "@ethersproject/providers": "^5.7.2", - "@gnosis.pm/safe-apps-provider": "^0.14.0", - "@gnosis.pm/safe-apps-sdk": "^7.8.0", "@kwenta/sdk": "workspace:*", "@material-ui/core": "^4.12.4", "@pythnetwork/pyth-evm-js": "1.17.0", @@ -41,7 +39,7 @@ "date-fns": "2.21.3", "date-fns-tz": "2.0.0", "echarts": "5.4.2", - "eslint-config-next": "^13.4.6", + "eslint-config-next": "^13.4.8", "ethcall": "4.7.2", "ethers": "5.7.2", "graphql-request": "3.4.0", @@ -50,7 +48,7 @@ "lightweight-charts": "4.0.1", "lodash": "4.17.21", "moment-business-time": "2.0.0", - "next": "^13.3.4", + "next": "^13.4.8", "next-compose-plugins": "2.2.1", "next-connect": "^0.13.0", "next-transpile-modules": "10.0.0", @@ -58,15 +56,16 @@ "react-dom": "^18.2.0", "react-i18next": "12.1.1", "react-infinite-scroll-component": "^6.1.0", - "react-is": "^18.0.0", + "react-is": "^18.2.0", "react-query": "3.39.3", "react-redux": "8.1.1", "react-responsive": "^9.0.2", - "react-rnd": "^10.3.7", + "react-rnd": "^10.4.1", "react-select": "4.3.1", "react-slick": "0.29.0", - "react-toastify": "^9.0.4", - "recharts": "^2.5.0", + "react-table": "7.7.0", + "react-toastify": "^9.1.3", + "recharts": "^2.7.2", "redux-logger": "^3.0.6", "redux-persist": "^6.0.0", "slick-carousel": "1.8.1", @@ -89,8 +88,8 @@ "@testing-library/react": "14.0.0", "@testing-library/react-hooks": "8.0.1", "@testing-library/user-event": "14.4.3", - "@typechain/ethers-v5": "^10.1.0", - "@types/cors": "^2.8.12", + "@typechain/ethers-v5": "^10.2.1", + "@types/cors": "^2.8.13", "@types/date-fns": "2.6.0", "@types/ethereum-block-by-date": "^1.4.1", "@types/jest": "27.0.2", @@ -111,7 +110,7 @@ "jest": "28.1.0", "jest-environment-jsdom": "28.1.0", "jest-preview": "^0.3.1", - "jest-transformer-svg": "^2.0.0", + "jest-transformer-svg": "^2.0.1", "next-router-mock": "0.9.3", "pinst": "3.0.0", "postcss": "^8.4.24", diff --git a/packages/app/src/components/Button/Button.tsx b/packages/app/src/components/Button/Button.tsx index 2d15ced91b..8875055b81 100644 --- a/packages/app/src/components/Button/Button.tsx +++ b/packages/app/src/components/Button/Button.tsx @@ -16,6 +16,7 @@ export type ButtonVariant = | 'yellow' | 'long' | 'short' + | 'staking-button' type BaseButtonProps = { $size: 'xsmall' | 'small' | 'medium' | 'large' @@ -25,6 +26,7 @@ type BaseButtonProps = { fullWidth?: boolean noOutline?: boolean textColor?: 'yellow' + outlineColor?: 'yellow' textTransform?: 'none' | 'uppercase' | 'capitalize' | 'lowercase' $active?: boolean $mono?: boolean @@ -151,9 +153,8 @@ const BaseButton = styled.button` } `} - ${(props) => - props.$variant === 'yellow' && + (props.$variant === 'yellow' || props.$variant === 'staking-button') && css` background: ${props.theme.colors.selectedTheme.button.yellow.fill}; border: 1px solid ${props.theme.colors.selectedTheme.button.yellow.border}; @@ -166,6 +167,11 @@ const BaseButton = styled.button` &::before { display: none; } + + ${props.$variant === 'staking-button' && + css` + border: 1px solid ${props.theme.colors.selectedTheme.button.yellow.text}; + `} `} font-family: ${(props) => @@ -233,6 +239,7 @@ type ButtonProps = { fullWidth?: boolean noOutline?: boolean textColor?: 'yellow' + outlineColor?: 'yellow' textTransform?: 'none' | 'uppercase' | 'capitalize' | 'lowercase' style?: React.CSSProperties disabled?: boolean diff --git a/packages/app/src/components/Button/TabButton.tsx b/packages/app/src/components/Button/TabButton.tsx index e36df52b56..ae8d1d81ab 100644 --- a/packages/app/src/components/Button/TabButton.tsx +++ b/packages/app/src/components/Button/TabButton.tsx @@ -21,6 +21,7 @@ export type TabButtonProps = { isRounded?: boolean onClick?: () => any flat?: boolean + variant?: 'noOutline' } const InnerButton: React.FC = React.memo( @@ -63,6 +64,7 @@ const TabButton: React.FC = React.memo( $nofill={props.nofill} $flat={flat} onClick={onClick} + $variant={props.variant} > @@ -127,6 +129,7 @@ const sharedStyle = css<{ &:hover { background: ${(props) => props.theme.colors.selectedTheme.newTheme.button.default.hover.background}; + border-width: 1px; } .title { @@ -184,6 +187,7 @@ const StyledButton = styled(Button).attrs({ size: 'small' })<{ $nofill?: boolean $flat?: boolean active?: boolean + $variant?: 'noOutline' | undefined }>` p { text-align: left; @@ -194,6 +198,15 @@ const StyledButton = styled(Button).attrs({ size: 'small' })<{ border-radius: ${props.isRounded ? '100px' : '8px'}; `} ${sharedStyle} + + ${(props) => + props.$variant === 'noOutline' && + css` + border: none; + border-radius: 100px; + padding: 10px 15px; + width: 75px; + `} ` export default TabButton diff --git a/packages/app/src/components/Checkbox.tsx b/packages/app/src/components/Checkbox.tsx index c2786ddf66..c2db014f67 100644 --- a/packages/app/src/components/Checkbox.tsx +++ b/packages/app/src/components/Checkbox.tsx @@ -1,5 +1,5 @@ import { FC, memo } from 'react' -import styled from 'styled-components' +import styled, { css } from 'styled-components' import { Body } from 'components/Text' @@ -8,18 +8,35 @@ type CheckboxProps = { label: string checked: boolean checkSide?: 'left' | 'right' + variant?: 'item' | 'table' onChange: () => void } export const Checkbox: FC = memo( - ({ id, label, checked, onChange, checkSide = 'left', ...props }) => ( + ({ id, label, checked, onChange, checkSide = 'left', variant = 'item', ...props }) => ( {checkSide === 'left' && ( - + )} {checkSide === 'right' && ( - + )} ) @@ -34,7 +51,7 @@ const CheckboxContainer = styled.div` gap: 8px; ` -const Input = styled.input` +const Input = styled.input<{ variant: 'item' | 'table' }>` -webkit-appearance: none; -moz-appearance: none; -o-appearance: none; @@ -66,9 +83,27 @@ const Input = styled.input` box-shadow: inset 1em 1em var(--form-control-color); background-color: ${(props) => props.theme.colors.selectedTheme.newTheme.text.primary}; } + &:checked::before { transform: scale(1); } + + ${(props) => + props.variant === 'table' && + css` + background-color: ${(props) => props.theme.colors.selectedTheme.newTheme.checkBox.background}; + width: 13px; + height: 13px; + border-radius: 4px; + + &::before { + clip-path: none; + } + &:checked::before { + background-color: ${props.theme.colors.selectedTheme.newTheme.checkBox.checked}; + border-radius: 4px; + } + `} ` const Label = styled.label` diff --git a/packages/app/src/components/Media.tsx b/packages/app/src/components/Media.tsx index 01fefb7770..8ea9289cf1 100644 --- a/packages/app/src/components/Media.tsx +++ b/packages/app/src/components/Media.tsx @@ -15,6 +15,14 @@ export const DesktopOnlyView: FC = memo(({ children }) => ( {children} )) +export const DesktopLargeOnlyView: FC = memo(({ children }) => ( + {children} +)) + +export const DesktopSmallOnlyView: FC = memo(({ children }) => ( + {children} +)) + export const TabletOnlyView: FC = memo(({ children }) => ( {children} diff --git a/packages/app/src/components/Nav/NavLink.tsx b/packages/app/src/components/Nav/NavLink.tsx index 10c1caae8c..fc9840c1fe 100644 --- a/packages/app/src/components/Nav/NavLink.tsx +++ b/packages/app/src/components/Nav/NavLink.tsx @@ -2,6 +2,8 @@ import Link from 'next/link' import React, { ReactNode } from 'react' import styled from 'styled-components' +import LinkIconLight from 'assets/svg/app/link-light.svg' +import { FlexDivRowCentered } from 'components/layout/flex' import { linkCSS } from 'styles/common' type NavButtonProps = { @@ -16,15 +18,25 @@ type NavButtonProps = { const NavButton: React.FC = ({ title, href, external, disabled, ...props }) => { return (
- + {external ? ( - {title} + + {title} + - + ) : ( + + + {title} + + + )}
) } diff --git a/packages/app/src/components/Rainbowkit/Gnosis/Gnosis.ts b/packages/app/src/components/Rainbowkit/Gnosis/Gnosis.ts deleted file mode 100644 index 75766ad1f7..0000000000 --- a/packages/app/src/components/Rainbowkit/Gnosis/Gnosis.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { Chain, Wallet } from '@rainbow-me/rainbowkit' - -import GnosisIcon from 'assets/png/rainbowkit/gnosis.png' - -import { SafeConnector } from './SafeConnector' - -type SafeOptions = { - chains: Chain[] -} - -const Safe = ({ chains }: SafeOptions): Wallet => ({ - id: 'safe', - iconBackground: '#FFF', - name: 'Gnosis Safe', - iconUrl: async () => GnosisIcon, - downloadUrls: { - android: 'https://play.google.com/store/apps/details?id=io.gnosis.safe', - ios: 'https://apps.apple.com/us/app/gnosis-safe/idid1515759131', - }, - // @ts-ignore - createConnector: () => { - const connector = new SafeConnector({ chains }) - return { - connector, - } - }, -}) - -export default Safe diff --git a/packages/app/src/components/Rainbowkit/Gnosis/SafeConnector.ts b/packages/app/src/components/Rainbowkit/Gnosis/SafeConnector.ts deleted file mode 100644 index 98550cd2b4..0000000000 --- a/packages/app/src/components/Rainbowkit/Gnosis/SafeConnector.ts +++ /dev/null @@ -1,151 +0,0 @@ -import { Web3Provider } from '@ethersproject/providers' -import { SafeAppProvider } from '@gnosis.pm/safe-apps-provider' -import SafeAppsSDK, { Opts as SafeOpts, SafeInfo } from '@gnosis.pm/safe-apps-sdk' -import { getAddress } from 'ethers/lib/utils' -import { Connector, Chain, ConnectorNotFoundError } from 'wagmi' - -function normalizeChainId(chainId: string | number) { - if (typeof chainId === 'string') { - const isHex = chainId.trim().substring(0, 2) - - return Number.parseInt(chainId, isHex === '0x' ? 16 : 10) - } - return chainId -} - -const __IS_SERVER__ = typeof window === 'undefined' -const __IS_IFRAME__ = !__IS_SERVER__ && window?.parent !== window - -class SafeConnector extends Connector { - readonly id = 'safe' - readonly name = 'Safe' - ready = !__IS_SERVER__ && __IS_IFRAME__ - - #provider?: SafeAppProvider - #sdk: SafeAppsSDK - #safe?: SafeInfo - - constructor(config: { chains?: Chain[]; options?: SafeOpts }) { - super({ ...config, options: config?.options }) - - this.#sdk = new SafeAppsSDK(config.options) - } - - async connect() { - const runningAsSafeApp = await this.#isSafeApp() - if (!runningAsSafeApp) { - throw new ConnectorNotFoundError() - } - - const provider = await this.getProvider() - if (provider.on) { - provider.on('accountsChanged', this.onAccountsChanged) - provider.on('chainChanged', this.onChainChanged) - provider.on('disconnect', this.onDisconnect) - } - - const account = await this.getAccount() - const id = await this.getChainId() - - return { - account, - provider, - chain: { id, unsupported: this.isChainUnsupported(id) }, - } - } - - async disconnect() { - const provider = await this.getProvider() - if (!provider?.removeListener) return - - provider.removeListener('accountsChanged', this.onAccountsChanged) - provider.removeListener('chainChanged', this.onChainChanged) - provider.removeListener('disconnect', this.onDisconnect) - } - - async getAccount() { - if (!this.#safe) { - throw new ConnectorNotFoundError() - } - - return getAddress(this.#safe.safeAddress) - } - - async getChainId() { - if (!this.#provider) { - throw new ConnectorNotFoundError() - } - - return normalizeChainId(this.#provider.chainId) - } - - async #getSafeInfo(): Promise { - if (!this.#sdk) { - throw new ConnectorNotFoundError() - } - if (!this.#safe) { - this.#safe = await this.#sdk.safe.getInfo() - } - return this.#safe - } - - async #isSafeApp(): Promise { - if (!this.ready) { - return false - } - - const safe = await Promise.race([ - this.#getSafeInfo(), - new Promise((resolve) => setTimeout(resolve, 300)), - ]) - return !!safe - } - - async getProvider() { - if (!this.#provider) { - const safe = await this.#getSafeInfo() - if (!safe) { - throw new Error('Could not load Safe information') - } - - this.#provider = new SafeAppProvider(safe, this.#sdk) - } - return this.#provider - } - - async getSigner() { - const provider = await this.getProvider() - const account = await this.getAccount() - return new Web3Provider(provider).getSigner(account) - } - - async isAuthorized() { - try { - const account = await this.getAccount() - return !!account - } catch { - return false - } - } - - protected onAccountsChanged(accounts: string[]) { - if (accounts.length === 0) this.emit('disconnect') - else this.emit('change', { account: getAddress(accounts[0]) }) - } - - protected isChainUnsupported(chainId: number) { - return !this.chains.some((x) => x.id === chainId) - } - - protected onChainChanged(chainId: string | number) { - const id = normalizeChainId(chainId) - const unsupported = this.isChainUnsupported(id) - this.emit('change', { chain: { id, unsupported } }) - } - - protected onDisconnect() { - this.emit('disconnect') - } -} - -export { SafeConnector } diff --git a/packages/app/src/components/Rainbowkit/Gnosis/index.ts b/packages/app/src/components/Rainbowkit/Gnosis/index.ts deleted file mode 100644 index 2654fdd89e..0000000000 --- a/packages/app/src/components/Rainbowkit/Gnosis/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { default } from './Gnosis' diff --git a/packages/app/src/components/StakeCard.tsx b/packages/app/src/components/StakeCard.tsx index bbb4b2047e..283ee88ca4 100644 --- a/packages/app/src/components/StakeCard.tsx +++ b/packages/app/src/components/StakeCard.tsx @@ -6,11 +6,15 @@ import styled from 'styled-components' import Button from 'components/Button' import NumericInput from 'components/Input/NumericInput' -import { FlexDivRowCentered } from 'components/layout/flex' +import { FlexDivCol, FlexDivRowCentered } from 'components/layout/flex' import SegmentedControl from 'components/SegmentedControl' import { DEFAULT_CRYPTO_DECIMALS, DEFAULT_TOKEN_DECIMALS } from 'constants/defaults' import { StakingCard } from 'sections/dashboard/Stake/card' -import { numericValueCSS } from 'styles/common' +import media from 'styles/media' + +import ErrorView from './ErrorView' +import Spacer from './Spacer' +import { Body, NumericValue } from './Text' type StakeCardProps = { title: string @@ -24,6 +28,9 @@ type StakeCardProps = { isUnstaked?: boolean | undefined isApproved?: boolean onApprove?: () => void + isStaking?: boolean + isUnstaking?: boolean + isApproving?: boolean } const StakeCard: FC = memo( @@ -39,6 +46,9 @@ const StakeCard: FC = memo( isUnstaked = false, isApproved, onApprove, + isStaking = false, + isUnstaking = false, + isApproving = false, }) => { const { t } = useTranslation() @@ -69,6 +79,10 @@ const StakeCard: FC = memo( return truncateNumbers(balance, DEFAULT_CRYPTO_DECIMALS) }, [balance]) + const isLoading = useMemo(() => { + return activeTab === 0 ? (isApproved ? isStaking : isApproving) : isUnstaking + }, [activeTab, isApproved, isApproving, isStaking, isUnstaking]) + const onMaxClick = useCallback(() => { setAmount(truncateNumbers(balance, DEFAULT_TOKEN_DECIMALS)) }, [balance]) @@ -79,10 +93,12 @@ const StakeCard: FC = memo( }, []) const handleSubmit = useCallback(() => { - if (!isApproved) { - onApprove?.() - } else if (isStakeEnabled) { - onStake(amount) + if (isStakeEnabled) { + if (isApproved) { + onStake(amount) + } else { + onApprove?.() + } } else if (isUnstakeEnabled) { onUnstake(amount) } @@ -112,25 +128,41 @@ const StakeCard: FC = memo( onChange={handleTabChange} selectedIndex={activeTab} /> - - -
{title}
- -
{t('dashboard.stake.tabs.stake-table.balance')}
-
- {balanceString} -
-
-
- -
- + + + + {title} + + {t('dashboard.stake.tabs.stake-table.balance')} + {balanceString} + + + + + + + + ) } @@ -142,29 +174,21 @@ const StyledFlexDivRowCentered = styled(FlexDivRowCentered)` const StakingInputCardContainer = styled(StakingCard)` min-height: 125px; - max-height: 250px; - display: flex; - flex-direction: column; - justify-content: space-between; + flex: 1; + margin-bottom: 0px; + ${media.lessThan('lg')` + width: 100%; + `} ` -const StakeInputHeader = styled.div` - display: flex; - justify-content: space-between; - align-items: center; - margin-bottom: 10px; +const StakeInputHeader = styled(FlexDivRowCentered)` + margin: 25px 0 10px; color: ${(props) => props.theme.colors.selectedTheme.title}; font-size: 14px; - - .max { - cursor: pointer; - color: ${(props) => props.theme.colors.selectedTheme.button.text.primary}; - ${numericValueCSS}; - } ` -const StakeInputContainer = styled.div` - margin: 20px 0; +const NumericValueButton = styled(NumericValue)` + cursor: pointer; ` export default StakeCard diff --git a/packages/app/src/components/Table/Pagination.tsx b/packages/app/src/components/Table/Pagination.tsx index ffb0a4cfaf..c7f073eee9 100644 --- a/packages/app/src/components/Table/Pagination.tsx +++ b/packages/app/src/components/Table/Pagination.tsx @@ -8,8 +8,9 @@ import RightEndArrowIcon from 'assets/svg/app/caret-right-end.svg' import RightArrowIcon from 'assets/svg/app/caret-right.svg' import { GridDivCenteredCol } from 'components/layout/grid' import { resetButtonCSS } from 'styles/common' +import media from 'styles/media' -type PaginationProps = { +export type PaginationProps = { pageIndex: number pageCount: number canNextPage: boolean @@ -18,6 +19,7 @@ type PaginationProps = { setPage: (page: number) => void previousPage: () => void nextPage: () => void + extra?: React.ReactNode } const Pagination: FC = React.memo( @@ -30,6 +32,7 @@ const Pagination: FC = React.memo( setPage, nextPage, previousPage, + extra, }) => { const { t } = useTranslation() @@ -37,35 +40,46 @@ const Pagination: FC = React.memo( const toLastPage = () => setPage(pageCount - 1) return ( - - - - - - - - - - - {t('common.pagination.page')}{' '} - {t('common.pagination.page-of-total-pages', { - page: pageIndex + 1, - totalPages: pageCount, - })} - - - - - - - - - - + <> + + + + + + + + + + + {t('common.pagination.page')}{' '} + {t('common.pagination.page-of-total-pages', { + page: pageIndex + 1, + totalPages: pageCount, + })} + + + + + + + + + + + {extra} + ) } ) +const ArrowButtonContainer = styled.div` + ${media.lessThan('lg')` + display: flex; + felx-direction: row; + column-gap: 5px; + `} +` + const PageInfo = styled.span` color: ${(props) => props.theme.colors.selectedTheme.gray}; ` @@ -76,6 +90,11 @@ const PaginationContainer = styled(GridDivCenteredCol)<{ compact: boolean }>` border-bottom-left-radius: 4px; border-bottom-right-radius: 4px; justify-items: center; + + ${media.lessThan('lg')` + border: ${(props) => props.theme.colors.selectedTheme.border}; + border-radius: 0px; + `} ` const ArrowButton = styled.button` @@ -90,6 +109,20 @@ const ArrowButton = styled.button` height: 14px; fill: ${(props) => props.theme.colors.selectedTheme.button.text.primary}; } + + ${media.lessThan('lg')` + border: none; + border-radius: 100px; + padding: 4px; + width: 24px; + height: 24px; + background: ${(props) => props.theme.colors.selectedTheme.newTheme.button.default.background}; + + svg { + width: 9px; + height: 9px; + } + `} ` export default Pagination diff --git a/packages/app/src/components/Table/StakingPagination.tsx b/packages/app/src/components/Table/StakingPagination.tsx new file mode 100644 index 0000000000..0ffac9b9fd --- /dev/null +++ b/packages/app/src/components/Table/StakingPagination.tsx @@ -0,0 +1,116 @@ +import React, { FC } from 'react' +import { useTranslation } from 'react-i18next' +import styled from 'styled-components' + +import LeftEndArrowIcon from 'assets/svg/app/caret-left-end.svg' +import LeftArrowIcon from 'assets/svg/app/caret-left.svg' +import RightEndArrowIcon from 'assets/svg/app/caret-right-end.svg' +import RightArrowIcon from 'assets/svg/app/caret-right.svg' +import { FlexDivRowCentered } from 'components/layout/flex' +import { GridDivCenteredCol } from 'components/layout/grid' + +type PaginationProps = { + pageIndex: number + pageCount: number + canNextPage: boolean + canPreviousPage: boolean + compact: boolean + setPage: (page: number) => void + previousPage: () => void + nextPage: () => void + extra?: React.ReactNode +} + +const StakingPagination: FC = React.memo( + ({ + pageIndex, + pageCount, + canNextPage = true, + canPreviousPage = true, + compact = false, + setPage, + nextPage, + previousPage, + extra, + }) => { + const { t } = useTranslation() + + const firstPage = () => setPage(0) + const toLastPage = () => setPage(pageCount - 1) + + return ( + + + + + + + + + + + + + + + + + + + + + + {t('common.pagination.page')}{' '} + {t('common.pagination.page-of-total-pages', { + page: pageIndex + 1, + totalPages: pageCount, + })} + + {extra} + + + ) + } +) + +const PaginationContainer = styled(GridDivCenteredCol)` + background: ${(props) => props.theme.colors.selectedTheme.newTheme.containers.cards.background}; + padding: 20px 25px; + border-radius: 100px; + border: 1px solid ${(props) => props.theme.colors.selectedTheme.newTheme.border.color}; + margin-top: 15px; +` + +const PageInfo = styled.span` + color: ${(props) => props.theme.colors.selectedTheme.gray}; + margin-left: 10px; + font-size: 13px; +` + +const PageInfoContainer = styled(GridDivCenteredCol)<{ compact: boolean }>` + grid-template-columns: auto 1fr auto; + padding: ${(props) => (props.compact ? '10px' : '15px')} 12px; + border-bottom-left-radius: 4px; + border-bottom-right-radius: 4px; +` + +const ArrowButton = styled.button` + background: ${(props) => props.theme.colors.selectedTheme.newTheme.button.default.background}; + border: none; + border-radius: 100px; + padding: 4px; + width: 24px; + height: 24px; + + &[disabled] { + cursor: default; + opacity: 0.5; + } + svg { + height: 9px; + width: 9px; + fill: ${(props) => props.theme.colors.selectedTheme.button.text.primary}; + } +` + +export default StakingPagination diff --git a/packages/app/src/components/Table/Table.tsx b/packages/app/src/components/Table/Table.tsx index 4369ed075c..25685652db 100644 --- a/packages/app/src/components/Table/Table.tsx +++ b/packages/app/src/components/Table/Table.tsx @@ -1,13 +1,13 @@ import { useReactTable, getCoreRowModel, - getPaginationRowModel, - getSortedRowModel, flexRender, PaginationState, + getPaginationRowModel, + getSortedRowModel, } from '@tanstack/react-table' import type { ColumnDef, Row, SortingState, VisibilityState } from '@tanstack/react-table' -import React, { DependencyList, useCallback, useMemo, useRef, useState } from 'react' +import React, { DependencyList, FC, useCallback, useMemo, useRef, useState } from 'react' import styled, { css } from 'styled-components' import { genericMemo } from 'types/helpers' @@ -18,13 +18,14 @@ import Loader from 'components/Loader' import { Body } from 'components/Text' import media from 'styles/media' -import Pagination from './Pagination' +import Pagination, { PaginationProps } from './Pagination' import TableBodyRow, { TableCell } from './TableBodyRow' const CARD_HEIGHT_MD = '50px' const CARD_HEIGHT_LG = '40px' const MAX_PAGE_ROWS = 100 const MAX_TOTAL_ROWS = 9999 +const SHORT_PAGE_SIZE = 5 export function compareNumericString(rowA: Row, rowB: Row, id: string, desc: boolean) { let a = parseFloat(rowA.getValue(id)) @@ -41,6 +42,17 @@ export function compareNumericString(rowA: Row, rowB: Row, id: stri return 0 } +function calculatePageSize( + showPagination: boolean, + showShortList: boolean | undefined, + pageSize: number | undefined +): number { + if (showPagination) { + return pageSize ? pageSize : MAX_PAGE_ROWS + } + return showShortList ? pageSize ?? SHORT_PAGE_SIZE : MAX_TOTAL_ROWS +} + type TableProps = { data: T[] columns: ColumnDef[] @@ -61,6 +73,8 @@ type TableProps = { noBottom?: boolean columnVisibility?: VisibilityState columnsDeps?: DependencyList + paginationExtra?: React.ReactNode + CustomPagination?: FC } const Table = ({ @@ -74,6 +88,7 @@ const Table = ({ pageSize = undefined, hideHeaders, highlightRowsOnHover, + showShortList, sortBy = [], lastRef = null, compactPagination = false, @@ -81,15 +96,16 @@ const Table = ({ noBottom = false, columnVisibility, columnsDeps = [], + paginationExtra, + CustomPagination, }: TableProps) => { const [sorting, setSorting] = useState(sortBy) const [pagination, setPagination] = useState({ pageIndex: 0, - pageSize: showPagination ? pageSize ?? MAX_PAGE_ROWS : MAX_TOTAL_ROWS, + pageSize: calculatePageSize(showPagination, showShortList, pageSize), }) // FIXME: It is probably better to memoize columns per-component. - const memoizedColumns = useMemo( () => columns, // eslint-disable-next-line react-hooks/exhaustive-deps @@ -110,6 +126,11 @@ const Table = ({ const defaultRef = useRef(null) + const shouldShowPagination = useMemo( + () => showPagination && !showShortList && data.length > table.getState().pagination.pageSize, + [data.length, showPagination, showShortList, table] + ) + const handleRowClick = useCallback( (row: Row) => () => { onTableRowClick?.(row) @@ -118,76 +139,92 @@ const Table = ({ ) return ( - - - {table.getHeaderGroups().map((headerGroup) => ( - - {headerGroup.headers.map((header) => { - return ( - - {flexRender(header.column.columnDef.header, header.getContext())} - {header.column.getCanSort() && ( - - {header.column.getIsSorted() ? ( - header.column.getIsSorted() === 'desc' ? ( - + <> + + + {table.getHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((header) => { + return ( + + {flexRender(header.column.columnDef.header, header.getContext())} + {header.column.getCanSort() && ( + + {header.column.getIsSorted() ? ( + header.column.getIsSorted() === 'desc' ? ( + + ) : ( + + ) ) : ( - - ) - ) : ( - <> - - - - )} - - )} - - ) - })} - - ))} - {isLoading ? ( - - ) : !!noResultsMessage && data.length === 0 ? ( - noResultsMessage - ) : ( - - {table.getRowModel().rows.map((row, idx) => { - const localRef = - lastRef && idx === table.getState().pagination.pageSize - 1 ? lastRef : defaultRef - return ( - - ) - })} - - )} - {showPagination && data.length > table.getState().pagination.pageSize ? ( - - ) : undefined} - - + <> + + + + )} + + )} + + ) + })} + + ))} + {isLoading ? ( + + ) : !!noResultsMessage && data.length === 0 ? ( + noResultsMessage + ) : ( + + {table.getRowModel().rows.map((row, idx) => { + const localRef = + lastRef && idx === table.getState().pagination.pageSize - 1 ? lastRef : defaultRef + return ( + + ) + })} + + )} + {(shouldShowPagination || paginationExtra) && !CustomPagination ? ( + + ) : undefined} + + + {CustomPagination && ( + + )} + ) } @@ -247,7 +284,6 @@ const ReactTable = styled.div<{ $rounded?: boolean; $noBottom?: boolean }>` display: flex; flex-direction: column; width: 100%; - height: 100%; overflow: auto; position: relative; border: ${(props) => props.theme.colors.selectedTheme.border}; diff --git a/packages/app/src/components/layout/flex.ts b/packages/app/src/components/layout/flex.ts index 88c2be36a2..0ea81dcb12 100644 --- a/packages/app/src/components/layout/flex.ts +++ b/packages/app/src/components/layout/flex.ts @@ -9,9 +9,10 @@ export const FlexDivCentered = styled(FlexDiv)` align-items: center; ` -export const FlexDivCol = styled(FlexDiv)<{ rowGap?: string }>` +export const FlexDivCol = styled(FlexDiv)<{ rowGap?: string; alignItems?: string }>` flex-direction: column; row-gap: ${(props) => props.rowGap || 'initial'}; + align-items: ${(props) => props.alignItems || 'initial'}; ` export const FlexDivColCentered = styled(FlexDivCol)` diff --git a/packages/app/src/components/layout/grid.ts b/packages/app/src/components/layout/grid.ts index 07203ca477..ea9ed49f77 100644 --- a/packages/app/src/components/layout/grid.ts +++ b/packages/app/src/components/layout/grid.ts @@ -23,7 +23,7 @@ export const SplitContainer = styled.div` grid-gap: 15px; margin-top: 10px; - ${media.greaterThan('mdUp')` + ${media.greaterThan('lg')` grid-template-columns: 1fr 1fr; `} ` diff --git a/packages/app/src/containers/Connector/config.ts b/packages/app/src/containers/Connector/config.ts index 52c830a135..dc7f661ddd 100644 --- a/packages/app/src/containers/Connector/config.ts +++ b/packages/app/src/containers/Connector/config.ts @@ -5,6 +5,7 @@ import { injectedWallet, metaMaskWallet, rainbowWallet, + safeWallet, walletConnectWallet, } from '@rainbow-me/rainbowkit/wallets' import { configureChains, createClient } from 'wagmi' @@ -23,11 +24,9 @@ import { jsonRpcProvider } from 'wagmi/providers/jsonRpc' import { publicProvider } from 'wagmi/providers/public' import BinanceIcon from 'assets/png/rainbowkit/binance.png' - -import Frame from '../../components/Rainbowkit/Frame' -import Safe from '../../components/Rainbowkit/Gnosis' -import Tally from '../../components/Rainbowkit/Tally' -import { BLAST_NETWORK_LOOKUP, STALL_TIMEOUT } from '../../constants/network' +import Frame from 'components/Rainbowkit/Frame' +import Tally from 'components/Rainbowkit/Tally' +import { BLAST_NETWORK_LOOKUP, STALL_TIMEOUT } from 'constants/network' const bscWithIcon: Chain = { ...bsc, @@ -71,7 +70,7 @@ const connectors = connectorsForWallets([ { groupName: 'Popular', wallets: [ - Safe({ chains }), + safeWallet({ chains }), metaMaskWallet({ projectId, chains }), rainbowWallet({ projectId, chains }), coinbaseWallet({ appName: 'Kwenta', chains }), diff --git a/packages/app/src/pages/dashboard/rewards.tsx b/packages/app/src/pages/dashboard/rewards.tsx deleted file mode 100644 index 647583894e..0000000000 --- a/packages/app/src/pages/dashboard/rewards.tsx +++ /dev/null @@ -1,37 +0,0 @@ -import Head from 'next/head' -import React, { ReactNode, useEffect } from 'react' -import { useTranslation } from 'react-i18next' - -import DashboardLayout from 'sections/dashboard/DashboardLayout' -import RewardsTabs from 'sections/dashboard/RewardsTabs' -import { useAppDispatch, useAppSelector } from 'state/hooks' -import { fetchClaimableRewards, fetchStakingData } from 'state/staking/actions' - -type RewardsComponent = React.FC & { getLayout: (page: ReactNode) => JSX.Element } - -const RewardsPage: RewardsComponent = () => { - const { t } = useTranslation() - const dispatch = useAppDispatch() - const walletAddress = useAppSelector(({ wallet }) => wallet.walletAddress) - - useEffect(() => { - if (!!walletAddress) { - dispatch(fetchStakingData()).then(() => { - dispatch(fetchClaimableRewards()) - }) - } - }, [dispatch, walletAddress]) - - return ( - <> - - {t('dashboard-rewards.page-title')} - - - - ) -} - -RewardsPage.getLayout = (page) => {page} - -export default RewardsPage diff --git a/packages/app/src/pages/dashboard/staking.tsx b/packages/app/src/pages/dashboard/staking.tsx index 0d99dad3fc..a892fa30da 100644 --- a/packages/app/src/pages/dashboard/staking.tsx +++ b/packages/app/src/pages/dashboard/staking.tsx @@ -1,13 +1,35 @@ +import { formatTruncatedDuration, truncateNumbers } from '@kwenta/sdk/utils' import Head from 'next/head' import { useRouter } from 'next/router' import React, { ReactNode, useCallback, useEffect, useMemo, useState } from 'react' import { useTranslation } from 'react-i18next' +import styled from 'styled-components' +import { NO_VALUE } from 'constants/placeholder' import DashboardLayout from 'sections/dashboard/DashboardLayout' +import EscrowTable from 'sections/dashboard/Stake/EscrowTable' import StakingPortfolio, { StakeTab } from 'sections/dashboard/Stake/StakingPortfolio' import StakingTabs from 'sections/dashboard/Stake/StakingTabs' +import { StakingCards } from 'sections/dashboard/Stake/types' +import { useFetchStakeMigrateData } from 'state/futures/hooks' import { useAppDispatch, useAppSelector } from 'state/hooks' -import { fetchClaimableRewards, fetchEscrowData, fetchStakingData } from 'state/staking/actions' +import { setStakingMigrationCompleted } from 'state/staking/reducer' +import { + selectClaimableBalance, + selectClaimableBalanceV2, + selectKwentaBalance, + selectKwentaRewards, + selectStakedEscrowedKwentaBalance, + selectStakedEscrowedKwentaBalanceV2, + selectStakedKwentaBalance, + selectStakedKwentaBalanceV2, + selectStakedResetTime, + selectStakingMigrationCompleted, + selectStakingMigrationRequired, + selectTotalVestable, + selectTotalVestableV2, +} from 'state/staking/selectors' +import media from 'styles/media' type StakingComponent = React.FC & { getLayout: (page: ReactNode) => JSX.Element } @@ -15,7 +37,27 @@ const StakingPage: StakingComponent = () => { const { t } = useTranslation() const router = useRouter() const dispatch = useAppDispatch() - const walletAddress = useAppSelector(({ wallet }) => wallet.walletAddress) + const claimableBalance = useAppSelector(selectClaimableBalance) + const stakedKwentaBalance = useAppSelector(selectStakedKwentaBalance) + const totalVestable = useAppSelector(selectTotalVestable) + const stakedEscrowedKwentaBalance = useAppSelector(selectStakedEscrowedKwentaBalance) + const claimableBalanceV2 = useAppSelector(selectClaimableBalanceV2) + const stakedKwentaBalanceV2 = useAppSelector(selectStakedKwentaBalanceV2) + const totalVestableV2 = useAppSelector(selectTotalVestableV2) + const stakedEscrowedKwentaBalanceV2 = useAppSelector(selectStakedEscrowedKwentaBalanceV2) + const kwentaBalance = useAppSelector(selectKwentaBalance) + const kwentaRewards = useAppSelector(selectKwentaRewards) + const stakedResetTime = useAppSelector(selectStakedResetTime) + const isMigrationRequired = useAppSelector(selectStakingMigrationRequired) + const isMigrationCompleted = useAppSelector(selectStakingMigrationCompleted) + + useFetchStakeMigrateData() + + useEffect(() => { + if (isMigrationRequired) { + dispatch(setStakingMigrationCompleted(false)) + } + }, [dispatch, isMigrationRequired]) const tabQuery = useMemo(() => { if (router.query.tab) { @@ -29,15 +71,6 @@ const StakingPage: StakingComponent = () => { const [currentTab, setCurrentTab] = useState(tabQuery ?? StakeTab.Staking) - useEffect(() => { - if (!!walletAddress) { - dispatch(fetchStakingData()).then(() => { - dispatch(fetchClaimableRewards()) - }) - dispatch(fetchEscrowData()) - } - }, [dispatch, walletAddress]) - const handleChangeTab = useCallback( (tab: StakeTab) => () => { setCurrentTab(tab) @@ -45,17 +78,224 @@ const StakingPage: StakingComponent = () => { [] ) + const timeLeft = useMemo( + () => + stakedResetTime > new Date().getTime() / 1000 + ? formatTruncatedDuration(stakedResetTime - new Date().getTime() / 1000) + : NO_VALUE, + [stakedResetTime] + ) + + const stakingInfo: StakingCards[] = useMemo( + () => [ + { + category: t('dashboard.stake.portfolio.balance.title'), + onClick: () => setCurrentTab(StakeTab.Staking), + card: [ + { + key: 'balance-liquid', + title: t('dashboard.stake.portfolio.balance.liquid'), + value: truncateNumbers(kwentaBalance, 2), + }, + { + key: 'balance-staked', + title: t('dashboard.stake.portfolio.balance.staked'), + value: truncateNumbers(stakedKwentaBalanceV2, 2), + }, + ], + }, + { + category: t('dashboard.stake.portfolio.escrow.title'), + onClick: () => setCurrentTab(StakeTab.Escrow), + card: [ + { + key: 'escrow-staked', + title: t('dashboard.stake.portfolio.escrow.staked'), + value: truncateNumbers(stakedEscrowedKwentaBalanceV2, 2), + }, + { + key: 'escrow-vestable', + title: t('dashboard.stake.portfolio.escrow.vestable'), + value: truncateNumbers(totalVestableV2, 2), + }, + ], + }, + { + category: t('dashboard.stake.portfolio.rewards.title'), + onClick: () => setCurrentTab(StakeTab.Staking), + card: [ + { + key: 'rewards-claimable', + title: t('dashboard.stake.portfolio.rewards.claimable'), + value: truncateNumbers(claimableBalanceV2, 2), + }, + { + key: 'rewards-trading', + title: t('dashboard.stake.portfolio.rewards.trading'), + value: truncateNumbers(kwentaRewards, 2), + }, + ], + }, + { + category: t('dashboard.stake.portfolio.early-vest-rewards.title'), + onClick: () => setCurrentTab(StakeTab.Staking), + card: [ + { + key: 'early-vest-rewards-claimable', + title: t('dashboard.stake.portfolio.early-vest-rewards.claimable'), + value: NO_VALUE, + }, + { + key: 'early-vest-rewards-epoch', + title: t('dashboard.stake.portfolio.early-vest-rewards.epoch'), + value: NO_VALUE, + }, + ], + }, + { + category: t('dashboard.stake.portfolio.cooldown.title'), + card: [ + { + key: 'cooldown-time-left', + title: t('dashboard.stake.portfolio.cooldown.time-left'), + value: timeLeft, + }, + ], + }, + ], + [ + claimableBalanceV2, + kwentaBalance, + kwentaRewards, + stakedEscrowedKwentaBalanceV2, + stakedKwentaBalanceV2, + t, + timeLeft, + totalVestableV2, + ] + ) + + const migrationInfo: StakingCards[] = useMemo( + () => [ + { + category: t('dashboard.stake.portfolio.balance.title'), + card: [ + { + key: 'balance-liquid', + title: t('dashboard.stake.portfolio.balance.liquid'), + value: truncateNumbers(kwentaBalance, 2), + }, + { + key: 'balance-staked', + title: t('dashboard.stake.portfolio.balance.staked-v1'), + value: truncateNumbers(stakedKwentaBalance, 2), + }, + ], + }, + { + category: t('dashboard.stake.portfolio.rewards.title'), + card: [ + { + key: 'rewards-claimable', + title: t('dashboard.stake.portfolio.rewards.staking-v1'), + value: truncateNumbers(claimableBalance, 2), + }, + { + key: 'rewards-trading', + title: t('dashboard.stake.portfolio.rewards.trading'), + value: truncateNumbers(kwentaRewards, 4), + }, + ], + }, + { + category: t('dashboard.stake.portfolio.escrow.title-v2'), + card: [ + { + key: 'escrow-staked', + title: t('dashboard.stake.portfolio.escrow.staked'), + value: truncateNumbers(stakedEscrowedKwentaBalanceV2, 2), + }, + { + key: 'escrow-vestable', + title: t('dashboard.stake.portfolio.escrow.vestable'), + value: truncateNumbers(totalVestableV2, 2), + }, + ], + }, + { + category: t('dashboard.stake.portfolio.escrow.title-v1'), + card: [ + { + key: 'escrow-staked', + title: t('dashboard.stake.portfolio.escrow.staked'), + value: truncateNumbers(stakedEscrowedKwentaBalance, 2), + }, + { + key: 'escrow-vestable', + title: t('dashboard.stake.portfolio.escrow.vestable'), + value: truncateNumbers(totalVestable, 2), + }, + ], + }, + ], + [ + t, + kwentaBalance, + stakedKwentaBalance, + claimableBalance, + kwentaRewards, + stakedEscrowedKwentaBalanceV2, + totalVestableV2, + stakedEscrowedKwentaBalance, + totalVestable, + ] + ) + + const { title, cardsInfo, stakingComponent } = useMemo(() => { + if (isMigrationCompleted) { + return { + title: t('dashboard.stake.portfolio.title'), + cardsInfo: stakingInfo, + stakingComponent: , + } + } else { + return { + title: t('dashboard.stake.tabs.migrate.title'), + cardsInfo: migrationInfo, + stakingComponent: ( + <> + + + + + ), + } + } + }, [currentTab, handleChangeTab, isMigrationCompleted, migrationInfo, stakingInfo, t]) + return ( <> {t('dashboard-stake.page-title')} - - + + {stakingComponent} ) } +const TableContainer = styled.div` + margin-top: 30px; + ${media.lessThan('lg')` + margin-top: 0px; + padding: 15px; + `} +` + StakingPage.getLayout = (page) => {page} export default StakingPage diff --git a/packages/app/src/queries/staking/utils.ts b/packages/app/src/queries/staking/utils.ts index ceb30d54df..419ceb5c92 100644 --- a/packages/app/src/queries/staking/utils.ts +++ b/packages/app/src/queries/staking/utils.ts @@ -1,6 +1,5 @@ import { ZERO_WEI } from '@kwenta/sdk/constants' import { NetworkId } from '@kwenta/sdk/types' -import { formatShortDate, toJSTimestamp } from '@kwenta/sdk/utils' import { wei } from '@synthetixio/wei' import { BigNumber } from 'ethers' @@ -59,8 +58,6 @@ export function getApy(totalStakedBalance: number, weekCounter: number) { export const parseEpochData = (index: number, networkId?: NetworkId) => { const { epochStart, epochEnd } = getEpochDetails(networkId ?? 10, index) - const startDate = formatShortDate(new Date(toJSTimestamp(epochStart))) - const endDate = formatShortDate(new Date(toJSTimestamp(epochEnd))) - const label = `Epoch ${index}: ${startDate} - ${endDate}` + const label = `Epoch ${index}` return { period: index, start: epochStart, end: epochEnd, label } } diff --git a/packages/app/src/sections/dashboard/DashboardLayout.tsx b/packages/app/src/sections/dashboard/DashboardLayout.tsx index 1513dd393b..56f1f5739a 100644 --- a/packages/app/src/sections/dashboard/DashboardLayout.tsx +++ b/packages/app/src/sections/dashboard/DashboardLayout.tsx @@ -19,8 +19,6 @@ enum Tab { Markets = 'markets', Governance = 'governance', Stake = 'staking', - Earn = 'earn', - Rewards = 'rewards', } const Tabs = Object.values(Tab) @@ -67,12 +65,6 @@ const DashboardLayout: FC<{ children?: ReactNode }> = ({ children }) => { active: activeTab === Tab.Stake, href: ROUTES.Dashboard.Stake, }, - { - name: Tab.Rewards, - label: t('dashboard.tabs.rewards'), - active: activeTab === Tab.Rewards, - href: ROUTES.Dashboard.Rewards, - }, { name: Tab.Governance, label: t('dashboard.tabs.governance'), @@ -93,16 +85,12 @@ const DashboardLayout: FC<{ children?: ReactNode }> = ({ children }) => { {t('dashboard.titles.trading')} {TABS.slice(0, 3).map(({ name, label, active, ...rest }) => ( - - {label} - + ))} {t('dashboard.titles.community')} {TABS.slice(3).map(({ name, label, active, ...rest }) => ( - - {label} - + ))} @@ -153,7 +141,7 @@ const StyledLeftSideContent = styled(LeftSideContent)` const StyledFullHeightContainer = styled.div` display: grid; - grid-template-columns: 150px 1fr 150px; + grid-template-columns: 165px 1fr 150px; height: 100%; padding: 0 15px; ` diff --git a/packages/app/src/sections/dashboard/RewardsTabs.tsx b/packages/app/src/sections/dashboard/RewardsTabs.tsx deleted file mode 100644 index e4002607cb..0000000000 --- a/packages/app/src/sections/dashboard/RewardsTabs.tsx +++ /dev/null @@ -1,300 +0,0 @@ -import { ZERO_WEI } from '@kwenta/sdk/constants' -import { formatNumber, truncateNumbers } from '@kwenta/sdk/utils' -import { wei } from '@synthetixio/wei' -import { BigNumber } from 'ethers' -import { useRouter } from 'next/router' -import { FC, useCallback, useMemo } from 'react' -import { Trans, useTranslation } from 'react-i18next' -import styled from 'styled-components' - -import LinkArrowIcon from 'assets/svg/app/link-arrow.svg' -import Button from 'components/Button' -import { FlexDivCol, FlexDivRow, FlexDivRowCentered } from 'components/layout/flex' -import Pill from 'components/Pill' -import Spacer from 'components/Spacer' -import { Body, Heading, LogoText } from 'components/Text' -import { EXTERNAL_LINKS } from 'constants/links' -import { NO_VALUE } from 'constants/placeholder' -import ROUTES from 'constants/routes' -import useGetFile from 'queries/files/useGetFile' -import { StakingCard } from 'sections/dashboard/Stake/card' -import { useAppDispatch, useAppSelector } from 'state/hooks' -import { - claimMultipleAllRewards, - claimMultipleOpRewards, - claimMultipleSnxOpRewards, -} from 'state/staking/actions' -import { - selectEpochPeriod, - selectKwentaRewards, - selectOpRewards, - selectSnxOpRewards, -} from 'state/staking/selectors' -import { selectNetwork, selectWallet } from 'state/wallet/selectors' -import media from 'styles/media' - -const RewardsTabs: FC = () => { - const { t } = useTranslation() - const dispatch = useAppDispatch() - const router = useRouter() - const network = useAppSelector(selectNetwork) - const walletAddress = useAppSelector(selectWallet) - const kwentaRewards = useAppSelector(selectKwentaRewards) - const opRewards = useAppSelector(selectOpRewards) - const snxOpRewards = useAppSelector(selectSnxOpRewards) - const epoch = useAppSelector(selectEpochPeriod) - - const goToStaking = useCallback(() => { - router.push(ROUTES.Dashboard.TradingRewards) - }, [router]) - - const handleClaimAll = useCallback(() => { - dispatch(claimMultipleAllRewards()) - }, [dispatch]) - - const handleClaimOp = useCallback(() => { - dispatch(claimMultipleOpRewards()) - }, [dispatch]) - - const handleClaimOpSnx = useCallback(() => { - dispatch(claimMultipleSnxOpRewards()) - }, [dispatch]) - - const estimatedKwentaRewardQuery = useGetFile( - `trading-rewards-snapshots/${network === 420 ? `goerli-` : ''}epoch-current.json` - ) - const estimatedKwentaReward = useMemo( - () => BigNumber.from(estimatedKwentaRewardQuery?.data?.claims[walletAddress!]?.amount ?? 0), - [estimatedKwentaRewardQuery?.data?.claims, walletAddress] - ) - - const estimatedOpQuery = useGetFile( - `trading-rewards-snapshots/${network === 420 ? `goerli-` : ''}epoch-current-op.json` - ) - const estimatedOp = useMemo( - () => BigNumber.from(estimatedOpQuery?.data?.claims[walletAddress!]?.amount ?? 0), - [estimatedOpQuery?.data?.claims, walletAddress] - ) - - const claimDisabledAll = useMemo( - () => kwentaRewards.add(opRewards).add(snxOpRewards).lte(0), - [opRewards, snxOpRewards, kwentaRewards] - ) - - const claimDisabledKwentaOp = useMemo(() => opRewards.lte(0), [opRewards]) - - const claimDisabledSnxOp = useMemo(() => snxOpRewards.lte(0), [snxOpRewards]) - - const REWARDS = [ - { - key: 'trading-rewards', - title: t('dashboard.rewards.trading-rewards.title'), - copy: t('dashboard.rewards.trading-rewards.copy'), - button: t('dashboard.rewards.staking'), - kwentaIcon: true, - linkIcon: true, - rewards: kwentaRewards, - estimatedRewards: truncateNumbers(wei(estimatedKwentaReward ?? ZERO_WEI), 4), - onClick: goToStaking, - isDisabled: false, - }, - { - key: 'op-rewards', - title: t('dashboard.rewards.op-rewards.title'), - copy: t('dashboard.rewards.op-rewards.copy'), - button: t('dashboard.rewards.claim'), - kwentaIcon: false, - linkIcon: false, - rewards: opRewards, - estimatedRewards: truncateNumbers(wei(estimatedOp ?? ZERO_WEI), 4), - onClick: handleClaimOp, - isDisabled: claimDisabledKwentaOp, - }, - { - key: 'snx-rewards', - title: t('dashboard.rewards.snx-rewards.title'), - copy: t('dashboard.rewards.snx-rewards.copy'), - button: t('dashboard.rewards.claim'), - kwentaIcon: false, - linkIcon: false, - rewards: snxOpRewards, - onClick: handleClaimOpSnx, - isDisabled: claimDisabledSnxOp, - }, - ] - - return ( - - - - - {t('dashboard.rewards.title')} - -
{t('dashboard.rewards.copy')}
-
- - {t('dashboard.rewards.claim-all')} - -
- - {REWARDS.map((reward) => ( - -
- - {reward.title} - -
{reward.copy}
-
-
- {t('dashboard.rewards.claimable')} - - - {truncateNumbers(reward.rewards, 4)} - -
-
- - - {t('dashboard.rewards.estimated')} - - - {!!reward.estimatedRewards - ? truncateNumbers(reward.estimatedRewards, 4) - : NO_VALUE} - - - - - {t('dashboard.rewards.epoch')} - - - {formatNumber(epoch, { minDecimals: 0 })} - - -
- -
- ))} -
- -
- , - ]} - /> -
-
-
- ) -} - -const Emphasis = styled.a` - color: ${(props) => props.theme.colors.selectedTheme.newTheme.text.primary}; -` - -const HeaderContainer = styled(FlexDivRowCentered)` - margin-bottom: 22.5px; - - ${media.lessThan('mdUp')` - flex-direction: column; - row-gap: 15px; - `} -` - -const RewardsTabContainer = styled.div` - ${media.lessThan('mdUp')` - padding: 15px; - `} - - ${media.greaterThan('mdUp')` - margin-top: 26px; - `} -` - -const CardGrid = styled(StakingCard)` - display: flex; - flex-direction: column; - justify-content: space-between; - row-gap: 25px; - - .title { - font-weight: 400; - font-size: 16px; - color: ${(props) => props.theme.colors.selectedTheme.newTheme.text.primary}; - } - - .value { - font-size: 13px; - line-height: 16px; - color: ${(props) => props.theme.colors.selectedTheme.newTheme.text.secondary}; - margin-top: 0px; - font-family: ${(props) => props.theme.fonts.regular}; - } -` - -const CardsContainer = styled.div` - display: grid; - width: 100%; - grid-template-columns: repeat(3, 1fr); - grid-gap: 20px; - margin-bottom: 20px; - - ${media.lessThan('mdUp')` - grid-template-columns: repeat(1, 1fr); - `} -` - -const StyledFlexDivCol = styled(FlexDivCol)` - .value { - font-size: 13px; - color: ${(props) => props.theme.colors.selectedTheme.newTheme.text.secondary}; - margin-top: 0px; - font-family: ${(props) => props.theme.fonts.regular}; - } - - .title { - font-weight: 400; - font-size: 16px; - color: ${(props) => props.theme.colors.selectedTheme.newTheme.text.primary}; - } -` - -export default RewardsTabs diff --git a/packages/app/src/sections/dashboard/Stake/EscrowTab.tsx b/packages/app/src/sections/dashboard/Stake/EscrowTab.tsx index a50fbb72df..dc2eaa796f 100644 --- a/packages/app/src/sections/dashboard/Stake/EscrowTab.tsx +++ b/packages/app/src/sections/dashboard/Stake/EscrowTab.tsx @@ -1,33 +1,168 @@ +import { formatPercent, truncateNumbers } from '@kwenta/sdk/utils' +import { useMemo } from 'react' +import { useTranslation } from 'react-i18next' import styled from 'styled-components' +import { FlexDivCol, FlexDivRow, FlexDivRowCentered } from 'components/layout/flex' +import { Body, Heading } from 'components/Text' +import { useAppSelector } from 'state/hooks' +import { + selectAPY, + selectAPYV2, + selectStakedEscrowedKwentaBalance, + selectStakedEscrowedKwentaBalanceV2, + selectTotalVestable, + selectTotalVestableV2, +} from 'state/staking/selectors' import media from 'styles/media' +import { StakingCard } from './card' import EscrowTable from './EscrowTable' import EscrowInputCard from './InputCards/EscrowInputCard' const EscrowTab = () => { + const { t } = useTranslation() + const apy = useAppSelector(selectAPY) + const apyV2 = useAppSelector(selectAPYV2) + const stakedEscrowedKwentaBalance = useAppSelector(selectStakedEscrowedKwentaBalance) + const stakedEscrowedKwentaBalanceV2 = useAppSelector(selectStakedEscrowedKwentaBalanceV2) + const totalVestable = useAppSelector(selectTotalVestable) + const totalVestableV2 = useAppSelector(selectTotalVestableV2) + + const stakingOverview = useMemo( + () => [ + { + category: 'Overview', + card: [ + { + key: 'overview-staked', + title: t('dashboard.stake.portfolio.escrow.staked'), + value: truncateNumbers(stakedEscrowedKwentaBalanceV2, 2), + }, + { + key: 'overview-apr', + title: t('dashboard.stake.portfolio.rewards.apr'), + value: formatPercent(apyV2, { minDecimals: 2 }), + }, + { + key: 'overview-vestable', + title: t('dashboard.stake.portfolio.escrow.vestable'), + value: truncateNumbers(totalVestableV2, 2), + }, + ], + }, + { + category: 'Staking V1', + card: [ + { + key: 'staking-v1-staked', + title: t('dashboard.stake.portfolio.escrow.staked'), + value: truncateNumbers(stakedEscrowedKwentaBalance, 2), + }, + { + key: 'staking-v1-apr', + title: t('dashboard.stake.portfolio.rewards.apr'), + value: formatPercent(apy, { minDecimals: 2 }), + }, + { + key: 'staking-v1-vestable', + title: t('dashboard.stake.portfolio.escrow.vestable'), + value: truncateNumbers(totalVestable, 2), + }, + ], + }, + ], + [ + apy, + apyV2, + stakedEscrowedKwentaBalance, + stakedEscrowedKwentaBalanceV2, + t, + totalVestable, + totalVestableV2, + ] + ) + return ( + + + + {t('dashboard.stake.tabs.escrow.title')} + + {stakingOverview.map(({ category, card }, i) => ( + + {category} + + {card.map(({ key, title, value }) => ( + + + {title} + + + {value} + + + ))} + + + ))} + + + - ) } -const EscrowTabContainer = styled.div` - ${media.greaterThan('mdUp')` +const CardsContainer = styled(FlexDivRowCentered)` + width: 100%; + justify-content: flex-start; + flex-wrap: wrap; + column-gap: 50px; + row-gap: 25px; + ${media.lessThan('lg')` + flex-direction: column; + align-items: flex-start; + row-gap: 25px; + `} +` + +const StyledHeading = styled(Heading)` + font-weight: 400; +` + +const CardGridContainer = styled(StakingCard)` + display: flex; + flex-direction: column; + justify-content: flex-start; + row-gap: 60px; + + ${media.lessThan('xl')` + row-gap: 25px; + `} +` + +const GridContainer = styled.div` + display: grid; + grid-template-columns: 1fr 1fr; + column-gap: 15px; + + ${media.lessThan('lg')` display: grid; - grid-template-columns: 1fr 1fr; - & > div { - flex: 1; + grid-template-columns: 1fr; + row-gap: 25px; + `} +` - &:first-child { - margin-right: 15px; - } - } +const EscrowTabContainer = styled.div` + ${media.greaterThan('lg')` + display: flex; + flex-direction: column; + row-gap: 15px; `} - ${media.lessThan('mdUp')` + ${media.lessThan('lg')` & > div:first-child { margin-bottom: 15px; } diff --git a/packages/app/src/sections/dashboard/Stake/EscrowTable.tsx b/packages/app/src/sections/dashboard/Stake/EscrowTable.tsx index 2f1829143d..d943501998 100644 --- a/packages/app/src/sections/dashboard/Stake/EscrowTable.tsx +++ b/packages/app/src/sections/dashboard/Stake/EscrowTable.tsx @@ -1,34 +1,55 @@ import { ZERO_WEI } from '@kwenta/sdk/constants' -import { truncateNumbers } from '@kwenta/sdk/utils' +import { formatNumber, formatPercent } from '@kwenta/sdk/utils' +import { wei } from '@synthetixio/wei' import { useCallback, useMemo, useState } from 'react' import { useTranslation } from 'react-i18next' import styled from 'styled-components' -import { DesktopOnlyView, MobileOrTabletView } from 'components/Media' +import Badge from 'components/Badge' +import Button from 'components/Button' +import { Checkbox } from 'components/Checkbox' +import { FlexDivCol, FlexDivRow, FlexDivRowCentered } from 'components/layout/flex' +import { DesktopLargeOnlyView, DesktopSmallOnlyView } from 'components/Media' import Table from 'components/Table' import { TableCellHead, TableHeader } from 'components/Table' -import { StakingCard } from 'sections/dashboard/Stake/card' +import StakingPagination from 'components/Table/StakingPagination' +import { Body } from 'components/Text' import { useAppDispatch, useAppSelector } from 'state/hooks' -import { vestEscrowedRewards } from 'state/staking/actions' +import { vestEscrowedRewards, vestEscrowedRewardsV2 } from 'state/staking/actions' +import { setSelectedEscrowVersion } from 'state/staking/reducer' +import { selectCombinedEscrowData, selectSelectedEscrowVersion } from 'state/staking/selectors' +import media from 'styles/media' +import common from 'styles/theme/colors/common' import VestConfirmationModal from './VestConfirmationModal' const EscrowTable = () => { const { t } = useTranslation() const dispatch = useAppDispatch() - const escrowData = useAppSelector(({ staking }) => staking.escrowData) + const escrowVersion = useAppSelector(selectSelectedEscrowVersion) + const escrowData = useAppSelector(selectCombinedEscrowData) + const [checkedState, setCheckedState] = useState(escrowData.map((_) => false)) const [checkAllState, setCheckAllState] = useState(false) const [isConfirmModalOpen, setConfirmModalOpen] = useState(false) const handleOnChange = useCallback( - (position: number) => () => { + (position: number) => { checkedState[position] = !checkedState[position] setCheckedState([...checkedState]) }, [checkedState] ) + const handleVersionChange = useCallback( + (version: number) => { + dispatch(setSelectedEscrowVersion(version)) + setCheckedState(escrowData.map((_) => false)) + setCheckAllState(false) + }, + [dispatch, escrowData] + ) + const selectAll = useCallback(() => { if (checkAllState) { setCheckedState(escrowData.map((_) => false)) @@ -66,51 +87,132 @@ const EscrowTable = () => { const handleVest = useCallback(async () => { if (vestEnabled) { - await dispatch(vestEscrowedRewards(ids)) + if (escrowVersion === 1) { + await dispatch(vestEscrowedRewards(ids)) + } else if (escrowVersion === 2) { + await dispatch(vestEscrowedRewardsV2(ids)) + } setCheckedState(escrowData.map((_) => false)) setCheckAllState(false) } setConfirmModalOpen(false) - }, [dispatch, escrowData, ids, vestEnabled]) + }, [dispatch, escrowData, escrowVersion, ids, vestEnabled]) const openConfirmModal = useCallback(() => setConfirmModalOpen(true), []) const closeConfirmModal = useCallback(() => setConfirmModalOpen(false), []) + const EscrowStatsContainer = () => ( + + + + + {t('dashboard.stake.tabs.escrow.total')} + {formatNumber(totalVestable, { minDecimals: 4 })} KWENTA + + + {t('dashboard.stake.tabs.escrow.fee')} + {formatNumber(totalFee, { minDecimals: 4 })} KWENTA + + + + handleVersionChange(1)} + > + {t('dashboard.stake.tabs.escrow.v1')} + + handleVersionChange(2)} + > + {t('dashboard.stake.tabs.escrow.v2')} + + + + + {escrowVersion === 1 + ? t('dashboard.stake.tabs.escrow.vest-v1') + : t('dashboard.stake.tabs.escrow.vest-v2')} + + + ) + return ( - - + + } columns={[ { - header: () => , + header: () => ( + + ), cell: (cellProps) => ( - handleOnChange(cellProps.row.index)} + label="" + variant="table" + checkSide="right" /> ), accessorKey: 'selected', size: 40, + enableSorting: false, }, { - header: () => ( - {t('dashboard.stake.tabs.escrow.date')} - ), + header: () => {t('dashboard.stake.tabs.escrow.date')}, cell: (cellProps) => {cellProps.row.original.date}, accessorKey: 'date', size: 65, }, + { + header: () => {t('dashboard.stake.tabs.escrow.amount')}, + cell: (cellProps) => ( + + + {formatNumber(cellProps.row.original.amount, { minDecimals: 4 })} + + {cellProps.row.original.version === 1 ? ( + + V1 + + ) : null} + + ), + accessorKey: 'amount', + size: 80, + }, { header: () => ( - +
{t('dashboard.stake.tabs.escrow.time-until-vestable')}
), @@ -120,121 +222,135 @@ const EscrowTable = () => { }, { header: () => ( - +
{t('dashboard.stake.tabs.escrow.immediately-vestable')}
), cell: (cellProps) => ( - {truncateNumbers(cellProps.row.original.vestable, 4)} + + {formatNumber(cellProps.row.original.amount, { minDecimals: 4 })} + ), accessorKey: 'immediatelyVestable', size: 80, }, { header: () => ( - {t('dashboard.stake.tabs.escrow.amount')} - ), - cell: (cellProps) => ( - {truncateNumbers(cellProps.row.original.amount, 4)} - ), - accessorKey: 'amount', - size: 80, - }, - { - header: () => ( - +
{t('dashboard.stake.tabs.escrow.early-vest-fee')}
), - cell: (cellProps) => ( - {truncateNumbers(cellProps.row.original.fee, 4)} - ), + cell: (cellProps) => { + const fee = wei(cellProps.row.original.fee) + return ( + + {`${formatNumber(cellProps.row.original.fee, { + minDecimals: 4, + })} (${formatPercent( + cellProps.row.original.amount !== null + ? fee.div(cellProps.row.original.amount) + : ZERO_WEI, + { minDecimals: 0 } + )})`} + + ) + }, accessorKey: 'earlyVestFee', - size: 80, + size: 90, }, { - header: () => ( - {t('dashboard.stake.tabs.escrow.status')} - ), - cell: (cellProps) => {cellProps.row.original.status}, + header: () => {t('dashboard.stake.tabs.escrow.status')}, + cell: (cellProps) => {cellProps.row.original.status}, accessorKey: 'status', size: 70, }, ]} /> -
- + + } columns={[ { - header: () => , + header: () => ( + + ), cell: (cellProps) => ( - handleOnChange(cellProps.row.index)} + label="" + variant="table" + checkSide="left" /> ), accessorKey: 'selected', - size: 40, + size: 30, + enableSorting: false, }, { - header: () => ( - {t('dashboard.stake.tabs.escrow.amount')} - ), + header: () => {t('dashboard.stake.tabs.escrow.amount')}, cell: (cellProps) => ( - {truncateNumbers(cellProps.row.original.amount, 4)} + + + {formatNumber(cellProps.row.original.amount, { minDecimals: 4 })} + + {cellProps.row.original.version === 1 ? ( + + V1 + + ) : null} + ), accessorKey: 'amount', - size: 80, + size: 90, }, { header: () => ( - {t('dashboard.stake.tabs.escrow.early-vest-fee')} - ), - cell: (cellProps) => ( - {truncateNumbers(cellProps.row.original.fee, 4)} + {t('dashboard.stake.tabs.escrow.early-vest-fee')} ), + cell: (cellProps) => { + const fee = wei(cellProps.row.original.fee) + return ( + + {formatNumber(cellProps.row.original.fee, { minDecimals: 4 })} + + {formatPercent( + cellProps.row.original.amount !== null + ? fee.div(cellProps.row.original.amount) + : ZERO_WEI, + { minDecimals: 0 } + )} + + + ) + }, accessorKey: 'earlyVestFee', - size: 80, + size: 100, }, { - header: () => ( - {t('dashboard.stake.tabs.escrow.status')} - ), - cell: (cellProps) => {cellProps.row.original.status}, + header: () => {t('dashboard.stake.tabs.escrow.status')}, + cell: (cellProps) => {cellProps.row.original.status}, accessorKey: 'status', - size: 70, + size: 50, }, ]} /> - - -
-
-
{t('dashboard.stake.tabs.escrow.total')}
-
- {truncateNumbers(totalVestable, 4)}{' '} - {t('dashboard.stake.tabs.stake-table.kwenta-token')} -
-
-
-
{t('dashboard.stake.tabs.escrow.fee')}
-
- {truncateNumbers(totalFee, 4)} {t('dashboard.stake.tabs.stake-table.kwenta-token')} -
-
- - {t('dashboard.stake.tabs.escrow.vest')} - -
-
+ {isConfirmModalOpen && ( { ) } -const EscrowTableContainer = styled(StakingCard)` +const StyledButton = styled(Button)` + padding: 10px 20px; +` +const Container = styled(FlexDivRow)` + align-items: flex-end; + ${media.lessThan('lg')` + justify-content: space-between; + width: 100%; + `} +` + +const ButtonsContainer = styled(FlexDivRowCentered)` + column-gap: 20px; + ${media.lessThan('sm')` + column-gap: 10px; + `} +` + +const LabelContainer = styled(FlexDivRow)` + ${media.lessThan('lg')` + align-items: flex-start; + justify-content: flex-start; + flex: 1 + `} + + ${media.lessThan('sm')` + flex: initial; + `} +` + +const LabelContainers = styled(FlexDivCol)` + ${media.lessThan('lg')` + justify-content: flex-start; + width: 100%; + `} +` + +const StatsContainer = styled(FlexDivRowCentered)` + ${media.lessThan('lg')` + flex-direction: column; + row-gap: 25px; + padding: 15px 15px; + background: ${(props) => props.theme.colors.selectedTheme.newTheme.containers.cards.background}; + border-bottom-left-radius: 10px; + border-bottom-right-radius: 10px; + align-items: flex-end; + `} +` + +const StyledBadge = styled(Badge)` + padding: 0 6px; +` + +const EscrowTableContainer = styled.div` display: flex; flex-direction: column; justify-content: space-between; + margin-bottom: 60px; + background: transparent; + border: none; ` const StyledTable = styled(Table)` width: 100%; - border: none; border-bottom-left-radius: 0; border-bottom-right-radius: 0; - + border-radius: 15px; ${TableCellHead} { &:first-child { - padding-left: 14px; + padding-left: 18px; } } ` as typeof Table -const TableCell = styled.div` - font-size: 11px; - color: ${(props) => props.theme.colors.selectedTheme.button.text.primary}; -` - -const EscrowStats = styled.div` +const TableCell = styled.div<{ $regular?: boolean }>` + font-size: 13px; + font-family: ${(props) => props.theme.fonts[props.$regular ? 'regular' : 'mono']}; + color: ${(props) => props.color || props.theme.colors.selectedTheme.button.text.primary}; display: flex; - justify-content: flex-end; - padding: 18px; - border-top: ${(props) => props.theme.colors.selectedTheme.border}; - - .stat-title { - font-size: 10px; - color: ${(props) => props.theme.colors.selectedTheme.text.label}; - } - - .stat-value { - font-size: 11px; - font-family: ${(props) => props.theme.fonts.mono}; - color: ${(props) => props.theme.colors.selectedTheme.text.value}; - margin-top: 4px; - } - - & > div { - display: flex; - align-items: center; - - & > *:not(:last-child) { - margin-right: 15px; - } - } -` - -const VestButton = styled.button` - border-width: 1px; - border-style: solid; - border-color: ${(props) => - props.disabled - ? props.theme.colors.selectedTheme.gray - : props.theme.colors.selectedTheme.yellow}; - height: 24px; - box-sizing: border-box; - border-radius: 14px; - cursor: ${(props) => (props.disabled ? 'not-allowed' : 'pointer')}; - background-color: transparent; - color: ${(props) => - props.disabled - ? props.theme.colors.selectedTheme.gray - : props.theme.colors.selectedTheme.yellow}; - font-family: ${(props) => props.theme.fonts.bold}; - font-size: 12px; - padding-left: 12px; - padding-right: 12px; - text-transform: uppercase; + flex-direction: column; ` export default EscrowTable diff --git a/packages/app/src/sections/dashboard/Stake/InputCards/EscrowInputCard.tsx b/packages/app/src/sections/dashboard/Stake/InputCards/EscrowInputCard.tsx index cde1b03948..dea7b9264c 100644 --- a/packages/app/src/sections/dashboard/Stake/InputCards/EscrowInputCard.tsx +++ b/packages/app/src/sections/dashboard/Stake/InputCards/EscrowInputCard.tsx @@ -4,43 +4,49 @@ import { useTranslation } from 'react-i18next' import StakeCard from 'components/StakeCard' import { useAppDispatch, useAppSelector } from 'state/hooks' -import { approveKwentaToken, stakeEscrow, unstakeEscrow } from 'state/staking/actions' +import { approveKwentaToken, stakeEscrowV2, unstakeEscrowV2 } from 'state/staking/actions' import { selectCanStakeEscrowedKwenta, selectCanUnstakeEscrowedKwenta, - selectIsKwentaTokenApproved, + selectIsApprovingKwenta, + selectIsKwentaTokenApprovedV2, selectIsStakedEscrowedKwenta, + selectIsStakingEscrowedKwenta, selectIsUnstakedEscrowedKwenta, - selectStakedEscrowedKwentaBalance, - selectUnstakedEscrowedKwentaBalance, + selectIsUnstakingEscrowedKwenta, + selectStakedEscrowedKwentaBalanceV2, + selectUnstakedEscrowedKwentaBalanceV2, } from 'state/staking/selectors' const EscrowInputCard: FC = () => { const { t } = useTranslation() const dispatch = useAppDispatch() - const stakedEscrowedKwentaBalance = useAppSelector(selectStakedEscrowedKwentaBalance) - const isKwentaTokenApproved = useAppSelector(selectIsKwentaTokenApproved) - const unstakedEscrowedKwentaBalance = useAppSelector(selectUnstakedEscrowedKwentaBalance) + const stakedEscrowedKwentaBalance = useAppSelector(selectStakedEscrowedKwentaBalanceV2) + const isKwentaTokenApproved = useAppSelector(selectIsKwentaTokenApprovedV2) + const unstakedEscrowedKwentaBalance = useAppSelector(selectUnstakedEscrowedKwentaBalanceV2) const stakeEnabled = useAppSelector(selectCanStakeEscrowedKwenta) const unstakeEnabled = useAppSelector(selectCanUnstakeEscrowedKwenta) const isStakedEscrowedKwenta = useAppSelector(selectIsStakedEscrowedKwenta) const isUnstakedEscrowedKwenta = useAppSelector(selectIsUnstakedEscrowedKwenta) + const isUnstakingEscrowedKwenta = useAppSelector(selectIsUnstakingEscrowedKwenta) + const isStakingEscrowedKwenta = useAppSelector(selectIsStakingEscrowedKwenta) + const isApprovingKwenta = useAppSelector(selectIsApprovingKwenta) const handleApprove = useCallback(() => { - dispatch(approveKwentaToken('kwenta')) + dispatch(approveKwentaToken('kwentaStakingV2')) }, [dispatch]) const handleStakeEscrow = useCallback( (amount: string) => { - dispatch(stakeEscrow(wei(amount).toBN())) + dispatch(stakeEscrowV2(wei(amount).toBN())) }, [dispatch] ) const handleUnstakeEscrow = useCallback( (amount: string) => { - dispatch(unstakeEscrow(wei(amount).toBN())) + dispatch(unstakeEscrowV2(wei(amount).toBN())) }, [dispatch] ) @@ -58,6 +64,9 @@ const EscrowInputCard: FC = () => { onStake={handleStakeEscrow} onUnstake={handleUnstakeEscrow} onApprove={handleApprove} + isStaking={isStakingEscrowedKwenta} + isUnstaking={isUnstakingEscrowedKwenta} + isApproving={isApprovingKwenta} /> ) } diff --git a/packages/app/src/sections/dashboard/Stake/InputCards/StakeInputCard.tsx b/packages/app/src/sections/dashboard/Stake/InputCards/StakeInputCard.tsx index 2f7263c01b..03e66f7ba6 100644 --- a/packages/app/src/sections/dashboard/Stake/InputCards/StakeInputCard.tsx +++ b/packages/app/src/sections/dashboard/Stake/InputCards/StakeInputCard.tsx @@ -1,20 +1,22 @@ import { wei } from '@synthetixio/wei' import _ from 'lodash' -import { FC, useCallback, useMemo } from 'react' +import { FC, useCallback } from 'react' import { useTranslation } from 'react-i18next' import StakeCard from 'components/StakeCard' import { useAppDispatch, useAppSelector } from 'state/hooks' -import { approveKwentaToken } from 'state/staking/actions' -import { stakeKwenta, unstakeKwenta } from 'state/staking/actions' +import { approveKwentaToken, stakeKwentaV2, unstakeKwentaV2 } from 'state/staking/actions' import { - selectIsKwentaTokenApproved, + selectCanStakeKwenta, + selectCanUnstakeKwenta, + selectIsApprovingKwenta, + selectIsKwentaTokenApprovedV2, selectIsStakedKwenta, selectIsStakingKwenta, selectIsUnstakedKwenta, selectIsUnstakingKwenta, selectKwentaBalance, - selectStakedKwentaBalance, + selectStakedKwentaBalanceV2, } from 'state/staking/selectors' const StakeInputCard: FC = () => { @@ -22,39 +24,34 @@ const StakeInputCard: FC = () => { const dispatch = useAppDispatch() const kwentaBalance = useAppSelector(selectKwentaBalance) - const stakedKwentaBalance = useAppSelector(selectStakedKwentaBalance) - const isKwentaTokenApproved = useAppSelector(selectIsKwentaTokenApproved) - const isStakingKwenta = useAppSelector(selectIsStakingKwenta) - const isUnstakingKwenta = useAppSelector(selectIsUnstakingKwenta) + const stakedKwentaBalance = useAppSelector(selectStakedKwentaBalanceV2) + const isKwentaTokenApproved = useAppSelector(selectIsKwentaTokenApprovedV2) const isStakedKwenta = useAppSelector(selectIsStakedKwenta) const isUnstakedKwenta = useAppSelector(selectIsUnstakedKwenta) + const stakeEnabled = useAppSelector(selectCanStakeKwenta) + const unstakeEnabled = useAppSelector(selectCanUnstakeKwenta) + const isUnstakingKwenta = useAppSelector(selectIsUnstakingKwenta) + const isStakingKwenta = useAppSelector(selectIsStakingKwenta) + const isApprovingKwenta = useAppSelector(selectIsApprovingKwenta) const handleApprove = useCallback(() => { - dispatch(approveKwentaToken('kwenta')) + dispatch(approveKwentaToken('kwentaStakingV2')) }, [dispatch]) const handleStakeKwenta = useCallback( (amount: string) => { - dispatch(stakeKwenta(wei(amount).toBN())) + dispatch(stakeKwentaV2(wei(amount).toBN())) }, [dispatch] ) const handleUnstakeKwenta = useCallback( (amount: string) => { - dispatch(unstakeKwenta(wei(amount).toBN())) + dispatch(unstakeKwentaV2(wei(amount).toBN())) }, [dispatch] ) - const stakeEnabled = useMemo(() => { - return kwentaBalance.gt(0) && !isStakingKwenta - }, [kwentaBalance, isStakingKwenta]) - - const unstakeEnabled = useMemo(() => { - return stakedKwentaBalance.gt(0) && !isUnstakingKwenta - }, [stakedKwentaBalance, isUnstakingKwenta]) - return ( { isUnstaked={isUnstakedKwenta} isApproved={isKwentaTokenApproved} onApprove={handleApprove} + isStaking={isStakingKwenta} + isUnstaking={isUnstakingKwenta} + isApproving={isApprovingKwenta} /> ) } diff --git a/packages/app/src/sections/dashboard/Stake/MigrationSteps.tsx b/packages/app/src/sections/dashboard/Stake/MigrationSteps.tsx new file mode 100644 index 0000000000..f4d5cd19e1 --- /dev/null +++ b/packages/app/src/sections/dashboard/Stake/MigrationSteps.tsx @@ -0,0 +1,159 @@ +import { truncateNumbers } from '@kwenta/sdk/utils' +import { wei } from '@synthetixio/wei' +import { useMemo, useCallback, FC, memo } from 'react' +import { Trans, useTranslation } from 'react-i18next' +import styled from 'styled-components' + +import Button from 'components/Button' +import { FlexDivCol, FlexDivRowCentered } from 'components/layout/flex' +import Spacer from 'components/Spacer' +import { Body, Heading } from 'components/Text' +import { StakingCard } from 'sections/dashboard/Stake/card' +import { useAppDispatch, useAppSelector } from 'state/hooks' +import { claimStakingRewards, unstakeKwenta } from 'state/staking/actions' +import { setStakingMigrationCompleted } from 'state/staking/reducer' +import { + selectClaimableBalance, + selectIsGettingReward, + selectIsUnstakingKwenta, + selectStakedKwentaBalance, + selectStakedKwentaBalanceV2, +} from 'state/staking/selectors' +import media from 'styles/media' + +const MigrationSteps: FC = memo(() => { + const { t } = useTranslation() + const dispatch = useAppDispatch() + const claimableBalance = useAppSelector(selectClaimableBalance) + const stakedKwentaBalance = useAppSelector(selectStakedKwentaBalance) + const stakedKwentaBalanceV2 = useAppSelector(selectStakedKwentaBalanceV2) + const isUnstakingKwenta = useAppSelector(selectIsUnstakingKwenta) + const isClaimingReward = useAppSelector(selectIsGettingReward) + + const handleGetReward = useCallback(() => { + dispatch(claimStakingRewards()) + }, [dispatch]) + + const handleUnstakeKwenta = useCallback( + () => dispatch(unstakeKwenta(wei(stakedKwentaBalance).toBN())), + [dispatch, stakedKwentaBalance] + ) + + const handleDismiss = useCallback(() => { + dispatch(setStakingMigrationCompleted(true)) + }, [dispatch]) + + const migrationSteps = useMemo( + () => [ + { + key: 'step-1', + copy: t('dashboard.stake.tabs.migrate.step-1-copy'), + label: t('dashboard.stake.tabs.migrate.rewards'), + value: truncateNumbers(claimableBalance, 2), + buttonLabel: t('dashboard.stake.tabs.migrate.claim'), + onClick: handleGetReward, + active: claimableBalance.gt(0), + loading: isClaimingReward, + }, + { + key: 'step-2', + copy: t('dashboard.stake.tabs.migrate.step-2-copy'), + label: t('dashboard.stake.tabs.migrate.staked'), + value: truncateNumbers(stakedKwentaBalance, 2), + buttonLabel: t('dashboard.stake.tabs.migrate.unstake'), + onClick: handleUnstakeKwenta, + active: claimableBalance.lte(0) && stakedKwentaBalance.gt(0), + loading: isUnstakingKwenta, + }, + { + key: 'step-3', + copy: t('dashboard.stake.tabs.migrate.step-3-copy'), + label: t('dashboard.stake.tabs.migrate.staked'), + value: truncateNumbers(stakedKwentaBalanceV2, 2), + buttonLabel: t('dashboard.stake.tabs.migrate.visit-v2'), + onClick: handleDismiss, + active: claimableBalance.lte(0) && stakedKwentaBalance.lte(0), + }, + ], + [ + claimableBalance, + handleDismiss, + handleGetReward, + handleUnstakeKwenta, + isClaimingReward, + isUnstakingKwenta, + stakedKwentaBalance, + stakedKwentaBalanceV2, + t, + ] + ) + + return ( + + {migrationSteps.map( + ({ key, copy, label, value, buttonLabel, active, onClick, loading }, i) => ( + + + ]} + /> + + + {copy} + + + + + + {label} + + + {value} + + + + + + ) + )} + + ) +}) + +const StepsContainer = styled(FlexDivRowCentered)` + margin: 30px 0; + ${media.lessThan('lg')` + flex-direction: column; + row-gap: 25px; + margin: 0; + margin-bottom: 25px; + `} +` + +const StyledStakingCard = styled(StakingCard)<{ $active: boolean }>` + width: 100%; + column-gap: 10px; + opacity: ${(props) => (props.$active ? '1' : '0.3')}; + padding: 25px; + height: 150px; + border: 1px solid + ${(props) => props.theme.colors.selectedTheme.newTheme.pill.yellow.outline.border}; +` + +const StyledHeading = styled(Heading)` + font-weight: 400; +` + +export default MigrationSteps diff --git a/packages/app/src/sections/dashboard/Stake/RewardsTab.tsx b/packages/app/src/sections/dashboard/Stake/RewardsTab.tsx new file mode 100644 index 0000000000..0f6a6dbc5d --- /dev/null +++ b/packages/app/src/sections/dashboard/Stake/RewardsTab.tsx @@ -0,0 +1,390 @@ +import { ZERO_WEI } from '@kwenta/sdk/constants' +import { formatDollars, formatNumber, formatPercent } from '@kwenta/sdk/utils' +import { wei } from '@synthetixio/wei' +import { FC, useCallback, useMemo } from 'react' +import { useTranslation } from 'react-i18next' +import styled, { css } from 'styled-components' + +import HelpIcon from 'assets/svg/app/question-mark.svg' +import OptimismLogo from 'assets/svg/providers/optimism.svg' +import Button from 'components/Button' +import { FlexDivCol, FlexDivRow, FlexDivRowCentered } from 'components/layout/flex' +import LabelContainer from 'components/Nav/DropDownLabel' +import Select, { DropdownIndicator, IndicatorSeparator } from 'components/Select' +import { Body, Heading } from 'components/Text' +import Tooltip from 'components/Tooltip/Tooltip' +import { NO_VALUE } from 'constants/placeholder' +import useIsL2 from 'hooks/useIsL2' +import { TradingRewardProps } from 'queries/staking/utils' +import { StakingCard } from 'sections/dashboard/Stake/card' +import { selectFuturesFees, selectFuturesFeesForAccount } from 'state/futures/selectors' +import { useAppDispatch, useAppSelector } from 'state/hooks' +import { claimMultipleAllRewards } from 'state/staking/actions' +import { setSelectedEpoch } from 'state/staking/reducer' +import { + selectEpochData, + selectEstimatedKwentaRewards, + selectEstimatedOpRewards, + selectIsClaimingAllRewards, + selectKwentaRewards, + selectOpRewards, + selectSelectedEpoch, + selectSnxOpRewards, +} from 'state/staking/selectors' +import media from 'styles/media' + +import { EpochValue, RewardsInfo } from './types' + +const RewardsTab: FC = ({ period = 0 }) => { + const { t } = useTranslation() + const dispatch = useAppDispatch() + const isL2 = useIsL2() + const epochData = useAppSelector(selectEpochData) + const selectedEpoch = useAppSelector(selectSelectedEpoch) + const kwentaRewards = useAppSelector(selectKwentaRewards) + const opRewards = useAppSelector(selectOpRewards) + const snxOpRewards = useAppSelector(selectSnxOpRewards) + const estimatedKwentaRewards = useAppSelector(selectEstimatedKwentaRewards) + const estimatedOpRewards = useAppSelector(selectEstimatedOpRewards) + const futuresFeePaid = useAppSelector(selectFuturesFeesForAccount) + const totalFuturesFeePaid = useAppSelector(selectFuturesFees) + const isClaimingAllRewards = useAppSelector(selectIsClaimingAllRewards) + + const handleClaimAll = useCallback(() => { + dispatch(claimMultipleAllRewards()) + }, [dispatch]) + + const claimDisabledAll = useMemo( + () => kwentaRewards.add(opRewards).add(snxOpRewards).eq(0) || isClaimingAllRewards, + [kwentaRewards, opRewards, snxOpRewards, isClaimingAllRewards] + ) + + const ratio = useMemo(() => { + return !!futuresFeePaid && wei(totalFuturesFeePaid).gt(0) + ? wei(futuresFeePaid).div(totalFuturesFeePaid) + : ZERO_WEI + }, [futuresFeePaid, totalFuturesFeePaid]) + + const rewardsInfo: RewardsInfo[] = useMemo( + () => [ + { + key: 'trading-rewards', + title: t('dashboard.rewards.trading-rewards.title'), + copy: t('dashboard.rewards.trading-rewards.copy'), + labels: [ + { + label: t('dashboard.stake.portfolio.rewards.title'), + value: formatNumber(kwentaRewards, { minDecimals: 4 }), + }, + { + label: t('dashboard.stake.tabs.trading-rewards.fee-paid'), + labelIcon: ( + + + + + + ), + value: formatDollars(futuresFeePaid, { minDecimals: 2 }), + }, + { + label: t('dashboard.stake.tabs.trading-rewards.fee-share'), + value: formatPercent(ratio, { minDecimals: 2 }), + }, + ], + info: [ + { + label: t('dashboard.stake.tabs.trading-rewards.period'), + value: `Epoch ${period}`, + }, + { + label: t('dashboard.stake.tabs.trading-rewards.total-pool-fees'), + value: formatDollars(totalFuturesFeePaid, { minDecimals: 2 }), + }, + { + label: t('dashboard.rewards.estimated'), + labelIcon: ( + + + + + + ), + value: formatNumber(estimatedKwentaRewards, { minDecimals: 4 }), + }, + ], + }, + { + key: 'op-rewards', + title: t('dashboard.rewards.op-rewards.title'), + copy: t('dashboard.rewards.op-rewards.copy'), + labels: [ + { + label: t('dashboard.stake.portfolio.rewards.title'), + value: formatNumber(opRewards, { minDecimals: 4 }), + valueIcon: , + }, + ], + info: [ + { + label: t('dashboard.rewards.estimated'), + value: formatNumber(estimatedOpRewards, { minDecimals: 4 }), + }, + ], + }, + { + key: 'snx-rewards', + title: t('dashboard.rewards.snx-rewards.title'), + copy: t('dashboard.rewards.snx-rewards.copy'), + labels: [ + { + label: t('dashboard.stake.portfolio.rewards.title'), + value: formatNumber(snxOpRewards, { minDecimals: 4 }), + valueIcon: , + }, + ], + info: [ + { + label: t('dashboard.rewards.estimated'), + value: NO_VALUE, + }, + ], + }, + ], + [ + estimatedKwentaRewards, + estimatedOpRewards, + futuresFeePaid, + kwentaRewards, + opRewards, + period, + ratio, + snxOpRewards, + t, + totalFuturesFeePaid, + ] + ) + + const handleChangeEpoch = useCallback( + (value: EpochValue) => () => { + dispatch(setSelectedEpoch(value.period)) + }, + [dispatch] + ) + + const formatOptionLabel = useCallback( + (option: EpochValue) => ( +
+ {option.label} +
+ ), + [handleChangeEpoch] + ) + + return ( + + + {t('dashboard.rewards.title')} + b.period - a.period)} + optionPadding="0px" + value={selectedEpoch} + menuWidth={110} + components={{ IndicatorSeparator, DropdownIndicator }} + isSearchable={false} + variant="flat" + isDisabled={!isL2} + /> + + + {rewardsInfo.map(({ key, title, copy, labels, info }) => ( + +
+ + {title} + + {copy} +
+ + + {labels.map(({ label, value, labelIcon, valueIcon }) => ( + + + {label} + {labelIcon} + + + {value} + {valueIcon} + + + ))} + + + {info.map(({ label, labelIcon, value, valueIcon }) => ( + + + {label} + {labelIcon} + + + {value} + {valueIcon} + + + ))} + + +
+ ))} + + + +
+
+ ) +} + +const CustomStyledTooltip = styled(Tooltip)` + padding: 10px; + white-space: normal; + left: -80px; + ${media.lessThan('md')` + width: 200px; + left: -150px; + `} +` + +const WithCursor = styled.div<{ cursor: 'help' }>` + cursor: ${(props) => props.cursor}; +` + +const selectlabel = css` + font-size: 12px; + color: ${(props) => props.theme.colors.selectedTheme.newTheme.text.secondary}; + font-family: ${(props) => props.theme.fonts.bold}; +` + +const IconContainer = styled(Body)` + display: flex; + flex-direction: row; + column-gap: 5px; + align-items: center; +` + +const SelectLabelContainer = styled(LabelContainer)` + ${selectlabel} + padding: 6px 12px; + height: 32px; +` + +const StakingSelect = styled(Select)` + height: 32px; + + .react-select__control { + border-radius: 50px; + border-width: 0px; + width: 110px; + } + .react-select__menu, + .react-select__menu-list { + border-radius: 20px; + background: ${(props) => props.theme.colors.selectedTheme.newTheme.button.default.background}; + border-width: 0px; + } + + .react-select__value-container { + padding: 0; + } + + .react-select__single-value > div > div { + ${selectlabel} + } + + .react-select__dropdown-indicator { + margin-right: 10px; + } +` + +const ButtonContainer = styled.div` + margin-bottom: 25px; + margin-left: 25px; + width: 100%; + display: flex; +` + +const RewardsContainer = styled(FlexDivCol)` + row-gap: 25px; + ${media.lessThan('lg')` + flex-direction: row; + column-gap: 25px; + flex-wrap: wrap; + `} +` + +const StyledHeading = styled(Heading)` + font-weight: 400; +` + +const HeaderContainer = styled(FlexDivRowCentered)` + margin-bottom: 22.5px; + justify-content: space-between; + width: 100%; + ${media.lessThan('mdUp')` + margin-bottom: 25px; + margin-top: 25px; + `} +` + +const RewardsTabContainer = styled.div` + ${media.lessThan('mdUp')` + padding: 0; + `} + + ${media.greaterThan('mdUp')` + margin-top: 26px; + margin-bottom: 60px; + `} +` + +const CardGrid = styled(StakingCard)` + display: flex; + flex-direction: column; + justify-content: space-between; + row-gap: 25px; + background: transparent; + border-width: 0px; +` + +const CardsContainer = styled(FlexDivRow)` + width: 100%; + justify-content: flex-start; + column-gap: 5px; + background: ${(props) => props.theme.colors.selectedTheme.newTheme.containers.cards.background}; + border-radius: 15px; + border: 1px solid ${(props) => props.theme.colors.selectedTheme.newTheme.border.color}; + flex: 1 1 0; + flex-wrap: wrap; +` + +export default RewardsTab diff --git a/packages/app/src/sections/dashboard/Stake/StakingHeading.tsx b/packages/app/src/sections/dashboard/Stake/StakingHeading.tsx new file mode 100644 index 0000000000..39213e3290 --- /dev/null +++ b/packages/app/src/sections/dashboard/Stake/StakingHeading.tsx @@ -0,0 +1,45 @@ +import { FC, memo } from 'react' +import { useTranslation } from 'react-i18next' +import styled from 'styled-components' + +import Button from 'components/Button' +import { FlexDivCol, FlexDivRowCentered } from 'components/layout/flex' +import { Heading } from 'components/Text' +import { EXTERNAL_LINKS } from 'constants/links' + +interface StakingHeadingProps { + title: string +} + +export const StakingHeading: FC = memo(({ title }) => { + const { t } = useTranslation() + + return ( + + + {title} + + window.open(EXTERNAL_LINKS.Docs.Staking, '_blank')} + > + {t('dashboard.stake.docs')} + + + ) +}) + +const TitleContainer = styled(FlexDivRowCentered)` + margin-bottom: 30px; +` + +const StyledButton = styled(Button)` + border-width: 0px; + color: ${(props) => props.theme.colors.selectedTheme.newTheme.text.secondary}; +` + +const StyledHeading = styled(Heading)` + font-weight: 400; +` diff --git a/packages/app/src/sections/dashboard/Stake/StakingPortfolio.tsx b/packages/app/src/sections/dashboard/Stake/StakingPortfolio.tsx index 07aa2bcd09..32bc52c422 100644 --- a/packages/app/src/sections/dashboard/Stake/StakingPortfolio.tsx +++ b/packages/app/src/sections/dashboard/Stake/StakingPortfolio.tsx @@ -1,145 +1,95 @@ -import { truncateNumbers } from '@kwenta/sdk/utils' -import { useRouter } from 'next/router' -import { FC } from 'react' -import { useTranslation } from 'react-i18next' -import styled from 'styled-components' +import { FC, memo } from 'react' +import styled, { css } from 'styled-components' -import Button from 'components/Button/Button' -import { FlexDivRowCentered } from 'components/layout/flex' -import { EXTERNAL_LINKS } from 'constants/links' -import ROUTES from 'constants/routes' -import { SplitStakingCard } from 'sections/dashboard/Stake/card' -import { Heading } from 'sections/earn/text' -import { useAppSelector } from 'state/hooks' -import { - selectClaimableBalance, - selectEscrowedKwentaBalance, - selectKwentaBalance, - selectStakedEscrowedKwentaBalance, - selectStakedKwentaBalance, - selectTotalVestable, -} from 'state/staking/selectors' +import { FlexDivCol, FlexDivRow, FlexDivRowCentered } from 'components/layout/flex' +import { Body } from 'components/Text' import media from 'styles/media' +import MigrationSteps from './MigrationSteps' +import { StakingHeading } from './StakingHeading' +import { StakingCards } from './types' + export enum StakeTab { Staking = 'staking', Escrow = 'escrow', - TradingRewards = 'trading-rewards', - Redemption = 'redemption', } type StakingPortfolioProps = { - setCurrentTab(tab: StakeTab): void + title: string + cardsInfo: StakingCards[] + isMigrationCompleted?: boolean } -const StakingPortfolio: FC = ({ setCurrentTab }) => { - const { t } = useTranslation() - const router = useRouter() - const kwentaBalance = useAppSelector(selectKwentaBalance) - const escrowedKwentaBalance = useAppSelector(selectEscrowedKwentaBalance) - const stakedEscrowedKwentaBalance = useAppSelector(selectStakedEscrowedKwentaBalance) - const stakedKwentaBalance = useAppSelector(selectStakedKwentaBalance) - const claimableBalance = useAppSelector(selectClaimableBalance) - const totalVestable = useAppSelector(selectTotalVestable) - - const DEFAULT_CARDS = [ - [ - { - key: 'Liquid', - title: t('dashboard.stake.portfolio.liquid'), - value: truncateNumbers(kwentaBalance, 2), - onClick: () => setCurrentTab(StakeTab.Staking), - }, - { - key: 'Escrow', - title: t('dashboard.stake.portfolio.escrow'), - value: truncateNumbers(escrowedKwentaBalance.sub(stakedEscrowedKwentaBalance), 2), - onClick: () => setCurrentTab(StakeTab.Escrow), - }, - ], - [ - { - key: 'Staked', - title: t('dashboard.stake.portfolio.staked'), - value: truncateNumbers(stakedKwentaBalance, 2), - onClick: () => setCurrentTab(StakeTab.Staking), - }, - { - key: 'StakedEscrow', - title: t('dashboard.stake.portfolio.staked-escrow'), - value: truncateNumbers(stakedEscrowedKwentaBalance, 2), - onClick: () => setCurrentTab(StakeTab.Escrow), - }, - ], - [ - { - key: 'Claimable', - title: t('dashboard.stake.portfolio.claimable'), - value: truncateNumbers(claimableBalance, 2), - onClick: () => setCurrentTab(StakeTab.Staking), - }, - { - key: 'Vestable', - title: t('dashboard.stake.portfolio.vestable'), - value: truncateNumbers(totalVestable, 2), - onClick: () => setCurrentTab(StakeTab.Escrow), - }, - ], - ] - - return ( - - - {t('dashboard.stake.portfolio.title')} - - - - - - - {DEFAULT_CARDS.map((card, i) => ( - - {card.map(({ key, title, value, onClick }) => ( -
-
{title}
-
{value}
-
- ))} -
- ))} -
-
- ) -} +const StakingPortfolio: FC = memo( + ({ title, cardsInfo, isMigrationCompleted = true }) => { + return ( + + + {!isMigrationCompleted && } + + {cardsInfo.map(({ category, card, onClick, icon }, i) => ( + + + {category} + {icon} + + + {card.map(({ key, title, value, onClick }) => ( + + {title} + + {value} + + + ))} + + + ))} + + + ) + } +) -const ButtonContainer = styled(FlexDivRowCentered)` - column-gap: 10px; +const LabelContainer = styled(Body)` + display: flex; + flex-direction: row; + column-gap: 5px; + align-items: center; ` -const StakingHeading = styled(FlexDivRowCentered)` - margin-bottom: 15px; +const StyledFlexDivCol = styled(FlexDivCol)` + ${(props) => + props.onClick && + css` + cursor: pointer; + `} + ${media.lessThan('lg')` + width: 135px; + `} ` const StakingPortfolioContainer = styled.div` ${media.lessThan('mdUp')` padding: 15px; `} - - ${media.greaterThan('mdUp')` + ${media.greaterThan('lg')` margin-top: 20px; - margin-bottom: 100px; `} ` -const CardsContainer = styled.div` - width: 100%; - display: grid; - grid-template-columns: repeat(auto-fill, minmax(334px, 1fr)); - grid-gap: 15px; +const CardsContainer = styled(FlexDivRowCentered)` + padding: 20px; + background: ${(props) => props.theme.colors.selectedTheme.newTheme.containers.cards.background}; + border-radius: 20px; + border: 1px solid ${(props) => props.theme.colors.selectedTheme.newTheme.border.color}; + justify-content: flex-start; + column-gap: 50px; + row-gap: 25px; + flex-flow: row wrap; + ${media.lessThan('md')` + column-gap: 25px; + `} ` export default StakingPortfolio diff --git a/packages/app/src/sections/dashboard/Stake/StakingTab.tsx b/packages/app/src/sections/dashboard/Stake/StakingTab.tsx index 4f0d066956..0331b46c7a 100644 --- a/packages/app/src/sections/dashboard/Stake/StakingTab.tsx +++ b/packages/app/src/sections/dashboard/Stake/StakingTab.tsx @@ -1,78 +1,209 @@ import { formatPercent, truncateNumbers } from '@kwenta/sdk/utils' -import { useCallback } from 'react' +import { useCallback, useMemo } from 'react' import { useTranslation } from 'react-i18next' import styled from 'styled-components' +import HelpIcon from 'assets/svg/app/question-mark.svg' import Button from 'components/Button' +import { FlexDivCol, FlexDivRow, FlexDivRowCentered } from 'components/layout/flex' import { SplitContainer } from 'components/layout/grid' -import { LogoText } from 'components/Text' +import { Body, Heading } from 'components/Text' +import Tooltip from 'components/Tooltip/Tooltip' +import { NO_VALUE } from 'constants/placeholder' import { StakingCard } from 'sections/dashboard/Stake/card' import { useAppDispatch, useAppSelector } from 'state/hooks' -import { getReward } from 'state/staking/actions' -import { selectAPY, selectClaimableBalance } from 'state/staking/selectors' +import { claimStakingRewardsV2, compoundRewards } from 'state/staking/actions' +import { + selectAPYV2, + selectClaimableBalanceV2, + selectIsCompoundingRewards, + selectIsGettingReward, + selectStakedKwentaBalanceV2, +} from 'state/staking/selectors' +import media from 'styles/media' import StakeInputCard from './InputCards/StakeInputCard' +import { StakingCards } from './types' const StakingTab = () => { const { t } = useTranslation() const dispatch = useAppDispatch() - const claimableBalance = useAppSelector(selectClaimableBalance) - const apy = useAppSelector(selectAPY) + const claimableBalance = useAppSelector(selectClaimableBalanceV2) + const stakedKwentaBalance = useAppSelector(selectStakedKwentaBalanceV2) + const isClaimingReward = useAppSelector(selectIsGettingReward) + const isCompoundingReward = useAppSelector(selectIsCompoundingRewards) + const apy = useAppSelector(selectAPYV2) const handleGetReward = useCallback(() => { - dispatch(getReward()) + dispatch(claimStakingRewardsV2()) }, [dispatch]) + const handleCompoundReward = useCallback(() => { + dispatch(compoundRewards()) + }, [dispatch]) + + const stakingAndRewardsInfo: StakingCards[] = useMemo( + () => [ + { + category: t('dashboard.stake.tabs.staking.title'), + card: [ + { + key: 'staking-staked', + title: t('dashboard.stake.portfolio.balance.staked'), + value: truncateNumbers(stakedKwentaBalance, 2), + }, + { + key: 'staking-apr', + title: t('dashboard.stake.portfolio.rewards.apr'), + value: formatPercent(apy, { minDecimals: 2 }), + }, + ], + flex: 1, + }, + { + category: t('dashboard.stake.portfolio.rewards.title'), + card: [ + { + key: 'rewards-claimable', + title: t('dashboard.stake.portfolio.rewards.claimable'), + value: truncateNumbers(claimableBalance, 2), + }, + ], + flex: 0.5, + }, + { + category: t('dashboard.stake.portfolio.early-vest-rewards.title'), + icon: ( + + + + + + ), + card: [ + { + key: 'early-vest-rewards-claimable', + title: t('dashboard.stake.portfolio.early-vest-rewards.claimable'), + value: NO_VALUE, + }, + { + key: 'early-vest-rewards-epoch', + title: t('dashboard.stake.portfolio.early-vest-rewards.epoch'), + value: NO_VALUE, + }, + ], + flex: 1, + }, + ], + [apy, claimableBalance, stakedKwentaBalance, t] + ) + return ( + - -
-
{t('dashboard.stake.tabs.staking.claimable-rewards')}
- {truncateNumbers(claimableBalance, 4)} -
-
-
{t('dashboard.stake.tabs.staking.annual-percentage-rate')}
-
{formatPercent(apy, { minDecimals: 2 })}
-
-
- + + {t('dashboard.stake.tabs.staking.staking-rewards.title')} + + + {stakingAndRewardsInfo.map(({ category, card, flex, icon }, i) => ( + + + {category} {icon} + + + {card.map(({ key, title, value }) => ( + + {title} + + {value} + + + ))} + + + ))} + + + + +
-
) } -const CardGridContainer = styled(StakingCard)` - display: flex; - flex-direction: column; - justify-content: space-between; +const CustomStyledTooltip = styled(Tooltip)` + padding: 10px; + white-space: normal; + top: -120px; + left: -200px; + ${media.lessThan('md')` + width: 200px; + left: -120px; + top: -130px; + `} ` -const CardGrid = styled.div` - display: grid; - grid-template-rows: 1fr 1fr; +const WithCursor = styled.div<{ cursor: 'help' }>` + cursor: ${(props) => props.cursor}; +` - & > div { - margin-bottom: 20px; - } +const LabelContainer = styled(Body)` + display: flex; + flex-direction: row; + column-gap: 5px; + align-items: center; +` + +const CardsContainer = styled(FlexDivRowCentered)` + width: 100%; + justify-content: flex-start; + flex-wrap: wrap; + column-gap: 50px; + row-gap: 25px; + margin: 50px 0; + ${media.lessThan('lg')` + margin: 30px 0; + `} +` - .value { - margin-top: 5px; - } +const StyledHeading = styled(Heading)` + font-weight: 400; +` - .title { - color: ${(props) => props.theme.colors.selectedTheme.title}; - } +const CardGridContainer = styled(StakingCard)` + display: flex; + flex-direction: column; + justify-content: space-between; + ${media.lessThan('lg')` + justify-content: flex-start; + `} ` export default StakingTab diff --git a/packages/app/src/sections/dashboard/Stake/StakingTabs.tsx b/packages/app/src/sections/dashboard/Stake/StakingTabs.tsx index 6bf81ab9a0..4604ff9e0e 100644 --- a/packages/app/src/sections/dashboard/Stake/StakingTabs.tsx +++ b/packages/app/src/sections/dashboard/Stake/StakingTabs.tsx @@ -1,31 +1,16 @@ -import { useCallback } from 'react' import { useTranslation } from 'react-i18next' import styled from 'styled-components' import TabButton from 'components/Button/TabButton' -import { FlexDivRowCentered } from 'components/layout/flex' -import LabelContainer from 'components/Nav/DropDownLabel' -import Select from 'components/Select' -import { DropdownIndicator, IndicatorSeparator } from 'components/Select' import { TabPanel } from 'components/Tab' -import useIsL2 from 'hooks/useIsL2' -import { useAppDispatch, useAppSelector } from 'state/hooks' -import { setSelectedEpoch } from 'state/staking/reducer' -import { selectEpochData, selectSelectedEpoch } from 'state/staking/selectors' +import { useAppSelector } from 'state/hooks' +import { selectSelectedEpoch } from 'state/staking/selectors' import media from 'styles/media' import EscrowTab from './EscrowTab' -import RedemptionTab from './RedemptionTab' +import RewardsTab from './RewardsTab' import { StakeTab } from './StakingPortfolio' import StakingTab from './StakingTab' -import TradingRewardsTab from './TradingRewardsTab' - -type EpochValue = { - period: number - start: number - end: number - label: string -} type StakingTabsProp = { currentTab: StakeTab @@ -34,83 +19,30 @@ type StakingTabsProp = { const StakingTabs: React.FC = ({ currentTab, onChangeTab }) => { const { t } = useTranslation() - const isL2 = useIsL2() - const dispatch = useAppDispatch() - - const epochData = useAppSelector(selectEpochData) const selectedEpoch = useAppSelector(selectSelectedEpoch) - const handleChangeEpoch = useCallback( - (value: EpochValue) => () => { - dispatch(setSelectedEpoch(value.period)) - }, - [dispatch] - ) - - const formatOptionLabel = useCallback( - (option: EpochValue) => ( -
- {option.label} -
- ), - [handleChangeEpoch] - ) - return ( - 768 - ? t('dashboard.stake.tabs.trading-rewards.title') - : t('dashboard.stake.tabs.trading-rewards.mobile-title') - } - onClick={onChangeTab(StakeTab.TradingRewards)} - active={currentTab === StakeTab.TradingRewards} - /> - - - {window.innerWidth < 768 && ( - {t('dashboard.stake.tabs.staking.current-trading-period')} - )} - - b.period - a.period)} - optionPadding="0px" - value={selectedEpoch} - menuWidth={240} - components={{ IndicatorSeparator, DropdownIndicator }} - isSearchable={false} - variant="flat" - isDisabled={!isL2} - /> - -
- - - = ({ currentTab, onChangeTab }) => - - -
) } -const SelectLabelContainer = styled(LabelContainer)` - font-size: 12px; -` - -const StakingSelect = styled(Select)` - height: 38px; - width: 100%; - .react-select__control, - .react-select__menu, - .react-select__menu-list { - border-radius: 20px; - background: ${(props) => props.theme.colors.selectedTheme.surfaceFill}; - } - - .react-select__value-container { - padding: 0; - } - - .react-select__single-value > div > div { - font-size: 12px; - } - - .react-select__dropdown-indicator { - margin-right: 10px; - } -` - -const StyledFlexDivRowCentered = styled(FlexDivRowCentered)<{ active: boolean }>` - display: ${(props) => (props.active ? 'flex' : 'none')}; - width: 24%; - ${media.lessThan('md')` - width: unset; - `} -` - -const PeriodLabel = styled.div` - font-size: 11px; - line-height: 11px; - display: flex; - align-items: center; - color: ${(props) => props.theme.colors.selectedTheme.button.text.primary}; - margin-left: 4px; - width: 50%; -` - const StakingTabsHeader = styled.div` display: flex; justify-content: space-between; - margin-bottom: 20px; + margin-top: 30px; + margin-bottom: 30px; ${media.lessThan('md')` flex-direction: column; row-gap: 10px; - margin-bottom: 10px; + margin-bottom: 25px; + margin-top: 0px; `} ` const StakingTabsContainer = styled.div` - margin-bottom: 50px; ${media.lessThan('md')` padding: 15px; `} @@ -193,12 +78,13 @@ const StakingTabsContainer = styled.div` const TabButtons = styled.div` display: flex; + & > button:not(:last-of-type) { - margin-right: 8px; + margin-right: 25px; } ${media.lessThan('md')` - justify-content: space-around; + justify-content: flex-start; `} ` diff --git a/packages/app/src/sections/dashboard/Stake/TradingRewardsTab.tsx b/packages/app/src/sections/dashboard/Stake/TradingRewardsTab.tsx index 29ee1795d1..fdc99bf586 100644 --- a/packages/app/src/sections/dashboard/Stake/TradingRewardsTab.tsx +++ b/packages/app/src/sections/dashboard/Stake/TradingRewardsTab.tsx @@ -235,7 +235,6 @@ const TradingRewardsTab: FC = memo(