feat: features 025-032 - favoritos, contatos, trabalhe-conosco, area-cliente, navbar, hero-light-dark, performance-homepage
Some checks failed
CI/CD → Deploy via SSH / Build & Push Docker Images (push) Successful in 1m0s
CI/CD → Deploy via SSH / Deploy via SSH (push) Successful in 4m35s
CI/CD → Deploy via SSH / Validate HTTPS & Endpoints (push) Failing after 46s

- feat(025): favoritos locais com FavoritesContext, HeartButton, PublicFavoritesPage
- feat(026): central de contatos admin (leads/contatos unificados)
- feat(027): configuração da página de contato via admin
- feat(028): trabalhe conosco - candidaturas com upload e admin
- feat(029): UX área do cliente - visitas, comparação, perfil
- feat(030): navbar UX - menu mobile, ThemeToggle, useFavorites
- feat(031): hero light/dark - imagens separadas por tema, upload, preview, seed
- feat(032): performance homepage - Promise.all parallel fetches, sessionStorage cache,
  preload hero image, loading=lazy nos cards, useInView hook, will-change carrossel,
  keyframes em index.css, AgentsCarousel e HomeScrollScene via props
- fix: light mode HomeScrollScene - gradiente, cores de texto, scroll hint

migrations: g1h2i3j4k5l6 (source em leads), h1i2j3k4l5m6 (contact_config),
            i1j2k3l4m5n6 (job_applications), j2k3l4m5n6o7 (hero theme images)
This commit is contained in:
MatheusAlves96 2026-04-22 22:35:17 -03:00
parent 6ef5a7a17e
commit cf5603243c
106 changed files with 11927 additions and 1367 deletions

View file

@ -0,0 +1,354 @@
# Implementation Plan: Trabalhe Conosco
**Branch**: `028-trabalhe-conosco` | **Date**: 2026-04-21 | **Spec**: [spec.md](spec.md)
**Input**: Feature specification from `/specs/028-trabalhe-conosco/spec.md`
## Summary
Criar a página pública `/trabalhe-conosco` com hero section, seção de benefícios (3 cards estáticos) e formulário de candidatura. O formulário submete via `POST /api/v1/jobs/apply` (endpoint público sem auth). As candidaturas são persistidas na tabela `job_applications` e recuperáveis pelo administrador via `GET /api/v1/admin/jobs` (paginado, protegido por `@require_admin`). Links adicionados no footer (coluna "A Imobiliária") e em `AgentsPage.tsx`. Dois novos blueprints Flask, novo model SQLAlchemy, migration Alembic, schemas Pydantic e uma nova página React com serviço Axios.
---
## Technical Context
**Language/Version**: Python 3.12 (backend) / TypeScript 5.5 (frontend)
**Primary Dependencies**: Flask 3.x, SQLAlchemy 2.x, Pydantic v2, PyJWT, Alembic (backend) · React 18, Tailwind CSS 3.4, react-router-dom v6, Axios (frontend)
**Storage**: PostgreSQL 16 — nova tabela `job_applications`
**Testing**: pytest (backend)
**Target Platform**: Linux server (Docker container)
**Project Type**: web-service (Flask REST API) + SPA (React)
**Performance Goals**: página pública carrega em < 2s (SC-006); listagem admin paginada (20/página)
**Constraints**: sem upload real de arquivo (apenas `file_name` como texto); sem envio de e-mail; sem rate limiting nesta versão; múltiplas candidaturas do mesmo e-mail são permitidas
**Scale/Scope**: volume baixo de candidaturas; paginação padrão 20/página
---
## Constitution Check
| Princípio | Status | Observação |
|-----------|--------|------------|
| **I. Design-First** | ✅ PASS | Hero, cards de benefícios e formulário seguem design tokens dark do `DESIGN.md`; cores `#5e6ad2`, tipografia Inter, cards com `bg-panel border-borderSubtle` |
| **II. Separation of Concerns** | ✅ PASS | Flask retorna JSON puro; React SPA consome via Axios; zero lógica de renderização no backend |
| **III. Spec-Driven** | ✅ PASS | `spec.md` com user stories P1/P2/P3 e acceptance scenarios; plan derivado do spec |
| **IV. Data Integrity** | ✅ PASS | Migration Alembic (`i1j2k3l4m5n6`); Pydantic valida todos os inputs; `email: EmailStr`; sem raw SQL |
| **V. Security** | ✅ PASS | Endpoint admin protegido por `@require_admin` (JWT); endpoint público não expõe dados internos; sem exposição de stack traces em erro 500 |
| **VI. Simplicity First** | ✅ PASS | Sem upload binário (justificado na spec), sem e-mail transacional, sem rate limiting nesta versão; página de admin React adiada para iteração futura (conforme Assumptions da spec) |
**Veredicto**: Sem violações. Pode prosseguir com implementação.
---
## Project Structure
### Documentação (esta feature)
```text
specs/028-trabalhe-conosco/
├── spec.md # Especificação de produto
├── data-model.md # Entidade JobApplication, migration, schemas
├── plan.md # Este arquivo
├── contracts/
│ └── jobs-api.md # Contratos dos 2 endpoints REST
└── tasks.md # (Phase 2 — gerado por /speckit.tasks)
```
### Código-fonte (raiz do repositório)
```text
backend/
├── app/
│ ├── models/
│ │ └── job_application.py # NOVO — modelo SQLAlchemy JobApplication
│ ├── schemas/
│ │ └── job_application.py # NOVO — JobApplicationIn, JobApplicationOut
│ ├── routes/
│ │ └── jobs.py # NOVO — jobs_public_bp + jobs_admin_bp
│ └── __init__.py # MODIFICAR — importar model + registrar blueprints
└── migrations/
└── versions/
└── i1j2k3l4m5n6_add_job_applications.py # NOVO — cria tabela + índices
frontend/
└── src/
├── types/
│ └── jobApplication.ts # NOVO — interface JobApplication
├── services/
│ └── jobs.ts # NOVO — submitApplication(), listApplications()
├── pages/
│ └── JobsPage.tsx # NOVO — página pública /trabalhe-conosco
├── App.tsx # MODIFICAR — adicionar rota /trabalhe-conosco
└── components/
└── Footer.tsx # MODIFICAR — link "Trabalhe Conosco" em "A Imobiliária"
└── pages/
└── AgentsPage.tsx # MODIFICAR — link/botão "Trabalhe Conosco"
```
---
## Backend: Arquitetura Técnica
### Model: `backend/app/models/job_application.py`
Entidade standalone com 9 campos. Ver [data-model.md](data-model.md) para schema completo e invariantes.
Campos principais:
- `id` — PK SERIAL
- `name`, `email`, `role_interest`, `message` — obrigatórios
- `phone`, `file_name` — opcionais
- `status` — default `"pending"` (extensível futuramente)
- `created_at` — server_default `now()`, imutável
### Schemas Pydantic: `backend/app/schemas/job_application.py`
**`JobApplicationIn`** (entrada do endpoint público):
- Valida `name` (não vazio, strip), `email` (EmailStr), `role_interest` (enum de 4 opções), `message` (não vazio, max 5000 chars)
- `phone` e `file_name` opcionais
**`JobApplicationOut`** (saída do endpoint admin):
- Retorna todos os campos incluindo `id`, `status` e `created_at`
- `model_config = ConfigDict(from_attributes=True)` para serialização ORM
### Blueprints: `backend/app/routes/jobs.py`
Dois blueprints no mesmo arquivo, seguindo o padrão de `routes/agents.py`:
```python
jobs_public_bp = Blueprint("jobs_public", __name__, url_prefix="/api/v1")
jobs_admin_bp = Blueprint("jobs_admin", __name__, url_prefix="/api/v1/admin")
```
**`POST /api/v1/jobs/apply`** (público, sem autenticação):
1. `request.get_json(silent=True) or {}`
2. `JobApplicationIn.model_validate(data)` → 422 com `exc.errors()` se inválido
3. Instanciar `JobApplication(...)` e `db.session.add` + `db.session.commit()`
4. Retornar `{"message": "Candidatura recebida com sucesso"}`, HTTP 201
**`GET /api/v1/admin/jobs`** (protegido por `@require_admin`):
1. Query params: `page` (default 1), `per_page` (default 20, max 100)
2. `JobApplication.query.order_by(JobApplication.created_at.desc()).paginate(page, per_page, error_out=False)`
3. Serializar com `JobApplicationOut` e retornar envelope paginado:
```json
{
"items": [...],
"total": 42,
"page": 1,
"per_page": 20,
"pages": 3
}
```
4. `@require_admin` dispara 401/403 automaticamente
### Registro em `backend/app/__init__.py`
Dois patches necessários:
```python
# Importar model (para Flask-Migrate detectar)
from app.models import job_application as _job_application_models # noqa: F401
# Importar e registrar blueprints
from app.routes.jobs import jobs_public_bp, jobs_admin_bp
app.register_blueprint(jobs_public_bp)
app.register_blueprint(jobs_admin_bp)
```
### Migration Alembic
Arquivo: `i1j2k3l4m5n6_add_job_applications.py`
- `down_revision = "h1i2j3k4l5m6"` (migration atual mais recente: `create_contact_config`)
- Cria tabela `job_applications` com 9 colunas
- Cria índices: `ix_job_applications_created_at`, `ix_job_applications_status`
- `downgrade()` desfaz índices e tabela
Ver código completo em [data-model.md](data-model.md).
---
## Frontend: Arquitetura Técnica
### Types: `frontend/src/types/jobApplication.ts`
```typescript
export interface JobApplicationPayload {
name: string
email: string
phone?: string
role_interest: string
message: string
file_name?: string
}
export interface JobApplication {
id: number
name: string
email: string
phone: string | null
role_interest: string
message: string
file_name: string | null
status: string
created_at: string
}
export interface JobApplicationsResponse {
items: JobApplication[]
total: number
page: number
per_page: number
pages: number
}
```
### Service: `frontend/src/services/jobs.ts`
```typescript
import api from './api'
import type { JobApplicationPayload, JobApplicationsResponse } from '../types/jobApplication'
export async function submitApplication(payload: JobApplicationPayload): Promise<void> {
await api.post('/api/v1/jobs/apply', payload)
}
export async function listApplications(
page = 1,
perPage = 20
): Promise<JobApplicationsResponse> {
const { data } = await api.get('/api/v1/admin/jobs', {
params: { page, per_page: perPage },
})
return data
}
```
### Página: `frontend/src/pages/JobsPage.tsx`
Estrutura da página (3 seções, de cima para baixo):
#### 1. Hero Section
```
bg-canvas | max-w-[1200px] mx-auto px-6 pt-16 pb-10
├── eyebrow: "Faça parte do nosso time" (text-[#5e6ad2] uppercase tracking-widest)
├── h1: "Trabalhe Conosco" (text-3xl md:text-4xl font-semibold text-textPrimary)
└── subtítulo: texto descritivo (text-textSecondary)
```
#### 2. Seção "Por que trabalhar conosco?" (3 cards estáticos)
```
max-w-[1200px] mx-auto px-6 py-10
├── h2: "Por que trabalhar conosco?" (text-xl font-semibold text-textPrimary mb-6)
└── grid grid-cols-1 md:grid-cols-3 gap-5
├── Card 1: ícone + "Ambiente Colaborativo" + descrição
├── Card 2: ícone + "Crescimento Profissional" + descrição
└── Card 3: ícone + "Remuneração Competitiva" + descrição
(cada card: bg-panel border border-borderSubtle rounded-2xl p-6)
```
#### 3. Formulário de Candidatura
```
max-w-[640px] mx-auto px-6 pb-20
├── h2: "Envie sua candidatura"
└── <form onSubmit={handleSubmit}>
├── name — input text, obrigatório
├── email — input email, obrigatório, validação RFC
├── phone — input tel, opcional
├── role_interest — select (4 opções), obrigatório
├── message — textarea, obrigatório, max 5000 chars, contador de chars
├── file (currículo) — input file, accept=".pdf", max 2MB (validação frontend only)
│ ao selecionar: setFileName(file.name), não envia binário
└── submit button "Enviar Candidatura"
```
**Gerenciamento de estado** (hooks locais, sem Redux/Context):
- `formData` — estado do formulário
- `fileName` — nome do arquivo selecionado (string | null)
- `errors` — Record<string, string> para mensagens por campo
- `submitting` — boolean, desabilita botão durante requisição
- `submitted` — boolean, exibe mensagem de sucesso e reseta form
- `serverError` — string | null, erro de rede/500
**Validação frontend** (antes de chamar `submitApplication`):
- `name`: obrigatório, trim
- `email`: obrigatório, regex RFC simples
- `role_interest`: obrigatório, não pode ser valor vazio/placeholder
- `message`: obrigatório, max 5000 chars
- `file`: se presente, extensão `.pdf` e tamanho ≤ 2 MB; apenas registra `file_name`
**Fluxo de submit**:
1. Validar campos → exibir erros por campo se inválido
2. `setSubmitting(true)`
3. `submitApplication({ name, email, phone, role_interest, message, file_name: fileName ?? undefined })`
4. Sucesso → `setSubmitted(true)`, resetar `formData`, `setFileName(null)`
5. Erro 422 → parsear `details` e mapear para `errors` por campo
6. Erro ≥ 500 ou rede → `setServerError("Erro ao enviar candidatura. Tente novamente.")`
7. `finally``setSubmitting(false)`
**Design tokens** (seguir padrão do projeto):
- Inputs: `w-full bg-panel border border-borderSubtle rounded-lg px-4 py-2.5 text-textPrimary placeholder:text-textQuaternary focus:outline-none focus:ring-2 focus:ring-[#5e6ad2]/50`
- Labels: `text-sm font-medium text-textSecondary mb-1.5`
- Erros: `text-xs text-red-400 mt-1`
- Botão submit: `w-full bg-[#5e6ad2] hover:bg-[#4f5bbf] text-white font-medium py-2.5 rounded-lg transition-colors duration-150 disabled:opacity-60`
- Mensagem de sucesso: `bg-emerald-500/10 border border-emerald-500/20 rounded-xl p-6 text-center`
### Modificação: `frontend/src/App.tsx`
Adicionar a rota da nova página:
```tsx
import JobsPage from './pages/JobsPage'
// ...
<Route path="/trabalhe-conosco" element={<JobsPage />} />
```
### Modificação: `frontend/src/components/Footer.tsx`
Adicionar link na coluna "A Imobiliária" (após "Política de Privacidade"):
```tsx
<FooterLink to="/trabalhe-conosco">Trabalhe Conosco</FooterLink>
```
### Modificação: `frontend/src/pages/AgentsPage.tsx`
Adicionar banner/botão após o grid de corretores e antes do `<Footer />`:
```tsx
{/* CTA Trabalhe Conosco */}
<div className="max-w-[1200px] mx-auto px-6 pb-20">
<div className="bg-panel border border-borderSubtle rounded-2xl p-8 flex flex-col sm:flex-row items-center justify-between gap-4">
<div>
<h2 className="text-lg font-semibold text-textPrimary">Quer fazer parte do time?</h2>
<p className="text-textSecondary text-sm mt-1">Envie sua candidatura e venha crescer conosco.</p>
</div>
<Link
to="/trabalhe-conosco"
className="shrink-0 bg-[#5e6ad2] hover:bg-[#4f5bbf] text-white font-medium px-5 py-2.5 rounded-lg transition-colors duration-150 text-sm"
>
Trabalhe Conosco
</Link>
</div>
</div>
```
---
## Sequência de Implementação
1. **Migration** — criar `i1j2k3l4m5n6_add_job_applications.py` e rodar `flask db upgrade`
2. **Model** — criar `backend/app/models/job_application.py`
3. **Schemas** — criar `backend/app/schemas/job_application.py`
4. **Routes** — criar `backend/app/routes/jobs.py`
5. **Register** — modificar `backend/app/__init__.py` (model import + blueprints)
6. **Types** — criar `frontend/src/types/jobApplication.ts`
7. **Service** — criar `frontend/src/services/jobs.ts`
8. **Page** — criar `frontend/src/pages/JobsPage.tsx`
9. **Route** — modificar `frontend/src/App.tsx`
10. **Footer** — modificar `frontend/src/components/Footer.tsx`
11. **AgentsPage** — modificar `frontend/src/pages/AgentsPage.tsx`
---
## Complexity Tracking
Sem violações de constituição — seção não aplicável.
| Violation | Why Needed | Simpler Alternative Rejected Because |
|-----------|------------|--------------------------------------|
| — | — | — |