210 lines
6.4 KiB
TypeScript
210 lines
6.4 KiB
TypeScript
import { produce } from "immer"
|
|
import { ReadonlyURLSearchParams } from "next/navigation"
|
|
import { createContext, useContext } from "react"
|
|
import { create, useStore } from "zustand"
|
|
|
|
import { bedTypeSchema } from "@/components/HotelReservation/EnterDetails/BedType/schema"
|
|
import { breakfastStoreSchema } from "@/components/HotelReservation/EnterDetails/Breakfast/schema"
|
|
import {
|
|
guestDetailsSchema,
|
|
signedInDetailsSchema,
|
|
} from "@/components/HotelReservation/EnterDetails/Details/schema"
|
|
import {
|
|
createSelectRateUrl,
|
|
getQueryParamsForEnterDetails,
|
|
} from "@/components/HotelReservation/SelectRate/RoomSelection/utils"
|
|
|
|
import type { BedTypeSchema } from "@/types/components/hotelReservation/enterDetails/bedType"
|
|
import { BookingData } from "@/types/components/hotelReservation/enterDetails/bookingData"
|
|
import { BreakfastPackage } from "@/types/components/hotelReservation/enterDetails/breakfast"
|
|
import { DetailsSchema } from "@/types/components/hotelReservation/enterDetails/details"
|
|
import { StepEnum } from "@/types/components/hotelReservation/enterDetails/step"
|
|
import { BreakfastPackageEnum } from "@/types/enums/breakfast"
|
|
|
|
const SESSION_STORAGE_KEY = "enterDetails"
|
|
|
|
type TotalPrice = {
|
|
local: { price: number; currency: string }
|
|
euro: { price: number; currency: string }
|
|
}
|
|
|
|
export interface EnterDetailsState {
|
|
userData: {
|
|
bedType: BedTypeSchema | undefined
|
|
breakfast: BreakfastPackage | BreakfastPackageEnum.NO_BREAKFAST | undefined
|
|
} & DetailsSchema
|
|
roomData: BookingData
|
|
steps: StepEnum[]
|
|
selectRateUrl: string
|
|
currentStep: StepEnum
|
|
totalPrice: TotalPrice
|
|
isSubmittingDisabled: boolean
|
|
isSummaryOpen: boolean
|
|
isValid: Record<StepEnum, boolean>
|
|
completeStep: (updatedData: Partial<EnterDetailsState["userData"]>) => void
|
|
navigate: (
|
|
step: StepEnum,
|
|
updatedData?: Record<
|
|
string,
|
|
string | boolean | number | BreakfastPackage | BedTypeSchema
|
|
>
|
|
) => void
|
|
setCurrentStep: (step: StepEnum) => void
|
|
toggleSummaryOpen: () => void
|
|
setTotalPrice: (totalPrice: TotalPrice) => void
|
|
setIsSubmittingDisabled: (isSubmittingDisabled: boolean) => void
|
|
}
|
|
|
|
export function initEditDetailsState(
|
|
currentStep: StepEnum,
|
|
searchParams: ReadonlyURLSearchParams,
|
|
isMember: boolean
|
|
) {
|
|
const isBrowser = typeof window !== "undefined"
|
|
const sessionData = isBrowser
|
|
? sessionStorage.getItem(SESSION_STORAGE_KEY)
|
|
: null
|
|
|
|
let roomData: BookingData
|
|
let selectRateUrl: string
|
|
if (searchParams?.size) {
|
|
const data = getQueryParamsForEnterDetails(searchParams)
|
|
roomData = data
|
|
selectRateUrl = `select-rate?${createSelectRateUrl(data)}`
|
|
}
|
|
|
|
const defaultUserData: EnterDetailsState["userData"] = {
|
|
bedType: undefined,
|
|
breakfast: undefined,
|
|
countryCode: "",
|
|
email: "",
|
|
firstName: "",
|
|
lastName: "",
|
|
phoneNumber: "",
|
|
join: false,
|
|
zipCode: "",
|
|
dateOfBirth: undefined,
|
|
termsAccepted: false,
|
|
membershipNo: "",
|
|
}
|
|
|
|
let inputUserData = {}
|
|
if (sessionData) {
|
|
inputUserData = JSON.parse(sessionData)
|
|
}
|
|
|
|
const validPaths = [StepEnum.selectBed]
|
|
|
|
let initialData: EnterDetailsState["userData"] = defaultUserData
|
|
|
|
const isValid = {
|
|
[StepEnum.selectBed]: false,
|
|
[StepEnum.breakfast]: false,
|
|
[StepEnum.details]: false,
|
|
[StepEnum.payment]: false,
|
|
}
|
|
|
|
const validatedBedType = bedTypeSchema.safeParse(inputUserData)
|
|
if (validatedBedType.success) {
|
|
validPaths.push(StepEnum.breakfast)
|
|
initialData = { ...initialData, ...validatedBedType.data }
|
|
isValid[StepEnum.selectBed] = true
|
|
}
|
|
const validatedBreakfast = breakfastStoreSchema.safeParse(inputUserData)
|
|
if (validatedBreakfast.success) {
|
|
validPaths.push(StepEnum.details)
|
|
initialData = { ...initialData, ...validatedBreakfast.data }
|
|
isValid[StepEnum.breakfast] = true
|
|
}
|
|
const detailsSchema = isMember ? signedInDetailsSchema : guestDetailsSchema
|
|
const validatedDetails = detailsSchema.safeParse(inputUserData)
|
|
if (validatedDetails.success) {
|
|
validPaths.push(StepEnum.payment)
|
|
initialData = { ...initialData, ...validatedDetails.data }
|
|
isValid[StepEnum.details] = true
|
|
}
|
|
|
|
if (!validPaths.includes(currentStep)) {
|
|
currentStep = validPaths.pop()! // We will always have at least one valid path
|
|
if (isBrowser) {
|
|
window.history.pushState(
|
|
{ step: currentStep },
|
|
"",
|
|
currentStep + window.location.search
|
|
)
|
|
}
|
|
}
|
|
|
|
return create<EnterDetailsState>()((set, get) => ({
|
|
userData: initialData,
|
|
roomData,
|
|
selectRateUrl,
|
|
steps: Object.values(StepEnum),
|
|
totalPrice: {
|
|
local: { price: 0, currency: "" },
|
|
euro: { price: 0, currency: "" },
|
|
},
|
|
isSummaryOpen: false,
|
|
isSubmittingDisabled: false,
|
|
setCurrentStep: (step) => set({ currentStep: step }),
|
|
navigate: (step, updatedData) =>
|
|
set(
|
|
produce((state) => {
|
|
const sessionStorage = window.sessionStorage
|
|
|
|
const previousDataString = sessionStorage.getItem(SESSION_STORAGE_KEY)
|
|
|
|
const previousData = JSON.parse(previousDataString || "{}")
|
|
|
|
sessionStorage.setItem(
|
|
SESSION_STORAGE_KEY,
|
|
JSON.stringify({ ...previousData, ...updatedData })
|
|
)
|
|
|
|
state.currentStep = step
|
|
window.history.pushState({ step }, "", step + window.location.search)
|
|
})
|
|
),
|
|
currentStep,
|
|
isValid,
|
|
completeStep: (updatedData) =>
|
|
set(
|
|
produce((state: EnterDetailsState) => {
|
|
state.isValid[state.currentStep] = true
|
|
|
|
const nextStep =
|
|
state.steps[state.steps.indexOf(state.currentStep) + 1]
|
|
|
|
state.userData = {
|
|
...state.userData,
|
|
...updatedData,
|
|
}
|
|
state.currentStep = nextStep
|
|
get().navigate(nextStep, updatedData)
|
|
})
|
|
),
|
|
toggleSummaryOpen: () => set({ isSummaryOpen: !get().isSummaryOpen }),
|
|
setTotalPrice: (totalPrice) => set({ totalPrice: totalPrice }),
|
|
setIsSubmittingDisabled: (isSubmittingDisabled) =>
|
|
set({ isSubmittingDisabled }),
|
|
}))
|
|
}
|
|
|
|
export type EnterDetailsStore = ReturnType<typeof initEditDetailsState>
|
|
|
|
export const EnterDetailsContext = createContext<EnterDetailsStore | null>(null)
|
|
|
|
export const useEnterDetailsStore = <T>(
|
|
selector: (store: EnterDetailsState) => T
|
|
): T => {
|
|
const enterDetailsContextStore = useContext(EnterDetailsContext)
|
|
|
|
if (!enterDetailsContextStore) {
|
|
throw new Error(
|
|
`useEnterDetailsStore must be used within EnterDetailsContextProvider`
|
|
)
|
|
}
|
|
|
|
return useStore(enterDetailsContextStore, selector)
|
|
}
|