From e6f2e6ccaf251eb098d57eab54d2b53db6a4359b Mon Sep 17 00:00:00 2001 From: vchikalkin Date: Tue, 29 Apr 2025 17:48:11 +0300 Subject: [PATCH] migrate from order context to zustand store --- .../orders/components/back-button.tsx | 8 +-- .../orders/components/contacts-grid/index.tsx | 10 ++-- .../components/date-select.tsx | 6 +-- .../orders/components/service-select.tsx | 6 +-- .../orders/components/submit-button.tsx | 11 ++-- apps/web/components/orders/order-form.tsx | 24 +++++---- apps/web/context/order/hooks.tsx | 34 ------------ apps/web/context/order/index.tsx | 54 ------------------- apps/web/context/order/reducer.tsx | 46 ---------------- apps/web/package.json | 3 +- apps/web/stores/order/index.tsx | 1 + apps/web/stores/order/store.ts | 52 ++++++++++++++++++ apps/web/{context => stores}/order/types.tsx | 19 +++---- pnpm-lock.yaml | 26 +++++++++ 14 files changed, 127 insertions(+), 173 deletions(-) delete mode 100644 apps/web/context/order/hooks.tsx delete mode 100644 apps/web/context/order/index.tsx delete mode 100644 apps/web/context/order/reducer.tsx create mode 100644 apps/web/stores/order/index.tsx create mode 100644 apps/web/stores/order/store.ts rename apps/web/{context => stores}/order/types.tsx (50%) diff --git a/apps/web/components/orders/components/back-button.tsx b/apps/web/components/orders/components/back-button.tsx index 9304277..7cc1708 100644 --- a/apps/web/components/orders/components/back-button.tsx +++ b/apps/web/components/orders/components/back-button.tsx @@ -1,13 +1,13 @@ 'use client'; -import { OrderContext } from '@/context/order'; +import { useOrderStore } from '@/stores/order'; import { Button } from '@repo/ui/components/ui/button'; -import { use } from 'react'; export function BackButton() { - const { prevStep, step } = use(OrderContext); + const step = useOrderStore((store) => store.step); + const previousStep = useOrderStore((store) => store.prevStep); function handleOnClick() { - prevStep(); + previousStep(); } if (['master-select', 'success'].includes(step)) return null; diff --git a/apps/web/components/orders/components/contacts-grid/index.tsx b/apps/web/components/orders/components/contacts-grid/index.tsx index 5ecc955..ec0612b 100644 --- a/apps/web/components/orders/components/contacts-grid/index.tsx +++ b/apps/web/components/orders/components/contacts-grid/index.tsx @@ -2,14 +2,15 @@ import { ContactsGridBase } from './components'; import { LoadingSpinner } from '@/components/common/spinner'; import { ContactsFilterProvider } from '@/context/contacts-filter'; -import { OrderContext } from '@/context/order'; import { useCustomerContacts } from '@/hooks/contacts'; +import { useOrderStore } from '@/stores/order'; import { withContext } from '@/utils/context'; -import { use, useEffect } from 'react'; +import { useEffect } from 'react'; export const MastersGrid = withContext(ContactsFilterProvider)(function () { const { contacts, isLoading, setFilter } = useCustomerContacts({ includeSelf: true }); - const { masterId, setMasterId } = use(OrderContext); + const masterId = useOrderStore((store) => store.masterId); + const setMasterId = useOrderStore((store) => store.setMasterId); useEffect(() => { setFilter('masters'); @@ -29,7 +30,8 @@ export const MastersGrid = withContext(ContactsFilterProvider)(function () { export const ClientsGrid = withContext(ContactsFilterProvider)(function () { const { contacts, isLoading, setFilter } = useCustomerContacts(); - const { clientId, setClientId } = use(OrderContext); + const clientId = useOrderStore((store) => store.clientId); + const setClientId = useOrderStore((store) => store.setClientId); useEffect(() => { setFilter('clients'); diff --git a/apps/web/components/orders/components/datetime-select/components/date-select.tsx b/apps/web/components/orders/components/datetime-select/components/date-select.tsx index e483aa7..892a596 100644 --- a/apps/web/components/orders/components/datetime-select/components/date-select.tsx +++ b/apps/web/components/orders/components/datetime-select/components/date-select.tsx @@ -1,11 +1,11 @@ 'use client'; -import { OrderContext } from '@/context/order'; +import { useOrderStore } from '@/stores/order'; import { Calendar } from '@repo/ui/components/ui/calendar'; import dayjs from 'dayjs'; -import { use } from 'react'; export function DateSelect() { - const { date: selectedDate, setDate } = use(OrderContext); + const selectedDate = useOrderStore((store) => store.date); + const setDate = useOrderStore((store) => store.setDate); return ( ) { - const { serviceId, setServiceId } = use(OrderContext); + const serviceId = useOrderStore((store) => store.serviceId); + const setServiceId = useOrderStore((store) => store.setServiceId); const selected = serviceId === documentId; diff --git a/apps/web/components/orders/components/submit-button.tsx b/apps/web/components/orders/components/submit-button.tsx index 2b3ba06..3b614c5 100644 --- a/apps/web/components/orders/components/submit-button.tsx +++ b/apps/web/components/orders/components/submit-button.tsx @@ -1,10 +1,10 @@ 'use client'; -import { OrderContext } from '@/context/order'; +import { useOrderStore } from '@/stores/order'; import { Button } from '@repo/ui/components/ui/button'; -import { use } from 'react'; export function SubmitButton() { - const { nextStep, step } = use(OrderContext); + const step = useOrderStore((store) => store.step); + const nextStep = useOrderStore((store) => store.nextStep); function handleOnClick() { if (step !== 'success') { @@ -22,7 +22,10 @@ export function SubmitButton() { } function useButtonDisabled() { - const { clientId, masterId, serviceId, step } = use(OrderContext); + const clientId = useOrderStore((state) => state.clientId); + const masterId = useOrderStore((state) => state.masterId); + const serviceId = useOrderStore((state) => state.serviceId); + const step = useOrderStore((state) => state.step); return ( (step === 'master-select' && !masterId) || diff --git a/apps/web/components/orders/order-form.tsx b/apps/web/components/orders/order-form.tsx index 68add5e..dfa4944 100644 --- a/apps/web/components/orders/order-form.tsx +++ b/apps/web/components/orders/order-form.tsx @@ -7,9 +7,9 @@ import { ServiceSelect, SubmitButton, } from './components'; -import { OrderContext, OrderContextProvider } from '@/context/order'; -import { withContext } from '@/utils/context'; -import { type JSX, use } from 'react'; +import { useProfileQuery } from '@/hooks/profile'; +import { useOrderStore } from '@/stores/order'; +import { type JSX, useEffect } from 'react'; const STEP_COMPONENTS: Record = { 'client-select': , @@ -18,12 +18,14 @@ const STEP_COMPONENTS: Record = { 'service-select': , }; -function getStepComponent(step: string) { - return STEP_COMPONENTS[step] ?? null; -} +export function OrderForm() { + const { data: customer } = useProfileQuery(); + const step = useOrderStore((store) => store.step); + const initStepSequence = useOrderStore((store) => store.initStepSequence); -export const OrderForm = withContext(OrderContextProvider)(function () { - const { step } = use(OrderContext); + useEffect(() => { + initStepSequence(customer?.role); + }, [customer?.role, initStepSequence]); return (
@@ -34,4 +36,8 @@ export const OrderForm = withContext(OrderContextProvider)(function () {
); -}); +} + +function getStepComponent(step: string) { + return STEP_COMPONENTS[step] ?? null; +} diff --git a/apps/web/context/order/hooks.tsx b/apps/web/context/order/hooks.tsx deleted file mode 100644 index b5b9a3b..0000000 --- a/apps/web/context/order/hooks.tsx +++ /dev/null @@ -1,34 +0,0 @@ -/* eslint-disable canonical/id-match */ -'use client'; -import { useStepsReducer } from './reducer'; -import { type Steps } from './types'; -import { useProfileQuery } from '@/hooks/profile'; -import { Enum_Customer_Role } from '@repo/graphql/types'; -import { useEffect, useReducer, useState } from 'react'; - -export function useAutoSetClient({ entityId, setEntityId }: ReturnType) { - const { data: profile } = useProfileQuery(); - - useEffect(() => { - if (profile?.role === Enum_Customer_Role.Client && !entityId) { - setEntityId(profile.documentId); - } - }, [entityId, profile?.documentId, profile?.role, setEntityId]); -} - -export function useEntityState() { - const [entityId, setEntityId] = useState(null); - return { entityId, setEntityId }; -} - -export function useStep() { - const stepsReducer = useStepsReducer(); - - const [state, dispatch] = useReducer(stepsReducer, { step: 'master-select' }); - - const setStep = (payload: Steps) => dispatch({ payload, type: 'SET_STEP' }); - const nextStep = () => dispatch({ type: 'NEXT_STEP' }); - const previousStep = () => dispatch({ type: 'PREV_STEP' }); - - return { nextStep, prevStep: previousStep, setStep, ...state }; -} diff --git a/apps/web/context/order/index.tsx b/apps/web/context/order/index.tsx deleted file mode 100644 index f270f51..0000000 --- a/apps/web/context/order/index.tsx +++ /dev/null @@ -1,54 +0,0 @@ -'use client'; -import { useAutoSetClient, useEntityState, useStep } from './hooks'; -import { type ContextType } from './types'; -import { createContext, type PropsWithChildren, useMemo, useState } from 'react'; - -export const OrderContext = createContext({} as ContextType); - -export function OrderContextProvider({ children }: Readonly) { - const { entityId: masterId, setEntityId: setMasterId } = useEntityState(); - const { entityId: serviceId, setEntityId: setServiceId } = useEntityState(); - const { entityId: clientId, setEntityId: setClientId } = useEntityState(); - - const [date, setDate] = useState(new Date()); - const [time, setTime] = useState(null); - - const { nextStep, prevStep, setStep, step } = useStep(); - - useAutoSetClient({ entityId: clientId, setEntityId: setClientId }); - - const value = useMemo( - () => ({ - clientId, - date, - masterId, - nextStep, - prevStep, - serviceId, - setClientId, - setDate, - setMasterId, - setServiceId, - setStep, - setTime, - step, - time, - }), - [ - clientId, - date, - masterId, - nextStep, - prevStep, - serviceId, - setClientId, - setMasterId, - setServiceId, - setStep, - step, - time, - ], - ); - - return {children}; -} diff --git a/apps/web/context/order/reducer.tsx b/apps/web/context/order/reducer.tsx deleted file mode 100644 index 11c6042..0000000 --- a/apps/web/context/order/reducer.tsx +++ /dev/null @@ -1,46 +0,0 @@ -/* eslint-disable canonical/id-match */ -import { type Action, type State, type Steps } from './types'; -import { useProfileQuery } from '@/hooks/profile'; -import { Enum_Customer_Role } from '@repo/graphql/types'; - -const masterSteps: Steps[] = [ - 'master-select', - 'client-select', - 'service-select', - 'datetime-select', - 'success', -]; - -const clientSteps = masterSteps.filter((step) => step !== 'client-select'); - -export function useStepsReducer() { - const { data: profile } = useProfileQuery(); - const stepsSequence = profile?.role === Enum_Customer_Role.Master ? masterSteps : clientSteps; - - return function (state: State, action: Action): State { - switch (action.type) { - case 'NEXT_STEP': { - const currentIndex = stepsSequence.indexOf(state.step); - const nextIndex = currentIndex + 1; - const nextStep = stepsSequence[nextIndex]; - - return nextStep ? { ...state, step: nextStep } : state; - } - - case 'PREV_STEP': { - const currentIndex = stepsSequence.indexOf(state.step); - const previousIndex = currentIndex - 1; - const previousStep = stepsSequence[previousIndex]; - - return previousStep ? { ...state, step: previousStep } : state; - } - - case 'SET_STEP': { - return { ...state, step: action.payload }; - } - - default: - return state; - } - }; -} diff --git a/apps/web/package.json b/apps/web/package.json index 55567f2..c76fca5 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -28,7 +28,8 @@ "react": "catalog:", "react-dom": "catalog:", "use-debounce": "^10.0.4", - "zod": "catalog:" + "zod": "catalog:", + "zustand": "^5.0.3" }, "devDependencies": { "@playwright/test": "^1.49.1", diff --git a/apps/web/stores/order/index.tsx b/apps/web/stores/order/index.tsx new file mode 100644 index 0000000..d406816 --- /dev/null +++ b/apps/web/stores/order/index.tsx @@ -0,0 +1 @@ +export * from './store'; diff --git a/apps/web/stores/order/store.ts b/apps/web/stores/order/store.ts new file mode 100644 index 0000000..f2e16dc --- /dev/null +++ b/apps/web/stores/order/store.ts @@ -0,0 +1,52 @@ +/* eslint-disable @typescript-eslint/naming-convention */ +/* eslint-disable canonical/id-match */ +import { type OrderStore, type Steps } from './types'; +import { Enum_Customer_Role } from '@repo/graphql/types'; +import { create } from 'zustand'; + +const MASTER_STEPS: Steps[] = [ + 'master-select', + 'client-select', + 'service-select', + 'datetime-select', + 'success', +]; + +const CLIENT_STEPS: Steps[] = MASTER_STEPS.filter((step) => step !== 'client-select'); + +export const useOrderStore = create((set, get) => ({ + _stepSequence: [], + clientId: null, + date: new Date(), + initStepSequence: (role) => { + const steps = role === Enum_Customer_Role.Master ? MASTER_STEPS : CLIENT_STEPS; + const initialStep = steps[0]; + set({ _stepSequence: steps, step: initialStep }); + }, + masterId: null, + nextStep: () => { + const { _stepSequence, step } = get(); + const index = _stepSequence.indexOf(step); + const next = _stepSequence[index + 1]; + if (next) set({ step: next }); + }, + prevStep: () => { + const { _stepSequence, step } = get(); + const index = _stepSequence.indexOf(step); + const previous = _stepSequence[index - 1]; + if (previous) set({ step: previous }); + }, + + serviceId: null, + setClientId: (id) => set({ clientId: id }), + setDate: (date) => set({ date }), + setMasterId: (id) => set({ masterId: id }), + setServiceId: (id) => set({ serviceId: id }), + setStep: (step) => set({ step }), + + setTime: (time) => set({ time }), + + step: 'master-select', + + time: null, +})); diff --git a/apps/web/context/order/types.tsx b/apps/web/stores/order/types.tsx similarity index 50% rename from apps/web/context/order/types.tsx rename to apps/web/stores/order/types.tsx index 7f1451d..437787a 100644 --- a/apps/web/context/order/types.tsx +++ b/apps/web/stores/order/types.tsx @@ -1,27 +1,24 @@ -export type Action = - | { payload: Steps; type: 'SET_STEP' } - | { type: 'NEXT_STEP' } - | { type: 'PREV_STEP' }; +import { type Enum_Customer_Role } from '@repo/graphql/types'; -export type ContextType = { +export type OrderStore = { + _stepSequence: Steps[]; clientId: null | string; date: Date; + initStepSequence: (role: Enum_Customer_Role | undefined) => void; masterId: null | string; nextStep: () => void; prevStep: () => void; serviceId: null | string; - setClientId: (clientId: null | string) => void; + setClientId: (id: null | string) => void; setDate: (date: Date) => void; - setMasterId: (customerId: null | string) => void; - setServiceId: (serviceId: null | string) => void; + setMasterId: (id: null | string) => void; + setServiceId: (id: null | string) => void; setStep: (step: Steps) => void; - setTime: (time: string) => void; + setTime: (time: null | string) => void; step: Steps; time: null | string; }; -export type State = { step: Steps }; - export type Steps = | 'client-select' | 'datetime-select' diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index bf5d002..a73f200 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -183,6 +183,9 @@ importers: zod: specifier: 'catalog:' version: 3.24.1 + zustand: + specifier: ^5.0.3 + version: 5.0.3(@types/react@19.1.2)(react@19.1.0) devDependencies: '@playwright/test': specifier: ^1.49.1 @@ -6170,6 +6173,24 @@ packages: zod@3.24.1: resolution: {integrity: sha512-muH7gBL9sI1nciMZV67X5fTKKBLtwpZ5VBp1vsOQzj1MhrBZ4wlVCm3gedKZWLp0Oyel8sIGfeiz54Su+OVT+A==} + zustand@5.0.3: + resolution: {integrity: sha512-14fwWQtU3pH4dE0dOpdMiWjddcH+QzKIgk1cl8epwSE7yag43k/AD/m4L6+K7DytAOr9gGBe3/EXj9g7cdostg==} + engines: {node: '>=12.20.0'} + peerDependencies: + '@types/react': '>=18.0.0' + immer: '>=9.0.6' + react: '>=18.0.0' + use-sync-external-store: '>=1.2.0' + peerDependenciesMeta: + '@types/react': + optional: true + immer: + optional: true + react: + optional: true + use-sync-external-store: + optional: true + snapshots: '@alloc/quick-lru@5.2.0': {} @@ -12821,3 +12842,8 @@ snapshots: zen-observable@0.8.15: {} zod@3.24.1: {} + + zustand@5.0.3(@types/react@19.1.2)(react@19.1.0): + optionalDependencies: + '@types/react': 19.1.2 + react: 19.1.0