Browse Groups
diff --git a/src/app/providers.tsx b/src/app/providers.tsx
index 98f8bf5..991c1d8 100644
--- a/src/app/providers.tsx
+++ b/src/app/providers.tsx
@@ -1,7 +1,15 @@
"use client";
-import React, { createContext, useContext, useState, useCallback, useEffect } from "react";
+import React, {
+ createContext,
+ useContext,
+ useState,
+ useCallback,
+ useEffect,
+ startTransition,
+} from "react";
import { connectWallet, getPublicKey, isFreighterInstalled } from "@/lib/wallet";
+import { OnboardingTour } from "@/components/OnboardingTour";
interface WalletContextType {
address: string | null;
@@ -11,6 +19,10 @@ interface WalletContextType {
disconnect: () => void;
}
+interface OnboardingContextType {
+ startTour: () => void;
+}
+
const WalletContext = createContext
({
address: null,
isConnected: false,
@@ -19,13 +31,22 @@ const WalletContext = createContext({
disconnect: () => {},
});
+const OnboardingContext = createContext({
+ startTour: () => {},
+});
+
export function useWallet() {
return useContext(WalletContext);
}
+export function useOnboardingTour() {
+ return useContext(OnboardingContext);
+}
+
export function Providers({ children }: { children: React.ReactNode }) {
const [address, setAddress] = useState(null);
const [isFreighterAvailable, setIsFreighterAvailable] = useState(false);
+ const [tourRunId, setTourRunId] = useState(0);
useEffect(() => {
isFreighterInstalled().then(setIsFreighterAvailable);
@@ -45,16 +66,27 @@ export function Providers({ children }: { children: React.ReactNode }) {
}, []);
return (
- {
+ startTransition(() => {
+ setTourRunId((value) => value + 1);
+ });
+ },
}}
>
- {children}
-
+
+ {children}
+
+
+
);
}
diff --git a/src/components/ConnectWallet.tsx b/src/components/ConnectWallet.tsx
index f039a0e..3c4de2e 100644
--- a/src/components/ConnectWallet.tsx
+++ b/src/components/ConnectWallet.tsx
@@ -13,6 +13,7 @@ export function ConnectWallet() {
href="https://www.freighter.app/"
target="_blank"
rel="noopener noreferrer"
+ data-tour="wallet-connect"
className="bg-gray-200 text-gray-700 px-4 py-2 rounded-lg text-sm font-medium hover:bg-gray-300"
>
Install Freighter
@@ -39,6 +40,7 @@ export function ConnectWallet() {
return (
diff --git a/src/components/OnboardingTour.tsx b/src/components/OnboardingTour.tsx
new file mode 100644
index 0000000..b428298
--- /dev/null
+++ b/src/components/OnboardingTour.tsx
@@ -0,0 +1,237 @@
+"use client";
+
+import { useEffect, useMemo, useState, startTransition } from "react";
+import { usePathname, useRouter } from "next/navigation";
+
+interface OnboardingTourProps {
+ runId: number;
+}
+
+interface TourStep {
+ id: string;
+ route: string;
+ selector: string;
+ title: string;
+ description: string;
+}
+
+interface HighlightRect {
+ top: number;
+ left: number;
+ width: number;
+ height: number;
+}
+
+const TOUR_STORAGE_KEY = "sorosave:onboarding-seen";
+
+const TOUR_STEPS: TourStep[] = [
+ {
+ id: "wallet",
+ route: "/",
+ selector: '[data-tour="wallet-connect"]',
+ title: "Connect your wallet first",
+ description:
+ "Freighter is the entry point for every savings action. The tour starts here so new members know where trust begins.",
+ },
+ {
+ id: "browse",
+ route: "/",
+ selector: '[data-tour="browse-groups"]',
+ title: "Browse active savings circles",
+ description:
+ "Open the groups list to compare contribution size, member count, and status before you commit.",
+ },
+ {
+ id: "join",
+ route: "/groups/1",
+ selector: '[data-tour="join-group"]',
+ title: "Join a forming group",
+ description:
+ "Forming groups still have open seats. This is where a new member joins before the rotation starts.",
+ },
+ {
+ id: "contribute",
+ route: "/groups/2",
+ selector: '[data-tour="contribute-group"]',
+ title: "Contribute when your cycle opens",
+ description:
+ "Once a group is active, this action keeps the pot moving. Members use it every round to stay in good standing.",
+ },
+];
+
+export function OnboardingTour({ runId }: OnboardingTourProps) {
+ const pathname = usePathname();
+ const router = useRouter();
+ const [isMounted, setIsMounted] = useState(false);
+ const [isRunning, setIsRunning] = useState(false);
+ const [stepIndex, setStepIndex] = useState(0);
+ const [highlightRect, setHighlightRect] = useState