import { useState, useCallback, useMemo, useEffect, useRef } from 'react'; import type { WizardStep } from '@/types/quote'; export interface WizardStepDef { id: string; title: string; description: string; } /** Map step id from URL hash to step index */ function stepIndexFromHash(steps: WizardStepDef[]): number { const hash = window.location.hash.replace('#', ''); if (!hash) return 0; const idx = steps.findIndex((s) => s.id === hash); return idx >= 0 ? idx : 0; } /** Determine which steps should be marked complete based on a restored index */ function restoredCompletedSteps(upToIndex: number): Set { const set = new Set(); for (let i = 0; i < upToIndex; i++) { set.add(i); } return set; } export interface UseWizardReturn { currentStep: number; steps: WizardStep[]; totalSteps: number; isFirstStep: boolean; isLastStep: boolean; goToStep: (step: number) => void; nextStep: () => void; prevStep: () => void; markStepComplete: (stepIndex: number) => void; markStepIncomplete: (stepIndex: number) => void; resetWizard: () => void; progress: number; canProceed: boolean; setCanProceed: (canProceed: boolean) => void; currentStepId: string; getStepByIndex: (index: number) => WizardStep | undefined; } /** * Dynamic wizard hook — accepts a step definition array that can change * as the user enables/disables services in the discovery step. */ export function useWizard(stepDefs: WizardStepDef[]): UseWizardReturn { const initialStep = stepIndexFromHash(stepDefs); const [currentStep, setCurrentStep] = useState(initialStep); const [completedSteps, setCompletedSteps] = useState>( () => restoredCompletedSteps(initialStep) ); const [canProceed, setCanProceed] = useState(true); const isPopstateRef = useRef(false); const prevStepDefsRef = useRef(stepDefs); const totalSteps = stepDefs.length; const isFirstStep = currentStep === 0; const isLastStep = currentStep === totalSteps - 1; // When stepDefs change (services toggled), keep current position valid useEffect(() => { const prevDefs = prevStepDefsRef.current; prevStepDefsRef.current = stepDefs; if (prevDefs.length === stepDefs.length) return; // If current step is beyond new length, clamp it if (currentStep >= stepDefs.length) { setCurrentStep(Math.max(0, stepDefs.length - 1)); } // If a step was removed, try to stay on the same step id const currentId = prevDefs[currentStep]?.id; if (currentId) { const newIndex = stepDefs.findIndex((s) => s.id === currentId); if (newIndex >= 0 && newIndex !== currentStep) { setCurrentStep(newIndex); } } }, [stepDefs, currentStep]); // Sync URL hash when currentStep changes useEffect(() => { if (isPopstateRef.current) { isPopstateRef.current = false; return; } const stepId = stepDefs[currentStep]?.id; if (stepId) { const newHash = `#${stepId}`; if (window.location.hash !== newHash) { window.history.pushState(null, '', newHash); } } }, [currentStep, stepDefs]); // Listen for browser back/forward useEffect(() => { const handlePopState = () => { const idx = stepIndexFromHash(stepDefs); isPopstateRef.current = true; setCurrentStep(idx); }; window.addEventListener('popstate', handlePopState); return () => window.removeEventListener('popstate', handlePopState); }, [stepDefs]); // Set initial hash if none present useEffect(() => { if (!window.location.hash) { const stepId = stepDefs[0]?.id; if (stepId) { window.history.replaceState(null, '', `#${stepId}`); } } }, []); // eslint-disable-line react-hooks/exhaustive-deps const steps: WizardStep[] = useMemo(() => { return stepDefs.map((step, index) => ({ ...step, isComplete: completedSteps.has(index), isActive: index === currentStep, })); }, [stepDefs, currentStep, completedSteps]); const currentStepId = useMemo(() => { return stepDefs[currentStep]?.id || ''; }, [currentStep, stepDefs]); const progress = useMemo(() => { if (totalSteps <= 1) return 100; return Math.round((currentStep / (totalSteps - 1)) * 100); }, [currentStep, totalSteps]); const goToStep = useCallback( (step: number) => { if (step >= 0 && step < totalSteps) { if (step <= currentStep || completedSteps.has(step - 1) || step === currentStep + 1) { setCurrentStep(step); } } }, [totalSteps, currentStep, completedSteps] ); const nextStep = useCallback(() => { if (!isLastStep && canProceed) { setCompletedSteps((prev) => new Set(prev).add(currentStep)); setCurrentStep((prev) => prev + 1); } }, [currentStep, isLastStep, canProceed]); const prevStep = useCallback(() => { if (!isFirstStep) { setCurrentStep((prev) => prev - 1); } }, [isFirstStep]); const markStepComplete = useCallback((stepIndex: number) => { setCompletedSteps((prev) => new Set(prev).add(stepIndex)); }, []); const markStepIncomplete = useCallback((stepIndex: number) => { setCompletedSteps((prev) => { const newSet = new Set(prev); newSet.delete(stepIndex); return newSet; }); }, []); const resetWizard = useCallback(() => { setCurrentStep(0); setCompletedSteps(new Set()); setCanProceed(true); window.history.replaceState(null, '', `#${stepDefs[0]?.id}`); }, [stepDefs]); const getStepByIndex = useCallback( (index: number): WizardStep | undefined => { return steps[index]; }, [steps] ); return { currentStep, steps, totalSteps, isFirstStep, isLastStep, goToStep, nextStep, prevStep, markStepComplete, markStepIncomplete, resetWizard, progress, canProceed, setCanProceed, currentStepId, getStepByIndex, }; }