Skip to content

Commit

Permalink
feat(wallet-dashboard): style stake overview (#4315)
Browse files Browse the repository at this point in the history
* feat(wallet-dashboard): style selected visual Assets.

* refactor(core): destructure metaKeys and metaValues from attributes

* refactor(wallet): move Collapsible component to core.

* feat(dashboard): integrate useAssetsDialog for asset details view

* fix(assets): update import path and enhance text styling in DetailsView

* refactor(wallet-dashboard): move state to page

* refactor(wallet-dashboard): rename handler functions for consistency and clarity

* refactor(dashboard): update state for asset view, improve code

* feat: style stake overview

* fix: naming and e2e tests

* fix: revert staking constants file naming

* fix(wallet-dashboard): unify asset transfer success and error handling in AssetDialog

* refactor(dashboard): adjust z-index for dialog and notifications;

* feat(dashboard): add refetch functionality after asset transfered

* fix: remove leftover text

* refactor(dashboard): rename callbacks for clarity in AssetDialog

* refactor(dashboard, cove): rename hooks, remove duplication.

* refactor(dashboard): remove unused asset details page

* fix: remove leftover comments

---------

Co-authored-by: Panteleymonchuk <[email protected]>
Co-authored-by: evavirseda <[email protected]>
Co-authored-by: Marc Espin <[email protected]>
  • Loading branch information
4 people authored Dec 4, 2024
1 parent fdd1b8a commit 2fdc59e
Show file tree
Hide file tree
Showing 18 changed files with 230 additions and 168 deletions.
1 change: 1 addition & 0 deletions apps/core/src/components/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,4 @@ export * from './Inputs';
export * from './QR';
export * from './collapsible';
export * from './providers';
export * from './stake';
Original file line number Diff line number Diff line change
Expand Up @@ -2,22 +2,15 @@
// Modifications Copyright (c) 2024 IOTA Stiftung
// SPDX-License-Identifier: Apache-2.0

import { NUM_OF_EPOCH_BEFORE_STAKING_REWARDS_REDEEMABLE } from '@iota/core';
import { determineCountDownText } from '_src/ui/app/shared/countdown-timer';
import {
type ExtendedDelegatedStake,
TimeUnit,
useFormatCoin,
useGetTimeBeforeEpochNumber,
useTimeAgo,
ImageIcon,
} from '@iota/core';
import { IOTA_TYPE_ARG } from '@iota/iota-sdk/utils';
import { Card, CardImage, CardType, CardBody, CardAction, CardActionType } from '@iota/apps-ui-kit';
import { useMemo } from 'react';
import { Link } from 'react-router-dom';

import { useIotaClientQuery } from '@iota/dapp-kit';
import { ImageIcon } from '../icon';
import { determineCountDownText, ExtendedDelegatedStake } from '../../utils';
import { TimeUnit, useFormatCoin, useGetTimeBeforeEpochNumber, useTimeAgo } from '../../hooks';
import { NUM_OF_EPOCH_BEFORE_STAKING_REWARDS_REDEEMABLE } from '../../constants';
import React from 'react';

export enum StakeState {
WarmUp = 'WARM_UP',
Expand All @@ -35,21 +28,22 @@ const STATUS_COPY: { [key in StakeState]: string } = {
[StakeState.InActive]: 'Inactive',
};

interface StakeCardProps {
interface StakedCardProps {
extendedStake: ExtendedDelegatedStake;
currentEpoch: number;
inactiveValidator?: boolean;
onClick: () => void;
}

// For delegationsRequestEpoch n through n + 2, show Start Earning
// Show epoch number or date/time for n + 3 epochs
export function StakeCard({
export function StakedCard({
extendedStake,
currentEpoch,
inactiveValidator = false,
}: StakeCardProps) {
const { stakedIotaId, principal, stakeRequestEpoch, estimatedReward, validatorAddress } =
extendedStake;
onClick,
}: StakedCardProps) {
const { principal, stakeRequestEpoch, estimatedReward, validatorAddress } = extendedStake;

// TODO: Once two step withdraw is available, add cool down and withdraw now logic
// For cool down epoch, show Available to withdraw add rewards to principal
Expand Down Expand Up @@ -115,32 +109,20 @@ export function StakeCard({
};

return (
<Link
data-testid="stake-card"
to={`/stake/delegation-detail?${new URLSearchParams({
validator: validatorAddress,
staked: stakedIotaId,
}).toString()}`}
className="no-underline"
>
<Card type={CardType.Default} isHoverable>
<CardImage>
<ImageIcon
src={validatorMeta?.imageUrl || null}
label={validatorMeta?.name || ''}
fallback={validatorMeta?.name || ''}
/>
</CardImage>
<CardBody
title={validatorMeta?.name || ''}
subtitle={`${principalStaked} ${symbol}`}
/>
<CardAction
title={rewardTime()}
subtitle={STATUS_COPY[delegationState]}
type={CardActionType.SupportingText}
<Card testId="staked-card" type={CardType.Default} isHoverable onClick={onClick}>
<CardImage>
<ImageIcon
src={validatorMeta?.imageUrl || null}
label={validatorMeta?.name || ''}
fallback={validatorMeta?.name || ''}
/>
</Card>
</Link>
</CardImage>
<CardBody title={validatorMeta?.name || ''} subtitle={`${principalStaked} ${symbol}`} />
<CardAction
title={rewardTime()}
subtitle={STATUS_COPY[delegationState]}
type={CardActionType.SupportingText}
/>
</Card>
);
}
4 changes: 4 additions & 0 deletions apps/core/src/components/stake/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
// Copyright (c) 2024 IOTA Stiftung
// SPDX-License-Identifier: Apache-2.0

export * from './StakedCard';
1 change: 1 addition & 0 deletions apps/core/src/constants/staking.constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,4 @@ export const DELEGATED_STAKES_QUERY_REFETCH_INTERVAL = 30_000;

export const NUM_OF_EPOCH_BEFORE_STAKING_REWARDS_REDEEMABLE = 2;
export const NUM_OF_EPOCH_BEFORE_STAKING_REWARDS_STARTS = 1;
export const MIN_NUMBER_IOTA_TO_STAKE = 1;
16 changes: 16 additions & 0 deletions apps/core/src/utils/determineCountDownText.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
// Copyright (c) Mysten Labs, Inc.
// Modifications Copyright (c) 2024 IOTA Stiftung
// SPDX-License-Identifier: Apache-2.0

export function determineCountDownText({
timeAgo,
label,
endLabel,
}: {
timeAgo: string;
label?: string;
endLabel?: string;
}): string {
const showLabel = timeAgo !== endLabel;
return showLabel ? `${label} ${timeAgo}` : timeAgo;
}
1 change: 1 addition & 0 deletions apps/core/src/utils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ export * from './api-env';
export * from './getExplorerPaths';
export * from './getExplorerLink';
export * from './truncateString';
export * from './determineCountDownText';

export * from './stake';
export * from './transaction';
Expand Down
6 changes: 6 additions & 0 deletions apps/ui-kit/src/lib/components/molecules/card/Card.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,10 @@ export interface CardProps {
* Use case: When the card is wrapped with a Link component
*/
isHoverable?: boolean;
/**
* The 'data-testid' attribute value (used in e2e tests)
*/
testId?: string;
}

export function Card({
Expand All @@ -40,6 +44,7 @@ export function Card({
isHoverable,
onClick,
children,
testId,
}: CardProps) {
return (
<div
Expand All @@ -53,6 +58,7 @@ export function Card({
'cursor-pointer': onClick,
},
)}
data-testid={testId}
>
{children}
</div>
Expand Down
175 changes: 129 additions & 46 deletions apps/wallet-dashboard/app/(protected)/staking/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,23 +3,43 @@

'use client';

import { AmountBox, Box, StakeCard, StakeDialog, Button, useStakeDialog } from '@/components';
import { StartStaking } from '@/components/staking-overview/StartStaking';
import {
Button,
ButtonSize,
ButtonType,
DisplayStats,
InfoBox,
InfoBoxStyle,
InfoBoxType,
Panel,
Title,
TitleSize,
} from '@iota/apps-ui-kit';
import { StakeDialog } from '@/components';
import { StakeDialogView } from '@/components/Dialogs/Staking/StakeDialog';
import {
ExtendedDelegatedStake,
formatDelegatedStake,
useFormatCoin,
useGetDelegatedStake,
useTotalDelegatedRewards,
useTotalDelegatedStake,
DELEGATED_STAKES_QUERY_REFETCH_INTERVAL,
DELEGATED_STAKES_QUERY_STALE_TIME,
StakedCard,
useFormatCoin,
} from '@iota/core';
import { useCurrentAccount } from '@iota/dapp-kit';
import { useCurrentAccount, useIotaClientQuery } from '@iota/dapp-kit';
import { IotaSystemStateSummary } from '@iota/iota-sdk/client';
import { Info } from '@iota/ui-icons';
import { useMemo } from 'react';
import { useStakeDialog } from '@/components/Dialogs/Staking/hooks/useStakeDialog';
import { IOTA_TYPE_ARG } from '@iota/iota-sdk/utils';
import { StakeDialogView } from '@/components/Dialogs/Staking/StakeDialog';

function StakingDashboardPage(): JSX.Element {
const account = useCurrentAccount();
const { data: system } = useIotaClientQuery('getLatestIotaSystemState');
const activeValidators = (system as IotaSystemStateSummary)?.activeValidators;

const {
isDialogStakeOpen,
Expand All @@ -42,13 +62,29 @@ function StakingDashboardPage(): JSX.Element {
const extendedStakes = delegatedStakeData ? formatDelegatedStake(delegatedStakeData) : [];
const totalDelegatedStake = useTotalDelegatedStake(extendedStakes);
const totalDelegatedRewards = useTotalDelegatedRewards(extendedStakes);
const [formattedDelegatedStake, stakeSymbol, stakeResult] = useFormatCoin(
const [totalDelegatedStakeFormatted, symbol] = useFormatCoin(
totalDelegatedStake,
IOTA_TYPE_ARG,
);
const [formattedDelegatedRewards, rewardsSymbol, rewardsResult] = useFormatCoin(
totalDelegatedRewards,
IOTA_TYPE_ARG,
const [totalDelegatedRewardsFormatted] = useFormatCoin(totalDelegatedRewards, IOTA_TYPE_ARG);

const delegations = useMemo(() => {
return delegatedStakeData?.flatMap((delegation) => {
return delegation.stakes.map((d) => ({
...d,
// flag any inactive validator for the stakeIota object
// if the stakingPoolId is not found in the activeValidators list flag as inactive
inactiveValidator: !activeValidators?.find(
({ stakingPoolId }) => stakingPoolId === delegation.stakingPool,
),
validatorAddress: delegation.validatorAddress,
}));
});
}, [activeValidators, delegatedStakeData]);

// Check if there are any inactive validators
const hasInactiveValidatorDelegation = delegations?.some(
({ inactiveValidator }) => inactiveValidator,
);

const viewStakeDetails = (extendedStake: ExtendedDelegatedStake) => {
Expand All @@ -57,46 +93,93 @@ function StakingDashboardPage(): JSX.Element {
};

return (
<>
<div className="flex flex-col items-center justify-center gap-4 pt-12">
<AmountBox
title="Currently staked"
amount={
stakeResult.isPending ? '-' : `${formattedDelegatedStake} ${stakeSymbol}`
}
/>
<AmountBox
title="Earned"
amount={`${
rewardsResult.isPending ? '-' : formattedDelegatedRewards
} ${rewardsSymbol}`}
/>
<Box title="Stakes">
<div className="flex flex-col items-center gap-4">
<h1>List of stakes</h1>
{extendedStakes?.map((extendedStake) => (
<StakeCard
key={extendedStake.stakedIotaId}
extendedStake={extendedStake}
onDetailsClick={viewStakeDetails}
/>
))}
<div className="flex justify-center">
<div className="w-3/4">
{(delegatedStakeData?.length ?? 0) > 0 ? (
<Panel>
<Title
title="Staking"
trailingElement={
<Button
onClick={() => handleNewStake()}
size={ButtonSize.Small}
type={ButtonType.Primary}
text="Stake"
/>
}
/>
<div className="flex h-full w-full flex-col flex-nowrap gap-md p-md--rs">
<div className="flex gap-xs">
<DisplayStats
label="Your stake"
value={totalDelegatedStakeFormatted}
supportingLabel={symbol}
/>
<DisplayStats
label="Earned"
value={totalDelegatedRewardsFormatted}
supportingLabel={symbol}
/>
</div>
<Title title="In progress" size={TitleSize.Small} />
<div className="flex max-h-[420px] w-full flex-1 flex-col items-start overflow-auto">
{hasInactiveValidatorDelegation ? (
<div className="mb-3">
<InfoBox
type={InfoBoxType.Default}
title="Earn with active validators"
supportingText="Unstake IOTA from the inactive validators and stake on an active validator to start earning rewards again."
icon={<Info />}
style={InfoBoxStyle.Elevated}
/>
</div>
) : null}
<div className="w-full gap-2">
{system &&
delegations
?.filter(({ inactiveValidator }) => inactiveValidator)
.map((delegation) => (
<StakedCard
extendedStake={delegation}
currentEpoch={Number(system.epoch)}
key={delegation.stakedIotaId}
inactiveValidator
onClick={() => viewStakeDetails(delegation)}
/>
))}
</div>
<div className="w-full gap-2">
{system &&
delegations
?.filter(({ inactiveValidator }) => !inactiveValidator)
.map((delegation) => (
<StakedCard
extendedStake={delegation}
currentEpoch={Number(system.epoch)}
key={delegation.stakedIotaId}
onClick={() => viewStakeDetails(delegation)}
/>
))}
</div>
</div>
</div>
<StakeDialog
stakedDetails={selectedStake}
isOpen={isDialogStakeOpen}
handleClose={handleCloseStakeDialog}
view={stakeDialogView}
setView={setStakeDialogView}
selectedValidator={selectedValidator}
setSelectedValidator={setSelectedValidator}
/>
</Panel>
) : (
<div className="flex h-[270px] p-lg">
<StartStaking />
</div>
</Box>
<Button onClick={handleNewStake}>New Stake</Button>
)}
</div>
{isDialogStakeOpen && stakeDialogView && (
<StakeDialog
stakedDetails={selectedStake}
isOpen={isDialogStakeOpen}
handleClose={handleCloseStakeDialog}
view={stakeDialogView}
setView={setStakeDialogView}
selectedValidator={selectedValidator}
setSelectedValidator={setSelectedValidator}
/>
)}
</>
</div>
);
}

Expand Down
Loading

0 comments on commit 2fdc59e

Please sign in to comment.