From c08105ded6ed39326db9621fec5601a8e8933d09 Mon Sep 17 00:00:00 2001 From: sirpiglr <49359077-sirpiglr@users.noreply.replit.com> Date: Mon, 15 Dec 2025 23:51:01 +0000 Subject: [PATCH] Introduce an interactive tutorial for new users to guide them through the platform Integrate a new tutorial system with step-by-step guidance, tooltips, and progress tracking. Add data attributes to the home page elements for tutorial targeting and wrap the Router in a TutorialProvider. Replit-Commit-Author: Agent Replit-Commit-Session-Id: 279f1558-c0e3-40e4-8217-be7e9f4c6eca Replit-Commit-Checkpoint-Type: intermediate_checkpoint Replit-Commit-Event-Id: fcbfb698-a413-4f05-9e02-74eaaa3905f8 Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/b984cb14-1d19-4944-922b-bc79e821ed35/279f1558-c0e3-40e4-8217-be7e9f4c6eca/eXgOAd7 Replit-Helium-Checkpoint-Created: true --- client/src/App.tsx | 7 +- client/src/components/Tutorial.tsx | 403 +++++++++++++++++++++++++++++ client/src/pages/home.tsx | 18 +- 3 files changed, 423 insertions(+), 5 deletions(-) create mode 100644 client/src/components/Tutorial.tsx diff --git a/client/src/App.tsx b/client/src/App.tsx index efd03ec..438611a 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -3,6 +3,7 @@ import { queryClient } from "./lib/queryClient"; import { QueryClientProvider } from "@tanstack/react-query"; import { Toaster } from "@/components/ui/toaster"; import { AuthProvider } from "@/lib/auth"; +import { TutorialProvider } from "@/components/Tutorial"; import NotFound from "@/pages/not-found"; import Home from "@/pages/home"; import Passport from "@/pages/passport"; @@ -49,8 +50,10 @@ function App() { return ( - - + + + + ); diff --git a/client/src/components/Tutorial.tsx b/client/src/components/Tutorial.tsx new file mode 100644 index 0000000..9dfcef6 --- /dev/null +++ b/client/src/components/Tutorial.tsx @@ -0,0 +1,403 @@ +import { useState, useEffect, createContext, useContext, ReactNode } from "react"; +import { motion, AnimatePresence } from "framer-motion"; +import { X, ChevronRight, ChevronLeft, CheckCircle, Sparkles } from "lucide-react"; + +export interface TutorialStep { + id: string; + title: string; + content: string; + target?: string; + position?: "top" | "bottom" | "left" | "right" | "center"; + action?: string; +} + +interface TutorialContextType { + isActive: boolean; + currentStep: number; + steps: TutorialStep[]; + startTutorial: (steps: TutorialStep[]) => void; + endTutorial: () => void; + nextStep: () => void; + prevStep: () => void; + skipTutorial: () => void; + hasCompletedTutorial: boolean; +} + +const TutorialContext = createContext(null); + +export function useTutorial() { + const context = useContext(TutorialContext); + if (!context) { + throw new Error("useTutorial must be used within TutorialProvider"); + } + return context; +} + +export function TutorialProvider({ children }: { children: ReactNode }) { + const [isActive, setIsActive] = useState(false); + const [currentStep, setCurrentStep] = useState(0); + const [steps, setSteps] = useState([]); + const [hasCompletedTutorial, setHasCompletedTutorial] = useState(false); + + useEffect(() => { + if (typeof window !== "undefined") { + setHasCompletedTutorial(localStorage.getItem("aethex_tutorial_completed") === "true"); + } + }, []); + + const startTutorial = (newSteps: TutorialStep[]) => { + setSteps(newSteps); + setCurrentStep(0); + setIsActive(true); + }; + + const endTutorial = () => { + setIsActive(false); + setCurrentStep(0); + setSteps([]); + setHasCompletedTutorial(true); + if (typeof window !== "undefined") { + localStorage.setItem("aethex_tutorial_completed", "true"); + } + }; + + const nextStep = () => { + if (currentStep < steps.length - 1) { + setCurrentStep(currentStep + 1); + } else { + endTutorial(); + } + }; + + const prevStep = () => { + if (currentStep > 0) { + setCurrentStep(currentStep - 1); + } + }; + + const skipTutorial = () => { + endTutorial(); + }; + + return ( + + {children} + + + ); +} + +function TutorialOverlay() { + const { isActive, currentStep, steps, nextStep, prevStep, skipTutorial } = useTutorial(); + const [targetRect, setTargetRect] = useState(null); + + const step = steps[currentStep]; + + useEffect(() => { + if (!isActive || !step?.target) { + setTargetRect(null); + return; + } + + const findTarget = () => { + const element = document.querySelector(`[data-tutorial="${step.target}"]`); + if (element) { + const rect = element.getBoundingClientRect(); + setTargetRect(rect); + element.scrollIntoView({ behavior: "smooth", block: "center" }); + } else { + setTargetRect(null); + } + }; + + findTarget(); + const interval = setInterval(findTarget, 500); + return () => clearInterval(interval); + }, [isActive, step]); + + if (!isActive || !step) return null; + + const getTooltipPosition = () => { + const viewportWidth = typeof window !== "undefined" ? window.innerWidth : 800; + const viewportHeight = typeof window !== "undefined" ? window.innerHeight : 600; + const isMobile = viewportWidth < 640; + const padding = 16; + const tooltipWidth = isMobile ? Math.min(320, viewportWidth - 32) : 360; + const tooltipHeight = 200; + + if (!targetRect || step.position === "center" || isMobile) { + return { + top: "50%", + left: "50%", + transform: "translate(-50%, -50%)", + maxWidth: `${viewportWidth - 32}px`, + }; + } + + let top = 0; + let left = 0; + + switch (step.position || "bottom") { + case "top": + top = targetRect.top - tooltipHeight - padding; + left = targetRect.left + targetRect.width / 2 - tooltipWidth / 2; + break; + case "bottom": + top = targetRect.bottom + padding; + left = targetRect.left + targetRect.width / 2 - tooltipWidth / 2; + break; + case "left": + top = targetRect.top + targetRect.height / 2 - tooltipHeight / 2; + left = targetRect.left - tooltipWidth - padding; + break; + case "right": + top = targetRect.top + targetRect.height / 2 - tooltipHeight / 2; + left = targetRect.right + padding; + break; + default: + top = targetRect.bottom + padding; + left = targetRect.left + targetRect.width / 2 - tooltipWidth / 2; + } + + left = Math.max(padding, Math.min(left, viewportWidth - tooltipWidth - padding)); + top = Math.max(padding, Math.min(top, viewportHeight - tooltipHeight - padding)); + + return { + top: `${top}px`, + left: `${left}px`, + maxWidth: `${tooltipWidth}px`, + }; + }; + + const progress = ((currentStep + 1) / steps.length) * 100; + + return ( + + + {/* Dark overlay with cutout */} + + + {/* Highlight target element */} + {targetRect && ( + + )} + + {/* Tooltip */} + + {/* Progress bar */} + + + + + {/* Close button */} + + + + + {/* Step indicator */} + + + + Step {currentStep + 1} of {steps.length} + + + + {/* Content */} + {step.title} + {step.content} + + {step.action && ( + {step.action} + )} + + {/* Navigation */} + + + Skip Tutorial + + + + {currentStep > 0 && ( + + Back + + )} + + {currentStep === steps.length - 1 ? ( + <> + Complete + > + ) : ( + <> + Next + > + )} + + + + + {/* Step dots */} + + {steps.map((_, i) => ( + + ))} + + + + + ); +} + +export const homeTutorialSteps: TutorialStep[] = [ + { + id: "welcome", + title: "Welcome to AeThex", + content: "This is the Operating System for the Metaverse. Let me show you around the platform and its key features.", + position: "center", + }, + { + id: "metrics", + title: "Live Ecosystem Metrics", + content: "These numbers update in real-time, showing the total number of architects, projects, and activity across the platform.", + target: "metrics-section", + position: "bottom", + }, + { + id: "axiom", + title: "Axiom - The Law", + content: "Click here to learn about our dual-entity model and view the investor pitch deck with real data and charts.", + target: "axiom-card", + position: "bottom", + }, + { + id: "codex", + title: "Codex - The Standard", + content: "The Foundation trains elite Metaverse Architects through gamified curriculum and verified certifications.", + target: "codex-card", + position: "bottom", + }, + { + id: "aegis", + title: "Aegis - The Shield", + content: "Real-time security for virtual environments. PII scrubbing, threat detection, and protection for every line of code.", + target: "aegis-card", + position: "bottom", + }, + { + id: "demos", + title: "Try It Yourself", + content: "Explore our demos: view a sample Passport credential, try the Terminal security demo, or browse the Tech Tree curriculum.", + target: "demo-section", + position: "top", + }, + { + id: "complete", + title: "You're All Set!", + content: "You now know the basics of AeThex. Start exploring, or visit our Foundation to begin your journey as a Metaverse Architect.", + position: "center", + }, +]; + +export const dashboardTutorialSteps: TutorialStep[] = [ + { + id: "welcome", + title: "Your Dashboard", + content: "Welcome to your personal command center. Here you can track your progress, achievements, and activity.", + position: "center", + }, + { + id: "profile", + title: "Your Profile", + content: "This shows your current level, XP, and verification status. Keep completing challenges to level up!", + target: "profile-section", + position: "right", + }, + { + id: "stats", + title: "Your Stats", + content: "Track your key metrics: total XP earned, current level, and your verification status.", + target: "stats-section", + position: "bottom", + }, +]; + +export function TutorialButton({ onClick }: { onClick: () => void }) { + return ( + + + + Start Tutorial + + + ); +} diff --git a/client/src/pages/home.tsx b/client/src/pages/home.tsx index 948f178..806b88b 100644 --- a/client/src/pages/home.tsx +++ b/client/src/pages/home.tsx @@ -1,14 +1,18 @@ +import { useEffect } from "react"; import { motion } from "framer-motion"; import { Link } from "wouter"; import { useQuery } from "@tanstack/react-query"; import { Shield, FileCode, Terminal as TerminalIcon, ChevronRight, BarChart3, Network, ExternalLink, Lock, Zap, Users, Globe, CheckCircle, ArrowRight, Star, - Award, Cpu, Building + Award, Cpu, Building, Sparkles } from "lucide-react"; import gridBg from '@assets/generated_images/dark_subtle_digital_grid_texture.png'; +import { useTutorial, homeTutorialSteps, TutorialButton } from "@/components/Tutorial"; export default function Home() { + const { startTutorial, hasCompletedTutorial, isActive } = useTutorial(); + const { data: metrics } = useQuery({ queryKey: ["metrics"], queryFn: async () => { @@ -99,6 +103,7 @@ export default function Home() { animate={{ opacity: 1 }} transition={{ delay: 0.2 }} className="grid grid-cols-2 md:grid-cols-4 gap-6 mb-16 w-full max-w-4xl" + data-tutorial="metrics-section" > {metrics.totalProfiles} @@ -130,6 +135,7 @@ export default function Home() { animate={{ opacity: 1, y: 0 }} transition={{ delay: 0.1 }} className="group relative border border-white/10 bg-card/50 p-8 hover:border-primary/50 transition-colors duration-300 cursor-pointer overflow-hidden h-full" + data-tutorial="axiom-card" > @@ -152,6 +158,7 @@ export default function Home() { animate={{ opacity: 1, y: 0 }} transition={{ delay: 0.2 }} className="group relative border border-white/10 bg-card/50 p-8 hover:border-secondary/50 transition-colors duration-300 cursor-pointer overflow-hidden h-full" + data-tutorial="codex-card" > @@ -168,7 +175,7 @@ export default function Home() { - + See It In Action - + View Sample Passport @@ -376,6 +383,11 @@ export default function Home() { + + {/* Tutorial Button */} + {!isActive && ( + startTutorial(homeTutorialSteps)} /> + )} ); }
{step.content}
{step.action}