Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions app/global.css
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}
24 changes: 5 additions & 19 deletions src/components/Avatar/Avatar.tsx
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -10,33 +9,20 @@ type AvatarProps = {
};

export function Avatar({ address, size = 30, rounded = true }: AvatarProps) {
const [useEffigy, setUseEffigy] = useState(true);
const [effigyErrorAddress, setEffigyErrorAddress] = useState<Address | null>(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 (
<div style={{ width: size, height: size }}>
<Image
src={useEffigy ? effigyUrl : dicebearUrl}
src={effigyActive ? effigyUrl : dicebearUrl}
alt={`Avatar for ${address}`}
width={size}
height={size}
style={{ borderRadius: rounded ? '50%' : '5px' }}
onError={() => setUseEffigy(false)}
onError={() => setEffigyErrorAddress(address)}
/>
</div>
);
Expand Down
21 changes: 7 additions & 14 deletions src/data-sources/morpho-api/market.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,24 +84,17 @@ export const fetchMorphoMarkets = async (network: SupportedNetworks): Promise<Ma

const response = await morphoGraphqlFetcher<MarketsGraphQLResponse>(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);
Expand Down
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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<string | null>(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);
Comment thread
coderabbitai[bot] marked this conversation as resolved.

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) {
Expand Down Expand Up @@ -96,8 +120,8 @@ export function EditMetadata({
</label>
<Input
size="sm"
value={nameInput}
onChange={(event) => setNameInput(event.target.value)}
value={computedNameInput}
onChange={(event) => handleNameChange(event.target.value)}
placeholder={defaultName}
disabled={!isOwner}
id={nameInputId}
Expand All @@ -117,8 +141,8 @@ export function EditMetadata({
</label>
<Input
size="sm"
value={symbolInput}
onChange={(event) => setSymbolInput(event.target.value)}
value={computedSymbolInput}
onChange={(event) => handleSymbolChange(event.target.value)}
placeholder={defaultSymbol}
maxLength={16}
disabled={!isOwner}
Expand Down
120 changes: 62 additions & 58 deletions src/features/market-detail/components/charts/borrowers-pie-chart.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
<div className="rounded-lg border border-border bg-background p-3 shadow-lg">
<p className="mb-1 font-medium text-sm">{data.name}</p>
{!data.isOther && <p className="mb-2 font-mono text-xs text-secondary">{getSlicedAddress(data.address as `0x${string}`)}</p>}
<div className="space-y-1">
<div className="flex items-center justify-between gap-4 text-sm">
<span className="text-secondary">Borrowed</span>
<div className="flex items-center gap-1 tabular-nums">
<span>{formatSimple(data.value)}</span>
<TokenIcon
address={market.loanAsset.address}
chainId={market.morphoBlue.chain.id}
symbol={market.loanAsset.symbol}
width={14}
height={14}
/>
</div>
</div>
<div className="flex items-center justify-between gap-4 text-sm">
<span className="text-secondary">Collateral</span>
<div className="flex items-center gap-1 tabular-nums">
<span>{formatSimple(data.collateral)}</span>
</div>
</div>
{!data.isOther && (
<div className="flex items-center justify-between gap-4 text-sm">
<span className="text-secondary">LTV</span>
<span className="tabular-nums">{data.ltv.toFixed(2)}%</span>
</div>
)}
<div className="flex items-center justify-between gap-4 text-sm">
<span className="text-secondary">% of Borrow</span>
<span className="tabular-nums">{formatPercentDisplay(data.percentage)}</span>
</div>
</div>
{data.isOther && <p className="mt-2 text-xs text-secondary">Click to {expandedOther ? 'collapse' : 'expand'}</p>}
</div>
);
}

export function BorrowersPieChart({ chainId, market, oraclePrice }: BorrowersPieChartProps) {
const { data: borrowers, isLoading, totalCount } = useAllMarketBorrowers(market.uniqueKey, chainId);
const { getVaultByAddress } = useVaultRegistry();
Expand Down Expand Up @@ -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 (
<div className="rounded-lg border border-border bg-background p-3 shadow-lg">
<p className="mb-1 font-medium text-sm">{data.name}</p>
{!data.isOther && <p className="mb-2 font-mono text-xs text-secondary">{getSlicedAddress(data.address as `0x${string}`)}</p>}
<div className="space-y-1">
<div className="flex items-center justify-between gap-4 text-sm">
<span className="text-secondary">Borrowed</span>
<div className="flex items-center gap-1 tabular-nums">
<span>{formatSimple(data.value)}</span>
<TokenIcon
address={market.loanAsset.address}
chainId={market.morphoBlue.chain.id}
symbol={market.loanAsset.symbol}
width={14}
height={14}
/>
</div>
</div>
<div className="flex items-center justify-between gap-4 text-sm">
<span className="text-secondary">Collateral</span>
<div className="flex items-center gap-1 tabular-nums">
<span>{formatSimple(data.collateral)}</span>
<TokenIcon
address={market.collateralAsset.address}
chainId={market.morphoBlue.chain.id}
symbol={market.collateralAsset.symbol}
width={14}
height={14}
/>
</div>
</div>
{!data.isOther && (
<div className="flex items-center justify-between gap-4 text-sm">
<span className="text-secondary">LTV</span>
<span className="tabular-nums">{data.ltv.toFixed(2)}%</span>
</div>
)}
<div className="flex items-center justify-between gap-4 text-sm">
<span className="text-secondary">% of Borrow</span>
<span className="tabular-nums">{formatPercentDisplay(data.percentage)}</span>
</div>
</div>
{data.isOther && <p className="mt-2 text-xs text-secondary">Click to {expandedOther ? 'collapse' : 'expand'}</p>}
</div>
);
};

if (isLoading) {
return (
<Card className="flex h-[350px] items-center justify-center border border-border bg-surface">
Expand Down Expand Up @@ -253,7 +257,7 @@ export function BorrowersPieChart({ chainId, market, oraclePrice }: BorrowersPie
/>
))}
</Pie>
<Tooltip content={<CustomTooltip />} />
<Tooltip content={<BorrowersPieTooltip expandedOther={expandedOther} market={market} />} />
<Legend
layout="vertical"
align="right"
Expand Down
Loading