Skip to content

Commit 14cc036

Browse files
authored
Merge pull request #676 from xch-dev/fix-offers
Fix dropdown selectors
2 parents b223aa5 + 3c67d0d commit 14cc036

File tree

5 files changed

+250
-306
lines changed

5 files changed

+250
-306
lines changed

src/components/selectors/DropdownSelector.tsx

Lines changed: 23 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import { t } from '@lingui/core/macro';
22
import { ChevronLeft, ChevronRight } from 'lucide-react';
3-
import { useMemo } from 'react';
43
import { Button } from '../ui/button';
54
import {
65
Select,
@@ -10,41 +9,36 @@ import {
109
SelectValue,
1110
} from '../ui/select';
1211

13-
export interface DropdownSelectorProps<T> {
14-
loadedItems: T[];
12+
export interface DropdownSelectorProps {
13+
loadedItems: string[];
1514
page: number;
1615
setPage?: (page: number) => void;
17-
renderItem: (item: T) => React.ReactNode;
18-
isSelected: (item: T) => boolean;
19-
setSelected: (item: T) => void;
20-
isDisabled?: (item: T) => boolean;
16+
renderItem: (item: string) => React.ReactNode;
17+
value: string | undefined;
18+
setValue: (item: string) => void;
19+
isDisabled?: (item: string) => boolean;
2120
pageSize?: number;
2221
className?: string;
2322
manualInput?: React.ReactNode;
2423
}
2524

26-
export function DropdownSelector<T>({
25+
export function DropdownSelector({
2726
loadedItems,
2827
page,
2928
setPage,
3029
renderItem,
31-
isSelected,
32-
setSelected,
30+
value,
31+
setValue,
3332
isDisabled,
3433
pageSize = 8,
3534
className,
3635
manualInput,
37-
}: DropdownSelectorProps<T>) {
38-
const foundIndex = useMemo(
39-
() => loadedItems.findIndex((item) => isSelected(item)),
40-
[loadedItems, isSelected],
41-
);
42-
36+
}: DropdownSelectorProps) {
4337
return (
4438
<Select
45-
value={foundIndex === -1 ? undefined : foundIndex.toString()}
39+
value={value}
4640
onValueChange={(value) => {
47-
setSelected(loadedItems[parseInt(value, 10)]);
41+
setValue(value);
4842
}}
4943
>
5044
<SelectTrigger
@@ -53,7 +47,11 @@ export function DropdownSelector<T>({
5347
className={`flex h-12 max-w-full items-center justify-between rounded-md border border-input bg-input-background px-3 ring-offset-background truncate ${className ?? ''}`}
5448
>
5549
<div className='flex items-center h-full text-left truncate'>
56-
<SelectValue placeholder={t`Select asset`} />
50+
{value ? (
51+
renderItem(value)
52+
) : (
53+
<SelectValue placeholder={t`Select asset`} />
54+
)}
5755
</div>
5856
</SelectTrigger>
5957
<SelectContent
@@ -112,23 +110,17 @@ export function DropdownSelector<T>({
112110
No items available
113111
</div>
114112
) : (
115-
loadedItems.map((item, i) => {
113+
loadedItems.map((item) => {
116114
const disabled = isDisabled?.(item) ?? false;
117115
return (
118116
<SelectItem
119-
value={i.toString()}
120-
// eslint-disable-next-line react/no-array-index-key
121-
key={i}
117+
value={item}
118+
key={item}
122119
role='option'
123-
aria-selected={i === foundIndex}
120+
aria-selected={item === value}
124121
aria-disabled={disabled}
125-
className={`px-2 py-1.5 text-sm rounded-sm truncate ${
126-
disabled
127-
? 'opacity-50 cursor-not-allowed'
128-
: i === foundIndex
129-
? 'bg-accent cursor-pointer'
130-
: 'hover:bg-accent cursor-pointer'
131-
}`}
122+
disabled={disabled}
123+
className={'px-2 py-1.5 text-sm rounded-sm truncate'}
132124
>
133125
{renderItem(item)}
134126
</SelectItem>

src/components/selectors/NftSelector.tsx

Lines changed: 69 additions & 106 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
import { commands, events, NftRecord } from '@/bindings';
22
import { useErrors } from '@/hooks/useErrors';
33
import { nftUri } from '@/lib/nftUri';
4-
import { addressInfo, isValidAddress } from '@/lib/utils';
4+
import { isValidAddress } from '@/lib/utils';
55
import { t } from '@lingui/core/macro';
6-
import { useCallback, useEffect, useMemo, useState } from 'react';
6+
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
77
import { Input } from '../ui/input';
88
import { DropdownSelector } from './DropdownSelector';
99

@@ -23,87 +23,81 @@ export function NftSelector({
2323
const { addError } = useErrors();
2424

2525
const [page, setPage] = useState(0);
26-
const [nfts, setNfts] = useState<NftRecord[]>([]);
27-
const [selectedNft, setSelectedNft] = useState<NftRecord | null>(null);
26+
const [nfts, setNfts] = useState<Record<string, NftRecord>>({});
27+
const [pageNftIds, setPageNftIds] = useState<string[]>([]);
2828
const [nftThumbnails, setNftThumbnails] = useState<Record<string, string>>(
2929
{},
3030
);
3131
const [searchTerm, setSearchTerm] = useState('');
32+
const inputRef = useRef<HTMLInputElement>(null);
3233

3334
const pageSize = 8;
3435

35-
// Initialize searchTerm when value is provided
36+
// Restore focus after NFT list updates
3637
useEffect(() => {
37-
if (value && value !== '' && !searchTerm) {
38-
setSearchTerm(value);
38+
if (searchTerm && inputRef.current) {
39+
inputRef.current.focus();
3940
}
40-
}, [value, searchTerm]);
41+
}, [nfts, searchTerm]);
4142

4243
const isValidNftId = useMemo(() => {
4344
return isValidAddress(searchTerm, 'nft');
4445
}, [searchTerm]);
4546

4647
useEffect(() => {
47-
// If we have a valid NFT ID, only fetch that specific NFT
48-
if (isValidNftId && searchTerm) {
49-
commands
50-
.getNft({ nft_id: searchTerm })
51-
.then((nftData) => {
52-
if (nftData.nft) {
53-
setNfts([nftData.nft]);
54-
} else {
55-
setNfts([]);
56-
}
57-
})
58-
.catch(() => {
59-
setNfts([]);
60-
});
61-
return;
62-
}
48+
const fetchNfts = async () => {
49+
const nfts: Record<string, NftRecord> = {};
50+
51+
if (value) {
52+
await commands
53+
.getNft({ nft_id: value })
54+
.then(({ nft }) => {
55+
if (nft) nfts[nft.launcher_id] = nft;
56+
})
57+
.catch(() => null);
58+
}
59+
60+
// If we have a valid NFT ID, only fetch that specific NFT
61+
if (isValidNftId && searchTerm) {
62+
await commands
63+
.getNft({ nft_id: searchTerm })
64+
.then(({ nft }) => {
65+
if (nft) {
66+
nfts[nft.launcher_id] = nft;
67+
setPageNftIds([nft.launcher_id]);
68+
}
69+
})
70+
.catch(() => null);
71+
} else {
72+
// Otherwise, fetch NFTs based on search term
73+
await commands
74+
.getNfts({
75+
name: searchTerm || null,
76+
offset: page * pageSize,
77+
limit: pageSize,
78+
include_hidden: false,
79+
sort_mode: 'name',
80+
collection_id: null,
81+
owner_did_id: null,
82+
minter_did_id: null,
83+
})
84+
.then((data) => {
85+
for (const nft of data.nfts) {
86+
nfts[nft.launcher_id] = nft;
87+
}
88+
setPageNftIds(data.nfts.map((nft) => nft.launcher_id));
89+
})
90+
.catch(addError);
91+
}
6392

64-
// Otherwise, fetch NFTs based on search term
65-
commands
66-
.getNfts({
67-
name: searchTerm || null,
68-
offset: page * pageSize,
69-
limit: pageSize,
70-
include_hidden: false,
71-
sort_mode: 'name',
72-
collection_id: null,
73-
owner_did_id: null,
74-
minter_did_id: null,
75-
})
76-
.then((data) => setNfts(data.nfts))
77-
.catch(addError);
78-
}, [addError, page, searchTerm, isValidNftId]);
93+
setNfts(nfts);
94+
};
7995

80-
useEffect(() => {
81-
if (isValidNftId) {
82-
commands
83-
.getNft({ nft_id: searchTerm })
84-
.then((data) => {
85-
setSelectedNft(data.nft);
86-
// onChange is already called in the input handler, don't call it again
87-
})
88-
.catch(addError);
89-
}
90-
}, [isValidNftId, searchTerm, addError]);
96+
fetchNfts();
97+
}, [addError, page, searchTerm, isValidNftId, value]);
9198

9299
const updateThumbnails = useCallback(async () => {
93-
const nftsToFetch = [...nfts.map((nft) => nft.launcher_id)];
94-
if (
95-
value &&
96-
value !== '' &&
97-
!nfts.find((nft) => nft.launcher_id === value)
98-
) {
99-
try {
100-
if (addressInfo(value).puzzleHash.length === 64) {
101-
nftsToFetch.push(value);
102-
}
103-
} catch {
104-
// The checksum failed
105-
}
106-
}
100+
const nftsToFetch = Object.keys(nfts);
107101

108102
return await Promise.all(
109103
nftsToFetch.map((nftId) =>
@@ -118,7 +112,7 @@ export function NftSelector({
118112
});
119113
setNftThumbnails(map);
120114
});
121-
}, [nfts, value]);
115+
}, [nfts]);
122116

123117
useEffect(() => {
124118
updateThumbnails();
@@ -133,57 +127,26 @@ export function NftSelector({
133127
};
134128
}, [updateThumbnails]);
135129

136-
// Load NFT record when a value is provided but not found in current nfts list
137-
useEffect(() => {
138-
if (
139-
value &&
140-
value !== '' &&
141-
!nfts.find((nft) => nft.launcher_id === value) &&
142-
(!selectedNft || selectedNft.launcher_id !== value)
143-
) {
144-
try {
145-
// Validate the NFT ID format
146-
if (isValidAddress(value, 'nft')) {
147-
commands
148-
.getNft({ nft_id: value })
149-
.then((data) => {
150-
setSelectedNft(data.nft);
151-
})
152-
.catch(addError);
153-
}
154-
} catch {
155-
// Handle any errors silently
156-
}
157-
}
158-
}, [value, selectedNft, nfts, addError]);
159-
160-
// Reset selectedNft when value is null or empty
161-
useEffect(() => {
162-
if (!value || value === '') {
163-
setSelectedNft(null);
164-
}
165-
}, [value]);
166-
167130
const defaultNftImage = nftUri(null, null);
168131

169132
return (
170133
<DropdownSelector
171-
loadedItems={nfts}
134+
loadedItems={pageNftIds}
172135
page={page}
173136
setPage={setPage}
174-
isDisabled={(nft) => disabled.includes(nft.launcher_id)}
175-
isSelected={(nft) => nft.launcher_id === selectedNft?.launcher_id}
176-
setSelected={(nft) => {
177-
setSelectedNft(nft);
178-
onChange(nft.launcher_id);
137+
value={value || undefined}
138+
setValue={(nftId) => {
139+
onChange(nftId);
179140
// Only clear search term if it's not a valid NFT ID (i.e., user clicked on an item from the list)
180141
if (!isValidAddress(searchTerm, 'nft')) {
181142
setSearchTerm('');
182143
}
183144
}}
145+
isDisabled={(nft) => disabled.includes(nft)}
184146
className={className}
185147
manualInput={
186148
<Input
149+
ref={inputRef}
187150
placeholder={t`Search by name or enter NFT ID`}
188151
value={searchTerm}
189152
onChange={(e) => {
@@ -196,24 +159,24 @@ export function NftSelector({
196159
}}
197160
/>
198161
}
199-
renderItem={(nft) => (
162+
renderItem={(nftId) => (
200163
<div className='flex items-center gap-2 w-full'>
201164
<img
202-
src={nftThumbnails[nft.launcher_id] ?? defaultNftImage}
165+
src={nftThumbnails[nftId] ?? defaultNftImage}
203166
className='w-10 h-10 rounded object-cover'
204167
alt=''
205168
aria-hidden='true'
206169
loading='lazy'
207170
/>
208171
<div className='flex flex-col truncate'>
209172
<span className='flex-grow truncate' role='text'>
210-
{nft.name}
173+
{nfts[nftId]?.name ?? 'Unknown NFT'}
211174
</span>
212175
<span
213176
className='text-xs text-muted-foreground truncate'
214177
aria-label='NFT ID'
215178
>
216-
{nft.launcher_id}
179+
{nftId}
217180
</span>
218181
</div>
219182
</div>

0 commit comments

Comments
 (0)