supabase
This commit is contained in:
@@ -1,9 +1,9 @@
|
||||
import React from 'react';
|
||||
import { View, StyleSheet, ViewStyle } from 'react-native';
|
||||
import { View, StyleSheet, ViewStyle, StyleProp } from 'react-native';
|
||||
|
||||
type Props = {
|
||||
children: React.ReactNode;
|
||||
style?: ViewStyle;
|
||||
style?: StyleProp<ViewStyle>;
|
||||
};
|
||||
|
||||
export const Card = ({ children, style }: Props) => {
|
||||
@@ -25,4 +25,3 @@ const styles = StyleSheet.create({
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
|
||||
@@ -1,315 +1,169 @@
|
||||
import React, { createContext, useContext, useEffect, useMemo, useState } from 'react';
|
||||
import { nanoid } from 'nanoid';
|
||||
import { Appointment, Barber, BarberShop, CartItem, Order, Product, Service, User } from '../types';
|
||||
import { mockShops, mockUsers } from '../data/mock';
|
||||
import { storage } from '../lib/storage';
|
||||
import { supabase } from '../lib/supabase';
|
||||
|
||||
type State = {
|
||||
user?: User;
|
||||
users: User[];
|
||||
shops: BarberShop[];
|
||||
appointments: Appointment[];
|
||||
orders: Order[];
|
||||
cart: CartItem[];
|
||||
};
|
||||
|
||||
type AppContextValue = State & {
|
||||
login: (email: string, password: string) => User | null;
|
||||
logout: () => void;
|
||||
register: (payload: Omit<User, 'id' | 'shopId'> & { shopName?: string }) => User | null;
|
||||
addToCart: (item: CartItem) => void;
|
||||
removeFromCart: (refId: string) => void;
|
||||
clearCart: () => void;
|
||||
createAppointment: (input: Omit<Appointment, 'id' | 'status' | 'total'>) => Appointment | null;
|
||||
placeOrder: (customerId: string, shopId?: string) => Order | null;
|
||||
updateAppointmentStatus: (id: string, status: Appointment['status']) => void;
|
||||
updateOrderStatus: (id: string, status: Order['status']) => void;
|
||||
addService: (shopId: string, service: Omit<Service, 'id'>) => void;
|
||||
updateService: (shopId: string, service: Service) => void;
|
||||
deleteService: (shopId: string, serviceId: string) => void;
|
||||
addProduct: (shopId: string, product: Omit<Product, 'id'>) => void;
|
||||
updateProduct: (shopId: string, product: Product) => void;
|
||||
deleteProduct: (shopId: string, productId: string) => void;
|
||||
addBarber: (shopId: string, barber: Omit<Barber, 'id'>) => void;
|
||||
updateBarber: (shopId: string, barber: Barber) => void;
|
||||
deleteBarber: (shopId: string, barberId: string) => void;
|
||||
};
|
||||
|
||||
const initialState: State = {
|
||||
user: undefined,
|
||||
users: mockUsers,
|
||||
shops: mockShops,
|
||||
appointments: [],
|
||||
orders: [],
|
||||
cart: [],
|
||||
addService: (shopId: string, service: Omit<Service, 'id'>) => Promise<void>;
|
||||
updateService: (shopId: string, service: Service) => Promise<void>;
|
||||
deleteService: (shopId: string, serviceId: string) => Promise<void>;
|
||||
refreshShops: () => Promise<void>;
|
||||
};
|
||||
|
||||
const AppContext = createContext<AppContextValue | undefined>(undefined);
|
||||
|
||||
export const AppProvider = ({ children }: { children: React.ReactNode }) => {
|
||||
const [state, setState] = useState<State>(initialState);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [shops, setShops] = useState<BarberShop[]>([]);
|
||||
const [cart, setCart] = useState<CartItem[]>([]);
|
||||
const [user, setUser] = useState<User | undefined>(undefined);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
// 🔹 Carregar utilizador autenticado
|
||||
useEffect(() => {
|
||||
const loadData = async () => {
|
||||
try {
|
||||
const saved = await storage.get('smart-agenda', initialState);
|
||||
setState(saved);
|
||||
} catch (err) {
|
||||
console.error('Error loading data:', err);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
const loadUser = async () => {
|
||||
const { data } = await supabase.auth.getUser();
|
||||
if (data.user) {
|
||||
setUser({
|
||||
id: data.user.id,
|
||||
email: data.user.email || '',
|
||||
role: 'barbearia', // ajustar se tiveres roles
|
||||
} as User);
|
||||
}
|
||||
};
|
||||
loadData();
|
||||
loadUser();
|
||||
}, []);
|
||||
|
||||
// 🔹 Buscar shops + services
|
||||
const refreshShops = async () => {
|
||||
console.log("A buscar shops...");
|
||||
|
||||
const { data: shopsData, error: shopsError } = await supabase
|
||||
.from('shops')
|
||||
.select('*');
|
||||
|
||||
if (shopsError) {
|
||||
console.error("Erro ao buscar shops:", shopsError);
|
||||
return;
|
||||
}
|
||||
|
||||
const { data: servicesData, error: servicesError } = await supabase
|
||||
.from('services')
|
||||
.select('*');
|
||||
|
||||
if (servicesError) {
|
||||
console.error("Erro ao buscar services:", servicesError);
|
||||
return;
|
||||
}
|
||||
|
||||
// Associar serviços às respetivas shops
|
||||
const shopsWithServices = shopsData.map((shop) => ({
|
||||
...shop,
|
||||
services: servicesData.filter((s) => s.shop_id === shop.id),
|
||||
products: [],
|
||||
barbers: [],
|
||||
}));
|
||||
|
||||
console.log("Shops carregadas:", shopsWithServices);
|
||||
|
||||
setShops(shopsWithServices);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!isLoading) {
|
||||
storage.set('smart-agenda', state);
|
||||
}
|
||||
}, [state, isLoading]);
|
||||
|
||||
const login = (email: string, password: string) => {
|
||||
const found = state.users.find((u) => u.email === email && u.password === password);
|
||||
if (found) {
|
||||
setState((s) => ({ ...s, user: found }));
|
||||
return found;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
const logout = () => setState((s) => ({ ...s, user: undefined }));
|
||||
|
||||
const register: AppContextValue['register'] = ({ shopName, ...payload }) => {
|
||||
const exists = state.users.some((u) => u.email === payload.email);
|
||||
if (exists) return null;
|
||||
|
||||
if (payload.role === 'barbearia') {
|
||||
const shopId = nanoid();
|
||||
const shop: BarberShop = {
|
||||
id: shopId,
|
||||
name: shopName || `Barbearia ${payload.name}`,
|
||||
address: 'Endereço a definir',
|
||||
rating: 0,
|
||||
barbers: [],
|
||||
services: [],
|
||||
products: [],
|
||||
};
|
||||
const user: User = { ...payload, id: nanoid(), role: 'barbearia', shopId };
|
||||
setState((s) => ({
|
||||
...s,
|
||||
user,
|
||||
users: [...s.users, user],
|
||||
shops: [...s.shops, shop],
|
||||
}));
|
||||
return user;
|
||||
}
|
||||
|
||||
const user: User = { ...payload, id: nanoid(), role: 'cliente' };
|
||||
setState((s) => ({
|
||||
...s,
|
||||
user,
|
||||
users: [...s.users, user],
|
||||
}));
|
||||
return user;
|
||||
};
|
||||
|
||||
const addToCart: AppContextValue['addToCart'] = (item) => {
|
||||
setState((s) => {
|
||||
const cart = [...s.cart];
|
||||
const idx = cart.findIndex((c) => c.refId === item.refId && c.type === item.type && c.shopId === item.shopId);
|
||||
if (idx >= 0) cart[idx].qty += item.qty;
|
||||
else cart.push(item);
|
||||
return { ...s, cart };
|
||||
});
|
||||
};
|
||||
|
||||
const removeFromCart: AppContextValue['removeFromCart'] = (refId) => {
|
||||
setState((s) => ({ ...s, cart: s.cart.filter((c) => c.refId !== refId) }));
|
||||
};
|
||||
|
||||
const clearCart = () => setState((s) => ({ ...s, cart: [] }));
|
||||
|
||||
const createAppointment: AppContextValue['createAppointment'] = (input) => {
|
||||
const shop = state.shops.find((s) => s.id === input.shopId);
|
||||
if (!shop) return null;
|
||||
const svc = shop.services.find((s) => s.id === input.serviceId);
|
||||
if (!svc) return null;
|
||||
|
||||
const exists = state.appointments.find(
|
||||
(ap) => ap.barberId === input.barberId && ap.date === input.date && ap.status !== 'cancelado'
|
||||
);
|
||||
if (exists) return null;
|
||||
|
||||
const appointment: Appointment = {
|
||||
...input,
|
||||
id: nanoid(),
|
||||
status: 'pendente',
|
||||
total: svc.price,
|
||||
const init = async () => {
|
||||
await refreshShops();
|
||||
setLoading(false);
|
||||
};
|
||||
init();
|
||||
}, []);
|
||||
|
||||
setState((s) => ({ ...s, appointments: [...s.appointments, appointment] }));
|
||||
return appointment;
|
||||
const logout = async () => {
|
||||
await supabase.auth.signOut();
|
||||
setUser(undefined);
|
||||
};
|
||||
|
||||
const placeOrder: AppContextValue['placeOrder'] = (customerId, onlyShopId) => {
|
||||
if (!state.cart.length) return null;
|
||||
const grouped = state.cart.reduce<Record<string, CartItem[]>>((acc, item) => {
|
||||
acc[item.shopId] = acc[item.shopId] || [];
|
||||
acc[item.shopId].push(item);
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
const entries = Object.entries(grouped).filter(([shopId]) => (onlyShopId ? shopId === onlyShopId : true));
|
||||
|
||||
const newOrders: Order[] = entries.map(([shopId, items]) => {
|
||||
const total = items.reduce((sum, item) => {
|
||||
const shop = state.shops.find((s) => s.id === item.shopId);
|
||||
if (!shop) return sum;
|
||||
const price =
|
||||
item.type === 'service'
|
||||
? shop.services.find((s) => s.id === item.refId)?.price ?? 0
|
||||
: shop.products.find((p) => p.id === item.refId)?.price ?? 0;
|
||||
return sum + price * item.qty;
|
||||
}, 0);
|
||||
|
||||
return {
|
||||
id: nanoid(),
|
||||
shopId,
|
||||
customerId,
|
||||
items,
|
||||
total,
|
||||
status: 'pendente',
|
||||
createdAt: new Date().toISOString(),
|
||||
};
|
||||
});
|
||||
|
||||
setState((s) => ({ ...s, orders: [...s.orders, ...newOrders], cart: [] }));
|
||||
return newOrders[0] ?? null;
|
||||
const addToCart = (item: CartItem) => {
|
||||
setCart((prev) => [...prev, item]);
|
||||
};
|
||||
|
||||
const updateAppointmentStatus: AppContextValue['updateAppointmentStatus'] = (id, status) => {
|
||||
setState((s) => ({
|
||||
...s,
|
||||
appointments: s.appointments.map((a) => (a.id === id ? { ...a, status } : a)),
|
||||
}));
|
||||
const clearCart = () => setCart([]);
|
||||
|
||||
// 🔹 CRUD SERVICES (SUPABASE REAL)
|
||||
|
||||
const addService = async (shopId: string, service: Omit<Service, 'id'>) => {
|
||||
const { error } = await supabase.from('services').insert([
|
||||
{
|
||||
shop_id: shopId,
|
||||
name: service.name,
|
||||
price: service.price,
|
||||
duration: service.duration,
|
||||
},
|
||||
]);
|
||||
|
||||
if (error) {
|
||||
console.error("Erro ao adicionar serviço:", error);
|
||||
return;
|
||||
}
|
||||
|
||||
await refreshShops();
|
||||
};
|
||||
|
||||
const updateOrderStatus: AppContextValue['updateOrderStatus'] = (id, status) => {
|
||||
setState((s) => ({
|
||||
...s,
|
||||
orders: s.orders.map((o) => (o.id === id ? { ...o, status } : o)),
|
||||
}));
|
||||
const updateService = async (shopId: string, service: Service) => {
|
||||
const { error } = await supabase
|
||||
.from('services')
|
||||
.update({
|
||||
name: service.name,
|
||||
price: service.price,
|
||||
duration: service.duration,
|
||||
})
|
||||
.eq('id', service.id);
|
||||
|
||||
if (error) {
|
||||
console.error("Erro ao atualizar serviço:", error);
|
||||
return;
|
||||
}
|
||||
|
||||
await refreshShops();
|
||||
};
|
||||
|
||||
const addService: AppContextValue['addService'] = (shopId, service) => {
|
||||
const entry: Service = { ...service, id: nanoid() };
|
||||
setState((s) => ({
|
||||
...s,
|
||||
shops: s.shops.map((shop) => (shop.id === shopId ? { ...shop, services: [...shop.services, entry] } : shop)),
|
||||
}));
|
||||
};
|
||||
const deleteService = async (shopId: string, serviceId: string) => {
|
||||
const { error } = await supabase
|
||||
.from('services')
|
||||
.delete()
|
||||
.eq('id', serviceId);
|
||||
|
||||
const updateService: AppContextValue['updateService'] = (shopId, service) => {
|
||||
setState((s) => ({
|
||||
...s,
|
||||
shops: s.shops.map((shop) =>
|
||||
shop.id === shopId ? { ...shop, services: shop.services.map((sv) => (sv.id === service.id ? service : sv)) } : shop
|
||||
),
|
||||
}));
|
||||
};
|
||||
if (error) {
|
||||
console.error("Erro ao apagar serviço:", error);
|
||||
return;
|
||||
}
|
||||
|
||||
const deleteService: AppContextValue['deleteService'] = (shopId, serviceId) => {
|
||||
setState((s) => ({
|
||||
...s,
|
||||
shops: s.shops.map((shop) =>
|
||||
shop.id === shopId ? { ...shop, services: shop.services.filter((sv) => sv.id !== serviceId) } : shop
|
||||
),
|
||||
}));
|
||||
};
|
||||
|
||||
const addProduct: AppContextValue['addProduct'] = (shopId, product) => {
|
||||
const entry: Product = { ...product, id: nanoid() };
|
||||
setState((s) => ({
|
||||
...s,
|
||||
shops: s.shops.map((shop) => (shop.id === shopId ? { ...shop, products: [...shop.products, entry] } : shop)),
|
||||
}));
|
||||
};
|
||||
|
||||
const updateProduct: AppContextValue['updateProduct'] = (shopId, product) => {
|
||||
setState((s) => ({
|
||||
...s,
|
||||
shops: s.shops.map((shop) =>
|
||||
shop.id === shopId ? { ...shop, products: shop.products.map((p) => (p.id === product.id ? product : p)) } : shop
|
||||
),
|
||||
}));
|
||||
};
|
||||
|
||||
const deleteProduct: AppContextValue['deleteProduct'] = (shopId, productId) => {
|
||||
setState((s) => ({
|
||||
...s,
|
||||
shops: s.shops.map((shop) =>
|
||||
shop.id === shopId ? { ...shop, products: shop.products.filter((p) => p.id !== productId) } : shop
|
||||
),
|
||||
}));
|
||||
};
|
||||
|
||||
const addBarber: AppContextValue['addBarber'] = (shopId, barber) => {
|
||||
const entry: Barber = { ...barber, id: nanoid() };
|
||||
setState((s) => ({
|
||||
...s,
|
||||
shops: s.shops.map((shop) => (shop.id === shopId ? { ...shop, barbers: [...shop.barbers, entry] } : shop)),
|
||||
}));
|
||||
};
|
||||
|
||||
const updateBarber: AppContextValue['updateBarber'] = (shopId, barber) => {
|
||||
setState((s) => ({
|
||||
...s,
|
||||
shops: s.shops.map((shop) =>
|
||||
shop.id === shopId ? { ...shop, barbers: shop.barbers.map((b) => (b.id === barber.id ? barber : b)) } : shop
|
||||
),
|
||||
}));
|
||||
};
|
||||
|
||||
const deleteBarber: AppContextValue['deleteBarber'] = (shopId, barberId) => {
|
||||
setState((s) => ({
|
||||
...s,
|
||||
shops: s.shops.map((shop) =>
|
||||
shop.id === shopId ? { ...shop, barbers: shop.barbers.filter((b) => b.id !== barberId) } : shop
|
||||
),
|
||||
}));
|
||||
await refreshShops();
|
||||
};
|
||||
|
||||
const value: AppContextValue = useMemo(
|
||||
() => ({
|
||||
...state,
|
||||
login,
|
||||
user,
|
||||
shops,
|
||||
cart,
|
||||
logout,
|
||||
register,
|
||||
addToCart,
|
||||
removeFromCart,
|
||||
clearCart,
|
||||
createAppointment,
|
||||
placeOrder,
|
||||
updateAppointmentStatus,
|
||||
updateOrderStatus,
|
||||
addService,
|
||||
updateService,
|
||||
deleteService,
|
||||
addProduct,
|
||||
updateProduct,
|
||||
deleteProduct,
|
||||
addBarber,
|
||||
updateBarber,
|
||||
deleteBarber,
|
||||
refreshShops,
|
||||
}),
|
||||
[state]
|
||||
[user, shops, cart]
|
||||
);
|
||||
|
||||
if (isLoading) {
|
||||
return null; // Ou um componente de loading
|
||||
}
|
||||
if (loading) return null;
|
||||
|
||||
return <AppContext.Provider value={value}>{children}</AppContext.Provider>;
|
||||
};
|
||||
@@ -318,5 +172,4 @@ export const useApp = () => {
|
||||
const ctx = useContext(AppContext);
|
||||
if (!ctx) throw new Error('useApp deve ser usado dentro de AppProvider');
|
||||
return ctx;
|
||||
};
|
||||
|
||||
};
|
||||
@@ -1,14 +1,16 @@
|
||||
import React from 'react';
|
||||
import { View, Text, StyleSheet, ScrollView, FlatList } from 'react-native';
|
||||
import { useNavigation } from '@react-navigation/native';
|
||||
import { NativeStackNavigationProp } from '@react-navigation/native-stack';
|
||||
import { useApp } from '../context/AppContext';
|
||||
import { Card } from '../components/ui/Card';
|
||||
import { Button } from '../components/ui/Button';
|
||||
import { Badge } from '../components/ui/Badge';
|
||||
import { currency } from '../lib/format';
|
||||
import { RootStackParamList } from '../navigation/types';
|
||||
|
||||
export default function Explore() {
|
||||
const navigation = useNavigation();
|
||||
const navigation = useNavigation<NativeStackNavigationProp<RootStackParamList>>();
|
||||
const { shops } = useApp();
|
||||
|
||||
return (
|
||||
@@ -32,14 +34,14 @@ export default function Explore() {
|
||||
</View>
|
||||
<View style={styles.buttons}>
|
||||
<Button
|
||||
onPress={() => navigation.navigate('ShopDetails' as never, { shopId: shop.id } as never)}
|
||||
onPress={() => navigation.navigate('ShopDetails', { shopId: shop.id })}
|
||||
variant="outline"
|
||||
style={styles.button}
|
||||
>
|
||||
Ver detalhes
|
||||
</Button>
|
||||
<Button
|
||||
onPress={() => navigation.navigate('Booking' as never, { shopId: shop.id } as never)}
|
||||
onPress={() => navigation.navigate('Booking', { shopId: shop.id })}
|
||||
style={styles.button}
|
||||
>
|
||||
Agendar
|
||||
@@ -106,4 +108,3 @@ const styles = StyleSheet.create({
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
|
||||
@@ -1,16 +1,18 @@
|
||||
import React, { useState, useMemo } from 'react';
|
||||
import { View, Text, StyleSheet, ScrollView, TouchableOpacity } from 'react-native';
|
||||
import { useRoute, useNavigation } from '@react-navigation/native';
|
||||
import { useRoute, useNavigation, RouteProp } from '@react-navigation/native';
|
||||
import { NativeStackNavigationProp } from '@react-navigation/native-stack';
|
||||
import { useApp } from '../context/AppContext';
|
||||
import { Card } from '../components/ui/Card';
|
||||
import { Button } from '../components/ui/Button';
|
||||
import { Badge } from '../components/ui/Badge';
|
||||
import { currency } from '../lib/format';
|
||||
import { RootStackParamList } from '../navigation/types';
|
||||
|
||||
export default function ShopDetails() {
|
||||
const route = useRoute();
|
||||
const navigation = useNavigation();
|
||||
const { shopId } = route.params as { shopId: string };
|
||||
const route = useRoute<RouteProp<RootStackParamList, 'ShopDetails'>>();
|
||||
const navigation = useNavigation<NativeStackNavigationProp<RootStackParamList>>();
|
||||
const { shopId } = route.params;
|
||||
const { shops, addToCart } = useApp();
|
||||
const shop = useMemo(() => shops.find((s) => s.id === shopId), [shops, shopId]);
|
||||
const [tab, setTab] = useState<'servicos' | 'produtos'>('servicos');
|
||||
@@ -29,7 +31,7 @@ export default function ShopDetails() {
|
||||
<Text style={styles.title}>{shop.name}</Text>
|
||||
<Text style={styles.address}>{shop.address}</Text>
|
||||
<Button
|
||||
onPress={() => navigation.navigate('Booking' as never, { shopId: shop.id } as never)}
|
||||
onPress={() => navigation.navigate('Booking', { shopId: shop.id })}
|
||||
style={styles.bookButton}
|
||||
>
|
||||
Agendar
|
||||
@@ -181,4 +183,3 @@ const styles = StyleSheet.create({
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
|
||||
54
web/src/components/ErrorBoundary.tsx
Normal file
54
web/src/components/ErrorBoundary.tsx
Normal file
@@ -0,0 +1,54 @@
|
||||
import React from 'react'
|
||||
|
||||
type ErrorBoundaryState = {
|
||||
hasError: boolean
|
||||
message?: string
|
||||
}
|
||||
|
||||
export class ErrorBoundary extends React.Component<React.PropsWithChildren, ErrorBoundaryState> {
|
||||
state: ErrorBoundaryState = { hasError: false }
|
||||
|
||||
static getDerivedStateFromError(error: Error): ErrorBoundaryState {
|
||||
return { hasError: true, message: error.message }
|
||||
}
|
||||
|
||||
componentDidCatch(error: Error) {
|
||||
console.error('App runtime error:', error)
|
||||
}
|
||||
|
||||
private handleReset = () => {
|
||||
try {
|
||||
localStorage.removeItem('smart-agenda')
|
||||
} catch (err) {
|
||||
console.error('Failed to clear local storage', err)
|
||||
}
|
||||
window.location.reload()
|
||||
}
|
||||
|
||||
render() {
|
||||
if (!this.state.hasError) return this.props.children
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-slate-50 text-slate-900 px-4 py-10">
|
||||
<div className="mx-auto max-w-xl rounded-xl border border-slate-200 bg-white p-6 shadow-sm space-y-4">
|
||||
<h1 className="text-xl font-semibold">A aplicação encontrou um erro</h1>
|
||||
<p className="text-sm text-slate-600">
|
||||
Vamos recuperar o estado local e recarregar a página.
|
||||
</p>
|
||||
{this.state.message ? (
|
||||
<pre className="rounded-md bg-slate-100 p-3 text-xs text-slate-700 overflow-auto">
|
||||
{this.state.message}
|
||||
</pre>
|
||||
) : null}
|
||||
<button
|
||||
type="button"
|
||||
onClick={this.handleReset}
|
||||
className="inline-flex items-center justify-center rounded-md bg-indigo-600 px-4 py-2 text-sm font-medium text-white hover:bg-indigo-700"
|
||||
>
|
||||
Limpar dados locais e recarregar
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -6,18 +6,6 @@ import { Button } from './ui/button';
|
||||
|
||||
export const ShopCard = ({ shop }: { shop: BarberShop }) => (
|
||||
<Card hover className="p-6 space-y-4 group">
|
||||
<div className="relative overflow-hidden rounded-xl border border-slate-100 bg-slate-100">
|
||||
{shop.imageUrl ? (
|
||||
<img
|
||||
src={shop.imageUrl}
|
||||
alt={`Foto de ${shop.name}`}
|
||||
className="h-36 w-full object-cover"
|
||||
loading="lazy"
|
||||
/>
|
||||
) : (
|
||||
<div className="h-36 w-full bg-gradient-to-br from-slate-900 via-slate-700 to-slate-500" />
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="space-y-2 flex-1">
|
||||
<div className="flex items-center gap-2">
|
||||
|
||||
@@ -1,41 +1,17 @@
|
||||
import { Link, useNavigate } from 'react-router-dom'
|
||||
import { MapPin, ShoppingCart, User, LogOut, Menu, X } from 'lucide-react'
|
||||
import { useApp } from '../../context/AppContext'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { supabase } from '../../lib/supabase'
|
||||
import { signOut } from '../../lib/auth'
|
||||
import { useState } from 'react'
|
||||
|
||||
export const Header = () => {
|
||||
const { cart, user } = useApp()
|
||||
const { user, cart, logout } = useApp()
|
||||
const navigate = useNavigate()
|
||||
const [mobileMenuOpen, setMobileMenuOpen] = useState(false)
|
||||
|
||||
// ✅ sessão Supabase (fonte única)
|
||||
const [isAuthed, setIsAuthed] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
let mounted = true
|
||||
|
||||
;(async () => {
|
||||
const { data } = await supabase.auth.getSession()
|
||||
if (!mounted) return
|
||||
setIsAuthed(!!data.session)
|
||||
})()
|
||||
|
||||
const { data: sub } = supabase.auth.onAuthStateChange((_event, session) => {
|
||||
setIsAuthed(!!session)
|
||||
})
|
||||
|
||||
return () => {
|
||||
mounted = false
|
||||
sub.subscription.unsubscribe()
|
||||
}
|
||||
}, [])
|
||||
|
||||
const handleLogout = async () => {
|
||||
await signOut()
|
||||
const handleLogout = () => {
|
||||
logout()
|
||||
navigate('/')
|
||||
setMobileMenuOpen(false)
|
||||
navigate('/login', { replace: true })
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -51,48 +27,35 @@ export const Header = () => {
|
||||
|
||||
{/* Desktop Navigation */}
|
||||
<nav className="hidden md:flex items-center gap-4">
|
||||
{user?.role !== 'barbearia' && (
|
||||
<>
|
||||
<Link
|
||||
to="/explorar"
|
||||
className="flex items-center gap-1.5 text-sm font-medium text-slate-700 hover:text-indigo-600 transition-colors px-3 py-1.5 rounded-lg hover:bg-indigo-50"
|
||||
>
|
||||
<MapPin size={16} />
|
||||
<span>Barbearias</span>
|
||||
</Link>
|
||||
<Link
|
||||
to="/explorar"
|
||||
className="flex items-center gap-1.5 text-sm font-medium text-slate-700 hover:text-indigo-600 transition-colors px-3 py-1.5 rounded-lg hover:bg-indigo-50"
|
||||
>
|
||||
<MapPin size={16} />
|
||||
<span>Barbearias</span>
|
||||
</Link>
|
||||
|
||||
<Link
|
||||
to="/carrinho"
|
||||
className="relative text-slate-700 hover:text-indigo-600 transition-colors p-2 rounded-lg hover:bg-indigo-50"
|
||||
>
|
||||
<ShoppingCart size={18} />
|
||||
{cart.length > 0 && (
|
||||
<span className="absolute -right-1 -top-1 rounded-full bg-gradient-to-r from-indigo-500 to-blue-600 px-1.5 py-0.5 text-[10px] font-bold text-white shadow-sm min-w-[18px] text-center">
|
||||
{cart.length}
|
||||
</span>
|
||||
)}
|
||||
</Link>
|
||||
</>
|
||||
)}
|
||||
<Link
|
||||
to="/carrinho"
|
||||
className="relative text-slate-700 hover:text-indigo-600 transition-colors p-2 rounded-lg hover:bg-indigo-50"
|
||||
>
|
||||
<ShoppingCart size={18} />
|
||||
{cart.length > 0 && (
|
||||
<span className="absolute -right-1 -top-1 rounded-full bg-gradient-to-r from-indigo-500 to-blue-600 px-1.5 py-0.5 text-[10px] font-bold text-white shadow-sm min-w-[18px] text-center">
|
||||
{cart.length}
|
||||
</span>
|
||||
)}
|
||||
</Link>
|
||||
|
||||
{isAuthed ? (
|
||||
{user ? (
|
||||
<div className="flex items-center gap-2">
|
||||
{user?.role === 'barbearia' && (
|
||||
<button
|
||||
onClick={() => navigate('/painel')}
|
||||
className="flex items-center gap-1.5 text-sm font-medium text-slate-700 hover:text-indigo-600 transition-colors px-3 py-1.5 rounded-lg hover:bg-indigo-50"
|
||||
type="button"
|
||||
>
|
||||
<span>Painel</span>
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
onClick={() => navigate('/perfil')}
|
||||
onClick={() => navigate(user.role === 'barbearia' ? '/painel' : '/perfil')}
|
||||
className="flex items-center gap-1.5 text-sm font-medium text-slate-700 hover:text-indigo-600 transition-colors px-3 py-1.5 rounded-lg hover:bg-indigo-50"
|
||||
type="button"
|
||||
>
|
||||
<User size={16} />
|
||||
<span className="max-w-[120px] truncate">Perfil</span>
|
||||
<span className="max-w-[120px] truncate">{user.name}</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
@@ -128,57 +91,41 @@ export const Header = () => {
|
||||
{mobileMenuOpen && (
|
||||
<div className="md:hidden border-t border-slate-200/60 bg-white/95 backdrop-blur-md animate-in slide-in-from-top">
|
||||
<nav className="px-4 py-3 space-y-2">
|
||||
{user?.role !== 'barbearia' && (
|
||||
<>
|
||||
<Link
|
||||
to="/explorar"
|
||||
onClick={() => setMobileMenuOpen(false)}
|
||||
className="flex items-center gap-2 text-sm font-medium text-slate-700 hover:text-amber-600 transition-colors px-3 py-2 rounded-lg hover:bg-amber-50"
|
||||
>
|
||||
<MapPin size={16} />
|
||||
Barbearias
|
||||
</Link>
|
||||
<Link
|
||||
to="/explorar"
|
||||
onClick={() => setMobileMenuOpen(false)}
|
||||
className="flex items-center gap-2 text-sm font-medium text-slate-700 hover:text-amber-600 transition-colors px-3 py-2 rounded-lg hover:bg-amber-50"
|
||||
>
|
||||
<MapPin size={16} />
|
||||
Barbearias
|
||||
</Link>
|
||||
|
||||
<Link
|
||||
to="/carrinho"
|
||||
onClick={() => setMobileMenuOpen(false)}
|
||||
className="flex items-center gap-2 text-sm font-medium text-slate-700 hover:text-amber-600 transition-colors px-3 py-2 rounded-lg hover:bg-amber-50"
|
||||
>
|
||||
<ShoppingCart size={16} />
|
||||
Carrinho
|
||||
{cart.length > 0 && (
|
||||
<span className="ml-auto rounded-full bg-amber-500 px-2 py-0.5 text-[10px] font-bold text-white">
|
||||
{cart.length}
|
||||
</span>
|
||||
)}
|
||||
</Link>
|
||||
</>
|
||||
)}
|
||||
<Link
|
||||
to="/carrinho"
|
||||
onClick={() => setMobileMenuOpen(false)}
|
||||
className="flex items-center gap-2 text-sm font-medium text-slate-700 hover:text-amber-600 transition-colors px-3 py-2 rounded-lg hover:bg-amber-50"
|
||||
>
|
||||
<ShoppingCart size={16} />
|
||||
Carrinho
|
||||
{cart.length > 0 && (
|
||||
<span className="ml-auto rounded-full bg-amber-500 px-2 py-0.5 text-[10px] font-bold text-white">
|
||||
{cart.length}
|
||||
</span>
|
||||
)}
|
||||
</Link>
|
||||
|
||||
{isAuthed ? (
|
||||
{user ? (
|
||||
<>
|
||||
{user?.role === 'barbearia' && (
|
||||
<button
|
||||
onClick={() => {
|
||||
navigate('/painel')
|
||||
setMobileMenuOpen(false)
|
||||
}}
|
||||
className="w-full flex items-center gap-2 text-sm font-medium text-slate-700 hover:text-amber-600 transition-colors px-3 py-2 rounded-lg hover:bg-amber-50 text-left"
|
||||
type="button"
|
||||
>
|
||||
Painel
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
onClick={() => {
|
||||
navigate('/perfil')
|
||||
navigate(user.role === 'barbearia' ? '/painel' : '/perfil')
|
||||
setMobileMenuOpen(false)
|
||||
}}
|
||||
className="w-full flex items-center gap-2 text-sm font-medium text-slate-700 hover:text-amber-600 transition-colors px-3 py-2 rounded-lg hover:bg-amber-50 text-left"
|
||||
type="button"
|
||||
>
|
||||
<User size={16} />
|
||||
Perfil
|
||||
{user.name}
|
||||
</button>
|
||||
|
||||
<button
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { createContext, useContext, useEffect, useMemo, useState } from 'react';
|
||||
import React, { createContext, useContext, useEffect, useState } from 'react';
|
||||
import { nanoid } from 'nanoid';
|
||||
import { Appointment, Barber, BarberShop, CartItem, Order, Product, Service, User } from '../types';
|
||||
import { mockShops, mockUsers } from '../data/mock';
|
||||
@@ -53,11 +53,26 @@ const AppContext = createContext<AppContextValue | undefined>(undefined);
|
||||
|
||||
export const AppProvider = ({ children }: { children: React.ReactNode }) => {
|
||||
const [state, setState] = useState<State>(() => {
|
||||
const stored = storage.get('smart-agenda', initialState) as State;
|
||||
const stored = storage.get<Partial<State> | null>('smart-agenda', null);
|
||||
const safeStored = stored && typeof stored === 'object' ? stored : {};
|
||||
const safeUsers = Array.isArray(safeStored.users) ? safeStored.users : initialState.users;
|
||||
const safeShops = Array.isArray(safeStored.shops) ? safeStored.shops : initialState.shops;
|
||||
const safeAppointments = Array.isArray(safeStored.appointments) ? safeStored.appointments : initialState.appointments;
|
||||
const safeOrders = Array.isArray(safeStored.orders) ? safeStored.orders : initialState.orders;
|
||||
const safeCart = Array.isArray(safeStored.cart) ? safeStored.cart : initialState.cart;
|
||||
const safeFavorites = Array.isArray(safeStored.favorites) ? safeStored.favorites : initialState.favorites;
|
||||
const safeUser = safeStored.user && typeof safeStored.user === 'object' ? safeStored.user : undefined;
|
||||
|
||||
return {
|
||||
...initialState,
|
||||
...stored,
|
||||
favorites: stored?.favorites ?? [],
|
||||
...safeStored,
|
||||
user: safeUser,
|
||||
users: safeUsers,
|
||||
shops: safeShops,
|
||||
appointments: safeAppointments,
|
||||
orders: safeOrders,
|
||||
cart: safeCart,
|
||||
favorites: safeFavorites,
|
||||
};
|
||||
});
|
||||
|
||||
@@ -380,33 +395,30 @@ export const AppProvider = ({ children }: { children: React.ReactNode }) => {
|
||||
}));
|
||||
};
|
||||
|
||||
const value: AppContextValue = useMemo(
|
||||
() => ({
|
||||
...state,
|
||||
login,
|
||||
logout,
|
||||
toggleFavorite,
|
||||
isFavorite,
|
||||
register,
|
||||
addToCart,
|
||||
removeFromCart,
|
||||
clearCart,
|
||||
createAppointment,
|
||||
placeOrder,
|
||||
updateAppointmentStatus,
|
||||
updateOrderStatus,
|
||||
addService,
|
||||
updateService,
|
||||
deleteService,
|
||||
addProduct,
|
||||
updateProduct,
|
||||
deleteProduct,
|
||||
addBarber,
|
||||
updateBarber,
|
||||
deleteBarber,
|
||||
}),
|
||||
[state]
|
||||
);
|
||||
const value: AppContextValue = {
|
||||
...state,
|
||||
login,
|
||||
logout,
|
||||
toggleFavorite,
|
||||
isFavorite,
|
||||
register,
|
||||
addToCart,
|
||||
removeFromCart,
|
||||
clearCart,
|
||||
createAppointment,
|
||||
placeOrder,
|
||||
updateAppointmentStatus,
|
||||
updateOrderStatus,
|
||||
addService,
|
||||
updateService,
|
||||
deleteService,
|
||||
addProduct,
|
||||
updateProduct,
|
||||
deleteProduct,
|
||||
addBarber,
|
||||
updateBarber,
|
||||
deleteBarber,
|
||||
};
|
||||
|
||||
return <AppContext.Provider value={value}>{children}</AppContext.Provider>;
|
||||
};
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
export const storage = {
|
||||
get<T>(key: string, fallback: T): T {
|
||||
const raw = localStorage.getItem(key);
|
||||
if (!raw) return fallback;
|
||||
try {
|
||||
const raw = localStorage.getItem(key);
|
||||
if (!raw) return fallback;
|
||||
return JSON.parse(raw) as T;
|
||||
} catch (err) {
|
||||
console.error('storage parse error', err);
|
||||
@@ -10,9 +10,12 @@ export const storage = {
|
||||
}
|
||||
},
|
||||
set<T>(key: string, value: T) {
|
||||
localStorage.setItem(key, JSON.stringify(value));
|
||||
try {
|
||||
localStorage.setItem(key, JSON.stringify(value));
|
||||
} catch (err) {
|
||||
console.error('storage write error', err);
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -3,16 +3,18 @@ import ReactDOM from 'react-dom/client';
|
||||
import App from './App';
|
||||
import './index.css';
|
||||
import { AppProvider } from './context/AppContext';
|
||||
import { ErrorBoundary } from './components/ErrorBoundary';
|
||||
|
||||
ReactDOM.createRoot(document.getElementById('root')!).render(
|
||||
<React.StrictMode>
|
||||
<AppProvider>
|
||||
<App />
|
||||
</AppProvider>
|
||||
<ErrorBoundary>
|
||||
<AppProvider>
|
||||
<App />
|
||||
</AppProvider>
|
||||
</ErrorBoundary>
|
||||
</React.StrictMode>
|
||||
);
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -8,11 +8,20 @@ import {
|
||||
ArrowRight, Star, Quote, Scissors, MapPin,
|
||||
Zap, Smartphone, Globe
|
||||
} from 'lucide-react';
|
||||
import { useEffect } from 'react';
|
||||
import { useApp } from '../context/AppContext';
|
||||
import { mockShops } from '../data/mock';
|
||||
|
||||
export default function Landing() {
|
||||
const { user } = useApp();
|
||||
const navigate = useNavigate();
|
||||
|
||||
useEffect(() => {
|
||||
if (!user) return;
|
||||
const target = user.role === 'barbearia' ? '/painel' : '/explorar';
|
||||
navigate(target, { replace: true });
|
||||
}, [user, navigate]);
|
||||
|
||||
const featuredShops = mockShops.slice(0, 3);
|
||||
|
||||
return (
|
||||
@@ -319,3 +328,4 @@ export default function Landing() {
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -5,12 +5,11 @@ export default defineConfig({
|
||||
plugins: [react()],
|
||||
server: {
|
||||
port: 5173,
|
||||
host: '0.0.0.0', // Permite acesso de outras interfaces
|
||||
host: '0.0.0.0',
|
||||
strictPort: false, // Tenta outra porta se 5173 estiver ocupada
|
||||
allowedHosts: ['smartagenda.epvc.pt'],
|
||||
allowedHosts: ['smartagenda.epvc.pt', 'localhost', '127.0.0.1'],
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user