mudanças

This commit is contained in:
2026-03-12 15:57:55 +00:00
parent 7911c2145b
commit 30c2071841
3 changed files with 108 additions and 183 deletions

View File

@@ -25,7 +25,7 @@ export default function AuthLogin() {
const { data, error } = await supabase.auth.signInWithPassword({ email, password })
if (error) throw error
const role = data.user?.user_metadata?.role
if (role) navigate(role === 'barbearia' ? '/painel' : '/explorar', { replace: true })
navigate(role === 'barbearia' ? '/painel' : '/explorar', { replace: true })
} catch {
setError('Email ou palavra-passe incorretos.')
setLoading(false)
@@ -33,18 +33,18 @@ export default function AuthLogin() {
}
return (
<div className="min-h-[80vh] flex items-center justify-center px-4">
<div className="w-full max-w-sm">
{/* Logo top */}
<div className="text-center mb-8">
<div className="inline-flex w-12 h-12 bg-emerald-600 rounded-2xl items-center justify-center mb-4 shadow-md">
<Scissors size={22} className="text-white" />
<div className="flex items-center justify-center py-10 px-4">
<div className="w-full" style={{ maxWidth: 400 }}>
{/* Logo */}
<div className="text-center mb-7">
<div className="inline-flex w-11 h-11 rounded-xl items-center justify-center mb-3 shadow" style={{ background: '#059669' }}>
<Scissors size={20} className="text-white" />
</div>
<h1 className="text-2xl font-bold text-slate-900">Bem-vindo de volta</h1>
<p className="text-sm text-slate-500 mt-1">Aceda à sua conta SmartAgenda</p>
<h1 className="text-xl font-bold text-slate-900">Bem-vindo de volta</h1>
<p className="text-sm text-slate-500 mt-0.5">Aceda à sua conta SmartAgenda</p>
</div>
<div className="bg-white rounded-2xl border border-slate-200 shadow-sm p-6">
<div className="bg-white rounded-2xl border border-slate-200 p-6 shadow-sm">
{error && (
<div className="mb-4 rounded-xl bg-rose-50 border border-rose-200 px-4 py-3 text-sm text-rose-700">
{error}
@@ -60,7 +60,8 @@ export default function AuthLogin() {
onChange={e => setEmail(e.target.value)}
placeholder="seu@email.com"
required
className="w-full px-3.5 py-2.5 rounded-xl border border-slate-200 text-sm focus:outline-none focus:ring-2 focus:ring-emerald-500 focus:border-transparent placeholder:text-slate-400"
className="w-full px-3.5 py-2.5 rounded-xl border border-slate-200 text-sm focus:outline-none focus:ring-2 focus:border-transparent placeholder:text-slate-400"
style={{ '--tw-ring-color': '#10b981' } as any}
/>
</div>
@@ -73,7 +74,7 @@ export default function AuthLogin() {
onChange={e => setPassword(e.target.value)}
placeholder="••••••••"
required
className="w-full px-3.5 py-2.5 pr-10 rounded-xl border border-slate-200 text-sm focus:outline-none focus:ring-2 focus:ring-emerald-500 focus:border-transparent placeholder:text-slate-400"
className="w-full px-3.5 py-2.5 pr-10 rounded-xl border border-slate-200 text-sm focus:outline-none focus:ring-2 focus:border-transparent placeholder:text-slate-400"
/>
<button type="button" onClick={() => setShowPw(!showPw)} className="absolute right-3 top-1/2 -translate-y-1/2 text-slate-400 hover:text-slate-600">
{showPw ? <EyeOff size={16} /> : <Eye size={16} />}
@@ -84,21 +85,20 @@ export default function AuthLogin() {
<button
type="submit"
disabled={loading}
className="w-full mt-2 py-2.5 rounded-xl bg-emerald-600 text-white text-sm font-semibold hover:bg-emerald-700 disabled:opacity-60 transition-colors flex items-center justify-center gap-2"
className="w-full py-2.5 rounded-xl text-white text-sm font-semibold disabled:opacity-60 transition-all flex items-center justify-center gap-2"
style={{ background: loading ? '#6ee7b7' : '#059669' }}
>
<LogIn size={16} />
<LogIn size={15} />
{loading ? 'A entrar...' : 'Entrar'}
</button>
</form>
<div className="text-center mt-5 pt-5 border-t border-slate-100">
<p className="text-sm text-slate-500">
Não tem conta?{' '}
<Link to="/registo" className="text-emerald-700 font-semibold hover:text-emerald-800">
Criar conta grátis
</Link>
</p>
</div>
<p className="text-center text-sm text-slate-500 mt-5">
Não tem conta?{' '}
<Link to="/registo" className="font-semibold hover:underline" style={{ color: '#059669' }}>
Criar conta grátis
</Link>
</p>
</div>
</div>
</div>

View File

@@ -1,14 +1,5 @@
/**
* @file AuthRegister.tsx
* @description Página de Registo para a versão Web. Permite a criação de
* novas contas segmentadas por perfil ('cliente' ou 'barbearia'). Interage
* diretamente com o serviço de autenticação do Supabase.
*/
import { FormEvent, useEffect, useState } from 'react'
import { Link, useNavigate } from 'react-router-dom'
import { Input } from '../components/ui/input'
import { Button } from '../components/ui/button'
import { Card } from '../components/ui/card'
import { UserPlus, User, Scissors } from 'lucide-react'
import { supabase } from '../lib/supabase'
@@ -16,58 +7,32 @@ export default function AuthRegister() {
const [name, setName] = useState('')
const [email, setEmail] = useState('')
const [password, setPassword] = useState('')
// Estado local para definir as permissões associadas à conta nova
const [role, setRole] = useState<'cliente' | 'barbearia'>('cliente')
// Condicional, só inserido no metadados se o tipo de utilizador for 'barbearia'
const [shopName, setShopName] = useState('')
const [error, setError] = useState('')
const [loading, setLoading] = useState(false)
const navigate = useNavigate()
/**
* Previne acesso à página de registo por utilizadores com Sessão ativa.
* Utiliza um padrão `mounted` para não alterar state se o componente for desmontado.
*/
// 🔐 Se já estiver logado, não pode ver o registo
useEffect(() => {
let mounted = true
; (async () => {
// Efetua um fetch à sessão existente injetada pelo SDK da Supabase
const { data } = await supabase.auth.getSession()
if (!mounted) return
if (data.session) {
navigate('/explorar', { replace: true })
}
if (data.session) navigate('/explorar', { replace: true })
})()
return () => {
mounted = false
}
return () => { mounted = false }
}, [navigate])
/**
* Processa a criação e envio do novo perfil e das informações para a BD via Supabase Auth.
* @param {FormEvent} e - Evento de submissão do formulário.
*/
async function onSubmit(e: FormEvent) {
e.preventDefault()
setError('')
// Regras básicas de negócio à submissão (proteção pre-API)
if (!name.trim()) return setError('Preencha o nome completo')
if (!email.trim()) return setError('Preencha o email')
if (!password.trim()) return setError('Preencha a senha')
if (role === 'barbearia' && !shopName.trim()) {
return setError('Informe o nome da barbearia')
}
if (role === 'barbearia' && !shopName.trim()) return setError('Informe o nome da barbearia')
try {
setLoading(true)
// Registo propriamente dito perante serviço do Supabase
// Encaminha, adicionalmente, opções nos metadados (Data) da auth do serviço para mapeamento da "profile" table na BD
const { data, error } = await supabase.auth.signUp({
email,
password,
@@ -75,31 +40,17 @@ export default function AuthRegister() {
data: {
name: name.trim(),
role,
// Apenas vincula shopName no json object se a rule aplicável confirmar que a role o exige
// Envia ambas as formatações (camelCase para web, snake_case para o trigger SQL)
shopName: role === 'barbearia' ? shopName.trim() : null,
shop_name: role === 'barbearia' ? shopName.trim() : null,
},
},
})
if (error) throw error
// 🔔 Se confirmação de email estiver ON (verificação pendente via SMTP em Supabase Configs)
if (!data.session) {
navigate('/login', {
replace: true,
state: {
msg: 'Conta criada! Confirma o email antes de fazer login.',
},
})
navigate('/login', { replace: true, state: { msg: 'Conta criada! Confirme o email antes de fazer login.' } })
return
}
// ✅ Login automático: Rotas divergentes de acordo com a função Role
navigate(role === 'barbearia' ? '/painel' : '/explorar', {
replace: true,
})
navigate(role === 'barbearia' ? '/painel' : '/explorar', { replace: true })
} catch (err: any) {
setError(err?.message || 'Erro ao criar conta')
} finally {
@@ -107,115 +58,91 @@ export default function AuthRegister() {
}
}
const field = (label: string, node: React.ReactNode) => (
<div>
<label className="block text-sm font-medium text-slate-700 mb-1.5">{label}</label>
{node}
</div>
)
const inputCls = "w-full px-3.5 py-2.5 rounded-xl border border-slate-200 text-sm focus:outline-none placeholder:text-slate-400"
return (
<div className="max-w-md mx-auto py-8">
<Card className="p-8 space-y-6">
<div className="text-center space-y-2">
<div className="inline-flex p-3 bg-gradient-to-br from-amber-500 to-amber-600 rounded-xl text-white shadow-lg mb-2">
<UserPlus size={24} />
<div className="flex items-center justify-center py-10 px-4">
<div className="w-full" style={{ maxWidth: 420 }}>
{/* Logo */}
<div className="text-center mb-7">
<div className="inline-flex w-11 h-11 rounded-xl items-center justify-center mb-3 shadow" style={{ background: '#059669' }}>
<UserPlus size={20} className="text-white" />
</div>
<h1 className="text-2xl font-bold text-slate-900">
Criar conta
</h1>
<p className="text-sm text-slate-600">
Escolha o tipo de acesso
</p>
<h1 className="text-xl font-bold text-slate-900">Criar conta</h1>
<p className="text-sm text-slate-500 mt-0.5">Escolha o seu perfil e comece </p>
</div>
{error && (
<div className="rounded-lg border border-rose-200 bg-rose-50 px-4 py-3 text-sm text-rose-700">
{error}
</div>
)}
<form className="space-y-5" onSubmit={onSubmit}>
{/* Tipo de conta */}
<div className="space-y-2">
<label className="text-sm font-medium text-slate-700">
Tipo de conta
</label>
<div className="grid grid-cols-2 gap-3">
{(['cliente', 'barbearia'] as const).map((r) => (
<button
key={r}
type="button"
onClick={() => {
setRole(r)
setError('')
}}
className={`p-4 rounded-xl border-2 transition-all ${role === r
? 'border-amber-500 bg-amber-50 shadow-md'
: 'border-slate-200 hover:border-amber-300'
}`}
>
<div className="flex flex-col items-center gap-2">
{r === 'cliente' ? (
<User size={20} className={role === r ? 'text-amber-600' : 'text-slate-400'} />
) : (
<Scissors size={20} className={role === r ? 'text-amber-600' : 'text-slate-400'} />
)}
<span className="text-sm font-semibold">
{r === 'cliente' ? 'Cliente' : 'Barbearia'}
</span>
</div>
</button>
))}
<div className="bg-white rounded-2xl border border-slate-200 p-6 shadow-sm">
{error && (
<div className="mb-4 rounded-xl bg-rose-50 border border-rose-200 px-4 py-3 text-sm text-rose-700">
{error}
</div>
</div>
<Input
label="Nome completo"
value={name}
onChange={(e) => setName(e.target.value)}
placeholder="João Silva"
required
/>
<Input
label="Email"
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="seu@email.com"
required
/>
<Input
label="Senha"
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
placeholder="••••••••"
required
/>
{role === 'barbearia' && (
<Input
label="Nome da barbearia"
value={shopName}
onChange={(e) => setShopName(e.target.value)}
placeholder="Barbearia XPTO"
required
/>
)}
<Button type="submit" className="w-full" size="lg" disabled={loading}>
{loading ? 'A criar conta…' : 'Criar conta'}
</Button>
</form>
<form className="space-y-4" onSubmit={onSubmit}>
{/* Tipo de conta */}
<div>
<label className="block text-sm font-medium text-slate-700 mb-2">Tipo de conta</label>
<div className="grid grid-cols-2 gap-2">
{(['cliente', 'barbearia'] as const).map(r => (
<button
key={r}
type="button"
onClick={() => { setRole(r); setError('') }}
className="flex flex-col items-center gap-1.5 py-3 px-2 rounded-xl border-2 transition-all text-sm font-medium"
style={{
borderColor: role === r ? '#059669' : '#e2e8f0',
background: role === r ? '#f0fdf4' : '#fff',
color: role === r ? '#065f46' : '#64748b',
}}
>
{r === 'cliente'
? <User size={18} style={{ color: role === r ? '#059669' : '#94a3b8' }} />
: <Scissors size={18} style={{ color: role === r ? '#059669' : '#94a3b8' }} />
}
{r === 'cliente' ? 'Cliente' : 'Barbearia'}
</button>
))}
</div>
</div>
<div className="text-center pt-4 border-t border-slate-200">
<p className="text-sm text-slate-600">
tem conta?{' '}
<Link
to="/login"
className="text-amber-700 font-semibold hover:text-amber-800"
{field('Nome completo',
<input value={name} onChange={e => setName(e.target.value)} placeholder="João Silva" required className={inputCls} />
)}
{field('Email',
<input type="email" value={email} onChange={e => setEmail(e.target.value)} placeholder="seu@email.com" required className={inputCls} />
)}
{field('Palavra-passe',
<input type="password" value={password} onChange={e => setPassword(e.target.value)} placeholder="••••••••" required className={inputCls} />
)}
{role === 'barbearia' && field('Nome da barbearia',
<input value={shopName} onChange={e => setShopName(e.target.value)} placeholder="Barbearia XPTO" required className={inputCls} />
)}
<button
type="submit"
disabled={loading}
className="w-full py-2.5 rounded-xl text-white text-sm font-semibold disabled:opacity-60 transition-all"
style={{ background: loading ? '#6ee7b7' : '#059669' }}
>
{loading ? 'A criar conta...' : 'Criar conta'}
</button>
</form>
<p className="text-center text-sm text-slate-500 mt-5">
tem conta?{' '}
<Link to="/login" className="font-semibold hover:underline" style={{ color: '#059669' }}>
Entrar
</Link>
</p>
</div>
</Card>
</div>
</div>
)
}

View File

@@ -173,22 +173,20 @@ export default function Landing() {
</section>
{/* CTA */}
<section className="rounded-2xl bg-slate-900 text-white px-6 py-14 md:px-12 text-center">
<div className="inline-flex w-12 h-12 bg-emerald-500/20 rounded-2xl items-center justify-center mb-5">
<section
className="rounded-2xl text-white px-6 py-14 md:px-12 text-center"
style={{ background: 'linear-gradient(135deg, #0f172a 0%, #134e4a 50%, #0f172a 100%)' }}
>
<div className="inline-flex w-12 h-12 rounded-2xl items-center justify-center mb-5" style={{ background: 'rgba(16,185,129,0.2)' }}>
<Scissors size={22} className="text-emerald-400" />
</div>
<h2 className="text-3xl md:text-4xl font-bold mb-3">Pronto para começar?</h2>
<p className="text-slate-400 max-w-lg mx-auto mb-8">
<p style={{ color: '#94a3b8' }} className="max-w-lg mx-auto mb-8">
Junte-se a centenas de barbearias que usam a SmartAgenda.
</p>
<div className="flex flex-wrap justify-center gap-3">
<Link to="/registo" className="inline-flex items-center gap-2 px-6 py-3 rounded-xl bg-emerald-500 text-white font-semibold text-sm hover:bg-emerald-400 transition-colors">
Criar conta grátis <ArrowRight size={16} />
</Link>
<Link to="/explorar" className="inline-flex items-center gap-2 px-6 py-3 rounded-xl bg-white/10 border border-white/20 text-white font-semibold text-sm hover:bg-white/20 transition-colors">
Explorar barbearias
</Link>
</div>
<Link to="/registo" className="inline-flex items-center gap-2 px-6 py-3 rounded-xl font-semibold text-sm transition-colors" style={{ background: '#10b981', color: '#fff' }}>
Criar conta grátis <ArrowRight size={16} />
</Link>
</section>
</div>
);