components/search/Search.vue
<template>
<div>
<NeoField :class="searchColumnClass">
<slot name="next-filter" />
<SearchBar
v-if="!hideSearchInput"
ref="searchRef"
v-model="name"
:query="query"
data-testid="search-bar"
@redirect="redirectToGalleryPageIfNeed"
@enter="nativeSearch"
@blur="onBlur"
/>
<div v-if="!isVisible && hideSearchInput">
<div
v-if="priceRangeDirty"
class="text-xs"
>
<PriceRange inline />
</div>
</div>
</NeoField>
</div>
</template>
<script lang="ts" setup>
import { NeoField } from '@kodadot1/brick'
import type { SearchQuery } from './types'
import { exist, existArray } from '@/utils/exist'
import PriceRange from '@/components/shared/format/PriceRange.vue'
import SearchBar from '@/components/search/SearchBar.vue'
import { useCollectionSearch } from '@/components/search/utils/useCollectionSearch'
const searchPageRoutePathList = ['collectibles', 'items']
const props = withDefaults(
defineProps<{
search?: string
sortByMultiple?: string[]
searchColumnClass?: string
listed?: boolean
hideFilter?: boolean
hideSearchInput?: boolean
}>(),
{
search: '',
sortByMultiple: () => [],
searchColumnClass: '',
listed: false,
hideFilter: false,
hideSearchInput: false,
},
)
const emit = defineEmits<{
(e: 'update:search', value: string): void
(e: 'update:sortByMultiple', value: string[]): void
(e: 'update:listed', listed: boolean): void
(e: 'resetPage'): void
(e: 'update:priceMin' | 'update:priceMax', value?: number): void
}>()
const { neoModal } = useProgrammatic()
const { $consola } = useNuxtApp()
const { urlPrefix } = usePrefix()
const { decimals } = useChain()
const { options: sortByOptions } = useRouteSortByOptions()
const route = useRoute()
const router = useRouter()
const searchRef = ref(null)
const isVisible = ref(false)
const name = ref('')
const priceRange = ref<
[number | string | undefined, number | string | undefined]
>([undefined, undefined])
const priceRangeDirty = ref(false)
const query = reactive<SearchQuery>({
search: route.query?.search?.toString() ?? '',
type: route.query?.type?.toString() ?? '',
sortByMultiple: props.sortByMultiple ?? [],
listed: route.query?.listed?.toString() === 'true',
})
const urlSearchQuery = computed(() => route.query.search)
const routePathList = computed(() =>
searchPageRoutePathList.map(
route => `/${urlPrefix.value}/explore/${route}`,
),
)
const searchQuery = computed({
get() {
return props.search
},
set(value: string) {
updateSearch(value)
},
})
const isExplorePage = computed(() => routePathList.value.includes(route.path))
type Listed = boolean | { listed: boolean, min?: string, max?: string }
const vListed = computed({
get() {
// eslint-disable-next-line vue/no-side-effects-in-computed-properties
query.listed = props.listed
return props.listed
},
set(listed: Listed) {
updateListed(listed)
},
})
const updateListed = useDebounceFn((value: string | Listed): boolean => {
let v: string
if (typeof value === 'string' || typeof value === 'boolean') {
v = String(value)
replaceUrl({ listed: v })
}
else {
const { listed, max, min } = value
v = String(listed)
replaceUrl({
listed,
max,
min,
})
}
const listed = v === 'true'
emit('update:listed', listed)
return listed
}, 50)
const replaceUrl = useDebounceFn(
(queryCondition: Record<string, any>, pathName?: string) => {
if (pathName && pathName !== route.path) {
return
}
const { page, ...restQuery } = route.query
router
.replace({
query: {
...restQuery,
search: searchQuery.value || route.query.search || undefined,
...queryCondition,
},
})
.catch($consola.warn)
// if a searchbar request or filter is set, pagination should always revert to page 1
emit('resetPage')
},
100,
)
const updateSortBy = useDebounceFn((value: string[] | string) => {
const final = (Array.isArray(value) ? value : [value]).filter(condition =>
sortByOptions.value.includes(condition),
)
const listed = final.some(
condition => condition.toLowerCase().indexOf('price') > -1,
)
if (listed && !vListed.value) {
vListed.value = true
}
replaceUrl({ sort: final })
emit('update:sortByMultiple', final)
return final
}, 400)
const updateSearch = (value: string): string => {
if (value !== route.query.search && value !== searchQuery.value) {
replaceUrl({ search: value ? value : undefined }, route.path)
}
emit('update:search', value)
return value
}
const focusInput = () => {
searchRef.value?.focusInput()
}
function bindFilterEvents(event: KeyboardEvent) {
switch (event.key) {
case 'b':
updateListed(!vListed.value)
break
case 'n':
updateSortBy(['BLOCK_NUMBER_DESC'])
break
case 'o':
updateSortBy(['BLOCK_NUMBER_ASC'])
break
case 'e':
updateSortBy(['PRICE_DESC'])
break
case 'c':
updateSortBy(['PRICE_ASC'])
break
}
}
function updatePriceRangeByQuery(minValue?: string, maxValue?: string) {
const min = Number(minValue)
const max = Number(maxValue)
if (Number.isNaN(min) && Number.isNaN(max)) {
return
}
priceRangeDirty.value = true
if (minValue) {
priceRange.value = [min, priceRange.value[1]]
priceRangeChangeMin(min * 10 ** decimals.value)
}
else {
priceRange.value = [priceRange.value[0], max]
priceRangeChangeMax(max * 10 ** decimals.value)
}
}
function nativeSearch() {
if (name.value) {
neoModal.closeAll()
}
redirectToGalleryPageIfNeed({ search: name.value })
searchQuery.value = name.value
}
function redirectToGalleryPageIfNeed(params?: Record<string, string>) {
const { isCollectionSearchMode } = useCollectionSearch()
if (!isExplorePage.value && !isCollectionSearchMode.value) {
router.push({
path: `/${urlPrefix.value}/explore/items`,
query: {
...route.query,
...params,
},
})
}
}
function priceRangeChangeMin(min?: number): void {
query.priceMin = min
emit('update:priceMin', min)
}
function priceRangeChangeMax(max?: number): void {
query.priceMax = max
emit('update:priceMax', max)
}
function onBlur() {
if (isExplorePage.value) {
updateSearch(name.value)
}
}
// clear search bar value when search is canceled via breadcrumbs
watch(urlSearchQuery, (urlSearchQuery) => {
if (urlSearchQuery == undefined) {
name.value = ''
}
})
useKeyboardEvents({ f: bindFilterEvents })
onMounted(() => {
if (!name.value && route.query.search) {
name.value = Array.isArray(route.query.search) ? '' : route.query.search
}
exist(route.query.search, updateSearch)
exist(route.query.min, v => updatePriceRangeByQuery(v))
exist(route.query.max, v => updatePriceRangeByQuery(undefined, v))
existArray(route.query.sort as string[], updateSortBy)
exist(route.query.listed, updateListed)
})
defineExpose({ focusInput })
</script>