mirror of
https://github.com/Frooodle/Stirling-PDF.git
synced 2026-03-04 02:20:19 +01:00
admin onboarding (#4863)
# Description of Changes <!-- Please provide a summary of the changes, including: - What was changed - Why the change was made - Any challenges encountered Closes #(issue_number) --> --- ## 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>
This commit is contained in:
124
frontend/src/core/contexts/AdminTourOrchestrationContext.tsx
Normal file
124
frontend/src/core/contexts/AdminTourOrchestrationContext.tsx
Normal file
@@ -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<AdminTourOrchestrationContextType | undefined>(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<string>('');
|
||||
|
||||
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<void> => {
|
||||
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 (
|
||||
<AdminTourOrchestrationContext.Provider value={value}>
|
||||
{children}
|
||||
</AdminTourOrchestrationContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export const useAdminTourOrchestration = (): AdminTourOrchestrationContextType => {
|
||||
const context = useContext(AdminTourOrchestrationContext);
|
||||
if (!context) {
|
||||
throw new Error('useAdminTourOrchestration must be used within AdminTourOrchestrationProvider');
|
||||
}
|
||||
return context;
|
||||
};
|
||||
@@ -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<TourType>('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,
|
||||
|
||||
Reference in New Issue
Block a user