• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

decentraland / marketplace / 8249081468

12 Mar 2024 12:48PM UTC coverage: 66.114% (-0.02%) from 66.13%
8249081468

Pull #2181

github

LautaroPetaccio
fix: No unused vars & correct linting script in CI
Pull Request #2181: fix: Linting rules one (no-empty, no-yield, no-case-declarations and no-unused-vars)

2509 of 4914 branches covered (51.06%)

Branch coverage included in aggregate %.

3 of 5 new or added lines in 4 files covered. (60.0%)

1 existing line in 1 file now uncovered.

7428 of 10116 relevant lines covered (73.43%)

72.12 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

88.27
/webapp/src/components/NamesPage/ClaimNamePage/ClaimNamePage.tsx
1
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
1✔
2
import { useLocation } from 'react-router-dom'
1✔
3
import classNames from 'classnames'
1✔
4
import { Button, Close, Container, Field, Icon, Loader, Popup, useTabletAndBelowMediaQuery } from 'decentraland-ui'
1✔
5
import { isErrorWithMessage } from 'decentraland-dapps/dist/lib'
1✔
6
import { t } from 'decentraland-dapps/dist/modules/translation/utils'
1✔
7
import { config } from '../../../config'
1✔
8
import infoIcon from '../../../images/infoIcon.png'
1✔
9
import ClaimNameImage from '../../../images/claim-name.svg'
1✔
10
import ClaimNameBanner from '../../../images/claim-name-banner.png'
1✔
11
import CreateImg from '../../../images/names/create.png'
1✔
12
import GovernanceImg from '../../../images/names/governance.png'
1✔
13
import LandmarkImg from '../../../images/names/landmark.png'
1✔
14
import OwnSpaceImg from '../../../images/names/own-space.png'
1✔
15
import Chest from '../../../images/names/chest.png'
1✔
16
import Passports from '../../../images/names/passports.png'
1✔
17
import { lists } from '../../../modules/vendor/decentraland/lists/api'
1✔
18
import { SortBy } from '../../../modules/routing/types'
1✔
19
import {
20
  MAX_NAME_SIZE,
21
  NameInvalidType,
22
  getNameInvalidType,
23
  hasNameMinLength,
24
  isNameAvailable,
25
  isNameValid
26
} from '../../../modules/ens/utils'
1✔
27
import { locations } from '../../../modules/routing/locations'
1✔
28
import { Section } from '../../../modules/vendor/decentraland'
1✔
29
import { NavigationTab } from '../../Navigation/Navigation.types'
1✔
30
import { builderUrl } from '../../../lib/environment'
1✔
31
import { Mana } from '../../Mana'
1✔
32
import { PageLayout } from '../../PageLayout'
1✔
33
import { Props } from './ClaimNamePage.types'
34
import styles from './ClaimNamePage.module.css'
1✔
35

36
const MARKETPLACE_URL = config.get('MARKETPLACE_URL', '')
1✔
37

38
const PLACEHOLDER_WIDTH = '94px'
1✔
39

40
const ClaimNamePage = (props: Props) => {
1✔
41
  const PLACEHOLDER_NAME = t('names_page.your_name')
27✔
42
  const { wallet, isConnecting, onClaim, onBrowse, onRedirect } = props
27✔
43
  const location = useLocation()
26✔
44
  const [isLoadingStatus, setIsLoadingStatus] = useState(false)
26✔
45
  const [bannedNames, setBannedNames] = useState<string[]>()
26✔
46
  const [isAvailable, setIsAvailable] = useState<boolean | undefined>(undefined)
26✔
47
  const debounceRef = useRef<ReturnType<typeof setTimeout> | null>(null)
26✔
48
  const isMobileOrTable = useTabletAndBelowMediaQuery()
26✔
49

50
  useEffect(() => {
26✔
51
    ;(async () => {
7✔
52
      try {
7✔
53
        const bannedNames = await lists.fetchBannedNames()
7✔
54
        setBannedNames(bannedNames)
5✔
55
      } catch (error) {
NEW
56
        console.error('Error fetching banned names', isErrorWithMessage(error) ? error.message : 'Unknown error')
×
57
      }
58
    })()
59
  }, [])
60

61
  const [name, setName] = useState(PLACEHOLDER_NAME)
26✔
62

63
  const handleNameChange = useCallback(
26✔
64
    async text => {
65
      const valid = isNameValid(text)
5✔
66
      const minLength = hasNameMinLength(text)
5✔
67
      if (valid && minLength) {
5✔
68
        try {
1✔
69
          if (bannedNames?.includes(text.toLocaleLowerCase())) {
1!
70
            setIsAvailable(undefined)
×
71
          } else {
72
            const isAvailable = await isNameAvailable(text)
1✔
73
            setIsAvailable(isAvailable)
1✔
74
          }
75
          setIsLoadingStatus(false)
1✔
76
        } catch (error) {
77
          console.log('error: ', error)
×
78
          setIsLoadingStatus(false)
×
79
        }
80
      }
81
    },
82
    [bannedNames]
83
  )
84

85
  const handleDebouncedChange = useCallback(
26✔
86
    text => {
87
      setName(text)
7✔
88
      const timeoutId = setTimeout(() => {
7✔
89
        if (debounceRef.current === timeoutId) {
5✔
90
          handleNameChange(text)
5✔
91
        }
92
      }, 1000)
93
      if (debounceRef.current) {
7!
94
        clearTimeout(debounceRef.current)
×
95
      }
96
      debounceRef.current = timeoutId
7✔
97
    },
98
    [handleNameChange]
99
  )
100

101
  useEffect(() => {
26✔
102
    if (name !== PLACEHOLDER_NAME && name.length && hasNameMinLength(name) && isNameValid(name)) {
14✔
103
      setIsLoadingStatus(true)
3✔
104
    } else if (!isNameValid(name)) {
11✔
105
      // turn off loading if an invalid character is typed
106
      setIsLoadingStatus(false)
4✔
107
    }
108
  }, [PLACEHOLDER_NAME, name])
109

110
  const handleClaim = useCallback(() => {
26✔
111
    if (!isConnecting && !wallet) {
1!
112
      onRedirect(locations.signIn(`${location.pathname}`))
×
113
    } else {
114
      const isValid = isNameValid(name)
1✔
115

116
      if (!isValid || !isAvailable) return
1!
117

118
      onClaim(name)
1✔
119
    }
120
  }, [isConnecting, wallet, onRedirect, location.pathname, name, isAvailable, onClaim])
121

122
  const inputRef = useRef<HTMLInputElement>(null)
26✔
123

124
  const [inputWidth, setInputWidth] = useState(PLACEHOLDER_WIDTH)
26✔
125

126
  // this fn is used to update the width of the input field so it has the suffix in the right place
127
  const updateWidth = (value: string) => {
26✔
128
    if (inputRef.current) {
7✔
129
      // Use a temporary span to measure the width of the input's content
130
      const tempSpan = document.createElement('span')
7✔
131
      tempSpan.innerHTML = value
7✔
132
      // Apply same font properties to the span
133
      tempSpan.style.fontSize = getComputedStyle(inputRef.current).fontSize
7✔
134
      tempSpan.style.fontFamily = getComputedStyle(inputRef.current).fontFamily
7✔
135
      tempSpan.style.visibility = 'hidden' // Hide the span element
136
      document.body.appendChild(tempSpan)
7✔
137
      // Update the width state to the width of the content plus a little extra space
138
      setInputWidth(`${tempSpan.offsetWidth + 2}px`)
7✔
139
      document.body.removeChild(tempSpan) // Clean up
140
    }
141
  }
142

143
  const [isInputFocus, setIsInputFocus] = useState(false)
26✔
144

145
  const onFieldClick = useCallback(() => {
26✔
146
    inputRef.current?.focus()
2✔
147
    setIsInputFocus(true)
2✔
148
  }, [])
149

150
  const onFieldKeyDown = useCallback(
26✔
151
    (event: React.KeyboardEvent<HTMLInputElement>) => {
152
      if (event.key === 'Enter') {
×
153
        handleClaim()
×
154
      }
155
    },
156
    [handleClaim]
157
  )
158

159
  const onFieldFocus = useCallback(() => {
26✔
160
    const inputValue = inputRef.current?.value
2✔
161
    if (inputValue === PLACEHOLDER_NAME) {
2!
162
      setName('')
×
163
    }
164
  }, [PLACEHOLDER_NAME])
165

166
  const renderRemainingCharacters = useCallback(() => {
26✔
167
    if (name !== PLACEHOLDER_NAME) {
26✔
168
      return <span className={styles.remainingCharacters}>{`${name.length}/${MAX_NAME_SIZE}`}</span>
169
    }
170
  }, [PLACEHOLDER_NAME, name])
171

172
  const nameInvalidType = useMemo(() => {
26✔
173
    return getNameInvalidType(name)
14✔
174
  }, [name])
175

176
  const onFieldChange = useCallback(
26✔
177
    (event: React.ChangeEvent<HTMLInputElement>) => {
178
      handleDebouncedChange(event.target.value)
7✔
179
      updateWidth(event.target.value)
7✔
180
    },
181
    [handleDebouncedChange]
182
  )
183

184
  const cards = useMemo(() => {
26✔
185
    return [
7✔
186
      {
187
        image: CreateImg,
188
        title: t('names_page.why.stand_out.title'),
189
        description: t('names_page.why.stand_out.description'),
190
        className: styles.standOut
191
      },
192
      {
193
        image: OwnSpaceImg,
194
        title: t('names_page.why.unlock.title'),
195
        description: t('names_page.why.unlock.description', {
196
          link: (
197
            <a
198
              href="https://decentraland.org/blog/about-decentraland/decentraland-worlds-your-own-virtual-space"
199
              className={styles.learnMore}
200
            >
201
              {t('global.learn_more')}
202
            </a>
203
          )
204
        })
205
      },
206
      {
207
        image: GovernanceImg,
208
        title: t('names_page.why.governance.title'),
209
        description: t('names_page.why.governance.description', {
210
          b: (children: React.ReactChildren) => <b className={styles.voting}>{children}</b>,
211
          link: (
212
            <a href="https://docs.decentraland.org/player/general/dao/overview/what-is-the-dao" className={styles.learnMore}>
213
              {t('global.learn_more')}
214
            </a>
215
          )
216
        })
217
      },
218
      {
219
        image: LandmarkImg,
220
        title: t('names_page.why.get_url.title'),
221
        description: t('names_page.why.get_url.description', {
222
          b: (children: React.ReactChildren) => <b className={styles.nameLink}>{children}</b>
223
        })
224
      }
225
    ]
226
  }, [])
227

228
  return (
229
    <PageLayout activeTab={NavigationTab.NAMES}>
230
      <div className={styles.claimNamePageContainer}>
231
        <div className={styles.claimNamePage}>
232
          <Container className={styles.mainContainer}>
233
            <div className={styles.gradient}>
234
              <div className={classNames(styles.claimContainer)}>
235
                {isInputFocus ? <Close onClick={() => setIsInputFocus(false)} /> : null}
✔
236
                <div className={styles.imageContainer}>
237
                  <div className={styles.imagePassportContainer}>
238
                    <img
239
                      className={classNames(!isInputFocus && styles.visible, styles.passportLogo)}
50✔
240
                      src={ClaimNameImage}
241
                      alt="Claim name"
242
                    />
243
                    <h2 className={classNames(isInputFocus && styles.fadeOut)}>{t('names_page.title')}</h2>
28✔
244
                  </div>
245
                  <img className={classNames(styles.banner, isInputFocus && styles.visible)} src={ClaimNameBanner} alt="Banner" />
28✔
246
                </div>
247

248
                <span className={styles.subtitle}>{t('names_page.subtitle')}</span>
249
                <div className={styles.claimInput} onClick={onFieldClick}>
250
                  <Field
251
                    onClick={onFieldClick}
252
                    value={name}
253
                    placeholder={t('names_page.name_placeholder')}
254
                    action={`${name.length}/${MAX_NAME_SIZE}`}
255
                    children={
256
                      <>
257
                        <input
258
                          ref={inputRef}
259
                          onKeyDown={onFieldKeyDown}
260
                          value={name}
261
                          style={{
262
                            maxWidth: name.length ? inputWidth : '1px'
26!
263
                          }}
264
                          onFocus={onFieldFocus}
265
                          onChange={onFieldChange}
266
                        />
267
                        <span className={styles.inputSuffix}>.dcl.eth</span>
268
                      </>
269
                    }
270
                  />
271
                  <div className={styles.remainingCharactersContainer}>
272
                    {isLoadingStatus ? <Loader active inline size="tiny" /> : null}
26✔
273
                    {renderRemainingCharacters()}
274
                  </div>
275
                  <Button primary onClick={handleClaim} disabled={!isAvailable || nameInvalidType !== null || isLoadingStatus}>
32✔
276
                    {t('names_page.claim_a_name')}
277
                  </Button>
278

279
                  {name &&
126✔
280
                  hasNameMinLength(name) &&
281
                  isNameValid(name) &&
282
                  isInputFocus &&
283
                  name !== PLACEHOLDER_NAME &&
284
                  isAvailable !== undefined &&
285
                  bannedNames !== undefined &&
286
                  !isLoadingStatus ? (
287
                    <div className={styles.availableContainer}>
288
                      {isAvailable ? (
289
                        <>
1✔
290
                          <Icon name="check" />
291
                          {t('names_page.available')}
292
                        </>
293
                      ) : (
294
                        <>
295
                          <Icon name="close" />
296
                          {t('names_page.not_available', {
297
                            link: (
298
                              <a
299
                                className={styles.marketplaceLinkContainer}
300
                                href={`${MARKETPLACE_URL}${locations.names({
301
                                  search: name,
302
                                  onlyOnSale: false,
303
                                  sortBy: SortBy.NEWEST,
304
                                  section: Section.ENS
305
                                })}`}
306
                                target="_blank"
307
                                rel="noopener noreferrer"
308
                              >
309
                                {t('names_page.marketplace')}
310
                                <Icon name="external" />
311
                              </a>
312
                            )
313
                          })}
314
                        </>
315
                      )}
316
                    </div>
317
                  ) : name && (!hasNameMinLength(name) || !isNameValid(name)) ? (
98✔
318
                    <div className={styles.availableContainer}>
319
                      <Icon
320
                        className={styles.warningIcon}
321
                        name={nameInvalidType === NameInvalidType.TOO_SHORT ? 'exclamation triangle' : 'close'}
8✔
322
                      />
323
                      {nameInvalidType === NameInvalidType.TOO_SHORT
8✔
324
                        ? t('names_page.name_too_short')
325
                        : nameInvalidType === NameInvalidType.TOO_LONG
6✔
326
                          ? t('names_page.name_too_long')
327
                          : nameInvalidType === NameInvalidType.HAS_SPACES
4✔
328
                            ? t('names_page.has_spaces')
329
                            : t('names_page.invalid_characters')}
330
                    </div>
331
                  ) : null}
332
                </div>
333

334
                <span className={classNames(styles.nameCost, isInputFocus && styles.fadeOut)}>
28✔
335
                  {t('names_page.name_cost', {
336
                    mana: (
337
                      <>
338
                        <Mana inline /> 100 MANA
339
                      </>
340
                    ),
341
                    network: <span className={styles.nameCostNetwork}>{t('names_page.ethereum_mainnet_network')}</span>
342
                  })}
343
                  <Popup
344
                    content={t('names_page.dao_tooltip', {
345
                      link: (
346
                        <a
347
                          href="https://decentraland.zone/governance/proposal/?id=a3bdc100-9b34-11ed-ae61-5f6dd0bf8358"
348
                          target="_blank"
349
                          rel="noopener noreferrer"
350
                        >
351
                          {t('global.learn_more')}
352
                        </a>
353
                      )
354
                    })}
355
                    position="top center"
356
                    hoverable
357
                    mouseLeaveDelay={500}
358
                    trigger={<img src={infoIcon} alt="info" className={styles.informationTooltip} />}
359
                    on="hover"
360
                  />
361
                  <div>
362
                    {t('names_page.name_cost_fiat', {
363
                      icon: <Icon name="credit card outline" />
364
                    })}
365
                    <span className={styles.cardsLabel}>{t('names_page.debit_and_credit_cards')}</span>
366
                  </div>
367
                </span>
368
              </div>
369
            </div>
370
            <div className={styles.ctasContainer}>
371
              <h1>{t('names_page.why_names')}</h1>
372
              <div className={styles.cardsContainer}>
373
                {cards.map((card, index) => (
374
                  <div key={index} className={styles.card}>
104✔
375
                    <div className={classNames(styles.whyImgContainer, card.className)}>
376
                      <img src={card.image} alt={card.title} />
377
                    </div>
378
                    <div className={styles.whyTextContainer}>
379
                      <span>{card.title}</span>
380
                      <p>{card.description}</p>
381
                    </div>
382
                  </div>
383
                ))}
384
              </div>
385
              <div className={classNames(styles.cardsContainer, styles.bottomContainer)}>
386
                <div className={styles.nameTakenCard}>
387
                  <div className={styles.buttons}>
388
                    <div>
389
                      <img src={Chest} alt="Chest" />
390
                    </div>
391
                    <div>
392
                      <h2> {t('names_page.ctas.name_taken.title')}</h2>
393
                      <span> {t('names_page.ctas.name_taken.description')}</span>
394
                      <Button onClick={() => onBrowse()}>{t('names_page.browse_names_being_resold')}</Button>
×
395
                    </div>
396
                  </div>
397
                </div>
398
                {!isMobileOrTable ? (
26!
399
                  <div className={styles.manageNames}>
400
                    <div className={styles.buttons}>
401
                      <div>
402
                        <img src={Passports} alt="passports" />
403
                      </div>
404
                      <div style={{ justifyContent: 'center' }}>
405
                        <h2> {t('names_page.ctas.manage.title')}</h2>
406
                        <Button inverted as={'a'} target="_blank" href={`${builderUrl}/names`}>
407
                          {t('names_page.manage_your_names')}
408
                        </Button>
409
                      </div>
410
                    </div>
411
                  </div>
412
                ) : null}
413
              </div>
414
            </div>
415
          </Container>
416
        </div>
417
      </div>
418
    </PageLayout>
419
  )
420
}
421

422
export default React.memo(ClaimNamePage)
7✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc