diff --git a/src/components/tutorial/GuidedTour.jsx b/src/components/tutorial/GuidedTour.jsx index 119288b9..db70ec12 100644 --- a/src/components/tutorial/GuidedTour.jsx +++ b/src/components/tutorial/GuidedTour.jsx @@ -61,9 +61,9 @@ export default function GuidedTour({ tourId, onClose }) { }, [stepIndex, tourId]); const handleSkip = useCallback(() => { - tutorialSystem.saveStep(tourId, 0); + tutorialSystem.skip(tourId, stepIndex); onClose?.(); - }, [tourId, onClose]); + }, [tourId, stepIndex, onClose]); if (!tour || !step) return null; diff --git a/src/components/tutorial/TourLauncher.jsx b/src/components/tutorial/TourLauncher.jsx index 068e72c6..ca286a16 100644 --- a/src/components/tutorial/TourLauncher.jsx +++ b/src/components/tutorial/TourLauncher.jsx @@ -1,5 +1,18 @@ import React, { useState, useEffect } from 'react'; -import { BookOpen, Play, RotateCcw, ChevronRight, Trophy, Clock, Zap, Award } from 'lucide-react'; +import { + BarChart3, + BookOpen, + CheckCircle, + Clock, + ExternalLink, + HelpCircle, + Play, + RotateCcw, + Search, + Trophy, + X, + Zap, +} from 'lucide-react'; import GuidedTour from './GuidedTour'; import tutorialSystem from '../../lib/tutorialSystem'; @@ -12,6 +25,9 @@ export default function TourLauncher() { const [open, setOpen] = useState(false); const [selectedCategory, setSelectedCategory] = useState('all'); const [showAchievements, setShowAchievements] = useState(false); + const [showHelpCenter, setShowHelpCenter] = useState(false); + const [showAnalytics, setShowAnalytics] = useState(false); + const [helpQuery, setHelpQuery] = useState(''); const [, forceUpdate] = useState(0); // Auto-start welcome tour for first-time visitors @@ -27,6 +43,17 @@ export default function TourLauncher() { const overallProgress = tutorialSystem.getOverallProgress(); const achievements = tutorialSystem.getAchievements(); const recommended = tutorialSystem.getRecommendedTour(); + const onboarding = tutorialSystem.getOnboardingStatus(); + const analytics = tutorialSystem.getAnalyticsSummary(); + const helpResults = helpQuery + ? tutorialSystem.searchHelp(helpQuery) + : tutorialSystem.getAllHelp().map((entry) => ({ + id: entry.title, + type: 'help', + title: entry.title, + description: entry.content, + learnMore: entry.learnMore, + })); const filteredTours = selectedCategory === 'all' ? tours @@ -39,8 +66,19 @@ export default function TourLauncher() { } function handleStartTour(tourId) { + tutorialSystem.dismissWelcome(); setActiveTour(tourId); setOpen(false); + setShowHelpCenter(false); + setShowAnalytics(false); + } + + function handleHelpQueryChange(query) { + setHelpQuery(query); + if (query.trim()) { + const resultCount = tutorialSystem.searchHelp(query).length; + tutorialSystem.recordHelpSearch(query, resultCount); + } } const difficultyColors = { @@ -63,7 +101,6 @@ export default function TourLauncher() { cursor: 'pointer', display: 'flex', alignItems: 'center', justifyContent: 'center', boxShadow: '0 4px 20px rgba(99,102,241,0.5)', color: '#fff', - position: 'relative', }} >

- Interactive Tutorials + Advanced Onboarding

- +
+ setShowHelpCenter(true)} icon={HelpCircle} /> + setShowAnalytics(true)} icon={BarChart3} /> + setShowAchievements(!showAchievements)} icon={Trophy} /> +
{/* Progress bar */} @@ -154,6 +184,78 @@ export default function TourLauncher() { + {/* Progressive onboarding checklist */} +
+
+
+
+ Onboarding checklist +
+
+ {onboarding.completed}/{onboarding.total} milestones complete +
+
+ {onboarding.next && ( + + )} +
+
+ {onboarding.milestones.map((milestone) => ( + + ))} +
+
+ {/* Achievements section */} {showAchievements && (
setActiveTour(null)} + onClose={() => { + setActiveTour(null); + forceUpdate(n => n + 1); + }} + /> + )} + + {showHelpCenter && ( + setShowHelpCenter(false)} + onStartTour={handleStartTour} + /> + )} + + {showAnalytics && ( + setShowAnalytics(false)} /> )} ); } + +function IconButton({ label, icon: Icon, onClick }) { + return ( + + ); +} + +function HelpCenterModal({ query, results, onQueryChange, onClose, onStartTour }) { + return ( +
+
+ +
+
+ + onQueryChange(event.target.value)} + placeholder="Search public keys, fees, contracts, alerts..." + style={{ + width: '100%', + padding: '10px 12px 10px 38px', + background: 'var(--bg-secondary, #0f172a)', + border: '1px solid var(--border, #334155)', + borderRadius: '8px', + color: 'var(--text-primary, #f1f5f9)', + fontSize: '13px', + }} + /> +
+
+
+
+ {results.map((result) => ( +
+
+
+
+ {result.type === 'tour' ? 'Guided tour' : 'Help article'} +
+
+ {result.title} +
+
+ {result.type === 'tour' && ( + + )} +
+

+ {result.description} +

+ {result.learnMore && ( + + Learn more + + )} +
+ ))} + {results.length === 0 && ( +
+ No help topics matched your search. +
+ )} +
+
+
+
+ ); +} + +function AnalyticsModal({ analytics, onClose }) { + const dropOffEntries = Object.entries(analytics.dropOffPoints); + + return ( +
+
+ +
+
+ + + + +
+
+
Engagement
+

+ Average tutorial session: {formatDuration(analytics.averageEngagementMs)}. Skipped tours: {analytics.skips}. +

+
+
+
Drop-off points
+ {dropOffEntries.length === 0 ? ( +

No drop-off points recorded yet.

+ ) : ( +
+ {dropOffEntries.map(([key, count]) => { + const [tourId, stepIndex] = key.split(':'); + return ( +
+ {tourId} step {Number(stepIndex) + 1} + {count} +
+ ); + })} +
+ )} +
+
+
+
+ ); +} + +function ModalHeader({ title, subtitle, onClose }) { + return ( +
+
+

{title}

+

{subtitle}

+
+ +
+ ); +} + +function MetricCard({ label, value }) { + return ( +
+
{label}
+
{value}
+
+ ); +} + +function formatDuration(ms) { + const totalSeconds = Math.max(0, Math.round(ms / 1000)); + const minutes = Math.floor(totalSeconds / 60); + const seconds = totalSeconds % 60; + if (minutes === 0) return `${seconds}s`; + return `${minutes}m ${seconds}s`; +} + +const modalBackdropStyle = { + position: 'fixed', + inset: 0, + zIndex: 10020, + background: 'rgba(0,0,0,0.72)', + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + padding: '20px', +}; + +const modalStyle = { + background: 'var(--bg-card, #1e293b)', + border: '1px solid var(--border, #334155)', + borderRadius: '12px', + width: '100%', + maxHeight: '88vh', + display: 'flex', + flexDirection: 'column', + overflow: 'hidden', + boxShadow: '0 24px 80px rgba(0,0,0,0.45)', +}; + +const panelStyle = { + background: 'var(--bg-secondary, #0f172a)', + border: '1px solid var(--border, #334155)', + borderRadius: '10px', + padding: '14px', +}; + +const panelTitleStyle = { + color: 'var(--text-primary, #f1f5f9)', + fontSize: '13px', + fontWeight: 800, +}; + +const panelCopyStyle = { + margin: '6px 0 0', + color: 'var(--text-secondary, #cbd5e1)', + fontSize: '12px', + lineHeight: 1.5, +}; + +const smallPrimaryButtonStyle = { + background: 'var(--accent, #6366f1)', + border: 'none', + borderRadius: '6px', + color: '#fff', + cursor: 'pointer', + display: 'flex', + alignItems: 'center', + gap: '4px', + fontSize: '12px', + fontWeight: 700, + padding: '6px 10px', + height: '30px', +}; diff --git a/src/lib/tutorialSystem.js b/src/lib/tutorialSystem.js index fda799d2..4580e0fe 100644 --- a/src/lib/tutorialSystem.js +++ b/src/lib/tutorialSystem.js @@ -398,6 +398,8 @@ export const ACHIEVEMENTS = { const STORAGE_KEY = 'tutorial_state'; const PROGRESS_KEY = 'tutorial_progress'; const ACHIEVEMENTS_KEY = 'tutorial_achievements'; +const ANALYTICS_KEY = 'tutorial_analytics'; +const ONBOARDING_KEY = 'tutorial_onboarding'; function loadState() { try { @@ -441,6 +443,61 @@ function saveAchievements(achievements) { } catch { /* ignore */ } } +function loadAnalytics() { + try { + return JSON.parse(localStorage.getItem(ANALYTICS_KEY) || '{}'); + } catch { + return {}; + } +} + +function saveAnalytics(analytics) { + try { + localStorage.setItem(ANALYTICS_KEY, JSON.stringify(analytics)); + } catch { /* ignore */ } +} + +function loadOnboarding() { + try { + return JSON.parse(localStorage.getItem(ONBOARDING_KEY) || '{}'); + } catch { + return {}; + } +} + +function saveOnboarding(onboarding) { + try { + localStorage.setItem(ONBOARDING_KEY, JSON.stringify(onboarding)); + } catch { /* ignore */ } +} + +const ONBOARDING_MILESTONES = [ + { + id: 'welcome-tour', + title: 'Complete the welcome tour', + description: 'Learn the dashboard layout and core account workflow.', + tourId: 'welcome', + }, + { + id: 'wallet-ready', + title: 'Review wallet options', + description: 'Compare Freighter and hardware wallet connection paths.', + tourId: 'wallet', + }, + { + id: 'transaction-basics', + title: 'Practice transaction building', + description: 'Walk through building, simulating, and signing safely.', + tourId: 'transactions', + }, + { + id: 'monitoring-ready', + title: 'Set up monitoring basics', + description: 'Explore alerts and notifications for account activity.', + tourId: 'alerts', + }, +]; + // ─── Public API ─────────────────────────────────────────────────────────────── export const tutorialSystem = { @@ -457,6 +514,7 @@ export const tutorialSystem = { // Update progress this.updateProgress(tourId, 100); + this.trackEvent('tour_completed', { tourId }); // Check for achievements this.checkAchievements(); @@ -466,6 +524,7 @@ export const tutorialSystem = { reset(tourId) { const state = loadState(); delete state[`completed_${tourId}`]; + delete state[`skipped_${tourId}`]; saveState(state); // Reset progress @@ -478,6 +537,8 @@ export const tutorialSystem = { resetAll() { saveState({}); saveProgress({}); + saveOnboarding({}); + saveAnalytics({}); }, /** Get saved step index for a tour */ @@ -490,6 +551,7 @@ export const tutorialSystem = { const state = loadState(); state[`step_${tourId}`] = stepIndex; saveState(state); + this.trackEvent('tour_step_viewed', { tourId, stepIndex }); // Update progress percentage const tour = this.getTour(tourId); @@ -556,10 +618,96 @@ export const tutorialSystem = { return Object.values(HELP_ENTRIES); }, + /** Search help entries and tour content */ + searchHelp(query) { + const normalized = String(query || '').trim().toLowerCase(); + if (!normalized) return []; + + const helpResults = Object.entries(HELP_ENTRIES) + .filter(([, entry]) => { + return `${entry.title} ${entry.content}`.toLowerCase().includes(normalized); + }) + .map(([id, entry]) => ({ + id, + type: 'help', + title: entry.title, + description: entry.content, + learnMore: entry.learnMore, + })); + + const tourResults = this.getTours() + .filter((tour) => { + const searchable = [ + tour.title, + tour.description, + tour.category, + ...tour.steps.flatMap((step) => [step.title, step.content, step.action, step.interactiveHint]), + ].join(' '); + return searchable.toLowerCase().includes(normalized); + }) + .map((tour) => ({ + id: tour.id, + type: 'tour', + title: tour.title, + description: tour.description, + tourId: tour.id, + })); + + return [...helpResults, ...tourResults]; + }, + + /** Record a user-submitted help search */ + recordHelpSearch(query, resultCount = 0) { + const normalized = String(query || '').trim().toLowerCase(); + if (normalized) { + this.trackEvent('help_searched', { query: normalized, resultCount }); + } + }, + /** Check if this is a first-time user (no tours completed) */ isFirstVisit() { const state = loadState(); - return !Object.keys(state).some(k => k.startsWith('completed_')); + const onboarding = loadOnboarding(); + return !onboarding.dismissedWelcome && !Object.keys(state).some(k => k.startsWith('completed_') || k.startsWith('skipped_')); + }, + + /** Mark a tour as skipped so onboarding can report drop-off points */ + skip(tourId, stepIndex = 0) { + const state = loadState(); + state[`skipped_${tourId}`] = { stepIndex, at: Date.now() }; + state[`step_${tourId}`] = 0; + saveState(state); + this.trackEvent('tour_skipped', { tourId, stepIndex }); + }, + + /** Dismiss the first-run welcome prompt without completing a tour */ + dismissWelcome() { + const onboarding = loadOnboarding(); + onboarding.dismissedWelcome = Date.now(); + saveOnboarding(onboarding); + this.trackEvent('onboarding_dismissed'); + }, + + /** Get progressive onboarding milestones */ + getOnboardingMilestones() { + return ONBOARDING_MILESTONES.map((milestone) => ({ + ...milestone, + completed: this.isCompleted(milestone.tourId), + progress: this.getProgress(milestone.tourId), + })); + }, + + /** Completion status for the advanced onboarding flow */ + getOnboardingStatus() { + const milestones = this.getOnboardingMilestones(); + const completed = milestones.filter((milestone) => milestone.completed).length; + return { + milestones, + completed, + total: milestones.length, + percentage: milestones.length ? Math.round((completed / milestones.length) * 100) : 0, + next: milestones.find((milestone) => !milestone.completed) || null, + }; }, /** Check and award achievements */ @@ -624,6 +772,7 @@ export const tutorialSystem = { const state = loadState(); state[`timer_${tourId}`] = Date.now(); saveState(state); + this.trackEvent('tour_started', { tourId }); }, stopTimer(tourId) { @@ -634,12 +783,55 @@ export const tutorialSystem = { state[`duration_${tourId}`] = (state[`duration_${tourId}`] || 0) + duration; delete state[`timer_${tourId}`]; saveState(state); + this.trackEvent('tour_engaged', { tourId, duration }); } }, getDuration(tourId) { return loadState()[`duration_${tourId}`] || 0; }, + + /** Capture local product analytics for onboarding and tutorial engagement */ + trackEvent(eventName, metadata = {}) { + const analytics = loadAnalytics(); + const events = analytics.events || []; + events.push({ + eventName, + metadata, + timestamp: Date.now(), + }); + + analytics.events = events.slice(-250); + analytics.updatedAt = Date.now(); + saveAnalytics(analytics); + }, + + /** Summarize onboarding completion, drop-off points, and engagement */ + getAnalyticsSummary() { + const analytics = loadAnalytics(); + const events = analytics.events || []; + const starts = events.filter((event) => event.eventName === 'tour_started'); + const completions = events.filter((event) => event.eventName === 'tour_completed'); + const skips = events.filter((event) => event.eventName === 'tour_skipped'); + const engagements = events.filter((event) => event.eventName === 'tour_engaged'); + const totalDuration = engagements.reduce((sum, event) => sum + (event.metadata?.duration || 0), 0); + const dropOffPoints = skips.reduce((acc, event) => { + const key = `${event.metadata?.tourId || 'unknown'}:${event.metadata?.stepIndex || 0}`; + acc[key] = (acc[key] || 0) + 1; + return acc; + }, {}); + + return { + onboarding: this.getOnboardingStatus(), + starts: starts.length, + completions: completions.length, + skips: skips.length, + completionRate: starts.length ? Math.round((completions.length / starts.length) * 100) : 0, + averageEngagementMs: engagements.length ? Math.round(totalDuration / engagements.length) : 0, + dropOffPoints, + helpSearches: events.filter((event) => event.eventName === 'help_searched').length, + }; + }, }; export default tutorialSystem;