feat: sessão #3 — lib (db/auth/email/validations), API routes, NextAuth v5, middleware, páginas account/shelters/shelter-dashboard, Prisma v7 fix
This commit is contained in:
119
app/auth/forgot-password/page.tsx
Normal file
119
app/auth/forgot-password/page.tsx
Normal file
@@ -0,0 +1,119 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import Link from 'next/link';
|
||||
import { PawPrint, Mail, ArrowLeft } from 'lucide-react';
|
||||
|
||||
export default function ForgotPasswordPage() {
|
||||
const [email, setEmail] = useState('');
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [sent, setSent] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
setError(null);
|
||||
setLoading(true);
|
||||
|
||||
try {
|
||||
// TODO: substituir por chamada real à API quando o fluxo de reset estiver implementado
|
||||
await new Promise((r) => setTimeout(r, 800));
|
||||
setSent(true);
|
||||
} catch {
|
||||
setError('Erro ao enviar email. Tenta de novo.');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (sent) {
|
||||
return (
|
||||
<div style={{ textAlign: 'center', maxWidth: '420px', padding: '48px 24px' }}>
|
||||
<div style={{ fontSize: '48px', marginBottom: '16px' }}>📬</div>
|
||||
<h1 style={{ fontFamily: 'var(--font-display)', fontWeight: 900, fontSize: '26px', color: 'var(--soil)', marginBottom: '12px' }}>
|
||||
Email enviado!
|
||||
</h1>
|
||||
<p style={{ fontFamily: 'var(--font-body)', color: 'var(--soil-mid)', marginBottom: '28px', lineHeight: 1.6 }}>
|
||||
Se <strong>{email}</strong> estiver registado, receberás um email com instruções para repor a tua palavra-passe.
|
||||
</p>
|
||||
<Link href="/auth/login" className="btn btn-secondary" style={{ display: 'inline-flex', justifyContent: 'center', gap: '8px' }}>
|
||||
<ArrowLeft size={15} />
|
||||
Voltar ao login
|
||||
</Link>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
width: '100%',
|
||||
maxWidth: '420px',
|
||||
background: 'var(--linen)',
|
||||
border: '1px solid var(--parchment)',
|
||||
borderRadius: '20px',
|
||||
padding: 'clamp(28px, 5vw, 44px)',
|
||||
boxShadow: 'var(--shadow-warm-md)',
|
||||
}}
|
||||
>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '8px', marginBottom: '28px' }}>
|
||||
<PawPrint size={20} style={{ color: 'var(--terra)' }} />
|
||||
<span className="logo" style={{ fontSize: '18px' }}>PawLink</span>
|
||||
</div>
|
||||
|
||||
<h1 style={{ fontFamily: 'var(--font-display)', fontWeight: 900, fontSize: '26px', color: 'var(--soil)', marginBottom: '8px', lineHeight: 1.15 }}>
|
||||
Esqueceste a palavra-passe?
|
||||
</h1>
|
||||
<p style={{ fontFamily: 'var(--font-body)', fontSize: '14px', color: 'var(--soil-mid)', marginBottom: '28px', lineHeight: 1.55 }}>
|
||||
Introduz o teu email e enviamos um link para repor a palavra-passe.
|
||||
</p>
|
||||
|
||||
{error && (
|
||||
<div style={{ background: 'rgba(196,80,26,0.08)', border: '1px solid rgba(196,80,26,0.25)', borderRadius: '10px', padding: '12px 16px', marginBottom: '20px', fontFamily: 'var(--font-body)', fontSize: '14px', color: 'var(--terra)' }}>
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<form onSubmit={handleSubmit} noValidate style={{ display: 'flex', flexDirection: 'column', gap: '16px' }}>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '6px' }}>
|
||||
<label htmlFor="forgot-email" style={{ fontFamily: 'var(--font-accent)', fontSize: '11px', letterSpacing: '0.08em', textTransform: 'uppercase', color: 'var(--soil-faint)' }}>
|
||||
Email
|
||||
</label>
|
||||
<div style={{ position: 'relative' }}>
|
||||
<Mail size={15} style={{ position: 'absolute', left: '14px', top: '50%', transform: 'translateY(-50%)', color: 'var(--soil-faint)', pointerEvents: 'none' }} />
|
||||
<input
|
||||
id="forgot-email"
|
||||
type="email"
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
placeholder="o.teu@email.pt"
|
||||
required
|
||||
autoComplete="email"
|
||||
style={{ width: '100%', padding: '12px 16px 12px 40px', background: 'var(--cream)', border: '1.5px solid var(--parchment)', borderRadius: '10px', fontFamily: 'var(--font-body)', fontSize: '15px', color: 'var(--soil)', outline: 'none', transition: 'border-color 180ms ease', minHeight: '48px' }}
|
||||
onFocus={(e) => (e.target.style.borderColor = 'var(--terra)')}
|
||||
onBlur={(e) => (e.target.style.borderColor = 'var(--parchment)')}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
id="forgot-submit"
|
||||
type="submit"
|
||||
className="btn btn-primary"
|
||||
disabled={loading}
|
||||
style={{ width: '100%', justifyContent: 'center', opacity: loading ? 0.75 : 1, cursor: loading ? 'not-allowed' : 'pointer' }}
|
||||
aria-label="Enviar email de recuperação"
|
||||
>
|
||||
{loading ? 'A enviar…' : 'Enviar link de recuperação'}
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<div style={{ marginTop: '20px', textAlign: 'center' }}>
|
||||
<Link href="/auth/login" style={{ fontFamily: 'var(--font-body)', fontSize: '14px', color: 'var(--soil-mid)', display: 'inline-flex', alignItems: 'center', gap: '6px' }}>
|
||||
<ArrowLeft size={14} />
|
||||
Voltar ao login
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
14
app/auth/layout.tsx
Normal file
14
app/auth/layout.tsx
Normal file
@@ -0,0 +1,14 @@
|
||||
import Header from '@/components/layout/Header';
|
||||
import Footer from '@/components/layout/Footer';
|
||||
|
||||
export default function AuthLayout({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<>
|
||||
<Header />
|
||||
<main style={{ flex: 1, display: 'flex', alignItems: 'center', justifyContent: 'center', padding: '48px 16px', background: 'var(--color-bg)' }}>
|
||||
{children}
|
||||
</main>
|
||||
<Footer />
|
||||
</>
|
||||
);
|
||||
}
|
||||
226
app/auth/login/page.tsx
Normal file
226
app/auth/login/page.tsx
Normal file
@@ -0,0 +1,226 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import Link from 'next/link';
|
||||
import { PawPrint, Eye, EyeOff, Mail, Lock } from 'lucide-react';
|
||||
import type { Metadata } from 'next';
|
||||
|
||||
export default function LoginPage() {
|
||||
const [showPass, setShowPass] = useState(false);
|
||||
const [email, setEmail] = useState('');
|
||||
const [password, setPassword] = useState('');
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const handleSubmit = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
setLoading(true);
|
||||
// TODO: substituir por NextAuth signIn quando NEXTAUTH_SECRET estiver configurado
|
||||
setTimeout(() => setLoading(false), 1200);
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
width: '100%',
|
||||
maxWidth: '420px',
|
||||
background: 'var(--color-surface)',
|
||||
border: '1px solid var(--color-border)',
|
||||
borderRadius: '20px',
|
||||
padding: 'clamp(28px, 5vw, 44px)',
|
||||
boxShadow: '0 8px 40px rgba(45,27,14,0.08)',
|
||||
}}
|
||||
>
|
||||
{/* Brand mark */}
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '8px', marginBottom: '28px' }}>
|
||||
<PawPrint size={20} style={{ color: 'var(--color-terra)' }} />
|
||||
<span
|
||||
style={{
|
||||
fontFamily: 'var(--font-playfair, Georgia, serif)',
|
||||
fontStyle: 'italic',
|
||||
fontWeight: 700,
|
||||
fontSize: '18px',
|
||||
color: 'var(--color-terra)',
|
||||
}}
|
||||
>
|
||||
PawLink
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<h1
|
||||
style={{
|
||||
fontFamily: 'var(--font-playfair, Georgia, serif)',
|
||||
fontWeight: 900,
|
||||
fontSize: '28px',
|
||||
color: 'var(--color-text)',
|
||||
marginBottom: '6px',
|
||||
lineHeight: 1.15,
|
||||
}}
|
||||
>
|
||||
Bem-vindo de volta.
|
||||
</h1>
|
||||
<p style={{ fontFamily: 'var(--font-body)', fontSize: '14px', color: 'var(--color-muted)', marginBottom: '28px' }}>
|
||||
Não tens conta?{' '}
|
||||
<Link
|
||||
href="/auth/register"
|
||||
style={{ color: 'var(--color-terra)', fontWeight: 500, textDecoration: 'underline', textDecorationColor: 'transparent', transition: 'text-decoration-color 180ms ease' }}
|
||||
onMouseEnter={e => ((e.target as HTMLElement).style.textDecorationColor = 'var(--color-terra)')}
|
||||
onMouseLeave={e => ((e.target as HTMLElement).style.textDecorationColor = 'transparent')}
|
||||
>
|
||||
Criar conta grátis
|
||||
</Link>
|
||||
</p>
|
||||
|
||||
<form onSubmit={handleSubmit} noValidate style={{ display: 'flex', flexDirection: 'column', gap: '16px' }}>
|
||||
{/* Email field */}
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '6px' }}>
|
||||
<label
|
||||
htmlFor="login-email"
|
||||
style={{ fontFamily: 'var(--font-mono, monospace)', fontSize: '11px', fontWeight: 500, letterSpacing: '0.08em', textTransform: 'uppercase', color: 'var(--color-muted)' }}
|
||||
>
|
||||
Email
|
||||
</label>
|
||||
<div style={{ position: 'relative' }}>
|
||||
<Mail size={15} style={{ position: 'absolute', left: '14px', top: '50%', transform: 'translateY(-50%)', color: 'var(--color-muted)', pointerEvents: 'none' }} />
|
||||
<input
|
||||
id="login-email"
|
||||
type="email"
|
||||
value={email}
|
||||
onChange={e => setEmail(e.target.value)}
|
||||
placeholder="o.teu@email.pt"
|
||||
required
|
||||
autoComplete="email"
|
||||
style={{
|
||||
width: '100%',
|
||||
padding: '12px 16px 12px 40px',
|
||||
background: 'var(--color-bg)',
|
||||
border: '1.5px solid var(--color-border)',
|
||||
borderRadius: '10px',
|
||||
fontFamily: 'var(--font-body)',
|
||||
fontSize: '15px',
|
||||
color: 'var(--color-text)',
|
||||
outline: 'none',
|
||||
transition: 'border-color 180ms ease',
|
||||
minHeight: '48px',
|
||||
}}
|
||||
onFocus={e => (e.target.style.borderColor = 'var(--color-terra)')}
|
||||
onBlur={e => (e.target.style.borderColor = 'var(--color-border)')}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Password field */}
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '6px' }}>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||
<label
|
||||
htmlFor="login-password"
|
||||
style={{ fontFamily: 'var(--font-mono, monospace)', fontSize: '11px', fontWeight: 500, letterSpacing: '0.08em', textTransform: 'uppercase', color: 'var(--color-muted)' }}
|
||||
>
|
||||
Palavra-passe
|
||||
</label>
|
||||
<Link
|
||||
href="/auth/forgot-password"
|
||||
style={{ fontFamily: 'var(--font-body)', fontSize: '12px', color: 'var(--color-terra)', textDecoration: 'none' }}
|
||||
>
|
||||
Esqueceste-a?
|
||||
</Link>
|
||||
</div>
|
||||
<div style={{ position: 'relative' }}>
|
||||
<Lock size={15} style={{ position: 'absolute', left: '14px', top: '50%', transform: 'translateY(-50%)', color: 'var(--color-muted)', pointerEvents: 'none' }} />
|
||||
<input
|
||||
id="login-password"
|
||||
type={showPass ? 'text' : 'password'}
|
||||
value={password}
|
||||
onChange={e => setPassword(e.target.value)}
|
||||
placeholder="••••••••"
|
||||
required
|
||||
autoComplete="current-password"
|
||||
style={{
|
||||
width: '100%',
|
||||
padding: '12px 44px 12px 40px',
|
||||
background: 'var(--color-bg)',
|
||||
border: '1.5px solid var(--color-border)',
|
||||
borderRadius: '10px',
|
||||
fontFamily: 'var(--font-body)',
|
||||
fontSize: '15px',
|
||||
color: 'var(--color-text)',
|
||||
outline: 'none',
|
||||
transition: 'border-color 180ms ease',
|
||||
minHeight: '48px',
|
||||
}}
|
||||
onFocus={e => (e.target.style.borderColor = 'var(--color-terra)')}
|
||||
onBlur={e => (e.target.style.borderColor = 'var(--color-border)')}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowPass(s => !s)}
|
||||
style={{
|
||||
position: 'absolute',
|
||||
right: '12px',
|
||||
top: '50%',
|
||||
transform: 'translateY(-50%)',
|
||||
background: 'none',
|
||||
border: 'none',
|
||||
cursor: 'pointer',
|
||||
color: 'var(--color-muted)',
|
||||
padding: '4px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
}}
|
||||
aria-label={showPass ? 'Ocultar palavra-passe' : 'Mostrar palavra-passe'}
|
||||
>
|
||||
{showPass ? <EyeOff size={16} /> : <Eye size={16} />}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Submit */}
|
||||
<button
|
||||
id="login-submit"
|
||||
type="submit"
|
||||
className="btn-primary"
|
||||
disabled={loading}
|
||||
style={{
|
||||
width: '100%',
|
||||
justifyContent: 'center',
|
||||
marginTop: '8px',
|
||||
opacity: loading ? 0.75 : 1,
|
||||
cursor: loading ? 'not-allowed' : 'pointer',
|
||||
}}
|
||||
aria-label="Entrar na conta PawLink"
|
||||
>
|
||||
{loading ? (
|
||||
<span style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
|
||||
<span style={{
|
||||
width: '14px', height: '14px',
|
||||
border: '2px solid rgba(255,255,255,0.3)',
|
||||
borderTopColor: 'white',
|
||||
borderRadius: '50%',
|
||||
display: 'inline-block',
|
||||
animation: 'spin 0.7s linear infinite',
|
||||
}} />
|
||||
A entrar…
|
||||
</span>
|
||||
) : (
|
||||
'Entrar'
|
||||
)}
|
||||
</button>
|
||||
</form>
|
||||
|
||||
{/* Divider */}
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '12px', margin: '20px 0' }}>
|
||||
<div style={{ flex: 1, height: '1px', background: 'var(--color-border)' }} />
|
||||
<span style={{ fontFamily: 'var(--font-mono, monospace)', fontSize: '10px', color: 'var(--color-muted)', letterSpacing: '0.06em', textTransform: 'uppercase' }}>ou</span>
|
||||
<div style={{ flex: 1, height: '1px', background: 'var(--color-border)' }} />
|
||||
</div>
|
||||
|
||||
<p style={{ textAlign: 'center', fontFamily: 'var(--font-body)', fontSize: '12px', color: 'var(--color-muted)', lineHeight: 1.5 }}>
|
||||
Ao entrar, aceitas os nossos{' '}
|
||||
<Link href="/terms" style={{ color: 'var(--color-terra)', textDecoration: 'none' }}>Termos de Utilização</Link>
|
||||
{' '}e a nossa{' '}
|
||||
<Link href="/privacy" style={{ color: 'var(--color-terra)', textDecoration: 'none' }}>Política de Privacidade</Link>.
|
||||
</p>
|
||||
|
||||
<style>{`@keyframes spin { to { transform: rotate(360deg); } }`}</style>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
211
app/auth/register/page.tsx
Normal file
211
app/auth/register/page.tsx
Normal file
@@ -0,0 +1,211 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import Link from 'next/link';
|
||||
import { PawPrint, Eye, EyeOff, Mail, Lock, User, Calendar, MapPin } from 'lucide-react';
|
||||
import { DISTRITOS } from '@/lib/validations/auth';
|
||||
|
||||
export default function RegisterPage() {
|
||||
const [showPass, setShowPass] = useState(false);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [success, setSuccess] = useState(false);
|
||||
|
||||
const [form, setForm] = useState({
|
||||
name: '',
|
||||
email: '',
|
||||
password: '',
|
||||
confirmPassword: '',
|
||||
birthdate: '',
|
||||
district: '',
|
||||
terms: false,
|
||||
});
|
||||
|
||||
const set = (k: keyof typeof form) => (
|
||||
e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement>
|
||||
) => setForm((f) => ({ ...f, [k]: e.target.type === 'checkbox' ? (e.target as HTMLInputElement).checked : e.target.value }));
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
setError(null);
|
||||
setLoading(true);
|
||||
|
||||
try {
|
||||
const res = await fetch('/api/auth/register', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(form),
|
||||
});
|
||||
|
||||
const data = await res.json();
|
||||
|
||||
if (!res.ok) {
|
||||
setError(data.error ?? 'Erro ao criar conta. Tenta de novo.');
|
||||
} else {
|
||||
setSuccess(true);
|
||||
}
|
||||
} catch {
|
||||
setError('Erro de rede. Verifica a tua ligação.');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const inputStyle: React.CSSProperties = {
|
||||
width: '100%',
|
||||
padding: '12px 16px 12px 40px',
|
||||
background: 'var(--cream)',
|
||||
border: '1.5px solid var(--parchment)',
|
||||
borderRadius: '10px',
|
||||
fontFamily: 'var(--font-body)',
|
||||
fontSize: '15px',
|
||||
color: 'var(--soil)',
|
||||
outline: 'none',
|
||||
transition: 'border-color 180ms ease',
|
||||
minHeight: '48px',
|
||||
};
|
||||
|
||||
const labelStyle: React.CSSProperties = {
|
||||
fontFamily: 'var(--font-accent)',
|
||||
fontSize: '11px',
|
||||
fontWeight: 400,
|
||||
letterSpacing: '0.08em',
|
||||
textTransform: 'uppercase' as const,
|
||||
color: 'var(--soil-faint)',
|
||||
};
|
||||
|
||||
if (success) {
|
||||
return (
|
||||
<div style={{ textAlign: 'center', maxWidth: '420px', padding: '48px 24px' }}>
|
||||
<div style={{ fontSize: '56px', marginBottom: '16px' }}>🐾</div>
|
||||
<h1 style={{ fontFamily: 'var(--font-display)', fontWeight: 900, fontSize: '28px', color: 'var(--soil)', marginBottom: '12px' }}>
|
||||
Conta criada!
|
||||
</h1>
|
||||
<p style={{ fontFamily: 'var(--font-body)', color: 'var(--soil-mid)', marginBottom: '24px', lineHeight: 1.6 }}>
|
||||
Bem-vindo/a à PawLink. Já podes explorar animais e fazer adopções.
|
||||
</p>
|
||||
<Link href="/auth/login" className="btn btn-primary" style={{ justifyContent: 'center' }}>
|
||||
Entrar na conta
|
||||
</Link>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
width: '100%',
|
||||
maxWidth: '480px',
|
||||
background: 'var(--linen)',
|
||||
border: '1px solid var(--parchment)',
|
||||
borderRadius: '20px',
|
||||
padding: 'clamp(28px, 5vw, 44px)',
|
||||
boxShadow: 'var(--shadow-warm-md)',
|
||||
}}
|
||||
>
|
||||
{/* Marca */}
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '8px', marginBottom: '28px' }}>
|
||||
<PawPrint size={20} style={{ color: 'var(--terra)' }} />
|
||||
<span className="logo" style={{ fontSize: '18px' }}>PawLink</span>
|
||||
</div>
|
||||
|
||||
<h1 style={{ fontFamily: 'var(--font-display)', fontWeight: 900, fontSize: '28px', color: 'var(--soil)', marginBottom: '6px', lineHeight: 1.15 }}>
|
||||
Criar conta.
|
||||
</h1>
|
||||
<p style={{ fontFamily: 'var(--font-body)', fontSize: '14px', color: 'var(--soil-mid)', marginBottom: '28px' }}>
|
||||
Já tens conta?{' '}
|
||||
<Link href="/auth/login" style={{ color: 'var(--terra)', fontWeight: 500 }}>
|
||||
Entrar
|
||||
</Link>
|
||||
</p>
|
||||
|
||||
{error && (
|
||||
<div style={{ background: 'rgba(196,80,26,0.08)', border: '1px solid rgba(196,80,26,0.25)', borderRadius: '10px', padding: '12px 16px', marginBottom: '20px', fontFamily: 'var(--font-body)', fontSize: '14px', color: 'var(--terra)' }}>
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<form onSubmit={handleSubmit} noValidate style={{ display: 'flex', flexDirection: 'column', gap: '16px' }}>
|
||||
{/* Nome */}
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '6px' }}>
|
||||
<label htmlFor="reg-name" style={labelStyle}>Nome completo</label>
|
||||
<div style={{ position: 'relative' }}>
|
||||
<User size={15} style={{ position: 'absolute', left: '14px', top: '50%', transform: 'translateY(-50%)', color: 'var(--soil-faint)', pointerEvents: 'none' }} />
|
||||
<input id="reg-name" type="text" value={form.name} onChange={set('name')} placeholder="O teu nome" required autoComplete="name" style={inputStyle} onFocus={e => (e.target.style.borderColor = 'var(--terra)')} onBlur={e => (e.target.style.borderColor = 'var(--parchment)')} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Email */}
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '6px' }}>
|
||||
<label htmlFor="reg-email" style={labelStyle}>Email</label>
|
||||
<div style={{ position: 'relative' }}>
|
||||
<Mail size={15} style={{ position: 'absolute', left: '14px', top: '50%', transform: 'translateY(-50%)', color: 'var(--soil-faint)', pointerEvents: 'none' }} />
|
||||
<input id="reg-email" type="email" value={form.email} onChange={set('email')} placeholder="o.teu@email.pt" required autoComplete="email" style={inputStyle} onFocus={e => (e.target.style.borderColor = 'var(--terra)')} onBlur={e => (e.target.style.borderColor = 'var(--parchment)')} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Password */}
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '6px' }}>
|
||||
<label htmlFor="reg-password" style={labelStyle}>Palavra-passe</label>
|
||||
<div style={{ position: 'relative' }}>
|
||||
<Lock size={15} style={{ position: 'absolute', left: '14px', top: '50%', transform: 'translateY(-50%)', color: 'var(--soil-faint)', pointerEvents: 'none' }} />
|
||||
<input id="reg-password" type={showPass ? 'text' : 'password'} value={form.password} onChange={set('password')} placeholder="Mín. 8 caract., 1 maiúscula, 1 número" required autoComplete="new-password" style={{ ...inputStyle, paddingRight: '44px' }} onFocus={e => (e.target.style.borderColor = 'var(--terra)')} onBlur={e => (e.target.style.borderColor = 'var(--parchment)')} />
|
||||
<button type="button" onClick={() => setShowPass(s => !s)} style={{ position: 'absolute', right: '12px', top: '50%', transform: 'translateY(-50%)', background: 'none', border: 'none', cursor: 'pointer', color: 'var(--soil-faint)', padding: '4px', display: 'flex', alignItems: 'center' }} aria-label={showPass ? 'Ocultar' : 'Mostrar'}>
|
||||
{showPass ? <EyeOff size={16} /> : <Eye size={16} />}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Confirmar password */}
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '6px' }}>
|
||||
<label htmlFor="reg-confirm" style={labelStyle}>Confirmar palavra-passe</label>
|
||||
<div style={{ position: 'relative' }}>
|
||||
<Lock size={15} style={{ position: 'absolute', left: '14px', top: '50%', transform: 'translateY(-50%)', color: 'var(--soil-faint)', pointerEvents: 'none' }} />
|
||||
<input id="reg-confirm" type="password" value={form.confirmPassword} onChange={set('confirmPassword')} placeholder="Repetir palavra-passe" required autoComplete="new-password" style={inputStyle} onFocus={e => (e.target.style.borderColor = 'var(--terra)')} onBlur={e => (e.target.style.borderColor = 'var(--parchment)')} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Data de nascimento */}
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '6px' }}>
|
||||
<label htmlFor="reg-birth" style={labelStyle}>Data de nascimento (mínimo 18 anos)</label>
|
||||
<div style={{ position: 'relative' }}>
|
||||
<Calendar size={15} style={{ position: 'absolute', left: '14px', top: '50%', transform: 'translateY(-50%)', color: 'var(--soil-faint)', pointerEvents: 'none' }} />
|
||||
<input id="reg-birth" type="date" value={form.birthdate} onChange={set('birthdate')} required style={{ ...inputStyle, colorScheme: 'light' }} onFocus={e => (e.target.style.borderColor = 'var(--terra)')} onBlur={e => (e.target.style.borderColor = 'var(--parchment)')} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Distrito */}
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '6px' }}>
|
||||
<label htmlFor="reg-district" style={labelStyle}>Distrito</label>
|
||||
<div style={{ position: 'relative' }}>
|
||||
<MapPin size={15} style={{ position: 'absolute', left: '14px', top: '50%', transform: 'translateY(-50%)', color: 'var(--soil-faint)', pointerEvents: 'none', zIndex: 1 }} />
|
||||
<select id="reg-district" value={form.district} onChange={set('district')} required style={{ ...inputStyle, appearance: 'none', paddingLeft: '40px', cursor: 'pointer' }} onFocus={e => (e.target.style.borderColor = 'var(--terra)')} onBlur={e => (e.target.style.borderColor = 'var(--parchment)')}>
|
||||
<option value="">Selecciona o teu distrito</option>
|
||||
{DISTRITOS.map(d => <option key={d} value={d}>{d}</option>)}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Termos */}
|
||||
<label style={{ display: 'flex', alignItems: 'flex-start', gap: '10px', cursor: 'pointer', fontFamily: 'var(--font-body)', fontSize: '13px', color: 'var(--soil-mid)', lineHeight: 1.5 }}>
|
||||
<input type="checkbox" checked={form.terms} onChange={set('terms')} required style={{ marginTop: '3px', width: '16px', height: '16px', accentColor: 'var(--terra)', flexShrink: 0 }} />
|
||||
Aceito os{' '}
|
||||
<Link href="/terms" target="_blank" style={{ color: 'var(--terra)' }}>Termos de Utilização</Link>
|
||||
{' '}e a{' '}
|
||||
<Link href="/privacy" target="_blank" style={{ color: 'var(--terra)' }}>Política de Privacidade</Link>.
|
||||
</label>
|
||||
|
||||
<button
|
||||
id="register-submit"
|
||||
type="submit"
|
||||
className="btn btn-primary"
|
||||
disabled={loading}
|
||||
style={{ width: '100%', justifyContent: 'center', marginTop: '8px', opacity: loading ? 0.75 : 1, cursor: loading ? 'not-allowed' : 'pointer' }}
|
||||
aria-label="Criar conta PawLink"
|
||||
>
|
||||
{loading ? 'A criar conta…' : 'Criar conta'}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user