"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() 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 | ChangeEvent ) { const newValue = evt.currentTarget.value setValue(name, newValue) dispatchInputValue(value) } function handleOnFocus(evt: FocusEvent) { 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 ( (value ? value.name : "")} onSelect={handleOnSelect} onInputValueChange={(inputValue) => dispatchInputValue(inputValue)} defaultHighlightedIndex={0} > {({ getInputProps, getItemProps, getLabelProps, getMenuProps, getRootProps, highlightedIndex, isOpen, openMenu, }) => (
{value ? ( // Adding hidden input to define hotel or city based on destination selection for basic form submit. ) : null}
)}
) } export function SearchSkeleton() { const intl = useIntl() return (
{intl.formatMessage({ id: "Where to?" })}
) } /** * 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 }