fix(SW-1177): fix localization problems in search history * fix(SW-1177): fix localization problems in search history When saving search history the whole location objects where stored and also used later. This made the list display in different languages if the user previously had search for something in another language. Another issue where that the list where also deduped based on name of the search item, which meant that there could be multiple history items for the same entity and the same ID, e.g. `Gothenburg` and `Göteborg`. Approved-by: Bianca Widstam
298 lines
8.6 KiB
TypeScript
298 lines
8.6 KiB
TypeScript
"use client"
|
|
import Downshift from "downshift"
|
|
import {
|
|
type ChangeEvent,
|
|
type FocusEvent,
|
|
type FormEvent,
|
|
useCallback,
|
|
useEffect,
|
|
useReducer,
|
|
} from "react"
|
|
import { useFormContext, useWatch } from "react-hook-form"
|
|
import { useIntl } from "react-intl"
|
|
|
|
import SkeletonShimmer from "@/components/SkeletonShimmer"
|
|
import Caption from "@/components/TempDesignSystem/Text/Caption"
|
|
import isValidJson from "@/utils/isValidJson"
|
|
|
|
import Input from "../Input"
|
|
import { init, localStorageKey, reducer, sessionStorageKey } from "./reducer"
|
|
import SearchList from "./SearchList"
|
|
|
|
import styles from "./search.module.css"
|
|
|
|
import type { BookingWidgetSchema } from "@/types/components/bookingWidget"
|
|
import {
|
|
ActionType,
|
|
type SetStorageData,
|
|
} from "@/types/components/form/bookingwidget"
|
|
import type { SearchHistoryItem, SearchProps } from "@/types/components/search"
|
|
import type { Location, Locations } from "@/types/trpc/routers/hotel/locations"
|
|
|
|
const name = "search"
|
|
|
|
export default function Search({ locations, handlePressEnter }: SearchProps) {
|
|
const { register, setValue, unregister, getValues } =
|
|
useFormContext<BookingWidgetSchema>()
|
|
const intl = useIntl()
|
|
const value = useWatch({ name })
|
|
const locationString = getValues("location")
|
|
const location =
|
|
locationString && isValidJson(locationString)
|
|
? JSON.parse(decodeURIComponent(locationString))
|
|
: null
|
|
const [state, dispatch] = useReducer(
|
|
reducer,
|
|
{ defaultLocations: locations, initialValue: value },
|
|
init
|
|
)
|
|
const handleMatchLocations = useCallback(
|
|
function (searchValue: string) {
|
|
return locations.filter((location) => {
|
|
return location.name.toLowerCase().includes(searchValue.toLowerCase())
|
|
})
|
|
},
|
|
[locations]
|
|
)
|
|
|
|
function handleClearSearchHistory() {
|
|
localStorage.removeItem(localStorageKey)
|
|
dispatch({ type: ActionType.CLEAR_HISTORY_LOCATIONS })
|
|
}
|
|
|
|
function dispatchInputValue(inputValue: string) {
|
|
if (inputValue) {
|
|
dispatch({
|
|
payload: { search: inputValue },
|
|
type: ActionType.SEARCH_LOCATIONS,
|
|
})
|
|
} else {
|
|
dispatch({ type: ActionType.CLEAR_SEARCH_LOCATIONS })
|
|
}
|
|
}
|
|
|
|
function handleOnChange(
|
|
evt: FormEvent<HTMLInputElement> | ChangeEvent<HTMLInputElement>
|
|
) {
|
|
const newValue = evt.currentTarget.value
|
|
setValue(name, newValue)
|
|
dispatchInputValue(value)
|
|
}
|
|
|
|
function handleOnFocus(evt: FocusEvent<HTMLInputElement>) {
|
|
const searchValue = evt.currentTarget.value
|
|
if (searchValue) {
|
|
const matchingLocations = handleMatchLocations(searchValue)
|
|
if (matchingLocations.length) {
|
|
dispatch({
|
|
payload: { search: searchValue },
|
|
type: ActionType.SEARCH_LOCATIONS,
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
function handleOnSelect(selectedItem: Location | null) {
|
|
if (selectedItem) {
|
|
const stringified = JSON.stringify(selectedItem)
|
|
setValue("location", encodeURIComponent(stringified))
|
|
sessionStorage.setItem(sessionStorageKey, stringified)
|
|
setValue(name, selectedItem.name)
|
|
|
|
const newHistoryItem: SearchHistoryItem = {
|
|
type: selectedItem.type,
|
|
id: selectedItem.id,
|
|
}
|
|
const oldSearchHistoryWithoutTheNew =
|
|
state.searchHistory?.filter(
|
|
(h) => h.type !== newHistoryItem.type || h.id !== newHistoryItem.id
|
|
) ?? []
|
|
const oldHistoryItems = oldSearchHistoryWithoutTheNew.map((h) => ({
|
|
id: h.id,
|
|
type: h.type,
|
|
}))
|
|
|
|
const searchHistory = [newHistoryItem, ...oldHistoryItems]
|
|
localStorage.setItem(localStorageKey, JSON.stringify(searchHistory))
|
|
|
|
const enhancedSearchHistory: Locations = [
|
|
...getEnhancedSearchHistory([newHistoryItem], locations),
|
|
...oldSearchHistoryWithoutTheNew,
|
|
]
|
|
dispatch({
|
|
payload: {
|
|
location: selectedItem,
|
|
searchHistory: enhancedSearchHistory,
|
|
},
|
|
type: ActionType.SELECT_ITEM,
|
|
})
|
|
} else {
|
|
sessionStorage.removeItem(sessionStorageKey)
|
|
}
|
|
}
|
|
|
|
useEffect(() => {
|
|
const searchData = sessionStorage.getItem(sessionStorageKey)
|
|
const searchHistory = localStorage.getItem(localStorageKey)
|
|
const payload: SetStorageData["payload"] = {}
|
|
if (searchData) {
|
|
payload.searchData = JSON.parse(searchData)
|
|
}
|
|
if (searchHistory) {
|
|
payload.searchHistory = getEnhancedSearchHistory(
|
|
JSON.parse(searchHistory),
|
|
locations
|
|
)
|
|
}
|
|
dispatch({
|
|
payload,
|
|
type: ActionType.SET_STORAGE_DATA,
|
|
})
|
|
}, [dispatch, locations])
|
|
|
|
const stayType = state.searchData?.type === "cities" ? "city" : "hotel"
|
|
const stayValue =
|
|
(value === state.searchData?.name &&
|
|
((state.searchData?.type === "cities" && state.searchData?.name) ||
|
|
state.searchData?.id)) ||
|
|
""
|
|
|
|
useEffect(() => {
|
|
if (stayType === "city") {
|
|
unregister("hotel")
|
|
setValue(stayType, stayValue, {
|
|
shouldValidate: true,
|
|
})
|
|
} else {
|
|
unregister("city")
|
|
setValue(stayType, Number(stayValue), {
|
|
shouldValidate: true,
|
|
})
|
|
}
|
|
}, [stayType, stayValue, unregister, setValue])
|
|
|
|
useEffect(() => {
|
|
if (location) {
|
|
sessionStorage.setItem(sessionStorageKey, JSON.stringify(location))
|
|
}
|
|
}, [location])
|
|
|
|
function getLocationLabel(): string {
|
|
if (location?.type === "hotels") {
|
|
return location?.relationships?.city?.name || ""
|
|
}
|
|
if (state.searchData?.type === "hotels") {
|
|
return state.searchData?.relationships?.city?.name || ""
|
|
}
|
|
|
|
return intl.formatMessage({ id: "Where to" })
|
|
}
|
|
|
|
return (
|
|
<Downshift
|
|
initialSelectedItem={state.searchData}
|
|
inputValue={value}
|
|
itemToString={(value) => (value ? value.name : "")}
|
|
onSelect={handleOnSelect}
|
|
onInputValueChange={(inputValue) => dispatchInputValue(inputValue)}
|
|
defaultHighlightedIndex={0}
|
|
>
|
|
{({
|
|
getInputProps,
|
|
getItemProps,
|
|
getLabelProps,
|
|
getMenuProps,
|
|
getRootProps,
|
|
highlightedIndex,
|
|
isOpen,
|
|
openMenu,
|
|
}) => (
|
|
<div className={styles.container}>
|
|
{value ? (
|
|
// Adding hidden input to define hotel or city based on destination selection for basic form submit.
|
|
<input type="hidden" {...register(stayType)} />
|
|
) : null}
|
|
<label {...getLabelProps({ htmlFor: name })} className={styles.label}>
|
|
<Caption
|
|
type="bold"
|
|
color={isOpen ? "uiTextActive" : "red"}
|
|
asChild
|
|
>
|
|
<span>{getLocationLabel()}</span>
|
|
</Caption>
|
|
</label>
|
|
<div {...getRootProps({}, { suppressRefError: true })}>
|
|
<label className={styles.searchInput}>
|
|
<Input
|
|
{...getInputProps({
|
|
id: name,
|
|
onFocus(evt) {
|
|
handleOnFocus(evt)
|
|
openMenu()
|
|
},
|
|
placeholder: intl.formatMessage({
|
|
id: "Destinations & hotels",
|
|
}),
|
|
...register(name, {
|
|
onChange: handleOnChange,
|
|
}),
|
|
onKeyDown: (e) => {
|
|
if (e.key === "Enter" && !isOpen) {
|
|
handlePressEnter()
|
|
}
|
|
},
|
|
type: "search",
|
|
})}
|
|
/>
|
|
</label>
|
|
</div>
|
|
<SearchList
|
|
getItemProps={getItemProps}
|
|
getMenuProps={getMenuProps}
|
|
handleClearSearchHistory={handleClearSearchHistory}
|
|
highlightedIndex={highlightedIndex}
|
|
isOpen={isOpen}
|
|
locations={state.locations}
|
|
search={state.search}
|
|
searchHistory={state.searchHistory}
|
|
/>
|
|
</div>
|
|
)}
|
|
</Downshift>
|
|
)
|
|
}
|
|
|
|
export function SearchSkeleton() {
|
|
const intl = useIntl()
|
|
return (
|
|
<div className={styles.container}>
|
|
<div className={styles.label}>
|
|
<Caption type="bold" color="red" asChild>
|
|
<span>{intl.formatMessage({ id: "Where to" })}</span>
|
|
</Caption>
|
|
</div>
|
|
<div>
|
|
<SkeletonShimmer width={"100%"} />
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
/**
|
|
* Takes a stored search history and returns the same history, but with the same
|
|
* data and the same format as the complete location objects
|
|
*/
|
|
function getEnhancedSearchHistory(
|
|
searchHistory: SearchHistoryItem[],
|
|
locations: Locations
|
|
): Locations {
|
|
return searchHistory
|
|
.map((historyItem) =>
|
|
locations.find(
|
|
(location) =>
|
|
location.type === historyItem.type && location.id === historyItem.id
|
|
)
|
|
)
|
|
.filter((r) => !!r) as Locations
|
|
}
|