diff --git a/public/widget/widget-execution-dark.png b/public/widget/widget-execution-dark.png new file mode 100644 index 000000000..f12429d0b Binary files /dev/null and b/public/widget/widget-execution-dark.png differ diff --git a/public/widget/widget-execution-light.png b/public/widget/widget-execution-light.png new file mode 100644 index 000000000..245167627 Binary files /dev/null and b/public/widget/widget-execution-light.png differ diff --git a/public/widget/widget-quotes-dark.png b/public/widget/widget-quotes-dark.png new file mode 100644 index 000000000..3ed1ece64 Binary files /dev/null and b/public/widget/widget-quotes-dark.png differ diff --git a/public/widget/widget-quotes-light.png b/public/widget/widget-quotes-light.png new file mode 100644 index 000000000..ee3dbbdf4 Binary files /dev/null and b/public/widget/widget-quotes-light.png differ diff --git a/public/widget/widget-review-bridge-dark.png b/public/widget/widget-review-bridge-dark.png new file mode 100644 index 000000000..854bc4c54 Binary files /dev/null and b/public/widget/widget-review-bridge-dark.png differ diff --git a/public/widget/widget-review-bridge-light.png b/public/widget/widget-review-bridge-light.png new file mode 100644 index 000000000..f3549a6bc Binary files /dev/null and b/public/widget/widget-review-bridge-light.png differ diff --git a/public/widget/widget-selection-dark.png b/public/widget/widget-selection-dark.png new file mode 100644 index 000000000..ebe14a7d6 Binary files /dev/null and b/public/widget/widget-selection-dark.png differ diff --git a/public/widget/widget-selection-light.png b/public/widget/widget-selection-light.png new file mode 100644 index 000000000..9762f9a8a Binary files /dev/null and b/public/widget/widget-selection-light.png differ diff --git a/public/widget/widget-success-dark.png b/public/widget/widget-success-dark.png new file mode 100644 index 000000000..8fa739505 Binary files /dev/null and b/public/widget/widget-success-dark.png differ diff --git a/public/widget/widget-success-light.png b/public/widget/widget-success-light.png new file mode 100644 index 000000000..03402ecf8 Binary files /dev/null and b/public/widget/widget-success-light.png differ diff --git a/src/app/api/widget-execution/route.tsx b/src/app/api/widget-execution/route.tsx new file mode 100644 index 000000000..820554fdb --- /dev/null +++ b/src/app/api/widget-execution/route.tsx @@ -0,0 +1,102 @@ +/* eslint-disable @next/next/no-img-element */ + +/** + * Image Generation of Widget for SEO pages + * Step 4 - Route execution + * + * Example: + * ``` + * http://localhost:3000/api/widget-execution?fromToken=0x0000000000000000000000000000000000000000&fromChainId=137&toToken=0x0000000000000000000000000000000000000000&toChainId=42161&amount=10&&theme=light&isSwap=true + * ``` + * + * @typedef {Object} SearchParams + * @property {string} fromToken - The token address to send from. + * @property {number} fromChainId - The chain ID to send from. + * @property {string} toToken - The token address to send to. + * @property {number} toChainId - The chain ID to send to. + * @property {number} amount - The amount of tokens. + * @property {number} [amountUSD] - The USD equivalent amount (optional). + * @property {boolean} [isSwap] - True if transaction is a swap, default and false if transaction is a bridge (optional). + * @property {'light'|'dark'} [theme] - The theme for the widget (optional). + * @property {'from'|'to'|'amount'} [highlighted] - The highlighted element (optional). + * + */ + +import type { ChainId } from '@lifi/sdk'; +import { ImageResponse } from 'next/og'; +import type { CSSProperties } from 'react'; +import type { HighlightedAreas } from 'src/components/ImageGeneration/ImageGeneration.types'; +import { imageResponseOptions } from 'src/components/ImageGeneration/imageResponseOptions'; +import { imageFrameStyles } from 'src/components/ImageGeneration/style'; +import WidgetExecutionImage from 'src/components/ImageGeneration/WidgetExecutionImage'; +import { fetchChainData } from 'src/utils/image-generation/fetchChainData'; +import { fetchTokenData } from 'src/utils/image-generation/fetchTokenData'; +import { parseSearchParams } from 'src/utils/image-generation/parseSearchParams'; + +const WIDGET_IMAGE_WIDTH = 416; +const WIDGET_IMAGE_HEIGHT = 432; +const WIDGET_IMAGE_SCALING_FACTOR = 2; + +export async function GET(request: Request) { + const { + fromChainId, + toChainId, + fromToken, + toToken, + isSwap, + theme, + amount, + highlighted, + } = parseSearchParams(request.url); + + // Fetch data asynchronously before rendering + const fromTokenData = await fetchTokenData(fromChainId, fromToken); + const toTokenData = await fetchTokenData(toChainId, toToken); + const fromChain = await fetchChainData(fromChainId as unknown as ChainId); + const toChain = await fetchChainData(toChainId as unknown as ChainId); + + const options = await imageResponseOptions({ + width: WIDGET_IMAGE_WIDTH, + height: WIDGET_IMAGE_HEIGHT, + scalingFactor: WIDGET_IMAGE_SCALING_FACTOR, + }); + + const imageFrameStyle = imageFrameStyles({ + width: WIDGET_IMAGE_WIDTH, + height: WIDGET_IMAGE_HEIGHT, + scalingFactor: WIDGET_IMAGE_SCALING_FACTOR, + }) as CSSProperties; + + const imageStyle = imageFrameStyles({ + width: WIDGET_IMAGE_WIDTH, + height: WIDGET_IMAGE_HEIGHT, + scalingFactor: WIDGET_IMAGE_SCALING_FACTOR, + }) as CSSProperties; + + return new ImageResponse( + ( +
+ Widget Example + +
+ ), + options, + ); +} diff --git a/src/app/api/widget-quotes/route.tsx b/src/app/api/widget-quotes/route.tsx new file mode 100644 index 000000000..449498ca0 --- /dev/null +++ b/src/app/api/widget-quotes/route.tsx @@ -0,0 +1,110 @@ +/* eslint-disable @next/next/no-img-element */ + +/** + * Image Generation of Widget for SEO pages + * Step 2 - Quotes + * + * Example: + * ``` + * http://localhost:3000/api/widget-quotes?fromToken=0x0000000000000000000000000000000000000000&fromChainId=137&toToken=0x0000000000000000000000000000000000000000&toChainId=42161&amount=10&highlighted=0&theme=light + * ``` + * + * @typedef {Object} SearchParams + * @property {string} fromToken - The token address to send from. + * @property {number} fromChainId - The chain ID to send from. + * @property {string} toToken - The token address to send to. + * @property {number} toChainId - The chain ID to send to. + * @property {number} amount - The amount of tokens. + * @property {number} [amountUSD] - The USD equivalent amount (optional). + * @property {boolean} [isSwap] - True if transaction is a swap, default and false if transaction is a bridge (optional). + * @property {'light'|'dark'} [theme] - The theme for the widget (optional). + * @property {'from'|'to'|'amount'|'0'|'1'|'2'} [highlighted] - The highlighted element, numbers refer to quote index (optional). + * + */ +import type { ChainId } from '@lifi/sdk'; +import { ImageResponse } from 'next/og'; +import type { CSSProperties } from 'react'; +import type { HighlightedAreas } from 'src/components/ImageGeneration/ImageGeneration.types'; +import { imageResponseOptions } from 'src/components/ImageGeneration/imageResponseOptions'; +import { imageFrameStyles } from 'src/components/ImageGeneration/style'; +import WidgetQuoteImage from 'src/components/ImageGeneration/WidgetQuotesImage'; +import { fetchChainData } from 'src/utils/image-generation/fetchChainData'; +import { fetchTokenData } from 'src/utils/image-generation/fetchTokenData'; +import { parseSearchParams } from 'src/utils/image-generation/parseSearchParams'; + +const WIDGET_IMAGE_WIDTH = 856; +const WIDGET_IMAGE_HEIGHT = 490; //376; +const WIDGET_IMAGE_SCALING_FACTOR = 2; + +export async function GET(request: Request) { + const { + fromChainId, + toChainId, + fromToken, + toToken, + isSwap, + theme, + amount, + highlighted, + amountUSD, + } = parseSearchParams(request.url); + + // Fetch data asynchronously before rendering + const fromTokenData = await fetchTokenData(fromChainId, fromToken); + const toTokenData = await fetchTokenData(toChainId, toToken); + const fromChain = await fetchChainData(fromChainId as unknown as ChainId); + const toChain = await fetchChainData(toChainId as unknown as ChainId); + + const routeAmount = + (parseFloat(fromTokenData?.priceUSD || '0') * parseFloat(amount || '0')) / + parseFloat(toTokenData?.priceUSD || '0'); + + const options = await imageResponseOptions({ + width: WIDGET_IMAGE_WIDTH, + height: WIDGET_IMAGE_HEIGHT, + scalingFactor: WIDGET_IMAGE_SCALING_FACTOR, + }); + + const imageFrameStyle = imageFrameStyles({ + width: WIDGET_IMAGE_WIDTH, + height: WIDGET_IMAGE_HEIGHT, + scalingFactor: WIDGET_IMAGE_SCALING_FACTOR, + }) as CSSProperties; + + const imageStyle = imageFrameStyles({ + width: WIDGET_IMAGE_WIDTH, + height: WIDGET_IMAGE_HEIGHT, + scalingFactor: WIDGET_IMAGE_SCALING_FACTOR, + }) as CSSProperties; + + const ImageResp = new ImageResponse( + ( +
+ Widget Quotes Example + +
+ ), + options, + ); + // console.timeEnd('start-time'); + return ImageResp; +} diff --git a/src/app/api/widget-review/route.tsx b/src/app/api/widget-review/route.tsx new file mode 100644 index 000000000..f4b599f7a --- /dev/null +++ b/src/app/api/widget-review/route.tsx @@ -0,0 +1,101 @@ +/* eslint-disable @next/next/no-img-element */ + +/** + * Image Generation of Widget for SEO pages + * Step 3 - Review quote + * + * Example: + * ``` + * http://localhost:3000/api/widget-review?fromToken=0x0000000000000000000000000000000000000000&fromChainId=137&toToken=0x0000000000000000000000000000000000000000&toChainId=42161&amount=10&highlighted=amount&theme=dark + * ``` + * + * @typedef {Object} SearchParams + * @property {string} fromToken - The token address to send from. + * @property {number} fromChainId - The chain ID to send from. + * @property {string} toToken - The token address to send to. + * @property {number} toChainId - The chain ID to send to. + * @property {number} amount - The amount of tokens. + * @property {boolean} [isSwap] - True if transaction is a swap, default and false if transaction is a bridge (optional). + * @property {'light'|'dark'} [theme] - The theme for the widget (optional). + * @property {'from'|'to'|'amount'} [highlighted] - The highlighted element (optional). + * + */ + +import type { ChainId } from '@lifi/sdk'; +import { ImageResponse } from 'next/og'; +import type { CSSProperties } from 'react'; +import type { HighlightedAreas } from 'src/components/ImageGeneration/ImageGeneration.types'; +import { imageResponseOptions } from 'src/components/ImageGeneration/imageResponseOptions'; +import { imageFrameStyles } from 'src/components/ImageGeneration/style'; +import WidgetReviewImage from 'src/components/ImageGeneration/WidgetReviewImage'; +import { fetchChainData } from 'src/utils/image-generation/fetchChainData'; +import { fetchTokenData } from 'src/utils/image-generation/fetchTokenData'; +import { parseSearchParams } from 'src/utils/image-generation/parseSearchParams'; + +const WIDGET_IMAGE_WIDTH = 416; +const WIDGET_IMAGE_HEIGHT = 440; +const WIDGET_IMAGE_SCALING_FACTOR = 2; + +export async function GET(request: Request) { + const { + fromChainId, + toChainId, + fromToken, + toToken, + isSwap, + theme, + amount, + highlighted, + } = parseSearchParams(request.url); + + // Fetch data asynchronously before rendering + const fromTokenData = await fetchTokenData(fromChainId, fromToken); + const toTokenData = await fetchTokenData(toChainId, toToken); + const fromChain = await fetchChainData(fromChainId as unknown as ChainId); + const toChain = await fetchChainData(toChainId as unknown as ChainId); + + const options = await imageResponseOptions({ + width: WIDGET_IMAGE_WIDTH, + height: WIDGET_IMAGE_HEIGHT, + scalingFactor: WIDGET_IMAGE_SCALING_FACTOR, + }); + + const imageFrameStyle = imageFrameStyles({ + width: WIDGET_IMAGE_WIDTH, + height: WIDGET_IMAGE_HEIGHT, + scalingFactor: WIDGET_IMAGE_SCALING_FACTOR, + }) as CSSProperties; + + const imageStyle = imageFrameStyles({ + width: WIDGET_IMAGE_WIDTH, + height: WIDGET_IMAGE_HEIGHT, + scalingFactor: WIDGET_IMAGE_SCALING_FACTOR, + }) as CSSProperties; + + return new ImageResponse( + ( +
+ Widget Review Example + +
+ ), + options, + ); +} diff --git a/src/app/api/widget-selection/route.tsx b/src/app/api/widget-selection/route.tsx new file mode 100644 index 000000000..c9c5f7b89 --- /dev/null +++ b/src/app/api/widget-selection/route.tsx @@ -0,0 +1,101 @@ +/* eslint-disable @next/next/no-img-element */ + +/** + * Image Generation of Widget for SEO pages + * Step 1 - Selecting Tokens + * + * Example: + * ``` + * http://localhost:3000/api/widget-selection?fromToken=0x8f3Cf7ad23Cd3CaDbD9735AFf958023239c6A063&fromChainId=137&toToken=0xdAC17F958D2ee523a2206206994597C13D831ec7&toChainId=1&amount=3&theme=dark + * ``` + * + * @typedef {Object} SearchParams + * @property {string} fromToken - The token address to send from. + * @property {number} fromChainId - The chain ID to send from. + * @property {string} toToken - The token address to send to. + * @property {number} toChainId - The chain ID to send to. + * @property {number} amount - The amount of tokens. + * @property {number} [amountUSD] - The USD equivalent amount (optional). + * @property {'light'|'dark'} [theme] - The theme for the widget (optional). + * @property {'from'|'to'|'amount'} [highlighted] - The highlighted element (optional). + * + */ + +import type { ChainId } from '@lifi/sdk'; +import { ImageResponse } from 'next/og'; +import type { CSSProperties } from 'react'; +import type { HighlightedAreas } from 'src/components/ImageGeneration/ImageGeneration.types'; +import { imageResponseOptions } from 'src/components/ImageGeneration/imageResponseOptions'; +import { imageFrameStyles } from 'src/components/ImageGeneration/style'; +import WidgetSelectionImage from 'src/components/ImageGeneration/WidgetSelectionImage'; +import { fetchChainData } from 'src/utils/image-generation/fetchChainData'; +import { fetchTokenData } from 'src/utils/image-generation/fetchTokenData'; +import { parseSearchParams } from 'src/utils/image-generation/parseSearchParams'; + +const WIDGET_IMAGE_WIDTH = 416; +const WIDGET_IMAGE_HEIGHT = 496; +const WIDGET_IMAGE_SCALING_FACTOR = 2; + +export async function GET(request: Request) { + const { + fromChainId, + toChainId, + fromToken, + toToken, + theme, + amount, + highlighted, + amountUSD, + } = parseSearchParams(request.url); + + // Fetch data asynchronously before rendering + const fromTokenData = await fetchTokenData(fromChainId, fromToken); + const toTokenData = await fetchTokenData(toChainId, toToken); + const fromChain = await fetchChainData(fromChainId as unknown as ChainId); + const toChain = await fetchChainData(toChainId as unknown as ChainId); + + const options = await imageResponseOptions({ + width: WIDGET_IMAGE_WIDTH, + height: WIDGET_IMAGE_HEIGHT, + scalingFactor: WIDGET_IMAGE_SCALING_FACTOR, + }); + + const imageFrameStyle = imageFrameStyles({ + width: WIDGET_IMAGE_WIDTH, + height: WIDGET_IMAGE_HEIGHT, + scalingFactor: WIDGET_IMAGE_SCALING_FACTOR, + }) as CSSProperties; + + const imageStyle = imageFrameStyles({ + width: WIDGET_IMAGE_WIDTH, + height: WIDGET_IMAGE_HEIGHT, + scalingFactor: WIDGET_IMAGE_SCALING_FACTOR, + }) as CSSProperties; + + return new ImageResponse( + ( +
+ Widget Selection Example + +
+ ), + options, + ); +} diff --git a/src/app/api/widget-success/route.tsx b/src/app/api/widget-success/route.tsx new file mode 100644 index 000000000..afdbf4121 --- /dev/null +++ b/src/app/api/widget-success/route.tsx @@ -0,0 +1,85 @@ +/* eslint-disable @next/next/no-img-element */ + +/** + * Image Generation of Widget for SEO pages + * Step 5 - Route success + * + * Example: + * ``` + * http://localhost:3000/api/widget-success?toToken=0x0000000000000000000000000000000000000000&toChainId=42161&amount=10&&theme=light&isSwap=true + * ``` + * + * @typedef {Object} SearchParams + * @property {string} toToken - The token address to send to. + * @property {number} toChainId - The chain ID to send to. + * @property {number} amount - The amount of tokens. + * @property {boolean} [isSwap] - True if transaction is a swap, default and false if transaction is a bridge (optional). + * @property {'light'|'dark'} [theme] - The theme for the widget (optional). + * + */ + +import type { ChainId } from '@lifi/sdk'; +import { ImageResponse } from 'next/og'; +import type { CSSProperties } from 'react'; +import { imageResponseOptions } from 'src/components/ImageGeneration/imageResponseOptions'; +import { imageFrameStyles } from 'src/components/ImageGeneration/style'; +import WidgetSuccessImage from 'src/components/ImageGeneration/WidgetSuccessImage'; +import { fetchChainData } from 'src/utils/image-generation/fetchChainData'; +import { fetchTokenData } from 'src/utils/image-generation/fetchTokenData'; +import { parseSearchParams } from 'src/utils/image-generation/parseSearchParams'; + +const WIDGET_IMAGE_WIDTH = 416; +const WIDGET_IMAGE_HEIGHT = 432; +const WIDGET_IMAGE_SCALING_FACTOR = 2; + +export async function GET(request: Request) { + const { toChainId, toToken, theme, amount, isSwap } = parseSearchParams( + request.url, + ); + + // Fetch data asynchronously before rendering + const toTokenData = await fetchTokenData(toChainId, toToken); + const toChain = await fetchChainData(toChainId as unknown as ChainId); + + const options = await imageResponseOptions({ + width: WIDGET_IMAGE_WIDTH, + height: WIDGET_IMAGE_HEIGHT, + scalingFactor: WIDGET_IMAGE_SCALING_FACTOR, + }); + + const imageFrameStyle = imageFrameStyles({ + width: WIDGET_IMAGE_WIDTH, + height: WIDGET_IMAGE_HEIGHT, + scalingFactor: WIDGET_IMAGE_SCALING_FACTOR, + }) as CSSProperties; + + const imageStyle = imageFrameStyles({ + width: WIDGET_IMAGE_WIDTH, + height: WIDGET_IMAGE_HEIGHT, + scalingFactor: WIDGET_IMAGE_SCALING_FACTOR, + }) as CSSProperties; + + return new ImageResponse( + ( +
+ Widget Example + +
+ ), + options, + ); +} diff --git a/src/components/AvatarBadge/NoMUI/AvatarBadgeNoMUI.tsx b/src/components/AvatarBadge/NoMUI/AvatarBadgeNoMUI.tsx new file mode 100644 index 000000000..f40caffaa --- /dev/null +++ b/src/components/AvatarBadge/NoMUI/AvatarBadgeNoMUI.tsx @@ -0,0 +1,83 @@ +/* eslint-disable jsx-a11y/alt-text */ +/* eslint-disable @next/next/no-img-element */ + +interface BadgeOffsetProps { + x?: number; + y?: number; +} + +type AvatarBadgeNoMUIProps = { + avatarSrc?: string; + badgeSrc?: string; + badgeOffset?: BadgeOffsetProps; + avatarSize: number; + badgeGap?: number; + badgeSize: number; + theme?: 'light' | 'dark'; +}; + +export const AvatarBadgeNoMUI = ({ + avatarSrc, + badgeSrc, + badgeOffset, + badgeGap, + avatarSize, + badgeSize, + theme, +}: AvatarBadgeNoMUIProps) => { + return ( +
+
+ +
+ +
+
+ ); +}; diff --git a/src/components/ImageGeneration/Field.tsx b/src/components/ImageGeneration/Field.tsx new file mode 100644 index 000000000..7a711b728 --- /dev/null +++ b/src/components/ImageGeneration/Field.tsx @@ -0,0 +1,230 @@ +/* eslint-disable jsx-a11y/alt-text */ +/* eslint-disable @next/next/no-img-element */ +import type { ExtendedChain, Token } from '@lifi/sdk'; +import { decimalFormatter } from 'src/utils/formatNumbers'; +import { getOffset, getWidth } from 'src/utils/image-generation/helpers'; +import { AvatarBadgeNoMUI } from '../AvatarBadge/NoMUI/AvatarBadgeNoMUI'; +import { FieldSkeleton } from './FieldSkeleton'; +import type { ImageTheme } from './ImageGeneration.types'; + +const Field = ({ + sx, + token, + chain, + type, + amount = 0, + extendedHeight, + theme, + amountUSD, + routeAmount, + routeAmountUSD, + highlighted, + fullWidth, + showSkeletons, +}: { + sx?: any; + token?: Token | null; + chain?: ExtendedChain | null; + theme?: ImageTheme; + type: + | 'amount' + | 'token' + | 'quote' + | 'review' + | 'quote-amount' + | 'amount-selection' + | 'button' + | 'title' + | 'card-title' + | 'success'; + amount?: number | null; + amountUSD?: number | null; + highlighted?: boolean | null; + routeAmount?: number | null; + routeAmountUSD?: number | null; + extendedHeight?: boolean; + fullWidth?: boolean; + showSkeletons?: boolean; +}) => { + // Function to calculate top offset based on conditions + + const containerOffset = getOffset(type, extendedHeight); + const containerWidth = getWidth(type, fullWidth); + const formatAmount = decimalFormatter('en', { + minimumFractionDigits: 0, + maximumFractionDigits: 6, + }); + + return ( +
+
+ {type !== 'button' && ( +
+ {token && chain && ( + + )} + {type === 'token' && ( +
+
+

+ {token?.symbol} +

+
+
+

+ {chain?.name} +

+
+
+ )} + {type !== 'token' && ( +
+

+ {formatAmount(routeAmount || amount || 0)} +

+ {!showSkeletons && token ? ( +
+

+ ${(routeAmountUSD || amountUSD || amount || 0).toFixed(2)} +

+ {type === 'review' && ( +

+ {`${token.symbol} on ${chain?.name}`} +

+ )} +
+ ) : ( + <> + {type === 'review' && ( + + )} + {type === 'success' && ( + + )} + {type === 'quote' && ( + + )} + + )} +
+ )} +
+ )} + {type === 'quote' && ( + + )} +
+
+ ); +}; + +export default Field; diff --git a/src/components/ImageGeneration/FieldSkeleton.tsx b/src/components/ImageGeneration/FieldSkeleton.tsx new file mode 100644 index 000000000..d868cc2f6 --- /dev/null +++ b/src/components/ImageGeneration/FieldSkeleton.tsx @@ -0,0 +1,26 @@ +/* eslint-disable jsx-a11y/alt-text */ +/* eslint-disable @next/next/no-img-element */ +import type { CSSProperties } from 'react'; + +export const FieldSkeleton = ({ + width, + height, + sx, +}: { + width: number; + height: number; + sx?: CSSProperties; +}) => { + return ( +
+ ); +}; diff --git a/src/components/ImageGeneration/Fields/AmountField.tsx b/src/components/ImageGeneration/Fields/AmountField.tsx new file mode 100644 index 000000000..52822c9e5 --- /dev/null +++ b/src/components/ImageGeneration/Fields/AmountField.tsx @@ -0,0 +1,92 @@ +/* eslint-disable jsx-a11y/alt-text */ +/* eslint-disable @next/next/no-img-element */ +import type { ExtendedChain, Token } from '@lifi/sdk'; +import type { CSSProperties } from 'react'; +import { decimalFormatter } from 'src/utils/formatNumbers'; +import { AvatarBadgeNoMUI } from '../../AvatarBadge/NoMUI/AvatarBadgeNoMUI'; +import { FieldSkeleton } from '../FieldSkeleton'; +import type { ImageTheme } from '../ImageGeneration.types'; +import { amountTextStyles } from '../style'; + +const AmountField = ({ + sx, + token, + chain, + amount = 0, + extendedHeight, + theme, + routeAmount, + highlighted, + fullWidth, +}: { + sx?: CSSProperties; + token?: Token | null; + chain?: ExtendedChain | null; + theme?: ImageTheme; + amount?: number | null; + highlighted?: boolean | null; + routeAmount?: number | null; + extendedHeight?: boolean; + fullWidth?: boolean; +}) => { + // Function to calculate top offset based on conditions + + const formatAmount = decimalFormatter('en', { + minimumFractionDigits: 0, + maximumFractionDigits: 6, + }); + + const amountTextStyle = amountTextStyles(theme); + + return ( +
+
+
+ {token && chain && ( + + )} + +
+

+ {formatAmount(routeAmount || amount || 0)} +

+ +
+
+
+
+ ); +}; + +export default AmountField; diff --git a/src/components/ImageGeneration/Fields/AmountSelectionField.tsx b/src/components/ImageGeneration/Fields/AmountSelectionField.tsx new file mode 100644 index 000000000..c0fdc2937 --- /dev/null +++ b/src/components/ImageGeneration/Fields/AmountSelectionField.tsx @@ -0,0 +1,99 @@ +/* eslint-disable jsx-a11y/alt-text */ +/* eslint-disable @next/next/no-img-element */ +import type { ExtendedChain, Token } from '@lifi/sdk'; +import type { CSSProperties } from 'react'; +import { decimalFormatter } from 'src/utils/formatNumbers'; +import { AvatarBadgeNoMUI } from '../../AvatarBadge/NoMUI/AvatarBadgeNoMUI'; +import type { ImageTheme } from '../ImageGeneration.types'; +import { + amountContainerStyles, + amountTextStyles, + fieldContainerStyles, + tokenTextStyles, +} from '../style'; + +const Field = ({ + sx, + token, + chain, + amount = 0, + theme, + amountUSD, + routeAmount, + routeAmountUSD, + highlighted, + fullWidth, + showSkeletons, +}: { + sx?: any; + token?: Token | null; + chain?: ExtendedChain | null; + theme?: ImageTheme; + amount?: number | null; + amountUSD?: number | null; + highlighted?: boolean | null; + routeAmount?: number | null; + routeAmountUSD?: number | null; + fullWidth?: boolean; + showSkeletons?: boolean; +}) => { + // Function to calculate top offset based on conditions + + const formatAmount = decimalFormatter('en', { + minimumFractionDigits: 0, + maximumFractionDigits: 6, + }); + const fieldContainerStyle = fieldContainerStyles(); + const amountTextStyle = amountTextStyles(theme); + const tokenTextStyle = tokenTextStyles('amount', theme); + + const amountContainerStyle = amountContainerStyles() as CSSProperties; + return ( +
+
+
+ {token && chain && ( + + )} +
+

+ {formatAmount(routeAmount || amount || 0)} +

+ {!showSkeletons && token && ( +
+

+ ${(routeAmountUSD || amountUSD || amount || 0).toFixed(2)} +

+
+ )} +
+
+
+
+ ); +}; + +export default Field; diff --git a/src/components/ImageGeneration/Fields/QuoteAmountField.tsx b/src/components/ImageGeneration/Fields/QuoteAmountField.tsx new file mode 100644 index 000000000..78d0c449b --- /dev/null +++ b/src/components/ImageGeneration/Fields/QuoteAmountField.tsx @@ -0,0 +1,100 @@ +/* eslint-disable jsx-a11y/alt-text */ +/* eslint-disable @next/next/no-img-element */ +import type { ExtendedChain, Token } from '@lifi/sdk'; +import type { CSSProperties } from 'react'; +import { decimalFormatter } from 'src/utils/formatNumbers'; +import { AvatarBadgeNoMUI } from '../../AvatarBadge/NoMUI/AvatarBadgeNoMUI'; +import type { ImageTheme } from '../ImageGeneration.types'; +import { + amountContainerStyles, + amountTextStyles, + fieldContainerStyles, + tokenTextStyles, +} from '../style'; + +const QuoteAmountField = ({ + sx, + token, + chain, + amount = 0, + extendedHeight, + theme, + amountUSD, + routeAmount, + routeAmountUSD, + highlighted, + fullWidth, + showSkeletons, +}: { + sx?: any; + token?: Token | null; + chain?: ExtendedChain | null; + theme?: ImageTheme; + amount?: number | null; + amountUSD?: number | null; + highlighted?: boolean | null; + routeAmount?: number | null; + routeAmountUSD?: number | null; + extendedHeight?: boolean; + fullWidth?: boolean; + showSkeletons?: boolean; +}) => { + // Function to calculate top offset based on conditions + const formatAmount = decimalFormatter('en', { + minimumFractionDigits: 0, + maximumFractionDigits: 6, + }); + + const fieldContainerStyle = fieldContainerStyles(extendedHeight); + const tokenTextStyle = tokenTextStyles('amount', theme); + const amountTextStyle = amountTextStyles(theme); + const amountContainerStyle = amountContainerStyles() as CSSProperties; + return ( +
+
+
+ {token && chain && ( + + )} +
+

+ {formatAmount(routeAmount || amount || 0)} +

+ {!showSkeletons && token && ( +
+

+ ${(routeAmountUSD || amountUSD || amount || 0).toFixed(2)} +

+
+ )} +
+
+
+
+ ); +}; + +export default QuoteAmountField; diff --git a/src/components/ImageGeneration/Fields/QuoteField.tsx b/src/components/ImageGeneration/Fields/QuoteField.tsx new file mode 100644 index 000000000..80f00b866 --- /dev/null +++ b/src/components/ImageGeneration/Fields/QuoteField.tsx @@ -0,0 +1,101 @@ +/* eslint-disable jsx-a11y/alt-text */ +/* eslint-disable @next/next/no-img-element */ +import type { ExtendedChain, Token } from '@lifi/sdk'; +import type { CSSProperties } from 'react'; +import { decimalFormatter } from 'src/utils/formatNumbers'; +import { AvatarBadgeNoMUI } from '../../AvatarBadge/NoMUI/AvatarBadgeNoMUI'; +import { FieldSkeleton } from '../FieldSkeleton'; +import type { ImageTheme } from '../ImageGeneration.types'; +import { + amountContainerStyles, + amountTextStyles, + fieldContainerStyles, + tokenTextStyles, +} from '../style'; + +const QuoteField = ({ + sx, + token, + chain, + amount = 0, + extendedHeight, + theme, + amountUSD, + routeAmount, + routeAmountUSD, + highlighted, + fullWidth, + showSkeletons, +}: { + sx?: any; + token?: Token | null; + chain?: ExtendedChain | null; + theme?: ImageTheme; + amount: number | null; + amountUSD?: number | null; + highlighted?: boolean | null; + routeAmount?: number | null; + routeAmountUSD?: number | null; + extendedHeight?: boolean; + fullWidth?: boolean; + showSkeletons?: boolean; +}) => { + // Function to calculate top offset based on conditions + + const formatAmount = decimalFormatter('en', { + minimumFractionDigits: 0, + maximumFractionDigits: 5, + }); + + const fieldContainerStyle = fieldContainerStyles(extendedHeight); + const tokenTextStyle = tokenTextStyles('amount', theme); + const amountTextStyle = amountTextStyles(theme); + const amountContainerStyle = amountContainerStyles() as CSSProperties; + return ( +
+
+
+ {token && chain && ( + + )} +
+

+ {formatAmount(routeAmount || amount || 0)} +

+ {!showSkeletons && token ? ( +
+

+ ${(routeAmountUSD || amountUSD || amount || 0).toFixed(2)} +

+
+ ) : ( + + )} +
+
+ +
+
+ ); +}; + +export default QuoteField; diff --git a/src/components/ImageGeneration/Fields/ReviewField.tsx b/src/components/ImageGeneration/Fields/ReviewField.tsx new file mode 100644 index 000000000..73a08c822 --- /dev/null +++ b/src/components/ImageGeneration/Fields/ReviewField.tsx @@ -0,0 +1,115 @@ +import type { ExtendedChain, Token } from '@lifi/sdk'; +import { AvatarBadgeNoMUI } from '../../AvatarBadge/NoMUI/AvatarBadgeNoMUI'; +import { FieldSkeleton } from '../FieldSkeleton'; +import type { ImageTheme } from '../ImageGeneration.types'; + +import { decimalFormatter } from 'src/utils/formatNumbers'; +import { + amountTextStyles, + fieldContainerStyles, + tokenTextStyles, +} from '../style'; + +const ReviewField = ({ + sx, + token, + chain, + amount = 0, + amountUSD, + routeAmount, + routeAmountUSD, + highlighted, + fullWidth, + showSkeletons, + theme, +}: { + sx?: any; + token?: Token | null; + chain?: ExtendedChain | null; + theme?: ImageTheme; + amount?: number | null; + amountUSD?: number | null; + highlighted?: boolean | null; + routeAmount?: number | null; + routeAmountUSD?: number | null; + extendedHeight?: boolean; + fullWidth?: boolean; + showSkeletons?: boolean; +}) => { + const formatAmount = decimalFormatter('en', { + minimumFractionDigits: 0, + maximumFractionDigits: 6, + }); + + const containerWidth = fullWidth ? 368 : 174; // Width based on fullWidth prop + const fieldContainerStyle = fieldContainerStyles(); + const tokenAmountTextStyle = tokenTextStyles('amount', theme); + const tokenSymbolTextStyle = tokenTextStyles('symbol', theme); + const amountTextStyle = amountTextStyles(theme); + return ( +
+
+
+ {token && chain && ( + + )} +
+

+ {formatAmount(routeAmount || amount || 0)} +

+ {!showSkeletons && token ? ( +
+

+ ${(routeAmountUSD || amountUSD || amount || 0).toFixed(2)} +

+

+ {`${token.symbol} on ${chain?.name}`} +

+
+ ) : ( + + )} +
+
+
+
+ ); +}; + +export default ReviewField; diff --git a/src/components/ImageGeneration/Fields/SuccessField.tsx b/src/components/ImageGeneration/Fields/SuccessField.tsx new file mode 100644 index 000000000..40c99392b --- /dev/null +++ b/src/components/ImageGeneration/Fields/SuccessField.tsx @@ -0,0 +1,76 @@ +/* eslint-disable jsx-a11y/alt-text */ +/* eslint-disable @next/next/no-img-element */ +import type { ExtendedChain, Token } from '@lifi/sdk'; +import type { CSSProperties } from 'react'; +import { decimalFormatter } from 'src/utils/formatNumbers'; +import { AvatarBadgeNoMUI } from '../../AvatarBadge/NoMUI/AvatarBadgeNoMUI'; +import { FieldSkeleton } from '../FieldSkeleton'; +import type { ImageTheme } from '../ImageGeneration.types'; +import { + amountContainerStyles, + amountTextStyles, + fieldContainerStyles, +} from '../style'; + +const SuccessField = ({ + token, + chain, + amount = 0, + extendedHeight, + theme, + routeAmount, +}: { + token?: Token | null; + chain?: ExtendedChain | null; + theme?: ImageTheme; + amount?: number | null; + routeAmount?: number | null; + extendedHeight?: boolean; +}) => { + // Function to calculate top offset based on conditions + + const formatAmount = decimalFormatter('en', { + minimumFractionDigits: 0, + maximumFractionDigits: 6, + }); + + const fieldContainerStyle = fieldContainerStyles(extendedHeight); + const amountTextStyle = amountTextStyles(theme); + const amountContainerStyle = amountContainerStyles() as CSSProperties; + + return ( +
+
+
+ {token && chain && ( + + )} +
+

+ {formatAmount(routeAmount || amount || 0)} +

+ +
+
+
+
+ ); +}; + +export default SuccessField; diff --git a/src/components/ImageGeneration/Fields/TokenField.tsx b/src/components/ImageGeneration/Fields/TokenField.tsx new file mode 100644 index 000000000..01e59bf4c --- /dev/null +++ b/src/components/ImageGeneration/Fields/TokenField.tsx @@ -0,0 +1,98 @@ +/* eslint-disable jsx-a11y/alt-text */ +/* eslint-disable @next/next/no-img-element */ +import type { ExtendedChain, Token } from '@lifi/sdk'; +import { AvatarBadgeNoMUI } from '../../AvatarBadge/NoMUI/AvatarBadgeNoMUI'; +import type { ImageTheme } from '../ImageGeneration.types'; +import { fieldContainerStyles } from '../style'; + +const TokenField = ({ + sx, + token, + chain, + theme, + highlighted, + fullWidth, +}: { + sx?: any; + token?: Token | null; + chain?: ExtendedChain | null; + theme?: ImageTheme; + highlighted?: boolean | null; + fullWidth?: boolean; +}) => { + // Function to calculate top offset based on conditions + + const fieldContainerStyle = fieldContainerStyles(); + + return ( +
+
+
+ {token && chain && ( + + )} +
+
+

+ {token?.symbol} +

+
+
+

+ {chain?.name} +

+
+
+
+
+
+ ); +}; + +export default TokenField; diff --git a/src/components/ImageGeneration/Fields/WidgetExpandedQuote.tsx b/src/components/ImageGeneration/Fields/WidgetExpandedQuote.tsx new file mode 100644 index 000000000..521a4ae96 --- /dev/null +++ b/src/components/ImageGeneration/Fields/WidgetExpandedQuote.tsx @@ -0,0 +1,103 @@ +/* eslint-disable jsx-a11y/alt-text */ +/* eslint-disable @next/next/no-img-element */ +import type { ExtendedChain, Token } from '@lifi/sdk'; +import type { CSSProperties } from 'react'; +import { decimalFormatter } from 'src/utils/formatNumbers'; +import { AvatarBadgeNoMUI } from '../../AvatarBadge/NoMUI/AvatarBadgeNoMUI'; +import { FieldSkeleton } from '../FieldSkeleton'; +import type { ImageTheme } from '../ImageGeneration.types'; +import { + amountContainerStyles, + amountTextStyles, + fieldContainerStyles, + tokenTextStyles, +} from '../style'; + +const WidgetExpandedQuote = ({ + sx, + token, + chain, + amount = 0, + extendedHeight, + theme, + amountUSD, + routeAmount, + routeAmountUSD, + highlighted, + fullWidth, + showSkeletons, +}: { + sx?: any; + token?: Token | null; + chain?: ExtendedChain | null; + theme?: ImageTheme; + amount?: number | null; + amountUSD?: number | null; + highlighted?: boolean | null; + routeAmount?: number | null; + routeAmountUSD?: number | null; + extendedHeight?: boolean; + fullWidth?: boolean; + showSkeletons?: boolean; +}) => { + // Function to calculate top offset based on conditions + + const formatAmount = decimalFormatter('en', { + minimumFractionDigits: 0, + maximumFractionDigits: 6, + }); + const fieldContainerStyle = fieldContainerStyles( + extendedHeight, + ) as CSSProperties; + const tokenTextStyle = tokenTextStyles('amount', theme) as CSSProperties; + const amountContainerStyle = amountContainerStyles() as CSSProperties; + const amountTextStyle = amountTextStyles(theme); + + return ( +
+
+
+ {token && chain && ( + + )} +
+

+ {formatAmount(routeAmount || amount || 0)} +

+ {!showSkeletons && token ? ( +
+

+ ${(routeAmountUSD || amountUSD || amount || 0).toFixed(2)} +

+
+ ) : ( + + )} +
+
+ +
+
+ ); +}; + +export default WidgetExpandedQuote; diff --git a/src/components/ImageGeneration/ImageGeneration.types.ts b/src/components/ImageGeneration/ImageGeneration.types.ts new file mode 100644 index 000000000..c76b8676f --- /dev/null +++ b/src/components/ImageGeneration/ImageGeneration.types.ts @@ -0,0 +1,3 @@ +export type HighlightedAreas = 'from' | 'to' | 'amount'; + +export type ImageTheme = 'light' | 'dark'; diff --git a/src/components/ImageGeneration/Labels/ButtonLabel.tsx b/src/components/ImageGeneration/Labels/ButtonLabel.tsx new file mode 100644 index 000000000..b476bcf21 --- /dev/null +++ b/src/components/ImageGeneration/Labels/ButtonLabel.tsx @@ -0,0 +1,46 @@ +/* eslint-disable jsx-a11y/alt-text */ +/* eslint-disable @next/next/no-img-element */ + +import type { CSSProperties } from 'react'; +import type { ImageTheme } from '../ImageGeneration.types'; + +const ButtonLabel = ({ + sx, + buttonLabel, + theme, + fullWidth, +}: { + sx?: CSSProperties; + theme?: ImageTheme; + buttonLabel?: string; + fullWidth?: boolean; +}) => { + return ( + !!buttonLabel && ( +
+

+ {buttonLabel} +

+
+ ) + ); +}; + +export default ButtonLabel; diff --git a/src/components/ImageGeneration/Labels/CardContent.tsx b/src/components/ImageGeneration/Labels/CardContent.tsx new file mode 100644 index 000000000..372ffed26 --- /dev/null +++ b/src/components/ImageGeneration/Labels/CardContent.tsx @@ -0,0 +1,34 @@ +/* eslint-disable jsx-a11y/alt-text */ +/* eslint-disable @next/next/no-img-element */ + +import type { CSSProperties } from 'react'; +import type { ImageTheme } from '../ImageGeneration.types'; + +const CardContent = ({ + sx, + cardContent, + theme, +}: { + sx?: CSSProperties; + cardContent?: string; + theme?: ImageTheme; +}) => { + return ( + !!cardContent && ( +

+ {cardContent} +

+ ) + ); +}; + +export default CardContent; diff --git a/src/components/ImageGeneration/Labels/CardTitle.tsx b/src/components/ImageGeneration/Labels/CardTitle.tsx new file mode 100644 index 000000000..9c39b2cc1 --- /dev/null +++ b/src/components/ImageGeneration/Labels/CardTitle.tsx @@ -0,0 +1,34 @@ +/* eslint-disable jsx-a11y/alt-text */ +/* eslint-disable @next/next/no-img-element */ + +import type { CSSProperties } from 'react'; +import type { ImageTheme } from '../ImageGeneration.types'; + +const CardTitle = ({ + sx, + cardTitle, + theme, +}: { + sx?: CSSProperties; + cardTitle?: string; + theme?: ImageTheme; +}) => { + return ( + !!cardTitle && ( +

+ {cardTitle} +

+ ) + ); +}; + +export default CardTitle; diff --git a/src/components/ImageGeneration/Labels/Label.tsx b/src/components/ImageGeneration/Labels/Label.tsx new file mode 100644 index 000000000..d50fa6dab --- /dev/null +++ b/src/components/ImageGeneration/Labels/Label.tsx @@ -0,0 +1,104 @@ +/* eslint-disable jsx-a11y/alt-text */ +/* eslint-disable @next/next/no-img-element */ + +import type { CSSProperties } from 'react'; +import type { ImageTheme } from '../ImageGeneration.types'; + +const Label = ({ + sx, + buttonLabel, + cardTitle, + cardContent, + title, + theme, + fullWidth, +}: { + sx?: CSSProperties; + cardTitle?: string; + cardContent?: string; + theme?: ImageTheme; + buttonLabel?: string; + title?: string; + fullWidth?: boolean; +}) => { + return ( + <> + {!!title && ( +
+

+ {title} +

+
+ )} + {!!cardTitle && ( +

+ {cardTitle} +

+ )} + {!!cardContent && ( +

+ {cardContent} +

+ )} + {!!buttonLabel && ( +
+

+ {buttonLabel} +

+
+ )} + + ); +}; + +export default Label; diff --git a/src/components/ImageGeneration/Labels/Title.tsx b/src/components/ImageGeneration/Labels/Title.tsx new file mode 100644 index 000000000..f67e9c3d3 --- /dev/null +++ b/src/components/ImageGeneration/Labels/Title.tsx @@ -0,0 +1,46 @@ +/* eslint-disable jsx-a11y/alt-text */ +/* eslint-disable @next/next/no-img-element */ + +import type { CSSProperties } from 'react'; +import type { ImageTheme } from '../ImageGeneration.types'; + +const Title = ({ + sx, + title, + theme, + fullWidth, +}: { + sx?: CSSProperties; + theme?: ImageTheme; + title: string; + fullWidth?: boolean; +}) => { + return ( + <> + {!!title && ( +
+

+ {title} +

+
+ )} + + ); +}; + +export default Title; diff --git a/src/components/ImageGeneration/WidgetExecutionImage.tsx b/src/components/ImageGeneration/WidgetExecutionImage.tsx new file mode 100644 index 000000000..653fd19d2 --- /dev/null +++ b/src/components/ImageGeneration/WidgetExecutionImage.tsx @@ -0,0 +1,122 @@ +import type { ExtendedChain, Token } from '@lifi/sdk'; +import type { CSSProperties } from 'react'; +import ReviewField from './Fields/ReviewField'; +import { FieldSkeleton } from './FieldSkeleton'; +import type { HighlightedAreas, ImageTheme } from './ImageGeneration.types'; +import ButtonLabel from './Labels/ButtonLabel'; +import CardContent from './Labels/CardContent'; +import CardTitle from './Labels/CardTitle'; +import Title from './Labels/Title'; +import { + contentContainerStyles, + contentPositioningStyles, + pageStyles, +} from './style'; + +const SCALING_FACTOR = 2; + +interface WidgetReviewImageProps { + fromChain?: ExtendedChain | null; + toChain?: ExtendedChain | null; + fromToken?: Token | null; + toToken?: Token | null; + theme?: ImageTheme; + isSwap?: boolean; + amount?: string | null; + width: number; + height: number; + highlighted?: HighlightedAreas; +} + +const WidgetExecutionImage = ({ + fromChain, + toChain, + theme, + fromToken, + isSwap, + toToken, + amount, + width, + height, + highlighted, +}: WidgetReviewImageProps) => { + const contentContainerStyle = contentContainerStyles({ + height, + width, + scalingFactor: SCALING_FACTOR, + }) as CSSProperties; + + const contentPositioningStyle = contentPositioningStyles() as CSSProperties; + const pageStyle = pageStyles() as CSSProperties; + + return ( +
+
+ { + // pages container --> + } +
+ + <CardTitle + cardTitle={isSwap ? 'Swap' : 'Swap and bridge'} + theme={theme} + /> + <ReviewField + chain={fromChain} + theme={theme} + token={fromToken} + amount={amount ? parseFloat(amount) : null} + fullWidth={false} + highlighted={highlighted === 'from'} + showSkeletons={true} + sx={{ padding: '0px 16px', marginTop: 14 }} + /> + <CardContent + cardContent={ + isSwap + ? 'Waiting for swap transaction' + : 'Waiting for bridge transaction' + } + sx={{ marginTop: 14 }} + theme={theme} + /> + <ReviewField + chain={toChain} + token={toToken} + theme={theme} + fullWidth={false} + highlighted={highlighted === 'to'} + amount={ + amount && fromToken && toToken + ? (parseFloat(amount) * parseFloat(fromToken?.priceUSD)) / + parseFloat(toToken?.priceUSD) + : null + } + showSkeletons={true} + sx={{ padding: '0px 16px', marginTop: 18 }} + /> + <FieldSkeleton + width={164} + height={12} + sx={{ marginTop: -12, marginLeft: 14 }} + /> + <ButtonLabel + buttonLabel={isSwap ? 'Start swapping' : 'Start bridging'} + theme={theme} + sx={{ marginTop: 53 }} + /> + </div> + </div> + </div> + ); +}; +export default WidgetExecutionImage; diff --git a/src/components/ImageGeneration/WidgetQuotesImage.tsx b/src/components/ImageGeneration/WidgetQuotesImage.tsx new file mode 100644 index 000000000..63b22c3b2 --- /dev/null +++ b/src/components/ImageGeneration/WidgetQuotesImage.tsx @@ -0,0 +1,150 @@ +import type { ExtendedChain, Token } from '@lifi/sdk'; +import type { CSSProperties } from 'react'; +import QuoteAmountField from './Fields/QuoteAmountField'; +import QuoteField from './Fields/QuoteField'; +import TokenField from './Fields/TokenField'; +import type { HighlightedAreas, ImageTheme } from './ImageGeneration.types'; +import Label from './Labels/Label'; +import { + contentContainerStyles, + contentPositioningStyles, + pageStyles, +} from './style'; + +const SCALING_FACTOR = 2; + +interface WidgetQuoteImageProps { + fromChain?: ExtendedChain | null; + toChain?: ExtendedChain | null; + fromToken?: Token | null; + toToken?: Token | null; + routeAmount?: number | null; + amount?: string | null; + isSwap?: boolean; + amountUSD?: string | null; + width: number; + height: number; + highlighted?: HighlightedAreas; + theme?: ImageTheme | null; +} + +const WidgetQuoteImage = ({ + fromChain, + toChain, + fromToken, + toToken, + theme, + amount, + isSwap, + amountUSD, + routeAmount, + width, + height, + highlighted, +}: WidgetQuoteImageProps) => { + const contentContainerStyle = contentContainerStyles({ + height, + width, + scalingFactor: SCALING_FACTOR, + }) as CSSProperties; + + const contentPositioningStyle = contentPositioningStyles() as CSSProperties; + const pageStyle = pageStyles() as CSSProperties; + + return ( + <div style={contentPositioningStyle}> + <div + style={{ + ...contentContainerStyle, + marginTop: 128, + }} + > + { + // pages container --> + } + <div style={{ display: 'flex', gap: 24 }}> + <div + style={{ + ...pageStyle, + alignItems: 'center', + }} + > + <div + style={{ + display: 'flex', + flexDirection: 'row', + gap: 18, + justifyContent: 'space-between', + }} + > + <TokenField + theme={theme || undefined} + chain={fromChain} + token={fromToken} + fullWidth={false} + highlighted={highlighted === 'from'} + /> + <TokenField + theme={theme || undefined} + chain={toChain} + token={toToken} + fullWidth={false} + highlighted={highlighted === 'to'} + /> + </div> + <QuoteAmountField + theme={theme || undefined} + chain={fromChain} + token={fromToken} + amount={amount ? parseFloat(amount) : undefined} + amountUSD={amountUSD ? parseFloat(amountUSD) : undefined} + fullWidth={true} + sx={{ marginTop: 16 }} + highlighted={highlighted === 'amount'} + /> + <Label + buttonLabel={isSwap ? 'Review swap' : 'Review bridge'} + theme={theme || undefined} + fullWidth={true} + sx={{ marginTop: 33, marginLeft: -59 }} + /> + </div> + <div + style={{ + ...pageStyle, + alignItems: 'center', + }} + > + <div style={{ display: 'flex', flexDirection: 'column' }}> + {Array(3) + .fill(0) + .map((_, index) => ( + <QuoteField + key={`widget-field-quote-${index}`} + theme={theme || undefined} + chain={fromChain} + token={fromToken} + fullWidth={true} + showSkeletons={true} + highlighted={index.toString() === highlighted} + routeAmount={((100 - index) / 100) * (routeAmount || 1)} + routeAmountUSD={ + ((100 - index) / 100) * + (parseFloat(amount || '1') * + parseFloat(fromToken?.priceUSD || '1')) + } + extendedHeight={index === 0} + amount={amount ? parseFloat(amount) : null} + amountUSD={amountUSD ? parseFloat(amountUSD) : undefined} + sx={{ ...(index !== 0 && { marginTop: 16, height: 110 }) }} + /> + ))} + </div> + </div> + </div> + </div> + </div> + ); +}; + +export default WidgetQuoteImage; diff --git a/src/components/ImageGeneration/WidgetReviewImage.tsx b/src/components/ImageGeneration/WidgetReviewImage.tsx new file mode 100644 index 000000000..811538c5a --- /dev/null +++ b/src/components/ImageGeneration/WidgetReviewImage.tsx @@ -0,0 +1,115 @@ +import type { ExtendedChain, Token } from '@lifi/sdk'; +import type { CSSProperties } from 'react'; +import ReviewField from './Fields/ReviewField'; +import { FieldSkeleton } from './FieldSkeleton'; +import type { HighlightedAreas, ImageTheme } from './ImageGeneration.types'; +import ButtonLabel from './Labels/ButtonLabel'; +import CardTitle from './Labels/CardTitle'; +import Title from './Labels/Title'; +import { + contentContainerStyles, + contentPositioningStyles, + pageStyles, +} from './style'; + +const SCALING_FACTOR = 2; + +interface WidgetReviewImageProps { + fromChain?: ExtendedChain | null; + toChain?: ExtendedChain | null; + fromToken?: Token | null; + toToken?: Token | null; + theme?: ImageTheme; + isSwap?: boolean; + amount?: string | null; + width: number; + height: number; + highlighted?: HighlightedAreas; + sx?: CSSProperties; +} + +const WidgetReviewImage = ({ + fromChain, + toChain, + theme, + fromToken, + isSwap, + toToken, + amount, + width, + height, + highlighted, + sx, +}: WidgetReviewImageProps) => { + const contentContainerStyle = contentContainerStyles({ + height, + width, + scalingFactor: SCALING_FACTOR, + }) as CSSProperties; + + const contentPositioningStyle = contentPositioningStyles() as CSSProperties; + const pageStyle = pageStyles() as CSSProperties; + + return ( + <div style={contentPositioningStyle}> + <div style={contentContainerStyle}> + { + // pages container --> + } + <div style={pageStyle}> + <Title + title={isSwap ? 'Review swap' : 'Review bridge'} + theme={theme} + fullWidth={true} + sx={{ + marginTop: 22, + alignSelf: 'flex-start', + marginLeft: -2, + }} + /> + <CardTitle + cardTitle={isSwap ? 'Swap' : 'Swap and bridge'} + theme={theme} + /> + <ReviewField + chain={fromChain} + theme={theme} + token={fromToken} + amount={amount ? parseFloat(amount) : null} + fullWidth={false} + highlighted={highlighted === 'from'} + showSkeletons={true} + sx={{ marginTop: 0 }} + /> + <ReviewField + chain={toChain} + token={toToken} + theme={theme} + fullWidth={false} + highlighted={highlighted === 'to'} + amount={ + amount && fromToken && toToken + ? (parseFloat(amount) * parseFloat(fromToken?.priceUSD)) / + parseFloat(toToken?.priceUSD) + : null + } + showSkeletons={true} + sx={{ marginTop: 8 }} + /> + <FieldSkeleton + width={164} + height={12} + sx={{ marginTop: -12, marginLeft: 14 }} + /> + <ButtonLabel + buttonLabel={isSwap ? 'Start swapping' : 'Start bridging'} + theme={theme} + sx={{ marginTop: 40 }} + /> + </div> + </div> + </div> + ); +}; + +export default WidgetReviewImage; diff --git a/src/components/ImageGeneration/WidgetSelectionImage.tsx b/src/components/ImageGeneration/WidgetSelectionImage.tsx new file mode 100644 index 000000000..31aeeb941 --- /dev/null +++ b/src/components/ImageGeneration/WidgetSelectionImage.tsx @@ -0,0 +1,94 @@ +// WidgetImageSSR.tsx + +import type { ExtendedChain, Token } from '@lifi/sdk'; +import type { CSSProperties } from 'react'; +import AmountField from './Fields/AmountField'; +import TokenField from './Fields/TokenField'; +import type { HighlightedAreas, ImageTheme } from './ImageGeneration.types'; +import { + contentContainerStyles, + contentPositioningStyles, + pageStyles, +} from './style'; + +const SCALING_FACTOR = 2; + +interface WidgetImageSSRProps { + fromChain?: ExtendedChain | null; + toChain?: ExtendedChain | null; + fromToken?: Token | null; + toToken?: Token | null; + amount?: string | null; + amountUSD?: string | null; + width: number; + height: number; + theme?: ImageTheme | null; + highlighted?: HighlightedAreas; +} + +const WidgetSelectionImage = ({ + fromChain, + toChain, + fromToken, + toToken, + amount, + amountUSD, + width, + height, + theme, + highlighted, +}: WidgetImageSSRProps) => { + const contentContainerStyle = contentContainerStyles({ + height, + width, + scalingFactor: SCALING_FACTOR, + }) as CSSProperties; + + const contentPositioningStyle = contentPositioningStyles() as CSSProperties; + const pageStyle = pageStyles() as CSSProperties; + + return ( + <div style={contentPositioningStyle}> + <div + style={{ + ...contentContainerStyle, + // marginTop: 64, + }} + > + <div + style={{ + ...pageStyle, + alignItems: 'center', + }} + > + <TokenField + chain={fromChain} + token={fromToken} + fullWidth={true} + theme={theme || undefined} + highlighted={highlighted === 'from'} + sx={{ marginTop: 64 }} + /> + <TokenField + chain={toChain} + token={toToken} + fullWidth={true} + theme={theme || undefined} + sx={{ marginTop: '16px' }} + highlighted={highlighted === 'to'} + /> + <AmountField + chain={fromChain} + token={fromToken} + theme={theme || undefined} + amount={amount ? parseFloat(amount) : undefined} + fullWidth={true} + highlighted={highlighted === 'amount'} + /> + </div> + </div> + </div> + ); +}; + +export default WidgetSelectionImage; diff --git a/src/components/ImageGeneration/WidgetSuccessImage.tsx b/src/components/ImageGeneration/WidgetSuccessImage.tsx new file mode 100644 index 000000000..748a07c95 --- /dev/null +++ b/src/components/ImageGeneration/WidgetSuccessImage.tsx @@ -0,0 +1,75 @@ +import type { ExtendedChain, Token } from '@lifi/sdk'; +import type { CSSProperties } from 'react'; +import SuccessField from './Fields/SuccessField'; +import { FieldSkeleton } from './FieldSkeleton'; +import type { ImageTheme } from './ImageGeneration.types'; +import Title from './Labels/Title'; +import { contentContainerStyles, contentPositioningStyles } from './style'; + +const SCALING_FACTOR = 2; + +interface WidgetReviewImageProps { + toChain?: ExtendedChain | null; + toToken?: Token | null; + theme?: ImageTheme; + isSwap?: boolean; + amount?: string | null; + width: number; + height: number; +} + +const WidgetSuccessImage = ({ + toChain, + theme, + isSwap, + toToken, + amount, + width, + height, +}: WidgetReviewImageProps) => { + const contentContainerStyle = contentContainerStyles({ + height, + width, + scalingFactor: SCALING_FACTOR, + }) as CSSProperties; + + const contentPositioningStyle = contentPositioningStyles() as CSSProperties; + + return ( + <div style={contentPositioningStyle}> + <div style={contentContainerStyle}> + { + // pages container --> + } + <div + style={{ + display: 'flex', + flexDirection: 'column', + margin: '0 22px', + }} + > + <Title + title={isSwap ? 'Swap successful' : 'Bridge successful'} + theme={theme} + fullWidth={true} + sx={{ + marginTop: 189, + marginLeft: 2, + alignSelf: 'flex-start', + }} + /> + <SuccessField + chain={toChain} + theme={theme} + token={toToken} + amount={amount ? parseFloat(amount) : null} + /> + <FieldSkeleton width={344} height={12} sx={{ marginTop: -42 }} /> + <FieldSkeleton width={184} height={12} sx={{ marginTop: 12 }} /> + </div> + </div> + </div> + ); +}; + +export default WidgetSuccessImage; diff --git a/src/components/ImageGeneration/imageResponseOptions.ts b/src/components/ImageGeneration/imageResponseOptions.ts new file mode 100644 index 000000000..14dfa8c47 --- /dev/null +++ b/src/components/ImageGeneration/imageResponseOptions.ts @@ -0,0 +1,57 @@ +import type { Font } from 'node_modules/next/dist/compiled/@vercel/og/satori'; + +export const imageResponseOptions = async ({ + width, + height, + scalingFactor, +}: { + width: number; + height: number; + scalingFactor: number; +}) => { + return { + headers: { + 'Cache-Control': `public, max-age=${60 * 60 * 1000 * 24}, immutable`, + }, + width: width * scalingFactor, + height: height * scalingFactor, + fonts: await getInterFonts(), // Await properly within the async function + }; +}; + +async function getInterFonts(): Promise<Font[]> { + // This is unfortunate but I can't figure out how to load local font files + // when deployed to vercel. + const [interRegular, interSemiBold, interBold] = await Promise.all([ + fetch(`https://fonts.cdnfonts.com/s/19795/Inter-Regular.woff`).then((res) => + res.arrayBuffer(), + ), + fetch(`https://fonts.cdnfonts.com/s/19795/Inter-SemiBold.woff`).then( + (res) => res.arrayBuffer(), + ), + fetch(`https://fonts.cdnfonts.com/s/19795/Inter-Bold.woff`).then((res) => + res.arrayBuffer(), + ), + ]); + + return [ + { + name: 'Inter', + data: interRegular, + style: 'normal', + weight: 400, + }, + { + name: 'Inter', + data: interSemiBold, + style: 'normal', + weight: 600, + }, + { + name: 'Inter', + data: interBold, + style: 'normal', + weight: 700, + }, + ]; +} diff --git a/src/components/ImageGeneration/style.ts b/src/components/ImageGeneration/style.ts new file mode 100644 index 000000000..ea5e276a5 --- /dev/null +++ b/src/components/ImageGeneration/style.ts @@ -0,0 +1,124 @@ +import type { ImageTheme } from './ImageGeneration.types'; + +interface ContainerStylesProps { + width: number; + height: number; + scalingFactor: number; +} + +export const imageFrameStyles = ({ + height, + width, + scalingFactor, +}: ContainerStylesProps) => { + return { + position: 'relative', + display: 'flex', + width: width * scalingFactor, + height: height * scalingFactor, + }; +}; + +export const imageStyles = ({ + height, + width, + scalingFactor, +}: ContainerStylesProps) => { + return { + margin: 'auto', + position: 'absolute', + top: 0, + left: 0, + objectFit: 'cover', + width: width * scalingFactor, + height: height * scalingFactor, + }; +}; + +export const contentContainerStyles = ({ + height, + width, + scalingFactor, +}: ContainerStylesProps) => { + return { + display: 'flex', + transformOrigin: 'top left', + position: 'relative', + transform: `scale(${scalingFactor})`, + height, + width, + }; +}; + +export const contentPositioningStyles = () => { + return { + display: 'flex', + position: 'absolute', + left: 0, + top: 0, + }; +}; + +export const pageStyles = () => { + return { + display: 'flex', + flexDirection: 'column', + margin: '0 24px', + }; +}; + +export const fieldContainerStyles = (extendedHeight?: boolean) => { + return { + display: 'flex', + height: extendedHeight ? 149.6 : 104, + borderRadius: '12px', + borderWidth: '1px', + justifyContent: 'space-between', + borderStyle: 'solid', + }; +}; + +export const amountContainerStyles = () => { + return { + display: 'flex', + flexDirection: 'column', + marginLeft: '16px', + width: '100%', + }; +}; + +export const amountTextStyles = (theme?: ImageTheme) => { + return { + color: theme === 'dark' ? '#ffffff' : '#000000', + margin: 0, + fontSize: 24, + lineHeight: 1, + fontWeight: 600, + }; +}; + +export const tokenTextStyles = ( + type: 'amount' | 'symbol', + theme?: ImageTheme, +) => { + let additionalStyles = {}; + if (type === 'amount') { + additionalStyles = { + color: theme === 'dark' ? '#ffffff' : '#747474', + }; + } else if (type === 'symbol') { + additionalStyles = { + color: theme === 'dark' ? 'rgb(187, 187, 187)' : '#747474', + marginLeft: 64, + }; + } + + return { + margin: 0, + fontSize: 12, + fontWeight: 500, + marginTop: 10, + letterSpacing: '0.00938em', + ...additionalStyles, + }; +}; diff --git a/src/components/Menus/WalletMenu/WalletCard.style.ts b/src/components/Menus/WalletMenu/WalletCard.style.ts index 7bd599a5f..82a16b214 100644 --- a/src/components/Menus/WalletMenu/WalletCard.style.ts +++ b/src/components/Menus/WalletMenu/WalletCard.style.ts @@ -1,13 +1,10 @@ 'use client'; -import { avatarMask32 } from '@/components/Mask.style'; +import { ButtonTransparent } from '@/components/Button'; import type { Breakpoint } from '@mui/material'; -import { alpha } from '@mui/material'; -import { Avatar, Badge, Container } from '@mui/material'; -import { styled } from '@mui/material/styles'; +import { alpha, Avatar, Badge, Container } from '@mui/material'; import type { ButtonProps as MuiButtonProps } from '@mui/material/Button/Button'; -import { getContrastAlphaColor } from '@/utils/colors'; -import { ButtonTransparent } from '@/components/Button'; +import { styled } from '@mui/material/styles'; export const WalletAvatar = styled(Avatar)(({ theme }) => ({ margin: 'auto', diff --git a/src/components/Menus/WalletMenu/WalletMenu.style.ts b/src/components/Menus/WalletMenu/WalletMenu.style.ts index ebbaf06ca..836ed4946 100644 --- a/src/components/Menus/WalletMenu/WalletMenu.style.ts +++ b/src/components/Menus/WalletMenu/WalletMenu.style.ts @@ -3,9 +3,7 @@ import { ButtonSecondary, ButtonTransparent } from '@/components/Button'; import { avatarMask32 } from '@/components/Mask.style'; import type { Breakpoint, ButtonProps } from '@mui/material'; -import { Box, Drawer } from '@mui/material'; -import { alpha } from '@mui/material'; -import { Avatar, Badge, Container } from '@mui/material'; +import { alpha, Avatar, Badge, Container, Drawer } from '@mui/material'; import { styled } from '@mui/material/styles'; export interface WalletButtonProps extends ButtonProps { diff --git a/src/components/Widgets/Widget.style.ts b/src/components/Widgets/Widget.style.ts index 2c744013b..0c5f3ed84 100644 --- a/src/components/Widgets/Widget.style.ts +++ b/src/components/Widgets/Widget.style.ts @@ -38,7 +38,7 @@ export const WidgetWrapper = styled(Box, { transitionDuration: '.3s', transitionTimingFunction: 'ease-in-out', marginTop: 0, - maxHeight: '100%', + maxHeight: welcomeScreenClosed ? '100%' : '50vh', ...(!welcomeScreenClosed && { cursor: 'pointer', // add margin-top to widget-wrapper when welcome-screen is closed @@ -70,7 +70,7 @@ export const WidgetWrapper = styled(Box, { marginTop: !welcomeScreenClosed ? DEFAULT_WIDGET_TOP_OFFSET_VAR : 0, [`@media screen and (min-height: 700px)`]: { // set default widget height - height: DEFAULT_WIDGET_HEIGHT, + height: welcomeScreenClosed ? DEFAULT_WIDGET_HEIGHT : '50vh', marginTop: !welcomeScreenClosed ? // (mid viewheight - ≈ 2/3 of widget height - navbar height ) `calc( ${DEFAULT_WIDGET_TOP_OFFSET_VAR} - 40px )` diff --git a/src/components/Widgets/WidgetEvents.tsx b/src/components/Widgets/WidgetEvents.tsx index 55d745089..b24d8b9be 100644 --- a/src/components/Widgets/WidgetEvents.tsx +++ b/src/components/Widgets/WidgetEvents.tsx @@ -281,46 +281,46 @@ export function WidgetEvents() { } }; - const onTokenSearch = async ({ value, tokens }: TokenSearchProps) => { - const lowercaseValue = value?.toLowerCase(); - const { isValid, addressType } = isValidEvmOrSvmAddress(lowercaseValue); - const SearchNothingFound = tokens?.length > 0 ? false : true; - const tokenAddress = tokens?.length > 0 ? tokens?.[0]?.address : ''; - const tokenName = tokens?.length > 0 ? tokens?.[0]?.name : ''; - const tokenSymbol = tokens?.length > 0 ? tokens?.[0]?.symbol : ''; - const tokenChainId = tokens?.length > 0 ? tokens?.[0]?.chainId : ''; + // const onTokenSearch = async ({ value, tokens }: TokenSearchProps) => { + // const lowercaseValue = value?.toLowerCase(); + // const { isValid, addressType } = isValidEvmOrSvmAddress(lowercaseValue); + // const SearchNothingFound = tokens?.length > 0 ? false : true; + // const tokenAddress = tokens?.length > 0 ? tokens?.[0]?.address : ''; + // const tokenName = tokens?.length > 0 ? tokens?.[0]?.name : ''; + // const tokenSymbol = tokens?.length > 0 ? tokens?.[0]?.symbol : ''; + // const tokenChainId = tokens?.length > 0 ? tokens?.[0]?.chainId : ''; - trackEvent({ - category: TrackingCategory.WidgetEvent, - action: TrackingAction.OnTokenSearch, - label: `token_search`, - data: { - [TrackingEventParameter.SearchValue]: lowercaseValue, - [TrackingEventParameter.SearchIsAddress]: isValid, - [TrackingEventParameter.SearchAddressType]: addressType as string, - [TrackingEventParameter.SearchNumberOfResult]: tokens?.length, - [TrackingEventParameter.SearchNothingFound]: SearchNothingFound, - [TrackingEventParameter.SearchFirstResultAddress]: tokenAddress, - [TrackingEventParameter.SearchFirstResultName]: tokenName, - [TrackingEventParameter.SearchFirstResultSymbol]: tokenSymbol, - [TrackingEventParameter.SearchFirstResultChainId]: tokenChainId, - }, - }); - }; + // trackEvent({ + // category: TrackingCategory.WidgetEvent, + // action: TrackingAction.OnTokenSearch, + // label: `token_search`, + // data: { + // [TrackingEventParameter.SearchValue]: lowercaseValue, + // [TrackingEventParameter.SearchIsAddress]: isValid, + // [TrackingEventParameter.SearchAddressType]: addressType as string, + // [TrackingEventParameter.SearchNumberOfResult]: tokens?.length, + // [TrackingEventParameter.SearchNothingFound]: SearchNothingFound, + // [TrackingEventParameter.SearchFirstResultAddress]: tokenAddress, + // [TrackingEventParameter.SearchFirstResultName]: tokenName, + // [TrackingEventParameter.SearchFirstResultSymbol]: tokenSymbol, + // [TrackingEventParameter.SearchFirstResultChainId]: tokenChainId, + // }, + // }); + // }; - const onRouteSelected = async ({ route, routes }: RouteSelectedProps) => { - const position = routes.findIndex((elem: Route) => elem.id === route.id); - const data = handleRouteEventDetails(route, { - [TrackingEventParameter.RoutePosition]: position, - }); + // const onRouteSelected = async ({ route, routes }: RouteSelectedProps) => { + // const position = routes.findIndex((elem: Route) => elem.id === route.id); + // const data = handleRouteEventDetails(route, { + // [TrackingEventParameter.RoutePosition]: position, + // }); - trackEvent({ - category: TrackingCategory.WidgetEvent, - action: TrackingAction.OnRouteSelected, - label: `route_selected`, - data, - }); - }; + // trackEvent({ + // category: TrackingCategory.WidgetEvent, + // action: TrackingAction.OnRouteSelected, + // label: `route_selected`, + // data, + // }); + // }; widgetEvents.on(WidgetEvent.RouteExecutionStarted, onRouteExecutionStarted); widgetEvents.on(WidgetEvent.RouteExecutionUpdated, onRouteExecutionUpdated); @@ -344,8 +344,8 @@ export function WidgetEvents() { WidgetEvent.DestinationChainTokenSelected, onDestinationChainTokenSelection, ); - widgetEvents.on(WidgetEvent.RouteSelected, onRouteSelected); - widgetEvents.on(WidgetEvent.TokenSearch, onTokenSearch); + // widgetEvents.on(WidgetEvent.RouteSelected, onRouteSelected); + // widgetEvents.on(WidgetEvent.TokenSearch, onTokenSearch); // widgetEvents.on(WidgetEvent.WidgetExpanded, onWidgetExpanded); diff --git a/src/utils/image-generation/fetchChainData.ts b/src/utils/image-generation/fetchChainData.ts new file mode 100644 index 000000000..f1d3a61d4 --- /dev/null +++ b/src/utils/image-generation/fetchChainData.ts @@ -0,0 +1,20 @@ +import type { ChainId } from '@lifi/sdk'; +import { ChainType, getChains } from '@lifi/sdk'; +import { getChainById } from '../tokenAndChain'; + +export async function fetchChainData(chainId: ChainId | null) { + if (!chainId) { + return null; + } + try { + const formattedChainId = + typeof chainId !== 'number' ? parseInt(chainId) : chainId; + const chainsData = await getChains({ + chainTypes: [ChainType.EVM, ChainType.SVM], + }); + return getChainById(chainsData, formattedChainId as ChainId); + } catch (error) { + console.error(`Error fetching chain data: ${error}`); + return null; + } +} diff --git a/src/utils/image-generation/fetchTokenData.ts b/src/utils/image-generation/fetchTokenData.ts new file mode 100644 index 000000000..4f8438863 --- /dev/null +++ b/src/utils/image-generation/fetchTokenData.ts @@ -0,0 +1,17 @@ +import type { ChainId } from '@lifi/sdk'; +import { getToken } from '@lifi/sdk'; + +export async function fetchTokenData( + chainId: string | null, + tokenAddress: string | null, +) { + if (!chainId || !tokenAddress) { + return null; + } + try { + return await getToken(parseInt(chainId) as ChainId, tokenAddress); + } catch (error) { + console.error(`Error fetching token data: ${error}`); + return null; + } +} diff --git a/src/utils/image-generation/helpers.ts b/src/utils/image-generation/helpers.ts new file mode 100644 index 000000000..a4898c530 --- /dev/null +++ b/src/utils/image-generation/helpers.ts @@ -0,0 +1,22 @@ +export const getOffset = (type?: string, extendedHeight?: boolean) => { + if (type === 'amount') { + return 46; + } + if (type === 'quote') { + if (extendedHeight) { + return 56; + } + return 16; + } else { + return 46; + } +}; +export const getWidth = (type?: string, fullWidth?: boolean) => { + if (type === 'quote') { + return 315; + } else if (fullWidth) { + return 368; + } else { + return 174; + } +}; diff --git a/src/utils/image-generation/parseSearchParams.ts b/src/utils/image-generation/parseSearchParams.ts new file mode 100644 index 000000000..d6d5d28b5 --- /dev/null +++ b/src/utils/image-generation/parseSearchParams.ts @@ -0,0 +1,14 @@ +export function parseSearchParams(url: string) { + const searchParams = new URL(url).searchParams; + return { + amount: searchParams.get('amount'), + fromToken: searchParams.get('fromToken'), + fromChainId: searchParams.get('fromChainId'), + toToken: searchParams.get('toToken'), + toChainId: searchParams.get('toChainId'), + highlighted: searchParams.get('highlighted'), + theme: searchParams.get('theme'), + isSwap: searchParams.get('isSwap'), + amountUSD: searchParams.get('amountUSD'), + }; +}