- 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)
316 lines
15 KiB
TypeScript
316 lines
15 KiB
TypeScript
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 />
|
|
</>
|
|
)
|
|
}
|