diff --git a/apps/evm/src/components/Icon/icons/dotShortcut.tsx b/apps/evm/src/components/Icon/icons/dotShortcut.tsx new file mode 100644 index 0000000000..18888b09b5 --- /dev/null +++ b/apps/evm/src/components/Icon/icons/dotShortcut.tsx @@ -0,0 +1,28 @@ +import type { SVGProps } from 'react'; + +const SvgDotShortcut = (props: SVGProps) => ( + + + + + + +); + +export default SvgDotShortcut; diff --git a/apps/evm/src/components/Icon/icons/index.ts b/apps/evm/src/components/Icon/icons/index.ts index 27c0ae09a3..7dec646edf 100644 --- a/apps/evm/src/components/Icon/icons/index.ts +++ b/apps/evm/src/components/Icon/icons/index.ts @@ -39,6 +39,7 @@ export { default as checkInlineDotted } from './checkInlineDotted'; export { default as mark } from './mark'; export { default as arrowShaft } from './arrowShaft'; export { default as notice } from './notice'; +export { default as dotShortcut } from './dotShortcut'; export { default as dots } from './dots'; export { default as exclamation } from './exclamation'; export { default as comment } from './comment'; @@ -66,6 +67,7 @@ export { default as shield2 } from './shield2'; export { default as lightning2 } from './lightning2'; export { default as graph } from './graph'; export { default as star } from './star'; +export { default as sparkle } from './sparkle'; export { default as download } from './download'; export { default as arrowUpFull2 } from './arrowUpFull2'; export { default as transactionFile } from './transactionFile'; diff --git a/apps/evm/src/components/Icon/icons/sparkle.tsx b/apps/evm/src/components/Icon/icons/sparkle.tsx new file mode 100644 index 0000000000..7e37b3a109 --- /dev/null +++ b/apps/evm/src/components/Icon/icons/sparkle.tsx @@ -0,0 +1,19 @@ +import type { SVGProps } from 'react'; + +const SvgSparkle = (props: SVGProps) => ( + + + +); + +export default SvgSparkle; diff --git a/apps/evm/src/libs/translations/translations/en.json b/apps/evm/src/libs/translations/translations/en.json index aa7993dfb7..64379d0776 100644 --- a/apps/evm/src/libs/translations/translations/en.json +++ b/apps/evm/src/libs/translations/translations/en.json @@ -1276,7 +1276,16 @@ } }, "tablesRefreshNote": "Refreshed hourly · Last refresh: {{date, distanceToNow}} ago", - "title": "Prime leaderboard" + "title": "Prime leaderboard", + "totalRewards": { + "title": "Total Prime rewards distributed this cycle" + }, + "userRewards": { + "eligibleMessage": "You are currently eligible for sharing the Prime rewards during this cycle. Supply assets below to share the rewards.", + "marketActions": "Open market actions", + "notEligibleMessage": "You are currently NOT eligible for sharing the Prime rewards during this cycle. Stake XVS to compete for Prime for next cycle.", + "title": "Your Prime rewards this cycle" + } }, "primeStatusBanner": { "becomePrimeTitle": "You can now become a Prime user", diff --git a/apps/evm/src/libs/translations/translations/ja.json b/apps/evm/src/libs/translations/translations/ja.json index ca3be1da49..b84490625f 100644 --- a/apps/evm/src/libs/translations/translations/ja.json +++ b/apps/evm/src/libs/translations/translations/ja.json @@ -1276,7 +1276,16 @@ } }, "tablesRefreshNote": "1時間ごとに更新 · 最終更新:{{date, distanceToNow}}前", - "title": "Prime リーダーボード" + "title": "Prime リーダーボード", + "totalRewards": { + "title": "今サイクルに分配された Prime 報酬の総額" + }, + "userRewards": { + "eligibleMessage": "今サイクルで Prime 報酬を分け合う対象になっています。下記の資産を供給して報酬を獲得しましょう。", + "marketActions": "マーケット操作を開く", + "notEligibleMessage": "今サイクルでは Prime 報酬を分け合う対象ではありません。XVS をステークして次のサイクルで Prime を目指しましょう。", + "title": "今サイクルのあなたの Prime 報酬" + } }, "primeStatusBanner": { "becomePrimeTitle": "Primeユーザーになれるようになりました", diff --git a/apps/evm/src/libs/translations/translations/th.json b/apps/evm/src/libs/translations/translations/th.json index 64b9981b45..bc8c044c7d 100644 --- a/apps/evm/src/libs/translations/translations/th.json +++ b/apps/evm/src/libs/translations/translations/th.json @@ -1276,7 +1276,16 @@ } }, "tablesRefreshNote": "รีเฟรชทุกชั่วโมง · รีเฟรชล่าสุด: {{date, distanceToNow}} ที่แล้ว", - "title": "กระดานผู้นำ Prime" + "title": "กระดานผู้นำ Prime", + "totalRewards": { + "title": "รางวัล Prime ทั้งหมดที่แจกในรอบนี้" + }, + "userRewards": { + "eligibleMessage": "คุณมีสิทธิ์ร่วมรับรางวัล Prime ในรอบนี้ จัดหาสินทรัพย์ด้านล่างเพื่อรับรางวัล", + "marketActions": "เปิดการดำเนินการของตลาด", + "notEligibleMessage": "คุณยังไม่มีสิทธิ์ร่วมรับรางวัล Prime ในรอบนี้ Stake XVS เพื่อแข่งขันรับ Prime ในรอบถัดไป", + "title": "รางวัล Prime ของคุณในรอบนี้" + } }, "primeStatusBanner": { "becomePrimeTitle": "ตอนนี้คุณสามารถเป็นผู้ใช้ Prime ได้แล้ว", diff --git a/apps/evm/src/libs/translations/translations/tr.json b/apps/evm/src/libs/translations/translations/tr.json index 7cf0897102..8ec3ddcfe6 100644 --- a/apps/evm/src/libs/translations/translations/tr.json +++ b/apps/evm/src/libs/translations/translations/tr.json @@ -1276,7 +1276,16 @@ } }, "tablesRefreshNote": "Saatlik yenilenir · Son yenileme: {{date, distanceToNow}} önce", - "title": "Prime lider tablosu" + "title": "Prime lider tablosu", + "totalRewards": { + "title": "Bu döngüde dağıtılan toplam Prime ödülü" + }, + "userRewards": { + "eligibleMessage": "Bu döngüde Prime ödüllerini paylaşmaya uygunsun. Ödülleri paylaşmak için aşağıdan varlık sağla.", + "marketActions": "Piyasa işlemlerini aç", + "notEligibleMessage": "Bu döngüde Prime ödüllerini paylaşmaya uygun değilsin. Sonraki döngüde Prime için yarışmak üzere XVS stake et.", + "title": "Bu döngüdeki Prime ödülleriniz" + } }, "primeStatusBanner": { "becomePrimeTitle": "Artık Prime kullanıcısı olabilirsiniz", diff --git a/apps/evm/src/libs/translations/translations/vi.json b/apps/evm/src/libs/translations/translations/vi.json index e9f3c8cabb..41f0a45ec2 100644 --- a/apps/evm/src/libs/translations/translations/vi.json +++ b/apps/evm/src/libs/translations/translations/vi.json @@ -1276,7 +1276,16 @@ } }, "tablesRefreshNote": "Làm mới mỗi giờ · Lần làm mới cuối: {{date, distanceToNow}} trước", - "title": "Bảng xếp hạng Prime" + "title": "Bảng xếp hạng Prime", + "totalRewards": { + "title": "Tổng thưởng Prime đã phân phối trong chu kỳ này" + }, + "userRewards": { + "eligibleMessage": "Bạn hiện đủ điều kiện chia sẻ phần thưởng Prime trong chu kỳ này. Hãy cung cấp tài sản bên dưới để nhận phần thưởng.", + "marketActions": "Mở thao tác thị trường", + "notEligibleMessage": "Bạn hiện KHÔNG đủ điều kiện chia sẻ phần thưởng Prime trong chu kỳ này. Stake XVS để cạnh tranh Prime cho chu kỳ tiếp theo.", + "title": "Phần thưởng Prime của bạn trong chu kỳ này" + } }, "primeStatusBanner": { "becomePrimeTitle": "Bạn đã có thể trở thành người dùng Prime", diff --git a/apps/evm/src/libs/translations/translations/zh-Hans.json b/apps/evm/src/libs/translations/translations/zh-Hans.json index 29b8fc3bae..f2202cf3c1 100644 --- a/apps/evm/src/libs/translations/translations/zh-Hans.json +++ b/apps/evm/src/libs/translations/translations/zh-Hans.json @@ -1276,7 +1276,16 @@ } }, "tablesRefreshNote": "每小时刷新 · 上次刷新:{{date, distanceToNow}}前", - "title": "Prime 排行榜" + "title": "Prime 排行榜", + "totalRewards": { + "title": "本周期已分配的 Prime 总奖励" + }, + "userRewards": { + "eligibleMessage": "你在本周期已具备分享 Prime 奖励的资格。在下方供给资产以分享奖励。", + "marketActions": "打开市场操作", + "notEligibleMessage": "你在本周期暂不具备分享 Prime 奖励的资格。质押 XVS 以在下一周期竞争 Prime。", + "title": "你本周期的 Prime 奖励" + } }, "primeStatusBanner": { "becomePrimeTitle": "你现在可以成为 Prime 用户", diff --git a/apps/evm/src/libs/translations/translations/zh-Hant.json b/apps/evm/src/libs/translations/translations/zh-Hant.json index 0edf97274b..b790ec678f 100644 --- a/apps/evm/src/libs/translations/translations/zh-Hant.json +++ b/apps/evm/src/libs/translations/translations/zh-Hant.json @@ -1276,7 +1276,16 @@ } }, "tablesRefreshNote": "每小時刷新 · 上次刷新:{{date, distanceToNow}}前", - "title": "Prime 排行榜" + "title": "Prime 排行榜", + "totalRewards": { + "title": "本週期已分配的 Prime 總獎勵" + }, + "userRewards": { + "eligibleMessage": "你在本週期已具備分享 Prime 獎勵的資格。在下方供給資產以分享獎勵。", + "marketActions": "開啟市場操作", + "notEligibleMessage": "你在本週期暫不具備分享 Prime 獎勵的資格。質押 XVS 以在下一週期競爭 Prime。", + "title": "你本週期的 Prime 獎勵" + } }, "primeStatusBanner": { "becomePrimeTitle": "你現在可以成為 Prime 用戶", diff --git a/apps/evm/src/pages/PrimeLeaderboard/MarketActions/__tests__/index.spec.tsx b/apps/evm/src/pages/PrimeLeaderboard/MarketActions/__tests__/index.spec.tsx new file mode 100644 index 0000000000..dd770f40f9 --- /dev/null +++ b/apps/evm/src/pages/PrimeLeaderboard/MarketActions/__tests__/index.spec.tsx @@ -0,0 +1,20 @@ +import { fireEvent, screen } from '@testing-library/react'; + +import { xvs } from '__mocks__/models/tokens'; +import { renderComponent } from 'testUtils/render'; + +import { MarketActions } from '..'; + +vi.mock('pages/Market/OperationForm', () => ({ + OperationForm: () =>
, +})); + +describe('pages/PrimeLeaderboard/MarketActions', () => { + it('opens the market operation modal when clicked', async () => { + renderComponent(); + + fireEvent.click(screen.getByLabelText('Open market actions')); + + expect(await screen.findByTestId('operation-form')).toBeInTheDocument(); + }); +}); diff --git a/apps/evm/src/pages/PrimeLeaderboard/MarketActions/index.tsx b/apps/evm/src/pages/PrimeLeaderboard/MarketActions/index.tsx new file mode 100644 index 0000000000..7209a09584 --- /dev/null +++ b/apps/evm/src/pages/PrimeLeaderboard/MarketActions/index.tsx @@ -0,0 +1,57 @@ +import { useState } from 'react'; + +import { useGetPools } from 'clients/api'; +import { Icon, Modal } from 'components'; +import { useTranslation } from 'libs/translations'; +import { useAccountAddress } from 'libs/wallet'; +import { OperationForm } from 'pages/Market/OperationForm'; +import type { Token } from 'types'; +import { areAddressesEqual } from 'utilities'; + +export interface MarketActionsProps { + token: Token; +} + +export const MarketActions: React.FC = ({ token }) => { + const { t } = useTranslation(); + const { accountAddress } = useAccountAddress(); + const { data: getPoolsData } = useGetPools({ accountAddress }); + const [isModalOpen, setIsModalOpen] = useState(false); + + const market = getPoolsData?.pools + .flatMap(pool => + pool.assets.map(asset => ({ asset, poolComptrollerAddress: pool.comptrollerAddress })), + ) + .find(({ asset }) => areAddressesEqual(asset.vToken.underlyingToken.address, token.address)); + + if (!market) { + return undefined; + } + + return ( + <> + + + {isModalOpen && ( + setIsModalOpen(false)} + > + setIsModalOpen(false)} + /> + + )} + + ); +}; diff --git a/apps/evm/src/pages/PrimeLeaderboard/MarketRewardRow/__tests__/index.spec.tsx b/apps/evm/src/pages/PrimeLeaderboard/MarketRewardRow/__tests__/index.spec.tsx new file mode 100644 index 0000000000..1f0ea92790 --- /dev/null +++ b/apps/evm/src/pages/PrimeLeaderboard/MarketRewardRow/__tests__/index.spec.tsx @@ -0,0 +1,19 @@ +import { screen } from '@testing-library/react'; + +import { usdc } from '__mocks__/models/tokens'; +import { renderComponent } from 'testUtils/render'; +import { MarketRewardRow } from '..'; + +describe('pages/PrimeLeaderboard/MarketRewardRow', () => { + it('renders the token, reward amount and trailing content', () => { + renderComponent( + + 3.78% + , + ); + + expect(screen.getByText(usdc.symbol)).toBeInTheDocument(); + expect(screen.getByText('$280.4K')).toBeInTheDocument(); + expect(screen.getByText('3.78%')).toBeInTheDocument(); + }); +}); diff --git a/apps/evm/src/pages/PrimeLeaderboard/MarketRewardRow/index.tsx b/apps/evm/src/pages/PrimeLeaderboard/MarketRewardRow/index.tsx new file mode 100644 index 0000000000..cd02999684 --- /dev/null +++ b/apps/evm/src/pages/PrimeLeaderboard/MarketRewardRow/index.tsx @@ -0,0 +1,36 @@ +import { TokenIconWithSymbol } from 'components'; +import type { Token } from 'types'; +import { formatCentsToReadableValue } from 'utilities'; + +export interface MarketRewardRowProps { + token: Token; + rewardsCents: number; + totalRewardsCents: number; + children?: React.ReactNode; +} + +export const MarketRewardRow: React.FC = ({ + token, + rewardsCents, + totalRewardsCents, + children, +}) => { + const progressPercentage = + totalRewardsCents > 0 ? Math.min(100, (rewardsCents / totalRewardsCents) * 100) : 0; + + return ( +
+ + + + {formatCentsToReadableValue({ value: rewardsCents })} + + +
+
+
+ + {children} +
+ ); +}; diff --git a/apps/evm/src/pages/PrimeLeaderboard/TotalRewardsCard/__tests__/index.spec.tsx b/apps/evm/src/pages/PrimeLeaderboard/TotalRewardsCard/__tests__/index.spec.tsx new file mode 100644 index 0000000000..45a8dd1039 --- /dev/null +++ b/apps/evm/src/pages/PrimeLeaderboard/TotalRewardsCard/__tests__/index.spec.tsx @@ -0,0 +1,26 @@ +import { screen } from '@testing-library/react'; + +import { usdc, xvs } from '__mocks__/models/tokens'; +import { renderComponent } from 'testUtils/render'; +import { TotalRewardsCard } from '..'; + +describe('pages/PrimeLeaderboard/TotalRewardsCard', () => { + it('renders the total and per-market rewards', () => { + renderComponent( + , + ); + + expect(screen.getByText('Total Prime rewards distributed this cycle')).toBeInTheDocument(); + expect(screen.getByText('$462.3K')).toBeInTheDocument(); + expect(screen.getByText('$280.4K')).toBeInTheDocument(); + expect(screen.getByText('$171.9K')).toBeInTheDocument(); + expect(screen.getByText(usdc.symbol)).toBeInTheDocument(); + expect(screen.getByText(xvs.symbol)).toBeInTheDocument(); + }); +}); diff --git a/apps/evm/src/pages/PrimeLeaderboard/TotalRewardsCard/index.tsx b/apps/evm/src/pages/PrimeLeaderboard/TotalRewardsCard/index.tsx index 7fdf44f630..b3b573a472 100644 --- a/apps/evm/src/pages/PrimeLeaderboard/TotalRewardsCard/index.tsx +++ b/apps/evm/src/pages/PrimeLeaderboard/TotalRewardsCard/index.tsx @@ -1,16 +1,54 @@ import { cn } from '@venusprotocol/ui'; +import { useTranslation } from 'libs/translations'; +import type { Token } from 'types'; +import { formatCentsToReadableValue } from 'utilities'; + +import { MarketRewardRow } from '../MarketRewardRow'; + +export interface MarketReward { + token: Token; + rewardsCents: number; +} + export interface TotalRewardsCardProps { + totalRewardsCents: number; + marketRewards: MarketReward[]; className?: string; } -export const TotalRewardsCard: React.FC = ({ className }) => ( -
- Total Prime Rewards Card -
-); +export const TotalRewardsCard: React.FC = ({ + totalRewardsCents, + marketRewards, + className, +}) => { + const { t } = useTranslation(); + + return ( +
+
+

{t('primeLeaderboard.totalRewards.title')}

+ +

+ {formatCentsToReadableValue({ value: totalRewardsCents })} +

+
+ +
+ {marketRewards.map(({ token, rewardsCents }) => ( + + ))} +
+
+ ); +}; diff --git a/apps/evm/src/pages/PrimeLeaderboard/TotalRewardsSection/__tests__/index.spec.tsx b/apps/evm/src/pages/PrimeLeaderboard/TotalRewardsSection/__tests__/index.spec.tsx new file mode 100644 index 0000000000..342a5d752f --- /dev/null +++ b/apps/evm/src/pages/PrimeLeaderboard/TotalRewardsSection/__tests__/index.spec.tsx @@ -0,0 +1,14 @@ +import { screen } from '@testing-library/react'; + +import { renderComponent } from 'testUtils/render'; + +import { TotalRewardsSection } from '..'; + +describe('pages/PrimeLeaderboard/TotalRewardsSection', () => { + it('renders the total Prime rewards from the data hook', () => { + renderComponent(); + + expect(screen.getByText('Total Prime rewards distributed this cycle')).toBeInTheDocument(); + expect(screen.getByText('$462.3K')).toBeInTheDocument(); + }); +}); diff --git a/apps/evm/src/pages/PrimeLeaderboard/TotalRewardsSection/index.tsx b/apps/evm/src/pages/PrimeLeaderboard/TotalRewardsSection/index.tsx new file mode 100644 index 0000000000..3202f6bf39 --- /dev/null +++ b/apps/evm/src/pages/PrimeLeaderboard/TotalRewardsSection/index.tsx @@ -0,0 +1,18 @@ +import { TotalRewardsCard } from '../TotalRewardsCard'; +import { useGetPrimeTotalRewards } from '../useGetPrimeTotalRewards'; + +export interface TotalRewardsSectionProps { + className?: string; +} + +export const TotalRewardsSection: React.FC = ({ className }) => { + const { totalRewardsCents, marketRewards } = useGetPrimeTotalRewards(); + + return ( + + ); +}; diff --git a/apps/evm/src/pages/PrimeLeaderboard/UserRewardsCard/__tests__/index.spec.tsx b/apps/evm/src/pages/PrimeLeaderboard/UserRewardsCard/__tests__/index.spec.tsx new file mode 100644 index 0000000000..316662e0fb --- /dev/null +++ b/apps/evm/src/pages/PrimeLeaderboard/UserRewardsCard/__tests__/index.spec.tsx @@ -0,0 +1,36 @@ +import { screen } from '@testing-library/react'; + +import { usdc, xvs } from '__mocks__/models/tokens'; +import { renderComponent } from 'testUtils/render'; +import { UserRewardsCard } from '..'; + +describe('pages/PrimeLeaderboard/UserRewardsCard', () => { + it('renders the user total and per-market rewards', () => { + renderComponent( + , + ); + + expect(screen.getByText('Your Prime rewards this cycle')).toBeInTheDocument(); + expect(screen.getByText('$18.4K')).toBeInTheDocument(); + expect(screen.getByText(usdc.symbol)).toBeInTheDocument(); + }); + + it('renders the provided content instead of the default headline', () => { + renderComponent( + Eligibility message} + />, + ); + + expect(screen.getByText('Eligibility message')).toBeInTheDocument(); + expect(screen.queryByText('$0')).not.toBeInTheDocument(); + }); +}); diff --git a/apps/evm/src/pages/PrimeLeaderboard/UserRewardsCard/index.tsx b/apps/evm/src/pages/PrimeLeaderboard/UserRewardsCard/index.tsx index bf9c332847..36d3edc9d3 100644 --- a/apps/evm/src/pages/PrimeLeaderboard/UserRewardsCard/index.tsx +++ b/apps/evm/src/pages/PrimeLeaderboard/UserRewardsCard/index.tsx @@ -1,16 +1,84 @@ import { cn } from '@venusprotocol/ui'; +import primeLogoSrc from 'assets/img/primeLogo.svg'; +import { useGetPools } from 'clients/api'; +import { Apy } from 'components'; +import { useTranslation } from 'libs/translations'; +import { useAccountAddress } from 'libs/wallet'; +import type { Token } from 'types'; +import { areAddressesEqual, formatCentsToReadableValue } from 'utilities'; + +import { MarketActions } from '../MarketActions'; +import { MarketRewardRow } from '../MarketRewardRow'; + +export interface UserMarketReward { + token: Token; + rewardsCents: number; +} + export interface UserRewardsCardProps { + totalRewardsCents: number; + marketRewards: UserMarketReward[]; + // Replaces the default headline (Prime badge + total amount), e.g. an eligibility message + content?: React.ReactNode; className?: string; } -export const UserRewardsCard: React.FC = ({ className }) => ( -
- Your Prime Rewards Card -
-); +export const UserRewardsCard: React.FC = ({ + totalRewardsCents, + marketRewards, + content, + className, +}) => { + const { t } = useTranslation(); + const { accountAddress } = useAccountAddress(); + const { data: getPoolsData } = useGetPools({ accountAddress }); + + return ( +
+
+

{t('primeLeaderboard.userRewards.title')}

+ + {content ?? ( +
+ + + + +

+ {formatCentsToReadableValue({ value: totalRewardsCents })} +

+
+ )} +
+ +
+ {marketRewards.map(({ token, rewardsCents }) => { + const asset = getPoolsData?.pools + .flatMap(pool => pool.assets) + .find(poolAsset => + areAddressesEqual(poolAsset.vToken.underlyingToken.address, token.address), + ); + + return ( + + {asset && } + + + + ); + })} +
+
+ ); +}; diff --git a/apps/evm/src/pages/PrimeLeaderboard/UserRewardsSection/__tests__/index.spec.tsx b/apps/evm/src/pages/PrimeLeaderboard/UserRewardsSection/__tests__/index.spec.tsx new file mode 100644 index 0000000000..d6c353dd21 --- /dev/null +++ b/apps/evm/src/pages/PrimeLeaderboard/UserRewardsSection/__tests__/index.spec.tsx @@ -0,0 +1,14 @@ +import { screen } from '@testing-library/react'; + +import { renderComponent } from 'testUtils/render'; + +import { UserRewardsSection } from '..'; + +describe('pages/PrimeLeaderboard/UserRewardsSection', () => { + it('renders the user Prime rewards from the data hook', () => { + renderComponent(); + + expect(screen.getByText('Your Prime rewards this cycle')).toBeInTheDocument(); + expect(screen.getByText('$18.4K')).toBeInTheDocument(); + }); +}); diff --git a/apps/evm/src/pages/PrimeLeaderboard/UserRewardsSection/index.tsx b/apps/evm/src/pages/PrimeLeaderboard/UserRewardsSection/index.tsx new file mode 100644 index 0000000000..da06e10039 --- /dev/null +++ b/apps/evm/src/pages/PrimeLeaderboard/UserRewardsSection/index.tsx @@ -0,0 +1,52 @@ +import { cn } from '@venusprotocol/ui'; + +import primeLogoSrc from 'assets/img/primeLogo.svg'; +import { Icon } from 'components'; +import { useTranslation } from 'libs/translations'; + +import { UserRewardsCard } from '../UserRewardsCard'; +import { useGetPrimeUserRewards } from '../useGetPrimeUserRewards'; + +export interface UserRewardsSectionProps { + className?: string; +} + +export const UserRewardsSection: React.FC = ({ className }) => { + const { t } = useTranslation(); + const { isPrime, totalRewardsCents, marketRewards } = useGetPrimeUserRewards(); + + const hasRewards = totalRewardsCents > 0; + + let content: React.ReactNode; + + if (!hasRewards) { + content = ( +
+ {isPrime ? ( + + + + ) : ( + + + + )} + +

+ {isPrime + ? t('primeLeaderboard.userRewards.eligibleMessage') + : t('primeLeaderboard.userRewards.notEligibleMessage')} +

+
+ ); + } + + return ( + + ); +}; diff --git a/apps/evm/src/pages/PrimeLeaderboard/__tests__/index.spec.tsx b/apps/evm/src/pages/PrimeLeaderboard/__tests__/index.spec.tsx index 788f8c4fd1..c3c6a43284 100644 --- a/apps/evm/src/pages/PrimeLeaderboard/__tests__/index.spec.tsx +++ b/apps/evm/src/pages/PrimeLeaderboard/__tests__/index.spec.tsx @@ -1,5 +1,6 @@ import { screen } from '@testing-library/react'; +import fakeAddress from '__mocks__/models/address'; import { renderComponent } from 'testUtils/render'; import PrimeLeaderboard from '..'; @@ -16,12 +17,12 @@ vi.mock('../EndOfCycle', () => ({ EndOfCycle: () =>
, })); -vi.mock('../TotalRewardsCard', () => ({ - TotalRewardsCard: () =>
, +vi.mock('../TotalRewardsSection', () => ({ + TotalRewardsSection: () =>
, })); -vi.mock('../UserRewardsCard', () => ({ - UserRewardsCard: () =>
, +vi.mock('../UserRewardsSection', () => ({ + UserRewardsSection: () =>
, })); vi.mock('../RewardTable', () => ({ @@ -37,15 +38,22 @@ vi.mock('../RankTable', () => ({ })); describe('pages/PrimeLeaderboard', () => { - it('renders every section', () => { - renderComponent(); + it('renders every section when the wallet is connected', () => { + renderComponent(, { accountAddress: fakeAddress }); expect(screen.getByTestId('hero')).toBeInTheDocument(); expect(screen.getByTestId('end-of-cycle')).toBeInTheDocument(); - expect(screen.getByTestId('total-rewards-card')).toBeInTheDocument(); - expect(screen.getByTestId('user-rewards-card')).toBeInTheDocument(); + expect(screen.getByTestId('total-rewards-section')).toBeInTheDocument(); + expect(screen.getByTestId('user-rewards-section')).toBeInTheDocument(); expect(screen.getByTestId('reward-table')).toBeInTheDocument(); expect(screen.getByTestId('rank-card')).toBeInTheDocument(); expect(screen.getByTestId('rank-table')).toBeInTheDocument(); }); + + it('hides the user rewards card when the wallet is not connected', () => { + renderComponent(); + + expect(screen.getByTestId('total-rewards-section')).toBeInTheDocument(); + expect(screen.queryByTestId('user-rewards-section')).not.toBeInTheDocument(); + }); }); diff --git a/apps/evm/src/pages/PrimeLeaderboard/index.tsx b/apps/evm/src/pages/PrimeLeaderboard/index.tsx index 73fa0cd6eb..5888f77af0 100644 --- a/apps/evm/src/pages/PrimeLeaderboard/index.tsx +++ b/apps/evm/src/pages/PrimeLeaderboard/index.tsx @@ -2,14 +2,15 @@ import { endOfMonth, subHours } from 'date-fns'; import { Card, Page, Spinner } from 'components'; import { useTranslation } from 'libs/translations'; +import { useAccountAddress } from 'libs/wallet'; import { EndOfCycle } from './EndOfCycle'; import { Hero } from './Hero'; import { RankCard } from './RankCard'; import { RankTable } from './RankTable'; import { RewardTable } from './RewardTable'; -import { TotalRewardsCard } from './TotalRewardsCard'; -import { UserRewardsCard } from './UserRewardsCard'; +import { TotalRewardsSection } from './TotalRewardsSection'; +import { UserRewardsSection } from './UserRewardsSection'; // TODO: use the cycle end date returned by the API const endOfCycleDate = endOfMonth(new Date()); @@ -19,6 +20,7 @@ const lastRefreshedAt = subHours(new Date(), 6); const PrimeLeaderboard: React.FC = () => { const { t } = useTranslation(); + const { accountAddress } = useAccountAddress(); return ( @@ -48,11 +50,15 @@ const PrimeLeaderboard: React.FC = () => {
-
- + {accountAddress ? ( +
+ - -
+ +
+ ) : ( + + )}
diff --git a/apps/evm/src/pages/PrimeLeaderboard/useGetPrimeTotalRewards/index.ts b/apps/evm/src/pages/PrimeLeaderboard/useGetPrimeTotalRewards/index.ts new file mode 100644 index 0000000000..aee88219e9 --- /dev/null +++ b/apps/evm/src/pages/PrimeLeaderboard/useGetPrimeTotalRewards/index.ts @@ -0,0 +1,26 @@ +import { useGetTokens } from 'libs/tokens'; + +import type { MarketReward } from '../TotalRewardsCard'; + +export interface UseGetPrimeTotalRewardsOutput { + totalRewardsCents: number; + marketRewards: MarketReward[]; +} + +// TODO: replace these placeholder values with the data returned by the API +const placeholderTotalRewardsCents = 46_230_000; +const placeholderMarketRewardsCents = [28_040_000, 17_190_000]; + +export const useGetPrimeTotalRewards = (): UseGetPrimeTotalRewardsOutput => { + const tokens = useGetTokens(); + + // TODO: replace these placeholder tokens with the real Prime markets returned by the API + const marketRewards = tokens + .slice(0, placeholderMarketRewardsCents.length) + .map((token, index) => ({ token, rewardsCents: placeholderMarketRewardsCents[index] })); + + return { + totalRewardsCents: placeholderTotalRewardsCents, + marketRewards, + }; +}; diff --git a/apps/evm/src/pages/PrimeLeaderboard/useGetPrimeUserRewards/index.ts b/apps/evm/src/pages/PrimeLeaderboard/useGetPrimeUserRewards/index.ts new file mode 100644 index 0000000000..10fee43c0c --- /dev/null +++ b/apps/evm/src/pages/PrimeLeaderboard/useGetPrimeUserRewards/index.ts @@ -0,0 +1,32 @@ +import { useGetTokens } from 'libs/tokens'; + +import type { UserMarketReward } from '../UserRewardsCard'; + +export interface UseGetPrimeUserRewardsOutput { + isPrime: boolean; + totalRewardsCents: number; + marketRewards: UserMarketReward[]; +} + +// TODO: replace these placeholder values with the data returned by the API +const placeholderIsPrime = true; +const placeholderTotalRewardsCents = 1_840_000; +const placeholderMarketRewardsCents = [1_140_000, 700_000]; + +export const useGetPrimeUserRewards = (): UseGetPrimeUserRewardsOutput => { + const tokens = useGetTokens(); + + // TODO: replace these placeholder tokens with the real Prime markets returned by the API + const marketRewards = tokens + .slice(0, placeholderMarketRewardsCents.length) + .map((token, index) => ({ + token, + rewardsCents: placeholderMarketRewardsCents[index], + })); + + return { + isPrime: placeholderIsPrime, + totalRewardsCents: placeholderTotalRewardsCents, + marketRewards, + }; +};