576 lines
19 KiB
TypeScript
576 lines
19 KiB
TypeScript
'use client';
|
|
|
|
import { useState, useEffect } from 'react';
|
|
import { notFound } from 'next/navigation';
|
|
import Image from 'next/image';
|
|
import Link from 'next/link';
|
|
import {
|
|
MapPin,
|
|
Clock,
|
|
CheckCircle2,
|
|
AlertTriangle,
|
|
ChevronRight,
|
|
ChevronLeft,
|
|
Heart,
|
|
X,
|
|
PawPrint,
|
|
Phone,
|
|
Mail,
|
|
ArrowLeft,
|
|
} from 'lucide-react';
|
|
import { MOCK_ANIMALS, formatAge, formatSex, Animal } from '@/lib/mock-data';
|
|
|
|
// TODO: substituir por query Prisma quando DATABASE_URL estiver configurada
|
|
|
|
function ConfirmationOverlay({ animal, onClose }: { animal: Animal; onClose: () => void }) {
|
|
const [pieces] = useState(() =>
|
|
Array.from({ length: 20 }, (_, i) => ({
|
|
id: i,
|
|
x: Math.random() * 100,
|
|
color: ['#C4501A', '#E8952A', '#3D6B4F', '#2D1B0E'][Math.floor(Math.random() * 4)],
|
|
delay: Math.random() * 0.8,
|
|
duration: 1.5 + Math.random() * 1.5,
|
|
size: 6 + Math.random() * 10,
|
|
}))
|
|
);
|
|
|
|
return (
|
|
<div className="confirmation-overlay" role="dialog" aria-modal="true" aria-label="Reserva confirmada">
|
|
{/* Confetti */}
|
|
{pieces.map(p => (
|
|
<div
|
|
key={p.id}
|
|
className="confetti-piece"
|
|
style={{
|
|
left: `${p.x}%`,
|
|
top: 0,
|
|
width: `${p.size}px`,
|
|
height: `${p.size}px`,
|
|
background: p.color,
|
|
animationDuration: `${p.duration}s`,
|
|
animationDelay: `${p.delay}s`,
|
|
}}
|
|
aria-hidden="true"
|
|
/>
|
|
))}
|
|
|
|
{/* Card */}
|
|
<div
|
|
style={{
|
|
background: 'var(--color-bg)',
|
|
border: '1px solid var(--color-border)',
|
|
borderRadius: '24px',
|
|
padding: 'clamp(32px, 5vw, 48px)',
|
|
maxWidth: '420px',
|
|
width: '90%',
|
|
textAlign: 'center',
|
|
position: 'relative',
|
|
zIndex: 1,
|
|
boxShadow: '0 24px 80px rgba(45,27,14,0.15)',
|
|
animation: 'bounceIn 500ms cubic-bezier(0.34, 1.56, 0.64, 1) both',
|
|
}}
|
|
>
|
|
{/* Paw icon */}
|
|
<div
|
|
style={{
|
|
width: '72px',
|
|
height: '72px',
|
|
background: 'var(--color-terra-light)',
|
|
borderRadius: '50%',
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
margin: '0 auto 20px',
|
|
animation: 'pawBounce 1.5s ease-in-out infinite',
|
|
}}
|
|
aria-hidden="true"
|
|
>
|
|
<PawPrint size={32} style={{ color: 'var(--color-terra)' }} />
|
|
</div>
|
|
|
|
<h2
|
|
style={{
|
|
fontFamily: 'var(--font-playfair, Georgia, serif)',
|
|
fontWeight: 900,
|
|
fontSize: '28px',
|
|
color: 'var(--color-text)',
|
|
marginBottom: '8px',
|
|
}}
|
|
>
|
|
Pedido enviado! 🎉
|
|
</h2>
|
|
<p
|
|
style={{
|
|
fontFamily: 'var(--font-body)',
|
|
fontSize: '16px',
|
|
color: 'var(--color-muted)',
|
|
lineHeight: 1.6,
|
|
marginBottom: '24px',
|
|
}}
|
|
>
|
|
O teu pedido de adopção do{' '}
|
|
<strong style={{ color: 'var(--color-text)', fontWeight: 600 }}>{animal.name}</strong>{' '}
|
|
foi enviado para o{' '}
|
|
<strong style={{ color: 'var(--color-text)', fontWeight: 600 }}>{animal.shelter.name}</strong>.
|
|
<br /><br />
|
|
Vais receber um email de confirmação em breve.
|
|
</p>
|
|
|
|
<div style={{ display: 'flex', flexDirection: 'column', gap: '10px' }}>
|
|
<button
|
|
className="btn-primary"
|
|
style={{ background: 'var(--color-sage)', width: '100%', justifyContent: 'center' }}
|
|
onClick={onClose}
|
|
id="confirmation-view-reservation"
|
|
>
|
|
<CheckCircle2 size={16} />
|
|
Ver a minha reserva
|
|
</button>
|
|
<button
|
|
className="btn-ghost"
|
|
style={{ width: '100%', justifyContent: 'center' }}
|
|
onClick={onClose}
|
|
>
|
|
Voltar à ficha
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function Lightbox({ photos, initial, onClose }: { photos: string[]; initial: number; onClose: () => void }) {
|
|
const [current, setCurrent] = useState(initial);
|
|
|
|
useEffect(() => {
|
|
const handler = (e: KeyboardEvent) => {
|
|
if (e.key === 'Escape') onClose();
|
|
if (e.key === 'ArrowRight') setCurrent(c => (c + 1) % photos.length);
|
|
if (e.key === 'ArrowLeft') setCurrent(c => (c - 1 + photos.length) % photos.length);
|
|
};
|
|
window.addEventListener('keydown', handler);
|
|
return () => window.removeEventListener('keydown', handler);
|
|
}, [photos.length, onClose]);
|
|
|
|
return (
|
|
<div
|
|
className="lightbox"
|
|
onClick={onClose}
|
|
role="dialog"
|
|
aria-modal="true"
|
|
aria-label="Galeria de fotos"
|
|
>
|
|
<button
|
|
onClick={onClose}
|
|
style={{
|
|
position: 'absolute',
|
|
top: '16px',
|
|
right: '16px',
|
|
background: 'rgba(255,255,255,0.1)',
|
|
border: 'none',
|
|
borderRadius: '50%',
|
|
width: '44px',
|
|
height: '44px',
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
cursor: 'pointer',
|
|
color: 'white',
|
|
}}
|
|
aria-label="Fechar galeria"
|
|
>
|
|
<X size={20} />
|
|
</button>
|
|
|
|
{photos.length > 1 && (
|
|
<>
|
|
<button
|
|
onClick={e => { e.stopPropagation(); setCurrent(c => (c - 1 + photos.length) % photos.length); }}
|
|
style={{
|
|
position: 'absolute',
|
|
left: '16px',
|
|
top: '50%',
|
|
transform: 'translateY(-50%)',
|
|
background: 'rgba(255,255,255,0.1)',
|
|
border: 'none',
|
|
borderRadius: '50%',
|
|
width: '44px',
|
|
height: '44px',
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
cursor: 'pointer',
|
|
color: 'white',
|
|
}}
|
|
aria-label="Foto anterior"
|
|
>
|
|
<ChevronLeft size={22} />
|
|
</button>
|
|
<button
|
|
onClick={e => { e.stopPropagation(); setCurrent(c => (c + 1) % photos.length); }}
|
|
style={{
|
|
position: 'absolute',
|
|
right: '16px',
|
|
top: '50%',
|
|
transform: 'translateY(-50%)',
|
|
background: 'rgba(255,255,255,0.1)',
|
|
border: 'none',
|
|
borderRadius: '50%',
|
|
width: '44px',
|
|
height: '44px',
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
cursor: 'pointer',
|
|
color: 'white',
|
|
}}
|
|
aria-label="Próxima foto"
|
|
>
|
|
<ChevronRight size={22} />
|
|
</button>
|
|
</>
|
|
)}
|
|
|
|
<div
|
|
style={{ position: 'relative', maxWidth: '90vw', maxHeight: '85vh', width: '100%' }}
|
|
onClick={e => e.stopPropagation()}
|
|
>
|
|
<Image
|
|
src={photos[current]}
|
|
alt={`Foto ${current + 1} de ${photos.length}`}
|
|
width={900}
|
|
height={675}
|
|
style={{ objectFit: 'contain', borderRadius: '12px', maxHeight: '85vh', width: 'auto' }}
|
|
/>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export default function AnimalDetailPage({ params }: { params: { id: string } }) {
|
|
const animal = MOCK_ANIMALS.find(a => a.id === params.id);
|
|
const [mainPhoto, setMainPhoto] = useState(0);
|
|
const [lightboxOpen, setLightboxOpen] = useState(false);
|
|
const [lightboxPhoto, setLightboxPhoto] = useState(0);
|
|
const [showConfirmation, setShowConfirmation] = useState(false);
|
|
|
|
if (!animal) return notFound();
|
|
|
|
const ageLabel = formatAge(animal.ageMonths);
|
|
|
|
const openLightbox = (index: number) => {
|
|
setLightboxPhoto(index);
|
|
setLightboxOpen(true);
|
|
};
|
|
|
|
return (
|
|
<>
|
|
{/* Lightbox */}
|
|
{lightboxOpen && (
|
|
<Lightbox
|
|
photos={animal.photos}
|
|
initial={lightboxPhoto}
|
|
onClose={() => setLightboxOpen(false)}
|
|
/>
|
|
)}
|
|
|
|
{/* Confirmation overlay */}
|
|
{showConfirmation && (
|
|
<ConfirmationOverlay animal={animal} onClose={() => setShowConfirmation(false)} />
|
|
)}
|
|
|
|
<div style={{ background: 'var(--color-bg)' }}>
|
|
{/* Breadcrumb */}
|
|
<div className="container" style={{ paddingTop: '24px', paddingBottom: '8px' }}>
|
|
<Link
|
|
href="/main/animals"
|
|
style={{
|
|
display: 'inline-flex',
|
|
alignItems: 'center',
|
|
gap: '6px',
|
|
fontFamily: 'var(--font-body)',
|
|
fontSize: '14px',
|
|
color: 'var(--color-muted)',
|
|
textDecoration: 'none',
|
|
transition: 'color 180ms ease',
|
|
}}
|
|
onMouseEnter={e => ((e.currentTarget as HTMLElement).style.color = 'var(--color-terra)')}
|
|
onMouseLeave={e => ((e.currentTarget as HTMLElement).style.color = 'var(--color-muted)')}
|
|
aria-label="Voltar à listagem de animais"
|
|
>
|
|
<ArrowLeft size={14} />
|
|
Todos os animais
|
|
</Link>
|
|
</div>
|
|
|
|
{/* Main content */}
|
|
<div className="container" style={{ padding: '16px 16px 80px' }}>
|
|
<div
|
|
style={{
|
|
display: 'grid',
|
|
gridTemplateColumns: '1fr',
|
|
gap: '40px',
|
|
}}
|
|
className="lg:grid-cols-2"
|
|
>
|
|
{/* Left — Gallery */}
|
|
<div>
|
|
{/* Main image */}
|
|
<div
|
|
style={{
|
|
borderRadius: '16px',
|
|
overflow: 'hidden',
|
|
aspectRatio: '4/3',
|
|
position: 'relative',
|
|
cursor: 'zoom-in',
|
|
marginBottom: '12px',
|
|
background: 'var(--color-linen)',
|
|
}}
|
|
onClick={() => openLightbox(mainPhoto)}
|
|
role="button"
|
|
aria-label={`Ver foto ${mainPhoto + 1} em tamanho completo`}
|
|
tabIndex={0}
|
|
onKeyDown={e => e.key === 'Enter' && openLightbox(mainPhoto)}
|
|
>
|
|
<Image
|
|
src={animal.photos[mainPhoto]}
|
|
alt={`${animal.name}, ${animal.breed} ${animal.sex === 'MALE' ? 'macho' : 'fêmea'}, foto ${mainPhoto + 1}`}
|
|
fill
|
|
style={{ objectFit: 'cover', transition: 'transform 400ms ease' }}
|
|
priority
|
|
/>
|
|
{animal.urgent && (
|
|
<div style={{ position: 'absolute', top: '16px', left: '16px' }}>
|
|
<span className="badge-urgent">
|
|
<AlertTriangle size={10} />
|
|
Urgente
|
|
</span>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* Thumbnails */}
|
|
{animal.photos.length > 1 && (
|
|
<div style={{ display: 'flex', gap: '8px', flexWrap: 'wrap' }}>
|
|
{animal.photos.map((photo, i) => (
|
|
<button
|
|
key={i}
|
|
onClick={() => setMainPhoto(i)}
|
|
style={{
|
|
width: '72px',
|
|
height: '54px',
|
|
borderRadius: '8px',
|
|
overflow: 'hidden',
|
|
position: 'relative',
|
|
border: `2px solid ${i === mainPhoto ? 'var(--color-terra)' : 'var(--color-border)'}`,
|
|
cursor: 'pointer',
|
|
transition: 'border-color 180ms ease',
|
|
padding: 0,
|
|
background: 'none',
|
|
}}
|
|
aria-label={`Ver foto ${i + 1}`}
|
|
aria-pressed={i === mainPhoto}
|
|
>
|
|
<Image
|
|
src={photo}
|
|
alt={`Miniatura ${i + 1}`}
|
|
fill
|
|
style={{ objectFit: 'cover' }}
|
|
sizes="72px"
|
|
/>
|
|
</button>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* Right — Info */}
|
|
<div style={{ display: 'flex', flexDirection: 'column', gap: '24px' }}>
|
|
{/* Name & tags */}
|
|
<div>
|
|
<h1
|
|
style={{
|
|
fontFamily: 'var(--font-playfair, Georgia, serif)',
|
|
fontWeight: 900,
|
|
fontSize: 'clamp(36px, 5vw, 52px)',
|
|
color: 'var(--color-text)',
|
|
lineHeight: 1.05,
|
|
marginBottom: '12px',
|
|
}}
|
|
>
|
|
{animal.name}
|
|
</h1>
|
|
|
|
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '8px', marginBottom: '16px' }}>
|
|
{[animal.breed, ageLabel, animal.sex === 'MALE' ? '♂ Macho' : '♀ Fêmea'].map((tag) => (
|
|
<span key={tag} className="tag">
|
|
{tag}
|
|
</span>
|
|
))}
|
|
{animal.sterilized && (
|
|
<span
|
|
className="tag"
|
|
style={{ color: 'var(--color-sage)', borderColor: 'var(--color-sage)', background: 'transparent' }}
|
|
>
|
|
<CheckCircle2 size={11} style={{ marginRight: '4px' }} />
|
|
Esterilizado/a
|
|
</span>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
{/* About section */}
|
|
<div>
|
|
<h2
|
|
style={{
|
|
fontFamily: 'var(--font-playfair, Georgia, serif)',
|
|
fontWeight: 700,
|
|
fontSize: '20px',
|
|
color: 'var(--color-text)',
|
|
marginBottom: '12px',
|
|
}}
|
|
>
|
|
<em style={{ fontStyle: 'italic' }}>Sobre</em> o {animal.name}
|
|
</h2>
|
|
<p
|
|
style={{
|
|
fontFamily: 'var(--font-body)',
|
|
fontSize: '16px',
|
|
lineHeight: 1.7,
|
|
color: 'var(--color-muted)',
|
|
}}
|
|
>
|
|
{animal.description}
|
|
</p>
|
|
</div>
|
|
|
|
{/* Shelter info */}
|
|
<div
|
|
style={{
|
|
background: 'var(--color-surface)',
|
|
border: '1px solid var(--color-border)',
|
|
borderRadius: '12px',
|
|
padding: '20px',
|
|
display: 'flex',
|
|
flexDirection: 'column',
|
|
gap: '12px',
|
|
}}
|
|
>
|
|
<h3
|
|
style={{
|
|
fontFamily: 'var(--font-body)',
|
|
fontWeight: 600,
|
|
fontSize: '14px',
|
|
color: 'var(--color-text)',
|
|
textTransform: 'uppercase',
|
|
letterSpacing: '0.06em',
|
|
}}
|
|
>
|
|
Canil responsável
|
|
</h3>
|
|
|
|
<div style={{ display: 'flex', flexDirection: 'column', gap: '8px' }}>
|
|
<div style={{ display: 'flex', alignItems: 'flex-start', gap: '10px' }}>
|
|
<MapPin size={15} style={{ color: 'var(--color-terra)', flexShrink: 0, marginTop: '2px' }} />
|
|
<div>
|
|
<p style={{ fontWeight: 600, fontSize: '15px', color: 'var(--color-text)', fontFamily: 'var(--font-body)' }}>
|
|
{animal.shelter.name}
|
|
</p>
|
|
<p style={{ fontSize: '13px', color: 'var(--color-muted)', fontFamily: 'var(--font-body)' }}>
|
|
{animal.shelter.address}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div style={{ display: 'flex', alignItems: 'center', gap: '10px' }}>
|
|
<Clock size={15} style={{ color: 'var(--color-terra)', flexShrink: 0 }} />
|
|
<p style={{ fontSize: '14px', color: 'var(--color-muted)', fontFamily: 'var(--font-body)' }}>
|
|
{animal.shelter.openHours}
|
|
</p>
|
|
</div>
|
|
|
|
<div style={{ display: 'flex', alignItems: 'center', gap: '10px' }}>
|
|
<Phone size={15} style={{ color: 'var(--color-terra)', flexShrink: 0 }} />
|
|
<a
|
|
href={`tel:${animal.shelter.phone.replace(/\s/g, '')}`}
|
|
style={{ fontSize: '14px', color: 'var(--color-terra)', fontFamily: 'var(--font-body)', textDecoration: 'none' }}
|
|
>
|
|
{animal.shelter.phone}
|
|
</a>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* CTA — desktop */}
|
|
<div
|
|
id="adoptar"
|
|
style={{ display: 'flex', flexDirection: 'column', gap: '10px' }}
|
|
className="hidden-mobile-cta"
|
|
>
|
|
<button
|
|
className="btn-primary"
|
|
style={{ justifyContent: 'center', fontSize: '15px', padding: '16px 32px' }}
|
|
onClick={() => setShowConfirmation(true)}
|
|
id="animal-adopt-btn"
|
|
aria-label={`Adoptar ${animal.name} — enviar pedido ao ${animal.shelter.name}`}
|
|
>
|
|
<Heart size={16} />
|
|
Adoptar o {animal.name}
|
|
<ChevronRight size={16} />
|
|
</button>
|
|
<p
|
|
style={{
|
|
fontFamily: 'var(--font-body)',
|
|
fontSize: '12px',
|
|
color: 'var(--color-muted)',
|
|
textAlign: 'center',
|
|
}}
|
|
>
|
|
Precisas de ter conta para adoptar. É gratuito e leva 2 minutos.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Sticky CTA bar — mobile */}
|
|
<div
|
|
style={{
|
|
position: 'fixed',
|
|
bottom: 0,
|
|
left: 0,
|
|
right: 0,
|
|
background: 'var(--color-bg)',
|
|
borderTop: '1px solid var(--color-border)',
|
|
padding: '12px 16px',
|
|
display: 'flex',
|
|
gap: '10px',
|
|
zIndex: 50,
|
|
backdropFilter: 'blur(12px)',
|
|
}}
|
|
className="mobile-sticky-cta"
|
|
>
|
|
<button
|
|
className="btn-secondary"
|
|
style={{ flex: 1, justifyContent: 'center', minHeight: '48px' }}
|
|
aria-label={`Guardar ${animal.name} nos favoritos`}
|
|
>
|
|
<Heart size={16} />
|
|
Guardar
|
|
</button>
|
|
<button
|
|
className="btn-primary"
|
|
style={{ flex: 2, justifyContent: 'center', minHeight: '48px' }}
|
|
onClick={() => setShowConfirmation(true)}
|
|
id="animal-adopt-mobile-btn"
|
|
aria-label={`Adoptar ${animal.name}`}
|
|
>
|
|
Adoptar o {animal.name}
|
|
<ChevronRight size={16} />
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</>
|
|
);
|
|
}
|