From 7fe7ebabd0c3c85280f568c491b50c8937a1722a Mon Sep 17 00:00:00 2001 From: Matthew Garrett Date: Tue, 28 Jan 2025 14:56:35 -0500 Subject: [PATCH 1/2] Adds wip of the new pool page Adds PriceRangeStep and DepositAmountsStep adds global classes fixes side tag Adds reset button functionality for pool page fixes errors hides the side tag on mobile fixes txn animation fixes txn animation run format fixes errors fixes another error --- apps/flame-defi/app/bridge/page.tsx | 2 +- .../components/DepositCard/DepositCard.tsx | 8 +- .../MobileWalletConnect.tsx | 3 +- .../NetworkSelector/NetworkSelector.tsx | 7 +- .../app/components/SideTag/SideTag.tsx | 33 +++ .../components/WithdrawCard/WithdrawCard.tsx | 2 +- apps/flame-defi/app/constants.ts | 73 +++++++ .../SingleWalletConnect.tsx | 1 + apps/flame-defi/app/globals.css | 12 +- apps/flame-defi/app/layout.tsx | 2 + .../pool/components/AreaChartWithRange.tsx | 111 ++++++++++ .../pool/components/DepositAmountsStep.tsx | 125 +++++++++++ .../app/pool/components/MiniAreaChart.tsx | 108 +++++++++ .../app/pool/components/NewPoolPosition.tsx | 137 ++++++++++++ .../app/pool/components/PriceRangeStep.tsx | 206 ++++++++++++++++++ .../app/pool/components/TokenPairsStep.tsx | 195 +++++++++++++++++ apps/flame-defi/app/pool/page.tsx | 68 ++++-- apps/flame-defi/app/pool/useDepositTxn.tsx | 13 ++ apps/flame-defi/app/swap/page.tsx | 54 ++--- apps/flame-defi/app/swap/useTokenModal.tsx | 49 ----- apps/flame-defi/app/swap/useTxnInfo.tsx | 2 +- apps/flame-defi/package.json | 2 + apps/flame-defi/tailwind.config.js | 1 + package-lock.json | 53 +++++ .../AnimatedDownArrowSpacer.tsx | 2 +- .../src/components/Dropdown/Dropdown.test.tsx | 9 +- .../ui/src/components/Dropdown/Dropdown.tsx | 6 +- .../SettingsPopover/SettingsPopover.tsx | 4 +- .../ui/src/components/SideTag/SideTag.tsx | 30 --- .../components/ToggleSwitch/ToggleSwitch.tsx | 42 ++++ .../TokenSelector/TokenSelector.tsx | 46 ++-- .../ui/src/components/TxnInfo/TxnInfo.tsx | 4 +- packages/ui/src/components/index.ts | 3 +- packages/ui/src/icons/MinusIcon.tsx | 22 ++ packages/ui/src/icons/ResetIcon.tsx | 26 +++ packages/ui/src/icons/index.ts | 2 + .../ui/src/shadcn-primitives/breadcrumbs.tsx | 114 ++++++++++ packages/ui/src/shadcn-primitives/index.ts | 1 + packages/ui/src/types.ts | 9 + 39 files changed, 1414 insertions(+), 173 deletions(-) create mode 100644 apps/flame-defi/app/components/SideTag/SideTag.tsx create mode 100644 apps/flame-defi/app/constants.ts create mode 100644 apps/flame-defi/app/pool/components/AreaChartWithRange.tsx create mode 100644 apps/flame-defi/app/pool/components/DepositAmountsStep.tsx create mode 100644 apps/flame-defi/app/pool/components/MiniAreaChart.tsx create mode 100644 apps/flame-defi/app/pool/components/NewPoolPosition.tsx create mode 100644 apps/flame-defi/app/pool/components/PriceRangeStep.tsx create mode 100644 apps/flame-defi/app/pool/components/TokenPairsStep.tsx create mode 100644 apps/flame-defi/app/pool/useDepositTxn.tsx delete mode 100644 apps/flame-defi/app/swap/useTokenModal.tsx delete mode 100644 packages/ui/src/components/SideTag/SideTag.tsx create mode 100644 packages/ui/src/components/ToggleSwitch/ToggleSwitch.tsx create mode 100644 packages/ui/src/icons/MinusIcon.tsx create mode 100644 packages/ui/src/icons/ResetIcon.tsx create mode 100644 packages/ui/src/shadcn-primitives/breadcrumbs.tsx diff --git a/apps/flame-defi/app/bridge/page.tsx b/apps/flame-defi/app/bridge/page.tsx index 2ed787c..0aca51f 100644 --- a/apps/flame-defi/app/bridge/page.tsx +++ b/apps/flame-defi/app/bridge/page.tsx @@ -46,7 +46,7 @@ export default function BridgePage(): React.ReactElement {
-
+
    {tabs.map((tab) => ( (""); const [isRecipientAddressEditable, setIsRecipientAddressEditable] = useState(false); + const handleEditRecipientClick = useCallback(() => { setIsRecipientAddressEditable(!isRecipientAddressEditable); }, [isRecipientAddressEditable]); + const handleEditRecipientSave = () => { setIsRecipientAddressEditable(false); // reset evmWalletState when user manually enters address resetEvmWalletState(); }; + const handleEditRecipientClear = () => { setIsRecipientAddressEditable(false); setRecipientAddressOverride(""); @@ -166,6 +169,7 @@ export default function DepositCard(): React.ReactElement { }, [selectedEvmChain, handleConnectEvmWallet]); const handleDeposit = async () => { + console.log("handleDeposit"); if (!selectedCosmosChain || !selectedIbcCurrency) { addNotification({ toastOpts: { @@ -359,7 +363,7 @@ export default function DepositCard(): React.ReactElement { {isAnimating ? ( ) : ( -
    +
    @@ -515,7 +519,7 @@ export default function DepositCard(): React.ReactElement {
    )} - {!isBridgePage && (
    {userAccount.address ? ( -
    +
    diff --git a/apps/flame-defi/app/components/NetworkSelector/NetworkSelector.tsx b/apps/flame-defi/app/components/NetworkSelector/NetworkSelector.tsx index 97dfbd5..b61292a 100644 --- a/apps/flame-defi/app/components/NetworkSelector/NetworkSelector.tsx +++ b/apps/flame-defi/app/components/NetworkSelector/NetworkSelector.tsx @@ -25,6 +25,11 @@ export default function NetworkSelector(): React.ReactElement { [selectFlameNetwork], ); + //NOTES: THING TO TRACK: + // evmChainOptions + // selectedEvmChain + // selectedEvmNetwork + return ( + handleInputChange(Number(e.target.value), true) + } + className="normalize-input w-[100%]" + placeholder="0" + /> + $0 +
    +
    +
    + {tokenOne?.Icon && } + {tokenOne?.symbol} +
    +
    + {/* TODO: This will be replaced with the wallet balance */} + {"0"} + {tokenOne?.symbol} + + Max + +
    +
    +
    +
    +
    null} + className={`flex flex-col rounded-md p-transition bg-semi-white border border-solid border-transparent hover:border-grey-medium mt-2 p-4`} + > +
    +
    + + handleInputChange(Number(e.target.value), false) + } + className="normalize-input w-[100%]" + placeholder="0" + /> + $0 +
    +
    +
    + {tokenTwo?.Icon && } + {tokenTwo?.symbol} +
    +
    + {/* TODO: This will be replaced with the wallet balance */} + {"0"} + {tokenTwo?.symbol} + + Max + +
    +
    +
    +
    + {/* TODO: This is a temp example of how we might conditionally render the action button */} + {actionText !== "Deposit" && ( +
    + {actionText} +
    + )} + {/* TODO: This is a temp example of how we might conditionally render the action button */} + {actionText === "Deposit" && ( + console.log("DO A DEPOSIT")} + buttonText="Deposit" + className="w-full mt-5" + /> + )} +
    + )} +
    + ); +} diff --git a/apps/flame-defi/app/pool/components/MiniAreaChart.tsx b/apps/flame-defi/app/pool/components/MiniAreaChart.tsx new file mode 100644 index 0000000..5cefe7e --- /dev/null +++ b/apps/flame-defi/app/pool/components/MiniAreaChart.tsx @@ -0,0 +1,108 @@ +// ChartExample.tsx +import React from "react"; +import ReactEChartsCore from "echarts-for-react/lib/core"; +import * as echarts from "echarts/core"; +import { + LineChart, + LineSeriesOption, + ScatterChart, + ScatterSeriesOption, +} from "echarts/charts"; +import { GridComponent, GridComponentOption } from "echarts/components"; +import { CanvasRenderer } from "echarts/renderers"; + +// Register only the chart types and components we use +echarts.use([LineChart, ScatterChart, GridComponent, CanvasRenderer]); + +// Type for ECharts option +type ECOption = echarts.ComposeOption< + LineSeriesOption | ScatterSeriesOption | GridComponentOption +>; + +const MiniAreaChart: React.FC = () => { + // Sample data (use your real data here) + const data: number[] = [ + 20, 18, 19, 17, 18, 16, 14, 15, 14, 16, 15, 18, 20, 22, 21, 23, + ]; + + // The last data point + const lastValue = data[data.length - 1]; + + const option: ECOption = { + // Remove all labels, axis lines, ticks, etc. + xAxis: { + type: "category", + data: data.map((_, i) => i.toString()), + show: false, + }, + yAxis: { + type: "value", + show: false, + // Add min and max to control the vertical positioning + min: (value: { min: number; max: number }) => + value.min - (value.max - value.min) * 0.2, + max: (value: { min: number; max: number }) => + value.max + (value.max - value.min) * 0.2, + }, + grid: { + left: 0, + right: 0, + top: 0, + bottom: 0, + }, + tooltip: { + show: false, // disable hover tooltip + }, + series: [ + // Main line + { + type: "line", + data, + smooth: false, + showSymbol: false, + lineStyle: { + color: "rgba(230, 149, 41, 1)", // green line (adjust as needed) + width: 2, + }, + // We'll use a markLine for the dotted baseline + markLine: { + symbol: ["none", "none"], + lineStyle: { + type: "dotted", + color: "rgba(230, 149, 41, 1)", + width: 1, + }, + silent: true, + symbolSize: [0, 0], + label: { + show: false, + }, + data: [{ yAxis: lastValue }], + }, + }, + // Dot (circle) on the last data point + { + type: "scatter", + data: [[data.length - 1, lastValue]], + symbolSize: 8, + itemStyle: { + color: "rgba(230, 149, 41, 1)", + }, + // No label or additional hover + emphasis: { disabled: true }, + }, + ], + }; + + return ( +
    + +
    + ); +}; + +export default MiniAreaChart; diff --git a/apps/flame-defi/app/pool/components/NewPoolPosition.tsx b/apps/flame-defi/app/pool/components/NewPoolPosition.tsx new file mode 100644 index 0000000..805e70c --- /dev/null +++ b/apps/flame-defi/app/pool/components/NewPoolPosition.tsx @@ -0,0 +1,137 @@ +import { + Breadcrumb, + BreadcrumbItem, + BreadcrumbList, + BreadcrumbPage, + BreadcrumbSeparator, + Button, +} from "@repo/ui/shadcn-primitives"; +import { useState } from "react"; +import TokenPairsStep from "./TokenPairsStep"; +import PriceRangeStep from "./PriceRangeStep"; +import DepositAmountsStep from "./DepositAmountsStep"; +import { feeData, tokens } from "../../constants"; +import { TokenItem } from "@repo/ui/types"; +import { ChevronDownIcon, ResetIcon } from "@repo/ui/icons"; +import { useAccount } from "wagmi"; + +export interface StepProps { + step: number; + setStep: (thing: number) => void; + tokenPair: TokenPair; + selectedFeeTier: FeeData | undefined; +} + +export interface TokenPair { + tokenOne: TokenItem | undefined; + tokenTwo: TokenItem | undefined; +} + +export interface FeeData { + id: number; + feePercent: string; + text: string; + tvl: string; + selectPercent: string; +} + +export default function NewPoolPosition({ + newPositionPage, + setNewPositionPage, +}: { + newPositionPage: boolean; + setNewPositionPage: (newPositionPage: boolean) => void; +}): React.ReactElement { + const userAccount = useAccount(); + const [step, setStep] = useState(0); + const [selectedFeeTier, setSelectedFeeTier] = useState( + feeData[0], + ); + const [tokenPair, setTokenPair] = useState({ + tokenOne: tokens[0], + tokenTwo: undefined, + }); + + const reset = () => { + setStep(0); + setTokenPair({ + tokenOne: tokens[0], + tokenTwo: undefined, + }); + setSelectedFeeTier(feeData[0]); + }; + + return ( +
    +
    + {userAccount.address && newPositionPage && ( +
    setNewPositionPage(false)} + > + +

    Your Positions

    +
    + )} +
    +
    + + + + + Token Pairs/Fees + + + + + + Price Range + + + + + + Deposit Amounts + + + + + +
    +
    + + + +
    +
    + ); +} diff --git a/apps/flame-defi/app/pool/components/PriceRangeStep.tsx b/apps/flame-defi/app/pool/components/PriceRangeStep.tsx new file mode 100644 index 0000000..37e44ae --- /dev/null +++ b/apps/flame-defi/app/pool/components/PriceRangeStep.tsx @@ -0,0 +1,206 @@ +"use client"; + +import { StepProps } from "./NewPoolPosition"; +import { EditIcon, MinusIcon, PlusIcon } from "@repo/ui/icons"; +import { ActionButton, ToggleSwitch } from "@repo/ui/components"; +import { useCallback, useEffect, useState } from "react"; +import AreaChartWithRange from "./AreaChartWithRange"; +import { Button } from "@repo/ui/shadcn-primitives"; +import MiniAreaChart from "./MiniAreaChart"; + +export default function PriceRangeStep({ + step, + setStep, + tokenPair, +}: StepProps): React.ReactElement { + const { tokenOne, tokenTwo } = tokenPair; + const [selectedToken, setSelectedToken] = useState(tokenOne?.symbol || ""); + const [selectedRange, setSelectedRange] = useState("Full Range"); + const [minPrice, setMinPrice] = useState(0); + const [maxPrice, setMaxPrice] = useState(Infinity); + + useEffect(() => { + if (step === 0) { + setSelectedToken(tokenOne?.symbol || ""); + setSelectedRange("Full Range"); + setMinPrice(0); + setMaxPrice(Infinity); + } + }, [step, tokenOne]); + + useEffect(() => { + if (selectedRange === "Full Range") { + setMinPrice(0); + setMaxPrice(Infinity); + } else { + setMinPrice(0.001); + setMaxPrice(4); + } + }, [selectedRange]); + + const handleMinPriceChange = useCallback( + (action: "increase" | "decrease") => { + if (action === "increase") { + setMinPrice(minPrice + 0.01); + } else { + const newPrice = minPrice - 0.01; + if (newPrice >= 0) { + setMinPrice(newPrice); + } + } + }, + [minPrice], + ); + + const handleMaxPriceChange = useCallback( + (action: "increase" | "decrease") => { + if (maxPrice === Infinity) { + setMaxPrice(0); + } else if (action === "increase") { + setMaxPrice(maxPrice + 0.01); + } else { + const newPrice = maxPrice - 0.01; + if (newPrice >= 0) { + setMaxPrice(newPrice); + } + } + }, + [maxPrice], + ); + + return ( +
    + {step > 1 && ( +
    +
    +
    + +
    +
    +
    + {selectedRange} +
    + +
    +
    +
    + )} + {step === 1 && ( +
    +
    +

    Set price range

    +
    + +
    +
    +
    + +
    +

    + {selectedRange === "Full Range" + ? "Providing full range liquidity ensures continuous market participation across all possible prices, offering simplicity but with potential for higher impermanent loss." + : "Custom range allows you to concentrate your liquidity within specific price bounds, enhancing capital efficiency and fee earnings but requiring more active management."} +

    +
    + +
    +
    +
    +
    + + Min Price + + setMinPrice(Number(e.target.value))} + /> + + {selectedToken === tokenOne?.symbol + ? tokenTwo?.symbol + : tokenOne?.symbol}{" "} + per {selectedToken} + +
    +
    +
    handleMinPriceChange("increase")} + > + +
    +
    handleMinPriceChange("decrease")} + > + +
    +
    +
    +
    +
    + + Max Price + + setMaxPrice(Number(e.target.value))} + /> + + {selectedToken === tokenOne?.symbol + ? tokenTwo?.symbol + : tokenOne?.symbol}{" "} + per {selectedToken} + +
    +
    +
    handleMaxPriceChange("increase")} + > + +
    +
    handleMaxPriceChange("decrease")} + > + +
    +
    +
    +
    + setStep(2)} + /> +
    + )} +
    + ); +} diff --git a/apps/flame-defi/app/pool/components/TokenPairsStep.tsx b/apps/flame-defi/app/pool/components/TokenPairsStep.tsx new file mode 100644 index 0000000..9bc44a7 --- /dev/null +++ b/apps/flame-defi/app/pool/components/TokenPairsStep.tsx @@ -0,0 +1,195 @@ +import { ActionButton, TokenSelector } from "@repo/ui/components"; +import { Button, DialogTrigger } from "@repo/ui/shadcn-primitives"; +import { TokenItem } from "@repo/ui/types"; +import { tokens, feeData } from "../../constants"; +import { useState } from "react"; +import { CheckMarkIcon, ChevronDownIcon, EditIcon } from "@repo/ui/icons"; +import { FeeData, TokenPair } from "./NewPoolPosition"; + +export interface TokenPairsStepProps { + step: number; + setStep: (thing: number) => void; + setTokenPair: (thing: TokenPair) => void; + tokenPair: TokenPair; + selectedFeeTier: FeeData | undefined; + setSelectedFeeTier: (thing: FeeData) => void; +} + +const CustomTokenButton = ({ + selectedToken, + defaultTitle, +}: { + selectedToken?: TokenItem | null; + defaultTitle: string; +}) => ( + +
    +
    + {selectedToken?.Icon && } +

    + {selectedToken?.symbol || defaultTitle} +

    +
    + +
    +
    +); + +export default function TokenPairsStep({ + step, + setStep, + setTokenPair, + tokenPair, + selectedFeeTier, + setSelectedFeeTier, +}: TokenPairsStepProps): React.ReactElement { + const [showMore, setShowMore] = useState(false); + const { tokenOne, tokenTwo } = tokenPair; + const setToken = ( + position: "tokenOne" | "tokenTwo", + token: TokenItem | undefined, + ) => { + setTokenPair({ + ...tokenPair, + [position]: token, + }); + }; + + return ( +
    + {step > 0 && ( +
    +
    +
    + {tokenOne?.Icon && } + {tokenTwo?.Icon && } +

    + {tokenOne?.symbol} / {tokenTwo?.symbol} +

    + + Fees: {selectedFeeTier?.feePercent} + +
    + +
    +
    + )} + {step === 0 && ( +
    +
    +

    Select pair

    +

    + Choose the tokens you want to provide liquidity for. +

    +
    + setToken("tokenOne", token)} + CustomTokenButton={CustomTokenButton} + /> + setToken("tokenTwo", token)} + CustomTokenButton={CustomTokenButton} + /> +
    +
    +
    +
    +

    Fee tier

    +

    + The amount earned providing liquidity. Choose an amount that + suits your risk tolerance and strategy. +

    +
    +
    +
    + + {selectedFeeTier?.feePercent} fee tier + {selectedFeeTier?.id === 0 && ( + + Recommended + + )} + + + {selectedFeeTier?.text} + +
    +
    + +
    +
    +
    + {feeData.map(({ feePercent, text, tvl, selectPercent }, i) => ( +
    feeData[i] && setSelectedFeeTier(feeData[i])} + > +
    + + {feePercent} + {selectedFeeTier?.id === i && ( + + )} + + {text} +
    +
    + {tvl} + + {selectPercent} + +
    +
    + ))} +
    + setStep(1)} + disabled={ + tokenPair.tokenOne === undefined || + tokenPair.tokenTwo === undefined + } + /> +
    +
    + )} +
    + ); +} diff --git a/apps/flame-defi/app/pool/page.tsx b/apps/flame-defi/app/pool/page.tsx index d832a5f..3c6c42c 100644 --- a/apps/flame-defi/app/pool/page.tsx +++ b/apps/flame-defi/app/pool/page.tsx @@ -1,28 +1,62 @@ +"use client"; + import { ActionButton } from "@repo/ui/components"; import { InboxIcon, PlusIcon } from "@repo/ui/icons"; +import { useEvmWallet } from "features/EvmWallet/hooks/useEvmWallet"; import type React from "react"; +import { useAccount } from "wagmi"; +import NewPoolPosition from "./components/NewPoolPosition"; +import { useState } from "react"; export default function PoolPage(): React.ReactElement { + const { connectEvmWallet } = useEvmWallet(); + // TODO: this is a temporary state to display the new position page + const [newPositionPage, setNewPositionPage] = useState(false); + const userAccount = useAccount(); + + // TODO: Actually think through how the your positions, connect and new position pages should be displayed + return (
    -
    -
    -

    Pools

    - -
    -
    -
    - -

    - Your active V3 liquidity positions will appear here. -

    - +
    + {userAccount.address && !newPositionPage && ( +
    + setNewPositionPage(true)} + buttonText="New Position" + className="mt-0" + PrefixIcon={PlusIcon} + /> +
    + )} + {userAccount.address && newPositionPage && ( + + )} + {!newPositionPage && ( +
    +

    + {newPositionPage ? "Create Position" : "Pools"} +

    +
    +
    + +

    + Your active V3 liquidity positions will appear here. +

    + {!userAccount.address && ( + connectEvmWallet()} + /> + )} +
    +
    -
    + )}
    ); diff --git a/apps/flame-defi/app/pool/useDepositTxn.tsx b/apps/flame-defi/app/pool/useDepositTxn.tsx new file mode 100644 index 0000000..8dc0e3b --- /dev/null +++ b/apps/flame-defi/app/pool/useDepositTxn.tsx @@ -0,0 +1,13 @@ +import { TokenState } from "@repo/ui/types"; + +export function useDepositTxn(inputOne: TokenState, inputTwo: TokenState) { + // TODO: pull in real token values and balances from api and calculate the txn info here + + if (inputOne.value === undefined || inputTwo.value === undefined) { + return "Enter an amount"; + } else if (inputOne.value === 0 || inputTwo.value === 0) { + return "Amount must be greater than 0"; + } else { + return "Deposit"; + } +} diff --git a/apps/flame-defi/app/swap/page.tsx b/apps/flame-defi/app/swap/page.tsx index 913749d..e8fb5d4 100644 --- a/apps/flame-defi/app/swap/page.tsx +++ b/apps/flame-defi/app/swap/page.tsx @@ -7,24 +7,14 @@ import { TokenSelector, SettingsPopover, } from "@repo/ui/components"; -import type { TokenItem } from "./useTokenModal"; -import { useTokenModal } from "./useTokenModal"; import { useState } from "react"; import { useAccount } from "wagmi"; import { useEvmWallet } from "features/EvmWallet"; import { useTxnInfo } from "./useTxnInfo"; -enum TOKEN_INPUTS { - TOKEN_ONE = "token_one", - TOKEN_TWO = "token_two", -} - -export interface TokenState { - token?: TokenItem | null; - value: number | undefined; -} +import { tokens, TOKEN_INPUTS } from "../constants"; +import { TokenState } from "@repo/ui/types"; export default function SwapPage(): React.ReactElement { - const { tokens } = useTokenModal(); const { connectEvmWallet } = useEvmWallet(); const userAccount = useAccount(); const [inputSelected, setInputSelected] = useState(TOKEN_INPUTS.TOKEN_ONE); @@ -59,29 +49,30 @@ export default function SwapPage(): React.ReactElement { return (
    -
    +
    -

    Swap

    +

    Swap

    null} onClick={() => setInputSelected(TOKEN_INPUTS.TOKEN_ONE)} - className={`flex flex-col rounded-md p-2 transition border border-solid border-transparent hover:border-grey-light ${ + className={`flex flex-col rounded-md p-4 transition border border-solid border-transparent hover:border-grey-medium ${ inputSelected === TOKEN_INPUTS.TOKEN_ONE ? "bg-background border-grey-medium" - : "bg-white/[0.04]" + : "bg-semi-white" }`} > +
    Sell
    handleInputChange(Number(e.target.value), true)} - className="w-[45%] sm:max-w-[62%] flex-1 bg-transparent focus:outline-none text-[36px] placeholder:text-grey-light [appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none" + className="normalize-input w-[45%] sm:max-w-[62%]" placeholder="0" /> -
    +
    ({ ...prev, token })) } /> + {inputOne.token ? ( +
    + {inputOne.value ? inputOne.value : "0"} + {inputOne.token?.symbol} + + Max + +
    + ) : ( +
    + )}
    - $100 + $0
    null} onClick={() => setInputSelected(TOKEN_INPUTS.TOKEN_TWO)} - className={`flex flex-col rounded-md p-2 transition border border-solid border-transparent hover:border-grey-light ${ + className={`flex flex-col rounded-md p-4 transition border border-solid border-transparent hover:border-grey-medium ${ inputSelected === TOKEN_INPUTS.TOKEN_TWO ? "bg-background border-grey-medium" - : "bg-white/[0.04]" + : "bg-semi-white" }`} > +
    Buy
    handleInputChange(Number(e.target.value), false)} - className="w-[45%] sm:max-w-[62%] flex-1 bg-transparent focus:outline-none text-[36px] placeholder:text-grey-light [appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none" + className="normalize-input w-[45%] sm:max-w-[62%]" placeholder="0" /> -
    +
    ({ ...prev, token })) } /> +
    - $100 + $0
    {!userAccount.address && ( @@ -147,7 +151,7 @@ export default function SwapPage(): React.ReactElement { )} {/* TODO: This is a temp example of how we might conditionally render the action button */} {userAccount.address && actionText !== "Swap" && ( -
    +
    {actionText}
    )} diff --git a/apps/flame-defi/app/swap/useTokenModal.tsx b/apps/flame-defi/app/swap/useTokenModal.tsx deleted file mode 100644 index 396c3ef..0000000 --- a/apps/flame-defi/app/swap/useTokenModal.tsx +++ /dev/null @@ -1,49 +0,0 @@ -import { - CelestiaIcon, - MilkTiaIcon, - StrideTiaIcon, - UsdcIcon, - WrappedTiaIcon, -} from "@repo/ui/icons"; -import { IconProps } from "@repo/ui/types"; - -export interface TokenItem { - Icon: React.ComponentType; - title: string; - symbol: string; -} - -// NOTE: temporary tokens until we have a real token list from a api -const tokens: TokenItem[] = [ - { - Icon: CelestiaIcon, - title: "TIA", - symbol: "TIA", - }, - { - Icon: WrappedTiaIcon, - title: "Wrapped Celestia", - symbol: "WTIA", - }, - { - Icon: MilkTiaIcon, - title: "Milk TIA", - symbol: "milkTIA", - }, - { - Icon: StrideTiaIcon, - title: "Stride TIA", - symbol: "stTIA", - }, - { - Icon: UsdcIcon, - title: "USDC", - symbol: "USDC", - }, -]; - -export function useTokenModal() { - return { - tokens, - }; -} diff --git a/apps/flame-defi/app/swap/useTxnInfo.tsx b/apps/flame-defi/app/swap/useTxnInfo.tsx index 66f1ef7..d5a5c20 100644 --- a/apps/flame-defi/app/swap/useTxnInfo.tsx +++ b/apps/flame-defi/app/swap/useTxnInfo.tsx @@ -1,4 +1,4 @@ -import { TokenState } from "./page"; +import { TokenState } from "@repo/ui/types"; export function useTxnInfo(inputOne: TokenState, inputTwo: TokenState) { // TODO: pull in real token values and balances from api and calculate the txn info here diff --git a/apps/flame-defi/package.json b/apps/flame-defi/package.json index 03f7712..d01e090 100644 --- a/apps/flame-defi/package.json +++ b/apps/flame-defi/package.json @@ -24,6 +24,8 @@ "@repo/ui": "*", "@tanstack/react-query": "^5.61.0", "chain-registry": "^1.69.45", + "echarts": "^5.6.0", + "echarts-for-react": "^3.0.2", "ethers": "^6.13.2", "next": "^15.1.0", "osmojs": "^16.15.0", diff --git a/apps/flame-defi/tailwind.config.js b/apps/flame-defi/tailwind.config.js index f9f31c8..2e32849 100644 --- a/apps/flame-defi/tailwind.config.js +++ b/apps/flame-defi/tailwind.config.js @@ -15,6 +15,7 @@ export default { colors: { white: 'hsl(var(--color-white))', whitest: 'hsl(var(--color-whitest))', + 'semi-white': 'hsl(var(--color-semi-white))', 'grey-dark': 'hsl(var(--color-grey-dark))', 'grey-medium': 'hsl(var(--color-grey-medium))', 'grey-light': 'hsl(var(--color-grey-light))', diff --git a/package-lock.json b/package-lock.json index 4ffc25f..0ba9682 100644 --- a/package-lock.json +++ b/package-lock.json @@ -33,6 +33,8 @@ "@repo/ui": "*", "@tanstack/react-query": "^5.61.0", "chain-registry": "^1.69.45", + "echarts": "^5.6.0", + "echarts-for-react": "^3.0.2", "ethers": "^6.13.2", "next": "^15.1.0", "osmojs": "^16.15.0", @@ -10093,6 +10095,36 @@ "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==" }, + "node_modules/echarts": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/echarts/-/echarts-5.6.0.tgz", + "integrity": "sha512-oTbVTsXfKuEhxftHqL5xprgLoc0k7uScAwtryCgWF6hPYFLRwOUHiFmHGCBKP5NPFNkDVopOieyUqYGH8Fa3kA==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "2.3.0", + "zrender": "5.6.1" + } + }, + "node_modules/echarts-for-react": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/echarts-for-react/-/echarts-for-react-3.0.2.tgz", + "integrity": "sha512-DRwIiTzx8JfwPOVgGttDytBqdp5VzCSyMRIxubgU/g2n9y3VLUmF2FK7Icmg/sNVkv4+rktmrLN9w22U2yy3fA==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "size-sensor": "^1.0.1" + }, + "peerDependencies": { + "echarts": "^3.0.0 || ^4.0.0 || ^5.0.0", + "react": "^15.0.0 || >=16.0.0" + } + }, + "node_modules/echarts/node_modules/tslib": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.0.tgz", + "integrity": "sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==", + "license": "0BSD" + }, "node_modules/eciesjs": { "version": "0.4.13", "resolved": "https://registry.npmjs.org/eciesjs/-/eciesjs-0.4.13.tgz", @@ -17120,6 +17152,12 @@ "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", "dev": true }, + "node_modules/size-sensor": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/size-sensor/-/size-sensor-1.0.2.tgz", + "integrity": "sha512-2NCmWxY7A9pYKGXNBfteo4hy14gWu47rg5692peVMst6lQLPKrVjhY+UTEsPI5ceFRJSl3gVgMYaUi/hKuaiKw==", + "license": "ISC" + }, "node_modules/slash": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", @@ -19272,6 +19310,21 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/zrender": { + "version": "5.6.1", + "resolved": "https://registry.npmjs.org/zrender/-/zrender-5.6.1.tgz", + "integrity": "sha512-OFXkDJKcrlx5su2XbzJvj/34Q3m6PvyCZkVPHGYpcCJ52ek4U/ymZyfuV1nKE23AyBJ51E/6Yr0mhZ7xGTO4ag==", + "license": "BSD-3-Clause", + "dependencies": { + "tslib": "2.3.0" + } + }, + "node_modules/zrender/node_modules/tslib": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.0.tgz", + "integrity": "sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==", + "license": "0BSD" + }, "node_modules/zustand": { "version": "4.5.6", "resolved": "https://registry.npmjs.org/zustand/-/zustand-4.5.6.tgz", diff --git a/packages/ui/src/components/AnimatedDownArrowSpacer/AnimatedDownArrowSpacer.tsx b/packages/ui/src/components/AnimatedDownArrowSpacer/AnimatedDownArrowSpacer.tsx index d554eab..fc7c421 100644 --- a/packages/ui/src/components/AnimatedDownArrowSpacer/AnimatedDownArrowSpacer.tsx +++ b/packages/ui/src/components/AnimatedDownArrowSpacer/AnimatedDownArrowSpacer.tsx @@ -8,7 +8,7 @@ export const AnimatedArrowSpacer = ({ {[100, 80, 60, 40, 20, 10, 5, 1].map((width, index) => (
    [] = [ - { label: "Option 1", value: "option1" }, - { label: "Option 2", value: "option2" }, - { label: "Option 3", value: "option3" }, -]; +import { Dropdown } from "./Dropdown"; // Adjust the import path as needed describe("Dropdown Component", () => { test("renders with placeholder text", () => { diff --git a/packages/ui/src/components/Dropdown/Dropdown.tsx b/packages/ui/src/components/Dropdown/Dropdown.tsx index 807cce8..71806b1 100644 --- a/packages/ui/src/components/Dropdown/Dropdown.tsx +++ b/packages/ui/src/components/Dropdown/Dropdown.tsx @@ -148,11 +148,11 @@ export const Dropdown = ({ type="button" key={option.label} className={`w-full text-left ${ - selectedOption?.value === option.value ? "bg-white/5" : "" + selectedOption?.value === option.value ? "bg-semi-white" : "" }`} onClick={() => handleSelect(option)} > - + {option.LeftIcon && ( @@ -173,7 +173,7 @@ export const Dropdown = ({ setIsActive(false); }} > - + {option.LeftIcon && ( diff --git a/packages/ui/src/components/SettingsPopover/SettingsPopover.tsx b/packages/ui/src/components/SettingsPopover/SettingsPopover.tsx index 72ccd5b..26fc998 100644 --- a/packages/ui/src/components/SettingsPopover/SettingsPopover.tsx +++ b/packages/ui/src/components/SettingsPopover/SettingsPopover.tsx @@ -18,9 +18,7 @@ import { GearIcon } from "@repo/ui/icons"; import { InfoTooltip } from "../InfoTooltip/InfoTooltip"; import { useState } from "react"; -interface SettingsPopoverProps {} - -export const SettingsPopover: React.FC = () => { +export const SettingsPopover: React.FC = () => { const [customSlippage, setCustomSlippage] = useState(""); const [expertMode, setExpertMode] = useState(false); const [showExpertModeDialog, setShowExpertModeDialog] = useState(false); diff --git a/packages/ui/src/components/SideTag/SideTag.tsx b/packages/ui/src/components/SideTag/SideTag.tsx deleted file mode 100644 index 75f3050..0000000 --- a/packages/ui/src/components/SideTag/SideTag.tsx +++ /dev/null @@ -1,30 +0,0 @@ -import { UpRightSquareIcon } from "../../icons"; - -interface SideTagProps { - label: string; - url: string; -} - -/** - * SideTag component to render a side tag with an icon and label. - * @param label - * @param url - * @param iconClass - */ -export const SideTag = ({ label, url }: SideTagProps) => { - return ( - - ); -}; diff --git a/packages/ui/src/components/ToggleSwitch/ToggleSwitch.tsx b/packages/ui/src/components/ToggleSwitch/ToggleSwitch.tsx new file mode 100644 index 0000000..fafe6f3 --- /dev/null +++ b/packages/ui/src/components/ToggleSwitch/ToggleSwitch.tsx @@ -0,0 +1,42 @@ +interface ToggleSwitchProps { + toggleOptions: string[]; + className?: string; + selectedOption: string; + setSelectedOption: (option: string) => void; +} + +export function ToggleSwitch({ + toggleOptions, + className, + selectedOption, + setSelectedOption, +}: ToggleSwitchProps) { + return ( +
    +
    + {toggleOptions.map((option, i) => ( +
    setSelectedOption(option)} + className={` + flex items-center justify-center + flex-1 py-2 px-4 + cursor-pointer relative + font-medium + ${selectedOption === option ? "text-white" : "text-grey-light"} + `} + > + {option} +
    + ))} +
    + ); +} diff --git a/packages/ui/src/components/TokenSelector/TokenSelector.tsx b/packages/ui/src/components/TokenSelector/TokenSelector.tsx index b812841..d450ec3 100644 --- a/packages/ui/src/components/TokenSelector/TokenSelector.tsx +++ b/packages/ui/src/components/TokenSelector/TokenSelector.tsx @@ -15,27 +15,26 @@ import { CloseIcon, SearchIcon, } from "../../icons"; -import { IconProps } from "../../types"; - -interface TokenItem { - Icon: React.ComponentType; - title: string; - symbol: string; -} +import { TokenItem } from "../../types"; interface TokenSelectorProps { tokens: TokenItem[]; defaultTitle?: string; setSelectedToken: (token: TokenItem) => void; selectedToken?: TokenItem | null; + CustomTokenButton?: (props: { + selectedToken?: TokenItem | null; + defaultTitle: string; + }) => React.ReactElement; } -export const TokenSelector: React.FC = ({ +export const TokenSelector = ({ tokens, defaultTitle = "Select token", selectedToken, setSelectedToken, -}: TokenSelectorProps) => { + CustomTokenButton, +}: TokenSelectorProps): React.ReactElement => { const [open, setOpen] = useState(false); const [filteredTokens, setFilteredTokens] = useState(tokens); const [searchQuery, setSearchQuery] = useState(""); @@ -58,15 +57,24 @@ export const TokenSelector: React.FC = ({ return ( - -
    - {selectedToken?.Icon && } -

    - {selectedToken?.symbol || defaultTitle} -

    - -
    -
    + {CustomTokenButton ? ( + + ) : ( + +
    + {selectedToken?.Icon && } +

    + {selectedToken?.symbol || defaultTitle} +

    + +
    +
    + )}
    @@ -88,7 +96,7 @@ export const TokenSelector: React.FC = ({
    handleSelectToken({ symbol, title, Icon })} key={symbol} - className={`flex items-center justify-between space-x-2 p-2 rounded-md hover:bg-white/[0.04] transition cursor-pointer ${selectedToken?.symbol === symbol ? "bg-white/[0.04]" : ""}`} + className={`flex items-center justify-between space-x-2 p-2 rounded-md hover:bg-semi-white transition cursor-pointer ${selectedToken?.symbol === symbol ? "bg-semi-white" : ""}`} >
    diff --git a/packages/ui/src/components/TxnInfo/TxnInfo.tsx b/packages/ui/src/components/TxnInfo/TxnInfo.tsx index 8e5cd95..27d897a 100644 --- a/packages/ui/src/components/TxnInfo/TxnInfo.tsx +++ b/packages/ui/src/components/TxnInfo/TxnInfo.tsx @@ -7,9 +7,7 @@ import { import { InfoTooltip } from "@repo/ui/components"; import { GasIcon } from "@repo/ui/icons"; -interface TxnInfoProps {} - -export const TxnInfo = ({}: TxnInfoProps) => { +export const TxnInfo = () => { // TODO: get the real calculated data return ( diff --git a/packages/ui/src/components/index.ts b/packages/ui/src/components/index.ts index f353faf..3d07c22 100644 --- a/packages/ui/src/components/index.ts +++ b/packages/ui/src/components/index.ts @@ -3,8 +3,9 @@ export * from "./CopyToClipboardButton/CopyToClipboardButton"; export * from "./Dropdown/Dropdown"; export * from "./AnimatedDownArrowSpacer/AnimatedDownArrowSpacer"; export * from "./Footer/Footer"; -export * from "./SideTag/SideTag"; +export * from "../../../../apps/flame-defi/app/components/SideTag/SideTag"; export * from "./TokenSelector/TokenSelector"; export * from "./SettingsPopover/SettingsPopover"; export * from "./InfoTooltip/InfoTooltip"; export * from "./TxnInfo/TxnInfo"; +export * from "./ToggleSwitch/ToggleSwitch"; diff --git a/packages/ui/src/icons/MinusIcon.tsx b/packages/ui/src/icons/MinusIcon.tsx new file mode 100644 index 0000000..3711932 --- /dev/null +++ b/packages/ui/src/icons/MinusIcon.tsx @@ -0,0 +1,22 @@ +import type { IconProps } from "../types"; + +export const MinusIcon: React.FC = ({ + className = "", + size = 24, +}: IconProps) => { + return ( + + ); +}; diff --git a/packages/ui/src/icons/ResetIcon.tsx b/packages/ui/src/icons/ResetIcon.tsx new file mode 100644 index 0000000..7d7dfcf --- /dev/null +++ b/packages/ui/src/icons/ResetIcon.tsx @@ -0,0 +1,26 @@ +import type { IconProps } from "../types"; + +export const ResetIcon: React.FC = ({ + className = "", + size = 24, +}: IconProps) => { + return ( + + ); +}; diff --git a/packages/ui/src/icons/index.ts b/packages/ui/src/icons/index.ts index 51509c4..da107fe 100644 --- a/packages/ui/src/icons/index.ts +++ b/packages/ui/src/icons/index.ts @@ -27,3 +27,5 @@ export * from "./ClipboardIcon"; export * from "./PowerIcon"; export * from "./SearchIcon"; export * from "./CheckMarkIcon"; +export * from "./MinusIcon"; +export * from "./ResetIcon"; diff --git a/packages/ui/src/shadcn-primitives/breadcrumbs.tsx b/packages/ui/src/shadcn-primitives/breadcrumbs.tsx new file mode 100644 index 0000000..9571b30 --- /dev/null +++ b/packages/ui/src/shadcn-primitives/breadcrumbs.tsx @@ -0,0 +1,114 @@ +import * as React from "react"; +import { Slot } from "@radix-ui/react-slot"; +import { ChevronRight, MoreHorizontal } from "lucide-react"; +import { cn } from "../lib/utils"; + +const Breadcrumb = React.forwardRef< + HTMLElement, + React.ComponentPropsWithoutRef<"nav"> & { + separator?: React.ReactNode; + } +>(({ ...props }, ref) =>