11import { commands , events , NftRecord } from '@/bindings' ;
22import { useErrors } from '@/hooks/useErrors' ;
33import { nftUri } from '@/lib/nftUri' ;
4- import { addressInfo , isValidAddress } from '@/lib/utils' ;
4+ import { isValidAddress } from '@/lib/utils' ;
55import { t } from '@lingui/core/macro' ;
6- import { useCallback , useEffect , useMemo , useState } from 'react' ;
6+ import { useCallback , useEffect , useMemo , useRef , useState } from 'react' ;
77import { Input } from '../ui/input' ;
88import { 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