Improve the demo feature of the project
This commit is contained in:
170
frontend/src/features/demo-onboarding/hooks/useDemoTour.ts
Normal file
170
frontend/src/features/demo-onboarding/hooks/useDemoTour.ts
Normal file
@@ -0,0 +1,170 @@
|
||||
import { useState, useCallback, useEffect } from 'react';
|
||||
import { driver, Driver } from 'driver.js';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { getDriverConfig } from '../config/driver-config';
|
||||
import { getDemoTourSteps, getMobileTourSteps } from '../config/tour-steps';
|
||||
import { getTourState, saveTourState, clearTourState, clearTourStartPending } from '../utils/tour-state';
|
||||
import { trackTourEvent } from '../utils/tour-analytics';
|
||||
import '../styles.css';
|
||||
|
||||
export const useDemoTour = () => {
|
||||
const navigate = useNavigate();
|
||||
const [tourActive, setTourActive] = useState(false);
|
||||
const [driverInstance, setDriverInstance] = useState<Driver | null>(null);
|
||||
|
||||
const isMobile = window.innerWidth < 768;
|
||||
|
||||
const handleTourDestroy = useCallback(() => {
|
||||
const state = getTourState();
|
||||
const currentStep = driverInstance?.getActiveIndex() || 0;
|
||||
|
||||
if (state && !state.completed) {
|
||||
saveTourState({
|
||||
currentStep,
|
||||
dismissed: true,
|
||||
});
|
||||
|
||||
trackTourEvent({
|
||||
event: 'tour_dismissed',
|
||||
step: currentStep,
|
||||
timestamp: Date.now(),
|
||||
});
|
||||
}
|
||||
|
||||
setTourActive(false);
|
||||
clearTourStartPending();
|
||||
}, [driverInstance]);
|
||||
|
||||
const handleStepComplete = useCallback((stepIndex: number) => {
|
||||
saveTourState({
|
||||
currentStep: stepIndex + 1,
|
||||
});
|
||||
|
||||
trackTourEvent({
|
||||
event: 'tour_step_completed',
|
||||
step: stepIndex,
|
||||
timestamp: Date.now(),
|
||||
});
|
||||
}, []);
|
||||
|
||||
const handleTourComplete = useCallback(() => {
|
||||
saveTourState({
|
||||
completed: true,
|
||||
currentStep: 0,
|
||||
});
|
||||
|
||||
trackTourEvent({
|
||||
event: 'tour_completed',
|
||||
timestamp: Date.now(),
|
||||
});
|
||||
|
||||
setTourActive(false);
|
||||
clearTourStartPending();
|
||||
|
||||
setTimeout(() => {
|
||||
trackTourEvent({
|
||||
event: 'conversion_cta_clicked',
|
||||
timestamp: Date.now(),
|
||||
});
|
||||
navigate('/register?from=demo_tour');
|
||||
}, 500);
|
||||
}, [navigate]);
|
||||
|
||||
const startTour = useCallback((fromStep: number = 0) => {
|
||||
console.log('[useDemoTour] startTour called with fromStep:', fromStep);
|
||||
|
||||
const steps = isMobile ? getMobileTourSteps() : getDemoTourSteps();
|
||||
console.log('[useDemoTour] Using', isMobile ? 'mobile' : 'desktop', 'steps, total:', steps.length);
|
||||
|
||||
// Check if first element exists
|
||||
const firstElement = steps[0]?.element;
|
||||
if (firstElement) {
|
||||
const el = document.querySelector(firstElement);
|
||||
console.log('[useDemoTour] First element exists:', !!el, 'selector:', firstElement);
|
||||
if (!el) {
|
||||
console.warn('[useDemoTour] First tour element not found in DOM! Delaying tour start...');
|
||||
// Retry after DOM is ready
|
||||
setTimeout(() => startTour(fromStep), 500);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const config = getDriverConfig(handleStepComplete);
|
||||
|
||||
const driverObj = driver({
|
||||
...config,
|
||||
onDestroyed: (element, step, options) => {
|
||||
const activeIndex = options.state?.activeIndex || 0;
|
||||
const isLastStep = activeIndex === steps.length - 1;
|
||||
|
||||
console.log('[useDemoTour] Tour destroyed, activeIndex:', activeIndex, 'isLastStep:', isLastStep);
|
||||
|
||||
if (isLastStep) {
|
||||
handleTourComplete();
|
||||
} else {
|
||||
handleTourDestroy();
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
driverObj.setSteps(steps);
|
||||
setDriverInstance(driverObj);
|
||||
|
||||
console.log('[useDemoTour] Driver instance created, starting tour...');
|
||||
|
||||
if (fromStep > 0 && fromStep < steps.length) {
|
||||
driverObj.drive(fromStep);
|
||||
} else {
|
||||
driverObj.drive();
|
||||
}
|
||||
|
||||
setTourActive(true);
|
||||
|
||||
trackTourEvent({
|
||||
event: 'tour_started',
|
||||
timestamp: Date.now(),
|
||||
});
|
||||
|
||||
saveTourState({
|
||||
currentStep: fromStep,
|
||||
completed: false,
|
||||
dismissed: false,
|
||||
});
|
||||
|
||||
clearTourStartPending();
|
||||
}, [isMobile, handleTourDestroy, handleStepComplete, handleTourComplete]);
|
||||
|
||||
const resumeTour = useCallback(() => {
|
||||
const state = getTourState();
|
||||
if (state && state.currentStep > 0) {
|
||||
startTour(state.currentStep);
|
||||
} else {
|
||||
startTour();
|
||||
}
|
||||
}, [startTour]);
|
||||
|
||||
const resetTour = useCallback(() => {
|
||||
clearTourState();
|
||||
if (driverInstance) {
|
||||
driverInstance.destroy();
|
||||
setDriverInstance(null);
|
||||
}
|
||||
setTourActive(false);
|
||||
}, [driverInstance]);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (driverInstance) {
|
||||
driverInstance.destroy();
|
||||
}
|
||||
};
|
||||
}, [driverInstance]);
|
||||
|
||||
return {
|
||||
startTour,
|
||||
resumeTour,
|
||||
resetTour,
|
||||
tourActive,
|
||||
tourState: getTourState(),
|
||||
};
|
||||
};
|
||||
Reference in New Issue
Block a user