Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/happy-garlics-argue.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@venusprotocol/evm": minor
---

feat: support user rank card
5 changes: 5 additions & 0 deletions .changeset/wise-toes-thank.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@venusprotocol/evm": minor
---

feat: add Prime total rewards card
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -46,3 +46,7 @@ Thumbs.db

# AI development artifacts
.ui-develop

# local file
*.local
*.local.*
41 changes: 41 additions & 0 deletions apps/evm/src/components/Icon/icons/barChart.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { type SVGProps, useId } from 'react';

const SvgBarChart = (props: SVGProps<SVGSVGElement>) => {
const maskId = useId();

return (
<svg
width="20"
height="20"
viewBox="0 0 20 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
{...props}
>
<mask id={maskId} fill="white">
<rect x="12" width="8" height="20" rx="1.49998" />
</mask>
<rect
x="12"
width="8"
height="20"
rx="1.49998"
stroke="currentColor"
strokeWidth="4"
mask={`url(#${maskId})`}
/>
<path
d="M7.5 8H12.5C12.7761 8 13 8.22387 13 8.5V19H7.5C7.22387 19 7 18.7761 7 18.5V8.5C7 8.22387 7.22387 8 7.5 8Z"
stroke="currentColor"
strokeWidth="2"
/>
<path
d="M1.5 14H6.5C6.77613 14 7 14.2239 7 14.5V19H1.5C1.22387 19 1 18.7761 1 18.5V14.5C1 14.2239 1.22387 14 1.5 14Z"
stroke="currentColor"
strokeWidth="2"
/>
</svg>
);
};

export default SvgBarChart;
28 changes: 28 additions & 0 deletions apps/evm/src/components/Icon/icons/dotShortcut.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import type { SVGProps } from 'react';

const SvgDotShortcut = (props: SVGProps<SVGSVGElement>) => (
<svg
width="16"
height="16"
viewBox="0 0 16 16"
fill="none"
xmlns="http://www.w3.org/2000/svg"
{...props}
>
<rect x="0.5" y="0.5" width="15" height="15" rx="7.5" stroke="currentColor" />
<path
d="M4 9C4.55228 9 5 8.55228 5 8C5 7.44772 4.55228 7 4 7C3.44772 7 3 7.44772 3 8C3 8.55228 3.44772 9 4 9Z"
fill="currentColor"
/>
<path
d="M8 9C8.55228 9 9 8.55228 9 8C9 7.44772 8.55228 7 8 7C7.44772 7 7 7.44772 7 8C7 8.55228 7.44772 9 8 9Z"
fill="currentColor"
/>
<path
d="M12 9C12.5523 9 13 8.55228 13 8C13 7.44772 12.5523 7 12 7C11.4477 7 11 7.44772 11 8C11 8.55228 11.4477 9 12 9Z"
fill="currentColor"
/>
</svg>
);

export default SvgDotShortcut;
25 changes: 25 additions & 0 deletions apps/evm/src/components/Icon/icons/graduationCap.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import type { SVGProps } from 'react';

const SvgGraduationCap = (props: SVGProps<SVGSVGElement>) => (
<svg
width="20"
height="20"
viewBox="0 0 18.7507 18.7507"
fill="none"
xmlns="http://www.w3.org/2000/svg"
{...props}
>
<g transform="translate(0 1.25)">
<path
d="M9.37573 12.5C9.26693 12.5 9.16002 12.4715 9.06557 12.4175L3.59448 9.29059C3.54694 9.26314 3.493 9.2487 3.43811 9.24872C3.38322 9.24874 3.3293 9.26322 3.28178 9.29071C3.23426 9.31819 3.19481 9.3577 3.16742 9.40527C3.14002 9.45284 3.12564 9.50679 3.12573 9.56169V12.5C3.12563 12.6115 3.1554 12.7211 3.21193 12.8172C3.26846 12.9134 3.3497 12.9927 3.44721 13.0468L9.07221 16.1718C9.16506 16.2234 9.26952 16.2505 9.37573 16.2505C9.48194 16.2505 9.5864 16.2234 9.67924 16.1718L15.3042 13.0468C15.4018 12.9927 15.483 12.9134 15.5395 12.8172C15.5961 12.7211 15.6258 12.6115 15.6257 12.5V9.56169C15.6258 9.50679 15.6114 9.45284 15.584 9.40527C15.5566 9.3577 15.5172 9.31819 15.4697 9.29071C15.4222 9.26322 15.3682 9.24874 15.3133 9.24872C15.2584 9.2487 15.2045 9.26314 15.157 9.29059L9.68588 12.4175C9.59143 12.4715 9.48452 12.5 9.37573 12.5Z"
fill="currentColor"
/>
<path
d="M18.7476 5.56637C18.7476 5.56637 18.7476 5.56325 18.7476 5.56208C18.7375 5.46301 18.7039 5.36779 18.6495 5.28435C18.5951 5.20091 18.5216 5.13167 18.4351 5.08239L9.6851 0.0823878C9.59064 0.0283988 9.48373 0 9.37494 0C9.26615 0 9.15924 0.0283988 9.06478 0.0823878L0.314784 5.08239C0.219154 5.13706 0.139673 5.21604 0.0843939 5.31133C0.0291144 5.40661 0 5.51481 0 5.62497C0 5.73512 0.0291144 5.84332 0.0843939 5.93861C0.139673 6.03389 0.219154 6.11287 0.314784 6.16754L9.06478 11.1675C9.15924 11.2215 9.26615 11.2499 9.37494 11.2499C9.48373 11.2499 9.59064 11.2215 9.6851 11.1675L17.3835 6.76872C17.3954 6.76185 17.4089 6.75824 17.4227 6.75825C17.4364 6.75826 17.4499 6.7619 17.4618 6.76879C17.4737 6.77569 17.4835 6.78559 17.4904 6.79751C17.4972 6.80943 17.5008 6.82295 17.5007 6.83669V12.4824C17.5007 12.8187 17.7593 13.1074 18.0956 13.1242C18.1801 13.1283 18.2646 13.1151 18.3439 13.0856C18.4232 13.0561 18.4956 13.0107 18.5569 12.9524C18.6181 12.894 18.6669 12.8238 18.7002 12.7461C18.7335 12.6683 18.7507 12.5846 18.7507 12.5V5.62497C18.7507 5.60539 18.7496 5.58584 18.7476 5.56637Z"
fill="currentColor"
/>
</g>
</svg>
);

export default SvgGraduationCap;
4 changes: 4 additions & 0 deletions apps/evm/src/components/Icon/icons/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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';
Expand Down Expand Up @@ -99,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';
19 changes: 19 additions & 0 deletions apps/evm/src/components/Icon/icons/sparkle.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import type { SVGProps } from 'react';

const SvgSparkle = (props: SVGProps<SVGSVGElement>) => (
<svg
width="16"
height="16"
viewBox="0 0 16 16"
fill="none"
xmlns="http://www.w3.org/2000/svg"
{...props}
>
<path
d="M3.73138 8.51568C3.84319 8.29954 4.15265 8.29967 4.26459 8.51568L5.31732 10.5528C5.34582 10.6078 5.39121 10.6522 5.44623 10.6807L7.48236 11.7344C7.69864 11.8463 7.69864 12.1548 7.48236 12.2667L5.44623 13.3204C5.39121 13.3489 5.34582 13.3933 5.31732 13.4483L4.26459 15.4854C4.15265 15.7014 3.84319 15.7015 3.73138 15.4854L2.67865 13.4483C2.6501 13.3933 2.60481 13.3488 2.54974 13.3204L0.51361 12.2667C0.297547 12.1548 0.297547 11.8463 0.51361 11.7344L2.54974 10.6807C2.60481 10.6522 2.6501 10.6078 2.67865 10.5528L3.73138 8.51568ZM10.2236 1.62505C10.3287 1.38611 10.6673 1.38614 10.7724 1.62505L11.8652 4.10845C12.0667 4.56644 12.4325 4.93239 12.8906 5.13384L15.374 6.22564C15.6129 6.33074 15.6129 6.67034 15.374 6.77544L12.8906 7.86724C12.4325 8.06869 12.0667 8.43463 11.8652 8.89263L10.7724 11.376C10.6673 11.6149 10.3287 11.615 10.2236 11.376L9.13177 8.89263C8.93027 8.43455 8.5635 8.06872 8.10541 7.86724L5.62298 6.77544C5.38403 6.67034 5.38403 6.33074 5.62298 6.22564L8.10541 5.13384C8.5635 4.93236 8.93027 4.56653 9.13177 4.10845L10.2236 1.62505ZM3.90912 0.171926C3.94641 0.100177 4.04943 0.10031 4.08685 0.171926L4.66498 1.29107C4.67443 1.30923 4.68979 1.32358 4.70795 1.33306L5.82611 1.91216C5.8982 1.94944 5.8982 2.05164 5.82611 2.08892L4.70795 2.66802C4.68979 2.6775 4.67443 2.69184 4.66498 2.71001L4.08685 3.82915C4.04943 3.90077 3.94641 3.9009 3.90912 3.82915L3.33099 2.71001C3.32147 2.69182 3.30627 2.67746 3.28802 2.66802L2.16986 2.08892C2.09819 2.05156 2.09819 1.94952 2.16986 1.91216L3.28802 1.33306C3.30627 1.32362 3.32147 1.30926 3.33099 1.29107L3.90912 0.171926Z"
fill="currentColor"
/>
</svg>
);

export default SvgSparkle;
Original file line number Diff line number Diff line change
@@ -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(<EligibilityStatus {...baseProps} />);

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(<EligibilityStatus {...baseProps} isPrime={false} />);

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(<EligibilityStatus {...baseProps} isCandidate={false} gapXvsTokens={5_432} />);

expect(screen.getByText('5,432 XVS')).toBeInTheDocument();
});

it('shows a generic message when the gap to the top 500 is large', () => {
renderComponent(
<EligibilityStatus {...baseProps} isCandidate={false} gapXvsTokens={200_000} />,
);

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(<EligibilityStatus {...baseProps} hasStakedXvs={false} isCandidate={false} />);

expect(screen.getByText('Stake XVS to compete for Prime.')).toBeInTheDocument();
});
});
74 changes: 74 additions & 0 deletions apps/evm/src/containers/PrimeRank/EligibilityStatus/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
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;
// Optional inline content appended to the end of the status message (e.g. a leaderboard link)
linkSlot?: React.ReactNode;
className?: string;
}

export const EligibilityStatus: React.FC<EligibilityStatusProps> = ({
hasStakedXvs,
isCandidate,
isPrime,
hasSupplied,
gapXvsTokens,
linkSlot,
className,
}) => {
const { t, Trans } = useTranslation();

const isEligible = hasStakedXvs && isCandidate;

if (isEligible && isPrime && hasSupplied) {
return (
<p className={cn('text-b1r text-white', className)}>
{t('primeLeaderboard.rankCard.eligibleSupplied')}
{linkSlot}
</p>
);
}

if (isEligible) {
return (
<p className={cn('text-b1r text-green', className)}>
{t('primeLeaderboard.rankCard.eligible')}
{linkSlot}
</p>
);
}

let stakeMessage: React.ReactNode = t('primeLeaderboard.rankCard.stakePrompt');

if (hasStakedXvs && gapXvsTokens <= TOP_500_GAP_THRESHOLD_XVS) {
stakeMessage = (
<Trans
i18nKey="primeLeaderboard.rankCard.stakeToReachTop"
values={{ amount: new BigNumber(gapXvsTokens).toFormat() }}
components={{ Highlight: <span className="text-b1s text-white" /> }}
/>
);
} else if (hasStakedXvs) {
stakeMessage = t('primeLeaderboard.rankCard.stakeMore');
}

return (
<div className={className}>
<p className="text-b1r text-yellow">{t('primeLeaderboard.rankCard.notEligible')}</p>
<p className="text-b1r text-white">
{stakeMessage}
{linkSlot}
</p>
</div>
);
};
70 changes: 70 additions & 0 deletions apps/evm/src/containers/PrimeRank/Footer/index.tsx
Original file line number Diff line number Diff line change
@@ -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<FooterProps> = ({ 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 (
<div className="flex flex-col gap-1 rounded-lg border border-dark-blue-hover p-3">
<div className="flex items-start justify-between">
<div className="flex flex-col">
<span className="text-b1r text-light-grey">
{t('primeLeaderboard.rankCard.rankLabel')}
</span>

<span className="text-p1s text-white">{rankLabel}</span>
</div>

<div className="flex flex-col items-end">
<span className="text-b1r text-light-grey">
{t('primeLeaderboard.rankCard.primeScoreLabel')}
</span>

<span className="text-p1s text-white">{primeScoreLabel}</span>
</div>
</div>

<EligibilityStatus
hasStakedXvs={hasStakedXvs}
isCandidate={isCandidate}
isPrime={isPrime}
hasSupplied={hasSupplied}
gapXvsTokens={gapXvsTokens}
linkSlot={
!hideLeaderboardLink && (
<span className="text-white">
{' '}
<Trans
i18nKey="primeLeaderboard.rankFooter.learnMore"
components={{
leaderboardLink: (
<Link className="text-blue underline" to={routes.primeLeaderboard.path} />
),
}}
/>
</span>
)
}
/>
</div>
);
};
22 changes: 22 additions & 0 deletions apps/evm/src/containers/PrimeRank/useGetPrimeRank/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
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 = {
hasStakedXvs: true,
isCandidate: true,
isPrime: true,
hasSupplied: true,
rank: 2,
primeScore: 542_500_000,
gapXvsTokens: 5_432,
};

export const useGetPrimeRank = (): PrimeRankData => placeholderRankData;
2 changes: 1 addition & 1 deletion apps/evm/src/containers/VaultCard/Simplified/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,8 @@ import {

import { InstitutionalVaultModal } from 'containers/VaultCard/InstitutionalVaultModal';
import { PendleVaultModal } from 'containers/VaultCard/PendleVaultModal';
import { VenusVaultModal } from 'containers/VenusVaultModal';
import { useState } from 'react';
import { VenusVaultModal } from '../VenusVaultModal';
import { Cell } from './Cell';

interface VaultCardSimplifiedProps {
Expand Down
Loading
Loading