feat: features 025-032 - favoritos, contatos, trabalhe-conosco, area-cliente, navbar, hero-light-dark, performance-homepage
- feat(025): favoritos locais com FavoritesContext, HeartButton, PublicFavoritesPage
- feat(026): central de contatos admin (leads/contatos unificados)
- feat(027): configuração da página de contato via admin
- feat(028): trabalhe conosco - candidaturas com upload e admin
- feat(029): UX área do cliente - visitas, comparação, perfil
- feat(030): navbar UX - menu mobile, ThemeToggle, useFavorites
- feat(031): hero light/dark - imagens separadas por tema, upload, preview, seed
- feat(032): performance homepage - Promise.all parallel fetches, sessionStorage cache,
preload hero image, loading=lazy nos cards, useInView hook, will-change carrossel,
keyframes em index.css, AgentsCarousel e HomeScrollScene via props
- fix: light mode HomeScrollScene - gradiente, cores de texto, scroll hint
migrations: g1h2i3j4k5l6 (source em leads), h1i2j3k4l5m6 (contact_config),
i1j2k3l4m5n6 (job_applications), j2k3l4m5n6o7 (hero theme images)
This commit is contained in:
parent
6ef5a7a17e
commit
cf5603243c
106 changed files with 11927 additions and 1367 deletions
95
frontend/src/components/FavoritesCardsGrid.tsx
Normal file
95
frontend/src/components/FavoritesCardsGrid.tsx
Normal file
|
|
@ -0,0 +1,95 @@
|
|||
import { Link } from 'react-router-dom';
|
||||
import HeartButton from './HeartButton';
|
||||
|
||||
export interface FavoriteCardEntry {
|
||||
id: string;
|
||||
slug: string;
|
||||
title: string;
|
||||
price: string;
|
||||
type: 'venda' | 'aluguel';
|
||||
photo: string | null;
|
||||
city: string | null;
|
||||
bedrooms: number;
|
||||
area_m2: number;
|
||||
}
|
||||
|
||||
function formatPrice(price: string, type: 'venda' | 'aluguel') {
|
||||
const num = parseFloat(price);
|
||||
if (isNaN(num)) return price;
|
||||
const formatted = num.toLocaleString('pt-BR', { style: 'currency', currency: 'BRL', maximumFractionDigits: 0 });
|
||||
return type === 'aluguel' ? `${formatted}/mês` : formatted;
|
||||
}
|
||||
|
||||
interface FavoritesCardsGridProps {
|
||||
entries: FavoriteCardEntry[];
|
||||
}
|
||||
|
||||
export default function FavoritesCardsGrid({ entries }: FavoritesCardsGridProps) {
|
||||
if (entries.length === 0) {
|
||||
return (
|
||||
<div className="rounded-xl border border-borderSubtle bg-panel p-16 text-center">
|
||||
<svg className="mx-auto mb-4 text-textTertiary" width="40" height="40" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M21 8.25c0-2.485-2.099-4.5-4.688-4.5-1.935 0-3.597 1.126-4.312 2.733-.715-1.607-2.377-2.733-4.313-2.733C5.1 3.75 3 5.765 3 8.25c0 7.22 9 12 9 12s9-4.78 9-12z" />
|
||||
</svg>
|
||||
<p className="text-textTertiary mb-4">Nenhum favorito ainda</p>
|
||||
<Link to="/imoveis" className="text-sm text-accent hover:text-accentHover transition">
|
||||
Explorar imóveis →
|
||||
</Link>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
{entries.map(entry => (
|
||||
<div
|
||||
key={entry.id}
|
||||
className="relative rounded-xl border border-borderSubtle bg-panel overflow-hidden hover:border-borderStandard transition group"
|
||||
>
|
||||
<div className="relative h-40 bg-surface">
|
||||
{entry.photo ? (
|
||||
<img
|
||||
src={entry.photo}
|
||||
alt={entry.title}
|
||||
className="w-full h-full object-cover group-hover:scale-[1.02] transition-transform duration-300"
|
||||
loading="lazy"
|
||||
/>
|
||||
) : (
|
||||
<div className="w-full h-full flex items-center justify-center text-textTertiary">
|
||||
<svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5">
|
||||
<rect x="3" y="3" width="18" height="18" rx="2" /><circle cx="8.5" cy="8.5" r="1.5" /><path d="M21 15l-5-5L5 21" />
|
||||
</svg>
|
||||
</div>
|
||||
)}
|
||||
<div className="absolute top-2 right-2 z-10">
|
||||
<HeartButton propertyId={entry.id} />
|
||||
</div>
|
||||
<span className={`absolute bottom-2 left-2 rounded-full text-[11px] font-semibold px-2 py-0.5 backdrop-blur-sm ${entry.type === 'venda' ? 'bg-brand/80 text-white' : 'bg-black/50 text-white/90 border border-white/20'}`}>
|
||||
{entry.type === 'venda' ? 'Venda' : 'Aluguel'}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="p-4">
|
||||
<Link to={entry.slug ? `/imoveis/${entry.slug}` : '#'} className="block">
|
||||
<p className="text-sm font-semibold text-textPrimary line-clamp-2 leading-snug">
|
||||
{entry.title}
|
||||
</p>
|
||||
{entry.city && (
|
||||
<p className="text-xs text-textTertiary mt-1 truncate">{entry.city}</p>
|
||||
)}
|
||||
{entry.price && (
|
||||
<p className="text-sm font-semibold text-textPrimary mt-2">
|
||||
{formatPrice(entry.price, entry.type)}
|
||||
</p>
|
||||
)}
|
||||
<div className="flex gap-3 mt-2 text-xs text-textTertiary">
|
||||
{entry.bedrooms > 0 && <span>{entry.bedrooms} qto{entry.bedrooms !== 1 ? 's' : ''}</span>}
|
||||
{entry.area_m2 > 0 && <span>{entry.area_m2} m²</span>}
|
||||
</div>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue