uchill/front_material/contexts/OnboardingContext.tsx

190 lines
6.0 KiB
TypeScript

'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<void>;
runTourManually: (pageKey: string, options?: { force?: boolean }) => void;
getProgress: () => { seen: number; total: number };
refreshProgress: () => Promise<void>;
}
const OnboardingContext = createContext<OnboardingContextType | null>(null);
export function useOnboarding() {
return useContext(OnboardingContext);
}
export function OnboardingProvider({ children }: { children: React.ReactNode }) {
const pathname = usePathname();
const { user } = useAuth();
const toursSeenRef = useRef<Record<string, boolean>>({});
const driverRef = useRef<Driver | null>(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 (
<OnboardingContext.Provider value={value}>
{children}
</OnboardingContext.Provider>
);
}