Skip to content

Commit

Permalink
Make search query work in desktop
Browse files Browse the repository at this point in the history
  • Loading branch information
onmax committed Sep 1, 2023
1 parent b07f28c commit 0533149
Show file tree
Hide file tree
Showing 6 changed files with 115 additions and 126 deletions.
2 changes: 1 addition & 1 deletion src/components/DesktopView.vue
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ const openSuggestions = ref(false)
class="absolute inset-0 max-w-[368px] transition-[transform,opacity] will-change-transform pointer-events-none bg-gradient-to-r from-space to-space/0"
/>
<aside class="absolute flex flex-col max-w-xs bottom-6 top-6 left-6 h-max pointer-events-none [&>*]:pointer-events-auto">
<div class="bg-white shadow-header transition-border-radius" :class="openSuggestions ? 'rounded-t-2xl' : 'rounded-2xl'" style="mask-image: linear-gradient(white, white);">
<div class="duration-75 bg-white shadow-header transition-border-radius" :class="openSuggestions ? 'rounded-t-2xl' : 'rounded-2xl'" style="mask-image: linear-gradient(white, white);">
<InteractionBar @open="openSuggestions = $event" />
<DesktopList :locations="singles" :clusters="clusters" :list-is-shown="listIsShown" />
</div>
Expand Down
141 changes: 70 additions & 71 deletions src/components/atoms/SearchBox.vue
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,10 @@ import {
ComboboxOptions,
TransitionRoot,
} from '@headlessui/vue'
import { vElementVisibility } from '@vueuse/components'
import { useDebounceFn } from '@vueuse/core'
import type { PropType } from 'vue'
import { computed, ref, useSlots, watchEffect } from 'vue'
import { useDebounceFn } from '@vueuse/core'
import { vElementVisibility } from '@vueuse/components'
import SearchIcon from '@/components/icons/icon-search.vue'
import CrossIcon from '@/components/icons/icon-cross.vue'
import { AutocompleteStatus } from '@/types'
Expand Down Expand Up @@ -53,6 +54,10 @@ const props = defineProps({
type: Boolean,
default: false,
},
status: {
type: String as PropType<AutocompleteStatus>,
required: true,
},
})
const emit = defineEmits({
Expand All @@ -66,15 +71,6 @@ const query = ref<string>()
const userCanCleanInput = computed(() => props.allowClean && query.value !== '' && query.value !== undefined)
const loading = ref(false)
const status = computed<AutocompleteStatus>(() => {
if (props.suggestions.length > 0)
return AutocompleteStatus.WithResults
if (loading.value)
return AutocompleteStatus.Loading
if (props.suggestions.length === 0 && query.value !== '')
return AutocompleteStatus.NoResults
return AutocompleteStatus.Initial
})
watchEffect(
async () => {
Expand Down Expand Up @@ -143,9 +139,7 @@ function onListVisibilityChange(isVisible: boolean) {
@update:model-value="emit('selected', selected)"
>
<ComboboxLabel v-if="hasLabel" class="capitalize text-space/40">
<slot name="label">
{{ label }}
</slot>
{{ label }}
</ComboboxLabel>
<div class="relative" :class="{ 'mt-1': hasLabel }">
<div
Expand Down Expand Up @@ -177,67 +171,72 @@ function onListVisibilityChange(isVisible: boolean) {
</button>
</div>
</div>
<TransitionRoot leave="transition ease-in duration-100" leave-from="opacity-100" leave-to="opacity-0">
<ComboboxOptions
v-element-visibility="onListVisibilityChange" data-combobox-options
:class="[
comboboxOptionsClasses,
{
'bg-white': bgCombobox === 'white',
'bg-space': bgCombobox === 'space',
},
]"
class="absolute z-40 overflow-auto text-base rounded-sm shadow-lg scroll-space focus:outline-none"
>
<div
v-if="AutocompleteStatus.WithResults !== status" class="relative px-4 py-2 cursor-default select-none" :class="{
'text-space/80': bgCombobox === 'white',
'text-white/80': bgCombobox === 'space',
}"
<Teleport to="body">
<TransitionRoot leave="transition ease-in duration-100" leave-from="opacity-100" leave-to="opacity-0">
<ComboboxOptions
v-element-visibility="onListVisibilityChange"
:class="[
comboboxOptionsClasses,
{
'bg-white': bgCombobox === 'white',
'bg-space': bgCombobox === 'space',
},
]"
class="absolute z-40 overflow-auto text-base shadow-lg scroll-space focus:outline-none"
>
<span v-if="status === AutocompleteStatus.Loading">
{{ $t('Loading...') }}
</span>
<span v-else-if="status === AutocompleteStatus.Initial">
{{ $t('Start typing...') }}
</span>
<span v-else-if="status === AutocompleteStatus.NoResults && query !== ''">
{{ $t('Nothing found.') }}
</span>
</div>

<ComboboxOption
v-for="suggestion in suggestions" v-else :key="suggestion.id" v-slot="{ selected: optionIsSelected, active }" as="template"
:value="suggestion"
>
<li
class="relative select-none py-1.5 flex items-center transition-colors cursor-pointer" :class="{
'hover:bg-space/[0.06] focus:bg-space/[0.06]': bgCombobox === 'white',
'hover:bg-white/10 focus:bg-white/10': bgCombobox === 'space',
'bg-space/[0.06]': bgCombobox === 'white' && active,
'bg-white/10': bgCombobox === 'space' && active,
'px-6 gap-x-6': size === 'sm',
'px-3 gap-x-2': size === 'md',
<div
v-if="AutocompleteStatus.WithResults !== status" class="relative px-4 py-2 cursor-default select-none" :class="{
'text-space/80': bgCombobox === 'white',
'text-white/80': bgCombobox === 'space',
}"
>
<span
class="block truncate" :class="{
'text-space': bgCombobox === 'white',
'text-white': bgCombobox === 'space',
}" v-html="sanitizeAndHighlightMatches(suggestion.label, suggestion.matchedSubstrings)"
/>
<span
v-if="optionIsSelected" class="absolute inset-y-0 left-0 flex items-center pl-3" :class="{
'text-white':
(active && bgCombobox === 'white') || (!active && bgCombobox === 'space'),
'text-space':
(!active && bgCombobox === 'white') || (active && bgCombobox === 'space'),
<span v-if="status === AutocompleteStatus.Loading">
{{ $t('Loading...') }}
</span>
<span v-else-if="status === AutocompleteStatus.Initial">
{{ $t('Start typing...') }}
</span>
<span v-else-if="status === AutocompleteStatus.Error">
{{ $t('Error loading results.') }}
</span>
<span v-else-if="status === AutocompleteStatus.NoResults && query !== ''">
{{ $t('Nothing found.') }}
</span>
</div>

<ComboboxOption
v-for="suggestion in suggestions" v-else :key="suggestion.id" v-slot="{ selected: optionIsSelected, active }" as="template"
:value="suggestion"
>
<li
class="relative select-none py-1.5 flex items-center transition-colors cursor-pointer" :class="{
'hover:bg-space/[0.06] focus:bg-space/[0.06]': bgCombobox === 'white',
'hover:bg-white/10 focus:bg-white/10': bgCombobox === 'space',
'bg-space/[0.06]': bgCombobox === 'white' && active,
'bg-white/10': bgCombobox === 'space' && active,
'px-6 gap-x-6': size === 'sm',
'px-3 gap-x-2': size === 'md',
}"
/>
</li>
</ComboboxOption>
</ComboboxOptions>
</TransitionRoot>
>
<span
class="block truncate" :class="{
'text-space': bgCombobox === 'white',
'text-white': bgCombobox === 'space',
}" v-html="sanitizeAndHighlightMatches(suggestion.label, suggestion.matchedSubstrings)"
/>
<span
v-if="optionIsSelected" class="absolute inset-y-0 left-0 flex items-center pl-3" :class="{
'text-white':
(active && bgCombobox === 'white') || (!active && bgCombobox === 'space'),
'text-space':
(!active && bgCombobox === 'white') || (active && bgCombobox === 'space'),
}"
/>
</li>
</ComboboxOption>
</ComboboxOptions>
</TransitionRoot>
</Teleport>
</div>
</Combobox>
</template>
37 changes: 5 additions & 32 deletions src/components/elements/InteractionBar.vue
Original file line number Diff line number Diff line change
Expand Up @@ -6,36 +6,11 @@ import { type Suggestion, SuggestionType } from '@/types'
import { useMap } from '@/stores/map'
import { useLocations } from '@/stores/locations'
const emit = defineEmits({
defineEmits({
open: (value: boolean) => value,
})
const { querySearch, suggestions } = useAutocomplete()
function searchBoxOpen(value: boolean) {
value ? showSearchBoxList() : hideSearchBoxList()
emit('open', value)
}
function hideSearchBoxList() {
const searchBoxList = document.querySelector('ul[data-combobox-options]') as HTMLElement | null
if (!searchBoxList)
return
searchBoxList.remove()
}
function showSearchBoxList() {
const searchBoxList = document.querySelector('[data-search-box] ul') as HTMLElement | null
if (!searchBoxList)
return
document.body.appendChild(searchBoxList)
searchBoxList.style.position = 'absolute'
searchBoxList.style.top = '36px'
searchBoxList.style.left = '24px'
searchBoxList.style.zIndex = '1000'
}
const { querySearch, status, suggestions } = useAutocomplete()
function onSelect(suggestion?: Suggestion) {
if (!suggestion)
Expand All @@ -50,18 +25,16 @@ function onSelect(suggestion?: Suggestion) {
useLocations().goToLocation(suggestion.id)
break
}
hideSearchBoxList()
}
</script>

<template>
<header class="relative z-10 flex items-center w-full p-10 py-6 pl-4 pr-6 desktop:p-4 gap-x-2 desktop:gap-x-4">
<img src="@/assets/logo.svg" :alt="$t('Crypto Map logo')" class="h-[22px]">
<SearchBox
:autocomplete="querySearch" :suggestions="suggestions" class="flex-1 w-full " rounded-full
combobox-options-classes="!rounded-t-0 !rounded-b-2xl desktop:w-[320px] desktop:!top-[88px] max-desktop:w-full max-desktop:!left-0 max-desktop:!top-[78px]" size="sm"
:placeholder="$t('Search Map')" data-search-box @open="searchBoxOpen" @selected="onSelect"
:autocomplete="querySearch" :suggestions="suggestions" :status="status" class="flex-1 w-full " rounded-full
combobox-options-classes="rounded-t-0 rounded-b-2xl desktop:w-[320px] desktop:top-[88px] desktop:left-6 max-desktop:w-full max-desktop:!left-[24px] max-desktop:!top-[78px]" size="sm"
:placeholder="$t('Search Map')" @selected="onSelect" @open="$emit('open', $event)"
/>
<CryptoMapModal />
<!-- TODO -->
Expand Down
6 changes: 3 additions & 3 deletions src/components/forms/NewCandidate.vue
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { CURRENCIES } from '@/database'
import { translateCurrency } from '@/translations'
import type { Currency, Suggestion } from '@/types'
const { googleSuggestions, autocompleteGoogleLocations } = useAutocomplete()
const { suggestions, status, querySearch } = useAutocomplete()
const selectedCurrencies = ref<Currency[]>([])
const selectedPlace = ref<Suggestion>()
Expand Down Expand Up @@ -47,8 +47,8 @@ async function onSubmit(captcha: string) {
</template>
<template #form>
<SearchBox
:autocomplete="(query: string) => autocompleteGoogleLocations(query)" :suggestions="googleSuggestions" :label="$t('Find location')"
:placeholder="$t('Type the name of the location')" combobox-options-classes="w-[calc(100%+4px)] -left-0.5 top-unset"
:autocomplete="(query: string) => querySearch(query, true)" :suggestions="suggestions" :status="status" :label="$t('Find location')"
:placeholder="$t('Type the name of the location')" combobox-options-classes="w-[calc(100%+4px)] -left-0.5 top-unset rounded-sm"
bg-combobox="space" input-id="search-input" :allow-clean="false" @selected="(selectedPlace = $event)"
/>

Expand Down
50 changes: 34 additions & 16 deletions src/composables/useAutocomplete.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { useDebounceFn } from '@vueuse/core'
import { computed, ref } from 'vue'
import { ref } from 'vue'
import { detectLanguage } from '@/i18n/i18n-setup'
import { useMap } from '@/stores/map'
import { AutocompleteStatus, type Suggestion, SuggestionType } from '@/types'
Expand All @@ -11,10 +11,9 @@ enum GoogleAutocompleteFor {
}

export function useAutocomplete() {
const status = ref<AutocompleteStatus>(AutocompleteStatus.NoResults)
const dbSuggestions = ref<Suggestion[]>([])
const status = ref<AutocompleteStatus>(AutocompleteStatus.Initial)
const googleSuggestions = ref<Suggestion[]>([])
const suggestions = computed(() => dbSuggestions.value.concat(googleSuggestions.value))
const suggestions = ref<Suggestion[]>([])

// Google Autocomplete
const sessionToken = ref<google.maps.places.AutocompleteSessionToken>()
Expand All @@ -33,52 +32,71 @@ export function useAutocomplete() {
? { locationBias: useMap().map?.getBounds() }
: undefined),
}

const fn = autocompleteFor === GoogleAutocompleteFor.Regions ? 'getQueryPredictions' : 'getPlacePredictions'
await autocompleteService.value?.[fn](request, (predictions, status) => {
if (status !== google.maps.places.PlacesServiceStatus.OK || !predictions)
return

googleSuggestions.value = predictions
const suggestions = predictions
.filter(p => !!p.place_id)
.map(p => ({
id: p.place_id as string,
label: p.description,
type: SuggestionType.GoogleLocation,
matchedSubstrings: p.matched_substrings,
}))

/* eslint-disable no-console */
console.group(`🔍 Google Autocomplete "${query}"`)
console.table(suggestions)
console.groupEnd()
/* eslint-enable no-console */

googleSuggestions.value = suggestions
})
}

async function autocompleteDatabase(query: string) {
const locations = await searchLocations(query)
dbSuggestions.value = locations.map(q => Object.assign(q, { type: SuggestionType.Location }))
return locations.map(q => Object.assign(q, { type: SuggestionType.Location }))
}

async function querySearch(query: string) {
// If we search just for new candidates, we don't need to search in the database
// and we just search locations in Google
async function querySearch(query: string, justNewCandidates = false) {
status.value = AutocompleteStatus.Loading

if (!query) {
dbSuggestions.value = []
googleSuggestions.value = []
suggestions.value = []
return
}

const result = await Promise.allSettled([autocompleteDatabase(query), autocompleteGoogle(query, GoogleAutocompleteFor.Regions)])
const result = justNewCandidates
? await Promise.allSettled([autocompleteGoogle(query, GoogleAutocompleteFor.Location)])
: await Promise.allSettled([autocompleteDatabase(query), autocompleteGoogle(query, GoogleAutocompleteFor.Regions)])

if (result.every(r => r.status === 'rejected')) {
status.value = AutocompleteStatus.Error
return
}
if (justNewCandidates) {
suggestions.value = googleSuggestions.value
}
else {
const db = result[0].status === 'fulfilled' ? result[0].value : [] as Suggestion[]
suggestions.value = [...db!, ...googleSuggestions.value]
}

status.value = suggestions.value.length ? AutocompleteStatus.WithResults : AutocompleteStatus.NoResults
}

const debouncer = useDebounceFn((query: string, justNewCandidates: boolean) => querySearch(query, justNewCandidates), 400)

return {
status,
suggestions,
dbSuggestions,
googleSuggestions,
autocompleteGoogleLocations: useDebounceFn((query: string) => autocompleteGoogle(query, GoogleAutocompleteFor.Location), 300),
querySearch: useDebounceFn((query: string) => querySearch(query), 300),
querySearch(query: string, justNewCandidates = false) {
status.value = AutocompleteStatus.Loading
debouncer(query, justNewCandidates)
},
}
}
5 changes: 2 additions & 3 deletions src/database.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,8 @@ async function fetchDb<T>(url: URL): Promise<T | undefined> {
const data: T = await response.json()

/* eslint-disable no-console */
console.group('🔍 Database GET')
console.log(`Fetching to ${url}`)
console.log(data)
console.group(`🔍 Database "${url.pathname.split('/').pop()}${url.search}"`)
console.table(data)
console.groupEnd()
/* eslint-enable no-console */

Expand Down

0 comments on commit 0533149

Please sign in to comment.