Merged in fix/SW-2457-destination-page-search-for-countries (pull request #1873)
* fix: able to search for countries on destinationpage * fix: filter out only desired types when using autocomplete Approved-by: Linus Flood
This commit is contained in:
@@ -57,6 +57,7 @@ export default function BookingWidgetClient({
|
|||||||
{
|
{
|
||||||
lang,
|
lang,
|
||||||
query: "",
|
query: "",
|
||||||
|
includeTypes: ["hotels", "cities"],
|
||||||
selectedHotelId: params.hotelId,
|
selectedHotelId: params.hotelId,
|
||||||
selectedCity: params.city,
|
selectedCity: params.city,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ export function JumpTo() {
|
|||||||
return (
|
return (
|
||||||
<FormProvider {...methods}>
|
<FormProvider {...methods}>
|
||||||
<Search
|
<Search
|
||||||
|
includeTypes={["cities", "hotels", "countries"]}
|
||||||
variant="rounded"
|
variant="rounded"
|
||||||
handlePressEnter={() => {
|
handlePressEnter={() => {
|
||||||
void 0
|
void 0
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ export default function SearchList({
|
|||||||
isOpen,
|
isOpen,
|
||||||
search,
|
search,
|
||||||
searchHistory,
|
searchHistory,
|
||||||
|
includeTypes,
|
||||||
}: SearchListProps) {
|
}: SearchListProps) {
|
||||||
const lang = useLang()
|
const lang = useLang()
|
||||||
const intl = useIntl()
|
const intl = useIntl()
|
||||||
@@ -53,10 +54,14 @@ export default function SearchList({
|
|||||||
isPending,
|
isPending,
|
||||||
isError,
|
isError,
|
||||||
} = trpc.autocomplete.destinations.useQuery(
|
} = trpc.autocomplete.destinations.useQuery(
|
||||||
{ query: debouncedSearch, lang },
|
{ query: debouncedSearch, lang, includeTypes },
|
||||||
{ enabled: autocompleteQueryEnabled }
|
{ enabled: autocompleteQueryEnabled }
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const typeFilteredSearchHistory = searchHistory?.filter((item) => {
|
||||||
|
return includeTypes.includes(item.type)
|
||||||
|
})
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
clearErrors(searchInputName)
|
clearErrors(searchInputName)
|
||||||
}, [search, clearErrors, searchInputName])
|
}, [search, clearErrors, searchInputName])
|
||||||
@@ -159,7 +164,7 @@ export default function SearchList({
|
|||||||
"We couldn't find a matching location for your search.",
|
"We couldn't find a matching location for your search.",
|
||||||
})}
|
})}
|
||||||
</Body>
|
</Body>
|
||||||
{searchHistory && searchHistory.length > 0 && (
|
{typeFilteredSearchHistory && typeFilteredSearchHistory.length > 0 && (
|
||||||
<>
|
<>
|
||||||
<Divider className={styles.noResultsDivider} color="beige" />
|
<Divider className={styles.noResultsDivider} color="beige" />
|
||||||
<Footnote
|
<Footnote
|
||||||
@@ -174,7 +179,7 @@ export default function SearchList({
|
|||||||
<List
|
<List
|
||||||
getItemProps={getItemProps}
|
getItemProps={getItemProps}
|
||||||
highlightedIndex={highlightedIndex}
|
highlightedIndex={highlightedIndex}
|
||||||
locations={searchHistory}
|
locations={typeFilteredSearchHistory}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Divider className={styles.divider} color="beige" />
|
<Divider className={styles.divider} color="beige" />
|
||||||
@@ -182,7 +187,7 @@ export default function SearchList({
|
|||||||
getItemProps={getItemProps}
|
getItemProps={getItemProps}
|
||||||
handleClearSearchHistory={handleClearSearchHistory}
|
handleClearSearchHistory={handleClearSearchHistory}
|
||||||
highlightedIndex={highlightedIndex}
|
highlightedIndex={highlightedIndex}
|
||||||
index={searchHistory.length}
|
index={typeFilteredSearchHistory.length}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
@@ -190,7 +195,8 @@ export default function SearchList({
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const displaySearchHistory = !debouncedSearch && searchHistory?.length
|
const displaySearchHistory =
|
||||||
|
!debouncedSearch && typeFilteredSearchHistory?.length
|
||||||
if (displaySearchHistory) {
|
if (displaySearchHistory) {
|
||||||
return (
|
return (
|
||||||
<Dialog getMenuProps={getMenuProps}>
|
<Dialog getMenuProps={getMenuProps}>
|
||||||
@@ -202,14 +208,14 @@ export default function SearchList({
|
|||||||
<List
|
<List
|
||||||
getItemProps={getItemProps}
|
getItemProps={getItemProps}
|
||||||
highlightedIndex={highlightedIndex}
|
highlightedIndex={highlightedIndex}
|
||||||
locations={searchHistory}
|
locations={typeFilteredSearchHistory}
|
||||||
/>
|
/>
|
||||||
<Divider className={styles.divider} color="beige" />
|
<Divider className={styles.divider} color="beige" />
|
||||||
<ClearSearchButton
|
<ClearSearchButton
|
||||||
getItemProps={getItemProps}
|
getItemProps={getItemProps}
|
||||||
handleClearSearchHistory={handleClearSearchHistory}
|
handleClearSearchHistory={handleClearSearchHistory}
|
||||||
highlightedIndex={highlightedIndex}
|
highlightedIndex={highlightedIndex}
|
||||||
index={searchHistory.length}
|
index={typeFilteredSearchHistory.length}
|
||||||
/>
|
/>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
)
|
)
|
||||||
@@ -224,6 +230,15 @@ export default function SearchList({
|
|||||||
<List
|
<List
|
||||||
getItemProps={getItemProps}
|
getItemProps={getItemProps}
|
||||||
highlightedIndex={highlightedIndex}
|
highlightedIndex={highlightedIndex}
|
||||||
|
label={intl.formatMessage({
|
||||||
|
defaultMessage: "Countries",
|
||||||
|
})}
|
||||||
|
locations={autocompleteData?.hits.countries ?? []}
|
||||||
|
/>
|
||||||
|
<List
|
||||||
|
getItemProps={getItemProps}
|
||||||
|
highlightedIndex={highlightedIndex}
|
||||||
|
initialIndex={autocompleteData?.hits.countries.length ?? 0}
|
||||||
label={intl.formatMessage({
|
label={intl.formatMessage({
|
||||||
defaultMessage: "Cities",
|
defaultMessage: "Cities",
|
||||||
})}
|
})}
|
||||||
@@ -232,7 +247,10 @@ export default function SearchList({
|
|||||||
<List
|
<List
|
||||||
getItemProps={getItemProps}
|
getItemProps={getItemProps}
|
||||||
highlightedIndex={highlightedIndex}
|
highlightedIndex={highlightedIndex}
|
||||||
initialIndex={autocompleteData?.hits.cities.length}
|
initialIndex={
|
||||||
|
(autocompleteData?.hits.countries.length ?? 0) +
|
||||||
|
(autocompleteData?.hits.cities.length ?? 0)
|
||||||
|
}
|
||||||
label={intl.formatMessage({
|
label={intl.formatMessage({
|
||||||
defaultMessage: "Hotels",
|
defaultMessage: "Hotels",
|
||||||
})}
|
})}
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ interface SearchProps {
|
|||||||
variant?: "rounded" | "default"
|
variant?: "rounded" | "default"
|
||||||
withSearchButton?: boolean
|
withSearchButton?: boolean
|
||||||
selectOnBlur?: boolean
|
selectOnBlur?: boolean
|
||||||
|
includeTypes: ("cities" | "hotels" | "countries")[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Search({
|
export function Search({
|
||||||
@@ -37,6 +38,7 @@ export function Search({
|
|||||||
variant,
|
variant,
|
||||||
withSearchButton = false,
|
withSearchButton = false,
|
||||||
selectOnBlur = false,
|
selectOnBlur = false,
|
||||||
|
includeTypes,
|
||||||
}: SearchProps) {
|
}: SearchProps) {
|
||||||
const { register, setValue, setFocus } = useFormContext()
|
const { register, setValue, setFocus } = useFormContext()
|
||||||
const intl = useIntl()
|
const intl = useIntl()
|
||||||
@@ -205,6 +207,7 @@ export function Search({
|
|||||||
search={searchTerm}
|
search={searchTerm}
|
||||||
searchHistory={searchHistory}
|
searchHistory={searchHistory}
|
||||||
searchInputName={SEARCH_TERM_NAME}
|
searchInputName={SEARCH_TERM_NAME}
|
||||||
|
includeTypes={includeTypes}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -45,6 +45,7 @@ export default function FormContent({
|
|||||||
handlePressEnter={onSubmit}
|
handlePressEnter={onSubmit}
|
||||||
selectOnBlur={true}
|
selectOnBlur={true}
|
||||||
inputName="search"
|
inputName="search"
|
||||||
|
includeTypes={["cities", "hotels"]}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.when}>
|
<div className={styles.when}>
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { getCacheClient } from "@/services/dataCache"
|
|||||||
import { safeTry } from "@/utils/safeTry"
|
import { safeTry } from "@/utils/safeTry"
|
||||||
|
|
||||||
import { getCityPageUrls } from "../contentstack/destinationCityPage/utils"
|
import { getCityPageUrls } from "../contentstack/destinationCityPage/utils"
|
||||||
|
import { getCountryPageUrls } from "../contentstack/destinationCountryPage/utils"
|
||||||
import { getHotelPageUrls } from "../contentstack/hotelPage/utils"
|
import { getHotelPageUrls } from "../contentstack/hotelPage/utils"
|
||||||
import { getCitiesByCountry, getCountries, getLocations } from "../hotels/utils"
|
import { getCitiesByCountry, getCountries, getLocations } from "../hotels/utils"
|
||||||
import { filterAndCategorizeAutoComplete } from "./util/filterAndCategorizeAutoComplete"
|
import { filterAndCategorizeAutoComplete } from "./util/filterAndCategorizeAutoComplete"
|
||||||
@@ -20,12 +21,14 @@ const destinationsAutoCompleteInputSchema = z.object({
|
|||||||
selectedHotelId: z.string().optional(),
|
selectedHotelId: z.string().optional(),
|
||||||
selectedCity: z.string().optional(),
|
selectedCity: z.string().optional(),
|
||||||
lang: z.nativeEnum(Lang),
|
lang: z.nativeEnum(Lang),
|
||||||
|
includeTypes: z.array(z.enum(["hotels", "cities", "countries"])),
|
||||||
})
|
})
|
||||||
|
|
||||||
type DestinationsAutoCompleteOutput = {
|
type DestinationsAutoCompleteOutput = {
|
||||||
hits: {
|
hits: {
|
||||||
hotels: AutoCompleteLocation[]
|
hotels: AutoCompleteLocation[]
|
||||||
cities: AutoCompleteLocation[]
|
cities: AutoCompleteLocation[]
|
||||||
|
countries: AutoCompleteLocation[]
|
||||||
}
|
}
|
||||||
currentSelection: {
|
currentSelection: {
|
||||||
hotel: (AutoCompleteLocation & { type: "hotels" }) | null
|
hotel: (AutoCompleteLocation & { type: "hotels" }) | null
|
||||||
@@ -39,12 +42,13 @@ export const getDestinationsAutoCompleteRoute = safeProtectedServiceProcedure
|
|||||||
const cacheClient = await getCacheClient()
|
const cacheClient = await getCacheClient()
|
||||||
|
|
||||||
const lang = input.lang || ctx.lang
|
const lang = input.lang || ctx.lang
|
||||||
|
|
||||||
const locations: AutoCompleteLocation[] = await cacheClient.cacheOrGet(
|
const locations: AutoCompleteLocation[] = await cacheClient.cacheOrGet(
|
||||||
`autocomplete:destinations:locations:${lang}`,
|
`autocomplete:destinations:locations:${lang}`,
|
||||||
async () => {
|
async () => {
|
||||||
const hotelUrlsPromise = safeTry(getHotelPageUrls(lang))
|
const hotelUrlsPromise = safeTry(getHotelPageUrls(lang))
|
||||||
const cityUrlsPromise = safeTry(getCityPageUrls(lang))
|
const cityUrlsPromise = safeTry(getCityPageUrls(lang))
|
||||||
|
const countryUrlsPromise = safeTry(getCountryPageUrls(lang))
|
||||||
const countries = await getCountries({
|
const countries = await getCountries({
|
||||||
lang: lang,
|
lang: lang,
|
||||||
serviceToken: ctx.serviceToken,
|
serviceToken: ctx.serviceToken,
|
||||||
@@ -53,6 +57,7 @@ export const getDestinationsAutoCompleteRoute = safeProtectedServiceProcedure
|
|||||||
if (!countries) {
|
if (!countries) {
|
||||||
throw new Error("Unable to fetch countries")
|
throw new Error("Unable to fetch countries")
|
||||||
}
|
}
|
||||||
|
|
||||||
const countryNames = countries.data.map((country) => country.name)
|
const countryNames = countries.data.map((country) => country.name)
|
||||||
const citiesByCountry = await getCitiesByCountry({
|
const citiesByCountry = await getCitiesByCountry({
|
||||||
countries: countryNames,
|
countries: countryNames,
|
||||||
@@ -68,12 +73,20 @@ export const getDestinationsAutoCompleteRoute = safeProtectedServiceProcedure
|
|||||||
|
|
||||||
const [hotelUrls, hotelUrlsError] = await hotelUrlsPromise
|
const [hotelUrls, hotelUrlsError] = await hotelUrlsPromise
|
||||||
const [cityUrls, cityUrlsError] = await cityUrlsPromise
|
const [cityUrls, cityUrlsError] = await cityUrlsPromise
|
||||||
|
const [countryUrls, countryUrlsError] = await countryUrlsPromise
|
||||||
|
|
||||||
if (hotelUrlsError || cityUrlsError || !hotelUrls || !cityUrls) {
|
if (
|
||||||
|
hotelUrlsError ||
|
||||||
|
cityUrlsError ||
|
||||||
|
countryUrlsError ||
|
||||||
|
!hotelUrls ||
|
||||||
|
!cityUrls ||
|
||||||
|
!countryUrls
|
||||||
|
) {
|
||||||
throw new Error("Unable to fetch location URLs")
|
throw new Error("Unable to fetch location URLs")
|
||||||
}
|
}
|
||||||
|
|
||||||
return locations
|
const hotelsAndCities = locations
|
||||||
.map((location) => {
|
.map((location) => {
|
||||||
let url: string | undefined
|
let url: string | undefined
|
||||||
|
|
||||||
@@ -96,13 +109,31 @@ export const getDestinationsAutoCompleteRoute = safeProtectedServiceProcedure
|
|||||||
})
|
})
|
||||||
.map(mapLocationToAutoCompleteLocation)
|
.map(mapLocationToAutoCompleteLocation)
|
||||||
.filter(isDefined)
|
.filter(isDefined)
|
||||||
|
|
||||||
|
const countryAutoCompleteLocations = countries.data.map((country) => {
|
||||||
|
const url = countryUrls.find(
|
||||||
|
(c) => c.country && c.country === country.name
|
||||||
|
)?.url
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: country.id,
|
||||||
|
name: country.name,
|
||||||
|
type: "countries",
|
||||||
|
searchTokens: [country.name],
|
||||||
|
destination: "",
|
||||||
|
url,
|
||||||
|
} satisfies AutoCompleteLocation
|
||||||
|
})
|
||||||
|
|
||||||
|
return [...hotelsAndCities, ...countryAutoCompleteLocations]
|
||||||
},
|
},
|
||||||
"1d"
|
"1d"
|
||||||
)
|
)
|
||||||
|
|
||||||
const hits = filterAndCategorizeAutoComplete({
|
const hits = filterAndCategorizeAutoComplete({
|
||||||
locations,
|
locations: locations,
|
||||||
query: input.query,
|
query: input.query,
|
||||||
|
includeTypes: input.includeTypes,
|
||||||
})
|
})
|
||||||
|
|
||||||
const selectedHotel = locations.find(
|
const selectedHotel = locations.find(
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { z } from "zod"
|
|||||||
export const autoCompleteLocationSchema = z.object({
|
export const autoCompleteLocationSchema = z.object({
|
||||||
id: z.string(),
|
id: z.string(),
|
||||||
name: z.string(),
|
name: z.string(),
|
||||||
type: z.enum(["cities", "hotels"]),
|
type: z.enum(["cities", "hotels", "countries"]),
|
||||||
searchTokens: z.array(z.string()),
|
searchTokens: z.array(z.string()),
|
||||||
destination: z.string(),
|
destination: z.string(),
|
||||||
url: z.string().optional(),
|
url: z.string().optional(),
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ export type DestinationsAutoCompleteOutput = {
|
|||||||
hits: {
|
hits: {
|
||||||
hotels: AutoCompleteLocation[]
|
hotels: AutoCompleteLocation[]
|
||||||
cities: AutoCompleteLocation[]
|
cities: AutoCompleteLocation[]
|
||||||
|
countries: AutoCompleteLocation[]
|
||||||
}
|
}
|
||||||
currentSelection: {
|
currentSelection: {
|
||||||
hotel: (AutoCompleteLocation & { type: "hotels" }) | null
|
hotel: (AutoCompleteLocation & { type: "hotels" }) | null
|
||||||
@@ -16,27 +17,50 @@ export type DestinationsAutoCompleteOutput = {
|
|||||||
export function filterAndCategorizeAutoComplete({
|
export function filterAndCategorizeAutoComplete({
|
||||||
locations,
|
locations,
|
||||||
query,
|
query,
|
||||||
|
includeTypes,
|
||||||
}: {
|
}: {
|
||||||
locations: AutoCompleteLocation[]
|
locations: AutoCompleteLocation[]
|
||||||
query: string
|
query: string
|
||||||
|
includeTypes: ("cities" | "hotels" | "countries")[]
|
||||||
}) {
|
}) {
|
||||||
const rankedLocations = filterAutoCompleteLocations(locations, query)
|
const rankedLocations = filterAutoCompleteLocations(locations, query)
|
||||||
|
|
||||||
const sortedCities = rankedLocations.filter(isCity)
|
const sortedCities = rankedLocations.filter(
|
||||||
const sortedHotels = rankedLocations.filter(isHotel)
|
(loc) => shouldIncludeType(includeTypes, loc) && isCity(loc)
|
||||||
|
)
|
||||||
|
const sortedHotels = rankedLocations.filter(
|
||||||
|
(loc) => shouldIncludeType(includeTypes, loc) && isHotel(loc)
|
||||||
|
)
|
||||||
|
const sortedCountries = rankedLocations.filter(
|
||||||
|
(loc) => shouldIncludeType(includeTypes, loc) && isCountry(loc)
|
||||||
|
)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
cities: sortedCities,
|
cities: sortedCities,
|
||||||
hotels: sortedHotels,
|
hotels: sortedHotels,
|
||||||
|
countries: sortedCountries,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function shouldIncludeType(
|
||||||
|
includedTypes: ("cities" | "hotels" | "countries")[],
|
||||||
|
location: AutoCompleteLocation
|
||||||
|
) {
|
||||||
|
return includedTypes.includes(location.type)
|
||||||
|
}
|
||||||
|
|
||||||
function isHotel(
|
function isHotel(
|
||||||
location: AutoCompleteLocation | null | undefined
|
location: AutoCompleteLocation | null | undefined
|
||||||
): location is AutoCompleteLocation & { type: "hotels" } {
|
): location is AutoCompleteLocation & { type: "hotels" } {
|
||||||
return !!location && location.type === "hotels"
|
return !!location && location.type === "hotels"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isCountry(
|
||||||
|
location: AutoCompleteLocation | null | undefined
|
||||||
|
): location is AutoCompleteLocation & { type: "countries" } {
|
||||||
|
return !!location && location.type === "countries"
|
||||||
|
}
|
||||||
|
|
||||||
function isCity(
|
function isCity(
|
||||||
location: AutoCompleteLocation | null | undefined
|
location: AutoCompleteLocation | null | undefined
|
||||||
): location is AutoCompleteLocation & { type: "cities" } {
|
): location is AutoCompleteLocation & { type: "cities" } {
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ export interface SearchListProps {
|
|||||||
searchInputName: string
|
searchInputName: string
|
||||||
search: string
|
search: string
|
||||||
searchHistory: AutoCompleteLocation[] | null
|
searchHistory: AutoCompleteLocation[] | null
|
||||||
|
includeTypes: ("cities" | "hotels" | "countries")[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface DialogProps
|
export interface DialogProps
|
||||||
|
|||||||
Reference in New Issue
Block a user