feat: add validation to enter details flow
This commit is contained in:
142
stores/enter-details.ts
Normal file
142
stores/enter-details.ts
Normal file
@@ -0,0 +1,142 @@
|
||||
import { produce } from "immer"
|
||||
import { createContext, useContext } from "react"
|
||||
import { create, useStore } from "zustand"
|
||||
|
||||
import { bedTypeSchema } from "@/components/HotelReservation/EnterDetails/BedType/schema"
|
||||
import { breakfastSchema } from "@/components/HotelReservation/EnterDetails/Breakfast/schema"
|
||||
import { detailsSchema } from "@/components/HotelReservation/EnterDetails/Details/schema"
|
||||
|
||||
import { DetailsSchema } from "@/types/components/enterDetails/details"
|
||||
import { StepEnum } from "@/types/components/enterDetails/step"
|
||||
import { bedTypeEnum } from "@/types/enums/bedType"
|
||||
import { breakfastEnum } from "@/types/enums/breakfast"
|
||||
|
||||
interface EnterDetailsState {
|
||||
data: {
|
||||
bedType: bedTypeEnum | undefined
|
||||
breakfast: breakfastEnum | undefined
|
||||
} & DetailsSchema
|
||||
steps: StepEnum[]
|
||||
currentStep: StepEnum
|
||||
isValid: Record<StepEnum, boolean>
|
||||
completeStep: (updatedData: Partial<EnterDetailsState["data"]>) => void
|
||||
navigate: (step: StepEnum, searchParams?: Record<string, string>) => void
|
||||
}
|
||||
|
||||
export function initEditDetailsState(currentStep: StepEnum) {
|
||||
const isBrowser = typeof window !== "undefined"
|
||||
const sessionData = isBrowser ? sessionStorage.getItem("editDetails") : null
|
||||
const search = isBrowser ? new URLSearchParams(window.location.search) : null
|
||||
|
||||
const defaultData: EnterDetailsState["data"] = {
|
||||
bedType: undefined,
|
||||
breakfast: undefined,
|
||||
countryCode: "",
|
||||
email: "",
|
||||
firstname: "",
|
||||
lastname: "",
|
||||
phoneNumber: "",
|
||||
}
|
||||
|
||||
let inputData = {}
|
||||
if (search?.size) {
|
||||
const searchParams: Record<string, string> = {}
|
||||
search.forEach((value, key) => {
|
||||
searchParams[key] = value
|
||||
})
|
||||
|
||||
inputData = searchParams
|
||||
} else if (sessionData) {
|
||||
inputData = JSON.parse(sessionData)
|
||||
}
|
||||
|
||||
const validPaths = [StepEnum.selectBed]
|
||||
|
||||
let initialData = defaultData
|
||||
|
||||
const isValid = {
|
||||
[StepEnum.selectBed]: false,
|
||||
[StepEnum.breakfast]: false,
|
||||
[StepEnum.details]: false,
|
||||
[StepEnum.payment]: false,
|
||||
}
|
||||
|
||||
const validatedBedType = bedTypeSchema.safeParse(inputData)
|
||||
if (validatedBedType.success) {
|
||||
validPaths.push(StepEnum.breakfast)
|
||||
initialData = { ...initialData, ...validatedBedType.data }
|
||||
isValid[StepEnum.selectBed] = true
|
||||
}
|
||||
const validatedBreakfast = breakfastSchema.safeParse(inputData)
|
||||
if (validatedBreakfast.success) {
|
||||
validPaths.push(StepEnum.details)
|
||||
initialData = { ...initialData, ...validatedBreakfast.data }
|
||||
isValid[StepEnum.breakfast] = true
|
||||
}
|
||||
const validatedDetails = detailsSchema.safeParse(inputData)
|
||||
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({}, "", currentStep + window.location.search)
|
||||
}
|
||||
}
|
||||
|
||||
return create<EnterDetailsState>()((set, get) => ({
|
||||
data: initialData,
|
||||
steps: Object.values(StepEnum),
|
||||
navigate: (step, searchParams) =>
|
||||
set(
|
||||
produce((state) => {
|
||||
const query = new URLSearchParams(window.location.search)
|
||||
if (searchParams) {
|
||||
Object.entries(searchParams).forEach(([key, value]) => {
|
||||
query.set(key, value)
|
||||
})
|
||||
}
|
||||
|
||||
state.currentStep = step
|
||||
window.history.pushState({}, "", step + "?" + query.toString())
|
||||
})
|
||||
),
|
||||
currentStep,
|
||||
isValid,
|
||||
completeStep: (updatedData) =>
|
||||
set(
|
||||
produce((state) => {
|
||||
state.isValid[state.currentStep] = true
|
||||
|
||||
const nextStep =
|
||||
state.steps[state.steps.indexOf(state.currentStep) + 1]
|
||||
|
||||
state.data = { ...state.data, ...updatedData }
|
||||
|
||||
state.currentStep = nextStep
|
||||
get().navigate(nextStep, updatedData)
|
||||
})
|
||||
),
|
||||
}))
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
Reference in New Issue
Block a user