import deepmerge from "deepmerge" import { produce } from "immer" import { useContext } from "react" import { create, useStore } from "zustand" import { DetailsContext } from "@/contexts/Details" import { add, calcTotalMemberPrice, calcTotalPublicPrice, checkIsSameRoom, extractGuestFromUser, getInitialRoomPrice, getInitialTotalPrice, navigate, writeToSessionStorage, } from "./helpers" import { StepEnum } from "@/types/enums/step" import type { DetailsState, FormValues, InitialState, PersistedState, } from "@/types/stores/enter-details" import type { SafeUser } from "@/types/user" const defaultGuestState = { countryCode: "", dateOfBirth: "", email: "", firstName: "", join: false, lastName: "", membershipNo: "", phoneNumber: "", zipCode: "", } export const detailsStorageName = "details-storage" export function createDetailsStore( initialState: InitialState, currentStep: StepEnum, searchParams: string, user: SafeUser ) { const isMember = !!user const formValues: FormValues = { bedType: initialState.bedType, booking: initialState.booking, /** TODO: Needs adjustment when breakfast included in rate is added */ breakfast: initialState.breakfast === false ? initialState.breakfast : undefined, guest: isMember ? deepmerge(defaultGuestState, extractGuestFromUser(user)) : defaultGuestState, } if (typeof window !== "undefined") { const unparsedStorage = sessionStorage.getItem(detailsStorageName) if (unparsedStorage) { const detailsStorage: PersistedState = JSON.parse(unparsedStorage) const isSameRoom = detailsStorage.booking ? checkIsSameRoom(initialState.booking, detailsStorage.booking) : false if (isSameRoom) { formValues.bedType = detailsStorage.bedType formValues.breakfast = detailsStorage.breakfast } if (!isMember) { formValues.guest = detailsStorage.guest } } } let steps = [ StepEnum.selectBed, StepEnum.breakfast, StepEnum.details, StepEnum.payment, ] /** * TODO: * - when included in rate, can packages still be received? * - no hotels yet with breakfast included in the rate so * impossible to build for atm. * * checking against initialState since that means the * hotel doesn't offer breakfast * * matching breakfast first so the steps array is altered * before the bedTypes possible step altering */ if (initialState.breakfast === false) { steps = steps.filter((step) => step !== StepEnum.breakfast) if (currentStep === StepEnum.breakfast) { currentStep = steps[1] } } if (initialState.bedType && currentStep === StepEnum.selectBed) { currentStep = steps[1] } const initialRoomPrice = getInitialRoomPrice(initialState.roomRate, isMember) const initialTotalPrice = getInitialTotalPrice( initialState.roomRate, isMember ) if (initialState.packages) { initialState.packages.forEach((pkg) => { if (initialTotalPrice.requested) { initialTotalPrice.requested.price = add( initialTotalPrice.requested.price, pkg.requestedPrice.totalPrice ) } initialTotalPrice.local.price = add( initialTotalPrice.local.price, pkg.localPrice.totalPrice ) }) } return create()((set) => ({ actions: { completeStep() { return set( produce((state: DetailsState) => { const currentStepIndex = state.steps.indexOf(state.currentStep) const nextStep = state.steps[currentStepIndex + 1] state.currentStep = nextStep navigate(nextStep, state.searchParamString) }) ) }, navigate(step: StepEnum) { return set( produce((state) => { state.currentStep = step navigate(step, state.searchParamString) }) ) }, setIsSubmittingDisabled(isSubmittingDisabled) { return set( produce((state: DetailsState) => { state.isSubmittingDisabled = isSubmittingDisabled }) ) }, setStep(step: StepEnum) { return set( produce((state: DetailsState) => { state.currentStep = step }) ) }, setTotalPrice(totalPrice) { return set( produce((state: DetailsState) => { state.totalPrice.requested = totalPrice.requested state.totalPrice.local = totalPrice.local }) ) }, toggleSummaryOpen() { return set( produce((state: DetailsState) => { state.isSummaryOpen = !state.isSummaryOpen }) ) }, togglePriceDetailsModalOpen() { return set( produce((state: DetailsState) => { state.isPriceDetailsModalOpen = !state.isPriceDetailsModalOpen }) ) }, updateBedType(bedType) { return set( produce((state: DetailsState) => { state.isValid["select-bed"] = true state.bedType = bedType writeToSessionStorage({ bedType }) const currentStepIndex = state.steps.indexOf(state.currentStep) const nextStep = state.steps[currentStepIndex + 1] state.currentStep = nextStep navigate(nextStep, state.searchParamString) }) ) }, updateBreakfast(breakfast) { return set( produce((state: DetailsState) => { state.isValid.breakfast = true const stateTotalRequestedPrice = state.totalPrice.requested?.price || 0 const stateTotalLocalPrice = state.totalPrice.local.price const addToTotalPrice = (state.breakfast === undefined || state.breakfast === false) && !!breakfast const subtractFromTotalPrice = (state.breakfast === undefined || state.breakfast) && breakfast === false if (addToTotalPrice) { const breakfastTotalRequestedPrice = parseInt( breakfast.requestedPrice.totalPrice ) const breakfastTotalPrice = parseInt( breakfast.localPrice.totalPrice ) state.totalPrice = { requested: state.totalPrice.requested && { currency: state.totalPrice.requested.currency, price: stateTotalRequestedPrice + breakfastTotalRequestedPrice, }, local: { currency: breakfast.localPrice.currency, price: stateTotalLocalPrice + breakfastTotalPrice, }, } } if (subtractFromTotalPrice) { let currency = state.totalPrice.local.currency let currentBreakfastTotalPrice = 0 let currentBreakfastTotalRequestedPrice = 0 if (state.breakfast) { currentBreakfastTotalPrice = parseInt( state.breakfast.localPrice.totalPrice ) currentBreakfastTotalRequestedPrice = parseInt( state.breakfast.requestedPrice.totalPrice ) currency = state.breakfast.localPrice.currency } let requestedPrice = stateTotalRequestedPrice - currentBreakfastTotalRequestedPrice if (requestedPrice < 0) { requestedPrice = 0 } let localPrice = stateTotalLocalPrice - currentBreakfastTotalPrice if (localPrice < 0) { localPrice = 0 } state.totalPrice = { requested: state.totalPrice.requested && { currency: state.totalPrice.requested.currency, price: requestedPrice, }, local: { currency, price: localPrice, }, } } state.breakfast = breakfast writeToSessionStorage({ breakfast }) const currentStepIndex = state.steps.indexOf(state.currentStep) const nextStep = state.steps[currentStepIndex + 1] state.currentStep = nextStep navigate(nextStep, state.searchParamString) }) ) }, updateDetails(data) { return set( produce((state: DetailsState) => { state.isValid.details = true state.guest.countryCode = data.countryCode state.guest.dateOfBirth = data.dateOfBirth state.guest.email = data.email state.guest.firstName = data.firstName state.guest.join = data.join state.guest.lastName = data.lastName if (data.join) { state.guest.membershipNo = undefined } else { state.guest.membershipNo = data.membershipNo } state.guest.phoneNumber = data.phoneNumber state.guest.zipCode = data.zipCode if (data.join || data.membershipNo || isMember) { const memberPrice = calcTotalMemberPrice(state) state.roomPrice = memberPrice.roomPrice state.totalPrice = memberPrice.totalPrice } else { const publicPrice = calcTotalPublicPrice(state) state.roomPrice = publicPrice.roomPrice state.totalPrice = publicPrice.totalPrice } writeToSessionStorage({ guest: data }) const currentStepIndex = state.steps.indexOf(state.currentStep) const nextStep = state.steps[currentStepIndex + 1] state.currentStep = nextStep navigate(nextStep, state.searchParamString) }) ) }, updateSeachParamString(searchParamString) { return set( produce((state: DetailsState) => { state.searchParamString = searchParamString }) ) }, }, searchParamString: searchParams, bedType: initialState.bedType ?? undefined, booking: initialState.booking, breakfast: initialState.breakfast === false ? initialState.breakfast : undefined, currentStep, formValues, guest: isMember ? deepmerge(defaultGuestState, extractGuestFromUser(user)) : defaultGuestState, isSubmittingDisabled: false, isSummaryOpen: false, isPriceDetailsModalOpen: false, isValid: { [StepEnum.selectBed]: false, [StepEnum.breakfast]: false, [StepEnum.details]: false, [StepEnum.payment]: false, }, packages: initialState.packages, roomPrice: initialRoomPrice, roomRate: initialState.roomRate, steps, totalPrice: initialTotalPrice, vat: initialState.vat, })) } export function useEnterDetailsStore(selector: (store: DetailsState) => T) { const store = useContext(DetailsContext) if (!store) { throw new Error("useEnterDetailsStore must be used within DetailsProvider") } return useStore(store, selector) }