- 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)
14 KiB
Implementation Plan: Trabalhe Conosco
Branch: 028-trabalhe-conosco | Date: 2026-04-21 | Spec: 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)
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)
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 para schema completo e invariantes.
Campos principais:
id— PK SERIALname,email,role_interest,message— obrigatóriosphone,file_name— opcionaisstatus— default"pending"(extensível futuramente)created_at— server_defaultnow(), 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) phoneefile_nameopcionais
JobApplicationOut (saída do endpoint admin):
- Retorna todos os campos incluindo
id,statusecreated_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:
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):
request.get_json(silent=True) or {}JobApplicationIn.model_validate(data)→ 422 comexc.errors()se inválido- Instanciar
JobApplication(...)edb.session.add+db.session.commit() - Retornar
{"message": "Candidatura recebida com sucesso"}, HTTP 201
GET /api/v1/admin/jobs (protegido por @require_admin):
- Query params:
page(default 1),per_page(default 20, max 100) JobApplication.query.order_by(JobApplication.created_at.desc()).paginate(page, per_page, error_out=False)- Serializar com
JobApplicationOute retornar envelope paginado:{ "items": [...], "total": 42, "page": 1, "per_page": 20, "pages": 3 } @require_admindispara 401/403 automaticamente
Registro em backend/app/__init__.py
Dois patches necessários:
# 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_applicationscom 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.
Frontend: Arquitetura Técnica
Types: frontend/src/types/jobApplication.ts
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
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áriofileName— nome do arquivo selecionado (string | null)errors— Record<string, string> para mensagens por camposubmitting— boolean, desabilita botão durante requisiçãosubmitted— boolean, exibe mensagem de sucesso e reseta formserverError— string | null, erro de rede/500
Validação frontend (antes de chamar submitApplication):
name: obrigatório, trimemail: obrigatório, regex RFC simplesrole_interest: obrigatório, não pode ser valor vazio/placeholdermessage: obrigatório, max 5000 charsfile: se presente, extensão.pdfe tamanho ≤ 2 MB; apenas registrafile_name
Fluxo de submit:
- Validar campos → exibir erros por campo se inválido
setSubmitting(true)submitApplication({ name, email, phone, role_interest, message, file_name: fileName ?? undefined })- Sucesso →
setSubmitted(true), resetarformData,setFileName(null) - Erro 422 → parsear
detailse mapear paraerrorspor campo - Erro ≥ 500 ou rede →
setServerError("Erro ao enviar candidatura. Tente novamente.") 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:
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"):
<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 />:
{/* 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
- Migration — criar
i1j2k3l4m5n6_add_job_applications.pye rodarflask db upgrade - Model — criar
backend/app/models/job_application.py - Schemas — criar
backend/app/schemas/job_application.py - Routes — criar
backend/app/routes/jobs.py - Register — modificar
backend/app/__init__.py(model import + blueprints) - Types — criar
frontend/src/types/jobApplication.ts - Service — criar
frontend/src/services/jobs.ts - Page — criar
frontend/src/pages/JobsPage.tsx - Route — modificar
frontend/src/App.tsx - Footer — modificar
frontend/src/components/Footer.tsx - 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 |
|---|---|---|
| — | — | — |