Skip to content

Commit 78a5212

Browse files
committed
Enhance profile previews, API, and UI behavior
Several improvements across wallet/profile previews, the profile-preview API, and related UI interactions. These address the issues raised by CodeRabbit on PR 922. - Wallet preview components (apps/web, packages/feed-ui) now robustly parse responses (falling back to raw text), surface loading/error states, and include focus/keyboard accessibility (focus/blur handlers and tabIndex). Error messages now include HTTP status where available. - API (/api/profile-preview/[user]) gains request timeouts (AbortController), guarded fetch handling, allowed CORS origins, and resilient cache reads/writes. The handler aggregates per-chain results, returns partial results with failedChains when some chain calls fail, and returns a 502 if all chains fail. - UI/UX refinements: Holders list rows no longer wrap the entire row in an external link; an external icon link to Etherscan is added (with click propagation stopped). Creator display in CoinInfo shows an "Unknown creator" fallback. Several components use walletSnippet fallbacks for display names, and VotePlacard prevents click/pointer propagation when interacting with nested wallet preview UI. These changes improve reliability, error visibility, accessibility, and prevent unintended navigation when interacting with wallet/profile elements.
1 parent 67dc9c8 commit 78a5212

8 files changed

Lines changed: 222 additions & 111 deletions

File tree

apps/web/src/components/HoldersSection/HoldersList.tsx

Lines changed: 43 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { useEnsData } from '@buildeross/hooks'
33
import { useChainStore } from '@buildeross/stores'
44
import { Avatar } from '@buildeross/ui/Avatar'
55
import { formatCryptoVal, walletSnippet } from '@buildeross/utils'
6-
import { Box, Flex, Text } from '@buildeross/zord'
6+
import { Box, Flex, Icon, Text } from '@buildeross/zord'
77
import { formatEther } from 'viem'
88

99
import { WalletProfilePreview } from '../WalletProfilePreview'
@@ -59,49 +59,56 @@ const HolderItem = ({ address, balance, isDrop = false }: HolderItemProps) => {
5959
const chainId = chain.id
6060

6161
return (
62-
<a
63-
href={`${ETHERSCAN_BASE_URL[chainId]}/address/${address}`}
64-
target="_blank"
65-
rel="noreferrer"
62+
<Flex
63+
align="center"
64+
justify="space-between"
65+
gap="x3"
66+
py="x1"
67+
px="x2"
68+
borderRadius="curved"
69+
className={holderLink}
6670
>
67-
<Flex
68-
align="center"
69-
justify="space-between"
70-
gap="x3"
71-
py="x1"
72-
px="x2"
73-
borderRadius="curved"
74-
className={holderLink}
71+
<WalletProfilePreview
72+
address={address}
73+
displayName={displayName}
74+
avatarSrc={ensAvatar}
7575
>
76-
<WalletProfilePreview
77-
address={address}
78-
displayName={displayName}
79-
avatarSrc={ensAvatar}
80-
>
81-
<Flex align="center" gap="x2" flex={1} minWidth={0}>
82-
<Avatar address={address} src={ensAvatar} size="32" />
83-
<Box minWidth={0} flex={1}>
84-
<Text
85-
variant="paragraph-sm"
86-
fontWeight="display"
87-
style={{
88-
overflow: 'hidden',
89-
textOverflow: 'ellipsis',
90-
whiteSpace: 'nowrap',
91-
}}
92-
>
93-
{displayName || walletSnippet(address)}
94-
</Text>
95-
</Box>
96-
</Flex>
97-
</WalletProfilePreview>
76+
<Flex align="center" gap="x2" flex={1} minWidth={0}>
77+
<Avatar address={address} src={ensAvatar} size="32" />
78+
<Box minWidth={0} flex={1}>
79+
<Text
80+
variant="paragraph-sm"
81+
fontWeight="display"
82+
style={{
83+
overflow: 'hidden',
84+
textOverflow: 'ellipsis',
85+
whiteSpace: 'nowrap',
86+
}}
87+
>
88+
{displayName || walletSnippet(address)}
89+
</Text>
90+
</Box>
91+
</Flex>
92+
</WalletProfilePreview>
93+
<Flex align="center" gap="x2" flexShrink={0}>
9894
<Flex direction="column" align="flex-end" flexShrink={0}>
9995
<Text variant="paragraph-sm" fontWeight="display">
10096
{isDrop ? balance.toString() : formatBalance(balance)}
10197
</Text>
10298
</Flex>
99+
<a
100+
href={`${ETHERSCAN_BASE_URL[chainId]}/address/${address}`}
101+
target="_blank"
102+
rel="noreferrer"
103+
aria-label={`View ${address} on Etherscan`}
104+
onClick={(event) => event.stopPropagation()}
105+
>
106+
<Flex align="center" justify="center" color="text3">
107+
<Icon id="external-16" size="sm" />
108+
</Flex>
109+
</a>
103110
</Flex>
104-
</a>
111+
</Flex>
105112
)
106113
}
107114

apps/web/src/components/WalletProfilePreview.tsx

Lines changed: 33 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@ interface WalletProfilePreviewResponse {
2828
totalVotes: number
2929
totalProposals: number
3030
}
31+
partial?: boolean
32+
failedChains?: number[]
3133
}
3234

3335
const fetcher = async (url: string): Promise<WalletProfilePreviewResponse> => {
@@ -37,13 +39,22 @@ const fetcher = async (url: string): Promise<WalletProfilePreviewResponse> => {
3739
},
3840
})
3941

40-
const body = await response.json()
42+
const rawBody = await response.text()
43+
let body: Partial<WalletProfilePreviewResponse> & { error?: string } = {}
44+
45+
if (rawBody) {
46+
try {
47+
body = JSON.parse(rawBody)
48+
} catch {
49+
body = { error: rawBody }
50+
}
51+
}
4152

4253
if (!response.ok) {
43-
throw new Error(body?.error || 'Failed to load wallet preview')
54+
throw new Error(body?.error || `Failed to load wallet preview (${response.status})`)
4455
}
4556

46-
return body
57+
return body as WalletProfilePreviewResponse
4758
}
4859

4960
const compactAddress = (address: string) => `${address.slice(0, 6)}...${address.slice(-6)}`
@@ -145,8 +156,11 @@ export const WalletProfilePreview = ({
145156
style={{ minWidth: 0 }}
146157
onMouseEnter={isMobile ? undefined : handleOpen}
147158
onMouseLeave={isMobile ? undefined : handleClose}
159+
onFocusCapture={isMobile ? undefined : handleOpen}
160+
onBlurCapture={isMobile ? undefined : handleClose}
148161
onPointerDownCapture={handleTriggerPointerDown}
149162
onClickCapture={handleTriggerClick}
163+
tabIndex={inline ? undefined : 0}
150164
>
151165
{children}
152166
</Box>
@@ -231,9 +245,7 @@ export const WalletProfilePreview = ({
231245
)}
232246
</Box>
233247

234-
<Text
235-
variant="paragraph-sm"
236-
color="text3"
248+
<Box
237249
pt="x1"
238250
style={{
239251
borderTop: '1px solid rgba(0, 0, 0, 0.08)',
@@ -242,9 +254,21 @@ export const WalletProfilePreview = ({
242254
whiteSpace: 'nowrap',
243255
}}
244256
>
245-
{data?.stats.totalDaos ?? 0} DAOs, {data?.stats.totalProposals ?? 0} proposals,{' '}
246-
{data?.stats.totalVotes ?? 0} votes
247-
</Text>
257+
{isLoading ? (
258+
<Text variant="paragraph-sm" color="text3">
259+
Loading profile...
260+
</Text>
261+
) : error ? (
262+
<Text variant="paragraph-sm" color="text3">
263+
Profile preview unavailable
264+
</Text>
265+
) : data ? (
266+
<Text variant="paragraph-sm" color="text3">
267+
{data.stats.totalDaos} DAOs, {data.stats.totalProposals} proposals,{' '}
268+
{data.stats.totalVotes} votes
269+
</Text>
270+
) : null}
271+
</Box>
248272

249273
{isMobile ? (
250274
<Box mt="x2" pt="x2" style={{ borderTop: '1px solid rgba(0, 0, 0, 0.08)' }}>

apps/web/src/modules/coin/CoinDetail/CoinInfo.tsx

Lines changed: 16 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -248,18 +248,22 @@ export const CoinInfo = ({
248248
<Text variant="label-sm" color="text3" mb="x2">
249249
Created by
250250
</Text>
251-
<WalletProfilePreview
252-
address={creatorAddress as Address}
253-
displayName={creatorDisplayName}
254-
avatarSrc={creatorAvatar}
255-
>
256-
<Flex align="center">
257-
<Avatar address={creatorAddress as Address} src={creatorAvatar} size="28" />
258-
<Text fontWeight="display" ml="x2">
259-
{creatorDisplayName || walletSnippet(creatorAddress as Address)}
260-
</Text>
261-
</Flex>
262-
</WalletProfilePreview>
251+
{creatorAddress ? (
252+
<WalletProfilePreview
253+
address={creatorAddress}
254+
displayName={creatorDisplayName}
255+
avatarSrc={creatorAvatar}
256+
>
257+
<Flex align="center">
258+
<Avatar address={creatorAddress} src={creatorAvatar} size="28" />
259+
<Text fontWeight="display" ml="x2">
260+
{creatorDisplayName || walletSnippet(creatorAddress)}
261+
</Text>
262+
</Flex>
263+
</WalletProfilePreview>
264+
) : (
265+
<Text fontWeight="display">Unknown creator</Text>
266+
)}
263267
</Box>
264268
)}
265269

0 commit comments

Comments
 (0)