182 lines
5.2 KiB
TypeScript
182 lines
5.2 KiB
TypeScript
"use client"
|
|
import Downshift from "downshift"
|
|
import {
|
|
ChangeEvent,
|
|
FocusEvent,
|
|
FormEvent,
|
|
useCallback,
|
|
useReducer,
|
|
} from "react"
|
|
import { useFormContext, useWatch } from "react-hook-form"
|
|
import { useIntl } from "react-intl"
|
|
|
|
import Body from "@/components/TempDesignSystem/Text/Body"
|
|
import Caption from "@/components/TempDesignSystem/Text/Caption"
|
|
|
|
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 } from "@/types/components/form/bookingwidget"
|
|
import type { SearchProps } from "@/types/components/search"
|
|
import type { Location } from "@/types/trpc/routers/hotel/locations"
|
|
|
|
const name = "search"
|
|
export default function Search({ locations }: SearchProps) {
|
|
const [state, dispatch] = useReducer(
|
|
reducer,
|
|
{ defaultLocations: locations },
|
|
init
|
|
)
|
|
const { register, setValue, trigger } = useFormContext<BookingWidgetSchema>()
|
|
const intl = useIntl()
|
|
const value = useWatch({ name })
|
|
|
|
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 handleOnBlur() {
|
|
if (!value && state.searchData?.name) {
|
|
setValue(name, state.searchData.name)
|
|
// Always need to manually trigger
|
|
// revalidation when setting value r-h-f
|
|
trigger()
|
|
}
|
|
}
|
|
|
|
function handleOnChange(
|
|
evt: FormEvent<HTMLInputElement> | ChangeEvent<HTMLInputElement>
|
|
) {
|
|
const value = evt.currentTarget.value
|
|
if (value) {
|
|
dispatch({
|
|
payload: { search: value },
|
|
type: ActionType.SEARCH_LOCATIONS,
|
|
})
|
|
} else {
|
|
dispatch({ type: ActionType.CLEAR_SEARCH_LOCATIONS })
|
|
}
|
|
}
|
|
|
|
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)
|
|
trigger()
|
|
|
|
const searchHistoryMap = new Map()
|
|
searchHistoryMap.set(selectedItem.name, selectedItem)
|
|
if (state.searchHistory) {
|
|
state.searchHistory.forEach((location) => {
|
|
searchHistoryMap.set(location.name, location)
|
|
})
|
|
}
|
|
|
|
const searchHistory: Location[] = []
|
|
searchHistoryMap.forEach((location) => {
|
|
searchHistory.push(location)
|
|
})
|
|
localStorage.setItem(localStorageKey, JSON.stringify(searchHistory))
|
|
dispatch({
|
|
payload: {
|
|
location: selectedItem,
|
|
searchHistory,
|
|
},
|
|
type: ActionType.SELECT_ITEM,
|
|
})
|
|
} else {
|
|
sessionStorage.removeItem(sessionStorageKey)
|
|
}
|
|
}
|
|
|
|
return (
|
|
<Downshift
|
|
initialSelectedItem={state.searchData}
|
|
inputValue={value}
|
|
itemToString={(value) => (value ? value.name : "")}
|
|
onSelect={handleOnSelect}
|
|
>
|
|
{({
|
|
closeMenu,
|
|
getInputProps,
|
|
getItemProps,
|
|
getLabelProps,
|
|
getMenuProps,
|
|
getRootProps,
|
|
highlightedIndex,
|
|
isOpen,
|
|
openMenu,
|
|
}) => (
|
|
<div className={styles.container}>
|
|
<label {...getLabelProps({ htmlFor: name })} className={styles.label}>
|
|
<Caption color={isOpen ? "uiTextActive" : "red"}>
|
|
{intl.formatMessage({ id: "Where to" })}
|
|
</Caption>
|
|
</label>
|
|
<div {...getRootProps({}, { suppressRefError: true })}>
|
|
<Input
|
|
{...getInputProps({
|
|
id: name,
|
|
onFocus(evt) {
|
|
handleOnFocus(evt)
|
|
openMenu()
|
|
},
|
|
placeholder: intl.formatMessage({
|
|
id: "Destinations & hotels",
|
|
}),
|
|
...register(name, {
|
|
onBlur: function () {
|
|
handleOnBlur()
|
|
closeMenu()
|
|
},
|
|
onChange: handleOnChange,
|
|
}),
|
|
type: "search",
|
|
})}
|
|
/>
|
|
</div>
|
|
<SearchList
|
|
getItemProps={getItemProps}
|
|
getMenuProps={getMenuProps}
|
|
handleClearSearchHistory={handleClearSearchHistory}
|
|
highlightedIndex={highlightedIndex}
|
|
isOpen={isOpen}
|
|
locations={state.locations}
|
|
search={state.search}
|
|
searchHistory={state.searchHistory}
|
|
/>
|
|
</div>
|
|
)}
|
|
</Downshift>
|
|
)
|
|
}
|