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:
173
app/shelter/dashboard/page.tsx
Normal file
173
app/shelter/dashboard/page.tsx
Normal file
@@ -0,0 +1,173 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
import Link from 'next/link';
|
||||
import { PawPrint, Plus, Calendar, Settings, ChevronRight, AlertTriangle } from 'lucide-react';
|
||||
|
||||
interface Animal {
|
||||
id: string;
|
||||
name: string;
|
||||
species: string;
|
||||
breed: string;
|
||||
status: string;
|
||||
urgent: boolean;
|
||||
photos: { url: string }[];
|
||||
}
|
||||
|
||||
interface Reservation {
|
||||
id: string;
|
||||
date: string;
|
||||
status: string;
|
||||
user: { name: string; email: string };
|
||||
animal: { name: string };
|
||||
}
|
||||
|
||||
const statusBg: Record<string, string> = {
|
||||
AVAILABLE: 'rgba(58,99,71,0.1)',
|
||||
RESERVED: 'rgba(212,136,10,0.1)',
|
||||
ADOPTED: 'rgba(35,20,8,0.08)',
|
||||
};
|
||||
const statusColor: Record<string, string> = {
|
||||
AVAILABLE: 'var(--sage)',
|
||||
RESERVED: 'var(--amber)',
|
||||
ADOPTED: 'var(--soil-mid)',
|
||||
};
|
||||
const statusLabel: Record<string, string> = {
|
||||
AVAILABLE: 'Disponível',
|
||||
RESERVED: 'Reservado',
|
||||
ADOPTED: 'Adoptado',
|
||||
};
|
||||
|
||||
export default function ShelterDashboardPage() {
|
||||
const [animals, setAnimals] = useState<Animal[]>([]);
|
||||
const [reservations, setReservations] = useState<Reservation[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
// TODO: substituir por API routes de shelter quando autenticação de canil estiver completa
|
||||
// Por agora usa mock
|
||||
setTimeout(() => {
|
||||
setAnimals([
|
||||
{ id: 'mock-1', name: 'Bobi', species: 'DOG', breed: 'Labrador', status: 'AVAILABLE', urgent: false, photos: [{ url: 'https://images.unsplash.com/photo-1587300003388-59208cc962cb?w=120&q=80' }] },
|
||||
{ id: 'mock-2', name: 'Luna', species: 'CAT', breed: 'Siamês', status: 'RESERVED', urgent: true, photos: [{ url: 'https://images.unsplash.com/photo-1514888286974-6c03e2ca1dba?w=120&q=80' }] },
|
||||
{ id: 'mock-3', name: 'Nala', species: 'DOG', breed: 'Golden Retriever', status: 'AVAILABLE', urgent: false, photos: [{ url: 'https://images.unsplash.com/photo-1601758125946-6ec2ef64daf8?w=120&q=80' }] },
|
||||
]);
|
||||
setReservations([
|
||||
{ id: 'r1', date: new Date(Date.now() + 86400000 * 2).toISOString(), status: 'PENDING', user: { name: 'Ana Costa', email: 'ana@email.pt' }, animal: { name: 'Luna' } },
|
||||
{ id: 'r2', date: new Date(Date.now() + 86400000 * 5).toISOString(), status: 'CONFIRMED', user: { name: 'João Silva', email: 'joao@email.pt' }, animal: { name: 'Bobi' } },
|
||||
]);
|
||||
setLoading(false);
|
||||
}, 600);
|
||||
}, []);
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div style={{ maxWidth: '960px', margin: '0 auto', padding: 'var(--space-7) var(--space-5)', display: 'flex', flexDirection: 'column', gap: '16px' }}>
|
||||
{[1,2,3].map(i => <div key={i} className="skeleton" style={{ height: '80px', borderRadius: '12px' }} />)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const available = animals.filter(a => a.status === 'AVAILABLE').length;
|
||||
const reserved = animals.filter(a => a.status === 'RESERVED').length;
|
||||
const urgent = animals.filter(a => a.urgent).length;
|
||||
const pending = reservations.filter(r => r.status === 'PENDING').length;
|
||||
|
||||
return (
|
||||
<div style={{ maxWidth: '960px', margin: '0 auto', padding: 'var(--space-7) var(--space-5)' }}>
|
||||
{/* Header */}
|
||||
<div style={{ marginBottom: 'var(--space-7)' }}>
|
||||
<p className="eyebrow" style={{ marginBottom: '8px' }}>Painel do canil</p>
|
||||
<div style={{ display: 'flex', alignItems: 'flex-end', justifyContent: 'space-between', flexWrap: 'wrap', gap: '16px' }}>
|
||||
<h1 style={{ fontFamily: 'var(--font-display)', fontWeight: 900, fontSize: 'clamp(28px, 5vw, 40px)', color: 'var(--soil)', letterSpacing: '-0.02em', lineHeight: 1.1 }}>
|
||||
Dashboard
|
||||
</h1>
|
||||
<Link href="/shelter/animals/new" className="btn btn-primary" style={{ gap: '8px' }}>
|
||||
<Plus size={15} />
|
||||
Adicionar animal
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Stats */}
|
||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(140px, 1fr))', gap: 'var(--space-4)', marginBottom: 'var(--space-7)' }}>
|
||||
{[
|
||||
{ label: 'Disponíveis', value: available, color: 'var(--sage)', emoji: '🐾' },
|
||||
{ label: 'Reservados', value: reserved, color: 'var(--amber)', emoji: '📅' },
|
||||
{ label: 'Urgentes', value: urgent, color: 'var(--terra)', emoji: '⚠️' },
|
||||
{ label: 'Reservas pend.', value: pending, color: 'var(--soil-mid)', emoji: '📋' },
|
||||
].map(({ label, value, color, emoji }) => (
|
||||
<div key={label} style={{ background: 'var(--linen)', border: '1px solid var(--parchment)', borderRadius: '16px', padding: '20px', boxShadow: 'var(--shadow-warm-sm)', textAlign: 'center' }}>
|
||||
<div style={{ fontSize: '28px', marginBottom: '8px' }}>{emoji}</div>
|
||||
<div style={{ fontFamily: 'var(--font-display)', fontWeight: 700, fontSize: '28px', color, lineHeight: 1 }}>{value}</div>
|
||||
<div style={{ fontFamily: 'var(--font-accent)', fontSize: '10px', color: 'var(--soil-faint)', textTransform: 'uppercase', letterSpacing: '0.08em', marginTop: '6px' }}>{label}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Reservas pendentes */}
|
||||
{reservations.length > 0 && (
|
||||
<section style={{ marginBottom: 'var(--space-7)' }}>
|
||||
<h2 style={{ fontFamily: 'var(--font-display)', fontWeight: 700, fontStyle: 'italic', fontSize: '20px', color: 'var(--soil)', marginBottom: '16px' }}>
|
||||
Próximas reservas
|
||||
</h2>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '10px' }}>
|
||||
{reservations.map(r => (
|
||||
<div key={r.id} style={{ background: 'var(--linen)', border: '1px solid var(--parchment)', borderRadius: '12px', padding: '16px 20px', display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: '12px', flexWrap: 'wrap', boxShadow: 'var(--shadow-warm-sm)' }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '12px' }}>
|
||||
<Calendar size={18} style={{ color: 'var(--terra)', flexShrink: 0 }} />
|
||||
<div>
|
||||
<div style={{ fontFamily: 'var(--font-body)', fontWeight: 500, fontSize: '15px', color: 'var(--soil)' }}>
|
||||
{r.user.name} → <strong>{r.animal.name}</strong>
|
||||
</div>
|
||||
<div style={{ fontFamily: 'var(--font-body)', fontStyle: 'italic', fontSize: '13px', color: 'var(--soil-mid)' }}>
|
||||
{new Date(r.date).toLocaleDateString('pt-PT', { weekday: 'long', day: 'numeric', month: 'long' })}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<span style={{ fontFamily: 'var(--font-accent)', fontSize: '10px', letterSpacing: '0.08em', textTransform: 'uppercase', color: r.status === 'PENDING' ? 'var(--amber)' : 'var(--sage)', padding: '5px 12px', background: r.status === 'PENDING' ? 'rgba(212,136,10,0.1)' : 'rgba(58,99,71,0.1)', borderRadius: '100px' }}>
|
||||
{r.status === 'PENDING' ? 'Pendente' : 'Confirmada'}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
|
||||
{/* Animais */}
|
||||
<section>
|
||||
<h2 style={{ fontFamily: 'var(--font-display)', fontWeight: 700, fontStyle: 'italic', fontSize: '20px', color: 'var(--soil)', marginBottom: '16px' }}>
|
||||
Os teus animais
|
||||
</h2>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '10px' }}>
|
||||
{animals.map(animal => (
|
||||
<div key={animal.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' }}>
|
||||
{animal.photos[0] && (
|
||||
<img src={animal.photos[0].url} alt={animal.name} style={{ width: '48px', height: '48px', borderRadius: '10px', objectFit: 'cover', flexShrink: 0 }} />
|
||||
)}
|
||||
<div>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
|
||||
<span style={{ fontFamily: 'var(--font-display)', fontWeight: 700, fontSize: '16px', color: 'var(--soil)' }}>{animal.name}</span>
|
||||
{animal.urgent && <AlertTriangle size={14} style={{ color: 'var(--amber)' }} aria-label="Urgente" />}
|
||||
</div>
|
||||
<div style={{ fontFamily: 'var(--font-accent)', fontSize: '10px', color: 'var(--soil-faint)', letterSpacing: '0.06em', textTransform: 'uppercase', marginTop: '2px' }}>
|
||||
{animal.species === 'DOG' ? 'Cão' : 'Gato'} · {animal.breed}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '12px' }}>
|
||||
<span style={{ fontFamily: 'var(--font-accent)', fontSize: '10px', letterSpacing: '0.08em', textTransform: 'uppercase', color: statusColor[animal.status], padding: '5px 12px', background: statusBg[animal.status], borderRadius: '100px' }}>
|
||||
{statusLabel[animal.status]}
|
||||
</span>
|
||||
<Link href={`/shelter/animals/${animal.id}/edit`} style={{ display: 'flex', alignItems: 'center', color: 'var(--soil-faint)', padding: '4px' }} aria-label={`Editar ${animal.name}`}>
|
||||
<Settings size={15} />
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user