From 3cf89b6eded59c56f1ed400eab5ae1967eeb079b Mon Sep 17 00:00:00 2001 From: Anthony Stirling <77850077+Frooodle@users.noreply.github.com> Date: Mon, 10 Nov 2025 13:47:43 +0000 Subject: [PATCH] admin onboarding (#4863) # Description of Changes --- ## Checklist ### General - [ ] I have read the [Contribution Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md) - [ ] I have read the [Stirling-PDF Developer Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md) (if applicable) - [ ] I have read the [How to add new languages to Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md) (if applicable) - [ ] I have performed a self-review of my own code - [ ] My changes generate no new warnings ### Documentation - [ ] I have updated relevant docs on [Stirling-PDF's doc repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/) (if functionality has heavily changed) - [ ] I have read the section [Add New Translation Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags) (for new translation tags only) ### Translations (if applicable) - [ ] I ran [`scripts/counter_translation.py`](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/docs/counter_translation.md) ### UI Changes (if applicable) - [ ] Screenshots or videos demonstrating the UI changes are attached (e.g., as comments or direct attachments in the PR) ### Testing (if applicable) - [ ] I have tested my changes locally. Refer to the [Testing Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing) for more details. --------- Co-authored-by: EthanHealy01 <80844253+EthanHealy01@users.noreply.github.com> --- .../public/locales/en-GB/translation.json | 19 +- frontend/src/core/components/AppProviders.tsx | 5 +- .../components/onboarding/OnboardingTour.css | 25 +++ .../components/onboarding/OnboardingTour.tsx | 185 +++++++++++++++++- .../core/components/shared/AppConfigModal.tsx | 3 +- .../core/components/shared/QuickAccessBar.tsx | 74 ++++++- .../AdminTourOrchestrationContext.tsx | 124 ++++++++++++ .../src/core/contexts/OnboardingContext.tsx | 15 +- 8 files changed, 431 insertions(+), 19 deletions(-) create mode 100644 frontend/src/core/contexts/AdminTourOrchestrationContext.tsx diff --git a/frontend/public/locales/en-GB/translation.json b/frontend/public/locales/en-GB/translation.json index 2e1417bf0..626f16f44 100644 --- a/frontend/public/locales/en-GB/translation.json +++ b/frontend/public/locales/en-GB/translation.json @@ -3539,7 +3539,13 @@ "account": "Account", "config": "Config", "adminSettings": "Admin Settings", - "allTools": "All Tools" + "allTools": "All Tools", + "helpMenu": { + "toolsTour": "Tools Tour", + "toolsTourDesc": "Learn what the tools can do", + "adminTour": "Admin Tour", + "adminTourDesc": "Explore admin settings & features" + } }, "admin": { "error": "Error", @@ -4674,6 +4680,17 @@ "startTour": "Start Tour", "startTourDescription": "Take a guided tour of Stirling PDF's key features" }, + "adminOnboarding": { + "welcome": "Welcome to the Admin Tour! Let's explore the powerful enterprise features and settings available to system administrators.", + "configButton": "Click the Config button to access all system settings and administrative controls.", + "settingsOverview": "This is the Settings Panel. Admin settings are organised by category for easy navigation.", + "teamsAndUsers": "Manage Teams and individual users here. You can invite new users via email, shareable links, or create custom accounts for them yourself.", + "systemCustomization": "We have extensive ways to customise the UI: System Settings let you change the app name and languages, Features allows server certificate management, and Endpoints lets you enable or disable specific tools for your users.", + "databaseSection": "For advanced production environments, we have settings to allow external database hookups so you can integrate with your existing infrastructure.", + "connectionsSection": "The Connections section supports various login methods including custom SSO and SAML providers like Google and GitHub, plus email integrations for notifications and communications.", + "adminTools": "Finally, we have advanced administration tools like Auditing to track system activity and Usage Analytics to monitor how your users interact with the platform.", + "wrapUp": "That's the admin tour! You've seen the enterprise features that make Stirling PDF a powerful, customisable solution for organisations. Access this tour anytime from the Help menu." + }, "workspace": { "title": "Workspace", "people": { diff --git a/frontend/src/core/components/AppProviders.tsx b/frontend/src/core/components/AppProviders.tsx index 46ce96477..2108b3550 100644 --- a/frontend/src/core/components/AppProviders.tsx +++ b/frontend/src/core/components/AppProviders.tsx @@ -14,6 +14,7 @@ import { ViewerProvider } from "@app/contexts/ViewerContext"; import { SignatureProvider } from "@app/contexts/SignatureContext"; import { OnboardingProvider } from "@app/contexts/OnboardingContext"; import { TourOrchestrationProvider } from "@app/contexts/TourOrchestrationContext"; +import { AdminTourOrchestrationProvider } from "@app/contexts/AdminTourOrchestrationContext"; import ErrorBoundary from "@app/components/shared/ErrorBoundary"; import { useScarfTracking } from "@app/hooks/useScarfTracking"; import { useAppInitialization } from "@app/hooks/useAppInitialization"; @@ -59,7 +60,9 @@ export function AppProviders({ children, appConfigRetryOptions }: AppProvidersPr - {children} + + {children} + diff --git a/frontend/src/core/components/onboarding/OnboardingTour.css b/frontend/src/core/components/onboarding/OnboardingTour.css index fb96ec1d2..9667e835a 100644 --- a/frontend/src/core/components/onboarding/OnboardingTour.css +++ b/frontend/src/core/components/onboarding/OnboardingTour.css @@ -6,3 +6,28 @@ ry: 8px; filter: drop-shadow(0 0 10px var(--mantine-primary-color-filled)); } + +/* Add glowing border to navigation items during admin tour */ +.modal-nav-item.tour-nav-glow { + position: relative; + box-shadow: + 0 0 0 2px var(--mantine-primary-color-filled), + 0 0 15px var(--mantine-primary-color-filled), + inset 0 0 15px rgba(59, 130, 246, 0.1); + border-radius: 8px; +} + +@keyframes pulse-glow { + 0%, 100% { + box-shadow: + 0 0 0 3px var(--mantine-primary-color-filled), + 0 0 20px var(--mantine-primary-color-filled), + inset 0 0 20px rgba(59, 130, 246, 0.1); + } + 50% { + box-shadow: + 0 0 0 3px var(--mantine-primary-color-filled), + 0 0 30px var(--mantine-primary-color-filled), + inset 0 0 30px rgba(59, 130, 246, 0.2); + } +} diff --git a/frontend/src/core/components/onboarding/OnboardingTour.tsx b/frontend/src/core/components/onboarding/OnboardingTour.tsx index 4a4a770f1..8f7608b14 100644 --- a/frontend/src/core/components/onboarding/OnboardingTour.tsx +++ b/frontend/src/core/components/onboarding/OnboardingTour.tsx @@ -5,6 +5,7 @@ import { useTranslation } from 'react-i18next'; import { CloseButton, ActionIcon } from '@mantine/core'; import { useFilesModalContext } from '@app/contexts/FilesModalContext'; import { useTourOrchestration } from '@app/contexts/TourOrchestrationContext'; +import { useAdminTourOrchestration } from '@app/contexts/AdminTourOrchestrationContext'; import ArrowForwardIcon from '@mui/icons-material/ArrowForward'; import CheckIcon from '@mui/icons-material/Check'; import TourWelcomeModal from '@app/components/onboarding/TourWelcomeModal'; @@ -32,6 +33,18 @@ enum TourStep { WRAP_UP, } +enum AdminTourStep { + WELCOME, + CONFIG_BUTTON, + SETTINGS_OVERVIEW, + TEAMS_AND_USERS, + SYSTEM_CUSTOMIZATION, + DATABASE_SECTION, + CONNECTIONS_SECTION, + ADMIN_TOOLS, + WRAP_UP, +} + function TourContent() { const { isOpen } = useOnboarding(); const { setIsOpen, setCurrentStep } = useTour(); @@ -54,8 +67,36 @@ function TourContent() { export default function OnboardingTour() { const { t } = useTranslation(); - const { completeTour, showWelcomeModal, setShowWelcomeModal, startTour } = useOnboarding(); + const { completeTour, showWelcomeModal, setShowWelcomeModal, startTour, tourType, isOpen } = useOnboarding(); const { openFilesModal, closeFilesModal } = useFilesModalContext(); + + // Helper to add glow to multiple elements + const addGlowToElements = (selectors: string[]) => { + selectors.forEach(selector => { + const element = document.querySelector(selector); + if (element) { + if (selector === '[data-tour="settings-content-area"]') { + element.classList.add('tour-content-glow'); + } else { + element.classList.add('tour-nav-glow'); + } + } + }); + }; + + // Helper to remove all glows + const removeAllGlows = () => { + document.querySelectorAll('.tour-content-glow').forEach(el => el.classList.remove('tour-content-glow')); + document.querySelectorAll('.tour-nav-glow').forEach(el => el.classList.remove('tour-nav-glow')); + }; + + // Cleanup glows when tour closes + React.useEffect(() => { + if (!isOpen) { + removeAllGlows(); + } + return () => removeAllGlows(); + }, [isOpen]); const { saveWorkbenchState, restoreWorkbenchState, @@ -70,6 +111,13 @@ export default function OnboardingTour() { modifyCropSettings, executeTool, } = useTourOrchestration(); + const { + saveAdminState, + restoreAdminState, + openConfigModal, + navigateToSection, + scrollNavToSection, + } = useAdminTourOrchestration(); // Define steps as object keyed by enum - TypeScript ensures all keys are present const stepsConfig: Record = { @@ -202,8 +250,125 @@ export default function OnboardingTour() { }, }; - // Convert to array using enum's numeric ordering - const steps = Object.values(stepsConfig); + // Define admin tour steps + const adminStepsConfig: Record = { + [AdminTourStep.WELCOME]: { + selector: '[data-tour="config-button"]', + content: t('adminOnboarding.welcome', "Welcome to the Admin Tour! Let's explore the powerful enterprise features and settings available to system administrators."), + position: 'right', + padding: 10, + action: () => { + saveAdminState(); + }, + }, + [AdminTourStep.CONFIG_BUTTON]: { + selector: '[data-tour="config-button"]', + content: t('adminOnboarding.configButton', "Click the Config button to access all system settings and administrative controls."), + position: 'right', + padding: 10, + actionAfter: () => { + openConfigModal(); + }, + }, + [AdminTourStep.SETTINGS_OVERVIEW]: { + selector: '.modal-nav', + content: t('adminOnboarding.settingsOverview', "This is the Settings Panel. Admin settings are organised by category for easy navigation."), + position: 'right', + padding: 0, + action: () => { + removeAllGlows(); + }, + }, + [AdminTourStep.TEAMS_AND_USERS]: { + selector: '[data-tour="admin-people-nav"]', + highlightedSelectors: ['[data-tour="admin-people-nav"]', '[data-tour="admin-teams-nav"]', '[data-tour="settings-content-area"]'], + content: t('adminOnboarding.teamsAndUsers', "Manage Teams and individual users here. You can invite new users via email, shareable links, or create custom accounts for them yourself."), + position: 'right', + padding: 10, + action: () => { + removeAllGlows(); + navigateToSection('people'); + setTimeout(() => { + addGlowToElements(['[data-tour="admin-people-nav"]', '[data-tour="admin-teams-nav"]', '[data-tour="settings-content-area"]']); + }, 100); + }, + }, + [AdminTourStep.SYSTEM_CUSTOMIZATION]: { + selector: '[data-tour="admin-adminGeneral-nav"]', + highlightedSelectors: ['[data-tour="admin-adminGeneral-nav"]', '[data-tour="admin-adminFeatures-nav"]', '[data-tour="admin-adminEndpoints-nav"]', '[data-tour="settings-content-area"]'], + content: t('adminOnboarding.systemCustomization', "We have extensive ways to customise the UI: System Settings let you change the app name and languages, Features allows server certificate management, and Endpoints lets you enable or disable specific tools for your users."), + position: 'right', + padding: 10, + action: () => { + removeAllGlows(); + navigateToSection('adminGeneral'); + setTimeout(() => { + addGlowToElements(['[data-tour="admin-adminGeneral-nav"]', '[data-tour="admin-adminFeatures-nav"]', '[data-tour="admin-adminEndpoints-nav"]', '[data-tour="settings-content-area"]']); + }, 100); + }, + }, + [AdminTourStep.DATABASE_SECTION]: { + selector: '[data-tour="admin-adminDatabase-nav"]', + highlightedSelectors: ['[data-tour="admin-adminDatabase-nav"]', '[data-tour="settings-content-area"]'], + content: t('adminOnboarding.databaseSection', "For advanced production environments, we have settings to allow external database hookups so you can integrate with your existing infrastructure."), + position: 'right', + padding: 10, + action: () => { + removeAllGlows(); + navigateToSection('adminDatabase'); + setTimeout(() => { + addGlowToElements(['[data-tour="admin-adminDatabase-nav"]', '[data-tour="settings-content-area"]']); + }, 100); + }, + }, + [AdminTourStep.CONNECTIONS_SECTION]: { + selector: '[data-tour="admin-adminConnections-nav"]', + highlightedSelectors: ['[data-tour="admin-adminConnections-nav"]', '[data-tour="settings-content-area"]'], + content: t('adminOnboarding.connectionsSection', "The Connections section supports various login methods including custom SSO and SAML providers like Google and GitHub, plus email integrations for notifications and communications."), + position: 'right', + padding: 10, + action: () => { + removeAllGlows(); + navigateToSection('adminConnections'); + setTimeout(() => { + addGlowToElements(['[data-tour="admin-adminConnections-nav"]', '[data-tour="settings-content-area"]']); + }, 100); + }, + actionAfter: async () => { + // Scroll for the NEXT step before it shows + await scrollNavToSection('adminAudit'); + }, + }, + [AdminTourStep.ADMIN_TOOLS]: { + selector: '[data-tour="admin-adminAudit-nav"]', + highlightedSelectors: ['[data-tour="admin-adminAudit-nav"]', '[data-tour="admin-adminUsage-nav"]', '[data-tour="settings-content-area"]'], + content: t('adminOnboarding.adminTools', "Finally, we have advanced administration tools like Auditing to track system activity and Usage Analytics to monitor how your users interact with the platform."), + position: 'right', + padding: 10, + action: () => { + // Just navigate, scroll already happened in previous step + removeAllGlows(); + navigateToSection('adminAudit'); + setTimeout(() => { + addGlowToElements(['[data-tour="admin-adminAudit-nav"]', '[data-tour="admin-adminUsage-nav"]', '[data-tour="settings-content-area"]']); + }, 100); + }, + }, + [AdminTourStep.WRAP_UP]: { + selector: '[data-tour="help-button"]', + content: t('adminOnboarding.wrapUp', "That's the admin tour! You've seen the enterprise features that make Stirling PDF a powerful, customisable solution for organisations. Access this tour anytime from the Help menu."), + position: 'right', + padding: 10, + action: () => { + removeAllGlows(); + }, + }, + }; + + // Select steps based on tour type + const steps = tourType === 'admin' + ? Object.values(adminStepsConfig) + : Object.values(stepsConfig); const advanceTour = ({ setCurrentStep, currentStep, steps, setIsOpen }: { setCurrentStep: (value: number | ((prev: number) => number)) => void; @@ -213,7 +378,11 @@ export default function OnboardingTour() { }) => { if (steps && currentStep === steps.length - 1) { setIsOpen(false); - restoreWorkbenchState(); + if (tourType === 'admin') { + restoreAdminState(); + } else { + restoreWorkbenchState(); + } completeTour(); } else if (steps) { setCurrentStep((s) => (s === steps.length - 1 ? 0 : s + 1)); @@ -222,7 +391,11 @@ export default function OnboardingTour() { const handleCloseTour = ({ setIsOpen }: { setIsOpen: (value: boolean) => void }) => { setIsOpen(false); - restoreWorkbenchState(); + if (tourType === 'admin') { + restoreAdminState(); + } else { + restoreWorkbenchState(); + } completeTour(); }; @@ -243,7 +416,9 @@ export default function OnboardingTour() { }} /> { diff --git a/frontend/src/core/components/shared/AppConfigModal.tsx b/frontend/src/core/components/shared/AppConfigModal.tsx index a080ca3c7..97f1ee295 100644 --- a/frontend/src/core/components/shared/AppConfigModal.tsx +++ b/frontend/src/core/components/shared/AppConfigModal.tsx @@ -154,6 +154,7 @@ const AppConfigModal: React.FC = ({ opened, onClose }) => { opacity: isDisabled ? 0.5 : 1, cursor: isDisabled ? 'not-allowed' : 'pointer', }} + data-tour={`admin-${item.key}-nav`} > {!isMobile && ( @@ -185,7 +186,7 @@ const AppConfigModal: React.FC = ({ opened, onClose }) => { {/* Right content */} -
+
{/* Sticky header with section title and small close button */}
((_, ref) => { const { t } = useTranslation(); @@ -179,7 +180,7 @@ const QuickAccessBar = forwardRef((_, ref) => { size: 'lg', type: 'action', onClick: () => { - startTour(); + // This will be overridden by the wrapper logic }, }, { @@ -258,11 +259,70 @@ const QuickAccessBar = forwardRef((_, ref) => { {/* Bottom section */} - {bottomButtons.map((config, index) => ( - - {renderNavButton(config, index)} - - ))} + {bottomButtons.map((buttonConfig, index) => { + // Handle help button with menu or direct action + if (buttonConfig.id === 'help') { + const isAdmin = config?.isAdmin === true; + + // If not admin, just show button that starts tools tour directly + if (!isAdmin) { + return ( +
startTour('tools')} + > + {renderNavButton(buttonConfig, index)} +
+ ); + } + + // If admin, show menu with both options + return ( +
+ + +
{renderNavButton(buttonConfig, index)}
+
+ + } + onClick={() => startTour('tools')} + > +
+
+ {t("quickAccess.helpMenu.toolsTour", "Tools Tour")} +
+
+ {t("quickAccess.helpMenu.toolsTourDesc", "Learn what the tools can do")} +
+
+
+ } + onClick={() => startTour('admin')} + > +
+
+ {t("quickAccess.helpMenu.adminTour", "Admin Tour")} +
+
+ {t("quickAccess.helpMenu.adminTourDesc", "Explore admin settings & features")} +
+
+
+
+
+
+ ); + } + + return ( + + {renderNavButton(buttonConfig, index)} + + ); + })}
diff --git a/frontend/src/core/contexts/AdminTourOrchestrationContext.tsx b/frontend/src/core/contexts/AdminTourOrchestrationContext.tsx new file mode 100644 index 000000000..3bac8f101 --- /dev/null +++ b/frontend/src/core/contexts/AdminTourOrchestrationContext.tsx @@ -0,0 +1,124 @@ +import React, { createContext, useContext, useCallback, useRef } from 'react'; +import { useNavigate, useLocation } from 'react-router-dom'; + +interface AdminTourOrchestrationContextType { + // State management + saveAdminState: () => void; + restoreAdminState: () => void; + + // Modal & navigation + openConfigModal: () => void; + closeConfigModal: () => void; + navigateToSection: (section: string) => void; + scrollNavToSection: (section: string) => void; + + // Section-specific actions + scrollToSetting: (settingId: string) => void; +} + +const AdminTourOrchestrationContext = createContext(undefined); + +export const AdminTourOrchestrationProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => { + const navigate = useNavigate(); + const location = useLocation(); + + // Store the user's location before tour starts + const savedLocationRef = useRef(''); + + const saveAdminState = useCallback(() => { + savedLocationRef.current = location.pathname; + console.log('Saving admin state, location:', location.pathname); + }, [location.pathname]); + + const restoreAdminState = useCallback(() => { + console.log('Restoring admin state, saved location:', savedLocationRef.current); + + // Navigate back to saved location or home + const targetPath = savedLocationRef.current || '/'; + navigate(targetPath, { replace: true }); + + savedLocationRef.current = ''; + }, [navigate]); + + const openConfigModal = useCallback(() => { + // Navigate to settings overview to open the modal + navigate('/settings/overview'); + }, [navigate]); + + const closeConfigModal = useCallback(() => { + // Navigate back to home to close the modal + navigate('/', { replace: true }); + }, [navigate]); + + const navigateToSection = useCallback((section: string) => { + navigate(`/settings/${section}`); + }, [navigate]); + + const scrollNavToSection = useCallback((section: string): Promise => { + return new Promise((resolve) => { + const navElement = document.querySelector(`[data-tour="admin-${section}-nav"]`) as HTMLElement; + const scrollContainer = document.querySelector('.modal-nav-scroll') as HTMLElement; + + if (navElement && scrollContainer) { + // Get the position of the nav element relative to the scroll container + const navTop = navElement.offsetTop; + const containerHeight = scrollContainer.clientHeight; + const navHeight = navElement.offsetHeight; + + // Calculate scroll position to center the element + const scrollTo = navTop - (containerHeight / 2) + (navHeight / 2); + + // Instant scroll to avoid timing issues + scrollContainer.scrollTo({ + top: Math.max(0, scrollTo), + behavior: 'auto' + }); + + // Use multiple animation frames to ensure browser has fully updated + requestAnimationFrame(() => { + requestAnimationFrame(() => { + requestAnimationFrame(() => { + resolve(); + }); + }); + }); + } else { + resolve(); + } + }); + }, []); + + const scrollToSetting = useCallback((settingId: string) => { + // Wait for the DOM to update, then scroll to the setting + setTimeout(() => { + const element = document.querySelector(`[data-tour="${settingId}"]`); + if (element) { + element.scrollIntoView({ behavior: 'smooth', block: 'center' }); + } + }, 300); + }, []); + + const value: AdminTourOrchestrationContextType = { + saveAdminState, + restoreAdminState, + openConfigModal, + closeConfigModal, + navigateToSection, + scrollNavToSection, + scrollToSetting, + }; + + return ( + + {children} + + ); +}; + +export const useAdminTourOrchestration = (): AdminTourOrchestrationContextType => { + const context = useContext(AdminTourOrchestrationContext); + if (!context) { + throw new Error('useAdminTourOrchestration must be used within AdminTourOrchestrationProvider'); + } + return context; +}; diff --git a/frontend/src/core/contexts/OnboardingContext.tsx b/frontend/src/core/contexts/OnboardingContext.tsx index 21bdadc4d..817f3dd5a 100644 --- a/frontend/src/core/contexts/OnboardingContext.tsx +++ b/frontend/src/core/contexts/OnboardingContext.tsx @@ -2,14 +2,17 @@ import React, { createContext, useContext, useState, useEffect, useCallback } fr import { usePreferences } from '@app/contexts/PreferencesContext'; import { useShouldShowWelcomeModal } from '@app/hooks/useShouldShowWelcomeModal'; +export type TourType = 'tools' | 'admin'; + interface OnboardingContextValue { isOpen: boolean; currentStep: number; + tourType: TourType; setCurrentStep: (step: number) => void; - startTour: () => void; + startTour: (type?: TourType) => void; closeTour: () => void; completeTour: () => void; - resetTour: () => void; + resetTour: (type?: TourType) => void; showWelcomeModal: boolean; setShowWelcomeModal: (show: boolean) => void; } @@ -20,6 +23,7 @@ export const OnboardingProvider: React.FC<{ children: React.ReactNode }> = ({ ch const { updatePreference } = usePreferences(); const [isOpen, setIsOpen] = useState(false); const [currentStep, setCurrentStep] = useState(0); + const [tourType, setTourType] = useState('tools'); const [showWelcomeModal, setShowWelcomeModal] = useState(false); const shouldShow = useShouldShowWelcomeModal(); @@ -30,7 +34,8 @@ export const OnboardingProvider: React.FC<{ children: React.ReactNode }> = ({ ch } }, [shouldShow]); - const startTour = useCallback(() => { + const startTour = useCallback((type: TourType = 'tools') => { + setTourType(type); setCurrentStep(0); setIsOpen(true); }, []); @@ -44,8 +49,9 @@ export const OnboardingProvider: React.FC<{ children: React.ReactNode }> = ({ ch updatePreference('hasCompletedOnboarding', true); }, [updatePreference]); - const resetTour = useCallback(() => { + const resetTour = useCallback((type: TourType = 'tools') => { updatePreference('hasCompletedOnboarding', false); + setTourType(type); setCurrentStep(0); setIsOpen(true); }, [updatePreference]); @@ -55,6 +61,7 @@ export const OnboardingProvider: React.FC<{ children: React.ReactNode }> = ({ ch value={{ isOpen, currentStep, + tourType, setCurrentStep, startTour, closeTour,