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:
122
apps/scandic-web/server/routers/autocomplete/destinations.ts
Normal file
122
apps/scandic-web/server/routers/autocomplete/destinations.ts
Normal 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
|
||||
}
|
||||
Reference in New Issue
Block a user