sass-imobiliaria/.specify/features/001-homepage/plan.md

36 KiB
Raw Permalink Blame History

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 320px1440px (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)

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):

[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):

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:
    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 shelldb.engine.connect()

Critério de sucesso: Banco acessível; flask db init gera pasta migrations/ sem erros.


1.3 Frontend — Inicialização

Comando:

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 /apihttp://localhost:5000 para desenvolvimento

Configuração do proxy Vite (vite.config.ts):

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)

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)

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

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

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)

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:

{
  "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)

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):

[
  {
    "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

# 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:

uv run flask --app app db downgrade base
uv run flask --app app db upgrade

2.5 Seeder (seeds/seed.py)

"""
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-config200 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-001FR-014, NFR-004NFR-010.


3.1 Tailwind Config (tailwind.config.ts)

Mapeamento completo dos tokens de DESIGN.md:

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:

@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

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

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

import axios from 'axios'

export const api = axios.create({
  baseURL: '/api/v1',
  timeout: 8000,
  headers: { 'Content-Type': 'application/json' },
})

src/services/homepage.ts

import { api } from './api'
import type { HomepageConfig } from '../types/homepage'

export async function getHomepageConfig(): Promise<HomepageConfig> {
  const { data } = await api.get<HomepageConfig>('/homepage-config')
  return data
}

src/services/properties.ts

import { api } from './api'
import type { Property } from '../types/property'

export async function getFeaturedProperties(): Promise<Property[]> {
  const { data } = await api.get<Property[]>('/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

// Estrutura semântica obrigatória (NFR-010)
<header role="banner">
  <nav aria-label="Navegação principal">
    {/* logo + links + hamburger */}
  </nav>
</header>

HeroSection (src/components/HeroSection.tsx)

Props:

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 (320px428px): 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 15


PropertyCard (src/components/PropertyCard.tsx)

Props:

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


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: <footer role="contentinfo"> (NFR-010)

Requisitos: FR-014, US-3 scenario 3


3.5 Page (src/pages/HomePage.tsx)

import React from 'react'
import { useEffect, useState } from 'react'
import Navbar from '../components/Navbar'
import HeroSection from '../components/HeroSection'
import FeaturedProperties from '../components/FeaturedProperties'
import AboutSection from '../components/AboutSection'
import CTASection from '../components/CTASection'
import Footer from '../components/Footer'
import { getHomepageConfig } from '../services/homepage'
import type { HomepageConfig } from '../types/homepage'

// Conteúdo fallback estático para quando a API falhar (spec edge case)
const FALLBACK_CONFIG: HomepageConfig = {
  hero_headline: 'Encontre o imóvel dos seus sonhos',
  hero_subheadline: null,
  hero_cta_label: 'Ver Imóveis',
  hero_cta_url: '/imoveis',
  featured_properties_limit: 6,
}

export default function HomePage() {
  const [config, setConfig] = useState<HomepageConfig>(FALLBACK_CONFIG)

  useEffect(() => {
    getHomepageConfig()
      .then(setConfig)
      .catch(() => { /* usa fallback silenciosamente */ })
  }, [])

  return (
    <main>
      <Navbar />
      <HeroSection
        headline={config.hero_headline}
        subheadline={config.hero_subheadline}
        ctaLabel={config.hero_cta_label}
        ctaUrl={config.hero_cta_url}
      />
      <FeaturedProperties />
      <AboutSection />
      <CTASection />
      <Footer />
    </main>
  )
}

Estrutura semântica: <main> envolve o conteúdo da página (NFR-010)


3.6 App Router (src/App.tsx)

import { BrowserRouter, Routes, Route } from 'react-router-dom'
import HomePage from './pages/HomePage'

export default function App() {
  return (
    <BrowserRouter>
      <Routes>
        <Route path="/" element={<HomePage />} />
        {/* /imoveis e /imoveis/:slug serão adicionados em features futuras */}
      </Routes>
    </BrowserRouter>
  )
}

Adicionar react-router-dom:

npm install react-router-dom
npm install -D @types/react-router-dom

Critérios de sucesso da Phase 3:

  • npm run build sem erros de TypeScript
  • Homepage renderiza em todos os breakpoints de 320px a 1440px (NFR-004)
  • Grades responsivas: 1→2→3 colunas conforme NFR-005
  • Tipografia do hero escala: 40px mobile → 48px tablet → 72px desktop (NFR-006)
  • Todas as imagens com alt descritivo (NFR-007)
  • Todos os botões navegáveis por teclado com foco visível (NFR-008)

Phase 4 — Integration & Polish

Objetivo: Conectar frontend ao backend real, tratar todos os estados de borda especificados, ajuste visual fino e verificação de contraste WCAG AA.

Rastreabilidade: NFR-001NFR-009, spec edge cases, US-1 scenario 4.


4.1 Integração API → Frontend

Checklist de integração:

  • Hero exibe conteúdo real de GET /api/v1/homepage-config
  • Grade de imóveis exibe dados reais de GET /api/v1/properties?featured=true
  • Quando headline atualizado via admin → reload da homepage reflete novo texto (US-4 scenario 1)
  • is_featured toggleado via admin → grade atualiza no próximo reload (US-4 scenario 2)
  • Subheadline null → elemento não exibido (não string vazia) — spec edge case
  • Headline 120+ caracteres → seção hero sem overflow (spec edge case)

CORS: Confirmar que flask-cors aceita apenas origin http://localhost:5173 em dev e o domínio de produção em prod. Nunca wildcard * em produção (princípio V).


4.2 Loading States & Error Handling

Estado Componente Comportamento
Carregando config HeroSection Skeleton de 23 linhas animadas substituindo headline/subheadline
Carregando imóveis FeaturedProperties 3 PropertyCardSkeleton side-by-side
API config falha HomePage Fallback FALLBACK_CONFIG silencioso — página não quebra
API imóveis falha FeaturedProperties Estado error — mensagem amigável, sem stack trace visível
Imóvel sem foto PropertyCard Imagem placeholder (/placeholder-property.jpg) com alt="Imóvel sem foto"
Grade vazia FeaturedProperties Mensagem "Nenhum imóvel em destaque no momento" — seção não colapsa

4.3 Visual Polish

Checklist de conformidade com DESIGN.md:

  • Background da página: #08090a em toda a extensão vertical
  • Navbar sticky com backdrop-blur e rgba(8,9,10,0.85) — não opaca
  • Headline do hero com letter-spacing: -1.584px em 72px desktop
  • Border dos cards: rgba(255,255,255,0.05) no estado default; rgba(255,255,255,0.08) no hover
  • Botão CTA: background #5e6ad2, hover #828fff, transição duration-200
  • Badges de tipo com bg-opacity — usando cores definidas no Tailwind config (sem inline)
  • Nenhum valor de cor ou espaçamento inline que não esteja no Tailwind config

Verificação de contraste WCAG 2.1 AA (NFR-009):

Par Ratio mínimo Verificar
#f7f8f8 sobre #08090a > 21:1 Texto primário/hero
#d0d6e0 sobre #08090a ~13:1 Texto secundário
#d0d6e0 sobre #191a1b ~9:1 Texto em cards
#7170ff sobre #191a1b verificar tool Badge violation risk — usar #828fff se necessário
Branco sobre #5e6ad2 verificar tool Botão CTA

Ferramenta recomendada: whocanuse.com ou browser DevTools Accessibility panel.


4.4 Performance

Checklist NFR-001NFR-003:

  • Imagens do seeder ≤ 300 KB cada (NFR-003) — usar imagens de desenvolvimento de picsum.photos redimensionadas
  • LCP medido via Lighthouse em rede 4G slow: deve ser < 2,5s (NFR-001)
  • Resposta de API medida via DevTools: deve ser < 500ms (NFR-002)
  • Build Vite com npm run build + npm run preview para teste de produção

4.5 Acessibilidade Final

Checklist NFR-007NFR-010:

  • <nav aria-label="...">, <main>, <footer role="contentinfo">, <header role="banner"> presentes
  • Tab order lógico: Navbar → Hero CTA → Cards → CTA → Footer links
  • Cada card de imóvel é focável e acionável via Enter/Space
  • Imagens com alt descritivo — não alt="" exceto se puramente decorativas
  • Focus ring visível em todos os elementos interativos (não removido com outline-none sem substituto)
  • Screen reader: verificar com NVDA/VoiceOver que a sequência de headings é lógica (h1 → h2 → h3)

Critérios de sucesso da Phase 4:

  • Lighthouse Score: Performance ≥ 90, Accessibility ≥ 95, Best Practices ≥ 95
  • Nenhum console error em dev ou prod
  • Verificação visual em Chrome DevTools: 375px (iPhone SE), 768px (iPad), 1280px (desktop)
  • Admin atualiza headline → homepage pública reflete sem redeploy (US-4 scenario 1)

Dependencies Summary

Backend

Pacote Versão mínima Razão
flask 3.0 Framework REST
flask-sqlalchemy 3.1 ORM (princípio IV)
flask-migrate 4.0 Migrações Alembic (princípio IV)
flask-cors 4.0 CORS explícito (princípio II/V)
pydantic 2.7 Validação de input/output (princípio IV)
psycopg2-binary 2.9 Driver PostgreSQL
python-dotenv 1.0 Loading de .env (princípio V)
pytest 8.0 Testes (dev)
pytest-flask 1.3 Fixtures Flask para testes (dev)

Frontend

Pacote Versão mínima Razão
react / react-dom 18 Framework SPA
typescript 5.4 Type safety
vite 5 Build tool (stack constraint)
tailwindcss 3 Styling com tokens DESIGN.md (stack constraint)
axios 1.6 HTTP client com interceptors
react-router-dom 6 Roteamento SPA

Sequência de Execução

Phase 1.1 (backend scaffold)
  → Phase 1.2 (banco de dados)
  → Phase 2.1 (models)
  → Phase 2.4 (migração)   ← pode ser paralelizado com Phase 1.3
  → Phase 2.2 (schemas)
  → Phase 2.3 (rotas)
  → Phase 2.5 (seeder)
  → Phase 1.3 (frontend scaffold)
  → Phase 3.1 (tailwind config)
  → Phase 3.2 (types)
  → Phase 3.3 (services)
  → Phase 3.4 (componentes — ordem: PropertyCard → FeaturedProperties → HeroSection → Navbar → AboutSection → CTASection → Footer)
  → Phase 3.5 (HomePage)
  → Phase 3.6 (App router)
  → Phase 4 (integração, polish, a11y, performance)

Artifacts Gerados

Arquivo Phase Descrição
plan.md Este arquivo
backend/pyproject.toml 1.1 Dependências Python fixadas
backend/app/__init__.py 1.1 App factory Flask
backend/app/config.py 1.1 Configuração por ambiente
backend/app/extensions.py 1.1 Instâncias db/migrate/cors
backend/app/models/property.py 2.1 Modelos Property e PropertyPhoto
backend/app/models/homepage.py 2.1 Modelo HomepageConfig
backend/app/schemas/property.py 2.2 Pydantic schemas de imóvel
backend/app/schemas/homepage.py 2.2 Pydantic schemas de config
backend/app/routes/homepage.py 2.3 GET /api/v1/homepage-config
backend/app/routes/properties.py 2.3 GET /api/v1/properties
backend/seeds/seed.py 2.5 Seeder de dados de exemplo
frontend/tailwind.config.ts 3.1 Tokens DESIGN.md → Tailwind
frontend/src/types/ 3.2 TypeScript interfaces
frontend/src/services/ 3.3 Camada de API (axios)
frontend/src/components/ 3.4 8 componentes da homepage
frontend/src/pages/HomePage.tsx 3.5 Página principal

Próximo passo: executar /speckit.tasks para decompor este plano em tasks implementáveis.