This commit is contained in:
2026-02-25 09:59:12 +00:00
parent a895f83db7
commit c4164e50a0
12 changed files with 303 additions and 434 deletions

View 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>
)
}
}

View File

@@ -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">

View File

@@ -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

View File

@@ -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>;
};

View File

@@ -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);
}
},
};

View File

@@ -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>
);

View File

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

View File

@@ -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'],
},
});