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:
245
app/main/animals/page.tsx
Normal file
245
app/main/animals/page.tsx
Normal file
@@ -0,0 +1,245 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useEffect, useMemo } from 'react';
|
||||
import { PawPrint, SlidersHorizontal } from 'lucide-react';
|
||||
import AnimalCard from '@/components/animals/AnimalCard';
|
||||
import FilterChips, { FILTER_OPTIONS } from '@/components/animals/FilterChips';
|
||||
import { MOCK_ANIMALS, Animal } from '@/lib/mock-data';
|
||||
|
||||
// TODO: substituir por query Prisma quando DATABASE_URL estiver configurada
|
||||
|
||||
function filterAnimals(animals: Animal[], filter: string): Animal[] {
|
||||
switch (filter) {
|
||||
case 'dog': return animals.filter(a => a.species === 'DOG');
|
||||
case 'cat': return animals.filter(a => a.species === 'CAT');
|
||||
case 'urgent': return animals.filter(a => a.urgent);
|
||||
case 'lisboa': return animals.filter(a => a.shelter.district.toLowerCase() === 'lisboa');
|
||||
case 'porto': return animals.filter(a => a.shelter.district.toLowerCase() === 'porto');
|
||||
case 'braga': return animals.filter(a => a.shelter.district.toLowerCase() === 'braga');
|
||||
case 'sintra': return animals.filter(a => a.shelter.district.toLowerCase() === 'sintra');
|
||||
case 'male': return animals.filter(a => a.sex === 'MALE');
|
||||
case 'female': return animals.filter(a => a.sex === 'FEMALE');
|
||||
case 'sterilized': return animals.filter(a => a.sterilized);
|
||||
default: return animals;
|
||||
}
|
||||
}
|
||||
|
||||
function SkeletonCard() {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
borderRadius: '16px',
|
||||
overflow: 'hidden',
|
||||
background: 'var(--color-surface)',
|
||||
border: '1px solid var(--color-border)',
|
||||
}}
|
||||
>
|
||||
<div className="skeleton" style={{ aspectRatio: '4/3', width: '100%' }} />
|
||||
<div style={{ padding: '16px', display: 'flex', flexDirection: 'column', gap: '10px' }}>
|
||||
<div className="skeleton" style={{ height: '26px', width: '60%', borderRadius: '6px' }} />
|
||||
<div className="skeleton" style={{ height: '16px', width: '80%', borderRadius: '6px' }} />
|
||||
<div className="skeleton" style={{ height: '14px', width: '50%', borderRadius: '6px' }} />
|
||||
<div style={{ display: 'flex', gap: '8px', marginTop: '8px' }}>
|
||||
<div className="skeleton" style={{ height: '40px', flex: 1, borderRadius: '100px' }} />
|
||||
<div className="skeleton" style={{ height: '40px', flex: 1, borderRadius: '100px' }} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function AnimalsPage() {
|
||||
const [activeFilter, setActiveFilter] = useState('all');
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
// Simulate async load
|
||||
useEffect(() => {
|
||||
const t = setTimeout(() => setLoading(false), 700);
|
||||
return () => clearTimeout(t);
|
||||
}, []);
|
||||
|
||||
// Re-show skeleton briefly when filter changes
|
||||
const handleFilterChange = (id: string) => {
|
||||
setLoading(true);
|
||||
setActiveFilter(id);
|
||||
setTimeout(() => setLoading(false), 350);
|
||||
};
|
||||
|
||||
const animals = useMemo(() => filterAnimals(MOCK_ANIMALS, activeFilter), [activeFilter]);
|
||||
|
||||
const urgentCount = MOCK_ANIMALS.filter(a => a.urgent).length;
|
||||
|
||||
return (
|
||||
<div style={{ background: 'var(--color-bg)', minHeight: '100vh' }}>
|
||||
{/* Page header */}
|
||||
<div
|
||||
style={{
|
||||
borderBottom: '1px solid var(--color-border)',
|
||||
padding: 'clamp(32px, 5vw, 56px) 0 0',
|
||||
background: 'var(--color-bg)',
|
||||
}}
|
||||
>
|
||||
<div className="container">
|
||||
{/* Breadcrumb */}
|
||||
<p
|
||||
style={{
|
||||
fontFamily: 'var(--font-mono, monospace)',
|
||||
fontSize: '11px',
|
||||
fontWeight: 500,
|
||||
letterSpacing: '0.1em',
|
||||
textTransform: 'uppercase',
|
||||
color: 'var(--color-muted)',
|
||||
marginBottom: '12px',
|
||||
}}
|
||||
>
|
||||
PawLink · Adopção
|
||||
</p>
|
||||
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'flex-start',
|
||||
justifyContent: 'space-between',
|
||||
flexWrap: 'wrap',
|
||||
gap: '16px',
|
||||
marginBottom: '24px',
|
||||
}}
|
||||
>
|
||||
<div>
|
||||
<h1
|
||||
style={{
|
||||
fontFamily: 'var(--font-playfair, Georgia, serif)',
|
||||
fontWeight: 900,
|
||||
fontSize: 'clamp(32px, 5vw, 48px)',
|
||||
color: 'var(--color-text)',
|
||||
lineHeight: 1.1,
|
||||
marginBottom: '8px',
|
||||
}}
|
||||
>
|
||||
Animais disponíveis
|
||||
</h1>
|
||||
<p
|
||||
style={{
|
||||
fontFamily: 'var(--font-body)',
|
||||
fontSize: '16px',
|
||||
color: 'var(--color-muted)',
|
||||
lineHeight: 1.5,
|
||||
}}
|
||||
>
|
||||
{MOCK_ANIMALS.length.toLocaleString('pt-PT')} animais à espera
|
||||
{urgentCount > 0 && (
|
||||
<span
|
||||
style={{
|
||||
marginLeft: '10px',
|
||||
fontFamily: 'var(--font-mono, monospace)',
|
||||
fontSize: '11px',
|
||||
fontWeight: 500,
|
||||
background: 'var(--color-amber)',
|
||||
color: 'var(--color-soil)',
|
||||
padding: '3px 8px',
|
||||
borderRadius: '100px',
|
||||
letterSpacing: '0.06em',
|
||||
textTransform: 'uppercase',
|
||||
}}
|
||||
>
|
||||
{urgentCount} urgentes
|
||||
</span>
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<button
|
||||
className="btn-ghost"
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '8px',
|
||||
border: '1.5px solid var(--color-border)',
|
||||
borderRadius: '12px',
|
||||
}}
|
||||
aria-label="Abrir filtros avançados"
|
||||
>
|
||||
<SlidersHorizontal size={16} />
|
||||
Filtros avançados
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Filter chips */}
|
||||
<FilterChips
|
||||
options={FILTER_OPTIONS}
|
||||
active={activeFilter}
|
||||
onChange={handleFilterChange}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Grid */}
|
||||
<div className="container" style={{ padding: '40px 16px' }}>
|
||||
{loading ? (
|
||||
<div className="animal-grid">
|
||||
{Array.from({ length: 6 }).map((_, i) => (
|
||||
<SkeletonCard key={i} />
|
||||
))}
|
||||
</div>
|
||||
) : animals.length > 0 ? (
|
||||
<>
|
||||
<div className="animal-grid">
|
||||
{animals.map((animal, index) => (
|
||||
<AnimalCard key={animal.id} animal={animal} index={index} />
|
||||
))}
|
||||
</div>
|
||||
<p
|
||||
style={{
|
||||
textAlign: 'center',
|
||||
marginTop: '40px',
|
||||
fontFamily: 'var(--font-mono, monospace)',
|
||||
fontSize: '12px',
|
||||
color: 'var(--color-muted)',
|
||||
letterSpacing: '0.06em',
|
||||
}}
|
||||
>
|
||||
{animals.length} de {MOCK_ANIMALS.length} animais • Dados de demonstração
|
||||
</p>
|
||||
</>
|
||||
) : (
|
||||
<div
|
||||
style={{
|
||||
textAlign: 'center',
|
||||
padding: '80px 24px',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
gap: '16px',
|
||||
}}
|
||||
>
|
||||
<PawPrint
|
||||
size={48}
|
||||
style={{ color: 'var(--color-fog)', strokeWidth: 1.5 }}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<h2
|
||||
style={{
|
||||
fontFamily: 'var(--font-display)',
|
||||
fontSize: '24px',
|
||||
fontWeight: 700,
|
||||
color: 'var(--color-text)',
|
||||
}}
|
||||
>
|
||||
Nenhum animal encontrado
|
||||
</h2>
|
||||
<p style={{ color: 'var(--color-muted)', fontFamily: 'var(--font-body)', fontSize: '16px' }}>
|
||||
Tenta um filtro diferente ou vê todos os animais disponíveis.
|
||||
</p>
|
||||
<button
|
||||
onClick={() => handleFilterChange('all')}
|
||||
className="btn-secondary"
|
||||
style={{ marginTop: '8px' }}
|
||||
>
|
||||
Remover filtros
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user