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

decentraland / marketplace / 6468133945

10 Oct 2023 10:36AM UTC coverage: 40.778% (+0.7%) from 40.103%
6468133945

Pull #2030

github

juanmahidalgo
test: add Test for SearchBarDropdown
Pull Request #2030: Feat: new search bar component

2260 of 6951 branches covered (0.0%)

Branch coverage included in aggregate %.

215 of 215 new or added lines in 20 files covered. (100.0%)

4280 of 9087 relevant lines covered (47.1%)

18.0 hits per line

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

0.81
/webapp/src/components/AssetTopbar/AssetTopbar.tsx
1
import { useCallback, useEffect, useRef, useState } from 'react'
2
import { NFTCategory } from '@dcl/schemas'
3
import classNames from 'classnames'
4
import {
5
  Close,
6
  Dropdown,
7
  DropdownProps,
8
  Field,
9
  Icon,
10
  useTabletAndBelowMediaQuery
11
} from 'decentraland-ui'
12
import { t } from 'decentraland-dapps/dist/modules/translation/utils'
13
import { useInput } from '../../lib/input'
14
import { getCountText } from './utils'
15
import { SortBy } from '../../modules/routing/types'
16
import { isCatalogView } from '../../modules/routing/utils'
17
import {
18
  getCategoryFromSection,
19
  getSectionFromCategory
20
} from '../../modules/routing/search'
21
import {
22
  isAccountView,
23
  isLandSection,
24
  isListsSection,
25
  persistIsMapProperty
26
} from '../../modules/ui/utils'
27
import trash from '../../images/trash.png'
28
import { Chip } from '../Chip'
29
import { Props } from './AssetTopbar.types'
30
import { SelectedFilters } from './SelectedFilters'
31
import { SearchBarDropdown } from './SearchBarDropdown'
32
import styles from './AssetTopbar.module.css'
33

34
export const AssetTopbar = ({
1✔
35
  search,
36
  view,
37
  count,
38
  isLoading,
39
  isMap,
40
  onlyOnSale,
41
  onlyOnRent,
42
  sortBy,
43
  section,
44
  hasFiltersEnabled,
45
  onBrowse,
46
  onClearFilters,
47
  onOpenFiltersModal,
48
  sortByOptions
49
}: Props): JSX.Element => {
50
  const isMobile = useTabletAndBelowMediaQuery()
×
51
  const searchBarFieldRef = useRef<HTMLDivElement>(null)
×
52
  const category = section ? getCategoryFromSection(section) : undefined
×
53
  const [searchValueForDropdown, setSearchValueForDropdown] = useState(search)
×
54
  const [shouldRenderSearchDropdown, setShouldRenderSearchDropdown] = useState(
×
55
    false
56
  )
57

58
  const handleInputChange = useCallback(
×
59
    text => {
60
      if (shouldRenderSearchDropdown) {
×
61
        setSearchValueForDropdown(text)
×
62
      } else if (text) {
×
63
        // common search, when the dropdown is not opened
64
        onBrowse({
×
65
          search: text,
66
          section: category ? getSectionFromCategory(category) : section
×
67
        })
68
      }
69
    },
70
    [category, onBrowse, section, shouldRenderSearchDropdown]
71
  )
72
  const [searchValue, setSearchValue] = useInput(search, handleInputChange, 500)
×
73

74
  const handleClearSearch = useCallback(() => {
×
75
    setSearchValue({ target: { value: '' } } as React.ChangeEvent<
×
76
      HTMLInputElement
77
    >)
78
    setSearchValueForDropdown('') // clears the input
×
79

80
    onBrowse({
×
81
      search: ''
82
    }) // triggers search with no search term
83
  }, [onBrowse, setSearchValue])
84

85
  const handleSearch = useCallback(
×
86
    ({
87
      value,
88
      contractAddresses
89
    }: {
90
      value?: string
91
      contractAddresses?: string[]
92
    }) => {
93
      if (value !== undefined && search !== value) {
×
94
        onBrowse({
×
95
          search: value,
96
          section: category ? getSectionFromCategory(category) : section
×
97
        })
98
      } else if (contractAddresses && contractAddresses.length) {
×
99
        onBrowse({
×
100
          contracts: contractAddresses,
101
          search: ''
102
        })
103
        handleClearSearch()
×
104
      }
105
      setShouldRenderSearchDropdown(false)
×
106
    },
107
    [category, handleClearSearch, onBrowse, search, section]
108
  )
109

110
  const handleOrderByDropdownChange = useCallback(
×
111
    (_, props: DropdownProps) => {
112
      const sortBy: SortBy = props.value as SortBy
×
113
      if (!onlyOnRent && !onlyOnSale && isLandSection(section)) {
×
114
        if (sortBy === SortBy.CHEAPEST_SALE) {
×
115
          onBrowse({ onlyOnSale: true, sortBy: SortBy.CHEAPEST })
×
116
        } else if (sortBy === SortBy.CHEAPEST_RENT) {
×
117
          onBrowse({ onlyOnRent: true, sortBy: SortBy.MAX_RENTAL_PRICE })
×
118
        }
119
      } else {
120
        onBrowse({ sortBy })
×
121
      }
122
    },
123
    [onlyOnRent, onlyOnSale, section, onBrowse]
124
  )
125

126
  const handleIsMapChange = useCallback(
×
127
    (isMap: boolean) => {
128
      persistIsMapProperty(isMap)
×
129

130
      onBrowse({
×
131
        isMap,
132
        isFullscreen: isMap,
133
        search: '',
134
        // Forces the onlyOnSale property in the defined cases so the users can see LAND on sale.
135
        onlyOnSale:
136
          (!onlyOnSale && onlyOnRent === false) ||
×
137
          (onlyOnSale === undefined && onlyOnRent === undefined) ||
138
          onlyOnSale
139
      })
140
    },
141
    [onBrowse, onlyOnSale, onlyOnRent]
142
  )
143

144
  useEffect(() => {
×
145
    const option = sortByOptions.find(option => option.value === sortBy)
×
146
    if (!option) {
×
147
      onBrowse({ sortBy: sortByOptions[0].value })
×
148
    }
149
  }, [onBrowse, sortBy, sortByOptions])
150

151
  const sortByValue = sortByOptions.find(option => option.value === sortBy)
×
152
    ? sortBy
153
    : sortByOptions[0].value
154

155
  useEffect(() => {
×
156
    // when the category changes, close the dropdown
157
    setShouldRenderSearchDropdown(false)
×
158
  }, [category])
159

160
  const handleFieldClick = useCallback(() => {
×
161
    // opens the dropdown on the field focus
162
    setShouldRenderSearchDropdown(
×
163
      category === NFTCategory.EMOTE || category === NFTCategory.WEARABLE
×
164
    )
165
  }, [category])
166

167
  const handleSearchBarDropdownClickOutside = useCallback(event => {
×
168
    // when clicking outside the dropdown, close it
169
    const containsClick = searchBarFieldRef.current?.contains(event.target)
×
170
    if (!containsClick) {
×
171
      setShouldRenderSearchDropdown(false)
×
172
    }
173
  }, [])
174

175
  const renderSearch = useCallback(() => {
×
176
    return (
×
177
      <SearchBarDropdown
178
        category={category}
179
        searchTerm={searchValueForDropdown}
180
        onSearch={handleSearch}
181
        onClickOutside={handleSearchBarDropdownClickOutside}
182
      />
183
    )
184
  }, [
185
    category,
186
    handleSearch,
187
    handleSearchBarDropdownClickOutside,
188
    searchValueForDropdown
189
  ])
190

191
  return (
×
192
    <div className={styles.assetTopbar} ref={searchBarFieldRef}>
193
      <div
194
        className={classNames(styles.searchContainer, {
195
          [styles.searchMap]: isMap
196
        })}
197
      >
198
        {!isMap && !isListsSection(section) && (
×
199
          <div className={styles.searchFieldContainer}>
200
            <Field
201
              className={styles.searchField}
202
              placeholder={t('nft_filters.search')}
203
              kind="full"
204
              value={searchValue}
205
              onChange={setSearchValue}
206
              icon={<Icon name="search" className="searchIcon" />}
207
              iconPosition="left"
208
              onClick={handleFieldClick}
209
            />
210
            {searchValue ? <Close onClick={handleClearSearch} /> : null}
×
211
          </div>
212
        )}
213
        {shouldRenderSearchDropdown && renderSearch()}
×
214
        {isLandSection(section) && !isAccountView(view!) && (
×
215
          <div
216
            className={classNames(styles.mapToggle, { [styles.map]: isMap })}
217
          >
218
            <Chip
219
              className="grid"
220
              icon="table"
221
              isActive={!isMap}
222
              onClick={handleIsMapChange.bind(null, false)}
223
            />
224
            <Chip
225
              className="atlas"
226
              icon="map marker alternate"
227
              isActive={isMap}
228
              onClick={handleIsMapChange.bind(null, true)}
229
            />
230
          </div>
231
        )}
232
      </div>
233
      {!isMap && (
×
234
        <div className={styles.infoRow}>
235
          {!isLoading ? (
×
236
            isListsSection(section) && !count ? null : (
×
237
              <div className={styles.countContainer}>
238
                <p className={styles.countText}>
239
                  {count && isCatalogView(view)
×
240
                    ? t(
241
                        search
×
242
                          ? 'nft_filters.query_results'
243
                          : 'nft_filters.results',
244
                        {
245
                          count: count.toLocaleString(),
246
                          search
247
                        }
248
                      )
249
                    : getCountText(count, search)}
250
                </p>
251
              </div>
252
            )
253
          ) : null}
254
          {!isListsSection(section) ? (
×
255
            <div className={styles.rightOptionsContainer}>
256
              <Dropdown
257
                direction="left"
258
                value={sortByValue}
259
                options={sortByOptions}
260
                onChange={handleOrderByDropdownChange}
261
              />
262
              {isMobile ? (
×
263
                <i
264
                  className={classNames(
265
                    styles.openFilters,
266
                    styles.openFiltersWrapper,
267
                    hasFiltersEnabled && styles.active
×
268
                  )}
269
                  onClick={onOpenFiltersModal}
270
                />
271
              ) : null}
272
            </div>
273
          ) : null}
274
        </div>
275
      )}
276
      {!isMap && hasFiltersEnabled ? (
×
277
        <div className={styles.selectedFiltersContainer}>
278
          <SelectedFilters />
279
          <button className={styles.clearFilters} onClick={onClearFilters}>
280
            <img src={trash} alt={t('filters.clear')} />
281
            {t('filters.clear')}
282
          </button>
283
        </div>
284
      ) : null}
285
    </div>
286
  )
287
}
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