"use client"; import { type ReactNode, useEffect } from "react"; import { BrowserRouter, MemoryRouter, Navigate, Outlet, Route, Routes, useLocation, } from "react-router-dom"; import { OnboardingProvider, type OnboardingStepConfig, type OnboardingWizardConfig, } from "./onboarding-context"; // ============================================================================= // UTILS // ============================================================================= /** * Detect if we're running inside Next.js * Uses __NEXT_DATA__ which is set by Next.js on the window object */ function isNextJs(): boolean { if (typeof window === "undefined") return false; return "__NEXT_DATA__" in window; } /** * Get the initial path from the current browser URL, relative to basePath */ function getInitialPathFromUrl(basePath: string): string { if (typeof window === "undefined") return "/"; const normalizedBase = basePath.endsWith("/") ? basePath.slice(0, -1) : basePath; const currentPath = window.location.pathname; if (currentPath.startsWith(normalizedBase)) { const relativePath = currentPath.slice(normalizedBase.length); return relativePath || "/"; } return "/"; } // ============================================================================= // URL SYNC (for MemoryRouter in Next.js) // ============================================================================= function UrlSync({ basePath }: { basePath: string }) { const location = useLocation(); useEffect(() => { const normalizedBase = basePath.endsWith("/") ? basePath.slice(0, -1) : basePath; const path = location.pathname === "/" ? "" : location.pathname; const fullPath = `${normalizedBase}${path}${location.search}${location.hash}`; if ( window.location.pathname + window.location.search + window.location.hash !== fullPath ) { window.history.replaceState(null, "", fullPath); } }, [location, basePath]); return null; } // ============================================================================= // ONBOARDING ROUTES // ============================================================================= interface OnboardingRoutesProps { steps: OnboardingStepConfig[]; layout?: React.ComponentType<{ children: ReactNode }>; } function OnboardingRoutes({ steps, layout: Layout }: OnboardingRoutesProps) { const firstStepPath = steps[0]?.path || ""; // Wrapper to match dashboard pattern - Layout wraps an Outlet const LayoutWrapper = Layout ? () => ( ) : undefined; return ( {LayoutWrapper ? ( }> {/* Redirect root to first step */} } /> {/* Step routes */} {steps.map((step) => { const StepComponent = step.component; return ( } /> ); })} {/* Fallback to first step */} } /> ) : ( <> {/* Redirect root to first step */} } /> {/* Step routes */} {steps.map((step) => { const StepComponent = step.component; return ( } /> ); })} {/* Fallback to first step */} } /> )} ); } // ============================================================================= // STEP COMPONENT WRAPPER // ============================================================================= /** * HOC to inject onboarding props into step components * Use this when defining step components to receive navigation helpers */ export function withOnboardingStep

( Component: React.ComponentType

, ): React.ComponentType< Omit > { return function WrappedComponent( props: Omit, ) { const { useOnboarding } = require("./onboarding-context"); const onboardingProps = useOnboarding(); return ; }; } // ============================================================================= // ONBOARDING WIZARD // ============================================================================= export interface OnboardingWizardProps extends OnboardingWizardConfig { /** Optional layout wrapper component */ layout?: React.ComponentType<{ children: ReactNode }>; } /** * Self-contained onboarding wizard with routing * * Automatically detects environment: * - In Next.js: Uses MemoryRouter with URL sync to avoid conflicts * - In Vite/SPA: Uses BrowserRouter for native browser history * * @example * ```tsx * navigate('/dashboard')} * /> * ``` */ export function OnboardingWizard({ basePath = "/", steps, onComplete, onExit, layout, }: OnboardingWizardProps) { const useMemoryRouter = isNextJs(); const initialPath = getInitialPathFromUrl(basePath); const config: OnboardingWizardConfig = { basePath, steps, onComplete, onExit, }; const content = ( ); if (useMemoryRouter) { return ( {content} ); } return {content}; }