"use client"; import { createContext, useCallback, useContext, useEffect, useMemo, useState, type ReactNode, } from "react"; import type { FunnelAnswers } from "./types"; import { saveFunnelState, loadFunnelState, clearExpiredFunnelStates, clearFunnelState, } from "./storage"; interface FunnelRuntimeState { answers: FunnelAnswers; history: string[]; version: number; } interface FunnelContextValue { state: Record; registerScreenVisit: (funnelId: string, screenId: string) => void; updateScreenAnswers: ( funnelId: string, screenId: string, answers: string[] ) => void; resetFunnel: (funnelId: string) => void; } const DEFAULT_RUNTIME_STATE: FunnelRuntimeState = { answers: {}, history: [], version: 0, }; function createInitialState(): FunnelRuntimeState { return { answers: {}, history: [], version: 0, }; } function arraysEqual(left: string[] | undefined, right: string[]): boolean { if (!left && right.length === 0) { return true; } if (!left || left.length !== right.length) { return false; } return left.every((value, index) => value === right[index]); } const FunnelContext = createContext(undefined); interface FunnelProviderProps { children: ReactNode; } export function FunnelProvider({ children }: FunnelProviderProps) { const [state, setState] = useState>({}); const [isHydrated, setIsHydrated] = useState(false); // Восстанавливаем состояние из localStorage при монтировании useEffect(() => { // Очищаем устаревшие состояния clearExpiredFunnelStates(); setIsHydrated(true); }, []); // Автоматически сохраняем состояние при изменениях useEffect(() => { if (!isHydrated) { return; } // Сохраняем состояние каждой воронки for (const [funnelId, funnelState] of Object.entries(state)) { saveFunnelState(funnelId, funnelState); } }, [state, isHydrated]); const registerScreenVisit = useCallback((funnelId: string, screenId: string) => { setState((prev) => { // Пытаемся загрузить сохраненное состояние, если его еще нет в памяти let previousState = prev[funnelId]; if (!previousState) { const loaded = loadFunnelState(funnelId); previousState = loaded ?? createInitialState(); } const history = previousState.history ?? []; let nextHistory = history; if (history.length === 0 || history[history.length - 1] !== screenId) { const existingIndex = history.indexOf(screenId); if (existingIndex === -1) { nextHistory = [...history, screenId]; } else if (existingIndex !== history.length - 1) { nextHistory = history.slice(0, existingIndex + 1); } } if (nextHistory === history) { return prev; } return { ...prev, [funnelId]: { ...previousState, history: nextHistory, }, }; }); }, []); const updateScreenAnswers = useCallback( (funnelId: string, screenId: string, answers: string[]) => { setState((prev) => { // Пытаемся загрузить сохраненное состояние, если его еще нет в памяти let previousState = prev[funnelId]; if (!previousState) { const loaded = loadFunnelState(funnelId); previousState = loaded ?? createInitialState(); } const previousAnswers = previousState.answers ?? {}; const existingAnswers = previousAnswers[screenId]; if (answers.length === 0) { if (!existingAnswers) { return prev; } const rest = { ...previousAnswers }; delete rest[screenId]; return { ...prev, [funnelId]: { ...previousState, answers: rest, }, }; } if (arraysEqual(existingAnswers, answers)) { return prev; } return { ...prev, [funnelId]: { ...previousState, answers: { ...previousAnswers, [screenId]: answers, }, }, }; }); }, [] ); const resetFunnel = useCallback((funnelId: string) => { setState((prev) => { const previousState = prev[funnelId]; if (!previousState) { return prev; } if ( previousState.history.length === 0 && Object.keys(previousState.answers).length === 0 ) { return prev; } // Очищаем состояние из localStorage clearFunnelState(funnelId); return { ...prev, [funnelId]: { ...createInitialState(), version: (previousState.version ?? 0) + 1, }, }; }); }, []); const value = useMemo( () => ({ state, registerScreenVisit, updateScreenAnswers, resetFunnel }), [state, registerScreenVisit, updateScreenAnswers, resetFunnel] ); return {children}; } function useFunnelContext() { const context = useContext(FunnelContext); if (!context) { throw new Error("useFunnelContext must be used within a FunnelProvider"); } return context; } export function useFunnelRuntime(funnelId: string) { const { state, registerScreenVisit, updateScreenAnswers, resetFunnel } = useFunnelContext(); const runtime = state[funnelId] ?? DEFAULT_RUNTIME_STATE; const setAnswers = useCallback( (screenId: string, answers: string[]) => { updateScreenAnswers(funnelId, screenId, answers); }, [funnelId, updateScreenAnswers] ); const register = useCallback( (screenId: string) => { registerScreenVisit(funnelId, screenId); }, [funnelId, registerScreenVisit] ); const reset = useCallback(() => { resetFunnel(funnelId); }, [funnelId, resetFunnel]); return { answers: runtime.answers, history: runtime.history, version: runtime.version, setAnswers, registerScreen: register, reset, }; }