diff --git a/src/components/Announcement/PrivateAnnoucement/InboxItemKyberAIWatchList.tsx b/src/components/Announcement/PrivateAnnoucement/InboxItemKyberAIWatchList.tsx index c1e091c5f6..19f2a3f02a 100644 --- a/src/components/Announcement/PrivateAnnoucement/InboxItemKyberAIWatchList.tsx +++ b/src/components/Announcement/PrivateAnnoucement/InboxItemKyberAIWatchList.tsx @@ -19,7 +19,7 @@ const ItemWrapper = styled.div` gap: 8px; ` export const TokenInfo = ({ - showPrice = true, + showPrice = false, logoSize = '12px', token, }: { @@ -73,7 +73,7 @@ function InboxItemBridge({ } return ( - + diff --git a/src/components/Announcement/PrivateAnnoucement/NotificationCenter/KyberAIWatchlist.tsx b/src/components/Announcement/PrivateAnnoucement/NotificationCenter/KyberAIWatchlist.tsx index c077f940ef..2bf1223fd5 100644 --- a/src/components/Announcement/PrivateAnnoucement/NotificationCenter/KyberAIWatchlist.tsx +++ b/src/components/Announcement/PrivateAnnoucement/NotificationCenter/KyberAIWatchlist.tsx @@ -53,7 +53,7 @@ export default function AnnouncementItem({ {!expand && minimalAssets.map((token, i) => ( - + {i === minimalAssets.length - 1 ? (minimalAssets.length < slice ? '' : ', ...') : ', '} ))} diff --git a/src/components/Announcement/helper.ts b/src/components/Announcement/helper.ts index 6dfcf4dfca..0ba391456d 100644 --- a/src/components/Announcement/helper.ts +++ b/src/components/Announcement/helper.ts @@ -1,10 +1,7 @@ import { ChainId } from '@kyberswap/ks-sdk-core' -import { useCallback } from 'react' -import AnnouncementApi from 'services/announcement' import { AnnouncementTemplatePopup, PopupContentAnnouncement, PopupItemType } from 'components/Announcement/type' import { TIMES_IN_SECS } from 'constants/index' -import { useAppDispatch } from 'state/hooks' const LsKey = 'ack-announcements' export const getAnnouncementsAckMap = () => JSON.parse(localStorage[LsKey] || '{}') @@ -42,20 +39,3 @@ export const isPopupCanShow = ( const isExpired = Date.now() < startAt * 1000 || Date.now() > endAt * 1000 return !isRead && !isExpired && isRightChain && isOwn } - -export const useInvalidateTags = (reducerPath: string) => { - const dispatch = useAppDispatch() - return useCallback( - (tag: string | string[]) => { - dispatch({ - type: `${reducerPath}/invalidateTags`, - payload: Array.isArray(tag) ? tag : [tag], - }) - }, - [dispatch, reducerPath], - ) -} - -export const useInvalidateTagAnnouncement = () => { - return useInvalidateTags(AnnouncementApi.reducerPath) -} diff --git a/src/components/Announcement/index.tsx b/src/components/Announcement/index.tsx index 3abf63be97..dd56ff5b27 100644 --- a/src/components/Announcement/index.tsx +++ b/src/components/Announcement/index.tsx @@ -10,13 +10,14 @@ import styled, { css } from 'styled-components' import AnnouncementView, { Tab } from 'components/Announcement/AnnoucementView' import DetailAnnouncementPopup from 'components/Announcement/Popups/DetailAnnouncementPopup' -import { formatNumberOfUnread, useInvalidateTagAnnouncement } from 'components/Announcement/helper' +import { formatNumberOfUnread } from 'components/Announcement/helper' import { Announcement, PrivateAnnouncement } from 'components/Announcement/type' import NotificationIcon from 'components/Icons/NotificationIcon' import MenuFlyout from 'components/MenuFlyout' import Modal from 'components/Modal' import { RTK_QUERY_TAGS } from 'constants/index' import useInterval from 'hooks/useInterval' +import { useInvalidateTagAnnouncement } from 'hooks/useInvalidateTags' import useMixpanel, { MIXPANEL_TYPE } from 'hooks/useMixpanel' import { ApplicationModal } from 'state/application/actions' import { useDetailAnnouncement, useModalOpen, useToggleNotificationCenter } from 'state/application/hooks' diff --git a/src/components/Header/web3/WalletModal/index.tsx b/src/components/Header/web3/WalletModal/index.tsx index 67e7834b65..0986a50436 100644 --- a/src/components/Header/web3/WalletModal/index.tsx +++ b/src/components/Header/web3/WalletModal/index.tsx @@ -32,6 +32,7 @@ import { useWalletModalToggle, } from 'state/application/hooks' import { useIsConnectingWallet } from 'state/authen/hooks' +import { ExternalLink } from 'theme' import { isEVMWallet, isOverriddenWallet, isSolanaWallet } from 'utils' import Option from './Option' @@ -316,13 +317,13 @@ export default function WalletModal() { By connecting a wallet, you accept{' '} - e.stopPropagation()}> + e.stopPropagation()}> KyberSwap‘s Terms of Use - {' '} + {' '} and consent to its{' '} - e.stopPropagation()}> + e.stopPropagation()}> Privacy Policy - + . Last updated: {dayjs(TERM_FILES_PATH.VERSION).format('DD MMM YYYY')} diff --git a/src/components/Menu/index.tsx b/src/components/Menu/index.tsx index 87115a075a..44796e4579 100644 --- a/src/components/Menu/index.tsx +++ b/src/components/Menu/index.tsx @@ -473,7 +473,7 @@ export default function Menu() { )} - { toggle() @@ -482,10 +482,10 @@ export default function Menu() { > Terms - + - { toggle() @@ -494,7 +494,7 @@ export default function Menu() { > Privacy Policy - + { // change account sign in => refresh participant info try { refetch() - dispatch( - kyberAIapi.util.invalidateTags([ - RTK_QUERY_TAGS.GET_WATCHLIST_TOKENS_KYBER_AI, - RTK_QUERY_TAGS.GET_WATCHLIST_INFO_KYBER_AI, - ]), - ) + invalidateTags([RTK_QUERY_TAGS.GET_WATCHLIST_TOKENS_KYBER_AI, RTK_QUERY_TAGS.GET_WATCHLIST_INFO_KYBER_AI]) } catch (error) {} - }, [userInfo?.identityId, refetch, dispatch]) + }, [userInfo?.identityId, refetch, invalidateTags]) if (loading && !loadedPage.current) return if (!canAccessPage) return diff --git a/src/components/YourCampaignTransactionsModal/index.tsx b/src/components/YourCampaignTransactionsModal/index.tsx index 52e151a255..68b3bc1962 100644 --- a/src/components/YourCampaignTransactionsModal/index.tsx +++ b/src/components/YourCampaignTransactionsModal/index.tsx @@ -5,11 +5,11 @@ import { CheckCircle, Copy, ExternalLink, Info, X } from 'react-feather' import { useSelector } from 'react-redux' import { useMedia } from 'react-use' import { Flex, Text } from 'rebass' +import { useGetTxsCampaignQuery } from 'services/campaign' import styled, { css } from 'styled-components' -import useSWR from 'swr' import Modal from 'components/Modal' -import { CAMPAIGN_YOUR_TRANSACTIONS_ITEM_PER_PAGE, SWR_KEYS } from 'constants/index' +import { CAMPAIGN_YOUR_TRANSACTIONS_ITEM_PER_PAGE } from 'constants/index' import { NETWORKS_INFO } from 'constants/networks' import { useActiveWeb3React } from 'hooks' import useCopyClipboard from 'hooks/useCopyClipboard' @@ -17,7 +17,6 @@ import useTheme from 'hooks/useTheme' import { AppState } from 'state' import { ApplicationModal } from 'state/application/actions' import { useModalOpen, useToggleYourCampaignTransactionsModal } from 'state/application/hooks' -import { CampaignProofData } from 'state/campaigns/actions' import { getEtherscanLink } from 'utils' import getShortenAddress from 'utils/getShortenAddress' @@ -32,37 +31,15 @@ export default function YourCampaignTransactionsModal() { const above768 = useMedia('(min-width: 768px)') const selectedCampaign = useSelector((state: AppState) => state.campaigns.selectedCampaign) - const { data: userCampaignTransactions } = useSWR( - account && selectedCampaign - ? SWR_KEYS.getCampaignTransactions( - selectedCampaign.id, - CAMPAIGN_YOUR_TRANSACTIONS_ITEM_PER_PAGE, - CAMPAIGN_YOUR_TRANSACTIONS_ITEM_PER_PAGE * (currentPage - 1), - account, - ) - : null, - async (url: string) => { - try { - const response = await fetch(url) - if (response.ok) { - const data = await response.json() - if (data && Array.isArray(data.data) && data.data.length) { - return data.data.map( - (item: any): CampaignProofData => ({ - id: item.id, - chainId: parseInt(item.chainId), - utcTimestamp: new Date(item.time).getTime(), - txPoint: item.txPoint, - txHash: item.tx, - }), - ) - } - } - return [] - } catch (err) { - console.error(err) - } + + const { data: userCampaignTransactions } = useGetTxsCampaignQuery( + { + campaignId: selectedCampaign?.id || 0, + limit: CAMPAIGN_YOUR_TRANSACTIONS_ITEM_PER_PAGE, + offset: CAMPAIGN_YOUR_TRANSACTIONS_ITEM_PER_PAGE * (currentPage - 1), + userAddress: account ?? '', }, + { skip: !selectedCampaign?.id }, ) const [isCopied, setCopied] = useCopyClipboard() diff --git a/src/components/swapv2/LimitOrder/ListOrder/index.tsx b/src/components/swapv2/LimitOrder/ListOrder/index.tsx index e21d7fc949..fb99eed161 100644 --- a/src/components/swapv2/LimitOrder/ListOrder/index.tsx +++ b/src/components/swapv2/LimitOrder/ListOrder/index.tsx @@ -6,11 +6,10 @@ import { Trash } from 'react-feather' import { useNavigate } from 'react-router-dom' import { useMedia } from 'react-use' import { Flex, Text } from 'rebass' -import limitOrderApi, { useGetListOrdersQuery } from 'services/limitOrder' +import { useGetListOrdersQuery } from 'services/limitOrder' import styled from 'styled-components' import { ReactComponent as NoDataIcon } from 'assets/svg/no-data.svg' -import { useInvalidateTags } from 'components/Announcement/helper' import { ButtonLight } from 'components/Button' import Column from 'components/Column' import LocalLoader from 'components/LocalLoader' @@ -22,6 +21,7 @@ import SubscribeNotificationButton from 'components/SubscribeButton' import useRequestCancelOrder from 'components/swapv2/LimitOrder/ListOrder/useRequestCancelOrder' import { EMPTY_ARRAY, RTK_QUERY_TAGS, TRANSACTION_STATE_DEFAULT } from 'constants/index' import { useActiveWeb3React } from 'hooks' +import { useInvalidateTagLimitOrder } from 'hooks/useInvalidateTags' import useMixpanel, { MIXPANEL_TYPE } from 'hooks/useMixpanel' import useParsedQueryString from 'hooks/useParsedQueryString' import useShowLoadingAtLeastTime from 'hooks/useShowLoadingAtLeastTime' @@ -203,7 +203,7 @@ export default function ListLimitOrder() { onReset() }, [chainId, orderType]) - const invalidateTag = useInvalidateTags(limitOrderApi.reducerPath) + const invalidateTag = useInvalidateTagLimitOrder() const refetchOrders = useCallback(() => { invalidateTag(RTK_QUERY_TAGS.GET_LIST_ORDERS) }, [invalidateTag]) diff --git a/src/constants/index.ts b/src/constants/index.ts index 3959404bda..e181ef45c2 100644 --- a/src/constants/index.ts +++ b/src/constants/index.ts @@ -5,7 +5,7 @@ import { v4 as uuid } from 'uuid' import { TransactionFlowState } from 'types/TransactionFlowState' -import { CAMPAIGN_BASE_URL as CAMPAIGN_BASE_DOMAIN } from './env' +import { CAMPAIGN_BASE_URL } from './env' import * as ENV from './env' import { EVM_MAINNET_NETWORKS, EVM_NETWORK, NETWORKS_INFO, SUPPORTED_NETWORKS, isEVM } from './networks' @@ -186,16 +186,10 @@ export const CAMPAIGN_YOUR_TRANSACTIONS_ITEM_PER_PAGE = 10000 export const ELASTIC_BASE_FEE_UNIT = 100_000 export const KYBERSWAP_SOURCE = '{"source":"kyberswap"}' -export const CAMPAIGN_BASE_URL = `${CAMPAIGN_BASE_DOMAIN}/api/v1/campaigns` export const SWR_KEYS = { - getGrantProgramLeaderBoard: (id: number | string) => `${CAMPAIGN_BASE_DOMAIN}/api/v1/competitions/${id}/leaderboard`, - getListGrantPrograms: `${CAMPAIGN_BASE_DOMAIN}/api/v1/competitions`, - getGrantProgram: (id: number | string) => `${CAMPAIGN_BASE_DOMAIN}/api/v1/competitions/${id}`, - getListCampaign: CAMPAIGN_BASE_URL, - getLeaderboard: (id: number) => CAMPAIGN_BASE_URL + '/' + id + '/leaderboard', - getLuckyWinners: (id: number) => CAMPAIGN_BASE_URL + '/' + id + '/lucky-winners', - getCampaignTransactions: (campaignId: number, limit: number, offset: number, account: string) => - `${CAMPAIGN_BASE_URL}/${campaignId}/proofs?limit=${limit}&offset=${offset}&userAddress=${account}`, + getGrantProgramLeaderBoard: (id: number | string) => `${CAMPAIGN_BASE_URL}/api/v1/competitions/${id}/leaderboard`, + getListGrantPrograms: `${CAMPAIGN_BASE_URL}/api/v1/competitions`, + getGrantProgram: (id: number | string) => `${CAMPAIGN_BASE_URL}/api/v1/competitions/${id}`, } // https://www.nasdaq.com/glossary/b/bip diff --git a/src/hooks/kyberdao/index.tsx b/src/hooks/kyberdao/index.tsx index b7606cb214..f292df57b6 100644 --- a/src/hooks/kyberdao/index.tsx +++ b/src/hooks/kyberdao/index.tsx @@ -495,6 +495,7 @@ export function useVotingInfo() { rewardStats: { knc: rewardStats ? +rewardStats.pending?.totalAmountInKNC + +rewardStats.liquidated?.totalAmountInKNC : 0, usd: rewardStats ? +rewardStats.pending?.totalAmountInUSD + +rewardStats.liquidated?.totalAmountInUSD : 0, + apr: rewardStats ? +rewardStats.apr : 0, }, } return result diff --git a/src/hooks/kyberdao/types.ts b/src/hooks/kyberdao/types.ts index 11867aecd9..081eca4f10 100644 --- a/src/hooks/kyberdao/types.ts +++ b/src/hooks/kyberdao/types.ts @@ -37,6 +37,7 @@ interface VoteStat { total_address_count: number total_vote_count: number votes: VoteDetail[] | null + quorum_status: number } export interface ProposalDetail { cancelled: boolean @@ -130,4 +131,5 @@ export interface RewardStats { totalAmountInKNC: string totalAmountInUSD: string } + apr: string } diff --git a/src/hooks/useInvalidateTags.ts b/src/hooks/useInvalidateTags.ts new file mode 100644 index 0000000000..3a1c9cb3e1 --- /dev/null +++ b/src/hooks/useInvalidateTags.ts @@ -0,0 +1,28 @@ +import { useCallback } from 'react' +import announcementApi from 'services/announcement' +import limitOrderApi from 'services/limitOrder' + +import kyberAIApi from 'pages/TrueSightV2/hooks/useKyberAIData' +import { useAppDispatch } from 'state/hooks' + +const useInvalidateTags = (api: any) => { + const dispatch = useAppDispatch() + return useCallback( + (tag: string | string[]) => { + dispatch(api.util.invalidateTags(Array.isArray(tag) ? tag : [tag])) + }, + [dispatch, api], + ) +} + +export const useInvalidateTagAnnouncement = () => { + return useInvalidateTags(announcementApi) +} + +export const useInvalidateTagKyberAi = () => { + return useInvalidateTags(kyberAIApi) +} + +export const useInvalidateTagLimitOrder = () => { + return useInvalidateTags(limitOrderApi) +} diff --git a/src/pages/Campaign/CampaignContent.tsx b/src/pages/Campaign/CampaignContent.tsx index 27a96703e6..c43b010e8a 100644 --- a/src/pages/Campaign/CampaignContent.tsx +++ b/src/pages/Campaign/CampaignContent.tsx @@ -1,6 +1,6 @@ import { Trans, t } from '@lingui/macro' import dayjs from 'dayjs' -import { useEffect, useMemo, useRef, useState } from 'react' +import { useCallback, useEffect, useMemo, useRef, useState } from 'react' import { isMobile } from 'react-device-detect' import { BarChart, ChevronDown, Clock, Share2, Star, Users } from 'react-feather' import { useSelector } from 'react-redux' @@ -8,7 +8,6 @@ import { useNavigate } from 'react-router-dom' import { useMedia } from 'react-use' import { Flex, Text } from 'rebass' import styled, { css } from 'styled-components' -import { useSWRConfig } from 'swr' import { ButtonEmpty, ButtonLight } from 'components/Button' import Divider from 'components/Divider' @@ -18,7 +17,6 @@ import ProgressBar from 'components/ProgressBar' import ShareModal from 'components/ShareModal' import { MouseoverTooltip, TextDashed } from 'components/Tooltip' import YourCampaignTransactionsModal from 'components/YourCampaignTransactionsModal' -import { SWR_KEYS } from 'constants/index' import { useActiveWeb3React, useWeb3React } from 'hooks' import useInterval from 'hooks/useInterval' import useMixpanel, { MIXPANEL_TYPE } from 'hooks/useMixpanel' @@ -57,6 +55,8 @@ import oembed2iframe from 'utils/oembed2iframe' import ModalSelectCampaign from './ModalSelectCampaign' +export const MINUTE_TO_REFRESH = 5 * 60 + const LoaderParagraphs = () => ( <> @@ -176,7 +176,7 @@ export default function Campaign({ refreshListCampaign, ...props }: CampaignProp const { mixpanelHandler } = useMixpanel() - const above768 = useMedia(`(min-width: ${MEDIA_WIDTHS.upToSmall}px)`) + const upToSmall = useMedia(`(min-width: ${MEDIA_WIDTHS.upToSmall}px)`) const campaignDetailImageRef = useRef(null) const [campaignDetailMediaLoadedMap, setCampaignDetailMediaLoadedMap] = useState<{ [id: string]: boolean }>({}) @@ -313,9 +313,12 @@ export default function Campaign({ refreshListCampaign, ...props }: CampaignProp const toggleSelectCampaignModal = useSelectCampaignModalToggle() const navigate = useNavigate() - const onSelectCampaign = (campaign: CampaignData) => { - navigate(getSlugUrlCampaign(campaign.id, campaign.name)) - } + const onSelectCampaign = useCallback( + (campaign: CampaignData) => { + navigate(getSlugUrlCampaign(campaign.id, campaign.name)) + }, + [navigate], + ) const now = Date.now() @@ -323,13 +326,9 @@ export default function Campaign({ refreshListCampaign, ...props }: CampaignProp loadingCampaignData, loadingCampaignDataError, data: campaigns, - selectedCampaignLeaderboardPageNumber, - selectedCampaignLeaderboardLookupAddress, } = useSelector((state: AppState) => state.campaigns) - const MINUTE_TO_REFRESH = 5 - const [campaignsRefreshIn, setCampaignsRefreshIn] = useState(MINUTE_TO_REFRESH * 60) - const { mutate } = useSWRConfig() + const [campaignsRefreshIn, setCampaignsRefreshIn] = useState(MINUTE_TO_REFRESH) const dispatch = useAppDispatch() useInterval( () => { @@ -339,10 +338,7 @@ export default function Campaign({ refreshListCampaign, ...props }: CampaignProp setCampaignData({ campaigns: campaigns.map(campaign => { if (campaign.id === selectedCampaign.id) { - return { - ...campaign, - status, - } + return { ...campaign, status } } return campaign }), @@ -364,36 +360,12 @@ export default function Campaign({ refreshListCampaign, ...props }: CampaignProp ) { updateCampaignStatus(CampaignStatus.ENDED) } - setCampaignsRefreshIn(prev => { - if (prev === 0) { - return MINUTE_TO_REFRESH * 60 - } - return prev - 1 - }) + setCampaignsRefreshIn(prev => (prev === 0 ? MINUTE_TO_REFRESH : prev - 1)) }, selectedCampaign?.campaignState === CampaignState.CampaignStateReady ? 1000 : null, true, ) - useEffect(() => { - if (campaignsRefreshIn === 0 && selectedCampaign) { - mutate([ - selectedCampaign, - SWR_KEYS.getLeaderboard(selectedCampaign.id), - selectedCampaignLeaderboardPageNumber, - selectedCampaignLeaderboardLookupAddress, - account, - ]) - } - }, [ - mutate, - campaignsRefreshIn, - selectedCampaign, - selectedCampaignLeaderboardPageNumber, - selectedCampaignLeaderboardLookupAddress, - account, - ]) - if (campaigns.length === 0 && loadingCampaignData) { return } @@ -409,218 +381,214 @@ export default function Campaign({ refreshListCampaign, ...props }: CampaignProp return ( <> - - - - - - - - - - Campaigns - - - - - - - - - - - - - { - setTimeout(() => { - if (selectedCampaign) - setCampaignDetailMediaLoadedMap(prev => ({ ...prev, [selectedCampaign.id]: true })) - }, 500) - }} - onError={() => { + + + + + + + + + Campaigns + + + + + + + + + + + + + { + setTimeout(() => { if (selectedCampaign) setCampaignDetailMediaLoadedMap(prev => ({ ...prev, [selectedCampaign.id]: true })) - if (campaignDetailImageRef && campaignDetailImageRef.current) { - campaignDetailImageRef.current.style.display = 'none' - } - }} + }, 500) + }} + onError={() => { + if (selectedCampaign) + setCampaignDetailMediaLoadedMap(prev => ({ ...prev, [selectedCampaign.id]: true })) + if (campaignDetailImageRef && campaignDetailImageRef.current) { + campaignDetailImageRef.current.style.display = 'none' + } + }} + /> + + + + {selectedCampaign?.name} + + + + + + + + mixpanelHandler(MIXPANEL_TYPE.CAMPAIGN_SHARE_TRADING_CONTEST_CLICKED, { + campaign_name: selectedCampaign?.name, + }) + } /> - - - - {selectedCampaign?.name} + + + + + + {selectedCampaign?.status === CampaignStatus.UPCOMING + ? t`Starting In` + : isOngoing + ? t`Ending In` + : t`Ended On`} - - - - - - - mixpanelHandler(MIXPANEL_TYPE.CAMPAIGN_SHARE_TRADING_CONTEST_CLICKED, { - campaign_name: selectedCampaign?.name, - }) - } - /> - - - - - - {selectedCampaign?.status === CampaignStatus.UPCOMING - ? t`Starting In` - : isOngoing - ? t`Ending In` - : t`Ended On`} + + {isSelectedCampaignMediaLoaded ? ( + <> + {selectedCampaign.status === CampaignStatus.UPCOMING && ( + + + {selectedCampaign + ? getFormattedTimeFromSecond((selectedCampaign.startTime - now) / 1000) + : '--'} + + + )} + {selectedCampaign.status === CampaignStatus.ONGOING && ( + + + {selectedCampaign ? getFormattedTimeFromSecond((selectedCampaign.endTime - now) / 1000) : '--'} + + + )} + {selectedCampaign.status === CampaignStatus.ENDED && ( + + {dayjs(selectedCampaign.endTime).format('YYYY-MM-DD HH:mm')} + + )} + + ) : ( + + )} + + + + Participants + + {!isMobile && } + {isSelectedCampaignMediaLoaded ? ( + + {selectedCampaignLeaderboard?.totalParticipants + ? formatNumberWithPrecisionRange(selectedCampaignLeaderboard.totalParticipants, 0, 0) + : '--'} - - {isSelectedCampaignMediaLoaded ? ( - <> - {selectedCampaign.status === CampaignStatus.UPCOMING && ( - - - {selectedCampaign - ? getFormattedTimeFromSecond((selectedCampaign.startTime - now) / 1000) - : '--'} - - - )} - {selectedCampaign.status === CampaignStatus.ONGOING && ( - - - {selectedCampaign - ? getFormattedTimeFromSecond((selectedCampaign.endTime - now) / 1000) - : '--'} - - - )} - {selectedCampaign.status === CampaignStatus.ENDED && ( - - {dayjs(selectedCampaign.endTime).format('YYYY-MM-DD HH:mm')} + ) : ( + + )} + + + + Your Rank + {isMobile && } + + {!isMobile && } + {isSelectedCampaignMediaLoaded ? ( + account ? ( + + + + {selectedCampaign?.userInfo?.rankNo + ? formatNumberWithPrecisionRange(selectedCampaign?.userInfo?.rankNo, 0, 2) + : '--'} - )} - - ) : ( - - )} - - - - Participants - - {!isMobile && } - {isSelectedCampaignMediaLoaded ? ( - - {selectedCampaignLeaderboard?.totalParticipants - ? formatNumberWithPrecisionRange(selectedCampaignLeaderboard.totalParticipants, 0, 0) - : '--'} - - ) : ( - - )} - - - - Your Rank - {isMobile && } - - {!isMobile && } - {isSelectedCampaignMediaLoaded ? ( - account ? ( - - - - {selectedCampaign?.userInfo?.rankNo - ? formatNumberWithPrecisionRange(selectedCampaign?.userInfo?.rankNo, 0, 2) - : '--'} - - {!isMobile && } - - - {above768 ? Your Transactions : History} - + {!isMobile && } - ) : ( - - Connect - - ) + + {upToSmall ? Your Transactions : History} + + ) : ( - - )} - - - - - setActiveTab(CampaignTab.HOW_TO_WIN)} - > - How to win - - setActiveTab(CampaignTab.REWARDS)} - > - Rewards - + + Connect + + ) + ) : ( + + )} + + + + + setActiveTab(CampaignTab.HOW_TO_WIN)} + > + How to win + + setActiveTab(CampaignTab.REWARDS)} + > + Rewards + + setActiveTab(CampaignTab.LEADERBOARD)} + > + Leaderboard + + {selectedCampaign && selectedCampaign.campaignState === CampaignState.CampaignStateDistributedRewards && ( setActiveTab(CampaignTab.LEADERBOARD)} + active={activeTab === CampaignTab.LUCKY_WINNER} + onClick={() => setActiveTab(CampaignTab.LUCKY_WINNER)} > - Leaderboard + Lucky Winners - {selectedCampaign && selectedCampaign.campaignState === CampaignState.CampaignStateDistributedRewards && ( - setActiveTab(CampaignTab.LUCKY_WINNER)} - > - Lucky Winners - - )} - + )} + - - {activeTab === CampaignTab.HOW_TO_WIN && } - {activeTab === CampaignTab.REWARDS && } - {activeTab === CampaignTab.LEADERBOARD && ( - - )} - {activeTab === CampaignTab.LUCKY_WINNER && ( - - )} - - - + + {activeTab === CampaignTab.HOW_TO_WIN && } + {activeTab === CampaignTab.REWARDS && } + {activeTab === CampaignTab.LEADERBOARD && ( + + )} + {activeTab === CampaignTab.LUCKY_WINNER && ( + + )} + + @@ -753,7 +721,9 @@ const PageWrapper = styled.div` padding: 32px 24px 50px; width: 100%; max-width: 1500px; - + display: flex; + gap: 24px; + min-height: calc(100vh - 84.34px - 24px - 24px - 62px); ${({ theme }) => theme.mediaWidth.upToSmall` ${css` padding: 24px 16px 100px; @@ -761,13 +731,6 @@ const PageWrapper = styled.div` `} ` -const CampaignContainer = styled.div` - display: flex; - gap: 24px; - min-height: calc(100vh - 84.34px - 24px - 24px - 62px); - overflow: auto; -` - const CampaignDetail = styled.div` flex: 2; overflow: auto; diff --git a/src/pages/Campaign/CampaignListAndSearch.tsx b/src/pages/Campaign/CampaignListAndSearch.tsx index c75ca9c18a..7457b1ccf4 100644 --- a/src/pages/Campaign/CampaignListAndSearch.tsx +++ b/src/pages/Campaign/CampaignListAndSearch.tsx @@ -1,5 +1,5 @@ import { Trans, t } from '@lingui/macro' -import { useCallback, useEffect, useRef, useState } from 'react' +import { memo, useCallback, useEffect, useRef, useState } from 'react' import { useSelector } from 'react-redux' import AutoSizer from 'react-virtualized-auto-sizer' import { VariableSizeList } from 'react-window' @@ -172,4 +172,4 @@ const CampaignListAndSearch = ({ ) } -export default CampaignListAndSearch +export default memo(CampaignListAndSearch) diff --git a/src/pages/Campaign/LeaderboardLayout.tsx b/src/pages/Campaign/LeaderboardLayout.tsx index 80ff77ca21..5e7b669170 100644 --- a/src/pages/Campaign/LeaderboardLayout.tsx +++ b/src/pages/Campaign/LeaderboardLayout.tsx @@ -1,11 +1,12 @@ import { Trans, t } from '@lingui/macro' import dayjs from 'dayjs' import { rgba } from 'polished' -import { useEffect } from 'react' +import { useEffect, useState } from 'react' import { Clock } from 'react-feather' import { useSelector } from 'react-redux' import { useMedia, useSize } from 'react-use' import { Flex, Text } from 'rebass' +import { useGetLuckyWinnersQuery } from 'services/campaign' import styled, { css } from 'styled-components' import Bronze from 'assets/svg/bronze_icon.svg' @@ -14,15 +15,13 @@ import Silver from 'assets/svg/silver_icon.svg' import InfoHelper from 'components/InfoHelper' import Pagination from 'components/Pagination' import Search, { Container as SearchContainer, Wrapper as SearchWrapper } from 'components/Search' -import { BIG_INT_ZERO, CAMPAIGN_LEADERBOARD_ITEM_PER_PAGE, DEFAULT_SIGNIFICANT } from 'constants/index' +import { BIG_INT_ZERO, CAMPAIGN_LEADERBOARD_ITEM_PER_PAGE, DEFAULT_SIGNIFICANT, EMPTY_ARRAY } from 'constants/index' import useTheme from 'hooks/useTheme' import { AppState } from 'state' import { CampaignState, CampaignStatus, RewardRandom } from 'state/campaigns/actions' import { useSelectedCampaignLeaderboardLookupAddressManager, useSelectedCampaignLeaderboardPageNumberManager, - useSelectedCampaignLuckyWinnerPageNumber, - useSelectedCampaignLuckyWinnersLookupAddressManager, } from 'state/campaigns/hooks' import { formatNumberWithPrecisionRange } from 'utils' import getShortenAddress from 'utils/getShortenAddress' @@ -48,35 +47,44 @@ export default function LeaderboardLayout({ )) - const { selectedCampaignLeaderboard, selectedCampaignLuckyWinners, selectedCampaign } = useSelector( - (state: AppState) => state.campaigns, - ) - - const [currentPage, setCurrentPage] = useSelectedCampaignLeaderboardPageNumberManager() - const [currentPageLuckyWinner, setCurrentPageLuckyWinner] = useSelectedCampaignLuckyWinnerPageNumber() + const { selectedCampaignLeaderboard, selectedCampaign } = useSelector((state: AppState) => state.campaigns) const [leaderboardSearchValue, setLeaderboardSearchValue] = useSelectedCampaignLeaderboardLookupAddressManager() - const [luckyWinnersSearchValue, setLuckyWinnersSearchValue] = useSelectedCampaignLuckyWinnersLookupAddressManager() + + const [luckyWinnersSearchValue, setLuckyWinnersSearchValue] = useState('') + const [searchValue, setSearchValue] = type === 'leaderboard' ? [leaderboardSearchValue, setLeaderboardSearchValue] : [luckyWinnersSearchValue, setLuckyWinnersSearchValue] + const [currentPageLuckyWinner, setCurrentPageLuckyWinner] = useState(0) + const { currentData: dataLuckWinners } = useGetLuckyWinnersQuery( + { + pageSize: CAMPAIGN_LEADERBOARD_ITEM_PER_PAGE, + pageNumber: currentPageLuckyWinner, + lookupAddress: luckyWinnersSearchValue, + campaignId: selectedCampaign?.id || 0, + }, + { skip: !selectedCampaign?.id }, + ) + + const luckyWinners = + (selectedCampaign?.campaignState === CampaignState.CampaignStateReady ? EMPTY_ARRAY : dataLuckWinners) || + EMPTY_ARRAY + + const [currentPage, setCurrentPage] = useSelectedCampaignLeaderboardPageNumberManager() let totalItems = 0 - if (type === 'leaderboard') { - if (selectedCampaignLeaderboard) { - totalItems = leaderboardSearchValue ? 1 : selectedCampaignLeaderboard.totalParticipants - } + if (type === 'leaderboard' && selectedCampaignLeaderboard) { + totalItems = leaderboardSearchValue ? 1 : selectedCampaignLeaderboard.totalParticipants } - if (type === 'lucky_winner') { - if (selectedCampaign && selectedCampaignLeaderboard) { - const randomRewards = selectedCampaign.rewardDistribution.filter(reward => reward.type === 'Random') - const totalRandomRewardItems = randomRewards.reduce( - (acc, reward) => acc + ((reward as RewardRandom).nWinners ?? 0), - 0, - ) - - totalItems = searchValue ? 1 : Math.min(totalRandomRewardItems, selectedCampaignLeaderboard.totalParticipants) - } + if (type === 'lucky_winner' && selectedCampaign && selectedCampaignLeaderboard) { + const randomRewards = selectedCampaign.rewardDistribution.filter(reward => reward.type === 'Random') + const totalRandomRewardItems = randomRewards.reduce( + (acc, reward) => acc + ((reward as RewardRandom).nWinners ?? 0), + 0, + ) + + totalItems = searchValue ? 1 : Math.min(totalRandomRewardItems, selectedCampaignLeaderboard.totalParticipants) } const refreshInMinute = Math.floor(refreshIn / 60) @@ -143,7 +151,7 @@ export default function LeaderboardLayout({ ) }) - const luckyWinnersTableBody = selectedCampaignLuckyWinners.map((luckyWinner, index) => { + const luckyWinnersTableBody = luckyWinners.map((luckyWinner, index) => { return ( diff --git a/src/pages/Campaign/ModalRegisterCampaignCaptcha.tsx b/src/pages/Campaign/ModalRegisterCampaignCaptcha.tsx index 49a28fef71..5c1cb04d74 100644 --- a/src/pages/Campaign/ModalRegisterCampaignCaptcha.tsx +++ b/src/pages/Campaign/ModalRegisterCampaignCaptcha.tsx @@ -1,13 +1,12 @@ import { Trans } from '@lingui/macro' -import axios from 'axios' import { createRef, memo, useCallback } from 'react' import ReCAPTCHA from 'react-google-recaptcha' import { Text } from 'rebass' +import { useJoinCampaignMutation } from 'services/campaign' import styled from 'styled-components' import { ModalCenter } from 'components/Modal' import { GOOGLE_RECAPTCHA_KEY } from 'constants/env' -import { CAMPAIGN_BASE_URL } from 'constants/index' import { useActiveWeb3React } from 'hooks' import useTheme from 'hooks/useTheme' import { ApplicationModal } from 'state/application/actions' @@ -44,7 +43,7 @@ const ModalRegisterCampaignCaptcha = ({ refreshListCampaign }: { refreshListCamp const theme = useTheme() const { account } = useActiveWeb3React() - + const [joinCampaign] = useJoinCampaignMutation() // Create an event handler, so you can call the verification on button click event or form submit const handleReCaptchaVerify = useCallback(async () => { if (!recaptchaCampaign.id || !account) return @@ -64,18 +63,13 @@ const ModalRegisterCampaignCaptcha = ({ refreshListCampaign }: { refreshListCamp const token = await recaptchaRef.current.getValue() await new Promise(r => setTimeout(r, 750)) toggleRegisterCampaignCaptchaModal() - const response = await axios({ - method: 'POST', - url: `${CAMPAIGN_BASE_URL}/${recaptchaCampaign.id}/participants`, - data: { - token, - address: account, - }, - }) - if (response.status === 200) { - refreshListCampaign() - toggleRegisterCampaignSuccessModal() - } + await joinCampaign({ + token, + address: account, + recaptchaId: recaptchaCampaign.id, + }).unwrap() + refreshListCampaign() + toggleRegisterCampaignSuccessModal() } catch (err) { console.error(err) } finally { @@ -91,6 +85,7 @@ const ModalRegisterCampaignCaptcha = ({ refreshListCampaign }: { refreshListCamp updateRecaptchaCampaignId, updateRecaptchaCampaignLoading, refreshListCampaign, + joinCampaign, ]) return ( diff --git a/src/pages/Campaign/ModalSelectCampaign.tsx b/src/pages/Campaign/ModalSelectCampaign.tsx index 6d5d8cb6f0..52510faf5e 100644 --- a/src/pages/Campaign/ModalSelectCampaign.tsx +++ b/src/pages/Campaign/ModalSelectCampaign.tsx @@ -27,7 +27,7 @@ const ModalSelectCampaign = (props: { }, 200) } return ( - +
diff --git a/src/pages/Campaign/index.tsx b/src/pages/Campaign/index.tsx index 77057cdc1f..7f073f2521 100644 --- a/src/pages/Campaign/index.tsx +++ b/src/pages/Campaign/index.tsx @@ -1,113 +1,28 @@ -import { ZERO } from '@kyberswap/ks-sdk-classic' -import { Fraction } from '@kyberswap/ks-sdk-core' -import axios from 'axios' -import { parseUnits } from 'ethers/lib/utils' -import JSBI from 'jsbi' -import { stringify } from 'querystring' import { useCallback, useEffect, useState } from 'react' import { useDispatch, useSelector } from 'react-redux' import { useLocation, useNavigate } from 'react-router-dom' -import useSWR, { mutate } from 'swr' -import useSWRImmutable from 'swr/immutable' +import { useGetCampaignsQuery, useGetLeaderboardQuery } from 'services/campaign' -import { APP_PATHS, CAMPAIGN_LEADERBOARD_ITEM_PER_PAGE, RESERVE_USD_DECIMALS, SWR_KEYS } from 'constants/index' +import { APP_PATHS, CAMPAIGN_LEADERBOARD_ITEM_PER_PAGE, EMPTY_ARRAY } from 'constants/index' import { useActiveWeb3React } from 'hooks' import useParsedQueryString from 'hooks/useParsedQueryString' import { - CampaignData, CampaignLeaderboard, - CampaignLeaderboardRanking, - CampaignLeaderboardReward, - CampaignLuckyWinner, - CampaignState, - CampaignStatus, - RewardDistribution, setCampaignDataByPage, setLastTimeRefreshData, setLoadingCampaignData, setLoadingCampaignDataError, setLoadingSelectedCampaignLeaderboard, - setLoadingSelectedCampaignLuckyWinners, setSelectedCampaign, setSelectedCampaignLeaderboard, - setSelectedCampaignLuckyWinners, } from 'state/campaigns/actions' import { AppState } from 'state/index' -import { SerializedToken } from 'state/user/actions' import { getCampaignIdFromSlug, getSlugUrlCampaign } from 'utils/campaign' -import CampaignContent from './CampaignContent' +import CampaignContent, { MINUTE_TO_REFRESH } from './CampaignContent' const MAXIMUM_ITEMS_PER_REQUEST = 10 -const getCampaignStatus = ({ endTime, startTime }: CampaignData) => { - const now = Date.now() - return endTime <= now ? CampaignStatus.ENDED : startTime >= now ? CampaignStatus.UPCOMING : CampaignStatus.ONGOING -} - -const formatRewards = (rewards: CampaignLeaderboardReward[]) => - rewards?.map( - (item: any): CampaignLeaderboardReward => ({ - rewardAmount: new Fraction( - item.RewardAmount, - JSBI.exponentiate(JSBI.BigInt(10), JSBI.BigInt(item?.Token?.decimals ?? 18)), - ), - ref: item.Ref, - claimed: item.Claimed, - token: item.Token, - }), - ) || [] - -const formatLeaderboardData = (data: CampaignLeaderboard) => { - const leaderboard: CampaignLeaderboard = { - ...data, - rankings: data.rankings - ? data.rankings.map( - (item: any): CampaignLeaderboardRanking => ({ - userAddress: item.userAddress, - totalPoint: item.totalPoint, - rankNo: item.rankNo, - rewardAmount: new Fraction( - item.rewardAmount || ZERO, - JSBI.exponentiate(JSBI.BigInt(10), JSBI.BigInt(item?.token?.decimals ?? 18)), - ), - rewardAmountUsd: new Fraction( - parseUnits(item?.rewardAmountUSD?.toString() || '0', RESERVE_USD_DECIMALS).toString(), - JSBI.exponentiate(JSBI.BigInt(10), JSBI.BigInt(RESERVE_USD_DECIMALS)), - ), - rewardInUSD: item.rewardInUSD, - token: item.token, - }), - ) - : [], - rewards: formatRewards(data.rewards), - } - return leaderboard -} - -const fetchLeaderBoard = ({ - pageNumber, - userAddress, - lookupAddress, - campaignId, -}: { - pageNumber: number - userAddress: string - lookupAddress: string - campaignId: number -}) => { - return axios({ - method: 'GET', - url: SWR_KEYS.getLeaderboard(campaignId), - params: { - pageSize: CAMPAIGN_LEADERBOARD_ITEM_PER_PAGE, - pageNumber, - userAddress, - lookupAddress, - }, - }).then(({ data }) => formatLeaderboardData(data.data)) -} - const LEADERBOARD_DEFAULT: CampaignLeaderboard = { finalizedAt: 0, distributedRewardsAt: 0, @@ -117,115 +32,6 @@ const LEADERBOARD_DEFAULT: CampaignLeaderboard = { rewards: [], } -const formatListCampaign = (response: CampaignData[]) => { - const campaigns: CampaignData[] = response.map((item: CampaignData) => ({ - ...item, - startTime: item.startTime * 1000, - endTime: item.endTime * 1000, - })) - const formattedCampaigns: CampaignData[] = campaigns.map((campaign: any) => { - const rewardDistribution: RewardDistribution[] = [] - if (campaign.rewardDistribution.single) { - campaign.rewardDistribution.single.forEach( - ({ - amount, - rank, - token, - rewardInUSD, - }: { - amount: string - rank: number - token: SerializedToken - rewardInUSD: boolean - }) => { - rewardDistribution.push({ - type: 'Single', - amount, - rank, - token, - rewardInUSD, - }) - }, - ) - } - if (campaign.rewardDistribution.range) { - campaign.rewardDistribution.range.forEach( - ({ - from, - to, - amount, - token, - rewardInUSD, - }: { - from: number - to: number - amount: string - token: SerializedToken - rewardInUSD: boolean - }) => { - rewardDistribution.push({ - type: 'Range', - from, - to, - amount, - token, - rewardInUSD, - }) - }, - ) - } - if (campaign.rewardDistribution.random) { - campaign.rewardDistribution.random.forEach( - ({ - from, - to, - amount, - numberOfWinners, - token, - rewardInUSD, - }: { - from: number - to: number - amount: string - numberOfWinners: number - token: SerializedToken - rewardInUSD: boolean - }) => { - rewardDistribution.push({ - type: 'Random', - from, - to, - amount, - nWinners: numberOfWinners, - token, - rewardInUSD, - }) - }, - ) - } - if (campaign?.userInfo?.tradingVolume) campaign.userInfo.tradingVolume = Number(campaign.userInfo.tradingVolume) - if (campaign.userInfo) campaign.userInfo.rewards = formatRewards(campaign.userInfo.rewards) - return { - ...campaign, - rewardDistribution, - status: getCampaignStatus(campaign), - eligibleTokens: campaign.eligibleTokens.map( - ({ chainId, name, symbol, address, logoURI, decimals }: SerializedToken) => { - return { - chainId, - name, - symbol, - address, - logoURI, - decimals, - } - }, - ), - } - }) - return formattedCampaigns -} - const getQueryDefault = (userAddress: string | undefined) => ({ campaignName: '', userAddress, @@ -247,7 +53,6 @@ export default function CampaignsUpdater() { }, []) const { data: currentCampaigns } = useSelector((state: AppState) => state.campaigns) - const getCampaignUrl = useCallback(() => `${SWR_KEYS.getListCampaign}?${stringify(queryParams)}`, [queryParams]) const loadMoreCampaign = useCallback(() => { if (!currentCampaigns.length) return @@ -259,24 +64,22 @@ export default function CampaignsUpdater() { }, [account, setQueryParams]) const { - data: campaignData, - isValidating: isLoadingCampaignData, + data: campaignData = EMPTY_ARRAY, + isFetching: isLoadingCampaignData, error: loadingCampaignDataError, - } = useSWR(getCampaignUrl(), async url => { - try { - const { data: response } = await axios.get(url) - const campaigns: CampaignData[] = response.data - setHasMoreCampaign(campaigns.length === MAXIMUM_ITEMS_PER_REQUEST) - return formatListCampaign(campaigns) - } catch (error) { - return [] - } - }) + refetch, + } = useGetCampaignsQuery(queryParams) + + useEffect(() => { + setHasMoreCampaign(campaignData.length === MAXIMUM_ITEMS_PER_REQUEST) + }, [campaignData]) const refreshListCampaign = useCallback(async () => { - await mutate(getCampaignUrl()) - dispatch(setLastTimeRefreshData()) - }, [getCampaignUrl, dispatch]) + try { + await refetch().unwrap() + dispatch(setLastTimeRefreshData()) + } catch (error) {} + }, [refetch, dispatch]) const slug = pathname.replace(APP_PATHS.CAMPAIGN, '') const { selectedCampaignId = getCampaignIdFromSlug(slug) } = useParsedQueryString<{ selectedCampaignId: string }>() @@ -294,7 +97,7 @@ export default function CampaignsUpdater() { replace: true, }) } - if (selectedCampaignId === undefined) { + if (!selectedCampaignId) { navigateFirsOne() return } @@ -311,7 +114,7 @@ export default function CampaignsUpdater() { }, [dispatch, isLoadingCampaignData]) useEffect(() => { - dispatch(setLoadingCampaignDataError(loadingCampaignDataError)) + dispatch(setLoadingCampaignDataError(!!loadingCampaignDataError)) }, [dispatch, loadingCampaignDataError]) /**********************CAMPAIGN LEADERBOARD**********************/ @@ -319,33 +122,15 @@ export default function CampaignsUpdater() { const { selectedCampaignLeaderboardPageNumber, selectedCampaignLeaderboardLookupAddress, selectedCampaign } = useSelector((state: AppState) => state.campaigns) - const { data: leaderboard, isValidating: isLoadingLeaderboard } = useSWRImmutable( - selectedCampaign - ? [ - selectedCampaign, - SWR_KEYS.getLeaderboard(selectedCampaign.id), - selectedCampaignLeaderboardPageNumber, - selectedCampaignLeaderboardLookupAddress, - account, - ] - : null, - async () => { - if (!selectedCampaign) { - return LEADERBOARD_DEFAULT - } - - try { - return fetchLeaderBoard({ - campaignId: selectedCampaign.id, - pageNumber: selectedCampaignLeaderboardPageNumber, - userAddress: account ?? '', - lookupAddress: selectedCampaignLeaderboardLookupAddress, - }) - } catch (err) { - console.error(err) - return LEADERBOARD_DEFAULT - } + const { currentData: leaderboard = LEADERBOARD_DEFAULT, isFetching: isLoadingLeaderboard } = useGetLeaderboardQuery( + { + campaignId: selectedCampaign?.id || 0, + pageNumber: selectedCampaignLeaderboardPageNumber, + userAddress: account ?? '', + lookupAddress: selectedCampaignLeaderboardLookupAddress, + pageSize: CAMPAIGN_LEADERBOARD_ITEM_PER_PAGE, }, + { skip: !selectedCampaign?.id, pollingInterval: MINUTE_TO_REFRESH * 1000 }, ) useEffect(() => { @@ -358,63 +143,6 @@ export default function CampaignsUpdater() { dispatch(setLoadingSelectedCampaignLeaderboard(isLoadingLeaderboard)) }, [dispatch, isLoadingLeaderboard]) - /**********************CAMPAIGN LUCKY WINNERS**********************/ - - const { selectedCampaignLuckyWinnersPageNumber, selectedCampaignLuckyWinnersLookupAddress } = useSelector( - (state: AppState) => state.campaigns, - ) - - const { data: luckyWinners, isValidating: isLoadingLuckyWinners } = useSWRImmutable( - selectedCampaign - ? [ - selectedCampaign, - SWR_KEYS.getLuckyWinners(selectedCampaign.id), - selectedCampaignLuckyWinnersPageNumber, - selectedCampaignLuckyWinnersLookupAddress, - ] - : null, - async () => { - if (!selectedCampaign || selectedCampaign.campaignState === CampaignState.CampaignStateReady) return [] - - try { - const response = await axios({ - method: 'GET', - url: SWR_KEYS.getLuckyWinners(selectedCampaign.id), - params: { - pageSize: CAMPAIGN_LEADERBOARD_ITEM_PER_PAGE, - pageNumber: selectedCampaignLuckyWinnersPageNumber, - lookupAddress: selectedCampaignLuckyWinnersLookupAddress, - }, - }) - const data = response.data.data - const luckyWinners: CampaignLuckyWinner[] = data.map( - (item: any): CampaignLuckyWinner => ({ - userAddress: item.userAddress, - rewardAmount: new Fraction( - item.rewardAmount, - JSBI.exponentiate(JSBI.BigInt(10), JSBI.BigInt(item?.token?.decimals ?? 18)), - ), - token: item.token, - }), - ) - return luckyWinners - } catch (err) { - console.error(err) - return [] - } - }, - ) - - useEffect(() => { - if (luckyWinners !== undefined) { - dispatch(setSelectedCampaignLuckyWinners({ luckyWinners: luckyWinners })) - } - }, [dispatch, luckyWinners]) - - useEffect(() => { - dispatch(setLoadingSelectedCampaignLuckyWinners(isLoadingLuckyWinners)) - }, [dispatch, isLoadingLuckyWinners]) - return ( These Terms and Conditions should be read in conjunction with the KyberSwap{' '} - Terms of Use, which lay out the terms and conditions - that apply to all KyberSwap activities. + Terms of Use, which lay out + the terms and conditions that apply to all KyberSwap activities. @@ -290,7 +290,7 @@ export default function KNCUtility() { By visiting KyberSwap and participating in the program, the User is deemed to have read, understood, and agreed to these Terms and Conditions and the KyberSwap{' '} - Terms of Use. + Terms of Use. @@ -308,9 +308,10 @@ export default function KNCUtility() { KyberSwap maintains the right, at its sole discretion, to take action or remove rewards against - the User who violates the KyberSwap Terms of Use{' '} - and/or violates, cheats, or exploits the program, including but not limited to, any suspicious - activities, or any attempts to circumvent these Terms and Conditions. + the User who violates the KyberSwap{' '} + Terms of Use and/or violates, + cheats, or exploits the program, including but not limited to, any suspicious activities, or any + attempts to circumvent these Terms and Conditions. diff --git a/src/pages/KyberDAO/Vote/ProposalItem/VoteInformation.tsx b/src/pages/KyberDAO/Vote/ProposalItem/VoteInformation.tsx index 4611e32bb1..02181dc345 100644 --- a/src/pages/KyberDAO/Vote/ProposalItem/VoteInformation.tsx +++ b/src/pages/KyberDAO/Vote/ProposalItem/VoteInformation.tsx @@ -1,4 +1,4 @@ -import { Trans } from '@lingui/macro' +import { Trans, t } from '@lingui/macro' import dayjs from 'dayjs' import { useMemo } from 'react' import { Text } from 'rebass' @@ -74,6 +74,12 @@ export default function VoteInformation({ proposal }: { proposal: ProposalDetail {Math.floor(proposal.vote_stats.total_vote_count).toLocaleString()} + + + Quorum Status + + {proposal.vote_stats.quorum_status === 1 ? t`Reached` : t`Not Reached`} + Your KIP Voting Power{' '} diff --git a/src/pages/KyberDAO/Vote/index.tsx b/src/pages/KyberDAO/Vote/index.tsx index c0c671a9fd..48cf4090b6 100644 --- a/src/pages/KyberDAO/Vote/index.tsx +++ b/src/pages/KyberDAO/Vote/index.tsx @@ -117,7 +117,7 @@ export default function Vote() { claimedRewardAmount, stakerInfo, stakerInfoNextEpoch, - rewardStats: { knc, usd }, + rewardStats: { knc, usd, apr }, } = useVotingInfo() const kncPrice = useKNCPrice() @@ -237,12 +237,23 @@ export default function Vote() { - - Total Voting Rewards - - - {(+knc?.toFixed(0)).toLocaleString() ?? '--'} KNC - + + + Total Voting Rewards + + + APR + + + + + {(+knc?.toFixed(0)).toLocaleString() ?? '--'} KNC + + + {apr.toFixed(2) ?? '--'}% + + + ~{(+usd?.toFixed(0)).toLocaleString() ?? '--'} USD diff --git a/src/pages/NotificationCenter/PrivateAnnouncement.tsx b/src/pages/NotificationCenter/PrivateAnnouncement.tsx index 92e0f9c352..3fab236c2e 100644 --- a/src/pages/NotificationCenter/PrivateAnnouncement.tsx +++ b/src/pages/NotificationCenter/PrivateAnnouncement.tsx @@ -11,10 +11,10 @@ import styled from 'styled-components' import { PRIVATE_ANN_TITLE } from 'components/Announcement/PrivateAnnoucement' import InboxItemNotificationCenter from 'components/Announcement/PrivateAnnoucement/NotificationCenter' -import { useInvalidateTagAnnouncement } from 'components/Announcement/helper' import { PrivateAnnouncement, PrivateAnnouncementType } from 'components/Announcement/type' import { getAnnouncementsTemplateIds } from 'constants/env' import { useActiveWeb3React } from 'hooks' +import { useInvalidateTagAnnouncement } from 'hooks/useInvalidateTags' import DeleteAllAlertsButton from 'pages/NotificationCenter/DeleteAllAlertsButton' import NoData from 'pages/NotificationCenter/NoData' import CommonPagination from 'pages/NotificationCenter/PriceAlerts/CommonPagination' diff --git a/src/pages/SwapV3/Tabs/LimitTab.tsx b/src/pages/SwapV3/Tabs/LimitTab.tsx index c46bd4b04c..59fe4c6cce 100644 --- a/src/pages/SwapV3/Tabs/LimitTab.tsx +++ b/src/pages/SwapV3/Tabs/LimitTab.tsx @@ -1,7 +1,6 @@ import { Trans } from '@lingui/macro' import { rgba } from 'polished' import { useLocation } from 'react-router-dom' -import { Text } from 'rebass' import { useGetNumberOfInsufficientFundOrdersQuery } from 'services/limitOrder' import styled from 'styled-components' @@ -45,22 +44,24 @@ export default function LimitTab({ onClick }: Props) { } return ( - - - Limit{' '} - {numberOfInsufficientFundOrders ? ( - - You have {numberOfInsufficientFundOrders} active orders that don't have sufficient funds - - } - > - {numberOfInsufficientFundOrders} - - ) : null} - + + Limit{' '} + {!!numberOfInsufficientFundOrders && ( + You have {numberOfInsufficientFundOrders} active orders that don't have sufficient funds + } + > + {numberOfInsufficientFundOrders} + + )} ) } diff --git a/src/pages/TrueSightV2/components/TokenFilter/WatchlistSelect.tsx b/src/pages/TrueSightV2/components/TokenFilter/WatchlistSelect.tsx index 769c363bb6..f2d040cef9 100644 --- a/src/pages/TrueSightV2/components/TokenFilter/WatchlistSelect.tsx +++ b/src/pages/TrueSightV2/components/TokenFilter/WatchlistSelect.tsx @@ -16,6 +16,7 @@ const Divider = styled.div` ` const CustomOption = styled(Row)` + cursor: pointer; :hover { background-color: ${({ theme }) => theme.background}; } diff --git a/src/pages/TrueSightV2/pages/RegisterWhitelist/index.tsx b/src/pages/TrueSightV2/pages/RegisterWhitelist/index.tsx index d294de62c9..e108f57116 100644 --- a/src/pages/TrueSightV2/pages/RegisterWhitelist/index.tsx +++ b/src/pages/TrueSightV2/pages/RegisterWhitelist/index.tsx @@ -10,7 +10,6 @@ import Column from 'components/Column' import DownloadWalletModal from 'components/DownloadWalletModal' import Row from 'components/Row' import { APP_PATHS } from 'constants/index' -import { useActiveWeb3React } from 'hooks' import { MIXPANEL_TYPE, useMixpanelKyberAI } from 'hooks/useMixpanel' import useTheme from 'hooks/useTheme' import SignInForm from 'pages/TrueSightV2/pages/RegisterWhitelist/SignInForm' @@ -18,7 +17,7 @@ import SubscribeForm from 'pages/TrueSightV2/pages/RegisterWhitelist/SubscribeFo import WaitListForm from 'pages/TrueSightV2/pages/RegisterWhitelist/WaitListForm' import VerifyCodeModal from 'pages/Verify/VerifyCodeModal' import { ApplicationModal } from 'state/application/actions' -import { useOpenModal, useWalletModalToggle } from 'state/application/hooks' +import { useOpenModal } from 'state/application/hooks' import { useSessionInfo } from 'state/authen/hooks' import { useIsWhiteListKyberAI } from 'state/user/hooks' import { ButtonText } from 'theme' @@ -32,8 +31,6 @@ export default function RegisterWhitelist({ showForm = true }: { showForm?: bool const navigate = useNavigate() const theme = useTheme() const mixpanelHandler = useMixpanelKyberAI() - const { account } = useActiveWeb3React() - const toggleWalletModal = useWalletModalToggle() const { isLogin } = useSessionInfo() const { isWhiteList, isWaitList, loading: isCheckingPermission } = useIsWhiteListKyberAI() @@ -66,7 +63,7 @@ export default function RegisterWhitelist({ showForm = true }: { showForm?: bool <> @@ -98,11 +95,11 @@ export default function RegisterWhitelist({ showForm = true }: { showForm?: bool ) - if (!account) + if (!isLogin) return ( - - Sign in with wallet + signIn()}> + Sign-In to Continue Don't have a wallet? diff --git a/src/services/campaign.ts b/src/services/campaign.ts new file mode 100644 index 0000000000..e7440c4ffa --- /dev/null +++ b/src/services/campaign.ts @@ -0,0 +1,260 @@ +import { ZERO } from '@kyberswap/ks-sdk-classic' +import { Fraction } from '@kyberswap/ks-sdk-core' +import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react' +import { parseUnits } from 'ethers/lib/utils' +import JSBI from 'jsbi' + +import { CAMPAIGN_BASE_URL } from 'constants/env' +import { RESERVE_USD_DECIMALS } from 'constants/index' +import { + CampaignData, + CampaignLeaderboard, + CampaignLeaderboardRanking, + CampaignLeaderboardReward, + CampaignLuckyWinner, + CampaignProofData, + CampaignStatus, + RewardDistribution, +} from 'state/campaigns/actions' +import { SerializedToken } from 'state/user/actions' + +const getCampaignStatus = ({ endTime, startTime }: CampaignData) => { + const now = Date.now() + return endTime <= now ? CampaignStatus.ENDED : startTime >= now ? CampaignStatus.UPCOMING : CampaignStatus.ONGOING +} + +const formatRewards = (rewards: CampaignLeaderboardReward[]) => + rewards?.map( + (item: any): CampaignLeaderboardReward => ({ + rewardAmount: new Fraction( + item.RewardAmount, + JSBI.exponentiate(JSBI.BigInt(10), JSBI.BigInt(item?.Token?.decimals ?? 18)), + ), + ref: item.Ref, + claimed: item.Claimed, + token: item.Token, + }), + ) || [] + +const formatListCampaign = (response: CampaignData[]) => { + const campaigns: CampaignData[] = response.map((item: CampaignData) => ({ + ...item, + startTime: item.startTime * 1000, + endTime: item.endTime * 1000, + })) + const formattedCampaigns: CampaignData[] = campaigns.map((campaign: any) => { + const rewardDistribution: RewardDistribution[] = [] + if (campaign.rewardDistribution.single) { + campaign.rewardDistribution.single.forEach( + ({ + amount, + rank, + token, + rewardInUSD, + }: { + amount: string + rank: number + token: SerializedToken + rewardInUSD: boolean + }) => { + rewardDistribution.push({ + type: 'Single', + amount, + rank, + token, + rewardInUSD, + }) + }, + ) + } + if (campaign.rewardDistribution.range) { + campaign.rewardDistribution.range.forEach( + ({ + from, + to, + amount, + token, + rewardInUSD, + }: { + from: number + to: number + amount: string + token: SerializedToken + rewardInUSD: boolean + }) => { + rewardDistribution.push({ + type: 'Range', + from, + to, + amount, + token, + rewardInUSD, + }) + }, + ) + } + if (campaign.rewardDistribution.random) { + campaign.rewardDistribution.random.forEach( + ({ + from, + to, + amount, + numberOfWinners, + token, + rewardInUSD, + }: { + from: number + to: number + amount: string + numberOfWinners: number + token: SerializedToken + rewardInUSD: boolean + }) => { + rewardDistribution.push({ + type: 'Random', + from, + to, + amount, + nWinners: numberOfWinners, + token, + rewardInUSD, + }) + }, + ) + } + if (campaign?.userInfo?.tradingVolume) campaign.userInfo.tradingVolume = Number(campaign.userInfo.tradingVolume) + if (campaign.userInfo) campaign.userInfo.rewards = formatRewards(campaign.userInfo.rewards) + return { + ...campaign, + rewardDistribution, + status: getCampaignStatus(campaign), + eligibleTokens: campaign.eligibleTokens.map( + ({ chainId, name, symbol, address, logoURI, decimals }: SerializedToken) => { + return { + chainId, + name, + symbol, + address, + logoURI, + decimals, + } + }, + ), + } + }) + return formattedCampaigns +} + +const formatLeaderboardData = (data: CampaignLeaderboard) => { + const leaderboard: CampaignLeaderboard = { + ...data, + rankings: data.rankings + ? data.rankings.map( + (item: any): CampaignLeaderboardRanking => ({ + userAddress: item.userAddress, + totalPoint: item.totalPoint, + rankNo: item.rankNo, + rewardAmount: new Fraction( + item.rewardAmount || ZERO, + JSBI.exponentiate(JSBI.BigInt(10), JSBI.BigInt(item?.token?.decimals ?? 18)), + ), + rewardAmountUsd: new Fraction( + parseUnits(item?.rewardAmountUSD?.toString() || '0', RESERVE_USD_DECIMALS).toString(), + JSBI.exponentiate(JSBI.BigInt(10), JSBI.BigInt(RESERVE_USD_DECIMALS)), + ), + rewardInUSD: item.rewardInUSD, + token: item.token, + }), + ) + : [], + rewards: formatRewards(data.rewards), + } + return leaderboard +} + +const formatLuckyWinners = (data: any[]) => { + const luckyWinners: CampaignLuckyWinner[] = data.map( + (item: any): CampaignLuckyWinner => ({ + userAddress: item.userAddress, + rewardAmount: new Fraction( + item.rewardAmount, + JSBI.exponentiate(JSBI.BigInt(10), JSBI.BigInt(item?.token?.decimals ?? 18)), + ), + token: item.token, + }), + ) + return luckyWinners +} + +const formatTxs = (data: any[]) => { + return data.map( + (item: any): CampaignProofData => ({ + id: item.id, + chainId: parseInt(item.chainId), + utcTimestamp: new Date(item.time).getTime(), + txPoint: item.txPoint, + txHash: item.tx, + }), + ) +} + +const campaignApi = createApi({ + reducerPath: 'campaignApi', + baseQuery: fetchBaseQuery({ baseUrl: `${CAMPAIGN_BASE_URL}/api/v1/campaigns` }), + endpoints: builder => ({ + getCampaigns: builder.query({ + query: params => ({ + params, + url: '', + }), + transformResponse: (data: any) => formatListCampaign(data?.data || []), + }), + getLeaderboard: builder.query< + any, + { pageSize: number; pageNumber: number; userAddress: string; lookupAddress: string; campaignId: number } + >({ + query: ({ campaignId, ...params }) => ({ + params, + url: `/${campaignId}/leaderboard`, + }), + transformResponse: (data: any) => formatLeaderboardData(data?.data), + }), + getLuckyWinners: builder.query< + CampaignLuckyWinner[], + { pageSize: number; pageNumber: number; lookupAddress: string; campaignId: number } + >({ + query: ({ campaignId, ...params }) => ({ + params, + url: `/${campaignId}/lucky-winners`, + }), + transformResponse: (data: any) => formatLuckyWinners(data?.data || []), + }), + getTxsCampaign: builder.query< + CampaignProofData[], + { limit: number; offset: number; userAddress: string; campaignId: number } + >({ + query: ({ campaignId, ...params }) => ({ + params, + url: `/${campaignId}/proofs`, + }), + transformResponse: (data: any) => formatTxs(data?.data || []), + }), + joinCampaign: builder.mutation({ + query: ({ recaptchaId, ...body }) => ({ + body, + method: 'POST', + url: `/${recaptchaId}/participants`, + }), + }), + }), +}) + +export const { + useGetCampaignsQuery, + useGetLeaderboardQuery, + useGetLuckyWinnersQuery, + useJoinCampaignMutation, + useGetTxsCampaignQuery, +} = campaignApi + +export default campaignApi diff --git a/src/state/campaigns/actions.ts b/src/state/campaigns/actions.ts index d8c3013db9..79e77b3f85 100644 --- a/src/state/campaigns/actions.ts +++ b/src/state/campaigns/actions.ts @@ -145,7 +145,7 @@ export const setLoadingCampaignData = createAction('campaigns/setLoadin export const setLastTimeRefreshData = createAction('campaigns/setLastTimeRefreshData') -export const setLoadingCampaignDataError = createAction('campaigns/setLoadingCampaignDataError') +export const setLoadingCampaignDataError = createAction('campaigns/setLoadingCampaignDataError') export const setSelectedCampaign = createAction<{ campaign: CampaignData }>('campaigns/setSelectedCampaign') @@ -164,18 +164,5 @@ export const setSelectedCampaignLeaderboardLookupAddress = createAction( export const setClaimingCampaignRewardId = createAction('campaigns/setClaimingCampaignRewardId') -export const setSelectedCampaignLuckyWinners = createAction<{ luckyWinners: CampaignLuckyWinner[] }>( - 'campaigns/setSelectedCampaignLuckyWinners', -) -export const setLoadingSelectedCampaignLuckyWinners = createAction( - 'campaigns/setLoadingSelectedCampaignLuckyWinners', -) -export const setSelectedCampaignLuckyWinnersPageNumber = createAction( - 'campaigns/setSelectedCampaignLuckyWinnersPageNumber', -) -export const setSelectedCampaignLuckyWinnersLookupAddress = createAction( - 'campaigns/setSelectedCampaignLuckyWinnersLookupAddress', -) - export const setRecaptchaCampaignId = createAction('campaigns/setRecaptchaCampaignId') export const setRecaptchaCampaignLoading = createAction('campaigns/setRecaptchaCampaignLoading') diff --git a/src/state/campaigns/hooks.ts b/src/state/campaigns/hooks.ts index b38df444a7..2151d9f883 100644 --- a/src/state/campaigns/hooks.ts +++ b/src/state/campaigns/hooks.ts @@ -10,8 +10,6 @@ import { setRecaptchaCampaignLoading, setSelectedCampaignLeaderboardLookupAddress, setSelectedCampaignLeaderboardPageNumber, - setSelectedCampaignLuckyWinnersLookupAddress, - setSelectedCampaignLuckyWinnersPageNumber, } from 'state/campaigns/actions' import { AppState } from 'state/index' @@ -31,20 +29,6 @@ export function useSelectedCampaignLeaderboardPageNumberManager(): [number, (pag return [selectedCampaignLeaderboardPageNumber, updateSelectedCampaignLeaderboardPageNumberCallback] } -export function useSelectedCampaignLuckyWinnerPageNumber(): [number, (page: number) => void] { - const page = useSelector((state: AppState) => state.campaigns.selectedCampaignLuckyWinnersPageNumber) - const dispatch = useDispatch() - - const setPage = useCallback( - (newPageNumber: number) => { - dispatch(setSelectedCampaignLuckyWinnersPageNumber(newPageNumber)) - }, - [dispatch], - ) - - return [page, setPage] -} - export function useSelectedCampaignLeaderboardLookupAddressManager() { const selectedCampaignLeaderboardLookupAddress = useSelector( (state: AppState) => state.campaigns.selectedCampaignLeaderboardLookupAddress, @@ -64,25 +48,6 @@ export function useSelectedCampaignLeaderboardLookupAddressManager() { ) } -export function useSelectedCampaignLuckyWinnersLookupAddressManager() { - const selectedCampaignLuckyWinnersLookupAddress = useSelector( - (state: AppState) => state.campaigns.selectedCampaignLuckyWinnersLookupAddress, - ) - const dispatch = useDispatch() - - const updateSelectedCampaignLuckyWinnersLookupAddressCallback = useCallback( - (newLookupAddress: string) => { - dispatch(setSelectedCampaignLuckyWinnersLookupAddress(newLookupAddress)) - }, - [dispatch], - ) - - return useMemo( - () => [selectedCampaignLuckyWinnersLookupAddress, updateSelectedCampaignLuckyWinnersLookupAddressCallback] as const, - [selectedCampaignLuckyWinnersLookupAddress, updateSelectedCampaignLuckyWinnersLookupAddressCallback], - ) -} - export function useRecaptchaCampaignManager() { const recaptchaCampaign = useSelector((state: AppState) => state.campaigns.recaptchaCampaign) const dispatch = useDispatch() diff --git a/src/state/campaigns/reducer.ts b/src/state/campaigns/reducer.ts index 35c921bb91..e9d12693dc 100644 --- a/src/state/campaigns/reducer.ts +++ b/src/state/campaigns/reducer.ts @@ -3,7 +3,6 @@ import { createReducer } from '@reduxjs/toolkit' import { CampaignData, CampaignLeaderboard, - CampaignLuckyWinner, setCampaignData, setCampaignDataByPage, setClaimingCampaignRewardId, @@ -11,35 +10,25 @@ import { setLoadingCampaignData, setLoadingCampaignDataError, setLoadingSelectedCampaignLeaderboard, - setLoadingSelectedCampaignLuckyWinners, setRecaptchaCampaignId, setRecaptchaCampaignLoading, setSelectedCampaign, setSelectedCampaignLeaderboard, setSelectedCampaignLeaderboardLookupAddress, setSelectedCampaignLeaderboardPageNumber, - setSelectedCampaignLuckyWinners, - setSelectedCampaignLuckyWinnersLookupAddress, - setSelectedCampaignLuckyWinnersPageNumber, } from './actions' interface CampaignsState { readonly data: CampaignData[] readonly loadingCampaignData: boolean - readonly loadingCampaignDataError: Error | undefined + readonly loadingCampaignDataError: boolean readonly selectedCampaign: CampaignData | undefined readonly selectedCampaignLeaderboard: CampaignLeaderboard | undefined - readonly loadingCampaignLeaderboard: boolean readonly selectedCampaignLeaderboardPageNumber: number readonly selectedCampaignLeaderboardLookupAddress: string - readonly selectedCampaignLuckyWinners: CampaignLuckyWinner[] - readonly loadingCampaignLuckyWinners: boolean - readonly selectedCampaignLuckyWinnersPageNumber: number - readonly selectedCampaignLuckyWinnersLookupAddress: string - readonly claimingCampaignRewardId: number | null // id that is being claimed readonly recaptchaCampaign: { @@ -53,20 +42,14 @@ interface CampaignsState { const initialState: CampaignsState = { data: [], loadingCampaignData: true, - loadingCampaignDataError: undefined, + loadingCampaignDataError: false, selectedCampaign: undefined, selectedCampaignLeaderboard: undefined, - loadingCampaignLeaderboard: false, selectedCampaignLeaderboardPageNumber: 0, selectedCampaignLeaderboardLookupAddress: '', - selectedCampaignLuckyWinners: [], - loadingCampaignLuckyWinners: false, - selectedCampaignLuckyWinnersPageNumber: 0, - selectedCampaignLuckyWinnersLookupAddress: '', - claimingCampaignRewardId: null, recaptchaCampaign: { @@ -136,30 +119,7 @@ export default createReducer(initialState, builder => selectedCampaignLeaderboardLookupAddress: lookupAddress, } }) - .addCase(setSelectedCampaignLuckyWinners, (state, { payload: { luckyWinners } }) => { - return { - ...state, - selectedCampaignLuckyWinners: luckyWinners, - } - }) - .addCase(setLoadingSelectedCampaignLuckyWinners, (state, { payload: loading }) => { - return { - ...state, - loadingCampaignLuckyWinners: loading, - } - }) - .addCase(setSelectedCampaignLuckyWinnersPageNumber, (state, { payload: pageNumber }) => { - return { - ...state, - selectedCampaignLuckyWinnersPageNumber: pageNumber, - } - }) - .addCase(setSelectedCampaignLuckyWinnersLookupAddress, (state, { payload: lookupAddress }) => { - return { - ...state, - selectedCampaignLuckyWinnersLookupAddress: lookupAddress, - } - }) + .addCase(setRecaptchaCampaignId, (state, { payload: id }) => { return { ...state, diff --git a/src/state/index.ts b/src/state/index.ts index caffdcdc0c..337177cb2d 100644 --- a/src/state/index.ts +++ b/src/state/index.ts @@ -1,26 +1,27 @@ import { configureStore } from '@reduxjs/toolkit' import { load, save } from 'redux-localstorage-simple' +import announcementApi, { publicAnnouncementApi } from 'services/announcement' import blockServiceApi from 'services/blockService' +import campaignApi from 'services/campaign' import coingeckoApi from 'services/coingecko' +import crosschainApi from 'services/crossChain' +import earningApi from 'services/earning' +import geckoTerminalApi from 'services/geckoTermial' +import identifyApi from 'services/identity' +import knProtocolApi from 'services/knprotocol' +import ksSettingApi from 'services/ksSetting' import kyberAISubscriptionApi from 'services/kyberAISubscription' +import kyberDAO from 'services/kyberDAO' +import limitOrderApi from 'services/limitOrder' import priceAlertApi from 'services/priceAlert' import routeApi from 'services/route' +import socialApi from 'services/social' import tokenApi from 'services/token' import { ENV_LEVEL } from 'constants/env' import { ENV_TYPE } from 'constants/type' import kyberAIApi from 'pages/TrueSightV2/hooks/useKyberAIData' -import announcementApi, { publicAnnouncementApi } from '../services/announcement' -import crosschainApi from '../services/crossChain' -import earningApi from '../services/earning' -import geckoTerminalApi from '../services/geckoTermial' -import identifyApi from '../services/identity' -import knProtocolApi from '../services/knprotocol' -import ksSettingApi from '../services/ksSetting' -import kyberDAO from '../services/kyberDAO' -import limitOrderApi from '../services/limitOrder' -import socialApi from '../services/social' import application from './application/reducer' import authen from './authen/reducer' import burnProAmm from './burn/proamm/reducer' @@ -97,6 +98,7 @@ const store = configureStore({ [coingeckoApi.reducerPath]: coingeckoApi.reducer, [limitOrderApi.reducerPath]: limitOrderApi.reducer, + [campaignApi.reducerPath]: campaignApi.reducer, [kyberAIApi.reducerPath]: kyberAIApi.reducer, [kyberAISubscriptionApi.reducerPath]: kyberAISubscriptionApi.reducer, [kyberDAO.reducerPath]: kyberDAO.reducer, @@ -128,6 +130,7 @@ const store = configureStore({ .concat(coingeckoApi.middleware) .concat(limitOrderApi.middleware) .concat(kyberAIApi.middleware) + .concat(campaignApi.middleware) .concat(kyberAISubscriptionApi.middleware) .concat(announcementApi.middleware) .concat(publicAnnouncementApi.middleware) diff --git a/src/theme/components.tsx b/src/theme/components.tsx index 5fb445e175..7afcfc381a 100644 --- a/src/theme/components.tsx +++ b/src/theme/components.tsx @@ -193,7 +193,7 @@ export function ExternalLink({ }: Omit, 'as' | 'ref'> & { href: string }) { const handleClick = useCallback( (event: React.MouseEvent) => { - onClick && onClick(event) + onClick?.(event) // don't prevent default, don't redirect if it's a new tab if (target === '_blank' || event.ctrlKey || event.metaKey) { } else { @@ -203,7 +203,13 @@ export function ExternalLink({ [target, onClick], ) return ( - + ) } @@ -221,13 +227,19 @@ export function ExternalLinkIcon({ console.debug('Fired outbound link event', href) } else { event.preventDefault() - navigateToUrl(href, false) + navigateToUrl(href, { _dangerousSkipCheckWhitelist: true, allowRelativePath: true }) } }, [href, target], ) return ( - + ) diff --git a/src/utils/redirect.ts b/src/utils/redirect.ts index 4dec7f623a..d472a54b66 100644 --- a/src/utils/redirect.ts +++ b/src/utils/redirect.ts @@ -6,15 +6,19 @@ import { useActiveWeb3React } from 'hooks' import { useChangeNetwork } from 'hooks/web3/useChangeNetwork' const whiteListDomains = [/https:\/\/(.+?\.)?kyberswap\.com$/, /https:\/\/(.+)\.kyberengineering\.io$/] -export const validateRedirectURL = (url: string | undefined, whitelistKyberSwap = true) => { + +type Options = { _dangerousSkipCheckWhitelist?: boolean; allowRelativePath?: boolean } +export const validateRedirectURL = ( + url: string | undefined, + { _dangerousSkipCheckWhitelist = false, allowRelativePath = false }: Options = {}, +) => { try { - if (!url) throw new Error() - const newUrl = new URL(url) // valid url + if (!url || url.endsWith('.js')) throw new Error() + const newUrl = allowRelativePath && url.startsWith('/') ? new URL(`${window.location.origin}${url}`) : new URL(url) if ( - url.endsWith('.js') || newUrl.pathname.endsWith('.js') || !['https:', 'http:'].includes(newUrl.protocol) || - (whitelistKyberSwap && !whiteListDomains.some(regex => newUrl.origin.match(regex))) + (!_dangerousSkipCheckWhitelist && !whiteListDomains.some(regex => newUrl.origin.match(regex))) ) { throw new Error() } @@ -24,8 +28,8 @@ export const validateRedirectURL = (url: string | undefined, whitelistKyberSwap } } -export const navigateToUrl = (url: string | undefined, whitelistKyberSwap = true) => { - const urlFormatted = validateRedirectURL(url, whitelistKyberSwap) +export const navigateToUrl = (url: string | undefined, options?: Options) => { + const urlFormatted = validateRedirectURL(url, options) if (urlFormatted) window.location.href = urlFormatted } @@ -46,7 +50,7 @@ export const useNavigateToUrl = () => { return } const { pathname, host, search } = new URL(actionURL) - if (!validateRedirectURL(actionURL, false)) return + if (!validateRedirectURL(actionURL, { _dangerousSkipCheckWhitelist: true })) return if (window.location.host === host) { navigate(`${pathname}${search}`) } else {