Skip to content

Commit

Permalink
feat(tangle-dapp): Add Operator Profile Page (#2537)
Browse files Browse the repository at this point in the history
  • Loading branch information
AtelyPham authored Sep 19, 2024
1 parent aecbf6b commit 677c600
Show file tree
Hide file tree
Showing 23 changed files with 596 additions and 65 deletions.
2 changes: 0 additions & 2 deletions apps/bridge-dapp/src/components/NoteAccountAvatarWithKey.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import { IconWithTooltip } from '@webb-tools/webb-ui-components/components/IconW
import { KeyValueWithButton } from '@webb-tools/webb-ui-components/components/KeyValueWithButton';
import { useCopyable } from '@webb-tools/webb-ui-components/hooks/useCopyable';
import type { PropsOf } from '@webb-tools/webb-ui-components/types';
import { shortenString } from '@webb-tools/webb-ui-components/utils/shortenString';
import type { ComponentProps, ElementRef } from 'react';
import { forwardRef } from 'react';
import { twMerge } from 'tailwind-merge';
Expand Down Expand Up @@ -55,7 +54,6 @@ const NoteAccountAvatarWithKey = forwardRef<
.join('')
: keyValue
}
shortenFn={isHiddenValue ? shortenString : undefined}
isDisabledTooltip={isHiddenValue}
copyProps={isHiddenValue ? copyableResult : undefined}
onCopyButtonClick={
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@ import {
formatTokenAmount,
fuzzyFilter,
numberToString,
shortenString,
} from '@webb-tools/webb-ui-components';
import { FC, useMemo } from 'react';

Expand Down Expand Up @@ -121,7 +120,6 @@ const staticColumns = [
cell: (props) => (
<div className="flex items-center">
<KeyValueWithButton
shortenFn={(note: string) => shortenString(note, 4)}
isHiddenLabel
size="sm"
keyValue={props.getValue()}
Expand Down
17 changes: 8 additions & 9 deletions apps/tangle-dapp/app/nomination/[validatorAddress]/InfoCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,14 @@ import {
Chip,
CopyWithTooltip,
ExternalLinkIcon,
SocialChip,
Typography,
} from '@webb-tools/webb-ui-components';
import { shortenString } from '@webb-tools/webb-ui-components/utils/shortenString';
import { FC } from 'react';
import { twMerge } from 'tailwind-merge';

import { TangleCard } from '../../../components';
import ValidatorSocials from '../../../components/ValidatorSocials';
import { EMPTY_VALUE_PLACEHOLDER } from '../../../constants';
import useNetworkStore from '../../../context/useNetworkStore';
import useValidatorInfoCard from '../../../data/validatorDetails/useValidatorInfoCard';
Expand Down Expand Up @@ -106,14 +106,13 @@ const InfoCard: FC<InfoCardProps> = ({
</div>

{/* Socials & Location */}
<div className="flex gap-2 min-h-[30px]">
<div className="flex items-center flex-1 gap-2">
{twitter && <SocialChip type="twitter" href={twitter} />}
{email && <SocialChip type="email" href={`mailto:${email}`} />}
{web && <SocialChip type="website" href={web} />}
</div>
{/* TODO: get location later */}
</div>
<ValidatorSocials
twitterUrl={twitter ?? ''}
email={email ?? ''}
webUrl={web ?? ''}
// TODO: get location later
location={undefined}
/>
</div>
</TangleCard>
);
Expand Down
7 changes: 1 addition & 6 deletions apps/tangle-dapp/app/restake/OperatorList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import { ListCardWrapper } from '@webb-tools/webb-ui-components/components/ListC
import { ListItem } from '@webb-tools/webb-ui-components/components/ListCard/ListItem';
import { ScrollArea } from '@webb-tools/webb-ui-components/components/ScrollArea';
import { Typography } from '@webb-tools/webb-ui-components/typography/Typography';
import { shortenString } from '@webb-tools/webb-ui-components/utils/shortenString';
import isFunction from 'lodash/isFunction';
import keys from 'lodash/keys';
import omitBy from 'lodash/omitBy';
Expand Down Expand Up @@ -110,11 +109,7 @@ const OperatorList = forwardRef<HTMLDivElement, Props>(
operatorIdentities?.[current]?.name || '<Unknown>'
}
description={
<KeyValueWithButton
size="sm"
keyValue={current}
shortenFn={shortenString}
/>
<KeyValueWithButton size="sm" keyValue={current} />
}
/>
</ListItem>
Expand Down
9 changes: 2 additions & 7 deletions apps/tangle-dapp/app/restake/OperatorsTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,19 +36,14 @@ const OperatorsTable: FC<Props> = ({
const operators = useMemo(
() =>
Object.entries(operatorMap).map<OperatorUI>(
([address, { delegations }]) => {
([address, { delegations, restakersCount }]) => {
const vaultAssets = delegations
.map((delegation) => ({
asset: assetMap[delegation.assetId],
amount: delegation.amount,
}))
.filter((vaultAsset) => Boolean(vaultAsset.asset));

const restakerSet = delegations.reduce((restakerSet, delegation) => {
restakerSet.add(delegation.delegatorAccountId);
return restakerSet;
}, new Set<string>());

const tvlInUsd = operatorTVL?.[address] ?? null;
const concentrationPercentage =
operatorConcentration?.[address] ?? null;
Expand All @@ -57,7 +52,7 @@ const OperatorsTable: FC<Props> = ({
address,
concentrationPercentage,
identityName: identities[address]?.name ?? '',
restakersCount: restakerSet.size,
restakersCount,
tvlInUsd,
vaultTokens: vaultAssets.map(({ asset, amount }) => ({
amount: +formatUnits(amount, asset.decimals),
Expand Down
175 changes: 175 additions & 0 deletions apps/tangle-dapp/app/restake/operators/[address]/OperatorInfoCard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
import { isHex } from '@polkadot/util';
import isValidUrl from '@webb-tools/dapp-types/utils/isValidUrl';
import { ExternalLinkLine } from '@webb-tools/icons/ExternalLinkLine';
import { Chip } from '@webb-tools/webb-ui-components/components/Chip';
import InfoIconWithTooltip from '@webb-tools/webb-ui-components/components/IconWithTooltip/InfoIconWithTooltip';
import { KeyValueWithButton } from '@webb-tools/webb-ui-components/components/KeyValueWithButton';
import { Typography } from '@webb-tools/webb-ui-components/typography/Typography';
import { shortenHex } from '@webb-tools/webb-ui-components/utils/shortenHex';
import { shortenString } from '@webb-tools/webb-ui-components/utils/shortenString';
import { type ComponentProps, type FC, type ReactNode, useMemo } from 'react';
import useSWRImmutable from 'swr/immutable';
import { twMerge } from 'tailwind-merge';

import GlassCard from '../../../../components/GlassCard/GlassCard';
import ValidatorSocials from '../../../../components/ValidatorSocials';
import { EMPTY_VALUE_PLACEHOLDER } from '../../../../constants';
import useNetworkStore from '../../../../context/useNetworkStore';
import type {
DelegatorInfo,
OperatorMap,
OperatorMetadata,
} from '../../../../types/restake';
import getTVLToDisplay from '../../../../utils/getTVLToDisplay';
import { getAccountInfo } from '../../../../utils/polkadot';
import AvatarWithText from '../../AvatarWithText';

interface Props extends Partial<ComponentProps<typeof GlassCard>> {
operatorAddress: string;
operatorData: OperatorMetadata | undefined;
operatorMap: OperatorMap;
delegatorInfo: DelegatorInfo | null;
operatorTVL: Record<string, number>;
}

const OperatorInfoCard: FC<Props> = ({
className,
operatorAddress,
operatorData,
operatorMap,
delegatorInfo,
operatorTVL,
...props
}) => {
const { rpcEndpoint } = useNetworkStore();

const isRestaked = useMemo<boolean>(() => {
if (delegatorInfo === null) {
return false;
}

const foundDelegation = delegatorInfo.delegations.find(
(delegation) => delegation.operatorAccountId === operatorAddress,
);

return Boolean(foundDelegation);
}, [delegatorInfo, operatorAddress]);

const totalRestaked = useMemo(
() => getTVLToDisplay(operatorTVL[operatorAddress]),
[operatorAddress, operatorTVL],
);

const restakersCount = useMemo(
() => operatorData?.restakersCount.toString() ?? EMPTY_VALUE_PLACEHOLDER,
[operatorData?.restakersCount],
);

const { data: operatorInfo } = useSWRImmutable(
[rpcEndpoint, operatorAddress],
(args) => getAccountInfo(...args),
);

const identityName = useMemo(() => {
const defaultName = isHex(operatorAddress)
? shortenHex(operatorAddress)
: shortenString(operatorAddress);

if (!operatorInfo) {
return defaultName;
}

return operatorInfo.name || defaultName;
}, [operatorAddress, operatorInfo]);

const validatorSocials = useMemo(() => {
const twitterHandle = operatorInfo?.twitter ?? '';
const webUrl = operatorInfo?.web ?? '';
const email = operatorInfo?.email ?? '';

const twitterUrl =
twitterHandle === '' || isValidUrl(twitterHandle)
? twitterHandle
: `https://x.com/${twitterHandle}`;

return {
twitterUrl,
webUrl,
email,
// TODO: Add location
location: '',
// TODO: Add github link
githubUrl: '',
};
}, [operatorInfo?.email, operatorInfo?.twitter, operatorInfo?.web]);

return (
<GlassCard {...props} className={twMerge('gap-10', className)}>
<div className="flex items-start justify-between">
<AvatarWithText
overrideAvatarProps={{
size: 'lg',
}}
overrideTypographyProps={{
variant: 'h4',
fw: 'bold',
}}
accountAddress={operatorAddress}
identityName={identityName}
description={
<div className="flex items-baseline gap-1">
<KeyValueWithButton
className="mt-1"
size="sm"
keyValue={operatorAddress}
/>

<ExternalLinkLine />
</div>
}
/>

{isRestaked && <Chip color="green">Restaked</Chip>}
</div>

<div className="flex flex-wrap gap-4">
<StatsItem label="Total Restake" value={totalRestaked} />
<StatsItem label="Restakers" value={restakersCount} />
</div>

<ValidatorSocials {...validatorSocials} />
</GlassCard>
);
};

export default OperatorInfoCard;

interface StatsItemProps {
label: string;
value: string;
info?: ReactNode;
}

const StatsItem: FC<StatsItemProps> = ({ label, value, info }) => {
return (
<div className="flex-1">
<Typography variant="h4" fw="bold">
{value}
</Typography>

<Typography
variant="h5"
fw="normal"
className="text-mono-120 dark:text-mono-100"
>
{label}
{info && (
<InfoIconWithTooltip
className="fill-mono-120 dark:fill-mono-100"
content={info}
/>
)}
</Typography>
</div>
);
};
Loading

0 comments on commit 677c600

Please sign in to comment.