migration from react-joyride to driver.js:
All checks were successful
Deploy to Test Environment / deploy-to-test (push) Successful in 18m52s
All checks were successful
Deploy to Test Environment / deploy-to-test (push) Successful in 18m52s
This commit is contained in:
@@ -1,87 +1,286 @@
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import type { Step, CallBackProps } from 'react-joyride';
|
||||
import { useEffect, useCallback, useRef } from 'react';
|
||||
import { driver, Driver, DriveStep } from 'driver.js';
|
||||
import 'driver.js/dist/driver.css';
|
||||
|
||||
const ONBOARDING_STORAGE_KEY = 'flyer_crawler_onboarding_completed';
|
||||
|
||||
export const useOnboardingTour = () => {
|
||||
const [runTour, setRunTour] = useState(false);
|
||||
const [stepIndex, setStepIndex] = useState(0);
|
||||
// Custom CSS to match design system: pastel colors, sharp borders, minimalist
|
||||
const DRIVER_CSS = `
|
||||
.driver-popover {
|
||||
background-color: #f0fdfa !important;
|
||||
border: 2px solid #0d9488 !important;
|
||||
border-radius: 0 !important;
|
||||
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06) !important;
|
||||
max-width: 320px !important;
|
||||
}
|
||||
|
||||
.driver-popover-title {
|
||||
color: #134e4a !important;
|
||||
font-size: 1rem !important;
|
||||
font-weight: 600 !important;
|
||||
margin-bottom: 0.5rem !important;
|
||||
}
|
||||
|
||||
.driver-popover-description {
|
||||
color: #1f2937 !important;
|
||||
font-size: 0.875rem !important;
|
||||
line-height: 1.5 !important;
|
||||
}
|
||||
|
||||
.driver-popover-progress-text {
|
||||
color: #0d9488 !important;
|
||||
font-size: 0.75rem !important;
|
||||
font-weight: 500 !important;
|
||||
}
|
||||
|
||||
.driver-popover-navigation-btns {
|
||||
gap: 0.5rem !important;
|
||||
}
|
||||
|
||||
.driver-popover-prev-btn,
|
||||
.driver-popover-next-btn {
|
||||
background-color: #14b8a6 !important;
|
||||
color: white !important;
|
||||
border: 1px solid #0d9488 !important;
|
||||
border-radius: 0 !important;
|
||||
padding: 0.5rem 1rem !important;
|
||||
font-size: 0.875rem !important;
|
||||
font-weight: 500 !important;
|
||||
transition: background-color 0.15s ease !important;
|
||||
}
|
||||
|
||||
.driver-popover-prev-btn:hover,
|
||||
.driver-popover-next-btn:hover {
|
||||
background-color: #115e59 !important;
|
||||
}
|
||||
|
||||
.driver-popover-prev-btn {
|
||||
background-color: #ccfbf1 !important;
|
||||
color: #134e4a !important;
|
||||
}
|
||||
|
||||
.driver-popover-prev-btn:hover {
|
||||
background-color: #99f6e4 !important;
|
||||
}
|
||||
|
||||
.driver-popover-close-btn {
|
||||
color: #0d9488 !important;
|
||||
font-size: 1.25rem !important;
|
||||
}
|
||||
|
||||
.driver-popover-close-btn:hover {
|
||||
color: #115e59 !important;
|
||||
}
|
||||
|
||||
.driver-popover-arrow-side-left,
|
||||
.driver-popover-arrow-side-right,
|
||||
.driver-popover-arrow-side-top,
|
||||
.driver-popover-arrow-side-bottom {
|
||||
border-color: #0d9488 !important;
|
||||
}
|
||||
|
||||
.driver-popover-arrow-side-left::after,
|
||||
.driver-popover-arrow-side-right::after,
|
||||
.driver-popover-arrow-side-top::after,
|
||||
.driver-popover-arrow-side-bottom::after {
|
||||
border-color: transparent !important;
|
||||
}
|
||||
|
||||
.driver-popover-arrow-side-left::before {
|
||||
border-right-color: #f0fdfa !important;
|
||||
}
|
||||
|
||||
.driver-popover-arrow-side-right::before {
|
||||
border-left-color: #f0fdfa !important;
|
||||
}
|
||||
|
||||
.driver-popover-arrow-side-top::before {
|
||||
border-bottom-color: #f0fdfa !important;
|
||||
}
|
||||
|
||||
.driver-popover-arrow-side-bottom::before {
|
||||
border-top-color: #f0fdfa !important;
|
||||
}
|
||||
|
||||
.driver-overlay {
|
||||
background-color: rgba(0, 0, 0, 0.5) !important;
|
||||
}
|
||||
|
||||
.driver-active-element {
|
||||
box-shadow: 0 0 0 4px #14b8a6 !important;
|
||||
}
|
||||
|
||||
/* Dark mode support */
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.driver-popover {
|
||||
background-color: #1f2937 !important;
|
||||
border-color: #14b8a6 !important;
|
||||
}
|
||||
|
||||
.driver-popover-title {
|
||||
color: #ccfbf1 !important;
|
||||
}
|
||||
|
||||
.driver-popover-description {
|
||||
color: #e5e7eb !important;
|
||||
}
|
||||
|
||||
.driver-popover-arrow-side-left::before {
|
||||
border-right-color: #1f2937 !important;
|
||||
}
|
||||
|
||||
.driver-popover-arrow-side-right::before {
|
||||
border-left-color: #1f2937 !important;
|
||||
}
|
||||
|
||||
.driver-popover-arrow-side-top::before {
|
||||
border-bottom-color: #1f2937 !important;
|
||||
}
|
||||
|
||||
.driver-popover-arrow-side-bottom::before {
|
||||
border-top-color: #1f2937 !important;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const tourSteps: DriveStep[] = [
|
||||
{
|
||||
element: '[data-tour="flyer-uploader"]',
|
||||
popover: {
|
||||
title: 'Upload Flyers',
|
||||
description:
|
||||
'Upload a grocery flyer here by clicking or dragging a PDF/image file. Our AI will extract prices and items automatically.',
|
||||
side: 'bottom',
|
||||
align: 'start',
|
||||
},
|
||||
},
|
||||
{
|
||||
element: '[data-tour="extracted-data-table"]',
|
||||
popover: {
|
||||
title: 'Extracted Items',
|
||||
description:
|
||||
'View all extracted items from your flyers here. You can watch items to track price changes and deals.',
|
||||
side: 'top',
|
||||
align: 'start',
|
||||
},
|
||||
},
|
||||
{
|
||||
element: '[data-tour="watch-button"]',
|
||||
popover: {
|
||||
title: 'Watch Items',
|
||||
description:
|
||||
'Click the eye icon to watch items and get notified when prices drop or deals appear.',
|
||||
side: 'left',
|
||||
align: 'start',
|
||||
},
|
||||
},
|
||||
{
|
||||
element: '[data-tour="watched-items"]',
|
||||
popover: {
|
||||
title: 'Watched Items',
|
||||
description:
|
||||
'Your watched items appear here. Track prices across different stores and get deal alerts.',
|
||||
side: 'left',
|
||||
align: 'start',
|
||||
},
|
||||
},
|
||||
{
|
||||
element: '[data-tour="price-chart"]',
|
||||
popover: {
|
||||
title: 'Active Deals',
|
||||
description:
|
||||
'Active deals show here with price comparisons. See which store has the best price!',
|
||||
side: 'left',
|
||||
align: 'start',
|
||||
},
|
||||
},
|
||||
{
|
||||
element: '[data-tour="shopping-list"]',
|
||||
popover: {
|
||||
title: 'Shopping Lists',
|
||||
description:
|
||||
'Create shopping lists from your watched items and get the best prices automatically.',
|
||||
side: 'left',
|
||||
align: 'start',
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
// Inject custom styles into the document head
|
||||
const injectStyles = () => {
|
||||
const styleId = 'driver-js-custom-styles';
|
||||
if (!document.getElementById(styleId)) {
|
||||
const style = document.createElement('style');
|
||||
style.id = styleId;
|
||||
style.textContent = DRIVER_CSS;
|
||||
document.head.appendChild(style);
|
||||
}
|
||||
};
|
||||
|
||||
export const useOnboardingTour = () => {
|
||||
const driverRef = useRef<Driver | null>(null);
|
||||
|
||||
const markTourComplete = useCallback(() => {
|
||||
localStorage.setItem(ONBOARDING_STORAGE_KEY, 'true');
|
||||
}, []);
|
||||
|
||||
const startTour = useCallback(() => {
|
||||
injectStyles();
|
||||
|
||||
if (driverRef.current) {
|
||||
driverRef.current.destroy();
|
||||
}
|
||||
|
||||
driverRef.current = driver({
|
||||
showProgress: true,
|
||||
steps: tourSteps,
|
||||
nextBtnText: 'Next',
|
||||
prevBtnText: 'Previous',
|
||||
doneBtnText: 'Done',
|
||||
progressText: 'Step {{current}} of {{total}}',
|
||||
onDestroyed: () => {
|
||||
markTourComplete();
|
||||
},
|
||||
});
|
||||
|
||||
driverRef.current.drive();
|
||||
}, [markTourComplete]);
|
||||
|
||||
const skipTour = useCallback(() => {
|
||||
if (driverRef.current) {
|
||||
driverRef.current.destroy();
|
||||
}
|
||||
markTourComplete();
|
||||
}, [markTourComplete]);
|
||||
|
||||
const replayTour = useCallback(() => {
|
||||
startTour();
|
||||
}, [startTour]);
|
||||
|
||||
// Auto-start tour on mount if not completed
|
||||
useEffect(() => {
|
||||
const hasCompletedOnboarding = localStorage.getItem(ONBOARDING_STORAGE_KEY);
|
||||
if (!hasCompletedOnboarding) {
|
||||
setRunTour(true);
|
||||
// Small delay to ensure DOM elements are mounted
|
||||
const timer = setTimeout(() => {
|
||||
startTour();
|
||||
}, 500);
|
||||
return () => clearTimeout(timer);
|
||||
}
|
||||
}, []);
|
||||
}, [startTour]);
|
||||
|
||||
const steps: Step[] = [
|
||||
{
|
||||
target: '[data-tour="flyer-uploader"]',
|
||||
content:
|
||||
'Upload a grocery flyer here by clicking or dragging a PDF/image file. Our AI will extract prices and items automatically.',
|
||||
disableBeacon: true,
|
||||
placement: 'bottom',
|
||||
},
|
||||
{
|
||||
target: '[data-tour="extracted-data-table"]',
|
||||
content:
|
||||
'View all extracted items from your flyers here. You can watch items to track price changes and deals.',
|
||||
placement: 'top',
|
||||
},
|
||||
{
|
||||
target: '[data-tour="watch-button"]',
|
||||
content:
|
||||
'Click the eye icon to watch items and get notified when prices drop or deals appear.',
|
||||
placement: 'left',
|
||||
},
|
||||
{
|
||||
target: '[data-tour="watched-items"]',
|
||||
content:
|
||||
'Your watched items appear here. Track prices across different stores and get deal alerts.',
|
||||
placement: 'left',
|
||||
},
|
||||
{
|
||||
target: '[data-tour="price-chart"]',
|
||||
content: 'Active deals show here with price comparisons. See which store has the best price!',
|
||||
placement: 'left',
|
||||
},
|
||||
{
|
||||
target: '[data-tour="shopping-list"]',
|
||||
content:
|
||||
'Create shopping lists from your watched items and get the best prices automatically.',
|
||||
placement: 'left',
|
||||
},
|
||||
];
|
||||
|
||||
const handleJoyrideCallback = useCallback((data: CallBackProps) => {
|
||||
const { status, index } = data;
|
||||
|
||||
if (status === 'finished' || status === 'skipped') {
|
||||
localStorage.setItem(ONBOARDING_STORAGE_KEY, 'true');
|
||||
setRunTour(false);
|
||||
setStepIndex(0);
|
||||
} else if (data.action === 'next' || data.action === 'prev') {
|
||||
setStepIndex(index + (data.action === 'next' ? 1 : 0));
|
||||
}
|
||||
}, []);
|
||||
|
||||
const skipTour = useCallback(() => {
|
||||
localStorage.setItem(ONBOARDING_STORAGE_KEY, 'true');
|
||||
setRunTour(false);
|
||||
setStepIndex(0);
|
||||
}, []);
|
||||
|
||||
const replayTour = useCallback(() => {
|
||||
setStepIndex(0);
|
||||
setRunTour(true);
|
||||
// Cleanup on unmount
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (driverRef.current) {
|
||||
driverRef.current.destroy();
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
return {
|
||||
runTour,
|
||||
steps,
|
||||
stepIndex,
|
||||
handleJoyrideCallback,
|
||||
skipTour,
|
||||
replayTour,
|
||||
startTour,
|
||||
};
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user