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:
161
components/animals/AnimalCard.tsx
Normal file
161
components/animals/AnimalCard.tsx
Normal file
@@ -0,0 +1,161 @@
|
||||
'use client';
|
||||
|
||||
import Link from 'next/link';
|
||||
import Image from 'next/image';
|
||||
import { MapPin, ChevronRight, AlertTriangle } from 'lucide-react';
|
||||
import { Animal, formatAge } from '@/lib/mock-data';
|
||||
|
||||
interface AnimalCardProps {
|
||||
animal: Animal;
|
||||
index?: number;
|
||||
}
|
||||
|
||||
export default function AnimalCard({ animal, index = 0 }: AnimalCardProps) {
|
||||
const ageLabel = formatAge(animal.ageMonths);
|
||||
const sexSymbol = animal.sex === 'MALE' ? '♂' : '♀';
|
||||
const sterilizedLabel = animal.sterilized ? '· Esterilizado ✓' : '';
|
||||
const subline = `${animal.breed} · ${ageLabel} ${sterilizedLabel}`.trim();
|
||||
|
||||
return (
|
||||
<article
|
||||
className="animal-card"
|
||||
style={{ animationDelay: `${index * 80}ms` }}
|
||||
aria-label={`${animal.name}, ${animal.breed}, ${ageLabel}, disponível no ${animal.shelter.name}`}
|
||||
>
|
||||
{/* Fotografia */}
|
||||
<div style={{ position: 'relative', aspectRatio: '3/2', overflow: 'hidden' }}>
|
||||
<Image
|
||||
src={animal.photos[0]}
|
||||
alt={`${animal.name} — ${animal.breed} ${animal.sex === 'MALE' ? 'macho' : 'fêmea'}, ${ageLabel}`}
|
||||
fill
|
||||
sizes="(max-width: 640px) 100vw, (max-width: 1024px) 50vw, 33vw"
|
||||
style={{
|
||||
objectFit: 'cover',
|
||||
transition: 'transform 400ms ease',
|
||||
}}
|
||||
onMouseEnter={e => { (e.currentTarget as HTMLElement).style.transform = 'scale(1.04)'; }}
|
||||
onMouseLeave={e => { (e.currentTarget as HTMLElement).style.transform = 'scale(1)'; }}
|
||||
/>
|
||||
|
||||
{/* Gradiente base da foto */}
|
||||
<div
|
||||
aria-hidden="true"
|
||||
style={{
|
||||
position: 'absolute',
|
||||
inset: 0,
|
||||
background: 'linear-gradient(to top, rgba(35,20,8,0.35) 0%, transparent 50%)',
|
||||
pointerEvents: 'none',
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Badge urgente — top right */}
|
||||
{animal.urgent && (
|
||||
<div style={{ position: 'absolute', top: '12px', right: '12px' }}>
|
||||
<span className="badge-urgent">
|
||||
<AlertTriangle size={9} strokeWidth={2.5} />
|
||||
Urgente
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Corpo do card */}
|
||||
<div
|
||||
style={{
|
||||
padding: '16px',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: '8px',
|
||||
flex: 1,
|
||||
}}
|
||||
>
|
||||
{/* Nome + sexo */}
|
||||
<div style={{ display: 'flex', alignItems: 'baseline', justifyContent: 'space-between', gap: '8px' }}>
|
||||
<h3
|
||||
style={{
|
||||
fontFamily: 'var(--font-display)',
|
||||
fontWeight: 700,
|
||||
fontSize: '20px',
|
||||
color: 'var(--soil)',
|
||||
lineHeight: 1.1,
|
||||
letterSpacing: '-0.01em',
|
||||
}}
|
||||
>
|
||||
{animal.name}
|
||||
</h3>
|
||||
<span
|
||||
style={{
|
||||
fontFamily: 'var(--font-display)',
|
||||
fontWeight: 700,
|
||||
fontSize: '16px',
|
||||
color: 'var(--soil-mid)',
|
||||
flexShrink: 0,
|
||||
}}
|
||||
aria-label={animal.sex === 'MALE' ? 'Macho' : 'Fêmea'}
|
||||
>
|
||||
{sexSymbol}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Raça · Idade · Esterilizado */}
|
||||
<p
|
||||
style={{
|
||||
fontFamily: 'var(--font-accent)',
|
||||
fontSize: '10px',
|
||||
fontWeight: 400,
|
||||
color: 'var(--soil-faint)',
|
||||
letterSpacing: '0.08em',
|
||||
textTransform: 'uppercase',
|
||||
lineHeight: 1.4,
|
||||
}}
|
||||
>
|
||||
{subline}
|
||||
</p>
|
||||
|
||||
{/* Localização */}
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '5px',
|
||||
fontFamily: 'var(--font-body)',
|
||||
fontStyle: 'italic',
|
||||
fontSize: '14px',
|
||||
color: 'var(--soil-mid)',
|
||||
marginTop: '2px',
|
||||
}}
|
||||
>
|
||||
<MapPin size={12} style={{ color: 'var(--terra)', flexShrink: 0 }} aria-hidden="true" />
|
||||
<span>{animal.shelter.name}, {animal.shelter.district}</span>
|
||||
</div>
|
||||
|
||||
{/* Spacer */}
|
||||
<div style={{ flex: 1 }} />
|
||||
|
||||
{/* Separador */}
|
||||
<hr style={{ border: 'none', borderTop: '1px solid var(--parchment)', margin: '4px 0' }} />
|
||||
|
||||
{/* Acções */}
|
||||
<div style={{ display: 'flex', gap: '8px' }}>
|
||||
<Link
|
||||
href={`/main/animals/${animal.id}`}
|
||||
className="btn btn-secondary"
|
||||
style={{ flex: 1, justifyContent: 'center', padding: '10px 12px', fontSize: '11px', minHeight: '40px' }}
|
||||
aria-label={`Ver ficha completa de ${animal.name}`}
|
||||
>
|
||||
Ver mais
|
||||
</Link>
|
||||
<Link
|
||||
href={`/main/animals/${animal.id}#adoptar`}
|
||||
className="btn btn-primary"
|
||||
style={{ flex: 1, justifyContent: 'center', padding: '10px 12px', fontSize: '11px', minHeight: '40px', gap: '4px' }}
|
||||
aria-label={`Iniciar processo de adopção de ${animal.name}`}
|
||||
>
|
||||
Adoptar
|
||||
<ChevronRight size={12} strokeWidth={2.5} />
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
);
|
||||
}
|
||||
60
components/animals/FilterChips.tsx
Normal file
60
components/animals/FilterChips.tsx
Normal file
@@ -0,0 +1,60 @@
|
||||
'use client';
|
||||
|
||||
import { PawPrint, Dog, Cat, MapPin, AlertTriangle } from 'lucide-react';
|
||||
|
||||
export interface FilterOption {
|
||||
id: string;
|
||||
label: string;
|
||||
icon?: React.ReactNode;
|
||||
}
|
||||
|
||||
export const FILTER_OPTIONS: FilterOption[] = [
|
||||
{ id: 'all', label: 'Todos', icon: <PawPrint size={12} /> },
|
||||
{ id: 'dog', label: 'Cães', icon: <Dog size={12} /> },
|
||||
{ id: 'cat', label: 'Gatos', icon: <Cat size={12} /> },
|
||||
{ id: 'urgent', label: 'Urgente', icon: <AlertTriangle size={12} /> },
|
||||
{ id: 'lisboa', label: 'Lisboa', icon: <MapPin size={12} /> },
|
||||
{ id: 'porto', label: 'Porto', icon: <MapPin size={12} /> },
|
||||
{ id: 'braga', label: 'Braga', icon: <MapPin size={12} /> },
|
||||
{ id: 'sintra', label: 'Sintra', icon: <MapPin size={12} /> },
|
||||
{ id: 'male', label: 'Macho ♂' },
|
||||
{ id: 'female', label: 'Fêmea ♀' },
|
||||
{ id: 'sterilized', label: 'Esterilizado' },
|
||||
];
|
||||
|
||||
interface FilterChipsProps {
|
||||
options: FilterOption[];
|
||||
active: string;
|
||||
onChange: (id: string) => void;
|
||||
}
|
||||
|
||||
export default function FilterChips({ options, active, onChange }: FilterChipsProps) {
|
||||
return (
|
||||
<div
|
||||
role="group"
|
||||
aria-label="Filtros de animais"
|
||||
className="filters-strip"
|
||||
>
|
||||
{options.map((option) => {
|
||||
const isActive = active === option.id;
|
||||
return (
|
||||
<button
|
||||
key={option.id}
|
||||
id={`filter-${option.id}`}
|
||||
className={`filter-chip${isActive ? ' active' : ''}`}
|
||||
onClick={() => onChange(option.id)}
|
||||
aria-pressed={isActive}
|
||||
aria-label={`Filtrar por: ${option.label}`}
|
||||
>
|
||||
{option.icon && (
|
||||
<span style={{ display: 'flex', alignItems: 'center' }} aria-hidden="true">
|
||||
{option.icon}
|
||||
</span>
|
||||
)}
|
||||
{option.label}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
175
components/layout/Footer.tsx
Normal file
175
components/layout/Footer.tsx
Normal file
@@ -0,0 +1,175 @@
|
||||
import Link from 'next/link';
|
||||
import { PawPrint, Heart } from 'lucide-react';
|
||||
|
||||
export default function Footer() {
|
||||
return (
|
||||
<footer
|
||||
style={{
|
||||
background: 'var(--color-soil)',
|
||||
color: 'var(--color-cream)',
|
||||
padding: '48px 0 24px',
|
||||
marginTop: 'auto',
|
||||
}}
|
||||
>
|
||||
<div className="container">
|
||||
{/* Top row */}
|
||||
<div
|
||||
style={{
|
||||
display: 'grid',
|
||||
gridTemplateColumns: '1fr',
|
||||
gap: '40px',
|
||||
marginBottom: '40px',
|
||||
}}
|
||||
className="md:grid-cols-3"
|
||||
>
|
||||
{/* Brand */}
|
||||
<div>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '8px', marginBottom: '12px' }}>
|
||||
<PawPrint size={20} style={{ color: 'var(--color-terra)' }} />
|
||||
<span
|
||||
style={{
|
||||
fontFamily: 'var(--font-playfair, Georgia, serif)',
|
||||
fontStyle: 'italic',
|
||||
fontWeight: 700,
|
||||
fontSize: '20px',
|
||||
color: 'var(--color-terra)',
|
||||
}}
|
||||
>
|
||||
PawLink
|
||||
</span>
|
||||
</div>
|
||||
<p
|
||||
style={{
|
||||
fontFamily: 'var(--font-body)',
|
||||
fontSize: '14px',
|
||||
lineHeight: 1.7,
|
||||
color: 'rgba(250,246,240,0.65)',
|
||||
maxWidth: '260px',
|
||||
}}
|
||||
>
|
||||
Conectamos adoptantes, doadores e canis em todo o território português. Cada adopção é uma vida transformada.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Links */}
|
||||
<div>
|
||||
<p
|
||||
style={{
|
||||
fontFamily: 'var(--font-mono, monospace)',
|
||||
fontSize: '10px',
|
||||
letterSpacing: '0.12em',
|
||||
textTransform: 'uppercase',
|
||||
color: 'rgba(250,246,240,0.4)',
|
||||
marginBottom: '16px',
|
||||
}}
|
||||
>
|
||||
Plataforma
|
||||
</p>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '10px' }}>
|
||||
{[
|
||||
{ href: '/main/animals', label: 'Explorar animais' },
|
||||
{ href: '/main/donate', label: 'Fazer uma doação' },
|
||||
{ href: '/main/shelters', label: 'Canis parceiros' },
|
||||
{ href: '/auth/register', label: 'Criar conta' },
|
||||
].map(({ href, label }) => (
|
||||
<Link
|
||||
key={href}
|
||||
href={href}
|
||||
style={{
|
||||
fontFamily: 'var(--font-body)',
|
||||
fontSize: '14px',
|
||||
color: 'rgba(250,246,240,0.7)',
|
||||
textDecoration: 'none',
|
||||
transition: 'color 180ms ease',
|
||||
}}
|
||||
onMouseEnter={e => ((e.target as HTMLElement).style.color = 'var(--color-terra)')}
|
||||
onMouseLeave={e => ((e.target as HTMLElement).style.color = 'rgba(250,246,240,0.7)')}
|
||||
>
|
||||
{label}
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Legal */}
|
||||
<div>
|
||||
<p
|
||||
style={{
|
||||
fontFamily: 'var(--font-mono, monospace)',
|
||||
fontSize: '10px',
|
||||
letterSpacing: '0.12em',
|
||||
textTransform: 'uppercase',
|
||||
color: 'rgba(250,246,240,0.4)',
|
||||
marginBottom: '16px',
|
||||
}}
|
||||
>
|
||||
Legal
|
||||
</p>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '10px' }}>
|
||||
{[
|
||||
{ href: '/privacy', label: 'Política de Privacidade' },
|
||||
{ href: '/terms', label: 'Termos de Utilização' },
|
||||
{ href: '/rgpd', label: 'RGPD' },
|
||||
{ href: '/contact', label: 'Contacto' },
|
||||
].map(({ href, label }) => (
|
||||
<Link
|
||||
key={href}
|
||||
href={href}
|
||||
style={{
|
||||
fontFamily: 'var(--font-body)',
|
||||
fontSize: '14px',
|
||||
color: 'rgba(250,246,240,0.7)',
|
||||
textDecoration: 'none',
|
||||
transition: 'color 180ms ease',
|
||||
}}
|
||||
onMouseEnter={e => ((e.target as HTMLElement).style.color = 'var(--color-terra)')}
|
||||
onMouseLeave={e => ((e.target as HTMLElement).style.color = 'rgba(250,246,240,0.7)')}
|
||||
>
|
||||
{label}
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Divider */}
|
||||
<div
|
||||
style={{
|
||||
borderTop: '1px solid rgba(250,246,240,0.1)',
|
||||
paddingTop: '24px',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: '8px',
|
||||
alignItems: 'center',
|
||||
textAlign: 'center',
|
||||
}}
|
||||
>
|
||||
<p
|
||||
style={{
|
||||
fontFamily: 'var(--font-body)',
|
||||
fontSize: '13px',
|
||||
color: 'rgba(250,246,240,0.4)',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '6px',
|
||||
}}
|
||||
>
|
||||
Feito com
|
||||
<Heart size={12} style={{ color: 'var(--color-terra)', fill: 'var(--color-terra)' }} />
|
||||
em Portugal · PawLink © {new Date().getFullYear()}
|
||||
</p>
|
||||
<p
|
||||
style={{
|
||||
fontFamily: 'var(--font-mono, monospace)',
|
||||
fontSize: '11px',
|
||||
color: 'rgba(250,246,240,0.25)',
|
||||
letterSpacing: '0.06em',
|
||||
}}
|
||||
>
|
||||
PAP 2024/2025
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
);
|
||||
}
|
||||
55
components/layout/Header.tsx
Normal file
55
components/layout/Header.tsx
Normal file
@@ -0,0 +1,55 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
import Link from 'next/link';
|
||||
import { PawPrint } from 'lucide-react';
|
||||
import SideMenu from './SideMenu';
|
||||
|
||||
export default function Header() {
|
||||
const [scrolled, setScrolled] = useState(false);
|
||||
const [menuOpen, setMenuOpen] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const handleScroll = () => setScrolled(window.scrollY > 8);
|
||||
window.addEventListener('scroll', handleScroll, { passive: true });
|
||||
return () => window.removeEventListener('scroll', handleScroll);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (menuOpen) {
|
||||
document.body.style.overflow = 'hidden';
|
||||
} else {
|
||||
document.body.style.overflow = '';
|
||||
}
|
||||
return () => { document.body.style.overflow = ''; };
|
||||
}, [menuOpen]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<header className={`header${scrolled ? ' scrolled' : ''}`}>
|
||||
{/* Logo */}
|
||||
<Link href="/" className="logo" aria-label="PawLink — Início">
|
||||
<PawPrint size={22} strokeWidth={2.5} />
|
||||
PawLink
|
||||
</Link>
|
||||
|
||||
{/* Hambúrguer animado */}
|
||||
<button
|
||||
id="menu-toggle"
|
||||
className={`menu-btn${menuOpen ? ' open' : ''}`}
|
||||
onClick={() => setMenuOpen(v => !v)}
|
||||
aria-label={menuOpen ? 'Fechar menu' : 'Abrir menu de navegação'}
|
||||
aria-expanded={menuOpen}
|
||||
aria-controls="side-menu"
|
||||
>
|
||||
<span />
|
||||
<span />
|
||||
<span />
|
||||
</button>
|
||||
</header>
|
||||
|
||||
{/* Side Menu — sempre montado, abre/fecha via CSS */}
|
||||
<SideMenu open={menuOpen} onClose={() => setMenuOpen(false)} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
210
components/layout/SideMenu.tsx
Normal file
210
components/layout/SideMenu.tsx
Normal file
@@ -0,0 +1,210 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
import Link from 'next/link';
|
||||
import {
|
||||
X,
|
||||
PawPrint,
|
||||
Heart,
|
||||
Gift,
|
||||
Home,
|
||||
Search,
|
||||
User,
|
||||
Sun,
|
||||
Moon,
|
||||
ChevronRight,
|
||||
Shield,
|
||||
} from 'lucide-react';
|
||||
|
||||
interface SideMenuProps {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
export default function SideMenu({ open, onClose }: SideMenuProps) {
|
||||
const [dark, setDark] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
setDark(
|
||||
document.documentElement.classList.contains('dark') ||
|
||||
document.documentElement.getAttribute('data-theme') === 'dark'
|
||||
);
|
||||
}, [open]);
|
||||
|
||||
useEffect(() => {
|
||||
const handler = (e: KeyboardEvent) => {
|
||||
if (e.key === 'Escape' && open) onClose();
|
||||
};
|
||||
window.addEventListener('keydown', handler);
|
||||
return () => window.removeEventListener('keydown', handler);
|
||||
}, [open, onClose]);
|
||||
|
||||
const toggleTheme = () => {
|
||||
const isDark = document.documentElement.classList.toggle('dark');
|
||||
document.documentElement.setAttribute('data-theme', isDark ? 'dark' : 'light');
|
||||
setDark(isDark);
|
||||
try { localStorage.setItem('pawlink-theme', isDark ? 'dark' : 'light'); } catch (_) {}
|
||||
};
|
||||
|
||||
const menuItems = [
|
||||
{ href: '/', icon: Home, label: 'Início' },
|
||||
{ href: '/main/animals', icon: Search, label: 'Explorar animais' },
|
||||
{ href: '/main/donate', icon: Gift, label: 'Fazer uma doação' },
|
||||
{ href: '/main/shelters', icon: PawPrint, label: 'Canis parceiros' },
|
||||
{ href: '/main/animals', icon: Heart, label: 'Adoptar' },
|
||||
];
|
||||
|
||||
const accountItems = [
|
||||
{ href: '/auth/login', icon: User, label: 'Entrar / Registar' },
|
||||
{ href: '/shelter/dashboard', icon: Shield, label: 'Área do Canil' },
|
||||
];
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* Overlay */}
|
||||
<div
|
||||
className={`side-menu-overlay${open ? ' open' : ''}`}
|
||||
onClick={onClose}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
|
||||
{/* Drawer */}
|
||||
<aside
|
||||
id="side-menu"
|
||||
className={`side-menu${open ? ' open' : ''}`}
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-label="Menu de navegação"
|
||||
aria-hidden={!open}
|
||||
>
|
||||
{/* Header do menu */}
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
padding: '20px 24px 16px',
|
||||
borderBottom: '1px solid var(--parchment)',
|
||||
flexShrink: 0,
|
||||
}}
|
||||
>
|
||||
<Link
|
||||
href="/"
|
||||
className="logo"
|
||||
onClick={onClose}
|
||||
style={{ fontSize: '18px' }}
|
||||
aria-label="PawLink — Início"
|
||||
>
|
||||
<PawPrint size={18} strokeWidth={2.5} />
|
||||
PawLink
|
||||
</Link>
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="menu-btn"
|
||||
style={{ width: '36px', height: '36px' }}
|
||||
aria-label="Fechar menu"
|
||||
>
|
||||
<X size={18} style={{ color: 'var(--soil)' }} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Nav */}
|
||||
<nav style={{ flex: 1, overflowY: 'auto', padding: '8px 16px' }}>
|
||||
{/* Secção principal */}
|
||||
<p
|
||||
style={{
|
||||
fontFamily: 'var(--font-accent, var(--font-fragment-mono))',
|
||||
fontSize: '10px',
|
||||
letterSpacing: '0.12em',
|
||||
textTransform: 'uppercase',
|
||||
color: 'var(--soil-faint)',
|
||||
padding: '16px 8px 8px',
|
||||
}}
|
||||
>
|
||||
Navegação
|
||||
</p>
|
||||
|
||||
{menuItems.map(({ href, icon: Icon, label }) => (
|
||||
<Link
|
||||
key={`${href}-${label}`}
|
||||
href={href}
|
||||
onClick={onClose}
|
||||
className="menu-item"
|
||||
style={{ justifyContent: 'space-between' }}
|
||||
>
|
||||
<span style={{ display: 'flex', alignItems: 'center', gap: '14px' }}>
|
||||
<Icon size={18} />
|
||||
{label}
|
||||
</span>
|
||||
<ChevronRight size={14} style={{ color: 'var(--soil-faint)' }} />
|
||||
</Link>
|
||||
))}
|
||||
|
||||
<div style={{ margin: '12px 8px', borderTop: '1px solid var(--parchment)' }} />
|
||||
|
||||
<p
|
||||
style={{
|
||||
fontFamily: 'var(--font-accent, var(--font-fragment-mono))',
|
||||
fontSize: '10px',
|
||||
letterSpacing: '0.12em',
|
||||
textTransform: 'uppercase',
|
||||
color: 'var(--soil-faint)',
|
||||
padding: '8px 8px',
|
||||
}}
|
||||
>
|
||||
Conta
|
||||
</p>
|
||||
|
||||
{accountItems.map(({ href, icon: Icon, label }) => (
|
||||
<Link
|
||||
key={href}
|
||||
href={href}
|
||||
onClick={onClose}
|
||||
className="menu-item"
|
||||
style={{ justifyContent: 'space-between' }}
|
||||
>
|
||||
<span style={{ display: 'flex', alignItems: 'center', gap: '14px' }}>
|
||||
<Icon size={18} />
|
||||
{label}
|
||||
</span>
|
||||
<ChevronRight size={14} style={{ color: 'var(--soil-faint)' }} />
|
||||
</Link>
|
||||
))}
|
||||
</nav>
|
||||
|
||||
{/* Footer — toggle de tema */}
|
||||
<div style={{ padding: '16px 24px', borderTop: '1px solid var(--parchment)', flexShrink: 0 }}>
|
||||
<button
|
||||
id="theme-toggle"
|
||||
onClick={toggleTheme}
|
||||
className="theme-toggle"
|
||||
aria-label={dark ? 'Activar modo claro' : 'Activar modo escuro'}
|
||||
>
|
||||
<span style={{ fontFamily: 'var(--font-body)', fontWeight: 500, fontSize: '14px', color: 'var(--soil)' }}>
|
||||
{dark ? 'Modo escuro activo' : 'Modo claro activo'}
|
||||
</span>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '6px',
|
||||
background: dark ? 'var(--dark-raised, #2E1E0E)' : 'var(--parchment)',
|
||||
borderRadius: '100px',
|
||||
padding: '5px 10px',
|
||||
transition: 'background 300ms ease',
|
||||
}}
|
||||
>
|
||||
{dark
|
||||
? <Moon size={14} style={{ color: 'var(--amber)' }} />
|
||||
: <Sun size={14} style={{ color: 'var(--terra)' }} />
|
||||
}
|
||||
<span style={{ fontSize: '12px', color: 'var(--soil-mid)', fontWeight: 500 }}>
|
||||
{dark ? 'Escuro' : 'Claro'}
|
||||
</span>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</aside>
|
||||
</>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user