import {AnimatePresence, motion} from 'framer-motion'; import {ArrowRight, Check, Code2, FileText, Mail, ShieldCheck, Users, Workflow, X, Zap} from 'lucide-react'; import {useRouter} from 'next/router'; import {useEffect, useMemo} from 'react'; import {useSWRConfig} from 'swr'; import {useActiveProject} from '../../lib/contexts/ActiveProjectProvider'; import {useOnboardingComplete} from '../../lib/hooks/useOnboardingComplete'; import {type OnboardingPath, useOnboardingPath} from '../../lib/hooks/useOnboardingPath'; import {useOnboardingStatus} from '../../lib/hooks/useOnboardingStatus'; import {useProjectSetupState} from '../../lib/hooks/useProjectSetupState'; // Keys the banner derives state from. Revalidated on every route change so // progress reflects the latest state as soon as the user moves between pages. const LIVE_KEY_FRAGMENTS = ['/setup-state', '/activity/stats', '/contacts?limit=1', '/campaigns?page=1&limit=1']; const BANNER_POLL_MS = 5_000; interface BannerStep { id: string; icon: React.ElementType; title: string; href: string; done: boolean; } function buildMarketingSteps(setupState: ReturnType['setupState']): BannerStep[] { if (!setupState) return []; return [ { id: 'domain', icon: ShieldCheck, title: 'Verify your domain', href: '/settings?tab=domains', done: setupState.hasVerifiedDomain, }, { id: 'contacts', icon: Users, title: 'Add contacts', href: '/contacts', done: setupState.contactCount > 0, }, { id: 'campaign', icon: Mail, title: 'Draft your first campaign', href: '/campaigns/create', done: setupState.lastCampaignSentAt !== null, }, ]; } function buildDeveloperSteps(setupState: ReturnType['setupState']): BannerStep[] { if (!setupState) return []; return [ { id: 'domain', icon: ShieldCheck, title: 'Verify your domain', href: '/settings?tab=domains', done: setupState.hasVerifiedDomain, }, { id: 'snippets', icon: Code2, title: 'Send your first email', href: '/onboarding/developer', done: false, }, ]; } function buildWorkflowSteps(setupState: ReturnType['setupState']): BannerStep[] { if (!setupState) return []; return [ { id: 'domain', icon: ShieldCheck, title: 'Verify your domain', href: '/settings?tab=domains', done: setupState.hasVerifiedDomain, }, { id: 'event', icon: Zap, title: 'Fire your first event', href: '/onboarding/workflows', done: false, }, { id: 'template', icon: FileText, title: 'Create a template', href: '/templates/create', done: false, }, { id: 'workflow', icon: Workflow, title: 'Build a workflow', href: '/workflows', done: setupState.hasEnabledWorkflow, }, ]; } function stepsForPath( path: OnboardingPath, setupState: ReturnType['setupState'], ): BannerStep[] { if (path === 'developer') return buildDeveloperSteps(setupState); if (path === 'workflows') return buildWorkflowSteps(setupState); return buildMarketingSteps(setupState); } export function OnboardingBanner() { const router = useRouter(); const {activeProject} = useActiveProject(); const status = useOnboardingStatus(); const {path, clearPath} = useOnboardingPath(activeProject?.id); const {markComplete} = useOnboardingComplete(activeProject?.id); const onOnboardingRoute = router.pathname.startsWith('/onboarding'); const bannerCandidate = !onOnboardingRoute && status === 'show' && Boolean(path); const {setupState} = useProjectSetupState(activeProject?.id); const {mutate} = useSWRConfig(); useEffect(() => { if (!bannerCandidate) return; const revalidate = () => { void mutate( key => typeof key === 'string' && LIVE_KEY_FRAGMENTS.some(fragment => key.includes(fragment)), ); }; const interval = window.setInterval(revalidate, BANNER_POLL_MS); router.events.on('routeChangeComplete', revalidate); return () => { window.clearInterval(interval); router.events.off('routeChangeComplete', revalidate); }; }, [bannerCandidate, mutate, router.events]); const steps = useMemo(() => { if (!path) return []; return stepsForPath(path, setupState); }, [path, setupState]); const visible = bannerCandidate && steps.length > 0; const doneCount = steps.filter(s => s.done).length; const currentStepId = steps.find(s => !s.done)?.id; const handleDismiss = () => { markComplete(); clearPath(); }; const handleStepClick = (href: string) => { void router.push(href); }; return ( {visible && (

Finish your setup

{doneCount} of {steps.length} {doneCount === 1 ? 'step' : 'steps'} done

{steps.map((step, index) => { const Icon = step.icon; const isCurrent = step.id === currentStepId; const base = 'group relative inline-flex items-center gap-2 rounded-lg border px-3 py-2 text-xs font-medium transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2'; const variant = step.done ? 'border-green-200 bg-green-50 text-green-900 hover:border-green-300' : isCurrent ? 'border-neutral-900 bg-neutral-900 text-white hover:bg-neutral-800' : 'border-neutral-200 bg-white text-neutral-700 hover:border-neutral-300 hover:text-neutral-900'; return ( ); })}
)}
); }