migration from react-joyride to driver.js:
All checks were successful
Deploy to Test Environment / deploy-to-test (push) Successful in 18m52s

This commit is contained in:
2026-01-21 10:07:38 -08:00
parent 65c38765c6
commit 3314063e25
8 changed files with 309 additions and 268 deletions

View File

@@ -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,
};
};