fix
This commit is contained in:
parent
aa956adebb
commit
c4ba2ff541
@ -81,6 +81,11 @@
|
|||||||
"align": "left",
|
"align": "left",
|
||||||
"color": "default"
|
"color": "default"
|
||||||
},
|
},
|
||||||
|
"bottomActionButton": {
|
||||||
|
"show": false,
|
||||||
|
"cornerRadius": "3xl",
|
||||||
|
"showPrivacyTermsConsent": false
|
||||||
|
},
|
||||||
"navigation": {
|
"navigation": {
|
||||||
"rules": [],
|
"rules": [],
|
||||||
"defaultNextScreenId": "relationship-status",
|
"defaultNextScreenId": "relationship-status",
|
||||||
|
|||||||
@ -4,12 +4,19 @@ import {
|
|||||||
createContext,
|
createContext,
|
||||||
useCallback,
|
useCallback,
|
||||||
useContext,
|
useContext,
|
||||||
|
useEffect,
|
||||||
useMemo,
|
useMemo,
|
||||||
useState,
|
useState,
|
||||||
type ReactNode,
|
type ReactNode,
|
||||||
} from "react";
|
} from "react";
|
||||||
|
|
||||||
import type { FunnelAnswers } from "./types";
|
import type { FunnelAnswers } from "./types";
|
||||||
|
import {
|
||||||
|
saveFunnelState,
|
||||||
|
loadFunnelState,
|
||||||
|
clearExpiredFunnelStates,
|
||||||
|
clearFunnelState,
|
||||||
|
} from "./storage";
|
||||||
|
|
||||||
interface FunnelRuntimeState {
|
interface FunnelRuntimeState {
|
||||||
answers: FunnelAnswers;
|
answers: FunnelAnswers;
|
||||||
@ -61,10 +68,35 @@ interface FunnelProviderProps {
|
|||||||
|
|
||||||
export function FunnelProvider({ children }: FunnelProviderProps) {
|
export function FunnelProvider({ children }: FunnelProviderProps) {
|
||||||
const [state, setState] = useState<Record<string, FunnelRuntimeState>>({});
|
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) => {
|
const registerScreenVisit = useCallback((funnelId: string, screenId: string) => {
|
||||||
setState((prev) => {
|
setState((prev) => {
|
||||||
const previousState = prev[funnelId] ?? createInitialState();
|
// Пытаемся загрузить сохраненное состояние, если его еще нет в памяти
|
||||||
|
let previousState = prev[funnelId];
|
||||||
|
if (!previousState) {
|
||||||
|
const loaded = loadFunnelState(funnelId);
|
||||||
|
previousState = loaded ?? createInitialState();
|
||||||
|
}
|
||||||
const history = previousState.history ?? [];
|
const history = previousState.history ?? [];
|
||||||
|
|
||||||
let nextHistory = history;
|
let nextHistory = history;
|
||||||
@ -96,7 +128,12 @@ export function FunnelProvider({ children }: FunnelProviderProps) {
|
|||||||
const updateScreenAnswers = useCallback(
|
const updateScreenAnswers = useCallback(
|
||||||
(funnelId: string, screenId: string, answers: string[]) => {
|
(funnelId: string, screenId: string, answers: string[]) => {
|
||||||
setState((prev) => {
|
setState((prev) => {
|
||||||
const previousState = prev[funnelId] ?? createInitialState();
|
// Пытаемся загрузить сохраненное состояние, если его еще нет в памяти
|
||||||
|
let previousState = prev[funnelId];
|
||||||
|
if (!previousState) {
|
||||||
|
const loaded = loadFunnelState(funnelId);
|
||||||
|
previousState = loaded ?? createInitialState();
|
||||||
|
}
|
||||||
const previousAnswers = previousState.answers ?? {};
|
const previousAnswers = previousState.answers ?? {};
|
||||||
const existingAnswers = previousAnswers[screenId];
|
const existingAnswers = previousAnswers[screenId];
|
||||||
|
|
||||||
@ -150,6 +187,9 @@ export function FunnelProvider({ children }: FunnelProviderProps) {
|
|||||||
return prev;
|
return prev;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Очищаем состояние из localStorage
|
||||||
|
clearFunnelState(funnelId);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...prev,
|
...prev,
|
||||||
[funnelId]: {
|
[funnelId]: {
|
||||||
|
|||||||
@ -89,6 +89,11 @@ export const BAKED_FUNNELS: Record<string, FunnelDefinition> = {
|
|||||||
"align": "left",
|
"align": "left",
|
||||||
"color": "default"
|
"color": "default"
|
||||||
},
|
},
|
||||||
|
"bottomActionButton": {
|
||||||
|
"show": false,
|
||||||
|
"cornerRadius": "3xl",
|
||||||
|
"showPrivacyTermsConsent": false
|
||||||
|
},
|
||||||
"navigation": {
|
"navigation": {
|
||||||
"rules": [],
|
"rules": [],
|
||||||
"defaultNextScreenId": "relationship-status",
|
"defaultNextScreenId": "relationship-status",
|
||||||
|
|||||||
157
src/lib/funnel/storage.ts
Normal file
157
src/lib/funnel/storage.ts
Normal file
@ -0,0 +1,157 @@
|
|||||||
|
import type { FunnelAnswers } from "./types";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Структура данных для хранения состояния воронки
|
||||||
|
*/
|
||||||
|
export interface FunnelRuntimeState {
|
||||||
|
answers: FunnelAnswers;
|
||||||
|
history: string[];
|
||||||
|
version: number;
|
||||||
|
timestamp?: number; // Для отслеживания времени последнего обновления
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Префикс для ключей в localStorage
|
||||||
|
*/
|
||||||
|
const STORAGE_PREFIX = "funnel_state_";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Время жизни данных в localStorage (7 дней)
|
||||||
|
*/
|
||||||
|
const STORAGE_TTL = 7 * 24 * 60 * 60 * 1000;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Проверяет доступность localStorage
|
||||||
|
*/
|
||||||
|
function isLocalStorageAvailable(): boolean {
|
||||||
|
try {
|
||||||
|
const testKey = "__storage_test__";
|
||||||
|
localStorage.setItem(testKey, "test");
|
||||||
|
localStorage.removeItem(testKey);
|
||||||
|
return true;
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Получает ключ для localStorage по ID воронки
|
||||||
|
*/
|
||||||
|
function getStorageKey(funnelId: string): string {
|
||||||
|
return `${STORAGE_PREFIX}${funnelId}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Сохраняет состояние воронки в localStorage
|
||||||
|
*/
|
||||||
|
export function saveFunnelState(
|
||||||
|
funnelId: string,
|
||||||
|
state: FunnelRuntimeState
|
||||||
|
): void {
|
||||||
|
if (!isLocalStorageAvailable()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const dataToSave: FunnelRuntimeState = {
|
||||||
|
...state,
|
||||||
|
timestamp: Date.now(),
|
||||||
|
};
|
||||||
|
|
||||||
|
localStorage.setItem(getStorageKey(funnelId), JSON.stringify(dataToSave));
|
||||||
|
} catch (error) {
|
||||||
|
console.warn("Failed to save funnel state to localStorage:", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Загружает состояние воронки из localStorage
|
||||||
|
*/
|
||||||
|
export function loadFunnelState(funnelId: string): FunnelRuntimeState | null {
|
||||||
|
if (!isLocalStorageAvailable()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const stored = localStorage.getItem(getStorageKey(funnelId));
|
||||||
|
if (!stored) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = JSON.parse(stored) as FunnelRuntimeState;
|
||||||
|
|
||||||
|
// Проверяем TTL
|
||||||
|
if (data.timestamp) {
|
||||||
|
const age = Date.now() - data.timestamp;
|
||||||
|
if (age > STORAGE_TTL) {
|
||||||
|
// Данные устарели, удаляем
|
||||||
|
clearFunnelState(funnelId);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return data;
|
||||||
|
} catch (error) {
|
||||||
|
console.warn("Failed to load funnel state from localStorage:", error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Очищает состояние воронки из localStorage
|
||||||
|
*/
|
||||||
|
export function clearFunnelState(funnelId: string): void {
|
||||||
|
if (!isLocalStorageAvailable()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
localStorage.removeItem(getStorageKey(funnelId));
|
||||||
|
} catch (error) {
|
||||||
|
console.warn("Failed to clear funnel state from localStorage:", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Очищает все устаревшие состояния воронок
|
||||||
|
*/
|
||||||
|
export function clearExpiredFunnelStates(): void {
|
||||||
|
if (!isLocalStorageAvailable()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const now = Date.now();
|
||||||
|
const keysToRemove: string[] = [];
|
||||||
|
|
||||||
|
// Проходим по всем ключам в localStorage
|
||||||
|
for (let i = 0; i < localStorage.length; i++) {
|
||||||
|
const key = localStorage.key(i);
|
||||||
|
if (!key?.startsWith(STORAGE_PREFIX)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const value = localStorage.getItem(key);
|
||||||
|
if (!value) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const data = JSON.parse(value) as FunnelRuntimeState;
|
||||||
|
if (data.timestamp && now - data.timestamp > STORAGE_TTL) {
|
||||||
|
keysToRemove.push(key);
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// Если не удалось распарсить - удаляем
|
||||||
|
keysToRemove.push(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Удаляем устаревшие ключи
|
||||||
|
for (const key of keysToRemove) {
|
||||||
|
localStorage.removeItem(key);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.warn("Failed to clear expired funnel states:", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user