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 (
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/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
/>
))}
- } />
+ } />