w-funnel/src/lib/funnel/FunnelProvider.tsx
dev.daminik00 c4ba2ff541 fix
2025-10-06 03:26:20 +02:00

252 lines
6.3 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"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<string, FunnelRuntimeState>;
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<FunnelContextValue | undefined>(undefined);
interface FunnelProviderProps {
children: ReactNode;
}
export function FunnelProvider({ children }: FunnelProviderProps) {
const [state, setState] = useState<Record<string, FunnelRuntimeState>>({});
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<FunnelContextValue>(
() => ({ state, registerScreenVisit, updateScreenAnswers, resetFunnel }),
[state, registerScreenVisit, updateScreenAnswers, resetFunnel]
);
return <FunnelContext.Provider value={value}>{children}</FunnelContext.Provider>;
}
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,
};
}