diff --git a/apps/cowswap-frontend/src/common/pure/CoWAMMBanner/CoWAmmBannerContent.tsx b/apps/cowswap-frontend/src/common/pure/CoWAMMBanner/CoWAmmBannerContent.tsx new file mode 100644 index 0000000000..2a2fd6d5dd --- /dev/null +++ b/apps/cowswap-frontend/src/common/pure/CoWAMMBanner/CoWAmmBannerContent.tsx @@ -0,0 +1,396 @@ +import React from 'react' +import { useCallback, useMemo, useRef } from 'react' + +import ICON_ARROW from '@cowprotocol/assets/cow-swap/arrow.svg' +import ICON_CURVE from '@cowprotocol/assets/cow-swap/icon-curve.svg' +import ICON_PANCAKESWAP from '@cowprotocol/assets/cow-swap/icon-pancakeswap.svg' +import ICON_SUSHISWAP from '@cowprotocol/assets/cow-swap/icon-sushi.svg' +import ICON_UNISWAP from '@cowprotocol/assets/cow-swap/icon-uni.svg' +import ICON_STAR from '@cowprotocol/assets/cow-swap/star-shine.svg' +import { ProductLogo, ProductVariant, UI } from '@cowprotocol/ui' + +import SVG from 'react-inlinesvg' +import { Textfit } from 'react-textfit' + +import { upToSmall, useMediaQuery } from 'legacy/hooks/useMediaQuery' +import { useIsDarkMode } from 'legacy/state/user/hooks' + +import { ArrowBackground } from './arrowBackground' +import { LpToken, StateKey, dummyData, lpTokenConfig } from './dummyData' +import * as styledEl from './styled' + +import { BannerLocation, DEMO_DROPDOWN_OPTIONS } from './index' +import { TokenLogo } from '../../../../../../libs/tokens/src/pure/TokenLogo' +import { USDC, WBTC } from '@cowprotocol/common-const' +import { SupportedChainId } from '@cowprotocol/cow-sdk' +import { DummyDataType, TwoLpScenario, InferiorYieldScenario } from './dummyData' + +const lpTokenIcons: Record = { + [LpToken.UniswapV2]: ICON_UNISWAP, + [LpToken.Sushiswap]: ICON_SUSHISWAP, + [LpToken.PancakeSwap]: ICON_PANCAKESWAP, + [LpToken.Curve]: ICON_CURVE, +} + +interface CoWAmmBannerContentProps { + id: string + title: string + ctaText: string + location: BannerLocation + isDemo: boolean + selectedState: StateKey + setSelectedState: (state: StateKey) => void + dummyData: typeof dummyData + lpTokenConfig: typeof lpTokenConfig + onCtaClick: () => void + onClose: () => void +} + +function isTwoLpScenario(scenario: DummyDataType[keyof DummyDataType]): scenario is TwoLpScenario { + return 'uniV2Apr' in scenario && 'sushiApr' in scenario +} + +function isInferiorYieldScenario(scenario: DummyDataType[keyof DummyDataType]): scenario is InferiorYieldScenario { + return 'poolsCount' in scenario +} + +const renderTextfit = ( + content: React.ReactNode, + mode: 'single' | 'multi', + minFontSize: number, + maxFontSize: number, + key: string, +) => ( + + {content} + +) + +export function CoWAmmBannerContent({ + id, + title, + ctaText, + location, + isDemo, + selectedState, + setSelectedState, + dummyData, + lpTokenConfig, + onCtaClick, + onClose, +}: CoWAmmBannerContentProps) { + const isMobile = useMediaQuery(upToSmall) + const isDarkMode = useIsDarkMode() + const arrowBackgroundRef = useRef(null) + + const handleCTAMouseEnter = useCallback(() => { + if (arrowBackgroundRef.current) { + arrowBackgroundRef.current.style.visibility = 'visible' + arrowBackgroundRef.current.style.opacity = '1' + } + }, []) + + const handleCTAMouseLeave = useCallback(() => { + if (arrowBackgroundRef.current) { + arrowBackgroundRef.current.style.visibility = 'hidden' + arrowBackgroundRef.current.style.opacity = '0' + } + }, []) + + const { apr } = dummyData[selectedState] + + const aprMessage = useMemo(() => { + if (selectedState === 'uniV2InferiorWithLowAverageYield') { + const currentData = dummyData[selectedState] + if (isInferiorYieldScenario(currentData)) { + return `${currentData.poolsCount}+` + } + } + return `+${apr.toFixed(1)}%` + }, [selectedState, apr, dummyData]) + + const comparisonMessage = useMemo(() => { + const currentData = dummyData[selectedState] + + if (!currentData) { + return 'Invalid state selected' + } + + const renderPoolInfo = (poolName: string) => ( + + higher APR available for your {poolName} pool: + +
+ +
+ WBTC-USDC +
+
+ ) + + if (isTwoLpScenario(currentData)) { + if (selectedState === 'twoLpsMixed') { + return renderPoolInfo('UNI-V2') + } else if (selectedState === 'twoLpsBothSuperior') { + const { uniV2Apr, sushiApr } = currentData + const higherAprPool = uniV2Apr > sushiApr ? 'UNI-V2' : 'SushiSwap' + return renderPoolInfo(higherAprPool) + } + } + + if (selectedState === 'uniV2Superior') { + return renderPoolInfo('UNI-V2') + } + + if (selectedState === 'uniV2InferiorWithLowAverageYield' && isInferiorYieldScenario(currentData)) { + return 'pools available to get yield on your assets!' + } + + if (currentData.hasCoWAmmPool) { + return `yield over average ${currentData.comparison} pool` + } else { + const tokens = lpTokenConfig[selectedState] + if (tokens.length > 1) { + const tokenNames = tokens + .map((token) => { + switch (token) { + case LpToken.UniswapV2: + return 'UNI-V2' + case LpToken.Sushiswap: + return 'Sushi' + case LpToken.PancakeSwap: + return 'PancakeSwap' + case LpToken.Curve: + return 'Curve' + default: + return '' + } + }) + .filter(Boolean) + + return `yield over average ${tokenNames.join(', ')} pool${tokenNames.length > 1 ? 's' : ''}` + } else { + return `yield over average UNI-V2 pool` + } + } + }, [selectedState, location, lpTokenConfig, isDarkMode]) + + const lpEmblems = useMemo(() => { + const tokens = lpTokenConfig[selectedState] + const totalItems = tokens.length + + const renderEmblemContent = () => ( + <> + + + + + + + + ) + + if (totalItems === 0 || selectedState === 'uniV2InferiorWithLowAverageYield') { + return ( + + + {selectedState === 'uniV2InferiorWithLowAverageYield' ? ( + + ) : ( + + )} + + {renderEmblemContent()} + + ) + } + + return ( + + + {tokens.map((token, index) => ( + + + + ))} + + {renderEmblemContent()} + + ) + }, [selectedState, lpTokenConfig, lpTokenIcons]) + + const renderProductLogo = useCallback( + (color: string) => ( + + ), + [], + ) + + const renderStarIcon = useCallback( + (props: any) => ( + + + + ), + [], + ) + + const renderTokenSelectorContent = () => ( + + + + + {renderProductLogo(isDarkMode ? UI.COLOR_COWAMM_LIGHT_GREEN : UI.COLOR_COWAMM_DARK_GREEN)} + {title} + + + {renderStarIcon({ size: 26, top: -16, right: 80, color: `var(${UI.COLOR_COWAMM_LIGHTER_GREEN})` })} +

{renderTextfit(aprMessage, 'single', 35, 65, `apr-${selectedState}`)}

+ + {renderTextfit(comparisonMessage, 'multi', 15, isMobile ? 15 : 21, `comparison-${selectedState}`)} + + {renderStarIcon({ size: 16, bottom: 3, right: 20, color: `var(${UI.COLOR_COWAMM_LIGHTER_GREEN})` })} +
+ + {ctaText} + +
+
+ ) + + const renderGlobalContent = () => { + return ( + + + + {renderProductLogo(UI.COLOR_COWAMM_LIGHT_GREEN)} + {title} + + + {renderStarIcon({ size: 36, top: -17, right: 80 })} +

{renderTextfit(aprMessage, 'single', isMobile ? 40 : 80, isMobile ? 50 : 80, `apr-${selectedState}`)}

+ + {renderTextfit(comparisonMessage, 'multi', 10, isMobile ? 21 : 28, `comparison-${selectedState}`)} + + {renderStarIcon({ size: 26, bottom: -10, right: 20 })} +
+ + {!isMobile && ( + + + {renderTextfit( + <> + One-click convert, boost yield + , + 'multi', + 10, + 30, + `boost-yield-${selectedState}`, + )} + + {lpEmblems} + + )} + + + {ctaText} + + + Pool analytics ↗ + + +
+ ) + } + + const renderDemoDropdown = () => ( + setSelectedState(e.target.value as StateKey)}> + {DEMO_DROPDOWN_OPTIONS.map((option) => ( + + ))} + + ) + + const content = (() => { + switch (location) { + case BannerLocation.TokenSelector: + return renderTokenSelectorContent() + case BannerLocation.Global: + return renderGlobalContent() + default: + return null + } + })() + + if (!content) { + return null + } + + return ( +
+ {content} + {isDemo && renderDemoDropdown()} +
+ ) +} diff --git a/apps/cowswap-frontend/src/common/pure/CoWAMMBanner/arrowBackground.tsx b/apps/cowswap-frontend/src/common/pure/CoWAMMBanner/arrowBackground.tsx new file mode 100644 index 0000000000..a4677c4674 --- /dev/null +++ b/apps/cowswap-frontend/src/common/pure/CoWAMMBanner/arrowBackground.tsx @@ -0,0 +1,88 @@ +import { forwardRef, useMemo } from 'react' +import { memo } from 'react' + +import { UI } from '@cowprotocol/ui' + +import styled from 'styled-components/macro' + +const ArrowBackgroundWrapper = styled.div` + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + pointer-events: none; + visibility: hidden; + opacity: 0; + transition: opacity 0.3s ease-in-out; +` + +const MIN_FONT_SIZE = 18 +const MAX_FONT_SIZE = 42 + +const Arrow = styled.div<{ delay: number; color: string; fontSize: number }>` + position: absolute; + font-size: ${({ fontSize }) => fontSize}px; + color: ${({ color }) => color}; + animation: float 2s infinite linear; + animation-delay: ${({ delay }) => delay}s; + + @keyframes float { + 0% { + transform: translateY(100%); + opacity: 0; + } + 10% { + opacity: 0.3; + } + 50% { + opacity: 0.3; + } + 90% { + opacity: 0.2; + } + 100% { + transform: translateY(-100%); + opacity: 0; + } + } +` + +export interface ArrowBackgroundProps { + count?: number + color?: string +} + +export const ArrowBackground = memo( + forwardRef( + ({ count = 36, color = `var(${UI.COLOR_COWAMM_LIGHT_GREEN})` }, ref) => { + const arrows = useMemo(() => { + return Array.from({ length: count }, (_, index) => ({ + delay: (index / count) * 4, + left: `${Math.random() * 100}%`, + top: `${Math.random() * 100}%`, + fontSize: Math.floor(Math.random() * (MAX_FONT_SIZE - MIN_FONT_SIZE + 1)) + MIN_FONT_SIZE, + })) + }, [count]) + + return ( + + {arrows.map((arrow, index) => ( + + ↑ + + ))} + + ) + }, + ), +) diff --git a/apps/cowswap-frontend/src/common/pure/CoWAMMBanner/cowAmmBannerState.ts b/apps/cowswap-frontend/src/common/pure/CoWAMMBanner/cowAmmBannerState.ts new file mode 100644 index 0000000000..bc0c9b3054 --- /dev/null +++ b/apps/cowswap-frontend/src/common/pure/CoWAMMBanner/cowAmmBannerState.ts @@ -0,0 +1,5 @@ +import { atom } from 'jotai' + +import { StateKey } from './dummyData' + +export const cowAmmBannerStateAtom = atom('noLp') diff --git a/apps/cowswap-frontend/src/common/pure/CoWAMMBanner/dummyData.ts b/apps/cowswap-frontend/src/common/pure/CoWAMMBanner/dummyData.ts new file mode 100644 index 0000000000..26492684f6 --- /dev/null +++ b/apps/cowswap-frontend/src/common/pure/CoWAMMBanner/dummyData.ts @@ -0,0 +1,115 @@ +export enum LpToken { + UniswapV2 = 'UniswapV2', + Sushiswap = 'Sushiswap', + PancakeSwap = 'PancakeSwap', + Curve = 'Curve', +} + +type BaseScenario = { + readonly apr: number + readonly comparison: string + readonly hasCoWAmmPool: boolean +} + +export type TwoLpScenario = BaseScenario & { + readonly uniV2Apr: number + readonly sushiApr: number +} + +export type InferiorYieldScenario = BaseScenario & { + readonly poolsCount: number +} + +export type DummyDataType = { + noLp: BaseScenario + uniV2Superior: BaseScenario + uniV2Inferior: BaseScenario + sushi: BaseScenario + curve: BaseScenario + pancake: BaseScenario & { readonly isYieldSuperior: boolean } + twoLpsMixed: TwoLpScenario + twoLpsBothSuperior: TwoLpScenario + threeLps: BaseScenario + fourLps: BaseScenario + uniV2InferiorWithLowAverageYield: InferiorYieldScenario +} + +export const dummyData: DummyDataType = { + noLp: { + apr: 1.5, + comparison: 'average UNI-V2 pool', + hasCoWAmmPool: false, + }, + uniV2Superior: { + apr: 2.1, + comparison: 'UNI-V2', + hasCoWAmmPool: true, + }, + uniV2Inferior: { + apr: 1.2, + comparison: 'UNI-V2', + hasCoWAmmPool: true, + }, + sushi: { + apr: 1.8, + comparison: 'SushiSwap', + hasCoWAmmPool: true, + }, + curve: { + apr: 1.3, + comparison: 'Curve', + hasCoWAmmPool: true, + }, + pancake: { + apr: 2.5, + comparison: 'PancakeSwap', + hasCoWAmmPool: true, + isYieldSuperior: true, + }, + twoLpsMixed: { + apr: 2.5, + comparison: 'UNI-V2 and SushiSwap', + hasCoWAmmPool: true, + uniV2Apr: 3.0, + sushiApr: 1.8, + } as TwoLpScenario, + twoLpsBothSuperior: { + apr: 3.2, + comparison: 'UNI-V2 and SushiSwap', + hasCoWAmmPool: true, + uniV2Apr: 3.5, + sushiApr: 2.9, + } as TwoLpScenario, + threeLps: { + apr: 2.2, + comparison: 'UNI-V2, SushiSwap, and Curve', + hasCoWAmmPool: false, + }, + fourLps: { + apr: 2.4, + comparison: 'UNI-V2, SushiSwap, Curve, and PancakeSwap', + hasCoWAmmPool: false, + }, + uniV2InferiorWithLowAverageYield: { + apr: 1.2, + comparison: 'UNI-V2', + hasCoWAmmPool: true, + poolsCount: 195, + }, +} + +export type StateKey = keyof typeof dummyData + +export const lpTokenConfig: Record = { + noLp: [], + uniV2Superior: [LpToken.UniswapV2], + uniV2Inferior: [LpToken.UniswapV2], + sushi: [LpToken.Sushiswap], + curve: [LpToken.Curve], + pancake: [LpToken.PancakeSwap], + twoLpsMixed: [LpToken.UniswapV2, LpToken.Sushiswap], + twoLpsBothSuperior: [LpToken.UniswapV2, LpToken.Sushiswap], + threeLps: [LpToken.UniswapV2, LpToken.Sushiswap, LpToken.Curve], + fourLps: [LpToken.UniswapV2, LpToken.Sushiswap, LpToken.Curve, LpToken.PancakeSwap], + uniV2InferiorWithLowAverageYield: [LpToken.UniswapV2], +} diff --git a/apps/cowswap-frontend/src/common/pure/CoWAMMBanner/index.tsx b/apps/cowswap-frontend/src/common/pure/CoWAMMBanner/index.tsx index 265c32cc46..5f831b0ece 100644 --- a/apps/cowswap-frontend/src/common/pure/CoWAMMBanner/index.tsx +++ b/apps/cowswap-frontend/src/common/pure/CoWAMMBanner/index.tsx @@ -1,237 +1,84 @@ -import { Media } from '@cowprotocol/ui' -import { ClosableBanner } from '@cowprotocol/ui' +import { useAtom } from 'jotai' +import { useCallback } from 'react' -import { X } from 'react-feather' -import styled from 'styled-components/macro' +import { ClosableBanner } from '@cowprotocol/ui' +import { useIsSmartContractWallet } from '@cowprotocol/wallet' import { cowAnalytics } from 'modules/analytics' -const BannerWrapper = styled.div` - --darkGreen: #194d05; - --lightGreen: #bcec79; - - position: fixed; - top: 76px; - right: 10px; - z-index: 3; - width: 400px; - height: 345px; - border-radius: 24px; - background-color: var(--darkGreen); - color: var(--lightGreen); - padding: 24px; - display: flex; - flex-flow: column wrap; - align-items: center; - justify-content: center; - gap: 24px; - overflow: hidden; - - ${Media.upToSmall()} { - width: 100%; - height: auto; - left: 0; - right: 0; - margin: 0 auto; - bottom: 57px; - top: initial; - border-bottom-left-radius: 0; - border-bottom-right-radius: 0; - box-shadow: 0 0 0 100vh rgb(0 0 0 / 40%); - z-index: 10; - } - - > i { - position: absolute; - top: -30px; - left: -30px; - width: 166px; - height: 42px; - border: 1px solid var(--lightGreen); - border-radius: 16px; - border-left: 0; - animation: bounceLeftRight 7s infinite; - animation-delay: 2s; - } - - &::before { - content: ''; - position: absolute; - top: 100px; - left: -23px; - width: 56px; - height: 190px; - border: 1px solid var(--lightGreen); - border-radius: 16px; - border-left: 0; - animation: bounceUpDown 7s infinite; - } - - &::after { - content: ''; - position: absolute; - bottom: -21px; - right: 32px; - width: 76px; - height: 36px; - border: 1px solid var(--lightGreen); - border-radius: 16px; - border-bottom: 0; - animation: bounceLeftRight 7s infinite; - animation-delay: 1s; - } - - > div { - display: flex; - flex-flow: column wrap; - gap: 24px; - width: 100%; - max-width: 75%; - margin: 0 auto; - } - - @keyframes bounceUpDown { - 0%, - 100% { - transform: translateY(0); - } - 50% { - transform: translateY(-7px); - } - } - - @keyframes bounceLeftRight { - 0%, - 100% { - transform: translateX(0); - } - 50% { - transform: translateX(7px); - } - } -` - -const Title = styled.h2` - font-size: 34px; - font-weight: bold; - margin: 0; - - ${Media.upToSmall()} { - font-size: 26px; - } -` - -const Description = styled.p` - font-size: 17px; - line-height: 1.5; - margin: 0; - - ${Media.upToSmall()} { - font-size: 15px; - } -` - -const CTAButton = styled.button` - background-color: var(--lightGreen); - color: var(--darkGreen); - border: none; - border-radius: 56px; - padding: 12px 24px; - font-size: 18px; - font-weight: bold; - cursor: pointer; - width: 100%; - max-width: 75%; - display: flex; - align-items: center; - justify-content: center; - gap: 8px; - transition: border-radius 0.2s ease-in-out; - - &:hover { - border-radius: 16px; - - > i { - transform: rotate(45deg); - } - } - - > i { - font-size: 22px; - font-weight: bold; - font-style: normal; - line-height: 1; - margin: 3px 0 0; - transition: transform 0.2s ease-in-out; - animation: spin 6s infinite; - } - - @keyframes spin { - 0% { - transform: rotate(0deg); - } - 20% { - transform: rotate(360deg); - } - 100% { - transform: rotate(360deg); - } - } -` +import { CoWAmmBannerContent } from './CoWAmmBannerContent' +import { cowAmmBannerStateAtom } from './cowAmmBannerState' +import { dummyData, lpTokenConfig } from './dummyData' + +const IS_DEMO_MODE = true +const ANALYTICS_URL = 'https://cow.fi/pools?utm_source=swap.cow.fi&utm_medium=web&utm_content=cow_amm_banner' + +export const DEMO_DROPDOWN_OPTIONS = [ + { value: 'noLp', label: '🚫 No LP Tokens' }, + { value: 'uniV2Superior', label: '⬆️ 🐴 UNI-V2 LP (Superior Yield)' }, + { value: 'uniV2Inferior', label: '⬇️ 🐴 UNI-V2 LP (Inferior Yield)' }, + { value: 'uniV2InferiorWithLowAverageYield', label: '⬇️ 🐴 UNI-V2 LP (Inferior Yield, Lower Average)' }, + { value: 'sushi', label: '⬇️ 🍣 SushiSwap LP (Inferior Yield)' }, + { value: 'curve', label: '⬇️ 🌈 Curve LP (Inferior Yield)' }, + { value: 'pancake', label: '⬇️ 🥞 PancakeSwap LP (Inferior Yield)' }, + { value: 'twoLpsMixed', label: '⬆️ 🐴 UNI-V2 (Superior) & ⬇️ 🍣 SushiSwap (Inferior) LPs' }, + { value: 'twoLpsBothSuperior', label: '⬆️ 🐴 UNI-V2 & ⬆️ 🍣 SushiSwap LPs (Both Superior, but UNI-V2 is higher)' }, + { value: 'threeLps', label: '⬇️ 🐴 UNI-V2, 🍣 SushiSwap & 🌈 Curve LPs (Inferior Yield)' }, + { value: 'fourLps', label: '⬇️ 🐴 UNI-V2, 🍣 SushiSwap, 🌈 Curve & 🥞 PancakeSwap LPs (Inferior Yield)' }, +] + +export enum BannerLocation { + Global = 'global', + TokenSelector = 'tokenSelector', +} -const CloseButton = styled(X)` - position: absolute; - top: 16px; - right: 16px; - cursor: pointer; - color: var(--lightGreen); - opacity: 0.6; - transition: opacity 0.2s ease-in-out; +interface BannerProps { + location: BannerLocation +} - &:hover { - opacity: 1; - } -` +export function CoWAmmBanner({ location }: BannerProps) { + const [selectedState, setSelectedState] = useAtom(cowAmmBannerStateAtom) -export function CoWAmmBanner() { - const handleCTAClick = () => { + const handleCTAClick = useCallback(() => { cowAnalytics.sendEvent({ category: 'CoW Swap', - action: 'CoW AMM Banner CTA Clicked', + action: `CoW AMM Banner [${location}] CTA Clicked`, }) - window.open( - 'https://balancer.fi/pools/cow?utm_source=swap.cow.fi&utm_medium=web&utm_content=cow_amm_banner', - '_blank' - ) - } + window.open(ANALYTICS_URL, '_blank') + }, [location]) - const handleClose = () => { + const handleClose = useCallback(() => { cowAnalytics.sendEvent({ category: 'CoW Swap', - action: 'CoW AMM Banner Closed', + action: `CoW AMM Banner [${location}] Closed`, }) - } - - return ClosableBanner('cow_amm_banner', (close) => ( - - - { - handleClose() - close() - }} - /> -
- Now live: the first MEV-capturing AMM - - CoW AMM shields you from LVR, so you can provide liquidity with less risk and more rewards. - -
- - LP on CoW AMM - -
+ }, [location]) + + const handleBannerClose = useCallback(() => { + handleClose() + }, [handleClose]) + + const bannerId = `cow_amm_banner_2024_va_${location}` + + const isSmartContractWallet = useIsSmartContractWallet() + + return ClosableBanner(bannerId, (close) => ( + { + handleBannerClose() + close() + }} + /> )) } diff --git a/apps/cowswap-frontend/src/common/pure/CoWAMMBanner/styled.ts b/apps/cowswap-frontend/src/common/pure/CoWAMMBanner/styled.ts new file mode 100644 index 0000000000..ba9093db41 --- /dev/null +++ b/apps/cowswap-frontend/src/common/pure/CoWAMMBanner/styled.ts @@ -0,0 +1,484 @@ +import { UI, Media } from '@cowprotocol/ui' + +import { X } from 'react-feather' +import styled, { keyframes } from 'styled-components/macro' +import { TokenLogoWrapper } from '../../../../../../libs/tokens/src/pure/TokenLogo' + +const arrowUpAnimation = keyframes` + 0% { + transform: translateY(100%); + opacity: 0; + } + 50% { + opacity: 1; + } + 100% { + transform: translateY(-100%); + opacity: 0; + } +` + +export const BannerWrapper = styled.div` + position: fixed; + top: 76px; + right: 10px; + z-index: 3; + width: 485px; + height: auto; + border-radius: 24px; + background-color: var(${UI.COLOR_COWAMM_DARK_GREEN}); + color: var(${UI.COLOR_COWAMM_DARK_GREEN}); + padding: 20px; + display: flex; + flex-flow: column wrap; + align-items: center; + justify-content: center; + gap: 20px; + overflow: hidden; + overflow: hidden; + transition: transform 0.2s ease; + + ${Media.upToSmall()} { + width: 100%; + height: auto; + left: 0; + right: 0; + margin: 0 auto; + bottom: 57px; + top: initial; + border-radius: 24px 24px 0 0; + box-shadow: 0 0 0 100vh rgb(0 0 0 / 40%); + z-index: 10; + } +` + +export const CloseButton = styled(X)<{ color?: string; top?: number }>` + position: absolute; + top: ${({ top = 16 }) => top}px; + right: 16px; + cursor: pointer; + color: ${({ color }) => color || `var(${UI.COLOR_COWAMM_LIGHT_GREEN})`}; + opacity: 0.6; + transition: opacity 0.2s ease-in-out; + + &:hover { + opacity: 1; + } +` + +export const Title = styled.h2<{ color?: string }>` + display: flex; + align-items: center; + gap: 8px; + font-size: 18px; + font-weight: bold; + margin: 0 auto 0 0; + color: ${({ color }) => color || `var(${UI.COLOR_COWAMM_LIGHT_GREEN})`}; + + ${Media.upToSmall()} { + font-size: 26px; + } +` + +export const Card = styled.div<{ + bgColor?: string + color?: string + height?: number | 'max-content' + borderColor?: string + borderWidth?: number + padding?: string + gap?: string +}>` + --default-height: 150px; + display: flex; + flex-flow: row nowrap; + align-items: center; + justify-content: center; + gap: ${({ gap }) => gap || '24px'}; + font-size: 30px; + line-height: 1.2; + font-weight: 500; + margin: 0; + width: 100%; + max-width: 100%; + height: ${({ height }) => + height === 'max-content' ? 'max-content' : height ? `${height}px` : 'var(--default-height)'}; + max-height: ${({ height }) => + height === 'max-content' ? 'max-content' : height ? `${height}px` : 'var(--default-height)'}; + border-radius: 16px; + padding: ${({ padding }) => padding || '24px'}; + background: ${({ bgColor }) => bgColor || 'transparent'}; + color: ${({ color }) => color || 'inherit'}; + border: ${({ borderWidth, borderColor }) => borderWidth && borderColor && `${borderWidth}px solid ${borderColor}`}; + position: relative; + + ${Media.upToSmall()} { + flex-flow: column wrap; + height: auto; + max-height: initial; + gap: 8px; + } + + > h3, + > span { + display: flex; + align-items: center; + justify-content: center; + margin: 0; + width: max-content; + height: 100%; + max-height: 100%; + color: inherit; + + ${Media.upToSmall()} { + width: 100%; + text-align: center; + } + + > div { + width: 100%; + height: 100%; + display: flex; + align-items: center; + justify-content: center; + } + } + + > h3 { + font-weight: bold; + letter-spacing: -2px; + } + + > span { + font-weight: inherit; + } + + > span b { + font-weight: 900; + color: var(${UI.COLOR_COWAMM_LIGHTER_GREEN}); + } +` + +export const PoolInfo = styled.div<{ + flow?: 'column' | 'row' + align?: 'flex-start' | 'center' + color?: string + bgColor?: string + tokenBorderColor?: string +}>` + display: flex; + align-items: ${({ align = 'flex-start' }) => align}; + flex-flow: ${({ flow = 'column' }) => flow}; + font-size: inherit; + gap: 10px; + + ${Media.upToSmall()} { + flex-flow: column wrap; + } + + > i { + font-style: normal; + background: ${({ bgColor }) => bgColor || `var(${UI.COLOR_COWAMM_LIGHT_BLUE})`}; + color: ${({ color }) => color || `var(${UI.COLOR_COWAMM_DARK_BLUE})`}; + display: flex; + flex-flow: row; + gap: 6px; + padding: 6px 12px 6px 6px; + height: min-content; + border-radius: 62px; + width: min-content; + box-shadow: var(${UI.BOX_SHADOW_2}); + + ${Media.upToSmall()} { + margin: 0 auto; + } + } + + > i > div { + display: flex; + } + + ${TokenLogoWrapper} { + border: 2px solid ${({ tokenBorderColor }) => tokenBorderColor || `var(${UI.COLOR_COWAMM_LIGHT_BLUE})`}; + + :last-child { + margin-left: -18px; + } + } +` + +export const CTAButton = styled.button<{ + bgColor?: string + bgHoverColor?: string + color?: string + size?: number + fontSize?: number + fontSizeMobile?: number +}>` + --size: ${({ size = 58 }) => size}px; + --font-size: ${({ fontSize = 24 }) => fontSize}px; + background: ${({ bgColor }) => bgColor || `var(${UI.COLOR_COWAMM_LIGHT_GREEN})`}; + color: ${({ color }) => color || `var(${UI.COLOR_COWAMM_DARK_GREEN})`}; + border: none; + border-radius: var(--size); + min-height: var(--size); + padding: 12px 24px; + font-size: var(--font-size); + font-weight: bold; + cursor: pointer; + width: 100%; + max-width: 100%; + display: flex; + align-items: center; + justify-content: center; + gap: 8px; + position: relative; + overflow: hidden; + z-index: 1; + + ${Media.upToSmall()} { + --font-size: ${({ fontSizeMobile = 21 }) => fontSizeMobile}px; + min-height: initial; + } + + &::before { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: ${({ bgHoverColor }) => bgHoverColor || `var(${UI.COLOR_COWAMM_LIGHTER_GREEN})`}; + z-index: -1; + transform: scaleX(0); + transform-origin: left; + transition: transform 2s ease-out; + } + + &:hover::before { + transform: scaleX(1); + } + + > * { + z-index: 2; + } +` + +export const SecondaryLink = styled.a` + color: var(${UI.COLOR_COWAMM_LIGHT_GREEN}); + font-size: 14px; + font-weight: 500; + text-decoration: none; + + &:hover { + text-decoration: underline; + } +` + +export const DEMO_DROPDOWN = styled.select` + position: fixed; + bottom: 20px; + right: 20px; + z-index: 999999999; + padding: 5px; + font-size: 14px; + + ${Media.upToSmall()} { + bottom: initial; + top: 0; + width: 100%; + right: 0; + left: 0; + } +` + +export const StarIcon = styled.div<{ + color?: string + size?: number + top?: number | 'initial' + left?: number | 'initial' + right?: number | 'initial' + bottom?: number | 'initial' +}>` + width: ${({ size = 16 }) => size}px; + height: ${({ size = 16 }) => size}px; + position: absolute; + top: ${({ top }) => (top === 'initial' ? 'initial' : top != null ? `${top}px` : 'initial')}; + left: ${({ left }) => (left === 'initial' ? 'initial' : left != null ? `${left}px` : 'initial')}; + right: ${({ right }) => (right === 'initial' ? 'initial' : right != null ? `${right}px` : 'initial')}; + bottom: ${({ bottom }) => (bottom === 'initial' ? 'initial' : bottom != null ? `${bottom}px` : 'initial')}; + color: ${({ color }) => color ?? `var(${UI.COLOR_COWAMM_LIGHT_BLUE})`}; + + > svg > path { + fill: ${({ color }) => color ?? 'currentColor'}; + } +` + +export const LpEmblems = styled.div` + display: flex; + gap: 8px; + width: 100%; + justify-content: center; + align-items: center; +` + +export const LpEmblemItemsWrapper = styled.div<{ totalItems: number }>` + display: ${({ totalItems }) => (totalItems > 2 ? 'grid' : 'flex')}; + gap: ${({ totalItems }) => (totalItems > 2 ? '0' : '8px')}; + width: 100%; + justify-content: center; + align-items: center; + + ${({ totalItems }) => + totalItems === 3 && + ` + grid-template: 1fr 1fr / 1fr 1fr; + justify-items: center; + + > :first-child { + grid-column: 1 / -1; + } + `} + + ${({ totalItems }) => + totalItems === 4 && + ` + grid-template: 1fr 1fr / 1fr 1fr; + `} +` + +export const LpEmblemItem = styled.div<{ + totalItems: number + index: number + isUSDC?: boolean +}>` + --size: ${({ totalItems }) => + totalItems === 4 ? '50px' : totalItems === 3 ? '65px' : totalItems === 2 ? '80px' : '104px'}; + width: var(--size); + height: var(--size); + padding: ${({ totalItems, isUSDC }) => + isUSDC ? '9px' : totalItems === 4 ? '10px' : totalItems >= 2 ? '15px' : '20px'}; + border-radius: 50%; + background: var(${UI.COLOR_COWAMM_DARK_GREEN}); + color: var(${UI.COLOR_COWAMM_LIGHT_GREEN}); + border: ${({ totalItems }) => + totalItems > 2 ? `2px solid var(${UI.COLOR_COWAMM_GREEN})` : `4px solid var(${UI.COLOR_COWAMM_GREEN})`}; + display: flex; + align-items: center; + justify-content: center; + position: relative; + + > svg { + width: 100%; + height: 100%; + } + + ${({ totalItems, index }) => { + const styleMap: Record> = { + 2: { + 0: 'margin-right: -42px;', + }, + 3: { + 0: 'margin-bottom: -20px; z-index: 10;', + 1: 'margin-top: -20px;', + 2: 'margin-top: -20px;', + }, + 4: { + 0: 'margin: -5px -10px -5px 0; z-index: 10;', + 1: 'margin-bottom: -5px; z-index: 10;', + 2: 'margin: -5px -10px 0 0;', + 3: 'margin-top: -5px;', + }, + } + + return styleMap[totalItems]?.[index] || '' + }} + + ${TokenLogoWrapper} { + width: 100%; + height: 100%; + } +` + +export const CoWAMMEmblemItem = styled.div` + --size: 104px; + width: var(--size); + height: var(--size); + border-radius: var(--size); + padding: 30px 30px 23px; + background: var(${UI.COLOR_COWAMM_LIGHT_GREEN}); + color: var(${UI.COLOR_COWAMM_DARK_GREEN}); + border: 4px solid var(${UI.COLOR_COWAMM_GREEN}); + display: flex; + align-items: center; + justify-content: center; +` + +export const EmblemArrow = styled.div` + --size: 32px; + width: var(--size); + height: var(--size); + min-width: var(--size); + border-radius: var(--size); + background: var(${UI.COLOR_COWAMM_DARK_GREEN}); + border: 3px solid var(${UI.COLOR_COWAMM_GREEN}); + margin: 0 -24px; + padding: 6px; + z-index: 10; + display: flex; + align-items: center; + justify-content: center; + color: var(${UI.COLOR_COWAMM_LIGHT_GREEN}); + + > svg > path { + fill: var(${UI.COLOR_COWAMM_LIGHT_GREEN}); + } +` + +export const ArrowBackground = styled.div` + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + pointer-events: none; + height: 100%; + width: 100%; + visibility: hidden; + opacity: 0; + transition: opacity 0.2s ease-in-out; +` + +export const Arrow = styled.span<{ delay: number }>` + position: absolute; + font-size: 28px; + color: rgba(255, 255, 255, 0.3); + animation: ${arrowUpAnimation} 1s linear infinite; + animation-delay: ${({ delay }) => delay}s; + left: ${() => Math.random() * 100}%; + font-weight: 500; +` + +export const TokenSelectorWrapper = styled.div` + z-index: 3; + width: 100%; + padding: 20px; + position: relative; +` + +export const TokenSelectorWrapperInner = styled.div<{ bgColor?: string; color?: string }>` + position: relative; + width: 100%; + height: auto; + border-radius: 24px; + background: ${({ bgColor }) => bgColor || `var(${UI.COLOR_COWAMM_LIGHT_GREEN})`}; + color: ${({ color }) => color || `var(${UI.COLOR_COWAMM_DARK_GREEN})`}; + padding: 14px; + margin: 0 auto; + display: flex; + flex-flow: column wrap; + align-items: center; + justify-content: center; + gap: 14px; + overflow: hidden; +` diff --git a/apps/cowswap-frontend/src/modules/application/containers/App/index.tsx b/apps/cowswap-frontend/src/modules/application/containers/App/index.tsx index e2db6d73bf..b8f9b4db77 100644 --- a/apps/cowswap-frontend/src/modules/application/containers/App/index.tsx +++ b/apps/cowswap-frontend/src/modules/application/containers/App/index.tsx @@ -93,6 +93,9 @@ export function App() { ) + // const { account } = useWalletInfo() + // const isChainIdUnsupported = useIsProviderNetworkUnsupported() + return ( }> @@ -127,6 +130,11 @@ export function App() { /> )} + {/* CoW AMM banner */} + {/*{!isInjectedWidgetMode && account && !isChainIdUnsupported && (*/} + {/* */} + {/*)}*/} + diff --git a/apps/cowswap-frontend/src/modules/tokensList/pure/SelectTokenModal/index.tsx b/apps/cowswap-frontend/src/modules/tokensList/pure/SelectTokenModal/index.tsx index 1dba5f772b..36b6ba37b2 100644 --- a/apps/cowswap-frontend/src/modules/tokensList/pure/SelectTokenModal/index.tsx +++ b/apps/cowswap-frontend/src/modules/tokensList/pure/SelectTokenModal/index.tsx @@ -88,7 +88,9 @@ export function SelectTokenModal(props: SelectTokenModalProps) { hideTooltip={hideFavoriteTokensTooltip} /> + + {inputValue.trim() ? ( ) : ( diff --git a/apps/cowswap-frontend/src/modules/tokensList/pure/TokensVirtualList/index.tsx b/apps/cowswap-frontend/src/modules/tokensList/pure/TokensVirtualList/index.tsx index 16e91c70a8..3637167df1 100644 --- a/apps/cowswap-frontend/src/modules/tokensList/pure/TokensVirtualList/index.tsx +++ b/apps/cowswap-frontend/src/modules/tokensList/pure/TokensVirtualList/index.tsx @@ -34,6 +34,8 @@ export function TokensVirtualList(props: TokensVirtualListProps) { const { values: balances, isLoading: balancesLoading } = balancesState const isWalletConnected = !!account + // const isInjectedWidgetMode = isInjectedWidget() + // const isChainIdUnsupported = useIsProviderNetworkUnsupported() const scrollTimeoutRef = useRef() const parentRef = useRef(null) @@ -65,6 +67,9 @@ export function TokensVirtualList(props: TokensVirtualListProps) { return ( + {/*{!isInjectedWidgetMode && account && !isChainIdUnsupported && (*/} + {/* */} + {/*)}*/} {items.map((virtualRow) => { diff --git a/libs/assets/src/cow-swap/icon-curve.svg b/libs/assets/src/cow-swap/icon-curve.svg new file mode 100644 index 0000000000..07688bbd5c --- /dev/null +++ b/libs/assets/src/cow-swap/icon-curve.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/libs/assets/src/cow-swap/icon-pancakeswap.svg b/libs/assets/src/cow-swap/icon-pancakeswap.svg new file mode 100644 index 0000000000..3367035850 --- /dev/null +++ b/libs/assets/src/cow-swap/icon-pancakeswap.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/libs/assets/src/cow-swap/icon-sushi.svg b/libs/assets/src/cow-swap/icon-sushi.svg new file mode 100644 index 0000000000..5e214be336 --- /dev/null +++ b/libs/assets/src/cow-swap/icon-sushi.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/libs/assets/src/cow-swap/icon-uni.svg b/libs/assets/src/cow-swap/icon-uni.svg new file mode 100644 index 0000000000..a88a09ae2e --- /dev/null +++ b/libs/assets/src/cow-swap/icon-uni.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/libs/ui/src/enum.ts b/libs/ui/src/enum.ts index 8a2986e83b..7b083a0990 100644 --- a/libs/ui/src/enum.ts +++ b/libs/ui/src/enum.ts @@ -60,6 +60,18 @@ export enum UI { COLOR_DANGER_BG = '--cow-color-danger-bg', COLOR_DANGER_TEXT = '--cow-color-danger-text', + // CoW AMM Colors + COLOR_COWAMM_DARK_GREEN = '--cow-color-cowamm-dark-green', + COLOR_COWAMM_DARK_GREEN_OPACITY_30 = '--cow-color-cowamm-dark-green-opacity-30', + COLOR_COWAMM_DARK_GREEN_OPACITY_15 = '--cow-color-cowamm-dark-green-opacity-15', + COLOR_COWAMM_GREEN = '--cow-color-cowamm-green', + COLOR_COWAMM_LIGHT_GREEN = '--cow-color-cowamm-light-green', + COLOR_COWAMM_LIGHT_GREEN_OPACITY_30 = '--cow-color-cowamm-light-green-opacity-30', + COLOR_COWAMM_LIGHTER_GREEN = '--cow-color-cowamm-lighter-green', + COLOR_COWAMM_BLUE = '--cow-color-cowamm-blue', + COLOR_COWAMM_DARK_BLUE = '--cow-color-cowamm-dark-blue', + COLOR_COWAMM_LIGHT_BLUE = '--cow-color-cowamm-light-blue', + // ================================================================================ // Badge diff --git a/libs/ui/src/theme/ThemeColorVars.tsx b/libs/ui/src/theme/ThemeColorVars.tsx index 6f2860c49a..03acc39dcf 100644 --- a/libs/ui/src/theme/ThemeColorVars.tsx +++ b/libs/ui/src/theme/ThemeColorVars.tsx @@ -97,8 +97,20 @@ export const ThemeColorVars = css` ${UI.COLOR_GREEN}: ${({ theme }) => theme.success}; ${UI.COLOR_RED}: ${({ theme }) => theme.danger}; + // CoW AMM Colors + ${UI.COLOR_COWAMM_DARK_GREEN}: #194d05; + ${UI.COLOR_COWAMM_DARK_GREEN_OPACITY_30}: ${() => transparentize('#194d05', 0.7)}; + ${UI.COLOR_COWAMM_DARK_GREEN_OPACITY_15}: ${() => transparentize('#194d05', 0.85)}; + ${UI.COLOR_COWAMM_GREEN}: #2b6f0b; + ${UI.COLOR_COWAMM_LIGHT_GREEN}: #bcec79; + ${UI.COLOR_COWAMM_LIGHT_GREEN_OPACITY_30}: ${() => transparentize('#bcec79', 0.7)}; + ${UI.COLOR_COWAMM_LIGHTER_GREEN}: #dcf8a7; + ${UI.COLOR_COWAMM_BLUE}: #3fc4ff; + ${UI.COLOR_COWAMM_DARK_BLUE}: #012F7A; + ${UI.COLOR_COWAMM_LIGHT_BLUE}: #ccf8ff; + // Base - ${UI.COLOR_CONTAINER_BG_02}: ${UI.COLOR_PAPER}; + ${UI.COLOR_CONTAINER_BG_02}: var(${UI.COLOR_PAPER}); ${UI.MODAL_BACKDROP}: var(${UI.COLOR_TEXT}); ${UI.BORDER_RADIUS_NORMAL}: 24px; ${UI.PADDING_NORMAL}: 24px; @@ -115,7 +127,7 @@ export const ThemeColorVars = css` ${UI.COLOR_TEXT_OPACITY_25}: ${({ theme }) => transparentize(theme.text, 0.75)}; ${UI.COLOR_TEXT_OPACITY_10}: ${({ theme }) => transparentize(theme.text, 0.9)}; ${UI.COLOR_TEXT2}: ${({ theme }) => transparentize(theme.text, 0.3)}; - ${UI.COLOR_LINK}: ${`var(${UI.COLOR_PRIMARY})`}; + ${UI.COLOR_LINK}: var(${UI.COLOR_PRIMARY}); ${UI.COLOR_LINK_OPACITY_10}: ${({ theme }) => transparentize(theme.info, 0.9)}; // Font Weights & Sizes