diff --git a/projects/dex-ui/src/assets/images/home-banner.svg b/projects/dex-ui/src/assets/images/home-banner.svg new file mode 100644 index 0000000000..bd43ec9f77 --- /dev/null +++ b/projects/dex-ui/src/assets/images/home-banner.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/projects/dex-ui/src/assets/images/tokens/WETH.svg b/projects/dex-ui/src/assets/images/tokens/WETH.svg index 854eb9194e..0a304a4438 100644 --- a/projects/dex-ui/src/assets/images/tokens/WETH.svg +++ b/projects/dex-ui/src/assets/images/tokens/WETH.svg @@ -1,5 +1,5 @@ - + diff --git a/projects/dex-ui/src/breakpoints.ts b/projects/dex-ui/src/breakpoints.ts index e901f52c42..d723f3efa9 100644 --- a/projects/dex-ui/src/breakpoints.ts +++ b/projects/dex-ui/src/breakpoints.ts @@ -1,3 +1,38 @@ export const size = { - mobile: '769px', -} \ No newline at end of file + mobile: "769px", + tablet: "1024px" +}; + +const mediaSizes = { + mobile: 769, + tablet: 1024, + desktop: 1200 +}; + +/// we add 1px to the mobile and tablet sizes so that the media queries don't overlap +export const mediaQuery = { + sm: { + // 769px & above + up: `@media (min-width: ${mediaSizes.mobile}px)`, + // 768px & below + only: `@media (max-width: ${mediaSizes.mobile - 1}px)` + }, + md: { + // 1024px & above + up: `@media (min-width: ${mediaSizes.tablet}px)`, + // between 769px & 1024px + only: `@media (min-width: ${mediaSizes.mobile}px) and (max-width: ${mediaSizes.tablet - 1}px)`, + // 1024px & below + down: `@media (max-width: ${mediaSizes.tablet}px)` + }, + lg: { + // 1200px & below + down: `@media (max-width: ${mediaSizes.desktop}px)`, + // 1200px & above + only: `@media (min-width: ${mediaSizes.desktop}px)` + }, + between: { + // between 769px & 1200px + smAndLg: `@media (min-width: ${mediaSizes.mobile}px) and (max-width: ${mediaSizes.desktop - 1}px)` + } +}; diff --git a/projects/dex-ui/src/components/Frame/ContractInfoMarquee.tsx b/projects/dex-ui/src/components/Frame/ContractInfoMarquee.tsx new file mode 100644 index 0000000000..2f40b627b8 --- /dev/null +++ b/projects/dex-ui/src/components/Frame/ContractInfoMarquee.tsx @@ -0,0 +1,125 @@ +import React from "react"; + +import styled, { keyframes } from "styled-components"; + +type ContractMarqueeInfo = Record; + +const CarouselData: ContractMarqueeInfo = { + ADDRESS: [ + { + display: "0x1584B668643617D18321a0BEc6EF3786F4b8Eb7B", + url: "https://etherscan.io/address/0xBA51AAAA95aeEFc1292515b36D86C51dC7877773" + } + ], + AUDIT: [ + { display: "HALBORN", url: "/halborn-basin-audit.pdf" }, + { display: "CYFRIN", url: "/cyfrin-basin-audit.pdf" }, + { display: "CODE4RENA", url: "https://code4rena.com/reports/2023-07-basin" } + ], + V1: [{ display: "WHITEPAPER", url: "/basin.pdf" }] +}; + +const speedPerItem = 16; // approx same speed as TokenMarquee +const itemGap = 24; +const numItems = 4; +const singleItemWidth = 1107.44; + +export const ContractInfoMarquee = () => { + const data = Object.entries(CarouselData); + + const totalItemWidth = numItems * singleItemWidth; + const totalGapWidth = numItems * itemGap; + + const totalWidth = totalItemWidth + totalGapWidth; + + const repeatableWidth = totalWidth / numItems; + const animationDuration = numItems * speedPerItem; + + return ( + + + <> + {Array(numItems) + .fill(null) + .map((_, idx) => ( + + {data.map(([key, data], idx) => ( + + + {key.toUpperCase()}: + {data.map(({ display, url }, i) => ( + + {display} + {data.length > 1 && i + 1 < data.length ? <>{","} : ""} + + ))} + + / + + ))} + + ))} + + + + ); +}; + +const Scroller = styled.div<{ x: number; duration: number }>` + background: #fff; + padding: 16px 48px; + box-sizing: border-box; + border-top: 1px solid #000; + + animation-name: ${(props) => marquee(props.x)}; + animation-duration: ${(props) => props.duration}s; + animation-iteration-count: infinite; + animation-timing-function: linear; +`; + +const marquee = (x: number) => keyframes` + 0% { transform: translateX(0px); } + 100% { transform: translateX(-${x}px);} +`; + +const CarouselRow = styled.div` + display: flex; + flex-direction: row; + justify-content: flex-start; + gap: 24px; +`; + +const Container = styled.div` + display: flex; + flex-direction: row; + gap: 24px; +`; + +const RowContainer = styled.div` + display: flex; + flex-direction: row; + gap: 24px; +`; + +const InfoRow = styled.div` + display: flex; + flex-direction: row; + gap: 8px; + white-space: nowrap; +`; + +const InfoText = styled.div` + font-size: 16px; + font-style: normal; + font-weight: 400; + line-height: 24px; +`; + +const TextLink = styled.a` + color: #46b955; + font-size: 16px; + font-weight: 600; + line-height: 24px; + letter-spacing: 0.32px; + text-decoration-line: underline; +`; diff --git a/projects/dex-ui/src/components/Frame/Footer.tsx b/projects/dex-ui/src/components/Frame/Footer.tsx index ad7f0942b8..75ebc37a4c 100644 --- a/projects/dex-ui/src/components/Frame/Footer.tsx +++ b/projects/dex-ui/src/components/Frame/Footer.tsx @@ -1,24 +1,24 @@ import React from "react"; import styled from "styled-components"; import { BeanstalkLogoBlack, Discord, Github, Twitter } from "../Icons"; -import { size } from "src/breakpoints"; +import { mediaQuery, size } from "src/breakpoints"; export const Footer = () => ( -
+ ๐Ÿ“ƒ Protocol Documentation -
+ Visit the Docs โ†’
-
+ ๐Ÿ‘พ Basin Bug Bounty Program -
+ Learn More โ†’
@@ -54,7 +54,7 @@ const Container = styled.footer` const Box = styled.a` display: flex; - flex: 1; + flex: 2; border-left: 1px solid black; justify-content: center; align-items: center; @@ -67,6 +67,16 @@ const Box = styled.a` :first-child { border-left: none; } + + ${mediaQuery.md.only} { + flex-wrap: wrap; + gap: 8px; + flex-flow: column; + } +`; + +const InfoText = styled.div` + whitespace: nowrap; `; const SmallBox = styled.a` @@ -82,4 +92,5 @@ const SmallBox = styled.a` const StyledLink = styled.span` text-decoration: underline; + white-space: nowrap; `; diff --git a/projects/dex-ui/src/components/Liquidity/AddLiquidity.tsx b/projects/dex-ui/src/components/Liquidity/AddLiquidity.tsx index 6f239877ce..2704723c1a 100644 --- a/projects/dex-ui/src/components/Liquidity/AddLiquidity.tsx +++ b/projects/dex-ui/src/components/Liquidity/AddLiquidity.tsx @@ -16,14 +16,18 @@ import useSdk from "src/utils/sdk/useSdk"; import { useWellReserves } from "src/wells/useWellReserves"; import { Checkbox } from "../Checkbox"; import { size } from "src/breakpoints"; +import { LoadingTemplate } from "src/components/LoadingTemplate"; -type AddLiquidityProps = { - well: Well; +type BaseAddLiquidityProps = { slippage: number; slippageSettingsClickHandler: () => void; handleSlippageValueChange: (value: string) => void; }; +type AddLiquidityProps = { + well: Well; +} & BaseAddLiquidityProps; + export type AddLiquidityQuote = { quote: { quote: TokenValue[]; @@ -31,7 +35,7 @@ export type AddLiquidityQuote = { estimate: TokenValue; }; -export const AddLiquidity = ({ well, slippage, slippageSettingsClickHandler, handleSlippageValueChange }: AddLiquidityProps) => { +const AddLiquidityContent = ({ well, slippage, slippageSettingsClickHandler, handleSlippageValueChange }: AddLiquidityProps) => { const { address } = useAccount(); const [amounts, setAmounts] = useState({}); const inputs = Object.values(amounts); @@ -413,6 +417,30 @@ export const AddLiquidity = ({ well, slippage, slippageSettingsClickHandler, han ); }; +export const AddLiquidity: React.FC = (props) => { + if (!props.well || props.loading) { + return ( +
+ + + + + + + + + + + + + +
+ ); + } + + return ; +}; + const LargeGapContainer = styled.div` display: flex; flex-direction: column; diff --git a/projects/dex-ui/src/components/Liquidity/QuoteDetails.tsx b/projects/dex-ui/src/components/Liquidity/QuoteDetails.tsx index 7ad785ef8e..bbdf879b4c 100644 --- a/projects/dex-ui/src/components/Liquidity/QuoteDetails.tsx +++ b/projects/dex-ui/src/components/Liquidity/QuoteDetails.tsx @@ -8,7 +8,7 @@ import SlippagePanel from "./SlippagePanel"; import { ChevronDown, Info } from "../Icons"; import { ImageButton } from "../ImageButton"; import { Tooltip } from "../Tooltip"; -import { BodyXS } from "../Typography"; +import { BodyS } from "../Typography"; import { size } from "src/breakpoints"; import { displayTokenSymbol } from "src/utils/format"; @@ -343,6 +343,6 @@ const QuoteContainer = styled.div` display: flex; flex-direction: column; @media (max-width: ${size.mobile}) { - ${BodyXS} + ${BodyS} } `; diff --git a/projects/dex-ui/src/components/Liquidity/RemoveLiquidity.tsx b/projects/dex-ui/src/components/Liquidity/RemoveLiquidity.tsx index 218ed2085a..0271902a95 100644 --- a/projects/dex-ui/src/components/Liquidity/RemoveLiquidity.tsx +++ b/projects/dex-ui/src/components/Liquidity/RemoveLiquidity.tsx @@ -21,15 +21,19 @@ import { useWellReserves } from "src/wells/useWellReserves"; import { Checkbox } from "../Checkbox"; import { size } from "src/breakpoints"; import { displayTokenSymbol } from "src/utils/format"; +import { LoadingTemplate } from "../LoadingTemplate"; -type RemoveLiquidityProps = { - well: Well; +type BaseRemoveLiquidityProps = { slippage: number; slippageSettingsClickHandler: () => void; handleSlippageValueChange: (value: string) => void; }; -export const RemoveLiquidity = ({ well, slippage, slippageSettingsClickHandler, handleSlippageValueChange }: RemoveLiquidityProps) => { +type RemoveLiquidityProps = { + well: Well; +} & BaseRemoveLiquidityProps; + +const RemoveLiquidityContent = ({ well, slippage, slippageSettingsClickHandler, handleSlippageValueChange }: RemoveLiquidityProps) => { const { address } = useAccount(); const [wellLpToken, setWellLpToken] = useState(null); @@ -443,6 +447,39 @@ export const RemoveLiquidity = ({ well, slippage, slippageSettingsClickHandler, ); }; +export const RemoveLiquidity: React.FC<{ well: Well | undefined; loading: boolean } & BaseRemoveLiquidityProps> = (props) => { + if (!props.well || props.loading) { + return ( + + + + + + + + + + + + + + + + + + + + + + + + + ); + } + + return ; +}; + type ReadOnlyRowProps = { selected?: boolean; }; @@ -452,7 +489,7 @@ const LargeGapContainer = styled.div` flex-direction: column; gap: 24px; @media (max-width: ${size.mobile}) { - margin-bottom: 40px; + margin-bottom: 64px; } `; diff --git a/projects/dex-ui/src/components/LoadingItem.tsx b/projects/dex-ui/src/components/LoadingItem.tsx new file mode 100644 index 0000000000..22f228c790 --- /dev/null +++ b/projects/dex-ui/src/components/LoadingItem.tsx @@ -0,0 +1,31 @@ +import React, { FC } from "react"; +import { Skeleton, SkeletonProps } from "./Skeleton"; + +type BaseProps = { + loading?: boolean; + children: React.ReactNode | React.ReactNode[]; +}; + +type WithOnLoadingProps = BaseProps & { + onLoading: JSX.Element | null; + loadProps?: never; // Indicate that skeletonProps should not be provided +}; + +type WithoutOnLoadingProps = BaseProps & { + onLoading?: never; + loadProps: SkeletonProps; // Ensure SkeletonProps are provided +}; + +type Props = WithOnLoadingProps | WithoutOnLoadingProps; + +export const LoadingItem: FC = ({ loading, onLoading, children, loadProps }) => { + if (!loading) { + return <>{children}; + } + + if (onLoading !== undefined) { + return <>{onLoading}; + } + + return ; +}; diff --git a/projects/dex-ui/src/components/LoadingTemplate.tsx b/projects/dex-ui/src/components/LoadingTemplate.tsx new file mode 100644 index 0000000000..fb99da29a3 --- /dev/null +++ b/projects/dex-ui/src/components/LoadingTemplate.tsx @@ -0,0 +1,177 @@ +import React from "react"; +import styled from "styled-components"; +import { Skeleton } from "./Skeleton"; +import { ArrowButton } from "./Swap/ArrowButton"; + +type MarginProps = { + left?: number; + right?: number; + bottom?: number; + top?: number; +}; + +type DimensionProps = { + height?: number; + width?: number; +}; + +const getMarginStyles = (props: { margin?: MarginProps }) => ` + margin-bottom: ${props.margin?.bottom ? props.margin.bottom : 0}px; + margin-top: ${props.margin?.top ? props.margin.top : 0}px; + margin-right: ${props.margin?.right ? props.margin.right : 0}px; + margin-left: ${props.margin?.left ? props.margin.left : 0}px; +`; + +export function LoadingTemplate(props: FlexProps & { children: React.ReactNode }) { + return ; +} + +LoadingTemplate.Input = () => ( + + {"-"} + + + + + +); + +LoadingTemplate.Arrow = () => ( + + {}} /> + +); + +LoadingTemplate.LabelValue = ({ + height, + labelWidth = 90, + valueWidth = 70 +}: { + height?: number; + labelWidth?: number; + valueWidth?: number; +}) => ( + + + + + + + + +); + +LoadingTemplate.Button = () => ( + + + +); + +LoadingTemplate.Item = ({ height, width, margin }: DimensionProps & { margin?: MarginProps }) => ( + + + +); + +LoadingTemplate.Flex = (props: FlexProps & { children: React.ReactNode }) => ; + +LoadingTemplate.TokenLogo = ({ count = 1, size }: { count?: number; size: number }) => { + if (count === 0) return null; + + if (count === 1) { + return ( + + + + ); + } + + return ( + + {Array(count) + .fill(null) + .map((_, i) => ( + + + + ))} + + ); +}; + +type FlexProps = { + row?: boolean; + gap?: number; + alignItems?: string; + justifyContent?: string; + width?: string | number; +}; + +const FlexBox = styled.div` + display: flex; + ${(props) => ` + flex-direction: ${props.row ? "row" : "column"}; + gap: ${props.gap || 0}px; + ${props.alignItems && `align-items: ${props.alignItems};`} + ${props.justifyContent && `justify-content: ${props.justifyContent};`} + ${props.width && `width: ${typeof props.width === "string" ? props.width : `${props.width}px`}`} + `} +`; + +const Background = styled.div<{ width?: number; height?: number; margin?: MarginProps; circle?: boolean; rounded?: boolean }>` + display: flex; + background: white; + ${(props) => ` + height: ${props.height ? `${props.height}px` : "100%"}; + width: ${props.width ? `${props.width}px` : "100%"}; + border-radius: ${props.circle ? "50%" : props.rounded === true ? "4px" : "0px"}; + ${getMarginStyles(props)} + `} +`; + +const OutputRow = styled.div` + display: flex; + flex-direction: row; + justify-content: space-between; + align-items: center; +`; + +const LoadingInputItem = styled.div` + display: flex; + flex-direction: row; + justify-content: space-between; + box-sizing: border-box; + background: #fff; + border: 0.5px solid #d1d5db; + padding: 23.5px 8px; + outline: 0.5px solid black; +`; + +const SkeletonRow = styled.div` + display: flex; + flex-direction: row; + align-items: center; + gap: 8px; +`; + +const ArrowContainer = styled.div` + // border: 1px dashed orange; + display: flex; + flex-direction: row; + justify-content: center; +`; + +type Responsive = + | { + sm?: T; + md?: T; + lg?: T; + } + | T; + +type FlexingProps = { + height: number; + width: number; + alignItems?: string; + justifyContent?: string; +}; diff --git a/projects/dex-ui/src/components/PageComponents/Title.tsx b/projects/dex-ui/src/components/PageComponents/Title.tsx index b157a97bc0..e4cff5c663 100644 --- a/projects/dex-ui/src/components/PageComponents/Title.tsx +++ b/projects/dex-ui/src/components/PageComponents/Title.tsx @@ -1,7 +1,7 @@ import React from "react"; import { FC } from "src/types"; import styled from "styled-components"; -import { BodyL, BodyXS, H2 } from "../Typography"; +import { BodyL, BodyS, BodyXS, H2 } from "../Typography"; import { Link } from "react-router-dom"; import { size } from "src/breakpoints"; @@ -55,7 +55,7 @@ const TitleText = styled.div` ${(props) => props.fontWeight && `font-weight: ${props.fontWeight}`}; text-transform: uppercase; @media (max-width: ${size.mobile}) { - ${({ largeOnMobile }) => (largeOnMobile ? `${H2}` : `${BodyXS}`)} + ${({ largeOnMobile }) => (largeOnMobile ? `${H2}` : `${BodyS}`)} } `; const ParentText = styled(Link)` @@ -64,6 +64,9 @@ const ParentText = styled(Link)` text-decoration: none; text-transform: uppercase; @media (max-width: ${size.mobile}) { - ${BodyXS} + ${BodyS} + } + :hover { + color: #000000; } `; diff --git a/projects/dex-ui/src/components/Skeleton.tsx b/projects/dex-ui/src/components/Skeleton.tsx new file mode 100644 index 0000000000..94812e2c8d --- /dev/null +++ b/projects/dex-ui/src/components/Skeleton.tsx @@ -0,0 +1,79 @@ +import React from "react"; + +import styled, { css, keyframes } from "styled-components"; + +type MarginProps = { + bottom?: number; + top?: number; + left?: number; + right?: number; +}; + +export type SkeletonProps = { + height: number; + width?: number; + // if true, rounded will be ignored + circle?: boolean; + // defaults to false + rounded?: boolean; + // defaults to pulse + shimmer?: boolean; + // margin: + margin?: MarginProps; +}; + +export const Skeleton: React.FC = (props) => { + if (props.shimmer) { + return ; + } + + return ; +}; + +const pulse = () => keyframes` + 0% { + opacity: 1; + } + 50% { + opacity: 0.5; + } + 100% { + opacity: 1; + } +`; + +const shimmer = () => keyframes` + 0% { + background-position: -800px 0; + } + 100% { + background-position: 800px 0; + } +`; + +const SkeletonBase = css` + display: inline-block; + ${(props) => ` + height: ${props.height ? `${props.height}px` : "100%"}; + width: ${props.width ? `${props.width}px` : "100%"}; + border-radius: ${props.circle ? "50%" : props.rounded === true ? "4px" : "0px"}; + margin-top: ${props.margin?.top || 0}px; + margin-bottom: ${props.margin?.bottom || 0}px; + margin-right: ${props.margin?.right || 0}px; + margin-left: ${props.margin?.left || 0}px; + `} +`; + +const SkeletonPulse = styled.div` + ${SkeletonBase} + background: linear-gradient(to right, #e5e7eb 8%, #F3F4F6 18%, #e5e7eb 33%); + background-size: 1200px 100%; + animation: ${pulse} 2s ease-in-out infinite; +`; + +const SkeletonShimmer = styled.div` + ${SkeletonBase} + background: linear-gradient(65deg, #f3f4f6 8%, #e5e7eb 18%, #f3f4f6 33%); + background-size: 1200px 100%; + animation: ${shimmer} 2s linear infinite; +`; diff --git a/projects/dex-ui/src/components/Swap/Button.tsx b/projects/dex-ui/src/components/Swap/Button.tsx index 5df82e4c85..7d5410bb0c 100644 --- a/projects/dex-ui/src/components/Swap/Button.tsx +++ b/projects/dex-ui/src/components/Swap/Button.tsx @@ -70,6 +70,6 @@ const StyledButton = styled.button` ${BodyXS} font-weight: 600; padding: 8px 8px; - height: 40px; + height: 48x; } `; diff --git a/projects/dex-ui/src/components/TabButton.tsx b/projects/dex-ui/src/components/TabButton.tsx index a2fcac7611..6e6d98e810 100644 --- a/projects/dex-ui/src/components/TabButton.tsx +++ b/projects/dex-ui/src/components/TabButton.tsx @@ -28,7 +28,7 @@ export const TabButton = styled.button<{ active?: boolean; stretch?: boolean; bo @media (max-width: ${size.mobile}) { ${BodyXS} - height: 40px; + height: 48px; font-weight: ${({ bold, active }) => (bold || active ? "600" : "normal")}; padding: 8px 8px; line-height: 18px; diff --git a/projects/dex-ui/src/components/Well/Activity/WellHistory.tsx b/projects/dex-ui/src/components/Well/Activity/WellHistory.tsx index 05ee8ad726..fcac9cd2d5 100644 --- a/projects/dex-ui/src/components/Well/Activity/WellHistory.tsx +++ b/projects/dex-ui/src/components/Well/Activity/WellHistory.tsx @@ -9,14 +9,63 @@ import { TokenValue } from "@beanstalk/sdk"; import { TabButton } from "src/components/TabButton"; import { size } from "src/breakpoints"; import { useTokenSupply } from "src/tokens/useTokenSupply"; +import { LoadingTemplate } from "src/components/LoadingTemplate"; -type WellHistoryProps = { - well: Well; +type BaseWellHistoryProps = { tokenPrices: (TokenValue | null)[]; reservesUSD: TokenValue; }; -export const WellHistory = ({ well, tokenPrices, reservesUSD }: WellHistoryProps) => { +type WellHistoryProps = { + well: Well; +} & BaseWellHistoryProps; + +const WellHistorySkeleton: React.FC<{}> = () => ( + + + + + + {""} + {""} + + + + + <> + {Array(10) + .fill(null) + .map((_, rowIdx) => ( + + + + + + + + + + + + + + + ))} + + +
{""}{""}
+ + + + + + + +
+
+); + +const WellHistoryContent: React.FC = ({ well, tokenPrices, reservesUSD }) => { const { data: events, isLoading: loading } = useWellHistory(well); const [filter, setFilter] = useState(null); const eventsPerPage = 10; @@ -30,6 +79,10 @@ export const WellHistory = ({ well, tokenPrices, reservesUSD }: WellHistoryProps const isNonEmptyWell = lpTokenSupply.totalSupply && lpTokenSupply.totalSupply.gt(0); const lpTokenPrice = lpTokenSupply.totalSupply && isNonEmptyWell ? reservesUSD.div(lpTokenSupply.totalSupply) : TokenValue.ZERO; + if (loading) { + return ; + } + const eventRows: JSX.Element[] = (events || []) .filter((e: WellEvent) => filter === null || e.type == filter) .map( @@ -38,87 +91,97 @@ export const WellHistory = ({ well, tokenPrices, reservesUSD }: WellHistoryProps return ( - {!loading && ( - <> - {/*
+ {/*
*/} - - - - - Value - Description - - - - - {eventRows.length ? ( - eventRows - ) : ( - <> - - No events to show - - - )} - {isNonEmptyWell ? ( - <> - - - - setCurrentPage(currentPage > 1 ? currentPage - 1 : 1)} - > - โ† - - {`Page ${currentPage} of ${totalPages}`} - setCurrentPage(currentPage < totalPages ? currentPage + 1 : totalPages)} - > - โ†’ - - - - - - - - setCurrentPage(currentPage > 1 ? currentPage - 1 : 1)} - > - โ† - - {`Page ${currentPage} of ${totalPages}`} - setCurrentPage(currentPage < totalPages ? currentPage + 1 : totalPages)} - > - โ†’ - - - - - - ) : null} - -
ActionTime
- - )} + + + + + Value + Description + + + + + {eventRows.length ? ( + eventRows + ) : ( + <> + + No events to show + + + )} + {!loading && isNonEmptyWell ? ( + <> + + + + setCurrentPage(currentPage > 1 ? currentPage - 1 : 1)} + > + โ† + + {`Page ${currentPage} of ${totalPages}`} + setCurrentPage(currentPage < totalPages ? currentPage + 1 : totalPages)} + > + โ†’ + + + + + + + + setCurrentPage(currentPage > 1 ? currentPage - 1 : 1)} + > + โ† + + {`Page ${currentPage} of ${totalPages}`} + setCurrentPage(currentPage < totalPages ? currentPage + 1 : totalPages)} + > + โ†’ + + + + + + ) : null} + +
ActionTime
); }; +export const WellHistory: React.FC = (props) => { + if (props.loading || !props.well) { + return ; + } + + return ; +}; + +const DesktopOnlyTd = styled(Td)` + @media (max-width: ${size.mobile}) { + display: none; + } +`; + const WellHistoryContainer = styled.div` display: flex; `; @@ -179,3 +242,9 @@ const NoEventsData = styled.div` font-size: 14px; } `; + +const LoadingRow = styled(Row)` + :hover { + cursor: default; + } +`; diff --git a/projects/dex-ui/src/components/Well/Chart/ChartSection.tsx b/projects/dex-ui/src/components/Well/Chart/ChartSection.tsx index bb92eadd10..4f5e9ddcf8 100644 --- a/projects/dex-ui/src/components/Well/Chart/ChartSection.tsx +++ b/projects/dex-ui/src/components/Well/Chart/ChartSection.tsx @@ -9,20 +9,21 @@ import { TabButton } from "src/components/TabButton"; import useWellChartData from "src/wells/useWellChartData"; import { ChartContainer } from "./ChartStyles"; import { BottomDrawer } from "src/components/BottomDrawer"; -import { size } from "src/breakpoints"; +import { mediaQuery, size } from "src/breakpoints"; +import { LoadingTemplate } from "src/components/LoadingTemplate"; function timeToLocal(originalTime: number) { const d = new Date(originalTime * 1000); return Date.UTC(d.getFullYear(), d.getMonth(), d.getDate(), d.getHours(), d.getMinutes(), d.getSeconds(), d.getMilliseconds()) / 1000; } -export const ChartSection: FC<{ well: Well }> = ({ well }) => { +const ChartSectionContent: FC<{ well: Well }> = ({ well }) => { const [tab, setTab] = useState(0); const [showDropdown, setShowDropdown] = useState(false); const [timePeriod, setTimePeriod] = useState("week"); const [dropdownButtonText, setDropdownButtonText] = useState("1 WEEK"); - const { data: chartData, refetch, error, isLoading } = useWellChartData(well, timePeriod); + const { data: chartData, refetch, error, isLoading: chartDataLoading } = useWellChartData(well, timePeriod); const [liquidityData, setLiquidityData] = useState([]); const [volumeData, setVolumeData] = useState([]); @@ -74,57 +75,59 @@ export const ChartSection: FC<{ well: Well }> = ({ well }) => { return ( - showTab(e, 0)} active={tab === 0} hover> - LIQUIDITY - - showTab(e, 1)} active={tab === 1} hover> - VOLUME - - { - setShowDropdown(!showDropdown); - }} - > - {dropdownButtonText} - - { - setChartRange("day"); - }} - > - 1 DAY - - { - setChartRange("week"); - }} - > - 1 WEEK - - { - setChartRange("month"); - }} - > - 1 MONTH - - { - setChartRange("all"); - }} - > - ALL - - - + <> + showTab(e, 0)} active={tab === 0} hover> + LIQUIDITY + + showTab(e, 1)} active={tab === 1} hover> + VOLUME + + { + setShowDropdown(!showDropdown); + }} + > + {dropdownButtonText} + + { + setChartRange("day"); + }} + > + 1 DAY + + { + setChartRange("week"); + }} + > + 1 WEEK + + { + setChartRange("month"); + }} + > + 1 MONTH + + { + setChartRange("all"); + }} + > + ALL + + + + setChartTypeDrawerOpen(true)}>{tab === 0 ? "LIQUIDITY" : "VOLUME"} @@ -178,15 +181,56 @@ export const ChartSection: FC<{ well: Well }> = ({ well }) => { - {error !== null && {`Error Loading Chart Data :(`}} - {isLoading && Loading Chart Data...} - {tab === 0 && !error && !isLoading && } - {tab === 1 && !error && !isLoading && } + {error !== null && {`Error Loading Chart Data :(`}} + {chartDataLoading && ( + + + + + + + )} + {tab === 0 && !error && !chartDataLoading && } + {tab === 1 && !error && !chartDataLoading && } ); }; +export const ChartSection: FC<{ well: Well | undefined; loading?: boolean }> = ({ well, loading }) => { + if (!well || loading) { + return ( + + + + {""} + + {""} + {""} + + + {""} + {""} + + + + + + + + + ); + } + + return ; +}; + const ChartLoader = styled(ChartContainer)` + padding: 24px; + justify-content: flex-start; + box-sizing: border-box; +`; + +const ChartError = styled(ChartContainer)` justify-content: center; align-items: center; `; @@ -260,3 +304,26 @@ const FilterButton = styled.div` height: 40px; } `; + +const LoadingTabButton = styled(TabButton)<{ width: number }>` + min-height: 48px; + min-width: ${(props) => props.width}px; + + :hover { + cursor: default; + } +`; + +const LoadingFilterButton = styled(FilterButton)<{ width: number }>` + min-height: 48px; + min-width: ${(props) => props.width}px; + + :hover { + cursor: default; + background: white; + } + + ${mediaQuery.sm.only} { + min-height: 40px; + } +`; diff --git a/projects/dex-ui/src/components/Well/LiquidityBox.tsx b/projects/dex-ui/src/components/Well/LiquidityBox.tsx index 52ad1254f5..75b6f83936 100644 --- a/projects/dex-ui/src/components/Well/LiquidityBox.tsx +++ b/projects/dex-ui/src/components/Well/LiquidityBox.tsx @@ -2,10 +2,9 @@ import React, { useMemo } from "react"; import styled from "styled-components"; import { TokenValue } from "@beanstalk/sdk"; -import { Well } from "@beanstalk/sdk/Wells"; -import { size } from "src/breakpoints"; -import { BodyCaps, BodyXS, LinksButtonText, TextNudge } from "src/components/Typography"; +import { mediaQuery } from "src/breakpoints"; +import { BodyCaps, BodyS, LinksButtonText, TextNudge } from "src/components/Typography"; import { InfoBox } from "src/components/InfoBox"; import { TokenLogo } from "src/components/TokenLogo"; import { Tooltip } from "src/components/Tooltip"; @@ -15,9 +14,12 @@ import { formatUSD } from "src/utils/format"; import { useWellLPTokenPrice } from "src/wells/useWellLPTokenPrice"; import { useLPPositionSummary } from "src/tokens/useLPPositionSummary"; import { useBeanstalkSiloWhitelist } from "src/wells/useBeanstalkSiloWhitelist"; +import { LoadingItem } from "src/components/LoadingItem"; +import { Well } from "@beanstalk/sdk/Wells"; type Props = { well: Well | undefined; + loading: boolean; }; const tooltipProps = { @@ -31,8 +33,8 @@ const tooltipProps = { const displayTV = (value?: TokenValue) => (value?.gt(0) ? value.toHuman("short") : "-"); -export const LiquidityBox: FC = (props) => { - const well = useMemo(() => props.well, [props.well]); +export const LiquidityBox: FC = ({ well: _well, loading }) => { + const well = useMemo(() => _well, [_well]); const { getPositionWithWell } = useLPPositionSummary(); const { getIsWhitelisted } = useBeanstalkSiloWhitelist(); @@ -55,19 +57,27 @@ export const LiquidityBox: FC = (props) => { - My Liquidity + + + {"My Liquidity"} + + - - - {displayTV(position?.total)} - + + + + {displayTV(position?.total)} + + - In my Wallet + + In my Wallet + {displayTV(position?.external)} - {isWhitelisted ? ( + {!loading && isWhitelisted ? ( <> Deposited in the Silo @@ -82,32 +92,34 @@ export const LiquidityBox: FC = (props) => { - {isWhitelisted ? ( - - - {"Wallet: "} -
${externalUSD.toHuman("short")}
-
- - - {"Silo Deposits: "} -
${siloUSD.toHuman("short")}
-
- - {"Farm Balance: "} -
${internalUSD.toHuman("short")}
-
- - } - > - USD TOTAL: {formatUSD(USDTotal)} -
- ) : ( - <>USD TOTAL: {formatUSD(USDTotal)} - )} + + {isWhitelisted ? ( + + + {"Wallet: "} +
${externalUSD.toHuman("short")}
+
+ + + {"Silo Deposits: "} +
${siloUSD.toHuman("short")}
+
+ + {"Farm Balance: "} +
${internalUSD.toHuman("short")}
+
+ + } + > + <>USD TOTAL: {formatUSD(USDTotal)} +
+ ) : ( + <>USD TOTAL: {formatUSD(USDTotal)} + )} +
@@ -116,10 +128,12 @@ export const LiquidityBox: FC = (props) => { const BoxHeader = styled.div` ${BodyCaps} - @media (max-width: ${size.mobile}) { - ${BodyXS} + min-height: 24px; + ${mediaQuery.sm.only} { + ${BodyS} } `; + const BoxHeaderAmount = styled.div` display: flex; align-items: center; diff --git a/projects/dex-ui/src/components/Well/OtherSection.tsx b/projects/dex-ui/src/components/Well/OtherSection.tsx index 85567ec2b2..0918d985ce 100644 --- a/projects/dex-ui/src/components/Well/OtherSection.tsx +++ b/projects/dex-ui/src/components/Well/OtherSection.tsx @@ -1,23 +1,23 @@ import React from "react"; import { FC } from "src/types"; -import { Row, TBody, THead, Table, Th, Td } from "./Table"; +import { Row, TBody, THead, Table, Td, Th } from "./Table"; import { Well } from "@beanstalk/sdk/Wells"; import styled from "styled-components"; import { size } from "src/breakpoints"; +import { displayTokenSymbol } from "src/utils/format"; +import { Token } from "@beanstalk/sdk"; +import { Skeleton } from "../Skeleton"; -type Props = { - well: Well; -}; - -export const OtherSection: FC = ({ well }) => { +type Props = { well: Well }; - const tableItems = [ - {name: "Multi Flow Pump", address: "0xBA510f10E3095B83a0F33aa9ad2544E22570a87C"}, - {name: "Constant Product 2", address: "0xBA510C20FD2c52E4cb0d23CFC3cCD092F9165a6E"}, - {name: "Well Implementation", address: "0xBA510e11eEb387fad877812108a3406CA3f43a4B"}, - {name: "Aquifer", address: "0xBA51AAAA95aeEFc1292515b36D86C51dC7877773"} - ]; +const tableItems = [ + { name: "Multi Flow Pump", address: "0xBA510f10E3095B83a0F33aa9ad2544E22570a87C" }, + { name: "Constant Product 2", address: "0xBA510C20FD2c52E4cb0d23CFC3cCD092F9165a6E" }, + { name: "Well Implementation", address: "0xBA510e11eEb387fad877812108a3406CA3f43a4B" }, + { name: "Aquifer", address: "0xBA51AAAA95aeEFc1292515b36D86C51dC7877773" } +]; +const OtherSectionContent: FC = ({ well }) => { return (
@@ -44,7 +44,7 @@ export const OtherSection: FC = ({ well }) => { {well.address} @@ -55,19 +55,27 @@ export const OtherSection: FC = ({ well }) => { - {well.tokens!.map(function (token, index) { + {well.tokens?.map(function (token, index) { return ( - + {token.address || `-`} - + {token.address.substr(0, 5) + "..." + token.address.substr(token.address.length - 5) || `-`} @@ -97,6 +105,47 @@ export const OtherSection: FC = ({ well }) => { ); }; +const loadingItemProps = { + sm: { height: 24, width: 100 }, + lg: { height: 24, width: 200 } +}; + +export const OtherSection: FC<{ well: Well | undefined; loading?: boolean }> = ({ well, loading }) => { + if (!well || loading) { + return ( +
+
- Well LP Token - {well.lpToken?.symbol} + Well LP Token - {displayTokenSymbol(well.lpToken as Token)} {`Token ${index + 1} - ${token.symbol}`}
+ + + + {""} + {""} + + + + {Array(8) + .fill(null) + .map((_, idx) => ( + + + + + + + + + + ))} + +
{""}
+ +
+
+ ); + } + return ; +}; + const Detail = styled.span` color: #4b5563; font-weight: 600; @@ -135,3 +184,9 @@ const MobileTh = styled(Th)` display: none; } `; + +const LoadingRow = styled(Row)` + :hover { + cursor: default; + } +`; diff --git a/projects/dex-ui/src/components/Well/Reserves.tsx b/projects/dex-ui/src/components/Well/Reserves.tsx index 0ee1047043..794a3c82a6 100644 --- a/projects/dex-ui/src/components/Well/Reserves.tsx +++ b/projects/dex-ui/src/components/Well/Reserves.tsx @@ -1,6 +1,6 @@ import React from "react"; import styled from "styled-components"; -import { BodyL, BodyXS, TextNudge } from "../Typography"; +import { BodyL, BodyS, TextNudge } from "../Typography"; import { FC } from "src/types"; import { Token, TokenValue } from "@beanstalk/sdk"; import { TokenLogo } from "../TokenLogo"; @@ -38,7 +38,7 @@ const Symbol = styled.div` ${BodyL} color: #4B5563; @media (max-width: ${size.mobile}) { - ${BodyXS} + ${BodyS} } `; const Wrapper = styled.div` @@ -54,7 +54,7 @@ const Amount = styled.div` text-align: right; color: #000000; @media (max-width: ${size.mobile}) { - ${BodyXS} + ${BodyS} font-weight: 600; } `; @@ -65,6 +65,6 @@ const Percent = styled.div` text-align: right; color: #9ca3af; @media (max-width: ${size.mobile}) { - ${BodyXS} + ${BodyS} } `; diff --git a/projects/dex-ui/src/components/Well/Table/MyWellPositionRow.tsx b/projects/dex-ui/src/components/Well/Table/MyWellPositionRow.tsx new file mode 100644 index 0000000000..778d005e57 --- /dev/null +++ b/projects/dex-ui/src/components/Well/Table/MyWellPositionRow.tsx @@ -0,0 +1,270 @@ +import { TokenValue } from "@beanstalk/sdk"; +import React, { FC, ReactNode } from "react"; +import { Row, Td } from "src/components/Table"; +import { TokenLogo } from "src/components/TokenLogo"; +import styled from "styled-components"; +import { mediaQuery, size } from "src/breakpoints"; +import { displayTokenSymbol, formatNum, formatUSD } from "src/utils/format"; +import { useWellLPTokenPrice } from "src/wells/useWellLPTokenPrice"; +import { LPBalanceSummary } from "src/tokens/useLPPositionSummary"; +import { useBeanstalkSiloWhitelist } from "src/wells/useBeanstalkSiloWhitelist"; +import { Tooltip } from "src/components/Tooltip"; +import { Well } from "@beanstalk/sdk/Wells"; +import { Skeleton } from "src/components/Skeleton"; + +import { useNavigate } from "react-router-dom"; +import { useIsMobile } from "src/utils/ui/useIsMobile"; + +const PositionBreakdown: React.FC<{ + items: { external: TokenValue; silo: TokenValue; internal: TokenValue; total: TokenValue }; + isWhitelisted: boolean; + isLP: boolean; + totalDisplay: string; +}> = ({ items, isWhitelisted, totalDisplay, isLP = true }) => { + const formatFn = isLP ? formatNum : formatUSD; + const isMobile = useIsMobile(); + + const getTooltipProps = () => { + let base = { side: "right", offsetX: 3, offsetY: -100, arrowSize: 4, arrowOffset: 40 }; + + if (isMobile) { + if (isLP) { + base.offsetY = -162; + base.arrowOffset = 67; + } else { + base.side = "left"; + base.offsetX = -5; + base.offsetY = -96; + base.arrowOffset = 43; + } + } else if (!isMobile && !isLP) { + base.side = "left"; + base.offsetX = -10; + base.offsetY = -100; + } + + return base; + }; + + return isWhitelisted ? ( + + + {"Wallet Balance:"} + {formatFn(items.external)} + + + {"Silo Deposits:"} + {formatFn(items.silo)} + + + {"Farm Balance:"} + {formatFn(items.internal)} + + + } + > + {totalDisplay} + + ) : ( + {totalDisplay} + ); +}; + +export const MyWellPositionRow: FC<{ + well: Well | undefined; + position: LPBalanceSummary | undefined; + prices: ReturnType["data"]; +}> = ({ well, position, prices }) => { + const navigate = useNavigate(); + const { getIsWhitelisted } = useBeanstalkSiloWhitelist(); + + const lpAddress = well?.lpToken?.address; + const lpToken = well?.lpToken; + + if (!well || !position || position.total.lte(0) || !lpAddress || !lpToken) { + return null; + } + + const tokens = well.tokens || []; + const logos: ReactNode[] = []; + const symbols: string[] = []; + const gotoWell = () => navigate(`/wells/${well.address}`); + + tokens.map((token: any) => { + logos.push(); + symbols.push(token.symbol); + }); + + const lpPrice = lpAddress && lpAddress in prices ? prices[lpAddress] : undefined; + const whitelisted = getIsWhitelisted(well); + + const positionsUSD = { + total: lpPrice?.mul(position.total) || TokenValue.ZERO, + external: lpPrice?.mul(position.external) || TokenValue.ZERO, + silo: lpPrice?.mul(position.silo) || TokenValue.ZERO, + internal: lpPrice?.mul(position.internal) || TokenValue.ZERO + }; + + return ( + + + + {logos} + {symbols.join("/")} + + + + + + + + + + + + + + + {logos} + {symbols.join("/")} + {/* {deployer} */} + + + + + + + + + + + + ); +}; + +export const MyWellPositionLoadingRow: FC<{}> = () => ( + + + + + + + + + + + + + + + + + + + + + + + +); + +const LoadingColumn = styled.div<{ align?: "right" | "left" }>` + display: flex; + flex-direction: column; + gap: 8px; + ${(props) => ` + align-items: ${props.align === "right" ? "flex-end" : "flex-start"}; + `} + + ${mediaQuery.sm.only} { + gap: 4px; + } +`; + +const TableRow = styled(Row)` + @media (max-width: ${size.mobile}) { + height: 66px; + } +`; + +const DesktopContainer = styled(Td)` + @media (max-width: ${size.mobile}) { + display: none; + } +`; + +const MobileContainer = styled(Td)` + padding: 8px 16px; + @media (min-width: ${size.mobile}) { + display: none; + } +`; + +const WellDetail = styled.div` + display: flex; + flex-direction: row; + gap: 8px; + @media (min-width: ${size.mobile}) { + flex-direction: column; + } +`; + +const TokenLogos = styled.div` + display: flex; + div:not(:first-child) { + margin-left: -8px; + } +`; +const TokenSymbols = styled.div` + font-size: 20px; + line-height: 24px; + color: #1c1917; + @media (max-width: ${size.mobile}) { + font-size: 14px; + margin-top: 2px; + } +`; + +const WellLPBalance = styled.div` + font-size: 20px; + line-height: 24px; + @media (max-width: ${size.mobile}) { + font-size: 14px; + font-weight: normal; + } +`; + +const BalanceContainer = styled.div<{ left?: boolean }>` + display: flex; + justify-content: ${(props) => (props.left ? "flex-start" : "flex-end")}; +`; + +const Breakdown = styled.div` + display: flex; + flex-direction: column; + width: 100%; + gap: 4px; + @media (max-width: ${size.mobile}) { + gap: 0px; + } +`; + +const BreakdownRow = styled.div` + display: flex; + flex-direction: row; + justify-content: space-between; + gap: 4px; +`; diff --git a/projects/dex-ui/src/components/Well/Table/WellDetailRow.tsx b/projects/dex-ui/src/components/Well/Table/WellDetailRow.tsx new file mode 100644 index 0000000000..9f151385d3 --- /dev/null +++ b/projects/dex-ui/src/components/Well/Table/WellDetailRow.tsx @@ -0,0 +1,212 @@ +import { TokenValue } from "@beanstalk/sdk"; +import React, { FC, ReactNode } from "react"; +import { useNavigate } from "react-router-dom"; +import { Row, Td } from "src/components/Table"; +import { TokenLogo } from "src/components/TokenLogo"; +import styled from "styled-components"; +import { mediaQuery, size } from "src/breakpoints"; +import { formatNum } from "src/utils/format"; +import { Well } from "@beanstalk/sdk/Wells"; +import { Skeleton } from "src/components/Skeleton"; + +/// format value with 2 decimals, if value is less than 1M, otherwise use short format +const formatMayDecimals = (tv: TokenValue | undefined) => { + if (!tv) return "-.--"; + if (tv.lt(1_000_000)) { + return formatNum(tv, { minDecimals: 2, maxDecimals: 2 }); + } + return tv.toHuman("short"); +}; + +export const WellDetailRow: FC<{ + well: Well | undefined; + liquidity: TokenValue | undefined; + functionName: string | undefined; +}> = ({ well, liquidity, functionName }) => { + const navigate = useNavigate(); + + if (!well) return null; + + const tokens = well?.tokens || []; + const logos: ReactNode[] = []; + const smallLogos: ReactNode[] = []; + const symbols: string[] = []; + const gotoWell = () => navigate(`/wells/${well?.address}`); + + tokens.map((token: any) => { + logos.push(); + smallLogos.push(); + symbols.push(token.symbol); + }); + + return ( + + + + {logos} + {symbols.join("/")} + + + + {functionName || "Price Function"} + + + 0.00% + + + ${liquidity ? liquidity.toHuman("short") : "-.--"} + + + + {{smallLogos[0]}} + {formatMayDecimals(well.reserves?.[0])} + + + {{smallLogos[1]}} + {formatMayDecimals(well.reserves?.[1])} + + {well.reserves && well.reserves.length > 2 ? {`+ ${well.reserves.length - 2} MORE`} : null} + + + + {logos} + {symbols.join("/")} + + ${formatNum(liquidity, { minDecimals: 2 })} + + + ); +}; + +export const WellDetailLoadingRow: FC<{}> = () => { + return ( + {}}> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ); +}; + +const LoadingColumn = styled.div<{ align?: "right" | "left" }>` + display: flex; + flex-direction: column; + gap: 8px; + ${(props) => ` + align-items: ${props.align === "right" ? "flex-end" : "flex-start"}; + `} + + ${mediaQuery.sm.only} { + gap: 4px; + } +`; + +const TableRow = styled(Row)` + @media (max-width: ${size.mobile}) { + height: 66px; + } +`; + +const DesktopContainer = styled(Td)` + @media (max-width: ${size.mobile}) { + display: none; + } +`; + +const MobileContainer = styled(Td)` + padding: 8px 16px; + @media (min-width: ${size.mobile}) { + display: none; + } +`; + +const WellDetail = styled.div` + display: flex; + flex-direction: row; + gap: 8px; + @media (min-width: ${size.mobile}) { + flex-direction: column; + } +`; + +const TokenLogos = styled.div` + display: flex; + div:not(:first-child) { + margin-left: -8px; + } +`; +const TokenSymbols = styled.div` + font-size: 20px; + line-height: 24px; + color: #1c1917; + @media (max-width: ${size.mobile}) { + font-size: 14px; + margin-top: 2px; + } +`; + +const Amount = styled.div` + font-weight: 500; + font-size: 20px; + line-height: 24px; + color: #1c1917; + + @media (max-width: ${size.mobile}) { + font-size: 14px; + font-weight: normal; + } +`; + +const Reserves = styled.div` + display: flex; + flex-direction: row; + justify-content flex-end; + align-items: center; + gap: 4px; + flex: 1; +`; + +const MoreReserves = styled.div` + color: #9ca3af; +`; + +const TradingFee = styled.div` + font-size: 20px; + line-height: 24px; + color: #4b5563; + text-transform: uppercase; +`; + +const WellPricing = styled.div` + font-size: 20px; + line-height: 24px; + text-transform: capitalize; +`; + +const TokenLogoWrapper = styled.div` + margin-bottom: 2px; +`; diff --git a/projects/dex-ui/src/pages/Home.tsx b/projects/dex-ui/src/pages/Home.tsx index 0d3482ff32..0ce89f0893 100644 --- a/projects/dex-ui/src/pages/Home.tsx +++ b/projects/dex-ui/src/pages/Home.tsx @@ -1,67 +1,126 @@ /* eslint-disable jsx-a11y/accessible-emoji */ import React from "react"; -import { size } from "src/breakpoints"; -import { Link } from "react-router-dom"; -import { RightArrowCircle } from "src/components/Icons"; +import { mediaQuery } from "src/breakpoints"; + import styled from "styled-components"; +import shapesIcons from "src/assets/images/home-banner.svg"; +import { BodyL } from "src/components/Typography"; +import { ContractInfoMarquee } from "src/components/Frame/ContractInfoMarquee"; + +const copy = { + build: "Use components written, audited and deployed by other developers for your custom liquidity pool.", + deploy: "Liquidity pools with unique pricing functions for more granular market making.", + fees: "Trade assets using liquidity pools that donโ€™t impose trading fees." +}; + +const links = { + multiFlowPump: "/multi-flow-pump.pdf", + whitepaper: "/basin.pdf", + docs: "https://docs.basin.exchange/", + wells: "/#/wells", + swap: "/#/swap" +}; export const Home = () => { return ( - - - - - - - ๐Ÿ”ฎ Multi-block MEV manipulation resistant oracle{" "} - - whitepaper - - - - - A Composable EVM-native DEX - - Customizable liquidity pools with shared components.   - - Read the whitepaper โ†’ - - - - - - - ๐Ÿ”ฎ - {" "} - Build using components - - - - โšก๏ธ - {" "} - Deploy flexible liquidity - - - - โค๏ธ - {" "} - Zero-fee swaps - - - - + <> + + + + + + Multi-Flow Pump is here! +
+ Explore the multi-block MEV manipulation resistant Oracle framework, with easy + integration for everyone. +
+
+ + Get Started โ†’ + +
+
+ + + A Composable EVM-native DEX + + Customizable liquidity pools with shared components.  + + Read the whitepaper โ†’ + + + + + + + + ๐Ÿ”ฎ + +  Build using components + + {copy.build} + + + +
+ + โšก๏ธ + +  Deploy flexible liquidity +
+
+ {copy.deploy} +
+ + +
+ + โค๏ธ + +  Zero-fee swaps +
+
+ {copy.fees} +
+
+
+ + + +
+
+ ); }; +const MarqueeContainer = styled.div` + position: fixed; + bottom: 72px; + + ${mediaQuery.sm.only} { + position: fixed; + left: 0; + bottom: 0; + } +`; + const Container = styled.div` height: calc(100% - 24px); - padding: 12px; - @media (min-width: ${size.mobile}) { - padding: 0px; + padding-top: 12px; + padding-left: 12px; + padding-right: 12px; + padding-bottom: 0px; + + ${mediaQuery.sm.up} { + padding-top: 32px; + padding-left: 48px; + padding-right: 48px; + height: 100%; width: 100%; justify-content: center; align-items: center; + box-sizing: border-box; } `; @@ -70,29 +129,91 @@ const Content = styled.div` flex-direction: column; justify-content: space-between; height: 100%; - @media (min-width: ${size.mobile}) { - gap: 48px; - justify-content: center; + + ${mediaQuery.sm.up} { + justify-content: space-between; align-items: center; } `; -const MevBubble = styled.div` - display: none; - @media (min-width: ${size.mobile}) { - display: flex; - box-sizing: border-box; - flex-direction: row; - justify-content: center; +const MevBanner = styled.div` + background: #fff; + width: 100%; + border: 0.25px solid #9ca3af; + ${mediaQuery.sm.only} { + display: none; + } +`; + +const MevBannerBG = styled.div` + background: url(${shapesIcons}); + background-size: contain; + background-repeat: no-repeat; + background-position: right; + + display: flex; + flex-direction: row; + align-items: center; + justify-content: space-between; + height: auto; + padding: 24px; + width: 100%; + box-sizing: border-box; +`; + +const MevInfo = styled.div` + display: flex; + flex-direction: column; + gap: 8px; +`; + +const MevTitle = styled.div` + ${BodyL} +`; + +const GetStartedContainer = styled.a` + :focus { + text-decoration: none; + } +`; + +const GetStarted = styled.div` + display: flex; + justify-content: center; + align-items: center; + padding: 12px; + background: #000; + outline: 0.5px solid #000; + color: #fff; + font-weight: 600; + font-size: 16px; + line-height: 24px; + letter-spacing: 0.32px; + white-space: nowrap; + cursor: pointer; + + :hover { + outline: 2px solid #46b955; + } + + :focus { + outline: 2px solid #46b955; + } +`; + +const InfoContainer = styled.div` + display: flex; + flex-direction: column; + gap: 8px; + box-sizing: border-box; + height: 100%; + + ${mediaQuery.sm.up} { + padding-top: min(25%, 185px); + justify-content: flex-start align-items: center; - padding: 8px; - gap: 8px; - height: 40px; - line-height: 16px; - width: 522px; - background: #ffffff; - border: 0.25px solid #4b5563; - border-radius: 100px; + width: 100%; + gap: 72px; } `; @@ -100,9 +221,8 @@ const TitleSubtitleContainer = styled.div` display: flex; flex-direction: column; gap: 8px; - @media (min-width: ${size.mobile}) { - display: flex; - flex-direction: column; + ${mediaQuery.sm.up} { + align-items: center; gap: 48px; } `; @@ -111,10 +231,11 @@ const Title = styled.div` font-size: 32px; font-weight: 600; line-height: 40px; - @media (min-width: ${size.mobile}) { + ${mediaQuery.sm.up} { font-style: normal; font-size: 72px; line-height: 100%; + text-align: center; } `; @@ -128,7 +249,7 @@ const SubTitle = styled.div` line-height: 22px; color: #4b5563; gap: 8px; - @media (min-width: ${size.mobile}) { + ${mediaQuery.sm.up} { flex-direction: row; font-size: 20px; line-height: 24px; @@ -138,16 +259,6 @@ const SubTitle = styled.div` } `; -const OracleWP = styled.a` - color: #46b955; - text-decoration: none; - display: flex; - align-items: center; - :hover { - text-decoration: underline; - } -`; - const WhitepaperLink = styled.a` font-weight: 400; font-size: 14px; @@ -157,65 +268,129 @@ const WhitepaperLink = styled.a` text-decoration: none; display: flex; align-items: center; + white-space: nowrap; + margin-left: 4px; :hover { text-decoration: underline; } - @media (min-width: ${size.mobile}) { + ${mediaQuery.sm.up} { font-size: 20px; line-height: 24px; } `; -const Boxes = styled.div` - box-sizing: border-box; +const AccordionContainer = styled.div` + /// Desktop display: flex; - flex-direction: column; - gap: 12px; - justify-content: space-around; - position: fixed; - bottom: 12px; - width: calc(100vw - 24px); - @media (min-width: ${size.mobile}) { - flex-direction: row; + flex-direction: row; + justify-content: space-between; + align-items: center; + gap: 24px; + width: 100%; + + /// Tablet + ${mediaQuery.md.only} { + width: 100%; + flex-direction: column; + gap: 12px; + } + + /// Mobile + ${mediaQuery.sm.only} { + flex-direction: column; position: relative; - bottom: 0px; - gap: 48px; - padding: 0 48px; - width: 100vw; + bottom: calc(57px + 12px); // 57px is the height of the contract info marquee + gap: 12px; + justify-content: space-around; + position: fixed; + width: calc(100vw - 24px); } `; -const Box = styled(Link)` +const Emoji = styled.span` + margin-right: 4px; +`; + +const AccordionItem = styled.a` display: flex; + flex-direction: column; justify-content: center; align-items: center; - - background: #f9f8f6; + background-color: #f9f9f9; + color: #444; + cursor: pointer; + padding: 24px; border: 0.5px solid #4b5563; - flex-grow: 1; - - font-weight: 600; - font-size: 14px; - line-height: 22px; - padding: 12px; - + outline: 1.5px solid white; + text-align: left; + width: 33%; + transition: background-color 0.3s ease; + overflow: hidden; + max-height: 113px; // Initial max-height + box-sizing: border-box; text-decoration: none; - color: black; - :hover { + &:hover { + border: 1.5px solid #46b955; background-color: #f0fdf4; + outline: 0.5px solid transparent; + max-height: 250px; // Adjust as needed for your content } - @media (min-width: ${size.mobile}) { - padding: 0px; - font-size: 24px; - line-height: 32px; - height: 80px; + ${mediaQuery.md.up} { + padding: 24px; + height: 100%; + } + + ${mediaQuery.md.only} { + width: calc(100vw - 86px); + height: auto; + :last-child { + margin-bottom: 24px; + } + } + + ${mediaQuery.sm.only} { + width: calc(100vw - 24px); + max-height: 80px; + padding: 12px; } `; -const Emoji = styled.span` - margin-right: 4px; +const AccordionContent = styled.div` + overflow: hidden; + opacity: 0; // Initially hidden + transition: opacity 0.3s ease-out, max-height 0.3s ease-out; + max-height: 0; + width: 100%; // Ensure it takes full width + + ${AccordionItem}:hover & { + padding-top: 12px; + opacity: 1; + max-height: 200px; // Adjust as needed for your content + } + + ${mediaQuery.sm.only} { + display: none; + } +`; + +const AccordionTitle = styled.div` + text-align: center; + width: 100%; + font-weight: 600; + font-size: 24px; + line-height: 32px; + + ${mediaQuery.md.only} { + font-size: 20px; + line-height: 24px; + } + + ${mediaQuery.sm.only} { + font-size: 14px; + line-height: 22px; + } `; diff --git a/projects/dex-ui/src/pages/Liquidity.tsx b/projects/dex-ui/src/pages/Liquidity.tsx index 2e6cf729a2..50f29b6a45 100644 --- a/projects/dex-ui/src/pages/Liquidity.tsx +++ b/projects/dex-ui/src/pages/Liquidity.tsx @@ -1,6 +1,5 @@ -import React, { useCallback, useEffect, useState } from "react"; -import { useNavigate, useParams } from "react-router-dom"; -import { useWell } from "src/wells/useWell"; +import React, { useCallback, useEffect, useRef, useState } from "react"; +import { useNavigate } from "react-router-dom"; import styled from "styled-components"; import { Page } from "src/components/Page"; import { LiquidityBox } from "src/components/Well/LiquidityBox"; @@ -16,26 +15,19 @@ import { Log } from "src/utils/logger"; import { BodyXS, TextNudge } from "src/components/Typography"; import { ImageButton } from "src/components/ImageButton"; import { ChevronDown } from "src/components/Icons"; -import { size } from "src/breakpoints"; -import { Loading } from "../components/Loading"; -import { Error } from "../components/Error"; +import { mediaQuery, size } from "src/breakpoints"; +import { Error } from "src/components/Error"; +import { LoadingItem } from "src/components/LoadingItem"; +import { useWellWithParams } from "src/wells/useWellWithParams"; export const Liquidity = () => { - const { address: wellAddress } = useParams<"address">(); + const { well, loading, error } = useWellWithParams(); const navigate = useNavigate(); - const { well, loading, error } = useWell(wellAddress!); + const [wellFunctionName, setWellFunctionName] = useState("This Well's Function"); - const [isMobile, setIsMobile] = useState(window.matchMedia(`(max-width: ${size.mobile})`).matches); - const [tab, setTab] = useState(isMobile ? null : 0); + const [tab, setTab] = useState(0); - // Media query - useEffect(() => { - window.matchMedia(`(max-width: ${size.mobile})`).addEventListener("change", (event) => setIsMobile(event.matches)); - - return () => { - window.matchMedia(`(max-width: ${size.mobile})`).removeEventListener("change", (event) => setIsMobile(event.matches)); - }; - }, []); + const scrollRef = useRef(null); // Slippage-related const [showSlippageSettings, setShowSlippageSettings] = useState(false); @@ -49,7 +41,6 @@ export const Liquidity = () => { Log.module("liquidity").debug(`Slippage changed: ${parseFloat(value)}`); setSlippage(parseFloat(value)); }; - // /Slippage-related const [open, setOpen] = useState(false); const toggle = useCallback(() => { @@ -66,8 +57,6 @@ export const Liquidity = () => { run(); }, [well]); - if (loading) return ; - if (error) { return ; } @@ -76,58 +65,68 @@ export const Liquidity = () => { -