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:
189
app/main/account/page.tsx
Normal file
189
app/main/account/page.tsx
Normal file
@@ -0,0 +1,189 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
import Link from 'next/link';
|
||||
import { User, MapPin, Calendar, Heart, Gift, Settings, LogOut, ChevronRight } from 'lucide-react';
|
||||
|
||||
interface UserProfile {
|
||||
id: string;
|
||||
name: string;
|
||||
email: string;
|
||||
district: string;
|
||||
role: string;
|
||||
emailVerified: boolean;
|
||||
createdAt: string;
|
||||
_count: { reservations: number; donations: number };
|
||||
}
|
||||
|
||||
interface Reservation {
|
||||
id: string;
|
||||
date: string;
|
||||
status: string;
|
||||
animal: {
|
||||
name: string;
|
||||
species: string;
|
||||
photos: { url: string }[];
|
||||
shelter: { name: string; district: string };
|
||||
};
|
||||
}
|
||||
|
||||
export default function AccountPage() {
|
||||
const [user, setUser] = useState<UserProfile | null>(null);
|
||||
const [reservations, setReservations] = useState<Reservation[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
Promise.all([
|
||||
fetch('/api/users/me').then(r => r.json()),
|
||||
fetch('/api/reservations').then(r => r.json()),
|
||||
])
|
||||
.then(([u, r]) => {
|
||||
setUser(u);
|
||||
setReservations(Array.isArray(r) ? r : []);
|
||||
})
|
||||
.finally(() => setLoading(false));
|
||||
}, []);
|
||||
|
||||
const statusColor: Record<string, string> = {
|
||||
PENDING: 'var(--amber)',
|
||||
CONFIRMED: 'var(--sage)',
|
||||
CANCELLED: 'var(--soil-faint)',
|
||||
COMPLETED: 'var(--sage)',
|
||||
};
|
||||
const statusLabel: Record<string, string> = {
|
||||
PENDING: 'Pendente',
|
||||
CONFIRMED: 'Confirmada',
|
||||
CANCELLED: 'Cancelada',
|
||||
COMPLETED: 'Concluída',
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div style={{ maxWidth: '800px', margin: '0 auto', padding: 'var(--space-7) var(--space-5)' }}>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '16px' }}>
|
||||
{[1,2,3].map(i => (
|
||||
<div key={i} className="skeleton" style={{ height: '80px', borderRadius: '12px' }} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!user) {
|
||||
return (
|
||||
<div style={{ textAlign: 'center', padding: 'var(--space-9) var(--space-5)' }}>
|
||||
<p style={{ fontFamily: 'var(--font-body)', color: 'var(--soil-mid)', marginBottom: '20px' }}>
|
||||
Precisas de estar autenticado para ver esta página.
|
||||
</p>
|
||||
<Link href="/auth/login" className="btn btn-primary">Entrar</Link>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={{ maxWidth: '800px', margin: '0 auto', padding: 'var(--space-7) var(--space-5)' }}>
|
||||
{/* Header da conta */}
|
||||
<div style={{ marginBottom: 'var(--space-7)' }}>
|
||||
<p className="eyebrow" style={{ marginBottom: '8px' }}>A tua conta</p>
|
||||
<h1 style={{ fontFamily: 'var(--font-display)', fontWeight: 900, fontSize: 'clamp(28px, 5vw, 40px)', color: 'var(--soil)', letterSpacing: '-0.02em', lineHeight: 1.1 }}>
|
||||
Olá, <em style={{ fontStyle: 'italic', color: 'var(--terra)' }}>{user.name.split(' ')[0]}.</em>
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
{/* Stats */}
|
||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(160px, 1fr))', gap: 'var(--space-4)', marginBottom: 'var(--space-7)' }}>
|
||||
{[
|
||||
{ icon: Heart, label: 'Reservas', value: user._count.reservations },
|
||||
{ icon: Gift, label: 'Doações', value: user._count.donations },
|
||||
].map(({ icon: Icon, label, value }) => (
|
||||
<div key={label} style={{ background: 'var(--linen)', border: '1px solid var(--parchment)', borderRadius: '16px', padding: '20px', display: 'flex', alignItems: 'center', gap: '14px', boxShadow: 'var(--shadow-warm-sm)' }}>
|
||||
<div style={{ width: '40px', height: '40px', borderRadius: '12px', background: 'var(--terra-glow)', display: 'flex', alignItems: 'center', justifyContent: 'center', flexShrink: 0 }}>
|
||||
<Icon size={18} style={{ color: 'var(--terra)' }} />
|
||||
</div>
|
||||
<div>
|
||||
<div style={{ fontFamily: 'var(--font-accent)', fontSize: '11px', color: 'var(--soil-faint)', textTransform: 'uppercase', letterSpacing: '0.08em', marginBottom: '4px' }}>{label}</div>
|
||||
<div style={{ fontFamily: 'var(--font-display)', fontWeight: 700, fontSize: '22px', color: 'var(--soil)' }}>{value}</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Informação pessoal */}
|
||||
<section style={{ background: 'var(--linen)', border: '1px solid var(--parchment)', borderRadius: '16px', padding: '24px', marginBottom: 'var(--space-6)', boxShadow: 'var(--shadow-warm-sm)' }}>
|
||||
<h2 style={{ fontFamily: 'var(--font-display)', fontWeight: 700, fontSize: '18px', color: 'var(--soil)', marginBottom: '20px', fontStyle: 'italic' }}>
|
||||
Dados pessoais
|
||||
</h2>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '14px' }}>
|
||||
{[
|
||||
{ icon: User, label: 'Nome', value: user.name },
|
||||
{ icon: Settings, label: 'Email', value: user.email },
|
||||
{ icon: MapPin, label: 'Distrito', value: user.district },
|
||||
{ icon: Calendar, label: 'Membro desde', value: new Date(user.createdAt).toLocaleDateString('pt-PT', { year: 'numeric', month: 'long' }) },
|
||||
].map(({ icon: Icon, label, value }) => (
|
||||
<div key={label} style={{ display: 'flex', alignItems: 'center', gap: '12px', padding: '12px', background: 'var(--cream)', borderRadius: '10px' }}>
|
||||
<Icon size={16} style={{ color: 'var(--soil-faint)', flexShrink: 0 }} />
|
||||
<div style={{ flex: 1 }}>
|
||||
<div style={{ fontFamily: 'var(--font-accent)', fontSize: '10px', color: 'var(--soil-faint)', textTransform: 'uppercase', letterSpacing: '0.08em' }}>{label}</div>
|
||||
<div style={{ fontFamily: 'var(--font-body)', fontSize: '15px', color: 'var(--soil)', marginTop: '2px' }}>{value}</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Histórico de reservas */}
|
||||
<section>
|
||||
<h2 style={{ fontFamily: 'var(--font-display)', fontWeight: 700, fontSize: '18px', color: 'var(--soil)', marginBottom: '16px', fontStyle: 'italic' }}>
|
||||
As tuas reservas
|
||||
</h2>
|
||||
|
||||
{reservations.length === 0 ? (
|
||||
<div style={{ background: 'var(--linen)', border: '1px solid var(--parchment)', borderRadius: '16px', padding: '40px', textAlign: 'center' }}>
|
||||
<div style={{ fontSize: '40px', marginBottom: '12px', opacity: 0.4 }}>🐾</div>
|
||||
<p style={{ fontFamily: 'var(--font-body)', color: 'var(--soil-mid)', marginBottom: '20px' }}>
|
||||
Ainda não tens reservas. Começa por explorar os animais disponíveis.
|
||||
</p>
|
||||
<Link href="/main/animals" className="btn btn-primary" style={{ justifyContent: 'center' }}>
|
||||
Explorar animais
|
||||
</Link>
|
||||
</div>
|
||||
) : (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '12px' }}>
|
||||
{reservations.map((res) => (
|
||||
<div
|
||||
key={res.id}
|
||||
style={{ background: 'var(--linen)', border: '1px solid var(--parchment)', borderRadius: '12px', padding: '16px 20px', display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: '12px', boxShadow: 'var(--shadow-warm-sm)', flexWrap: 'wrap' }}
|
||||
>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '14px' }}>
|
||||
{res.animal.photos[0] && (
|
||||
<img src={res.animal.photos[0].url} alt={res.animal.name} style={{ width: '48px', height: '48px', borderRadius: '10px', objectFit: 'cover', flexShrink: 0 }} />
|
||||
)}
|
||||
<div>
|
||||
<div style={{ fontFamily: 'var(--font-display)', fontWeight: 700, fontSize: '16px', color: 'var(--soil)' }}>{res.animal.name}</div>
|
||||
<div style={{ fontFamily: 'var(--font-body)', fontStyle: 'italic', fontSize: '13px', color: 'var(--soil-mid)' }}>
|
||||
{res.animal.shelter.name} · {new Date(res.date).toLocaleDateString('pt-PT', { day: 'numeric', month: 'long', year: 'numeric' })}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<span style={{ fontFamily: 'var(--font-accent)', fontSize: '10px', letterSpacing: '0.08em', textTransform: 'uppercase', color: statusColor[res.status] ?? 'var(--soil-mid)', padding: '5px 12px', background: `${statusColor[res.status]}18`, borderRadius: '100px', border: `1px solid ${statusColor[res.status]}40` }}>
|
||||
{statusLabel[res.status] ?? res.status}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
|
||||
{/* Logout */}
|
||||
<div style={{ marginTop: 'var(--space-7)', paddingTop: 'var(--space-5)', borderTop: '1px solid var(--parchment)' }}>
|
||||
<Link
|
||||
href="/api/auth/signout"
|
||||
style={{ display: 'inline-flex', alignItems: 'center', gap: '8px', fontFamily: 'var(--font-body)', fontSize: '14px', color: 'var(--soil-mid)', textDecoration: 'none', padding: '10px 0' }}
|
||||
>
|
||||
<LogOut size={15} />
|
||||
Terminar sessão
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user