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,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 />
</>
)
}