Files
igny8/frontend/src/store/onboardingStore.ts

103 lines
3.2 KiB
TypeScript

/**
* Onboarding Store (Zustand)
* Manages welcome/guide screen state and dismissal
* Syncs with backend UserSettings for cross-device persistence
*/
import { create } from 'zustand';
import { persist } from 'zustand/middleware';
import { fetchUserSetting, createUserSetting, updateUserSetting } from '../services/api';
interface OnboardingState {
isGuideDismissed: boolean;
isGuideVisible: boolean;
isLoading: boolean;
lastSyncedAt: Date | null;
// Actions
dismissGuide: () => Promise<void>;
showGuide: () => void;
toggleGuide: () => void;
loadFromBackend: () => Promise<void>;
syncToBackend: (dismissed: boolean) => Promise<void>;
}
const GUIDE_SETTING_KEY = 'workflow_guide_dismissed';
export const useOnboardingStore = create<OnboardingState>()(
persist<OnboardingState>(
(set, get) => ({
isGuideDismissed: false,
isGuideVisible: false,
isLoading: false,
lastSyncedAt: null,
loadFromBackend: async () => {
const state = get();
// Avoid hammering the endpoint; re-fetch at most every 5 minutes
if (state.lastSyncedAt) {
const elapsedMs = Date.now() - state.lastSyncedAt.getTime();
if (elapsedMs < 5 * 60 * 1000) {
return;
}
}
if (state.isLoading) return;
set({ isLoading: true });
try {
const setting = await fetchUserSetting(GUIDE_SETTING_KEY);
const dismissed = setting.value?.dismissed === true;
set({
isGuideDismissed: dismissed,
isGuideVisible: !dismissed,
lastSyncedAt: new Date(),
isLoading: false
});
} catch (error: any) {
// 404 means setting doesn't exist yet - that's fine, use local state
if (error?.status === 429) {
// Throttled: back off and don't spam warnings
} else if (error?.status !== 404) {
console.warn('Failed to load guide dismissal from backend:', error);
}
set({ isLoading: false });
}
},
syncToBackend: async (dismissed: boolean) => {
try {
const data = { value: { dismissed, dismissed_at: new Date().toISOString() } };
try {
await updateUserSetting(GUIDE_SETTING_KEY, data);
} catch (error: any) {
// If setting doesn't exist, create it
if (error.status === 404) {
await createUserSetting({ key: GUIDE_SETTING_KEY, value: data.value });
} else {
throw error;
}
}
set({ lastSyncedAt: new Date() });
} catch (error) {
console.warn('Failed to sync guide dismissal to backend:', error);
// Don't throw - local state is still updated
}
},
dismissGuide: async () => {
set({ isGuideDismissed: true, isGuideVisible: false });
// Sync to backend asynchronously
await get().syncToBackend(true);
},
showGuide: () => set({ isGuideVisible: true }),
toggleGuide: () => set((state) => ({ isGuideVisible: !state.isGuideVisible })),
}),
{
name: 'onboarding-storage',
}
)
);