diff --git a/.eslintrc.js b/.eslintrc.js index b2920d3a..ab3e12ca 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -70,7 +70,7 @@ module.exports = { // Forbid types '@typescript-eslint/ban-types': 'error', - '@typescript-eslint/no-explicit-any': ['error', { fixToUnknown: true }], + '@typescript-eslint/no-explicit-any': ['warn', { fixToUnknown: false }], '@typescript-eslint/no-invalid-void-type': 'error', '@typescript-eslint/no-unsafe-argument': 'error', '@typescript-eslint/no-unsafe-assignment': 'error', diff --git a/app/fonts.ts b/app/fonts.ts index a83b327e..e79aef5f 100644 --- a/app/fonts.ts +++ b/app/fonts.ts @@ -35,7 +35,7 @@ export const inter = localFont({ }); export const monospace = localFont({ - src: '../src/fonts/KodeMono/KodeMono-VariableFont_wght.ttf', + src: '../src/fonts/JetBrains_Mono/JetBrainsMono-VariableFont_wght.ttf', display: 'swap', variable: '--font-monospace', }); diff --git a/app/global.css b/app/global.css index dd1b3bfa..21ee7548 100644 --- a/app/global.css +++ b/app/global.css @@ -70,11 +70,11 @@ body { text-size-adjust: 100%; } -.bg-secondary { +.bg-surface { background-color: var(--color-background-secondary); } -.bg-primary { +.bg-main { background-color: var(--color-background); } diff --git a/app/history/components/HistoryTable.tsx b/app/history/components/HistoryTable.tsx index e89c3cc2..c2ca125e 100644 --- a/app/history/components/HistoryTable.tsx +++ b/app/history/components/HistoryTable.tsx @@ -1,13 +1,13 @@ import React from 'react'; import { useMemo, useState } from 'react'; -import { Pagination } from '@nextui-org/react'; +import { Link, Pagination } from '@nextui-org/react'; import { Table, TableHeader, TableBody, TableColumn, TableRow, TableCell } from '@nextui-org/table'; import { ExternalLinkIcon } from '@radix-ui/react-icons'; import moment from 'moment'; import Image from 'next/image'; import { formatReadable, formatBalance } from '@/utils/balance'; -import { getExplorerTxURL, getMarketURL } from '@/utils/external'; +import { getExplorerTxURL } from '@/utils/external'; import { actionTypeToText } from '@/utils/morpho'; import { getNetworkImg } from '@/utils/networks'; import { findToken } from '@/utils/tokens'; @@ -33,8 +33,8 @@ export function HistoryTable({ history }: HistoryTableProps) { return ( @@ -86,19 +86,14 @@ export function HistoryTable({ history }: HistoryTableProps) { {/* id */}
-

{tx.data.market.uniqueKey.slice(2, 8)}

-
+
diff --git a/app/home/HomePage.tsx b/app/home/HomePage.tsx index eb64cc99..a5a30364 100644 --- a/app/home/HomePage.tsx +++ b/app/home/HomePage.tsx @@ -19,7 +19,7 @@ export default function HomePage() { const { address } = useAccount(); return ( -
+
diff --git a/app/info/components/info.tsx b/app/info/components/info.tsx index a5a0da35..91829267 100644 --- a/app/info/components/info.tsx +++ b/app/info/components/info.tsx @@ -98,7 +98,7 @@ function InfoPage() { } return ( -
+
@@ -108,7 +108,7 @@ function InfoPage() { > {sections.map((section, index) => (
-
+

{section.mainTitle}

diff --git a/app/info/components/risk.tsx b/app/info/components/risk.tsx index a03d07f4..f0a2a1d0 100644 --- a/app/info/components/risk.tsx +++ b/app/info/components/risk.tsx @@ -60,7 +60,7 @@ function RiskPage() {

This page covers advanced topics. For a comprehensive overview of Monarch, please visit our{' '} - + introduction page . @@ -69,18 +69,18 @@ function RiskPage() {

When choosing direct lending over vaults, you gain more control but also take on more responsibilities. The following aspects become{' '} - your direct responsibility: + your direct responsibility:

{riskSections.map((section) => (

{section.mainTitle}

{section.subTitle}

-
{section.content}
+
{section.content}
))} -

+

While these responsibilities require more active management, they also offer opportunities for advanced users to optimize their lending strategies. Always conduct your own research and ensure you're prepared to handle these responsibilities before engaging in direct diff --git a/app/info/components/sectionData.tsx b/app/info/components/sectionData.tsx index b54272c6..d980a43e 100644 --- a/app/info/components/sectionData.tsx +++ b/app/info/components/sectionData.tsx @@ -6,7 +6,7 @@ import vaultsImage from '../../../src/imgs/intro/vaults.png'; function Card({ title, items }: { title: string; items: string[] }) { return ( -

+

{title}

    {items.map((item, index) => ( diff --git a/app/market/[chainId]/[marketid]/RateChart.tsx b/app/market/[chainId]/[marketid]/RateChart.tsx new file mode 100644 index 00000000..2a46d7c3 --- /dev/null +++ b/app/market/[chainId]/[marketid]/RateChart.tsx @@ -0,0 +1,328 @@ +/* eslint-disable react/no-unstable-nested-components */ + +import React, { useCallback, useState } from 'react'; +import { Card, CardHeader, CardBody } from '@nextui-org/card'; +import { Progress } from '@nextui-org/progress'; +import { Spinner } from '@nextui-org/spinner'; +import { + AreaChart, + Area, + XAxis, + YAxis, + CartesianGrid, + Tooltip, + Legend, + ResponsiveContainer, +} from 'recharts'; +import ButtonGroup from '@/components/ButtonGroup'; +import { CHART_COLORS } from '@/constants/chartColors'; +import { + TimeseriesDataPoint, + MarketHistoricalData, + Market, + TimeseriesOptions, +} from '@/utils/types'; + +type RateChartProps = { + historicalData: MarketHistoricalData['rates'] | undefined; + market: Market; + isLoading: boolean; + apyTimeframe: '1day' | '7day' | '30day'; + setApyTimeframe: (timeframe: '1day' | '7day' | '30day') => void; + setTimeRangeAndRefetch: (days: number, type: 'rate') => void; + rateTimeRange: TimeseriesOptions; +}; + +function RateChart({ + historicalData, + market, + isLoading, + apyTimeframe, + setApyTimeframe, + setTimeRangeAndRefetch, + rateTimeRange, +}: RateChartProps) { + const [visibleLines, setVisibleLines] = useState({ + supplyApy: true, + borrowApy: true, + rateAtUTarget: true, + }); + + const getChartData = () => { + if (!historicalData) return []; + const { supplyApy, borrowApy, rateAtUTarget } = historicalData; + + return supplyApy.map((point: TimeseriesDataPoint, index: number) => ({ + x: point.x, + supplyApy: point.y, + borrowApy: borrowApy[index]?.y || 0, + rateAtUTarget: rateAtUTarget[index]?.y || 0, + })); + }; + + const formatPercentage = (value: number) => `${(value * 100).toFixed(2)}%`; + + const getCurrentApyValue = (type: 'supply' | 'borrow') => { + return type === 'supply' ? market.state.supplyApy : market.state.borrowApy; + }; + + const getAverageApyValue = (type: 'supply' | 'borrow') => { + if (!historicalData) return 0; + const data = type === 'supply' ? historicalData.supplyApy : historicalData.borrowApy; + return data.length > 0 ? data.reduce((sum, point) => sum + point.y, 0) / data.length : 0; + }; + + const getCurrentRateAtUTargetValue = () => { + return market.state.rateAtUTarget; + }; + + const getAverageRateAtUTargetValue = () => { + if (!historicalData?.rateAtUTarget) return 0; + return ( + historicalData.rateAtUTarget.reduce((sum, point) => sum + point.y, 0) / + historicalData.rateAtUTarget.length + ); + }; + + const getCurrentUtilizationRate = () => { + return market.state.utilization; + }; + + const getAverageUtilizationRate = () => { + if (!historicalData?.utilization) return 0; + return ( + historicalData.utilization.reduce((sum, point) => sum + point.y, 0) / + historicalData.utilization.length + ); + }; + + const formatTime = (unixTime: number) => { + const date = new Date(unixTime * 1000); + if (rateTimeRange.endTimestamp - rateTimeRange.startTimestamp <= 86400) { + return date.toLocaleTimeString(undefined, { hour: '2-digit', minute: '2-digit' }); + } + return date.toLocaleDateString(undefined, { month: 'short', day: 'numeric' }); + }; + + const timeframeOptions = [ + { key: '1day', label: '1D', value: '1day' }, + { key: '7day', label: '7D', value: '7day' }, + { key: '30day', label: '30D', value: '30day' }, + ]; + + const handleTimeframeChange = useCallback( + (value: string) => { + setApyTimeframe(value as '1day' | '7day' | '30day'); + const days = value === '1day' ? 1 : value === '7day' ? 7 : 30; + setTimeRangeAndRefetch(days, 'rate'); + }, + [setApyTimeframe, setTimeRangeAndRefetch], + ); + + return ( + + + Rates + + + +
    +
    + {isLoading ? ( +
    + +
    + ) : ( + + + + + + + + + + + + + + + + + + + `${(value * 100).toFixed(2)}%`} /> + new Date(unixTime * 1000).toLocaleString()} + formatter={(value: number) => `${(value * 100).toFixed(2)}%`} + /> + { + const dataKey = e.dataKey as keyof typeof visibleLines; + setVisibleLines((prev) => ({ + ...prev, + [dataKey]: !prev[dataKey], + })); + }} + formatter={(value, entry) => ( + + {value} + + )} + /> + + + + + + )} +
    +
    +
    +
    +

    Current Rates

    +
    + +
    +
    + Supply APY: + + {formatPercentage(getCurrentApyValue('supply'))} + +
    +
    + Borrow APY: + + {formatPercentage(getCurrentApyValue('borrow'))} + +
    +
    + Rate at U Target: + + {formatPercentage(getCurrentRateAtUTargetValue())} + +
    +
    + +
    +

    + Historical Averages{' '} + ({apyTimeframe}) +

    + {isLoading ? ( +
    + +
    + ) : ( + <> +
    + Utilization Rate: + + {formatPercentage(getAverageUtilizationRate())} + +
    +
    + Supply APY: + + {formatPercentage(getAverageApyValue('supply'))} + +
    +
    + Borrow APY: + + {formatPercentage(getAverageApyValue('borrow'))} + +
    +
    + Rate at U Target: + + {formatPercentage(getAverageRateAtUTargetValue())} + +
    + + )} +
    +
    +
    +
    +
    +
    + ); +} + +export default RateChart; diff --git a/app/market/[chainId]/[marketid]/VolumeChart.tsx b/app/market/[chainId]/[marketid]/VolumeChart.tsx new file mode 100644 index 00000000..b09f3cb7 --- /dev/null +++ b/app/market/[chainId]/[marketid]/VolumeChart.tsx @@ -0,0 +1,372 @@ +/* eslint-disable react/no-unstable-nested-components */ + +import React, { useCallback, useState } from 'react'; +import { Card, CardHeader, CardBody } from '@nextui-org/card'; +import { Spinner } from '@nextui-org/spinner'; +import { + AreaChart, + Area, + XAxis, + YAxis, + CartesianGrid, + Tooltip, + Legend, + ResponsiveContainer, +} from 'recharts'; +import { formatUnits } from 'viem'; +import ButtonGroup from '@/components/ButtonGroup'; +import { CHART_COLORS } from '@/constants/chartColors'; +import { formatReadable } from '@/utils/balance'; +import { + TimeseriesDataPoint, + MarketHistoricalData, + Market, + TimeseriesOptions, +} from '@/utils/types'; + +type VolumeChartProps = { + historicalData: MarketHistoricalData['volumes'] | undefined; + market: Market; + isLoading: boolean; + volumeView: 'USD' | 'Asset'; + volumeTimeframe: '1day' | '7day' | '30day'; + setVolumeTimeframe: (timeframe: '1day' | '7day' | '30day') => void; + setTimeRangeAndRefetch: (days: number, type: 'volume') => void; + volumeTimeRange: TimeseriesOptions; + setVolumeView: (view: 'USD' | 'Asset') => void; +}; + +function VolumeChart({ + historicalData, + market, + isLoading, + volumeView, + volumeTimeframe, + setVolumeTimeframe, + setTimeRangeAndRefetch, + volumeTimeRange, + setVolumeView, +}: VolumeChartProps) { + const formatYAxis = (value: number) => { + if (volumeView === 'USD') { + return `$${formatReadable(value)}`; + } else { + return formatReadable(value); + } + }; + + const formatTime = (unixTime: number) => { + const date = new Date(unixTime * 1000); + if (volumeTimeRange.endTimestamp - volumeTimeRange.startTimestamp <= 24 * 60 * 60) { + return date.toLocaleTimeString(undefined, { hour: '2-digit', minute: '2-digit' }); + } + return date.toLocaleDateString(undefined, { month: 'short', day: 'numeric' }); + }; + + const getVolumeChartData = () => { + if (!historicalData) return []; + + const supplyData = + volumeView === 'USD' ? historicalData.supplyAssetsUsd : historicalData.supplyAssets; + const borrowData = + volumeView === 'USD' ? historicalData.borrowAssetsUsd : historicalData.borrowAssets; + const liquidityData = + volumeView === 'USD' ? historicalData.liquidityAssetsUsd : historicalData.liquidityAssets; + + // Process all data in a single loop + return supplyData + .map((point: TimeseriesDataPoint, index: number) => { + // Get corresponding points from other series + const borrowPoint = borrowData[index]; + const liquidityPoint = liquidityData[index]; + + // Convert values based on view type + const supplyValue = + volumeView === 'USD' + ? point.y + : Number(formatUnits(BigInt(point.y), market.loanAsset.decimals)); + const borrowValue = + volumeView === 'USD' + ? borrowPoint?.y || 0 + : Number(formatUnits(BigInt(borrowPoint?.y || 0), market.loanAsset.decimals)); + const liquidityValue = + volumeView === 'USD' + ? liquidityPoint?.y || 0 + : Number(formatUnits(BigInt(liquidityPoint?.y || 0), market.loanAsset.decimals)); + + // Check if any timestamps has USD value exceeds 100B + if (historicalData.supplyAssetsUsd[index].y >= 100_000_000_000) { + return null; + } + + return { + x: point.x, + supply: supplyValue, + borrow: borrowValue, + liquidity: liquidityValue, + }; + }) + .filter((point): point is NonNullable => point !== null); + }; + + const formatValue = (value: number) => { + const formattedValue = formatReadable(value); + return volumeView === 'USD' + ? `$${formattedValue}` + : `${formattedValue} ${market.loanAsset.symbol}`; + }; + + const getCurrentVolumeStats = (type: 'supply' | 'borrow' | 'liquidity') => { + const data = + volumeView === 'USD' + ? historicalData?.[`${type}AssetsUsd`] + : historicalData?.[`${type}Assets`]; + if (!data || data.length === 0) return { current: 0, netChange: 0, netChangePercentage: 0 }; + + const current = + volumeView === 'USD' + ? data[data.length - 1].y + : Number(formatUnits(BigInt(data[data.length - 1].y), market.loanAsset.decimals)); + const start = + volumeView === 'USD' + ? data[0].y + : Number(formatUnits(BigInt(data[0].y), market.loanAsset.decimals)); + const netChange = current - start; + const netChangePercentage = start !== 0 ? (netChange / start) * 100 : 0; + + return { current, netChange, netChangePercentage }; + }; + + const getAverageVolumeStats = (type: 'supply' | 'borrow' | 'liquidity') => { + const data = + volumeView === 'USD' + ? historicalData?.[`${type}AssetsUsd`] + : historicalData?.[`${type}Assets`]; + if (!data || data.length === 0) return 0; + const sum = data.reduce( + (acc, point) => + acc + + Number( + volumeView === 'USD' ? point.y : formatUnits(BigInt(point.y), market.loanAsset.decimals), + ), + 0, + ); + return sum / data.length; + }; + + const volumeViewOptions = [ + { key: 'USD', label: 'USD', value: 'USD' }, + { key: 'Asset', label: market.loanAsset.symbol, value: 'Asset' }, + ]; + + const timeframeOptions = [ + { key: '1day', label: '1D', value: '1day' }, + { key: '7day', label: '7D', value: '7day' }, + { key: '30day', label: '30D', value: '30day' }, + ]; + + const handleTimeframeChange = useCallback( + (value: string) => { + setVolumeTimeframe(value as '1day' | '7day' | '30day'); + const days = value === '1day' ? 1 : value === '7day' ? 7 : 30; + setTimeRangeAndRefetch(days, 'volume'); + }, + [setVolumeTimeframe, setTimeRangeAndRefetch], + ); + + const [visibleLines, setVisibleLines] = useState({ + supply: true, + borrow: true, + liquidity: true, + }); + + return ( + + + Volumes +
    + setVolumeView(value as 'USD' | 'Asset')} + size="sm" + variant="default" + /> + +
    +
    + +
    +
    + {isLoading ? ( +
    + +
    + ) : ( + + + + + + + + + + + + + + + + + + + + new Date(unixTime * 1000).toLocaleString()} + formatter={(value: number, name: string) => [formatValue(value), name]} + /> + { + const dataKey = e.dataKey as keyof typeof visibleLines; + setVisibleLines((prev) => ({ + ...prev, + [dataKey]: !prev[dataKey], + })); + }} + formatter={(value, entry) => ( + + {value} + + )} + /> + + + + + + )} +
    +
    +
    +
    +

    Current Volumes

    + {['supply', 'borrow', 'liquidity'].map((type) => { + const stats = getCurrentVolumeStats(type as 'supply' | 'borrow' | 'liquidity'); + return ( +
    + {type}: + + {formatValue(stats.current)} + 0 + ? 'ml-2 text-green-500' + : 'ml-2 text-red-500' + } + > + ({stats.netChangePercentage > 0 ? '+' : ''} + {stats.netChangePercentage.toFixed(2)}%) + + +
    + ); + })} +
    + +
    +

    + Historical Averages{' '} + ({volumeTimeframe}) +

    + {isLoading ? ( +
    + +
    + ) : ( + <> + {['supply', 'borrow', 'liquidity'].map((type) => ( +
    + {type}: + + {formatValue( + getAverageVolumeStats(type as 'supply' | 'borrow' | 'liquidity'), + )} + +
    + ))} + + )} +
    +
    +
    +
    +
    +
    + ); +} + +export default VolumeChart; diff --git a/app/market/[chainId]/[marketid]/content.tsx b/app/market/[chainId]/[marketid]/content.tsx new file mode 100644 index 00000000..6b5c1e71 --- /dev/null +++ b/app/market/[chainId]/[marketid]/content.tsx @@ -0,0 +1,293 @@ +'use client'; + +import { useState, useCallback } from 'react'; +import { Button } from '@nextui-org/button'; +import { Card, CardHeader, CardBody } from '@nextui-org/card'; +import { Spinner } from '@nextui-org/spinner'; +import { ExternalLinkIcon, ChevronLeftIcon } from '@radix-ui/react-icons'; +import Image from 'next/image'; +import Link from 'next/link'; +import { useParams, useRouter } from 'next/navigation'; +import { formatUnits } from 'viem'; +import { OracleFeedInfo } from '@/components/FeedInfo/OracleFeedInfo'; +import Header from '@/components/layout/header/Header'; +import OracleVendorBadge from '@/components/OracleVendorBadge'; +import { useMarket, useMarketHistoricalData } from '@/hooks/useMarket'; +import MORPHO_LOGO from '@/imgs/tokens/morpho.svg'; +import { getExplorerURL, getMarketURL } from '@/utils/external'; +import { getIRMTitle } from '@/utils/morpho'; +import { SupportedNetworks } from '@/utils/networks'; +import { findToken } from '@/utils/tokens'; +import { TimeseriesOptions } from '@/utils/types'; +import RateChart from './RateChart'; +import VolumeChart from './VolumeChart'; + +const NOW = Math.floor(Date.now() / 1000); +const WEEK_IN_SECONDS = 7 * 24 * 60 * 60; + +function MarketContent() { + const { marketid, chainId } = useParams(); + + const network = Number(chainId as string) as SupportedNetworks; + + const router = useRouter(); + const [rateTimeRange, setRateTimeRange] = useState({ + startTimestamp: NOW - WEEK_IN_SECONDS, + endTimestamp: NOW, + interval: 'HOUR', + }); + const [volumeTimeRange, setVolumeTimeRange] = useState({ + startTimestamp: NOW - WEEK_IN_SECONDS, + endTimestamp: NOW, + interval: 'HOUR', + }); + const [apyTimeframe, setApyTimeframe] = useState<'1day' | '7day' | '30day'>('7day'); + const [volumeTimeframe, setVolumeTimeframe] = useState<'1day' | '7day' | '30day'>('7day'); + const [volumeView, setVolumeView] = useState<'USD' | 'Asset'>('USD'); + + const { + data: market, + isLoading: isMarketLoading, + error: marketError, + } = useMarket(marketid as string, network); + const { + data: historicalData, + isLoading: isHistoricalLoading, + refetch: refetchHistoricalData, + } = useMarketHistoricalData(marketid as string, network, rateTimeRange, volumeTimeRange); + + const setTimeRangeAndRefetch = useCallback( + (days: number, type: 'rate' | 'volume') => { + const endTimestamp = Math.floor(Date.now() / 1000); + const startTimestamp = endTimestamp - days * 24 * 60 * 60; + const newTimeRange = { + startTimestamp, + endTimestamp, + interval: days > 30 ? 'DAY' : 'HOUR', + } as TimeseriesOptions; + + if (type === 'rate') { + setRateTimeRange(newTimeRange); + void refetchHistoricalData.rates(); + } else { + setVolumeTimeRange(newTimeRange); + void refetchHistoricalData.volumes(); + } + }, + [refetchHistoricalData, setRateTimeRange, setVolumeTimeRange], + ); + + if (isMarketLoading) { + return ( +
    + +
    + ); + } + + if (marketError) { + return
    Error: {(marketError as Error).message}
    ; + } + + if (!market) { + return
    Market data not available
    ; + } + + const loanImg = findToken(market.loanAsset.address, market.morphoBlue.chain.id)?.img; + const collateralImg = findToken(market.collateralAsset.address, market.morphoBlue.chain.id)?.img; + + const cardStyle = 'bg-surface rounded-md shadow-sm p-4'; + + const averageLTV = + !market.state.collateralAssetsUsd || + !market.state.borrowAssetsUsd || + market.state.collateralAssetsUsd <= 0 + ? 0 + : (parseFloat(market.state.borrowAssetsUsd) / market.state.collateralAssetsUsd) * 100; + + return ( + <> +
    +
    + {/* navigation bottons */} +
    + + + +
    + +

    + {market.loanAsset.symbol}/{market.collateralAsset.symbol} Market +

    + +
    + + Basic Info + +
    +
    + Loan Asset: +
    + {loanImg && ( + + )} + + {market.loanAsset.symbol} + +
    +
    +
    + Collateral Asset: +
    + {collateralImg && ( + + )} + + {market.collateralAsset.symbol} + +
    +
    +
    + IRM: + + {getIRMTitle(market.irmAddress)} + +
    +
    +
    +
    + + + LLTV Info + +
    +
    + LLTV: + {formatUnits(BigInt(market.lltv), 16)}% +
    +
    + Average LTV: + {averageLTV.toFixed(2)}% +
    +
    +
    +
    + + + Oracle Info + +
    +
    + Vendor: + {market.oracle.data && ( + + {' '} + + + )} +
    +
    +

    Feed Routes:

    + {market.oracle.data && ( +
    + {' '} + + + + {' '} +
    + )} +
    +
    +
    +
    +
    + + + + +
    + + ); +} + +export default MarketContent; diff --git a/app/market/[chainId]/[marketid]/page.tsx b/app/market/[chainId]/[marketid]/page.tsx new file mode 100644 index 00000000..3a92c7b3 --- /dev/null +++ b/app/market/[chainId]/[marketid]/page.tsx @@ -0,0 +1,11 @@ +import { Metadata } from 'next'; +import MarketContent from './content'; + +export const metadata: Metadata = { + title: 'Market Details | Morpho Blue', + description: 'Detailed information about a specific market on Morpho Blue', +}; + +export default function MarketPage() { + return ; +} diff --git a/app/markets/components/AdvancedSearchBar.tsx b/app/markets/components/AdvancedSearchBar.tsx index a0d8ebb6..f03446bb 100644 --- a/app/markets/components/AdvancedSearchBar.tsx +++ b/app/markets/components/AdvancedSearchBar.tsx @@ -193,15 +193,15 @@ function AdvancedSearchBar({ onFocus={handleInputFocus} endContent={} classNames={{ - inputWrapper: 'bg-secondary rounded-sm w-full lg:w-[600px]', - input: 'bg-secondary rounded-sm text-xs', + inputWrapper: 'bg-surface rounded-sm w-full lg:w-[600px]', + input: 'bg-surface rounded-sm text-xs', }} autoComplete="off" /> {showSuggestions && suggestions.length > 0 && (
      {suggestions.map((suggestion, index) => { diff --git a/app/markets/components/AssetFilter.tsx b/app/markets/components/AssetFilter.tsx index 0a6081c5..71a9c275 100644 --- a/app/markets/components/AssetFilter.tsx +++ b/app/markets/components/AssetFilter.tsx @@ -81,8 +81,8 @@ export default function AssetFilter({ return (
      {isOpen && !loading && ( -
      +
      ))}
    -
    +
    @@ -43,7 +43,7 @@ export default function Positions() { diff --git a/app/positions/components/PositionsSummaryTable.tsx b/app/positions/components/PositionsSummaryTable.tsx index fe83d90b..55cdaf10 100644 --- a/app/positions/components/PositionsSummaryTable.tsx +++ b/app/positions/components/PositionsSummaryTable.tsx @@ -268,7 +268,7 @@ export function PositionsSummaryTable({
@@ -308,15 +308,10 @@ export default function MarketProgram({ return ( - - + + {market.uniqueKey.slice(2, 8)} - + {market.loanAsset.symbol} {market.collateralAsset.symbol} diff --git a/app/rewards/components/RewardContent.tsx b/app/rewards/components/RewardContent.tsx index 671576f2..71eea80b 100644 --- a/app/rewards/components/RewardContent.tsx +++ b/app/rewards/components/RewardContent.tsx @@ -2,6 +2,7 @@ import { useMemo, useState } from 'react'; import { useParams } from 'next/navigation'; +import ButtonGroup from '@/components/ButtonGroup'; import Header from '@/components/layout/header/Header'; import EmptyScreen from '@/components/Status/EmptyScreen'; import LoadingScreen from '@/components/Status/LoadingScreen'; @@ -11,6 +12,11 @@ import { filterMarketRewards, filterUniformRewards } from '@/utils/rewardHelpers import MarketProgram from './MarketProgram'; import UniformProgram from './UniformProgram'; // You'll need to create this component +const programOptions = [ + { key: 'market', label: 'Market Program', value: 'market' }, + { key: 'uniform', label: 'Uniform Program', value: 'uniform' }, +]; + export default function Rewards() { const { account } = useParams<{ account: string }>(); const [activeProgram, setActiveProgram] = useState<'market' | 'uniform'>('market'); @@ -57,30 +63,12 @@ export default function Rewards() {
-
- - -
+ setActiveProgram(value as 'market' | 'uniform')} + size="md" + />
diff --git a/app/rewards/components/UniformProgram.tsx b/app/rewards/components/UniformProgram.tsx index 567b4fab..f4557145 100644 --- a/app/rewards/components/UniformProgram.tsx +++ b/app/rewards/components/UniformProgram.tsx @@ -76,13 +76,13 @@ export default function UniformProgram({ .

-
+
diff --git a/docs/Styling.md b/docs/Styling.md new file mode 100644 index 00000000..0d1b8746 --- /dev/null +++ b/docs/Styling.md @@ -0,0 +1,11 @@ +# Styling and CSS guidelines + +## Colors + +- background: `bg-main` +- card, button: `bg-surface` + +## Tailwind-Compatible Component + +- primary: `text-primary` +- secondary: `text-secondary` diff --git a/package.json b/package.json index f7a7e6b2..335a3c0b 100644 --- a/package.json +++ b/package.json @@ -60,6 +60,7 @@ "react-spinners": "^0.14.1", "react-table": "^7.8.0", "react-toastify": "^10.0.5", + "recharts": "^2.13.0", "rehype-pretty-code": "0.12.3", "rehype-stringify": "^10.0.0", "remark-parse": "^11.0.0", diff --git a/src/components/Button/Button.tsx b/src/components/Button/Button.tsx index 46ba71c0..be223ac7 100644 --- a/src/components/Button/Button.tsx +++ b/src/components/Button/Button.tsx @@ -30,7 +30,7 @@ export default function Button({ className={clsx( 'flex w-full items-center justify-center', 'py-4 text-sm', - variant === 'primary' ? 'bg-monarch-white' : 'bg-primary', + variant === 'primary' ? 'bg-monarch-white' : 'bg-main', variant === 'primary' ? 'text-black' : 'text-white', disabled && variant === 'primary' ? 'bg-gray-400' : null, disabled && variant === 'secondary' ? 'bg-boat-color-gray-900' : null, diff --git a/src/components/ButtonGroup.tsx b/src/components/ButtonGroup.tsx new file mode 100644 index 00000000..90e2badb --- /dev/null +++ b/src/components/ButtonGroup.tsx @@ -0,0 +1,98 @@ +'use client'; + +import { ReactNode } from 'react'; +import clsx from 'clsx'; + +export type ButtonOption = { + key: string; + label: ReactNode; + value: string; +}; + +type ButtonGroupProps = { + options: ButtonOption[]; + value: string; + onChange: (value: ButtonOption['value']) => void; + size?: 'sm' | 'md' | 'lg'; + variant?: 'default' | 'primary'; +}; + +const sizeClasses = { + sm: 'px-3 py-2 text-sm', + md: 'px-4 py-3 text-base', + lg: 'px-6 py-4 text-lg', +}; + +const variantStyles = { + default: (isSelected: boolean) => [ + isSelected ? 'bg-hovered hover:bg-surface z-10' : 'bg-surface hover:bg-hovered', + 'border border-divider', + 'shadow-sm', + ], + primary: (isSelected: boolean) => [ + isSelected + ? [ + 'z-10 bg-primary hover:bg-primary/90', + 'shadow-[0_2px_8px_-2px] shadow-primary/30', + 'border-primary/80', + ] + : [ + 'bg-surface hover:bg-surface/90', + 'hover:shadow-[0_2px_8px_-2px] hover:shadow-primary/20', + 'border-primary/60 hover:border-primary/80', + ], + 'border', + 'before:absolute before:inset-0 before:rounded-[inherit]', + isSelected + ? 'before:bg-gradient-to-b before:from-white/10 before:to-transparent' + : 'before:bg-gradient-to-b before:from-transparent before:to-black/5', + ], +}; + +export default function ButtonGroup({ + options, + value, + onChange, + size = 'md', + variant = 'default', +}: ButtonGroupProps) { + return ( +
+ {options.map((option, index) => { + const isFirst = index === 0; + const isLast = index === options.length - 1; + const isSelected = option.value === value; + + return ( + + ); + })} +
+ ); +} diff --git a/src/components/Input/Input.tsx b/src/components/Input/Input.tsx index e6d9952e..21b688fc 100644 --- a/src/components/Input/Input.tsx +++ b/src/components/Input/Input.tsx @@ -70,7 +70,7 @@ export default function Input({ diff --git a/src/components/OracleVendorBadge.tsx b/src/components/OracleVendorBadge.tsx index 722a827e..4c317b2c 100644 --- a/src/components/OracleVendorBadge.tsx +++ b/src/components/OracleVendorBadge.tsx @@ -20,7 +20,7 @@ const renderVendorIcon = (vendor: OracleVendors) => function OracleVendorBadge({ oracleData, - showText = true, + showText = false, useTooltip = true, }: OracleVendorBadgeProps) { const { vendors } = parseOracleVendors(oracleData); @@ -28,10 +28,10 @@ function OracleVendorBadge({ const noFeeds = vendors.length === 0; const content = ( -
- {!useTooltip && showText && ( +
+ {showText && ( - {noFeeds ? 'No Oracle' : vendors.join(', ')}: + {noFeeds ? 'No Oracle' : vendors.join(', ')} )} {noFeeds ? ( diff --git a/src/components/RiskNotificationModal.tsx b/src/components/RiskNotificationModal.tsx index aee02b40..87132b36 100644 --- a/src/components/RiskNotificationModal.tsx +++ b/src/components/RiskNotificationModal.tsx @@ -47,7 +47,7 @@ export default function RiskNotificationModal() { Monarch enables direct lending to the Morpho Blue protocol. Before proceeding, it's important to understand the key aspects of this approach. For a comprehensive overview, please visit our{' '} - + introduction page . @@ -65,12 +65,12 @@ export default function RiskNotificationModal() { While this approach offers more control, it also requires a deeper understanding of market dynamics. For a detailed explanation of the risks and considerations, please read our{' '} - + risk assessment page .

-
+
-
@@ -57,7 +54,7 @@ export default function SearchOrConnect({ path }: { path: string }) {
setInputAddress(e.target.value)} placeholder="0x..." diff --git a/src/components/Status/EmptyScreen.tsx b/src/components/Status/EmptyScreen.tsx index 64f364b5..db795019 100644 --- a/src/components/Status/EmptyScreen.tsx +++ b/src/components/Status/EmptyScreen.tsx @@ -8,7 +8,7 @@ type EmptyScreenProps = { export default function EmptyScreen({ message = 'No data', hint }: EmptyScreenProps) { return ( -
+

{message}

{hint}

diff --git a/src/components/Status/LoadingScreen.tsx b/src/components/Status/LoadingScreen.tsx index c9e73692..2aea444e 100644 --- a/src/components/Status/LoadingScreen.tsx +++ b/src/components/Status/LoadingScreen.tsx @@ -8,7 +8,7 @@ type LoadingScreenProps = { export default function LoadingScreen({ message = 'Loading...' }: LoadingScreenProps) { return ( -
+

{message}

diff --git a/src/components/SupplyProcessModal.tsx b/src/components/SupplyProcessModal.tsx index eedcc62a..79c41647 100644 --- a/src/components/SupplyProcessModal.tsx +++ b/src/components/SupplyProcessModal.tsx @@ -58,11 +58,11 @@ export function SupplyProcessModal({