feat: features 025-032 - favoritos, contatos, trabalhe-conosco, area-cliente, navbar, hero-light-dark, performance-homepage
- 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:
parent
6ef5a7a17e
commit
cf5603243c
106 changed files with 11927 additions and 1367 deletions
354
specs/028-trabalhe-conosco/plan.md
Normal file
354
specs/028-trabalhe-conosco/plan.md
Normal 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 |
|
||||
|-----------|------------|--------------------------------------|
|
||||
| — | — | — |
|
||||
Loading…
Add table
Add a link
Reference in a new issue