Skip to content

Commit b62c2b2

Browse files
authored
Merge pull request #733 from xch-dev/shadcn-selector
cherry pick - rewrite asset selectors with shadcn Command component
2 parents 23ab79c + 92d86ba commit b62c2b2

File tree

10 files changed

+651
-398
lines changed

10 files changed

+651
-398
lines changed

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@
5858
"bs58": "^6.0.0",
5959
"class-variance-authority": "^0.7.1",
6060
"clsx": "^2.1.1",
61+
"cmdk": "^1.1.1",
6162
"emoji-mart": "^5.6.0",
6263
"framer-motion": "^12.33.0",
6364
"lucide-react": "^0.445.0",

pnpm-lock.yaml

Lines changed: 21 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/components/selectors/AssetSelector.tsx

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,6 @@ export function AssetSelector({
9999
checkAssetsInOffers();
100100
}, [offering, assets.nfts, assets.options]);
101101

102-
// Generate unique IDs for new items
103102
const generateId = useCallback(() => Date.now() + Math.random(), []);
104103

105104
const addToken = () => {
@@ -260,7 +259,7 @@ export function AssetSelector({
260259
<div className='flex flex-grow-0'>
261260
<TokenAmountInput
262261
id={`${prefix}-cat-${i}-amount`}
263-
className='!border-l-0 z-10 !rounded-l-none !rounded-r-none w-[150px] h-12'
262+
className='!border-l-0 z-10 !rounded-l-none !rounded-r-none h-12'
264263
placeholder={t`Amount`}
265264
value={amount}
266265
onValueChange={(values) =>
@@ -274,7 +273,7 @@ export function AssetSelector({
274273
<TooltipTrigger asChild>
275274
<Button
276275
variant='outline'
277-
className='!border-l-0 !rounded-none h-12 px-2 text-xs'
276+
className='!border-l-0 !rounded-none h-12 pl-1.5 pr-1 text-xs'
278277
onClick={() => setMaxTokenAmount(i, assetId)}
279278
>
280279
<ArrowUpToLine className='h-3 w-3 mr-1' />

src/components/selectors/DropdownSelector.tsx

Lines changed: 0 additions & 140 deletions
This file was deleted.

src/components/selectors/NftSelector.tsx

Lines changed: 81 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,8 @@ import { useErrors } from '@/hooks/useErrors';
33
import { nftUri } from '@/lib/nftUri';
44
import { isValidAddress } from '@/lib/utils';
55
import { t } from '@lingui/core/macro';
6-
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
7-
import { Input } from '../ui/input';
8-
import { DropdownSelector } from './DropdownSelector';
6+
import { useCallback, useEffect, useMemo, useState } from 'react';
7+
import { SearchableSelect } from './SearchableSelect';
98

109
export interface NftSelectorProps {
1110
value: string | null;
@@ -29,17 +28,9 @@ export function NftSelector({
2928
{},
3029
);
3130
const [searchTerm, setSearchTerm] = useState('');
32-
const inputRef = useRef<HTMLInputElement>(null);
3331

3432
const pageSize = 8;
3533

36-
// Restore focus after NFT list updates
37-
useEffect(() => {
38-
if (searchTerm && inputRef.current) {
39-
inputRef.current.focus();
40-
}
41-
}, [nfts, searchTerm]);
42-
4334
const isValidNftId = useMemo(() => {
4435
return isValidAddress(searchTerm, 'nft');
4536
}, [searchTerm]);
@@ -129,58 +120,88 @@ export function NftSelector({
129120

130121
const defaultNftImage = nftUri(null, null);
131122

132-
return (
133-
<DropdownSelector
134-
loadedItems={pageNftIds}
135-
page={page}
136-
setPage={setPage}
137-
value={value || undefined}
138-
setValue={(nftId) => {
123+
const handleSelect = useCallback(
124+
(nftId: string | null) => {
125+
if (nftId) {
139126
onChange(nftId);
140-
// Only clear search term if it's not a valid NFT ID (i.e., user clicked on an item from the list)
141-
if (!isValidAddress(searchTerm, 'nft')) {
142-
setSearchTerm('');
143-
}
144-
}}
145-
isDisabled={(nft) => disabled.includes(nft)}
146-
className={className}
147-
manualInput={
148-
<Input
149-
ref={inputRef}
150-
placeholder={t`Search by name or enter NFT ID`}
151-
value={searchTerm}
152-
onChange={(e) => {
153-
const newValue = e.target.value;
154-
setSearchTerm(newValue);
155-
156-
if (isValidAddress(newValue, 'nft')) {
157-
onChange(newValue);
158-
}
159-
}}
160-
/>
161127
}
162-
renderItem={(nftId) => (
163-
<div className='flex items-center gap-2 w-full'>
164-
<img
165-
src={nftThumbnails[nftId] ?? defaultNftImage}
166-
className='w-10 h-10 rounded object-cover'
167-
alt=''
168-
aria-hidden='true'
169-
loading='lazy'
170-
/>
171-
<div className='flex flex-col truncate'>
172-
<span className='flex-grow truncate' role='text'>
173-
{nfts[nftId]?.name ?? 'Unknown NFT'}
174-
</span>
175-
<span
176-
className='text-xs text-muted-foreground truncate'
177-
aria-label='NFT ID'
178-
>
179-
{nftId}
180-
</span>
181-
</div>
128+
},
129+
[onChange],
130+
);
131+
132+
const handleManualInput = useCallback(
133+
(nftId: string) => {
134+
onChange(nftId);
135+
},
136+
[onChange],
137+
);
138+
139+
const handleSearchChange = useCallback(
140+
(search: string) => {
141+
setSearchTerm(search);
142+
// Reset to first page when search changes
143+
if (page !== 0) {
144+
setPage(0);
145+
}
146+
},
147+
[page],
148+
);
149+
150+
// Get the NFT records for the current page
151+
const nftItems = useMemo(() => {
152+
return pageNftIds.map((id) => nfts[id]).filter(Boolean) as NftRecord[];
153+
}, [pageNftIds, nfts]);
154+
155+
const renderNft = useCallback(
156+
(nft: NftRecord) => (
157+
<div className='flex items-center gap-2 min-w-0'>
158+
<img
159+
src={nftThumbnails[nft.launcher_id] ?? defaultNftImage}
160+
className='w-10 h-10 rounded object-cover flex-shrink-0'
161+
alt=''
162+
aria-hidden='true'
163+
loading='lazy'
164+
/>
165+
<div className='flex flex-col min-w-0'>
166+
<span className='truncate' role='text'>
167+
{nft.name ?? 'Unknown NFT'}
168+
</span>
169+
<span
170+
className='text-xs text-muted-foreground truncate'
171+
aria-label='NFT ID'
172+
>
173+
{nft.launcher_id}
174+
</span>
182175
</div>
183-
)}
176+
</div>
177+
),
178+
[nftThumbnails, defaultNftImage],
179+
);
180+
181+
const validateNftId = useCallback((value: string) => {
182+
return isValidAddress(value, 'nft');
183+
}, []);
184+
185+
return (
186+
<SearchableSelect
187+
value={value || undefined}
188+
onSelect={handleSelect}
189+
items={nftItems}
190+
getItemId={(nft) => nft.launcher_id}
191+
renderItem={renderNft}
192+
onSearchChange={handleSearchChange}
193+
shouldFilter={false}
194+
validateManualInput={validateNftId}
195+
onManualInput={handleManualInput}
196+
page={page}
197+
onPageChange={setPage}
198+
pageSize={pageSize}
199+
hasMorePages={pageNftIds.length >= pageSize}
200+
disabled={disabled}
201+
className={className}
202+
placeholder={t`Select NFT`}
203+
searchPlaceholder={t`Search by name or enter NFT ID`}
204+
emptyMessage={t`No NFTs found.`}
184205
/>
185206
);
186207
}

0 commit comments

Comments
 (0)