diff --git a/.github/workflows/sonarqube.yaml b/.github/workflows/sonarqube.yaml new file mode 100644 index 00000000..0fce219b --- /dev/null +++ b/.github/workflows/sonarqube.yaml @@ -0,0 +1,21 @@ +on: + pull_request: + types: [opened, synchronize, reopened] + +name: PR Static Code Analysis +jobs: + static-code-analysis: + runs-on: ubuntu-latest + steps: + - name: Code Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + - name: SonarQube Scan + uses: sonarsource/sonarqube-scan-action@v2.1.0 + env: + SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} + SONAR_HOST_URL: ${{ secrets.SONAR_HOST_URL }} + with: + args: > + -Dsonar.projectKey=Schrodinger-AI-interface \ No newline at end of file diff --git a/src/api/type.d.ts b/src/api/type.d.ts index e24b443f..19724cdb 100644 --- a/src/api/type.d.ts +++ b/src/api/type.d.ts @@ -65,11 +65,13 @@ interface IPointItem { inviteRate: number; thirdFollowersNumber: number; thirdRate: number; + ecoEarnReward: number; } interface IGetPointsData { pointDetails: Array; hasBoundAddress: boolean; + evmAddress?: string; } interface ICatItemModel { diff --git a/src/assets/img/icons/scarce.svg b/src/assets/img/icons/scarce.svg new file mode 100644 index 00000000..bb7cf2f7 --- /dev/null +++ b/src/assets/img/icons/scarce.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/src/components/EarnList/components/ActionComponent.tsx b/src/components/EarnList/components/ActionComponent.tsx new file mode 100644 index 00000000..b6f01dd3 --- /dev/null +++ b/src/components/EarnList/components/ActionComponent.tsx @@ -0,0 +1,131 @@ +import { useAccountModal, useConnectModal } from '@rainbow-me/rainbowkit'; +import { useGetSignature } from 'hooks/useGetSignature'; +import { bindAddress as bindEvmAddress } from 'api/request'; +import { useWalletService } from 'hooks/useWallet'; +import { Button, HashAddress, ToolTip } from 'aelf-design'; +import { useResponsive } from 'hooks/useResponsive'; +import { useCallback, useEffect, useState } from 'react'; +import { useAccount } from 'wagmi'; +import { useCmsInfo } from 'redux/hooks'; +import { ReactComponent as ArrowIcon } from 'assets/img/icons/arrow.svg'; +import React from 'react'; +import { openExternalLink } from 'utils/openlink'; +import { OmittedType, getOmittedStr } from 'utils/addressFormatting'; +import CommonCopy from 'components/CommonCopy'; + +function ActionComponent({ + data, + hasBoundAddress = false, + bindAddress, + boundEvmAddress, +}: { + data: IPointItem; + hasBoundAddress?: boolean; + bindAddress?: () => void; + boundEvmAddress?: string; +}) { + const { symbol } = data; + const { getSignInfo } = useGetSignature(); + const { wallet } = useWalletService(); + const [connected, setConnected] = useState(false); + const [evmAddress, setEvmAddress] = useState(''); + const [bindLoading, setBindLoading] = useState(false); + const cmsInfo = useCmsInfo(); + const { isLG } = useResponsive(); + + const { openConnectModal } = useConnectModal(); + const { openAccountModal } = useAccountModal(); + + const account = useAccount(); + + useEffect(() => { + setConnected(account.isConnected); + setEvmAddress(account.address || ''); + }, [account]); + + const jumpToEcoEarn = () => { + const link = cmsInfo?.ecoEarn || cmsInfo?.gitbookEcoEarn; + if (!link) return; + openExternalLink(link, '_blank'); + }; + + const sign = useCallback(async () => { + try { + if (bindLoading) return; + setBindLoading(true); + const info = `${wallet.address}-${evmAddress}`; + const res = await getSignInfo(info); + if (res?.publicKey && res.signature && evmAddress) { + await bindEvmAddress({ + aelfAddress: wallet.address, + evmAddress, + signature: res.signature, + publicKey: res.publicKey, + }); + bindAddress && bindAddress(); + } + } catch (error) { + console.log('=====account error', error); + } + setBindLoading(false); + }, [bindAddress, bindLoading, evmAddress, getSignInfo, wallet.address]); + + if (cmsInfo?.needBindEvm && cmsInfo.needBindEvm.includes(symbol) && !hasBoundAddress) { + return ( +
+ {connected ? ( +
+
+ +
+ +
+ ) : ( +
+ + Connect EVM wallet + +
+ )} +
+ ); + } + return ( +
+ {cmsInfo?.needBindEvm && cmsInfo.needBindEvm.includes(symbol) && boundEvmAddress ? ( + + + + {getOmittedStr(boundEvmAddress, OmittedType.CUSTOM, { + prevLen: 4, + endLen: 4, + limitLen: 9, + })} + + + + ) : null} +
+ Details + +
+
+ ); +} + +export default React.memo(ActionComponent); diff --git a/src/components/EarnList/components/ColumnsTitle.tsx b/src/components/EarnList/components/ColumnsTitle.tsx new file mode 100644 index 00000000..646fe600 --- /dev/null +++ b/src/components/EarnList/components/ColumnsTitle.tsx @@ -0,0 +1,27 @@ +import React from 'react'; +import { ReactComponent as Question } from 'assets/img/icons/question.svg'; +import { ToolTip } from 'aelf-design'; + +function ColumnsTitle({ title, tooltip }: { title: string; tooltip?: string[] }) { + return ( +
+ {tooltip && tooltip.length ? ( + + {tooltip.map((item, index) => { + return

{item}

; + })} +
+ } + className="mr-[4px]"> + + + ) : null} + + {title} + + ); +} + +export default React.memo(ColumnsTitle); diff --git a/src/components/EarnList/components/TableCell.tsx b/src/components/EarnList/components/TableCell.tsx new file mode 100644 index 00000000..63f182b1 --- /dev/null +++ b/src/components/EarnList/components/TableCell.tsx @@ -0,0 +1,26 @@ +import React, { ReactElement } from 'react'; +import { ReactComponent as Question } from 'assets/img/icons/question.svg'; +import { ToolTip } from 'aelf-design'; + +function TableCell({ value, tooltip }: { value: string | ReactElement; tooltip?: string[] }) { + return ( +
+ {typeof value === 'string' ? {value} : value} + {tooltip && tooltip.length ? ( + + {tooltip.map((item, index) => { + return

{item}

; + })} +
+ } + className="ml-[4px] inline-block"> + + + ) : null} + + ); +} + +export default React.memo(TableCell); diff --git a/src/components/EarnList/index.module.css b/src/components/EarnList/index.module.css new file mode 100644 index 00000000..fe6d71a2 --- /dev/null +++ b/src/components/EarnList/index.module.css @@ -0,0 +1,18 @@ +.earn-list { + @apply mt-[24px]; + :global { + .aelfd-table-thead { + @apply bg-neutralDefaultBg rounded-lg; + + > tr > th { + @apply bg-neutralDefaultBg; + } + .aelfd-table-cell { + @apply rounded-none; + } + .aelfd-table-cell::before { + @apply !bg-transparent; + } + } + } +} \ No newline at end of file diff --git a/src/components/EarnList/index.tsx b/src/components/EarnList/index.tsx index e6fa864a..0a75dedf 100644 --- a/src/components/EarnList/index.tsx +++ b/src/components/EarnList/index.tsx @@ -1,201 +1,136 @@ import { formatTokenPrice } from 'utils/format'; import { EarnAmountCount } from './components/EarnAmountCount'; import BigNumber from 'bignumber.js'; -import { useAccountModal, useConnectModal } from '@rainbow-me/rainbowkit'; -import { useGetSignature } from 'hooks/useGetSignature'; -import { bindAddress as bindEvmAddress } from 'api/request'; -import { useWalletService } from 'hooks/useWallet'; -import { ReactComponent as Question } from 'assets/img/icons/question.svg'; -import { Button, HashAddress, ToolTip } from 'aelf-design'; -import { useResponsive } from 'hooks/useResponsive'; -import { useCallback, useEffect, useState } from 'react'; -import { useAccount } from 'wagmi'; +import { Table } from 'aelf-design'; +import { useMemo, useState } from 'react'; +import { TableColumnsType } from 'antd'; +import ActionComponent from './components/ActionComponent'; +import ColumnsTitle from './components/ColumnsTitle'; +import TableCell from './components/TableCell'; import { useCmsInfo } from 'redux/hooks'; -import { ReactComponent as WalletIcon } from 'assets/img/icons/wallet.svg'; -import { ReactComponent as LinkIcon } from 'assets/img/icons/link-white.svg'; -import clsx from 'clsx'; +import { useResponsive } from 'hooks/useResponsive'; +import styles from './index.module.css'; interface ITokenEarnListProps { dataSource: Array; hasBoundAddress?: boolean; + boundEvmAddress?: string; bindAddress?: () => void; } -function PointItem({ - data, - hasBoundAddress = false, - bindAddress, -}: { - data: IPointItem; - hasBoundAddress?: boolean; - bindAddress?: () => void; -}) { - const { symbol, amount } = data; - const { getSignInfo } = useGetSignature(); - const { wallet } = useWalletService(); - const [connected, setConnected] = useState(false); - const [evmAddress, setEvmAddress] = useState(''); - const [bindLoading, setBindLoading] = useState(false); - const cmsInfo = useCmsInfo(); - - const { openConnectModal } = useConnectModal(); - const { openAccountModal } = useAccountModal(); - - const account = useAccount(); - - useEffect(() => { - console.log('=====useAccountEffect', account); - setConnected(account.isConnected); - setEvmAddress(account.address || ''); - }, [account]); - - const sign = useCallback(async () => { - try { - if (bindLoading) return; - setBindLoading(true); - console.log('=====account sign'); - const info = `${wallet.address}-${evmAddress}`; - const res = await getSignInfo(info); - if (res?.publicKey && res.signature && evmAddress) { - console.log('=====account SignInfo', res, evmAddress, wallet.address); - - await bindEvmAddress({ - aelfAddress: wallet.address, - evmAddress, - signature: res.signature, - publicKey: res.publicKey, - }); - bindAddress && bindAddress(); - } - } catch (error) { - /* empty */ - console.log('=====account error', error); - } - setBindLoading(false); - }, [bindAddress, bindLoading, evmAddress, getSignInfo, wallet.address]); - - if (cmsInfo?.needBindEvm && cmsInfo.needBindEvm.includes(symbol)) { - if (!hasBoundAddress) { - return ( -
- {connected ? ( -
-
- -
- - -
- ) : ( -
- -
- )} -
- ); - } - } - return ( - - {amount - ? `${formatTokenPrice( - BigNumber(amount) - .dividedBy(10 ** 8) - .toNumber(), - )} ${symbol}` - : '--'} - - ); -} - -function PointListItem({ - data, +export function TokenEarnList({ + dataSource, hasBoundAddress = false, bindAddress, -}: { - data: IPointItem; - hasBoundAddress?: boolean; - bindAddress?: () => void; -}) { - const { displayName, symbol, action, amount, ...props } = data; + boundEvmAddress, +}: ITokenEarnListProps) { const cmsInfo = useCmsInfo(); const { isLG } = useResponsive(); - return ( -
-
- {displayName} - {cmsInfo?.needBindEvm && cmsInfo.needBindEvm.includes(symbol) ? ( - -

- 1. Receive point rewards by contributing LP to the SGR/USDT trading pair on Uniswap V3, with a fee - rate of 1%. Provide the EVM address of the LP and start accumulating daily rewards. -

-

2. An aelf address can only be bound to one EVM address, and vice versa.

-

- 3. Do note that after an address is bound, it cannot be changed. Therefore, please confirm that the - EVM address is correct. -

-
- } - trigger={isLG ? 'click' : 'hover'}> - - - ) : null} -
- - {action === 'SelfIncrease' ? ( - <> - - {` ${symbol}`} - - ) : ( - - )} - - - ); -} + const columns: TableColumnsType = useMemo(() => { + return [ + { + title: , + dataIndex: 'displayName', + key: 'symbol', + width: isLG ? 160 : 'max-content', + render: (displayName: string, record: IPointItem) => { + if (cmsInfo?.needBindEvm && cmsInfo.needBindEvm.includes(record.symbol)) { + return ( + {displayName}} + tooltip={[ + '1. Receive point rewards by contributing LP to the SGR/USDT trading pair on Uniswap V3, with a fee rate of 1%. Provide the EVM address of the LP and start accumulating daily rewards.', + '2. An aelf address can only be bound to one EVM address, and vice versa.', + '3. Do note that after an address is bound, it cannot be changed. Therefore, please confirm that the EVM address is correct.', + ]} + /> + ); + } + return {displayName}} />; + }, + }, + { + title: , + dataIndex: 'amount', + key: 'amount', + width: isLG ? 300 : 400, + render: (amount: number, record: IPointItem) => { + if (record.action === 'SelfIncrease') { + return ( + + + {` ${record.symbol}`} + + ) : ( + '--' + ) + } + /> + ); + } + return ( + + ); + }, + }, + { + title: ( + + ), + dataIndex: 'ecoEarnReward', + key: 'ecoEarnReward', + width: isLG ? 300 : 'max-content', + render: (ecoEarnReward: string) => { + return ; + }, + }, + { + title: , + dataIndex: 'action', + key: 'action', + width: isLG ? 115 : 'max-content', + fixed: isLG ? 'right' : false, + render: (_: null, record: IPointItem) => { + return ( + + ); + }, + }, + ]; + }, [bindAddress, cmsInfo?.needBindEvm, hasBoundAddress, isLG, boundEvmAddress]); -export function TokenEarnList({ dataSource, hasBoundAddress = false, bindAddress }: ITokenEarnListProps) { if (!dataSource?.length) return null; return ( -
- {dataSource.map((pointDetail) => ( - - ))} +
+ ); } diff --git a/src/components/TraitsList/index.tsx b/src/components/TraitsList/index.tsx index 010e4b9c..ef4d3c39 100644 --- a/src/components/TraitsList/index.tsx +++ b/src/components/TraitsList/index.tsx @@ -6,6 +6,7 @@ import useResponsive from 'hooks/useResponsive'; import { useMemo } from 'react'; import { formatPercent } from 'utils/format'; import TextEllipsis from 'components/TextEllipsis'; +import Image from 'next/image'; interface ITraitItem { item: ITrait; @@ -18,16 +19,38 @@ interface ITraitsListProps { showNew?: boolean; } +const scarceWidth = 20; + function TraitsItem({ item, showNew, isLG }: ITraitItem) { - const { traitType, value, percent } = item; + const { traitType, value, percent, isRare } = item; return (
- +
+
+ +
+ + {isRare ? ( + + ) : null} +
+
{formatPercent(percent)}%
{showNew && }
diff --git a/src/pageComponents/detail/components/ItemInfo.tsx b/src/pageComponents/detail/components/ItemInfo.tsx index 0e94055e..6e0d1765 100644 --- a/src/pageComponents/detail/components/ItemInfo.tsx +++ b/src/pageComponents/detail/components/ItemInfo.tsx @@ -14,6 +14,9 @@ import { useResponsive } from 'hooks/useResponsive'; import TextEllipsis from 'components/TextEllipsis'; import HonourLabel from 'components/ItemCard/components/HonourLabel'; import { renameSymbol } from 'utils/renameSymbol'; +import Image from 'next/image'; + +const scarceWidth = 20; export default function ItemInfo({ detail, @@ -47,16 +50,38 @@ export default function ItemInfo({ (item: ITrait) => { return ( -
+
- +
+
+ +
+ + {item.isRare ? ( + + ) : null} +
diff --git a/src/pageComponents/points/index.tsx b/src/pageComponents/points/index.tsx index e5547289..a4ff9ef7 100644 --- a/src/pageComponents/points/index.tsx +++ b/src/pageComponents/points/index.tsx @@ -8,6 +8,9 @@ import { useTimeoutFn } from 'react-use'; import useLoading from 'hooks/useLoading'; import useGetLoginStatus from 'redux/hooks/useGetLoginStatus'; import { useDisconnect } from 'wagmi'; +import { getDomain } from 'utils'; +import { useJumpToPage } from 'hooks/useJumpToPage'; +import { useCmsInfo } from 'redux/hooks'; export default function PointsPage() { const { wallet } = useWalletService(); @@ -15,17 +18,21 @@ export default function PointsPage() { const router = useRouter(); const { showLoading, closeLoading } = useLoading(); const [hasBoundAddress, setHasBoundAddress] = useState(false); + const [boundEvmAddress, setBoundEvmAddress] = useState(); const { disconnect } = useDisconnect(); + const { jumpToPage } = useJumpToPage(); + const cmsInfo = useCmsInfo(); const getPointsData = useCallback( async (address: string) => { if (!address) return; showLoading(); const response = await getPoints({ - domain: document.location.host, + domain: getDomain(), address: address, }); setHasBoundAddress(response.hasBoundAddress); + setBoundEvmAddress(response.evmAddress); closeLoading(); return response; }, @@ -40,10 +47,10 @@ export default function PointsPage() { }, }); - const toBindAddress = async () => { + const toBindAddress = useCallback(async () => { setHasBoundAddress(true); getPointsData(wallet.address); - }; + }, [getPointsData, wallet.address]); useTimeoutFn(() => { if (!isLogin) { @@ -58,10 +65,27 @@ export default function PointsPage() {

Details of credits token earned for this domain

+ {cmsInfo?.pixiePoints ? ( +

+ You can earn more points by operating an independent subdomain,{' '} + { + jumpToPage({ + link: cmsInfo?.pixiePoints, + linkType: 'externalLink', + }); + }}> + Become Advocate + +

+ ) : null} + {data?.pointDetails.length ? ( ) : ( diff --git a/src/redux/types/reducerTypes.ts b/src/redux/types/reducerTypes.ts index 84526748..c51e1c91 100644 --- a/src/redux/types/reducerTypes.ts +++ b/src/redux/types/reducerTypes.ts @@ -156,6 +156,8 @@ export type TGlobalConfigType = { }>; forestActivity?: string; ecoEarn?: string; + gitbookEcoEarn?: string; + pixiePoints?: string; showNftQuantity?: number; [key: string]: any; }; diff --git a/src/types/tokens.ts b/src/types/tokens.ts index f5e666ee..cb145c82 100644 --- a/src/types/tokens.ts +++ b/src/types/tokens.ts @@ -4,6 +4,7 @@ export interface ITrait { traitType: string; value: string; percent: number; + isRare?: boolean; } export type TBaseSGRToken = {