import { useState, useEffect, useCallback, useRef } from 'react' import { createPortal } from 'react-dom' import { useTranslation } from 'react-i18next' interface OnboardingStep { target: string // data-onboarding attribute value title: string description: string placement: 'top' | 'bottom' | 'left' | 'right' } interface OnboardingProps { steps: OnboardingStep[] onComplete: () => void onSkip: () => void } const STORAGE_KEY = 'onboarding_completed' export function useOnboarding() { const [isCompleted, setIsCompleted] = useState(() => { return localStorage.getItem(STORAGE_KEY) === 'true' }) const complete = useCallback(() => { localStorage.setItem(STORAGE_KEY, 'true') setIsCompleted(true) }, []) const reset = useCallback(() => { localStorage.removeItem(STORAGE_KEY) setIsCompleted(false) }, []) return { isCompleted, complete, reset } } export default function Onboarding({ steps, onComplete, onSkip }: OnboardingProps) { const { t } = useTranslation() const [currentStep, setCurrentStep] = useState(0) const [targetRect, setTargetRect] = useState(null) const [isVisible, setIsVisible] = useState(false) const tooltipRef = useRef(null) const step = steps[currentStep] // Find and highlight target element useEffect(() => { const findTarget = () => { const target = document.querySelector(`[data-onboarding="${step.target}"]`) if (target) { const rect = target.getBoundingClientRect() setTargetRect(rect) // Scroll element into view if needed target.scrollIntoView({ behavior: 'smooth', block: 'center' }) // Delay visibility for smooth animation setTimeout(() => setIsVisible(true), 100) } } setIsVisible(false) const timer = setTimeout(findTarget, 300) return () => clearTimeout(timer) }, [step.target]) // Recalculate position on resize/scroll useEffect(() => { const updatePosition = () => { const target = document.querySelector(`[data-onboarding="${step.target}"]`) if (target) { setTargetRect(target.getBoundingClientRect()) } } window.addEventListener('resize', updatePosition) window.addEventListener('scroll', updatePosition, true) return () => { window.removeEventListener('resize', updatePosition) window.removeEventListener('scroll', updatePosition, true) } }, [step.target]) const handleNext = () => { if (currentStep < steps.length - 1) { setCurrentStep(currentStep + 1) } else { onComplete() } } const handlePrev = () => { if (currentStep > 0) { setCurrentStep(currentStep - 1) } } const handleSkip = () => { onSkip() } // Calculate tooltip position const getTooltipStyle = (): React.CSSProperties => { if (!targetRect) return { opacity: 0 } const padding = 16 const tooltipWidth = 320 const tooltipHeight = tooltipRef.current?.offsetHeight || 150 let top = 0 let left = 0 switch (step.placement) { case 'bottom': top = targetRect.bottom + padding left = targetRect.left + targetRect.width / 2 - tooltipWidth / 2 break case 'top': top = targetRect.top - tooltipHeight - padding left = targetRect.left + targetRect.width / 2 - tooltipWidth / 2 break case 'left': top = targetRect.top + targetRect.height / 2 - tooltipHeight / 2 left = targetRect.left - tooltipWidth - padding break case 'right': top = targetRect.top + targetRect.height / 2 - tooltipHeight / 2 left = targetRect.right + padding break } // Keep within viewport const viewportWidth = window.innerWidth const viewportHeight = window.innerHeight if (left < padding) left = padding if (left + tooltipWidth > viewportWidth - padding) { left = viewportWidth - tooltipWidth - padding } if (top < padding) top = padding if (top + tooltipHeight > viewportHeight - padding) { top = viewportHeight - tooltipHeight - padding } return { top, left, width: tooltipWidth, opacity: isVisible ? 1 : 0, transform: isVisible ? 'scale(1)' : 'scale(0.95)', } } // Spotlight style const getSpotlightStyle = (): React.CSSProperties => { if (!targetRect) return { opacity: 0 } const padding = 8 return { top: targetRect.top - padding, left: targetRect.left - padding, width: targetRect.width + padding * 2, height: targetRect.height + padding * 2, opacity: isVisible ? 1 : 0, } } return createPortal(
{/* Spotlight */}
{/* Tooltip */}
{/* Progress indicator */}
{steps.map((s, index) => (
))}
{/* Content */}

{step.title}

{step.description}

{/* Actions */}
{currentStep > 0 && ( )}
{/* Click handler to advance on target click */} {targetRect && (
)}
, document.body ) }