feat: Add guided tour functionality using driver.js#13
Conversation
- Integrated driver.js for a guided tour experience in the application. - Added a new hook `useTour` to manage tour steps and interactions. - Implemented tour steps for key UI elements including tabs, device list, and settings. - Updated translations for tour-related texts in multiple languages. - Added a button to start the tour in the app header. - Styled tour popovers with a custom CSS theme. - Included IDs for various elements to facilitate tour navigation. - Added new dependencies in package.json for driver.js.
There was a problem hiding this comment.
Pull request overview
This pull request adds a guided tour functionality to the AirShare application using the driver.js library. The tour walks users through key features including file transfer, device management, history viewing, and settings configuration.
Changes:
- Added driver.js library (v1.4.0) as a new dependency
- Created a custom React hook (useTour) to manage tour steps and lifecycle
- Added tour-related translations across 6 languages (English, Chinese, Italian, French, Spanish, German)
- Modified multiple components to add unique IDs for tour navigation
Reviewed changes
Copilot reviewed 16 out of 17 changed files in this pull request and generated 12 comments.
Show a summary per file
| File | Description |
|---|---|
| package.json / package-lock.json | Added driver.js v1.4.0 dependency and updated various esbuild packages |
| src/hooks/useTour.ts | New hook implementing tour logic with driver.js integration |
| src/driver.css | Custom CSS theme for tour popovers matching app design |
| src/App.tsx | Added tour initialization, help button in header, and IDs to tab elements |
| src/i18n/*.json | Added tour-related translation strings in 6 languages |
| src/components/DeviceDetection.tsx | Added ID for tour navigation |
| src/components/DeviceList.tsx | Added ID for tour navigation |
| src/components/TransferHistory.tsx | Added ID for tour navigation |
| src/components/Settings.tsx | Added IDs for appearance and language settings |
| src/components/FileTransfer.tsx | Added ID to file drop zone |
| src/components/DynamicSidebar.tsx | Added aria-labels to carousel navigation buttons |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| id="start-tour-btn" | ||
| onClick={startTour} | ||
| className="p-2 rounded-full hover:bg-secondary/80 transition-colors" | ||
| title={t('tour.start', 'Start Tour')} |
There was a problem hiding this comment.
The tour button is missing an aria-label attribute. While it has a title attribute, an aria-label would improve accessibility for screen reader users. Add aria-label to make the button's purpose clear to assistive technologies.
| title={t('tour.start', 'Start Tour')} | |
| title={t('tour.start', 'Start Tour')} | |
| aria-label={t('tour.start', 'Start Tour')} |
| setTimeout(() => { | ||
| driverObj.current?.moveNext(); | ||
| }, 400); | ||
| } | ||
| } | ||
| }, | ||
| { | ||
| element: '#file-drop-zone', | ||
| popover: { | ||
| title: t('tour.dropzone.title', 'Send Files'), | ||
| description: t('tour.dropzone.description', 'Drag and drop files here to start sharing.'), | ||
| side: "right", | ||
| align: 'start' | ||
| } | ||
| }, | ||
| { | ||
| element: '#tab-devices', | ||
| popover: { | ||
| title: t('tour.devices.title', 'Devices'), | ||
| description: t('tour.devices.description', 'See online devices and manage trusted connections.'), | ||
| side: "bottom", | ||
| align: 'start', | ||
| onNextClick: () => { | ||
| setActiveTab('devices'); | ||
| setTimeout(() => { | ||
| driverObj.current?.moveNext(); | ||
| }, 400); | ||
| } | ||
| } | ||
| }, | ||
| { | ||
| element: '#device-list-container', | ||
| popover: { | ||
| title: t('tour.devicelist.title', 'Device List'), | ||
| description: t('tour.devicelist.description', 'All detected devices on your network appear here. Click to send files to them.'), | ||
| side: "top", | ||
| align: 'center' | ||
| } | ||
| }, | ||
| { | ||
| element: '#tab-history', | ||
| popover: { | ||
| title: t('tour.history.title', 'History'), | ||
| description: t('tour.history.description', 'View your recent transfers and statistics.'), | ||
| side: "bottom", | ||
| align: 'start', | ||
| onNextClick: () => { | ||
| setActiveTab('history'); | ||
| setTimeout(() => { | ||
| driverObj.current?.moveNext(); | ||
| }, 400); | ||
| } | ||
| } | ||
| }, | ||
| { | ||
| element: '#history-list', | ||
| popover: { | ||
| title: t('tour.historylist.title', 'Transfer Log'), | ||
| description: t('tour.historylist.description', 'Review past transfers, statuses, and manage records.'), | ||
| side: "top", | ||
| align: 'center' | ||
| } | ||
| }, | ||
| { | ||
| element: '#tab-settings', | ||
| popover: { | ||
| title: t('tour.settings.title', 'Settings'), | ||
| description: t('tour.settings.description', 'Configure language, theme, and other preferences.'), | ||
| side: "bottom", | ||
| align: 'start', | ||
| onNextClick: () => { | ||
| setActiveTab('settings'); | ||
| setTimeout(() => { | ||
| driverObj.current?.moveNext(); | ||
| }, 400); | ||
| } | ||
| } | ||
| }, | ||
| { | ||
| element: '#appearance-settings', | ||
| popover: { | ||
| title: t('tour.appearance.title', 'Appearance'), | ||
| description: t('tour.appearance.description', 'Customize the look and feel, including Dark Mode.'), | ||
| side: "left", | ||
| align: 'start' | ||
| } | ||
| }, | ||
| { | ||
| element: '#start-tour-btn', | ||
| popover: { | ||
| title: t('tour.help.title', 'Need Help?'), | ||
| description: t('tour.help.description', 'Click here to restart this tour anytime.'), | ||
| side: "bottom", | ||
| align: 'end' | ||
| } | ||
| } | ||
| ]; | ||
|
|
||
| driverObj.current = driver({ | ||
| showProgress: true, | ||
| animate: true, | ||
| steps: steps, | ||
| nextBtnText: t('common.next', 'Next'), | ||
| prevBtnText: t('common.prev', 'Previous'), | ||
| doneBtnText: t('common.done', 'Done'), | ||
| popoverClass: 'driverjs-theme', | ||
| onDestroyed: () => { | ||
| localStorage.setItem('tour_completed', 'true'); | ||
| setActiveTab('transfer'); | ||
| }, | ||
| }); | ||
| }, [t, setActiveTab]); | ||
|
|
||
| const startTour = () => { | ||
| const el = document.getElementById('tab-transfer'); | ||
| if(el) el.click(); | ||
|
|
||
| setTimeout(() => { | ||
| if (driverObj.current) { | ||
| driverObj.current.drive(); | ||
| } | ||
| }, 100); | ||
| }; | ||
|
|
||
| const checkAndStartTour = () => { | ||
| const tourCompleted = localStorage.getItem('tour_completed'); | ||
| if (!tourCompleted) { | ||
| // Small delay to ensure UI is ready | ||
| setTimeout(() => { | ||
| startTour(); | ||
| }, 1500); |
There was a problem hiding this comment.
The setTimeout delays (400ms, 100ms, and 1500ms) are magic numbers without explanation. Consider extracting these as named constants with descriptive names (e.g., TAB_TRANSITION_DELAY, TOUR_START_DELAY, INITIAL_TOUR_DELAY) to improve code readability and maintainability.
| "privacy.questions": "¿Preguntas? Contáctanos en", | ||
| "terms.questions": "¿Preguntas? Contáctanos en" | ||
| "terms.questions": "¿Preguntas? Contáctanos en", | ||
| "common.next": "Siguiente", | ||
| "common.prev": "Anterior", | ||
| "common.done": "Hecho", | ||
| "tour.welcome.title": "Bienvenido a AirShare", | ||
| "tour.welcome.description": "Tu solución segura y local de intercambio de archivos multiplataforma.", | ||
| "tour.dropzone.title": "Enviar archivos", | ||
| "tour.dropzone.description": "Arrastra y suelta archivos aquí para comenzar a compartir.", | ||
| "tour.transfer.title": "Transferir", | ||
| "tour.transfer.description": "Envía archivos a otros dispositivos rápidamente.", | ||
| "tour.devices.title": "Dispositivos", | ||
| "tour.devices.description": "Ver dispositivos en línea y administrar conexiones de confianza.", | ||
| "tour.devicelist.title": "Lista de dispositivos", | ||
| "tour.devicelist.description": "Todos los dispositivos detectados en su red aparecen aquí.", | ||
| "tour.history.title": "Historial", | ||
| "tour.history.description": "Ver tus transferencias recientes y estadísticas.", | ||
| "tour.historylist.title": "Registro de transferencias", | ||
| "tour.historylist.description": "Revise transferencias pasadas y administre registros.", | ||
| "tour.settings.title": "Configuración", | ||
| "tour.settings.description": "Configura idioma, tema y otras preferencias.", | ||
| "tour.appearance.title": "Apariencia", | ||
| "tour.appearance.description": "Personaliza la apariencia, incluido el modo oscuro.", | ||
| "tour.help.title": "¿Necesitas ayuda?", | ||
| "tour.help.description": "Haz clic aquí para reiniciar este recorrido en cualquier momento.", | ||
| "tour.exit.confirm": "¿Estás seguro de que quieres salir del recorrido?", | ||
| "tour.start": "Iniciar recorrido" |
There was a problem hiding this comment.
Inconsistent indentation in the JSON file. Line 261 uses a tab character while line 262 has no leading whitespace at all, and lines 263-287 use 2 spaces. This should be standardized to match the existing file format (2 spaces) for consistency throughout the file.
There was a problem hiding this comment.
@copilot open a new pull request to apply changes based on this feedback
| useEffect(() => { | ||
| const steps: DriveStep[] = [ | ||
| { | ||
| element: '#app-header', | ||
| popover: { | ||
| title: t('tour.welcome.title', 'Welcome to AirShare'), | ||
| description: t('tour.welcome.description', 'Your secure, local cross-platform file sharing solution.'), | ||
| side: "bottom", | ||
| align: 'start', | ||
| showButtons: ['next', 'close'] | ||
| } | ||
| }, | ||
| { | ||
| element: '#tab-transfer', | ||
| popover: { | ||
| title: t('tour.transfer.title', 'Transfer'), | ||
| description: t('tour.transfer.description', 'Send files to other devices quickly.'), | ||
| side: "bottom", | ||
| align: 'start', | ||
| onNextClick: () => { | ||
| setActiveTab('transfer'); | ||
| setTimeout(() => { | ||
| driverObj.current?.moveNext(); | ||
| }, 400); | ||
| } | ||
| } | ||
| }, | ||
| { | ||
| element: '#file-drop-zone', | ||
| popover: { | ||
| title: t('tour.dropzone.title', 'Send Files'), | ||
| description: t('tour.dropzone.description', 'Drag and drop files here to start sharing.'), | ||
| side: "right", | ||
| align: 'start' | ||
| } | ||
| }, | ||
| { | ||
| element: '#tab-devices', | ||
| popover: { | ||
| title: t('tour.devices.title', 'Devices'), | ||
| description: t('tour.devices.description', 'See online devices and manage trusted connections.'), | ||
| side: "bottom", | ||
| align: 'start', | ||
| onNextClick: () => { | ||
| setActiveTab('devices'); | ||
| setTimeout(() => { | ||
| driverObj.current?.moveNext(); | ||
| }, 400); | ||
| } | ||
| } | ||
| }, | ||
| { | ||
| element: '#device-list-container', | ||
| popover: { | ||
| title: t('tour.devicelist.title', 'Device List'), | ||
| description: t('tour.devicelist.description', 'All detected devices on your network appear here. Click to send files to them.'), | ||
| side: "top", | ||
| align: 'center' | ||
| } | ||
| }, | ||
| { | ||
| element: '#tab-history', | ||
| popover: { | ||
| title: t('tour.history.title', 'History'), | ||
| description: t('tour.history.description', 'View your recent transfers and statistics.'), | ||
| side: "bottom", | ||
| align: 'start', | ||
| onNextClick: () => { | ||
| setActiveTab('history'); | ||
| setTimeout(() => { | ||
| driverObj.current?.moveNext(); | ||
| }, 400); | ||
| } | ||
| } | ||
| }, | ||
| { | ||
| element: '#history-list', | ||
| popover: { | ||
| title: t('tour.historylist.title', 'Transfer Log'), | ||
| description: t('tour.historylist.description', 'Review past transfers, statuses, and manage records.'), | ||
| side: "top", | ||
| align: 'center' | ||
| } | ||
| }, | ||
| { | ||
| element: '#tab-settings', | ||
| popover: { | ||
| title: t('tour.settings.title', 'Settings'), | ||
| description: t('tour.settings.description', 'Configure language, theme, and other preferences.'), | ||
| side: "bottom", | ||
| align: 'start', | ||
| onNextClick: () => { | ||
| setActiveTab('settings'); | ||
| setTimeout(() => { | ||
| driverObj.current?.moveNext(); | ||
| }, 400); | ||
| } | ||
| } | ||
| }, | ||
| { | ||
| element: '#appearance-settings', | ||
| popover: { | ||
| title: t('tour.appearance.title', 'Appearance'), | ||
| description: t('tour.appearance.description', 'Customize the look and feel, including Dark Mode.'), | ||
| side: "left", | ||
| align: 'start' | ||
| } | ||
| }, | ||
| { | ||
| element: '#start-tour-btn', | ||
| popover: { | ||
| title: t('tour.help.title', 'Need Help?'), | ||
| description: t('tour.help.description', 'Click here to restart this tour anytime.'), | ||
| side: "bottom", | ||
| align: 'end' | ||
| } | ||
| } | ||
| ]; | ||
|
|
||
| driverObj.current = driver({ | ||
| showProgress: true, | ||
| animate: true, | ||
| steps: steps, | ||
| nextBtnText: t('common.next', 'Next'), | ||
| prevBtnText: t('common.prev', 'Previous'), | ||
| doneBtnText: t('common.done', 'Done'), | ||
| popoverClass: 'driverjs-theme', | ||
| onDestroyed: () => { | ||
| localStorage.setItem('tour_completed', 'true'); | ||
| setActiveTab('transfer'); | ||
| }, | ||
| }); | ||
| }, [t, setActiveTab]); |
There was a problem hiding this comment.
The driver object is recreated on every render when translations or setActiveTab change. This is inefficient and could cause issues with tour state. Consider memoizing the driver configuration or using useCallback for the initialization to avoid recreating the driver instance unnecessarily.
| localStorage.setItem('tour_completed', 'true'); | ||
| setActiveTab('transfer'); | ||
| }, | ||
| }); |
There was a problem hiding this comment.
The driver object is not cleaned up when the component unmounts. This could lead to memory leaks if the tour is active when the component is unmounted. Add a cleanup function in the useEffect to call driverObj.current?.destroy() on unmount.
| }); | |
| }); | |
| return () => { | |
| if (driverObj.current) { | |
| driverObj.current.destroy(); | |
| driverObj.current = null; | |
| } | |
| }; |
| const el = document.getElementById('tab-transfer'); | ||
| if(el) el.click(); |
There was a problem hiding this comment.
The code directly manipulates DOM elements using getElementById and click() which is not aligned with React best practices. Consider using a ref or state management to control tab switching instead of direct DOM manipulation. This approach is fragile and could break if the element ID changes or if the component structure is modified.
| const el = document.getElementById('tab-transfer'); | |
| if(el) el.click(); | |
| // Ensure the transfer tab is active before starting the tour | |
| setActiveTab('transfer'); |
| * align: 'start' // "start", "center", "end" | ||
| * } | ||
| * } | ||
| */ |
There was a problem hiding this comment.
The 'align' property is duplicated on line 22. Remove the duplicate line as it serves no purpose and may cause confusion.
| * align: 'start' // "start", "center", "end" | |
| * } | |
| * } | |
| */ | |
| * } | |
| * } | |
| */ | |
| */ |
|
|
||
| useEffect(() => { | ||
| checkAndStartTour(); | ||
| }, []); |
There was a problem hiding this comment.
The checkAndStartTour function is called in useEffect without being included in the dependency array. This violates React's exhaustive-deps rule and could lead to stale closures. Either add checkAndStartTour to the dependency array or wrap it with useCallback to ensure it has a stable reference.
| }, []); | |
| }, [checkAndStartTour]); |
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Gecko129 <207233579+Gecko129@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Fix inconsistent indentation in es.json translation file
useTourto manage tour steps and interactions.