From c4ba2ff54113ffcaaca1919e1d8b16396e62c697 Mon Sep 17 00:00:00 2001 From: "dev.daminik00" Date: Mon, 6 Oct 2025 03:26:20 +0200 Subject: [PATCH] fix --- public/funnels/soulmate.json | 5 + src/lib/funnel/FunnelProvider.tsx | 44 ++++++++- src/lib/funnel/bakedFunnels.ts | 5 + src/lib/funnel/storage.ts | 157 ++++++++++++++++++++++++++++++ 4 files changed, 209 insertions(+), 2 deletions(-) create mode 100644 src/lib/funnel/storage.ts diff --git a/public/funnels/soulmate.json b/public/funnels/soulmate.json index 20b2799..b0df7af 100644 --- a/public/funnels/soulmate.json +++ b/public/funnels/soulmate.json @@ -81,6 +81,11 @@ "align": "left", "color": "default" }, + "bottomActionButton": { + "show": false, + "cornerRadius": "3xl", + "showPrivacyTermsConsent": false + }, "navigation": { "rules": [], "defaultNextScreenId": "relationship-status", diff --git a/src/lib/funnel/FunnelProvider.tsx b/src/lib/funnel/FunnelProvider.tsx index b69cb69..b28b987 100644 --- a/src/lib/funnel/FunnelProvider.tsx +++ b/src/lib/funnel/FunnelProvider.tsx @@ -4,12 +4,19 @@ 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; @@ -61,10 +68,35 @@ interface FunnelProviderProps { 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) => { - const previousState = prev[funnelId] ?? createInitialState(); + // Пытаемся загрузить сохраненное состояние, если его еще нет в памяти + let previousState = prev[funnelId]; + if (!previousState) { + const loaded = loadFunnelState(funnelId); + previousState = loaded ?? createInitialState(); + } const history = previousState.history ?? []; let nextHistory = history; @@ -96,7 +128,12 @@ export function FunnelProvider({ children }: FunnelProviderProps) { const updateScreenAnswers = useCallback( (funnelId: string, screenId: string, answers: string[]) => { 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 existingAnswers = previousAnswers[screenId]; @@ -150,6 +187,9 @@ export function FunnelProvider({ children }: FunnelProviderProps) { return prev; } + // Очищаем состояние из localStorage + clearFunnelState(funnelId); + return { ...prev, [funnelId]: { diff --git a/src/lib/funnel/bakedFunnels.ts b/src/lib/funnel/bakedFunnels.ts index ac01d40..bd4609b 100644 --- a/src/lib/funnel/bakedFunnels.ts +++ b/src/lib/funnel/bakedFunnels.ts @@ -89,6 +89,11 @@ export const BAKED_FUNNELS: Record = { "align": "left", "color": "default" }, + "bottomActionButton": { + "show": false, + "cornerRadius": "3xl", + "showPrivacyTermsConsent": false + }, "navigation": { "rules": [], "defaultNextScreenId": "relationship-status", diff --git a/src/lib/funnel/storage.ts b/src/lib/funnel/storage.ts new file mode 100644 index 0000000..7bf3e92 --- /dev/null +++ b/src/lib/funnel/storage.ts @@ -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); + } +}