diff --git a/frontend/app/components/OnboardingWizard.tsx b/frontend/app/components/OnboardingWizard.tsx new file mode 100644 index 00000000..4162bcfc --- /dev/null +++ b/frontend/app/components/OnboardingWizard.tsx @@ -0,0 +1,115 @@ +"use client"; + +import React from "react"; +import { useOnboarding } from "../context/OnboardingContext"; +import { ArrowLeft, ArrowRight, CheckCircle2, Sparkles, X } from "lucide-react"; +import { Button } from "./ui/Button"; + +const ONBOARDING_STEPS = [ + { + title: "Welcome to Nestera!", + description: "Your gateway to smart savings and decentralized finance.", + icon: , + }, + { + title: "Connect Your Wallet", + description: "Link your crypto wallet to start using all features.", + icon: , + }, + { + title: "Explore the Dashboard", + description: "Get familiar with your portfolio, goals, and savings pools.", + icon: , + }, + { + title: "Create Your First Goal", + description: "Set a savings target and start growing your funds.", + icon: , + }, + { + title: "Make Your First Deposit", + description: "Fund your goal and watch your savings grow with yield.", + icon: , + }, + { + title: "You're All Set!", + description: "Congratulations! You're ready to start your savings journey.", + icon: , + }, +]; + +export function OnboardingWizard() { + const { + isOnboardingActive, + currentStep, + nextStep, + prevStep, + completeOnboarding, + skipOnboarding, + } = useOnboarding(); + + if (!isOnboardingActive) return null; + + const step = ONBOARDING_STEPS[currentStep]; + const isLastStep = currentStep === ONBOARDING_STEPS.length - 1; + const isFirstStep = currentStep === 0; + + return ( +
+
+
+ + Step {currentStep + 1} of {ONBOARDING_STEPS.length} + + +
+ +
+
+
+ +
+
+ {step.icon} +
+

{step.title}

+

{step.description}

+
+ +
+ {!isFirstStep && ( + + )} + +
+
+
+ ); +} diff --git a/frontend/app/context/OnboardingContext.tsx b/frontend/app/context/OnboardingContext.tsx new file mode 100644 index 00000000..7dca4fad --- /dev/null +++ b/frontend/app/context/OnboardingContext.tsx @@ -0,0 +1,128 @@ +"use client"; + +import React, { + createContext, + useCallback, + useContext, + useEffect, + useMemo, + useState, +} from "react"; + +const ONBOARDING_STORAGE_KEY = "nestera-onboarding-completed"; + +interface OnboardingContextValue { + isOnboardingCompleted: boolean; + isOnboardingActive: boolean; + currentStep: number; + startOnboarding: () => void; + completeOnboarding: () => void; + resetOnboarding: () => void; + nextStep: () => void; + prevStep: () => void; + goToStep: (step: number) => void; + skipOnboarding: () => void; +} + +const OnboardingContext = createContext(null); + +function readInitialOnboardingState(): boolean { + if (typeof window === "undefined") return false; + try { + return window.localStorage.getItem(ONBOARDING_STORAGE_KEY) === "true"; + } catch { + return false; + } +} + +export function OnboardingProvider({ children }: { children: React.ReactNode }) { + const [isOnboardingCompleted, setIsOnboardingCompleted] = useState(() => + readInitialOnboardingState() + ); + const [isOnboardingActive, setIsOnboardingActive] = useState(false); + const [currentStep, setCurrentStep] = useState(0); + + useEffect(() => { + if (!isOnboardingCompleted) { + setIsOnboardingActive(true); + } + }, [isOnboardingCompleted]); + + const startOnboarding = useCallback(() => { + setCurrentStep(0); + setIsOnboardingActive(true); + }, []); + + const completeOnboarding = useCallback(() => { + setIsOnboardingActive(false); + setIsOnboardingCompleted(true); + try { + window.localStorage.setItem(ONBOARDING_STORAGE_KEY, "true"); + } catch {} + }, []); + + const resetOnboarding = useCallback(() => { + setIsOnboardingCompleted(false); + try { + window.localStorage.removeItem(ONBOARDING_STORAGE_KEY); + } catch {} + startOnboarding(); + }, [startOnboarding]); + + const nextStep = useCallback(() => { + setCurrentStep((prev) => prev + 1); + }, []); + + const prevStep = useCallback(() => { + setCurrentStep((prev) => Math.max(0, prev - 1)); + }, []); + + const goToStep = useCallback((step: number) => { + setCurrentStep(step); + }, []); + + const skipOnboarding = useCallback(() => { + completeOnboarding(); + }, [completeOnboarding]); + + const value = useMemo( + () => ({ + isOnboardingCompleted, + isOnboardingActive, + currentStep, + startOnboarding, + completeOnboarding, + resetOnboarding, + nextStep, + prevStep, + goToStep, + skipOnboarding, + }), + [ + isOnboardingCompleted, + isOnboardingActive, + currentStep, + startOnboarding, + completeOnboarding, + resetOnboarding, + nextStep, + prevStep, + goToStep, + skipOnboarding, + ] + ); + + return ( + + {children} + + ); +} + +export function useOnboarding() { + const context = useContext(OnboardingContext); + if (!context) { + throw new Error("useOnboarding must be used within OnboardingProvider"); + } + return context; +} diff --git a/frontend/app/dashboard/DashboardProviders.tsx b/frontend/app/dashboard/DashboardProviders.tsx index 94023b39..acfdf706 100644 --- a/frontend/app/dashboard/DashboardProviders.tsx +++ b/frontend/app/dashboard/DashboardProviders.tsx @@ -2,13 +2,12 @@ import React from "react"; import { FeatureFlagProvider } from "../context/FeatureFlagContext"; +import { WalletProvider } from "../context/WalletContext"; +import { OnboardingProvider } from "../context/OnboardingContext"; +import { OnboardingWizard } from "../components/OnboardingWizard"; import { useWallet } from "../context/WalletContext"; -/** - * Client-side providers for the dashboard. - * Bridges server layout → client context. - */ -export default function DashboardProviders({ +function InnerDashboardProviders({ children, }: { children: React.ReactNode; @@ -18,6 +17,25 @@ export default function DashboardProviders({ return ( {children} + ); } + +/** + * Client-side providers for the dashboard. + * Bridges server layout → client context. + */ +export default function DashboardProviders({ + children, +}: { + children: React.ReactNode; +}) { + return ( + + + {children} + + + ); +} diff --git a/frontend/app/dashboard/settings/SettingsClient.tsx b/frontend/app/dashboard/settings/SettingsClient.tsx index b996349d..8af79b58 100644 --- a/frontend/app/dashboard/settings/SettingsClient.tsx +++ b/frontend/app/dashboard/settings/SettingsClient.tsx @@ -4,6 +4,7 @@ import React from "react"; import { useForm } from "react-hook-form"; import { Settings } from "lucide-react"; import { useTranslations } from "next-intl"; +import { useOnboarding } from "../../context/OnboardingContext"; type Prefs = { emailNotifications?: boolean; @@ -16,6 +17,7 @@ type Prefs = { export default function SettingsClient() { const t = useTranslations("Settings"); + const { resetOnboarding } = useOnboarding(); const [optimisticPrefs, setOptimisticPrefs] = React.useState({ emailNotifications: false, inAppNotifications: false, @@ -211,6 +213,25 @@ export default function SettingsClient() {

)} + +
+

Tutorial

+
+
+
Show onboarding tutorial again
+
+ Replay the interactive guide to Nestera +
+
+ +
+
);