diff --git a/frontend/src/app/jobs/new/page.tsx b/frontend/src/app/jobs/new/page.tsx index 18fd0de..ec20259 100644 --- a/frontend/src/app/jobs/new/page.tsx +++ b/frontend/src/app/jobs/new/page.tsx @@ -14,6 +14,7 @@ import { Label } from "@/components/ui/label"; import { Checkbox } from "@/components/ui/checkbox"; import { RichTextEditor } from "@/components/rich-text-editor"; import { useTranslation } from "@/lib/i18n"; +import { translations } from "./translations"; type Step = 1 | 2 | 3 | 4; @@ -70,15 +71,155 @@ const contentByLocale = { freeHighlight: "GRÁTIS!", resumesText: "Mais de", resumesHighlight: "50 mil currículos cadastrados", + heroFeatures: [ + "Uma das maiores comunidades de profissionais do mercado", + "Plataforma com alta visibilidade e acesso diário", + "Grande movimentação de candidaturas todos os dias", + "Novos talentos se conectando constantemente", + ], steps: ["Detalhes da vaga", "Pré-visualização", "Informações de faturamento", "Pagamento"], languageLabel: "Idioma da descrição da vaga *", languageHint: "Recomendação: escreva na língua principal do país em que a vaga está sendo anunciada.", selectPlaceholder: "Selecione", - languageOptions: { - "pt-BR": "Português", - en: "English", - es: "Español", + languageOptions: { "pt-BR": "Português", en: "English", es: "Español" }, + labels: { + jobTitle: "Título da vaga *", + jobTitlePlaceholder: "Ex: Desenvolvedor Full Stack", + jobTitleHint: "caracteres. Evite abreviações e excesso de pontuação.", + location: "Localidade da vaga *", + locationPlaceholder: "Ex: São Paulo", + locationHint: "Apenas uma localização por anúncio.", + country: "País *", + contractType: "Tipo de contrato", + workingHours: "Jornada de trabalho", + salary: "Salário", + salaryMode: "Modo de pagamento", + currency: "Moeda", + period: "Período", + salaryMin: "Mín.", + salaryMax: "Máx.", + salaryValue: "Valor", + description: "Descrição da oferta *", + descriptionPlaceholder: "Organize em parágrafos e listas com habilidades e qualificações...", + aboutCompany: "Sobre a empresa (opcional)", + hideOnJob: "Ocultar na vaga", + companyName: "Nome da empresa", + companyNamePlaceholder: "Ex: Tech Company Ltda", + companyWebsite: "Site da empresa", + companyWebsitePlaceholder: "https://empresa.com", + employeeCount: "Número de empregados", + foundedYear: "Ano de fundação", + foundedYearPlaceholder: "Ex: 2012", + cnpj: "CNPJ", + cnpjPlaceholder: "00.000.000/0000-00", + companyDescription: "Descrição da empresa", + companyDescPlaceholder: "Convide os candidatos a conhecer a organização...", + applicationChannel: "Como receber candidaturas", + resumeRequirement: "Requerer envio de currículo?", + applicationEmail: "E-mail para candidatura", + applicationUrl: "Link externo (HTTPS)", + applicationPhone: "Telefone para candidatura (com DDI)", + applicationEmailPlaceholder: "jobs@empresa.com", + applicationUrlPlaceholder: "https://empresa.com/carreiras", + applicationPhonePlaceholder: "+55 11999998888", }, + options: { + select: "Selecione", + any: "Qualquer", + salaryRange: "Faixa salarial", + salaryFixed: "Salário fixo", + permanent: "Permanente", + contract: "Contrato (PJ)", + training: "Estágio/Trainee", + temporary: "Temporário", + voluntary: "Voluntário", + fullTime: "Tempo integral", + partTime: "Meio período", + hourly: "Hora", + daily: "Dia", + weekly: "Semana", + monthly: "Mês", + yearly: "Ano", + resumeRequired: "Obrigatório", + resumeOptional: "Opcional", + resumeNone: "Não solicitado", + channelEmail: "E-mail", + channelUrl: "Link externo", + channelPhone: "Telefone", + }, + preview: { + title: "Pré-visualização", + jobTitle: "Título", + location: "Localidade", + country: "País", + contract: "Contrato/Jornada", + applications: "Candidaturas", + resume: "Currículo", + company: "Empresa", + estimatedPrice: "Preço estimado", + notInformed: "Não informado", + }, + billing: { + fiscalType: "Tipo fiscal", + fiscalCompany: "Pessoa jurídica", + fiscalIndividual: "Pessoa física", + fiscalDocument: "Documento fiscal *", + cnpjLabel: "CNPJ", + cpfLabel: "CPF/NIF", + billingCountry: "País de faturamento *", + billingEmail: "E-mail de faturamento *", + billingEmailPlaceholder: "fiscal@empresa.com", + billingAddress: "Endereço de cobrança *", + billingAddressPlaceholder: "Rua, número, cidade, estado e CEP", + contactName: "Nome do responsável", + contactNamePlaceholder: "Seu nome", + contactLastName: "Sobrenome", + contactLastNamePlaceholder: "Seu sobrenome", + phone: "Telefone fixo", + phonePlaceholder: "(00) 0000-0000", + mobile: "Celular", + mobilePlaceholder: "(00) 00000-0000", + termsAccept: "Li e aceito as", + termsLink: "Condições Legais", + termsAnd: "e a", + privacyLink: "Política de Privacidade", + marketingAccept: "Autorizo comunicações comerciais do GoHorse Jobs.", + }, + payment: { + method: "Método de pagamento *", + card: "Cartão de crédito", + pix: "PIX", + boleto: "Boleto", + plan: "Plano", + job: "Vaga", + country: "País", + billing: "Faturamento", + submit: "ANUNCIAR VAGA GRÁTIS", + submitting: "PROCESSANDO PAGAMENTO...", + }, + buttons: { + back: "Voltar", + continue: "Continuar", + }, + candidateLink: "Você é um candidato?", + candidateCta: "Cadastre-se grátis aqui!", + errors: { + fillRequired: "Preencha título, localidade, país, idioma e descrição da vaga.", + titleTooLong: "O título da vaga deve ter no máximo 65 caracteres.", + invalidEmail: "Informe um e-mail de candidatura válido.", + invalidUrl: "Informe uma URL HTTPS válida para candidatura.", + invalidPhone: "Informe um telefone com DDI válido. Exemplo: +55 11999998888", + salaryFixedRequired: "Informe o salário fixo.", + salaryRangeRequired: "Informe a faixa salarial (mínimo e máximo).", + invalidCnpj: "CNPJ da empresa inválido.", + billingRequired: "Preencha documento, país, endereço e e-mail de faturamento.", + billingEmailInvalid: "Informe um e-mail de faturamento válido.", + termsRequired: "Você precisa aceitar as condições legais para continuar.", + paymentRequired: "Selecione um método de pagamento.", + registerError: "Erro ao registrar empresa", + unexpectedError: "Erro inesperado ao publicar vaga", + }, + success: "Vaga cadastrada com sucesso!", }, en: { heroTitle: "Post jobs quickly and efficiently", @@ -86,15 +227,155 @@ const contentByLocale = { freeHighlight: "FOR FREE!", resumesText: "More than", resumesHighlight: "50 thousand resumes registered", + heroFeatures: [ + "One of the largest professional communities in the market", + "Platform with high visibility and daily access", + "High volume of applications every day", + "New talent connecting constantly", + ], steps: ["Job details", "Preview", "Billing information", "Payment"], languageLabel: "Job description language *", languageHint: "Recommendation: write in the main language of the country where the job is being advertised.", selectPlaceholder: "Select", - languageOptions: { - "pt-BR": "Portuguese", - en: "English", - es: "Spanish", + languageOptions: { "pt-BR": "Portuguese", en: "English", es: "Spanish" }, + labels: { + jobTitle: "Job title *", + jobTitlePlaceholder: "e.g. Full Stack Developer", + jobTitleHint: "characters. Avoid abbreviations and excessive punctuation.", + location: "Job location *", + locationPlaceholder: "e.g. New York", + locationHint: "Only one location per listing.", + country: "Country *", + contractType: "Contract type", + workingHours: "Working hours", + salary: "Salary", + salaryMode: "Payment mode", + currency: "Currency", + period: "Period", + salaryMin: "Min.", + salaryMax: "Max.", + salaryValue: "Amount", + description: "Job description *", + descriptionPlaceholder: "Organize in paragraphs and lists with skills and qualifications...", + aboutCompany: "About the company (optional)", + hideOnJob: "Hide on listing", + companyName: "Company name", + companyNamePlaceholder: "e.g. Tech Company Ltd", + companyWebsite: "Company website", + companyWebsitePlaceholder: "https://company.com", + employeeCount: "Number of employees", + foundedYear: "Founded year", + foundedYearPlaceholder: "e.g. 2012", + cnpj: "Tax ID", + cnpjPlaceholder: "00.000.000/0000-00", + companyDescription: "Company description", + companyDescPlaceholder: "Invite candidates to learn about the organization...", + applicationChannel: "How to receive applications", + resumeRequirement: "Require resume submission?", + applicationEmail: "Application email", + applicationUrl: "External link (HTTPS)", + applicationPhone: "Application phone (with country code)", + applicationEmailPlaceholder: "jobs@company.com", + applicationUrlPlaceholder: "https://company.com/careers", + applicationPhonePlaceholder: "+1 5551234567", }, + options: { + select: "Select", + any: "Any", + salaryRange: "Salary range", + salaryFixed: "Fixed salary", + permanent: "Permanent", + contract: "Contract", + training: "Internship/Trainee", + temporary: "Temporary", + voluntary: "Voluntary", + fullTime: "Full-time", + partTime: "Part-time", + hourly: "Hour", + daily: "Day", + weekly: "Week", + monthly: "Month", + yearly: "Year", + resumeRequired: "Required", + resumeOptional: "Optional", + resumeNone: "Not requested", + channelEmail: "Email", + channelUrl: "External link", + channelPhone: "Phone", + }, + preview: { + title: "Preview", + jobTitle: "Title", + location: "Location", + country: "Country", + contract: "Contract/Hours", + applications: "Applications", + resume: "Resume", + company: "Company", + estimatedPrice: "Estimated price", + notInformed: "Not informed", + }, + billing: { + fiscalType: "Fiscal type", + fiscalCompany: "Company", + fiscalIndividual: "Individual", + fiscalDocument: "Tax document *", + cnpjLabel: "Tax ID", + cpfLabel: "CPF/NIF", + billingCountry: "Billing country *", + billingEmail: "Billing email *", + billingEmailPlaceholder: "billing@company.com", + billingAddress: "Billing address *", + billingAddressPlaceholder: "Street, number, city, state and zip code", + contactName: "Contact name", + contactNamePlaceholder: "Your name", + contactLastName: "Last name", + contactLastNamePlaceholder: "Your last name", + phone: "Landline", + phonePlaceholder: "(00) 0000-0000", + mobile: "Mobile", + mobilePlaceholder: "(00) 00000-0000", + termsAccept: "I have read and accept the", + termsLink: "Legal Terms", + termsAnd: "and the", + privacyLink: "Privacy Policy", + marketingAccept: "I authorize commercial communications from GoHorse Jobs.", + }, + payment: { + method: "Payment method *", + card: "Credit card", + pix: "PIX", + boleto: "Bank slip", + plan: "Plan", + job: "Job", + country: "Country", + billing: "Billing", + submit: "POST JOB FOR FREE", + submitting: "PROCESSING PAYMENT...", + }, + buttons: { + back: "Back", + continue: "Continue", + }, + candidateLink: "Are you a candidate?", + candidateCta: "Sign up for free here!", + errors: { + fillRequired: "Please fill in title, location, country, language and job description.", + titleTooLong: "Job title must be at most 65 characters.", + invalidEmail: "Please enter a valid application email.", + invalidUrl: "Please enter a valid HTTPS URL for applications.", + invalidPhone: "Please enter a valid phone with country code. Example: +1 5551234567", + salaryFixedRequired: "Please enter the fixed salary.", + salaryRangeRequired: "Please enter the salary range (min and max).", + invalidCnpj: "Invalid company tax ID.", + billingRequired: "Please fill in document, country, address and billing email.", + billingEmailInvalid: "Please enter a valid billing email.", + termsRequired: "You must accept the legal terms to continue.", + paymentRequired: "Please select a payment method.", + registerError: "Error registering company", + unexpectedError: "Unexpected error publishing job", + }, + success: "Job posted successfully!", }, es: { heroTitle: "Publique vacantes de forma rápida y eficiente", @@ -102,18 +383,160 @@ const contentByLocale = { freeHighlight: "¡GRATIS!", resumesText: "Más de", resumesHighlight: "50 mil currículums registrados", + heroFeatures: [ + "Una de las mayores comunidades de profesionales del mercado", + "Plataforma con alta visibilidad y acceso diario", + "Gran volumen de candidaturas todos los días", + "Nuevos talentos conectándose constantemente", + ], steps: ["Detalles de la vacante", "Vista previa", "Información de facturación", "Pago"], languageLabel: "Idioma de la descripción de la vacante *", languageHint: "Recomendación: escriba en el idioma principal del país donde se anuncia la vacante.", selectPlaceholder: "Seleccione", - languageOptions: { - "pt-BR": "Portugués", - en: "Inglés", - es: "Español", + languageOptions: { "pt-BR": "Portugués", en: "Inglés", es: "Español" }, + labels: { + jobTitle: "Título de la vacante *", + jobTitlePlaceholder: "Ej: Desarrollador Full Stack", + jobTitleHint: "caracteres. Evite abreviaciones y exceso de puntuación.", + location: "Ubicación de la vacante *", + locationPlaceholder: "Ej: Ciudad de México", + locationHint: "Solo una ubicación por anuncio.", + country: "País *", + contractType: "Tipo de contrato", + workingHours: "Jornada laboral", + salary: "Salario", + salaryMode: "Modo de pago", + currency: "Moneda", + period: "Período", + salaryMin: "Mín.", + salaryMax: "Máx.", + salaryValue: "Valor", + description: "Descripción de la oferta *", + descriptionPlaceholder: "Organice en párrafos y listas con habilidades y cualificaciones...", + aboutCompany: "Sobre la empresa (opcional)", + hideOnJob: "Ocultar en la vacante", + companyName: "Nombre de la empresa", + companyNamePlaceholder: "Ej: Tech Company Ltda", + companyWebsite: "Sitio web de la empresa", + companyWebsitePlaceholder: "https://empresa.com", + employeeCount: "Número de empleados", + foundedYear: "Año de fundación", + foundedYearPlaceholder: "Ej: 2012", + cnpj: "ID Fiscal", + cnpjPlaceholder: "00.000.000/0000-00", + companyDescription: "Descripción de la empresa", + companyDescPlaceholder: "Invite a los candidatos a conocer la organización...", + applicationChannel: "Cómo recibir candidaturas", + resumeRequirement: "¿Requerir envío de currículum?", + applicationEmail: "Email para candidaturas", + applicationUrl: "Enlace externo (HTTPS)", + applicationPhone: "Teléfono para candidaturas (con código de país)", + applicationEmailPlaceholder: "jobs@empresa.com", + applicationUrlPlaceholder: "https://empresa.com/carreras", + applicationPhonePlaceholder: "+34 612345678", }, + options: { + select: "Seleccione", + any: "Cualquiera", + salaryRange: "Rango salarial", + salaryFixed: "Salario fijo", + permanent: "Permanente", + contract: "Contrato", + training: "Pasantía/Trainee", + temporary: "Temporal", + voluntary: "Voluntario", + fullTime: "Tiempo completo", + partTime: "Medio tiempo", + hourly: "Hora", + daily: "Día", + weekly: "Semana", + monthly: "Mes", + yearly: "Año", + resumeRequired: "Obligatorio", + resumeOptional: "Opcional", + resumeNone: "No solicitado", + channelEmail: "Correo electrónico", + channelUrl: "Enlace externo", + channelPhone: "Teléfono", + }, + preview: { + title: "Vista previa", + jobTitle: "Título", + location: "Ubicación", + country: "País", + contract: "Contrato/Jornada", + applications: "Candidaturas", + resume: "Currículum", + company: "Empresa", + estimatedPrice: "Precio estimado", + notInformed: "No informado", + }, + billing: { + fiscalType: "Tipo fiscal", + fiscalCompany: "Persona jurídica", + fiscalIndividual: "Persona física", + fiscalDocument: "Documento fiscal *", + cnpjLabel: "ID Fiscal", + cpfLabel: "CPF/NIF", + billingCountry: "País de facturación *", + billingEmail: "Email de facturación *", + billingEmailPlaceholder: "fiscal@empresa.com", + billingAddress: "Dirección de facturación *", + billingAddressPlaceholder: "Calle, número, ciudad, estado y código postal", + contactName: "Nombre del responsable", + contactNamePlaceholder: "Su nombre", + contactLastName: "Apellido", + contactLastNamePlaceholder: "Su apellido", + phone: "Teléfono fijo", + phonePlaceholder: "(00) 0000-0000", + mobile: "Celular", + mobilePlaceholder: "(00) 00000-0000", + termsAccept: "He leído y acepto las", + termsLink: "Condiciones Legales", + termsAnd: "y la", + privacyLink: "Política de Privacidad", + marketingAccept: "Autorizo comunicaciones comerciales de GoHorse Jobs.", + }, + payment: { + method: "Método de pago *", + card: "Tarjeta de crédito", + pix: "PIX", + boleto: "Transferencia bancaria", + plan: "Plan", + job: "Vacante", + country: "País", + billing: "Facturación", + submit: "PUBLICAR VACANTE GRATIS", + submitting: "PROCESANDO PAGO...", + }, + buttons: { + back: "Volver", + continue: "Continuar", + }, + candidateLink: "¿Eres candidato?", + candidateCta: "¡Regístrate gratis aquí!", + errors: { + fillRequired: "Complete título, ubicación, país, idioma y descripción de la vacante.", + titleTooLong: "El título de la vacante debe tener como máximo 65 caracteres.", + invalidEmail: "Ingrese un email de candidatura válido.", + invalidUrl: "Ingrese una URL HTTPS válida para candidaturas.", + invalidPhone: "Ingrese un teléfono con código de país válido. Ejemplo: +34 612345678", + salaryFixedRequired: "Ingrese el salario fijo.", + salaryRangeRequired: "Ingrese el rango salarial (mínimo y máximo).", + invalidCnpj: "ID fiscal de la empresa inválido.", + billingRequired: "Complete documento, país, dirección y email de facturación.", + billingEmailInvalid: "Ingrese un email de facturación válido.", + termsRequired: "Debe aceptar las condiciones legales para continuar.", + paymentRequired: "Seleccione un método de pago.", + registerError: "Error al registrar empresa", + unexpectedError: "Error inesperado al publicar vacante", + }, + success: "¡Vacante publicada con éxito!", }, } as const; +type LocaleKey = keyof typeof contentByLocale; + const mapDescriptionLanguageToApi = (language: string) => { if (language === "pt-BR") return "pt"; return language; @@ -122,7 +545,7 @@ const mapDescriptionLanguageToApi = (language: string) => { export default function PostJobPage() { const router = useRouter(); const { locale } = useTranslation(); - const pageCopy = contentByLocale[locale]; + const c = contentByLocale[locale as LocaleKey] || contentByLocale["pt-BR"]; const [step, setStep] = useState(1); const [loading, setLoading] = useState(false); @@ -189,64 +612,53 @@ export default function PostJobPage() { const validateStep1 = () => { if (!job.title || !job.location || !job.country || !job.description || !job.descriptionLanguage) { - toast.error("Preencha título, localidade, país, idioma e descrição da vaga."); + toast.error(c.errors.fillRequired); return false; } - if (job.title.length > 65) { - toast.error("O título da vaga deve ter no máximo 65 caracteres."); + toast.error(c.errors.titleTooLong); return false; } - if (job.applicationChannel === "email" && !isEmail(job.applicationEmail)) { - toast.error("Informe um e-mail de candidatura válido."); + toast.error(c.errors.invalidEmail); return false; } - if (job.applicationChannel === "url" && !isHttpsUrl(job.applicationUrl)) { - toast.error("Informe uma URL HTTPS válida para candidatura."); + toast.error(c.errors.invalidUrl); return false; } - if (job.applicationChannel === "phone" && !isPhoneWithDDI(job.applicationPhone)) { - toast.error("Informe um telefone com DDI válido. Exemplo: +55 11999998888"); + toast.error(c.errors.invalidPhone); return false; } - if (job.salaryMode === "fixed" && !job.salaryFixed) { - toast.error("Informe o salário fixo."); + toast.error(c.errors.salaryFixedRequired); return false; } - if (job.salaryMode === "range" && (!job.salaryMin || !job.salaryMax)) { - toast.error("Informe a faixa salarial (mínimo e máximo)."); + toast.error(c.errors.salaryRangeRequired); return false; } - if (company.document && !isValidCNPJ(company.document)) { - toast.error("CNPJ da empresa inválido."); + toast.error(c.errors.invalidCnpj); return false; } - return true; }; const validateStep3 = () => { if (!billing.document || !billing.billingCountry || !billing.address || !billing.contactEmail) { - toast.error("Preencha documento, país, endereço e e-mail de faturamento."); + toast.error(c.errors.billingRequired); return false; } - if (!isEmail(billing.contactEmail)) { - toast.error("Informe um e-mail de faturamento válido."); + toast.error(c.errors.billingEmailInvalid); return false; } - if (!billing.acceptTerms) { - toast.error("Você precisa aceitar as condições legais para continuar."); + toast.error(c.errors.termsRequired); return false; } - return true; }; @@ -256,12 +668,10 @@ export default function PostJobPage() { setStep(2); return; } - if (step === 2) { setStep(3); return; } - if (step === 3) { if (!validateStep3()) return; setStep(4); @@ -270,9 +680,8 @@ export default function PostJobPage() { const handleSubmit = async () => { if (!validateStep1() || !validateStep3()) return; - if (!paymentMethod) { - toast.error("Selecione um método de pagamento."); + toast.error(c.errors.paymentRequired); return; } @@ -300,7 +709,7 @@ export default function PostJobPage() { if (!registerRes.ok) { const err = await registerRes.json(); - throw new Error(err.message || "Erro ao registrar empresa"); + throw new Error(err.message || c.errors.registerError); } const { token } = await registerRes.json(); @@ -349,16 +758,16 @@ export default function PostJobPage() { if (!jobRes.ok) { const err = await jobRes.json(); - throw new Error(err.message || "Erro ao criar vaga"); + throw new Error(err.message || c.errors.registerError); } localStorage.setItem("token", token); localStorage.setItem("auth_token", token); - toast.success("Vaga cadastrada com sucesso!"); + toast.success(c.success); router.push("/dashboard/jobs"); } catch (error) { - const message = error instanceof Error ? error.message : "Erro inesperado ao publicar vaga"; + const message = error instanceof Error ? error.message : c.errors.unexpectedError; toast.error(message); } finally { setLoading(false); @@ -373,16 +782,11 @@ export default function PostJobPage() {

- {pageCopy.heroTitle} + {c.heroTitle}

    - {[ - "Uma das maiores comunidades de profissionais do mercado", - "Plataforma com alta visibilidade e acesso diário", - "Grande movimentação de candidaturas todos os dias", - "Novos talentos se conectando constantemente", - ].map((item) => ( + {c.heroFeatures.map((item) => (
  • {item} @@ -402,14 +806,14 @@ export default function PostJobPage() {

    - {pageCopy.freeTitle}
    {pageCopy.freeHighlight} + {c.freeTitle}
    {c.freeHighlight}

    - {pageCopy.resumesText} {pageCopy.resumesHighlight} + {c.resumesText} {c.resumesHighlight}

    - {pageCopy.steps.map((label, index) => { + {c.steps.map((label, index) => { const number = (index + 1) as Step; return (
    = number ? "bg-[#ef9225] text-white border-[#ef9225]" : "bg-white text-[#1d2c44]"}`}> @@ -419,30 +823,31 @@ export default function PostJobPage() { })}
    + {/* STEP 1 */} {step === 1 && (
    - + setJob({ ...job, title: e.target.value })} />

    - {job.title.length}/65 caracteres. Evite abreviações e excesso de pontuação. + {job.title.length}/65 {c.labels.jobTitleHint}

    - + setJob({ ...job, location: e.target.value })} /> -

    Apenas uma localização por anúncio.

    +

    {c.labels.locationHint}