# Implementation Plan: Homepage (Página Inicial) **Branch**: `001-homepage` | **Date**: 2026-04-13 | **Spec**: `spec.md` **Input**: Feature specification from `spec.md` --- ## Summary Implementar a homepage pública do SaaS Imobiliária — uma landing page com hero configurável, grade de imóveis em destaque, seção Sobre, seção CTA e rodapé. O conteúdo dinâmico (headline, imóveis em destaque) é servido por uma API Flask REST. O frontend é uma SPA React (TypeScript + Vite) estilizada com Tailwind alinhado ao design system Linear-inspired documentado em `DESIGN.md`. Abordagem técnica: Flask para API backend (sem SSR), React SPA para todos os rendering, PostgreSQL via SQLAlchemy com Flask-Migrate, Pydantic v2 para validação de input/output nas rotas. --- ## Technical Context **Language/Version**: Python 3.12 (backend) / TypeScript ~5.4 com React 18 (frontend) **Primary Dependencies**: Flask 3.x, Flask-SQLAlchemy, Flask-Migrate, Pydantic v2, Flask-CORS; React 18, Vite 5, Tailwind CSS 3, Axios **Storage**: PostgreSQL 16 (desenvolvimento local via Docker) **Testing**: pytest (backend); Vitest + React Testing Library (frontend) **Target Platform**: Web — browser moderno (desktop + mobile) **Project Type**: Web Application (backend REST API + frontend SPA) **Performance Goals**: LCP < 2,5s; API response < 500ms (NFR-001, NFR-002) **Constraints**: Imagens de card ≤ 300 KB (NFR-003); suporte a breakpoints 320px–1440px (NFR-004) **Scale/Scope**: MVP; homepage pública + painel admin básico para configuração de conteúdo --- ## Constitution Check *Verificado em 2026-04-13 — todos os gates passam.* | # | Princípio | Status | Nota | |---|-----------|--------|------| | I | Design-First | ✅ PASS | Tailwind config mapeará todos os tokens de `DESIGN.md`; nenhum valor inline fora do config | | II | Separation of Concerns | ✅ PASS | Flask emite JSON puro; React é SPA; CORS configurado explicitamente | | III | Spec-Driven Development | ✅ PASS | `spec.md` existe com user stories e acceptance scenarios; este plano precede a implementação | | IV | Data Integrity | ✅ PASS | Todos os inputs validados com Pydantic; migrações via Flask-Migrate; Numeric para `price` | | V | Security | ✅ PASS | Rotas admin protegidas com autenticação; secrets em variáveis de ambiente; sem credenciais no frontend | | VI | Simplicity First | ✅ PASS | Sem abstrações prematuras; sem padrão Repository; SQLAlchemy ORM diretamente nas rotas | --- ## Project Structure ### Source Code (repository root) ```text backend/ ├── app/ │ ├── __init__.py # Flask app factory (create_app) │ ├── config.py # Config classes (Dev, Prod, Test) — lê de env vars │ ├── extensions.py # db, migrate, cors (instâncias únicas) │ ├── models/ │ │ ├── __init__.py │ │ ├── property.py # Property, PropertyPhoto │ │ └── homepage.py # HomepageConfig │ ├── schemas/ │ │ ├── __init__.py │ │ ├── property.py # PropertyOut, PropertyPhotoOut │ │ └── homepage.py # HomepageConfigOut, HomepageConfigIn │ └── routes/ │ ├── __init__.py │ ├── homepage.py # GET /api/v1/homepage-config │ └── properties.py # GET /api/v1/properties ├── migrations/ # Alembic — gerenciado pelo Flask-Migrate ├── seeds/ │ └── seed.py # Dados de exemplo para desenvolvimento ├── tests/ │ ├── conftest.py │ ├── test_homepage.py │ └── test_properties.py ├── .env.example ├── pyproject.toml # uv project + dependências └── uv.lock # Commitável frontend/ ├── src/ │ ├── components/ │ │ ├── Navbar.tsx │ │ ├── HeroSection.tsx │ │ ├── FeaturedProperties.tsx │ │ ├── PropertyCard.tsx │ │ ├── PropertyCardSkeleton.tsx │ │ ├── AboutSection.tsx │ │ ├── CTASection.tsx │ │ └── Footer.tsx │ ├── pages/ │ │ └── HomePage.tsx │ ├── services/ │ │ ├── api.ts # Instância Axios + interceptors │ │ ├── homepage.ts # getHomepageConfig() │ │ └── properties.ts # getFeaturedProperties() │ ├── types/ │ │ ├── homepage.ts # HomepageConfig interface │ │ └── property.ts # Property, PropertyPhoto interfaces │ ├── App.tsx │ └── main.tsx ├── public/ │ └── placeholder-property.jpg ├── tailwind.config.ts # Tokens alinhados ao DESIGN.md ├── index.html ├── vite.config.ts ├── tsconfig.json └── package.json ``` --- ## Phase 1 — Project Structure Setup **Objetivo**: Scaffolding do backend Flask e frontend React com todas as ferramentas configuradas, banco conectado e build passando. Nenhuma lógica de negócio ainda. ### 1.1 Backend — Inicialização **Arquivos a criar**: | Arquivo | Responsabilidade | |---------|-----------------| | `backend/pyproject.toml` | Configuração do projeto uv com dependências fixadas | | `backend/app/__init__.py` | `create_app()` factory registrando blueprints e extensões | | `backend/app/config.py` | `DevelopmentConfig`, `ProductionConfig`, `TestingConfig` lendo `DATABASE_URL`, `SECRET_KEY`, `CORS_ORIGINS` de env vars | | `backend/app/extensions.py` | Instâncias de `db = SQLAlchemy()`, `migrate = Migrate()`, `cors = CORS()` — inicializadas em `create_app()` | | `backend/.env.example` | Template com variáveis obrigatórias (sem valores reais) | **Dependências** (`pyproject.toml`): ```toml [project] name = "saas-imobiliaria-backend" requires-python = ">=3.12" dependencies = [ "flask>=3.0", "flask-sqlalchemy>=3.1", "flask-migrate>=4.0", "flask-cors>=4.0", "pydantic>=2.7", "psycopg2-binary>=2.9", "python-dotenv>=1.0", ] [dependency-groups] dev = [ "pytest>=8.0", "pytest-flask>=1.3", ] ``` **Variáveis de ambiente obrigatórias** (`.env.example`): ``` DATABASE_URL=postgresql://user:password@localhost:5432/saas_imobiliaria SECRET_KEY=change-me-in-production CORS_ORIGINS=http://localhost:5173 FLASK_ENV=development ``` **Config pattern** (`app/config.py`): ```python import os class BaseConfig: SECRET_KEY = os.environ["SECRET_KEY"] SQLALCHEMY_TRACK_MODIFICATIONS = False class DevelopmentConfig(BaseConfig): DEBUG = True SQLALCHEMY_DATABASE_URI = os.environ["DATABASE_URL"] class ProductionConfig(BaseConfig): DEBUG = False SQLALCHEMY_DATABASE_URI = os.environ["DATABASE_URL"] config = { "development": DevelopmentConfig, "production": ProductionConfig, "default": DevelopmentConfig, } ``` **Critério de sucesso**: `uv run flask --app app shell` abre sem erros; `uv run pytest` passa (0 testes, setup ok). --- ### 1.2 Banco de Dados — PostgreSQL + SQLAlchemy **Passos**: 1. Criar container Docker para PostgreSQL local: ```bash docker run -d \ --name saas_imob_db \ -e POSTGRES_USER=imob \ -e POSTGRES_PASSWORD=imob_dev \ -e POSTGRES_DB=saas_imobiliaria \ -p 5432:5432 \ postgres:16-alpine ``` 2. Inicializar o repositório de migrações: `uv run flask --app app db init` 3. Confirmar conexão via `uv run flask --app app shell` → `db.engine.connect()` **Critério de sucesso**: Banco acessível; `flask db init` gera pasta `migrations/` sem erros. --- ### 1.3 Frontend — Inicialização **Comando**: ```bash npm create vite@latest frontend -- --template react-ts cd frontend npm install npm install -D tailwindcss postcss autoprefixer npx tailwindcss init -p npm install axios ``` **Arquivos a criar/editar**: | Arquivo | Ação | |---------|------| | `frontend/tailwind.config.ts` | Estender com todos os tokens de `DESIGN.md` (ver Phase 3) | | `frontend/src/index.css` | Importar Tailwind; configurar Inter Variable via `@font-face` | | `frontend/vite.config.ts` | Configurar proxy `/api` → `http://localhost:5000` para desenvolvimento | **Configuração do proxy Vite** (`vite.config.ts`): ```typescript import { defineConfig } from 'vite' import react from '@vitejs/plugin-react' export default defineConfig({ plugins: [react()], server: { proxy: { '/api': { target: 'http://localhost:5000', changeOrigin: true, }, }, }, }) ``` **Critério de sucesso**: `npm run dev` sobe em `localhost:5173`; `npm run build` passa sem erros de TypeScript. --- ## Phase 2 — Backend API (Flask) **Objetivo**: Modelos de banco, migrações, rotas REST e seeder com dados de exemplo. Ao final desta phase, `GET /api/v1/homepage-config` e `GET /api/v1/properties?featured=true` retornam JSON válido. **Rastreabilidade**: US-2 (grade de imóveis), US-4 (config admin), FR-005, FR-006, FR-008, FR-009. --- ### 2.1 Models #### `Property` (`app/models/property.py`) ```python import uuid from decimal import Decimal from app.extensions import db class PropertyType(db.Enum): VENDA = "venda" ALUGUEL = "aluguel" class Property(db.Model): __tablename__ = "properties" id = db.Column(db.UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) title = db.Column(db.String(200), nullable=False) slug = db.Column(db.String(220), unique=True, nullable=False) address = db.Column(db.String(300), nullable=True) price = db.Column(db.Numeric(12, 2), nullable=False) type = db.Column(db.Enum("venda", "aluguel", name="property_type"), nullable=False) bedrooms = db.Column(db.Integer, nullable=False) bathrooms = db.Column(db.Integer, nullable=False) area_m2 = db.Column(db.Integer, nullable=False) is_featured = db.Column(db.Boolean, nullable=False, default=False) is_active = db.Column(db.Boolean, nullable=False, default=True) created_at = db.Column(db.DateTime, nullable=False, server_default=db.func.now()) photos = db.relationship( "PropertyPhoto", backref="property", order_by="PropertyPhoto.display_order", cascade="all, delete-orphan", ) class PropertyPhoto(db.Model): __tablename__ = "property_photos" id = db.Column(db.Integer, primary_key=True, autoincrement=True) property_id = db.Column( db.UUID(as_uuid=True), db.ForeignKey("properties.id", ondelete="CASCADE"), nullable=False, ) url = db.Column(db.String(500), nullable=False) alt_text = db.Column(db.String(200), nullable=False, default="") display_order = db.Column(db.Integer, nullable=False, default=0) ``` **Regras de integridade**: - `price` usa `Numeric(12, 2)` — nunca `Float` (princípio IV) - `property_type` é enum nativo do PostgreSQL — rejeita valores inválidos no DB - `ondelete="CASCADE"` nas fotos — sem fotos órfãs #### `HomepageConfig` (`app/models/homepage.py`) ```python from app.extensions import db class HomepageConfig(db.Model): __tablename__ = "homepage_config" id = db.Column(db.Integer, primary_key=True, autoincrement=True) hero_headline = db.Column(db.String(120), nullable=False) hero_subheadline = db.Column(db.String(240), nullable=True) hero_cta_label = db.Column(db.String(40), nullable=False, default="Ver Imóveis") hero_cta_url = db.Column(db.String(200), nullable=False, default="/imoveis") featured_properties_limit = db.Column(db.Integer, nullable=False, default=6) updated_at = db.Column( db.DateTime, nullable=False, server_default=db.func.now(), onupdate=db.func.now(), ) ``` **Constraints**: - `hero_headline` NOT NULL — FR-016 (validation gate no schema Pydantic) - `featured_properties_limit` padrão 6, máximo 12 — aplicado na rota (não no DB, para evitar complexidade desnecessária) - Tabela usa row única (id=1); upsert gerenciado pela rota admin --- ### 2.2 Pydantic Schemas #### `app/schemas/property.py` ```python from __future__ import annotations from decimal import Decimal from uuid import UUID from pydantic import BaseModel, ConfigDict, field_validator from typing import Literal class PropertyPhotoOut(BaseModel): model_config = ConfigDict(from_attributes=True) url: str alt_text: str display_order: int class PropertyOut(BaseModel): model_config = ConfigDict(from_attributes=True) id: UUID title: str slug: str price: Decimal type: Literal["venda", "aluguel"] bedrooms: int bathrooms: int area_m2: int is_featured: bool photos: list[PropertyPhotoOut] ``` #### `app/schemas/homepage.py` ```python from __future__ import annotations from pydantic import BaseModel, ConfigDict, field_validator class HomepageConfigOut(BaseModel): model_config = ConfigDict(from_attributes=True) hero_headline: str hero_subheadline: str | None hero_cta_label: str hero_cta_url: str featured_properties_limit: int class HomepageConfigIn(BaseModel): hero_headline: str hero_subheadline: str | None = None hero_cta_label: str = "Ver Imóveis" hero_cta_url: str = "/imoveis" featured_properties_limit: int = 6 @field_validator("hero_headline") @classmethod def headline_not_empty(cls, v: str) -> str: if not v.strip(): raise ValueError("hero_headline não pode ser vazio") return v @field_validator("featured_properties_limit") @classmethod def limit_in_range(cls, v: int) -> int: if not (1 <= v <= 12): raise ValueError("featured_properties_limit deve estar entre 1 e 12") return v ``` --- ### 2.3 Rotas #### `GET /api/v1/homepage-config` (`app/routes/homepage.py`) ```python from flask import Blueprint, jsonify from app.models.homepage import HomepageConfig from app.schemas.homepage import HomepageConfigOut homepage_bp = Blueprint("homepage", __name__, url_prefix="/api/v1") @homepage_bp.get("/homepage-config") def get_homepage_config(): config = HomepageConfig.query.first() if config is None: return jsonify({"error": "Homepage config not found"}), 404 return jsonify(HomepageConfigOut.model_validate(config).model_dump()) ``` **Response shape**: ```json { "hero_headline": "Encontre o imóvel dos seus sonhos", "hero_subheadline": "Mais de 200 imóveis disponíveis em toda a região", "hero_cta_label": "Ver Imóveis", "hero_cta_url": "/imoveis", "featured_properties_limit": 6 } ``` #### `GET /api/v1/properties` (`app/routes/properties.py`) ```python from flask import Blueprint, jsonify, request from app.models.property import Property from app.schemas.property import PropertyOut properties_bp = Blueprint("properties", __name__, url_prefix="/api/v1") @properties_bp.get("/properties") def list_properties(): featured_param = request.args.get("featured", "").lower() query = Property.query.filter_by(is_active=True) if featured_param == "true": # Busca o limite configurado from app.models.homepage import HomepageConfig config = HomepageConfig.query.first() limit = config.featured_properties_limit if config else 6 query = ( query .filter_by(is_featured=True) .order_by(Property.created_at.desc()) .limit(limit) ) properties = query.all() return jsonify([PropertyOut.model_validate(p).model_dump(mode="json") for p in properties]) ``` **Response shape** (`?featured=true`): ```json [ { "id": "uuid", "title": "Apartamento 3 quartos — Centro", "slug": "apartamento-3-quartos-centro", "price": "750000.00", "type": "venda", "bedrooms": 3, "bathrooms": 2, "area_m2": 98, "is_featured": true, "photos": [ { "url": "https://...", "alt_text": "Sala de estar", "display_order": 0 } ] } ] ``` --- ### 2.4 Migração Inicial ```bash # Gera a migração a partir dos models uv run flask --app app db migrate -m "initial schema: properties, property_photos, homepage_config" # Aplica ao banco uv run flask --app app db upgrade ``` Verificar que o downgrade funciona antes de commitar: ```bash uv run flask --app app db downgrade base uv run flask --app app db upgrade ``` --- ### 2.5 Seeder (`seeds/seed.py`) ```python """ Popula o banco com dados de exemplo para desenvolvimento. Uso: uv run python seeds/seed.py """ from app import create_app from app.extensions import db from app.models.homepage import HomepageConfig from app.models.property import Property, PropertyPhoto def seed(): app = create_app() with app.app_context(): db.session.query(PropertyPhoto).delete() db.session.query(Property).delete() db.session.query(HomepageConfig).delete() config = HomepageConfig( hero_headline="Encontre o imóvel dos seus sonhos", hero_subheadline="Mais de 200 imóveis disponíveis em toda a região", hero_cta_label="Ver Imóveis", hero_cta_url="/imoveis", featured_properties_limit=6, ) db.session.add(config) sample_properties = [ { "title": "Apartamento 3 quartos — Centro", "slug": "apartamento-3-quartos-centro", "price": "750000.00", "type": "venda", "bedrooms": 3, "bathrooms": 2, "area_m2": 98, "is_featured": True, }, # ... mais 5 imóveis para cobrir o limite padrão de 6 ] for data in sample_properties: prop = Property(**data) db.session.add(prop) db.session.commit() print("Seed concluído.") if __name__ == "__main__": seed() ``` **Critérios de sucesso da Phase 2**: - `GET /api/v1/homepage-config` → `200 OK` com JSON válido - `GET /api/v1/properties?featured=true` → array com até 6 imóveis - `GET /api/v1/properties?featured=true` sem imóveis em destaque → `[]` (sem erro 500) - `uv run pytest` passa em `tests/test_homepage.py` e `tests/test_properties.py` --- ## Phase 3 — Frontend (React + TypeScript) **Objetivo**: Implementar todos os componentes da homepage estilizados conforme `DESIGN.md`, consumindo a API backend. Ao final, a homepage pública está visualmente completa e responsiva. **Rastreabilidade**: US-1 (hero/nav), US-2 (imóveis em destaque), US-3 (sobre/CTA/rodapé), FR-001–FR-014, NFR-004–NFR-010. --- ### 3.1 Tailwind Config (`tailwind.config.ts`) Mapeamento completo dos tokens de `DESIGN.md`: ```typescript import type { Config } from 'tailwindcss' export default { content: ['./index.html', './src/**/*.{ts,tsx}'], theme: { extend: { colors: { // Backgrounds 'mkt-black': '#08090a', 'panel-dark': '#0f1011', 'surface-elevated': '#191a1b', 'surface-secondary': '#28282c', // Text 'text-primary': '#f7f8f8', 'text-secondary': '#d0d6e0', 'text-tertiary': '#8a8f98', 'text-quaternary': '#62666d', // Brand 'brand-indigo': '#5e6ad2', 'accent-violet': '#7170ff', 'accent-hover': '#828fff', // Borders (solid) 'border-primary': '#23252a', 'border-secondary': '#34343a', 'border-tertiary': '#3e3e44', 'line-tint': '#141516', // Status 'status-green': '#27a644', 'status-emerald': '#10b981', }, fontFamily: { sans: [ '"Inter Variable"', 'SF Pro Display', '-apple-system', 'system-ui', 'Segoe UI', 'Roboto', 'sans-serif', ], mono: [ '"Berkeley Mono"', 'ui-monospace', 'SF Mono', 'Menlo', 'monospace', ], }, fontWeight: { light: '300', normal: '400', medium: '510', // Linear signature weight semibold: '590', }, letterSpacing: { 'display-xl': '-1.584px', // 72px hero 'display-lg': '-1.408px', // 64px display: '-1.056px', // 48px h1: '-0.704px', // 32px h2: '-0.288px', // 24px h3: '-0.24px', // 20px body: '-0.165px', // 15-18px }, backdropBlur: { navbar: '12px', }, }, }, plugins: [], } satisfies Config ``` **Inter Variable** — adicionar ao `src/index.css`: ```css @import url('https://fonts.googleapis.com/css2?family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&display=swap'); /* Ou via @font-face com arquivo local se disponível */ @tailwind base; @tailwind components; @tailwind utilities; @layer base { html { font-feature-settings: "cv01", "ss03"; } body { @apply bg-mkt-black text-text-primary font-sans antialiased; } } ``` --- ### 3.2 Types (`src/types/`) #### `src/types/homepage.ts` ```typescript export interface HomepageConfig { hero_headline: string hero_subheadline: string | null hero_cta_label: string hero_cta_url: string featured_properties_limit: number } ``` #### `src/types/property.ts` ```typescript export interface PropertyPhoto { url: string alt_text: string display_order: number } export interface Property { id: string title: string slug: string price: string // Decimal serializado como string type: 'venda' | 'aluguel' bedrooms: number bathrooms: number area_m2: number is_featured: boolean photos: PropertyPhoto[] } ``` --- ### 3.3 API Service Layer (`src/services/`) #### `src/services/api.ts` ```typescript import axios from 'axios' export const api = axios.create({ baseURL: '/api/v1', timeout: 8000, headers: { 'Content-Type': 'application/json' }, }) ``` #### `src/services/homepage.ts` ```typescript import { api } from './api' import type { HomepageConfig } from '../types/homepage' export async function getHomepageConfig(): Promise { const { data } = await api.get('/homepage-config') return data } ``` #### `src/services/properties.ts` ```typescript import { api } from './api' import type { Property } from '../types/property' export async function getFeaturedProperties(): Promise { const { data } = await api.get('/properties', { params: { featured: 'true' }, }) return data } ``` --- ### 3.4 Componentes #### `Navbar` (`src/components/Navbar.tsx`) **Especificação visual**: - Background: `rgba(8,9,10,0.85)` + `backdrop-blur-navbar` - Sticky no topo, `z-50` - Logotipo à esquerda (texto ou SVG, `font-medium text-text-primary`) - Links à direita: "Imóveis", "Sobre", "Contato" (`text-sm text-text-secondary hover:text-text-primary`) - Mobile (<768px): hamburger menu — links em dropdown/drawer - Border bottom: `border-b border-white/5` **Requisitos**: FR-001, FR-002, FR-003, NFR-008 ```tsx // Estrutura semântica obrigatória (NFR-010)
``` --- #### `HeroSection` (`src/components/HeroSection.tsx`) **Props**: ```typescript interface HeroSectionProps { headline: string subheadline: string | null ctaLabel: string ctaUrl: string } ``` **Especificação visual**: - Background: `#08090a` com gradiente sutil radial centralizado (`rgba(94,106,210,0.08)`) - Headline: `text-[72px] leading-none tracking-display-xl font-medium text-text-primary` no desktop - Tablet (768px): `text-[48px] tracking-display` - Mobile (320px–428px): `text-[40px]` - Subheadline (quando presente): `text-xl text-text-secondary` com `font-light` - Botão CTA: background `#5e6ad2`, hover `#828fff`, texto branco, padding `-y-3 -x-6`, `rounded-md` - Focus ring visível para acessibilidade (NFR-008) - Subheadline ausente → elemento não renderizado (não string vazia) — FR-edge case **Estado de carregamento**: Skeleton para headline e subheadline (3 linhas animadas). **Requisitos**: FR-004, FR-005, US-1 acceptance scenarios 1–5 --- #### `PropertyCard` (`src/components/PropertyCard.tsx`) **Props**: ```typescript interface PropertyCardProps { property: Property } ``` **Especificação visual**: - Container: `bg-surface-elevated rounded-xl border border-white/5 overflow-hidden cursor-pointer` hover: `border-white/8 shadow-lg transition-all duration-200` - Foto: `aspect-[4/3] w-full object-cover` - Placeholder (`placeholder-property.jpg`) quando `photos.length === 0` (FR-011) - Atributo `alt` = `property.title` (NFR-007) - Badge de tipo: `rounded-full text-xs font-medium px-2.5 py-1` - Venda: `bg-brand-indigo/20 text-accent-violet` - Aluguel: `bg-status-green/20 text-status-green` - Preço: Formatado com `Intl.NumberFormat('pt-BR', { style: 'currency', currency: 'BRL' })` Fonte: `text-lg font-medium text-text-primary` - Stats (quartos/banheiros/área): ícones SVG inline + `text-sm text-text-secondary` - Clique navega para `/imoveis/{slug}` (FR-007, US-2 scenario 3) --- #### `PropertyCardSkeleton` (`src/components/PropertyCardSkeleton.tsx`) **Especificação visual**: - Mesma estrutura de card com `animate-pulse` - Blocos cinza (`bg-surface-secondary rounded`) nos lugares de foto, badge, preço e stats - Exibido durante loading de `getFeaturedProperties()` (spec edge case: estado de carregamento) --- #### `FeaturedProperties` (`src/components/FeaturedProperties.tsx`) **Estado interno**: `loading | success | error | empty` **Comportamentos**: - `loading`: renderiza 3 `PropertyCardSkeleton` (NFR edge case) - `success` com dados: grade de cards (`grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6`) - `success` sem dados (`[]`): mensagem "Nenhum imóvel em destaque no momento" (FR-010) - `error`: fallback estático — não lança exceção para o usuário (spec edge case: API indisponível) **Responsivo**: 1 col mobile → 2 col tablet → 3 col desktop (NFR-005) **Requisitos**: FR-006, FR-008, FR-009, FR-010, US-2 --- #### `AboutSection` (`src/components/AboutSection.tsx`) **Conteúdo estático** (MVP — não configurável via API nesta feature): - Seção com nome da agência e parágrafo de descrição - Background alternado: `bg-panel-dark` - Título: `text-3xl font-medium tracking-h1 text-text-primary` - Corpo: `text-base text-text-secondary leading-relaxed` **Requisitos**: FR-012, US-3 scenario 1 --- #### `CTASection` (`src/components/CTASection.tsx`) **Conteúdo estático** (MVP): - Convite ao contato com número de telefone e/ou e-mail - Botão/link clicável como elemento acionável obrigatório (FR-013) - Background: `bg-surface-elevated` com border top `border-t border-white/5` **Requisitos**: FR-013, US-3 scenario 2 --- #### `Footer` (`src/components/Footer.tsx`) **Conteúdo**: - Informações de contato da agência (e-mail ou telefone) — FR-014 - Links de navegação: Imóveis, Sobre, Contato - Copyright **Estrutura semântica**: `