Skip to content
This repository was archived by the owner on Jul 20, 2025. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from 3 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
7 changes: 7 additions & 0 deletions package-lock.json

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

3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@
"@popperjs/core": "^2.11.8",
"react-hook-form": "^7.54.0",
"react-popper": "^2.3.0",
"decimal.js-light": "^2.5.1",
"tw-colors": "^3.3.2"
}
}
}
19 changes: 19 additions & 0 deletions src/components/SubSection/SubSection.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import type { Meta, StoryObj } from "@storybook/react";

import { SubSection } from "./SubSection";
import { Text } from "../Text";

const meta: Meta<typeof SubSection> = {
component: SubSection,
tags: ["autodocs"],
};

export default meta;

type Story = StoryObj<typeof meta>;

export const Default: Story = {
args: {
children: <Text>Lorem ipsum dolor sit amet</Text>,
},
};
16 changes: 16 additions & 0 deletions src/components/SubSection/SubSection.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import type { CSSProperties, ReactNode } from "react";
import { twJoin } from "tailwind-merge";

export const SubSection = ({
children,
style,
className,
}: {
children: ReactNode;
style?: CSSProperties;
className?: string;
}) => (
<div className={twJoin("flex rounded bg-secondary-highlight p-4 text-accent-primary", className)} style={style}>
{children}
</div>
);
1 change: 1 addition & 0 deletions src/components/SubSection/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { SubSection } from "./SubSection";
1 change: 1 addition & 0 deletions src/components/SubSection/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { SubSection } from "./SubSection";
6 changes: 6 additions & 0 deletions src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ export * from "./components/Card";
export * from "./components/Toggle";
export * from "./components/List";
export * from "./components/Badge";
export * from "./components/SubSection";

export * from "./widgets/form/Form";
export * from "./widgets/form/NumberField";
Expand All @@ -26,7 +27,12 @@ export * from "./widgets/form/SelectField";
export * from "./widgets/form/HiddenField";
export * from "./widgets/form/hooks";

export * from "./widgets/new-design/AmountSubsection";
export * from "./widgets/new-design/FeesSection";
export * from "./widgets/new-design/PreviewModal";

export * from "./components/ListLegacy";

export { ScrollLocker } from "./context/Dialog.context";
export { useFormContext, useFormState, useWatch } from "react-hook-form";
export { SubSection } from "./components/SubSection";
62 changes: 62 additions & 0 deletions src/utils/helpers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import { Decimal } from "decimal.js-light";

export interface FormatCurrencyOptions<T extends string = string> {
prefix?: T;
precision?: number;
zeroDisplay?: string;
format?: Intl.NumberFormatOptions;
}

const defaultFormatOptions: Required<FormatCurrencyOptions> = {
prefix: "$",
precision: 2,
zeroDisplay: "-",
format: {
minimumFractionDigits: 2,
maximumFractionDigits: 2,
},
};

export function formatCurrency(currencyValue: number, options: FormatCurrencyOptions = {}): string {
const { prefix, precision, zeroDisplay, format } = {
...defaultFormatOptions,
...options,
};

if (currencyValue === 0) return zeroDisplay;

const formatted = currencyValue.toLocaleString("en", format ?? { maximumFractionDigits: precision });

return `${prefix}${formatted}`;
}

/**
* Converts token amount to formatted currency string
* @param amount The amount of token
* @param price The price of token in currency
* @param options Formatting options
*/
export function calculateTokenValueInCurrency(amount: number, price: number, options?: FormatCurrencyOptions): string {
const currencyValue = amount * price;
return formatCurrency(currencyValue, options);
}

/**
* Limits the number of decimal places of a given number to a specified maximum.
*
* @param value The original number that you want to limit the decimal places for.
* @param maxDecimals The maximum number of decimal places that the result should have.
* @returns The number rounded to the specified number of decimal places.
*
* @example
* maxDecimals(3.14159, 2); // returns 3.14
* maxDecimals(1.005, 2); // returns 1.01
* maxDecimals(10, 0); // returns 10
* maxDecimals(0.00010000, 8); // returns 0.0001
* maxDecimals(0.00010000, 8); // returns 0.0001
* maxDecimals(3.141, 3); // returns 3.141
* maxDecimals(3.149, 3); // returns 3.149
*/
export const maxDecimals = (value: number, maxDecimals: number, rm?: number): number => {
return new Decimal(value).toDecimalPlaces(maxDecimals, rm).toNumber();
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import type { Meta, StoryObj } from "@storybook/react";
import * as yup from "yup";

import { AmountSubsection } from "./AmountSubsection";
import { Form } from "@/widgets/form/Form";

const meta: Meta<typeof AmountSubsection> = {
component: AmountSubsection,
tags: ["autodocs"],
};

export default meta;

type Story = StoryObj<typeof meta>;

// Define a simple validation schema for the field used in this story
const schema = yup
.object()
.shape({
amount: yup.number().required().positive(),
})
.required();

export const Default: Story = {
args: {
fieldName: "amount",
currencyIcon: "https://via.placeholder.com/40",
currencyName: "BBN",
placeholder: "Enter Amount",
displayBalance: true,
balanceDetails: {
balance: 1234.56789,
symbol: "BBN",
price: 0.25,
displayUSD: true,
decimals: 8,
},
},
decorators: [
(Story) => (
<Form onChange={console.log} schema={schema}>
<Story />
</Form>
),
],
};
95 changes: 95 additions & 0 deletions src/widgets/new-design/AmountSubsection/AmountSubsection.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import { HiddenField } from "@/widgets/form/HiddenField";
import { SubSection } from "@/components/SubSection";
import { useFormContext, useWatch } from "react-hook-form";

import { calculateTokenValueInCurrency, maxDecimals } from "@/utils/helpers";

interface BalanceDetails {
balance: number | string;
symbol: string;
price?: number;
displayUSD?: boolean;
decimals?: number;
}

interface Props {
fieldName: string;
currencyIcon: string;
currencyName: string;
placeholder?: string;
displayBalance?: boolean;
balanceDetails?: BalanceDetails;
min?: string;
step?: string;
autoFocus?: boolean;
}

export const AmountSubsection = ({
fieldName,
currencyIcon,
currencyName,
displayBalance,
placeholder = "Enter Amount",
balanceDetails,
min = "0",
step = "any",
autoFocus = true,
}: Props) => {
const amount = useWatch({ name: fieldName, defaultValue: "" });
const { setValue } = useFormContext();

const amountValue = parseFloat((amount as string) || "0");
const amountUsd = calculateTokenValueInCurrency(amountValue, balanceDetails?.price ?? 0, {
zeroDisplay: "$0.00",
});

const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setValue(fieldName, e.target.value, {
shouldValidate: true,
shouldDirty: true,
shouldTouch: true,
});
};

const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.key === "ArrowUp" || e.key === "ArrowDown") {
e.preventDefault();
}
};

return (
<SubSection className="flex w-full flex-col content-center justify-between gap-4">
<div className="flex w-full flex-row content-center items-center justify-between font-normal">
<div className="flex items-center gap-2">
<img src={currencyIcon} alt={currencyName} className="h-10 max-h-[2.5rem] w-10 max-w-[2.5rem]" />
<div className="text-lg">{currencyName}</div>
</div>
<input
type="number"
value={amount ?? ""}
min={min}
step={step}
onChange={handleInputChange}
onKeyDown={handleKeyDown}
placeholder={placeholder}
autoFocus={autoFocus}
className="w-2/3 bg-transparent text-right text-lg outline-none"
/>
</div>
<HiddenField name={fieldName} defaultValue="" />

{balanceDetails && displayBalance ? (
<div className="flex w-full flex-row content-center justify-between text-sm">
<div>
Stakable:{" "}
<span className="cursor-default">
{maxDecimals(Number(balanceDetails.balance), balanceDetails.decimals ?? 8)}
</span>{" "}
{balanceDetails.symbol}
</div>
{balanceDetails.displayUSD && balanceDetails.price !== undefined && <div>{amountUsd} USD</div>}
</div>
) : null}
</SubSection>
);
};
1 change: 1 addition & 0 deletions src/widgets/new-design/AmountSubsection/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { AmountSubsection } from "./AmountSubsection";
20 changes: 20 additions & 0 deletions src/widgets/new-design/FeesSection/BBNFeeAmount.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { FeeItem } from "./FeeItem";

interface BBNFeeAmountProps {
amount: number | string;
coinSymbol: string;
hint?: string;
title?: string;
className?: string;
decimals?: number;
}

export function BBNFeeAmount({ amount, coinSymbol, hint, title, className, decimals = 5 }: BBNFeeAmountProps) {
const formattedAmount = typeof amount === "number" ? amount.toFixed(decimals) : amount;

return (
<FeeItem title={title ?? `${coinSymbol} Network Fee`} hint={hint} className={className}>
{formattedAmount} {coinSymbol}
</FeeItem>
);
}
28 changes: 28 additions & 0 deletions src/widgets/new-design/FeesSection/BTCFeeAmount.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { FeeItem } from "./FeeItem";

interface BTCFeeAmountProps {
amount: number | string;
coinSymbol: string;
hint?: string;
title?: string;
className?: string;
decimals?: number;
}

export function BTCFeeAmount({ amount, coinSymbol, hint, title, className, decimals = 8 }: BTCFeeAmountProps) {
const formattedAmount =
typeof amount === "number"
? amount === 0
? "0"
: (() => {
const str = amount.toFixed(decimals);
return str.replace(/0+$/, "").replace(/\.$/, "");
})()
: amount;

return (
<FeeItem title={title ?? `${coinSymbol} Network Fee`} hint={hint} className={className}>
{formattedAmount} {coinSymbol}
</FeeItem>
);
}
24 changes: 24 additions & 0 deletions src/widgets/new-design/FeesSection/BTCFeeRate.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { FeeItem } from "./FeeItem";
import { Button } from "../../../components/Button";
import { FaPen } from "react-icons/fa6";

interface BTCFeeRateProps {
value: number | string;
onEdit?: () => void;
title?: string;
className?: string;
}

export function BTCFeeRate({ value, onEdit, title = "Network Fee Rate", className }: BTCFeeRateProps) {
return (
<FeeItem title={title} className={className}>
<span>{value} sats/vB</span>

{onEdit && (
<Button size="small" variant="outlined" className="h-6 w-6 pl-1 text-secondary-strokeDark" onClick={onEdit}>
<FaPen size={16} className="text-secondary-strokeDark" />
</Button>
)}
</FeeItem>
);
}
Loading