Update infrastructure and frontend configuration

This commit is contained in:
GoHorse Deploy 2026-02-11 13:24:12 +00:00
parent 2649e48a8b
commit 1569deb1ce
13 changed files with 764 additions and 772 deletions

View file

@ -6,14 +6,11 @@ services:
- ./frontend:/app - ./frontend:/app
command: sh -c "npm install && npm run dev -- -p 3000" command: sh -c "npm install && npm run dev -- -p 3000"
ports: ports:
- "3005:3000" - "3000:3000"
environment: environment:
- NEXT_PUBLIC_API_URL=http://gohorse-backend.dokku.rede5.com.br - NEXT_PUBLIC_API_URL=https://api-local.gohorsejobs.com
- API_URL=https://api-local.gohorsejobs.com
- BACKOFFICE_URL=https://b-local.gohorsejobs.com
- SEEDER_API_URL=https://s-local.gohorsejobs.com
- HOST=0.0.0.0 - HOST=0.0.0.0
- HOSTNAME=0.0.0.0
labels:
- "traefik.enable=true"
- "traefik.http.routers.gohorse-frontend.rule=Host(`dev.gohorsejobs.com`)"
- "traefik.http.services.gohorse-frontend.loadbalancer.server.port=3000"
- "traefik.http.routers.gohorse-frontend.entrypoints=web"
restart: always restart: always

View file

@ -7,7 +7,7 @@ FROM base AS deps
WORKDIR /app WORKDIR /app
COPY package.json package-lock.json ./ COPY package.json package-lock.json ./
# Install dependencies using npm # Install dependencies using npm
RUN npm ci RUN npm install
# Stage 3: Builder # Stage 3: Builder
FROM base AS builder FROM base AS builder

View file

@ -1,23 +1,25 @@
import type { NextConfig } from "next"; import type { NextConfig } from "next";
const nextConfig: NextConfig = { const nextConfig: NextConfig = {
// Use standalone output for Docker (smaller image) output: "standalone",
output: "standalone",
// Performance optimizations // Performance optimizations
poweredByHeader: false, // Remove X-Powered-By header (security) poweredByHeader: false,
compress: true, // Enable gzip compression compress: true,
// Optional: Configure allowed image domains // Allow dev server behind proxy
images: { allowedDevOrigins: ["https://dev.gohorsejobs.com"],
remotePatterns: [
{ // Optional: Configure allowed image domains
protocol: "https", images: {
hostname: "**", remotePatterns: [
}, {
], protocol: "https",
qualities: [25, 50, 75, 80, 90, 100], hostname: "**",
}, },
],
qualities: [25, 50, 75, 80, 90, 100],
},
}; };
export default nextConfig; export default nextConfig;

View file

@ -6,6 +6,7 @@ import { Footer } from "@/components/footer"
import { Search, MapPin, Users, Briefcase, Star, TrendingUp, Building2, Globe, Heart, Filter } from "lucide-react" import { Search, MapPin, Users, Briefcase, Star, TrendingUp, Building2, Globe, Heart, Filter } from "lucide-react"
import Image from "next/image" import Image from "next/image"
import Link from "next/link" import Link from "next/link"
import { useTranslation } from "@/lib/i18n"
interface Company { interface Company {
id: number id: number
@ -22,6 +23,7 @@ interface Company {
} }
export default function CompaniesPage() { export default function CompaniesPage() {
const { t } = useTranslation()
const [searchTerm, setSearchTerm] = useState("") const [searchTerm, setSearchTerm] = useState("")
const [selectedIndustry, setSelectedIndustry] = useState("Todas") const [selectedIndustry, setSelectedIndustry] = useState("Todas")
const [selectedSize, setSelectedSize] = useState("Todos") const [selectedSize, setSelectedSize] = useState("Todos")
@ -171,7 +173,6 @@ export default function CompaniesPage() {
<main className="flex-1"> <main className="flex-1">
{/* Hero Section */} {/* Hero Section */}
<section className="relative py-20 md:py-28 overflow-hidden"> <section className="relative py-20 md:py-28 overflow-hidden">
{/* Imagem de fundo empresas.jpg sem overlay laranja */}
<div className="absolute inset-0 z-0"> <div className="absolute inset-0 z-0">
<Image <Image
src="/empresas.jpg" src="/empresas.jpg"
@ -182,7 +183,6 @@ export default function CompaniesPage() {
priority priority
/> />
</div> </div>
{/* Overlay preto com opacidade 20% */}
<div className="absolute inset-0 z-10 bg-black opacity-20"></div> <div className="absolute inset-0 z-10 bg-black opacity-20"></div>
<div className="absolute inset-0 opacity-10 z-20"> <div className="absolute inset-0 opacity-10 z-20">
<div className="absolute top-0 left-0 w-full h-full bg-[radial-gradient(circle_at_30%_50%,rgba(255,255,255,0.2)_0%,transparent_50%)]"></div> <div className="absolute top-0 left-0 w-full h-full bg-[radial-gradient(circle_at_30%_50%,rgba(255,255,255,0.2)_0%,transparent_50%)]"></div>
@ -191,24 +191,22 @@ export default function CompaniesPage() {
<div className="container mx-auto px-4 sm:px-6 lg:px-8 relative z-10"> <div className="container mx-auto px-4 sm:px-6 lg:px-8 relative z-10">
<div className="max-w-4xl mx-auto text-center text-white"> <div className="max-w-4xl mx-auto text-center text-white">
<h1 className="text-4xl md:text-5xl lg:text-6xl font-bold mb-6 drop-shadow-[0_2px_8px_rgba(0,0,0,0.9)]"> <h1 className="text-4xl md:text-5xl lg:text-6xl font-bold mb-6 drop-shadow-[0_2px_8px_rgba(0,0,0,0.9)]">
Descubra as Melhores Empresas {t("companiesPage.hero.title")}
</h1> </h1>
<p className="text-xl md:text-2xl mb-8 opacity-95 drop-shadow-[0_2px_8px_rgba(0,0,0,0.9)]"> <p className="text-xl md:text-2xl mb-8 opacity-95 drop-shadow-[0_2px_8px_rgba(0,0,0,0.9)]">
Conheça empresas incríveis que estão contratando agora {t("companiesPage.hero.subtitle")}
</p> </p>
{/* Search Bar */}
<div className="max-w-3xl mx-auto bg-white rounded-full p-2 shadow-lg"> <div className="max-w-3xl mx-auto bg-white rounded-full p-2 shadow-lg">
<div className="relative"> <div className="relative">
<Search className="absolute left-6 top-1/2 transform -translate-y-1/2 text-gray-400 w-5 h-5" /> <Search className="absolute left-6 top-1/2 transform -translate-y-1/2 text-gray-400 w-5 h-5" />
<input <input
type="text" type="text"
placeholder="Buscar empresas por nome ou setor..." placeholder={t("companiesPage.hero.searchPlaceholder")}
value={searchTerm} value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)} onChange={(e) => setSearchTerm(e.target.value)}
className="w-full pl-14 pr-4 py-3 rounded-full text-gray-900 focus:outline-none focus:ring-2 focus:ring-[#F0932B] text-lg bg-white" className="w-full pl-14 pr-4 py-3 rounded-full text-gray-900 focus:outline-none focus:ring-2 focus:ring-[#F0932B] text-lg bg-white"
/> />
</div> </div>
</div> </div>
</div> </div>
@ -221,7 +219,7 @@ export default function CompaniesPage() {
<div className="flex flex-wrap gap-4 items-center"> <div className="flex flex-wrap gap-4 items-center">
<div className="flex items-center gap-2 text-gray-700"> <div className="flex items-center gap-2 text-gray-700">
<Filter className="w-5 h-5" /> <Filter className="w-5 h-5" />
<span className="font-semibold">Filtros:</span> <span className="font-semibold">{t("companiesPage.filters.label")}</span>
</div> </div>
<select <select
@ -245,7 +243,7 @@ export default function CompaniesPage() {
</select> </select>
<div className="ml-auto text-sm text-gray-600"> <div className="ml-auto text-sm text-gray-600">
<span className="font-semibold text-[#F0932B]">{filteredCompanies.length}</span> empresas encontradas <span className="font-semibold text-[#F0932B]">{filteredCompanies.length}</span> {t("companiesPage.filters.companiesFound")}
</div> </div>
</div> </div>
</div> </div>
@ -257,7 +255,7 @@ export default function CompaniesPage() {
<div className="container mx-auto px-4 sm:px-6 lg:px-8"> <div className="container mx-auto px-4 sm:px-6 lg:px-8">
<div className="flex items-center gap-3 mb-8"> <div className="flex items-center gap-3 mb-8">
<Star className="w-6 h-6 text-[#F0932B]" /> <Star className="w-6 h-6 text-[#F0932B]" />
<h2 className="text-3xl font-bold text-gray-900">Empresas em Destaque</h2> <h2 className="text-3xl font-bold text-gray-900">{t("companiesPage.featured.title")}</h2>
</div> </div>
<div className="grid md:grid-cols-2 gap-6"> <div className="grid md:grid-cols-2 gap-6">
@ -307,13 +305,13 @@ export default function CompaniesPage() {
<div className="flex items-center justify-between pt-4 border-t border-gray-100"> <div className="flex items-center justify-between pt-4 border-t border-gray-100">
<div className="flex items-center gap-2 text-[#F0932B] font-semibold"> <div className="flex items-center gap-2 text-[#F0932B] font-semibold">
<Briefcase className="w-5 h-5" /> <Briefcase className="w-5 h-5" />
<span>{company.openJobs} vagas abertas</span> <span>{company.openJobs} {t("companiesPage.featured.openJobs")}</span>
</div> </div>
<Link <Link
href={`/companies/${company.id}`} href={`/companies/${company.id}`}
className="text-[#F0932B] hover:underline font-semibold" className="text-[#F0932B] hover:underline font-semibold"
> >
Ver perfil {t("companiesPage.featured.viewProfile")} &rarr;
</Link> </Link>
</div> </div>
</div> </div>
@ -326,7 +324,7 @@ export default function CompaniesPage() {
{/* All Companies Grid */} {/* All Companies Grid */}
<section className="py-16 bg-white"> <section className="py-16 bg-white">
<div className="container mx-auto px-4 sm:px-6 lg:px-8"> <div className="container mx-auto px-4 sm:px-6 lg:px-8">
<h2 className="text-3xl font-bold text-gray-900 mb-8">Todas as Empresas</h2> <h2 className="text-3xl font-bold text-gray-900 mb-8">{t("companiesPage.all.title")}</h2>
<div className="grid md:grid-cols-2 lg:grid-cols-3 gap-6"> <div className="grid md:grid-cols-2 lg:grid-cols-3 gap-6">
{filteredCompanies.map((company) => ( {filteredCompanies.map((company) => (
@ -340,7 +338,7 @@ export default function CompaniesPage() {
</div> </div>
{company.featured && ( {company.featured && (
<span className="bg-yellow-100 text-yellow-700 text-xs font-semibold px-2 py-1 rounded-full"> <span className="bg-yellow-100 text-yellow-700 text-xs font-semibold px-2 py-1 rounded-full">
Destaque {t("companiesPage.all.featured")}
</span> </span>
)} )}
</div> </div>
@ -372,13 +370,13 @@ export default function CompaniesPage() {
<div className="pt-4 border-t border-gray-100 flex items-center justify-between"> <div className="pt-4 border-t border-gray-100 flex items-center justify-between">
<span className="text-[#F0932B] font-semibold text-sm"> <span className="text-[#F0932B] font-semibold text-sm">
{company.openJobs} vagas {company.openJobs} {t("companiesPage.all.positions")}
</span> </span>
<Link <Link
href={`/companies/${company.id}`} href={`/companies/${company.id}`}
className="text-gray-600 hover:text-[#F0932B] font-semibold text-sm transition-colors" className="text-gray-600 hover:text-[#F0932B] font-semibold text-sm transition-colors"
> >
Ver mais {t("companiesPage.all.viewMore")} &rarr;
</Link> </Link>
</div> </div>
</div> </div>
@ -388,8 +386,8 @@ export default function CompaniesPage() {
{filteredCompanies.length === 0 && ( {filteredCompanies.length === 0 && (
<div className="text-center py-16"> <div className="text-center py-16">
<Building2 className="w-16 h-16 text-gray-300 mx-auto mb-4" /> <Building2 className="w-16 h-16 text-gray-300 mx-auto mb-4" />
<h3 className="text-xl font-semibold text-gray-900 mb-2">Nenhuma empresa encontrada</h3> <h3 className="text-xl font-semibold text-gray-900 mb-2">{t("companiesPage.empty.title")}</h3>
<p className="text-gray-600">Tente ajustar seus filtros de busca</p> <p className="text-gray-600">{t("companiesPage.empty.subtitle")}</p>
</div> </div>
)} )}
</div> </div>

View file

@ -4,7 +4,7 @@ import { useState, useCallback, useEffect } from "react"
import { Button } from "@/components/ui/button" import { Button } from "@/components/ui/button"
import { mockJobs } from "@/lib/mock-data" import { mockJobs } from "@/lib/mock-data"
import Link from "next/link" import Link from "next/link"
import { ArrowRight, CheckCircle2, ChevronLeft, ChevronRight } from 'lucide-react' import { ArrowRight, CheckCircle2, ChevronLeft, ChevronRight } from "lucide-react"
import Image from "next/image" import Image from "next/image"
import { motion } from "framer-motion" import { motion } from "framer-motion"
import { useTranslation } from "@/lib/i18n" import { useTranslation } from "@/lib/i18n"
@ -12,14 +12,13 @@ import { Navbar } from "@/components/navbar"
import { Footer } from "@/components/footer" import { Footer } from "@/components/footer"
import { HomeSearch } from "@/components/home-search" import { HomeSearch } from "@/components/home-search"
import { JobCard } from "@/components/job-card" import { JobCard } from "@/components/job-card"
import useEmblaCarousel from 'embla-carousel-react' import useEmblaCarousel from "embla-carousel-react"
export default function Home() { export default function Home() {
const { t } = useTranslation() const { t } = useTranslation()
// Embla Carousel for Latest Jobs
const [emblaRef, emblaApi] = useEmblaCarousel({ const [emblaRef, emblaApi] = useEmblaCarousel({
align: 'start', align: "start",
loop: false, loop: false,
skipSnaps: false, skipSnaps: false,
dragFree: true dragFree: true
@ -43,10 +42,9 @@ export default function Home() {
useEffect(() => { useEffect(() => {
if (!emblaApi) return if (!emblaApi) return
onSelect(emblaApi) onSelect(emblaApi)
emblaApi.on('reInit', onSelect) emblaApi.on("reInit", onSelect)
emblaApi.on('select', onSelect) emblaApi.on("select", onSelect)
}, [emblaApi, onSelect]) }, [emblaApi, onSelect])
return ( return (
@ -56,7 +54,6 @@ export default function Home() {
<main className="flex-grow"> <main className="flex-grow">
{/* Hero Section */} {/* Hero Section */}
<section className="relative h-[500px] flex items-center justify-center bg-[#1F2F40]"> <section className="relative h-[500px] flex items-center justify-center bg-[#1F2F40]">
{/* Background Image with Overlay */}
<div className="absolute inset-0 z-0"> <div className="absolute inset-0 z-0">
<Image <Image
src="/10.png" src="/10.png"
@ -76,8 +73,8 @@ export default function Home() {
transition={{ duration: 0.5 }} transition={{ duration: 0.5 }}
className="text-4xl md:text-5xl lg:text-6xl font-bold text-white mb-6 leading-tight tracking-tight" className="text-4xl md:text-5xl lg:text-6xl font-bold text-white mb-6 leading-tight tracking-tight"
> >
Encontre a Vaga de TI <br className="hidden sm:block" /> {t("home.hero.title")} <br className="hidden sm:block" />
dos Seus Sonhos. {t("home.hero.titleLine2")}
</motion.h1> </motion.h1>
<motion.p <motion.p
@ -86,7 +83,7 @@ export default function Home() {
transition={{ duration: 0.5, delay: 0.1 }} transition={{ duration: 0.5, delay: 0.1 }}
className="text-lg md:text-xl text-gray-300 mb-8 max-w-xl leading-relaxed" className="text-lg md:text-xl text-gray-300 mb-8 max-w-xl leading-relaxed"
> >
Conectamos você com as melhores empresas e techs. {t("home.hero.subtitle")}
</motion.p> </motion.p>
<motion.div <motion.div
@ -96,7 +93,7 @@ export default function Home() {
> >
<Link href="/jobs"> <Link href="/jobs">
<Button className="h-12 px-8 bg-orange-500 hover:bg-orange-600 text-white font-bold text-lg rounded-md shadow-lg transition-transform hover:scale-105"> <Button className="h-12 px-8 bg-orange-500 hover:bg-orange-600 text-white font-bold text-lg rounded-md shadow-lg transition-transform hover:scale-105">
Buscar Vagas {t("home.hero.cta")}
</Button> </Button>
</Link> </Link>
</motion.div> </motion.div>
@ -111,12 +108,12 @@ export default function Home() {
</div> </div>
</section> </section>
{/* Latest Jobs Section - WITH CAROUSEL */} {/* Latest Jobs Section */}
<section className="py-12 bg-gray-50"> <section className="py-12 bg-gray-50">
<div className="container mx-auto px-4"> <div className="container mx-auto px-4">
<div className="flex items-center justify-between mb-8"> <div className="flex items-center justify-between mb-8">
<h2 className="text-2xl md:text-3xl font-bold text-gray-900"> <h2 className="text-2xl md:text-3xl font-bold text-gray-900">
Últimas Vagas Cadastradas {t("home.featuredJobs.title")}
</h2> </h2>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Button <Button
@ -157,11 +154,11 @@ export default function Home() {
<div className="container mx-auto px-4"> <div className="container mx-auto px-4">
<div className="flex items-center justify-between mb-8"> <div className="flex items-center justify-between mb-8">
<h2 className="text-2xl md:text-3xl font-bold text-gray-900"> <h2 className="text-2xl md:text-3xl font-bold text-gray-900">
Mais Vagas {t("home.moreJobs.title")}
</h2> </h2>
<Link href="/jobs"> <Link href="/jobs">
<Button className="bg-orange-500 hover:bg-orange-600 text-white font-bold"> <Button className="bg-orange-500 hover:bg-orange-600 text-white font-bold">
Ver Todas Vagas {t("home.moreJobs.viewAll")}
</Button> </Button>
</Link> </Link>
</div> </div>
@ -174,28 +171,24 @@ export default function Home() {
</div> </div>
</section> </section>
{/* Bottom CTA Section */} {/* Bottom CTA Section */}
<section className="py-16 bg-white"> <section className="py-16 bg-white">
<div className="container mx-auto px-4"> <div className="container mx-auto px-4">
<div className="bg-[#1F2F40] rounded-[2rem] p-8 md:p-16 relative overflow-hidden text-center md:text-left flex flex-col md:flex-row items-center justify-between min-h-[400px]"> <div className="bg-[#1F2F40] rounded-[2rem] p-8 md:p-16 relative overflow-hidden text-center md:text-left flex flex-col md:flex-row items-center justify-between min-h-[400px]">
{/* Content */}
<div className="relative z-10 max-w-xl"> <div className="relative z-10 max-w-xl">
<h2 className="text-3xl md:text-4xl font-bold text-white mb-4 leading-tight"> <h2 className="text-3xl md:text-4xl font-bold text-white mb-4 leading-tight">
Milhares de oportunidades <br /> esperam você. {t("home.cta.title")}
</h2> </h2>
<p className="text-base text-gray-300 mb-8"> <p className="text-base text-gray-300 mb-8">
Conecte cargos, talentos, tomada de ações de vagas. {t("home.cta.subtitle")}
</p> </p>
<Link href="/register/user"> <Link href="/register/user">
<Button className="h-12 px-8 bg-white text-gray-900 hover:bg-gray-100 font-bold text-lg rounded-md"> <Button className="h-12 px-8 bg-white text-gray-900 hover:bg-gray-100 font-bold text-lg rounded-md">
Cadastre-se {t("home.cta.button")}
</Button> </Button>
</Link> </Link>
</div> </div>
{/* Image Background for CTA */}
<div className="absolute inset-0 z-0"> <div className="absolute inset-0 z-0">
<div className="absolute right-0 top-0 h-full w-full md:w-2/3 lg:w-1/2"> <div className="absolute right-0 top-0 h-full w-full md:w-2/3 lg:w-1/2">
<Image <Image
@ -204,7 +197,6 @@ export default function Home() {
fill fill
className="object-cover object-center md:object-right opacity-40 md:opacity-100" className="object-cover object-center md:object-right opacity-40 md:opacity-100"
/> />
{/* Gradient Overlay to blend with dark background */}
<div className="absolute inset-0 bg-gradient-to-t md:bg-gradient-to-r from-[#1F2F40] via-[#1F2F40]/30 to-transparent" /> <div className="absolute inset-0 bg-gradient-to-t md:bg-gradient-to-r from-[#1F2F40] via-[#1F2F40]/30 to-transparent" />
</div> </div>
</div> </div>

View file

@ -12,55 +12,55 @@ export function Footer() {
<div className="container mx-auto px-4 sm:px-6 lg:px-8 max-w-8xl"> <div className="container mx-auto px-4 sm:px-6 lg:px-8 max-w-8xl">
<div className="grid grid-cols-1 md:grid-cols-4 gap-8"> <div className="grid grid-cols-1 md:grid-cols-4 gap-8">
<div> <div>
<h3 className="font-bold mb-4 text-gray-900 text-lg">Informações</h3> <h3 className="font-bold mb-4 text-gray-900 text-lg">{t("footerMain.info.title")}</h3>
<ul className="space-y-3"> <ul className="space-y-3">
<li className="flex items-center gap-2"> <li className="flex items-center gap-2">
<span className="w-1.5 h-1.5 bg-gray-400 rounded-full inline-block"></span> <span className="w-1.5 h-1.5 bg-gray-400 rounded-full inline-block"></span>
<Link href="/about" className="text-sm text-gray-600 hover:text-primary transition-colors"> <Link href="/about" className="text-sm text-gray-600 hover:text-primary transition-colors">
Sobre nós {t("footerMain.info.about")}
</Link> </Link>
</li> </li>
<li className="flex items-center gap-2"> <li className="flex items-center gap-2">
<span className="w-1.5 h-1.5 bg-gray-400 rounded-full inline-block"></span> <span className="w-1.5 h-1.5 bg-gray-400 rounded-full inline-block"></span>
<Link href="/contact" className="text-sm text-gray-600 hover:text-primary transition-colors"> <Link href="/contact" className="text-sm text-gray-600 hover:text-primary transition-colors">
Contato {t("footerMain.info.contact")}
</Link> </Link>
</li> </li>
</ul> </ul>
</div> </div>
<div> <div>
<h3 className="font-bold mb-4 text-gray-900 text-lg">Para Candidatos</h3> <h3 className="font-bold mb-4 text-gray-900 text-lg">{t("footerMain.candidates.title")}</h3>
<ul className="space-y-3"> <ul className="space-y-3">
<li className="flex items-center gap-2"> <li className="flex items-center gap-2">
<span className="w-1.5 h-1.5 bg-gray-400 rounded-full inline-block"></span> <span className="w-1.5 h-1.5 bg-gray-400 rounded-full inline-block"></span>
<Link href="/jobs" className="text-sm text-gray-600 hover:text-primary transition-colors"> <Link href="/jobs" className="text-sm text-gray-600 hover:text-primary transition-colors">
Buscar Vagas {t("footerMain.candidates.searchJobs")}
</Link> </Link>
</li> </li>
<li className="flex items-center gap-2"> <li className="flex items-center gap-2">
<span className="w-1.5 h-1.5 bg-gray-400 rounded-full inline-block"></span> <span className="w-1.5 h-1.5 bg-gray-400 rounded-full inline-block"></span>
<Link href="/register/user" className="text-sm text-gray-600 hover:text-primary transition-colors"> <Link href="/register/user" className="text-sm text-gray-600 hover:text-primary transition-colors">
Criar Conta {t("footerMain.candidates.createAccount")}
</Link> </Link>
</li> </li>
</ul> </ul>
</div> </div>
<div> <div>
<h3 className="font-bold mb-4 text-gray-900 text-lg">Para Empresas</h3> <h3 className="font-bold mb-4 text-gray-900 text-lg">{t("footerMain.companies.title")}</h3>
<ul className="space-y-3"> <ul className="space-y-3">
<li className="flex items-center gap-2"> <li className="flex items-center gap-2">
<span className="w-1.5 h-1.5 bg-gray-400 rounded-full inline-block"></span> <span className="w-1.5 h-1.5 bg-gray-400 rounded-full inline-block"></span>
<Link href="/publicar-vaga" className="text-sm text-gray-600 hover:text-primary transition-colors"> <Link href="/publicar-vaga" className="text-sm text-gray-600 hover:text-primary transition-colors">
Publicar Vaga {t("footerMain.companies.postJob")}
</Link> </Link>
</li> </li>
</ul> </ul>
</div> </div>
<div> <div>
<h3 className="font-bold mb-4 text-gray-900 text-lg">Redes Sociais</h3> <h3 className="font-bold mb-4 text-gray-900 text-lg">{t("footerMain.social.title")}</h3>
<div className="flex gap-3"> <div className="flex gap-3">
<a href="#" className="w-10 h-10 rounded-lg border-2 border-gray-300 hover:bg-gray-50 text-[#1F2F40] flex items-center justify-center transition-colors"> <a href="#" className="w-10 h-10 rounded-lg border-2 border-gray-300 hover:bg-gray-50 text-[#1F2F40] flex items-center justify-center transition-colors">
<svg className="w-5 h-5" fill="currentColor" viewBox="0 0 24 24"> <svg className="w-5 h-5" fill="currentColor" viewBox="0 0 24 24">
@ -92,7 +92,7 @@ export function Footer() {
</div> </div>
<div className="mt-12 pt-8 border-t border-gray-200 text-center"> <div className="mt-12 pt-8 border-t border-gray-200 text-center">
<p className="text-sm text-gray-600">© {currentYear} GoHorse jobs. Todos os direitos reservados.</p> <p className="text-sm text-gray-600">&copy; {currentYear} {t("footerMain.copyright")}</p>
</div> </div>
</div> </div>
</footer> </footer>

View file

@ -34,7 +34,7 @@ export function HomeSearch() {
<div className="relative"> <div className="relative">
<Search className="absolute left-3 top-3 h-5 w-5 text-gray-400" /> <Search className="absolute left-3 top-3 h-5 w-5 text-gray-400" />
<Input <Input
placeholder="Digite cargo, empresa ou palavra-chave" placeholder={t("home.search.placeholder")}
className="pl-10 h-12 bg-gray-50 border-gray-200" className="pl-10 h-12 bg-gray-50 border-gray-200"
value={searchTerm} value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)} onChange={(e) => setSearchTerm(e.target.value)}
@ -45,42 +45,42 @@ export function HomeSearch() {
<div className="grid grid-cols-1 md:grid-cols-5 gap-4"> <div className="grid grid-cols-1 md:grid-cols-5 gap-4">
{/* Contract Type */} {/* Contract Type */}
<div className="space-y-2"> <div className="space-y-2">
<label className="text-xs font-semibold text-gray-700 uppercase tracking-wide">Tipo de contratação</label> <label className="text-xs font-semibold text-gray-700 uppercase tracking-wide">{t("home.search.contractType")}</label>
<Select value={type} onValueChange={setType}> <Select value={type} onValueChange={setType}>
<SelectTrigger className="!h-12 bg-gray-50 border-gray-200 w-full"> <SelectTrigger className="!h-12 bg-gray-50 border-gray-200 w-full">
<SelectValue placeholder="Selecione" /> <SelectValue placeholder={t("home.search.select")} />
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
<SelectItem value="all">Todos</SelectItem> <SelectItem value="all">{t("jobs.filters.all")}</SelectItem>
<SelectItem value="contract">PJ</SelectItem> <SelectItem value="contract">{t("home.search.pj")}</SelectItem>
<SelectItem value="full-time">CLT</SelectItem> <SelectItem value="full-time">{t("home.search.clt")}</SelectItem>
<SelectItem value="freelance">Freelancer</SelectItem> <SelectItem value="freelance">{t("home.search.freelancer")}</SelectItem>
</SelectContent> </SelectContent>
</Select> </Select>
</div> </div>
{/* Work Mode */} {/* Work Mode */}
<div className="space-y-2"> <div className="space-y-2">
<label className="text-xs font-semibold text-gray-700 uppercase tracking-wide">Regime de Trabalho</label> <label className="text-xs font-semibold text-gray-700 uppercase tracking-wide">{t("home.search.workMode")}</label>
<Select value={workMode} onValueChange={setWorkMode}> <Select value={workMode} onValueChange={setWorkMode}>
<SelectTrigger className="!h-12 bg-gray-50 border-gray-200 w-full"> <SelectTrigger className="!h-12 bg-gray-50 border-gray-200 w-full">
<SelectValue placeholder="Selecione" /> <SelectValue placeholder={t("home.search.select")} />
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
<SelectItem value="all">Todos</SelectItem> <SelectItem value="all">{t("jobs.filters.all")}</SelectItem>
<SelectItem value="remote">Remoto</SelectItem> <SelectItem value="remote">{t("home.search.homeOffice")}</SelectItem>
<SelectItem value="hybrid">Híbrido</SelectItem> <SelectItem value="hybrid">{t("home.search.hybrid")}</SelectItem>
<SelectItem value="onsite">Presencial</SelectItem> <SelectItem value="onsite">{t("home.search.presencial")}</SelectItem>
</SelectContent> </SelectContent>
</Select> </Select>
</div> </div>
{/* Location */} {/* Location */}
<div className="space-y-2"> <div className="space-y-2">
<label className="text-xs font-semibold text-gray-700 uppercase tracking-wide">Cidade e estado</label> <label className="text-xs font-semibold text-gray-700 uppercase tracking-wide">{t("home.search.location")}</label>
<div className="relative"> <div className="relative">
<Input <Input
placeholder="Cidade e estado" placeholder={t("home.search.location")}
className="h-12 bg-gray-50 border-gray-200" className="h-12 bg-gray-50 border-gray-200"
value={location} value={location}
onChange={(e) => setLocation(e.target.value)} onChange={(e) => setLocation(e.target.value)}
@ -90,7 +90,7 @@ export function HomeSearch() {
{/* Salary */} {/* Salary */}
<div className="space-y-2"> <div className="space-y-2">
<label className="text-xs font-semibold text-gray-700 uppercase tracking-wide">Pretensão salarial</label> <label className="text-xs font-semibold text-gray-700 uppercase tracking-wide">{t("home.search.salary")}</label>
<div className="relative"> <div className="relative">
<Input <Input
placeholder="R$ 0,00" placeholder="R$ 0,00"
@ -108,7 +108,7 @@ export function HomeSearch() {
className="w-full h-12 bg-orange-500 hover:bg-orange-600 text-white font-bold text-lg shadow-md transition-all active:scale-95" className="w-full h-12 bg-orange-500 hover:bg-orange-600 text-white font-bold text-lg shadow-md transition-all active:scale-95"
> >
<Search className="w-5 h-5 mr-2" /> <Search className="w-5 h-5 mr-2" />
Filtrar Vagas {t("home.search.filter")}
</Button> </Button>
</div> </div>
</div> </div>

View file

@ -33,8 +33,8 @@ export function LanguageSwitcher() {
{locales.map((l) => ( {locales.map((l) => (
<DropdownMenuItem <DropdownMenuItem
key={l.code} key={l.code}
onClick={() => { onSelect={() => {
console.log(`[LanguageSwitcher] Clicking ${l.code}`); console.log(`[LanguageSwitcher] Selecting ${l.code}`);
setLocale(l.code); setLocale(l.code);
}} }}
className="flex items-center gap-2 cursor-pointer focus:outline-none focus:bg-accent focus:text-accent-foreground" className="flex items-center gap-2 cursor-pointer focus:outline-none focus:bg-accent focus:text-accent-foreground"

View file

@ -21,6 +21,10 @@ export function Navbar() {
setMounted(true) setMounted(true)
}, []) }, [])
if (!mounted) {
return null; // Or a loading skeleton to avoid mismatch
}
// Static labels for SSR, translated labels for client // Static labels for SSR, translated labels for client
const navigationItems = [ const navigationItems = [
{ href: "/jobs", label: mounted ? t('nav.jobs') : "Vagas" }, { href: "/jobs", label: mounted ? t('nav.jobs') : "Vagas" },

View file

@ -211,41 +211,47 @@
}, },
"home": { "home": {
"hero": { "hero": {
"title": "Find the right job, the simple way", "title": "Find Your Dream IT Job.",
"subtitle": "Connecting candidates and companies quickly and directly", "titleLine2": "",
"searchJobs": "Search jobs", "subtitle": "We connect you with the best companies and tech opportunities.",
"imCompany": "I'm a company", "cta": "Search Jobs"
"postJob": "Post a job"
}, },
"featured": { "search": {
"title": "Featured Jobs", "placeholder": "Enter keywords",
"subtitle": "Selected opportunities for you", "filter": "Filter Jobs",
"viewAll": "View all jobs" "contractType": "Contract Type",
"workMode": "Work Mode",
"location": "City and State",
"salary": "Salary Expectation",
"select": "Select",
"pj": "Contractor",
"clt": "Full-time",
"freelancer": "Freelancer",
"homeOffice": "Remote",
"presencial": "On-site",
"hybrid": "Hybrid"
}, },
"howItWorks": { "featuredJobs": {
"title": "How it works?", "title": "Latest Registered Jobs",
"subtitle": "Three simple steps to your next opportunity", "yesterday": "Yesterday",
"step1": { "apply": "Apply now",
"title": "1. Sign up", "viewJob": "View Job",
"description": "Create your free profile in just a few minutes" "favorite": "Favorite"
},
"step2": {
"title": "2. Send your resume",
"description": "Add your experiences and skills"
},
"step3": {
"title": "3. Get found",
"description": "Receive offers from interested companies"
}
}, },
"testimonials": { "levels": {
"title": "What our users say?", "junior": "Junior",
"subtitle": "Success stories from those who found their opportunity" "mid": "Mid-level",
"senior": "Senior"
},
"moreJobs": {
"title": "More Jobs",
"viewAll": "View All Jobs"
}, },
"cta": { "cta": {
"title": "Ready to start?", "badge": "Social Networks",
"subtitle": "Create your free profile and start receiving job offers today!", "title": "Thousands of opportunities await you.",
"button": "Create free profile" "subtitle": "Connect positions, talents, and job actions.",
"button": "Sign up"
} }
}, },
"jobs": { "jobs": {
@ -320,14 +326,11 @@
"remote": "Remote" "remote": "Remote"
}, },
"footer": { "footer": {
"company": "Company", "vagas": "Jobs",
"about": "About", "empresas": "Companies",
"careers": "Careers", "blog": "Blog",
"jobsByTech": "Jobs by Technology", "login": "Login",
"legal": "Legal", "copyright": "Ghorse jobs. All rights reserved."
"privacy": "Privacy Policy",
"terms": "Terms of Use",
"copyright": "© {year} GoHorse Jobs. All rights reserved."
}, },
"auth": { "auth": {
"login": { "login": {
@ -1295,56 +1298,52 @@
"subjectRequired": "Subject is required" "subjectRequired": "Subject is required"
} }
}, },
"home": { "companiesPage": {
"hero": { "hero": {
"title": "Find Your Dream IT Job.", "title": "Discover the Best Companies",
"titleLine2": "", "subtitle": "Meet amazing companies that are hiring now",
"subtitle": "We connect you with the best companies and tech opportunities.", "searchPlaceholder": "Search companies by name or industry..."
"cta": "Search Jobs"
}, },
"search": { "filters": {
"placeholder": "Enter keywords", "label": "Filters:",
"filter": "Filter Jobs", "allIndustries": "All",
"contractType": "Contract Type", "allSizes": "All",
"workMode": "Work Mode", "companiesFound": "companies found"
"location": "City and State",
"salary": "Salary Expectation",
"select": "Select",
"pj": "Contractor",
"clt": "Full-time",
"freelancer": "Freelancer",
"homeOffice": "Remote",
"presencial": "On-site",
"hybrid": "Hybrid"
}, },
"featuredJobs": { "featured": {
"title": "Latest Registered Jobs", "title": "Featured Companies",
"yesterday": "Yesterday", "openJobs": "open positions",
"apply": "Apply now", "viewProfile": "View profile"
"viewJob": "View Job",
"favorite": "Favorite"
}, },
"levels": { "all": {
"junior": "Junior", "title": "All Companies",
"mid": "Mid-level", "featured": "Featured",
"senior": "Senior" "positions": "positions",
"viewMore": "View more"
}, },
"moreJobs": { "empty": {
"title": "More Jobs", "title": "No companies found",
"viewAll": "View All Jobs" "subtitle": "Try adjusting your search filters"
},
"cta": {
"badge": "Social Networks",
"title": "Thousands of opportunities await you.",
"subtitle": "Connect positions, talents, and job actions.",
"button": "Sign up"
} }
}, },
"footer": { "footerMain": {
"vagas": "Jobs", "info": {
"empresas": "Companies", "title": "Information",
"blog": "Blog", "about": "About Us",
"login": "Login", "contact": "Contact"
"copyright": "Ghorse jobs. All rights reserved." },
"candidates": {
"title": "For Candidates",
"searchJobs": "Search Jobs",
"createAccount": "Create Account"
},
"companies": {
"title": "For Companies",
"postJob": "Post a Job"
},
"social": {
"title": "Social Media"
},
"copyright": "GoHorse Jobs. All rights reserved."
} }
} }

View file

@ -211,41 +211,47 @@
}, },
"home": { "home": {
"hero": { "hero": {
"title": "Encuentra el empleo correcto, de forma sencilla", "title": "Encuentra el Trabajo de TI",
"subtitle": "Conectamos candidatos y empresas de forma rápida y directa", "titleLine2": "de Tus Sueños.",
"searchJobs": "Buscar empleos", "subtitle": "Te conectamos con las mejores empresas y tecnologías.",
"imCompany": "Soy empresa", "cta": "Buscar Empleos"
"postJob": "Publicar vacante"
}, },
"featured": { "search": {
"title": "Empleos Destacados", "placeholder": "Ingresa palabras clave",
"subtitle": "Oportunidades seleccionadas para ti", "filter": "Filtrar Empleos",
"viewAll": "Ver todos los empleos" "contractType": "Tipo de contratación",
"workMode": "Régimen de Trabajo",
"location": "Ciudad y estado",
"salary": "Expectativa salarial",
"select": "Seleccione",
"pj": "Contratista",
"clt": "Tiempo completo",
"freelancer": "Freelancer",
"homeOffice": "Remoto",
"presencial": "Presencial",
"hybrid": "Híbrido"
}, },
"howItWorks": { "featuredJobs": {
"title": "¿Cómo funciona?", "title": "Últimos Empleos Registrados",
"subtitle": "Tres pasos sencillos para tu próxima oportunidad", "yesterday": "Ayer",
"step1": { "apply": "Aplicar ahora",
"title": "1. Regístrate", "viewJob": "Ver Empleo",
"description": "Crea tu perfil gratis en pocos minutos" "favorite": "Favorito"
},
"step2": {
"title": "2. Envía tu currículum",
"description": "Agrega tus experiencias y habilidades"
},
"step3": {
"title": "3. Sé encontrado",
"description": "Recibe ofertas de empresas interesadas"
}
}, },
"testimonials": { "levels": {
"title": "¿Qué dicen nuestros usuarios?", "junior": "Junior",
"subtitle": "Historias de éxito de quienes encontraron su oportunidad" "mid": "Semi-senior",
"senior": "Senior"
},
"moreJobs": {
"title": "Más Empleos",
"viewAll": "Ver Todos los Empleos"
}, },
"cta": { "cta": {
"title": "¿Listo para empezar?", "badge": "Redes Sociales",
"subtitle": "¡Crea tu perfil gratis y comienza a recibir ofertas de empleo hoy!", "title": "Miles de oportunidades te esperan.",
"button": "Crear perfil gratis" "subtitle": "Conecta posiciones, talentos, acciones de empleos.",
"button": "Regístrate"
} }
}, },
"jobs": { "jobs": {
@ -320,14 +326,11 @@
"remote": "Remoto" "remote": "Remoto"
}, },
"footer": { "footer": {
"company": "Empresa", "vagas": "Empleos",
"about": "Sobre", "empresas": "Empresas",
"careers": "Carreras", "blog": "Blog",
"jobsByTech": "Empleos por Tecnología", "login": "Iniciar sesión",
"legal": "Legal", "copyright": "Ghorse jobs. Todos los derechos reservados."
"privacy": "Política de Privacidad",
"terms": "Términos de Uso",
"copyright": "© {year} GoHorse Jobs. Todos los derechos reservados."
}, },
"auth": { "auth": {
"login": { "login": {
@ -1296,56 +1299,52 @@
"subjectRequired": "El asunto es requerido" "subjectRequired": "El asunto es requerido"
} }
}, },
"home": { "companiesPage": {
"hero": { "hero": {
"title": "Encuentra el Trabajo de TI", "title": "Descubre las Mejores Empresas",
"titleLine2": "de Tus Sueños.", "subtitle": "Conoce empresas increíbles que están contratando ahora",
"subtitle": "Te conectamos con las mejores empresas y tecnologías.", "searchPlaceholder": "Buscar empresas por nombre o sector..."
"cta": "Buscar Empleos"
}, },
"search": { "filters": {
"placeholder": "Ingresa palabras clave", "label": "Filtros:",
"filter": "Filtrar Empleos", "allIndustries": "Todas",
"contractType": "Tipo de contratación", "allSizes": "Todos",
"workMode": "Régimen de Trabajo", "companiesFound": "empresas encontradas"
"location": "Ciudad y estado",
"salary": "Expectativa salarial",
"select": "Seleccione",
"pj": "Contratista",
"clt": "Tiempo completo",
"freelancer": "Freelancer",
"homeOffice": "Remoto",
"presencial": "Presencial",
"hybrid": "Híbrido"
}, },
"featuredJobs": { "featured": {
"title": "Últimos Empleos Registrados", "title": "Empresas Destacadas",
"yesterday": "Ayer", "openJobs": "vacantes abiertas",
"apply": "Aplicar ahora", "viewProfile": "Ver perfil"
"viewJob": "Ver Empleo",
"favorite": "Favorito"
}, },
"levels": { "all": {
"junior": "Junior", "title": "Todas las Empresas",
"mid": "Semi-senior", "featured": "Destacada",
"senior": "Senior" "positions": "vacantes",
"viewMore": "Ver más"
}, },
"moreJobs": { "empty": {
"title": "Más Empleos", "title": "No se encontraron empresas",
"viewAll": "Ver Todos los Empleos" "subtitle": "Intenta ajustar tus filtros de búsqueda"
},
"cta": {
"badge": "Redes Sociales",
"title": "Miles de oportunidades te esperan.",
"subtitle": "Conecta posiciones, talentos, acciones de empleos.",
"button": "Regístrate"
} }
}, },
"footer": { "footerMain": {
"vagas": "Empleos", "info": {
"empresas": "Empresas", "title": "Información",
"blog": "Blog", "about": "Sobre nosotros",
"login": "Iniciar sesión", "contact": "Contacto"
"copyright": "Ghorse jobs. Todos los derechos reservados." },
"candidates": {
"title": "Para Candidatos",
"searchJobs": "Buscar Empleos",
"createAccount": "Crear Cuenta"
},
"companies": {
"title": "Para Empresas",
"postJob": "Publicar Empleo"
},
"social": {
"title": "Redes Sociales"
},
"copyright": "GoHorse Jobs. Todos los derechos reservados."
} }
} }

View file

@ -252,39 +252,46 @@
"home": { "home": {
"hero": { "hero": {
"title": "Encontre a Vaga de TI", "title": "Encontre a Vaga de TI",
"titleLine2": "dos Seus Sonhos", "titleLine2": "dos Seus Sonhos.",
"subtitle": "Conectamos você com as melhores empresas e techs.", "subtitle": "Conectamos você com as melhores empresas e techs.",
"cta": "Buscar Vagas" "cta": "Buscar Vagas"
}, },
"featured": { "search": {
"title": "Vagas em Destaque", "placeholder": "Digite as palavras-chave",
"subtitle": "Oportunidades selecionadas para você", "filter": "Filtrar Vagas",
"viewAll": "Ver todas as vagas" "contractType": "Tipo de contratação",
"workMode": "Regime de Trabalho",
"location": "Cidade e estado",
"salary": "Pretensão salarial",
"select": "Selecione",
"pj": "PJ",
"clt": "CLT",
"freelancer": "Freelancer",
"homeOffice": "Home-Office",
"presencial": "Presencial",
"hybrid": "Híbrido"
}, },
"howItWorks": { "featuredJobs": {
"title": "Como Funciona?", "title": "Últimas Vagas Cadastradas",
"subtitle": "Três passos simples para sua próxima oportunidade", "yesterday": "Ontem",
"step1": { "apply": "Aplicar agora",
"title": "1. Cadastre-se", "viewJob": "Ver Vaga",
"description": "Crie seu perfil gratuitamente em poucos minutos" "favorite": "Favoritar"
},
"step2": {
"title": "2. Envie seu currículo",
"description": "Adicione suas experiências e habilidades"
},
"step3": {
"title": "3. Seja encontrado",
"description": "Receba ofertas de empresas interessadas"
}
}, },
"testimonials": { "levels": {
"title": "O que nossos usuários dizem?", "junior": "Júnior",
"subtitle": "Histórias de sucesso de quem encontrou sua oportunidade" "mid": "Pleno",
"senior": "Sênior"
},
"moreJobs": {
"title": "Mais Vagas",
"viewAll": "Ver Todas Vagas"
}, },
"cta": { "cta": {
"title": "Pronto para começar?", "badge": "Redes Sociais",
"subtitle": "Crie seu perfil gratuito e comece a receber ofertas de emprego hoje mesmo!", "title": "Milhares de oportunidades esperam você.",
"button": "Criar perfil gratuito" "subtitle": "Conecte cargos, talentos, tomada de ações de vagas.",
"button": "Cadastre-se"
} }
}, },
"jobs": { "jobs": {
@ -386,14 +393,11 @@
"remote": "Remoto" "remote": "Remoto"
}, },
"footer": { "footer": {
"company": "Empresa", "vagas": "Vagas",
"about": "Sobre", "empresas": "Empresas",
"careers": "Carreiras", "blog": "Blog",
"jobsByTech": "Vagas por Tecnologia", "login": "Login",
"legal": "Legal", "copyright": "Ghorse jobs. Todos os direitos reservados."
"privacy": "Política de Privacidade",
"terms": "Termos de Uso",
"copyright": "© {year} GoHorse Jobs. Todos os direitos reservados."
}, },
"auth": { "auth": {
"login": { "login": {
@ -1329,56 +1333,52 @@
"subjectRequired": "O assunto é obrigatório" "subjectRequired": "O assunto é obrigatório"
} }
}, },
"home": { "companiesPage": {
"hero": { "hero": {
"title": "Encontre a Vaga de TI", "title": "Descubra as Melhores Empresas",
"titleLine2": "dos Seus Sonhos.", "subtitle": "Conheça empresas incríveis que estão contratando agora",
"subtitle": "Conectamos você com as melhores empresas e techs.", "searchPlaceholder": "Buscar empresas por nome ou setor..."
"cta": "Buscar Vagas"
}, },
"search": { "filters": {
"placeholder": "Digite as palavras-chave", "label": "Filtros:",
"filter": "Filtrar Vagas", "allIndustries": "Todas",
"contractType": "Tipo de contratação", "allSizes": "Todos",
"workMode": "Regime de Trabalho", "companiesFound": "empresas encontradas"
"location": "Cidade e estado",
"salary": "Pretensão salarial",
"select": "Selecione",
"pj": "PJ",
"clt": "CLT",
"freelancer": "Freelancer",
"homeOffice": "Home-Office",
"presencial": "Presencial",
"hybrid": "Híbrido"
}, },
"featuredJobs": { "featured": {
"title": "Últimas Vagas Cadastradas", "title": "Empresas em Destaque",
"yesterday": "Ontem", "openJobs": "vagas abertas",
"apply": "Aplicar agora", "viewProfile": "Ver perfil"
"viewJob": "Ver Vaga",
"favorite": "Favoritar"
}, },
"levels": { "all": {
"junior": "Júnior", "title": "Todas as Empresas",
"mid": "Pleno", "featured": "Destaque",
"senior": "Sênior" "positions": "vagas",
"viewMore": "Ver mais"
}, },
"moreJobs": { "empty": {
"title": "Mais Vagas", "title": "Nenhuma empresa encontrada",
"viewAll": "Ver Todas Vagas" "subtitle": "Tente ajustar seus filtros de busca"
},
"cta": {
"badge": "Redes Sociais",
"title": "Milhares de oportunidades esperam você.",
"subtitle": "Conecte cargos, talentos, tomada de ações de vagas.",
"button": "Cadastre-se"
} }
}, },
"footer": { "footerMain": {
"vagas": "Vagas", "info": {
"empresas": "Empresas", "title": "Informações",
"blog": "Blog", "about": "Sobre nós",
"login": "Login", "contact": "Contato"
"copyright": "Ghorse jobs. Todos os direitos reservados." },
"candidates": {
"title": "Para Candidatos",
"searchJobs": "Buscar Vagas",
"createAccount": "Criar Conta"
},
"companies": {
"title": "Para Empresas",
"postJob": "Publicar Vaga"
},
"social": {
"title": "Redes Sociais"
},
"copyright": "GoHorse Jobs. Todos os direitos reservados."
} }
} }

View file

@ -1,6 +1,6 @@
'use client'; 'use client';
import React, { createContext, useContext, useState, useCallback, ReactNode, useEffect } from 'react'; import React, { createContext, useContext, useState, useCallback, ReactNode, useEffect, startTransition } from 'react';
import en from '@/i18n/en.json'; import en from '@/i18n/en.json';
import es from '@/i18n/es.json'; import es from '@/i18n/es.json';
import ptBR from '@/i18n/pt-BR.json'; import ptBR from '@/i18n/pt-BR.json';
@ -32,20 +32,19 @@ const normalizeLocale = (language: string): Locale => {
const getInitialLocale = (): Locale => { const getInitialLocale = (): Locale => {
if (typeof window === 'undefined') return 'en'; if (typeof window === 'undefined') return 'en';
const storedLocale = localStorage.getItem(localeStorageKey); const storedLocale = localStorage.getItem(localeStorageKey);
if (storedLocale && storedLocale in dictionaries) { if (storedLocale && Object.keys(dictionaries).includes(storedLocale)) {
return storedLocale as Locale; return storedLocale as Locale;
} }
// Default to English instead of browser language for consistency if (typeof navigator !== 'undefined' && navigator.language) {
return normalizeLocale(navigator.language);
}
return 'en'; return 'en';
}; };
export function I18nProvider({ children }: { children: ReactNode }) { export function I18nProvider({ children }: { children: ReactNode }) {
// FIX: Initialize with 'en' to match Server-Side Rendering (SSR)
// This prevents hydration mismatch errors.
const [locale, setLocaleState] = useState<Locale>('en'); const [locale, setLocaleState] = useState<Locale>('en');
const setLocale = (newLocale: Locale) => { const setLocale = (newLocale: Locale) => {
console.log(`[I18n] Setting locale to: ${newLocale}`);
setLocaleState(newLocale); setLocaleState(newLocale);
}; };
@ -57,13 +56,12 @@ export function I18nProvider({ children }: { children: ReactNode }) {
if (value && typeof value === 'object' && value !== null && k in value) { if (value && typeof value === 'object' && value !== null && k in value) {
value = (value as Record<string, unknown>)[k]; value = (value as Record<string, unknown>)[k];
} else { } else {
return key; // Return key if not found return key;
} }
} }
if (typeof value !== 'string') return key; if (typeof value !== 'string') return key;
// Replace parameters like {count}, {time}, {year}
if (params) { if (params) {
return Object.entries(params).reduce( return Object.entries(params).reduce(
(str, [paramKey, paramValue]) => str.replace(`{${paramKey}}`, String(paramValue)), (str, [paramKey, paramValue]) => str.replace(`{${paramKey}}`, String(paramValue)),
@ -74,12 +72,15 @@ export function I18nProvider({ children }: { children: ReactNode }) {
return value; return value;
}, [locale]); }, [locale]);
// Sync from localStorage on mount (Client-side only) // Restore locale from localStorage after hydration completes.
// Using startTransition to mark this as non-urgent so React
// finishes hydration before re-rendering with the stored locale.
useEffect(() => { useEffect(() => {
const stored = getInitialLocale(); const stored = getInitialLocale();
if (stored !== 'en') { if (stored !== 'en') {
console.log('[I18n] Restoring locale from storage on client:', stored); startTransition(() => {
setLocaleState(stored); setLocaleState(stored);
});
} }
}, []); }, []);