Add RequireAuth and events module for user authentication and event management
This commit is contained in:
@@ -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} />
|
||||
}
|
||||
|
||||
20
web/src/components/auth/RequireAuth.tsx
Normal file
20
web/src/components/auth/RequireAuth.tsx
Normal 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}</>
|
||||
}
|
||||
@@ -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
26
web/src/lib/events.ts
Normal 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[]
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -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>
|
||||
),
|
||||
},
|
||||
],
|
||||
},
|
||||
]);
|
||||
|
||||
|
||||
|
||||
])
|
||||
|
||||
Reference in New Issue
Block a user