Skip to content
Open
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
42 changes: 42 additions & 0 deletions apps/evm/src/components/Icon/icons/barChart.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { type SVGProps, useId } from 'react';

const SvgBarChart = (props: SVGProps<SVGSVGElement>) => {
const maskId = `bar-chart-${useId().replace(/[^a-zA-Z0-9]/g, '')}`;

return (
<svg
width="20"
height="20"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
{...props}
>
<mask id={maskId} fill="white">
<rect x="14" y="2" width="8" height="20" rx="1.49998" />
</mask>
<rect
x="14"
y="2"
width="8"
height="20"
rx="1.49998"
stroke="currentColor"
strokeWidth="4"
mask={`url(#${maskId})`}
/>
<path
d="M9.5 10H14.5C14.7761 10 15 10.2239 15 10.5V21H9.5C9.22387 21 9 20.7761 9 20.5V10.5C9 10.2239 9.22387 10 9.5 10Z"
stroke="currentColor"
strokeWidth="2"
/>
<path
d="M3.5 16H8.5C8.77613 16 9 16.2239 9 16.5V21H3.5C3.22387 21 3 20.7761 3 20.5V16.5C3 16.2239 3.22387 16 3.5 16Z"
stroke="currentColor"
strokeWidth="2"
/>
</svg>
);
};

export default SvgBarChart;
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;
2 changes: 2 additions & 0 deletions apps/evm/src/components/Icon/icons/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { screen } from '@testing-library/react';

import { renderComponent } from 'testUtils/render';

import { EligibilityStatus } from '..';

const baseProps = {
hasStakedXvs: true,
isCandidate: true,
gapXvsTokens: 5_432,
};

describe('pages/PrimeLeaderboard/RankCard/EligibilityStatus', () => {
it('shows the eligible message for candidates', () => {
renderComponent(<EligibilityStatus {...baseProps} />);

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();
});
});
61 changes: 61 additions & 0 deletions apps/evm/src/containers/PrimeRank/EligibilityStatus/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
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;
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,
gapXvsTokens,
linkSlot,
className,
}) => {
const { t, Trans } = useTranslation();

const isEligible = hasStakedXvs && isCandidate;

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>
);
};
63 changes: 63 additions & 0 deletions apps/evm/src/containers/PrimeRank/Footer/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { routes } from 'constants/routing';
import { Link } from 'containers/Link';
import { useTranslation } from 'libs/translations';

import { EligibilityStatus } from '../EligibilityStatus';
import { getRankLabels } from '../getRankLabels';
import { useGetPrimeRank } from '../useGetPrimeRank';

export interface FooterProps {
hideLeaderboardLink?: boolean;
}

export const Footer: React.FC<FooterProps> = ({ hideLeaderboardLink }) => {
const { t, Trans } = useTranslation();

const rankData = useGetPrimeRank();
const { hasStakedXvs, isCandidate, gapXvsTokens } = rankData;

const { rankLabel, primeScoreLabel } = getRankLabels(rankData);

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}
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>
);
};
21 changes: 21 additions & 0 deletions apps/evm/src/containers/PrimeRank/getRankLabels/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import BigNumber from 'bignumber.js';

import { shortenValueWithSuffix } from 'utilities';

import type { PrimeRankData } from '../useGetPrimeRank';

export interface RankLabels {
rankLabel: string;
primeScoreLabel: string;
}

export const getRankLabels = ({
hasStakedXvs,
rank,
primeScore,
}: Pick<PrimeRankData, 'hasStakedXvs' | 'rank' | 'primeScore'>): RankLabels => ({
rankLabel: hasStakedXvs ? `#${rank}` : '#-',
primeScoreLabel: hasStakedXvs
? shortenValueWithSuffix({ value: new BigNumber(primeScore) })
: '-',
});
18 changes: 18 additions & 0 deletions apps/evm/src/containers/PrimeRank/useGetPrimeRank/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
export interface PrimeRankData {
hasStakedXvs: boolean;
isCandidate: 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,
rank: 2,
primeScore: 542_500_000,
gapXvsTokens: 5_432,
};

export const useGetPrimeRank = (): PrimeRankData => placeholderRankData;
Comment on lines +10 to +18

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Hardcoded placeholder visible to all connected users

Every connected user who visits the Prime Leaderboard will see rank #2, score 542.5M, and the congratulatory "Congrats! You're in the Top 500 during last cycle and qualified for Prime Rewards." message because isPrime: true, hasSupplied: true, and isCandidate: true are hardcoded. This data feeds into both RankSection (the leaderboard page card) and the Footer shown inside the VenusVaultModal stake tab (when primeLeaderboard feature flag is on). If the feature flag for the page or the modal footer is enabled before the API integration lands, users will see completely incorrect eligibility status and rank numbers.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

mock data for testnet

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
2 changes: 1 addition & 1 deletion apps/evm/src/containers/VaultCard/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { useState } from 'react';

import { Card, LabeledInlineContent, LayeredValues, NoticeWarning } from 'components';
import { CopyAddressButton } from 'containers/CopyAddressButton';
import { VenusVaultModal } from 'containers/VenusVaultModal';
import useConvertMantissaToReadableTokenString from 'hooks/useConvertMantissaToReadableTokenString';
import { useTranslation } from 'libs/translations';
import { useAccountAddress } from 'libs/wallet';
Expand All @@ -23,7 +24,6 @@ import { PrimeEligibilityInlineContent } from './PrimeEligibilityInlineContent';
import { Progress } from './Progress';
import { StatusLabel } from './StatusLabel';
import { VaultName } from './VaultName';
import { VenusVaultModal } from './VenusVaultModal';

export interface VaultProps {
vault: Vault;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<StakeFormProps> = ({ vault, onClose }) => {
export const StakeForm: React.FC<StakeFormProps> = ({
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;
Expand Down Expand Up @@ -101,7 +112,15 @@ export const StakeForm: React.FC<StakeFormProps> = ({ vault, onClose }) => {
fromTokenFieldLabel={t('vaultCard.vaultModal.stakeForm.depositField.label')}
submitButtonLabel={t('vaultCard.vaultModal.stakeForm.submitButton.label')}
fromTokenPriceCents={vault.stakedTokenPriceCents.toNumber()}
footer={<Footer action="stake" vault={vault} fromAmountTokens={fromAmountTokens} />}
footer={
<div className="flex flex-col gap-4">
<Footer action="stake" vault={vault} fromAmountTokens={fromAmountTokens} />

{showPrimeRankFooter && (
<PrimeRankFooter hideLeaderboardLink={hidePrimeLeaderboardLink} />
)}
</div>
}
/>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,12 @@ import BigNumber from 'bignumber.js';

import { useGetVaiVaultUserInfo, useWithdrawFromVaiVault } from 'clients/api';
import { NULL_ADDRESS } from 'constants/address';
import { TransactionForm } from 'containers/VaultCard/TransactionForm';
import { useForm } from 'containers/VaultCard/useForm';
import { useTranslation } from 'libs/translations';
import { useAccountAddress } from 'libs/wallet';
import type { VenusVault } from 'types';
import { convertMantissaToTokens, convertTokensToMantissa } from 'utilities';
import { TransactionForm } from '../../../TransactionForm';
import { Footer } from '../../Footer';

export interface WithdrawFromVaiVaultFormProps {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,9 @@ import {
} from 'clients/api';
import { InfoIcon, NoticeWarning, Spinner, TextButton } from 'components';
import { NULL_ADDRESS } from 'constants/address';
import { Footer } from 'containers/VaultCard/VenusVaultModal/Footer';
import { TransactionForm } from 'containers/VaultCard/TransactionForm';
import { useForm } from 'containers/VaultCard/useForm';
import { Footer } from 'containers/VenusVaultModal/Footer';
import { isBefore } from 'date-fns/isBefore';
import useConvertMantissaToReadableTokenString from 'hooks/useConvertMantissaToReadableTokenString';
import { useNow } from 'hooks/useNow';
Expand All @@ -21,7 +22,6 @@ import { useAccountAddress } from 'libs/wallet';
import type { VenusVault } from 'types';
import { convertTokensToMantissa } from 'utilities';
import { convertMantissaToTokens } from 'utilities/convertMantissaToTokens';
import { TransactionForm } from '../../../../TransactionForm';

export interface RequestWithdrawalFormProps {
vault: VenusVault;
Expand Down
Loading
Loading