103 lines
2.2 KiB
TypeScript
103 lines
2.2 KiB
TypeScript
|
|
import { create } from 'zustand';
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Onboarding Store — manages the guided tour state for first-time users.
|
||
|
|
* Persists to localStorage so progress survives page reloads.
|
||
|
|
*/
|
||
|
|
|
||
|
|
const STORAGE_KEY = 'leocrm_onboarding';
|
||
|
|
|
||
|
|
export interface OnboardingState {
|
||
|
|
isActive: boolean;
|
||
|
|
step: number;
|
||
|
|
completed: boolean;
|
||
|
|
skipped: boolean;
|
||
|
|
start: () => void;
|
||
|
|
next: () => void;
|
||
|
|
prev: () => void;
|
||
|
|
skip: () => void;
|
||
|
|
complete: () => void;
|
||
|
|
goToStep: (n: number) => void;
|
||
|
|
reset: () => void;
|
||
|
|
}
|
||
|
|
|
||
|
|
interface PersistedData {
|
||
|
|
step: number;
|
||
|
|
completed: boolean;
|
||
|
|
skipped: boolean;
|
||
|
|
}
|
||
|
|
|
||
|
|
function loadPersisted(): Partial<OnboardingState> {
|
||
|
|
try {
|
||
|
|
const raw = localStorage.getItem(STORAGE_KEY);
|
||
|
|
if (!raw) return {};
|
||
|
|
const data: PersistedData = JSON.parse(raw);
|
||
|
|
return {
|
||
|
|
step: data.step ?? 0,
|
||
|
|
completed: data.completed ?? false,
|
||
|
|
skipped: data.skipped ?? false,
|
||
|
|
};
|
||
|
|
} catch {
|
||
|
|
return {};
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
function persist(state: OnboardingState): void {
|
||
|
|
try {
|
||
|
|
const data: PersistedData = {
|
||
|
|
step: state.step,
|
||
|
|
completed: state.completed,
|
||
|
|
skipped: state.skipped,
|
||
|
|
};
|
||
|
|
localStorage.setItem(STORAGE_KEY, JSON.stringify(data));
|
||
|
|
} catch {
|
||
|
|
// localStorage may be unavailable in some contexts
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
const initial = loadPersisted();
|
||
|
|
|
||
|
|
export const useOnboardingStore = create<OnboardingState>((set, get) => ({
|
||
|
|
isActive: false,
|
||
|
|
step: initial.step ?? 0,
|
||
|
|
completed: initial.completed ?? false,
|
||
|
|
skipped: initial.skipped ?? false,
|
||
|
|
|
||
|
|
start: () => {
|
||
|
|
set({ isActive: true, step: 0, completed: false, skipped: false });
|
||
|
|
persist(get());
|
||
|
|
},
|
||
|
|
|
||
|
|
next: () => {
|
||
|
|
const s = get();
|
||
|
|
set({ step: s.step + 1 });
|
||
|
|
persist(get());
|
||
|
|
},
|
||
|
|
|
||
|
|
prev: () => {
|
||
|
|
const s = get();
|
||
|
|
set({ step: Math.max(0, s.step - 1) });
|
||
|
|
persist(get());
|
||
|
|
},
|
||
|
|
|
||
|
|
skip: () => {
|
||
|
|
set({ isActive: false, skipped: true });
|
||
|
|
persist(get());
|
||
|
|
},
|
||
|
|
|
||
|
|
complete: () => {
|
||
|
|
set({ isActive: false, completed: true, step: 0 });
|
||
|
|
persist(get());
|
||
|
|
},
|
||
|
|
|
||
|
|
goToStep: (n: number) => {
|
||
|
|
set({ step: Math.max(0, n) });
|
||
|
|
persist(get());
|
||
|
|
},
|
||
|
|
|
||
|
|
reset: () => {
|
||
|
|
set({ isActive: false, step: 0, completed: false, skipped: false });
|
||
|
|
persist(get());
|
||
|
|
},
|
||
|
|
}));
|