From 226f239ae7fec1a6b24c9127275d62fffbe192c8 Mon Sep 17 00:00:00 2001 From: cuzz-venus Date: Wed, 10 Jun 2026 21:28:36 +0800 Subject: [PATCH 01/18] feat: support prime total rewards card --- .gitignore | 4 + .../libs/translations/translations/en.json | 5 +- .../libs/translations/translations/ja.json | 5 +- .../libs/translations/translations/th.json | 5 +- .../libs/translations/translations/tr.json | 5 +- .../libs/translations/translations/vi.json | 5 +- .../translations/translations/zh-Hans.json | 5 +- .../translations/translations/zh-Hant.json | 5 +- .../TotalRewardsCard/index.tsx | 69 ++++++++++++--- .../PrimeLeaderboard/__tests__/index.spec.tsx | 12 ++- apps/evm/src/pages/PrimeLeaderboard/index.tsx | 84 ++++++++++++------- 11 files changed, 156 insertions(+), 48 deletions(-) diff --git a/.gitignore b/.gitignore index 95193f1fef..fc73fb49db 100644 --- a/.gitignore +++ b/.gitignore @@ -46,3 +46,7 @@ Thumbs.db # AI development artifacts .ui-develop + +# local file +*.local +*.local.* diff --git a/apps/evm/src/libs/translations/translations/en.json b/apps/evm/src/libs/translations/translations/en.json index 6fec5fb946..af7bdb73ce 100644 --- a/apps/evm/src/libs/translations/translations/en.json +++ b/apps/evm/src/libs/translations/translations/en.json @@ -1247,7 +1247,10 @@ }, "primeLeaderboard": { "description": "Stake XVS into the governance vault and receive Prime rewards on top of your yields.", - "title": "Prime leaderboard" + "title": "Prime leaderboard", + "totalRewards": { + "title": "Total Prime rewards distributed 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 044d230983..a964bf2ff0 100644 --- a/apps/evm/src/libs/translations/translations/ja.json +++ b/apps/evm/src/libs/translations/translations/ja.json @@ -1247,7 +1247,10 @@ }, "primeLeaderboard": { "description": "ガバナンスボールトに XVS をステークして、利回りに加えて Prime 報酬を獲得しましょう。", - "title": "Prime リーダーボード" + "title": "Prime リーダーボード", + "totalRewards": { + "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 79cdf03251..b9d64837fb 100644 --- a/apps/evm/src/libs/translations/translations/th.json +++ b/apps/evm/src/libs/translations/translations/th.json @@ -1247,7 +1247,10 @@ }, "primeLeaderboard": { "description": "สเตก XVS เข้าสู่ governance vault แล้วรับรางวัล Prime เพิ่มเติมจากผลตอบแทนของคุณ", - "title": "กระดานผู้นำ Prime" + "title": "กระดานผู้นำ Prime", + "totalRewards": { + "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 95094edb00..7044903c2c 100644 --- a/apps/evm/src/libs/translations/translations/tr.json +++ b/apps/evm/src/libs/translations/translations/tr.json @@ -1247,7 +1247,10 @@ }, "primeLeaderboard": { "description": "XVS’yi governance kasasına stake edin ve getirilerinize ek olarak Prime ödülleri kazanın.", - "title": "Prime lider tablosu" + "title": "Prime lider tablosu", + "totalRewards": { + "title": "Bu döngüde dağıtılan toplam Prime ödülü" + } }, "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 9407ef6b6f..39ddc919a1 100644 --- a/apps/evm/src/libs/translations/translations/vi.json +++ b/apps/evm/src/libs/translations/translations/vi.json @@ -1247,7 +1247,10 @@ }, "primeLeaderboard": { "description": "Stake XVS vào governance vault và nhận thêm phần thưởng Prime bên cạnh lợi nhuận của bạn.", - "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" + } }, "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 71205bd22c..c27d5afc91 100644 --- a/apps/evm/src/libs/translations/translations/zh-Hans.json +++ b/apps/evm/src/libs/translations/translations/zh-Hans.json @@ -1247,7 +1247,10 @@ }, "primeLeaderboard": { "description": "将 XVS 质押到治理金库,在收益之上再赚取 Prime 奖励。", - "title": "Prime 排行榜" + "title": "Prime 排行榜", + "totalRewards": { + "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 32c6c54f00..ff5db769a8 100644 --- a/apps/evm/src/libs/translations/translations/zh-Hant.json +++ b/apps/evm/src/libs/translations/translations/zh-Hant.json @@ -1247,7 +1247,10 @@ }, "primeLeaderboard": { "description": "將 XVS 質押到治理金庫,在收益之上再賺取 Prime 獎勵。", - "title": "Prime 排行榜" + "title": "Prime 排行榜", + "totalRewards": { + "title": "本週期已分配的 Prime 總獎勵" + } }, "primeStatusBanner": { "becomePrimeTitle": "你現在可以成為 Prime 用戶", diff --git a/apps/evm/src/pages/PrimeLeaderboard/TotalRewardsCard/index.tsx b/apps/evm/src/pages/PrimeLeaderboard/TotalRewardsCard/index.tsx index 7fdf44f630..2c65e52d12 100644 --- a/apps/evm/src/pages/PrimeLeaderboard/TotalRewardsCard/index.tsx +++ b/apps/evm/src/pages/PrimeLeaderboard/TotalRewardsCard/index.tsx @@ -1,16 +1,65 @@ import { cn } from '@venusprotocol/ui'; +import { TokenIconWithSymbol } from 'components'; +import { useTranslation } from 'libs/translations'; +import type { Token } from 'types'; +import { formatCentsToReadableValue } from 'utilities'; + +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 }) => ( +
+ + +
+ + {formatCentsToReadableValue({ value: rewardsCents })} + + +
+
+
+
+
+ ))} +
+
+ ); +}; diff --git a/apps/evm/src/pages/PrimeLeaderboard/__tests__/index.spec.tsx b/apps/evm/src/pages/PrimeLeaderboard/__tests__/index.spec.tsx index 788f8c4fd1..318e719c9c 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 '..'; @@ -37,8 +38,8 @@ 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(); @@ -48,4 +49,11 @@ describe('pages/PrimeLeaderboard', () => { 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-card')).toBeInTheDocument(); + expect(screen.queryByTestId('user-rewards-card')).not.toBeInTheDocument(); + }); }); diff --git a/apps/evm/src/pages/PrimeLeaderboard/index.tsx b/apps/evm/src/pages/PrimeLeaderboard/index.tsx index a596561f62..2aac3fd879 100644 --- a/apps/evm/src/pages/PrimeLeaderboard/index.tsx +++ b/apps/evm/src/pages/PrimeLeaderboard/index.tsx @@ -1,4 +1,6 @@ import { Card, Page } from 'components'; +import { useGetTokens } from 'libs/tokens'; +import { useAccountAddress } from 'libs/wallet'; import { EndOfCycle } from './EndOfCycle'; import { Hero } from './Hero'; @@ -8,43 +10,67 @@ import { RewardTable } from './RewardTable'; import { TotalRewardsCard } from './TotalRewardsCard'; import { UserRewardsCard } from './UserRewardsCard'; -const PrimeLeaderboard: React.FC = () => ( - -
-
-
+// TODO: use the reward pool data returned by the API +const placeholderTotalRewardsCents = 46_230_000; +const placeholderMarketRewardsCents = [28_040_000, 17_190_000]; + +const PrimeLeaderboard: React.FC = () => { + const { accountAddress } = useAccountAddress(); + const tokens = useGetTokens(); + + const marketRewards = tokens + .slice(0, placeholderMarketRewardsCents.length) + .map((token, index) => ({ token, rewardsCents: placeholderMarketRewardsCents[index] })); + + const totalRewardsCard = ( + + ); + + return ( + +
+
+
+
-
- + - + -

- Refreshed daily · Last refresh: 6h ago -

+

+ Refreshed daily · Last refresh: 6h ago +

-
- -
- +
+ + {accountAddress ? ( +
+ {totalRewardsCard} - -
+ +
+ ) : ( + totalRewardsCard + )} - - + + - - + + - - -
- -); + +
+
+
+ ); +}; export default PrimeLeaderboard; From f0b0494b8cafe15344a363d6db0fb5a8d9aacaa5 Mon Sep 17 00:00:00 2001 From: cuzz-venus Date: Wed, 10 Jun 2026 21:29:22 +0800 Subject: [PATCH 02/18] feat: support prime total rewards card --- .changeset/wise-toes-thank.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/wise-toes-thank.md diff --git a/.changeset/wise-toes-thank.md b/.changeset/wise-toes-thank.md new file mode 100644 index 0000000000..d05ff03ad2 --- /dev/null +++ b/.changeset/wise-toes-thank.md @@ -0,0 +1,5 @@ +--- +"@venusprotocol/evm": minor +--- + +feat: support prime toatal rewards card From f8eca10e4d3c23d332a09a3ea1d2579b9bbc156e Mon Sep 17 00:00:00 2001 From: cuzz-venus Date: Thu, 11 Jun 2026 19:24:31 +0800 Subject: [PATCH 03/18] feat: support prime total rewards card --- .changeset/wise-toes-thank.md | 2 +- .../MarketRewardRow/__tests__/index.spec.tsx | 19 ++++++++++ .../MarketRewardRow/index.tsx | 36 +++++++++++++++++++ .../TotalRewardsCard/__tests__/index.spec.tsx | 26 ++++++++++++++ .../TotalRewardsCard/index.tsx | 27 +++++--------- 5 files changed, 90 insertions(+), 20 deletions(-) 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 diff --git a/.changeset/wise-toes-thank.md b/.changeset/wise-toes-thank.md index d05ff03ad2..10724a30a8 100644 --- a/.changeset/wise-toes-thank.md +++ b/.changeset/wise-toes-thank.md @@ -2,4 +2,4 @@ "@venusprotocol/evm": minor --- -feat: support prime toatal rewards card +feat: add Prime total rewards card 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..bb026e85f6 --- /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, +}) => ( +
+ + +
+ + {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 2c65e52d12..b3b573a472 100644 --- a/apps/evm/src/pages/PrimeLeaderboard/TotalRewardsCard/index.tsx +++ b/apps/evm/src/pages/PrimeLeaderboard/TotalRewardsCard/index.tsx @@ -1,10 +1,11 @@ import { cn } from '@venusprotocol/ui'; -import { TokenIconWithSymbol } from 'components'; 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; @@ -40,24 +41,12 @@ export const TotalRewardsCard: React.FC = ({
{marketRewards.map(({ token, rewardsCents }) => ( -
- - -
- - {formatCentsToReadableValue({ value: rewardsCents })} - - -
-
-
-
-
+ ))}
From 40d46c5d349546ca4487a495fd75f92570697323 Mon Sep 17 00:00:00 2001 From: cuzz-venus Date: Thu, 11 Jun 2026 20:25:23 +0800 Subject: [PATCH 04/18] feat: support prime total rewards card --- .../MarketRewardRow/index.tsx | 26 +++++++++---------- 1 file changed, 12 insertions(+), 14 deletions(-) diff --git a/apps/evm/src/pages/PrimeLeaderboard/MarketRewardRow/index.tsx b/apps/evm/src/pages/PrimeLeaderboard/MarketRewardRow/index.tsx index bb026e85f6..4fa5abcf7c 100644 --- a/apps/evm/src/pages/PrimeLeaderboard/MarketRewardRow/index.tsx +++ b/apps/evm/src/pages/PrimeLeaderboard/MarketRewardRow/index.tsx @@ -15,22 +15,20 @@ export const MarketRewardRow: React.FC = ({ totalRewardsCents, children, }) => ( -
- +
+ -
- - {formatCentsToReadableValue({ value: rewardsCents })} - + + {formatCentsToReadableValue({ value: rewardsCents })} + -
-
-
- - {children} +
+
+ + {children}
); From cf7bc7f0d82f55ec2ceef7607d02077bf23d76a8 Mon Sep 17 00:00:00 2001 From: cuzz-venus Date: Thu, 11 Jun 2026 20:37:51 +0800 Subject: [PATCH 05/18] feat: support prime total rewards card --- .../MarketRewardRow/index.tsx | 32 ++++++++++--------- apps/evm/src/pages/PrimeLeaderboard/index.tsx | 2 ++ 2 files changed, 19 insertions(+), 15 deletions(-) diff --git a/apps/evm/src/pages/PrimeLeaderboard/MarketRewardRow/index.tsx b/apps/evm/src/pages/PrimeLeaderboard/MarketRewardRow/index.tsx index 4fa5abcf7c..cd02999684 100644 --- a/apps/evm/src/pages/PrimeLeaderboard/MarketRewardRow/index.tsx +++ b/apps/evm/src/pages/PrimeLeaderboard/MarketRewardRow/index.tsx @@ -14,21 +14,23 @@ export const MarketRewardRow: React.FC = ({ rewardsCents, totalRewardsCents, children, -}) => ( -
- +}) => { + const progressPercentage = + totalRewardsCents > 0 ? Math.min(100, (rewardsCents / totalRewardsCents) * 100) : 0; - - {formatCentsToReadableValue({ value: rewardsCents })} - + return ( +
+ -
-
-
+ + {formatCentsToReadableValue({ value: rewardsCents })} + + +
+
+
- {children} -
-); + {children} +
+ ); +}; diff --git a/apps/evm/src/pages/PrimeLeaderboard/index.tsx b/apps/evm/src/pages/PrimeLeaderboard/index.tsx index 2aac3fd879..85e484e853 100644 --- a/apps/evm/src/pages/PrimeLeaderboard/index.tsx +++ b/apps/evm/src/pages/PrimeLeaderboard/index.tsx @@ -18,6 +18,8 @@ const PrimeLeaderboard: React.FC = () => { const { accountAddress } = useAccountAddress(); const tokens = useGetTokens(); + // TODO: replace these placeholder tokens (currently the first tokens from the list, unrelated + // to the actual Prime reward markets) with the real markets returned by the API const marketRewards = tokens .slice(0, placeholderMarketRewardsCents.length) .map((token, index) => ({ token, rewardsCents: placeholderMarketRewardsCents[index] })); From 3b75ee765f784854419b49f97443ccea67d38ba1 Mon Sep 17 00:00:00 2001 From: cuzz-venus Date: Sun, 14 Jun 2026 17:49:07 +0800 Subject: [PATCH 06/18] feat: support prime user 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 | 18 +++++ .../libs/translations/translations/ja.json | 18 +++++ .../libs/translations/translations/th.json | 18 +++++ .../libs/translations/translations/tr.json | 18 +++++ .../libs/translations/translations/vi.json | 18 +++++ .../translations/translations/zh-Hans.json | 18 +++++ .../translations/translations/zh-Hant.json | 18 +++++ .../UserRewardsCard/__tests__/index.spec.tsx | 24 ++++++ .../UserRewardsCard/index.tsx | 81 ++++++++++++++++--- apps/evm/src/pages/PrimeLeaderboard/index.tsx | 12 ++- 13 files changed, 281 insertions(+), 11 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/UserRewardsCard/__tests__/index.spec.tsx 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 af7bdb73ce..94f405a43e 100644 --- a/apps/evm/src/libs/translations/translations/en.json +++ b/apps/evm/src/libs/translations/translations/en.json @@ -1250,6 +1250,24 @@ "title": "Prime leaderboard", "totalRewards": { "title": "Total Prime rewards distributed this cycle" + }, + "userRewards": { + "marketActions": "Open market actions", + "title": "Your Prime rewards this cycle" + }, + "rankTable": { + "columns": { + "wallet": "Wallet", + "primeScore": "Prime score" + }, + "walletTooltip": "Users ranked by Prime score. The top 500 wallets become eligible for Prime in the next cycle." + }, + "rewardTable": { + "columns": { + "wallet": "Wallet", + "totalRewards": "Total rewards", + "marketRewards": "{{symbol}} rewards" + } } }, "primeStatusBanner": { diff --git a/apps/evm/src/libs/translations/translations/ja.json b/apps/evm/src/libs/translations/translations/ja.json index a964bf2ff0..29556e613c 100644 --- a/apps/evm/src/libs/translations/translations/ja.json +++ b/apps/evm/src/libs/translations/translations/ja.json @@ -1250,6 +1250,24 @@ "title": "Prime リーダーボード", "totalRewards": { "title": "今サイクルに分配された Prime 報酬の総額" + }, + "userRewards": { + "marketActions": "マーケット操作を開く", + "title": "今サイクルのあなたの Prime 報酬" + }, + "rankTable": { + "columns": { + "wallet": "ウォレット", + "primeScore": "Prime スコア" + }, + "walletTooltip": "Prime スコア順のランキングです。上位 500 のウォレットが次のサイクルで Prime の対象になります。" + }, + "rewardTable": { + "columns": { + "wallet": "ウォレット", + "totalRewards": "報酬合計", + "marketRewards": "{{symbol}} の報酬" + } } }, "primeStatusBanner": { diff --git a/apps/evm/src/libs/translations/translations/th.json b/apps/evm/src/libs/translations/translations/th.json index b9d64837fb..a498ceaaef 100644 --- a/apps/evm/src/libs/translations/translations/th.json +++ b/apps/evm/src/libs/translations/translations/th.json @@ -1250,6 +1250,24 @@ "title": "กระดานผู้นำ Prime", "totalRewards": { "title": "รางวัล Prime ทั้งหมดที่แจกในรอบนี้" + }, + "userRewards": { + "marketActions": "เปิดการดำเนินการของตลาด", + "title": "รางวัล Prime ของคุณในรอบนี้" + }, + "rankTable": { + "columns": { + "wallet": "กระเป๋าเงิน", + "primeScore": "คะแนน Prime" + }, + "walletTooltip": "ผู้ใช้จัดอันดับตามคะแนน Prime กระเป๋าเงิน 500 อันดับแรกจะมีสิทธิ์รับ Prime ในรอบถัดไป" + }, + "rewardTable": { + "columns": { + "wallet": "กระเป๋าเงิน", + "totalRewards": "รางวัลทั้งหมด", + "marketRewards": "รางวัล {{symbol}}" + } } }, "primeStatusBanner": { diff --git a/apps/evm/src/libs/translations/translations/tr.json b/apps/evm/src/libs/translations/translations/tr.json index 7044903c2c..a34122f083 100644 --- a/apps/evm/src/libs/translations/translations/tr.json +++ b/apps/evm/src/libs/translations/translations/tr.json @@ -1250,6 +1250,24 @@ "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" + }, + "rankTable": { + "columns": { + "wallet": "Cüzdan", + "primeScore": "Prime puanı" + }, + "walletTooltip": "Kullanıcılar Prime puanına göre sıralanır. İlk 500 cüzdan bir sonraki döngüde Prime için uygun olur." + }, + "rewardTable": { + "columns": { + "wallet": "Cüzdan", + "totalRewards": "Toplam ödüller", + "marketRewards": "{{symbol}} ödülleri" + } } }, "primeStatusBanner": { diff --git a/apps/evm/src/libs/translations/translations/vi.json b/apps/evm/src/libs/translations/translations/vi.json index 39ddc919a1..7cc01984f6 100644 --- a/apps/evm/src/libs/translations/translations/vi.json +++ b/apps/evm/src/libs/translations/translations/vi.json @@ -1250,6 +1250,24 @@ "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" + }, + "rankTable": { + "columns": { + "wallet": "Ví", + "primeScore": "Điểm Prime" + }, + "walletTooltip": "Người dùng được xếp hạng theo điểm Prime. 500 ví dẫn đầu sẽ đủ điều kiện nhận Prime trong chu kỳ tiếp theo." + }, + "rewardTable": { + "columns": { + "wallet": "Ví", + "totalRewards": "Tổng phần thưởng", + "marketRewards": "Phần thưởng {{symbol}}" + } } }, "primeStatusBanner": { diff --git a/apps/evm/src/libs/translations/translations/zh-Hans.json b/apps/evm/src/libs/translations/translations/zh-Hans.json index c27d5afc91..bfcc65c7a2 100644 --- a/apps/evm/src/libs/translations/translations/zh-Hans.json +++ b/apps/evm/src/libs/translations/translations/zh-Hans.json @@ -1250,6 +1250,24 @@ "title": "Prime 排行榜", "totalRewards": { "title": "本周期已分配的 Prime 总奖励" + }, + "userRewards": { + "marketActions": "打开市场操作", + "title": "你本周期的 Prime 奖励" + }, + "rankTable": { + "columns": { + "wallet": "钱包", + "primeScore": "Prime 分数" + }, + "walletTooltip": "用户按 Prime 分数排名。排名前 500 的钱包将在下个周期获得 Prime 资格。" + }, + "rewardTable": { + "columns": { + "wallet": "钱包", + "totalRewards": "总奖励", + "marketRewards": "{{symbol}} 奖励" + } } }, "primeStatusBanner": { diff --git a/apps/evm/src/libs/translations/translations/zh-Hant.json b/apps/evm/src/libs/translations/translations/zh-Hant.json index ff5db769a8..6f5e0d5cfd 100644 --- a/apps/evm/src/libs/translations/translations/zh-Hant.json +++ b/apps/evm/src/libs/translations/translations/zh-Hant.json @@ -1250,6 +1250,24 @@ "title": "Prime 排行榜", "totalRewards": { "title": "本週期已分配的 Prime 總獎勵" + }, + "userRewards": { + "marketActions": "開啟市場操作", + "title": "你本週期的 Prime 獎勵" + }, + "rankTable": { + "columns": { + "wallet": "錢包", + "primeScore": "Prime 分數" + }, + "walletTooltip": "使用者依 Prime 分數排名。排名前 500 的錢包將在下個週期取得 Prime 資格。" + }, + "rewardTable": { + "columns": { + "wallet": "錢包", + "totalRewards": "總獎勵", + "marketRewards": "{{symbol}} 獎勵" + } } }, "primeStatusBanner": { 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/index.tsx b/apps/evm/src/pages/PrimeLeaderboard/index.tsx index 85e484e853..0993f16550 100644 --- a/apps/evm/src/pages/PrimeLeaderboard/index.tsx +++ b/apps/evm/src/pages/PrimeLeaderboard/index.tsx @@ -12,7 +12,9 @@ import { UserRewardsCard } from './UserRewardsCard'; // TODO: use the reward pool data returned by the API const placeholderTotalRewardsCents = 46_230_000; +const placeholderUserRewardsCents = 1_840_000; const placeholderMarketRewardsCents = [28_040_000, 17_190_000]; +const placeholderApyPercentage = 3.78; const PrimeLeaderboard: React.FC = () => { const { accountAddress } = useAccountAddress(); @@ -24,6 +26,11 @@ const PrimeLeaderboard: React.FC = () => { .slice(0, placeholderMarketRewardsCents.length) .map((token, index) => ({ token, rewardsCents: placeholderMarketRewardsCents[index] })); + const userMarketRewards = marketRewards.map(reward => ({ + ...reward, + apyPercentage: placeholderApyPercentage, + })); + const totalRewardsCard = ( {
{totalRewardsCard} - +
) : ( totalRewardsCard From 32da2f1ec119eb4b17461321715911e283e035ac Mon Sep 17 00:00:00 2001 From: cuzz-venus Date: Sun, 14 Jun 2026 23:03:54 +0800 Subject: [PATCH 07/18] fix: remove unused reward/rank table translation keys These keys belong to the leaderboard tables feature and are not referenced on this branch, so i18next-parser prunes them. Regenerate the catalogs so the committed translations match the parser output and CI passes. Co-Authored-By: Claude Opus 4.8 --- .../evm/src/libs/translations/translations/en.json | 14 -------------- .../evm/src/libs/translations/translations/ja.json | 14 -------------- .../evm/src/libs/translations/translations/th.json | 14 -------------- .../evm/src/libs/translations/translations/tr.json | 14 -------------- .../evm/src/libs/translations/translations/vi.json | 14 -------------- .../libs/translations/translations/zh-Hans.json | 14 -------------- .../libs/translations/translations/zh-Hant.json | 14 -------------- 7 files changed, 98 deletions(-) diff --git a/apps/evm/src/libs/translations/translations/en.json b/apps/evm/src/libs/translations/translations/en.json index 94f405a43e..7099854cf8 100644 --- a/apps/evm/src/libs/translations/translations/en.json +++ b/apps/evm/src/libs/translations/translations/en.json @@ -1254,20 +1254,6 @@ "userRewards": { "marketActions": "Open market actions", "title": "Your Prime rewards this cycle" - }, - "rankTable": { - "columns": { - "wallet": "Wallet", - "primeScore": "Prime score" - }, - "walletTooltip": "Users ranked by Prime score. The top 500 wallets become eligible for Prime in the next cycle." - }, - "rewardTable": { - "columns": { - "wallet": "Wallet", - "totalRewards": "Total rewards", - "marketRewards": "{{symbol}} rewards" - } } }, "primeStatusBanner": { diff --git a/apps/evm/src/libs/translations/translations/ja.json b/apps/evm/src/libs/translations/translations/ja.json index 29556e613c..6492ef9031 100644 --- a/apps/evm/src/libs/translations/translations/ja.json +++ b/apps/evm/src/libs/translations/translations/ja.json @@ -1254,20 +1254,6 @@ "userRewards": { "marketActions": "マーケット操作を開く", "title": "今サイクルのあなたの Prime 報酬" - }, - "rankTable": { - "columns": { - "wallet": "ウォレット", - "primeScore": "Prime スコア" - }, - "walletTooltip": "Prime スコア順のランキングです。上位 500 のウォレットが次のサイクルで Prime の対象になります。" - }, - "rewardTable": { - "columns": { - "wallet": "ウォレット", - "totalRewards": "報酬合計", - "marketRewards": "{{symbol}} の報酬" - } } }, "primeStatusBanner": { diff --git a/apps/evm/src/libs/translations/translations/th.json b/apps/evm/src/libs/translations/translations/th.json index a498ceaaef..1c5ccba165 100644 --- a/apps/evm/src/libs/translations/translations/th.json +++ b/apps/evm/src/libs/translations/translations/th.json @@ -1254,20 +1254,6 @@ "userRewards": { "marketActions": "เปิดการดำเนินการของตลาด", "title": "รางวัล Prime ของคุณในรอบนี้" - }, - "rankTable": { - "columns": { - "wallet": "กระเป๋าเงิน", - "primeScore": "คะแนน Prime" - }, - "walletTooltip": "ผู้ใช้จัดอันดับตามคะแนน Prime กระเป๋าเงิน 500 อันดับแรกจะมีสิทธิ์รับ Prime ในรอบถัดไป" - }, - "rewardTable": { - "columns": { - "wallet": "กระเป๋าเงิน", - "totalRewards": "รางวัลทั้งหมด", - "marketRewards": "รางวัล {{symbol}}" - } } }, "primeStatusBanner": { diff --git a/apps/evm/src/libs/translations/translations/tr.json b/apps/evm/src/libs/translations/translations/tr.json index a34122f083..f97b74e296 100644 --- a/apps/evm/src/libs/translations/translations/tr.json +++ b/apps/evm/src/libs/translations/translations/tr.json @@ -1254,20 +1254,6 @@ "userRewards": { "marketActions": "Piyasa işlemlerini aç", "title": "Bu döngüdeki Prime ödülleriniz" - }, - "rankTable": { - "columns": { - "wallet": "Cüzdan", - "primeScore": "Prime puanı" - }, - "walletTooltip": "Kullanıcılar Prime puanına göre sıralanır. İlk 500 cüzdan bir sonraki döngüde Prime için uygun olur." - }, - "rewardTable": { - "columns": { - "wallet": "Cüzdan", - "totalRewards": "Toplam ödüller", - "marketRewards": "{{symbol}} ödülleri" - } } }, "primeStatusBanner": { diff --git a/apps/evm/src/libs/translations/translations/vi.json b/apps/evm/src/libs/translations/translations/vi.json index 7cc01984f6..c0688e19ab 100644 --- a/apps/evm/src/libs/translations/translations/vi.json +++ b/apps/evm/src/libs/translations/translations/vi.json @@ -1254,20 +1254,6 @@ "userRewards": { "marketActions": "Mở thao tác thị trường", "title": "Phần thưởng Prime của bạn trong chu kỳ này" - }, - "rankTable": { - "columns": { - "wallet": "Ví", - "primeScore": "Điểm Prime" - }, - "walletTooltip": "Người dùng được xếp hạng theo điểm Prime. 500 ví dẫn đầu sẽ đủ điều kiện nhận Prime trong chu kỳ tiếp theo." - }, - "rewardTable": { - "columns": { - "wallet": "Ví", - "totalRewards": "Tổng phần thưởng", - "marketRewards": "Phần thưởng {{symbol}}" - } } }, "primeStatusBanner": { diff --git a/apps/evm/src/libs/translations/translations/zh-Hans.json b/apps/evm/src/libs/translations/translations/zh-Hans.json index bfcc65c7a2..6413657e30 100644 --- a/apps/evm/src/libs/translations/translations/zh-Hans.json +++ b/apps/evm/src/libs/translations/translations/zh-Hans.json @@ -1254,20 +1254,6 @@ "userRewards": { "marketActions": "打开市场操作", "title": "你本周期的 Prime 奖励" - }, - "rankTable": { - "columns": { - "wallet": "钱包", - "primeScore": "Prime 分数" - }, - "walletTooltip": "用户按 Prime 分数排名。排名前 500 的钱包将在下个周期获得 Prime 资格。" - }, - "rewardTable": { - "columns": { - "wallet": "钱包", - "totalRewards": "总奖励", - "marketRewards": "{{symbol}} 奖励" - } } }, "primeStatusBanner": { diff --git a/apps/evm/src/libs/translations/translations/zh-Hant.json b/apps/evm/src/libs/translations/translations/zh-Hant.json index 6f5e0d5cfd..cf82a1203f 100644 --- a/apps/evm/src/libs/translations/translations/zh-Hant.json +++ b/apps/evm/src/libs/translations/translations/zh-Hant.json @@ -1254,20 +1254,6 @@ "userRewards": { "marketActions": "開啟市場操作", "title": "你本週期的 Prime 獎勵" - }, - "rankTable": { - "columns": { - "wallet": "錢包", - "primeScore": "Prime 分數" - }, - "walletTooltip": "使用者依 Prime 分數排名。排名前 500 的錢包將在下個週期取得 Prime 資格。" - }, - "rewardTable": { - "columns": { - "wallet": "錢包", - "totalRewards": "總獎勵", - "marketRewards": "{{symbol}} 獎勵" - } } }, "primeStatusBanner": { From 28d6cac8d6d2feab0402a3aa38dc9d761a01ebe1 Mon Sep 17 00:00:00 2001 From: cuzz-venus Date: Mon, 15 Jun 2026 11:05:03 +0800 Subject: [PATCH 08/18] feat: support user rank card --- .changeset/happy-garlics-argue.md | 5 + .../src/components/Icon/icons/barChart.tsx | 37 ++++++ .../components/Icon/icons/graduationCap.tsx | 25 ++++ apps/evm/src/components/Icon/icons/index.ts | 2 + .../libs/translations/translations/en.json | 16 +++ .../libs/translations/translations/ja.json | 16 +++ .../libs/translations/translations/th.json | 16 +++ .../libs/translations/translations/tr.json | 16 +++ .../libs/translations/translations/vi.json | 16 +++ .../translations/translations/zh-Hans.json | 16 +++ .../translations/translations/zh-Hant.json | 16 +++ .../__tests__/index.spec.tsx | 17 +++ .../LastCycleSummaryModal/index.tsx | 89 +++++++++++++ .../PrimeRewardBadge/index.tsx | 18 +++ .../RankCard/__tests__/index.spec.tsx | 24 ++++ .../pages/PrimeLeaderboard/RankCard/index.tsx | 118 ++++++++++++++++-- .../TotalRewardsCard/index.tsx | 6 +- .../UserRewardsCard/index.tsx | 52 +++++--- 18 files changed, 473 insertions(+), 32 deletions(-) create mode 100644 .changeset/happy-garlics-argue.md create mode 100644 apps/evm/src/components/Icon/icons/barChart.tsx create mode 100644 apps/evm/src/components/Icon/icons/graduationCap.tsx create mode 100644 apps/evm/src/pages/PrimeLeaderboard/LastCycleSummaryModal/__tests__/index.spec.tsx create mode 100644 apps/evm/src/pages/PrimeLeaderboard/LastCycleSummaryModal/index.tsx create mode 100644 apps/evm/src/pages/PrimeLeaderboard/PrimeRewardBadge/index.tsx create mode 100644 apps/evm/src/pages/PrimeLeaderboard/RankCard/__tests__/index.spec.tsx diff --git a/.changeset/happy-garlics-argue.md b/.changeset/happy-garlics-argue.md new file mode 100644 index 0000000000..d833fc25a6 --- /dev/null +++ b/.changeset/happy-garlics-argue.md @@ -0,0 +1,5 @@ +--- +"@venusprotocol/evm": minor +--- + +feat: support user rank card diff --git a/apps/evm/src/components/Icon/icons/barChart.tsx b/apps/evm/src/components/Icon/icons/barChart.tsx new file mode 100644 index 0000000000..5e74433cc6 --- /dev/null +++ b/apps/evm/src/components/Icon/icons/barChart.tsx @@ -0,0 +1,37 @@ +import type { SVGProps } from 'react'; + +const SvgBarChart = (props: SVGProps) => ( + + + + + + + + +); + +export default SvgBarChart; diff --git a/apps/evm/src/components/Icon/icons/graduationCap.tsx b/apps/evm/src/components/Icon/icons/graduationCap.tsx new file mode 100644 index 0000000000..d13ba7e57b --- /dev/null +++ b/apps/evm/src/components/Icon/icons/graduationCap.tsx @@ -0,0 +1,25 @@ +import type { SVGProps } from 'react'; + +const SvgGraduationCap = (props: SVGProps) => ( + + + + + + +); + +export default SvgGraduationCap; diff --git a/apps/evm/src/components/Icon/icons/index.ts b/apps/evm/src/components/Icon/icons/index.ts index 7dec646edf..6029313a50 100644 --- a/apps/evm/src/components/Icon/icons/index.ts +++ b/apps/evm/src/components/Icon/icons/index.ts @@ -101,3 +101,5 @@ export { default as resilientOracle } from './resilientOracle'; export { default as sunset } from './sunset'; export { default as fullScreen } from './fullScreen'; export { default as switch } from './switch'; +export { default as barChart } from './barChart'; +export { default as graduationCap } from './graduationCap'; diff --git a/apps/evm/src/libs/translations/translations/en.json b/apps/evm/src/libs/translations/translations/en.json index 7099854cf8..5f62081a42 100644 --- a/apps/evm/src/libs/translations/translations/en.json +++ b/apps/evm/src/libs/translations/translations/en.json @@ -1247,6 +1247,22 @@ }, "primeLeaderboard": { "description": "Stake XVS into the governance vault and receive Prime rewards on top of your yields.", + "lastCycleSummary": { + "eligibleMessage": "You qualified for Prime in this cycle. Take positions in Prime markets to earn additional rewards.", + "notEligibleMessage": "You did not qualify for Prime during the last cycle. Stake XVS to compete for Prime in the next cycle.", + "title": "Last Cycle Prime Summary", + "totalRewardsTitle": "Total Prime rewards distributed during the last cycle" + }, + "rankCard": { + "connectPrompt": "Connect wallet to check your Prime eligibility.", + "eligible": "You are currently eligible for Prime during the next cycle", + "notEligible": "You are currently not eligible for Prime during the next cycle.", + "primeScoreLabel": "Prime score", + "rankLabel": "Your rank", + "rulesButton": "Rules", + "stakeButton": "Stake XVS", + "stakePrompt": "Stake XVS to compete for Prime." + }, "title": "Prime leaderboard", "totalRewards": { "title": "Total Prime rewards distributed this cycle" diff --git a/apps/evm/src/libs/translations/translations/ja.json b/apps/evm/src/libs/translations/translations/ja.json index 6492ef9031..436b4a484d 100644 --- a/apps/evm/src/libs/translations/translations/ja.json +++ b/apps/evm/src/libs/translations/translations/ja.json @@ -1247,6 +1247,22 @@ }, "primeLeaderboard": { "description": "ガバナンスボールトに XVS をステークして、利回りに加えて Prime 報酬を獲得しましょう。", + "lastCycleSummary": { + "eligibleMessage": "今サイクルで Prime の対象になりました。Prime マーケットでポジションを取ると追加報酬を獲得できます。", + "notEligibleMessage": "前サイクルでは Prime の対象ではありませんでした。XVS をステークして次のサイクルで Prime を目指しましょう。", + "title": "前サイクルの Prime サマリー", + "totalRewardsTitle": "前サイクルに分配された Prime 報酬の合計" + }, + "rankCard": { + "connectPrompt": "ウォレットを接続して Prime の対象かを確認してください。", + "eligible": "あなたは次のサイクルで Prime の対象です", + "notEligible": "あなたは次のサイクルで Prime の対象ではありません。", + "primeScoreLabel": "Prime スコア", + "rankLabel": "あなたのランク", + "rulesButton": "ルール", + "stakeButton": "XVS をステーク", + "stakePrompt": "XVS をステークして Prime を目指しましょう。" + }, "title": "Prime リーダーボード", "totalRewards": { "title": "今サイクルに分配された Prime 報酬の総額" diff --git a/apps/evm/src/libs/translations/translations/th.json b/apps/evm/src/libs/translations/translations/th.json index 1c5ccba165..acbe5270cc 100644 --- a/apps/evm/src/libs/translations/translations/th.json +++ b/apps/evm/src/libs/translations/translations/th.json @@ -1247,6 +1247,22 @@ }, "primeLeaderboard": { "description": "สเตก XVS เข้าสู่ governance vault แล้วรับรางวัล Prime เพิ่มเติมจากผลตอบแทนของคุณ", + "lastCycleSummary": { + "eligibleMessage": "คุณมีสิทธิ์รับ Prime ในรอบนี้ เปิดสถานะในตลาด Prime เพื่อรับรางวัลเพิ่มเติม", + "notEligibleMessage": "คุณไม่มีสิทธิ์รับ Prime ในรอบที่แล้ว Stake XVS เพื่อแข่งขันรับ Prime ในรอบถัดไป", + "title": "สรุป Prime รอบที่แล้ว", + "totalRewardsTitle": "รางวัล Prime ทั้งหมดที่แจกจ่ายในรอบที่แล้ว" + }, + "rankCard": { + "connectPrompt": "เชื่อมต่อกระเป๋าเงินเพื่อตรวจสอบสิทธิ์ Prime ของคุณ", + "eligible": "คุณมีสิทธิ์รับ Prime ในรอบถัดไป", + "notEligible": "คุณยังไม่มีสิทธิ์รับ Prime ในรอบถัดไป", + "primeScoreLabel": "คะแนน Prime", + "rankLabel": "อันดับของคุณ", + "rulesButton": "กฎ", + "stakeButton": "Stake XVS", + "stakePrompt": "Stake XVS เพื่อแข่งขันรับ Prime" + }, "title": "กระดานผู้นำ Prime", "totalRewards": { "title": "รางวัล Prime ทั้งหมดที่แจกในรอบนี้" diff --git a/apps/evm/src/libs/translations/translations/tr.json b/apps/evm/src/libs/translations/translations/tr.json index f97b74e296..932404c8a9 100644 --- a/apps/evm/src/libs/translations/translations/tr.json +++ b/apps/evm/src/libs/translations/translations/tr.json @@ -1247,6 +1247,22 @@ }, "primeLeaderboard": { "description": "XVS’yi governance kasasına stake edin ve getirilerinize ek olarak Prime ödülleri kazanın.", + "lastCycleSummary": { + "eligibleMessage": "Bu döngüde Prime için uygun oldun. Ek ödül kazanmak için Prime piyasalarında pozisyon al.", + "notEligibleMessage": "Son döngüde Prime için uygun değildin. Bir sonraki döngüde Prime için yarışmak üzere XVS stake et.", + "title": "Son Döngü Prime Özeti", + "totalRewardsTitle": "Son döngüde dağıtılan toplam Prime ödülleri" + }, + "rankCard": { + "connectPrompt": "Prime uygunluğunu görmek için cüzdanını bağla.", + "eligible": "Bir sonraki döngüde Prime için uygunsun", + "notEligible": "Bir sonraki döngüde Prime için uygun değilsin.", + "primeScoreLabel": "Prime puanı", + "rankLabel": "Sıralaman", + "rulesButton": "Kurallar", + "stakeButton": "XVS stake et", + "stakePrompt": "Prime için yarışmak üzere XVS stake et." + }, "title": "Prime lider tablosu", "totalRewards": { "title": "Bu döngüde dağıtılan toplam Prime ödülü" diff --git a/apps/evm/src/libs/translations/translations/vi.json b/apps/evm/src/libs/translations/translations/vi.json index c0688e19ab..9dca739706 100644 --- a/apps/evm/src/libs/translations/translations/vi.json +++ b/apps/evm/src/libs/translations/translations/vi.json @@ -1247,6 +1247,22 @@ }, "primeLeaderboard": { "description": "Stake XVS vào governance vault và nhận thêm phần thưởng Prime bên cạnh lợi nhuận của bạn.", + "lastCycleSummary": { + "eligibleMessage": "Bạn đủ điều kiện nhận Prime trong chu kỳ này. Hãy mở vị thế trong các thị trường Prime để nhận thêm phần thưởng.", + "notEligibleMessage": "Bạn không đủ điều kiện nhận Prime trong chu kỳ trước. Stake XVS để cạnh tranh Prime trong chu kỳ tiếp theo.", + "title": "Tóm tắt Prime chu kỳ trước", + "totalRewardsTitle": "Tổng phần thưởng Prime đã phân phối trong chu kỳ trước" + }, + "rankCard": { + "connectPrompt": "Kết nối ví để kiểm tra điều kiện Prime của bạn.", + "eligible": "Bạn hiện đủ điều kiện nhận Prime trong chu kỳ tiếp theo", + "notEligible": "Bạn hiện không đủ điều kiện nhận Prime trong chu kỳ tiếp theo.", + "primeScoreLabel": "Điểm Prime", + "rankLabel": "Hạng của bạn", + "rulesButton": "Quy tắc", + "stakeButton": "Stake XVS", + "stakePrompt": "Stake XVS để cạnh tranh 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" diff --git a/apps/evm/src/libs/translations/translations/zh-Hans.json b/apps/evm/src/libs/translations/translations/zh-Hans.json index 6413657e30..1f5f78fd95 100644 --- a/apps/evm/src/libs/translations/translations/zh-Hans.json +++ b/apps/evm/src/libs/translations/translations/zh-Hans.json @@ -1247,6 +1247,22 @@ }, "primeLeaderboard": { "description": "将 XVS 质押到治理金库,在收益之上再赚取 Prime 奖励。", + "lastCycleSummary": { + "eligibleMessage": "你在本周期已具备 Prime 资格。在 Prime 市场建仓以赚取额外奖励。", + "notEligibleMessage": "你在上一周期未具备 Prime 资格。质押 XVS 以在下一周期竞争 Prime。", + "title": "上一周期 Prime 汇总", + "totalRewardsTitle": "上一周期分发的 Prime 总奖励" + }, + "rankCard": { + "connectPrompt": "连接钱包以查看你的 Prime 资格。", + "eligible": "你当前已具备下个周期的 Prime 资格", + "notEligible": "你当前不具备下个周期的 Prime 资格。", + "primeScoreLabel": "Prime 分数", + "rankLabel": "你的排名", + "rulesButton": "规则", + "stakeButton": "质押 XVS", + "stakePrompt": "质押 XVS 以竞争 Prime。" + }, "title": "Prime 排行榜", "totalRewards": { "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 cf82a1203f..46e0964cd4 100644 --- a/apps/evm/src/libs/translations/translations/zh-Hant.json +++ b/apps/evm/src/libs/translations/translations/zh-Hant.json @@ -1247,6 +1247,22 @@ }, "primeLeaderboard": { "description": "將 XVS 質押到治理金庫,在收益之上再賺取 Prime 獎勵。", + "lastCycleSummary": { + "eligibleMessage": "你在本週期已具備 Prime 資格。在 Prime 市場建倉以賺取額外獎勵。", + "notEligibleMessage": "你在上一週期未具備 Prime 資格。質押 XVS 以在下一週期競爭 Prime。", + "title": "上一週期 Prime 彙總", + "totalRewardsTitle": "上一週期分發的 Prime 總獎勵" + }, + "rankCard": { + "connectPrompt": "連接錢包以查看你的 Prime 資格。", + "eligible": "你目前已具備下個週期的 Prime 資格", + "notEligible": "你目前不具備下個週期的 Prime 資格。", + "primeScoreLabel": "Prime 分數", + "rankLabel": "你的排名", + "rulesButton": "規則", + "stakeButton": "質押 XVS", + "stakePrompt": "質押 XVS 以競爭 Prime。" + }, "title": "Prime 排行榜", "totalRewards": { "title": "本週期已分配的 Prime 總獎勵" diff --git a/apps/evm/src/pages/PrimeLeaderboard/LastCycleSummaryModal/__tests__/index.spec.tsx b/apps/evm/src/pages/PrimeLeaderboard/LastCycleSummaryModal/__tests__/index.spec.tsx new file mode 100644 index 0000000000..233ec76d1b --- /dev/null +++ b/apps/evm/src/pages/PrimeLeaderboard/LastCycleSummaryModal/__tests__/index.spec.tsx @@ -0,0 +1,17 @@ +import { screen } from '@testing-library/react'; + +import { renderComponent } from 'testUtils/render'; + +import { LastCycleSummaryModal } from '..'; + +describe('pages/PrimeLeaderboard/LastCycleSummaryModal', () => { + it('renders the last cycle total and user Prime rewards', () => { + renderComponent( {}} />); + + expect(screen.getByText('Last Cycle Prime Summary')).toBeInTheDocument(); + expect( + screen.getByText('Total Prime rewards distributed during the last cycle'), + ).toBeInTheDocument(); + expect(screen.getByText('Your Prime rewards this cycle')).toBeInTheDocument(); + }); +}); diff --git a/apps/evm/src/pages/PrimeLeaderboard/LastCycleSummaryModal/index.tsx b/apps/evm/src/pages/PrimeLeaderboard/LastCycleSummaryModal/index.tsx new file mode 100644 index 0000000000..24a209b984 --- /dev/null +++ b/apps/evm/src/pages/PrimeLeaderboard/LastCycleSummaryModal/index.tsx @@ -0,0 +1,89 @@ +import { cn } from '@venusprotocol/ui'; + +import { Icon, Modal } from 'components'; +import { useGetTokens } from 'libs/tokens'; +import { useTranslation } from 'libs/translations'; + +import { PrimeRewardBadge } from '../PrimeRewardBadge'; +import { TotalRewardsCard } from '../TotalRewardsCard'; +import { UserRewardsCard } from '../UserRewardsCard'; + +// TODO: replace these placeholder values with the data returned by the API +const placeholderTotalRewardsCents = 46_230_000; +const placeholderTotalMarketRewardsCents = [28_040_000, 17_190_000]; +const placeholderUserRewardsCents = 1_840_000; +const placeholderUserMarketRewardsCents = [1_140_000, 700_000]; +const placeholderApyPercentage = 3.78; +const placeholderHasRewards = true; +const placeholderIsEligible = true; + +export interface LastCycleSummaryModalProps { + isOpen: boolean; + handleClose: () => void; +} + +export const LastCycleSummaryModal: React.FC = ({ + isOpen, + handleClose, +}) => { + const { t } = useTranslation(); + const tokens = useGetTokens(); + + // TODO: replace these placeholder tokens with the real Prime markets returned by the API + const markets = tokens.slice(0, placeholderTotalMarketRewardsCents.length); + + const totalMarketRewards = markets.map((token, index) => ({ + token, + rewardsCents: placeholderTotalMarketRewardsCents[index], + })); + + const userMarketRewards = markets.map((token, index) => ({ + token, + rewardsCents: placeholderUserMarketRewardsCents[index], + apyPercentage: placeholderApyPercentage, + })); + + const userRewardsContent = placeholderHasRewards ? undefined : ( +
+ {placeholderIsEligible ? ( + + ) : ( + + + + )} + +

+ {placeholderIsEligible + ? t('primeLeaderboard.lastCycleSummary.eligibleMessage') + : t('primeLeaderboard.lastCycleSummary.notEligibleMessage')} +

+
+ ); + + return ( + +
+ + + +
+
+ ); +}; diff --git a/apps/evm/src/pages/PrimeLeaderboard/PrimeRewardBadge/index.tsx b/apps/evm/src/pages/PrimeLeaderboard/PrimeRewardBadge/index.tsx new file mode 100644 index 0000000000..f53f545f45 --- /dev/null +++ b/apps/evm/src/pages/PrimeLeaderboard/PrimeRewardBadge/index.tsx @@ -0,0 +1,18 @@ +import { cn } from '@venusprotocol/ui'; + +import primeLogoSrc from 'assets/img/primeLogo.svg'; + +export interface PrimeRewardBadgeProps { + className?: string; +} + +export const PrimeRewardBadge: React.FC = ({ className }) => ( + + + +); diff --git a/apps/evm/src/pages/PrimeLeaderboard/RankCard/__tests__/index.spec.tsx b/apps/evm/src/pages/PrimeLeaderboard/RankCard/__tests__/index.spec.tsx new file mode 100644 index 0000000000..eba976f8a2 --- /dev/null +++ b/apps/evm/src/pages/PrimeLeaderboard/RankCard/__tests__/index.spec.tsx @@ -0,0 +1,24 @@ +import { screen } from '@testing-library/react'; + +import fakeAddress from '__mocks__/models/address'; +import { renderComponent } from 'testUtils/render'; + +import { RankCard } from '..'; + +describe('pages/PrimeLeaderboard/RankCard', () => { + it('renders the rank, Prime score and actions when the wallet is connected', () => { + renderComponent(, { accountAddress: fakeAddress }); + + expect(screen.getByText('#2')).toBeInTheDocument(); + expect(screen.getByText('542.5M')).toBeInTheDocument(); + expect(screen.getByText('Stake XVS')).toBeInTheDocument(); + expect(screen.getByText('Rules')).toBeInTheDocument(); + }); + + it('prompts to connect the wallet when none is connected', () => { + renderComponent(); + + expect(screen.getByText('Connect wallet to check your Prime eligibility.')).toBeInTheDocument(); + expect(screen.queryByText('#2')).not.toBeInTheDocument(); + }); +}); diff --git a/apps/evm/src/pages/PrimeLeaderboard/RankCard/index.tsx b/apps/evm/src/pages/PrimeLeaderboard/RankCard/index.tsx index 92ea68015b..ddecb049fa 100644 --- a/apps/evm/src/pages/PrimeLeaderboard/RankCard/index.tsx +++ b/apps/evm/src/pages/PrimeLeaderboard/RankCard/index.tsx @@ -1,16 +1,112 @@ -import { cn } from '@venusprotocol/ui'; +import { Button, ButtonWrapper, cn } from '@venusprotocol/ui'; +import BigNumber from 'bignumber.js'; +import { useState } from 'react'; + +import { Icon } from 'components'; +import { routes } from 'constants/routing'; +import { Link } from 'containers/Link'; +import { useTranslation } from 'libs/translations'; +import { useAccountAddress, useAuthModal } from 'libs/wallet'; +import { shortenValueWithSuffix } from 'utilities'; + +import { LastCycleSummaryModal } from '../LastCycleSummaryModal'; + +// TODO: replace these placeholder values with the rank data returned by the API +const placeholderRank = 2; +const placeholderPrimeScore = 542_500_000; +const placeholderIsEligible = true; export interface RankCardProps { className?: string; } -export const RankCard: React.FC = ({ className }) => ( -
- Rank Card -
-); +export const RankCard: React.FC = ({ className }) => { + const { t } = useTranslation(); + const { accountAddress } = useAccountAddress(); + const { openAuthModal } = useAuthModal(); + const [isSummaryModalOpen, setIsSummaryModalOpen] = useState(false); + + const cardClassName = cn('flex h-58 flex-col rounded-lg bg-background-active p-4', className); + + if (!accountAddress) { + return ( +
+
+ + + + +

{t('primeLeaderboard.rankCard.connectPrompt')}

+
+ + +
+ ); + } + + return ( + <> +
+
+
+
+ + {t('primeLeaderboard.rankCard.rankLabel')} + + + {placeholderIsEligible ? `#${placeholderRank}` : '#-'} + +
+ +
+ + {t('primeLeaderboard.rankCard.primeScoreLabel')} + + + {placeholderIsEligible + ? shortenValueWithSuffix({ value: new BigNumber(placeholderPrimeScore) }) + : '-'} + +
+
+ + {placeholderIsEligible ? ( +

{t('primeLeaderboard.rankCard.eligible')}

+ ) : ( +
+

{t('primeLeaderboard.rankCard.notEligible')}

+

{t('primeLeaderboard.rankCard.stakePrompt')}

+
+ )} +
+ +
+ + {t('primeLeaderboard.rankCard.stakeButton')} + + + +
+
+ + setIsSummaryModalOpen(false)} + /> + + ); +}; diff --git a/apps/evm/src/pages/PrimeLeaderboard/TotalRewardsCard/index.tsx b/apps/evm/src/pages/PrimeLeaderboard/TotalRewardsCard/index.tsx index b3b573a472..8831b3bd8b 100644 --- a/apps/evm/src/pages/PrimeLeaderboard/TotalRewardsCard/index.tsx +++ b/apps/evm/src/pages/PrimeLeaderboard/TotalRewardsCard/index.tsx @@ -14,12 +14,14 @@ export interface MarketReward { export interface TotalRewardsCardProps { totalRewardsCents: number; marketRewards: MarketReward[]; + title?: React.ReactNode; className?: string; } export const TotalRewardsCard: React.FC = ({ totalRewardsCents, marketRewards, + title, className, }) => { const { t } = useTranslation(); @@ -32,7 +34,9 @@ export const TotalRewardsCard: React.FC = ({ )} >
-

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

+

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

{formatCentsToReadableValue({ value: totalRewardsCents })} diff --git a/apps/evm/src/pages/PrimeLeaderboard/UserRewardsCard/index.tsx b/apps/evm/src/pages/PrimeLeaderboard/UserRewardsCard/index.tsx index 262e664d4a..bb52eb1cdb 100644 --- a/apps/evm/src/pages/PrimeLeaderboard/UserRewardsCard/index.tsx +++ b/apps/evm/src/pages/PrimeLeaderboard/UserRewardsCard/index.tsx @@ -1,12 +1,12 @@ 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'; +import { PrimeRewardBadge } from '../PrimeRewardBadge'; export interface UserMarketReward { token: Token; @@ -17,12 +17,20 @@ export interface UserMarketReward { export interface UserRewardsCardProps { totalRewardsCents: number; marketRewards: UserMarketReward[]; + // Replaces the default headline (Prime badge + total amount). Used by the rules modal to show a + // contextual message instead of the amount + content?: React.ReactNode; + // Toggles the per-market Prime APY and actions menu, which are hidden when the card is used as a + // read-only summary + showMarketActions?: boolean; className?: string; } export const UserRewardsCard: React.FC = ({ totalRewardsCents, marketRewards, + content, + showMarketActions = true, className, }) => { const { t } = useTranslation(); @@ -37,15 +45,15 @@ export const UserRewardsCard: React.FC = ({

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

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

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

-
+

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

+
+ )}
@@ -56,19 +64,23 @@ export const UserRewardsCard: React.FC = ({ rewardsCents={rewardsCents} totalRewardsCents={totalRewardsCents} > -
- + {showMarketActions && ( + <> +
+ - {formatPercentageToReadableValue(apyPercentage)} -
+ {formatPercentageToReadableValue(apyPercentage)} +
- + + + )} ))}
From f8319405d17fc73917a4a415b7608ae74701aa25 Mon Sep 17 00:00:00 2001 From: cuzz-venus Date: Mon, 15 Jun 2026 15:13:48 +0800 Subject: [PATCH 09/18] feat: support user rank card --- .../libs/translations/translations/en.json | 7 +- .../libs/translations/translations/ja.json | 5 +- .../libs/translations/translations/th.json | 5 +- .../libs/translations/translations/tr.json | 5 +- .../libs/translations/translations/vi.json | 5 +- .../translations/translations/zh-Hans.json | 5 +- .../translations/translations/zh-Hant.json | 5 +- .../__tests__/index.spec.tsx | 4 +- .../RankCard/ConnectPrompt/index.tsx | 28 ++++ .../__tests__/index.spec.tsx | 55 ++++++++ .../RankCard/EligibilityStatus/index.tsx | 66 +++++++++ .../RankCard/RankActions/index.tsx | 35 +++++ .../RankCard/RankSummary/index.tsx | 26 ++++ .../RankCard/__tests__/index.spec.tsx | 22 ++- .../pages/PrimeLeaderboard/RankCard/index.tsx | 132 ++++++------------ apps/evm/src/pages/PrimeLeaderboard/index.tsx | 22 ++- 16 files changed, 321 insertions(+), 106 deletions(-) create mode 100644 apps/evm/src/pages/PrimeLeaderboard/RankCard/ConnectPrompt/index.tsx create mode 100644 apps/evm/src/pages/PrimeLeaderboard/RankCard/EligibilityStatus/__tests__/index.spec.tsx create mode 100644 apps/evm/src/pages/PrimeLeaderboard/RankCard/EligibilityStatus/index.tsx create mode 100644 apps/evm/src/pages/PrimeLeaderboard/RankCard/RankActions/index.tsx create mode 100644 apps/evm/src/pages/PrimeLeaderboard/RankCard/RankSummary/index.tsx diff --git a/apps/evm/src/libs/translations/translations/en.json b/apps/evm/src/libs/translations/translations/en.json index 5f62081a42..54d40f405e 100644 --- a/apps/evm/src/libs/translations/translations/en.json +++ b/apps/evm/src/libs/translations/translations/en.json @@ -1255,13 +1255,16 @@ }, "rankCard": { "connectPrompt": "Connect wallet to check your Prime eligibility.", - "eligible": "You are currently eligible for Prime during the next cycle", + "eligible": "You are currently eligible for Prime during the next cycle.", + "eligibleSupplied": "Congrats! You're in the Top 500 during last cycle and qualified for Prime Rewards.", "notEligible": "You are currently not eligible for Prime during the next cycle.", "primeScoreLabel": "Prime score", "rankLabel": "Your rank", "rulesButton": "Rules", "stakeButton": "Stake XVS", - "stakePrompt": "Stake XVS to compete for Prime." + "stakeMore": "Stake more XVS to compete for Prime during the next cycle.", + "stakePrompt": "Stake XVS to compete for Prime.", + "stakeToReachTop": "Stake {{amount}} XVS to reach the top #500 and become eligible for Prime during the next cycle." }, "title": "Prime leaderboard", "totalRewards": { diff --git a/apps/evm/src/libs/translations/translations/ja.json b/apps/evm/src/libs/translations/translations/ja.json index 436b4a484d..165b80a455 100644 --- a/apps/evm/src/libs/translations/translations/ja.json +++ b/apps/evm/src/libs/translations/translations/ja.json @@ -1256,12 +1256,15 @@ "rankCard": { "connectPrompt": "ウォレットを接続して Prime の対象かを確認してください。", "eligible": "あなたは次のサイクルで Prime の対象です", + "eligibleSupplied": "おめでとうございます!前サイクルでトップ 500 に入り、Prime 報酬の対象になりました。", "notEligible": "あなたは次のサイクルで Prime の対象ではありません。", "primeScoreLabel": "Prime スコア", "rankLabel": "あなたのランク", "rulesButton": "ルール", "stakeButton": "XVS をステーク", - "stakePrompt": "XVS をステークして Prime を目指しましょう。" + "stakeMore": "次のサイクルで Prime を目指して、さらに XVS をステークしましょう。", + "stakePrompt": "XVS をステークして Prime を目指しましょう。", + "stakeToReachTop": "あと {{amount}} XVS をステークするとトップ #500 に入り、次のサイクルで Prime の対象になります。" }, "title": "Prime リーダーボード", "totalRewards": { diff --git a/apps/evm/src/libs/translations/translations/th.json b/apps/evm/src/libs/translations/translations/th.json index acbe5270cc..9bab2db8a4 100644 --- a/apps/evm/src/libs/translations/translations/th.json +++ b/apps/evm/src/libs/translations/translations/th.json @@ -1256,12 +1256,15 @@ "rankCard": { "connectPrompt": "เชื่อมต่อกระเป๋าเงินเพื่อตรวจสอบสิทธิ์ Prime ของคุณ", "eligible": "คุณมีสิทธิ์รับ Prime ในรอบถัดไป", + "eligibleSupplied": "ยินดีด้วย! คุณอยู่ใน Top 500 ในรอบที่แล้วและมีสิทธิ์รับ Prime Rewards", "notEligible": "คุณยังไม่มีสิทธิ์รับ Prime ในรอบถัดไป", "primeScoreLabel": "คะแนน Prime", "rankLabel": "อันดับของคุณ", "rulesButton": "กฎ", "stakeButton": "Stake XVS", - "stakePrompt": "Stake XVS เพื่อแข่งขันรับ Prime" + "stakeMore": "Stake XVS เพิ่มเพื่อแข่งขันรับ Prime ในรอบถัดไป", + "stakePrompt": "Stake XVS เพื่อแข่งขันรับ Prime", + "stakeToReachTop": "Stake อีก {{amount}} XVS เพื่อขึ้นไปอยู่ใน top #500 และมีสิทธิ์รับ Prime ในรอบถัดไป" }, "title": "กระดานผู้นำ Prime", "totalRewards": { diff --git a/apps/evm/src/libs/translations/translations/tr.json b/apps/evm/src/libs/translations/translations/tr.json index 932404c8a9..f15aebd044 100644 --- a/apps/evm/src/libs/translations/translations/tr.json +++ b/apps/evm/src/libs/translations/translations/tr.json @@ -1256,12 +1256,15 @@ "rankCard": { "connectPrompt": "Prime uygunluğunu görmek için cüzdanını bağla.", "eligible": "Bir sonraki döngüde Prime için uygunsun", + "eligibleSupplied": "Tebrikler! Son döngüde ilk 500'e girdin ve Prime Ödülleri için uygun oldun.", "notEligible": "Bir sonraki döngüde Prime için uygun değilsin.", "primeScoreLabel": "Prime puanı", "rankLabel": "Sıralaman", "rulesButton": "Kurallar", "stakeButton": "XVS stake et", - "stakePrompt": "Prime için yarışmak üzere XVS stake et." + "stakeMore": "Bir sonraki döngüde Prime için yarışmak üzere daha fazla XVS stake et.", + "stakePrompt": "Prime için yarışmak üzere XVS stake et.", + "stakeToReachTop": "İlk #500'e girmek ve bir sonraki döngüde Prime için uygun olmak için {{amount}} XVS stake et." }, "title": "Prime lider tablosu", "totalRewards": { diff --git a/apps/evm/src/libs/translations/translations/vi.json b/apps/evm/src/libs/translations/translations/vi.json index 9dca739706..ea0b39a918 100644 --- a/apps/evm/src/libs/translations/translations/vi.json +++ b/apps/evm/src/libs/translations/translations/vi.json @@ -1256,12 +1256,15 @@ "rankCard": { "connectPrompt": "Kết nối ví để kiểm tra điều kiện Prime của bạn.", "eligible": "Bạn hiện đủ điều kiện nhận Prime trong chu kỳ tiếp theo", + "eligibleSupplied": "Chúc mừng! Bạn đã vào Top 500 trong chu kỳ trước và đủ điều kiện nhận Prime Rewards.", "notEligible": "Bạn hiện không đủ điều kiện nhận Prime trong chu kỳ tiếp theo.", "primeScoreLabel": "Điểm Prime", "rankLabel": "Hạng của bạn", "rulesButton": "Quy tắc", "stakeButton": "Stake XVS", - "stakePrompt": "Stake XVS để cạnh tranh Prime." + "stakeMore": "Stake thêm XVS để cạnh tranh Prime trong chu kỳ tiếp theo.", + "stakePrompt": "Stake XVS để cạnh tranh Prime.", + "stakeToReachTop": "Stake thêm {{amount}} XVS để vào top #500 và đủ điều kiện nhận Prime trong chu kỳ tiếp theo." }, "title": "Bảng xếp hạng Prime", "totalRewards": { diff --git a/apps/evm/src/libs/translations/translations/zh-Hans.json b/apps/evm/src/libs/translations/translations/zh-Hans.json index 1f5f78fd95..903810cbd3 100644 --- a/apps/evm/src/libs/translations/translations/zh-Hans.json +++ b/apps/evm/src/libs/translations/translations/zh-Hans.json @@ -1256,12 +1256,15 @@ "rankCard": { "connectPrompt": "连接钱包以查看你的 Prime 资格。", "eligible": "你当前已具备下个周期的 Prime 资格", + "eligibleSupplied": "恭喜!你在上个周期进入了前 500 名并获得了 Prime 奖励。", "notEligible": "你当前不具备下个周期的 Prime 资格。", "primeScoreLabel": "Prime 分数", "rankLabel": "你的排名", "rulesButton": "规则", "stakeButton": "质押 XVS", - "stakePrompt": "质押 XVS 以竞争 Prime。" + "stakeMore": "质押更多 XVS 以在下个周期竞争 Prime。", + "stakePrompt": "质押 XVS 以竞争 Prime。", + "stakeToReachTop": "再质押 {{amount}} XVS 即可进入前 #500,并在下个周期获得 Prime 资格。" }, "title": "Prime 排行榜", "totalRewards": { diff --git a/apps/evm/src/libs/translations/translations/zh-Hant.json b/apps/evm/src/libs/translations/translations/zh-Hant.json index 46e0964cd4..5eee33ad1c 100644 --- a/apps/evm/src/libs/translations/translations/zh-Hant.json +++ b/apps/evm/src/libs/translations/translations/zh-Hant.json @@ -1256,12 +1256,15 @@ "rankCard": { "connectPrompt": "連接錢包以查看你的 Prime 資格。", "eligible": "你目前已具備下個週期的 Prime 資格", + "eligibleSupplied": "恭喜!你在上個週期進入了前 500 名並獲得了 Prime 獎勵。", "notEligible": "你目前不具備下個週期的 Prime 資格。", "primeScoreLabel": "Prime 分數", "rankLabel": "你的排名", "rulesButton": "規則", "stakeButton": "質押 XVS", - "stakePrompt": "質押 XVS 以競爭 Prime。" + "stakeMore": "質押更多 XVS 以在下個週期競爭 Prime。", + "stakePrompt": "質押 XVS 以競爭 Prime。", + "stakeToReachTop": "再質押 {{amount}} XVS 即可進入前 #500,並在下個週期取得 Prime 資格。" }, "title": "Prime 排行榜", "totalRewards": { diff --git a/apps/evm/src/pages/PrimeLeaderboard/LastCycleSummaryModal/__tests__/index.spec.tsx b/apps/evm/src/pages/PrimeLeaderboard/LastCycleSummaryModal/__tests__/index.spec.tsx index 233ec76d1b..6e2b8718a4 100644 --- a/apps/evm/src/pages/PrimeLeaderboard/LastCycleSummaryModal/__tests__/index.spec.tsx +++ b/apps/evm/src/pages/PrimeLeaderboard/LastCycleSummaryModal/__tests__/index.spec.tsx @@ -5,10 +5,10 @@ import { renderComponent } from 'testUtils/render'; import { LastCycleSummaryModal } from '..'; describe('pages/PrimeLeaderboard/LastCycleSummaryModal', () => { - it('renders the last cycle total and user Prime rewards', () => { + it('renders the last cycle total and user Prime rewards', async () => { renderComponent( {}} />); - expect(screen.getByText('Last Cycle Prime Summary')).toBeInTheDocument(); + expect(await screen.findByText('Last Cycle Prime Summary')).toBeInTheDocument(); expect( screen.getByText('Total Prime rewards distributed during the last cycle'), ).toBeInTheDocument(); diff --git a/apps/evm/src/pages/PrimeLeaderboard/RankCard/ConnectPrompt/index.tsx b/apps/evm/src/pages/PrimeLeaderboard/RankCard/ConnectPrompt/index.tsx new file mode 100644 index 0000000000..b9a14957bc --- /dev/null +++ b/apps/evm/src/pages/PrimeLeaderboard/RankCard/ConnectPrompt/index.tsx @@ -0,0 +1,28 @@ +import { Button } from '@venusprotocol/ui'; + +import { Icon } from 'components'; +import { useTranslation } from 'libs/translations'; + +export interface ConnectPromptProps { + onConnect: () => void; +} + +export const ConnectPrompt: React.FC = ({ onConnect }) => { + const { t } = useTranslation(); + + return ( + <> +
+ + + + +

{t('primeLeaderboard.rankCard.connectPrompt')}

+
+ + + + ); +}; diff --git a/apps/evm/src/pages/PrimeLeaderboard/RankCard/EligibilityStatus/__tests__/index.spec.tsx b/apps/evm/src/pages/PrimeLeaderboard/RankCard/EligibilityStatus/__tests__/index.spec.tsx new file mode 100644 index 0000000000..3225e4c5ad --- /dev/null +++ b/apps/evm/src/pages/PrimeLeaderboard/RankCard/EligibilityStatus/__tests__/index.spec.tsx @@ -0,0 +1,55 @@ +import { screen } from '@testing-library/react'; + +import { renderComponent } from 'testUtils/render'; + +import { EligibilityStatus } from '..'; + +const baseProps = { + hasStakedXvs: true, + isCandidate: true, + isPrime: true, + hasSupplied: true, + gapXvsTokens: 5_432, +}; + +describe('pages/PrimeLeaderboard/RankCard/EligibilityStatus', () => { + it('congratulates Prime users who supplied and are candidates', () => { + renderComponent(); + + expect( + screen.getByText( + "Congrats! You're in the Top 500 during last cycle and qualified for Prime Rewards.", + ), + ).toBeInTheDocument(); + }); + + it('shows the eligible message for candidates without Prime rewards', () => { + renderComponent(); + + expect( + screen.getByText('You are currently eligible for Prime during the next cycle.'), + ).toBeInTheDocument(); + }); + + it('shows the exact XVS to stake when the gap to the top 500 is small', () => { + renderComponent(); + + expect(screen.getByText('5,432 XVS')).toBeInTheDocument(); + }); + + it('shows a generic message when the gap to the top 500 is large', () => { + renderComponent( + , + ); + + expect( + screen.getByText('Stake more XVS to compete for Prime during the next cycle.'), + ).toBeInTheDocument(); + }); + + it('prompts to stake when no XVS is staked', () => { + renderComponent(); + + expect(screen.getByText('Stake XVS to compete for Prime.')).toBeInTheDocument(); + }); +}); diff --git a/apps/evm/src/pages/PrimeLeaderboard/RankCard/EligibilityStatus/index.tsx b/apps/evm/src/pages/PrimeLeaderboard/RankCard/EligibilityStatus/index.tsx new file mode 100644 index 0000000000..f7861ae6b7 --- /dev/null +++ b/apps/evm/src/pages/PrimeLeaderboard/RankCard/EligibilityStatus/index.tsx @@ -0,0 +1,66 @@ +import { cn } from '@venusprotocol/ui'; +import BigNumber from 'bignumber.js'; + +import { useTranslation } from 'libs/translations'; + +// Maximum XVS gap to the top #500 for which the exact amount left to stake is shown +const TOP_500_GAP_THRESHOLD_XVS = 100_000; + +export interface EligibilityStatusProps { + hasStakedXvs: boolean; + isCandidate: boolean; + isPrime: boolean; + hasSupplied: boolean; + gapXvsTokens: number; + className?: string; +} + +export const EligibilityStatus: React.FC = ({ + hasStakedXvs, + isCandidate, + isPrime, + hasSupplied, + gapXvsTokens, + className, +}) => { + const { t, Trans } = useTranslation(); + + const isEligible = hasStakedXvs && isCandidate; + + if (isEligible && isPrime && hasSupplied) { + return ( +

+ {t('primeLeaderboard.rankCard.eligibleSupplied')} +

+ ); + } + + if (isEligible) { + return ( +

+ {t('primeLeaderboard.rankCard.eligible')} +

+ ); + } + + let stakeMessage: React.ReactNode = t('primeLeaderboard.rankCard.stakePrompt'); + + if (hasStakedXvs && gapXvsTokens <= TOP_500_GAP_THRESHOLD_XVS) { + stakeMessage = ( + }} + /> + ); + } else if (hasStakedXvs) { + stakeMessage = t('primeLeaderboard.rankCard.stakeMore'); + } + + return ( +
+

{t('primeLeaderboard.rankCard.notEligible')}

+

{stakeMessage}

+
+ ); +}; diff --git a/apps/evm/src/pages/PrimeLeaderboard/RankCard/RankActions/index.tsx b/apps/evm/src/pages/PrimeLeaderboard/RankCard/RankActions/index.tsx new file mode 100644 index 0000000000..cf94b5433a --- /dev/null +++ b/apps/evm/src/pages/PrimeLeaderboard/RankCard/RankActions/index.tsx @@ -0,0 +1,35 @@ +import { Button, ButtonWrapper } from '@venusprotocol/ui'; +import { useState } from 'react'; + +import { Icon } from 'components'; +import { routes } from 'constants/routing'; +import { Link } from 'containers/Link'; +import { useTranslation } from 'libs/translations'; + +import { LastCycleSummaryModal } from '../../LastCycleSummaryModal'; + +export const RankActions: React.FC = () => { + const { t } = useTranslation(); + const [isSummaryModalOpen, setIsSummaryModalOpen] = useState(false); + + return ( + <> +
+ + {t('primeLeaderboard.rankCard.stakeButton')} + + + +
+ + {isSummaryModalOpen && ( + setIsSummaryModalOpen(false)} /> + )} + + ); +}; diff --git a/apps/evm/src/pages/PrimeLeaderboard/RankCard/RankSummary/index.tsx b/apps/evm/src/pages/PrimeLeaderboard/RankCard/RankSummary/index.tsx new file mode 100644 index 0000000000..97ad06adbd --- /dev/null +++ b/apps/evm/src/pages/PrimeLeaderboard/RankCard/RankSummary/index.tsx @@ -0,0 +1,26 @@ +import { useTranslation } from 'libs/translations'; + +export interface RankSummaryProps { + rankLabel: string; + primeScoreLabel: string; +} + +export const RankSummary: React.FC = ({ rankLabel, primeScoreLabel }) => { + const { t } = useTranslation(); + + return ( +
+
+ {t('primeLeaderboard.rankCard.rankLabel')} + {rankLabel} +
+ +
+ + {t('primeLeaderboard.rankCard.primeScoreLabel')} + + {primeScoreLabel} +
+
+ ); +}; diff --git a/apps/evm/src/pages/PrimeLeaderboard/RankCard/__tests__/index.spec.tsx b/apps/evm/src/pages/PrimeLeaderboard/RankCard/__tests__/index.spec.tsx index eba976f8a2..017530fcf0 100644 --- a/apps/evm/src/pages/PrimeLeaderboard/RankCard/__tests__/index.spec.tsx +++ b/apps/evm/src/pages/PrimeLeaderboard/RankCard/__tests__/index.spec.tsx @@ -1,13 +1,23 @@ import { screen } from '@testing-library/react'; +import noop from 'noop-ts'; -import fakeAddress from '__mocks__/models/address'; import { renderComponent } from 'testUtils/render'; -import { RankCard } from '..'; +import { type PrimeRankData, RankCard } from '..'; + +const rankData: PrimeRankData = { + hasStakedXvs: true, + isCandidate: true, + isPrime: true, + hasSupplied: true, + rank: 2, + primeScore: 542_500_000, + gapXvsTokens: 5_432, +}; describe('pages/PrimeLeaderboard/RankCard', () => { - it('renders the rank, Prime score and actions when the wallet is connected', () => { - renderComponent(, { accountAddress: fakeAddress }); + it('renders the rank, Prime score and actions when connected', () => { + renderComponent(); expect(screen.getByText('#2')).toBeInTheDocument(); expect(screen.getByText('542.5M')).toBeInTheDocument(); @@ -15,8 +25,8 @@ describe('pages/PrimeLeaderboard/RankCard', () => { expect(screen.getByText('Rules')).toBeInTheDocument(); }); - it('prompts to connect the wallet when none is connected', () => { - renderComponent(); + it('prompts to connect the wallet when not connected', () => { + renderComponent(); expect(screen.getByText('Connect wallet to check your Prime eligibility.')).toBeInTheDocument(); expect(screen.queryByText('#2')).not.toBeInTheDocument(); diff --git a/apps/evm/src/pages/PrimeLeaderboard/RankCard/index.tsx b/apps/evm/src/pages/PrimeLeaderboard/RankCard/index.tsx index ddecb049fa..04ffb3f7b4 100644 --- a/apps/evm/src/pages/PrimeLeaderboard/RankCard/index.tsx +++ b/apps/evm/src/pages/PrimeLeaderboard/RankCard/index.tsx @@ -1,112 +1,70 @@ -import { Button, ButtonWrapper, cn } from '@venusprotocol/ui'; +import { cn } from '@venusprotocol/ui'; import BigNumber from 'bignumber.js'; -import { useState } from 'react'; -import { Icon } from 'components'; -import { routes } from 'constants/routing'; -import { Link } from 'containers/Link'; -import { useTranslation } from 'libs/translations'; -import { useAccountAddress, useAuthModal } from 'libs/wallet'; import { shortenValueWithSuffix } from 'utilities'; -import { LastCycleSummaryModal } from '../LastCycleSummaryModal'; +import { ConnectPrompt } from './ConnectPrompt'; +import { EligibilityStatus } from './EligibilityStatus'; +import { RankActions } from './RankActions'; +import { RankSummary } from './RankSummary'; -// TODO: replace these placeholder values with the rank data returned by the API -const placeholderRank = 2; -const placeholderPrimeScore = 542_500_000; -const placeholderIsEligible = true; +export interface PrimeRankData { + hasStakedXvs: boolean; + isCandidate: boolean; + isPrime: boolean; + hasSupplied: boolean; + rank: number; + primeScore: number; + gapXvsTokens: number; +} export interface RankCardProps { + isUserConnected: boolean; + onConnect: () => void; + rankData: PrimeRankData; className?: string; } -export const RankCard: React.FC = ({ className }) => { - const { t } = useTranslation(); - const { accountAddress } = useAccountAddress(); - const { openAuthModal } = useAuthModal(); - const [isSummaryModalOpen, setIsSummaryModalOpen] = useState(false); - +export const RankCard: React.FC = ({ + isUserConnected, + onConnect, + rankData, + className, +}) => { const cardClassName = cn('flex h-58 flex-col rounded-lg bg-background-active p-4', className); - if (!accountAddress) { + if (!isUserConnected) { return (
-
- - - - -

{t('primeLeaderboard.rankCard.connectPrompt')}

-
- - +
); } - return ( - <> -
-
-
-
- - {t('primeLeaderboard.rankCard.rankLabel')} - - - {placeholderIsEligible ? `#${placeholderRank}` : '#-'} - -
- -
- - {t('primeLeaderboard.rankCard.primeScoreLabel')} - - - {placeholderIsEligible - ? shortenValueWithSuffix({ value: new BigNumber(placeholderPrimeScore) }) - : '-'} - -
-
+ const { hasStakedXvs, isCandidate, isPrime, hasSupplied, rank, primeScore, gapXvsTokens } = + rankData; - {placeholderIsEligible ? ( -

{t('primeLeaderboard.rankCard.eligible')}

- ) : ( -
-

{t('primeLeaderboard.rankCard.notEligible')}

-

{t('primeLeaderboard.rankCard.stakePrompt')}

-
- )} -
+ const rankLabel = hasStakedXvs ? `#${rank}` : '#-'; + const primeScoreLabel = hasStakedXvs + ? shortenValueWithSuffix({ value: new BigNumber(primeScore) }) + : '-'; -
- - {t('primeLeaderboard.rankCard.stakeButton')} - + return ( +
+
+ - -
+
- setIsSummaryModalOpen(false)} - /> - + +
); }; diff --git a/apps/evm/src/pages/PrimeLeaderboard/index.tsx b/apps/evm/src/pages/PrimeLeaderboard/index.tsx index 0993f16550..c54444116a 100644 --- a/apps/evm/src/pages/PrimeLeaderboard/index.tsx +++ b/apps/evm/src/pages/PrimeLeaderboard/index.tsx @@ -1,10 +1,10 @@ import { Card, Page } from 'components'; import { useGetTokens } from 'libs/tokens'; -import { useAccountAddress } from 'libs/wallet'; +import { useAccountAddress, useAuthModal } from 'libs/wallet'; import { EndOfCycle } from './EndOfCycle'; import { Hero } from './Hero'; -import { RankCard } from './RankCard'; +import { type PrimeRankData, RankCard } from './RankCard'; import { RankTable } from './RankTable'; import { RewardTable } from './RewardTable'; import { TotalRewardsCard } from './TotalRewardsCard'; @@ -16,8 +16,20 @@ const placeholderUserRewardsCents = 1_840_000; const placeholderMarketRewardsCents = [28_040_000, 17_190_000]; const placeholderApyPercentage = 3.78; +// TODO: replace this placeholder with the rank data returned by the API +const placeholderRankData: PrimeRankData = { + hasStakedXvs: true, + isCandidate: true, + isPrime: true, + hasSupplied: true, + rank: 2, + primeScore: 542_500_000, + gapXvsTokens: 5_432, +}; + const PrimeLeaderboard: React.FC = () => { const { accountAddress } = useAccountAddress(); + const { openAuthModal } = useAuthModal(); const tokens = useGetTokens(); // TODO: replace these placeholder tokens (currently the first tokens from the list, unrelated @@ -76,7 +88,11 @@ const PrimeLeaderboard: React.FC = () => { - + openAuthModal({ analyticVariant: 'primeLeaderboardRankCard' })} + rankData={placeholderRankData} + /> From 62bff1f6a330d9bda41e5b7d79c8229d6f2e9536 Mon Sep 17 00:00:00 2001 From: cuzz-venus Date: Mon, 15 Jun 2026 16:08:47 +0800 Subject: [PATCH 10/18] feat: support total rewards --- .../__tests__/index.spec.tsx | 14 +++++++ .../TotalRewardsSection/index.tsx | 18 +++++++++ .../__tests__/index.spec.tsx | 14 +++++++ .../UserRewardsSection/index.tsx | 18 +++++++++ .../PrimeLeaderboard/__tests__/index.spec.tsx | 16 ++++---- apps/evm/src/pages/PrimeLeaderboard/index.tsx | 39 +++---------------- .../useGetPrimeTotalRewards/index.ts | 26 +++++++++++++ .../useGetPrimeUserRewards/index.ts | 31 +++++++++++++++ 8 files changed, 134 insertions(+), 42 deletions(-) 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/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/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/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 318e719c9c..c3c6a43284 100644 --- a/apps/evm/src/pages/PrimeLeaderboard/__tests__/index.spec.tsx +++ b/apps/evm/src/pages/PrimeLeaderboard/__tests__/index.spec.tsx @@ -17,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', () => ({ @@ -43,8 +43,8 @@ describe('pages/PrimeLeaderboard', () => { 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(); @@ -53,7 +53,7 @@ describe('pages/PrimeLeaderboard', () => { it('hides the user rewards card when the wallet is not connected', () => { renderComponent(); - expect(screen.getByTestId('total-rewards-card')).toBeInTheDocument(); - expect(screen.queryByTestId('user-rewards-card')).not.toBeInTheDocument(); + 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 0993f16550..b2e11206df 100644 --- a/apps/evm/src/pages/PrimeLeaderboard/index.tsx +++ b/apps/evm/src/pages/PrimeLeaderboard/index.tsx @@ -1,5 +1,4 @@ import { Card, Page } from 'components'; -import { useGetTokens } from 'libs/tokens'; import { useAccountAddress } from 'libs/wallet'; import { EndOfCycle } from './EndOfCycle'; @@ -7,36 +6,11 @@ import { Hero } from './Hero'; import { RankCard } from './RankCard'; import { RankTable } from './RankTable'; import { RewardTable } from './RewardTable'; -import { TotalRewardsCard } from './TotalRewardsCard'; -import { UserRewardsCard } from './UserRewardsCard'; - -// TODO: use the reward pool data returned by the API -const placeholderTotalRewardsCents = 46_230_000; -const placeholderUserRewardsCents = 1_840_000; -const placeholderMarketRewardsCents = [28_040_000, 17_190_000]; -const placeholderApyPercentage = 3.78; +import { TotalRewardsSection } from './TotalRewardsSection'; +import { UserRewardsSection } from './UserRewardsSection'; const PrimeLeaderboard: React.FC = () => { const { accountAddress } = useAccountAddress(); - const tokens = useGetTokens(); - - // TODO: replace these placeholder tokens (currently the first tokens from the list, unrelated - // to the actual Prime reward markets) with the real markets returned by the API - const marketRewards = tokens - .slice(0, placeholderMarketRewardsCents.length) - .map((token, index) => ({ token, rewardsCents: placeholderMarketRewardsCents[index] })); - - const userMarketRewards = marketRewards.map(reward => ({ - ...reward, - apyPercentage: placeholderApyPercentage, - })); - - const totalRewardsCard = ( - - ); return ( @@ -61,15 +35,12 @@ const PrimeLeaderboard: React.FC = () => { {accountAddress ? (
- {totalRewardsCard} + - +
) : ( - totalRewardsCard + )} 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 d086115b6590ff298de6472bf2f621478a851a5a Mon Sep 17 00:00:00 2001 From: cuzz-venus Date: Mon, 15 Jun 2026 16:46:50 +0800 Subject: [PATCH 11/18] feat: rank section style --- .../RankSection/__tests__/index.spec.tsx | 21 +++++++++++++++++ .../PrimeLeaderboard/RankSection/index.tsx | 23 +++++++++++++++++++ .../PrimeLeaderboard/__tests__/index.spec.tsx | 4 ++-- apps/evm/src/pages/PrimeLeaderboard/index.tsx | 22 +++--------------- .../PrimeLeaderboard/useGetPrimeRank/index.ts | 14 +++++++++++ 5 files changed, 63 insertions(+), 21 deletions(-) create mode 100644 apps/evm/src/pages/PrimeLeaderboard/RankSection/__tests__/index.spec.tsx create mode 100644 apps/evm/src/pages/PrimeLeaderboard/RankSection/index.tsx create mode 100644 apps/evm/src/pages/PrimeLeaderboard/useGetPrimeRank/index.ts diff --git a/apps/evm/src/pages/PrimeLeaderboard/RankSection/__tests__/index.spec.tsx b/apps/evm/src/pages/PrimeLeaderboard/RankSection/__tests__/index.spec.tsx new file mode 100644 index 0000000000..727cfe3202 --- /dev/null +++ b/apps/evm/src/pages/PrimeLeaderboard/RankSection/__tests__/index.spec.tsx @@ -0,0 +1,21 @@ +import { screen } from '@testing-library/react'; + +import fakeAddress from '__mocks__/models/address'; +import { renderComponent } from 'testUtils/render'; + +import { RankSection } from '..'; + +describe('pages/PrimeLeaderboard/RankSection', () => { + it('renders the rank card with the data hook when connected', () => { + renderComponent(, { accountAddress: fakeAddress }); + + expect(screen.getByText('#2')).toBeInTheDocument(); + expect(screen.getByText('542.5M')).toBeInTheDocument(); + }); + + it('prompts to connect the wallet when not connected', () => { + renderComponent(); + + expect(screen.getByText('Connect wallet to check your Prime eligibility.')).toBeInTheDocument(); + }); +}); diff --git a/apps/evm/src/pages/PrimeLeaderboard/RankSection/index.tsx b/apps/evm/src/pages/PrimeLeaderboard/RankSection/index.tsx new file mode 100644 index 0000000000..dc723e3477 --- /dev/null +++ b/apps/evm/src/pages/PrimeLeaderboard/RankSection/index.tsx @@ -0,0 +1,23 @@ +import { useAccountAddress, useAuthModal } from 'libs/wallet'; + +import { RankCard } from '../RankCard'; +import { useGetPrimeRank } from '../useGetPrimeRank'; + +export interface RankSectionProps { + className?: string; +} + +export const RankSection: React.FC = ({ className }) => { + const { accountAddress } = useAccountAddress(); + const { openAuthModal } = useAuthModal(); + const rankData = useGetPrimeRank(); + + return ( + openAuthModal({ analyticVariant: 'primeLeaderboardRankCard' })} + rankData={rankData} + className={className} + /> + ); +}; diff --git a/apps/evm/src/pages/PrimeLeaderboard/__tests__/index.spec.tsx b/apps/evm/src/pages/PrimeLeaderboard/__tests__/index.spec.tsx index c3c6a43284..6d378863b0 100644 --- a/apps/evm/src/pages/PrimeLeaderboard/__tests__/index.spec.tsx +++ b/apps/evm/src/pages/PrimeLeaderboard/__tests__/index.spec.tsx @@ -29,8 +29,8 @@ vi.mock('../RewardTable', () => ({ RewardTable: () =>
, })); -vi.mock('../RankCard', () => ({ - RankCard: () =>
, +vi.mock('../RankSection', () => ({ + RankSection: () =>
, })); vi.mock('../RankTable', () => ({ diff --git a/apps/evm/src/pages/PrimeLeaderboard/index.tsx b/apps/evm/src/pages/PrimeLeaderboard/index.tsx index 1acc4d940a..40ffcba770 100644 --- a/apps/evm/src/pages/PrimeLeaderboard/index.tsx +++ b/apps/evm/src/pages/PrimeLeaderboard/index.tsx @@ -1,28 +1,16 @@ import { Card, Page } from 'components'; -import { useAccountAddress, useAuthModal } from 'libs/wallet'; +import { useAccountAddress } from 'libs/wallet'; import { EndOfCycle } from './EndOfCycle'; import { Hero } from './Hero'; -import { type PrimeRankData, RankCard } from './RankCard'; +import { RankSection } from './RankSection'; import { RankTable } from './RankTable'; import { RewardTable } from './RewardTable'; import { TotalRewardsSection } from './TotalRewardsSection'; import { UserRewardsSection } from './UserRewardsSection'; -// TODO: replace this placeholder with the rank data returned by the API -const placeholderRankData: PrimeRankData = { - hasStakedXvs: true, - isCandidate: true, - isPrime: true, - hasSupplied: true, - rank: 2, - primeScore: 542_500_000, - gapXvsTokens: 5_432, -}; - const PrimeLeaderboard: React.FC = () => { const { accountAddress } = useAccountAddress(); - const { openAuthModal } = useAuthModal(); return ( @@ -59,11 +47,7 @@ const PrimeLeaderboard: React.FC = () => { - openAuthModal({ analyticVariant: 'primeLeaderboardRankCard' })} - rankData={placeholderRankData} - /> + diff --git a/apps/evm/src/pages/PrimeLeaderboard/useGetPrimeRank/index.ts b/apps/evm/src/pages/PrimeLeaderboard/useGetPrimeRank/index.ts new file mode 100644 index 0000000000..62e733a08e --- /dev/null +++ b/apps/evm/src/pages/PrimeLeaderboard/useGetPrimeRank/index.ts @@ -0,0 +1,14 @@ +import type { PrimeRankData } from '../RankCard'; + +// TODO: replace this placeholder with the rank data returned by the API +const placeholderRankData: PrimeRankData = { + hasStakedXvs: true, + isCandidate: true, + isPrime: true, + hasSupplied: true, + rank: 2, + primeScore: 542_500_000, + gapXvsTokens: 5_432, +}; + +export const useGetPrimeRank = (): PrimeRankData => placeholderRankData; From de401167432d7e20962bf3a61496401dc73e4e64 Mon Sep 17 00:00:00 2001 From: cuzz-venus Date: Mon, 15 Jun 2026 16:49:35 +0800 Subject: [PATCH 12/18] feat: rank section style --- apps/evm/src/pages/PrimeLeaderboard/__tests__/index.spec.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/evm/src/pages/PrimeLeaderboard/__tests__/index.spec.tsx b/apps/evm/src/pages/PrimeLeaderboard/__tests__/index.spec.tsx index 6d378863b0..c03186009c 100644 --- a/apps/evm/src/pages/PrimeLeaderboard/__tests__/index.spec.tsx +++ b/apps/evm/src/pages/PrimeLeaderboard/__tests__/index.spec.tsx @@ -30,7 +30,7 @@ vi.mock('../RewardTable', () => ({ })); vi.mock('../RankSection', () => ({ - RankSection: () =>
, + RankSection: () =>
, })); vi.mock('../RankTable', () => ({ @@ -46,7 +46,7 @@ describe('pages/PrimeLeaderboard', () => { 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-section')).toBeInTheDocument(); expect(screen.getByTestId('rank-table')).toBeInTheDocument(); }); From 382ad06e026003f6c3a46c3fe26c8cecad837e99 Mon Sep 17 00:00:00 2001 From: cuzz-venus Date: Mon, 15 Jun 2026 17:08:42 +0800 Subject: [PATCH 13/18] feat: rank section style --- .../src/components/Icon/icons/barChart.tsx | 70 ++++++++++--------- .../LastCycleSummaryModal/index.tsx | 43 +++++++----- 2 files changed, 62 insertions(+), 51 deletions(-) diff --git a/apps/evm/src/components/Icon/icons/barChart.tsx b/apps/evm/src/components/Icon/icons/barChart.tsx index 5e74433cc6..17884eea87 100644 --- a/apps/evm/src/components/Icon/icons/barChart.tsx +++ b/apps/evm/src/components/Icon/icons/barChart.tsx @@ -1,37 +1,41 @@ -import type { SVGProps } from 'react'; +import { type SVGProps, useId } from 'react'; -const SvgBarChart = (props: SVGProps) => ( - - - - - ) => { + const maskId = useId(); + + return ( + - - - -); + viewBox="0 0 20 20" + fill="none" + xmlns="http://www.w3.org/2000/svg" + {...props} + > + + + + + + + + ); +}; export default SvgBarChart; diff --git a/apps/evm/src/pages/PrimeLeaderboard/LastCycleSummaryModal/index.tsx b/apps/evm/src/pages/PrimeLeaderboard/LastCycleSummaryModal/index.tsx index 24a209b984..e8bba2eca7 100644 --- a/apps/evm/src/pages/PrimeLeaderboard/LastCycleSummaryModal/index.tsx +++ b/apps/evm/src/pages/PrimeLeaderboard/LastCycleSummaryModal/index.tsx @@ -43,25 +43,32 @@ export const LastCycleSummaryModal: React.FC = ({ apyPercentage: placeholderApyPercentage, })); - const userRewardsContent = placeholderHasRewards ? undefined : ( -
- {placeholderIsEligible ? ( - - ) : ( - - - - )} + let userRewardsContent: React.ReactNode; -

- {placeholderIsEligible - ? t('primeLeaderboard.lastCycleSummary.eligibleMessage') - : t('primeLeaderboard.lastCycleSummary.notEligibleMessage')} -

-
- ); + if (!placeholderHasRewards) { + userRewardsContent = ( +
+ {placeholderIsEligible ? ( + + ) : ( + + + + )} + +

+ {placeholderIsEligible + ? t('primeLeaderboard.lastCycleSummary.eligibleMessage') + : t('primeLeaderboard.lastCycleSummary.notEligibleMessage')} +

+
+ ); + } return ( Date: Mon, 15 Jun 2026 18:52:20 +0800 Subject: [PATCH 14/18] fix: remove last cycle modal --- .../libs/translations/translations/en.json | 6 -- .../libs/translations/translations/ja.json | 6 -- .../libs/translations/translations/th.json | 6 -- .../libs/translations/translations/tr.json | 6 -- .../libs/translations/translations/vi.json | 6 -- .../translations/translations/zh-Hans.json | 6 -- .../translations/translations/zh-Hant.json | 6 -- .../__tests__/index.spec.tsx | 17 ---- .../LastCycleSummaryModal/index.tsx | 96 ------------------- .../RankCard/RankActions/index.tsx | 32 +++---- 10 files changed, 11 insertions(+), 176 deletions(-) delete mode 100644 apps/evm/src/pages/PrimeLeaderboard/LastCycleSummaryModal/__tests__/index.spec.tsx delete mode 100644 apps/evm/src/pages/PrimeLeaderboard/LastCycleSummaryModal/index.tsx diff --git a/apps/evm/src/libs/translations/translations/en.json b/apps/evm/src/libs/translations/translations/en.json index 54d40f405e..cdca1a2058 100644 --- a/apps/evm/src/libs/translations/translations/en.json +++ b/apps/evm/src/libs/translations/translations/en.json @@ -1247,12 +1247,6 @@ }, "primeLeaderboard": { "description": "Stake XVS into the governance vault and receive Prime rewards on top of your yields.", - "lastCycleSummary": { - "eligibleMessage": "You qualified for Prime in this cycle. Take positions in Prime markets to earn additional rewards.", - "notEligibleMessage": "You did not qualify for Prime during the last cycle. Stake XVS to compete for Prime in the next cycle.", - "title": "Last Cycle Prime Summary", - "totalRewardsTitle": "Total Prime rewards distributed during the last cycle" - }, "rankCard": { "connectPrompt": "Connect wallet to check your Prime eligibility.", "eligible": "You are currently eligible for Prime during the next cycle.", diff --git a/apps/evm/src/libs/translations/translations/ja.json b/apps/evm/src/libs/translations/translations/ja.json index 165b80a455..e0d5d517a8 100644 --- a/apps/evm/src/libs/translations/translations/ja.json +++ b/apps/evm/src/libs/translations/translations/ja.json @@ -1247,12 +1247,6 @@ }, "primeLeaderboard": { "description": "ガバナンスボールトに XVS をステークして、利回りに加えて Prime 報酬を獲得しましょう。", - "lastCycleSummary": { - "eligibleMessage": "今サイクルで Prime の対象になりました。Prime マーケットでポジションを取ると追加報酬を獲得できます。", - "notEligibleMessage": "前サイクルでは Prime の対象ではありませんでした。XVS をステークして次のサイクルで Prime を目指しましょう。", - "title": "前サイクルの Prime サマリー", - "totalRewardsTitle": "前サイクルに分配された Prime 報酬の合計" - }, "rankCard": { "connectPrompt": "ウォレットを接続して Prime の対象かを確認してください。", "eligible": "あなたは次のサイクルで Prime の対象です", diff --git a/apps/evm/src/libs/translations/translations/th.json b/apps/evm/src/libs/translations/translations/th.json index 9bab2db8a4..4052ebd921 100644 --- a/apps/evm/src/libs/translations/translations/th.json +++ b/apps/evm/src/libs/translations/translations/th.json @@ -1247,12 +1247,6 @@ }, "primeLeaderboard": { "description": "สเตก XVS เข้าสู่ governance vault แล้วรับรางวัล Prime เพิ่มเติมจากผลตอบแทนของคุณ", - "lastCycleSummary": { - "eligibleMessage": "คุณมีสิทธิ์รับ Prime ในรอบนี้ เปิดสถานะในตลาด Prime เพื่อรับรางวัลเพิ่มเติม", - "notEligibleMessage": "คุณไม่มีสิทธิ์รับ Prime ในรอบที่แล้ว Stake XVS เพื่อแข่งขันรับ Prime ในรอบถัดไป", - "title": "สรุป Prime รอบที่แล้ว", - "totalRewardsTitle": "รางวัล Prime ทั้งหมดที่แจกจ่ายในรอบที่แล้ว" - }, "rankCard": { "connectPrompt": "เชื่อมต่อกระเป๋าเงินเพื่อตรวจสอบสิทธิ์ Prime ของคุณ", "eligible": "คุณมีสิทธิ์รับ Prime ในรอบถัดไป", diff --git a/apps/evm/src/libs/translations/translations/tr.json b/apps/evm/src/libs/translations/translations/tr.json index f15aebd044..d3806deeee 100644 --- a/apps/evm/src/libs/translations/translations/tr.json +++ b/apps/evm/src/libs/translations/translations/tr.json @@ -1247,12 +1247,6 @@ }, "primeLeaderboard": { "description": "XVS’yi governance kasasına stake edin ve getirilerinize ek olarak Prime ödülleri kazanın.", - "lastCycleSummary": { - "eligibleMessage": "Bu döngüde Prime için uygun oldun. Ek ödül kazanmak için Prime piyasalarında pozisyon al.", - "notEligibleMessage": "Son döngüde Prime için uygun değildin. Bir sonraki döngüde Prime için yarışmak üzere XVS stake et.", - "title": "Son Döngü Prime Özeti", - "totalRewardsTitle": "Son döngüde dağıtılan toplam Prime ödülleri" - }, "rankCard": { "connectPrompt": "Prime uygunluğunu görmek için cüzdanını bağla.", "eligible": "Bir sonraki döngüde Prime için uygunsun", diff --git a/apps/evm/src/libs/translations/translations/vi.json b/apps/evm/src/libs/translations/translations/vi.json index ea0b39a918..a624801ac0 100644 --- a/apps/evm/src/libs/translations/translations/vi.json +++ b/apps/evm/src/libs/translations/translations/vi.json @@ -1247,12 +1247,6 @@ }, "primeLeaderboard": { "description": "Stake XVS vào governance vault và nhận thêm phần thưởng Prime bên cạnh lợi nhuận của bạn.", - "lastCycleSummary": { - "eligibleMessage": "Bạn đủ điều kiện nhận Prime trong chu kỳ này. Hãy mở vị thế trong các thị trường Prime để nhận thêm phần thưởng.", - "notEligibleMessage": "Bạn không đủ điều kiện nhận Prime trong chu kỳ trước. Stake XVS để cạnh tranh Prime trong chu kỳ tiếp theo.", - "title": "Tóm tắt Prime chu kỳ trước", - "totalRewardsTitle": "Tổng phần thưởng Prime đã phân phối trong chu kỳ trước" - }, "rankCard": { "connectPrompt": "Kết nối ví để kiểm tra điều kiện Prime của bạn.", "eligible": "Bạn hiện đủ điều kiện nhận Prime trong chu kỳ tiếp theo", diff --git a/apps/evm/src/libs/translations/translations/zh-Hans.json b/apps/evm/src/libs/translations/translations/zh-Hans.json index 903810cbd3..7345daf2be 100644 --- a/apps/evm/src/libs/translations/translations/zh-Hans.json +++ b/apps/evm/src/libs/translations/translations/zh-Hans.json @@ -1247,12 +1247,6 @@ }, "primeLeaderboard": { "description": "将 XVS 质押到治理金库,在收益之上再赚取 Prime 奖励。", - "lastCycleSummary": { - "eligibleMessage": "你在本周期已具备 Prime 资格。在 Prime 市场建仓以赚取额外奖励。", - "notEligibleMessage": "你在上一周期未具备 Prime 资格。质押 XVS 以在下一周期竞争 Prime。", - "title": "上一周期 Prime 汇总", - "totalRewardsTitle": "上一周期分发的 Prime 总奖励" - }, "rankCard": { "connectPrompt": "连接钱包以查看你的 Prime 资格。", "eligible": "你当前已具备下个周期的 Prime 资格", diff --git a/apps/evm/src/libs/translations/translations/zh-Hant.json b/apps/evm/src/libs/translations/translations/zh-Hant.json index 5eee33ad1c..0f97a51c0f 100644 --- a/apps/evm/src/libs/translations/translations/zh-Hant.json +++ b/apps/evm/src/libs/translations/translations/zh-Hant.json @@ -1247,12 +1247,6 @@ }, "primeLeaderboard": { "description": "將 XVS 質押到治理金庫,在收益之上再賺取 Prime 獎勵。", - "lastCycleSummary": { - "eligibleMessage": "你在本週期已具備 Prime 資格。在 Prime 市場建倉以賺取額外獎勵。", - "notEligibleMessage": "你在上一週期未具備 Prime 資格。質押 XVS 以在下一週期競爭 Prime。", - "title": "上一週期 Prime 彙總", - "totalRewardsTitle": "上一週期分發的 Prime 總獎勵" - }, "rankCard": { "connectPrompt": "連接錢包以查看你的 Prime 資格。", "eligible": "你目前已具備下個週期的 Prime 資格", diff --git a/apps/evm/src/pages/PrimeLeaderboard/LastCycleSummaryModal/__tests__/index.spec.tsx b/apps/evm/src/pages/PrimeLeaderboard/LastCycleSummaryModal/__tests__/index.spec.tsx deleted file mode 100644 index 6e2b8718a4..0000000000 --- a/apps/evm/src/pages/PrimeLeaderboard/LastCycleSummaryModal/__tests__/index.spec.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import { screen } from '@testing-library/react'; - -import { renderComponent } from 'testUtils/render'; - -import { LastCycleSummaryModal } from '..'; - -describe('pages/PrimeLeaderboard/LastCycleSummaryModal', () => { - it('renders the last cycle total and user Prime rewards', async () => { - renderComponent( {}} />); - - expect(await screen.findByText('Last Cycle Prime Summary')).toBeInTheDocument(); - expect( - screen.getByText('Total Prime rewards distributed during the last cycle'), - ).toBeInTheDocument(); - expect(screen.getByText('Your Prime rewards this cycle')).toBeInTheDocument(); - }); -}); diff --git a/apps/evm/src/pages/PrimeLeaderboard/LastCycleSummaryModal/index.tsx b/apps/evm/src/pages/PrimeLeaderboard/LastCycleSummaryModal/index.tsx deleted file mode 100644 index e8bba2eca7..0000000000 --- a/apps/evm/src/pages/PrimeLeaderboard/LastCycleSummaryModal/index.tsx +++ /dev/null @@ -1,96 +0,0 @@ -import { cn } from '@venusprotocol/ui'; - -import { Icon, Modal } from 'components'; -import { useGetTokens } from 'libs/tokens'; -import { useTranslation } from 'libs/translations'; - -import { PrimeRewardBadge } from '../PrimeRewardBadge'; -import { TotalRewardsCard } from '../TotalRewardsCard'; -import { UserRewardsCard } from '../UserRewardsCard'; - -// TODO: replace these placeholder values with the data returned by the API -const placeholderTotalRewardsCents = 46_230_000; -const placeholderTotalMarketRewardsCents = [28_040_000, 17_190_000]; -const placeholderUserRewardsCents = 1_840_000; -const placeholderUserMarketRewardsCents = [1_140_000, 700_000]; -const placeholderApyPercentage = 3.78; -const placeholderHasRewards = true; -const placeholderIsEligible = true; - -export interface LastCycleSummaryModalProps { - isOpen: boolean; - handleClose: () => void; -} - -export const LastCycleSummaryModal: React.FC = ({ - isOpen, - handleClose, -}) => { - const { t } = useTranslation(); - const tokens = useGetTokens(); - - // TODO: replace these placeholder tokens with the real Prime markets returned by the API - const markets = tokens.slice(0, placeholderTotalMarketRewardsCents.length); - - const totalMarketRewards = markets.map((token, index) => ({ - token, - rewardsCents: placeholderTotalMarketRewardsCents[index], - })); - - const userMarketRewards = markets.map((token, index) => ({ - token, - rewardsCents: placeholderUserMarketRewardsCents[index], - apyPercentage: placeholderApyPercentage, - })); - - let userRewardsContent: React.ReactNode; - - if (!placeholderHasRewards) { - userRewardsContent = ( -
- {placeholderIsEligible ? ( - - ) : ( - - - - )} - -

- {placeholderIsEligible - ? t('primeLeaderboard.lastCycleSummary.eligibleMessage') - : t('primeLeaderboard.lastCycleSummary.notEligibleMessage')} -

-
- ); - } - - return ( - -
- - - -
-
- ); -}; diff --git a/apps/evm/src/pages/PrimeLeaderboard/RankCard/RankActions/index.tsx b/apps/evm/src/pages/PrimeLeaderboard/RankCard/RankActions/index.tsx index cf94b5433a..f25d49ca04 100644 --- a/apps/evm/src/pages/PrimeLeaderboard/RankCard/RankActions/index.tsx +++ b/apps/evm/src/pages/PrimeLeaderboard/RankCard/RankActions/index.tsx @@ -1,35 +1,25 @@ import { Button, ButtonWrapper } from '@venusprotocol/ui'; -import { useState } from 'react'; import { Icon } from 'components'; import { routes } from 'constants/routing'; import { Link } from 'containers/Link'; import { useTranslation } from 'libs/translations'; -import { LastCycleSummaryModal } from '../../LastCycleSummaryModal'; - export const RankActions: React.FC = () => { const { t } = useTranslation(); - const [isSummaryModalOpen, setIsSummaryModalOpen] = useState(false); return ( - <> -
- - {t('primeLeaderboard.rankCard.stakeButton')} - - - -
+
+ + {t('primeLeaderboard.rankCard.stakeButton')} + - {isSummaryModalOpen && ( - setIsSummaryModalOpen(false)} /> - )} - + +
); }; From 3a622938c432f1abadaeef6ed1ec98441bfbfab3 Mon Sep 17 00:00:00 2001 From: cuzz-venus Date: Mon, 15 Jun 2026 19:47:10 +0800 Subject: [PATCH 15/18] feat: add rules --- .../libs/translations/translations/en.json | 15 +++ .../libs/translations/translations/ja.json | 15 +++ .../libs/translations/translations/th.json | 15 +++ .../libs/translations/translations/tr.json | 15 +++ .../libs/translations/translations/vi.json | 15 +++ .../translations/translations/zh-Hans.json | 15 +++ .../translations/translations/zh-Hant.json | 15 +++ .../RankCard/RankActions/index.tsx | 30 +++--- .../RulesModal/__tests__/index.spec.tsx | 15 +++ .../PrimeLeaderboard/RulesModal/index.tsx | 98 +++++++++++++++++++ 10 files changed, 237 insertions(+), 11 deletions(-) create mode 100644 apps/evm/src/pages/PrimeLeaderboard/RulesModal/__tests__/index.spec.tsx create mode 100644 apps/evm/src/pages/PrimeLeaderboard/RulesModal/index.tsx diff --git a/apps/evm/src/libs/translations/translations/en.json b/apps/evm/src/libs/translations/translations/en.json index cdca1a2058..993af60a7c 100644 --- a/apps/evm/src/libs/translations/translations/en.json +++ b/apps/evm/src/libs/translations/translations/en.json @@ -1260,6 +1260,21 @@ "stakePrompt": "Stake XVS to compete for Prime.", "stakeToReachTop": "Stake {{amount}} XVS to reach the top #500 and become eligible for Prime during the next cycle." }, + "rulesModal": { + "boostColumn": "Boost", + "contribution": "For each deposit, your contribution is XVS × boost × days held. Your total Prime score is the sum across all your deposits.", + "days": "days", + "daysHeldColumn": "Days held", + "footer": "To climb the leaderboard: stake more, and hold longer.", + "intro1": "Your Prime score decides your spot on the leaderboard. The Top 500 each month become Prime holders and share the monthly reward pool.", + "intro2": "Your score rewards both how much XVS you stake and how long you hold it. Each deposit earns a loyalty boost based on how old it is:", + "max": "max", + "point1": "Both the boost and the days count are capped at 90 days — holding longer doesn't increase the score per XVS further.", + "point2": "Withdrawing? Your newest deposits go first. Your oldest, highest-boost stake is protected.", + "point3": "The score updates live — but your Prime status is locked in at the monthly snapshot.", + "thingsToKnowTitle": "A few things to know:", + "title": "Rules" + }, "title": "Prime leaderboard", "totalRewards": { "title": "Total Prime rewards distributed this cycle" diff --git a/apps/evm/src/libs/translations/translations/ja.json b/apps/evm/src/libs/translations/translations/ja.json index e0d5d517a8..beb851cb4e 100644 --- a/apps/evm/src/libs/translations/translations/ja.json +++ b/apps/evm/src/libs/translations/translations/ja.json @@ -1260,6 +1260,21 @@ "stakePrompt": "XVS をステークして Prime を目指しましょう。", "stakeToReachTop": "あと {{amount}} XVS をステークするとトップ #500 に入り、次のサイクルで Prime の対象になります。" }, + "rulesModal": { + "boostColumn": "ブースト", + "contribution": "各預け入れについて、あなたの貢献度は XVS × ブースト × 保有日数 です。Prime の合計スコアは、すべての預け入れの合計です。", + "days": "日", + "daysHeldColumn": "保有日数", + "footer": "リーダーボードで上位を目指すには、より多くステークし、より長く保有しましょう。", + "intro1": "Prime スコア がリーダーボードでの順位を決めます。毎月 上位 500 名 が Prime ホルダーとなり、月間報酬プールを分け合います。", + "intro2": "スコアは、ステークした XVS の 保有期間 の両方を評価します。各預け入れは、その経過期間に応じてロイヤルティブーストを獲得します:", + "max": "上限", + "point1": "ブーストと日数はどちらも 90 日が上限です —— それ以上保有しても XVS あたりのスコアはこれ以上増えません。", + "point2": "出金する場合は?最新の預け入れが先に引き出されます。 最も古く、ブーストが最も高いステークは保護されます。", + "point3": "スコアは リアルタイム で更新されます —— ただし Prime ステータスは 毎月のスナップショット で確定します。", + "thingsToKnowTitle": "知っておくべきこと:", + "title": "ルール" + }, "title": "Prime リーダーボード", "totalRewards": { "title": "今サイクルに分配された Prime 報酬の総額" diff --git a/apps/evm/src/libs/translations/translations/th.json b/apps/evm/src/libs/translations/translations/th.json index 4052ebd921..3b8a58c4a4 100644 --- a/apps/evm/src/libs/translations/translations/th.json +++ b/apps/evm/src/libs/translations/translations/th.json @@ -1260,6 +1260,21 @@ "stakePrompt": "Stake XVS เพื่อแข่งขันรับ Prime", "stakeToReachTop": "Stake อีก {{amount}} XVS เพื่อขึ้นไปอยู่ใน top #500 และมีสิทธิ์รับ Prime ในรอบถัดไป" }, + "rulesModal": { + "boostColumn": "ตัวคูณ", + "contribution": "สำหรับการฝากแต่ละครั้ง การมีส่วนร่วมของคุณคือ XVS × ตัวคูณ × จำนวนวันที่ถือ คะแนน Prime รวมของคุณคือผลรวมของการฝากทั้งหมด", + "days": "วัน", + "daysHeldColumn": "จำนวนวันที่ถือ", + "footer": "เพื่อไต่อันดับลีดเดอร์บอร์ด: stake มากขึ้นและถือนานขึ้น", + "intro1": "คะแนน Prime ของคุณกำหนดอันดับของคุณบนลีดเดอร์บอร์ด 500 อันดับแรก ในแต่ละเดือนจะกลายเป็นผู้ถือ Prime และแบ่งปันพูลรางวัลรายเดือน", + "intro2": "คะแนนของคุณให้รางวัลทั้ง จำนวน XVS ที่คุณ stake และ ระยะเวลา ที่คุณถือ แต่ละการฝากจะได้รับตัวคูณความภักดีตามระยะเวลาที่ถือ:", + "max": "สูงสุด", + "point1": "ทั้งตัวคูณและจำนวนวันถูกจำกัดที่ 90 วัน — การถือนานกว่านั้นจะไม่เพิ่มคะแนนต่อ XVS อีก", + "point2": "ถอนเงิน? เงินฝากล่าสุดของคุณจะถูกหักก่อน เงิน stake ที่เก่าที่สุดและมีตัวคูณสูงสุดของคุณจะได้รับการปกป้อง", + "point3": "คะแนนอัปเดต แบบเรียลไทม์ — แต่สถานะ Prime ของคุณจะถูกล็อกที่ สแน็ปช็อตรายเดือน", + "thingsToKnowTitle": "สิ่งที่ควรรู้:", + "title": "กฎ" + }, "title": "กระดานผู้นำ Prime", "totalRewards": { "title": "รางวัล Prime ทั้งหมดที่แจกในรอบนี้" diff --git a/apps/evm/src/libs/translations/translations/tr.json b/apps/evm/src/libs/translations/translations/tr.json index d3806deeee..e7888ce1cc 100644 --- a/apps/evm/src/libs/translations/translations/tr.json +++ b/apps/evm/src/libs/translations/translations/tr.json @@ -1260,6 +1260,21 @@ "stakePrompt": "Prime için yarışmak üzere XVS stake et.", "stakeToReachTop": "İlk #500'e girmek ve bir sonraki döngüde Prime için uygun olmak için {{amount}} XVS stake et." }, + "rulesModal": { + "boostColumn": "Artış", + "contribution": "Her yatırma için katkınız XVS × artış × tutulan gün şeklindedir. Toplam Prime puanınız tüm yatırmalarınızın toplamıdır.", + "days": "gün", + "daysHeldColumn": "Tutulan gün", + "footer": "Liderlik tablosunda yükselmek için: daha fazla stake edin ve daha uzun süre tutun.", + "intro1": "Prime puanınız liderlik tablosundaki yerinizi belirler. Her ay ilk 500 Prime sahibi olur ve aylık ödül havuzunu paylaşır.", + "intro2": "Puanınız hem stake ettiğiniz XVS miktarını hem de ne kadar süre tuttuğunuzu ödüllendirir. Her yatırma, ne kadar eski olduğuna göre bir sadakat artışı kazanır:", + "max": "maks.", + "point1": "Hem artış hem de gün sayısı 90 günle sınırlıdır — daha uzun tutmak XVS başına puanı daha fazla artırmaz.", + "point2": "Çekiyor musunuz? En yeni yatırmalarınız önce çıkar. En eski, en yüksek artışlı stake’iniz korunur.", + "point3": "Puan canlı olarak güncellenir — ancak Prime durumunuz aylık anlık görüntüde sabitlenir.", + "thingsToKnowTitle": "Bilmeniz gereken birkaç şey:", + "title": "Kurallar" + }, "title": "Prime lider tablosu", "totalRewards": { "title": "Bu döngüde dağıtılan toplam Prime ödülü" diff --git a/apps/evm/src/libs/translations/translations/vi.json b/apps/evm/src/libs/translations/translations/vi.json index a624801ac0..903180e469 100644 --- a/apps/evm/src/libs/translations/translations/vi.json +++ b/apps/evm/src/libs/translations/translations/vi.json @@ -1260,6 +1260,21 @@ "stakePrompt": "Stake XVS để cạnh tranh Prime.", "stakeToReachTop": "Stake thêm {{amount}} XVS để vào top #500 và đủ điều kiện nhận Prime trong chu kỳ tiếp theo." }, + "rulesModal": { + "boostColumn": "Hệ số tăng", + "contribution": "Với mỗi khoản nạp, đóng góp của bạn là XVS × hệ số tăng × số ngày nắm giữ. Tổng điểm Prime của bạn là tổng của tất cả các khoản nạp.", + "days": "ngày", + "daysHeldColumn": "Số ngày nắm giữ", + "footer": "Để leo lên bảng xếp hạng: stake nhiều hơn và nắm giữ lâu hơn.", + "intro1": "Điểm Prime của bạn quyết định vị trí của bạn trên bảng xếp hạng. Top 500 mỗi tháng sẽ trở thành người nắm giữ Prime và chia sẻ quỹ thưởng hàng tháng.", + "intro2": "Điểm số thưởng cho cả số lượng XVS bạn stake và thời gian bạn nắm giữ. Mỗi khoản nạp nhận được hệ số tăng trung thành dựa trên thời gian đã nắm giữ:", + "max": "tối đa", + "point1": "Cả hệ số tăng và số ngày đều giới hạn ở 90 ngày — nắm giữ lâu hơn không làm tăng thêm điểm trên mỗi XVS.", + "point2": "Rút tiền? Các khoản nạp mới nhất sẽ bị trừ trước. Khoản stake lâu nhất, có hệ số tăng cao nhất của bạn được bảo vệ.", + "point3": "Điểm số cập nhật theo thời gian thực — nhưng trạng thái Prime của bạn được chốt tại ảnh chụp hàng tháng.", + "thingsToKnowTitle": "Một vài điều cần biết:", + "title": "Quy tắc" + }, "title": "Bảng xếp hạng Prime", "totalRewards": { "title": "Tổng thưởng Prime đã phân phối 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 7345daf2be..d98909fb79 100644 --- a/apps/evm/src/libs/translations/translations/zh-Hans.json +++ b/apps/evm/src/libs/translations/translations/zh-Hans.json @@ -1260,6 +1260,21 @@ "stakePrompt": "质押 XVS 以竞争 Prime。", "stakeToReachTop": "再质押 {{amount}} XVS 即可进入前 #500,并在下个周期获得 Prime 资格。" }, + "rulesModal": { + "boostColumn": "加成", + "contribution": "对每笔存入,你的贡献为 XVS × 加成 × 持有天数。你的 Prime 总分是所有存入的总和。", + "days": "天", + "daysHeldColumn": "持有天数", + "footer": "想在排行榜上更进一步:质押更多,持有更久。", + "intro1": "你的 Prime 分数 决定你在排行榜上的位置。每月 前 500 名 成为 Prime 持有者,共享当月奖励池。", + "intro2": "你的分数同时奖励你质押 XVS 的 数量时长。每笔存入都会根据其存放时间获得忠诚度加成:", + "max": "上限", + "point1": "加成和计入天数均以 90 天为上限 —— 持有更久不会进一步提升每 XVS 的分数。", + "point2": "提取时?最新的存入会被优先扣除。 你最早、加成最高的质押会受到保护。", + "point3": "分数 实时 更新 —— 但你的 Prime 状态在 每月快照 时锁定。", + "thingsToKnowTitle": "需要了解的几点:", + "title": "规则" + }, "title": "Prime 排行榜", "totalRewards": { "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 0f97a51c0f..d86b0010d7 100644 --- a/apps/evm/src/libs/translations/translations/zh-Hant.json +++ b/apps/evm/src/libs/translations/translations/zh-Hant.json @@ -1260,6 +1260,21 @@ "stakePrompt": "質押 XVS 以競爭 Prime。", "stakeToReachTop": "再質押 {{amount}} XVS 即可進入前 #500,並在下個週期取得 Prime 資格。" }, + "rulesModal": { + "boostColumn": "加成", + "contribution": "對每筆存入,你的貢獻為 XVS × 加成 × 持有天數。你的 Prime 總分是所有存入的總和。", + "days": "天", + "daysHeldColumn": "持有天數", + "footer": "想在排行榜上更進一步:質押更多,持有更久。", + "intro1": "你的 Prime 分數 決定你在排行榜上的位置。每月 前 500 名 成為 Prime 持有者,共享當月獎勵池。", + "intro2": "你的分數同時獎勵你質押 XVS 的 數量時長。每筆存入都會根據其存放時間獲得忠誠度加成:", + "max": "上限", + "point1": "加成和計入天數均以 90 天為上限 —— 持有更久不會進一步提升每 XVS 的分數。", + "point2": "提取時?最新的存入會被優先扣除。 你最早、加成最高的質押會受到保護。", + "point3": "分數 即時 更新 —— 但你的 Prime 狀態在 每月快照 時鎖定。", + "thingsToKnowTitle": "需要了解的幾點:", + "title": "規則" + }, "title": "Prime 排行榜", "totalRewards": { "title": "本週期已分配的 Prime 總獎勵" diff --git a/apps/evm/src/pages/PrimeLeaderboard/RankCard/RankActions/index.tsx b/apps/evm/src/pages/PrimeLeaderboard/RankCard/RankActions/index.tsx index f25d49ca04..e2e995b621 100644 --- a/apps/evm/src/pages/PrimeLeaderboard/RankCard/RankActions/index.tsx +++ b/apps/evm/src/pages/PrimeLeaderboard/RankCard/RankActions/index.tsx @@ -1,25 +1,33 @@ import { Button, ButtonWrapper } from '@venusprotocol/ui'; +import { useState } from 'react'; import { Icon } from 'components'; import { routes } from 'constants/routing'; import { Link } from 'containers/Link'; import { useTranslation } from 'libs/translations'; +import { RulesModal } from '../../RulesModal'; + export const RankActions: React.FC = () => { const { t } = useTranslation(); + const [isRulesModalOpen, setIsRulesModalOpen] = useState(false); return ( -
- - {t('primeLeaderboard.rankCard.stakeButton')} - + <> +
+ + {t('primeLeaderboard.rankCard.stakeButton')} + + + +
- -
+ {isRulesModalOpen && setIsRulesModalOpen(false)} />} + ); }; diff --git a/apps/evm/src/pages/PrimeLeaderboard/RulesModal/__tests__/index.spec.tsx b/apps/evm/src/pages/PrimeLeaderboard/RulesModal/__tests__/index.spec.tsx new file mode 100644 index 0000000000..5b3629b2f8 --- /dev/null +++ b/apps/evm/src/pages/PrimeLeaderboard/RulesModal/__tests__/index.spec.tsx @@ -0,0 +1,15 @@ +import { screen } from '@testing-library/react'; + +import { renderComponent } from 'testUtils/render'; + +import { RulesModal } from '..'; + +describe('pages/PrimeLeaderboard/RulesModal', () => { + it('renders the rules content', async () => { + renderComponent( {}} />); + + expect(await screen.findByText('Rules')).toBeInTheDocument(); + expect(screen.getByText('Days held')).toBeInTheDocument(); + expect(screen.getByText('2.0× (max)')).toBeInTheDocument(); + }); +}); diff --git a/apps/evm/src/pages/PrimeLeaderboard/RulesModal/index.tsx b/apps/evm/src/pages/PrimeLeaderboard/RulesModal/index.tsx new file mode 100644 index 0000000000..66efaeb6eb --- /dev/null +++ b/apps/evm/src/pages/PrimeLeaderboard/RulesModal/index.tsx @@ -0,0 +1,98 @@ +import { Modal } from 'components'; +import { useTranslation } from 'libs/translations'; + +export interface RulesModalProps { + isOpen: boolean; + handleClose: () => void; +} + +export const RulesModal: React.FC = ({ isOpen, handleClose }) => { + const { t, Trans } = useTranslation(); + + const highlight = ; + + const daysLabel = t('primeLeaderboard.rulesModal.days'); + const maxLabel = t('primeLeaderboard.rulesModal.max'); + + const boostTiers = [ + { range: '0 – 29', boost: '1.0×', isMax: false }, + { range: '30 – 59', boost: '1.3×', isMax: false }, + { range: '60 – 89', boost: '1.6×', isMax: false }, + { range: '90+', boost: '2.0×', isMax: true }, + ]; + + return ( + +
+
+

+ +

+ +

+ +

+
+ + + + + + + + + + + + {boostTiers.map(tier => ( + + + + + + ))} + +
+ {t('primeLeaderboard.rulesModal.daysHeldColumn')} + + {t('primeLeaderboard.rulesModal.boostColumn')} +
+ {`${tier.range} ${daysLabel}`} + + {tier.isMax ? `${tier.boost} (${maxLabel})` : tier.boost} +
+ +
+

+ +

+ +
+

{t('primeLeaderboard.rulesModal.thingsToKnowTitle')}

+ +
    +
  • + +
  • + +
  • + +
  • + +
  • + +
  • +
+
+ +

{t('primeLeaderboard.rulesModal.footer')}

+
+
+
+ ); +}; From 352d3085446bf316b13fe1a179eea753915c57c7 Mon Sep 17 00:00:00 2001 From: cuzz-venus Date: Mon, 15 Jun 2026 19:57:06 +0800 Subject: [PATCH 16/18] feat: add rules --- .../libs/translations/translations/en.json | 4 +- .../libs/translations/translations/ja.json | 4 +- .../libs/translations/translations/th.json | 4 +- .../libs/translations/translations/tr.json | 4 +- .../libs/translations/translations/vi.json | 4 +- .../translations/translations/zh-Hans.json | 4 +- .../translations/translations/zh-Hant.json | 4 +- .../PrimeLeaderboard/RulesModal/constants.ts | 6 +++ .../PrimeLeaderboard/RulesModal/index.tsx | 52 +++++++++++-------- 9 files changed, 50 insertions(+), 36 deletions(-) create mode 100644 apps/evm/src/pages/PrimeLeaderboard/RulesModal/constants.ts diff --git a/apps/evm/src/libs/translations/translations/en.json b/apps/evm/src/libs/translations/translations/en.json index 993af60a7c..79279892f3 100644 --- a/apps/evm/src/libs/translations/translations/en.json +++ b/apps/evm/src/libs/translations/translations/en.json @@ -1262,13 +1262,13 @@ }, "rulesModal": { "boostColumn": "Boost", + "boostMax": "{{boost}} (max)", "contribution": "For each deposit, your contribution is XVS × boost × days held. Your total Prime score is the sum across all your deposits.", - "days": "days", + "daysHeld": "{{range}} days", "daysHeldColumn": "Days held", "footer": "To climb the leaderboard: stake more, and hold longer.", "intro1": "Your Prime score decides your spot on the leaderboard. The Top 500 each month become Prime holders and share the monthly reward pool.", "intro2": "Your score rewards both how much XVS you stake and how long you hold it. Each deposit earns a loyalty boost based on how old it is:", - "max": "max", "point1": "Both the boost and the days count are capped at 90 days — holding longer doesn't increase the score per XVS further.", "point2": "Withdrawing? Your newest deposits go first. Your oldest, highest-boost stake is protected.", "point3": "The score updates live — but your Prime status is locked in at the monthly snapshot.", diff --git a/apps/evm/src/libs/translations/translations/ja.json b/apps/evm/src/libs/translations/translations/ja.json index beb851cb4e..3235d96a8e 100644 --- a/apps/evm/src/libs/translations/translations/ja.json +++ b/apps/evm/src/libs/translations/translations/ja.json @@ -1262,13 +1262,13 @@ }, "rulesModal": { "boostColumn": "ブースト", + "boostMax": "{{boost}}(上限)", "contribution": "各預け入れについて、あなたの貢献度は XVS × ブースト × 保有日数 です。Prime の合計スコアは、すべての預け入れの合計です。", - "days": "日", + "daysHeld": "{{range}} 日", "daysHeldColumn": "保有日数", "footer": "リーダーボードで上位を目指すには、より多くステークし、より長く保有しましょう。", "intro1": "Prime スコア がリーダーボードでの順位を決めます。毎月 上位 500 名 が Prime ホルダーとなり、月間報酬プールを分け合います。", "intro2": "スコアは、ステークした XVS の 保有期間 の両方を評価します。各預け入れは、その経過期間に応じてロイヤルティブーストを獲得します:", - "max": "上限", "point1": "ブーストと日数はどちらも 90 日が上限です —— それ以上保有しても XVS あたりのスコアはこれ以上増えません。", "point2": "出金する場合は?最新の預け入れが先に引き出されます。 最も古く、ブーストが最も高いステークは保護されます。", "point3": "スコアは リアルタイム で更新されます —— ただし Prime ステータスは 毎月のスナップショット で確定します。", diff --git a/apps/evm/src/libs/translations/translations/th.json b/apps/evm/src/libs/translations/translations/th.json index 3b8a58c4a4..3fda2f1b48 100644 --- a/apps/evm/src/libs/translations/translations/th.json +++ b/apps/evm/src/libs/translations/translations/th.json @@ -1262,13 +1262,13 @@ }, "rulesModal": { "boostColumn": "ตัวคูณ", + "boostMax": "{{boost}} (สูงสุด)", "contribution": "สำหรับการฝากแต่ละครั้ง การมีส่วนร่วมของคุณคือ XVS × ตัวคูณ × จำนวนวันที่ถือ คะแนน Prime รวมของคุณคือผลรวมของการฝากทั้งหมด", - "days": "วัน", + "daysHeld": "{{range}} วัน", "daysHeldColumn": "จำนวนวันที่ถือ", "footer": "เพื่อไต่อันดับลีดเดอร์บอร์ด: stake มากขึ้นและถือนานขึ้น", "intro1": "คะแนน Prime ของคุณกำหนดอันดับของคุณบนลีดเดอร์บอร์ด 500 อันดับแรก ในแต่ละเดือนจะกลายเป็นผู้ถือ Prime และแบ่งปันพูลรางวัลรายเดือน", "intro2": "คะแนนของคุณให้รางวัลทั้ง จำนวน XVS ที่คุณ stake และ ระยะเวลา ที่คุณถือ แต่ละการฝากจะได้รับตัวคูณความภักดีตามระยะเวลาที่ถือ:", - "max": "สูงสุด", "point1": "ทั้งตัวคูณและจำนวนวันถูกจำกัดที่ 90 วัน — การถือนานกว่านั้นจะไม่เพิ่มคะแนนต่อ XVS อีก", "point2": "ถอนเงิน? เงินฝากล่าสุดของคุณจะถูกหักก่อน เงิน stake ที่เก่าที่สุดและมีตัวคูณสูงสุดของคุณจะได้รับการปกป้อง", "point3": "คะแนนอัปเดต แบบเรียลไทม์ — แต่สถานะ Prime ของคุณจะถูกล็อกที่ สแน็ปช็อตรายเดือน", diff --git a/apps/evm/src/libs/translations/translations/tr.json b/apps/evm/src/libs/translations/translations/tr.json index e7888ce1cc..8c0622a421 100644 --- a/apps/evm/src/libs/translations/translations/tr.json +++ b/apps/evm/src/libs/translations/translations/tr.json @@ -1262,13 +1262,13 @@ }, "rulesModal": { "boostColumn": "Artış", + "boostMax": "{{boost}} (maks.)", "contribution": "Her yatırma için katkınız XVS × artış × tutulan gün şeklindedir. Toplam Prime puanınız tüm yatırmalarınızın toplamıdır.", - "days": "gün", + "daysHeld": "{{range}} gün", "daysHeldColumn": "Tutulan gün", "footer": "Liderlik tablosunda yükselmek için: daha fazla stake edin ve daha uzun süre tutun.", "intro1": "Prime puanınız liderlik tablosundaki yerinizi belirler. Her ay ilk 500 Prime sahibi olur ve aylık ödül havuzunu paylaşır.", "intro2": "Puanınız hem stake ettiğiniz XVS miktarını hem de ne kadar süre tuttuğunuzu ödüllendirir. Her yatırma, ne kadar eski olduğuna göre bir sadakat artışı kazanır:", - "max": "maks.", "point1": "Hem artış hem de gün sayısı 90 günle sınırlıdır — daha uzun tutmak XVS başına puanı daha fazla artırmaz.", "point2": "Çekiyor musunuz? En yeni yatırmalarınız önce çıkar. En eski, en yüksek artışlı stake’iniz korunur.", "point3": "Puan canlı olarak güncellenir — ancak Prime durumunuz aylık anlık görüntüde sabitlenir.", diff --git a/apps/evm/src/libs/translations/translations/vi.json b/apps/evm/src/libs/translations/translations/vi.json index 903180e469..2e7e9ef22a 100644 --- a/apps/evm/src/libs/translations/translations/vi.json +++ b/apps/evm/src/libs/translations/translations/vi.json @@ -1262,13 +1262,13 @@ }, "rulesModal": { "boostColumn": "Hệ số tăng", + "boostMax": "{{boost}} (tối đa)", "contribution": "Với mỗi khoản nạp, đóng góp của bạn là XVS × hệ số tăng × số ngày nắm giữ. Tổng điểm Prime của bạn là tổng của tất cả các khoản nạp.", - "days": "ngày", + "daysHeld": "{{range}} ngày", "daysHeldColumn": "Số ngày nắm giữ", "footer": "Để leo lên bảng xếp hạng: stake nhiều hơn và nắm giữ lâu hơn.", "intro1": "Điểm Prime của bạn quyết định vị trí của bạn trên bảng xếp hạng. Top 500 mỗi tháng sẽ trở thành người nắm giữ Prime và chia sẻ quỹ thưởng hàng tháng.", "intro2": "Điểm số thưởng cho cả số lượng XVS bạn stake và thời gian bạn nắm giữ. Mỗi khoản nạp nhận được hệ số tăng trung thành dựa trên thời gian đã nắm giữ:", - "max": "tối đa", "point1": "Cả hệ số tăng và số ngày đều giới hạn ở 90 ngày — nắm giữ lâu hơn không làm tăng thêm điểm trên mỗi XVS.", "point2": "Rút tiền? Các khoản nạp mới nhất sẽ bị trừ trước. Khoản stake lâu nhất, có hệ số tăng cao nhất của bạn được bảo vệ.", "point3": "Điểm số cập nhật theo thời gian thực — nhưng trạng thái Prime của bạn được chốt tại ảnh chụp hàng tháng.", diff --git a/apps/evm/src/libs/translations/translations/zh-Hans.json b/apps/evm/src/libs/translations/translations/zh-Hans.json index d98909fb79..221314504c 100644 --- a/apps/evm/src/libs/translations/translations/zh-Hans.json +++ b/apps/evm/src/libs/translations/translations/zh-Hans.json @@ -1262,13 +1262,13 @@ }, "rulesModal": { "boostColumn": "加成", + "boostMax": "{{boost}}(上限)", "contribution": "对每笔存入,你的贡献为 XVS × 加成 × 持有天数。你的 Prime 总分是所有存入的总和。", - "days": "天", + "daysHeld": "{{range}} 天", "daysHeldColumn": "持有天数", "footer": "想在排行榜上更进一步:质押更多,持有更久。", "intro1": "你的 Prime 分数 决定你在排行榜上的位置。每月 前 500 名 成为 Prime 持有者,共享当月奖励池。", "intro2": "你的分数同时奖励你质押 XVS 的 数量时长。每笔存入都会根据其存放时间获得忠诚度加成:", - "max": "上限", "point1": "加成和计入天数均以 90 天为上限 —— 持有更久不会进一步提升每 XVS 的分数。", "point2": "提取时?最新的存入会被优先扣除。 你最早、加成最高的质押会受到保护。", "point3": "分数 实时 更新 —— 但你的 Prime 状态在 每月快照 时锁定。", diff --git a/apps/evm/src/libs/translations/translations/zh-Hant.json b/apps/evm/src/libs/translations/translations/zh-Hant.json index d86b0010d7..a3155a6d78 100644 --- a/apps/evm/src/libs/translations/translations/zh-Hant.json +++ b/apps/evm/src/libs/translations/translations/zh-Hant.json @@ -1262,13 +1262,13 @@ }, "rulesModal": { "boostColumn": "加成", + "boostMax": "{{boost}}(上限)", "contribution": "對每筆存入,你的貢獻為 XVS × 加成 × 持有天數。你的 Prime 總分是所有存入的總和。", - "days": "天", + "daysHeld": "{{range}} 天", "daysHeldColumn": "持有天數", "footer": "想在排行榜上更進一步:質押更多,持有更久。", "intro1": "你的 Prime 分數 決定你在排行榜上的位置。每月 前 500 名 成為 Prime 持有者,共享當月獎勵池。", "intro2": "你的分數同時獎勵你質押 XVS 的 數量時長。每筆存入都會根據其存放時間獲得忠誠度加成:", - "max": "上限", "point1": "加成和計入天數均以 90 天為上限 —— 持有更久不會進一步提升每 XVS 的分數。", "point2": "提取時?最新的存入會被優先扣除。 你最早、加成最高的質押會受到保護。", "point3": "分數 即時 更新 —— 但你的 Prime 狀態在 每月快照 時鎖定。", diff --git a/apps/evm/src/pages/PrimeLeaderboard/RulesModal/constants.ts b/apps/evm/src/pages/PrimeLeaderboard/RulesModal/constants.ts new file mode 100644 index 0000000000..67f0e2e1b4 --- /dev/null +++ b/apps/evm/src/pages/PrimeLeaderboard/RulesModal/constants.ts @@ -0,0 +1,6 @@ +export const BOOST_TIERS: { range: string; boost: string; isMax?: boolean }[] = [ + { range: '0 – 29', boost: '1.0×' }, + { range: '30 – 59', boost: '1.3×' }, + { range: '60 – 89', boost: '1.6×' }, + { range: '90+', boost: '2.0×', isMax: true }, +]; diff --git a/apps/evm/src/pages/PrimeLeaderboard/RulesModal/index.tsx b/apps/evm/src/pages/PrimeLeaderboard/RulesModal/index.tsx index 66efaeb6eb..a328841e8f 100644 --- a/apps/evm/src/pages/PrimeLeaderboard/RulesModal/index.tsx +++ b/apps/evm/src/pages/PrimeLeaderboard/RulesModal/index.tsx @@ -1,6 +1,8 @@ import { Modal } from 'components'; import { useTranslation } from 'libs/translations'; +import { BOOST_TIERS } from './constants'; + export interface RulesModalProps { isOpen: boolean; handleClose: () => void; @@ -9,18 +11,6 @@ export interface RulesModalProps { export const RulesModal: React.FC = ({ isOpen, handleClose }) => { const { t, Trans } = useTranslation(); - const highlight = ; - - const daysLabel = t('primeLeaderboard.rulesModal.days'); - const maxLabel = t('primeLeaderboard.rulesModal.max'); - - const boostTiers = [ - { range: '0 – 29', boost: '1.0×', isMax: false }, - { range: '30 – 59', boost: '1.3×', isMax: false }, - { range: '60 – 89', boost: '1.6×', isMax: false }, - { range: '90+', boost: '2.0×', isMax: true }, - ]; - return ( = ({ isOpen, handleClose }) =

- + }} + />

- + }} + />

@@ -53,14 +49,14 @@ export const RulesModal: React.FC = ({ isOpen, handleClose }) = - {boostTiers.map(tier => ( - + {BOOST_TIERS.map(({ range, boost, isMax }) => ( + - {`${tier.range} ${daysLabel}`} + {t('primeLeaderboard.rulesModal.daysHeld', { range })} - {tier.isMax ? `${tier.boost} (${maxLabel})` : tier.boost} + {isMax ? t('primeLeaderboard.rulesModal.boostMax', { boost }) : boost} ))} @@ -69,7 +65,10 @@ export const RulesModal: React.FC = ({ isOpen, handleClose }) =

- + }} + />

@@ -77,15 +76,24 @@ export const RulesModal: React.FC = ({ isOpen, handleClose }) =
  • - + }} + />
  • - + }} + />
  • - + }} + />
From b1a9a9f09d0f130553b7f397be8cf64c4598e41f Mon Sep 17 00:00:00 2001 From: cuzz-venus Date: Mon, 15 Jun 2026 21:48:23 +0800 Subject: [PATCH 17/18] feat: add stake modal --- .../__tests__/index.spec.tsx | 0 .../PrimeRank}/EligibilityStatus/index.tsx | 10 ++- .../src/containers/PrimeRank/Footer/index.tsx | 70 +++++++++++++++++++ .../PrimeRank}/useGetPrimeRank/index.ts | 10 ++- .../containers/VaultCard/Simplified/index.tsx | 2 +- apps/evm/src/containers/VaultCard/index.tsx | 2 +- .../Footer/__tests__/index.spec.tsx | 0 .../calculateDailyVaultEarnings/index.ts | 0 .../VenusVaultModal/Footer/index.tsx | 0 .../StakeForm/__tests__/index.spec.tsx | 0 .../VenusVaultModal/StakeForm/index.tsx | 27 +++++-- .../__tests__/index.spec.tsx | 0 .../WithdrawFromVaiVaultForm/index.tsx | 2 +- .../__tests__/index.spec.tsx | 0 .../RequestWithdrawalForm/index.tsx | 4 +- .../__snapshots__/index.spec.tsx.snap | 0 .../__tests__/index.spec.tsx | 0 .../WithdrawalRequestList/index.tsx | 0 .../WithdrawFromVestingVaultForm/index.tsx | 0 .../VenusVaultModal/WithdrawTab/index.tsx | 0 .../{VaultCard => }/VenusVaultModal/index.tsx | 18 ++++- .../libs/translations/translations/en.json | 3 + .../libs/translations/translations/ja.json | 3 + .../libs/translations/translations/th.json | 3 + .../libs/translations/translations/tr.json | 3 + .../libs/translations/translations/vi.json | 3 + .../translations/translations/zh-Hans.json | 3 + .../translations/translations/zh-Hant.json | 3 + .../RankCard/RankActions/index.tsx | 14 ++-- .../RankCard/__tests__/index.spec.tsx | 3 +- .../pages/PrimeLeaderboard/RankCard/index.tsx | 13 +--- .../PrimeLeaderboard/RankSection/index.tsx | 2 +- .../StakeXvsModal/__tests__/index.spec.tsx | 22 ++++++ .../PrimeLeaderboard/StakeXvsModal/index.tsx | 25 +++++++ 34 files changed, 212 insertions(+), 33 deletions(-) rename apps/evm/src/{pages/PrimeLeaderboard/RankCard => containers/PrimeRank}/EligibilityStatus/__tests__/index.spec.tsx (100%) rename apps/evm/src/{pages/PrimeLeaderboard/RankCard => containers/PrimeRank}/EligibilityStatus/index.tsx (86%) create mode 100644 apps/evm/src/containers/PrimeRank/Footer/index.tsx rename apps/evm/src/{pages/PrimeLeaderboard => containers/PrimeRank}/useGetPrimeRank/index.ts (64%) rename apps/evm/src/containers/{VaultCard => }/VenusVaultModal/Footer/__tests__/index.spec.tsx (100%) rename apps/evm/src/containers/{VaultCard => }/VenusVaultModal/Footer/calculateDailyVaultEarnings/index.ts (100%) rename apps/evm/src/containers/{VaultCard => }/VenusVaultModal/Footer/index.tsx (100%) rename apps/evm/src/containers/{VaultCard => }/VenusVaultModal/StakeForm/__tests__/index.spec.tsx (100%) rename apps/evm/src/containers/{VaultCard => }/VenusVaultModal/StakeForm/index.tsx (75%) rename apps/evm/src/containers/{VaultCard => }/VenusVaultModal/WithdrawTab/WithdrawFromVaiVaultForm/__tests__/index.spec.tsx (100%) rename apps/evm/src/containers/{VaultCard => }/VenusVaultModal/WithdrawTab/WithdrawFromVaiVaultForm/index.tsx (96%) rename apps/evm/src/containers/{VaultCard => }/VenusVaultModal/WithdrawTab/WithdrawFromVestingVaultForm/RequestWithdrawalForm/__tests__/index.spec.tsx (100%) rename apps/evm/src/containers/{VaultCard => }/VenusVaultModal/WithdrawTab/WithdrawFromVestingVaultForm/RequestWithdrawalForm/index.tsx (98%) rename apps/evm/src/containers/{VaultCard => }/VenusVaultModal/WithdrawTab/WithdrawFromVestingVaultForm/WithdrawalRequestList/__tests__/__snapshots__/index.spec.tsx.snap (100%) rename apps/evm/src/containers/{VaultCard => }/VenusVaultModal/WithdrawTab/WithdrawFromVestingVaultForm/WithdrawalRequestList/__tests__/index.spec.tsx (100%) rename apps/evm/src/containers/{VaultCard => }/VenusVaultModal/WithdrawTab/WithdrawFromVestingVaultForm/WithdrawalRequestList/index.tsx (100%) rename apps/evm/src/containers/{VaultCard => }/VenusVaultModal/WithdrawTab/WithdrawFromVestingVaultForm/index.tsx (100%) rename apps/evm/src/containers/{VaultCard => }/VenusVaultModal/WithdrawTab/index.tsx (100%) rename apps/evm/src/containers/{VaultCard => }/VenusVaultModal/index.tsx (71%) create mode 100644 apps/evm/src/pages/PrimeLeaderboard/StakeXvsModal/__tests__/index.spec.tsx create mode 100644 apps/evm/src/pages/PrimeLeaderboard/StakeXvsModal/index.tsx diff --git a/apps/evm/src/pages/PrimeLeaderboard/RankCard/EligibilityStatus/__tests__/index.spec.tsx b/apps/evm/src/containers/PrimeRank/EligibilityStatus/__tests__/index.spec.tsx similarity index 100% rename from apps/evm/src/pages/PrimeLeaderboard/RankCard/EligibilityStatus/__tests__/index.spec.tsx rename to apps/evm/src/containers/PrimeRank/EligibilityStatus/__tests__/index.spec.tsx diff --git a/apps/evm/src/pages/PrimeLeaderboard/RankCard/EligibilityStatus/index.tsx b/apps/evm/src/containers/PrimeRank/EligibilityStatus/index.tsx similarity index 86% rename from apps/evm/src/pages/PrimeLeaderboard/RankCard/EligibilityStatus/index.tsx rename to apps/evm/src/containers/PrimeRank/EligibilityStatus/index.tsx index f7861ae6b7..7558c4c8d7 100644 --- a/apps/evm/src/pages/PrimeLeaderboard/RankCard/EligibilityStatus/index.tsx +++ b/apps/evm/src/containers/PrimeRank/EligibilityStatus/index.tsx @@ -12,6 +12,8 @@ export interface EligibilityStatusProps { isPrime: boolean; hasSupplied: boolean; gapXvsTokens: number; + // Optional inline content appended to the end of the status message (e.g. a leaderboard link) + linkSlot?: React.ReactNode; className?: string; } @@ -21,6 +23,7 @@ export const EligibilityStatus: React.FC = ({ isPrime, hasSupplied, gapXvsTokens, + linkSlot, className, }) => { const { t, Trans } = useTranslation(); @@ -31,6 +34,7 @@ export const EligibilityStatus: React.FC = ({ return (

{t('primeLeaderboard.rankCard.eligibleSupplied')} + {linkSlot}

); } @@ -39,6 +43,7 @@ export const EligibilityStatus: React.FC = ({ return (

{t('primeLeaderboard.rankCard.eligible')} + {linkSlot}

); } @@ -60,7 +65,10 @@ export const EligibilityStatus: React.FC = ({ return (

{t('primeLeaderboard.rankCard.notEligible')}

-

{stakeMessage}

+

+ {stakeMessage} + {linkSlot} +

); }; diff --git a/apps/evm/src/containers/PrimeRank/Footer/index.tsx b/apps/evm/src/containers/PrimeRank/Footer/index.tsx new file mode 100644 index 0000000000..14922a2b0c --- /dev/null +++ b/apps/evm/src/containers/PrimeRank/Footer/index.tsx @@ -0,0 +1,70 @@ +import BigNumber from 'bignumber.js'; + +import { routes } from 'constants/routing'; +import { Link } from 'containers/Link'; +import { useTranslation } from 'libs/translations'; +import { shortenValueWithSuffix } from 'utilities'; + +import { EligibilityStatus } from '../EligibilityStatus'; +import { useGetPrimeRank } from '../useGetPrimeRank'; + +export interface FooterProps { + hideLeaderboardLink?: boolean; +} + +export const Footer: React.FC = ({ hideLeaderboardLink }) => { + const { t, Trans } = useTranslation(); + + const { hasStakedXvs, isCandidate, isPrime, hasSupplied, rank, primeScore, gapXvsTokens } = + useGetPrimeRank(); + + const rankLabel = hasStakedXvs ? `#${rank}` : '#-'; + const primeScoreLabel = hasStakedXvs + ? shortenValueWithSuffix({ value: new BigNumber(primeScore) }) + : '-'; + + return ( +
+
+
+ + {t('primeLeaderboard.rankCard.rankLabel')} + + + {rankLabel} +
+ +
+ + {t('primeLeaderboard.rankCard.primeScoreLabel')} + + + {primeScoreLabel} +
+
+ + + {' '} + + ), + }} + /> + + ) + } + /> +
+ ); +}; diff --git a/apps/evm/src/pages/PrimeLeaderboard/useGetPrimeRank/index.ts b/apps/evm/src/containers/PrimeRank/useGetPrimeRank/index.ts similarity index 64% rename from apps/evm/src/pages/PrimeLeaderboard/useGetPrimeRank/index.ts rename to apps/evm/src/containers/PrimeRank/useGetPrimeRank/index.ts index 62e733a08e..ad1b0159d6 100644 --- a/apps/evm/src/pages/PrimeLeaderboard/useGetPrimeRank/index.ts +++ b/apps/evm/src/containers/PrimeRank/useGetPrimeRank/index.ts @@ -1,4 +1,12 @@ -import type { PrimeRankData } from '../RankCard'; +export interface PrimeRankData { + hasStakedXvs: boolean; + isCandidate: boolean; + isPrime: boolean; + hasSupplied: boolean; + rank: number; + primeScore: number; + gapXvsTokens: number; +} // TODO: replace this placeholder with the rank data returned by the API const placeholderRankData: PrimeRankData = { diff --git a/apps/evm/src/containers/VaultCard/Simplified/index.tsx b/apps/evm/src/containers/VaultCard/Simplified/index.tsx index 795b222e39..115f9e9bc4 100644 --- a/apps/evm/src/containers/VaultCard/Simplified/index.tsx +++ b/apps/evm/src/containers/VaultCard/Simplified/index.tsx @@ -20,7 +20,7 @@ import { import { InstitutionalVaultModal } from 'containers/VaultCard/InstitutionalVaultModal'; import { PendleVaultModal } from 'containers/VaultCard/PendleVaultModal'; import { useState } from 'react'; -import { VenusVaultModal } from '../VenusVaultModal'; +import { VenusVaultModal } from 'containers/VenusVaultModal'; import { Cell } from './Cell'; interface VaultCardSimplifiedProps { diff --git a/apps/evm/src/containers/VaultCard/index.tsx b/apps/evm/src/containers/VaultCard/index.tsx index 9999974118..5488473fb2 100644 --- a/apps/evm/src/containers/VaultCard/index.tsx +++ b/apps/evm/src/containers/VaultCard/index.tsx @@ -23,7 +23,7 @@ import { PrimeEligibilityInlineContent } from './PrimeEligibilityInlineContent'; import { Progress } from './Progress'; import { StatusLabel } from './StatusLabel'; import { VaultName } from './VaultName'; -import { VenusVaultModal } from './VenusVaultModal'; +import { VenusVaultModal } from 'containers/VenusVaultModal'; export interface VaultProps { vault: Vault; diff --git a/apps/evm/src/containers/VaultCard/VenusVaultModal/Footer/__tests__/index.spec.tsx b/apps/evm/src/containers/VenusVaultModal/Footer/__tests__/index.spec.tsx similarity index 100% rename from apps/evm/src/containers/VaultCard/VenusVaultModal/Footer/__tests__/index.spec.tsx rename to apps/evm/src/containers/VenusVaultModal/Footer/__tests__/index.spec.tsx diff --git a/apps/evm/src/containers/VaultCard/VenusVaultModal/Footer/calculateDailyVaultEarnings/index.ts b/apps/evm/src/containers/VenusVaultModal/Footer/calculateDailyVaultEarnings/index.ts similarity index 100% rename from apps/evm/src/containers/VaultCard/VenusVaultModal/Footer/calculateDailyVaultEarnings/index.ts rename to apps/evm/src/containers/VenusVaultModal/Footer/calculateDailyVaultEarnings/index.ts diff --git a/apps/evm/src/containers/VaultCard/VenusVaultModal/Footer/index.tsx b/apps/evm/src/containers/VenusVaultModal/Footer/index.tsx similarity index 100% rename from apps/evm/src/containers/VaultCard/VenusVaultModal/Footer/index.tsx rename to apps/evm/src/containers/VenusVaultModal/Footer/index.tsx diff --git a/apps/evm/src/containers/VaultCard/VenusVaultModal/StakeForm/__tests__/index.spec.tsx b/apps/evm/src/containers/VenusVaultModal/StakeForm/__tests__/index.spec.tsx similarity index 100% rename from apps/evm/src/containers/VaultCard/VenusVaultModal/StakeForm/__tests__/index.spec.tsx rename to apps/evm/src/containers/VenusVaultModal/StakeForm/__tests__/index.spec.tsx diff --git a/apps/evm/src/containers/VaultCard/VenusVaultModal/StakeForm/index.tsx b/apps/evm/src/containers/VenusVaultModal/StakeForm/index.tsx similarity index 75% rename from apps/evm/src/containers/VaultCard/VenusVaultModal/StakeForm/index.tsx rename to apps/evm/src/containers/VenusVaultModal/StakeForm/index.tsx index 0bed53d3eb..179ffaedb2 100644 --- a/apps/evm/src/containers/VaultCard/VenusVaultModal/StakeForm/index.tsx +++ b/apps/evm/src/containers/VenusVaultModal/StakeForm/index.tsx @@ -2,23 +2,34 @@ import BigNumber from 'bignumber.js'; import { useGetBalanceOf, useStakeInVault } from 'clients/api'; import { NULL_ADDRESS } from 'constants/address'; +import { Footer as PrimeRankFooter } from 'containers/PrimeRank/Footer'; +import { TransactionForm } from 'containers/VaultCard/TransactionForm'; import { useForm } from 'containers/VaultCard/useForm'; import { useGetContractAddress } from 'hooks/useGetContractAddress'; +import { useIsFeatureEnabled } from 'hooks/useIsFeatureEnabled'; import useTokenApproval from 'hooks/useTokenApproval'; import { useTranslation } from 'libs/translations'; import { useAccountAddress } from 'libs/wallet'; -import type { Vault } from 'types'; +import { type Vault, VaultCategory } from 'types'; import { convertMantissaToTokens, convertTokensToMantissa } from 'utilities'; -import { TransactionForm } from '../../TransactionForm'; import { Footer } from '../Footer'; export interface StakeFormProps { vault: Vault; onClose: () => void; + hidePrimeLeaderboardLink?: boolean; } -export const StakeForm: React.FC = ({ vault, onClose }) => { +export const StakeForm: React.FC = ({ + vault, + onClose, + hidePrimeLeaderboardLink, +}) => { const { t } = useTranslation(); + + const isPrimeLeaderboardEnabled = useIsFeatureEnabled({ name: 'primeLeaderboard' }); + const showPrimeRankFooter = + isPrimeLeaderboardEnabled && vault.category === VaultCategory.GOVERNANCE; const { accountAddress } = useAccountAddress(); const fromToken = vault.stakedToken; @@ -101,7 +112,15 @@ export const StakeForm: React.FC = ({ vault, onClose }) => { fromTokenFieldLabel={t('vaultCard.vaultModal.stakeForm.depositField.label')} submitButtonLabel={t('vaultCard.vaultModal.stakeForm.submitButton.label')} fromTokenPriceCents={vault.stakedTokenPriceCents.toNumber()} - footer={