diff --git a/web/src/components/auth/RequireAuth.tsx b/web/src/components/auth/RequireAuth.tsx
index 86e4636..e5e487e 100644
--- a/web/src/components/auth/RequireAuth.tsx
+++ b/web/src/components/auth/RequireAuth.tsx
@@ -1,20 +1,14 @@
-import { useEffect, useState } from 'react'
+import { useApp } from '../../context/AppContext'
import { Navigate } from 'react-router-dom'
-import { getUser } from '../../lib/auth'
+/**
+ * Protege rotas que requerem autenticação.
+ * Usa o AppContext (que já tem o estado auth sincronizado com Supabase)
+ * para evitar uma chamada de rede extra que causava blank screen.
+ */
export function RequireAuth({ children }: { children: React.ReactNode }) {
- const [loading, setLoading] = useState(true)
- const [ok, setOk] = useState(false)
+ const { user } = useApp()
- useEffect(() => {
- ;(async () => {
- const user = await getUser()
- setOk(!!user)
- setLoading(false)
- })()
- }, [])
-
- if (loading) return null
- if (!ok) return
+ if (!user) return
return <>{children}>
}
diff --git a/web/src/context/AppContext.tsx b/web/src/context/AppContext.tsx
index cc611db..a5e342d 100644
--- a/web/src/context/AppContext.tsx
+++ b/web/src/context/AppContext.tsx
@@ -1,9 +1,6 @@
/**
* @file AppContext.tsx
* @description Gestor de Estado Global (Context API) da aplicação Web.
- * Este ficheiro gere uma arquitetura híbrida: coordena a Autenticação e Perfis (Profiles)
- * diretamente com o Supabase, enquanto mantém as entidades transacionais
- * (Shops, Appointments, Cart) num regime de persistência local 'mockada' via LocalStorage.
*/
import React, { createContext, useContext, useEffect, useState } from 'react';
import { nanoid } from 'nanoid';
@@ -51,7 +48,7 @@ type AppContextValue = State & {
const initialState: State = {
user: undefined,
users: mockUsers,
- shops: [], // Removes mockShops integration
+ shops: [],
appointments: [],
orders: [],
cart: [],
@@ -61,16 +58,7 @@ const initialState: State = {
const AppContext = createContext(undefined);
export const AppProvider = ({ children }: { children: React.ReactNode }) => {
- // Se já há sessão guardada, não mostra loading (app aparece de imediato)
- const hasStoredUser = Boolean((() => {
- try {
- const raw = localStorage.getItem('smart-agenda');
- if (!raw) return false;
- const parsed = JSON.parse(raw);
- return parsed?.user != null;
- } catch { return false; }
- })());
- const [loading, setLoading] = useState(!hasStoredUser);
+ const [loading, setLoading] = useState(true);
const [state, setState] = useState(() => {
const stored = storage.get | null>('smart-agenda', null);
const safeStored = stored && typeof stored === 'object' ? stored : {};
@@ -86,7 +74,7 @@ export const AppProvider = ({ children }: { children: React.ReactNode }) => {
...safeStored,
user: safeUser,
users: safeUsers,
- shops: [], // Start empty, will be populated by refreshShops
+ shops: [],
appointments: safeAppointments,
orders: safeOrders,
cart: safeCart,
@@ -95,30 +83,41 @@ export const AppProvider = ({ children }: { children: React.ReactNode }) => {
});
const refreshShops = async () => {
- const { data: shopsData, error: shopsError } = await supabase.from('shops').select('*');
- if (shopsError) return;
+ try {
+ const { data: shopsData, error: shopsError } = await supabase.from('shops').select('*');
+ if (shopsError || !shopsData) return;
- const { data: servicesData, error: servicesError } = await supabase.from('services').select('*');
- if (servicesError) return;
+ const { data: servicesData } = await supabase.from('services').select('*');
- const fetchedShops = shopsData.map((shop) => ({
- ...shop,
- services: servicesData.filter((s) => s.shop_id === shop.id),
- products: [],
- barbers: [],
- }));
+ const fetchedShops: BarberShop[] = shopsData.map((shop) => ({
+ id: shop.id,
+ name: shop.name,
+ address: shop.address ?? '',
+ rating: shop.rating ?? 0,
+ imageUrl: shop.image_url ?? shop.imageUrl ?? undefined,
+ services: (servicesData ?? [])
+ .filter((s) => s.shop_id === shop.id)
+ .map((s) => ({
+ id: s.id,
+ name: s.name,
+ price: s.price ?? 0,
+ duration: s.duration ?? 30,
+ barberIds: s.barber_ids ?? [],
+ })),
+ products: [],
+ barbers: [],
+ }));
- setState((s) => ({ ...s, shops: fetchedShops }));
+ setState((s) => ({ ...s, shops: fetchedShops }));
+ } catch (err) {
+ console.error('refreshShops error:', err);
+ }
};
useEffect(() => {
storage.set('smart-agenda', state);
}, [state]);
- /**
- * Hook de Inicialização Master — sequencial para evitar race condition.
- * Ordem garantida: autenticar utilizador → carregar shops da BD → libertar UI.
- */
useEffect(() => {
let mounted = true;
@@ -126,10 +125,6 @@ export const AppProvider = ({ children }: { children: React.ReactNode }) => {
setState((s) => ({ ...s, user: undefined }));
};
- /**
- * Sincroniza o Perfil do Utilizador a partir do Supabase Database ('profiles').
- * Cria automaticamente a shop se a Role for 'barbearia'.
- */
const applyProfile = async (userId: string, email?: string | null) => {
const { data, error } = await supabase
.from('profiles')
@@ -147,6 +142,9 @@ export const AppProvider = ({ children }: { children: React.ReactNode }) => {
const displayName = data.name?.trim() || (email ? email.split('@')[0] : 'Utilizador');
const shopId = role === 'barbearia' ? userId : undefined;
+ let needsInsert = false;
+ let shopNameToInsert = '';
+
setState((s) => {
const nextUser: User = {
id: userId,
@@ -162,92 +160,66 @@ export const AppProvider = ({ children }: { children: React.ReactNode }) => {
: [...s.users, nextUser];
let shops = s.shops;
- if (data.shop_name) {
- const shopIdFromOwner = userId;
- const exists = shops.some((shop) => shop.id === shopIdFromOwner);
-
+ if (data.shop_name && shopId) {
+ const exists = shops.some((shop) => shop.id === shopId);
if (!exists) {
- const shopName = data.shop_name.trim();
- const shop: BarberShop = {
- id: shopIdFromOwner,
- name: shopName,
+ shopNameToInsert = data.shop_name.trim();
+ needsInsert = true;
+ const newShop: BarberShop = {
+ id: shopId,
+ name: shopNameToInsert,
address: 'Endereço a definir',
rating: 0,
barbers: [],
services: [],
products: [],
};
- shops = [...shops, shop];
-
- // 🔹 INSERT NA BD SUPABASE
- void supabase.from('shops').insert([
- {
- id: shopIdFromOwner,
- name: shopName,
- address: 'Endereço a definir',
- rating: 0,
- }
- ]).then(({ error: insertError }) => {
- if (insertError && insertError.code !== '23505') {
- console.error('Erro ao sincronizar shop nova na BD:', insertError);
- }
- });
+ shops = [...shops, newShop];
}
}
- return {
- ...s,
- user: nextUser,
- users,
- shops,
- };
+ return { ...s, user: nextUser, users, shops };
});
- };
- /**
- * Boot sequencial: autenticar primeiro, depois carregar shops.
- * Isso garante que quando o Dashboard renderiza, a shop já está no estado.
- */
- const boot = async () => {
- try {
- const { data } = await supabase.auth.getSession();
- if (!mounted) return;
-
- const session = data.session;
- if (session?.user) {
- // 1. Aplicar perfil (adiciona shop ao estado local se necessário)
- await applyProfile(session.user.id, session.user.email);
- // 2. Recarregar todas as shops da BD (garante sync completo)
- if (mounted) await refreshShops();
- } else {
- clearUser();
- // Mesmo sem user, carrega shops para o Explore page
- if (mounted) await refreshShops();
+ // Inserir na BD se necessário (fora do setState)
+ if (needsInsert && shopId) {
+ const { error: insertErr } = await supabase.from('shops').insert([{
+ id: shopId,
+ name: shopNameToInsert,
+ address: 'Endereço a definir',
+ rating: 0,
+ }]);
+ if (insertErr && insertErr.code !== '23505') {
+ console.error('Erro ao inserir shop:', insertErr);
}
- } catch (err) {
- console.error('Erro durante o boot:', err);
- } finally {
- if (mounted) setLoading(false);
}
};
- void boot();
-
- /**
- * Listener de eventos Auth (login/logout em tempo real).
- */
- const { data: sub } = supabase.auth.onAuthStateChange(async (_event, session) => {
+ // Usa onAuthStateChange que dispara INITIAL_SESSION automaticamente
+ // Evita duplicação com getSession() separado
+ const { data: sub } = supabase.auth.onAuthStateChange(async (event, session) => {
if (!mounted) return;
+
if (session?.user) {
await applyProfile(session.user.id, session.user.email);
if (mounted) await refreshShops();
} else {
clearUser();
+ if (mounted) await refreshShops();
}
+
+ // Liberta o loading após o primeiro evento
+ if (mounted) setLoading(false);
});
+ // Fallback: se o onAuthStateChange não disparar em 5s, liberta o loading
+ const fallback = setTimeout(() => {
+ if (mounted) setLoading(false);
+ }, 5000);
+
return () => {
mounted = false;
+ clearTimeout(fallback);
sub.subscription.unsubscribe();
};
}, []);
@@ -292,21 +264,12 @@ export const AppProvider = ({ children }: { children: React.ReactNode }) => {
products: [],
};
const user: User = { ...payload, id: userId, role: 'barbearia', shopId };
- setState((s) => ({
- ...s,
- user,
- users: [...s.users, user],
- shops: [...s.shops, shop],
- }));
+ setState((s) => ({ ...s, user, users: [...s.users, user], shops: [...s.shops, shop] }));
return true;
}
const user: User = { ...payload, id: nanoid(), role: 'cliente' };
- setState((s) => ({
- ...s,
- user,
- users: [...s.users, user],
- }));
+ setState((s) => ({ ...s, user, users: [...s.users, user] }));
return true;
};
@@ -337,13 +300,7 @@ export const AppProvider = ({ children }: { children: React.ReactNode }) => {
);
if (exists) return null;
- const appointment: Appointment = {
- ...input,
- id: nanoid(),
- status: 'pendente',
- total: svc.price,
- };
-
+ const appointment: Appointment = { ...input, id: nanoid(), status: 'pendente', total: svc.price };
setState((s) => ({ ...s, appointments: [...s.appointments, appointment] }));
return appointment;
};
@@ -369,15 +326,7 @@ export const AppProvider = ({ children }: { children: React.ReactNode }) => {
return sum + price * item.qty;
}, 0);
- return {
- id: nanoid(),
- shopId,
- customerId,
- items,
- total,
- status: 'pendente',
- createdAt: new Date().toISOString(),
- };
+ return { id: nanoid(), shopId, customerId, items, total, status: 'pendente', createdAt: new Date().toISOString() };
});
setState((s) => ({ ...s, orders: [...s.orders, ...newOrders], cart: [] }));
@@ -385,17 +334,11 @@ export const AppProvider = ({ children }: { children: React.ReactNode }) => {
};
const updateAppointmentStatus: AppContextValue['updateAppointmentStatus'] = (id, status) => {
- setState((s) => ({
- ...s,
- appointments: s.appointments.map((a) => (a.id === id ? { ...a, status } : a)),
- }));
+ setState((s) => ({ ...s, appointments: s.appointments.map((a) => (a.id === id ? { ...a, status } : a)) }));
};
const updateOrderStatus: AppContextValue['updateOrderStatus'] = (id, status) => {
- setState((s) => ({
- ...s,
- orders: s.orders.map((o) => (o.id === id ? { ...o, status } : o)),
- }));
+ setState((s) => ({ ...s, orders: s.orders.map((o) => (o.id === id ? { ...o, status } : o)) }));
};
const addService: AppContextValue['addService'] = (shopId, service) => {
@@ -477,16 +420,8 @@ export const AppProvider = ({ children }: { children: React.ReactNode }) => {
};
const updateShopDetails: AppContextValue['updateShopDetails'] = async (shopId, payload) => {
- const { error } = await supabase
- .from('shops')
- .update(payload)
- .eq('id', shopId);
-
- if (error) {
- console.error('Failed to update shop details:', error);
- return;
- }
-
+ const { error } = await supabase.from('shops').update(payload).eq('id', shopId);
+ if (error) { console.error('updateShopDetails error:', error); return; }
setState((s) => ({
...s,
shops: s.shops.map((shop) => (shop.id === shopId ? { ...shop, ...payload } : shop)),
@@ -520,13 +455,20 @@ export const AppProvider = ({ children }: { children: React.ReactNode }) => {
refreshShops,
};
- // Loading Shield — mostra spinner enquanto autentica pela primeira vez
- if (loading) return (
-
- );
+ if (loading) {
+ return (
+
+ );
+ }
return {children};
};