diff --git a/.env b/.env new file mode 100644 index 0000000..239faa1 --- /dev/null +++ b/.env @@ -0,0 +1,2 @@ +VITE_SUPABASE_URL=https://mnhgpnqkwuqzpvfrwftp.supabase.co +VITE_SUPABASE_ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Im1uaGdwbnFrd3VxenB2ZnJ3ZnRwIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NjU3MTk4NTUsImV4cCI6MjA4MTI5NTg1NX0.DBYmhgiZoCmA0AlejJRsTh85HxRDEnG_ihkEQ2cXcpk diff --git a/index.html b/index.html index e44b7c3..36fccef 100644 --- a/index.html +++ b/index.html @@ -5,49 +5,64 @@ - FoodSnap - Nutritional Intelligence - - - + + + - @@ -60,11 +75,11 @@ theme: { extend: { fontFamily: { - sans: ['"Plus Jakarta Sans"', 'Inter', 'sans-serif'], // More modern font stack + sans: ['"Plus Jakarta Sans"', 'Inter', 'sans-serif'], }, colors: { gray: { - 25: '#fcfcfd', // Lighter background + 25: '#fcfcfd', 50: '#f9fafb', 100: '#f3f4f6', 200: '#e5e7eb', @@ -99,8 +114,8 @@ 'xl': '0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 8px 10px -6px rgba(0, 0, 0, 0.1)', '2xl': '0 25px 50px -12px rgba(0, 0, 0, 0.25)', 'glow': '0 0 40px rgba(16, 185, 129, 0.2)', - 'premium': '0 20px 40px -5px rgba(0, 0, 0, 0.1), 0 10px 20px -5px rgba(0, 0, 0, 0.04)', // New premium shadow - 'card': '0 0 0 1px rgba(0,0,0,0.03), 0 2px 8px rgba(0,0,0,0.04), 0 8px 32px rgba(0,0,0,0.04)', // Enhanced card shadow + 'premium': '0 20px 40px -5px rgba(0, 0, 0, 0.1), 0 10px 20px -5px rgba(0, 0, 0, 0.04)', + 'card': '0 0 0 1px rgba(0,0,0,0.03), 0 2px 8px rgba(0,0,0,0.04), 0 8px 32px rgba(0,0,0,0.04)', }, backgroundImage: { 'gradient-radial': 'radial-gradient(var(--tw-gradient-stops))', diff --git a/public/robots.txt b/public/robots.txt new file mode 100644 index 0000000..63e503e --- /dev/null +++ b/public/robots.txt @@ -0,0 +1,6 @@ +User-agent: * +Allow: / +Disallow: /admin +Disallow: /dashboard + +Sitemap: https://foodsnap.ai/sitemap.xml diff --git a/public/sitemap.xml b/public/sitemap.xml new file mode 100644 index 0000000..e6e6aed --- /dev/null +++ b/public/sitemap.xml @@ -0,0 +1,21 @@ + + + + https://foodsnap.ai/ + 2026-02-17 + daily + 1.0 + + + https://foodsnap.ai/login + 2026-02-17 + monthly + 0.8 + + + https://foodsnap.ai/register + 2026-02-17 + monthly + 0.8 + + diff --git a/src/App.tsx b/src/App.tsx index 1cb43f5..ca91712 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -15,228 +15,58 @@ 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 { UserProvider, useUser } from './contexts/UserContext'; // Import UserContext 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 [currentView, setCurrentView] = useState<'home' | 'faq'>('home'); - const [user, setUser] = useState(null); - const [isAdminView, setIsAdminView] = useState(false); - const [isProfessionalView, setIsProfessionalView] = useState(false); - const [isLoadingSession, setIsLoadingSession] = useState(true); + // Consume UserContext + const { + user, + loading, + isAdminView, + isProfessionalView, + isCompletingProfile, + toggleAdminView, + setIsProfessionalView, + logout, + refreshProfile + } = useUser(); - // Check active session on load - // Check active session on load + // Effect to handle "Complete Profile" flow automatically 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) { - 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'; - } - } - - 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); + if (isCompletingProfile) { + setAuthMode('register'); + setIsModalOpen(true); } - - }; + }, [isCompletingProfile]); 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); - } + await refreshProfile(); + // Login intent logic handled inside context or simply by state update + localStorage.removeItem('login_intent'); }; // Helper function for navigating @@ -245,7 +75,7 @@ const AppContent: React.FC = () => { window.scrollTo({ top: 0, behavior: 'smooth' }); }; - if (isLoadingSession) { + if (loading) { return (
@@ -255,12 +85,12 @@ const AppContent: React.FC = () => { // Rota Admin if (user && isAdminView && user.is_admin) { - return ; + return ; } // Rota Profissional if (user && isProfessionalView) { - return setIsProfessionalView(false)} onLogout={handleLogout} />; + return setIsProfessionalView(false)} onLogout={logout} />; } // Rota Dashboard Usuário @@ -268,7 +98,7 @@ const AppContent: React.FC = () => { return ( setIsProfessionalView(true)} /> @@ -282,7 +112,7 @@ const AppContent: React.FC = () => { onRegister={() => handleOpenRegister('starter')} onLogin={handleOpenLogin} onOpenTools={() => setIsToolsOpen(true)} - onNavigate={handleNavigate} // Passa navegação + onNavigate={handleNavigate} />
@@ -303,7 +133,7 @@ const AppContent: React.FC = () => {