From e3b52dc009785c34c2d68144294bfe28a7accb99 Mon Sep 17 00:00:00 2001 From: ghaddaditw <40211818-ghaddaditw@users.noreply.replit.com> Date: Sun, 8 Jun 2025 07:17:57 +0000 Subject: [PATCH] Guide new users with an interactive tour highlighting key features Implements an onboarding tour using React, TypeScript, and CSS to guide users. Replit-Commit-Author: Agent Replit-Commit-Session-Id: c5f0c281-8dd8-4846-b452-4a07bcd21062 Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/9777c70b-fc38-4831-8d6b-78dfffe041b0/44c8f610-b797-4c56-a2e5-1122aff07267.jpg --- .../components/onboarding/OnboardingTour.tsx | 239 ++++++++++++++++ .../src/components/onboarding/TourManager.tsx | 260 ++++++++++++++++++ client/src/index.css | 24 ++ 3 files changed, 523 insertions(+) create mode 100644 client/src/components/onboarding/OnboardingTour.tsx create mode 100644 client/src/components/onboarding/TourManager.tsx diff --git a/client/src/components/onboarding/OnboardingTour.tsx b/client/src/components/onboarding/OnboardingTour.tsx new file mode 100644 index 0000000..4693542 --- /dev/null +++ b/client/src/components/onboarding/OnboardingTour.tsx @@ -0,0 +1,239 @@ +import React, { useState, useEffect } from 'react'; +import { useTranslation } from 'react-i18next'; +import { X, ChevronLeft, ChevronRight, Check } from 'lucide-react'; +import { Button } from '@/components/ui/button'; +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; +import { Badge } from '@/components/ui/badge'; + +export interface TourStep { + id: string; + target: string; + title: string; + content: string; + position?: 'top' | 'bottom' | 'left' | 'right'; + action?: 'click' | 'hover' | 'none'; + optional?: boolean; +} + +interface OnboardingTourProps { + isActive: boolean; + onComplete: () => void; + onSkip: () => void; + steps: TourStep[]; +} + +export const OnboardingTour: React.FC = ({ + isActive, + onComplete, + onSkip, + steps +}) => { + const { t } = useTranslation(); + const [currentStepIndex, setCurrentStepIndex] = useState(0); + const [highlightedElement, setHighlightedElement] = useState(null); + const [tooltipPosition, setTooltipPosition] = useState({ x: 0, y: 0 }); + + const currentStep = steps[currentStepIndex]; + const isLastStep = currentStepIndex === steps.length - 1; + const isFirstStep = currentStepIndex === 0; + + useEffect(() => { + if (!isActive || !currentStep) return; + + const targetElement = document.querySelector(currentStep.target) as HTMLElement; + if (targetElement) { + setHighlightedElement(targetElement); + + // Calculate tooltip position + const rect = targetElement.getBoundingClientRect(); + const position = currentStep.position || 'bottom'; + + let x = rect.left + rect.width / 2; + let y = rect.bottom + 10; + + switch (position) { + case 'top': + y = rect.top - 10; + break; + case 'left': + x = rect.left - 10; + y = rect.top + rect.height / 2; + break; + case 'right': + x = rect.right + 10; + y = rect.top + rect.height / 2; + break; + case 'bottom': + default: + y = rect.bottom + 10; + break; + } + + setTooltipPosition({ x, y }); + + // Scroll element into view + targetElement.scrollIntoView({ + behavior: 'smooth', + block: 'center', + inline: 'center' + }); + + // Add highlight class + targetElement.classList.add('tour-highlight'); + + // Add pulsing animation for interactive elements + if (currentStep.action && currentStep.action !== 'none') { + targetElement.classList.add('tour-pulse'); + } + } + + return () => { + if (targetElement) { + targetElement.classList.remove('tour-highlight', 'tour-pulse'); + } + }; + }, [currentStep, isActive]); + + const handleNext = () => { + if (isLastStep) { + onComplete(); + } else { + setCurrentStepIndex(prev => prev + 1); + } + }; + + const handlePrevious = () => { + if (!isFirstStep) { + setCurrentStepIndex(prev => prev - 1); + } + }; + + const handleSkip = () => { + onSkip(); + }; + + const handleStepClick = (index: number) => { + setCurrentStepIndex(index); + }; + + if (!isActive || !currentStep) return null; + + return ( + <> + {/* Overlay */} +
+ + {/* Highlight spotlight */} + {highlightedElement && ( +
+ )} + + {/* Tooltip */} + + +
+ + {t(currentStep.title)} + + +
+
+ + {currentStepIndex + 1} / {steps.length} + + {currentStep.optional && ( + + {t('onboarding.optional')} + + )} +
+
+ + +

+ {t(currentStep.content)} +

+ + {currentStep.action && currentStep.action !== 'none' && ( +
+

+ {currentStep.action === 'click' && t('onboarding.clickInstruction')} + {currentStep.action === 'hover' && t('onboarding.hoverInstruction')} +

+
+ )} + +
+ + +
+ {steps.map((_, index) => ( +
+ + +
+
+
+ + ); +}; \ No newline at end of file diff --git a/client/src/components/onboarding/TourManager.tsx b/client/src/components/onboarding/TourManager.tsx new file mode 100644 index 0000000..54091a6 --- /dev/null +++ b/client/src/components/onboarding/TourManager.tsx @@ -0,0 +1,260 @@ +import React, { createContext, useContext, useState, useEffect } from 'react'; +import { OnboardingTour, TourStep } from './OnboardingTour'; +import { useAuth } from '@/hooks/useAuth'; + +interface TourManagerContextType { + startTour: (tourId: string, steps: TourStep[]) => void; + skipTour: () => void; + completeTour: () => void; + isActive: boolean; + currentTour: string | null; +} + +const TourManagerContext = createContext(undefined); + +export const useTourManager = () => { + const context = useContext(TourManagerContext); + if (!context) { + throw new Error('useTourManager must be used within a TourManagerProvider'); + } + return context; +}; + +interface TourManagerProviderProps { + children: React.ReactNode; +} + +export const TourManagerProvider: React.FC = ({ children }) => { + const { user } = useAuth(); + const [isActive, setIsActive] = useState(false); + const [currentTour, setCurrentTour] = useState(null); + const [currentSteps, setCurrentSteps] = useState([]); + + // Check if user should see onboarding tour + useEffect(() => { + if (user && !user.hasCompletedOnboarding) { + // Auto-start main tour for new users + const mainTourSteps = getMainTourSteps(); + startTour('main', mainTourSteps); + } + }, [user]); + + const startTour = (tourId: string, steps: TourStep[]) => { + setCurrentTour(tourId); + setCurrentSteps(steps); + setIsActive(true); + }; + + const skipTour = () => { + setIsActive(false); + setCurrentTour(null); + setCurrentSteps([]); + + // Mark onboarding as completed if it's the main tour + if (currentTour === 'main' && user) { + markOnboardingComplete(); + } + }; + + const completeTour = () => { + setIsActive(false); + setCurrentTour(null); + setCurrentSteps([]); + + // Mark onboarding as completed if it's the main tour + if (currentTour === 'main' && user) { + markOnboardingComplete(); + } + }; + + const markOnboardingComplete = async () => { + try { + const response = await fetch('/api/auth/complete-onboarding', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + credentials: 'include', + }); + + if (!response.ok) { + console.error('Failed to mark onboarding as complete'); + } + } catch (error) { + console.error('Error completing onboarding:', error); + } + }; + + const value = { + startTour, + skipTour, + completeTour, + isActive, + currentTour, + }; + + return ( + + {children} + + + ); +}; + +// Define tour steps for different sections +export const getMainTourSteps = (): TourStep[] => [ + { + id: 'welcome', + target: '[data-tour="sidebar"]', + title: 'onboarding.welcome.title', + content: 'onboarding.welcome.content', + position: 'right' + }, + { + id: 'navigation', + target: '[data-tour="sidebar-nav"]', + title: 'onboarding.navigation.title', + content: 'onboarding.navigation.content', + position: 'right', + action: 'hover' + }, + { + id: 'dashboard', + target: '[data-tour="dashboard-link"]', + title: 'onboarding.dashboard.title', + content: 'onboarding.dashboard.content', + position: 'right', + action: 'click' + }, + { + id: 'tasks', + target: '[data-tour="tasks-section"]', + title: 'onboarding.tasks.title', + content: 'onboarding.tasks.content', + position: 'bottom' + }, + { + id: 'create-task', + target: '[data-tour="create-task-btn"]', + title: 'onboarding.createTask.title', + content: 'onboarding.createTask.content', + position: 'bottom', + action: 'click', + optional: true + }, + { + id: 'finances', + target: '[data-tour="finances-link"]', + title: 'onboarding.finances.title', + content: 'onboarding.finances.content', + position: 'right' + }, + { + id: 'voice-control', + target: '[data-tour="voice-button"]', + title: 'onboarding.voice.title', + content: 'onboarding.voice.content', + position: 'top', + optional: true + }, + { + id: 'ai-assistant', + target: '[data-tour="ai-link"]', + title: 'onboarding.ai.title', + content: 'onboarding.ai.content', + position: 'right' + }, + { + id: 'profile', + target: '[data-tour="profile-menu"]', + title: 'onboarding.profile.title', + content: 'onboarding.profile.content', + position: 'left' + }, + { + id: 'settings', + target: '[data-tour="settings-link"]', + title: 'onboarding.settings.title', + content: 'onboarding.settings.content', + position: 'left', + optional: true + } +]; + +export const getTasksTourSteps = (): TourStep[] => [ + { + id: 'task-list', + target: '[data-tour="task-list"]', + title: 'onboarding.taskList.title', + content: 'onboarding.taskList.content', + position: 'top' + }, + { + id: 'task-filters', + target: '[data-tour="task-filters"]', + title: 'onboarding.taskFilters.title', + content: 'onboarding.taskFilters.content', + position: 'bottom' + }, + { + id: 'task-create', + target: '[data-tour="task-create-form"]', + title: 'onboarding.taskCreate.title', + content: 'onboarding.taskCreate.content', + position: 'left' + } +]; + +export const getFinancesTourSteps = (): TourStep[] => [ + { + id: 'financial-overview', + target: '[data-tour="financial-overview"]', + title: 'onboarding.financialOverview.title', + content: 'onboarding.financialOverview.content', + position: 'top' + }, + { + id: 'add-record', + target: '[data-tour="add-record-btn"]', + title: 'onboarding.addRecord.title', + content: 'onboarding.addRecord.content', + position: 'bottom', + action: 'click' + }, + { + id: 'budget-planner', + target: '[data-tour="budget-planner"]', + title: 'onboarding.budgetPlanner.title', + content: 'onboarding.budgetPlanner.content', + position: 'left' + } +]; + +export const getVoiceTourSteps = (): TourStep[] => [ + { + id: 'voice-recorder', + target: '[data-tour="voice-recorder"]', + title: 'onboarding.voiceRecorder.title', + content: 'onboarding.voiceRecorder.content', + position: 'top' + }, + { + id: 'voice-commands', + target: '[data-tour="voice-commands"]', + title: 'onboarding.voiceCommands.title', + content: 'onboarding.voiceCommands.content', + position: 'bottom' + }, + { + id: 'voice-shortcuts', + target: '[data-tour="voice-shortcuts"]', + title: 'onboarding.voiceShortcuts.title', + content: 'onboarding.voiceShortcuts.content', + position: 'left' + } +]; \ No newline at end of file diff --git a/client/src/index.css b/client/src/index.css index 01fc283..a462e1e 100644 --- a/client/src/index.css +++ b/client/src/index.css @@ -110,6 +110,30 @@ } } + /* Onboarding Tour Styles */ + .tour-highlight { + position: relative; + z-index: 60; + box-shadow: 0 0 0 4px rgba(59, 130, 246, 0.5); + border-radius: 8px; + transition: all 0.3s ease-in-out; + } + + .tour-pulse { + animation: tour-pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite; + } + + @keyframes tour-pulse { + 0%, 100% { + transform: scale(1); + box-shadow: 0 0 0 4px rgba(59, 130, 246, 0.5); + } + 50% { + transform: scale(1.02); + box-shadow: 0 0 0 8px rgba(59, 130, 246, 0.3); + } + } + /* Custom scrollbars */ ::-webkit-scrollbar { width: 6px;