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
This commit is contained in:
sirpiglr 2025-12-15 23:51:01 +00:00
parent 8d68667725
commit c08105ded6
3 changed files with 423 additions and 5 deletions

View file

@ -3,6 +3,7 @@ import { queryClient } from "./lib/queryClient";
import { QueryClientProvider } from "@tanstack/react-query"; import { QueryClientProvider } from "@tanstack/react-query";
import { Toaster } from "@/components/ui/toaster"; import { Toaster } from "@/components/ui/toaster";
import { AuthProvider } from "@/lib/auth"; import { AuthProvider } from "@/lib/auth";
import { TutorialProvider } from "@/components/Tutorial";
import NotFound from "@/pages/not-found"; import NotFound from "@/pages/not-found";
import Home from "@/pages/home"; import Home from "@/pages/home";
import Passport from "@/pages/passport"; import Passport from "@/pages/passport";
@ -49,8 +50,10 @@ function App() {
return ( return (
<QueryClientProvider client={queryClient}> <QueryClientProvider client={queryClient}>
<AuthProvider> <AuthProvider>
<Toaster /> <TutorialProvider>
<Router /> <Toaster />
<Router />
</TutorialProvider>
</AuthProvider> </AuthProvider>
</QueryClientProvider> </QueryClientProvider>
); );

View file

@ -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<TutorialContextType | null>(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<TutorialStep[]>([]);
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 (
<TutorialContext.Provider
value={{
isActive,
currentStep,
steps,
startTutorial,
endTutorial,
nextStep,
prevStep,
skipTutorial,
hasCompletedTutorial,
}}
>
{children}
<TutorialOverlay />
</TutorialContext.Provider>
);
}
function TutorialOverlay() {
const { isActive, currentStep, steps, nextStep, prevStep, skipTutorial } = useTutorial();
const [targetRect, setTargetRect] = useState<DOMRect | null>(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 (
<AnimatePresence>
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
className="fixed inset-0 z-[100]"
role="dialog"
aria-modal="true"
aria-label="Platform tutorial"
>
{/* Dark overlay with cutout */}
<div className="absolute inset-0 bg-black/80" aria-hidden="true" />
{/* Highlight target element */}
{targetRect && (
<motion.div
initial={{ opacity: 0, scale: 0.95 }}
animate={{ opacity: 1, scale: 1 }}
className="absolute border-2 border-primary rounded-lg pointer-events-none"
style={{
top: targetRect.top - 4,
left: targetRect.left - 4,
width: targetRect.width + 8,
height: targetRect.height + 8,
boxShadow: "0 0 0 9999px rgba(0, 0, 0, 0.75), 0 0 20px rgba(234, 179, 8, 0.5)",
}}
/>
)}
{/* Tooltip */}
<motion.div
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: 10 }}
className="absolute bg-card border border-white/10 p-6 w-[360px] max-w-[90vw] shadow-2xl"
style={getTooltipPosition()}
role="alertdialog"
aria-labelledby="tutorial-title"
aria-describedby="tutorial-content"
>
{/* Progress bar */}
<div className="absolute top-0 left-0 right-0 h-1 bg-white/10">
<motion.div
className="h-full bg-primary"
initial={{ width: 0 }}
animate={{ width: `${progress}%` }}
transition={{ duration: 0.3 }}
/>
</div>
{/* Close button */}
<button
onClick={skipTutorial}
className="absolute top-3 right-3 text-muted-foreground hover:text-white transition-colors"
data-testid="button-tutorial-close"
>
<X className="w-4 h-4" />
</button>
{/* Step indicator */}
<div className="flex items-center gap-2 mb-4">
<Sparkles className="w-4 h-4 text-primary" />
<span className="text-xs text-primary font-bold uppercase tracking-wider">
Step {currentStep + 1} of {steps.length}
</span>
</div>
{/* Content */}
<h3 id="tutorial-title" className="text-lg font-display text-white uppercase mb-2">{step.title}</h3>
<p id="tutorial-content" className="text-sm text-muted-foreground mb-6 leading-relaxed">{step.content}</p>
{step.action && (
<p className="text-xs text-primary/80 mb-4 italic">{step.action}</p>
)}
{/* Navigation */}
<div className="flex items-center justify-between">
<button
onClick={skipTutorial}
className="text-xs text-muted-foreground hover:text-white transition-colors"
data-testid="button-tutorial-skip"
>
Skip Tutorial
</button>
<div className="flex items-center gap-2">
{currentStep > 0 && (
<button
onClick={prevStep}
className="flex items-center gap-1 px-3 py-2 text-xs text-muted-foreground hover:text-white transition-colors"
data-testid="button-tutorial-prev"
>
<ChevronLeft className="w-3 h-3" /> Back
</button>
)}
<button
onClick={nextStep}
className="flex items-center gap-1 px-4 py-2 bg-primary text-background text-xs font-bold uppercase tracking-wider hover:bg-primary/90 transition-colors"
data-testid="button-tutorial-next"
>
{currentStep === steps.length - 1 ? (
<>
<CheckCircle className="w-3 h-3" /> Complete
</>
) : (
<>
Next <ChevronRight className="w-3 h-3" />
</>
)}
</button>
</div>
</div>
{/* Step dots */}
<div className="flex justify-center gap-1 mt-4">
{steps.map((_, i) => (
<div
key={i}
className={`w-2 h-2 rounded-full transition-colors ${
i === currentStep ? "bg-primary" : i < currentStep ? "bg-primary/50" : "bg-white/20"
}`}
/>
))}
</div>
</motion.div>
</motion.div>
</AnimatePresence>
);
}
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 (
<button
onClick={onClick}
className="fixed bottom-6 right-6 z-50 bg-primary text-background p-3 rounded-full shadow-lg hover:bg-primary/90 transition-colors group"
data-testid="button-start-tutorial"
>
<Sparkles className="w-5 h-5" />
<span className="absolute right-full mr-3 top-1/2 -translate-y-1/2 bg-card border border-white/10 px-3 py-1 text-xs text-white whitespace-nowrap opacity-0 group-hover:opacity-100 transition-opacity">
Start Tutorial
</span>
</button>
);
}

View file

@ -1,14 +1,18 @@
import { useEffect } from "react";
import { motion } from "framer-motion"; import { motion } from "framer-motion";
import { Link } from "wouter"; import { Link } from "wouter";
import { useQuery } from "@tanstack/react-query"; import { useQuery } from "@tanstack/react-query";
import { import {
Shield, FileCode, Terminal as TerminalIcon, ChevronRight, BarChart3, Network, Shield, FileCode, Terminal as TerminalIcon, ChevronRight, BarChart3, Network,
ExternalLink, Lock, Zap, Users, Globe, CheckCircle, ArrowRight, Star, ExternalLink, Lock, Zap, Users, Globe, CheckCircle, ArrowRight, Star,
Award, Cpu, Building Award, Cpu, Building, Sparkles
} from "lucide-react"; } from "lucide-react";
import gridBg from '@assets/generated_images/dark_subtle_digital_grid_texture.png'; import gridBg from '@assets/generated_images/dark_subtle_digital_grid_texture.png';
import { useTutorial, homeTutorialSteps, TutorialButton } from "@/components/Tutorial";
export default function Home() { export default function Home() {
const { startTutorial, hasCompletedTutorial, isActive } = useTutorial();
const { data: metrics } = useQuery({ const { data: metrics } = useQuery({
queryKey: ["metrics"], queryKey: ["metrics"],
queryFn: async () => { queryFn: async () => {
@ -99,6 +103,7 @@ export default function Home() {
animate={{ opacity: 1 }} animate={{ opacity: 1 }}
transition={{ delay: 0.2 }} transition={{ delay: 0.2 }}
className="grid grid-cols-2 md:grid-cols-4 gap-6 mb-16 w-full max-w-4xl" className="grid grid-cols-2 md:grid-cols-4 gap-6 mb-16 w-full max-w-4xl"
data-tutorial="metrics-section"
> >
<div className="text-center p-4 border border-white/5 bg-card/30 hover:border-primary/30 transition-colors"> <div className="text-center p-4 border border-white/5 bg-card/30 hover:border-primary/30 transition-colors">
<div className="text-3xl font-display font-bold text-primary">{metrics.totalProfiles}</div> <div className="text-3xl font-display font-bold text-primary">{metrics.totalProfiles}</div>
@ -130,6 +135,7 @@ export default function Home() {
animate={{ opacity: 1, y: 0 }} animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.1 }} 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" 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"
> >
<div className="absolute top-0 left-0 w-full h-1 bg-gradient-to-r from-transparent via-primary/50 to-transparent opacity-0 group-hover:opacity-100 transition-opacity" /> <div className="absolute top-0 left-0 w-full h-1 bg-gradient-to-r from-transparent via-primary/50 to-transparent opacity-0 group-hover:opacity-100 transition-opacity" />
<div className="flex justify-between items-start mb-6"> <div className="flex justify-between items-start mb-6">
@ -152,6 +158,7 @@ export default function Home() {
animate={{ opacity: 1, y: 0 }} animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.2 }} 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" 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"
> >
<div className="absolute top-0 left-0 w-full h-1 bg-gradient-to-r from-transparent via-secondary/50 to-transparent opacity-0 group-hover:opacity-100 transition-opacity" /> <div className="absolute top-0 left-0 w-full h-1 bg-gradient-to-r from-transparent via-secondary/50 to-transparent opacity-0 group-hover:opacity-100 transition-opacity" />
<div className="flex justify-between items-start mb-6"> <div className="flex justify-between items-start mb-6">
@ -168,7 +175,7 @@ export default function Home() {
</motion.div> </motion.div>
</a> </a>
<a href="https://aethex.studio" target="_blank" rel="noopener noreferrer" className="block"> <a href="https://aethex.studio" target="_blank" rel="noopener noreferrer" className="block" data-tutorial="aegis-card">
<motion.div <motion.div
initial={{ opacity: 0, y: 20 }} initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }} animate={{ opacity: 1, y: 0 }}
@ -330,7 +337,7 @@ export default function Home() {
<p className="text-2xl font-display text-white uppercase">See It In Action</p> <p className="text-2xl font-display text-white uppercase">See It In Action</p>
</motion.div> </motion.div>
<div className="flex flex-wrap justify-center gap-4"> <div className="flex flex-wrap justify-center gap-4" data-tutorial="demo-section">
<Link href="/passport"> <Link href="/passport">
<button className="text-sm border border-white/10 px-6 py-3 text-muted-foreground hover:text-white hover:border-white/30 transition-colors" data-testid="button-sample-passport"> <button className="text-sm border border-white/10 px-6 py-3 text-muted-foreground hover:text-white hover:border-white/30 transition-colors" data-testid="button-sample-passport">
View Sample Passport View Sample Passport
@ -376,6 +383,11 @@ export default function Home() {
</div> </div>
</div> </div>
{/* Tutorial Button */}
{!isActive && (
<TutorialButton onClick={() => startTutorial(homeTutorialSteps)} />
)}
</div> </div>
); );
} }