fix: login/registo funcional — signIn NextAuth, SessionProvider, NEXTAUTH_SECRET real, error handling
This commit is contained in:
@@ -6,51 +6,66 @@ import { validateAge } from '@/lib/auth/age-validation';
|
|||||||
import { sendEmail, buildWelcomeHtml } from '@/lib/email';
|
import { sendEmail, buildWelcomeHtml } from '@/lib/email';
|
||||||
|
|
||||||
export async function POST(request: Request) {
|
export async function POST(request: Request) {
|
||||||
const body = await request.json();
|
try {
|
||||||
const parsed = registerSchema.safeParse(body);
|
const body = await request.json();
|
||||||
|
const parsed = registerSchema.safeParse(body);
|
||||||
|
|
||||||
if (!parsed.success) {
|
if (!parsed.success) {
|
||||||
const errors = parsed.error.flatten().fieldErrors;
|
const errors = parsed.error.flatten().fieldErrors;
|
||||||
const firstError = Object.values(errors).flat()[0];
|
const firstError = Object.values(errors).flat()[0];
|
||||||
return NextResponse.json({ error: firstError ?? 'Dados inválidos.' }, { status: 400 });
|
return NextResponse.json({ error: firstError ?? 'Dados inválidos.' }, { status: 400 });
|
||||||
|
}
|
||||||
|
|
||||||
|
const { name, email, password, birthdate, district } = parsed.data;
|
||||||
|
|
||||||
|
// Validação de +18 sempre no servidor
|
||||||
|
const ageCheck = validateAge(birthdate);
|
||||||
|
if (!ageCheck.valid) {
|
||||||
|
return NextResponse.json({ error: ageCheck.error }, { status: 400 });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verificar se email já existe
|
||||||
|
const existing = await prisma.user.findUnique({ where: { email } });
|
||||||
|
if (existing) {
|
||||||
|
return NextResponse.json({ error: 'Este email já está registado.' }, { status: 409 });
|
||||||
|
}
|
||||||
|
|
||||||
|
const hashedPassword = await hashPassword(password);
|
||||||
|
|
||||||
|
const user = await prisma.user.create({
|
||||||
|
data: {
|
||||||
|
name,
|
||||||
|
email,
|
||||||
|
password: hashedPassword,
|
||||||
|
birthdate: new Date(birthdate),
|
||||||
|
district,
|
||||||
|
},
|
||||||
|
select: { id: true, name: true, email: true },
|
||||||
|
});
|
||||||
|
|
||||||
|
// Email de boas-vindas (não bloqueia a resposta)
|
||||||
|
sendEmail({
|
||||||
|
to: user.email,
|
||||||
|
subject: 'Bem-vindo à PetLink 🐾',
|
||||||
|
html: buildWelcomeHtml({ userName: user.name }),
|
||||||
|
}).catch(console.error);
|
||||||
|
|
||||||
|
return NextResponse.json(
|
||||||
|
{ message: 'Conta criada com sucesso.', userId: user.id },
|
||||||
|
{ status: 201 }
|
||||||
|
);
|
||||||
|
} catch (err: unknown) {
|
||||||
|
console.error('[POST /api/auth/register]', err);
|
||||||
|
|
||||||
|
// Erro de ligação à base de dados — mensagem clara ao utilizador
|
||||||
|
const msg = err instanceof Error ? err.message : '';
|
||||||
|
if (msg.includes('P1001') || msg.includes('connect') || msg.includes('ECONNREFUSED')) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: 'Base de dados temporariamente indisponível. Tenta mais tarde.' },
|
||||||
|
{ status: 503 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return NextResponse.json({ error: 'Erro interno. Tenta de novo.' }, { status: 500 });
|
||||||
}
|
}
|
||||||
|
|
||||||
const { name, email, password, birthdate, district } = parsed.data;
|
|
||||||
|
|
||||||
// Validação de +18 sempre no servidor
|
|
||||||
const ageCheck = validateAge(birthdate);
|
|
||||||
if (!ageCheck.valid) {
|
|
||||||
return NextResponse.json({ error: ageCheck.error }, { status: 400 });
|
|
||||||
}
|
|
||||||
|
|
||||||
// Verificar se email já existe
|
|
||||||
const existing = await prisma.user.findUnique({ where: { email } });
|
|
||||||
if (existing) {
|
|
||||||
return NextResponse.json({ error: 'Este email já está registado.' }, { status: 409 });
|
|
||||||
}
|
|
||||||
|
|
||||||
const hashedPassword = await hashPassword(password);
|
|
||||||
|
|
||||||
const user = await prisma.user.create({
|
|
||||||
data: {
|
|
||||||
name,
|
|
||||||
email,
|
|
||||||
password: hashedPassword,
|
|
||||||
birthdate: new Date(birthdate),
|
|
||||||
district,
|
|
||||||
},
|
|
||||||
select: { id: true, name: true, email: true },
|
|
||||||
});
|
|
||||||
|
|
||||||
// Email de boas-vindas (não bloqueia)
|
|
||||||
sendEmail({
|
|
||||||
to: user.email,
|
|
||||||
subject: 'Bem-vindo à PetLink 🐾',
|
|
||||||
html: buildWelcomeHtml({ userName: user.name }),
|
|
||||||
}).catch(console.error);
|
|
||||||
|
|
||||||
return NextResponse.json(
|
|
||||||
{ message: 'Conta criada com sucesso.', userId: user.id },
|
|
||||||
{ status: 201 }
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,44 +2,89 @@
|
|||||||
|
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import { PawPrint, Eye, EyeOff, Mail, Lock } from 'lucide-react';
|
import { useRouter } from 'next/navigation';
|
||||||
import type { Metadata } from 'next';
|
import { signIn } from 'next-auth/react';
|
||||||
|
import { PawPrint, Eye, EyeOff, Mail, Lock, AlertCircle } from 'lucide-react';
|
||||||
|
|
||||||
export default function LoginPage() {
|
export default function LoginPage() {
|
||||||
|
const router = useRouter();
|
||||||
const [showPass, setShowPass] = useState(false);
|
const [showPass, setShowPass] = useState(false);
|
||||||
const [email, setEmail] = useState('');
|
const [email, setEmail] = useState('');
|
||||||
const [password, setPassword] = useState('');
|
const [password, setPassword] = useState('');
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [error, setError] = useState('');
|
||||||
|
|
||||||
const handleSubmit = (e: React.FormEvent) => {
|
const handleSubmit = async (e: React.FormEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
setError('');
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
// TODO: substituir por NextAuth signIn quando NEXTAUTH_SECRET estiver configurado
|
|
||||||
setTimeout(() => setLoading(false), 1200);
|
try {
|
||||||
|
const result = await signIn('credentials', {
|
||||||
|
email,
|
||||||
|
password,
|
||||||
|
redirect: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (result?.error) {
|
||||||
|
setError('Email ou palavra-passe incorrectos. Tenta de novo.');
|
||||||
|
} else {
|
||||||
|
router.push('/');
|
||||||
|
router.refresh();
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
setError('Erro de rede. Verifica a tua ligação e tenta de novo.');
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const inputStyle: React.CSSProperties = {
|
||||||
|
width: '100%',
|
||||||
|
padding: '12px 16px 12px 40px',
|
||||||
|
background: 'var(--color-bg, var(--cream))',
|
||||||
|
border: '1.5px solid var(--color-border, var(--parchment))',
|
||||||
|
borderRadius: '10px',
|
||||||
|
fontFamily: 'var(--font-body)',
|
||||||
|
fontSize: '15px',
|
||||||
|
color: 'var(--color-text, var(--soil))',
|
||||||
|
outline: 'none',
|
||||||
|
transition: 'border-color 180ms ease',
|
||||||
|
minHeight: '48px',
|
||||||
|
boxSizing: 'border-box',
|
||||||
|
};
|
||||||
|
|
||||||
|
const labelStyle: React.CSSProperties = {
|
||||||
|
fontFamily: 'var(--font-accent, monospace)',
|
||||||
|
fontSize: '11px',
|
||||||
|
fontWeight: 400,
|
||||||
|
letterSpacing: '0.08em',
|
||||||
|
textTransform: 'uppercase',
|
||||||
|
color: 'var(--color-muted, var(--soil-faint))',
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
width: '100%',
|
width: '100%',
|
||||||
maxWidth: '420px',
|
maxWidth: '420px',
|
||||||
background: 'var(--color-surface)',
|
background: 'var(--color-surface, var(--linen))',
|
||||||
border: '1px solid var(--color-border)',
|
border: '1px solid var(--color-border, var(--parchment))',
|
||||||
borderRadius: '20px',
|
borderRadius: '20px',
|
||||||
padding: 'clamp(28px, 5vw, 44px)',
|
padding: 'clamp(28px, 5vw, 44px)',
|
||||||
boxShadow: '0 8px 40px rgba(45,27,14,0.08)',
|
boxShadow: '0 8px 40px rgba(45,27,14,0.08)',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{/* Brand mark */}
|
{/* Marca */}
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: '8px', marginBottom: '28px' }}>
|
<div style={{ display: 'flex', alignItems: 'center', gap: '8px', marginBottom: '28px' }}>
|
||||||
<PawPrint size={20} style={{ color: 'var(--color-terra)' }} />
|
<PawPrint size={20} style={{ color: 'var(--color-terra, var(--terra))' }} />
|
||||||
<span
|
<span
|
||||||
style={{
|
style={{
|
||||||
fontFamily: 'var(--font-playfair, Georgia, serif)',
|
fontFamily: 'var(--font-playfair, Georgia, serif)',
|
||||||
fontStyle: 'italic',
|
fontStyle: 'italic',
|
||||||
fontWeight: 700,
|
fontWeight: 700,
|
||||||
fontSize: '18px',
|
fontSize: '20px',
|
||||||
color: 'var(--color-terra)',
|
color: 'var(--color-terra, var(--terra))',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
PetLink
|
PetLink
|
||||||
@@ -48,39 +93,59 @@ export default function LoginPage() {
|
|||||||
|
|
||||||
<h1
|
<h1
|
||||||
style={{
|
style={{
|
||||||
fontFamily: 'var(--font-playfair, Georgia, serif)',
|
fontFamily: 'var(--font-playfair, Georgia, serif)',
|
||||||
fontWeight: 900,
|
fontWeight: 900,
|
||||||
fontSize: '28px',
|
fontSize: '28px',
|
||||||
color: 'var(--color-text)',
|
color: 'var(--color-text, var(--soil))',
|
||||||
marginBottom: '6px',
|
marginBottom: '6px',
|
||||||
lineHeight: 1.15,
|
lineHeight: 1.15,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Bem-vindo de volta.
|
Iniciar sessão.
|
||||||
</h1>
|
</h1>
|
||||||
<p style={{ fontFamily: 'var(--font-body)', fontSize: '14px', color: 'var(--color-muted)', marginBottom: '28px' }}>
|
<p
|
||||||
Não tens conta?{' '}
|
style={{
|
||||||
<Link
|
fontFamily: 'var(--font-body)',
|
||||||
href="/auth/register"
|
fontSize: '14px',
|
||||||
style={{ color: 'var(--color-terra)', fontWeight: 500, textDecoration: 'underline', textDecorationColor: 'transparent', transition: 'text-decoration-color 180ms ease' }}
|
color: 'var(--color-muted, var(--soil-mid))',
|
||||||
onMouseEnter={e => ((e.target as HTMLElement).style.textDecorationColor = 'var(--color-terra)')}
|
marginBottom: '28px',
|
||||||
onMouseLeave={e => ((e.target as HTMLElement).style.textDecorationColor = 'transparent')}
|
}}
|
||||||
>
|
>
|
||||||
Criar conta grátis
|
Ainda não tens conta?{' '}
|
||||||
|
<Link href="/auth/register" style={{ color: 'var(--color-terra, var(--terra))', fontWeight: 500 }}>
|
||||||
|
Registar
|
||||||
</Link>
|
</Link>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
{/* Erro */}
|
||||||
|
{error && (
|
||||||
|
<div
|
||||||
|
role="alert"
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: '10px',
|
||||||
|
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(--color-terra, var(--terra))',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<AlertCircle size={16} style={{ flexShrink: 0 }} />
|
||||||
|
{error}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<form onSubmit={handleSubmit} noValidate style={{ display: 'flex', flexDirection: 'column', gap: '16px' }}>
|
<form onSubmit={handleSubmit} noValidate style={{ display: 'flex', flexDirection: 'column', gap: '16px' }}>
|
||||||
{/* Email field */}
|
{/* Email */}
|
||||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '6px' }}>
|
<div style={{ display: 'flex', flexDirection: 'column', gap: '6px' }}>
|
||||||
<label
|
<label htmlFor="login-email" style={labelStyle}>Email</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' }}>
|
<div style={{ position: 'relative' }}>
|
||||||
<Mail size={15} style={{ position: 'absolute', left: '14px', top: '50%', transform: 'translateY(-50%)', color: 'var(--color-muted)', pointerEvents: 'none' }} />
|
<Mail size={15} style={{ position: 'absolute', left: '14px', top: '50%', transform: 'translateY(-50%)', color: 'var(--color-muted, var(--soil-faint))', pointerEvents: 'none' }} />
|
||||||
<input
|
<input
|
||||||
id="login-email"
|
id="login-email"
|
||||||
type="email"
|
type="email"
|
||||||
@@ -89,83 +154,42 @@ export default function LoginPage() {
|
|||||||
placeholder="o.teu@email.pt"
|
placeholder="o.teu@email.pt"
|
||||||
required
|
required
|
||||||
autoComplete="email"
|
autoComplete="email"
|
||||||
style={{
|
style={inputStyle}
|
||||||
width: '100%',
|
onFocus={e => (e.target.style.borderColor = 'var(--color-terra, var(--terra))')}
|
||||||
padding: '12px 16px 12px 40px',
|
onBlur={e => (e.target.style.borderColor = 'var(--color-border, var(--parchment))')}
|
||||||
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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Password field */}
|
{/* Password */}
|
||||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '6px' }}>
|
<div style={{ display: 'flex', flexDirection: 'column', gap: '6px' }}>
|
||||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||||
<label
|
<label htmlFor="login-password" style={labelStyle}>Palavra-passe</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
|
<Link
|
||||||
href="/auth/forgot-password"
|
href="/auth/forgot-password"
|
||||||
style={{ fontFamily: 'var(--font-body)', fontSize: '12px', color: 'var(--color-terra)', textDecoration: 'none' }}
|
style={{ fontFamily: 'var(--font-body)', fontSize: '12px', color: 'var(--color-terra, var(--terra))', textDecoration: 'none' }}
|
||||||
>
|
>
|
||||||
Esqueceste-a?
|
Esqueceste-a?
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
<div style={{ position: 'relative' }}>
|
<div style={{ position: 'relative' }}>
|
||||||
<Lock size={15} style={{ position: 'absolute', left: '14px', top: '50%', transform: 'translateY(-50%)', color: 'var(--color-muted)', pointerEvents: 'none' }} />
|
<Lock size={15} style={{ position: 'absolute', left: '14px', top: '50%', transform: 'translateY(-50%)', color: 'var(--color-muted, var(--soil-faint))', pointerEvents: 'none' }} />
|
||||||
<input
|
<input
|
||||||
id="login-password"
|
id="login-password"
|
||||||
type={showPass ? 'text' : 'password'}
|
type={showPass ? 'text' : 'password'}
|
||||||
value={password}
|
value={password}
|
||||||
onChange={e => setPassword(e.target.value)}
|
onChange={e => setPassword(e.target.value)}
|
||||||
placeholder="••••••••"
|
placeholder="A tua palavra-passe"
|
||||||
required
|
required
|
||||||
autoComplete="current-password"
|
autoComplete="current-password"
|
||||||
style={{
|
style={{ ...inputStyle, paddingRight: '44px' }}
|
||||||
width: '100%',
|
onFocus={e => (e.target.style.borderColor = 'var(--color-terra, var(--terra))')}
|
||||||
padding: '12px 44px 12px 40px',
|
onBlur={e => (e.target.style.borderColor = 'var(--color-border, var(--parchment))')}
|
||||||
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
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => setShowPass(s => !s)}
|
onClick={() => setShowPass(s => !s)}
|
||||||
style={{
|
style={{ position: 'absolute', right: '12px', top: '50%', transform: 'translateY(-50%)', background: 'none', border: 'none', cursor: 'pointer', color: 'var(--color-muted, var(--soil-faint))', padding: '4px', display: 'flex', alignItems: 'center' }}
|
||||||
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'}
|
aria-label={showPass ? 'Ocultar palavra-passe' : 'Mostrar palavra-passe'}
|
||||||
>
|
>
|
||||||
{showPass ? <EyeOff size={16} /> : <Eye size={16} />}
|
{showPass ? <EyeOff size={16} /> : <Eye size={16} />}
|
||||||
@@ -177,49 +201,44 @@ export default function LoginPage() {
|
|||||||
<button
|
<button
|
||||||
id="login-submit"
|
id="login-submit"
|
||||||
type="submit"
|
type="submit"
|
||||||
className="btn-primary"
|
|
||||||
disabled={loading}
|
disabled={loading}
|
||||||
style={{
|
style={{
|
||||||
width: '100%',
|
width: '100%',
|
||||||
justifyContent: 'center',
|
padding: '14px 24px',
|
||||||
marginTop: '8px',
|
background: loading ? 'var(--color-muted, var(--soil-faint))' : 'var(--color-terra, var(--terra))',
|
||||||
opacity: loading ? 0.75 : 1,
|
color: 'white',
|
||||||
cursor: loading ? 'not-allowed' : 'pointer',
|
border: 'none',
|
||||||
|
borderRadius: '100px',
|
||||||
|
fontFamily: 'var(--font-body)',
|
||||||
|
fontWeight: 600,
|
||||||
|
fontSize: '15px',
|
||||||
|
cursor: loading ? 'not-allowed' : 'pointer',
|
||||||
|
transition: 'all 200ms ease',
|
||||||
|
marginTop: '8px',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
gap: '8px',
|
||||||
}}
|
}}
|
||||||
aria-label="Entrar na conta PetLink"
|
aria-label="Entrar na conta PetLink"
|
||||||
>
|
>
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<span style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
|
<>
|
||||||
<span style={{
|
<span style={{ display: 'inline-block', width: '16px', height: '16px', border: '2px solid rgba(255,255,255,0.4)', borderTopColor: 'white', borderRadius: '50%', animation: 'spin 0.7s linear infinite' }} />
|
||||||
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…
|
A entrar…
|
||||||
</span>
|
</>
|
||||||
) : (
|
) : 'Iniciar sessão'}
|
||||||
'Entrar'
|
|
||||||
)}
|
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
{/* Registar */}
|
||||||
|
<p style={{ textAlign: 'center', fontFamily: 'var(--font-body)', fontSize: '13px', color: 'var(--color-muted, var(--soil-mid))', marginTop: '4px' }}>
|
||||||
|
Não tens conta?{' '}
|
||||||
|
<Link href="/auth/register" style={{ color: 'var(--color-terra, var(--terra))', fontWeight: 500 }}>
|
||||||
|
Criar conta grátis
|
||||||
|
</Link>
|
||||||
|
</p>
|
||||||
</form>
|
</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>
|
<style>{`@keyframes spin { to { transform: rotate(360deg); } }`}</style>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import type { Metadata } from 'next';
|
import type { Metadata } from 'next';
|
||||||
import { Playfair_Display, Lora, Fragment_Mono } from 'next/font/google';
|
import { Playfair_Display, Lora, Fragment_Mono } from 'next/font/google';
|
||||||
import Script from 'next/script';
|
import Script from 'next/script';
|
||||||
|
import Providers from './providers';
|
||||||
import './globals.css';
|
import './globals.css';
|
||||||
|
|
||||||
const playfair = Playfair_Display({
|
const playfair = Playfair_Display({
|
||||||
@@ -73,7 +74,7 @@ export default function RootLayout({
|
|||||||
fontFamily: 'var(--font-lora, var(--font-body, Lora, Georgia, serif))',
|
fontFamily: 'var(--font-lora, var(--font-body, Lora, Georgia, serif))',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{children}
|
<Providers>{children}</Providers>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
);
|
);
|
||||||
|
|||||||
7
app/providers.tsx
Normal file
7
app/providers.tsx
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { SessionProvider } from 'next-auth/react';
|
||||||
|
|
||||||
|
export default function Providers({ children }: { children: React.ReactNode }) {
|
||||||
|
return <SessionProvider>{children}</SessionProvider>;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user