44'use client' ;
55
66import { Close , Search } from '@iota/apps-ui-icons' ;
7- import { Button , ButtonType , ButtonUnstyled , Input , InputType } from '@iota/apps-ui-kit' ;
7+ import {
8+ Button ,
9+ ButtonType ,
10+ ButtonUnstyled ,
11+ Input ,
12+ InputType ,
13+ LoadingIndicator ,
14+ } from '@iota/apps-ui-kit' ;
815import { ConnectButton , useCurrentWallet } from '@iota/dapp-kit' ;
16+ import { validateIotaName } from '@iota/iota-names-sdk' ;
917import { useCallback , useMemo , useState } from 'react' ;
1018
1119import { AuctionBidDialog } from '@/auctions/components/dialogs/AuctionBidDialog' ;
1220import { useGetAuctionMetadata } from '@/auctions/hooks/useGetAuctionMetadata' ;
13- import { useNameRecord , usePriceList } from '@/hooks' ;
14- import { formatNanosToIota } from '@/lib/utils' ;
15- import { denormalizeName } from '@/lib/utils/format/formatNames' ;
21+ import { isAuctionActive } from '@/auctions/lib/utils' ;
22+ import { NameRecordData , useNameRecord , usePriceList } from '@/hooks' ;
23+ import { denormalizeName , normalizeName } from '@/lib/utils/format/formatNames' ;
24+ import { formatNanosToIota } from '@/lib/utils/format/formatNanosToIota' ;
1625
1726import { PurchaseNameDialog } from '../dialogs/PurchaseNameDialog' ;
1827import { NamePurchaseCard } from '../NamePurchaseCard' ;
1928
20- function getValidationError (
21- name : string ,
22- minLength : number = 3 ,
23- maxLength : number = 64 ,
24- ) : string | null {
25- const IOTA_NAME_REGEX = / ^ [ a - z 0 - 9 ] (?: [ a - z 0 - 9 - ] * [ a - z 0 - 9 ] ) ? $ / i;
26- if ( ! name ) return null ;
27-
28- if ( name . includes ( '.' ) ) {
29- return 'No subnames allowed' ;
30- }
31- if ( ! IOTA_NAME_REGEX . test ( name ) ) {
32- return 'Invalid characters. Only a-z, 0-9, and hyphens (not at the beginning or end) are allowed' ;
33- }
34- if ( name . length < minLength || name . length > maxLength ) {
35- return `Name must be ${ minLength } -${ maxLength } characters long` ;
36- }
37- return null ;
38- }
39-
4029interface AvailabilityCheckProps {
4130 autoFocusInput ?: boolean ;
4231 onCompleted ?: ( ) => void ;
4332}
33+
4434export function AvailabilityCheck ( { autoFocusInput, onCompleted } : AvailabilityCheckProps ) {
45- const { isConnected } = useCurrentWallet ( ) ;
4635 const [ searchValue , setSearchValue ] = useState < string > ( '' ) ;
4736 const [ name , setName ] = useState < string > ( '' ) ;
48- const [ isPurchaseDialogOpen , setPurchaseDialogOpen ] = useState ( false ) ;
49- const [ isAuctionBidDialogOpen , setAuctionDialogOpen ] = useState ( false ) ;
50-
51- const { data : nameRecordData , error } = useNameRecord ( name ) ;
52- const { data : priceList } = usePriceList ( ) ;
5337
54- const { data : auctionMetadata , isLoading : isAuctionMetadataLoading } =
55- useGetAuctionMetadata ( name ) ;
56-
57- const isAvailable = nameRecordData ?. type === 'available' ;
58- const isUnavailable = nameRecordData ?. type === 'unavailable' ;
59- const isAuctionInProgress =
60- isUnavailable &&
61- auctionMetadata ?. endTimestamp &&
62- auctionMetadata . endTimestamp . getTime ( ) > Date . now ( ) ;
63-
64- // User can bid in existing auctions or if there is no auction and the name is not taken
65- const canBid = isAuctionInProgress || isAvailable ;
38+ const {
39+ data : auctionMetadata ,
40+ error : auctionError ,
41+ isLoading : isLoadingAuctionMetadat ,
42+ } = useGetAuctionMetadata ( name ) ;
43+ const {
44+ data : nameRecordData ,
45+ error : nameError ,
46+ isLoading : isLoadingNameRecord ,
47+ } = useNameRecord ( name ) ;
48+ const { data : priceList , error : priceError , isLoading : isLoadingPriceLst } = usePriceList ( ) ;
6649
6750 const validationError = useMemo (
68- ( ) => getValidationError ( searchValue , priceList ?. minLength , priceList ?. maxLength ) ,
51+ ( ) =>
52+ searchValue
53+ ? validateIotaName (
54+ `${ searchValue } .iota` ,
55+ priceList ?. minLength ,
56+ priceList ?. maxLength ,
57+ false ,
58+ )
59+ : null ,
6960 [ searchValue , priceList ] ,
7061 ) ;
7162
72- const errorMessage = error ?. message ?? validationError ?? '' ;
73- const enableSearch = Boolean ( searchValue ) && ! errorMessage ;
74-
7563 const handleSearch = useCallback ( ( ) => {
7664 if ( searchValue ) setName ( `${ searchValue } .iota` ) ;
7765 } , [ searchValue ] ) ;
@@ -83,31 +71,21 @@ export function AvailabilityCheck({ autoFocusInput, onCompleted }: AvailabilityC
8371 }
8472 }
8573
86- function handleBid ( ) {
87- setAuctionDialogOpen ( false ) ;
88- setSearchValue ( '' ) ;
89- setName ( '' ) ;
90- onCompleted ?.( ) ;
91- }
92-
93- function handlePurchase ( ) {
94- setPurchaseDialogOpen ( false ) ;
74+ function handleBidOrPurchase ( ) {
9575 setSearchValue ( '' ) ;
9676 setName ( '' ) ;
9777 onCompleted ?.( ) ;
9878 }
9979
100- const statusMessage =
101- isUnavailable && ! isAuctionInProgress
102- ? 'Name is already taken.'
103- : isAuctionInProgress
104- ? 'In auction'
105- : undefined ;
106- const purchasePrice = isAvailable ? nameRecordData . price : undefined ;
107- const bidPrice = auctionMetadata ?. minBidNanos || purchasePrice ;
108- const cleanName = denormalizeName ( name ) ;
80+ const errorMessage =
81+ auctionError ?. message || nameError ?. message || priceError ?. message || validationError || '' ;
82+ const isLoading = isLoadingAuctionMetadat || isLoadingNameRecord || isLoadingPriceLst ;
10983
110- const isAuctionLoading = name && ( ! nameRecordData || isAuctionMetadataLoading ) ;
84+ const normalizedName = normalizeName ( name ) ;
85+ const enableSearch = Boolean ( searchValue ) && ! errorMessage ;
86+ const isAuctionInProgress = auctionMetadata ? isAuctionActive ( auctionMetadata ) : false ;
87+ const isUnavailable = nameRecordData ?. type === 'unavailable' ;
88+ const isNameTaken = isUnavailable && ! isAuctionInProgress ;
11189
11290 const inputTrailingElement = (
11391 < div className = "flex flex-row gap-xs" >
@@ -131,14 +109,6 @@ export function AvailabilityCheck({ autoFocusInput, onCompleted }: AvailabilityC
131109
132110 return (
133111 < div className = "flex flex-col items-center w-full space-y-4" >
134- { isPurchaseDialogOpen && isAvailable && (
135- < PurchaseNameDialog
136- name = { name }
137- open = { isPurchaseDialogOpen }
138- setOpen = { setPurchaseDialogOpen }
139- onPurchase = { handlePurchase }
140- />
141- ) }
142112 < div className = "flex flex-col gap-2xl w-full max-w-[744px]" >
143113 < div className = "flex gap-x-sm items-baseline justify-center w-full" >
144114 < Input
@@ -155,70 +125,155 @@ export function AvailabilityCheck({ autoFocusInput, onCompleted }: AvailabilityC
155125 trailingElement = { inputTrailingElement }
156126 />
157127 </ div >
158- { nameRecordData && ! errorMessage && (
159- < div className = "flex flex-col items-center space-y-4 w-full" >
160- { ! isAuctionInProgress && (
161- < NamePurchaseCard
162- name = { cleanName }
163- isAvailable = { ! ! ( ! isUnavailable || isAuctionInProgress ) }
164- price = {
165- purchasePrice
166- ? formatNanosToIota ( purchasePrice , {
167- showIotaSymbol : false ,
168- } )
169- : undefined
170- }
171- priceSupportingText = { isAvailable ? 'Price' : undefined }
172- statusMessage = { statusMessage }
173- >
174- { isUnavailable ? null : isConnected ? (
175- < Button
176- type = { ButtonType . Secondary }
177- text = "Buy"
178- onClick = { ( ) => setPurchaseDialogOpen ( true ) }
179- />
180- ) : (
181- < ConnectButton connectText = "Connect" />
182- ) }
183- </ NamePurchaseCard >
184- ) }
185-
186- { isAuctionLoading ? (
187- < p > Loading...</ p >
188- ) : canBid ? (
189- < NamePurchaseCard
190- name = { cleanName }
191- isAvailable = { ! ! ( ! isUnavailable || isAuctionInProgress ) }
192- price = {
193- bidPrice
194- ? formatNanosToIota ( bidPrice , { showIotaSymbol : false } )
195- : undefined
196- }
197- priceSupportingText = "Minimum bid"
198- statusMessage = { statusMessage }
199- >
200- { isConnected ? (
201- < Button
202- type = { ButtonType . Primary }
203- text = "Bid"
204- onClick = { ( ) => setAuctionDialogOpen ( true ) }
205- />
206- ) : (
207- < ConnectButton connectText = "Connect" />
208- ) }
209- </ NamePurchaseCard >
210- ) : null }
211- </ div >
128+ < div className = "flex flex-col items-center space-y-4 w-full" >
129+ { isLoading ? (
130+ < LoadingIndicator />
131+ ) : isNameTaken ? (
132+ < NamePurchaseCard
133+ name = { normalizedName }
134+ isAvailable = { false }
135+ statusMessage = "Name is already taken."
136+ > </ NamePurchaseCard >
137+ ) : (
138+ nameRecordData && (
139+ < >
140+ < PurchaseName
141+ name = { name }
142+ nameRecordData = { nameRecordData }
143+ onCompleted = { handleBidOrPurchase }
144+ />
145+
146+ < BidName
147+ name = { name }
148+ nameRecordData = { nameRecordData }
149+ onCompleted = { handleBidOrPurchase }
150+ />
151+ </ >
152+ )
153+ ) }
154+ </ div >
155+ </ div >
156+ </ div >
157+ ) ;
158+ }
159+
160+ function BidName ( {
161+ name,
162+ nameRecordData,
163+ onCompleted,
164+ } : {
165+ name : string ;
166+ nameRecordData : NameRecordData ;
167+ onCompleted : ( ) => void ;
168+ } ) {
169+ const { isConnected } = useCurrentWallet ( ) ;
170+ const [ isAuctionBidDialogOpen , setAuctionDialogOpen ] = useState ( false ) ;
171+ const { data : auctionMetadata } = useGetAuctionMetadata ( name ) ;
172+
173+ const isAvailable = nameRecordData ?. type === 'available' ;
174+ const isUnavailable = nameRecordData ?. type === 'unavailable' ;
175+ const isAuctionInProgress = auctionMetadata ? isAuctionActive ( auctionMetadata ) : false ;
176+ const isAllowedToBid = isAvailable || ( isUnavailable && isAuctionInProgress ) || false ;
177+
178+ function handleBid ( ) {
179+ setAuctionDialogOpen ( false ) ;
180+ onCompleted ( ) ;
181+ }
182+
183+ const purchasePrice = nameRecordData ?. type === 'available' ? nameRecordData . price : undefined ;
184+ // If there is no auction yet, then we use the purchase price as minimum
185+ const bidPrice = auctionMetadata ?. minBidNanos || purchasePrice ;
186+ const formattedBidPrice = bidPrice
187+ ? formatNanosToIota ( bidPrice , { showIotaSymbol : false } )
188+ : undefined ;
189+ const normalizedName = normalizeName ( name ) ;
190+
191+ return (
192+ < >
193+ < NamePurchaseCard
194+ name = { normalizedName }
195+ isAvailable = { isAllowedToBid }
196+ price = { formattedBidPrice }
197+ priceSupportingText = "Minimum bid"
198+ statusMessage = { isAuctionInProgress ? 'In auction' : '' }
199+ >
200+ { isConnected ? (
201+ < Button
202+ type = { ButtonType . Primary }
203+ text = "Bid"
204+ onClick = { ( ) => setAuctionDialogOpen ( true ) }
205+ />
206+ ) : (
207+ < ConnectButton connectText = "Connect" />
212208 ) }
209+ </ NamePurchaseCard >
213210
214- { isAuctionBidDialogOpen && (
215- < AuctionBidDialog
216- name = { name }
217- closeDialog = { ( ) => setAuctionDialogOpen ( false ) }
218- onCompleted = { handleBid }
211+ { isAuctionBidDialogOpen && (
212+ < AuctionBidDialog
213+ name = { name }
214+ closeDialog = { ( ) => setAuctionDialogOpen ( false ) }
215+ onCompleted = { handleBid }
216+ />
217+ ) }
218+ </ >
219+ ) ;
220+ }
221+
222+ function PurchaseName ( {
223+ name,
224+ nameRecordData,
225+ onCompleted,
226+ } : {
227+ name : string ;
228+ nameRecordData : NameRecordData ;
229+ onCompleted : ( ) => void ;
230+ } ) {
231+ const { isConnected } = useCurrentWallet ( ) ;
232+ const [ isPurchaseDialogOpen , setPurchaseDialogOpen ] = useState ( false ) ;
233+
234+ const isAvailable = nameRecordData ?. type === 'available' ;
235+ const isUnavailable = nameRecordData ?. type === 'unavailable' ;
236+
237+ function handlePurchase ( ) {
238+ setPurchaseDialogOpen ( false ) ;
239+ onCompleted ( ) ;
240+ }
241+
242+ const purchasePrice = nameRecordData ?. type === 'available' ? nameRecordData . price : undefined ;
243+ const formattedPurchasePrice = purchasePrice
244+ ? formatNanosToIota ( purchasePrice , {
245+ showIotaSymbol : false ,
246+ } )
247+ : undefined ;
248+ const normalizedName = normalizeName ( name ) ;
249+
250+ return (
251+ < >
252+ < NamePurchaseCard
253+ name = { normalizedName }
254+ isAvailable = { isAvailable }
255+ price = { formattedPurchasePrice }
256+ priceSupportingText = { isAvailable ? 'Price' : undefined }
257+ >
258+ { isUnavailable ? null : isConnected ? (
259+ < Button
260+ type = { ButtonType . Secondary }
261+ text = "Buy"
262+ onClick = { ( ) => setPurchaseDialogOpen ( true ) }
219263 />
264+ ) : (
265+ < ConnectButton connectText = "Connect" />
220266 ) }
221- </ div >
222- </ div >
267+ </ NamePurchaseCard >
268+
269+ { isPurchaseDialogOpen && (
270+ < PurchaseNameDialog
271+ name = { name }
272+ open = { isPurchaseDialogOpen }
273+ setOpen = { setPurchaseDialogOpen }
274+ onPurchase = { handlePurchase }
275+ />
276+ ) }
277+ </ >
223278 ) ;
224279}
0 commit comments