Add RequireAuth and events module for user authentication and event management

This commit is contained in:
2026-01-21 09:52:34 +00:00
parent 9cd34d3ff1
commit 2d1f09154f
7 changed files with 242 additions and 113 deletions

View File

@@ -1,23 +1,6 @@
import { RouterProvider } from 'react-router-dom';
import { router } from './routes';
const App = () => <RouterProvider router={router} />;
import { useEffect } from 'react'
import { signIn } from './lib/auth'
useEffect(() => {
;(async () => {
try {
await signIn('TEU_EMAIL', 'TUA_PASSWORD')
console.log('LOGIN OK')
} catch (e: any) {
console.log('LOGIN ERRO:', e.message)
}
})()
}, [])
export default App;
import { RouterProvider } from 'react-router-dom'
import { router } from './routes'
export default function App() {
return <RouterProvider router={router} />
}

View File

@@ -0,0 +1,20 @@
import { useEffect, useState } from 'react'
import { Navigate } from 'react-router-dom'
import { getUser } from '../../lib/auth'
export function RequireAuth({ children }: { children: React.ReactNode }) {
const [loading, setLoading] = useState(true)
const [ok, setOk] = useState(false)
useEffect(() => {
;(async () => {
const user = await getUser()
setOk(!!user)
setLoading(false)
})()
}, [])
if (loading) return null
if (!ok) return <Navigate to="/login" replace />
return <>{children}</>
}

View File

@@ -1,19 +1,13 @@
import { supabase } from './supabase'
export async function signIn(email: string, password: string) {
const { data, error } = await supabase.auth.signInWithPassword({
email,
password,
})
const { data, error } = await supabase.auth.signInWithPassword({ email, password })
if (error) throw error
return data
}
export async function signUp(email: string, password: string) {
const { data, error } = await supabase.auth.signUp({
email,
password,
})
const { data, error } = await supabase.auth.signUp({ email, password })
if (error) throw error
return data
}
@@ -24,6 +18,7 @@ export async function signOut() {
}
export async function getUser() {
const { data } = await supabase.auth.getUser()
const { data, error } = await supabase.auth.getUser()
if (error) return null
return data.user
}

26
web/src/lib/events.ts Normal file
View File

@@ -0,0 +1,26 @@
import { supabase } from './supabase'
import { getUser } from './auth'
export type EventRow = {
id: number
title: string
description: string | null
date: string
user_id: string
}
// LISTAR eventos do utilizador logado
export async function listEvents() {
const user = await getUser()
if (!user) {
throw new Error('Utilizador não autenticado')
}
const { data, error } = await supabase
.from('events')
.select('*')
.order('date', { ascending: true })
if (error) throw error
return (data ?? []) as EventRow[]
}

View File

@@ -1,34 +1,38 @@
import { FormEvent, useEffect, useState } from 'react';
import { useNavigate, Link } from 'react-router-dom';
import { Input } from '../components/ui/input';
import { Button } from '../components/ui/button';
import { Card } from '../components/ui/card';
import { useApp } from '../context/AppContext';
import { LogIn, Mail, Lock } from 'lucide-react';
import { FormEvent, useEffect, useState } from 'react'
import { useNavigate, Link } from 'react-router-dom'
import { Input } from '../components/ui/input'
import { Button } from '../components/ui/button'
import { Card } from '../components/ui/card'
import { LogIn } from 'lucide-react'
import { signIn, getUser } from '../lib/auth'
export default function AuthLogin() {
const [email, setEmail] = useState('cliente@demo.com');
const [password, setPassword] = useState('123');
const [error, setError] = useState('');
const { login, user } = useApp();
const navigate = useNavigate();
const [email, setEmail] = useState('cliente@demo.com')
const [password, setPassword] = useState('123')
const [error, setError] = useState('')
const navigate = useNavigate()
// Se já estiver logado, redireciona
useEffect(() => {
if (!user) return;
const target = user.role === 'barbearia' ? '/painel' : '/explorar';
navigate(target, { replace: true });
}, [user, navigate]);
;(async () => {
const user = await getUser()
if (user) {
navigate('/explorar', { replace: true })
}
})()
}, [navigate])
const onSubmit = (e: FormEvent) => {
e.preventDefault();
const ok = login(email, password);
if (!ok) {
setError('Credenciais inválidas');
} else {
const target = user?.role === 'barbearia' ? '/painel' : '/explorar';
navigate(target);
async function handleLogin(e: FormEvent) {
e.preventDefault()
setError('')
try {
await signIn(email, password)
navigate('/explorar', { replace: true })
} catch {
setError('Credenciais inválidas')
}
};
}
return (
<div className="max-w-md mx-auto py-8">
@@ -37,8 +41,12 @@ export default function AuthLogin() {
<div className="inline-flex p-3 bg-gradient-to-br from-amber-500 to-amber-600 rounded-xl text-white shadow-lg mb-2">
<LogIn size={24} />
</div>
<h1 className="text-2xl font-bold text-slate-900">Bem-vindo de volta</h1>
<p className="text-sm text-slate-600">Entre na sua conta para continuar</p>
<h1 className="text-2xl font-bold text-slate-900">
Bem-vindo de volta
</h1>
<p className="text-sm text-slate-600">
Entre na sua conta para continuar
</p>
</div>
<div className="bg-amber-50 border border-amber-200 rounded-lg p-3 text-xs text-amber-800">
@@ -47,31 +55,32 @@ export default function AuthLogin() {
<p>Barbearia: barber@demo.com / 123</p>
</div>
<form className="space-y-4" onSubmit={onSubmit}>
<form className="space-y-4" onSubmit={handleLogin}>
<Input
label="Email"
type="email"
value={email}
onChange={(e) => {
setEmail(e.target.value);
setError('');
setEmail(e.target.value)
setError('')
}}
required
error={error ? undefined : undefined}
placeholder="seu@email.com"
/>
<Input
label="Senha"
type="password"
value={password}
onChange={(e) => {
setPassword(e.target.value);
setError('');
setPassword(e.target.value)
setError('')
}}
required
error={error}
placeholder="••••••••"
/>
<Button type="submit" className="w-full" size="lg">
Entrar
</Button>
@@ -80,17 +89,15 @@ export default function AuthLogin() {
<div className="text-center pt-4 border-t border-slate-200">
<p className="text-sm text-slate-600">
Não tem conta?{' '}
<Link to="/registo" className="text-amber-700 font-semibold hover:text-amber-800 transition-colors">
<Link
to="/registo"
className="text-amber-700 font-semibold hover:text-amber-800 transition-colors"
>
Criar conta
</Link>
</p>
</div>
</Card>
</div>
);
)
}

View File

@@ -1,35 +1,57 @@
import { Card } from '../components/ui/card';
import { Badge } from '../components/ui/badge';
import { currency } from '../lib/format';
import { useApp } from '../context/AppContext';
import { Calendar, ShoppingBag, User, Clock } from 'lucide-react';
import { useEffect, useState } from 'react'
import { Card } from '../components/ui/card'
import { Badge } from '../components/ui/badge'
import { currency } from '../lib/format'
import { useApp } from '../context/AppContext'
import { Calendar, ShoppingBag, User, Clock } from 'lucide-react'
import { listEvents, type EventRow } from '../lib/events'
const statusColor: Record<string, 'amber' | 'green' | 'slate' | 'red'> = {
pendente: 'amber',
confirmado: 'green',
concluido: 'green',
cancelado: 'red',
};
}
const statusLabel: Record<string, string> = {
pendente: 'Pendente',
confirmado: 'Confirmado',
concluido: 'Concluído',
cancelado: 'Cancelado',
};
}
export default function Profile() {
const { user, appointments, orders, shops } = useApp();
const { user, appointments, orders, shops } = useApp()
// ✅ Supabase events
const [events, setEvents] = useState<EventRow[]>([])
const [loadingEvents, setLoadingEvents] = useState(true)
const [eventsError, setEventsError] = useState('')
useEffect(() => {
;(async () => {
try {
setEventsError('')
const data = await listEvents()
setEvents(data)
} catch (e: any) {
setEventsError(e.message || 'Erro ao carregar eventos')
} finally {
setLoadingEvents(false)
}
})()
}, [])
if (!user) {
return (
<div className="text-center py-12">
<p className="text-slate-600">Faça login para ver o perfil.</p>
</div>
);
)
}
const myAppointments = appointments.filter((a) => a.customerId === user.id);
const myOrders = orders.filter((o) => o.customerId === user.id);
const myAppointments = appointments.filter((a) => a.customerId === user.id)
const myOrders = orders.filter((o) => o.customerId === user.id)
return (
<div className="space-y-8">
@@ -49,6 +71,56 @@ export default function Profile() {
</div>
</Card>
{/* ✅ Supabase Events Section */}
<section className="space-y-4">
<div className="flex items-center gap-2">
<Calendar size={20} className="text-amber-600" />
<h2 className="text-xl font-bold text-slate-900">Eventos (Supabase)</h2>
<Badge color="slate" variant="soft">
{loadingEvents ? '…' : events.length}
</Badge>
</div>
{loadingEvents ? (
<Card className="p-8 text-center">
<p className="text-slate-600 font-medium">A carregar eventos</p>
</Card>
) : eventsError ? (
<Card className="p-8 text-center">
<p className="text-rose-600 font-medium">{eventsError}</p>
<p className="text-sm text-slate-500 mt-1">
Confirma que estás logado e que as policies RLS estão corretas.
</p>
</Card>
) : events.length === 0 ? (
<Card className="p-8 text-center">
<Calendar size={48} className="mx-auto text-slate-300 mb-3" />
<p className="text-slate-600 font-medium">Nenhum evento ainda</p>
<p className="text-sm text-slate-500 mt-1">
Cria um evento para aparecer aqui.
</p>
</Card>
) : (
<div className="space-y-3">
{events.map((ev) => (
<Card key={ev.id} hover className="p-5">
<div className="flex items-start justify-between gap-4">
<div className="flex-1 space-y-2">
<h3 className="font-bold text-slate-900">{ev.title}</h3>
{ev.description && (
<p className="text-sm text-slate-600">{ev.description}</p>
)}
<p className="text-xs text-slate-500">
{new Date(ev.date).toLocaleString()}
</p>
</div>
</div>
</Card>
))}
</div>
)}
</section>
{/* Appointments Section */}
<section className="space-y-4">
<div className="flex items-center gap-2">
@@ -65,8 +137,8 @@ export default function Profile() {
) : (
<div className="space-y-3">
{myAppointments.map((a) => {
const shop = shops.find((s) => s.id === a.shopId);
const service = shop?.services.find((s) => s.id === a.serviceId);
const shop = shops.find((s) => s.id === a.shopId)
const service = shop?.services.find((s) => s.id === a.serviceId)
return (
<Card key={a.id} hover className="p-5">
<div className="flex items-start justify-between gap-4">
@@ -90,7 +162,7 @@ export default function Profile() {
</div>
</div>
</Card>
);
)
})}
</div>
)}
@@ -112,7 +184,7 @@ export default function Profile() {
) : (
<div className="space-y-3">
{myOrders.map((o) => {
const shop = shops.find((s) => s.id === o.shopId);
const shop = shops.find((s) => s.id === o.shopId)
return (
<Card key={o.id} hover className="p-5">
<div className="flex items-start justify-between gap-4">
@@ -132,23 +204,20 @@ export default function Profile() {
minute: '2-digit',
})}
</p>
<p className="text-xs text-slate-600">{o.items.length} {o.items.length === 1 ? 'item' : 'itens'}</p>
<p className="text-xs text-slate-600">
{o.items.length} {o.items.length === 1 ? 'item' : 'itens'}
</p>
</div>
<div className="text-right">
<p className="text-lg font-bold text-amber-600">{currency(o.total)}</p>
</div>
</div>
</Card>
);
)
})}
</div>
)}
</section>
</div>
);
)
}

View File

@@ -1,14 +1,16 @@
import { createBrowserRouter } from 'react-router-dom';
import { Shell } from './components/layout/Shell';
import Landing from './pages/Landing';
import AuthLogin from './pages/AuthLogin';
import AuthRegister from './pages/AuthRegister';
import Explore from './pages/Explore';
import ShopDetails from './pages/ShopDetails';
import Booking from './pages/Booking';
import Cart from './pages/Cart';
import Profile from './pages/Profile';
import Dashboard from './pages/Dashboard';
import { createBrowserRouter } from 'react-router-dom'
import { Shell } from './components/layout/Shell'
import { RequireAuth } from './components/auth/RequireAuth'
import Landing from './pages/Landing'
import AuthLogin from './pages/AuthLogin'
import AuthRegister from './pages/AuthRegister'
import Explore from './pages/Explore'
import ShopDetails from './pages/ShopDetails'
import Booking from './pages/Booking'
import Cart from './pages/Cart'
import Profile from './pages/Profile'
import Dashboard from './pages/Dashboard'
export const router = createBrowserRouter([
{
@@ -19,13 +21,40 @@ export const router = createBrowserRouter([
{ path: '/registo', element: <AuthRegister /> },
{ path: '/explorar', element: <Explore /> },
{ path: '/barbearia/:id', element: <ShopDetails /> },
{ path: '/agendar/:id', element: <Booking /> },
{ path: '/carrinho', element: <Cart /> },
{ path: '/perfil', element: <Profile /> },
{ path: '/painel', element: <Dashboard /> },
// ✅ PROTEGIDAS (precisam de login)
{
path: '/agendar/:id',
element: (
<RequireAuth>
<Booking />
</RequireAuth>
),
},
{
path: '/carrinho',
element: (
<RequireAuth>
<Cart />
</RequireAuth>
),
},
{
path: '/perfil',
element: (
<RequireAuth>
<Profile />
</RequireAuth>
),
},
{
path: '/painel',
element: (
<RequireAuth>
<Dashboard />
</RequireAuth>
),
},
],
},
]);
])