From f1cced8a7ae310cebd084f7620e0a33766ad752f Mon Sep 17 00:00:00 2001 From: cuzz-venus Date: Tue, 16 Jun 2026 14:37:01 +0800 Subject: [PATCH 1/6] feat: add prime total rewards card --- .../src/components/Icon/icons/dotShortcut.tsx | 28 +++++++ apps/evm/src/components/Icon/icons/index.ts | 2 + .../evm/src/components/Icon/icons/sparkle.tsx | 19 +++++ .../libs/translations/translations/en.json | 9 ++- .../libs/translations/translations/ja.json | 9 ++- .../libs/translations/translations/th.json | 9 ++- .../libs/translations/translations/tr.json | 9 ++- .../libs/translations/translations/vi.json | 9 ++- .../translations/translations/zh-Hans.json | 9 ++- .../translations/translations/zh-Hant.json | 9 ++- .../MarketRewardRow/__tests__/index.spec.tsx | 19 +++++ .../MarketRewardRow/index.tsx | 36 +++++++++ .../TotalRewardsCard/__tests__/index.spec.tsx | 26 ++++++ .../TotalRewardsCard/index.tsx | 58 ++++++++++--- .../__tests__/index.spec.tsx | 14 ++++ .../TotalRewardsSection/index.tsx | 18 +++++ .../UserRewardsCard/__tests__/index.spec.tsx | 24 ++++++ .../UserRewardsCard/index.tsx | 81 ++++++++++++++++--- .../__tests__/index.spec.tsx | 14 ++++ .../UserRewardsSection/index.tsx | 18 +++++ .../PrimeLeaderboard/__tests__/index.spec.tsx | 24 ++++-- apps/evm/src/pages/PrimeLeaderboard/index.tsx | 18 +++-- .../useGetPrimeTotalRewards/index.ts | 26 ++++++ .../useGetPrimeUserRewards/index.ts | 31 +++++++ 24 files changed, 478 insertions(+), 41 deletions(-) create mode 100644 apps/evm/src/components/Icon/icons/dotShortcut.tsx create mode 100644 apps/evm/src/components/Icon/icons/sparkle.tsx create mode 100644 apps/evm/src/pages/PrimeLeaderboard/MarketRewardRow/__tests__/index.spec.tsx create mode 100644 apps/evm/src/pages/PrimeLeaderboard/MarketRewardRow/index.tsx create mode 100644 apps/evm/src/pages/PrimeLeaderboard/TotalRewardsCard/__tests__/index.spec.tsx create mode 100644 apps/evm/src/pages/PrimeLeaderboard/TotalRewardsSection/__tests__/index.spec.tsx create mode 100644 apps/evm/src/pages/PrimeLeaderboard/TotalRewardsSection/index.tsx create mode 100644 apps/evm/src/pages/PrimeLeaderboard/UserRewardsCard/__tests__/index.spec.tsx create mode 100644 apps/evm/src/pages/PrimeLeaderboard/UserRewardsSection/__tests__/index.spec.tsx create mode 100644 apps/evm/src/pages/PrimeLeaderboard/UserRewardsSection/index.tsx create mode 100644 apps/evm/src/pages/PrimeLeaderboard/useGetPrimeTotalRewards/index.ts create mode 100644 apps/evm/src/pages/PrimeLeaderboard/useGetPrimeUserRewards/index.ts 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 d0faebafe4..84fe0a1902 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 7eca3d3797..3d45658cea 100644 --- a/apps/evm/src/libs/translations/translations/en.json +++ b/apps/evm/src/libs/translations/translations/en.json @@ -1284,7 +1284,14 @@ } }, "tablesRefreshNote": "Refreshed hourly · Last refresh: {{date, distanceToNow}} ago", - "title": "Prime leaderboard" + "title": "Prime leaderboard", + "totalRewards": { + "title": "Total Prime rewards distributed this cycle" + }, + "userRewards": { + "marketActions": "Open market actions", + "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 fd7d8df62c..d5ff1c89bf 100644 --- a/apps/evm/src/libs/translations/translations/ja.json +++ b/apps/evm/src/libs/translations/translations/ja.json @@ -1284,7 +1284,14 @@ } }, "tablesRefreshNote": "1時間ごとに更新 · 最終更新:{{date, distanceToNow}}前", - "title": "Prime リーダーボード" + "title": "Prime リーダーボード", + "totalRewards": { + "title": "今サイクルに分配された Prime 報酬の総額" + }, + "userRewards": { + "marketActions": "マーケット操作を開く", + "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 7d1d92c854..f013050711 100644 --- a/apps/evm/src/libs/translations/translations/th.json +++ b/apps/evm/src/libs/translations/translations/th.json @@ -1284,7 +1284,14 @@ } }, "tablesRefreshNote": "รีเฟรชทุกชั่วโมง · รีเฟรชล่าสุด: {{date, distanceToNow}} ที่แล้ว", - "title": "กระดานผู้นำ Prime" + "title": "กระดานผู้นำ Prime", + "totalRewards": { + "title": "รางวัล Prime ทั้งหมดที่แจกในรอบนี้" + }, + "userRewards": { + "marketActions": "เปิดการดำเนินการของตลาด", + "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 31c8454ed0..0c013120d4 100644 --- a/apps/evm/src/libs/translations/translations/tr.json +++ b/apps/evm/src/libs/translations/translations/tr.json @@ -1284,7 +1284,14 @@ } }, "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": { + "marketActions": "Piyasa işlemlerini aç", + "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 b320c7b3e3..158616e7d4 100644 --- a/apps/evm/src/libs/translations/translations/vi.json +++ b/apps/evm/src/libs/translations/translations/vi.json @@ -1284,7 +1284,14 @@ } }, "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": { + "marketActions": "Mở thao tác thị trường", + "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 bcd40cef59..19ad5bf0ef 100644 --- a/apps/evm/src/libs/translations/translations/zh-Hans.json +++ b/apps/evm/src/libs/translations/translations/zh-Hans.json @@ -1284,7 +1284,14 @@ } }, "tablesRefreshNote": "每小时刷新 · 上次刷新:{{date, distanceToNow}}前", - "title": "Prime 排行榜" + "title": "Prime 排行榜", + "totalRewards": { + "title": "本周期已分配的 Prime 总奖励" + }, + "userRewards": { + "marketActions": "打开市场操作", + "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 a6ced2859d..aa44056043 100644 --- a/apps/evm/src/libs/translations/translations/zh-Hant.json +++ b/apps/evm/src/libs/translations/translations/zh-Hant.json @@ -1284,7 +1284,14 @@ } }, "tablesRefreshNote": "每小時刷新 · 上次刷新:{{date, distanceToNow}}前", - "title": "Prime 排行榜" + "title": "Prime 排行榜", + "totalRewards": { + "title": "本週期已分配的 Prime 總獎勵" + }, + "userRewards": { + "marketActions": "開啟市場操作", + "title": "你本週期的 Prime 獎勵" + } }, "primeStatusBanner": { "becomePrimeTitle": "你現在可以成為 Prime 用戶", 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..abfb04f527 --- /dev/null +++ b/apps/evm/src/pages/PrimeLeaderboard/UserRewardsCard/__tests__/index.spec.tsx @@ -0,0 +1,24 @@ +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, per-market rewards and Prime APYs', () => { + renderComponent( + , + ); + + expect(screen.getByText('Your Prime rewards this cycle')).toBeInTheDocument(); + expect(screen.getByText('$18.4K')).toBeInTheDocument(); + expect(screen.getByText(usdc.symbol)).toBeInTheDocument(); + expect(screen.getByText('3.78%')).toBeInTheDocument(); + }); +}); diff --git a/apps/evm/src/pages/PrimeLeaderboard/UserRewardsCard/index.tsx b/apps/evm/src/pages/PrimeLeaderboard/UserRewardsCard/index.tsx index bf9c332847..262e664d4a 100644 --- a/apps/evm/src/pages/PrimeLeaderboard/UserRewardsCard/index.tsx +++ b/apps/evm/src/pages/PrimeLeaderboard/UserRewardsCard/index.tsx @@ -1,16 +1,77 @@ import { cn } from '@venusprotocol/ui'; +import primeLogoSrc from 'assets/img/primeLogo.svg'; +import { Icon } from 'components'; +import { useTranslation } from 'libs/translations'; +import type { Token } from 'types'; +import { formatCentsToReadableValue, formatPercentageToReadableValue } from 'utilities'; + +import { MarketRewardRow } from '../MarketRewardRow'; + +export interface UserMarketReward { + token: Token; + rewardsCents: number; + apyPercentage: number; +} + export interface UserRewardsCardProps { + totalRewardsCents: number; + marketRewards: UserMarketReward[]; className?: string; } -export const UserRewardsCard: React.FC = ({ className }) => ( -
- Your Prime Rewards Card -
-); +export const UserRewardsCard: React.FC = ({ + totalRewardsCents, + marketRewards, + className, +}) => { + const { t } = useTranslation(); + + return ( +
+
+

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

+ +
+ + + + +

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

+
+
+ +
+ {marketRewards.map(({ token, rewardsCents, apyPercentage }) => ( + +
+ + + {formatPercentageToReadableValue(apyPercentage)} +
+ + +
+ ))} +
+
+ ); +}; 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..30d60b7580 --- /dev/null +++ b/apps/evm/src/pages/PrimeLeaderboard/UserRewardsSection/index.tsx @@ -0,0 +1,18 @@ +import { UserRewardsCard } from '../UserRewardsCard'; +import { useGetPrimeUserRewards } from '../useGetPrimeUserRewards'; + +export interface UserRewardsSectionProps { + className?: string; +} + +export const UserRewardsSection: React.FC = ({ className }) => { + const { totalRewardsCents, marketRewards } = useGetPrimeUserRewards(); + + 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..37618a19fe --- /dev/null +++ b/apps/evm/src/pages/PrimeLeaderboard/useGetPrimeUserRewards/index.ts @@ -0,0 +1,31 @@ +import { useGetTokens } from 'libs/tokens'; + +import type { UserMarketReward } from '../UserRewardsCard'; + +export interface UseGetPrimeUserRewardsOutput { + totalRewardsCents: number; + marketRewards: UserMarketReward[]; +} + +// TODO: replace these placeholder values with the data returned by the API +const placeholderTotalRewardsCents = 1_840_000; +const placeholderMarketRewardsCents = [1_140_000, 700_000]; +const placeholderApyPercentage = 3.78; + +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], + apyPercentage: placeholderApyPercentage, + })); + + return { + totalRewardsCents: placeholderTotalRewardsCents, + marketRewards, + }; +}; From 3c5b1501d2c07914d5b8c2e44324f4b44afc010a Mon Sep 17 00:00:00 2001 From: cuzz-venus Date: Tue, 16 Jun 2026 17:43:26 +0800 Subject: [PATCH 2/6] feat: support market modal in prime leaderboard --- .../MarketActions/__tests__/index.spec.tsx | 20 +++++++ .../PrimeLeaderboard/MarketActions/index.tsx | 57 +++++++++++++++++++ .../UserRewardsCard/index.tsx | 9 +-- 3 files changed, 79 insertions(+), 7 deletions(-) create mode 100644 apps/evm/src/pages/PrimeLeaderboard/MarketActions/__tests__/index.spec.tsx create mode 100644 apps/evm/src/pages/PrimeLeaderboard/MarketActions/index.tsx 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/UserRewardsCard/index.tsx b/apps/evm/src/pages/PrimeLeaderboard/UserRewardsCard/index.tsx index 262e664d4a..502c7e93ea 100644 --- a/apps/evm/src/pages/PrimeLeaderboard/UserRewardsCard/index.tsx +++ b/apps/evm/src/pages/PrimeLeaderboard/UserRewardsCard/index.tsx @@ -6,6 +6,7 @@ import { useTranslation } from 'libs/translations'; import type { Token } from 'types'; import { formatCentsToReadableValue, formatPercentageToReadableValue } from 'utilities'; +import { MarketActions } from '../MarketActions'; import { MarketRewardRow } from '../MarketRewardRow'; export interface UserMarketReward { @@ -62,13 +63,7 @@ export const UserRewardsCard: React.FC = ({ {formatPercentageToReadableValue(apyPercentage)}
- + ))}
From e79b33e84b851e8d45d4b13e3429e3efd3a360a9 Mon Sep 17 00:00:00 2001 From: cuzz-venus Date: Wed, 17 Jun 2026 16:44:31 +0800 Subject: [PATCH 3/6] feat: show prime eligibility states in user rewards card Drive the user rewards card headline by Prime eligibility (rewards amount, eligible-to-supply prompt, or not-eligible prompt) and render per-market Prime APYs with the shared Apy component. Co-Authored-By: Claude Opus 4.8 --- .../libs/translations/translations/en.json | 2 + .../libs/translations/translations/ja.json | 2 + .../libs/translations/translations/th.json | 2 + .../libs/translations/translations/tr.json | 2 + .../libs/translations/translations/vi.json | 2 + .../translations/translations/zh-Hans.json | 2 + .../translations/translations/zh-Hant.json | 2 + .../UserRewardsCard/__tests__/index.spec.tsx | 20 ++++-- .../UserRewardsCard/index.tsx | 64 +++++++++++-------- .../UserRewardsSection/index.tsx | 36 ++++++++++- .../useGetPrimeUserRewards/index.ts | 5 +- 11 files changed, 106 insertions(+), 33 deletions(-) diff --git a/apps/evm/src/libs/translations/translations/en.json b/apps/evm/src/libs/translations/translations/en.json index 3d45658cea..d519bf2c54 100644 --- a/apps/evm/src/libs/translations/translations/en.json +++ b/apps/evm/src/libs/translations/translations/en.json @@ -1289,7 +1289,9 @@ "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" } }, diff --git a/apps/evm/src/libs/translations/translations/ja.json b/apps/evm/src/libs/translations/translations/ja.json index d5ff1c89bf..3cc5f0a989 100644 --- a/apps/evm/src/libs/translations/translations/ja.json +++ b/apps/evm/src/libs/translations/translations/ja.json @@ -1289,7 +1289,9 @@ "title": "今サイクルに分配された Prime 報酬の総額" }, "userRewards": { + "eligibleMessage": "今サイクルで Prime 報酬を分け合う対象になっています。下記の資産を供給して報酬を獲得しましょう。", "marketActions": "マーケット操作を開く", + "notEligibleMessage": "今サイクルでは Prime 報酬を分け合う対象ではありません。XVS をステークして次のサイクルで Prime を目指しましょう。", "title": "今サイクルのあなたの Prime 報酬" } }, diff --git a/apps/evm/src/libs/translations/translations/th.json b/apps/evm/src/libs/translations/translations/th.json index f013050711..a0754432a7 100644 --- a/apps/evm/src/libs/translations/translations/th.json +++ b/apps/evm/src/libs/translations/translations/th.json @@ -1289,7 +1289,9 @@ "title": "รางวัล Prime ทั้งหมดที่แจกในรอบนี้" }, "userRewards": { + "eligibleMessage": "คุณมีสิทธิ์ร่วมรับรางวัล Prime ในรอบนี้ จัดหาสินทรัพย์ด้านล่างเพื่อรับรางวัล", "marketActions": "เปิดการดำเนินการของตลาด", + "notEligibleMessage": "คุณยังไม่มีสิทธิ์ร่วมรับรางวัล Prime ในรอบนี้ Stake XVS เพื่อแข่งขันรับ Prime ในรอบถัดไป", "title": "รางวัล Prime ของคุณในรอบนี้" } }, diff --git a/apps/evm/src/libs/translations/translations/tr.json b/apps/evm/src/libs/translations/translations/tr.json index 0c013120d4..625377a79e 100644 --- a/apps/evm/src/libs/translations/translations/tr.json +++ b/apps/evm/src/libs/translations/translations/tr.json @@ -1289,7 +1289,9 @@ "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" } }, diff --git a/apps/evm/src/libs/translations/translations/vi.json b/apps/evm/src/libs/translations/translations/vi.json index 158616e7d4..db3d6bfad4 100644 --- a/apps/evm/src/libs/translations/translations/vi.json +++ b/apps/evm/src/libs/translations/translations/vi.json @@ -1289,7 +1289,9 @@ "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" } }, diff --git a/apps/evm/src/libs/translations/translations/zh-Hans.json b/apps/evm/src/libs/translations/translations/zh-Hans.json index 19ad5bf0ef..b84c212f27 100644 --- a/apps/evm/src/libs/translations/translations/zh-Hans.json +++ b/apps/evm/src/libs/translations/translations/zh-Hans.json @@ -1289,7 +1289,9 @@ "title": "本周期已分配的 Prime 总奖励" }, "userRewards": { + "eligibleMessage": "你在本周期已具备分享 Prime 奖励的资格。在下方供给资产以分享奖励。", "marketActions": "打开市场操作", + "notEligibleMessage": "你在本周期暂不具备分享 Prime 奖励的资格。质押 XVS 以在下一周期竞争 Prime。", "title": "你本周期的 Prime 奖励" } }, diff --git a/apps/evm/src/libs/translations/translations/zh-Hant.json b/apps/evm/src/libs/translations/translations/zh-Hant.json index aa44056043..8867afd447 100644 --- a/apps/evm/src/libs/translations/translations/zh-Hant.json +++ b/apps/evm/src/libs/translations/translations/zh-Hant.json @@ -1289,7 +1289,9 @@ "title": "本週期已分配的 Prime 總獎勵" }, "userRewards": { + "eligibleMessage": "你在本週期已具備分享 Prime 獎勵的資格。在下方供給資產以分享獎勵。", "marketActions": "開啟市場操作", + "notEligibleMessage": "你在本週期暫不具備分享 Prime 獎勵的資格。質押 XVS 以在下一週期競爭 Prime。", "title": "你本週期的 Prime 獎勵" } }, diff --git a/apps/evm/src/pages/PrimeLeaderboard/UserRewardsCard/__tests__/index.spec.tsx b/apps/evm/src/pages/PrimeLeaderboard/UserRewardsCard/__tests__/index.spec.tsx index abfb04f527..316662e0fb 100644 --- a/apps/evm/src/pages/PrimeLeaderboard/UserRewardsCard/__tests__/index.spec.tsx +++ b/apps/evm/src/pages/PrimeLeaderboard/UserRewardsCard/__tests__/index.spec.tsx @@ -5,13 +5,13 @@ import { renderComponent } from 'testUtils/render'; import { UserRewardsCard } from '..'; describe('pages/PrimeLeaderboard/UserRewardsCard', () => { - it('renders the user total, per-market rewards and Prime APYs', () => { + it('renders the user total and per-market rewards', () => { renderComponent( , ); @@ -19,6 +19,18 @@ describe('pages/PrimeLeaderboard/UserRewardsCard', () => { expect(screen.getByText('Your Prime rewards this cycle')).toBeInTheDocument(); expect(screen.getByText('$18.4K')).toBeInTheDocument(); expect(screen.getByText(usdc.symbol)).toBeInTheDocument(); - expect(screen.getByText('3.78%')).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 502c7e93ea..36d3edc9d3 100644 --- a/apps/evm/src/pages/PrimeLeaderboard/UserRewardsCard/index.tsx +++ b/apps/evm/src/pages/PrimeLeaderboard/UserRewardsCard/index.tsx @@ -1,10 +1,12 @@ import { cn } from '@venusprotocol/ui'; import primeLogoSrc from 'assets/img/primeLogo.svg'; -import { Icon } from 'components'; +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 { formatCentsToReadableValue, formatPercentageToReadableValue } from 'utilities'; +import { areAddressesEqual, formatCentsToReadableValue } from 'utilities'; import { MarketActions } from '../MarketActions'; import { MarketRewardRow } from '../MarketRewardRow'; @@ -12,21 +14,25 @@ import { MarketRewardRow } from '../MarketRewardRow'; export interface UserMarketReward { token: Token; rewardsCents: number; - apyPercentage: 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 = ({ totalRewardsCents, marketRewards, + content, className, }) => { const { t } = useTranslation(); + const { accountAddress } = useAccountAddress(); + const { data: getPoolsData } = useGetPools({ accountAddress }); return (
= ({ className, )} > -
+

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

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

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

-
+

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

+
+ )}
- {marketRewards.map(({ token, rewardsCents, apyPercentage }) => ( - -
- + {marketRewards.map(({ token, rewardsCents }) => { + const asset = getPoolsData?.pools + .flatMap(pool => pool.assets) + .find(poolAsset => + areAddressesEqual(poolAsset.vToken.underlyingToken.address, token.address), + ); - {formatPercentageToReadableValue(apyPercentage)} -
+ return ( + + {asset && } - - - ))} + +
+ ); + })}
); diff --git a/apps/evm/src/pages/PrimeLeaderboard/UserRewardsSection/index.tsx b/apps/evm/src/pages/PrimeLeaderboard/UserRewardsSection/index.tsx index 30d60b7580..da06e10039 100644 --- a/apps/evm/src/pages/PrimeLeaderboard/UserRewardsSection/index.tsx +++ b/apps/evm/src/pages/PrimeLeaderboard/UserRewardsSection/index.tsx @@ -1,3 +1,9 @@ +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'; @@ -6,12 +12,40 @@ export interface UserRewardsSectionProps { } export const UserRewardsSection: React.FC = ({ className }) => { - const { totalRewardsCents, marketRewards } = useGetPrimeUserRewards(); + 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/useGetPrimeUserRewards/index.ts b/apps/evm/src/pages/PrimeLeaderboard/useGetPrimeUserRewards/index.ts index 37618a19fe..10fee43c0c 100644 --- a/apps/evm/src/pages/PrimeLeaderboard/useGetPrimeUserRewards/index.ts +++ b/apps/evm/src/pages/PrimeLeaderboard/useGetPrimeUserRewards/index.ts @@ -3,14 +3,15 @@ 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]; -const placeholderApyPercentage = 3.78; export const useGetPrimeUserRewards = (): UseGetPrimeUserRewardsOutput => { const tokens = useGetTokens(); @@ -21,10 +22,10 @@ export const useGetPrimeUserRewards = (): UseGetPrimeUserRewardsOutput => { .map((token, index) => ({ token, rewardsCents: placeholderMarketRewardsCents[index], - apyPercentage: placeholderApyPercentage, })); return { + isPrime: placeholderIsPrime, totalRewardsCents: placeholderTotalRewardsCents, marketRewards, }; From 4cb45db544194e3f1c47c513e6506714cad52c75 Mon Sep 17 00:00:00 2001 From: cuzz-venus Date: Mon, 22 Jun 2026 17:10:25 +0800 Subject: [PATCH 4/6] fix: review comments --- apps/evm/src/components/Icon/icons/index.ts | 1 - .../evm/src/components/Icon/icons/sparkle.tsx | 19 ------- .../src/containers/MarketFormModal/index.tsx | 45 +++++++++++++++ apps/evm/src/containers/MarketTable/index.tsx | 33 +++-------- .../libs/translations/translations/en.json | 4 +- .../libs/translations/translations/ja.json | 2 + .../libs/translations/translations/th.json | 2 + .../libs/translations/translations/tr.json | 2 + .../libs/translations/translations/vi.json | 2 + .../translations/translations/zh-Hans.json | 2 + .../translations/translations/zh-Hant.json | 2 + .../PrimeLeaderboard/MarketActions/index.tsx | 57 ------------------- .../__tests__/index.spec.tsx | 12 ++-- .../MarketActionsButton/index.tsx | 45 +++++++++++++++ .../MarketRewardRow/index.tsx | 15 ++++- .../UserRewardsCard/index.tsx | 54 +++++++++++------- .../UserRewardsSection/index.tsx | 8 ++- .../useGetPrimeUserRewards/index.ts | 4 +- 18 files changed, 173 insertions(+), 136 deletions(-) delete mode 100644 apps/evm/src/components/Icon/icons/sparkle.tsx create mode 100644 apps/evm/src/containers/MarketFormModal/index.tsx delete mode 100644 apps/evm/src/pages/PrimeLeaderboard/MarketActions/index.tsx rename apps/evm/src/pages/PrimeLeaderboard/{MarketActions => MarketActionsButton}/__tests__/index.spec.tsx (59%) create mode 100644 apps/evm/src/pages/PrimeLeaderboard/MarketActionsButton/index.tsx diff --git a/apps/evm/src/components/Icon/icons/index.ts b/apps/evm/src/components/Icon/icons/index.ts index 84fe0a1902..f9fade9b09 100644 --- a/apps/evm/src/components/Icon/icons/index.ts +++ b/apps/evm/src/components/Icon/icons/index.ts @@ -67,7 +67,6 @@ 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 deleted file mode 100644 index 7e37b3a109..0000000000 --- a/apps/evm/src/components/Icon/icons/sparkle.tsx +++ /dev/null @@ -1,19 +0,0 @@ -import type { SVGProps } from 'react'; - -const SvgSparkle = (props: SVGProps) => ( - - - -); - -export default SvgSparkle; diff --git a/apps/evm/src/containers/MarketFormModal/index.tsx b/apps/evm/src/containers/MarketFormModal/index.tsx new file mode 100644 index 0000000000..249560562e --- /dev/null +++ b/apps/evm/src/containers/MarketFormModal/index.tsx @@ -0,0 +1,45 @@ +import type { Address } from 'viem'; + +import { Modal, type ModalProps, ProtectionModeIndicator } from 'components'; +// TODO: import OperationForm from containers once it is stable. There's ongoing work happening on +// it, so moving it now could generate conflicts +import { OperationForm, type OperationFormProps } from 'pages/Market/OperationForm'; +import type { Asset } from 'types'; + +export interface MarketFormModalProps { + asset: Asset; + poolComptrollerAddress: Address; + onClose: ModalProps['handleClose']; + onSubmitSuccess?: OperationFormProps['onSubmitSuccess']; +} + +export const MarketFormModal: React.FC = ({ + asset, + poolComptrollerAddress, + onClose, + onSubmitSuccess, +}) => ( + + {asset.isProtectionModeEnabled && ( + + )} + {asset.vToken.underlyingToken.symbol} + + } + handleClose={onClose} + > + + +); diff --git a/apps/evm/src/containers/MarketTable/index.tsx b/apps/evm/src/containers/MarketTable/index.tsx index b9a4e265f5..bb1c509495 100644 --- a/apps/evm/src/containers/MarketTable/index.tsx +++ b/apps/evm/src/containers/MarketTable/index.tsx @@ -3,10 +3,11 @@ import { cn } from '@venusprotocol/ui'; import { useMemo, useState } from 'react'; import type { Address } from 'viem'; -import { Card, Modal, ProtectionModeIndicator, Table, type TableProps } from 'components'; +import { Card, Table, type TableProps } from 'components'; import { routes } from 'constants/routing'; import { Controls } from 'containers/Controls'; import { GatedAssetAcknowledgementModal } from 'containers/GatedAssetAcknowledgementModal'; +import { MarketFormModal } from 'containers/MarketFormModal'; import { SwitchChainNotice } from 'containers/SwitchChainNotice'; import { useBreakpointUp } from 'hooks/responsive'; import { useCollateral } from 'hooks/useCollateral'; @@ -14,9 +15,6 @@ import { useUserChainSettings } from 'hooks/useUserChainSettings'; import { handleError } from 'libs/errors'; import { useTranslation } from 'libs/translations'; import { useAccountChainId, useChainId } from 'libs/wallet'; -// TODO: move OperationForm to containers once it is stable. There's ongoing work happening on it, -// so moving it now could generate conflicts -import { OperationForm } from 'pages/Market/OperationForm'; import type { Asset, EModeGroup } from 'types'; import { RowControl } from './RowControl'; import pauseIconSrc from './pause.svg'; @@ -233,28 +231,11 @@ export const MarketTable: React.FC = ({ )} {selectedAsset && !shouldDisplayGatedAssetsAcknowledgementModal && ( - - {selectedAsset.isProtectionModeEnabled && ( - - )} - {selectedAsset.vToken.underlyingToken.symbol} - - } - handleClose={handleCloseMarketModal} - > - - + )} ); diff --git a/apps/evm/src/libs/translations/translations/en.json b/apps/evm/src/libs/translations/translations/en.json index d519bf2c54..7cfc6e6297 100644 --- a/apps/evm/src/libs/translations/translations/en.json +++ b/apps/evm/src/libs/translations/translations/en.json @@ -1291,7 +1291,9 @@ "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.", + "notEligibleMessage": "You are currently NOT eligible for sharing the Prime rewards during this cycle. Stake XVS to compete for Prime in the next cycle.", + "primeLogoAlt": "Venus Prime logo", + "rewardShareAriaLabel": "Share of total rewards", "title": "Your Prime rewards this cycle" } }, diff --git a/apps/evm/src/libs/translations/translations/ja.json b/apps/evm/src/libs/translations/translations/ja.json index 3cc5f0a989..62be053d73 100644 --- a/apps/evm/src/libs/translations/translations/ja.json +++ b/apps/evm/src/libs/translations/translations/ja.json @@ -1292,6 +1292,8 @@ "eligibleMessage": "今サイクルで Prime 報酬を分け合う対象になっています。下記の資産を供給して報酬を獲得しましょう。", "marketActions": "マーケット操作を開く", "notEligibleMessage": "今サイクルでは Prime 報酬を分け合う対象ではありません。XVS をステークして次のサイクルで Prime を目指しましょう。", + "primeLogoAlt": "Venus Prime ロゴ", + "rewardShareAriaLabel": "報酬総額に占める割合", "title": "今サイクルのあなたの Prime 報酬" } }, diff --git a/apps/evm/src/libs/translations/translations/th.json b/apps/evm/src/libs/translations/translations/th.json index a0754432a7..898e60e397 100644 --- a/apps/evm/src/libs/translations/translations/th.json +++ b/apps/evm/src/libs/translations/translations/th.json @@ -1292,6 +1292,8 @@ "eligibleMessage": "คุณมีสิทธิ์ร่วมรับรางวัล Prime ในรอบนี้ จัดหาสินทรัพย์ด้านล่างเพื่อรับรางวัล", "marketActions": "เปิดการดำเนินการของตลาด", "notEligibleMessage": "คุณยังไม่มีสิทธิ์ร่วมรับรางวัล Prime ในรอบนี้ Stake XVS เพื่อแข่งขันรับ Prime ในรอบถัดไป", + "primeLogoAlt": "โลโก้ Venus Prime", + "rewardShareAriaLabel": "สัดส่วนของรางวัลทั้งหมด", "title": "รางวัล Prime ของคุณในรอบนี้" } }, diff --git a/apps/evm/src/libs/translations/translations/tr.json b/apps/evm/src/libs/translations/translations/tr.json index 625377a79e..e87f04fd65 100644 --- a/apps/evm/src/libs/translations/translations/tr.json +++ b/apps/evm/src/libs/translations/translations/tr.json @@ -1292,6 +1292,8 @@ "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.", + "primeLogoAlt": "Venus Prime logosu", + "rewardShareAriaLabel": "Toplam ödüllerdeki pay", "title": "Bu döngüdeki Prime ödülleriniz" } }, diff --git a/apps/evm/src/libs/translations/translations/vi.json b/apps/evm/src/libs/translations/translations/vi.json index db3d6bfad4..d42600c2ab 100644 --- a/apps/evm/src/libs/translations/translations/vi.json +++ b/apps/evm/src/libs/translations/translations/vi.json @@ -1292,6 +1292,8 @@ "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.", + "primeLogoAlt": "Logo Venus Prime", + "rewardShareAriaLabel": "Tỷ lệ trên tổng phần thưởng", "title": "Phần thưởng Prime của bạn trong chu kỳ này" } }, diff --git a/apps/evm/src/libs/translations/translations/zh-Hans.json b/apps/evm/src/libs/translations/translations/zh-Hans.json index b84c212f27..6bbf067e12 100644 --- a/apps/evm/src/libs/translations/translations/zh-Hans.json +++ b/apps/evm/src/libs/translations/translations/zh-Hans.json @@ -1292,6 +1292,8 @@ "eligibleMessage": "你在本周期已具备分享 Prime 奖励的资格。在下方供给资产以分享奖励。", "marketActions": "打开市场操作", "notEligibleMessage": "你在本周期暂不具备分享 Prime 奖励的资格。质押 XVS 以在下一周期竞争 Prime。", + "primeLogoAlt": "Venus Prime 标志", + "rewardShareAriaLabel": "占总奖励的比例", "title": "你本周期的 Prime 奖励" } }, diff --git a/apps/evm/src/libs/translations/translations/zh-Hant.json b/apps/evm/src/libs/translations/translations/zh-Hant.json index 8867afd447..2168a5a20e 100644 --- a/apps/evm/src/libs/translations/translations/zh-Hant.json +++ b/apps/evm/src/libs/translations/translations/zh-Hant.json @@ -1292,6 +1292,8 @@ "eligibleMessage": "你在本週期已具備分享 Prime 獎勵的資格。在下方供給資產以分享獎勵。", "marketActions": "開啟市場操作", "notEligibleMessage": "你在本週期暫不具備分享 Prime 獎勵的資格。質押 XVS 以在下一週期競爭 Prime。", + "primeLogoAlt": "Venus Prime 標誌", + "rewardShareAriaLabel": "佔總獎勵的比例", "title": "你本週期的 Prime 獎勵" } }, diff --git a/apps/evm/src/pages/PrimeLeaderboard/MarketActions/index.tsx b/apps/evm/src/pages/PrimeLeaderboard/MarketActions/index.tsx deleted file mode 100644 index 7209a09584..0000000000 --- a/apps/evm/src/pages/PrimeLeaderboard/MarketActions/index.tsx +++ /dev/null @@ -1,57 +0,0 @@ -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/MarketActions/__tests__/index.spec.tsx b/apps/evm/src/pages/PrimeLeaderboard/MarketActionsButton/__tests__/index.spec.tsx similarity index 59% rename from apps/evm/src/pages/PrimeLeaderboard/MarketActions/__tests__/index.spec.tsx rename to apps/evm/src/pages/PrimeLeaderboard/MarketActionsButton/__tests__/index.spec.tsx index dd770f40f9..f5a859de82 100644 --- a/apps/evm/src/pages/PrimeLeaderboard/MarketActions/__tests__/index.spec.tsx +++ b/apps/evm/src/pages/PrimeLeaderboard/MarketActionsButton/__tests__/index.spec.tsx @@ -1,17 +1,21 @@ import { fireEvent, screen } from '@testing-library/react'; -import { xvs } from '__mocks__/models/tokens'; +import { assetData } from '__mocks__/models/asset'; import { renderComponent } from 'testUtils/render'; -import { MarketActions } from '..'; +import { MarketActionsButton } from '..'; vi.mock('pages/Market/OperationForm', () => ({ OperationForm: () =>
, })); -describe('pages/PrimeLeaderboard/MarketActions', () => { +describe('pages/PrimeLeaderboard/MarketActionsButton', () => { it('opens the market operation modal when clicked', async () => { - renderComponent(); + const asset = assetData[0]; + + renderComponent( + , + ); fireEvent.click(screen.getByLabelText('Open market actions')); diff --git a/apps/evm/src/pages/PrimeLeaderboard/MarketActionsButton/index.tsx b/apps/evm/src/pages/PrimeLeaderboard/MarketActionsButton/index.tsx new file mode 100644 index 0000000000..a433d5e3c9 --- /dev/null +++ b/apps/evm/src/pages/PrimeLeaderboard/MarketActionsButton/index.tsx @@ -0,0 +1,45 @@ +import { useState } from 'react'; +import type { Address } from 'viem'; + +import { Icon } from 'components'; +import { MarketFormModal } from 'containers/MarketFormModal'; +import { useTranslation } from 'libs/translations'; +import type { Asset } from 'types'; + +export interface MarketActionsButtonProps { + asset: Asset; + poolComptrollerAddress: Address; +} + +export const MarketActionsButton: React.FC = ({ + asset, + poolComptrollerAddress, +}) => { + const { t } = useTranslation(); + const [isModalOpen, setIsModalOpen] = useState(false); + + return ( + <> + + + {isModalOpen && ( + setIsModalOpen(false)} + onSubmitSuccess={() => setIsModalOpen(false)} + /> + )} + + ); +}; diff --git a/apps/evm/src/pages/PrimeLeaderboard/MarketRewardRow/index.tsx b/apps/evm/src/pages/PrimeLeaderboard/MarketRewardRow/index.tsx index cd02999684..d7a8a549d4 100644 --- a/apps/evm/src/pages/PrimeLeaderboard/MarketRewardRow/index.tsx +++ b/apps/evm/src/pages/PrimeLeaderboard/MarketRewardRow/index.tsx @@ -1,4 +1,5 @@ -import { TokenIconWithSymbol } from 'components'; +import { ProgressBar, TokenIconWithSymbol } from 'components'; +import { useTranslation } from 'libs/translations'; import type { Token } from 'types'; import { formatCentsToReadableValue } from 'utilities'; @@ -15,6 +16,8 @@ export const MarketRewardRow: React.FC = ({ totalRewardsCents, children, }) => { + const { t } = useTranslation(); + const progressPercentage = totalRewardsCents > 0 ? Math.min(100, (rewardsCents / totalRewardsCents) * 100) : 0; @@ -26,8 +29,14 @@ export const MarketRewardRow: React.FC = ({ {formatCentsToReadableValue({ value: rewardsCents })} -
-
+
+
{children} diff --git a/apps/evm/src/pages/PrimeLeaderboard/UserRewardsCard/index.tsx b/apps/evm/src/pages/PrimeLeaderboard/UserRewardsCard/index.tsx index 36d3edc9d3..d9ae42460b 100644 --- a/apps/evm/src/pages/PrimeLeaderboard/UserRewardsCard/index.tsx +++ b/apps/evm/src/pages/PrimeLeaderboard/UserRewardsCard/index.tsx @@ -8,7 +8,7 @@ import { useAccountAddress } from 'libs/wallet'; import type { Token } from 'types'; import { areAddressesEqual, formatCentsToReadableValue } from 'utilities'; -import { MarketActions } from '../MarketActions'; +import { MarketActionsButton } from '../MarketActionsButton'; import { MarketRewardRow } from '../MarketRewardRow'; export interface UserMarketReward { @@ -34,6 +34,20 @@ export const UserRewardsCard: React.FC = ({ const { accountAddress } = useAccountAddress(); const { data: getPoolsData } = useGetPools({ accountAddress }); + const marketRewardsWithMarket = marketRewards.map(marketReward => { + const pool = getPoolsData?.pools.find(currentPool => + currentPool.assets.some(poolAsset => + areAddressesEqual(poolAsset.vToken.underlyingToken.address, marketReward.token.address), + ), + ); + + const asset = pool?.assets.find(poolAsset => + areAddressesEqual(poolAsset.vToken.underlyingToken.address, marketReward.token.address), + ); + + return { ...marketReward, asset, poolComptrollerAddress: pool?.comptrollerAddress }; + }); + return (
= ({ {content ?? (
- + {t('primeLeaderboard.userRewards.primeLogoAlt')}

@@ -58,26 +76,20 @@ export const UserRewardsCard: React.FC = ({

- {marketRewards.map(({ token, rewardsCents }) => { - const asset = getPoolsData?.pools - .flatMap(pool => pool.assets) - .find(poolAsset => - areAddressesEqual(poolAsset.vToken.underlyingToken.address, token.address), - ); - - return ( - - {asset && } + {marketRewardsWithMarket.map(({ token, rewardsCents, asset, poolComptrollerAddress }) => ( + + {asset && } - - - ); - })} + {asset && poolComptrollerAddress && ( + + )} + + ))}
); diff --git a/apps/evm/src/pages/PrimeLeaderboard/UserRewardsSection/index.tsx b/apps/evm/src/pages/PrimeLeaderboard/UserRewardsSection/index.tsx index da06e10039..85b3f2e178 100644 --- a/apps/evm/src/pages/PrimeLeaderboard/UserRewardsSection/index.tsx +++ b/apps/evm/src/pages/PrimeLeaderboard/UserRewardsSection/index.tsx @@ -24,10 +24,14 @@ export const UserRewardsSection: React.FC = ({ classNam
{isPrime ? ( - + {t('primeLeaderboard.userRewards.primeLogoAlt')} ) : ( - + )} diff --git a/apps/evm/src/pages/PrimeLeaderboard/useGetPrimeUserRewards/index.ts b/apps/evm/src/pages/PrimeLeaderboard/useGetPrimeUserRewards/index.ts index 10fee43c0c..6fa2690b0f 100644 --- a/apps/evm/src/pages/PrimeLeaderboard/useGetPrimeUserRewards/index.ts +++ b/apps/evm/src/pages/PrimeLeaderboard/useGetPrimeUserRewards/index.ts @@ -9,8 +9,8 @@ export interface UseGetPrimeUserRewardsOutput { } // TODO: replace these placeholder values with the data returned by the API -const placeholderIsPrime = true; -const placeholderTotalRewardsCents = 1_840_000; +const placeholderIsPrime = false; +const placeholderTotalRewardsCents = 0; const placeholderMarketRewardsCents = [1_140_000, 700_000]; export const useGetPrimeUserRewards = (): UseGetPrimeUserRewardsOutput => { From 7b0e6be8e69f2e88a3886b9a977301e008abcaa2 Mon Sep 17 00:00:00 2001 From: cuzz-venus Date: Mon, 22 Jun 2026 18:09:51 +0800 Subject: [PATCH 5/6] fix: review comments --- .../pages/PrimeLeaderboard/useGetPrimeUserRewards/index.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/evm/src/pages/PrimeLeaderboard/useGetPrimeUserRewards/index.ts b/apps/evm/src/pages/PrimeLeaderboard/useGetPrimeUserRewards/index.ts index 6fa2690b0f..10fee43c0c 100644 --- a/apps/evm/src/pages/PrimeLeaderboard/useGetPrimeUserRewards/index.ts +++ b/apps/evm/src/pages/PrimeLeaderboard/useGetPrimeUserRewards/index.ts @@ -9,8 +9,8 @@ export interface UseGetPrimeUserRewardsOutput { } // TODO: replace these placeholder values with the data returned by the API -const placeholderIsPrime = false; -const placeholderTotalRewardsCents = 0; +const placeholderIsPrime = true; +const placeholderTotalRewardsCents = 1_840_000; const placeholderMarketRewardsCents = [1_140_000, 700_000]; export const useGetPrimeUserRewards = (): UseGetPrimeUserRewardsOutput => { From 6085a576de2d36c677672edf8b1ff64aa574bad0 Mon Sep 17 00:00:00 2001 From: cuzz-venus Date: Tue, 23 Jun 2026 01:35:41 +0800 Subject: [PATCH 6/6] fix: review comments --- .../src/containers/MarketFormModal/index.tsx | 60 +++++++++++-------- apps/evm/src/containers/MarketTable/index.tsx | 12 +--- 2 files changed, 36 insertions(+), 36 deletions(-) diff --git a/apps/evm/src/containers/MarketFormModal/index.tsx b/apps/evm/src/containers/MarketFormModal/index.tsx index 249560562e..990621582f 100644 --- a/apps/evm/src/containers/MarketFormModal/index.tsx +++ b/apps/evm/src/containers/MarketFormModal/index.tsx @@ -1,6 +1,8 @@ import type { Address } from 'viem'; import { Modal, type ModalProps, ProtectionModeIndicator } from 'components'; +import { GatedAssetAcknowledgementModal } from 'containers/GatedAssetAcknowledgementModal'; +import { useUserChainSettings } from 'hooks/useUserChainSettings'; // TODO: import OperationForm from containers once it is stable. There's ongoing work happening on // it, so moving it now could generate conflicts import { OperationForm, type OperationFormProps } from 'pages/Market/OperationForm'; @@ -18,28 +20,36 @@ export const MarketFormModal: React.FC = ({ poolComptrollerAddress, onClose, onSubmitSuccess, -}) => ( - - {asset.isProtectionModeEnabled && ( - - )} - {asset.vToken.underlyingToken.symbol} - - } - handleClose={onClose} - > - - -); +}) => { + const [userChainSettings] = useUserChainSettings(); + + if (asset.isGated && !userChainSettings.doNotShowGatedAssetModal) { + return ; + } + + return ( + + {asset.isProtectionModeEnabled && ( + + )} + {asset.vToken.underlyingToken.symbol} + + } + handleClose={onClose} + > + + + ); +}; diff --git a/apps/evm/src/containers/MarketTable/index.tsx b/apps/evm/src/containers/MarketTable/index.tsx index bb1c509495..a88d0d51e0 100644 --- a/apps/evm/src/containers/MarketTable/index.tsx +++ b/apps/evm/src/containers/MarketTable/index.tsx @@ -6,12 +6,10 @@ import type { Address } from 'viem'; import { Card, Table, type TableProps } from 'components'; import { routes } from 'constants/routing'; import { Controls } from 'containers/Controls'; -import { GatedAssetAcknowledgementModal } from 'containers/GatedAssetAcknowledgementModal'; import { MarketFormModal } from 'containers/MarketFormModal'; import { SwitchChainNotice } from 'containers/SwitchChainNotice'; import { useBreakpointUp } from 'hooks/responsive'; import { useCollateral } from 'hooks/useCollateral'; -import { useUserChainSettings } from 'hooks/useUserChainSettings'; import { handleError } from 'libs/errors'; import { useTranslation } from 'libs/translations'; import { useAccountChainId, useChainId } from 'libs/wallet'; @@ -75,10 +73,6 @@ export const MarketTable: React.FC = ({ const { toggleCollateral } = useCollateral(); - const [userChainSettings] = useUserChainSettings(); - const shouldDisplayGatedAssetsAcknowledgementModal = - selectedAsset?.isGated && !userChainSettings.doNotShowGatedAssetModal; - // The fallback breakpoint is just to satisfy TS here, it is not actually used const _isBreakpointUp = useBreakpointUp(breakpoint || '2xl'); const isBreakpointUp = !!breakpoint && _isBreakpointUp; @@ -226,11 +220,7 @@ export const MarketTable: React.FC = ({ {...otherTableProps} /> - {shouldDisplayGatedAssetsAcknowledgementModal && ( - setSelectedAsset(undefined)} /> - )} - - {selectedAsset && !shouldDisplayGatedAssetsAcknowledgementModal && ( + {selectedAsset && (