diff --git a/src/app/bakery/components/Banners/BalanceBanner.tsx b/src/app/bakery/components/Banners/BalanceBanner.tsx new file mode 100644 index 00000000..8ad00761 --- /dev/null +++ b/src/app/bakery/components/Banners/BalanceBanner.tsx @@ -0,0 +1,195 @@ +"use client"; + +import clsx from "clsx"; +import { ReactNode } from "react"; +import { Logo } from "@breadcoop/ui"; +import { useConnectedUser } from "@/app/core/hooks/useConnectedUser"; +import { useTokenBalances } from "@/app/core/context/TokenBalanceContext/TokenBalanceContext"; +import { XDAIIcon } from "@/app/core/components/Icons/TokenIcons"; +import type { TSwapMode } from "../Swap/Panel"; +import { ArrowIcon } from "./Shared"; + +const GAS_FLOOR = 0.001; + +type BalanceBannerProps = { + onSelectTab: (mode: TSwapMode) => void; +}; + +export function BalanceBanner({ onSelectTab }: BalanceBannerProps) { + const { user } = useConnectedUser(); + const { xDAI, BREAD } = useTokenBalances(); + + if (user.status !== "CONNECTED") return null; + if (!xDAI || xDAI.status !== "SUCCESS") return null; + if (!BREAD || BREAD.status !== "SUCCESS") return null; + + const xdaiBalance = parseFloat(xDAI.value); + const breadBalance = parseFloat(BREAD.value); + + if (xdaiBalance < GAS_FLOOR) { + return ; + } + + if (breadBalance === 0) { + return ; + } + + return null; +} + +function NoGasBanner({ onSelectTab }: BalanceBannerProps) { + const title = "You need xDAI to bake"; + const description = + "xDAI pays for gas on Gnosis Chain. Bridge in from another chain or buy with fiat to get started."; + + return ( + + {/* large */} +
+
+ +
+ +
+ onSelectTab("BRIDGE")} + > + Bridge + + onSelectTab("BUY")} + > + Buy + +
+
+ {/* small */} +
+ +
+ onSelectTab("BRIDGE")} + withArrow + > + Bridge + + onSelectTab("BUY")} + withArrow + > + Buy + +
+
+
+ ); +} + +function NoBreadBanner({ onSelectTab }: BalanceBannerProps) { + const title = "Ready to bake?"; + const description = + "You have xDAI but no BREAD yet. Bake BREAD to join the Solidarity Fund and support a worker-owned future."; + + return ( + + {/* large */} +
+
+ +
+ +
+ onSelectTab("BAKE")} + > + Bake BREAD + +
+
+ {/* small */} +
+ + onSelectTab("BAKE")} + > + Bake BREAD + +
+
+ ); +} + +function InlineText({ + title, + description, +}: { + title: string; + description: string; +}) { + return ( +
+ + {title} + + {description} +
+ ); +} + +function StackedText({ + title, + description, +}: { + title: string; + description: string; +}) { + return ( +
+ {title} + {description} +
+ ); +} + +function FullWidthBand({ children }: { children: ReactNode }) { + return ( +
+
{children}
+
+ ); +} + +function PillButton({ + children, + onClick, + variant, + withArrow = false, +}: { + children: string; + onClick: () => void; + variant: "primary" | "secondary"; + withArrow?: boolean; +}) { + return ( + + ); +} diff --git a/src/app/bakery/components/Swap/NewSwap.tsx b/src/app/bakery/components/Swap/NewSwap.tsx index 8d686f83..da4c86cf 100644 --- a/src/app/bakery/components/Swap/NewSwap.tsx +++ b/src/app/bakery/components/Swap/NewSwap.tsx @@ -1,5 +1,12 @@ import { useConnectedUser } from "@/app/core/hooks/useConnectedUser"; -import React, { ChangeEvent, useCallback, useState, useEffect } from "react"; +import React, { + ChangeEvent, + Dispatch, + SetStateAction, + useCallback, + useState, + useEffect, +} from "react"; import { Address } from "viem"; import { useSearchParams } from "next/navigation"; // import { FromPanel } from "./FromPanel"; @@ -38,16 +45,15 @@ const notes: Record = { "BUY": "Clicking the button will open the Peer website where you can complete your purchase of xDAI to bake into BREAD.", }; -const initialSwapState: TSwapState = { - mode: "BAKE", - value: "", +type NewSwapProps = { + swapState: TSwapState; + setSwapState: Dispatch>; }; -const NewSwap = () => { +const NewSwap = ({ swapState, setSwapState }: NewSwapProps) => { const { user, isSafe } = useConnectedUser(); const [connectedAccountAddress, setConnectedAccountAddress] = useState(null); - const [swapState, setSwapState] = useState(initialSwapState); const searchParams = useSearchParams(); const { toastDispatch } = useToast(); const { isMobile } = useStrictMobile() @@ -69,13 +75,15 @@ const NewSwap = () => { // eslint-disable-next-line react-hooks/exhaustive-deps }, [searchParams.get("peer")]); - if ( - user.status === "CONNECTED" && - user.address !== connectedAccountAddress - ) { - setConnectedAccountAddress(user.address); - setSwapState((state) => ({ ...state, value: "" })); - } + useEffect(() => { + if ( + user.status === "CONNECTED" && + user.address !== connectedAccountAddress + ) { + setConnectedAccountAddress(user.address); + setSwapState((state) => ({ ...state, value: "" })); + } + }, [user, connectedAccountAddress, setSwapState]); const clearInputValue = useCallback(() => { setSwapState((state) => ({ ...state, value: "" })); diff --git a/src/app/components/HomeBanner.tsx b/src/app/components/HomeBanner.tsx new file mode 100644 index 00000000..a046563d --- /dev/null +++ b/src/app/components/HomeBanner.tsx @@ -0,0 +1,25 @@ +"use client"; + +import dynamic from "next/dynamic"; +import type { TSwapMode } from "../bakery/components/Swap/Panel"; + +const BalanceBanner = dynamic( + () => + import("../bakery/components/Banners/BalanceBanner").then( + (m) => m.BalanceBanner + ), + { ssr: false } +); + +export const SWAP_WIDGET_ANCHOR_ID = "swap-widget"; + +export default function HomeBanner() { + const handleSelectTab = (mode: TSwapMode) => { + if (typeof window === "undefined") return; + window.location.hash = mode.toLowerCase(); + const el = document.getElementById(SWAP_WIDGET_ANCHOR_ID); + if (el) el.scrollIntoView({ behavior: "smooth", block: "start" }); + }; + + return ; +} diff --git a/src/app/components/SwapWrapper.tsx b/src/app/components/SwapWrapper.tsx index b95ec2d4..d59a78b4 100644 --- a/src/app/components/SwapWrapper.tsx +++ b/src/app/components/SwapWrapper.tsx @@ -1,21 +1,45 @@ "use client"; import dynamic from "next/dynamic"; -// import NewSwap from "../bakery/components/Swap/NewSwap"; - -// const Swap = dynamic(() => import("../bakery/components/Swap/Swap"), { -// ssr: false, -// }); +import { useEffect, useState } from "react"; +import type { TSwapMode, TSwapState } from "../bakery/components/Swap/Panel"; +import { SWAP_WIDGET_ANCHOR_ID } from "./HomeBanner"; const NewSwap = dynamic(() => import("../bakery/components/Swap/NewSwap"), { ssr: false, }); +const initialSwapState: TSwapState = { + mode: "BAKE", + value: "", +}; + +const VALID_MODES: TSwapMode[] = ["BAKE", "BURN", "BRIDGE", "BUY"]; + +function readModeFromHash(): TSwapMode | null { + if (typeof window === "undefined") return null; + const hash = window.location.hash.replace(/^#/, "").toUpperCase(); + return (VALID_MODES as string[]).includes(hash) ? (hash as TSwapMode) : null; +} + export default function SwapWrapper() { + const [swapState, setSwapState] = useState(initialSwapState); + + useEffect(() => { + const mode = readModeFromHash(); + if (mode) setSwapState({ mode, value: "" }); + + const onHashChange = () => { + const next = readModeFromHash(); + if (next) setSwapState({ mode: next, value: "" }); + }; + window.addEventListener("hashchange", onHashChange); + return () => window.removeEventListener("hashchange", onHashChange); + }, []); + return ( - <> - {/* */} - - +
+ +
); } diff --git a/src/app/page.tsx b/src/app/page.tsx index 49fe74e9..3667f22b 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -4,6 +4,7 @@ import { type Metadata } from "next"; import { AppTitle } from "./bakery/components/AppTitle"; import FAQWrapper from "./components/FAQWrapper"; +import HomeBanner from "./components/HomeBanner"; import SwapWrapper from "./components/SwapWrapper"; import { Body, Heading1, Heading2, LiftedButton } from "@breadcoop/ui"; import { Apy } from "./bakery/components/Apy"; @@ -19,6 +20,8 @@ import { generateMetadata } from "@/lib/site-metadata"; export default function Home() { return ( + <> +
@@ -78,5 +81,6 @@ export default function Home() {
+ ); }