'use client'; import { createContext, useContext, useEffect, useRef, useCallback } from 'react'; import { usePathname } from 'next/navigation'; import { driver, type DriveStep, type Driver } from 'driver.js'; import 'driver.js/dist/driver.css'; import '@/styles/driver-onboarding.css'; import { MENTOR_ONBOARDING, CLIENT_ONBOARDING, PARENT_ONBOARDING, getOnboardingKey, getOnboardingProgress, } from '@/lib/onboarding-steps'; import { getProfileSettings, updateProfileSettings } from '@/api/profile'; import { useAuth } from '@/contexts/AuthContext'; type Role = 'mentor' | 'client' | 'parent'; interface OnboardingContextType { markTourSeen: (pageId: string) => Promise; runTourManually: (pageKey: string, options?: { force?: boolean }) => void; getProgress: () => { seen: number; total: number }; refreshProgress: () => Promise; } const OnboardingContext = createContext(null); export function useOnboarding() { return useContext(OnboardingContext); } export function OnboardingProvider({ children }: { children: React.ReactNode }) { const pathname = usePathname(); const { user } = useAuth(); const toursSeenRef = useRef>({}); const driverRef = useRef(null); const markTourSeen = useCallback(async (pageId: string) => { if (!pageId) return; toursSeenRef.current[pageId] = true; try { await updateProfileSettings({ onboarding_tours_seen: { [pageId]: true }, }); } catch { // ignore } }, []); const runTour = useCallback( ( config: { pageId: string; steps: { element?: string; popover: { title: string; description: string; side?: string; align?: string } }[] }, skipSeenCheck?: boolean ) => { if (!skipSeenCheck && toursSeenRef.current[config.pageId]) return; if (!config.steps.length) return; // Преобразуем шаги в формат driver.js const steps: DriveStep[] = config.steps .map((s) => { // driver.js: если элемент не найден, step показывается как overlay по центру const element = s.element && document.querySelector(s.element) ? s.element : undefined; return { element: element || undefined, popover: { title: s.popover.title, description: s.popover.description, side: (s.popover.side as 'top' | 'right' | 'bottom' | 'left') || 'bottom', align: (s.popover.align as 'start' | 'center' | 'end') || 'center', }, }; }) .filter((s) => s.element || s.popover); if (steps.length === 0) return; if (driverRef.current) { driverRef.current.destroy(); driverRef.current = null; } const driverObj = driver({ showProgress: true, steps, nextBtnText: 'Далее', prevBtnText: 'Назад', doneBtnText: 'Понятно', progressText: '{{current}} из {{total}}', popoverClass: 'driver-onboarding-friendly', onDestroyStarted: () => { markTourSeen(config.pageId); driverObj.destroy(); driverRef.current = null; }, }); driverRef.current = driverObj; driverObj.drive(); }, [markTourSeen] ); const runTourManually = useCallback( (pageKey: string, options?: { force?: boolean }) => { const role = user?.role as Role; if (!role || !['mentor', 'client', 'parent'].includes(role)) return; const configs = role === 'mentor' ? MENTOR_ONBOARDING : role === 'client' ? CLIENT_ONBOARDING : PARENT_ONBOARDING; const config = configs[pageKey]; if (config) runTour(config, options?.force); }, [user?.role, runTour] ); const getProgress = useCallback(() => { const role = user?.role as Role; if (!role || !['mentor', 'client', 'parent'].includes(role)) return { seen: 0, total: 0 }; return getOnboardingProgress(toursSeenRef.current, role); }, [user?.role]); const refreshProgress = useCallback(async () => { try { const settings = await getProfileSettings(); const seen = settings?.onboarding_tours_seen ?? {}; toursSeenRef.current = { ...toursSeenRef.current, ...seen }; } catch { // ignore } }, []); const runTourRef = useRef(runTour); runTourRef.current = runTour; useEffect(() => { if (!user) return; getProfileSettings() .then((s) => { const seen = s?.onboarding_tours_seen ?? {}; toursSeenRef.current = { ...toursSeenRef.current, ...seen }; }) .catch(() => {}); }, [user?.id]); useEffect(() => { if (!user || !pathname) return; const role = user.role as Role; if (!['mentor', 'client', 'parent'].includes(role)) return; if (pathname.startsWith('/login') || pathname.startsWith('/register') || pathname.startsWith('/livekit')) return; const key = getOnboardingKey(pathname, role); if (!key) return; const configs = role === 'mentor' ? MENTOR_ONBOARDING : role === 'client' ? CLIENT_ONBOARDING : PARENT_ONBOARDING; const config = configs[key]; if (!config) return; let cancelled = false; const loadAndRun = async () => { try { const settings = await getProfileSettings(); if (cancelled) return; const seen = settings?.onboarding_tours_seen ?? {}; toursSeenRef.current = { ...toursSeenRef.current, ...seen }; if (seen[config.pageId]) return; setTimeout(() => { if (!cancelled) runTourRef.current(config); }, 600); } catch { // при ошибке не показываем тур } }; loadAndRun(); return () => { cancelled = true; }; }, [pathname, user]); const value: OnboardingContextType = { markTourSeen, runTourManually, getProgress, refreshProgress, }; return ( {children} ); }