diff --git a/package.json b/package.json index 7e788f6..9f03114 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,8 @@ "@stellar/freighter-api": "^2.0.0", "next": "^14.2.0", "react": "^18.3.0", - "react-dom": "^18.3.0" + "react-dom": "^18.3.0", + "react-joyride": "^2.9.3" }, "devDependencies": { "@types/node": "^20.0.0", diff --git a/src/app/providers.tsx b/src/app/providers.tsx index 98f8bf5..c91856f 100644 --- a/src/app/providers.tsx +++ b/src/app/providers.tsx @@ -2,6 +2,7 @@ import React, { createContext, useContext, useState, useCallback, useEffect } from "react"; import { connectWallet, getPublicKey, isFreighterInstalled } from "@/lib/wallet"; +import { OnboardingTour } from "@/components/OnboardingTour"; interface WalletContextType { address: string | null; @@ -54,6 +55,7 @@ export function Providers({ children }: { children: React.ReactNode }) { disconnect, }} > + {children} ); diff --git a/src/components/OnboardingTour.tsx b/src/components/OnboardingTour.tsx new file mode 100644 index 0000000..81a439e --- /dev/null +++ b/src/components/OnboardingTour.tsx @@ -0,0 +1,96 @@ +"use client"; + +import React, { useEffect, useState } from "react"; +import Joyride, { CallBackProps, STATUS, Step } from "react-joyride"; + +export const OnboardingTour = () => { + const [run, setRun] = useState(false); + const [isMounted, setIsMounted] = useState(false); + + useEffect(() => { + setIsMounted(true); + // Check local storage to see if tour was already completed + const hasSeenTour = localStorage.getItem("sorosave_tour_completed"); + if (!hasSeenTour) { + setRun(true); + } + + // Listen for custom event to trigger replay + const handleReplay = () => { + localStorage.removeItem("sorosave_tour_completed"); + setRun(true); + }; + + window.addEventListener("replay_onboarding_tour", handleReplay); + return () => { + window.removeEventListener("replay_onboarding_tour", handleReplay); + }; + }, []); + + const steps: Step[] = [ + { + target: "body", + content: "Welcome to Sorosave! Let's take a quick tour of our platform.", + placement: "center", + disableBeacon: true, + }, + { + target: ".tour-wallet-connect", + content: "First, connect your wallet to get started.", + placement: "bottom", + }, + { + target: ".tour-browse-groups", + content: "Browse available saving groups to find one that fits your goals.", + placement: "bottom", + }, + { + target: ".tour-join-group", + content: "Join a group to participate in decentralized savings.", + placement: "bottom", + }, + { + target: ".tour-contribute", + content: "Finally, make regular contributions and watch your savings grow!", + placement: "top", + } + ]; + + const handleJoyrideCallback = (data: CallBackProps) => { + const { status } = data; + const finishedStatuses: string[] = [STATUS.FINISHED, STATUS.SKIPPED]; + + // If the tour is finished or the user skips it + if (finishedStatuses.includes(status)) { + setRun(false); + localStorage.setItem("sorosave_tour_completed", "true"); + } + }; + + if (!isMounted) return null; + + return ( + + ); +}; + +// Expose a helper to trigger the tour manually from Settings/Help menus +export const triggerTourReplay = () => { + if (typeof window !== "undefined") { + window.dispatchEvent(new Event("replay_onboarding_tour")); + } +};