diff --git a/app/global.css b/app/global.css index 7eef9a2f..c7e9c0e3 100644 --- a/app/global.css +++ b/app/global.css @@ -513,3 +513,15 @@ pre { border-radius: 20px; border: transparent; } + +/* Accessibility: Respect user's preference for reduced motion */ +@media (prefers-reduced-motion: reduce) { + *, + *::before, + *::after { + animation-duration: 0.01ms !important; + animation-iteration-count: 1 !important; + transition-duration: 0.01ms !important; + scroll-behavior: auto !important; + } +} diff --git a/src/components/Avatar/Avatar.tsx b/src/components/Avatar/Avatar.tsx index 498b8197..8a8a3d57 100644 --- a/src/components/Avatar/Avatar.tsx +++ b/src/components/Avatar/Avatar.tsx @@ -1,7 +1,6 @@ -import { useEffect, useState } from 'react'; +import { useState } from 'react'; import Image from 'next/image'; import type { Address } from 'viem'; -import { getMorphoAddress } from '@/utils/morpho'; type AvatarProps = { address: Address; @@ -10,33 +9,20 @@ type AvatarProps = { }; export function Avatar({ address, size = 30, rounded = true }: AvatarProps) { - const [useEffigy, setUseEffigy] = useState(true); + const [effigyErrorAddress, setEffigyErrorAddress] = useState
(null); + const effigyActive = effigyErrorAddress !== address; const effigyUrl = `https://effigy.im/a/${address}.svg`; const dicebearUrl = `https://api.dicebear.com/7.x/pixel-art/png?seed=${address}`; - useEffect(() => { - const checkEffigyAvailability = async () => { - const effigyMockurl = `https://effigy.im/a/${getMorphoAddress(1)}.png`; - try { - const response = await fetch(effigyMockurl, { method: 'HEAD' }); - setUseEffigy(response.ok); - } catch (_error) { - setUseEffigy(false); - } - }; - - void checkEffigyAvailability(); - }, []); - return (
{`Avatar setUseEffigy(false)} + onError={() => setEffigyErrorAddress(address)} />
); diff --git a/src/data-sources/morpho-api/market.ts b/src/data-sources/morpho-api/market.ts index 16a57a8a..4fe936f9 100644 --- a/src/data-sources/morpho-api/market.ts +++ b/src/data-sources/morpho-api/market.ts @@ -84,24 +84,17 @@ export const fetchMorphoMarkets = async (network: SupportedNetworks): Promise(marketsQuery, variables); - // Handle NOT_FOUND - break pagination loop - if (!response) { - console.warn(`No markets found in Morpho API for network ${network} at skip ${skip}.`); - break; - } - - if (!response.data || !response.data.markets) { - console.warn(`Market data not found in Morpho API response for network ${network} at skip ${skip}.`); - break; + // Handle failed pages - skip to next page instead of breaking entirely + // This handles corrupted market records that cause NOT_FOUND errors + if (!response || !response.data?.markets?.items || !response.data.markets.pageInfo) { + console.warn(`[Markets] Skipping failed page at skip=${skip} for network ${network}`); + skip += pageSize; // Skip ahead to next page + if (totalCount > 0 && skip >= totalCount) break; + continue; } const { items, pageInfo } = response.data.markets; - if (!items || !Array.isArray(items) || !pageInfo) { - console.warn(`No market items or page info found in response for network ${network} at skip ${skip}.`); - break; - } - // Process and add markets to the collection const processedMarkets = items.map(processMarketData); allMarkets.push(...processedMarkets); diff --git a/src/features/autovault/components/vault-detail/settings/EditMetadata.tsx b/src/features/autovault/components/vault-detail/settings/EditMetadata.tsx index a3cb197f..0e530e1a 100644 --- a/src/features/autovault/components/vault-detail/settings/EditMetadata.tsx +++ b/src/features/autovault/components/vault-detail/settings/EditMetadata.tsx @@ -1,6 +1,6 @@ 'use client'; -import { useCallback, useEffect, useId, useState } from 'react'; +import { useCallback, useId, useState, useRef, useEffect } from 'react'; import { Input } from '@/components/ui/input'; import { Button } from '@/components/ui/button'; import { Spinner } from '@/components/ui/spinner'; @@ -36,30 +36,54 @@ export function EditMetadata({ const previousName = currentName.trim(); const previousSymbol = currentSymbol.trim(); - const [nameInput, setNameInput] = useState(previousName !== '' ? previousName : defaultName); - const [symbolInput, setSymbolInput] = useState(previousSymbol !== '' ? previousSymbol : defaultSymbol); + // Track if user has edited each field + const nameEdited = useRef(false); + const symbolEdited = useRef(false); + + const [nameInput, setNameInput] = useState(''); + const [symbolInput, setSymbolInput] = useState(''); const [metadataError, setMetadataError] = useState(null); + // Compute values during render - use default if not edited, otherwise use stored value + const computedNameInput = nameEdited.current ? nameInput : (previousName !== '' ? previousName : defaultName); + const computedSymbolInput = symbolEdited.current ? symbolInput : (previousSymbol !== '' ? previousSymbol : defaultSymbol); + + const handleNameChange = useCallback((value: string) => { + nameEdited.current = true; + setNameInput(value); + }, []); + + const handleSymbolChange = useCallback((value: string) => { + symbolEdited.current = true; + setSymbolInput(value); + }, []); + const { needSwitchChain, switchToNetwork } = useMarketNetwork({ targetChainId: chainId, }); - // Reset inputs when current values change - useEffect(() => { - setNameInput(previousName !== '' ? previousName : defaultName); - setSymbolInput(previousSymbol !== '' ? previousSymbol : defaultSymbol); - }, [previousName, previousSymbol, defaultName, defaultSymbol]); - - const trimmedName = nameInput.trim(); - const trimmedSymbol = symbolInput.trim(); - const metadataChanged = trimmedName !== previousName || trimmedSymbol !== previousSymbol; - // Clear error when inputs change useEffect(() => { - if (metadataError && metadataChanged) { + if (metadataError) { setMetadataError(null); } - }, [metadataChanged, metadataError]); + }, [computedNameInput, computedSymbolInput, metadataError]); + + // Reset name edit state when upstream name changes + useEffect(() => { + nameEdited.current = false; + setNameInput(''); + }, [previousName]); + + // Reset symbol edit state when upstream symbol changes + useEffect(() => { + symbolEdited.current = false; + setSymbolInput(''); + }, [previousSymbol]); + + const trimmedName = computedNameInput.trim(); + const trimmedSymbol = computedSymbolInput.trim(); + const metadataChanged = trimmedName !== previousName || trimmedSymbol !== previousSymbol; const handleMetadataSubmit = useCallback(async () => { if (!metadataChanged) { @@ -96,8 +120,8 @@ export function EditMetadata({ setNameInput(event.target.value)} + value={computedNameInput} + onChange={(event) => handleNameChange(event.target.value)} placeholder={defaultName} disabled={!isOwner} id={nameInputId} @@ -117,8 +141,8 @@ export function EditMetadata({ setSymbolInput(event.target.value)} + value={computedSymbolInput} + onChange={(event) => handleSymbolChange(event.target.value)} placeholder={defaultSymbol} maxLength={16} disabled={!isOwner} diff --git a/src/features/market-detail/components/borrowers-table.tsx b/src/features/market-detail/components/borrowers-table.tsx index a4d088e1..96b6c734 100644 --- a/src/features/market-detail/components/borrowers-table.tsx +++ b/src/features/market-detail/components/borrowers-table.tsx @@ -40,10 +40,15 @@ export function BorrowersTable({ chainId, market, minShares, oraclePrice, onOpen const hasActiveFilter = minShares !== '0'; const tableKey = `borrowers-table-${currentPage}`; - // Calculate LTV for each borrower + // Calculate LTV and Days to Liquidation for each borrower // LTV = borrowAssets / (collateral * oraclePrice) - const borrowersWithLTV = useMemo(() => { - if (!oraclePrice || oraclePrice === 0n) return []; + // Days to Liquidation = ln(lltv/ltv) / ln(1 + borrowApy) * 365 + // (using continuous compounding: r = ln(1 + APY) to convert annual APY to continuous rate) + const borrowersWithMetrics = useMemo(() => { + if (!oraclePrice) return []; + + const lltv = Number(market.lltv) / 1e16; // lltv in WAD format (e.g., 8e17 = 80%) + const borrowApy = market.state.borrowApy; return borrowers.map((borrower) => { const borrowAssets = BigInt(borrower.borrowAssets); @@ -54,18 +59,29 @@ export function BorrowersTable({ chainId, market, minShares, oraclePrice, onOpen const collateralValueInLoan = (collateral * oraclePrice) / BigInt(10 ** 36); // Calculate LTV as a percentage - // LTV = (borrowAssets / collateralValue) * 100 let ltv = 0; if (collateralValueInLoan > 0n) { ltv = Number((borrowAssets * 10000n) / collateralValueInLoan) / 100; } + // Calculate Days to Liquidation + // Only calculate if borrower has position, LTV > 0, and borrow rate > 0 + let daysToLiquidation: number | null = null; + if (ltv > 0 && borrowApy > 0 && lltv > ltv) { + // Use continuous compounding: LTV(t) = LTV * e^(r * t) where r = ln(1 + APY) + // Solve for t when LTV(t) = lltv: t = ln(lltv/ltv) / r + const continuousRate = Math.log(1 + borrowApy); + const yearsToLiquidation = Math.log(lltv / ltv) / continuousRate; + daysToLiquidation = Math.max(0, Math.round(yearsToLiquidation * 365)); + } + return { ...borrower, ltv, + daysToLiquidation, }; }); - }, [borrowers, oraclePrice]); + }, [borrowers, oraclePrice, market.lltv, market.state.borrowApy]); return (
@@ -116,26 +132,36 @@ export function BorrowersTable({ chainId, market, minShares, oraclePrice, onOpen BORROWED COLLATERAL LTV + DAYS TO LIQ. % OF BORROW - {borrowersWithLTV.length === 0 && !isLoading ? ( + {borrowersWithMetrics.length === 0 && !isLoading ? ( No borrowers found for this market ) : ( - borrowersWithLTV.map((borrower) => { + borrowersWithMetrics.map((borrower) => { const totalBorrow = BigInt(market.state.borrowAssets); const borrowerAssets = BigInt(borrower.borrowAssets); const percentOfBorrow = totalBorrow > 0n ? (Number(borrowerAssets) / Number(totalBorrow)) * 100 : 0; const percentDisplay = percentOfBorrow < 0.01 && percentOfBorrow > 0 ? '<0.01%' : `${percentOfBorrow.toFixed(2)}%`; + // Color code days to liquidation + const daysDisplay = borrower.daysToLiquidation !== null + ? borrower.daysToLiquidation < 30 + ? `${borrower.daysToLiquidation}` + : borrower.daysToLiquidation > 365 + ? '>365' + : `${borrower.daysToLiquidation}` + : '—'; + return ( @@ -175,6 +201,7 @@ export function BorrowersTable({ chainId, market, minShares, oraclePrice, onOpen
{borrower.ltv.toFixed(2)}% + {daysDisplay} {percentDisplay} ); diff --git a/src/features/market-detail/components/charts/borrowers-pie-chart.tsx b/src/features/market-detail/components/charts/borrowers-pie-chart.tsx index 6544df1b..bd1ad12a 100644 --- a/src/features/market-detail/components/charts/borrowers-pie-chart.tsx +++ b/src/features/market-detail/components/charts/borrowers-pie-chart.tsx @@ -35,6 +35,67 @@ type PieDataItem = { const TOP_POSITIONS_TO_SHOW = 8; const OTHER_COLOR = '#64748B'; // Grey for "Other" category +// Helper function at module scope +const formatPercentDisplay = (percent: number): string => { + if (percent < 0.01 && percent > 0) return '<0.01%'; + return `${percent.toFixed(2)}%`; +}; + +// Custom tooltip at module scope +function BorrowersPieTooltip({ + active, + payload, + expandedOther, + market, +}: { + active?: boolean; + payload?: { payload: PieDataItem }[]; + expandedOther: boolean; + market: Market; +}) { + if (!active || !payload || !payload[0]) return null; + const data = payload[0].payload; + + return ( +
+

{data.name}

+ {!data.isOther &&

{getSlicedAddress(data.address as `0x${string}`)}

} +
+
+ Borrowed +
+ {formatSimple(data.value)} + +
+
+
+ Collateral +
+ {formatSimple(data.collateral)} +
+
+ {!data.isOther && ( +
+ LTV + {data.ltv.toFixed(2)}% +
+ )} +
+ % of Borrow + {formatPercentDisplay(data.percentage)} +
+
+ {data.isOther &&

Click to {expandedOther ? 'collapse' : 'expand'}

} +
+ ); +} + export function BorrowersPieChart({ chainId, market, oraclePrice }: BorrowersPieChartProps) { const { data: borrowers, isLoading, totalCount } = useAllMarketBorrowers(market.uniqueKey, chainId); const { getVaultByAddress } = useVaultRegistry(); @@ -145,63 +206,6 @@ export function BorrowersPieChart({ chainId, market, oraclePrice }: BorrowersPie // Extract the "Other" entry once for use in expanded section const otherEntry = useMemo(() => pieData.find((d) => d.isOther), [pieData]); - // Format percentage display (matches table) - const formatPercentDisplay = (percent: number): string => { - if (percent < 0.01 && percent > 0) return '<0.01%'; - return `${percent.toFixed(2)}%`; - }; - - const CustomTooltip = ({ active, payload }: { active?: boolean; payload?: { payload: PieDataItem }[] }) => { - if (!active || !payload || !payload[0]) return null; - const data = payload[0].payload; - - return ( -
-

{data.name}

- {!data.isOther &&

{getSlicedAddress(data.address as `0x${string}`)}

} -
-
- Borrowed -
- {formatSimple(data.value)} - -
-
-
- Collateral -
- {formatSimple(data.collateral)} - -
-
- {!data.isOther && ( -
- LTV - {data.ltv.toFixed(2)}% -
- )} -
- % of Borrow - {formatPercentDisplay(data.percentage)} -
-
- {data.isOther &&

Click to {expandedOther ? 'collapse' : 'expand'}

} -
- ); - }; - if (isLoading) { return ( @@ -253,7 +257,7 @@ export function BorrowersPieChart({ chainId, market, oraclePrice }: BorrowersPie /> ))} - } /> + } /> +

Top {data.position.toLocaleString()}

+
+
+ Actual + {data.cumulativePercent.toFixed(1)}% +
+
+ If equal + {data.idealPercent.toFixed(1)}% +
+
+ + ); +} + export function ConcentrationChart({ positions, totalCount, isLoading, title, color }: ConcentrationChartProps) { const { chartData, meaningfulCount, totalPercentShown } = useMemo(() => { const emptyResult = { chartData: [], meaningfulCount: 0, totalPercentShown: 0 }; @@ -75,29 +105,6 @@ export function ConcentrationChart({ positions, totalCount, isLoading, title, co }; }, [chartData]); - function CustomTooltip({ active, payload }: { active?: boolean; payload?: { payload: ConcentrationDataPoint }[] }) { - if (!active || !payload?.[0]) return null; - const data = payload[0].payload; - - if (data.position === 0) return null; - - return ( -
-

Top {data.position.toLocaleString()}

-
-
- Actual - {data.cumulativePercent.toFixed(1)}% -
-
- If equal - {data.idealPercent.toFixed(1)}% -
-
-
- ); - } - if (isLoading) { return ( @@ -197,7 +204,7 @@ export function ConcentrationChart({ positions, totalCount, isLoading, title, co /> } + content={} /> +

At {data.priceDrop}% price drop

+
+
+ % of Total Debt + {data.cumulativeDebtPercent.toFixed(1)}% +
+
+ Debt at Risk + + {formatReadable(data.cumulativeDebt)} {symbol} + +
+
+ + ); +} + export function DebtAtRiskChart({ chainId, market, oraclePrice }: DebtAtRiskChartProps) { const { data: borrowers, isLoading } = useAllMarketBorrowers(market.uniqueKey, chainId); const chartColors = useChartColors(); @@ -106,29 +138,6 @@ export function DebtAtRiskChart({ chainId, market, oraclePrice }: DebtAtRiskChar }; }, [borrowers, oraclePrice, market.loanAsset.decimals, lltv]); - const CustomTooltip = ({ active, payload }: { active?: boolean; payload?: { value: number; payload: RiskDataPoint }[] }) => { - if (!active || !payload || !payload[0]) return null; - const data = payload[0].payload; - - return ( -
-

At {data.priceDrop}% price drop

-
-
- % of Total Debt - {data.cumulativeDebtPercent.toFixed(1)}% -
-
- Debt at Risk - - {formatReadable(data.cumulativeDebt)} {market.loanAsset.symbol} - -
-
-
- ); - }; - if (isLoading) { return ( @@ -214,7 +223,7 @@ export function DebtAtRiskChart({ chainId, market, oraclePrice }: DebtAtRiskChar /> } + content={} /> string; + formatValue: (value: number) => string; +}) { + if (!active || !payload || payload.length === 0) return null; + + const dataPoint = payload[0]?.payload; + const timestamp = dataPoint?.timestamp ?? 0; + const blockNumber = dataPoint?.blockNumber; + + return ( +
+
+

+ {new Date(timestamp * 1000).toLocaleString(undefined, { + month: 'short', + day: 'numeric', + hour: '2-digit', + minute: '2-digit', + })} +

+ {blockNumber &&

Block #{blockNumber.toLocaleString()}

} +
+
+ {payload + .filter((entry) => entry.value > 0) + .sort((a, b) => b.value - a.value) + .map((entry) => ( +
+
+ + {getDisplayName(entry.dataKey)} +
+ {formatValue(entry.value)} +
+ ))} +
+
+ ); +} + export function SupplierPositionsChart({ marketId, chainId, market }: SupplierPositionsChartProps) { const selectedTimeframe = useMarketDetailChartState((s) => s.selectedTimeframe); const selectedTimeRange = useMarketDetailChartState((s) => s.selectedTimeRange); @@ -107,57 +162,6 @@ export function SupplierPositionsChart({ marketId, chainId, market }: SupplierPo }; }, [historicalData, selectedTimeframe]); - // Custom tooltip with block number - const CustomTooltip = ({ - active, - payload, - }: { - active?: boolean; - payload?: { dataKey: string; value: number; color: string; payload: { timestamp: number; blockNumber: number } }[]; - }) => { - if (!active || !payload || payload.length === 0) return null; - - const dataPoint = payload[0]?.payload; - const timestamp = dataPoint?.timestamp ?? 0; - const blockNumber = dataPoint?.blockNumber; - - return ( -
-
-

- {new Date(timestamp * 1000).toLocaleString(undefined, { - month: 'short', - day: 'numeric', - hour: '2-digit', - minute: '2-digit', - })} -

- {blockNumber &&

Block #{blockNumber.toLocaleString()}

} -
-
- {payload - .filter((entry) => entry.value > 0) - .sort((a, b) => b.value - a.value) - .map((entry) => ( -
-
- - {getDisplayName(entry.dataKey)} -
- {formatValue(entry.value)} -
- ))} -
-
- ); - }; - // Custom legend with toggleable items const renderLegend = () => (
@@ -264,7 +268,7 @@ export function SupplierPositionsChart({ marketId, chainId, market }: SupplierPo /> } + content={} /> {topSuppliers.map((supplier, index) => ( { + if (percent < 0.01 && percent > 0) return '<0.01%'; + return `${percent.toFixed(2)}%`; +}; + +// Custom tooltip at module scope +function SuppliersPieTooltip({ + active, + payload, + expandedOther, +}: { + active?: boolean; + payload?: { payload: PieDataItem }[]; + expandedOther: boolean; +}) { + if (!active || !payload || !payload[0]) return null; + const data = payload[0].payload; + + return ( +
+

{data.name}

+ {!data.isOther &&

{getSlicedAddress(data.address as `0x${string}`)}

} +
+
+ Supplied +
+ {formatSimple(data.value)} +
+
+
+ % of Supply + {formatPercentDisplay(data.percentage)} +
+
+ {data.isOther &&

Click to {expandedOther ? 'collapse' : 'expand'}

} +
+ ); +} + export function SuppliersPieChart({ chainId, market }: SuppliersPieChartProps) { const { data: suppliers, isLoading, totalCount } = useAllMarketSuppliers(market.uniqueKey, chainId); const { getVaultByAddress } = useVaultRegistry(); @@ -118,44 +158,6 @@ export function SuppliersPieChart({ chainId, market }: SuppliersPieChartProps) { // Extract the "Other" entry once for use in expanded section const otherEntry = useMemo(() => pieData.find((d) => d.isOther), [pieData]); - // Format percentage display (matches table) - const formatPercentDisplay = (percent: number): string => { - if (percent < 0.01 && percent > 0) return '<0.01%'; - return `${percent.toFixed(2)}%`; - }; - - const CustomTooltip = ({ active, payload }: { active?: boolean; payload?: { payload: PieDataItem }[] }) => { - if (!active || !payload || !payload[0]) return null; - const data = payload[0].payload; - - return ( -
-

{data.name}

- {!data.isOther &&

{getSlicedAddress(data.address as `0x${string}`)}

} -
-
- Supplied -
- {formatSimple(data.value)} - -
-
-
- % of Supply - {formatPercentDisplay(data.percentage)} -
-
- {data.isOther &&

Click to {expandedOther ? 'collapse' : 'expand'}

} -
- ); - }; - if (isLoading) { return ( @@ -207,7 +209,7 @@ export function SuppliersPieChart({ chainId, market }: SuppliersPieChartProps) { /> ))} - } /> + } /> { + // Check if the browser supports media queries + if (typeof window === 'undefined') return; + + const mediaQuery = window.matchMedia('(prefers-reduced-motion: reduce)'); + setPrefersReducedMotion(mediaQuery.matches); + + const handleChange = (event: MediaQueryListEvent) => { + setPrefersReducedMotion(event.matches); + }; + + mediaQuery.addEventListener('change', handleChange); + return () => mediaQuery.removeEventListener('change', handleChange); + }, []); + + return prefersReducedMotion; +} diff --git a/src/modals/borrow/borrow-modal.tsx b/src/modals/borrow/borrow-modal.tsx index 35d6c008..265426bb 100644 --- a/src/modals/borrow/borrow-modal.tsx +++ b/src/modals/borrow/borrow-modal.tsx @@ -33,10 +33,11 @@ export function BorrowModal({ }: BorrowModalProps): JSX.Element { const [mode, setMode] = useState<'borrow' | 'repay'>(defaultMode); - // Reset mode when defaultMode changes (e.g., modal re-opened with different mode) + // Sync mode with defaultMode when it changes useEffect(() => { setMode(defaultMode); }, [defaultMode]); + const { address: account } = useConnection(); // Get token balances diff --git a/src/modals/settings/monarch-settings/details/BlacklistedMarketsDetail.tsx b/src/modals/settings/monarch-settings/details/BlacklistedMarketsDetail.tsx index 0427a870..094a9c40 100644 --- a/src/modals/settings/monarch-settings/details/BlacklistedMarketsDetail.tsx +++ b/src/modals/settings/monarch-settings/details/BlacklistedMarketsDetail.tsx @@ -1,6 +1,6 @@ 'use client'; -import { useMemo, useState, useEffect } from 'react'; +import { useMemo, useState, useCallback } from 'react'; import { FiPlus } from 'react-icons/fi'; import { Cross2Icon } from '@radix-ui/react-icons'; import { Button } from '@/components/ui/button'; @@ -20,9 +20,10 @@ export function BlacklistedMarketsDetail() { useBlacklistedMarkets(); const { success: toastSuccess } = useStyledToast(); - useEffect(() => { + const handleSearchChange = useCallback((e: React.ChangeEvent) => { + setSearchQuery(e.target.value); setCurrentPage(1); - }, [searchQuery]); + }, []); const { blacklistedMarkets, availableMarkets } = useMemo(() => { const blacklisted: Market[] = []; @@ -132,7 +133,7 @@ export function BlacklistedMarketsDetail() { type="text" placeholder="Search to add markets (min 2 characters)..." value={searchQuery} - onChange={(e) => setSearchQuery(e.target.value)} + onChange={handleSearchChange} className="bg-hovered h-9 w-full rounded p-2 font-zen text-xs focus:border-primary focus:outline-none" />
diff --git a/src/utils/generateMetadata.ts b/src/utils/generateMetadata.ts index edd69e9e..67a8f4ef 100644 --- a/src/utils/generateMetadata.ts +++ b/src/utils/generateMetadata.ts @@ -10,7 +10,7 @@ type MetaTagsProps = { }; const deployUrl = process.env.BOAT_DEPLOY_URL ?? process.env.VERCEL_URL; -const defaultUrl = deployUrl ? `https://${deployUrl}` : `http://localhost:${process.env.PORT ?? 3000}`; +const defaultUrl = deployUrl ? `https://${deployUrl}` : 'https://monarchlend.xyz'; export const generateMetadata = ({ title = 'Monarch',