From c4164e50a043366b3b1722dd43bfd3b702201aed Mon Sep 17 00:00:00 2001 From: 230417 <230417@epvc.pt> Date: Wed, 25 Feb 2026 09:59:12 +0000 Subject: [PATCH] supabase --- src/components/ui/Card.tsx | 5 +- src/context/AppContext.tsx | 381 ++++++++------------------- src/pages/Explore.tsx | 9 +- src/pages/ShopDetails.tsx | 13 +- web/src/components/ErrorBoundary.tsx | 54 ++++ web/src/components/ShopCard.tsx | 12 - web/src/components/layout/Header.tsx | 153 ++++------- web/src/context/AppContext.tsx | 74 +++--- web/src/lib/storage.ts | 11 +- web/src/main.tsx | 10 +- web/src/pages/Landing.tsx | 10 + web/vite.config.ts | 5 +- 12 files changed, 303 insertions(+), 434 deletions(-) create mode 100644 web/src/components/ErrorBoundary.tsx diff --git a/src/components/ui/Card.tsx b/src/components/ui/Card.tsx index 68ce083..9939df8 100644 --- a/src/components/ui/Card.tsx +++ b/src/components/ui/Card.tsx @@ -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; }; export const Card = ({ children, style }: Props) => { @@ -25,4 +25,3 @@ const styles = StyleSheet.create({ }, }); - diff --git a/src/context/AppContext.tsx b/src/context/AppContext.tsx index d5fc913..0dee7ce 100644 --- a/src/context/AppContext.tsx +++ b/src/context/AppContext.tsx @@ -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 & { shopName?: string }) => User | null; addToCart: (item: CartItem) => void; - removeFromCart: (refId: string) => void; clearCart: () => void; - createAppointment: (input: Omit) => 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) => void; - updateService: (shopId: string, service: Service) => void; - deleteService: (shopId: string, serviceId: string) => void; - addProduct: (shopId: string, product: Omit) => void; - updateProduct: (shopId: string, product: Product) => void; - deleteProduct: (shopId: string, productId: string) => void; - addBarber: (shopId: string, barber: Omit) => 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) => Promise; + updateService: (shopId: string, service: Service) => Promise; + deleteService: (shopId: string, serviceId: string) => Promise; + refreshShops: () => Promise; }; const AppContext = createContext(undefined); export const AppProvider = ({ children }: { children: React.ReactNode }) => { - const [state, setState] = useState(initialState); - const [isLoading, setIsLoading] = useState(true); + const [shops, setShops] = useState([]); + const [cart, setCart] = useState([]); + const [user, setUser] = useState(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>((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) => { + 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 {children}; }; @@ -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; -}; - +}; \ No newline at end of file diff --git a/src/pages/Explore.tsx b/src/pages/Explore.tsx index 3005442..c9d5be7 100644 --- a/src/pages/Explore.tsx +++ b/src/pages/Explore.tsx @@ -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>(); const { shops } = useApp(); return ( @@ -32,14 +34,14 @@ export default function Explore() { + + + ) + } +} diff --git a/web/src/components/ShopCard.tsx b/web/src/components/ShopCard.tsx index b1fcc1c..c8667d0 100644 --- a/web/src/components/ShopCard.tsx +++ b/web/src/components/ShopCard.tsx @@ -6,18 +6,6 @@ import { Button } from './ui/button'; export const ShopCard = ({ shop }: { shop: BarberShop }) => ( -
- {shop.imageUrl ? ( - {`Foto - ) : ( -
- )} -
diff --git a/web/src/components/layout/Header.tsx b/web/src/components/layout/Header.tsx index eac458e..2b0972d 100644 --- a/web/src/components/layout/Header.tsx +++ b/web/src/components/layout/Header.tsx @@ -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 */}