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 = () => {