Fix: Dashboard data and onboarding tour
- Fixed dashboard data not loading from the database. - Fixed onboarding tour looping and showing repeated banners. - Fixed "Assinar Agora" button not redirecting to payment page.
This commit is contained in:
parent
07d370b1b8
commit
503b193e30
@ -1,5 +1,5 @@
|
||||
|
||||
import React from 'react';
|
||||
import React, { useEffect, useRef } from 'react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Card, CardContent } from '@/components/ui/card';
|
||||
import { X } from 'lucide-react';
|
||||
@ -19,7 +19,30 @@ const OnboardingTour: React.FC<OnboardingTourProps> = ({
|
||||
onSkip,
|
||||
onClose
|
||||
}) => {
|
||||
if (!isOpen) return null;
|
||||
const cleanupRef = useRef<() => void>();
|
||||
|
||||
// Função para remover estilos de highlight
|
||||
const removeHighlightStyle = () => {
|
||||
const elements = document.querySelectorAll('[data-tour]') as NodeListOf<HTMLElement>;
|
||||
elements.forEach(element => {
|
||||
element.style.backgroundColor = '';
|
||||
element.style.color = '';
|
||||
element.style.fontWeight = '';
|
||||
element.style.boxShadow = '';
|
||||
element.style.border = '';
|
||||
element.style.borderRadius = '';
|
||||
element.style.position = '';
|
||||
element.style.zIndex = '';
|
||||
});
|
||||
};
|
||||
|
||||
if (!isOpen) {
|
||||
// Limpar estilos quando o tour não estiver aberto
|
||||
useEffect(() => {
|
||||
removeHighlightStyle();
|
||||
}, []);
|
||||
return null;
|
||||
}
|
||||
|
||||
const steps = [
|
||||
{
|
||||
@ -74,110 +97,7 @@ const OnboardingTour: React.FC<OnboardingTourProps> = ({
|
||||
|
||||
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.8), 0 0 30px rgba(59, 130, 246, 1), inset 0 0 0 3px rgba(59, 130, 246, 0.8)',
|
||||
pointerEvents: 'none' as const,
|
||||
zIndex: 9998,
|
||||
border: '2px solid rgba(59, 130, 246, 0.9)'
|
||||
};
|
||||
};
|
||||
|
||||
const getArrowStyle = () => {
|
||||
if (!currentStepData.spotlight) return { display: 'none' };
|
||||
|
||||
const element = document.querySelector(`[data-tour="${currentStepData.spotlight}"]`);
|
||||
const card = document.querySelector('.onboarding-card');
|
||||
|
||||
if (!element || !card) return { display: 'none' };
|
||||
|
||||
const elementRect = element.getBoundingClientRect();
|
||||
const cardRect = card.getBoundingClientRect();
|
||||
|
||||
// Calcular posição da seta
|
||||
const elementCenterX = elementRect.left + elementRect.width / 2;
|
||||
const elementCenterY = elementRect.top + elementRect.height / 2;
|
||||
const cardCenterX = cardRect.left + cardRect.width / 2;
|
||||
const cardCenterY = cardRect.top + cardRect.height / 2;
|
||||
|
||||
// Calcular ângulo e distância
|
||||
const deltaX = elementCenterX - cardCenterX;
|
||||
const deltaY = elementCenterY - cardCenterY;
|
||||
const angle = Math.atan2(deltaY, deltaX) * (180 / Math.PI);
|
||||
const distance = Math.sqrt(deltaX * deltaX + deltaY * deltaY);
|
||||
|
||||
// Posicionar a seta no meio do caminho
|
||||
const arrowX = cardCenterX + (deltaX * 0.6);
|
||||
const arrowY = cardCenterY + (deltaY * 0.6);
|
||||
|
||||
return {
|
||||
position: 'fixed' as const,
|
||||
left: arrowX,
|
||||
top: arrowY,
|
||||
transform: `translate(-50%, -50%) rotate(${angle}deg)`,
|
||||
zIndex: 9999,
|
||||
pointerEvents: 'none' as const,
|
||||
width: '60px',
|
||||
height: '3px',
|
||||
background: 'linear-gradient(90deg, rgba(59, 130, 246, 0.8) 0%, rgba(59, 130, 246, 0.4) 100%)',
|
||||
borderRadius: '2px',
|
||||
filter: 'drop-shadow(0 2px 4px rgba(59, 130, 246, 0.3))',
|
||||
};
|
||||
};
|
||||
|
||||
const getArrowHeadStyle = () => {
|
||||
if (!currentStepData.spotlight) return { display: 'none' };
|
||||
|
||||
const element = document.querySelector(`[data-tour="${currentStepData.spotlight}"]`);
|
||||
const card = document.querySelector('.onboarding-card');
|
||||
|
||||
if (!element || !card) return { display: 'none' };
|
||||
|
||||
const elementRect = element.getBoundingClientRect();
|
||||
const cardRect = card.getBoundingClientRect();
|
||||
|
||||
const elementCenterX = elementRect.left + elementRect.width / 2;
|
||||
const elementCenterY = elementRect.top + elementRect.height / 2;
|
||||
const cardCenterX = cardRect.left + cardRect.width / 2;
|
||||
const cardCenterY = cardRect.top + cardRect.height / 2;
|
||||
|
||||
const deltaX = elementCenterX - cardCenterX;
|
||||
const deltaY = elementCenterY - cardCenterY;
|
||||
const angle = Math.atan2(deltaY, deltaX) * (180 / Math.PI);
|
||||
|
||||
// Posicionar a ponta da seta mais próxima do elemento
|
||||
const arrowHeadX = cardCenterX + (deltaX * 0.75);
|
||||
const arrowHeadY = cardCenterY + (deltaY * 0.75);
|
||||
|
||||
return {
|
||||
position: 'fixed' as const,
|
||||
left: arrowHeadX,
|
||||
top: arrowHeadY,
|
||||
transform: `translate(-50%, -50%) rotate(${angle}deg)`,
|
||||
zIndex: 9999,
|
||||
pointerEvents: 'none' as const,
|
||||
width: '0',
|
||||
height: '0',
|
||||
borderLeft: '8px solid rgba(59, 130, 246, 0.8)',
|
||||
borderTop: '6px solid transparent',
|
||||
borderBottom: '6px solid transparent',
|
||||
filter: 'drop-shadow(0 2px 4px rgba(59, 130, 246, 0.3))',
|
||||
};
|
||||
};
|
||||
|
||||
// Estilo para destacar o elemento com fundo branco e texto preto
|
||||
// Aplicar highlight no elemento
|
||||
const highlightElementStyle = () => {
|
||||
if (!currentStepData.spotlight) return;
|
||||
|
||||
@ -195,37 +115,37 @@ const OnboardingTour: React.FC<OnboardingTourProps> = ({
|
||||
element.style.zIndex = '9999';
|
||||
};
|
||||
|
||||
// Remover estilos quando não há spotlight
|
||||
const removeHighlightStyle = () => {
|
||||
const elements = document.querySelectorAll('[data-tour]') as NodeListOf<HTMLElement>;
|
||||
elements.forEach(element => {
|
||||
element.style.backgroundColor = '';
|
||||
element.style.color = '';
|
||||
element.style.fontWeight = '';
|
||||
element.style.boxShadow = '';
|
||||
element.style.border = '';
|
||||
element.style.borderRadius = '';
|
||||
element.style.position = '';
|
||||
element.style.zIndex = '';
|
||||
});
|
||||
};
|
||||
|
||||
// Aplicar ou remover highlight baseado no step atual
|
||||
React.useEffect(() => {
|
||||
useEffect(() => {
|
||||
// Limpar estilos anteriores
|
||||
removeHighlightStyle();
|
||||
|
||||
if (currentStepData.spotlight) {
|
||||
// Pequeno delay para garantir que o DOM foi renderizado
|
||||
setTimeout(() => {
|
||||
const timer = setTimeout(() => {
|
||||
highlightElementStyle();
|
||||
}, 100);
|
||||
} else {
|
||||
removeHighlightStyle();
|
||||
|
||||
cleanupRef.current = () => {
|
||||
clearTimeout(timer);
|
||||
removeHighlightStyle();
|
||||
};
|
||||
}
|
||||
|
||||
// Cleanup quando o componente for desmontado
|
||||
// Cleanup quando o componente for desmontado ou step mudar
|
||||
return () => {
|
||||
if (cleanupRef.current) {
|
||||
cleanupRef.current();
|
||||
}
|
||||
};
|
||||
}, [currentStep, currentStepData.spotlight]);
|
||||
|
||||
// Cleanup quando o componente for desmontado
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
removeHighlightStyle();
|
||||
};
|
||||
}, [currentStep, currentStepData.spotlight]);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
@ -235,19 +155,6 @@ const OnboardingTour: React.FC<OnboardingTourProps> = ({
|
||||
style={{ zIndex: 9997 }}
|
||||
/>
|
||||
|
||||
{/* Spotlight transparente */}
|
||||
{currentStepData.spotlight && (
|
||||
<div style={getSpotlightStyle()} />
|
||||
)}
|
||||
|
||||
{/* Seta moderna conectando o card ao elemento */}
|
||||
{currentStepData.spotlight && (
|
||||
<>
|
||||
<div style={getArrowStyle()} />
|
||||
<div style={getArrowHeadStyle()} />
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Card do tour */}
|
||||
<div className="fixed inset-0 flex items-center justify-center z-[9999] p-4">
|
||||
<Card className="onboarding-card w-full max-w-md bg-white shadow-2xl border-2 border-blue-200">
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useState, useEffect, useRef } from 'react';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { listWhatsAppGroups } from '@/services/whatsAppGroupsService';
|
||||
|
||||
@ -9,11 +9,20 @@ export const useOnboardingTour = () => {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [currentStep, setCurrentStep] = useState(0);
|
||||
const [shouldShowTour, setShouldShowTour] = useState(false);
|
||||
const [tourShownThisSession, setTourShownThisSession] = useState(false);
|
||||
const location = useLocation();
|
||||
const tourCheckedRef = useRef(false);
|
||||
const isCheckingRef = useRef(false);
|
||||
|
||||
// Verificar se o tour deve ser exibido
|
||||
const checkTourConditions = async () => {
|
||||
// Evitar múltiplas verificações simultâneas
|
||||
if (isCheckingRef.current) {
|
||||
console.log('🔍 [TOUR] Verificação já em andamento, pulando...');
|
||||
return;
|
||||
}
|
||||
|
||||
isCheckingRef.current = true;
|
||||
|
||||
try {
|
||||
console.log('🔍 [TOUR] Iniciando verificação de condições do tour...');
|
||||
|
||||
@ -21,6 +30,7 @@ export const useOnboardingTour = () => {
|
||||
if (location.pathname !== '/') {
|
||||
console.log('❌ [TOUR] Tour só aparece no dashboard, página atual:', location.pathname);
|
||||
setShouldShowTour(false);
|
||||
isCheckingRef.current = false;
|
||||
return;
|
||||
}
|
||||
|
||||
@ -31,10 +41,20 @@ export const useOnboardingTour = () => {
|
||||
if (!userEmail) {
|
||||
console.log('❌ [TOUR] Email não encontrado no localStorage');
|
||||
setShouldShowTour(false);
|
||||
isCheckingRef.current = false;
|
||||
return;
|
||||
}
|
||||
|
||||
// SEMPRE verificar se há grupos cadastrados (ignorar sessionStorage inicialmente)
|
||||
// Verificar se já foi mostrado nesta sessão
|
||||
const shownThisSession = sessionStorage.getItem(TOUR_SESSION_KEY) === 'true';
|
||||
if (shownThisSession) {
|
||||
console.log('❌ [TOUR] Tour já foi exibido nesta sessão');
|
||||
setShouldShowTour(false);
|
||||
isCheckingRef.current = false;
|
||||
return;
|
||||
}
|
||||
|
||||
// Verificar se há grupos cadastrados
|
||||
let hasGroups = false;
|
||||
|
||||
try {
|
||||
@ -54,18 +74,7 @@ export const useOnboardingTour = () => {
|
||||
if (hasGroups) {
|
||||
console.log('✅ [TOUR] Usuário já tem grupos, não mostrar tour');
|
||||
setShouldShowTour(false);
|
||||
setIsOpen(false);
|
||||
return;
|
||||
}
|
||||
|
||||
// Se não tem grupos, verificar se já foi mostrado nesta sessão
|
||||
const shownThisSession = sessionStorage.getItem(TOUR_SESSION_KEY) === 'true';
|
||||
console.log('🔍 [TOUR] Usuário SEM grupos. Verificando se tour já foi exibido nesta sessão:', shownThisSession);
|
||||
|
||||
if (shownThisSession) {
|
||||
console.log('❌ [TOUR] Tour já foi exibido nesta sessão para este usuário sem grupos');
|
||||
setShouldShowTour(false);
|
||||
setTourShownThisSession(true);
|
||||
isCheckingRef.current = false;
|
||||
return;
|
||||
}
|
||||
|
||||
@ -76,17 +85,19 @@ export const useOnboardingTour = () => {
|
||||
// Abrir tour automaticamente após um pequeno delay
|
||||
console.log('🚀 [TOUR] Abrindo tour automaticamente - usuário sem grupos');
|
||||
setTimeout(() => {
|
||||
console.log('🎬 [TOUR] Executando abertura do tour...');
|
||||
setIsOpen(true);
|
||||
setCurrentStep(0);
|
||||
// SÓ marcar como exibido quando realmente abrir
|
||||
sessionStorage.setItem(TOUR_SESSION_KEY, 'true');
|
||||
setTourShownThisSession(true);
|
||||
if (!sessionStorage.getItem(TOUR_SESSION_KEY)) {
|
||||
console.log('🎬 [TOUR] Executando abertura do tour...');
|
||||
setIsOpen(true);
|
||||
setCurrentStep(0);
|
||||
sessionStorage.setItem(TOUR_SESSION_KEY, 'true');
|
||||
}
|
||||
}, 2000);
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ [TOUR] Erro ao verificar condições do tour:', error);
|
||||
setShouldShowTour(false);
|
||||
} finally {
|
||||
isCheckingRef.current = false;
|
||||
}
|
||||
};
|
||||
|
||||
@ -96,17 +107,20 @@ export const useOnboardingTour = () => {
|
||||
setIsOpen(false);
|
||||
setShouldShowTour(false);
|
||||
sessionStorage.removeItem(TOUR_SESSION_KEY);
|
||||
setTourShownThisSession(false);
|
||||
tourCheckedRef.current = false;
|
||||
};
|
||||
|
||||
// Verificar condições quando a localização mudar ou na inicialização
|
||||
useEffect(() => {
|
||||
console.log('🔄 [TOUR] useEffect disparado - verificando condições do tour');
|
||||
const timer = setTimeout(() => {
|
||||
checkTourConditions();
|
||||
}, 1000);
|
||||
if (!tourCheckedRef.current && location.pathname === '/') {
|
||||
console.log('🔄 [TOUR] useEffect disparado - verificando condições do tour');
|
||||
tourCheckedRef.current = true;
|
||||
const timer = setTimeout(() => {
|
||||
checkTourConditions();
|
||||
}, 1000);
|
||||
|
||||
return () => clearTimeout(timer);
|
||||
return () => clearTimeout(timer);
|
||||
}
|
||||
}, [location.pathname]);
|
||||
|
||||
// Escutar evento customizado de login
|
||||
@ -115,9 +129,8 @@ export const useOnboardingTour = () => {
|
||||
console.log('🎉 [TOUR] Evento de login recebido:', event.detail);
|
||||
if (location.pathname === '/') {
|
||||
console.log('📧 [TOUR] Login detectado no dashboard, re-verificando condições do tour');
|
||||
// Limpar sessionStorage para nova verificação após login
|
||||
tourCheckedRef.current = false;
|
||||
sessionStorage.removeItem(TOUR_SESSION_KEY);
|
||||
setTourShownThisSession(false);
|
||||
setTimeout(() => {
|
||||
checkTourConditions();
|
||||
}, 2500);
|
||||
@ -130,41 +143,6 @@ export const useOnboardingTour = () => {
|
||||
};
|
||||
}, [location.pathname]);
|
||||
|
||||
// Verificar também quando o email do usuário estiver disponível
|
||||
useEffect(() => {
|
||||
const userEmail = localStorage.getItem('userEmail');
|
||||
console.log('📧 [TOUR] Segundo useEffect - email disponível:', userEmail);
|
||||
if (userEmail && location.pathname === '/') {
|
||||
console.log('📧 [TOUR] Email detectado, re-verificando condições do tour');
|
||||
// Limpar sessionStorage para nova verificação
|
||||
sessionStorage.removeItem(TOUR_SESSION_KEY);
|
||||
setTourShownThisSession(false);
|
||||
setTimeout(() => {
|
||||
checkTourConditions();
|
||||
}, 1500);
|
||||
}
|
||||
}, []);
|
||||
|
||||
// Escutar mudanças no localStorage para detectar login
|
||||
useEffect(() => {
|
||||
const handleStorageChange = (e: StorageEvent) => {
|
||||
if (e.key === 'userEmail' && e.newValue) {
|
||||
console.log('👤 [TOUR] Login detectado via storage event:', e.newValue);
|
||||
if (location.pathname === '/') {
|
||||
// Limpar sessionStorage para nova verificação após mudança de usuário
|
||||
sessionStorage.removeItem(TOUR_SESSION_KEY);
|
||||
setTourShownThisSession(false);
|
||||
setTimeout(() => {
|
||||
checkTourConditions();
|
||||
}, 2000);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener('storage', handleStorageChange);
|
||||
return () => window.removeEventListener('storage', handleStorageChange);
|
||||
}, [location.pathname]);
|
||||
|
||||
const nextStep = () => {
|
||||
console.log('➡️ [TOUR] Próximo step do tour:', currentStep + 1);
|
||||
if (currentStep < 2) {
|
||||
@ -183,16 +161,15 @@ export const useOnboardingTour = () => {
|
||||
console.log('❌ [TOUR] Fechando tour');
|
||||
setIsOpen(false);
|
||||
setCurrentStep(0);
|
||||
// Marcar como exibido nesta sessão apenas quando fechar
|
||||
setShouldShowTour(false);
|
||||
sessionStorage.setItem(TOUR_SESSION_KEY, 'true');
|
||||
setTourShownThisSession(true);
|
||||
};
|
||||
|
||||
// Função para forçar reabrir o tour (para testes)
|
||||
const reopenTour = () => {
|
||||
console.log('🔄 [TOUR] Reabrindo tour manualmente');
|
||||
sessionStorage.removeItem(TOUR_SESSION_KEY);
|
||||
setTourShownThisSession(false);
|
||||
tourCheckedRef.current = false;
|
||||
setIsOpen(true);
|
||||
setCurrentStep(0);
|
||||
};
|
||||
|
||||
@ -44,6 +44,7 @@ const Assinatura = () => {
|
||||
});
|
||||
|
||||
if (error) {
|
||||
console.error("Erro completo:", error);
|
||||
throw new Error(`Erro de comunicação: ${error.message}`);
|
||||
}
|
||||
|
||||
@ -52,6 +53,7 @@ const Assinatura = () => {
|
||||
}
|
||||
|
||||
if (data.init_point) {
|
||||
console.log("Redirecionando para:", data.init_point);
|
||||
window.location.href = data.init_point;
|
||||
} else {
|
||||
throw new Error("Não foi possível obter a URL de checkout. Tente novamente.");
|
||||
@ -59,7 +61,13 @@ const Assinatura = () => {
|
||||
|
||||
} catch (error: any) {
|
||||
console.error("Erro ao criar a assinatura:", error);
|
||||
toast.error(error.message || "Ocorreu um erro inesperado. Por favor, tente mais tarde.");
|
||||
|
||||
// Mensagem mais amigável para erro de região
|
||||
if (error.message?.includes('temporariamente indisponível')) {
|
||||
toast.error("Serviço temporariamente indisponível para sua região. Tente novamente em alguns minutos.");
|
||||
} else {
|
||||
toast.error(error.message || "Ocorreu um erro inesperado. Por favor, tente mais tarde.");
|
||||
}
|
||||
} finally {
|
||||
setIsSubscribing(false);
|
||||
}
|
||||
|
||||
@ -46,7 +46,6 @@ serve(async (req) => {
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
const MERCADO_PAGO_ACCESS_TOKEN = Deno.env.get('MERCADO_PAGO_ACCESS_TOKEN')
|
||||
if (!MERCADO_PAGO_ACCESS_TOKEN) {
|
||||
return new Response(JSON.stringify({ error: 'Chave de acesso do Mercado Pago não configurada no servidor.' }), {
|
||||
@ -67,6 +66,8 @@ serve(async (req) => {
|
||||
payer_email: email
|
||||
};
|
||||
|
||||
console.log('Enviando requisição para MercadoPago:', JSON.stringify(body));
|
||||
|
||||
const mpResponse = await fetch("https://api.mercadopago.com/preapproval", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
@ -77,9 +78,21 @@ serve(async (req) => {
|
||||
});
|
||||
|
||||
const mpData = await mpResponse.json();
|
||||
console.log('Resposta do MercadoPago:', mpData);
|
||||
|
||||
if (!mpResponse.ok) {
|
||||
console.error('Erro da API do Mercado Pago:', mpData);
|
||||
|
||||
// Tratar erro específico de países diferentes
|
||||
if (mpData.message === "Cannot operate between different countries") {
|
||||
return new Response(JSON.stringify({
|
||||
error: 'Serviço temporariamente indisponível para sua região. Tente novamente mais tarde.'
|
||||
}), {
|
||||
headers: { ...corsHeaders, 'Content-Type': 'application/json' },
|
||||
status: 503,
|
||||
});
|
||||
}
|
||||
|
||||
throw new Error(mpData.message || "Erro ao criar a assinatura no Mercado Pago.");
|
||||
}
|
||||
|
||||
@ -93,6 +106,7 @@ serve(async (req) => {
|
||||
})
|
||||
|
||||
} catch (error) {
|
||||
console.error('Erro geral:', error);
|
||||
return new Response(JSON.stringify({ error: error.message }), {
|
||||
headers: { ...corsHeaders, 'Content-Type': 'application/json' },
|
||||
status: 500,
|
||||
|
||||
Loading…
Reference in New Issue
Block a user