36 KiB
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)
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:
- 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 - Inicializar o repositório de migrações:
uv run flask --app app db init - 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:
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):
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:
priceusaNumeric(12, 2)— nuncaFloat(princípio IV)property_typeé enum nativo do PostgreSQL — rejeita valores inválidos no DBondelete="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_headlineNOT NULL — FR-016 (validation gate no schema Pydantic)featured_properties_limitpadrã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-config→200 OKcom JSON válidoGET /api/v1/properties?featured=true→ array com até 6 imóveisGET /api/v1/properties?featured=truesem imóveis em destaque →[](sem erro 500)uv run pytestpassa emtests/test_homepage.pyetests/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:
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:
#08090acom gradiente sutil radial centralizado (rgba(94,106,210,0.08)) - Headline:
text-[72px] leading-none tracking-display-xl font-medium text-text-primaryno desktop- Tablet (768px):
text-[48px] tracking-display - Mobile (320px–428px):
text-[40px]
- Tablet (768px):
- Subheadline (quando presente):
text-xl text-text-secondarycomfont-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:
interface PropertyCardProps {
property: Property
}
Especificação visual:
- Container:
bg-surface-elevated rounded-xl border border-white/5 overflow-hidden cursor-pointerhover:border-white/8 shadow-lg transition-all duration-200 - Foto:
aspect-[4/3] w-full object-cover- Placeholder (
placeholder-property.jpg) quandophotos.length === 0(FR-011) - Atributo
alt=property.title(NFR-007)
- Placeholder (
- 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
- Venda:
- 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 3PropertyCardSkeleton(NFR edge case)successcom dados: grade de cards (grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6)successsem 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-elevatedcom border topborder-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: <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 buildsem 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
altdescritivo (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-001–NFR-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_featuredtoggleado 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 2–3 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:
#08090aem toda a extensão vertical - Navbar sticky com
backdrop-blurergba(8,9,10,0.85)— não opaca - Headline do hero com
letter-spacing: -1.584pxem 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çãoduration-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-001–NFR-003:
- Imagens do seeder ≤ 300 KB cada (NFR-003) — usar imagens de desenvolvimento de
picsum.photosredimensionadas - 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 previewpara teste de produção
4.5 Acessibilidade Final
Checklist NFR-007–NFR-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
altdescritivo — nãoalt=""exceto se puramente decorativas - Focus ring visível em todos os elementos interativos (não removido com
outline-nonesem 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.