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:
Joakim Jäderberg
2025-04-29 06:19:25 +00:00
parent c1505ce50e
commit 52c99e8767
9 changed files with 95 additions and 15 deletions

View File

@@ -57,6 +57,7 @@ export default function BookingWidgetClient({
{
lang,
query: "",
includeTypes: ["hotels", "cities"],
selectedHotelId: params.hotelId,
selectedCity: params.city,
},

View File

@@ -31,6 +31,7 @@ export function JumpTo() {
return (
<FormProvider {...methods}>
<Search
includeTypes={["cities", "hotels", "countries"]}
variant="rounded"
handlePressEnter={() => {
void 0

View File

@@ -31,6 +31,7 @@ export default function SearchList({
isOpen,
search,
searchHistory,
includeTypes,
}: SearchListProps) {
const lang = useLang()
const intl = useIntl()
@@ -53,10 +54,14 @@ export default function SearchList({
isPending,
isError,
} = trpc.autocomplete.destinations.useQuery(
{ query: debouncedSearch, lang },
{ query: debouncedSearch, lang, includeTypes },
{ enabled: autocompleteQueryEnabled }
)
const typeFilteredSearchHistory = searchHistory?.filter((item) => {
return includeTypes.includes(item.type)
})
useEffect(() => {
clearErrors(searchInputName)
}, [search, clearErrors, searchInputName])
@@ -159,7 +164,7 @@ export default function SearchList({
"We couldn't find a matching location for your search.",
})}
</Body>
{searchHistory && searchHistory.length > 0 && (
{typeFilteredSearchHistory && typeFilteredSearchHistory.length > 0 && (
<>
<Divider className={styles.noResultsDivider} color="beige" />
<Footnote
@@ -174,7 +179,7 @@ export default function SearchList({
<List
getItemProps={getItemProps}
highlightedIndex={highlightedIndex}
locations={searchHistory}
locations={typeFilteredSearchHistory}
/>
<Divider className={styles.divider} color="beige" />
@@ -182,7 +187,7 @@ export default function SearchList({
getItemProps={getItemProps}
handleClearSearchHistory={handleClearSearchHistory}
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) {
return (
<Dialog getMenuProps={getMenuProps}>
@@ -202,14 +208,14 @@ export default function SearchList({
<List
getItemProps={getItemProps}
highlightedIndex={highlightedIndex}
locations={searchHistory}
locations={typeFilteredSearchHistory}
/>
<Divider className={styles.divider} color="beige" />
<ClearSearchButton
getItemProps={getItemProps}
handleClearSearchHistory={handleClearSearchHistory}
highlightedIndex={highlightedIndex}
index={searchHistory.length}
index={typeFilteredSearchHistory.length}
/>
</Dialog>
)
@@ -224,6 +230,15 @@ export default function SearchList({
<List
getItemProps={getItemProps}
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({
defaultMessage: "Cities",
})}
@@ -232,7 +247,10 @@ export default function SearchList({
<List
getItemProps={getItemProps}
highlightedIndex={highlightedIndex}
initialIndex={autocompleteData?.hits.cities.length}
initialIndex={
(autocompleteData?.hits.countries.length ?? 0) +
(autocompleteData?.hits.cities.length ?? 0)
}
label={intl.formatMessage({
defaultMessage: "Hotels",
})}

View File

@@ -28,6 +28,7 @@ interface SearchProps {
variant?: "rounded" | "default"
withSearchButton?: boolean
selectOnBlur?: boolean
includeTypes: ("cities" | "hotels" | "countries")[]
}
export function Search({
@@ -37,6 +38,7 @@ export function Search({
variant,
withSearchButton = false,
selectOnBlur = false,
includeTypes,
}: SearchProps) {
const { register, setValue, setFocus } = useFormContext()
const intl = useIntl()
@@ -205,6 +207,7 @@ export function Search({
search={searchTerm}
searchHistory={searchHistory}
searchInputName={SEARCH_TERM_NAME}
includeTypes={includeTypes}
/>
</div>
)}

View File

@@ -45,6 +45,7 @@ export default function FormContent({
handlePressEnter={onSubmit}
selectOnBlur={true}
inputName="search"
includeTypes={["cities", "hotels"]}
/>
</div>
<div className={styles.when}>

View File

@@ -8,6 +8,7 @@ import { getCacheClient } from "@/services/dataCache"
import { safeTry } from "@/utils/safeTry"
import { getCityPageUrls } from "../contentstack/destinationCityPage/utils"
import { getCountryPageUrls } from "../contentstack/destinationCountryPage/utils"
import { getHotelPageUrls } from "../contentstack/hotelPage/utils"
import { getCitiesByCountry, getCountries, getLocations } from "../hotels/utils"
import { filterAndCategorizeAutoComplete } from "./util/filterAndCategorizeAutoComplete"
@@ -20,12 +21,14 @@ const destinationsAutoCompleteInputSchema = z.object({
selectedHotelId: z.string().optional(),
selectedCity: z.string().optional(),
lang: z.nativeEnum(Lang),
includeTypes: z.array(z.enum(["hotels", "cities", "countries"])),
})
type DestinationsAutoCompleteOutput = {
hits: {
hotels: AutoCompleteLocation[]
cities: AutoCompleteLocation[]
countries: AutoCompleteLocation[]
}
currentSelection: {
hotel: (AutoCompleteLocation & { type: "hotels" }) | null
@@ -39,12 +42,13 @@ export const getDestinationsAutoCompleteRoute = safeProtectedServiceProcedure
const cacheClient = await getCacheClient()
const lang = input.lang || ctx.lang
const locations: AutoCompleteLocation[] = await cacheClient.cacheOrGet(
`autocomplete:destinations:locations:${lang}`,
async () => {
const hotelUrlsPromise = safeTry(getHotelPageUrls(lang))
const cityUrlsPromise = safeTry(getCityPageUrls(lang))
const countryUrlsPromise = safeTry(getCountryPageUrls(lang))
const countries = await getCountries({
lang: lang,
serviceToken: ctx.serviceToken,
@@ -53,6 +57,7 @@ export const getDestinationsAutoCompleteRoute = safeProtectedServiceProcedure
if (!countries) {
throw new Error("Unable to fetch countries")
}
const countryNames = countries.data.map((country) => country.name)
const citiesByCountry = await getCitiesByCountry({
countries: countryNames,
@@ -68,12 +73,20 @@ export const getDestinationsAutoCompleteRoute = safeProtectedServiceProcedure
const [hotelUrls, hotelUrlsError] = await hotelUrlsPromise
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")
}
return locations
const hotelsAndCities = locations
.map((location) => {
let url: string | undefined
@@ -96,13 +109,31 @@ export const getDestinationsAutoCompleteRoute = safeProtectedServiceProcedure
})
.map(mapLocationToAutoCompleteLocation)
.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"
)
const hits = filterAndCategorizeAutoComplete({
locations,
locations: locations,
query: input.query,
includeTypes: input.includeTypes,
})
const selectedHotel = locations.find(

View File

@@ -3,7 +3,7 @@ import { z } from "zod"
export const autoCompleteLocationSchema = z.object({
id: z.string(),
name: z.string(),
type: z.enum(["cities", "hotels"]),
type: z.enum(["cities", "hotels", "countries"]),
searchTokens: z.array(z.string()),
destination: z.string(),
url: z.string().optional(),

View File

@@ -6,6 +6,7 @@ export type DestinationsAutoCompleteOutput = {
hits: {
hotels: AutoCompleteLocation[]
cities: AutoCompleteLocation[]
countries: AutoCompleteLocation[]
}
currentSelection: {
hotel: (AutoCompleteLocation & { type: "hotels" }) | null
@@ -16,27 +17,50 @@ export type DestinationsAutoCompleteOutput = {
export function filterAndCategorizeAutoComplete({
locations,
query,
includeTypes,
}: {
locations: AutoCompleteLocation[]
query: string
includeTypes: ("cities" | "hotels" | "countries")[]
}) {
const rankedLocations = filterAutoCompleteLocations(locations, query)
const sortedCities = rankedLocations.filter(isCity)
const sortedHotels = rankedLocations.filter(isHotel)
const sortedCities = rankedLocations.filter(
(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 {
cities: sortedCities,
hotels: sortedHotels,
countries: sortedCountries,
}
}
function shouldIncludeType(
includedTypes: ("cities" | "hotels" | "countries")[],
location: AutoCompleteLocation
) {
return includedTypes.includes(location.type)
}
function isHotel(
location: AutoCompleteLocation | null | undefined
): location is AutoCompleteLocation & { 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(
location: AutoCompleteLocation | null | undefined
): location is AutoCompleteLocation & { type: "cities" } {

View File

@@ -24,6 +24,7 @@ export interface SearchListProps {
searchInputName: string
search: string
searchHistory: AutoCompleteLocation[] | null
includeTypes: ("cities" | "hotels" | "countries")[]
}
export interface DialogProps