diff --git a/README.md b/README.md index b860294b..5458460e 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@

Monarch

-
Easy access to Morpho Blue.
+
Customized lending on Morpho Blue.
LICENSE @@ -16,7 +16,7 @@ ## Overview -Monarch is an unofficial user interface designed to facilitate access to [Morpho Blue](https://github.com/morpho-org/morpho-blue) markets. It provides a streamlined way to supply to any markets created on the Morpho Blue protocol, without the need for MetaMorpho vaults. +Monarch is an unofficial user interface designed for composing custom lending strategies on [Morpho Blue](https://github.com/morpho-org/morpho-blue). It enables you to compose your own lending strategies by bundling multiple markets, each with your defined risk parameters. Access Morpho Blue directly without intermediaries, maintaining full control over your lending positions. ## Local Setup diff --git a/app/global.css b/app/global.css index 21ee7548..08d067ba 100644 --- a/app/global.css +++ b/app/global.css @@ -56,6 +56,8 @@ html { height: 100%; scroll-behavior: smooth; + /* Reserve space for scrollbar to prevent layout shifts */ + scrollbar-gutter: stable; } body { @@ -66,6 +68,8 @@ body { color: var(--color-text); font-family: Inter, sans-serif; overflow-x: hidden; + /* Add padding to account for scrollbar width in modern browsers */ + padding-right: calc(100vw - 100%); -webkit-text-size-adjust: 100%; text-size-adjust: 100%; } @@ -120,6 +124,10 @@ h1 { text-align: center; } +.table-body tr:not(.no-hover-effect tr, .no-hover-effect tr) { + border-left: 2px solid transparent; +} + .table-body tr:not(.no-hover-effect tr, .no-hover-effect tr):hover { background-color: var(--palette-bg-hovered); border-left: 2px solid var(--palette-orange); @@ -127,7 +135,7 @@ h1 { .table-body-focused { background-color: var(--palette-bg-hovered); - border-left: 2px solid var(--palette-orange); + border-left: 2px solid var(--palette-orange) !important; } svg { diff --git a/app/home/HomePage.tsx b/app/home/HomePage.tsx index a5a30364..b5644165 100644 --- a/app/home/HomePage.tsx +++ b/app/home/HomePage.tsx @@ -3,53 +3,117 @@ import React, { useState, useEffect } from 'react'; import { useAccount } from 'wagmi'; import PrimaryButton from '@/components/common/PrimaryButton'; -import Footer from '@/components/layout/footer/Footer'; -import HomeHeader from './_components/HomeHeader'; +import Header from '@/components/layout/header/Header'; export default function HomePage() { - const [isMorphoBlue, setIsMorphoBlue] = useState(false); + const [showCustomized, setShowCustomized] = useState(true); + const [riskYieldIndex, setRiskYieldIndex] = useState(0); + const [secondCounter, setSecondCounter] = useState(0); + + const riskYieldTerms = ['risk', 'yield']; + const secondPhrases = ['on Morpho Blue', 'with no intermediates']; useEffect(() => { - const interval = setInterval(() => { - setIsMorphoBlue((prev) => !prev); + // Toggle between customized and manage your own every 5 seconds + const customizedInterval = setInterval(() => { + setShowCustomized((prev) => !prev); + }, 5000); + + // Change risk/yield every 3 seconds when showing manage your own + const riskYieldInterval = setInterval(() => { + if (!showCustomized) { + setRiskYieldIndex((prev) => (prev + 1) % riskYieldTerms.length); + } }, 3000); - return () => clearInterval(interval); - }, []); + + // Second segment changes every 4 seconds + const secondInterval = setInterval(() => { + setSecondCounter((prev) => (prev + 1) % secondPhrases.length); + }, 4000); + + return () => { + clearInterval(customizedInterval); + clearInterval(riskYieldInterval); + clearInterval(secondInterval); + }; + }, [showCustomized]); + + const renderFirstPhrase = () => { + if (showCustomized) { + return ( + + Customized lending + + ); + } + return ( + + + Manage your own + + {riskYieldTerms.map((term, index) => ( + + {term} + + ))} + + + + ); + }; const { address } = useAccount(); return (
-
- -
-
-
- {' '} - {/* Fixed height container */} -

- Direct access to{' '} - - {isMorphoBlue ? '{Morpho Blue}' : 'the most decentralized lending protocol.'} - -

-
-
-
-
- - Why Monarch - - - View Portfolio - -
-
-
+
+
+
+
+

+
{renderFirstPhrase()}
+
+ {secondPhrases.map((phrase, index) => ( + + {phrase.includes('Morpho Blue') ? ( + + on Morpho Blue + + ) : ( + phrase + )} + + ))} +
+

+
+
+ + Why Monarch + + + Get Started + +
+
+
); } diff --git a/app/home/_components/Home.module.css b/app/home/_components/Home.module.css deleted file mode 100644 index c010849c..00000000 --- a/app/home/_components/Home.module.css +++ /dev/null @@ -1,102 +0,0 @@ -.HomeHeader { - --home-header-height: 400px; - - height: var(--home-header-height); -} - -.HomeHeaderGradient { - position: absolute; - z-index: -1; - top: 0; - left: 0; - width: 100vw; - height: var(--home-header-height); - background: linear-gradient(180deg, #ff5800 0%, #cb59ab 100%, #ea36b8 100%); -} - -.HomeHeaderHeadline { - margin-top: 45px; - color: white; - font-size: 92px; - font-weight: 400; - line-height: 85px; - text-align: center; - word-wrap: break-word; - - @media (width <= 768px) { - font-size: 64px; - } -} - -.HomeHeaderParagraph { - margin: 35px 0; - color: white; - font-size: 20px; - font-weight: 400; - line-height: 24px; - text-align: center; - word-wrap: break-word; - - @media (width <= 768px) { - padding: 0 20px; - font-size: 18px; - } -} - -.HomeHeaderCta { - display: flex; - justify-content: center; - margin: 16px 0 52px; - - @media (width <= 768px) { - width: 80%; - } -} - -.HomeHeaderWaves { - --home-header-waves-height: 25px; - - position: relative; - margin-top: 4px; - margin-bottom: var(--home-header-waves-height); -} - -.HomeHeaderWaves svg { - width: 100vw; - height: var(--home-header-waves-height); -} - -.HomeHeaderWavesParallax > use { - animation: move-forever 30s cubic-bezier(0.55, 0.5, 0.45, 0.5) infinite; -} - -.HomeHeaderWavesParallax > use:nth-child(1), -.HomeHeaderWavesParallax > use:nth-child(2) { - animation-delay: -2s; - animation-duration: 6s; -} - -.HomeHeaderWavesParallax > use:nth-child(3) { - animation-delay: -3s; - animation-duration: 12s; -} - -.HomeHeaderWavesParallax > use:nth-child(4) { - animation-delay: -4s; - animation-duration: 15s; -} - -.HomeHeaderWavesParallax > use:nth-child(5) { - animation-delay: -5s; - animation-duration: 20s; -} - -@keyframes move-forever { - 0% { - transform: translate3d(-90px, 0, 0); - } - - 100% { - transform: translate3d(85px, 0, 0); - } -} diff --git a/app/home/_components/HomeHeader.tsx b/app/home/_components/HomeHeader.tsx deleted file mode 100644 index 42597294..00000000 --- a/app/home/_components/HomeHeader.tsx +++ /dev/null @@ -1,10 +0,0 @@ -import Header from '@/components/layout/header/Header'; -import styles from './Home.module.css'; - -export default function HomeHeader() { - return ( -
-
-
- ); -} diff --git a/app/home/_components/WhyUseIt.tsx b/app/home/_components/WhyUseIt.tsx deleted file mode 100644 index 15dbfc6a..00000000 --- a/app/home/_components/WhyUseIt.tsx +++ /dev/null @@ -1,3 +0,0 @@ -export default function HomeMain() { - return
; -} diff --git a/app/markets/components/AssetFilter.tsx b/app/markets/components/AssetFilter.tsx index 71a9c275..290057e2 100644 --- a/app/markets/components/AssetFilter.tsx +++ b/app/markets/components/AssetFilter.tsx @@ -1,6 +1,7 @@ 'use client'; import { useState, useRef, useEffect, KeyboardEvent } from 'react'; import { ChevronDownIcon, TrashIcon } from '@radix-ui/react-icons'; +import { motion, AnimatePresence } from 'framer-motion'; import Image from 'next/image'; import { ERC20Token, infoToKey } from '@/utils/tokens'; @@ -115,57 +116,67 @@ export default function AssetFilter({
- {isOpen && !loading && ( -
- setQuery(e.target.value)} - placeholder="Search tokens..." - className="w-full border-none bg-transparent p-3 text-sm focus:outline-none" - /> -
- -
- + Clear All + + +
-
- - )} + + )} + ); } diff --git a/app/markets/components/MarketTableBody.tsx b/app/markets/components/MarketTableBody.tsx index 47c83de1..a8def89f 100644 --- a/app/markets/components/MarketTableBody.tsx +++ b/app/markets/components/MarketTableBody.tsx @@ -1,5 +1,6 @@ import React from 'react'; import { Tooltip } from '@nextui-org/tooltip'; +import { motion, AnimatePresence } from 'framer-motion'; import Image from 'next/image'; import { FaShieldAlt } from 'react-icons/fa'; import { GoStarFill, GoStar } from 'react-icons/go'; @@ -169,13 +170,26 @@ export function MarketTableBody({ - {expandedRowId === item.uniqueKey && ( - - - - - - )} + + {expandedRowId === item.uniqueKey && ( + + + +
+ +
+
+ + + )} +
); })} diff --git a/app/page.tsx b/app/page.tsx index 8a16c956..8c985418 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -3,7 +3,7 @@ import HomePage from './home/HomePage'; export const metadata = generateMetadata({ title: 'Monarch', - description: 'Permission-less access to morpho blue protocol', + description: 'Customized lending with Morpho Blue', images: 'themes.png', pathname: '', }); diff --git a/app/positions/components/FromAndToMarkets.tsx b/app/positions/components/FromAndToMarkets.tsx index 42982c15..a54a8f37 100644 --- a/app/positions/components/FromAndToMarkets.tsx +++ b/app/positions/components/FromAndToMarkets.tsx @@ -180,7 +180,8 @@ export function FromAndToMarkets({ variant="flat" className="h-5 min-w-0 px-2 text-xs" isDisabled={ - BigInt(marketPosition.supplyAssets) + BigInt(marketPosition.pendingDelta) <= + BigInt(marketPosition.supplyAssets) + + BigInt(marketPosition.pendingDelta) <= 0n } onClick={(e) => { @@ -192,7 +193,7 @@ export function FromAndToMarkets({ if (remainingAmount > 0n) { onSelectMax?.( marketPosition.market.uniqueKey, - Number(remainingAmount) + Number(remainingAmount), ); } }} diff --git a/app/positions/components/PositionsContent.tsx b/app/positions/components/PositionsContent.tsx index b89e3fce..d587003b 100644 --- a/app/positions/components/PositionsContent.tsx +++ b/app/positions/components/PositionsContent.tsx @@ -28,8 +28,8 @@ export default function Positions() {
-
-

Portfolio

+
+

Portfolio

diff --git a/app/positions/components/RebalanceModal.tsx b/app/positions/components/RebalanceModal.tsx index c9b37795..808e3ce1 100644 --- a/app/positions/components/RebalanceModal.tsx +++ b/app/positions/components/RebalanceModal.tsx @@ -357,7 +357,7 @@ export function RebalanceModal({ onPress={() => void handleExecuteRebalance()} isDisabled={isConfirming || rebalanceActions.length === 0} isLoading={isConfirming} - className="rounded-sm bg-orange-500 p-4 px-10 font-zen text-white opacity-80 transition-all duration-200 ease-in-out hover:scale-105 hover:opacity-100 disabled:opacity-50 dark:bg-orange-600" + className="rounded-sm bg-primary p-4 px-10 font-zen text-white opacity-80 transition-all duration-200 ease-in-out hover:scale-105 hover:opacity-100 disabled:opacity-50 dark:bg-primary" > {needSwitchChain ? 'Switch Network & Execute' : 'Execute Rebalance'} diff --git a/app/positions/components/SuppliedMarketsDetail.tsx b/app/positions/components/SuppliedMarketsDetail.tsx index d9fc917c..580a6271 100644 --- a/app/positions/components/SuppliedMarketsDetail.tsx +++ b/app/positions/components/SuppliedMarketsDetail.tsx @@ -1,5 +1,6 @@ import React from 'react'; import { Tooltip } from '@nextui-org/tooltip'; +import { motion } from 'framer-motion'; import Link from 'next/link'; import { IoWarningOutline } from 'react-icons/io5'; import OracleVendorBadge from '@/components/OracleVendorBadge'; @@ -57,164 +58,174 @@ export function SuppliedMarketsDetail({ }; return ( -
-
-
-

Collateral Exposure

-
- {groupedPosition.processedCollaterals.map((collateral, colIndex) => ( -
- ))} -
-
- {groupedPosition.processedCollaterals.map((collateral, colIndex) => ( - - +
+
+
+

Collateral Exposure

+
+ {groupedPosition.processedCollaterals.map((collateral, colIndex) => ( +
- ■ - {' '} - {collateral.symbol}: {formatReadable(collateral.percentage)}% - - ))} + title={`${collateral.symbol}: ${collateral.percentage.toFixed(2)}%`} + /> + ))} +
+
+ {groupedPosition.processedCollaterals.map((collateral, colIndex) => ( + + + ■ + {' '} + {collateral.symbol}: {formatReadable(collateral.percentage)}% + + ))} +
-
- - - - - - - - - - - - - - - {sortedMarkets.map((position) => { - const suppliedAmount = Number( - formatBalance(position.supplyAssets, position.market.loanAsset.decimals), - ); - const percentageOfPortfolio = - totalSupply > 0 ? (suppliedAmount / totalSupply) * 100 : 0; - const warningColor = getWarningColor(position.market.warningsWithDetail); +
MarketCollateralOracleLLTVAPYSupplied% of PortfolioActions
+ + + + + + + + + + + + + + {sortedMarkets.map((position) => { + const suppliedAmount = Number( + formatBalance(position.supplyAssets, position.market.loanAsset.decimals), + ); + const percentageOfPortfolio = + totalSupply > 0 ? (suppliedAmount / totalSupply) * 100 : 0; + const warningColor = getWarningColor(position.market.warningsWithDetail); - return ( - - + - + + - - - - - + + + + - - - ); - })} - -
MarketCollateralOracleLLTVAPYSupplied% of PortfolioActions
-
-
- {position.market.warningsWithDetail.length > 0 ? ( - } - placement="top" - > -
- -
-
- ) : ( -
- )} + return ( +
+
+
+ {position.market.warningsWithDetail.length > 0 ? ( + + } + placement="top" + > +
+ +
+
+ ) : ( +
+ )} +
+ {/* */} + + {position.market.uniqueKey.slice(2, 8)} + + {/* */}
- {/* */} - - {position.market.uniqueKey.slice(2, 8)} - - {/* */} -
-
- {position.market.collateralAsset ? ( -
- - {position.market.collateralAsset.symbol} +
+ {position.market.collateralAsset ? ( +
+ + {position.market.collateralAsset.symbol} +
+ ) : ( + 'N/A' + )} +
+
+
- ) : ( - 'N/A' - )} -
-
- -
-
- {formatBalance(position.market.lltv, 16)}% - - {formatReadable(position.market.dailyApys.netSupplyApy * 100)}% - - {formatReadable(suppliedAmount)} {position.market.loanAsset.symbol} - -
-
-
+
+ {formatBalance(position.market.lltv, 16)}% + + {formatReadable(position.market.dailyApys.netSupplyApy * 100)}% + + {formatReadable(suppliedAmount)} {position.market.loanAsset.symbol} + +
+
+
+
+ + {formatReadable(percentageOfPortfolio)}% +
- - {formatReadable(percentageOfPortfolio)}% - -
-
- - -
-
+ + + + + + + ); + })} + + +
+ ); } diff --git a/app/settings/faq/page.tsx b/app/settings/faq/page.tsx new file mode 100644 index 00000000..bedae538 --- /dev/null +++ b/app/settings/faq/page.tsx @@ -0,0 +1,56 @@ +'use client'; + +import Link from 'next/link'; + +function FAQPage() { + const faqs = [ + { + question: 'Where can I find the source code?', + answer: ( + + The source code is available on{' '} + + GitHub + + . + + ), + }, + { + question: 'Where can I get help?', + answer: ( + + Join our{' '} + + Telegram chat + {' '} + for support and discussions. + + ), + }, + ]; + + return ( +
+

Frequently Asked Questions

+
+ {faqs.map((faq, index) => ( +
+

{faq.question}

+

{faq.answer}

+
+ ))} +
+
+ ); +} + +export default FAQPage; diff --git a/src/components/common/PrimaryButton.tsx b/src/components/common/PrimaryButton.tsx index eda456cd..0645c88e 100644 --- a/src/components/common/PrimaryButton.tsx +++ b/src/components/common/PrimaryButton.tsx @@ -19,7 +19,7 @@ export default function PrimaryButton({ type="button" className={`${ isSecondary ? 'bg-surface' : 'bg-monarch-orange' - } rounded-sm p-4 px-10 font-zen opacity-80 transition-all duration-200 ease-in-out hover:opacity-100 ${className} hover:scale-105`} + } rounded-sm p-4 px-10 font-zen opacity-90 transition-all duration-200 ease-in-out hover:opacity-100 ${className} hover:scale-105`} > {children} diff --git a/src/components/layout/header/AccountConnect.tsx b/src/components/layout/header/AccountConnect.tsx index f4cdce15..d6ac5d93 100644 --- a/src/components/layout/header/AccountConnect.tsx +++ b/src/components/layout/header/AccountConnect.tsx @@ -1,7 +1,6 @@ import { ConnectButton } from '@rainbow-me/rainbowkit'; import '@rainbow-me/rainbowkit/styles.css'; import { AccountDropdown } from './AccountDropdown'; -import { AccountInfoPanel } from './AccountInfoPanel'; /** * AccountConnect @@ -46,14 +45,9 @@ function AccountConnect() { } return ( - <> -
- -
-
- -
- +
+ +
); })()}
diff --git a/src/components/layout/header/AccountDropdown.tsx b/src/components/layout/header/AccountDropdown.tsx index 8d955693..a03f952f 100644 --- a/src/components/layout/header/AccountDropdown.tsx +++ b/src/components/layout/header/AccountDropdown.tsx @@ -1,27 +1,42 @@ +import { useCallback, useState } from 'react'; +import { Name } from '@coinbase/onchainkit/identity'; import * as DropdownMenu from '@radix-ui/react-dropdown-menu'; +import { ExitIcon, ExternalLinkIcon } from '@radix-ui/react-icons'; import { clsx } from 'clsx'; -import { useAccount } from 'wagmi'; +import Link from 'next/link'; +import { usePathname } from 'next/navigation'; +import { FiSettings } from 'react-icons/fi'; +import { useAccount, useDisconnect } from 'wagmi'; import { Avatar } from '@/components/Avatar/Avatar'; -import { AccountInfoPanel } from './AccountInfoPanel'; +import { getSlicedAddress } from '@/utils/address'; +import { getExplorerURL } from '@/utils/external'; const DropdownMenuContentStyle = { marginTop: '-22px', }; export function AccountDropdown() { - const { address } = useAccount(); + const { address, chainId } = useAccount(); + const { disconnect } = useDisconnect(); + const pathname = usePathname(); + const [isOpen, setIsOpen] = useState(false); + + const handleDisconnectWallet = useCallback(() => { + disconnect(); + }, [disconnect]); + + if (!address) return null; return ( - + -
- {address && ( - - )} +
+
+ - +
+ +
+
+ +
+ + {getSlicedAddress(address)} + +
+ + + +
+ +
+ + + Settings + + + +
diff --git a/src/components/layout/header/AccountInfoPanel.tsx b/src/components/layout/header/AccountInfoPanel.tsx deleted file mode 100644 index f25f94d3..00000000 --- a/src/components/layout/header/AccountInfoPanel.tsx +++ /dev/null @@ -1,62 +0,0 @@ -import { useCallback } from 'react'; -import { Name } from '@coinbase/onchainkit/identity'; -import { ExitIcon, ExternalLinkIcon } from '@radix-ui/react-icons'; -import { clsx } from 'clsx'; -import Link from 'next/link'; -import { usePathname } from 'next/navigation'; -import { FiSettings } from 'react-icons/fi'; -import { useAccount, useDisconnect } from 'wagmi'; -import { Avatar } from '@/components/Avatar/Avatar'; -import { getSlicedAddress } from '@/utils/address'; -import { getExplorerURL } from '@/utils/external'; - -export function AccountInfoPanel() { - const { address, chainId } = useAccount(); - const { disconnect } = useDisconnect(); - const pathname = usePathname(); - - const handleDisconnectWallet = useCallback(() => { - disconnect(); - }, [disconnect]); - - if (!address) return null; - - return ( - <> -
- -
-
- -
- - {getSlicedAddress(address)} - -
- - - -
-
- - Settings - - - - - ); -} diff --git a/src/components/layout/header/Navbar.tsx b/src/components/layout/header/Navbar.tsx index c8e8983e..efb7c7c4 100644 --- a/src/components/layout/header/Navbar.tsx +++ b/src/components/layout/header/Navbar.tsx @@ -1,13 +1,11 @@ 'use client'; -import { useState } from 'react'; import { clsx } from 'clsx'; import Image from 'next/image'; import NextLink from 'next/link'; import { usePathname } from 'next/navigation'; import { useTheme } from 'next-themes'; import { FaRegMoon, FaSun } from 'react-icons/fa'; -import { RiMenu3Line, RiCloseLine } from 'react-icons/ri'; import { useAccount } from 'wagmi'; import logo from '../../imgs/logo.png'; import AccountConnect from './AccountConnect'; @@ -26,15 +24,16 @@ export function NavbarLink({ matchKey?: string; }) { const pathname = usePathname(); - const isActive = pathname.includes(matchKey ?? href); + const isActive = matchKey === '/' ? pathname === matchKey : pathname.includes(matchKey ?? href); return ( + ); } - export default Navbar; diff --git a/src/components/layout/header/NavbarMobile.tsx b/src/components/layout/header/NavbarMobile.tsx index 98815428..caad3d16 100644 --- a/src/components/layout/header/NavbarMobile.tsx +++ b/src/components/layout/header/NavbarMobile.tsx @@ -14,13 +14,13 @@ export default function NavbarMobile() { const navbarClass = [ 'flex flex-1 flex-grow items-center justify-between', - 'rounded-sm bg-main p-4 backdrop-blur-2xl', + 'rounded bg-surface p-4 backdrop-blur-2xl', 'mx-4', ].join(' '); if (isMobileMenuOpen) { return ( -