2026-02-17 20:49:42 +00:00
|
|
|
import React, { useState, useEffect } from 'react';
|
|
|
|
|
import Header from './components/landing/Header';
|
|
|
|
|
import Hero from './components/landing/Hero';
|
|
|
|
|
import CoachHighlight from './components/landing/CoachHighlight';
|
|
|
|
|
import HowItWorks from './components/landing/HowItWorks';
|
|
|
|
|
import Features from './components/landing/Features';
|
|
|
|
|
import Testimonials from './components/landing/Testimonials';
|
|
|
|
|
import Pricing from './components/landing/Pricing';
|
|
|
|
|
import FAQ from './components/landing/FAQ';
|
|
|
|
|
import Footer from './components/landing/Footer';
|
|
|
|
|
import RegistrationModal from './components/modals/RegistrationModal';
|
|
|
|
|
import CalculatorsModal from './components/modals/CalculatorsModal';
|
|
|
|
|
import Dashboard from './pages/Dashboard';
|
|
|
|
|
import AdminPanel from './pages/AdminPanel';
|
|
|
|
|
import ProfessionalDashboard from './pages/ProfessionalDashboard';
|
|
|
|
|
import FAQPage from './pages/FAQPage';
|
|
|
|
|
import { LanguageProvider } from './contexts/LanguageContext';
|
|
|
|
|
import { supabase } from './lib/supabase';
|
|
|
|
|
import { Loader2 } from 'lucide-react';
|
|
|
|
|
|
|
|
|
|
import { User } from './types';
|
|
|
|
|
|
|
|
|
|
// removed User interface definition
|
|
|
|
|
|
|
|
|
|
const AppContent: React.FC = () => {
|
|
|
|
|
const [isModalOpen, setIsModalOpen] = useState(false);
|
|
|
|
|
const [isToolsOpen, setIsToolsOpen] = useState(false);
|
|
|
|
|
const [authMode, setAuthMode] = useState<'login' | 'register'>('register');
|
|
|
|
|
const [selectedPlan, setSelectedPlan] = useState('starter');
|
|
|
|
|
const [currentView, setCurrentView] = useState<'home' | 'faq'>('home'); // Estado de navegação
|
|
|
|
|
const [isCompletingProfile, setIsCompletingProfile] = useState(false); // Novo estado para controle de perfil incompleto
|
|
|
|
|
|
|
|
|
|
const [user, setUser] = useState<User | null>(null);
|
|
|
|
|
const [isAdminView, setIsAdminView] = useState(false);
|
|
|
|
|
const [isProfessionalView, setIsProfessionalView] = useState(false);
|
|
|
|
|
const [isLoadingSession, setIsLoadingSession] = useState(true);
|
|
|
|
|
|
|
|
|
|
// Check active session on load
|
|
|
|
|
// Check active session on load
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
let mounted = true;
|
|
|
|
|
|
|
|
|
|
const initializeAuth = async () => {
|
|
|
|
|
try {
|
|
|
|
|
// Obter sessão inicial sem race conditions complexas
|
|
|
|
|
const { data: { session }, error } = await supabase.auth.getSession();
|
|
|
|
|
|
|
|
|
|
if (error) {
|
|
|
|
|
console.error("Erro ao obter sessão inicial:", error);
|
|
|
|
|
if (mounted) setIsLoadingSession(false);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (session?.user) {
|
|
|
|
|
console.log("App: Sessão encontrada, carregando perfil...");
|
|
|
|
|
if (mounted) {
|
|
|
|
|
await fetchUserProfile(session.user.id, session.user.email);
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
console.log("App: Nenhuma sessão ativa.");
|
|
|
|
|
if (mounted) setIsLoadingSession(false);
|
|
|
|
|
}
|
|
|
|
|
} catch (err) {
|
|
|
|
|
console.error("Erro inesperado na autenticação:", err);
|
|
|
|
|
if (mounted) setIsLoadingSession(false);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
initializeAuth();
|
|
|
|
|
|
|
|
|
|
const { data: { subscription } } = supabase.auth.onAuthStateChange(async (event, session) => {
|
|
|
|
|
console.log(`Auth event: ${event}`);
|
|
|
|
|
if (!mounted) return;
|
|
|
|
|
|
|
|
|
|
if (event === 'SIGNED_OUT') {
|
|
|
|
|
setUser(null);
|
|
|
|
|
setIsAdminView(false);
|
|
|
|
|
setIsProfessionalView(false);
|
|
|
|
|
setIsLoadingSession(false);
|
|
|
|
|
setCurrentView('home');
|
|
|
|
|
setIsCompletingProfile(false);
|
|
|
|
|
} else if (event === 'SIGNED_IN' || event === 'TOKEN_REFRESHED') {
|
|
|
|
|
if (session?.user) {
|
|
|
|
|
// Apenas recarrega se o usuário ainda não estiver setado ou se mudou
|
|
|
|
|
// Mas para garantir atualização de claims/perfil, recarregamos.
|
|
|
|
|
await fetchUserProfile(session.user.id, session.user.email);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
return () => {
|
|
|
|
|
mounted = false;
|
|
|
|
|
subscription.unsubscribe();
|
|
|
|
|
};
|
|
|
|
|
}, []);
|
|
|
|
|
|
|
|
|
|
const fetchUserProfile = async (userId: string, email?: string) => {
|
|
|
|
|
try {
|
|
|
|
|
let profile = null;
|
|
|
|
|
|
|
|
|
|
// Tentativa única de buscar perfil. O cliente Supabase já trata retries de rede.
|
|
|
|
|
const { data, error } = await supabase
|
|
|
|
|
.from('profiles')
|
|
|
|
|
.select('*')
|
|
|
|
|
.eq('id', userId)
|
|
|
|
|
.maybeSingle();
|
|
|
|
|
|
|
|
|
|
if (error) throw error;
|
|
|
|
|
|
|
|
|
|
profile = data;
|
|
|
|
|
|
|
|
|
|
// Se não tem perfil ou falta telefone, solicitamos completar cadastro
|
|
|
|
|
if (!profile || !profile.phone_e164) {
|
|
|
|
|
console.warn("Perfil incompleto. Solicitando dados.");
|
|
|
|
|
// Não fazemos signOut, apenas abrimos o modal para completar
|
|
|
|
|
setIsCompletingProfile(true);
|
|
|
|
|
setAuthMode('register'); // Visualmente irrelevante pois isCompletingProfile domina
|
|
|
|
|
setIsModalOpen(true);
|
|
|
|
|
// User fica null, então cai na Landing Page com Modal aberto. Perfeito.
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Se perfil ok, garante que flag de completar está false
|
|
|
|
|
setIsCompletingProfile(false);
|
|
|
|
|
|
|
|
|
|
const { data: entitlement } = await supabase
|
|
|
|
|
.from('user_entitlements')
|
|
|
|
|
.select('*')
|
|
|
|
|
.eq('user_id', userId)
|
|
|
|
|
.maybeSingle();
|
|
|
|
|
|
|
|
|
|
let plan: 'free' | 'pro' | 'trial' = 'free';
|
|
|
|
|
if (entitlement) {
|
2026-02-17 21:54:23 +00:00
|
|
|
const code = entitlement.entitlement_code;
|
|
|
|
|
const isActive = entitlement.is_active;
|
|
|
|
|
|
|
|
|
|
// Map various paid plans to 'pro' status
|
|
|
|
|
if (isActive && (code === 'pro' || code === 'mensal' || code === 'trimestral' || code === 'anual')) {
|
|
|
|
|
plan = 'pro';
|
|
|
|
|
} else if (isActive && code === 'trial') {
|
|
|
|
|
plan = 'trial';
|
|
|
|
|
}
|
2026-02-17 20:49:42 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
setUser({
|
|
|
|
|
id: userId,
|
|
|
|
|
name: profile.full_name || 'Usuário',
|
|
|
|
|
email: email || profile.email || '',
|
|
|
|
|
phone: profile.phone_e164,
|
|
|
|
|
public_id: profile.public_id,
|
|
|
|
|
avatar: undefined, // Column does not exist in DB, using undefined to trigger UI fallback
|
|
|
|
|
is_admin: profile.is_admin,
|
|
|
|
|
is_professional: profile.is_professional,
|
|
|
|
|
plan: plan,
|
|
|
|
|
plan_valid_until: entitlement?.valid_until
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Auto-switch logic:
|
|
|
|
|
// If user is professional, default to Professional View.
|
|
|
|
|
// If user is Admin, they usually see Admin View but we might default to user dashboard for them unless they toggle.
|
|
|
|
|
// Logic requested: "System verifies and logs him in correct panel".
|
|
|
|
|
|
|
|
|
|
const loginIntent = localStorage.getItem('login_intent');
|
|
|
|
|
|
|
|
|
|
if (profile.is_professional) {
|
|
|
|
|
setIsProfessionalView(true);
|
|
|
|
|
} else {
|
|
|
|
|
setIsProfessionalView(false);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Override if explicit intent was set (though we removed the button, old intents might linger, safe to ignore or keep)
|
|
|
|
|
if (loginIntent === 'user') {
|
|
|
|
|
// If they explicitly wanted user view but are pro, maybe respect it?
|
|
|
|
|
// For now, let's stick to the requested "System verifies" rule above.
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('Error fetching profile:', error);
|
|
|
|
|
setUser(null);
|
|
|
|
|
} finally {
|
|
|
|
|
setIsLoadingSession(false);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const handleOpenRegister = (plan: string = 'starter') => {
|
|
|
|
|
setSelectedPlan(plan);
|
|
|
|
|
setAuthMode('register');
|
|
|
|
|
setIsModalOpen(true);
|
|
|
|
|
setIsCompletingProfile(false);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const handleOpenLogin = (context?: 'user' | 'professional') => {
|
|
|
|
|
// If context is professional, we can store this intent to redirect after login
|
|
|
|
|
// For now, we'll use a simple localStorage flag or state
|
|
|
|
|
if (context === 'professional') {
|
|
|
|
|
localStorage.setItem('login_intent', 'professional');
|
|
|
|
|
} else {
|
|
|
|
|
localStorage.setItem('login_intent', 'user');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
setAuthMode('login');
|
|
|
|
|
setIsModalOpen(true);
|
|
|
|
|
setIsCompletingProfile(false);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const handleAuthSuccess = async () => {
|
|
|
|
|
setIsModalOpen(false);
|
|
|
|
|
setIsCompletingProfile(false);
|
|
|
|
|
// Force refresh profile to ensure we have latest data
|
|
|
|
|
const { data: { session } } = await supabase.auth.getSession();
|
|
|
|
|
if (session?.user) {
|
|
|
|
|
await fetchUserProfile(session.user.id, session.user.email);
|
|
|
|
|
|
|
|
|
|
// Check intent
|
|
|
|
|
const intent = localStorage.getItem('login_intent');
|
|
|
|
|
if (intent === 'professional') {
|
|
|
|
|
// We can't know for sure if they are pro yet inside this function scope easily unless we use the user state which might be stale
|
|
|
|
|
// But fetchUserProfile updates 'user'.
|
|
|
|
|
// Ideally we wait for user state to update.
|
|
|
|
|
// For simplicity, let's rely on the useEffect that watches 'user' or just check 'isProfessionalView' toggle inside fetchUserProfile?
|
|
|
|
|
// Lets keep it simple: We just set the view if the profile allows it.
|
|
|
|
|
// Actually, fetchUserProfile runs, updates User.
|
|
|
|
|
// We can check localstorage IN fetchUserProfile.
|
|
|
|
|
}
|
|
|
|
|
localStorage.removeItem('login_intent');
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const handleLogout = async () => {
|
|
|
|
|
await supabase.auth.signOut();
|
|
|
|
|
setUser(null);
|
|
|
|
|
setIsAdminView(false);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const toggleAdminView = () => {
|
|
|
|
|
if (user?.is_admin) {
|
|
|
|
|
setIsAdminView(!isAdminView);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Helper function for navigating
|
|
|
|
|
const handleNavigate = (view: 'home' | 'faq') => {
|
|
|
|
|
setCurrentView(view);
|
|
|
|
|
window.scrollTo({ top: 0, behavior: 'smooth' });
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
if (isLoadingSession) {
|
|
|
|
|
return (
|
|
|
|
|
<div className="min-h-screen flex items-center justify-center bg-white">
|
|
|
|
|
<Loader2 className="w-8 h-8 animate-spin text-brand-600" />
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Rota Admin
|
|
|
|
|
if (user && isAdminView && user.is_admin) {
|
|
|
|
|
return <AdminPanel user={user} onExitAdmin={toggleAdminView} onLogout={handleLogout} />;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Rota Profissional
|
|
|
|
|
if (user && isProfessionalView) {
|
|
|
|
|
return <ProfessionalDashboard user={user} onExit={() => setIsProfessionalView(false)} onLogout={handleLogout} />;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Rota Dashboard Usuário
|
|
|
|
|
if (user) {
|
|
|
|
|
return (
|
|
|
|
|
<Dashboard
|
|
|
|
|
user={user}
|
|
|
|
|
onLogout={handleLogout}
|
|
|
|
|
onOpenAdmin={user.is_admin ? toggleAdminView : undefined}
|
|
|
|
|
onOpenPro={() => setIsProfessionalView(true)}
|
|
|
|
|
/>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Rota Pública (Landing Page ou FAQ Page)
|
|
|
|
|
return (
|
|
|
|
|
<div className="min-h-screen bg-white text-gray-900 font-sans selection:bg-brand-100 selection:text-brand-900">
|
|
|
|
|
<Header
|
|
|
|
|
onRegister={() => handleOpenRegister('starter')}
|
|
|
|
|
onLogin={handleOpenLogin}
|
|
|
|
|
onOpenTools={() => setIsToolsOpen(true)}
|
|
|
|
|
onNavigate={handleNavigate} // Passa navegação
|
|
|
|
|
/>
|
|
|
|
|
|
|
|
|
|
<main>
|
|
|
|
|
{currentView === 'home' ? (
|
|
|
|
|
<>
|
|
|
|
|
<Hero onRegister={() => handleOpenRegister('starter')} />
|
|
|
|
|
<CoachHighlight onRegister={() => handleOpenRegister('starter')} />
|
|
|
|
|
<HowItWorks />
|
|
|
|
|
<Features />
|
|
|
|
|
<Testimonials />
|
|
|
|
|
<Pricing onRegister={handleOpenRegister} />
|
|
|
|
|
<FAQ />
|
|
|
|
|
</>
|
|
|
|
|
) : (
|
|
|
|
|
<FAQPage onBack={() => handleNavigate('home')} />
|
|
|
|
|
)}
|
|
|
|
|
</main>
|
|
|
|
|
|
|
|
|
|
<Footer
|
|
|
|
|
onRegister={() => handleOpenRegister('starter')}
|
|
|
|
|
onNavigate={handleNavigate} // Passa navegação
|
|
|
|
|
/>
|
|
|
|
|
|
|
|
|
|
<RegistrationModal
|
|
|
|
|
isOpen={isModalOpen}
|
|
|
|
|
onClose={() => setIsModalOpen(false)}
|
|
|
|
|
plan={selectedPlan}
|
|
|
|
|
mode={authMode}
|
|
|
|
|
isCompletingProfile={isCompletingProfile} // Passa o estado de completar perfil
|
|
|
|
|
onSuccess={handleAuthSuccess}
|
|
|
|
|
/>
|
|
|
|
|
|
|
|
|
|
<CalculatorsModal
|
|
|
|
|
isOpen={isToolsOpen}
|
|
|
|
|
onClose={() => setIsToolsOpen(false)}
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const App: React.FC = () => {
|
|
|
|
|
return (
|
|
|
|
|
<LanguageProvider>
|
|
|
|
|
<AppContent />
|
|
|
|
|
</LanguageProvider>
|
|
|
|
|
);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
export default App;
|