Skip to content

Commit

Permalink
feat(frontend): add SI balance formatting (#262)
Browse files Browse the repository at this point in the history
  • Loading branch information
nikitayutanov authored Dec 22, 2024
1 parent 686b26b commit dc935f8
Show file tree
Hide file tree
Showing 23 changed files with 297 additions and 186 deletions.
1 change: 1 addition & 0 deletions frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
"@hookform/resolvers": "3.9.0",
"@polkadot/api": "14.3.1",
"@polkadot/react-identicon": "3.11.3",
"@polkadot/util": "13.2.3",
"@tanstack/react-query": "5.61.5",
"@web3modal/wagmi": "5.1.3",
"graphql": "16.9.0",
Expand Down
3 changes: 3 additions & 0 deletions frontend/pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

30 changes: 30 additions & 0 deletions frontend/src/components/formatted-balance/fortmatted-balance.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { formatBalance } from '@polkadot/util';
import { ComponentProps } from 'react';
import { formatUnits } from 'viem';

import { TruncatedText } from '../layout';
import { Tooltip } from '../tooltip';

type Props = {
value: bigint;
decimals: number;
symbol: string;
tooltipPosition?: ComponentProps<typeof Tooltip>['position'];
className?: string;
};

function FormattedBalance({ value, decimals, symbol, tooltipPosition, className }: Props) {
const formattedValue = formatUnits(value, decimals);
const compactBalance = formatBalance(value, { decimals, withUnit: symbol, withZero: false });

return (
<Tooltip value={`${formattedValue} ${symbol}`} position={tooltipPosition}>
<TruncatedText
value={compactBalance === '0' ? `${compactBalance} ${symbol}` : compactBalance}
className={className}
/>
</Tooltip>
);
}

export { FormattedBalance };
3 changes: 3 additions & 0 deletions frontend/src/components/formatted-balance/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { FormattedBalance } from './fortmatted-balance';

export { FormattedBalance };
2 changes: 2 additions & 0 deletions frontend/src/components/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { Card } from './card';
import { CopyButton } from './copy-button';
import { FeeAndTimeFooter } from './fee-and-time-footer';
import { Input, Checkbox, Radio, Select, Textarea } from './form';
import { FormattedBalance } from './formatted-balance';
import { Container, Footer, Header, ErrorBoundary, PrivateRoute, Skeleton, TruncatedText } from './layout';
import { LinkButton } from './link-button';
import { Tooltip } from './tooltip';
Expand All @@ -24,4 +25,5 @@ export {
CopyButton,
Tooltip,
FeeAndTimeFooter,
FormattedBalance,
};
46 changes: 19 additions & 27 deletions frontend/src/components/tooltip/tooltip.module.scss
Original file line number Diff line number Diff line change
@@ -1,30 +1,35 @@
@keyframes fadeIn {
from {
opacity: 0;
}

to {
opacity: 1;
}
}

.container,
.skeleton {
flex-shrink: 0;
}

.container {
display: flex;

position: relative;

svg {
transition: 0.25s opacity;

&:hover {
&:hover {
.body {
opacity: 0.75;
}
}
}

&:hover {
.tooltip {
opacity: 1;
}
}
.body {
display: flex;

transition: 0.25s opacity;
}

.tooltip {
position: absolute;
z-index: 11;

padding: 10px 16px;

Expand All @@ -42,18 +47,5 @@
border-radius: 4px;
box-shadow: 0px 8px 16px 0px rgba(#000, 0.24);

opacity: 0;
pointer-events: none;
transition: 0.25s opacity;
}

.top {
bottom: calc(100% + 8px);
left: 50%;
transform: translateX(-50%);
}

.bottom-end {
top: calc(100% + 8px);
right: 0;
animation: fadeIn 0.25s;
}
107 changes: 92 additions & 15 deletions frontend/src/components/tooltip/tooltip.tsx
Original file line number Diff line number Diff line change
@@ -1,32 +1,109 @@
import { ReactNode } from 'react';
import { CSSProperties, ReactNode, useEffect, useRef, useState } from 'react';
import { createPortal } from 'react-dom';

import { SVGComponent } from '@/types';
import { cx } from '@/utils';

import { Skeleton } from '../layout';

import QuestionSVG from './question.svg?react';
import styles from './tooltip.module.scss';

type BaseProps = {
text?: string;
children?: ReactNode;
position?: 'top' | 'bottom-end';
type Props = {
value?: ReactNode;
position?: 'top' | 'right' | 'bottom-end';
SVG?: SVGComponent;
children?: ReactNode;
};

type TextProps = BaseProps & { text: string };
type ChildrenProps = BaseProps & { children: ReactNode };
type Props = TextProps | ChildrenProps;
function TooltipComponent({ value, style }: { value: ReactNode; style: CSSProperties }) {
const [root, setRoot] = useState<HTMLElement>();

useEffect(() => {
const ID = 'tooltip-root';
const existingRoot = document.getElementById(ID);

if (existingRoot) return setRoot(existingRoot);

const newRoot = document.createElement('div');
newRoot.id = ID;
document.body.appendChild(newRoot);

setRoot(newRoot);

return () => {
if (!newRoot) return;

document.body.removeChild(newRoot);
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);

if (!root) return null;

return createPortal(
<div className={styles.tooltip} style={style}>
{typeof value === 'string' ? <p className={styles.heading}>{value}</p> : value}
</div>,
root,
);
}

function Tooltip({ value, position = 'top', SVG = QuestionSVG, children }: Props) {
const [style, setStyle] = useState<CSSProperties>();
const containerRef = useRef<HTMLDivElement>(null);

const handleMouseEnter = () => {
const container = containerRef.current;

if (!container) return;

const containerRect = container.getBoundingClientRect();

const GAP = 8;
let top = 0;
let left = 0;
let transform = '';

switch (position) {
case 'top': {
top = containerRect.top + window.scrollY - GAP;
left = containerRect.left + window.scrollX + containerRect.width / 2;
transform = 'translate(-50%, -100%)';

break;
}

case 'right': {
top = containerRect.top + window.scrollY + containerRect.height / 2;
left = containerRect.right + window.scrollX + GAP;
transform = 'translateY(-50%)';

break;
}
case 'bottom-end': {
top = containerRect.bottom + window.scrollY + GAP;
left = containerRect.right - window.scrollX;
transform = 'translate(-100%, 0)';

break;
}

default:
break;
}

setStyle({ top, left, transform });
};

function Tooltip({ text, children, position = 'top', SVG = QuestionSVG }: Props) {
return (
<div className={styles.container}>
<SVG />
<div
className={styles.container}
onMouseEnter={handleMouseEnter}
onMouseLeave={() => setStyle(undefined)}
ref={containerRef}>
<div className={styles.body}>{children || <SVG />}</div>

<div className={cx(styles.tooltip, styles[position])}>
{text ? <p className={styles.heading}>{text}</p> : children}
</div>
{style && <TooltipComponent value={value} style={style} />}
</div>
);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
import { HexString } from '@gear-js/api';
import { getVaraAddress } from '@gear-js/react-hooks';
import { Modal } from '@gear-js/vara-ui';
import { formatUnits } from 'viem';

import { CopyButton, FeeAndTimeFooter, LinkButton, TruncatedText } from '@/components';
import { CopyButton, FeeAndTimeFooter, FormattedBalance, LinkButton, TruncatedText } from '@/components';
import { useEthFee, useVaraFee } from '@/features/swap/hooks';
import { useTokens } from '@/hooks';
import { cx } from '@/utils';
Expand Down Expand Up @@ -56,8 +55,6 @@ function TransactionModal({
const sourceSymbol = symbols?.[source as HexString] || 'Unit';
const destinationSymbol = symbols?.[destination as HexString] || 'Unit';

const formattedAmount = formatUnits(BigInt(amount), decimals?.[source as HexString] ?? 0);

const formattedSenderAddress = isGearNetwork ? getVaraAddress(sender) : sender;
const formattedReceiverAddress = isGearNetwork ? receiver : getVaraAddress(receiver);

Expand All @@ -83,11 +80,14 @@ function TransactionModal({
</header>
)}

<p className={cx(styles.pairs, renderProgressBar && styles.loading)}>
<div className={cx(styles.pairs, renderProgressBar && styles.loading)}>
<span className={styles.tx}>
<span className={styles.amount}>
{formattedAmount} {sourceSymbol}
</span>
<FormattedBalance
value={BigInt(amount)}
decimals={decimals?.[source as HexString] ?? 0}
symbol={sourceSymbol}
className={styles.amount}
/>

<span className={styles.label}>on</span>

Expand All @@ -100,9 +100,12 @@ function TransactionModal({
<ArrowSVG className={styles.arrowSvg} />

<span className={styles.tx}>
<span className={styles.amount}>
{formattedAmount} {destinationSymbol}
</span>
<FormattedBalance
value={BigInt(amount)}
decimals={decimals?.[source as HexString] ?? 0}
symbol={destinationSymbol}
className={styles.amount}
/>

<span className={styles.label}>on</span>

Expand All @@ -123,7 +126,7 @@ function TransactionModal({
<span className={styles.label}>To</span>
<TruncatedText value={formattedReceiverAddress} className={styles.value} />
</span>
</p>
</div>

{renderProgressBar?.()}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
import { HexString } from '@gear-js/api';
import { getVaraAddress } from '@gear-js/react-hooks';
import { formatUnits } from 'viem';

import TokenPlaceholderSVG from '@/assets/token-placeholder.svg?react';
import VaraSVG from '@/assets/vara.svg?react';
import { Skeleton, TruncatedText } from '@/components';
import { FormattedBalance, Skeleton, TruncatedText } from '@/components';
import { TOKEN_SVG } from '@/consts';
import { cx } from '@/utils';

Expand Down Expand Up @@ -41,8 +40,6 @@ function TransactionPair(props: Props) {
const formattedSenderAddress = isGearNetwork ? getVaraAddress(sender) : sender;
const formattedReceiverAddress = isGearNetwork ? receiver : getVaraAddress(receiver);

const formattedAmount = formatUnits(BigInt(amount), decimals[sourceHex] ?? 0);

return (
<div className={cx(styles.pair, isCompact && styles.compact)}>
<div className={styles.tx}>
Expand All @@ -52,7 +49,13 @@ function TransactionPair(props: Props) {
</div>

<div>
<TruncatedText value={`${formattedAmount} ${sourceSymbol}`} className={styles.amount} />
<FormattedBalance
value={BigInt(amount)}
decimals={decimals[sourceHex] ?? 0}
symbol={sourceSymbol}
className={styles.amount}
/>

<TruncatedText value={formattedSenderAddress} className={styles.address} />
</div>
</div>
Expand All @@ -66,7 +69,13 @@ function TransactionPair(props: Props) {
</div>

<div>
<TruncatedText value={`${formattedAmount} ${destinationSymbol}`} className={styles.amount} />
<FormattedBalance
value={BigInt(amount)}
decimals={decimals[sourceHex] ?? 0}
symbol={destinationSymbol}
className={styles.amount}
/>

<TruncatedText value={formattedReceiverAddress} className={styles.address} />
</div>
</div>
Expand Down
Loading

0 comments on commit dc935f8

Please sign in to comment.