diff --git a/.changeset/quick-ads-pretend.md b/.changeset/quick-ads-pretend.md new file mode 100644 index 000000000..2d65685da --- /dev/null +++ b/.changeset/quick-ads-pretend.md @@ -0,0 +1,6 @@ +--- +"@namehash/namekit-react": minor +"@namehash/ens-utils": minor +--- + +Create DisplayedPrice and DisplayedPriceConversion components + new price utilities diff --git a/apps/storybook.namekit.io/global.css b/apps/storybook.namekit.io/global.css index 5deb6578c..d44c17412 100644 --- a/apps/storybook.namekit.io/global.css +++ b/apps/storybook.namekit.io/global.css @@ -5,6 +5,5 @@ .colorful-text { font-weight: 700; - text-decoration: underline; color: lightgreen; } diff --git a/apps/storybook.namekit.io/stories/Namekit/CurrencySymbol.stories.tsx b/apps/storybook.namekit.io/stories/Namekit/CurrencySymbol.stories.tsx index 8469d547a..fc2fb7bdc 100644 --- a/apps/storybook.namekit.io/stories/Namekit/CurrencySymbol.stories.tsx +++ b/apps/storybook.namekit.io/stories/Namekit/CurrencySymbol.stories.tsx @@ -1,96 +1,104 @@ import { Currency } from "@namehash/ens-utils"; -import { CurrencySymbol } from "@namehash/namekit-react/client"; -import { CurrencySymbolSize } from "@namehash/namekit-react/client"; +import { + CurrencySymbol, + CurrencySymbology, +} from "@namehash/namekit-react/client"; +import { CurrencyIconSize } from "@namehash/namekit-react/client"; import type { Meta, StoryObj } from "@storybook/react"; -export const ETH: Story = { - args: { - currency: Currency.Eth, - size: CurrencySymbolSize.Large, - }, -}; -export const USD: Story = { - args: { - currency: Currency.Usd, - size: CurrencySymbolSize.Large, +const meta: Meta = { + component: CurrencySymbol, + title: "Namekit/CurrencySymbol", + argTypes: { + currency: { + options: Object.values(Currency), + control: { + labels: Object.keys(Currency), + type: "select", + }, + }, + iconSize: { + options: Object.values(CurrencyIconSize), + if: { arg: "symbology", eq: CurrencySymbology.Icon }, + control: { + labels: { + [CurrencyIconSize.Small]: "Small (16px)", + [CurrencyIconSize.Large]: "Large (20px)", + }, + type: "select", + }, + }, + className: { + if: { arg: "symbology", neq: CurrencySymbology.Icon }, + control: { + type: "text", + }, + }, + describeCurrencyInTooltip: { control: { type: "boolean" } }, + symbology: { + options: Object.keys(CurrencySymbology), + control: { type: "select" }, + }, }, -}; -export const USDC: Story = { args: { - currency: Currency.Usdc, - size: CurrencySymbolSize.Large, + iconSize: CurrencyIconSize.Small, + describeCurrencyInTooltip: false, + symbology: CurrencySymbology.TextSymbol, }, }; -export const WETH: Story = { + +export default meta; + +type Story = StoryObj; + +export const AsAnAcronym: Story = { args: { - currency: Currency.Weth, - size: CurrencySymbolSize.Large, + currency: Currency.Eth, + symbology: CurrencySymbology.Acronym, }, }; -export const DAI: Story = { +export const AsATextSymbol: Story = { args: { - currency: Currency.Dai, - size: CurrencySymbolSize.Large, + currency: Currency.Eth, + symbology: CurrencySymbology.TextSymbol, }, }; -export const SmallSize: Story = { +export const AsASmallIcon: Story = { args: { - size: CurrencySymbolSize.Small, currency: Currency.Eth, + iconSize: CurrencyIconSize.Small, + symbology: CurrencySymbology.Icon, }, }; -export const LargeSize: Story = { +export const AsALargeIcon: Story = { args: { - size: CurrencySymbolSize.Large, currency: Currency.Eth, + iconSize: CurrencyIconSize.Large, + symbology: CurrencySymbology.Icon, }, }; -export const WithCustomSymbolColor: Story = { +export const WithCustomIconColor: Story = { + argTypes: { + fill: { control: { type: "color" } }, + }, args: { - symbolFillColor: "#007bff", - size: CurrencySymbolSize.Large, currency: Currency.Eth, + symbology: CurrencySymbology.Icon, + fill: "#007bff", }, }; -export const ShowingTooltipDescription: Story = { +export const WithCustomFontStyle: Story = { + argTypes: { + className: { control: { type: "text" } }, + }, args: { - describeCurrencyInTooltip: true, - size: CurrencySymbolSize.Large, currency: Currency.Eth, + className: "nk-text-3xl colorful-text", }, }; -export const NotShowingTooltipDescription: Story = { +export const ShowingTooltipDescription: Story = { args: { - describeCurrencyInTooltip: false, - size: CurrencySymbolSize.Large, currency: Currency.Eth, + describeCurrencyInTooltip: true, }, }; - -const meta: Meta = { - component: CurrencySymbol, - title: "Namekit/CurrencySymbol", - argTypes: { - symbolFillColor: { control: "color" }, - currency: { - options: [ - Currency.Eth, - Currency.Usd, - Currency.Usdc, - Currency.Weth, - Currency.Dai, - ], - control: { type: "select" }, - }, - size: { - options: Object.keys(CurrencySymbolSize), - mapping: CurrencySymbolSize, - control: { type: "select" }, - }, - describeCurrencyInTooltip: { control: { type: "boolean" } }, - }, -}; - -export default meta; - -type Story = StoryObj; diff --git a/apps/storybook.namekit.io/stories/Namekit/DisplayedPrice.stories.tsx b/apps/storybook.namekit.io/stories/Namekit/DisplayedPrice.stories.tsx new file mode 100644 index 000000000..f4f56bdc0 --- /dev/null +++ b/apps/storybook.namekit.io/stories/Namekit/DisplayedPrice.stories.tsx @@ -0,0 +1,364 @@ +import { + CurrencySymbol, + DisplayedPrice, + CurrencySymbology, + PriceDisplaySize, + CurrencySymbolPosition, + CurrencyIconSize, +} from "@namehash/namekit-react/client"; +import type { Meta, StoryObj } from "@storybook/react"; +import { + convertCurrencyWithRates, + Currency, + numberAsPrice, + Price, + PriceCurrencyFormat, +} from "@namehash/ens-utils"; +import React, { useEffect, useState } from "react"; + +const getCurrencyIconSizeBasedOnPriceDisplaySize = ( + displaySize: PriceDisplaySize | undefined, +) => { + switch (displaySize) { + case PriceDisplaySize.Large: + return CurrencyIconSize.Large; + case PriceDisplaySize.Medium: + return CurrencyIconSize.Large; + case PriceDisplaySize.Small: + return CurrencyIconSize.Small; + case PriceDisplaySize.Micro: + return CurrencyIconSize.Small; + default: + return CurrencyIconSize.Large; + } +}; + +const meta: Meta = { + component: DisplayedPrice, + title: "Namekit/DisplayedPrice", + argTypes: { + /** + * `price` options definition for storybook's UI visitors: + * + * Storybook does not provide a straigh-forward way of customizing typed objects + * properties. Since we want to provide storie's docs visitors a way of testing + * multiple `Price` typed objects as `price` values of `DisplayedName` config, + * + * A set of examples are defined as `options` and `mapping` properties. The + * number of stories that are commonly used by applications was finger-picked to be + * these examples. If you want to further test and customize this component, please + * consider installing @namehash/namekit-react dependency and using it locally! + */ + price: { + options: [ + "Example With ETH price", + "Example With WETH price", + "Example With USD price", + "Example With USDC price", + "Example With DAI price", + "Example With GAS price", + ], + mapping: { + ["Example With ETH price"]: numberAsPrice(1, Currency.Eth), + ["Example With WETH price"]: numberAsPrice(1, Currency.Weth), + ["Example With USD price"]: numberAsPrice(1, Currency.Usd), + ["Example With USDC price"]: numberAsPrice(1, Currency.Usdc), + ["Example With DAI price"]: numberAsPrice(1, Currency.Dai), + ["Example With GAS price"]: numberAsPrice(1, Currency.Gas), + }, + }, + /** + * `symbol` options definition for storybook's UI visitors: + * + * Building JSX components out of the box 🧠 is not an ability all have, so, + * we provide a set of examples for `symbol` property. + * + * + * P.S.: For those that have the necessary abilities please note how you could set + * any text, any HTML element, or even React components you want as this prop's value! + * + * WhateverJSX: a simple JSX with a text saying "Whatever JSX you want" + * undefined: undefined value, which results in`@namehash/namekit-react`'s default symbology + * null: null value, which results in no symbol + */ + symbol: { + control: { + type: "select", + labels: { + WhateverJSX: "Whatever JSX you want", + undefined: "undefined (uses default symbology)", + null: "null", + }, + }, + options: ["WhateverJSX", "undefined", "null"], + mapping: { + WhateverJSX:

Whatever JSX you want

, + undefined: undefined, + null: null, + }, + }, + /** + * The possible `symbolPosition` values for `DisplayedPrice` component. + */ + symbolPosition: { + options: Object.keys(CurrencySymbolPosition), + mapping: CurrencySymbolPosition, + control: { + type: "select", + }, + }, + /** + * The possible `displaySize` values for `DisplayedPrice` component. + */ + displaySize: { + options: Object.keys(PriceDisplaySize), + mapping: PriceDisplaySize, + control: { + type: "select", + labels: { + [PriceDisplaySize.Large]: "Large (24px)", + [PriceDisplaySize.Medium]: "Medium (20px)", + [PriceDisplaySize.Small]: "Small (14px)", + [PriceDisplaySize.Micro]: "Micro (12px for mobile, 14px for desktop)", + }, + }, + }, + }, + args: { + symbolPosition: CurrencySymbolPosition.Left, + displaySize: PriceDisplaySize.Medium, + }, +}; + +declare global { + interface BigInt { + toJSON(): string; + } +} + +BigInt.prototype.toJSON = function () { + return `${this.toString()}n`; +}; + +export default meta; + +type Story = StoryObj; + +const defaultStoriesPrice = numberAsPrice(1, Currency.Eth); + +const exchangeRatesRecord = { + [Currency.Weth]: 2277.56570676, + [Currency.Eth]: 2277.56570676, + [Currency.Usd]: 1, + [Currency.Dai]: 1, + [Currency.Usdc]: 1, + [Currency.Gas]: 1, +}; + +export const LargeDisplaySize: Story = { + args: { + price: numberAsPrice(1, Currency.Eth), + displaySize: PriceDisplaySize.Large, + }, + render: (args, ctx) => { + /** + * Gather existing pre-defined `DisplayedPrice` stories + * configuration and gets `options` and `mapping` values + * in order to set the first element of `options` as default `price` + * (a `DisplayedPrice`) property value. + * + * This enhances storybook's storie's documentation UI (through a select dropdown, + * the story is now able to set one of the `options` as the default `option` of `select`). + */ + const defaultPriceArgTypesOptions = ctx.argTypes.price.options; + const defaultPriceArgTypesMapping = ctx.argTypes.price.mapping; + + let price: Price; + if (!!defaultPriceArgTypesOptions && defaultPriceArgTypesMapping) { + price = defaultPriceArgTypesMapping[defaultPriceArgTypesOptions[0]]; + } + + /** + * Below there is a conversion of the `price` value to the `args.price.currency` + * currency. This is done by using the `convertCurrencyWithRates` function from + * `@namehash/ens-utils` package. Why we do this? Because we want to show the + * `price` value in a different currency than the one it was defined, with + * the exchange rates defined in `exchangeRatesRecord`. + * + * Please refer to the definition of `price` argTypes to understand why + * we contraint the `price` value to be one of the `options` defined. + */ + const convertedPrice = convertCurrencyWithRates( + defaultStoriesPrice, + args.price.currency, + exchangeRatesRecord, + ); + + return ; + }, +}; +export const MediumDisplaySize: Story = { + args: { + price: numberAsPrice(2000, Currency.Usd), + displaySize: PriceDisplaySize.Medium, + }, +}; +export const SmallDisplaySize: Story = { + args: { + price: numberAsPrice(2000, Currency.Usd), + displaySize: PriceDisplaySize.Small, + }, +}; +export const MicroDisplaySize: Story = { + args: { + price: numberAsPrice(2000, Currency.Usd), + displaySize: PriceDisplaySize.Micro, + }, +}; +export const DefaultSymbol: Story = { + args: { + price: numberAsPrice(1, Currency.Eth), + symbolPosition: CurrencySymbolPosition.Left, + }, +}; +export const CustomSymbolUsingOurCurrencySymbol: Story = { + args: { + price: numberAsPrice(1, Currency.Eth), + symbol: ( + + ), + }, + render: (args) => { + const [currency, setCurrency] = useState(args.price.currency); + const [currencySize, setCurrencySize] = useState( + CurrencyIconSize.Large, + ); + + useEffect(() => { + setCurrencySize( + getCurrencyIconSizeBasedOnPriceDisplaySize(args.displaySize), + ); + }, [args.displaySize]); + + useEffect(() => { + setCurrency(args.price.currency); + }, [args.price.currency]); + + return ( + + } + /> + ); + }, +}; +export const CustomSymbolUsingCustomAcronymSymbology: Story = { + args: { + price: numberAsPrice(1, Currency.Eth), + symbol: ( + + ), + }, +}; +export const CustomSymbolDoingWhateverYouWant: Story = { + args: { + price: numberAsPrice(1, Currency.Eth), + symbol:

Whatever you want

, + }, +}; +export const CurrencyWithSymbolAtTheRight: Story = { + args: { + price: numberAsPrice(1, Currency.Eth), + symbolPosition: CurrencySymbolPosition.Right, + }, +}; +export const OverflowDisplayPriceWithCustomCurrencyIcon: Story = { + args: { + price: numberAsPrice(100000000, Currency.Usd), + displaySize: PriceDisplaySize.Small, + }, + render: (args) => { + const [currency, setCurrency] = useState(args.price.currency); + const [currencySize, setCurrencySize] = useState(); + + useEffect(() => { + setCurrencySize( + getCurrencyIconSizeBasedOnPriceDisplaySize(args.displaySize), + ); + }, [args.displaySize]); + + useEffect(() => { + setCurrency(args.price.currency); + }, [args.price.currency]); + + return ( + + ) + } + /> + ); + }, +}; +export const UnderflowDisplayPriceWithCustomCurrencyIcon: Story = { + args: { + price: numberAsPrice(0.01, Currency.Usdc), + displaySize: PriceDisplaySize.Small, + }, + render: (args) => { + const [currency, setCurrency] = useState(args.price.currency); + const [currencySize, setCurrencySize] = useState(); + + useEffect(() => { + setCurrencySize( + getCurrencyIconSizeBasedOnPriceDisplaySize(args.displaySize), + ); + }, [args.displaySize]); + + useEffect(() => { + setCurrency(args.price.currency); + }, [args.price.currency]); + + return ( + + ) + } + /> + ); + }, +}; diff --git a/apps/storybook.namekit.io/stories/Namekit/DisplayedPriceConversion.stories.tsx b/apps/storybook.namekit.io/stories/Namekit/DisplayedPriceConversion.stories.tsx new file mode 100644 index 000000000..aab64baf6 --- /dev/null +++ b/apps/storybook.namekit.io/stories/Namekit/DisplayedPriceConversion.stories.tsx @@ -0,0 +1,147 @@ +import { Currency, numberAsPrice } from "@namehash/ens-utils"; +import { + CurrencySymbol, + CurrencySymbology, + CurrencySymbolPosition, + CurrencyIconSize, + DisplayedPriceConversion, + PriceDisplaySize, +} from "@namehash/namekit-react/client"; +import type { Meta, StoryObj } from "@storybook/react"; +import React, { useEffect, useState } from "react"; + +const meta: Meta = { + component: DisplayedPriceConversion, + title: "Namekit/DisplayedPriceConversion", + argTypes: { + /** + * In this story the default price is OneETH + * + * This was done so we can ensure that price conversions + * are nicely set. Please refer to the `convertedPrice` + * prop to see the converted price options. + */ + price: { + options: ["OneETH"], + mapping: { + OneETH: numberAsPrice(1, Currency.Eth), + }, + }, + /** + * Below values give visitors the ability to + * convert OneETH to any of the currencies below: + */ + convertedPrice: { + options: ["ToUSD", "ToUSDC", "ToWETH", "ToDAI"], + mapping: { + ToUSD: numberAsPrice(2000, Currency.Usd), + ToUSDC: numberAsPrice(2000, Currency.Usdc), + ToWETH: numberAsPrice(1, Currency.Weth), + ToDAI: numberAsPrice(2000, Currency.Dai), + }, + }, + symbol: { + control: { + type: "select", + labels: { + WhateverJSX: "Whatever JSX you want", + undefined: "undefined (uses default symbology)", + null: "null", + }, + }, + options: ["WhateverJSX", "undefined", "null"], + mapping: { + WhateverJSX:

Whatever JSX you want

, + undefined: undefined, + null: null, + }, + }, + convertedPriceSymbology: { + options: Object.keys(CurrencySymbology), + control: { type: "select" }, + }, + /** + * The possible `displaySize` values for `DisplayedPrice` component. + */ + displaySize: { + options: Object.keys(PriceDisplaySize), + mapping: PriceDisplaySize, + control: { + type: "select", + labels: { + [PriceDisplaySize.Large]: "Large (24px)", + [PriceDisplaySize.Medium]: "Medium (20px)", + [PriceDisplaySize.Small]: "Small (14px)", + [PriceDisplaySize.Micro]: "Micro (12px for mobile, 14px for desktop)", + }, + }, + }, + /** + * The possible `symbolPosition` values for `DisplayedPrice` component. + */ + symbolPosition: { + options: Object.keys(CurrencySymbolPosition), + mapping: CurrencySymbolPosition, + control: { + type: "select", + }, + }, + }, + args: { + price: numberAsPrice(1, Currency.Eth), + convertedPrice: numberAsPrice(2000, Currency.Usd), + symbol: undefined, + convertedPriceSymbology: CurrencySymbology.TextSymbol, + displaySize: PriceDisplaySize.Medium, + symbolPosition: CurrencySymbolPosition.Left, + }, +}; + +declare global { + interface BigInt { + toJSON(): string; + } +} + +BigInt.prototype.toJSON = function () { + return `${this.toString()}n`; +}; + +export default meta; + +type Story = StoryObj; + +export const ConvertedPriceWithSymbolSymbology: Story = { + args: { + price: numberAsPrice(1, Currency.Eth), + convertedPrice: numberAsPrice(2000, Currency.Usd), + convertedPriceSymbology: CurrencySymbology.TextSymbol, + displaySize: PriceDisplaySize.Medium, + symbolPosition: CurrencySymbolPosition.Left, + }, +}; +export const ConvertedPriceWithAcronymSymbology: Story = { + args: { + price: numberAsPrice(1, Currency.Eth), + convertedPrice: numberAsPrice(2000, Currency.Usd), + convertedPriceSymbology: CurrencySymbology.Acronym, + displaySize: PriceDisplaySize.Medium, + symbolPosition: CurrencySymbolPosition.Left, + }, +}; +export const PriceWithIconSymbology: Story = { + args: { + price: numberAsPrice(1, Currency.Eth), + convertedPrice: numberAsPrice(2000, Currency.Usd), + symbol: ( + + ), + convertedPriceSymbology: CurrencySymbology.Icon, + displaySize: PriceDisplaySize.Medium, + symbolPosition: CurrencySymbolPosition.Left, + }, +}; diff --git a/packages/ens-utils/src/price.ts b/packages/ens-utils/src/price.ts index 75ac307d2..928a1ae29 100644 --- a/packages/ens-utils/src/price.ts +++ b/packages/ens-utils/src/price.ts @@ -1,8 +1,11 @@ -import { Currency, PriceCurrencyFormat, parseStringToCurrency } from "./currency"; +import { + Currency, + PriceCurrencyFormat, + parseStringToCurrency, +} from "./currency"; import { approxScaleBigInt, stringToBigInt } from "./number"; export interface Price { - // TODO: consider adding a constraint where value is never negative /** * The value of the price. This is a BigInt to avoid floating point math issues when working with prices. @@ -23,19 +26,21 @@ export interface ExchangeRates extends Partial> {} /** * Builds a Price object. * @param value the value of the price. This is a BigInt to avoid floating point math issues when working with prices. - * For example, a price of 1.23 USD would be represented as 123n with a currency of USD. - * Note that the value is always in the smallest unit of the currency (e.g. cents for USD, wei for ETH). - * See the CurrencyConfig for the related currency for the number of decimals to use when converting the value to a human-readable format. - * @param currency - * @returns + * For example, a price of 1.23 USD would be represented as 123n with a currency of USD. + * Note that the value is always in the smallest unit of the currency (e.g. cents for USD, wei for ETH). + * See the CurrencyConfig for the related currency for the number of decimals to use when converting the value to a human-readable format. + * @param currency + * @returns */ -export const buildPrice = (value: bigint | string, currency: Currency | string): Price => { - - let priceValue : bigint; - let priceCurrency : Currency; +export const buildPrice = ( + value: bigint | string, + currency: Currency | string, +): Price => { + let priceValue: bigint; + let priceCurrency: Currency; if (typeof value === "string") { - priceValue = stringToBigInt(value) + priceValue = stringToBigInt(value); } else { priceValue = value; } @@ -47,7 +52,7 @@ export const buildPrice = (value: bigint | string, currency: Currency | string): } return { value: priceValue, currency: priceCurrency }; -} +}; export const priceAsNumber = (price: Price): number => { return ( @@ -61,12 +66,12 @@ export const numberAsPrice = (number: number, currency: Currency): Price => { // Fix the number's displayed decimals (e.g. from 0.00001 to 0.00001) const numberWithCorrectCurrencyDecimals = Number( - number.toFixed(currencyDecimals) + number.toFixed(currencyDecimals), ); // Remove the decimals from the number (e.g. from 0.00001 to 1) const numberWithoutDecimals = Number( - numberWithCorrectCurrencyDecimals * 10 ** currencyDecimals + numberWithCorrectCurrencyDecimals * 10 ** currencyDecimals, ).toFixed(0); /* @@ -75,7 +80,9 @@ export const numberAsPrice = (number: number, currency: Currency): Price => { */ const numberReadyToBeConvertedToBigInt = Number(numberWithoutDecimals); - // Safely convert the number to BigInt + /* + Safely convert the number to BigInt + */ return { value: BigInt(numberReadyToBeConvertedToBigInt), currency, @@ -102,7 +109,7 @@ export const addPrices = (prices: Array): Price => { export const subtractPrices = (price1: Price, price2: Price): Price => { if (price1.currency !== price2.currency) { throw new Error( - `Cannot subtract price of currency ${price1.currency} to price of currency ${price2.currency}` + `Cannot subtract price of currency ${price1.currency} to price of currency ${price2.currency}`, ); } else { return { @@ -150,7 +157,7 @@ export const formattedPrice = ({ ) { // If formatted number is 0.0 but real 'value' is not 0, then we show the Underflow price formattedAmount = String( - PriceCurrencyFormat[price.currency].MinDisplayValue + PriceCurrencyFormat[price.currency].MinDisplayValue, ); } else if (wouldDisplayAsZero && price.value == 0n) { // But if the real 'value' is really 0, then we show 0.00 (in the correct number of Display Decimals) @@ -158,7 +165,7 @@ export const formattedPrice = ({ formattedAmount = prefix.padEnd( Number(PriceCurrencyFormat[price.currency].DisplayDecimals) + prefix.length, - "0" + "0", ); } @@ -168,10 +175,10 @@ export const formattedPrice = ({ formattedAmount = displayNumber.toLocaleString("en-US", { minimumFractionDigits: Number( - PriceCurrencyFormat[price.currency].DisplayDecimals + PriceCurrencyFormat[price.currency].DisplayDecimals, ), maximumFractionDigits: Number( - PriceCurrencyFormat[price.currency].DisplayDecimals + PriceCurrencyFormat[price.currency].DisplayDecimals, ), }); @@ -197,7 +204,7 @@ export const formattedPrice = ({ export const approxScalePrice = ( price: Price, scaleFactor: number, - digitsOfPrecision = 20n + digitsOfPrecision = 20n, ): Price => { return { value: approxScaleBigInt(price.value, scaleFactor, digitsOfPrecision), @@ -208,13 +215,13 @@ export const approxScalePrice = ( export const convertCurrencyWithRates = ( fromPrice: Price, toCurrency: Currency, - exchangeRates: ExchangeRates + exchangeRates: ExchangeRates, ): Price => { if (typeof exchangeRates[toCurrency] === "undefined") { throw new Error(`Exchange rate for currency ${toCurrency} not found`); } else if (typeof exchangeRates[fromPrice.currency] === "undefined") { throw new Error( - `Exchange rate for currency ${fromPrice.currency} not found` + `Exchange rate for currency ${fromPrice.currency} not found`, ); } diff --git a/packages/namekit-react/src/client.ts b/packages/namekit-react/src/client.ts index 5ec1faf8a..b15e09032 100644 --- a/packages/namekit-react/src/client.ts +++ b/packages/namekit-react/src/client.ts @@ -4,6 +4,13 @@ import "./styles.css"; export { Tooltip } from "./components/Tooltip"; export { CurrencySymbol, - CurrencySymbolSize, -} from "./components/CurrencySymbol/CurrencySymbol"; + CurrencySymbology, + CurrencyIconSize, +} from "./components/CurrencySymbol"; +export { + DisplayedPrice, + PriceDisplaySize, + CurrencySymbolPosition, +} from "./components/DisplayedPrice"; +export { DisplayedPriceConversion } from "./components/DisplayedPriceConversion"; export { TruncatedText } from "./components/TruncatedText"; diff --git a/packages/namekit-react/src/components/CurrencySymbol.tsx b/packages/namekit-react/src/components/CurrencySymbol.tsx new file mode 100644 index 000000000..b2b68659c --- /dev/null +++ b/packages/namekit-react/src/components/CurrencySymbol.tsx @@ -0,0 +1,246 @@ +import { PriceCurrencyFormat, Currency } from "@namehash/ens-utils"; +import { Tooltip } from "./Tooltip"; + +import { UsdcIcon } from "./icons/UsdcIcon"; +import { WethIcon } from "./icons/WethIcon"; +import { EthIcon } from "./icons/EthIcon"; +import { DaiIcon } from "./icons/DaiIcon"; +import { GasIcon } from "./icons/GasIcon"; + +import React from "react"; + +/** + * The size of the `CurrencySymbol` icon. + * This is defined as a mapping from a `CurrencyIconSize` to a number. + * This number represents the size (width and height) that the icon for a `CurrencySymbol` should be displayed at, in pixels. + */ +export const CurrencyIconSize = { + Small: 16, + Large: 20, +} as const; +export type CurrencyIconSize = + (typeof CurrencyIconSize)[keyof typeof CurrencyIconSize]; + +export const CurrencySymbology = { + /** + * The symbol displayed for `Currency` will be its acronym as text. (e.g. "USD"). + */ + Acronym: "Acronym", + /** + * The symbol displayed for `Currency` will be its text-based symbol (e.g. "$"). + */ + TextSymbol: "TextSymbol", + /** + * The symbol displayed for `Currency` will be its graphical icon (e.g. `EthIcon` that renders a SVG). If `Currency` is `Currency.Usd` then it will render as `CurrencySymbology.TextSymbol` instead. + */ + Icon: "Icon", +} as const; +export type CurrencySymbology = + (typeof CurrencySymbology)[keyof typeof CurrencySymbology]; + +interface CurrencySymbolProps extends React.ComponentProps { + /** + * The `Currency` to display the symbol for. + */ + currency: Currency; + + /** + * The size of the `CurrencySymbol` icon. + * + * Defaults to `CurrencyIconSize.Small`. + * Only applicable when `symbology` is `CurrencySymbology.Icon`. + */ + iconSize?: CurrencyIconSize; + + /** + * Classes applied to text-based symbologies (e.g. className="myCustomClassForTextSize") + * + * Defaults to "". + * Useful when `symbology` is `CurrencySymbology.TextSymbol` or `CurrencySymbology.Acronym`. + */ + className?: string; + + /** + * If `true`, hovering over the `CurrencySymbol` will display the + * name of `currency` in a `Tooltip`. If `false` then the `CurrencySymbol` + * won't have any `Tooltip`. + * + * Defaults to `false`. + */ + describeCurrencyInTooltip?: boolean; + + /** + * The symbology to use when displaying `currency`. + * + * Defaults to `CurrencySymbology.TextSymbol`. + */ + symbology?: CurrencySymbology; +} + +/** + * @param currency: Currency. The currency to get the Icon for (e.g. Currency.Eth) + * @param iconSize: CurrencyIconSize. The Icon size (width and height), in pixels. + * @param className: string. The classes to apply to the Icon. + */ +const getCurrencyIcon = ({ + currency, + iconSize = CurrencyIconSize.Small, + className = "", + ...props +}: { + currency: Currency; + iconSize: CurrencyIconSize; + className?: string; +}): JSX.Element => { + let symbology = <>; + + switch (currency) { + case Currency.Usd: + /** + * USD is always displayed as a text symbol, never being represented as a SVG icon. + */ + return ( + + ); + case Currency.Usdc: + symbology = ( + + ); + break; + case Currency.Dai: + symbology = ( + + ); + break; + case Currency.Weth: + symbology = ( + + ); + break; + case Currency.Eth: + symbology = ( + + ); + break; + case Currency.Gas: + symbology = ( + + ); + break; + } + + return ( + + {symbology} + + ); +}; + +/** + * Returns a JSX.Element containing the currency's symbol, acronym or icon. + * @param currency: Currency. The currency to get the symbology for (e.g. Currency.Eth) + * @param symbology: CurrencySymbology. The symbology to use (e.g. CurrencySymbology.TextSymbol) + * @param iconSize: CurrencyIconSize. The size to use for Icon Symbology. + * For other symbologies it is ignored. We suggest you use props to define + * other symbologies sizes, as these are not SVGs but texts, instead. (e.g. className="myCustomClassForTextSize") + * @param className: string. The classes to apply to the symbology. + * @returns: JSX.Element. The currency's symbol, acronym or icon inside a JSX.Element where all extra props will be applied. + */ +const getCurrencySymbology = ({ + currency, + symbology = CurrencySymbology.TextSymbol, + iconSize = CurrencyIconSize.Small, + className = "", + ...props +}: { + currency: Currency; + symbology: CurrencySymbology; + iconSize?: CurrencyIconSize; + className?: string; +}): JSX.Element => { + switch (symbology) { + case CurrencySymbology.Acronym: + return ( +

+ {PriceCurrencyFormat[currency].Acronym} +

+ ); + case CurrencySymbology.TextSymbol: + return ( +

+ {PriceCurrencyFormat[currency].Symbol} +

+ ); + case CurrencySymbology.Icon: + return getCurrencyIcon({ currency, iconSize, className, ...props }); + } +}; + +export const CurrencySymbol = ({ + currency, + iconSize = CurrencyIconSize.Small, + describeCurrencyInTooltip = false, + symbology = CurrencySymbology.TextSymbol, + className = "", + ...props +}: CurrencySymbolProps): JSX.Element => { + const symbologyToDisplay = ( + <> + {getCurrencySymbology({ + currency, + iconSize, + symbology, + className, + ...props, + })} + + ); + + if (!describeCurrencyInTooltip) return symbologyToDisplay; + + return ( + + <>{PriceCurrencyFormat[currency].ExtendedCurrencyNameSingular} + + ); +}; diff --git a/packages/namekit-react/src/components/CurrencySymbol/CurrencySymbol.tsx b/packages/namekit-react/src/components/CurrencySymbol/CurrencySymbol.tsx deleted file mode 100644 index 9a07dd429..000000000 --- a/packages/namekit-react/src/components/CurrencySymbol/CurrencySymbol.tsx +++ /dev/null @@ -1,90 +0,0 @@ -import { PriceCurrencyFormat, Currency } from "@namehash/ens-utils"; -import { Tooltip } from "../Tooltip"; - -import { UsdcSymbol } from "./UsdcSymbol"; -import { WethSymbol } from "./WethSymbol"; -import { EthSymbol } from "./EthSymbol"; -import { DaiSymbol } from "./DaiSymbol"; -import React from "react"; - -export enum CurrencySymbolSize { - Small = "nk-w-4", - Large = "nk-w-5", -} - -interface CurrencySymbolProps { - /** - * The `Currency` to display the symbol for. - */ - currency: Currency; - - /** - * The size of the `CurrencySymbol`. - * - * Defaults to `CurrencySymbolSize.Small`. - */ - size: CurrencySymbolSize; - - /** - * If `true`, hovering over the `CurrencySymbol` will display the - * name of `currency` in a `Tooltip`. If `false` then the `CurrencySymbol` - * won't have any `Tooltip`. - * - * Defaults to `true`. - */ - describeCurrencyInTooltip: boolean; - - /** - * Optional. Defines a custom color for the `CurrencySymbol` that overrides - * the default symbol color for `currency`. - * - * If defined, must be formatted as a hex color code. - * - * If undefined, defaults to the default symbol color for `currency`. - */ - symbolFillColor?: string; -} - -export const CurrencySymbol = ({ - currency, - size = CurrencySymbolSize.Small, - describeCurrencyInTooltip = true, - symbolFillColor = undefined, -}: CurrencySymbolProps) => { - let symbol: JSX.Element; - - switch (currency) { - case Currency.Usd: - symbol = ( -

- $ -

- ); - break; - case Currency.Usdc: - symbol = ; - break; - case Currency.Dai: - symbol = ; - break; - case Currency.Weth: - symbol = ; - break; - case Currency.Eth: - symbol = ; - break; - default: - // TODO: We haven't created symbols for `Currency.Gas` yet. - throw new Error( - `Error creating CurrencySymbol: unsupported Currency: "${currency}".`, - ); - } - - if (!describeCurrencyInTooltip) return symbol; - - return ( - - <>{PriceCurrencyFormat[currency].ExtendedCurrencyNameSingular} - - ); -}; diff --git a/packages/namekit-react/src/components/DisplayedPrice.tsx b/packages/namekit-react/src/components/DisplayedPrice.tsx new file mode 100644 index 000000000..451cca0ba --- /dev/null +++ b/packages/namekit-react/src/components/DisplayedPrice.tsx @@ -0,0 +1,86 @@ +import { type Price, formattedPrice } from "@namehash/ens-utils"; +import React from "react"; +import cc from "classcat"; +import { CurrencySymbol } from "./CurrencySymbol"; + +export const PriceDisplaySize = { + Micro: "nk-text-[12px] md:nk-text-[14px] nk-font-normal", + Small: "nk-text-[14px] nk-font-semibold", + Medium: "nk-text-[20px] nk-font-semibold", + Large: "nk-text-[24px] nk-font-bold", +} as const; +export type PriceDisplaySize = + (typeof PriceDisplaySize)[keyof typeof PriceDisplaySize]; + +export const CurrencySymbolPosition = { + /** + * Display the currency symbol to the left of the price. (e.g. $1.00) + */ + Left: "nk-mr-1.5", + /** + * Display the currency symbol to the right of the price. (e.g. 1.00$) + */ + Right: "nk-ml-1.5", +} as const; +export type CurrencySymbolPosition = + (typeof CurrencySymbolPosition)[keyof typeof CurrencySymbolPosition]; + +export interface DisplayedPriceProps { + /** + * The price value to be displayed + * @example { currency: Currency.Eth, value: 1000000000000000000n } + */ + price: Price; + /** + * The symbol for the `Currency` of `price`. to display alongside the `Price` value. + * + * If `symbol` is `undefined`: a default `CurrencySymbol` is displayed alongside the `Price` value. + * If `symbol` is `null`: no symbol is displayed alongside the `Price` value. + * Else: the custom `symbol` is displayed alongside the `Price` value. + */ + symbol?: React.ReactNode | null; + /** + * The position of the currency symbol relative to the price. + * Defaults to `CurrencySymbolPosition.Left`. + * + * If `CurrencySymbolPosition.Left`, the currency symbol will be displayed to the left of the price. + * If `CurrencySymbolPosition.Right`, the currency symbol will be displayed to the right of the price. + */ + symbolPosition?: CurrencySymbolPosition; + displaySize?: PriceDisplaySize; +} + +export const DisplayedPrice = ({ + price, + symbol, + symbolPosition = CurrencySymbolPosition.Left, + displaySize = PriceDisplaySize.Small, +}: DisplayedPriceProps) => { + const displayDefaultSymbol = symbol === undefined; + + const symbolToDisplay = displayDefaultSymbol ? ( + + ) : ( + symbol + ); + + const displayedPrice = ( +
+ {symbolPosition === CurrencySymbolPosition.Left && symbolToDisplay} +

{formattedPrice({ price })}

+ {symbolPosition === CurrencySymbolPosition.Right && symbolToDisplay} +
+ ); + + return displayedPrice; +}; diff --git a/packages/namekit-react/src/components/DisplayedPriceConversion.tsx b/packages/namekit-react/src/components/DisplayedPriceConversion.tsx new file mode 100644 index 000000000..9c582a934 --- /dev/null +++ b/packages/namekit-react/src/components/DisplayedPriceConversion.tsx @@ -0,0 +1,40 @@ +import { DisplayedPrice, DisplayedPriceProps } from "./DisplayedPrice"; +import { Price } from "@namehash/ens-utils"; +import { Tooltip } from "./Tooltip"; +import React from "react"; +import { CurrencySymbol, CurrencySymbology } from "./CurrencySymbol"; + +interface DisplayedPriceConversionProps extends DisplayedPriceProps { + convertedPrice: Price; + convertedPriceSymbology: CurrencySymbology; +} + +export const DisplayedPriceConversion = ({ + convertedPriceSymbology = CurrencySymbology.TextSymbol, + convertedPrice, + ...props +}: DisplayedPriceConversionProps) => { + return ( + + } + > + + } + /> + + ); +}; diff --git a/packages/namekit-react/src/components/CurrencySymbol/DaiSymbol.tsx b/packages/namekit-react/src/components/icons/DaiIcon.tsx similarity index 94% rename from packages/namekit-react/src/components/CurrencySymbol/DaiSymbol.tsx rename to packages/namekit-react/src/components/icons/DaiIcon.tsx index 7733d7144..c77c83d82 100644 --- a/packages/namekit-react/src/components/CurrencySymbol/DaiSymbol.tsx +++ b/packages/namekit-react/src/components/icons/DaiIcon.tsx @@ -2,7 +2,7 @@ import React, { type SVGProps } from "react"; const DAI_DEFAULT_COLOR = "#F5AC37"; -export const DaiSymbol = (props: SVGProps) => { +export const DaiIcon = (props: SVGProps) => { return ( ) => { - + diff --git a/packages/namekit-react/src/components/CurrencySymbol/EthSymbol.tsx b/packages/namekit-react/src/components/icons/EthIcon.tsx similarity index 91% rename from packages/namekit-react/src/components/CurrencySymbol/EthSymbol.tsx rename to packages/namekit-react/src/components/icons/EthIcon.tsx index f2ea88093..c627a6b30 100644 --- a/packages/namekit-react/src/components/CurrencySymbol/EthSymbol.tsx +++ b/packages/namekit-react/src/components/icons/EthIcon.tsx @@ -2,12 +2,12 @@ import React, { type SVGProps } from "react"; const ETH_DEFAULT_COLOR = "#272727"; -export const EthSymbol = (props: SVGProps) => { +export const EthIcon = (props: SVGProps) => { return ( ) => { + return ( + + + + + + ); +}; diff --git a/packages/namekit-react/src/components/CurrencySymbol/UsdcSymbol.tsx b/packages/namekit-react/src/components/icons/UsdcIcon.tsx similarity index 93% rename from packages/namekit-react/src/components/CurrencySymbol/UsdcSymbol.tsx rename to packages/namekit-react/src/components/icons/UsdcIcon.tsx index ad305aa76..c52815b1c 100644 --- a/packages/namekit-react/src/components/CurrencySymbol/UsdcSymbol.tsx +++ b/packages/namekit-react/src/components/icons/UsdcIcon.tsx @@ -2,7 +2,7 @@ import React, { type SVGProps } from "react"; const USDC_DEFAULT_COLOR = "#2775CA"; -export const UsdcSymbol = (props: SVGProps) => { +export const UsdcIcon = (props: SVGProps) => { return ( ) => { - + diff --git a/packages/namekit-react/src/components/CurrencySymbol/WethSymbol.tsx b/packages/namekit-react/src/components/icons/WethIcon.tsx similarity index 96% rename from packages/namekit-react/src/components/CurrencySymbol/WethSymbol.tsx rename to packages/namekit-react/src/components/icons/WethIcon.tsx index 481d253f7..3ac360741 100644 --- a/packages/namekit-react/src/components/CurrencySymbol/WethSymbol.tsx +++ b/packages/namekit-react/src/components/icons/WethIcon.tsx @@ -18,7 +18,7 @@ const WETH_DEFAULT_COLORS = [ "#DA3979", ]; -export const WethSymbol = (props: SVGProps) => { +export const WethIcon = (props: SVGProps) => { return (