# Tasks: Homepage (Página Inicial) **Feature**: `001-homepage` **Branch**: `001-homepage` **Input**: `spec.md`, `plan.md`, `DESIGN.md`, `.specify/memory/constitution.md` **Generated**: 2026-04-13 **Status**: Ready for implementation --- ## Format ``` - [ ] T[NNN] [P?] [USN?] Description — path/to/file.ext ``` - **[P]** — Paralelizável (arquivo diferente, sem dependência de tarefa incompleta) - **[USN]** — User Story associada (US1–US4) - IDs sequenciais na ordem de execução recomendada --- ## Phase 1: Setup — Scaffolding do Projeto **Objetivo**: Criar a estrutura de pastas, dependências e arquivos de configuração. Nenhuma lógica de negócio ainda. | ID | Complexidade | Deps | spec_ref | |----|-------------|------|----------| | T001 | S | — | plan.md §1.1 | | T002 | S | T001 | plan.md §1.1 | | T003 | S | T001 | plan.md §1.1 | | T004 | S | T001 | plan.md §1.1 | | T005 | S | T001 | plan.md §1.1 | | T006 | M | — | plan.md §1.3 | | T007 | S | T006 | plan.md §1.3 | | T008 | M | T007 | plan.md §3.1, DESIGN.md | | T009 | S | T007 | plan.md §3.1 | | T010 | S | T006 | plan.md §1.3 | - [X] T001 Criar estrutura de diretórios do backend — `backend/app/models/`, `backend/app/schemas/`, `backend/app/routes/`, `backend/seeds/`, `backend/tests/`, `backend/migrations/` - **Done when**: Todos os diretórios existem conforme `plan.md` project structure; `__init__.py` vazio em cada subpacote Python. - [X] T002 Criar `backend/pyproject.toml` com dependências Flask, SQLAlchemy, Flask-Migrate, Flask-CORS, Pydantic v2, psycopg2-binary, python-dotenv, pytest, pytest-flask — `backend/pyproject.toml` - **Done when**: `uv sync` executa sem erro; `uv run python -c "import flask; import pydantic"` passa. - [X] T003 Criar `backend/app/config.py` com `DevelopmentConfig`, `ProductionConfig`, `TestingConfig` lendo `DATABASE_URL`, `SECRET_KEY`, `CORS_ORIGINS` de variáveis de ambiente — `backend/app/config.py` - **Done when**: `from app.config import config` importa sem erro; chave ausente levanta `KeyError` explícito. - [X] T004 Criar `backend/app/extensions.py` com instâncias únicas `db = SQLAlchemy()`, `migrate = Migrate()`, `cors = CORS()` — `backend/app/extensions.py` - **Done when**: `from app.extensions import db, migrate, cors` importa sem erro; nenhuma extensão é inicializada neste arquivo (apenas instanciada). - [X] T005 Criar `backend/.env.example` com `DATABASE_URL`, `SECRET_KEY`, `CORS_ORIGINS`, `FLASK_ENV` — `backend/.env.example` - **Done when**: Arquivo presente sem valores reais; todos os campos obrigatórios do `config.py` cobertos. - [X] T006 [P] Criar projeto frontend com `npm create vite@latest frontend -- --template react-ts` — `frontend/` - **Done when**: `cd frontend && npm run dev` sobe em `localhost:5173` sem erros. - [X] T007 Instalar dependências do frontend: `tailwindcss`, `postcss`, `autoprefixer`, `axios`, `react-router-dom`, `@types/react-router-dom` — `frontend/package.json` - **Done when**: `npm install` completa; `node_modules/tailwindcss` e `node_modules/axios` existem; `npm run build` passa. - [X] T008 Criar `frontend/tailwind.config.ts` com todos os tokens de `DESIGN.md`: cores `mkt-black`, `panel-dark`, `surface-elevated`, `brand-indigo`, `accent-violet`, `accent-hover`, pesos `medium: 510`, `semibold: 590`, letter-spacing para display sizes, fontFamily Inter Variable — `frontend/tailwind.config.ts` - **Done when**: `npx tailwindcss --input src/index.css --output /dev/null` compila sem aviso; classe `bg-mkt-black` e `text-brand-indigo` existem no output CSS gerado. - [X] T009 Configurar `frontend/src/index.css`: importar Tailwind (`@tailwind base/components/utilities`), `@import` Inter Variable via Google Fonts, `@layer base` com `font-feature-settings: "cv01", "ss03"` e `body { @apply bg-mkt-black text-text-primary font-sans antialiased }` — `frontend/src/index.css` - **Done when**: Página abre com fundo `#08090a` e fonte Inter Variable sem inline style. - [X] T010 [P] Configurar `frontend/vite.config.ts` com proxy `/api` → `http://localhost:5000` e `changeOrigin: true` — `frontend/vite.config.ts` - **Done when**: `npm run build` passa sem erros TypeScript; proxy configurado no bloco `server.proxy`. **Checkpoint Phase 1**: `uv sync` e `npm run dev` funcionam; estrutura de pastas completa conforme `plan.md`. --- ## Phase 2: Foundational — Infraestrutura Flask + PostgreSQL **Objetivo**: Flask app factory funcional, banco de dados conectado, Flask-Migrate inicializado. Bloqueia todas as fases de user story. **⚠️ CRÍTICO**: Nenhuma User Story pode ser implementada antes desta fase estar completa. | ID | Complexidade | Deps | spec_ref | |----|-------------|------|----------| | T011 | S | T005 | plan.md §1.2 | | T012 | M | T002, T003, T004 | plan.md §1.1 | | T013 | S | T011, T012 | plan.md §1.2 | | T014 | S | T013 | plan.md §1.2 | - [X] T011 Iniciar container 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`; copiar `backend/.env.example` para `backend/.env` e preencher com valores locais — `backend/.env` - **Done when**: `docker ps` mostra container `saas_imob_db` Running; `psql postgresql://imob:imob_dev@localhost:5432/saas_imobiliaria -c "\l"` lista o banco. - [X] T012 Criar `backend/app/__init__.py` com `create_app(config_name="default")` que inicializa `db`, `migrate`, `cors` e registra blueprints de `app.routes` — `backend/app/__init__.py` - **Done when**: `uv run flask --app app shell` abre sem traceback; `db.engine.connect()` no shell retorna sem erro. - [X] T013 Inicializar Flask-Migrate: `uv run flask --app app db init` dentro de `backend/` — `backend/migrations/` - **Done when**: Pasta `backend/migrations/` criada com `env.py` e `versions/` pelo Alembic. - [X] T014 Confirmar conexão DB no shell Flask: `db.engine.connect()` — nenhum arquivo criado - **Done when**: Comando retorna `Connection` sem exceção; PostgreSQL aceita a conexão com `DATABASE_URL` do `.env`. **Checkpoint Phase 2**: `uv run flask --app app db init` ok; `uv run pytest` passa (0 testes, setup ok). --- ## Phase 3: User Story 4 — Admin Configura Conteúdo da Homepage (Priority: P1) **Goal**: API backend completamente funcional: `GET /api/v1/homepage-config` e `GET /api/v1/properties?featured=true` retornando JSON válido com dados do seeder. **Independent Test**: `curl http://localhost:5000/api/v1/homepage-config` retorna `200` com JSON contendo `hero_headline`; `curl "http://localhost:5000/api/v1/properties?featured=true"` retorna array de até 6 imóveis; `uv run pytest` passa nos dois arquivos de teste. | ID | Complexidade | Deps | spec_ref | |----|-------------|------|----------| | T015 | M | T012 | plan.md §2.1, spec.md US4, FR-006, FR-008 | | T016 | S | T012 | plan.md §2.1, spec.md US4, FR-005 | | T017 | M | T015 | plan.md §2.2, spec.md FR-007 | | T018 | M | T016 | plan.md §2.2, spec.md FR-005, FR-016 | | T019 | S | T015, T016 | plan.md §2.4 | | T020 | S | T019 | plan.md §2.4 | | T021 | M | T018, T020 | plan.md §2.3, spec.md US4 scenario 1 | | T022 | M | T017, T020 | plan.md §2.3, spec.md US2, FR-006–009 | | T023 | S | T021, T022 | plan.md §1.1 | | T024 | M | T023 | plan.md §2.5, spec.md US4 scenario 2 | | T025 | S | T012 | plan.md §2 | | T026 | M | T025, T021 | plan.md §2, spec.md US4 scenarios 1–3 | | T027 | M | T025, T022 | plan.md §2, spec.md US2 scenarios 1–2, FR-010 | - [X] T015 [P] [US4] Criar `backend/app/models/property.py` com classes `Property` (UUID pk, title, slug, address, price Numeric(12,2), type enum `venda|aluguel`, bedrooms, bathrooms, area_m2, is_featured, is_active, created_at) e `PropertyPhoto` (id, property_id FK CASCADE, url, alt_text, display_order) com relationship order_by display_order — `backend/app/models/property.py` - **Done when**: `from app.models.property import Property, PropertyPhoto` importa sem erro; campos declarados exatamente conforme `plan.md §2.1`; `price` usa `Numeric(12,2)`, nunca `Float`. - [X] T016 [P] [US4] Criar `backend/app/models/homepage.py` com classe `HomepageConfig` (id Integer PK, hero_headline String(120) NOT NULL, hero_subheadline String(240) nullable, hero_cta_label String(40) default "Ver Imóveis", hero_cta_url String(200) default "/imoveis", featured_properties_limit Integer default 6, updated_at DateTime server_default+onupdate) — `backend/app/models/homepage.py` - **Done when**: `from app.models.homepage import HomepageConfig` importa sem erro; `hero_headline` é NOT NULL no modelo; `featured_properties_limit` tem default 6. - [X] T017 [US4] Criar `backend/app/schemas/property.py` com `PropertyPhotoOut` e `PropertyOut` (Pydantic v2, `model_config = ConfigDict(from_attributes=True)`, `price: Decimal`, `type: Literal["venda", "aluguel"]`, `photos: list[PropertyPhotoOut]`) — `backend/app/schemas/property.py` - **Done when**: `PropertyOut.model_validate(property_instance)` funciona em teste manual; `price` serializa como `Decimal` (não float). - [X] T018 [US4] Criar `backend/app/schemas/homepage.py` com `HomepageConfigOut` e `HomepageConfigIn` (Pydantic v2); `HomepageConfigIn` com `@field_validator("hero_headline")` rejeitando string vazia e `@field_validator("featured_properties_limit")` rejeitando valores fora de 1–12 — `backend/app/schemas/homepage.py` - **Done when**: `HomepageConfigIn(hero_headline="")` levanta `ValidationError`; `HomepageConfigIn(featured_properties_limit=13)` levanta `ValidationError`; instância válida passa. - [X] T019 [US4] Gerar migração inicial com Flask-Migrate: `uv run flask --app app db migrate -m "initial schema: properties, property_photos, homepage_config"` — `backend/migrations/versions/` - **Done when**: Arquivo de migração criado em `backend/migrations/versions/`; revisão manual confirma tabelas `properties`, `property_photos`, `homepage_config` e enum `property_type` presentes. - [X] T020 [US4] Aplicar migração e testar ciclo upgrade/downgrade: `flask db upgrade`, `flask db downgrade base`, `flask db upgrade` — banco de dados - **Done when**: Ambos os comandos executam sem erro; tabelas existem no banco após upgrade final; `\dt` no psql lista as três tabelas. - [X] T021 [US4] Criar `backend/app/routes/homepage.py` com Blueprint `homepage_bp` e rota `GET /api/v1/homepage-config` retornando `HomepageConfigOut.model_validate(config).model_dump()` ou `404` se nenhum registro — `backend/app/routes/homepage.py` - **Done when**: `curl http://localhost:5000/api/v1/homepage-config` retorna `200` com JSON contendo `hero_headline` após seeder; retorna `404` se tabela vazia. - [X] T022 [US4] Criar `backend/app/routes/properties.py` com Blueprint `properties_bp` e rota `GET /api/v1/properties` que filtra por `is_active=True`; quando `featured=true`, filtra por `is_featured=True`, ordena por `created_at DESC`, aplica `limit` de `HomepageConfig.featured_properties_limit` (fallback 6) — `backend/app/routes/properties.py` - **Done when**: `curl "http://localhost:5000/api/v1/properties?featured=true"` retorna array JSON; quando nenhum imóvel featured, retorna `[]` (não 500); limite máximo respeitado conforme config. - [X] T023 [US4] Registrar `homepage_bp` e `properties_bp` no `create_app()` de `backend/app/__init__.py`; importar models de `property` e `homepage` para garantir que Flask-Migrate os detecte — `backend/app/__init__.py` - **Done when**: `flask routes` lista `/api/v1/homepage-config` e `/api/v1/properties`; CORS permite `http://localhost:5173`. - [X] T024 [US4] Criar `backend/seeds/seed.py` que apaga e recria: 1 `HomepageConfig` com headline/subheadline/cta configurados e 6 `Property` com `is_featured=True`, tipos variados (venda/aluguel), e pelo menos 1 `PropertyPhoto` por imóvel usando URLs do `picsum.photos` — `backend/seeds/seed.py` - **Done when**: `uv run python seeds/seed.py` executa sem erro; `GET /api/v1/properties?featured=true` retorna exatamente 6 imóveis; cada imóvel tem `photos` com pelo menos 1 entrada. - [X] T025 [US4] Criar `backend/tests/conftest.py` com fixture `app` (usando `TestingConfig` com SQLite em memória ou PostgreSQL de teste) e fixture `client` (`app.test_client()`) — `backend/tests/conftest.py` - **Done when**: `uv run pytest --collect-only` descobre conftest sem error; fixture `client` disponível nos testes. - [X] T026 [P] [US4] Criar `backend/tests/test_homepage.py` com testes: (1) `GET /api/v1/homepage-config` → `200` com campos obrigatórios; (2) `GET /api/v1/homepage-config` sem registro → `404`; (3) `HomepageConfigIn(hero_headline="")` → `ValidationError` — `backend/tests/test_homepage.py` - **Done when**: `uv run pytest tests/test_homepage.py -v` passa com 3 testes verdes. - [X] T027 [P] [US4] Criar `backend/tests/test_properties.py` com testes: (1) `GET /api/v1/properties?featured=true` → `200` com array; (2) resultado contém campos `id`, `title`, `slug`, `price`, `type`, `bedrooms`, `bathrooms`, `area_m2`, `photos`; (3) sem imóveis featured → `200` com `[]` (não 500) — `backend/tests/test_properties.py` - **Done when**: `uv run pytest tests/test_properties.py -v` passa com 3 testes verdes. **Checkpoint Phase 3 (US4)**: `uv run pytest` passa; ambos os endpoints retornam JSON válido; seeder popula 6 imóveis. --- ## Phase 4: User Story 1 — Visitante Experimenta o Hero e a Navegação (Priority: P1) 🎯 MVP **Goal**: Navbar sticky e HeroSection renderizando com conteúdo real da API; fallback silencioso quando API falha; responsivo em todos os breakpoints. **Independent Test**: Abrir `http://localhost:5173` com backend rodando — Navbar exibe logo + links; Hero exibe headline da API; CTA redireciona para `/imoveis`; sem backend, FALLBACK_CONFIG é exibido silenciosamente. | ID | Complexidade | Deps | spec_ref | |----|-------------|------|----------| | T028 | S | T010 | plan.md §3.2, spec.md US1 | | T029 | S | T010 | plan.md §3.2, spec.md US2 | | T030 | S | T028 | plan.md §3.3 | | T031 | S | T030 | plan.md §3.3 | | T032 | S | T030 | plan.md §3.3 | | T033 | M | T008, T009 | plan.md §3.4, spec.md FR-001–003, NFR-004 | | T034 | L | T031, T033 | plan.md §3.4, spec.md FR-004, US1 scenarios 1–5, NFR-006 | | T035 | M | T031, T034 | plan.md §3.5, spec.md US1 scenario 4, edge-cases | | T036 | S | T035 | plan.md §3.6 | | T037 | S | T036 | plan.md §3.6 | | T038 | S | T007 | plan.md §3.4, spec.md FR-011 | - [X] T028 [P] [US1] Criar `frontend/src/types/homepage.ts` com interface `HomepageConfig` (hero_headline, hero_subheadline `string | null`, hero_cta_label, hero_cta_url, featured_properties_limit) — `frontend/src/types/homepage.ts` - **Done when**: Arquivo exporta interface sem erro TypeScript; todos os campos opcionais/nullable corretamente tipados. - [X] T029 [P] [US1] Criar `frontend/src/types/property.ts` com interfaces `PropertyPhoto` (url, alt_text, display_order) e `Property` (id, title, slug, price `string`, type `'venda' | 'aluguel'`, bedrooms, bathrooms, area_m2, is_featured, photos) — `frontend/src/types/property.ts` - **Done when**: Arquivo exporta ambas as interfaces sem erro TypeScript; `price` é `string` (Decimal serializado do backend). - [X] T030 [US1] Criar `frontend/src/services/api.ts` com instância Axios (`baseURL: '/api/v1'`, `timeout: 8000`, header `Content-Type: application/json`) — `frontend/src/services/api.ts` - **Done when**: `import { api } from './api'` compila sem erro; baseURL aponta para `/api/v1`. - [X] T031 [US1] Criar `frontend/src/services/homepage.ts` com `getHomepageConfig(): Promise` chamando `api.get('/homepage-config')` — `frontend/src/services/homepage.ts` - **Done when**: Função exportada compila sem erro TypeScript; retorna tipo `Promise`. - [X] T032 [US1] Criar `frontend/src/services/properties.ts` com `getFeaturedProperties(): Promise` chamando `api.get('/properties', { params: { featured: 'true' } })` — `frontend/src/services/properties.ts` - **Done when**: Função exportada compila sem erro TypeScript; retorna tipo `Promise`. - [X] T033 [US1] Criar `frontend/src/components/Navbar.tsx`: header semântico (`