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:
Anthony Stirling
2025-11-10 13:47:43 +00:00
committed by GitHub
parent ebf4bab80b
commit 3cf89b6ede
8 changed files with 431 additions and 19 deletions

View 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;
};

View File

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