fix(i18n): validated English messages Approved-by: Bianca Widstam Approved-by: Christian Andolf
297 lines
8.7 KiB
TypeScript
297 lines
8.7 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(decodeURIComponent(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 {
|
|
const fallbackLabel = intl.formatMessage({ id: "Where to?" })
|
|
if (location?.type === "hotels") {
|
|
return location?.relationships?.city?.name || fallbackLabel
|
|
}
|
|
if (state.searchData?.type === "hotels") {
|
|
return state.searchData?.relationships?.city?.name || fallbackLabel
|
|
}
|
|
return fallbackLabel
|
|
}
|
|
|
|
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: "Hotels & Destinations",
|
|
}),
|
|
...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
|
|
}
|