mudanças
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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 já</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">
|
||||
Já 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">
|
||||
Já tem conta?{' '}
|
||||
<Link to="/login" className="font-semibold hover:underline" style={{ color: '#059669' }}>
|
||||
Entrar
|
||||
</Link>
|
||||
</p>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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 já 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>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user