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
316
frontend/src/pages/JobsPage.tsx
Normal file
316
frontend/src/pages/JobsPage.tsx
Normal file
|
|
@ -0,0 +1,316 @@
|
|||
import { useState } from 'react'
|
||||
import { Link } from 'react-router-dom'
|
||||
import Footer from '../components/Footer'
|
||||
import Navbar from '../components/Navbar'
|
||||
import { submitJobApplication, type JobApplicationPayload } from '../services/jobs'
|
||||
|
||||
const ROLE_OPTIONS = [
|
||||
'Corretor(a)',
|
||||
'Assistente Administrativo',
|
||||
'Estagiário(a)',
|
||||
'Outro',
|
||||
]
|
||||
|
||||
const BENEFITS = [
|
||||
{
|
||||
icon: (
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.75" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
|
||||
<path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2" /><circle cx="9" cy="7" r="4" /><path d="M23 21v-2a4 4 0 0 0-3-3.87" /><path d="M16 3.13a4 4 0 0 1 0 7.75" />
|
||||
</svg>
|
||||
),
|
||||
title: 'Equipe colaborativa',
|
||||
description: 'Trabalhe com profissionais experientes em um ambiente de apoio mútuo e crescimento constante.',
|
||||
},
|
||||
{
|
||||
icon: (
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.75" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
|
||||
<polyline points="22 12 18 12 15 21 9 3 6 12 2 12" />
|
||||
</svg>
|
||||
),
|
||||
title: 'Crescimento real',
|
||||
description: 'Plano de carreira claro, metas atingíveis e reconhecimento de resultados individuais e coletivos.',
|
||||
},
|
||||
{
|
||||
icon: (
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.75" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
|
||||
<circle cx="12" cy="12" r="10" /><path d="M12 8v4l3 3" />
|
||||
</svg>
|
||||
),
|
||||
title: 'Flexibilidade',
|
||||
description: 'Horários adaptáveis, comissionamento competitivo e autonomia para gerenciar sua agenda.',
|
||||
},
|
||||
]
|
||||
|
||||
interface FormState {
|
||||
name: string
|
||||
email: string
|
||||
phone: string
|
||||
role_interest: string
|
||||
message: string
|
||||
file_name: string
|
||||
privacy: boolean
|
||||
}
|
||||
|
||||
const INITIAL: FormState = {
|
||||
name: '',
|
||||
email: '',
|
||||
phone: '',
|
||||
role_interest: '',
|
||||
message: '',
|
||||
file_name: '',
|
||||
privacy: false,
|
||||
}
|
||||
|
||||
function InputField({
|
||||
label, name, type = 'text', required = false, value, onChange, placeholder,
|
||||
}: {
|
||||
label: string
|
||||
name: string
|
||||
type?: string
|
||||
required?: boolean
|
||||
value: string
|
||||
onChange: (e: React.ChangeEvent<HTMLInputElement>) => void
|
||||
placeholder?: string
|
||||
}) {
|
||||
return (
|
||||
<div className="flex flex-col gap-1.5">
|
||||
<label htmlFor={name} className="text-sm font-medium text-textSecondary">
|
||||
{label}{required && <span className="text-red-400 ml-0.5">*</span>}
|
||||
</label>
|
||||
<input
|
||||
id={name}
|
||||
name={name}
|
||||
type={type}
|
||||
required={required}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
placeholder={placeholder}
|
||||
className="h-10 rounded-lg border border-borderSubtle bg-surface px-3 text-sm text-textPrimary placeholder:text-textQuaternary focus:outline-none focus:ring-2 focus:ring-[#5e6ad2]/40 focus:border-[#5e6ad2] transition"
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default function JobsPage() {
|
||||
const [form, setForm] = useState<FormState>(INITIAL)
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [success, setSuccess] = useState(false)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
|
||||
function handleChange(
|
||||
e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement>
|
||||
) {
|
||||
const { name, value, type } = e.target
|
||||
if (type === 'checkbox') {
|
||||
setForm((prev) => ({ ...prev, [name]: (e.target as HTMLInputElement).checked }))
|
||||
} else {
|
||||
setForm((prev) => ({ ...prev, [name]: value }))
|
||||
}
|
||||
}
|
||||
|
||||
async function handleSubmit(e: React.FormEvent) {
|
||||
e.preventDefault()
|
||||
if (!form.privacy) {
|
||||
setError('Você precisa aceitar a política de privacidade.')
|
||||
return
|
||||
}
|
||||
setLoading(true)
|
||||
setError(null)
|
||||
try {
|
||||
const payload: JobApplicationPayload = {
|
||||
name: form.name,
|
||||
email: form.email,
|
||||
phone: form.phone || undefined,
|
||||
role_interest: form.role_interest,
|
||||
message: form.message,
|
||||
file_name: form.file_name || undefined,
|
||||
}
|
||||
await submitJobApplication(payload)
|
||||
setSuccess(true)
|
||||
setForm(INITIAL)
|
||||
} catch {
|
||||
setError('Não foi possível enviar sua candidatura. Tente novamente.')
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Navbar />
|
||||
<main className="min-h-screen bg-canvas pt-14">
|
||||
|
||||
{/* Hero */}
|
||||
<section className="bg-panel border-b border-borderSubtle">
|
||||
<div className="max-w-[760px] mx-auto px-6 py-16 text-center">
|
||||
<span className="inline-flex items-center gap-1.5 rounded-full border border-[#5e6ad2]/30 bg-[#5e6ad2]/10 px-3 py-1 text-xs font-medium text-[#5e6ad2] mb-5">
|
||||
Oportunidades
|
||||
</span>
|
||||
<h1 className="text-3xl sm:text-4xl font-semibold text-textPrimary tracking-tight mb-4">
|
||||
Trabalhe Conosco
|
||||
</h1>
|
||||
<p className="text-base text-textSecondary leading-relaxed max-w-[540px] mx-auto">
|
||||
Faça parte de um time apaixonado pelo mercado imobiliário. Envie seu currículo e conte-nos por que você quer crescer com a gente.
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Benefícios */}
|
||||
<section className="max-w-[1000px] mx-auto px-6 py-14">
|
||||
<h2 className="text-lg font-semibold text-textPrimary mb-8 text-center">
|
||||
Por que trabalhar conosco?
|
||||
</h2>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-3 gap-6">
|
||||
{BENEFITS.map((b) => (
|
||||
<div key={b.title} className="rounded-xl border border-borderSubtle bg-panel p-6 flex flex-col gap-3">
|
||||
<span className="text-[#5e6ad2]">{b.icon}</span>
|
||||
<h3 className="text-sm font-semibold text-textPrimary">{b.title}</h3>
|
||||
<p className="text-sm text-textTertiary leading-relaxed">{b.description}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Formulário */}
|
||||
<section className="max-w-[680px] mx-auto px-6 pb-20">
|
||||
<div className="rounded-xl border border-borderSubtle bg-panel p-8">
|
||||
<h2 className="text-lg font-semibold text-textPrimary mb-1">
|
||||
Envie sua candidatura
|
||||
</h2>
|
||||
<p className="text-sm text-textTertiary mb-6">
|
||||
Preencha os campos abaixo e entraremos em contato.
|
||||
</p>
|
||||
|
||||
{success ? (
|
||||
<div className="rounded-lg border border-green-500/30 bg-green-500/10 p-6 text-center">
|
||||
<svg className="mx-auto mb-3 text-green-400" width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
|
||||
<path d="M22 11.08V12a10 10 0 1 1-5.93-9.14" /><polyline points="22 4 12 14.01 9 11.01" />
|
||||
</svg>
|
||||
<p className="text-sm font-medium text-green-400">Candidatura enviada com sucesso!</p>
|
||||
<p className="text-xs text-textTertiary mt-1">Entraremos em contato em breve.</p>
|
||||
<button
|
||||
onClick={() => setSuccess(false)}
|
||||
className="mt-4 text-xs text-[#5e6ad2] hover:underline"
|
||||
>
|
||||
Enviar outra candidatura
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<form onSubmit={handleSubmit} noValidate className="flex flex-col gap-5">
|
||||
{/* Dados pessoais */}
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||
<InputField
|
||||
label="Nome completo"
|
||||
name="name"
|
||||
required
|
||||
value={form.name}
|
||||
onChange={handleChange}
|
||||
placeholder="Seu nome"
|
||||
/>
|
||||
<InputField
|
||||
label="E-mail"
|
||||
name="email"
|
||||
type="email"
|
||||
required
|
||||
value={form.email}
|
||||
onChange={handleChange}
|
||||
placeholder="seu@email.com"
|
||||
/>
|
||||
<InputField
|
||||
label="Telefone"
|
||||
name="phone"
|
||||
type="tel"
|
||||
value={form.phone}
|
||||
onChange={handleChange}
|
||||
placeholder="(11) 99999-9999"
|
||||
/>
|
||||
{/* Cargo de interesse */}
|
||||
<div className="flex flex-col gap-1.5">
|
||||
<label htmlFor="role_interest" className="text-sm font-medium text-textSecondary">
|
||||
Cargo de interesse<span className="text-red-400 ml-0.5">*</span>
|
||||
</label>
|
||||
<select
|
||||
id="role_interest"
|
||||
name="role_interest"
|
||||
required
|
||||
value={form.role_interest}
|
||||
onChange={handleChange}
|
||||
className="h-10 rounded-lg border border-borderSubtle bg-surface px-3 text-sm text-textPrimary focus:outline-none focus:ring-2 focus:ring-[#5e6ad2]/40 focus:border-[#5e6ad2] transition"
|
||||
>
|
||||
<option value="">Selecione…</option>
|
||||
{ROLE_OPTIONS.map((r) => (
|
||||
<option key={r} value={r}>{r}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Nome do arquivo */}
|
||||
<InputField
|
||||
label="Nome do arquivo do currículo (PDF)"
|
||||
name="file_name"
|
||||
value={form.file_name}
|
||||
onChange={handleChange}
|
||||
placeholder="curriculo_joao_silva.pdf"
|
||||
/>
|
||||
|
||||
{/* Mensagem */}
|
||||
<div className="flex flex-col gap-1.5">
|
||||
<label htmlFor="message" className="text-sm font-medium text-textSecondary">
|
||||
Apresentação<span className="text-red-400 ml-0.5">*</span>
|
||||
</label>
|
||||
<textarea
|
||||
id="message"
|
||||
name="message"
|
||||
required
|
||||
rows={5}
|
||||
maxLength={5000}
|
||||
value={form.message}
|
||||
onChange={handleChange}
|
||||
placeholder="Fale um pouco sobre você, sua experiência e por que quer trabalhar conosco…"
|
||||
className="rounded-lg border border-borderSubtle bg-surface px-3 py-2.5 text-sm text-textPrimary placeholder:text-textQuaternary focus:outline-none focus:ring-2 focus:ring-[#5e6ad2]/40 focus:border-[#5e6ad2] transition resize-none"
|
||||
/>
|
||||
<span className="text-xs text-textQuaternary text-right">
|
||||
{form.message.length}/5000
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Política */}
|
||||
<label className="flex items-start gap-2.5 cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
name="privacy"
|
||||
checked={form.privacy}
|
||||
onChange={handleChange}
|
||||
className="mt-0.5 h-4 w-4 rounded border-borderSubtle accent-[#5e6ad2]"
|
||||
/>
|
||||
<span className="text-xs text-textTertiary leading-relaxed">
|
||||
Li e aceito a{' '}
|
||||
<Link to="/politica-de-privacidade" className="text-[#5e6ad2] hover:underline" target="_blank" rel="noopener noreferrer">
|
||||
Política de Privacidade
|
||||
</Link>
|
||||
.
|
||||
</span>
|
||||
</label>
|
||||
|
||||
{error && (
|
||||
<p className="text-sm text-red-400">{error}</p>
|
||||
)}
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
disabled={loading}
|
||||
className="w-full rounded-lg bg-[#5e6ad2] py-2.5 text-sm font-semibold text-white hover:bg-[#6872d8] transition disabled:opacity-60 disabled:cursor-not-allowed"
|
||||
>
|
||||
{loading ? 'Enviando…' : 'ENVIAR CANDIDATURA'}
|
||||
</button>
|
||||
</form>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
</main>
|
||||
<Footer />
|
||||
</>
|
||||
)
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue