feat: SEO implementation and Stability Audit fixes (use UserContext, ErrorBoundary, Env Vars)

This commit is contained in:
Marcio Bevervanso 2026-02-17 20:01:09 -03:00
parent 5758504d4e
commit 3849edad10
10 changed files with 364 additions and 246 deletions

2
.env Normal file
View file

@ -0,0 +1,2 @@
VITE_SUPABASE_URL=https://mnhgpnqkwuqzpvfrwftp.supabase.co
VITE_SUPABASE_ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Im1uaGdwbnFrd3VxenB2ZnJ3ZnRwIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NjU3MTk4NTUsImV4cCI6MjA4MTI5NTg1NX0.DBYmhgiZoCmA0AlejJRsTh85HxRDEnG_ihkEQ2cXcpk

View file

@ -5,49 +5,64 @@
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>FoodSnap - Nutritional Intelligence</title>
<!-- Tailwind CSS -->
<script src="https://cdn.tailwindcss.com"></script>
<script>
tailwind.config = {
theme: {
extend: {
colors: {
brand: {
50: '#ecfdf5',
100: '#d1fae5',
200: '#a7f3d0',
300: '#6ee7b7',
400: '#34d399',
500: '#10b981',
600: '#059669',
700: '#047857',
800: '#065f46',
900: '#064e3b',
950: '#022c22',
}
<!-- Primary Meta Tags -->
<title>FoodSnap.ai - Nutricionista de Bolso com Inteligência Artificial</title>
<meta name="title" content="FoodSnap.ai - Nutricionista de Bolso com Inteligência Artificial" />
<meta name="description"
content="Transforme sua dieta com o FoodSnap.ai. Fotografe seu prato e receba análise nutricional completa (calorias, macros) via WhatsApp em segundos. Teste Grátis!" />
<meta name="keywords"
content="nutrição ia, contador de calorias foto, dieta whatsapp, nutricionista artificial, emagrecimento ia, food tracker, macro calculator" />
<meta name="author" content="FoodSnap AI" />
<meta name="robots" content="index, follow" />
<link rel="canonical" href="https://foodsnap.ai" />
<!-- Open Graph / Facebook -->
<meta property="og:type" content="website" />
<meta property="og:url" content="https://foodsnap.ai/" />
<meta property="og:title" content="FoodSnap.ai - Seu Nutricionista IA no WhatsApp" />
<meta property="og:description"
content="Analise calorias e macros apenas tirando uma foto. Sem digitação, sem apps pesados. Tudo pelo WhatsApp." />
<meta property="og:image" content="https://foodsnap.ai/og-image.jpg" />
<!-- Twitter -->
<meta property="twitter:card" content="summary_large_image" />
<meta property="twitter:url" content="https://foodsnap.ai/" />
<meta property="twitter:title" content="FoodSnap.ai - Nutrição Inteligente" />
<meta property="twitter:description"
content="Chega de contar calorias manualmente. Deixe a IA fazer isso por você." />
<meta property="twitter:image" content="https://foodsnap.ai/og-image.jpg" />
<!-- JSON-LD Structured Data -->
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "SoftwareApplication",
"name": "FoodSnap.ai",
"applicationCategory": "HealthApplication",
"operatingSystem": "Web, WhatsApp",
"offers": {
"@type": "Offer",
"price": "0",
"priceCurrency": "BRL"
},
fontFamily: {
sans: ['"Plus Jakarta Sans"', 'Inter', 'sans-serif'],
},
boxShadow: {
'premium': '0 20px 40px -6px rgba(0, 0, 0, 0.1)',
'glow': '0 0 20px rgba(5, 150, 105, 0.3)',
'card-hover': '0 10px 30px -5px rgba(0, 0, 0, 0.08)',
},
backgroundImage: {
'noise': "url('https://grainy-gradients.vercel.app/noise.svg')",
}
}
"description": "Aplicativo de nutrição baseado em IA que analisa fotos de comida para contagem de calorias e macros através do WhatsApp.",
"aggregateRating": {
"@type": "AggregateRating",
"ratingValue": "4.8",
"ratingCount": "1250"
}
}
</script>
<!-- Tailwind CSS -->
<script src="https://cdn.tailwindcss.com"></script>
<!-- Fonts -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&display=swap"
<link
href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&family=Plus+Jakarta+Sans:wght@400;500;600;700&display=swap"
rel="stylesheet">
<!-- Configurações de Tema e Tratamento de Erros -->
@ -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))',

6
public/robots.txt Normal file
View file

@ -0,0 +1,6 @@
User-agent: *
Allow: /
Disallow: /admin
Disallow: /dashboard
Sitemap: https://foodsnap.ai/sitemap.xml

21
public/sitemap.xml Normal file
View file

@ -0,0 +1,21 @@
<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
<url>
<loc>https://foodsnap.ai/</loc>
<lastmod>2026-02-17</lastmod>
<changefreq>daily</changefreq>
<priority>1.0</priority>
</url>
<url>
<loc>https://foodsnap.ai/login</loc>
<lastmod>2026-02-17</lastmod>
<changefreq>monthly</changefreq>
<priority>0.8</priority>
</url>
<url>
<loc>https://foodsnap.ai/register</loc>
<lastmod>2026-02-17</lastmod>
<changefreq>monthly</changefreq>
<priority>0.8</priority>
</url>
</urlset>

View file

@ -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<User | null>(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
if (isCompletingProfile) {
setAuthMode('register');
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);
}
};
}, [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.
}
await refreshProfile();
// Login intent logic handled inside context or simply by state update
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
@ -245,7 +75,7 @@ const AppContent: React.FC = () => {
window.scrollTo({ top: 0, behavior: 'smooth' });
};
if (isLoadingSession) {
if (loading) {
return (
<div className="min-h-screen flex items-center justify-center bg-white">
<Loader2 className="w-8 h-8 animate-spin text-brand-600" />
@ -255,12 +85,12 @@ const AppContent: React.FC = () => {
// Rota Admin
if (user && isAdminView && user.is_admin) {
return <AdminPanel user={user} onExitAdmin={toggleAdminView} onLogout={handleLogout} />;
return <AdminPanel user={user} onExitAdmin={toggleAdminView} onLogout={logout} />;
}
// Rota Profissional
if (user && isProfessionalView) {
return <ProfessionalDashboard user={user} onExit={() => setIsProfessionalView(false)} onLogout={handleLogout} />;
return <ProfessionalDashboard user={user} onExit={() => setIsProfessionalView(false)} onLogout={logout} />;
}
// Rota Dashboard Usuário
@ -268,7 +98,7 @@ const AppContent: React.FC = () => {
return (
<Dashboard
user={user}
onLogout={handleLogout}
onLogout={logout}
onOpenAdmin={user.is_admin ? toggleAdminView : undefined}
onOpenPro={() => 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}
/>
<main>
@ -303,7 +133,7 @@ const AppContent: React.FC = () => {
<Footer
onRegister={() => handleOpenRegister('starter')}
onNavigate={handleNavigate} // Passa navegação
onNavigate={handleNavigate}
/>
<RegistrationModal
@ -311,7 +141,7 @@ const AppContent: React.FC = () => {
onClose={() => setIsModalOpen(false)}
plan={selectedPlan}
mode={authMode}
isCompletingProfile={isCompletingProfile} // Passa o estado de completar perfil
isCompletingProfile={isCompletingProfile}
onSuccess={handleAuthSuccess}
/>
@ -325,9 +155,11 @@ const AppContent: React.FC = () => {
const App: React.FC = () => {
return (
<UserProvider>
<LanguageProvider>
<AppContent />
</LanguageProvider>
</UserProvider>
);
};

View file

@ -0,0 +1,64 @@
import React, { Component, ErrorInfo, ReactNode } from 'react';
import { AlertCircle, RefreshCw } from 'lucide-react';
interface Props {
children?: ReactNode;
}
interface State {
hasError: boolean;
error: Error | null;
}
class ErrorBoundary extends Component<Props, State> {
public state: State = {
hasError: false,
error: null
};
public static getDerivedStateFromError(error: Error): State {
return { hasError: true, error };
}
public componentDidCatch(error: Error, errorInfo: ErrorInfo) {
console.error('Uncaught error:', error, errorInfo);
}
public render() {
if (this.state.hasError) {
return (
<div className="min-h-screen flex flex-col items-center justify-center bg-gray-50 p-6 text-center">
<div className="bg-white p-8 rounded-2xl shadow-xl max-w-md w-full border border-gray-100">
<div className="w-16 h-16 bg-red-50 rounded-full flex items-center justify-center mx-auto mb-6">
<AlertCircle size={32} className="text-red-500" />
</div>
<h1 className="text-2xl font-bold text-gray-900 mb-3">Ops! Algo deu errado.</h1>
<p className="text-gray-500 mb-8">
Encontramos um erro inesperado. Tente recarregar a página.
</p>
{/* Opcional: Mostrar erro técnico em desenvolvimento */}
{import.meta.env.DEV && this.state.error && (
<div className="mb-6 p-4 bg-gray-100 rounded-lg text-left overflow-auto max-h-40 text-xs font-mono text-gray-600">
{this.state.error.toString()}
</div>
)}
<button
onClick={() => window.location.reload()}
className="w-full py-3 bg-brand-600 hover:bg-brand-700 text-white rounded-xl font-bold flex items-center justify-center gap-2 transition-colors shadow-lg shadow-brand-500/20"
>
<RefreshCw size={20} />
Recarregar Página
</button>
</div>
</div>
);
}
return this.props.children;
}
}
export default ErrorBoundary;

View file

@ -0,0 +1,172 @@
import React, { createContext, useContext, useState, useEffect, ReactNode } from 'react';
import { supabase } from '../lib/supabase';
import { User } from '../types';
interface UserContextType {
user: User | null;
loading: boolean;
isAdminView: boolean;
isProfessionalView: boolean;
isCompletingProfile: boolean;
toggleAdminView: () => void;
setIsProfessionalView: (isPro: boolean) => void;
logout: () => Promise<void>;
refreshProfile: () => Promise<void>;
}
const UserContext = createContext<UserContextType | undefined>(undefined);
export const UserProvider: React.FC<{ children: ReactNode }> = ({ children }) => {
const [user, setUser] = useState<User | null>(null);
const [loading, setLoading] = useState(true);
const [isAdminView, setIsAdminView] = useState(false);
const [isProfessionalView, setIsProfessionalView] = useState(false);
const [isCompletingProfile, setIsCompletingProfile] = useState(false);
const fetchUserProfile = async (userId: string, email?: string) => {
try {
const { data: profile, error } = await supabase
.from('profiles')
.select('*')
.eq('id', userId)
.maybeSingle();
if (error) throw error;
if (!profile || !profile.phone_e164) {
console.warn("UserContext: Perfil incompleto.");
setIsCompletingProfile(true);
// Mantemos user null ou parcial? App.tsx usava null para triggerar modal na landing
// Vamos setar null para garantir que caia na landing, mas com flag isCompletingProfile true
setUser(null);
return;
}
setIsCompletingProfile(false);
// Fetch Entitlements
const { data: entitlement } = await supabase
.from('user_entitlements')
.select('*')
.eq('user_id', userId)
.maybeSingle();
let plan: 'free' | 'pro' | 'trial' = 'free';
if (entitlement?.is_active) {
const code = entitlement.entitlement_code;
if (['pro', 'mensal', 'trimestral', 'anual'].includes(code)) plan = 'pro';
else if (code === 'trial') plan = 'trial';
}
const userData: User = {
id: userId,
name: profile.full_name || 'Usuário',
email: email || profile.email || '',
phone: profile.phone_e164,
public_id: profile.public_id,
is_admin: profile.is_admin,
is_professional: profile.is_professional,
plan: plan,
plan_valid_until: entitlement?.valid_until
};
setUser(userData);
// Auto-switch view logic
if (profile.is_professional) {
setIsProfessionalView(true);
}
} catch (error) {
console.error('UserContext: Erro ao carregar perfil', error);
setUser(null);
}
};
useEffect(() => {
let mounted = true;
// Função única de inicialização para evitar race conditions
const initSession = async () => {
try {
const { data: { session }, error } = await supabase.auth.getSession();
if (error) throw error;
if (session?.user) {
if (mounted) await fetchUserProfile(session.user.id, session.user.email);
}
} catch (error) {
console.error("UserContext: Falha na sessão inicial", error);
} finally {
if (mounted) setLoading(false);
}
};
initSession();
const { data: { subscription } } = supabase.auth.onAuthStateChange(async (event, session) => {
console.log(`UserContext Auth Event: ${event}`);
if (!mounted) return;
if (event === 'SIGNED_OUT') {
setUser(null);
setIsAdminView(false);
setIsProfessionalView(false);
setIsCompletingProfile(false);
setLoading(false);
} else if (event === 'SIGNED_IN' || event === 'TOKEN_REFRESHED') {
// Apenas recarrega se necessário.
// Para simplificar e garantir dados frescos, recarregamos.
if (session?.user) {
await fetchUserProfile(session.user.id, session.user.email);
}
setLoading(false);
}
});
return () => {
mounted = false;
subscription.unsubscribe();
};
}, []);
const logout = async () => {
await supabase.auth.signOut();
// State updates handled by onAuthStateChange
};
const toggleAdminView = () => {
if (user?.is_admin) setIsAdminView(prev => !prev);
};
const refreshProfile = async () => {
const { data: { session } } = await supabase.auth.getSession();
if (session?.user) {
await fetchUserProfile(session.user.id, session.user.email);
}
};
return (
<UserContext.Provider value={{
user,
loading,
isAdminView,
isProfessionalView,
isCompletingProfile,
toggleAdminView,
setIsProfessionalView,
logout,
refreshProfile
}}>
{children}
</UserContext.Provider>
);
};
export const useUser = () => {
const context = useContext(UserContext);
if (context === undefined) {
throw new Error('useUser must be used within a UserProvider');
}
return context;
};

View file

@ -1,8 +1,11 @@
import { createClient } from "@supabase/supabase-js";
import { Database } from "./database.types";
// Credentials provided in the prompt
const SUPABASE_URL = "https://mnhgpnqkwuqzpvfrwftp.supabase.co";
const SUPABASE_ANON_KEY = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Im1uaGdwbnFrd3VxenB2ZnJ3ZnRwIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NjU3MTk4NTUsImV4cCI6MjA4MTI5NTg1NX0.DBYmhgiZoCmA0AlejJRsTh85HxRDEnG_ihkEQ2cXcpk";
const SUPABASE_URL = import.meta.env.VITE_SUPABASE_URL;
const SUPABASE_ANON_KEY = import.meta.env.VITE_SUPABASE_ANON_KEY;
if (!SUPABASE_URL || !SUPABASE_ANON_KEY) {
throw new Error("Missing Supabase environment variables. Please check your .env file.");
}
export const supabase = createClient<Database>(SUPABASE_URL, SUPABASE_ANON_KEY);

View file

@ -1,6 +1,7 @@
import React from 'react';
import { createRoot } from 'react-dom/client';
import App from './App';
import ErrorBoundary from './components/common/ErrorBoundary';
import './index.css';
const container = document.getElementById('root');
@ -9,7 +10,9 @@ if (container) {
const root = createRoot(container);
root.render(
<React.StrictMode>
<ErrorBoundary>
<App />
</ErrorBoundary>
</React.StrictMode>
);
} else {

View file

@ -1 +1 @@
v2.67.1
v2.75.0