Skip to content

Commit 17ce46a

Browse files
committed
Improve search output
1 parent b79060f commit 17ce46a

File tree

8 files changed

+146
-47
lines changed

8 files changed

+146
-47
lines changed

packages/app/src/discovery/engine.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -97,10 +97,10 @@ impl Discovery for DiscoveryEngine {
9797
async fn query_search(&self, service: &ENSService, query: String) -> Result<Vec<Profile>, ()> {
9898
let index = self.client.index("profiles");
9999

100-
// Create search with query and limit to 5 results
100+
// Create search with query and limit to 12 results
101101
let search = index.search()
102102
.with_query(&query)
103-
.with_limit(5)
103+
.with_limit(12)
104104
.build();
105105

106106
// Execute the search

packages/app/src/routes/universal.rs

Lines changed: 16 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -50,31 +50,33 @@ pub async fn get(
5050
Query(query): Query<FreshQuery>,
5151
State(state): State<Arc<crate::AppState>>,
5252
) -> Result<Json<Profile>, RouteError> {
53-
get_bulk(
53+
let response = get_bulk(
5454
Qs(UniversalGetBulkQuery {
5555
fresh: query,
5656
queries: vec![name_or_address],
5757
}),
5858
State(state.clone()),
5959
)
60-
.await
61-
.map(|mut res| {
62-
63-
// TODO: +1 on cache hit popularity discover
64-
for profile in &res.response {
65-
if let BulkResponse::Ok(profile) = profile {
66-
let profile = profile.clone();
67-
let _ = state.service.cache.cache_hit(&profile.name);
68-
if let Some(discovery) = &state.service.discovery {
69-
let _ = discovery.discover_name(&profile);
60+
.await;
61+
62+
match response {
63+
Ok(mut res) => {
64+
for profile in &res.response {
65+
if let BulkResponse::Ok(profile) = profile {
66+
let profile = profile.clone();
67+
let _ = state.service.cache.cache_hit(&profile.name).await;
68+
if let Some(discovery) = &state.service.discovery {
69+
let _ = discovery.discover_name(&profile).await;
70+
}
7071
}
7172
}
72-
}
7373

74-
Result::<_, _>::from(res.0.response.remove(0))
74+
Result::<_, _>::from(res.response.remove(0))
7575
.map(Json)
7676
.map_err(RouteError::from)
77-
})?
77+
}
78+
Err(e) => Err(e),
79+
}
7880
}
7981

8082
#[derive(Deserialize, IntoParams, ToSchema)]
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import { useState } from 'react';
2+
import { LuWallet } from 'react-icons/lu';
3+
4+
interface ChainIconProps {
5+
chain: string;
6+
iconUrl: string;
7+
size?: 'sm' | 'md';
8+
className?: string;
9+
}
10+
11+
export function ChainIcon({ chain, iconUrl, size = 'sm', className = '' }: ChainIconProps) {
12+
const [showFallback, setShowFallback] = useState(false);
13+
14+
const iconSize = size === 'sm' ? 16 : 20;
15+
const containerClasses = `text-gray-400 ${size === 'sm' ? 'h-4 w-4' : 'h-5 w-5'} ${className}`;
16+
17+
if (showFallback) {
18+
return (
19+
<span className={containerClasses}>
20+
<LuWallet size={iconSize} />
21+
</span>
22+
);
23+
}
24+
25+
return (
26+
<img
27+
src={iconUrl}
28+
alt={`${chain} icon`}
29+
className={containerClasses}
30+
onError={() => setShowFallback(true)}
31+
/>
32+
);
33+
}

packages/search/src/hooks/useSearch.ts

Lines changed: 3 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -24,16 +24,7 @@ export interface SearchResult {
2424
pronouns?: string;
2525
[key: string]: string | undefined;
2626
};
27-
chains?: {
28-
eth?: string;
29-
btc?: string;
30-
sol?: string;
31-
arbitrum?: string;
32-
optimism?: string;
33-
polygon?: string;
34-
ltc?: string;
35-
[key: string]: string | undefined;
36-
};
27+
chains?: Record<string, string>;
3728
fresh?: number;
3829
resolver?: string;
3930
ccip_urls?: string[];
@@ -56,6 +47,6 @@ export const useSearch = (searchTerm: string) => {
5647
return response.data;
5748
},
5849
enabled: Boolean(searchTerm.trim()),
59-
staleTime: 1000 * 60 * 5, // 5 minutes
50+
staleTime: 1000 * 10, // 10 seconds
6051
});
61-
};
52+
};

packages/search/src/routes/$profileId.lazy.tsx

Lines changed: 20 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,12 @@ import {
1010
LuSend,
1111
LuCopy,
1212
LuCalendar,
13-
LuUser
13+
LuUser,
14+
LuWallet
1415
} from "react-icons/lu";
1516
import { useState } from 'react';
17+
import { getChainIconUrl } from '../utils/chainIcons';
18+
import { ChainIcon } from '../components/ChainIcon';
1619

1720
export const Route = createLazyFileRoute('/$profileId')({
1821
component: Profile,
@@ -170,13 +173,15 @@ function Profile() {
170173
<dt className="text-sm font-medium text-gray-500">Ethereum Address</dt>
171174
<dd className="mt-1 text-sm text-gray-900 flex items-center">
172175
<span className="truncate max-w-xs">{profile.address}</span>
173-
<button
174-
onClick={() => copyToClipboard(profile.address, 'address')}
175-
className="ml-2 text-gray-400 hover:text-gray-600"
176-
title="Copy address"
177-
>
178-
<LuCopy className="h-4 w-4" />
179-
</button>
176+
{profile.address && (
177+
<button
178+
onClick={() => copyToClipboard(profile.address!, 'address')}
179+
className="ml-2 text-gray-400 hover:text-gray-600"
180+
title="Copy address"
181+
>
182+
<LuCopy className="h-4 w-4" />
183+
</button>
184+
)}
180185
</dd>
181186
</div>
182187
</div>
@@ -360,14 +365,16 @@ function Profile() {
360365
{Object.entries(profile.chains).map(([chain, address]) => (
361366
<div key={chain} className="flex items-start">
362367
<div className="mt-1 flex-shrink-0 text-gray-400">
363-
<svg className="h-5 w-5" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
364-
<path d="M3 15V9M8 5L16 5V19L8 19M21 15V9" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
365-
</svg>
368+
<ChainIcon
369+
chain={chain}
370+
iconUrl={getChainIconUrl(chain)}
371+
size="md"
372+
/>
366373
</div>
367374
<div className="ml-3">
368375
<dt className="text-sm font-medium text-gray-500">{chain.toUpperCase()}</dt>
369-
<dd className="mt-1 text-sm text-gray-900 flex items-center break-all">
370-
<span className="font-mono">{address as string}</span>
376+
<dd className="mt-1 text-sm text-gray-900 flex items-center">
377+
<span className="font-mono">{address}</span>
371378
<button
372379
onClick={() => copyToClipboard(address as string, `${chain} address`)}
373380
className="ml-2 text-gray-400 hover:text-gray-600"

packages/search/src/routes/index.lazy.tsx

Lines changed: 37 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,11 @@ import { useState, useEffect, useRef } from 'react';
33
import { useSearch } from '../hooks/useSearch';
44
import { useProfile } from '../hooks/useProfile';
55
import { Link } from '@tanstack/react-router';
6-
import { LuSearch, LuMapPin, LuMail, LuGlobe, LuTwitter, LuGithub, LuMessageSquare, LuSend } from "react-icons/lu";
6+
import { LuSearch, LuMapPin, LuMail, LuGlobe, LuTwitter, LuGithub, LuMessageSquare, LuSend, LuWallet } from "react-icons/lu";
77
import { useDebounce } from 'use-debounce';
8+
import { getChainIconUrl } from '../utils/chainIcons';
9+
import { shouldAttemptDirectLookup } from '../utils/validation';
10+
import { ChainIcon } from '../components/ChainIcon';
811

912
export const Route = createLazyFileRoute('/')({
1013
component: Home,
@@ -172,7 +175,7 @@ function Home() {
172175

173176
{/* Profile information with avatar */}
174177
<div className="p-2">
175-
<div className="flex items-start space-x-2">
178+
<div className="flex items-start space-x-2 pb-3">
176179
{/* Avatar */}
177180
<div className={`${profile.header || profile.records?.header ? '-mt-7' : ''} flex-shrink-0`}>
178181
{getProfilePicture(profile) ? (
@@ -200,6 +203,22 @@ function Home() {
200203
{getDescription(profile)}
201204
</p>
202205

206+
{/* Chain addresses */}
207+
{profile.chains && Object.keys(profile.chains).length > 0 && (
208+
<div className="mt-1.5 flex flex-wrap gap-x-2 gap-y-1">
209+
{Object.entries(profile.chains).map(([chain, address]) => (
210+
<div key={chain} className="flex items-center text-xs text-gray-500" title={`${chain.toUpperCase()}: ${address}`}>
211+
<ChainIcon
212+
chain={chain}
213+
iconUrl={getChainIconUrl(chain)}
214+
className="mr-1"
215+
/>
216+
<span className="truncate max-w-[100px]">{address}</span>
217+
</div>
218+
))}
219+
</div>
220+
)}
221+
203222
{/* Profile metadata - making it more compact */}
204223
<div className="mt-1.5 flex flex-wrap gap-x-3 gap-y-1 text-xs text-gray-500">
205224
{profile.records?.location && (
@@ -256,12 +275,21 @@ function Home() {
256275
);
257276
}
258277

259-
// Case 2: No results found for an active search - try direct profile lookup
260-
if (debouncedSearchTerm && !isInitialLoading && !isLoading) {
278+
// Case 2: No results found for an active search - try direct profile lookup only if it looks like an ENS name or address
279+
if (debouncedSearchTerm && !isInitialLoading && !isLoading && shouldAttemptDirectLookup(debouncedSearchTerm)) {
261280
return <ProfileFallback searchTerm={debouncedSearchTerm} />;
262281
}
263282

264-
// Case 3: Initial loading with no previous data
283+
// Case 3: No results found and not a valid ENS name/address format
284+
if (debouncedSearchTerm && !isInitialLoading && !isLoading) {
285+
return (
286+
<div className="p-6 text-center">
287+
<p className="text-gray-500">No results found for "{debouncedSearchTerm}"</p>
288+
</div>
289+
);
290+
}
291+
292+
// Case 4: Initial loading with no previous data
265293
if (isInitialLoading) {
266294
return (
267295
<div className="text-center p-6">
@@ -273,10 +301,13 @@ function Home() {
273301
);
274302
}
275303

276-
// Case 4: Default state - no search input yet
304+
// Case 5: Default state - no search input yet
277305
return (
278306
<div className="p-6 text-center">
279307
<p className="text-gray-500">Enter a search term to find profiles</p>
308+
<p className="text-sm text-gray-400 mt-2">
309+
Try searching for an ENS name (e.g., "vitalik.eth") or an Ethereum address
310+
</p>
280311
</div>
281312
);
282313
};
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
export const getChainIconUrl = (chain: string): string => {
2+
// Convert chain name to lowercase for consistency
3+
const chainName = chain.toLowerCase();
4+
5+
// Map eth to ethereum for icon URL
6+
const normalizedChain = chainName === 'eth' ? 'ethereum' : chainName;
7+
8+
return `https://frame.nyc3.cdn.digitaloceanspaces.com/icons/${normalizedChain}.svg`;
9+
};
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
/**
2+
* Validates if a string could be an ENS name
3+
* Basic check: must contain a dot and have characters on both sides
4+
* More detailed spec: https://docs.ens.domains/contract-api-reference/name-processing
5+
*/
6+
export const isValidENSNameFormat = (name: string): boolean => {
7+
// Basic check for dot and characters on both sides
8+
// This regex allows for UTF-8 characters, numbers, and hyphens
9+
// but requires at least one character on each side of a dot
10+
return /^[^\s.]+\.[^\s.]+$/.test(name);
11+
};
12+
13+
/**
14+
* Validates if a string matches Ethereum address format
15+
* Must be 0x followed by 40 hex characters
16+
*/
17+
export const isValidEthereumAddress = (address: string): boolean => {
18+
return /^0x[a-fA-F0-9]{40}$/.test(address);
19+
};
20+
21+
/**
22+
* Determines if a search term should trigger a direct profile lookup
23+
*/
24+
export const shouldAttemptDirectLookup = (searchTerm: string): boolean => {
25+
return isValidENSNameFormat(searchTerm) || isValidEthereumAddress(searchTerm);
26+
};

0 commit comments

Comments
 (0)