'use client' import { useState, useEffect, useCallback } from 'react' interface TourStep { target: string | null // CSS selector, or null = centered modal title: string body: string position: 'top' | 'bottom' | 'right' | 'center' onEnter?: () => void // fired when this step becomes active noOverlay?: boolean // skip dark overlay (use when target is inside a modal) } const STEPS: TourStep[] = [ { target: '[data-tour="tabs"]', title: 'Browse by occasion or category', body: 'Tap any tab to filter the collection — or "All" to see everything. Seasonal occasions like birthdays and weddings appear here automatically.', position: 'bottom', }, { target: '[data-tour="first-card"]', title: 'Tap any arrangement to customize', body: 'Each card shows your arrangement options. Tap the card or "Customize & Order" to open the builder.', position: 'bottom', }, { target: '[data-tour="color-section"]', title: 'Pick your balloon colors', body: 'Tap a color family to browse 40+ latex shades. Build your palette by selecting one or more colors.', position: 'bottom', noOverlay: true, onEnter: () => { const card = document.querySelector('[data-tour="first-card"] .product-card') as HTMLElement | null card?.click() }, }, { target: '[data-tour="add-to-order"]', title: 'Add to your order', body: "When you're happy with your colors and options, tap \"Add to Order\" at the bottom of the screen to add the arrangement to your cart.", position: 'center', noOverlay: true, }, { target: null, title: 'Delivery or pickup — your choice', body: "At checkout, pick a delivery time and we'll bring everything to you, or choose a pickup time at our Milford, CT shop. Payment is fully secure.", position: 'center', onEnter: () => { window.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape', bubbles: true })) }, }, ] const PAD = 10 // px padding around spotlight const TIP_WIDTH = 300 // tooltip width in px interface Props { onDone: () => void onStart?: () => void } export default function GuidedTour({ onDone, onStart }: Props) { const [step, setStep] = useState(0) const [targetRect, setTargetRect] = useState(null) const current = STEPS[step] const measureTarget = useCallback(() => { if (!current.target) { setTargetRect(null); return } const el = document.querySelector(current.target) if (el) setTargetRect(el.getBoundingClientRect()) }, [current.target]) // Switch to All tab and pick the 11" Latex card as the tour example on mount useEffect(() => { onStart?.() // eslint-disable-next-line react-hooks/exhaustive-deps }, []) // On step change: fire onEnter, poll until target appears, then scroll + measure. useEffect(() => { setTargetRect(null) // clear stale rect immediately current.onEnter?.() if (!current.target) return let cancelled = false let attempts = 0 const MAX = 15 const tryMeasure = () => { if (cancelled) return const el = document.querySelector(current.target!) if (el) { // Scroll within modal body (more reliable than scrollIntoView in fixed containers on mobile) const scrollParent = el.closest('.modal-card-body') as HTMLElement | null if (scrollParent) { const parentRect = scrollParent.getBoundingClientRect() const elRect = el.getBoundingClientRect() scrollParent.scrollTop = elRect.top - parentRect.top + scrollParent.scrollTop - 20 } else { el.scrollIntoView({ behavior: 'smooth', block: 'start' }) } // Measure after layout settles (double-rAF ensures paint is done) requestAnimationFrame(() => { requestAnimationFrame(() => { if (!cancelled) measureTarget() }) }) } else if (attempts < MAX) { attempts++ setTimeout(tryMeasure, 200) } } tryMeasure() return () => { cancelled = true } // eslint-disable-next-line react-hooks/exhaustive-deps }, [step]) // Keep spotlight synced with resize / scroll useEffect(() => { window.addEventListener('resize', measureTarget) window.addEventListener('scroll', measureTarget, true) return () => { window.removeEventListener('resize', measureTarget) window.removeEventListener('scroll', measureTarget, true) } }, [measureTarget]) // Hide the Bulma modal-background for the entire tour so there's no dark flash when modals open useEffect(() => { const style = document.createElement('style') style.textContent = '.modal-background { opacity: 0 !important; pointer-events: none !important; }' document.head.appendChild(style) return () => style.remove() }, []) const next = () => { if (step < STEPS.length - 1) setStep((s) => s + 1) else onDone() } const prev = () => setStep((s) => Math.max(0, s - 1)) // ── Spotlight geometry ─────────────────────────────────────────────────────── const spot = targetRect ? { top: targetRect.top - PAD, left: targetRect.left - PAD, width: targetRect.width + PAD * 2, height: targetRect.height + PAD * 2, } : null // ── Tooltip geometry ───────────────────────────────────────────────────────── const vw = typeof window !== 'undefined' ? window.innerWidth : 800 const vh = typeof window !== 'undefined' ? window.innerHeight : 600 let tooltipStyle: React.CSSProperties = {} if (!spot || current.position === 'center') { tooltipStyle = { position: 'fixed', top: '50%', left: '50%', transform: 'translate(-50%, -50%)', width: Math.min(TIP_WIDTH, vw - 32), zIndex: 10002, } } else if (current.position === 'bottom') { const left = Math.min( Math.max(8, spot.left + spot.width / 2 - TIP_WIDTH / 2), vw - TIP_WIDTH - 8 ) tooltipStyle = { position: 'fixed', top: spot.top + spot.height + 12, left, width: Math.min(TIP_WIDTH, vw - 16), zIndex: 10002, } } else if (current.position === 'top') { const left = Math.min( Math.max(8, spot.left + spot.width / 2 - TIP_WIDTH / 2), vw - TIP_WIDTH - 8 ) tooltipStyle = { position: 'fixed', bottom: vh - spot.top + 12, left, width: Math.min(TIP_WIDTH, vw - 16), zIndex: 10002, } } else { // right tooltipStyle = { position: 'fixed', top: Math.max(8, spot.top + spot.height / 2 - 80), left: Math.min(spot.left + spot.width + 12, vw - TIP_WIDTH - 8), width: Math.min(TIP_WIDTH, vw - 16), zIndex: 10002, } } const isLast = step === STEPS.length - 1 const isFirst = step === 0 return ( <> {/* Dark overlay — only when not targeting a modal element */} {!current.noOverlay && (
)} {/* Spotlight — dark surround with cutout */} {spot && !current.noOverlay && (
)} {/* Highlight ring — bright border, no overlay, for modal steps */} {spot && current.noOverlay && (
)} {/* Tooltip card */}
e.stopPropagation()} > {/* Step dots */}
{STEPS.map((_, i) => (
))}

{current.title}

{current.body}

{!isFirst && ( )}
) }