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.
This commit is contained in:
gpt-engineer-app[bot] 2025-06-22 00:10:51 +00:00
parent dcdfa9ae9c
commit 6a9e57eb9a
5 changed files with 314 additions and 52 deletions

View File

@ -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<LayoutProps> = ({ 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 (
<div className="flex h-screen overflow-hidden">
{isMobile ? (
<Sheet open={sidebarOpen} onOpenChange={setSidebarOpen}>
<SheetTrigger asChild>
<Button variant="ghost" size="icon" className="md:hidden fixed top-4 left-4 z-20">
<Menu className="h-5 w-5" />
<span className="sr-only">Menu</span>
</Button>
</SheetTrigger>
<SheetContent side="left" className="p-0 w-[250px]">
<Sidebar isOpen={true} onClose={handleCloseSidebar} />
</SheetContent>
</Sheet>
) : (
<div className="hidden md:block w-64 flex-shrink-0">
<Sidebar isOpen={true} onClose={() => {}} />
</div>
<div className="min-h-screen bg-background flex">
{/* Mobile sidebar overlay */}
{isMobile && sidebarOpen && (
<div
className="fixed inset-0 z-40 bg-black bg-opacity-50"
onClick={handleSidebarClose}
/>
)}
<div className="flex flex-col flex-1 overflow-hidden">
<Header />
<main className="flex-1 overflow-auto p-4 pt-16 md:pt-4 md:p-6">
{children}
{/* Sidebar */}
<div className={`
${isMobile
? `fixed inset-y-0 left-0 z-50 transform transition-transform duration-300 ${
sidebarOpen ? 'translate-x-0' : '-translate-x-full'
}`
: 'relative'
}
`}>
<Sidebar isOpen={sidebarOpen} onClose={handleSidebarClose} />
</div>
{/* Main content */}
<div className="flex-1 flex flex-col overflow-hidden">
<Header onMenuToggle={handleMenuToggle} />
<main className="flex-1 overflow-y-auto">
<div className="container mx-auto px-4 py-6">
{children}
</div>
</main>
</div>
{/* Help Icon */}
<HelpIcon />
{/* Tour de Onboarding */}
<OnboardingTour
isOpen={tourOpen}
currentStep={currentStep}
onNext={nextStep}
onSkip={skipTour}
onClose={closeTour}
/>
</div>
);
};
export default Layout;
}

View File

@ -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"
>
<MessageSquareText className="mr-2 h-4 w-4 transition-transform group-hover:scale-110" />
<span>Conectar WhatsApp</span>
@ -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"
>
<Users className="mr-2 h-4 w-4 transition-transform group-hover:scale-110" />
<span>Grupos</span>

View File

@ -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<OnboardingTourProps> = ({
isOpen,
currentStep,
onNext,
onSkip,
onClose
}) => {
if (!isOpen) return null;
const steps = [
{
title: "Passo 1 - Conecte seu WhatsApp",
content: (
<>
<p className="mb-4">
Para que o app funcione, você precisa integrar seu WhatsApp com nossa plataforma.
</p>
<p className="mb-4">
Isso permitirá que suas transações feitas no WhatsApp apareçam automaticamente aqui no Finance Home.
</p>
<p className="font-semibold text-blue-600">
👉 Clique em "Conectar WhatsApp", digite o código da cidade + seu número de WhatsApp e depois clique em "Criar Instância".
</p>
</>
),
spotlight: 'whatsapp-menu'
},
{
title: "Passo 2 - Crie seu Grupo com a IA",
content: (
<>
<p className="mb-4">
Agora você precisa criar um grupo no WhatsApp com o nosso bot de IA chamado Angelina.
</p>
<p className="mb-4">
Nele, você vai mandar mensagens, áudios ou comprovantes, e o sistema vai registrar automaticamente suas despesas e receitas.
</p>
<p className="font-semibold text-blue-600">
👉 Clique em "Grupos", escolha o nome do grupo e clique em "Cadastrar Grupo".
</p>
</>
),
spotlight: 'grupos-menu'
},
{
title: "Pronto! 🚀",
content: (
<>
<p className="text-lg mb-4">
Agora você pode começar a usar o Finance Home de forma automática.
</p>
<p className="text-base">
Tudo que você enviar para o grupo vai ser registrado no app.
</p>
</>
),
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 */}
<div
className="fixed inset-0 bg-black bg-opacity-70 z-[9997]"
style={{ zIndex: 9997 }}
/>
{/* Spotlight */}
{currentStepData.spotlight && (
<div style={getSpotlightStyle()} />
)}
{/* Card do tour */}
<div className="fixed inset-0 flex items-center justify-center z-[9999] p-4">
<Card className="w-full max-w-md bg-white shadow-2xl">
<CardContent className="p-6">
<div className="flex justify-between items-start mb-4">
<h3 className="text-xl font-bold text-gray-800">
{currentStepData.title}
</h3>
<Button
variant="ghost"
size="sm"
onClick={onClose}
className="p-1 h-8 w-8"
>
<X className="h-4 w-4" />
</Button>
</div>
<div className="text-gray-600 mb-6">
{currentStepData.content}
</div>
<div className="flex justify-between items-center">
<div className="flex space-x-1">
{steps.map((_, index) => (
<div
key={index}
className={`h-2 w-2 rounded-full ${
index === currentStep ? 'bg-blue-600' : 'bg-gray-300'
}`}
/>
))}
</div>
<div className="flex space-x-2">
<Button variant="outline" onClick={onSkip}>
Pular Tour
</Button>
<Button onClick={onNext}>
{currentStep === steps.length - 1 ? 'Finalizar' : 'Próximo'}
</Button>
</div>
</div>
</CardContent>
</Card>
</div>
</>
);
};
export default OnboardingTour;

View File

@ -0,0 +1,3 @@
export { default as OnboardingTour } from './OnboardingTour';
export { useOnboardingTour } from '@/hooks/useOnboardingTour';

View File

@ -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
};
};