feat(SW-66, SW-348): search functionality and ui
This commit is contained in:
180
components/Forms/BookingWidget/FormContent/Search/index.tsx
Normal file
180
components/Forms/BookingWidget/FormContent/Search/index.tsx
Normal file
@@ -0,0 +1,180 @@
|
||||
"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 { 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 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 | "clear-search") {
|
||||
if (selectedItem === "clear-search") {
|
||||
localStorage.removeItem(localStorageKey)
|
||||
dispatch({ type: ActionType.CLEAR_HISTORY_LOCATIONS })
|
||||
} else 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 })}>
|
||||
<Body asChild>
|
||||
<input
|
||||
{...getInputProps({
|
||||
className: styles.input,
|
||||
id: name,
|
||||
onFocus(evt) {
|
||||
handleOnFocus(evt)
|
||||
openMenu()
|
||||
},
|
||||
placeholder: intl.formatMessage({
|
||||
id: "Destinations & hotels",
|
||||
}),
|
||||
...register(name, {
|
||||
onBlur: function () {
|
||||
handleOnBlur()
|
||||
closeMenu()
|
||||
},
|
||||
onChange: handleOnChange,
|
||||
}),
|
||||
type: "search",
|
||||
})}
|
||||
/>
|
||||
</Body>
|
||||
</div>
|
||||
<SearchList
|
||||
getItemProps={getItemProps}
|
||||
getMenuProps={getMenuProps}
|
||||
highlightedIndex={highlightedIndex}
|
||||
isOpen={isOpen}
|
||||
locations={state.locations}
|
||||
search={state.search}
|
||||
searchHistory={state.searchHistory}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</Downshift>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user