mirror of
https://github.com/AeThex-Corporation/AeThex-OS.git
synced 2026-04-18 06:17:21 +00:00
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:
parent
8d68667725
commit
c08105ded6
3 changed files with 423 additions and 5 deletions
|
|
@ -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 (
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<AuthProvider>
|
||||
<Toaster />
|
||||
<Router />
|
||||
<TutorialProvider>
|
||||
<Toaster />
|
||||
<Router />
|
||||
</TutorialProvider>
|
||||
</AuthProvider>
|
||||
</QueryClientProvider>
|
||||
);
|
||||
|
|
|
|||
403
client/src/components/Tutorial.tsx
Normal file
403
client/src/components/Tutorial.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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"
|
||||
>
|
||||
<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>
|
||||
|
|
@ -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"
|
||||
>
|
||||
<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">
|
||||
|
|
@ -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"
|
||||
>
|
||||
<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">
|
||||
|
|
@ -168,7 +175,7 @@ export default function Home() {
|
|||
</motion.div>
|
||||
</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
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
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>
|
||||
</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">
|
||||
<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
|
||||
|
|
@ -376,6 +383,11 @@ export default function Home() {
|
|||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
{/* Tutorial Button */}
|
||||
{!isActive && (
|
||||
<TutorialButton onClick={() => startTutorial(homeTutorialSteps)} />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue