From 6a9e57eb9aaa4a7b00082cc8764d33430fcd1025 Mon Sep 17 00:00:00 2001 From: "gpt-engineer-app[bot]" <159125892+gpt-engineer-app[bot]@users.noreply.github.com> Date: Sun, 22 Jun 2025 00:10:51 +0000 Subject: [PATCH] Add onboarding tour for WhatsApp and Groups Implement an onboarding tour that guides users through connecting WhatsApp and creating a group. The tour should only appear until the user has created at least one instance and one group. --- src/components/layout/Layout.tsx | 110 +++++++------ src/components/layout/Sidebar.tsx | 2 + src/components/onboarding/OnboardingTour.tsx | 160 +++++++++++++++++++ src/components/onboarding/index.ts | 3 + src/hooks/useOnboardingTour.ts | 91 +++++++++++ 5 files changed, 314 insertions(+), 52 deletions(-) create mode 100644 src/components/onboarding/OnboardingTour.tsx create mode 100644 src/components/onboarding/index.ts create mode 100644 src/hooks/useOnboardingTour.ts diff --git a/src/components/layout/Layout.tsx b/src/components/layout/Layout.tsx index ff5768b..70bb6ec 100644 --- a/src/components/layout/Layout.tsx +++ b/src/components/layout/Layout.tsx @@ -1,69 +1,75 @@ -import React, { useState, useEffect } from 'react'; -import Sidebar from '@/components/layout/Sidebar'; -import Header from '@/components/layout/Header'; -import HelpIcon from '@/components/help/HelpIcon'; -import { useToast } from "@/components/ui/use-toast"; -import { Menu } from 'lucide-react'; -import { Button } from "@/components/ui/button"; -import { Sheet, SheetContent, SheetTrigger } from "@/components/ui/sheet"; +import { useState } from 'react'; +import { useIsMobile } from '@/hooks/use-mobile'; +import Header from './Header'; +import Sidebar from './Sidebar'; +import OnboardingTour from '@/components/onboarding/OnboardingTour'; +import { useOnboardingTour } from '@/hooks/useOnboardingTour'; interface LayoutProps { children: React.ReactNode; } -const Layout: React.FC = ({ children }) => { - const { toast } = useToast(); - const [isMobile, setIsMobile] = useState(false); +export default function Layout({ children }: LayoutProps) { const [sidebarOpen, setSidebarOpen] = useState(false); + const isMobile = useIsMobile(); - useEffect(() => { - const checkIfMobile = () => { - setIsMobile(window.innerWidth < 768); - }; - - checkIfMobile(); - window.addEventListener('resize', checkIfMobile); - - return () => { - window.removeEventListener('resize', checkIfMobile); - }; - }, []); + const { + isOpen: tourOpen, + currentStep, + nextStep, + skipTour, + closeTour + } = useOnboardingTour(); - const handleCloseSidebar = () => { + const handleSidebarClose = () => { setSidebarOpen(false); }; - + + const handleMenuToggle = () => { + setSidebarOpen(!sidebarOpen); + }; + return ( -
- {isMobile ? ( - - - - - - - - - ) : ( -
- {}} /> -
+
+ {/* Mobile sidebar overlay */} + {isMobile && sidebarOpen && ( +
)} -
-
-
- {children} + + {/* Sidebar */} +
+ +
+ + {/* Main content */} +
+
+
+
+ {children} +
- - {/* Help Icon */} - + + {/* Tour de Onboarding */} +
); -}; - -export default Layout; +} diff --git a/src/components/layout/Sidebar.tsx b/src/components/layout/Sidebar.tsx index b4e84d3..63eaff8 100644 --- a/src/components/layout/Sidebar.tsx +++ b/src/components/layout/Sidebar.tsx @@ -112,6 +112,7 @@ export default function Sidebar({ isOpen, onClose }: SidebarProps) { to="/whatsapp" className={({ isActive }) => getNavLinkClass(isActive)} onClick={isMobile ? onClose : undefined} + data-tour="whatsapp-menu" > Conectar WhatsApp @@ -120,6 +121,7 @@ export default function Sidebar({ isOpen, onClose }: SidebarProps) { to="/grupos-whatsapp" className={({ isActive }) => getNavLinkClass(isActive)} onClick={isMobile ? onClose : undefined} + data-tour="grupos-menu" > Grupos diff --git a/src/components/onboarding/OnboardingTour.tsx b/src/components/onboarding/OnboardingTour.tsx new file mode 100644 index 0000000..aa6d19d --- /dev/null +++ b/src/components/onboarding/OnboardingTour.tsx @@ -0,0 +1,160 @@ + +import React from 'react'; +import { Button } from '@/components/ui/button'; +import { Card, CardContent } from '@/components/ui/card'; +import { X } from 'lucide-react'; + +interface OnboardingTourProps { + isOpen: boolean; + currentStep: number; + onNext: () => void; + onSkip: () => void; + onClose: () => void; +} + +const OnboardingTour: React.FC = ({ + isOpen, + currentStep, + onNext, + onSkip, + onClose +}) => { + if (!isOpen) return null; + + const steps = [ + { + title: "Passo 1 - Conecte seu WhatsApp", + content: ( + <> +

+ Para que o app funcione, você precisa integrar seu WhatsApp com nossa plataforma. +

+

+ Isso permitirá que suas transações feitas no WhatsApp apareçam automaticamente aqui no Finance Home. +

+

+ 👉 Clique em "Conectar WhatsApp", digite o código da cidade + seu número de WhatsApp e depois clique em "Criar Instância". +

+ + ), + spotlight: 'whatsapp-menu' + }, + { + title: "Passo 2 - Crie seu Grupo com a IA", + content: ( + <> +

+ Agora você precisa criar um grupo no WhatsApp com o nosso bot de IA chamado Angelina. +

+

+ Nele, você vai mandar mensagens, áudios ou comprovantes, e o sistema vai registrar automaticamente suas despesas e receitas. +

+

+ 👉 Clique em "Grupos", escolha o nome do grupo e clique em "Cadastrar Grupo". +

+ + ), + spotlight: 'grupos-menu' + }, + { + title: "Pronto! 🚀", + content: ( + <> +

+ Agora você pode começar a usar o Finance Home de forma automática. +

+

+ Tudo que você enviar para o grupo vai ser registrado no app. +

+ + ), + spotlight: null + } + ]; + + const currentStepData = steps[currentStep]; + + const getSpotlightStyle = () => { + if (!currentStepData.spotlight) return {}; + + const element = document.querySelector(`[data-tour="${currentStepData.spotlight}"]`); + if (!element) return {}; + + const rect = element.getBoundingClientRect(); + return { + position: 'absolute' as const, + left: rect.left - 10, + top: rect.top - 10, + width: rect.width + 20, + height: rect.height + 20, + borderRadius: '8px', + boxShadow: '0 0 0 9999px rgba(0, 0, 0, 0.7), 0 0 20px rgba(59, 130, 246, 0.8)', + pointerEvents: 'none' as const, + zIndex: 9998 + }; + }; + + return ( + <> + {/* Overlay escuro */} +
+ + {/* Spotlight */} + {currentStepData.spotlight && ( +
+ )} + + {/* Card do tour */} +
+ + +
+

+ {currentStepData.title} +

+ +
+ +
+ {currentStepData.content} +
+ +
+
+ {steps.map((_, index) => ( +
+ ))} +
+ +
+ + +
+
+ + +
+ + ); +}; + +export default OnboardingTour; diff --git a/src/components/onboarding/index.ts b/src/components/onboarding/index.ts new file mode 100644 index 0000000..28b85a4 --- /dev/null +++ b/src/components/onboarding/index.ts @@ -0,0 +1,3 @@ + +export { default as OnboardingTour } from './OnboardingTour'; +export { useOnboardingTour } from '@/hooks/useOnboardingTour'; diff --git a/src/hooks/useOnboardingTour.ts b/src/hooks/useOnboardingTour.ts new file mode 100644 index 0000000..d290b29 --- /dev/null +++ b/src/hooks/useOnboardingTour.ts @@ -0,0 +1,91 @@ + +import { useState, useEffect } from 'react'; +import { useLocation } from 'react-router-dom'; +import { useWhatsAppInstances } from '@/hooks/useWhatsAppInstances'; +import { listWhatsAppGroups } from '@/services/whatsAppGroupsService'; + +export const useOnboardingTour = () => { + const [isOpen, setIsOpen] = useState(false); + const [currentStep, setCurrentStep] = useState(0); + const [shouldShowTour, setShouldShowTour] = useState(false); + const location = useLocation(); + const { instances } = useWhatsAppInstances(); + + // Verificar se o tour deve ser exibido + const checkTourConditions = async () => { + try { + // Verificar se há instâncias conectadas + const hasConnectedInstance = instances.some(instance => + instance.status === 'connected' || instance.connectionState === 'open' + ); + + // Verificar se há grupos cadastrados + const groups = await listWhatsAppGroups(); + const hasGroups = groups.length > 0; + + // Tour deve aparecer se NÃO tiver instância OU NÃO tiver grupos + const shouldShow = !hasConnectedInstance || !hasGroups; + + console.log('Tour conditions:', { + hasConnectedInstance, + hasGroups, + shouldShow, + instances: instances.length + }); + + setShouldShowTour(shouldShow); + + // Se deve mostrar o tour e não está aberto, abrir automaticamente + if (shouldShow && !isOpen) { + setIsOpen(true); + setCurrentStep(0); + } else if (!shouldShow && isOpen) { + // Se as condições foram atendidas, fechar o tour + setIsOpen(false); + } + } catch (error) { + console.error('Erro ao verificar condições do tour:', error); + setShouldShowTour(false); + } + }; + + // Verificar condições quando instâncias mudarem + useEffect(() => { + if (instances.length >= 0) { // Verificar mesmo quando não há instâncias + checkTourConditions(); + } + }, [instances]); + + // Verificar condições periodicamente + useEffect(() => { + const interval = setInterval(checkTourConditions, 5000); // A cada 5 segundos + return () => clearInterval(interval); + }, [instances]); + + const nextStep = () => { + if (currentStep < 2) { + setCurrentStep(currentStep + 1); + } else { + closeTour(); + } + }; + + const skipTour = () => { + closeTour(); + }; + + const closeTour = () => { + setIsOpen(false); + setCurrentStep(0); + }; + + return { + isOpen, + currentStep, + shouldShowTour, + nextStep, + skipTour, + closeTour, + setCurrentStep + }; +};