Merged in feature/autocomplete-search (pull request #1725)

Feature/autocomplete search

* wip autocomplete search

* add skeletons to loading

* Using aumlauts/accents when searching will still give results
remove unused reducer
sort autocomplete results

* remove testcode

* Add tests for autocomplete

* cleanup tests

* use node@20

* use node 22

* use node22

* merge
fix: search button outside of viewport

* merge

* remove more unused code

* fix: error message when empty search field in booking widget

* fix: don't display empty white box when search field is empty and no searchHistory is present

* merge

* fix: set height of shimmer for search skeleton

* rename autocomplete trpc -> destinationsAutocomplete

* more accute cache key naming

* fix: able to control wether bookingwidget is visible on startPage
fix: sticky booking widget under alert

* remove unused code

* fix: skeletons
fix: error overlay on search startpage

* remove extra .nvmrc

* merge


Approved-by: Linus Flood
This commit is contained in:
Joakim Jäderberg
2025-04-09 10:43:08 +00:00
parent 7e6abe1f03
commit da07e8a458
40 changed files with 1024 additions and 666 deletions

View File

@@ -0,0 +1,122 @@
import { z } from "zod"
import { Lang } from "@/constants/languages"
import { safeProtectedServiceProcedure } from "@/server/trpc"
import { getCacheClient } from "@/services/dataCache"
import { getCitiesByCountry, getCountries, getLocations } from "../hotels/utils"
import { filterLocationByQuery } from "./util/filterLocationByQuery"
import { mapLocationToAutoCompleteLocation } from "./util/mapLocationToAutoCompleteLocation"
import { sortAutocompleteLocations } from "./util/sortAutocompleteLocations"
import type { AutoCompleteLocation } from "./schema"
const destinationsAutoCompleteInputSchema = z.object({
query: z.string(),
selectedHotelId: z.string().optional(),
selectedCity: z.string().optional(),
lang: z.nativeEnum(Lang),
})
type DestinationsAutoCompleteOutput = {
hits: {
hotels: AutoCompleteLocation[]
cities: AutoCompleteLocation[]
}
currentSelection: {
hotel: (AutoCompleteLocation & { type: "hotels" }) | null
city: (AutoCompleteLocation & { type: "cities" }) | null
}
}
export const getDestinationsAutoCompleteRoute = safeProtectedServiceProcedure
.input(destinationsAutoCompleteInputSchema)
.query(async ({ ctx, input }): Promise<DestinationsAutoCompleteOutput> => {
const cacheClient = await getCacheClient()
const locations: AutoCompleteLocation[] = await cacheClient.cacheOrGet(
`autocomplete:destinations:locations:${input.lang}`,
async () => {
const lang = input.lang || ctx.lang
const countries = await getCountries({
lang: lang,
serviceToken: ctx.serviceToken,
})
if (!countries) {
throw new Error("Unable to fetch countries")
}
const countryNames = countries.data.map((country) => country.name)
const citiesByCountry = await getCitiesByCountry({
countries: countryNames,
serviceToken: ctx.serviceToken,
lang,
})
const locations = await getLocations({
lang: lang,
serviceToken: ctx.serviceToken,
citiesByCountry: citiesByCountry,
})
return locations
.map(mapLocationToAutoCompleteLocation)
.filter(isDefined)
},
"1d"
)
const filteredLocations = locations.filter((location) =>
filterLocationByQuery({ location, query: input.query })
)
const selectedHotel = locations.find(
(location) =>
location.type === "hotels" && location.id === input.selectedHotelId
)
const selectedCity = locations.find(
(location) =>
location.type === "cities" && location.name === input.selectedCity
)
const sortedCities = sortAutocompleteLocations(
filteredLocations.filter(isCity),
input.query
)
const sortedHotels = sortAutocompleteLocations(
filteredLocations.filter(isHotel),
input.query
)
return {
hits: {
cities: sortedCities,
hotels: sortedHotels,
},
currentSelection: {
city: isCity(selectedCity) ? selectedCity : null,
hotel: isHotel(selectedHotel) ? selectedHotel : null,
},
}
})
function isHotel(
location: AutoCompleteLocation | null | undefined
): location is AutoCompleteLocation & { type: "hotels" } {
return !!location && location.type === "hotels"
}
function isCity(
location: AutoCompleteLocation | null | undefined
): location is AutoCompleteLocation & { type: "cities" } {
return !!location && location.type === "cities"
}
function isDefined(
value: AutoCompleteLocation | null | undefined
): value is AutoCompleteLocation {
return !!value
}