diff --git a/docker-compose.yml b/docker-compose.yml index 1cb26c8..eb4b8d1 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,19 +1,16 @@ -services: - frontend: - image: node:20-alpine - working_dir: /app - volumes: - - ./frontend:/app - command: sh -c "npm install && npm run dev -- -p 3000" - ports: - - "3005:3000" - environment: - - NEXT_PUBLIC_API_URL=http://gohorse-backend.dokku.rede5.com.br - - 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 +services: + frontend: + image: node:20-alpine + working_dir: /app + volumes: + - ./frontend:/app + command: sh -c "npm install && npm run dev -- -p 3000" + ports: + - "3000:3000" + environment: + - 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 + restart: always diff --git a/frontend/Dockerfile b/frontend/Dockerfile index 18edfd8..8342060 100644 --- a/frontend/Dockerfile +++ b/frontend/Dockerfile @@ -7,7 +7,7 @@ FROM base AS deps WORKDIR /app COPY package.json package-lock.json ./ # Install dependencies using npm -RUN npm ci +RUN npm install # Stage 3: Builder FROM base AS builder diff --git a/frontend/next.config.ts b/frontend/next.config.ts index 1f2979c..69da248 100644 --- a/frontend/next.config.ts +++ b/frontend/next.config.ts @@ -1,23 +1,25 @@ -import type { NextConfig } from "next"; - -const nextConfig: NextConfig = { - // Use standalone output for Docker (smaller image) - output: "standalone", - - // Performance optimizations - poweredByHeader: false, // Remove X-Powered-By header (security) - compress: true, // Enable gzip compression - - // Optional: Configure allowed image domains - images: { - remotePatterns: [ - { - protocol: "https", - hostname: "**", - }, - ], - qualities: [25, 50, 75, 80, 90, 100], - }, -}; - -export default nextConfig; +import type { NextConfig } from "next"; + +const nextConfig: NextConfig = { + output: "standalone", + + // Performance optimizations + poweredByHeader: false, + compress: true, + + // Allow dev server behind proxy + allowedDevOrigins: ["https://dev.gohorsejobs.com"], + + // Optional: Configure allowed image domains + images: { + remotePatterns: [ + { + protocol: "https", + hostname: "**", + }, + ], + qualities: [25, 50, 75, 80, 90, 100], + }, +}; + +export default nextConfig; diff --git a/frontend/src/app/companies/page.tsx b/frontend/src/app/companies/page.tsx index 2ef2197..ba6c39e 100644 --- a/frontend/src/app/companies/page.tsx +++ b/frontend/src/app/companies/page.tsx @@ -6,6 +6,7 @@ import { Footer } from "@/components/footer" import { Search, MapPin, Users, Briefcase, Star, TrendingUp, Building2, Globe, Heart, Filter } from "lucide-react" import Image from "next/image" import Link from "next/link" +import { useTranslation } from "@/lib/i18n" interface Company { id: number @@ -22,6 +23,7 @@ interface Company { } export default function CompaniesPage() { + const { t } = useTranslation() const [searchTerm, setSearchTerm] = useState("") const [selectedIndustry, setSelectedIndustry] = useState("Todas") const [selectedSize, setSelectedSize] = useState("Todos") @@ -158,7 +160,7 @@ export default function CompaniesPage() { company.description.toLowerCase().includes(searchTerm.toLowerCase()) const matchesIndustry = selectedIndustry === "Todas" || company.industry === selectedIndustry const matchesSize = selectedSize === "Todos" || company.employees === selectedSize - + return matchesSearch && matchesIndustry && matchesSize }) @@ -171,7 +173,6 @@ export default function CompaniesPage() {
{/* Hero Section */}
- {/* Imagem de fundo empresas.jpg sem overlay laranja */}
- {/* Overlay preto com opacidade 20% */}
- +

- Descubra as Melhores Empresas + {t("companiesPage.hero.title")}

- Conheça empresas incríveis que estão contratando agora + {t("companiesPage.hero.subtitle")}

- - {/* Search Bar */} +
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" /> -
@@ -221,9 +219,9 @@ export default function CompaniesPage() {
- Filtros: + {t("companiesPage.filters.label")}
- +
- {filteredCompanies.length} empresas encontradas + {filteredCompanies.length} {t("companiesPage.filters.companiesFound")}
@@ -257,7 +255,7 @@ export default function CompaniesPage() {
-

Empresas em Destaque

+

{t("companiesPage.featured.title")}

@@ -307,13 +305,13 @@ export default function CompaniesPage() {
- {company.openJobs} vagas abertas + {company.openJobs} {t("companiesPage.featured.openJobs")}
- Ver perfil → + {t("companiesPage.featured.viewProfile")} →
@@ -326,7 +324,7 @@ export default function CompaniesPage() { {/* All Companies Grid */}
-

Todas as Empresas

+

{t("companiesPage.all.title")}

{filteredCompanies.map((company) => ( @@ -340,13 +338,13 @@ export default function CompaniesPage() {
{company.featured && ( - Destaque + {t("companiesPage.all.featured")} )}

{company.name}

- +
{company.industry} @@ -372,13 +370,13 @@ export default function CompaniesPage() {
- {company.openJobs} vagas + {company.openJobs} {t("companiesPage.all.positions")} - Ver mais → + {t("companiesPage.all.viewMore")} →
@@ -388,8 +386,8 @@ export default function CompaniesPage() { {filteredCompanies.length === 0 && (
-

Nenhuma empresa encontrada

-

Tente ajustar seus filtros de busca

+

{t("companiesPage.empty.title")}

+

{t("companiesPage.empty.subtitle")}

)}
diff --git a/frontend/src/app/page.tsx b/frontend/src/app/page.tsx index 4b8a7ae..d665f3f 100644 --- a/frontend/src/app/page.tsx +++ b/frontend/src/app/page.tsx @@ -1,219 +1,211 @@ -"use client" - -import { useState, useCallback, useEffect } from "react" -import { Button } from "@/components/ui/button" -import { mockJobs } from "@/lib/mock-data" -import Link from "next/link" -import { ArrowRight, CheckCircle2, ChevronLeft, ChevronRight } from 'lucide-react' -import Image from "next/image" -import { motion } from "framer-motion" -import { useTranslation } from "@/lib/i18n" -import { Navbar } from "@/components/navbar" -import { Footer } from "@/components/footer" -import { HomeSearch } from "@/components/home-search" -import { JobCard } from "@/components/job-card" -import useEmblaCarousel from 'embla-carousel-react' - -export default function Home() { - const { t } = useTranslation() - - // Embla Carousel for Latest Jobs - const [emblaRef, emblaApi] = useEmblaCarousel({ - align: 'start', - loop: false, - skipSnaps: false, - dragFree: true - }) - - const [prevBtnDisabled, setPrevBtnDisabled] = useState(true) - const [nextBtnDisabled, setNextBtnDisabled] = useState(true) - - const scrollPrev = useCallback(() => { - if (emblaApi) emblaApi.scrollPrev() - }, [emblaApi]) - - const scrollNext = useCallback(() => { - if (emblaApi) emblaApi.scrollNext() - }, [emblaApi]) - - const onSelect = useCallback((emblaApi: any) => { - setPrevBtnDisabled(!emblaApi.canScrollPrev()) - setNextBtnDisabled(!emblaApi.canScrollNext()) - }, []) - - useEffect(() => { - if (!emblaApi) return - - onSelect(emblaApi) - emblaApi.on('reInit', onSelect) - emblaApi.on('select', onSelect) - }, [emblaApi, onSelect]) - - return ( -
- - -
- {/* Hero Section */} -
- {/* Background Image with Overlay */} -
- Background -
-
- -
-
- - Encontre a Vaga de TI
- dos Seus Sonhos. -
- - - Conectamos você com as melhores empresas e techs. - - - - - - - -
-
-
- - {/* Search Section */} -
-
- -
-
- - {/* Latest Jobs Section - WITH CAROUSEL */} -
-
-
-

- Últimas Vagas Cadastradas -

-
- - -
-
- -
-
- {mockJobs.slice(0, 8).map((job, index) => ( -
- -
- ))} -
-
-
-
- - {/* More Jobs Section */} -
-
-
-

- Mais Vagas -

- - - -
- -
- {mockJobs.slice(0, 8).map((job, index) => ( - - ))} -
-
-
- - - {/* Bottom CTA Section */} -
-
-
- - {/* Content */} -
-

- Milhares de oportunidades
esperam você. -

-

- Conecte cargos, talentos, tomada de ações de vagas. -

- - - -
- - {/* Image Background for CTA */} -
-
- Professional - {/* Gradient Overlay to blend with dark background */} -
-
-
-
-
-
-
- -
-
- ) -} +"use client" + +import { useState, useCallback, useEffect } from "react" +import { Button } from "@/components/ui/button" +import { mockJobs } from "@/lib/mock-data" +import Link from "next/link" +import { ArrowRight, CheckCircle2, ChevronLeft, ChevronRight } from "lucide-react" +import Image from "next/image" +import { motion } from "framer-motion" +import { useTranslation } from "@/lib/i18n" +import { Navbar } from "@/components/navbar" +import { Footer } from "@/components/footer" +import { HomeSearch } from "@/components/home-search" +import { JobCard } from "@/components/job-card" +import useEmblaCarousel from "embla-carousel-react" + +export default function Home() { + const { t } = useTranslation() + + const [emblaRef, emblaApi] = useEmblaCarousel({ + align: "start", + loop: false, + skipSnaps: false, + dragFree: true + }) + + const [prevBtnDisabled, setPrevBtnDisabled] = useState(true) + const [nextBtnDisabled, setNextBtnDisabled] = useState(true) + + const scrollPrev = useCallback(() => { + if (emblaApi) emblaApi.scrollPrev() + }, [emblaApi]) + + const scrollNext = useCallback(() => { + if (emblaApi) emblaApi.scrollNext() + }, [emblaApi]) + + const onSelect = useCallback((emblaApi: any) => { + setPrevBtnDisabled(!emblaApi.canScrollPrev()) + setNextBtnDisabled(!emblaApi.canScrollNext()) + }, []) + + useEffect(() => { + if (!emblaApi) return + onSelect(emblaApi) + emblaApi.on("reInit", onSelect) + emblaApi.on("select", onSelect) + }, [emblaApi, onSelect]) + + return ( +
+ + +
+ {/* Hero Section */} +
+
+ Background +
+
+ +
+
+ + {t("home.hero.title")}
+ {t("home.hero.titleLine2")} +
+ + + {t("home.hero.subtitle")} + + + + + + + +
+
+
+ + {/* Search Section */} +
+
+ +
+
+ + {/* Latest Jobs Section */} +
+
+
+

+ {t("home.featuredJobs.title")} +

+
+ + +
+
+ +
+
+ {mockJobs.slice(0, 8).map((job, index) => ( +
+ +
+ ))} +
+
+
+
+ + {/* More Jobs Section */} +
+
+
+

+ {t("home.moreJobs.title")} +

+ + + +
+ +
+ {mockJobs.slice(0, 8).map((job, index) => ( + + ))} +
+
+
+ + {/* Bottom CTA Section */} +
+
+
+
+

+ {t("home.cta.title")} +

+

+ {t("home.cta.subtitle")} +

+ + + +
+ +
+
+ Professional +
+
+
+
+
+
+
+ +
+
+ ) +} diff --git a/frontend/src/components/footer.tsx b/frontend/src/components/footer.tsx index f8007b7..c0dfa0f 100644 --- a/frontend/src/components/footer.tsx +++ b/frontend/src/components/footer.tsx @@ -12,55 +12,55 @@ export function Footer() {
-

Informações

+

{t("footerMain.info.title")}

  • - Sobre nós + {t("footerMain.info.about")}
  • - Contato + {t("footerMain.info.contact")}
-

Para Candidatos

+

{t("footerMain.candidates.title")}

  • - Buscar Vagas + {t("footerMain.candidates.searchJobs")}
  • - Criar Conta + {t("footerMain.candidates.createAccount")}
-

Para Empresas

+

{t("footerMain.companies.title")}

  • - Publicar Vaga + {t("footerMain.companies.postJob")}
-

Redes Sociais

+

{t("footerMain.social.title")}

-

© {currentYear} GoHorse jobs. Todos os direitos reservados.

+

© {currentYear} {t("footerMain.copyright")}

diff --git a/frontend/src/components/home-search.tsx b/frontend/src/components/home-search.tsx index 2f8abd7..89d56a5 100644 --- a/frontend/src/components/home-search.tsx +++ b/frontend/src/components/home-search.tsx @@ -1,117 +1,117 @@ -"use client" - -import { useState } from "react" -import { Search, MapPin, DollarSign, Briefcase } from "lucide-react" -import { Button } from "@/components/ui/button" -import { Input } from "@/components/ui/input" -import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select" -import { useRouter } from "next/navigation" -import { useTranslation } from "@/lib/i18n" - -export function HomeSearch() { - const router = useRouter() - const { t } = useTranslation() - const [searchTerm, setSearchTerm] = useState("") - const [location, setLocation] = useState("") - const [type, setType] = useState("") - const [workMode, setWorkMode] = useState("") - const [salary, setSalary] = useState("") - - const handleSearch = () => { - const params = new URLSearchParams() - if (searchTerm) params.set("q", searchTerm) - if (location) params.set("location", location) - if (type && type !== "all") params.set("type", type) - if (workMode && workMode !== "all") params.set("mode", workMode) - if (salary) params.set("salary", salary) - - router.push(`/jobs?${params.toString()}`) - } - - return ( -
-
-
- - setSearchTerm(e.target.value)} - /> -
-
- -
- {/* Contract Type */} -
- - -
- - {/* Work Mode */} -
- - -
- - {/* Location */} -
- -
- setLocation(e.target.value)} - /> -
-
- - {/* Salary */} -
- -
- setSalary(e.target.value)} - /> -
-
- - {/* Search Button */} -
- -
-
-
- ) -} +"use client" + +import { useState } from "react" +import { Search, MapPin, DollarSign, Briefcase } from "lucide-react" +import { Button } from "@/components/ui/button" +import { Input } from "@/components/ui/input" +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select" +import { useRouter } from "next/navigation" +import { useTranslation } from "@/lib/i18n" + +export function HomeSearch() { + const router = useRouter() + const { t } = useTranslation() + const [searchTerm, setSearchTerm] = useState("") + const [location, setLocation] = useState("") + const [type, setType] = useState("") + const [workMode, setWorkMode] = useState("") + const [salary, setSalary] = useState("") + + const handleSearch = () => { + const params = new URLSearchParams() + if (searchTerm) params.set("q", searchTerm) + if (location) params.set("location", location) + if (type && type !== "all") params.set("type", type) + if (workMode && workMode !== "all") params.set("mode", workMode) + if (salary) params.set("salary", salary) + + router.push(`/jobs?${params.toString()}`) + } + + return ( +
+
+
+ + setSearchTerm(e.target.value)} + /> +
+
+ +
+ {/* Contract Type */} +
+ + +
+ + {/* Work Mode */} +
+ + +
+ + {/* Location */} +
+ +
+ setLocation(e.target.value)} + /> +
+
+ + {/* Salary */} +
+ +
+ setSalary(e.target.value)} + /> +
+
+ + {/* Search Button */} +
+ +
+
+
+ ) +} diff --git a/frontend/src/components/language-switcher.tsx b/frontend/src/components/language-switcher.tsx index 41e55f0..08c37d3 100644 --- a/frontend/src/components/language-switcher.tsx +++ b/frontend/src/components/language-switcher.tsx @@ -33,8 +33,8 @@ export function LanguageSwitcher() { {locales.map((l) => ( { - console.log(`[LanguageSwitcher] Clicking ${l.code}`); + onSelect={() => { + console.log(`[LanguageSwitcher] Selecting ${l.code}`); setLocale(l.code); }} className="flex items-center gap-2 cursor-pointer focus:outline-none focus:bg-accent focus:text-accent-foreground" diff --git a/frontend/src/components/navbar.tsx b/frontend/src/components/navbar.tsx index b43579b..2f01dcf 100644 --- a/frontend/src/components/navbar.tsx +++ b/frontend/src/components/navbar.tsx @@ -21,6 +21,10 @@ export function Navbar() { setMounted(true) }, []) + if (!mounted) { + return null; // Or a loading skeleton to avoid mismatch + } + // Static labels for SSR, translated labels for client const navigationItems = [ { href: "/jobs", label: mounted ? t('nav.jobs') : "Vagas" }, diff --git a/frontend/src/i18n/en.json b/frontend/src/i18n/en.json index 5cccd87..70e6342 100644 --- a/frontend/src/i18n/en.json +++ b/frontend/src/i18n/en.json @@ -211,41 +211,47 @@ }, "home": { "hero": { - "title": "Find the right job, the simple way", - "subtitle": "Connecting candidates and companies quickly and directly", - "searchJobs": "Search jobs", - "imCompany": "I'm a company", - "postJob": "Post a job" + "title": "Find Your Dream IT Job.", + "titleLine2": "", + "subtitle": "We connect you with the best companies and tech opportunities.", + "cta": "Search Jobs" }, - "featured": { - "title": "Featured Jobs", - "subtitle": "Selected opportunities for you", - "viewAll": "View all jobs" + "search": { + "placeholder": "Enter keywords", + "filter": "Filter 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": { - "title": "How it works?", - "subtitle": "Three simple steps to your next opportunity", - "step1": { - "title": "1. Sign up", - "description": "Create your free profile in just a few minutes" - }, - "step2": { - "title": "2. Send your resume", - "description": "Add your experiences and skills" - }, - "step3": { - "title": "3. Get found", - "description": "Receive offers from interested companies" - } + "featuredJobs": { + "title": "Latest Registered Jobs", + "yesterday": "Yesterday", + "apply": "Apply now", + "viewJob": "View Job", + "favorite": "Favorite" }, - "testimonials": { - "title": "What our users say?", - "subtitle": "Success stories from those who found their opportunity" + "levels": { + "junior": "Junior", + "mid": "Mid-level", + "senior": "Senior" + }, + "moreJobs": { + "title": "More Jobs", + "viewAll": "View All Jobs" }, "cta": { - "title": "Ready to start?", - "subtitle": "Create your free profile and start receiving job offers today!", - "button": "Create free profile" + "badge": "Social Networks", + "title": "Thousands of opportunities await you.", + "subtitle": "Connect positions, talents, and job actions.", + "button": "Sign up" } }, "jobs": { @@ -320,14 +326,11 @@ "remote": "Remote" }, "footer": { - "company": "Company", - "about": "About", - "careers": "Careers", - "jobsByTech": "Jobs by Technology", - "legal": "Legal", - "privacy": "Privacy Policy", - "terms": "Terms of Use", - "copyright": "© {year} GoHorse Jobs. All rights reserved." + "vagas": "Jobs", + "empresas": "Companies", + "blog": "Blog", + "login": "Login", + "copyright": "Ghorse jobs. All rights reserved." }, "auth": { "login": { @@ -1295,56 +1298,52 @@ "subjectRequired": "Subject is required" } }, - "home": { + "companiesPage": { "hero": { - "title": "Find Your Dream IT Job.", - "titleLine2": "", - "subtitle": "We connect you with the best companies and tech opportunities.", - "cta": "Search Jobs" + "title": "Discover the Best Companies", + "subtitle": "Meet amazing companies that are hiring now", + "searchPlaceholder": "Search companies by name or industry..." }, - "search": { - "placeholder": "Enter keywords", - "filter": "Filter 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" + "filters": { + "label": "Filters:", + "allIndustries": "All", + "allSizes": "All", + "companiesFound": "companies found" }, - "featuredJobs": { - "title": "Latest Registered Jobs", - "yesterday": "Yesterday", - "apply": "Apply now", - "viewJob": "View Job", - "favorite": "Favorite" + "featured": { + "title": "Featured Companies", + "openJobs": "open positions", + "viewProfile": "View profile" }, - "levels": { - "junior": "Junior", - "mid": "Mid-level", - "senior": "Senior" + "all": { + "title": "All Companies", + "featured": "Featured", + "positions": "positions", + "viewMore": "View more" }, - "moreJobs": { - "title": "More Jobs", - "viewAll": "View All Jobs" - }, - "cta": { - "badge": "Social Networks", - "title": "Thousands of opportunities await you.", - "subtitle": "Connect positions, talents, and job actions.", - "button": "Sign up" + "empty": { + "title": "No companies found", + "subtitle": "Try adjusting your search filters" } }, - "footer": { - "vagas": "Jobs", - "empresas": "Companies", - "blog": "Blog", - "login": "Login", - "copyright": "Ghorse jobs. All rights reserved." + "footerMain": { + "info": { + "title": "Information", + "about": "About Us", + "contact": "Contact" + }, + "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." } } \ No newline at end of file diff --git a/frontend/src/i18n/es.json b/frontend/src/i18n/es.json index 79ebfe3..8962608 100644 --- a/frontend/src/i18n/es.json +++ b/frontend/src/i18n/es.json @@ -211,41 +211,47 @@ }, "home": { "hero": { - "title": "Encuentra el empleo correcto, de forma sencilla", - "subtitle": "Conectamos candidatos y empresas de forma rápida y directa", - "searchJobs": "Buscar empleos", - "imCompany": "Soy empresa", - "postJob": "Publicar vacante" + "title": "Encuentra el Trabajo de TI", + "titleLine2": "de Tus Sueños.", + "subtitle": "Te conectamos con las mejores empresas y tecnologías.", + "cta": "Buscar Empleos" }, - "featured": { - "title": "Empleos Destacados", - "subtitle": "Oportunidades seleccionadas para ti", - "viewAll": "Ver todos los empleos" + "search": { + "placeholder": "Ingresa palabras clave", + "filter": "Filtrar 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": { - "title": "¿Cómo funciona?", - "subtitle": "Tres pasos sencillos para tu próxima oportunidad", - "step1": { - "title": "1. Regístrate", - "description": "Crea tu perfil gratis en pocos minutos" - }, - "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" - } + "featuredJobs": { + "title": "Últimos Empleos Registrados", + "yesterday": "Ayer", + "apply": "Aplicar ahora", + "viewJob": "Ver Empleo", + "favorite": "Favorito" }, - "testimonials": { - "title": "¿Qué dicen nuestros usuarios?", - "subtitle": "Historias de éxito de quienes encontraron su oportunidad" + "levels": { + "junior": "Junior", + "mid": "Semi-senior", + "senior": "Senior" + }, + "moreJobs": { + "title": "Más Empleos", + "viewAll": "Ver Todos los Empleos" }, "cta": { - "title": "¿Listo para empezar?", - "subtitle": "¡Crea tu perfil gratis y comienza a recibir ofertas de empleo hoy!", - "button": "Crear perfil gratis" + "badge": "Redes Sociales", + "title": "Miles de oportunidades te esperan.", + "subtitle": "Conecta posiciones, talentos, acciones de empleos.", + "button": "Regístrate" } }, "jobs": { @@ -320,14 +326,11 @@ "remote": "Remoto" }, "footer": { - "company": "Empresa", - "about": "Sobre", - "careers": "Carreras", - "jobsByTech": "Empleos por Tecnología", - "legal": "Legal", - "privacy": "Política de Privacidad", - "terms": "Términos de Uso", - "copyright": "© {year} GoHorse Jobs. Todos los derechos reservados." + "vagas": "Empleos", + "empresas": "Empresas", + "blog": "Blog", + "login": "Iniciar sesión", + "copyright": "Ghorse jobs. Todos los derechos reservados." }, "auth": { "login": { @@ -1296,56 +1299,52 @@ "subjectRequired": "El asunto es requerido" } }, - "home": { + "companiesPage": { "hero": { - "title": "Encuentra el Trabajo de TI", - "titleLine2": "de Tus Sueños.", - "subtitle": "Te conectamos con las mejores empresas y tecnologías.", - "cta": "Buscar Empleos" + "title": "Descubre las Mejores Empresas", + "subtitle": "Conoce empresas increíbles que están contratando ahora", + "searchPlaceholder": "Buscar empresas por nombre o sector..." }, - "search": { - "placeholder": "Ingresa palabras clave", - "filter": "Filtrar 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" + "filters": { + "label": "Filtros:", + "allIndustries": "Todas", + "allSizes": "Todos", + "companiesFound": "empresas encontradas" }, - "featuredJobs": { - "title": "Últimos Empleos Registrados", - "yesterday": "Ayer", - "apply": "Aplicar ahora", - "viewJob": "Ver Empleo", - "favorite": "Favorito" + "featured": { + "title": "Empresas Destacadas", + "openJobs": "vacantes abiertas", + "viewProfile": "Ver perfil" }, - "levels": { - "junior": "Junior", - "mid": "Semi-senior", - "senior": "Senior" + "all": { + "title": "Todas las Empresas", + "featured": "Destacada", + "positions": "vacantes", + "viewMore": "Ver más" }, - "moreJobs": { - "title": "Más Empleos", - "viewAll": "Ver Todos los Empleos" - }, - "cta": { - "badge": "Redes Sociales", - "title": "Miles de oportunidades te esperan.", - "subtitle": "Conecta posiciones, talentos, acciones de empleos.", - "button": "Regístrate" + "empty": { + "title": "No se encontraron empresas", + "subtitle": "Intenta ajustar tus filtros de búsqueda" } }, - "footer": { - "vagas": "Empleos", - "empresas": "Empresas", - "blog": "Blog", - "login": "Iniciar sesión", - "copyright": "Ghorse jobs. Todos los derechos reservados." + "footerMain": { + "info": { + "title": "Información", + "about": "Sobre nosotros", + "contact": "Contacto" + }, + "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." } } \ No newline at end of file diff --git a/frontend/src/i18n/pt-BR.json b/frontend/src/i18n/pt-BR.json index 2b76cd3..8427e1c 100644 --- a/frontend/src/i18n/pt-BR.json +++ b/frontend/src/i18n/pt-BR.json @@ -252,39 +252,46 @@ "home": { "hero": { "title": "Encontre a Vaga de TI", - "titleLine2": "dos Seus Sonhos", + "titleLine2": "dos Seus Sonhos.", "subtitle": "Conectamos você com as melhores empresas e techs.", "cta": "Buscar Vagas" }, - "featured": { - "title": "Vagas em Destaque", - "subtitle": "Oportunidades selecionadas para você", - "viewAll": "Ver todas as vagas" + "search": { + "placeholder": "Digite as palavras-chave", + "filter": "Filtrar 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": { - "title": "Como Funciona?", - "subtitle": "Três passos simples para sua próxima oportunidade", - "step1": { - "title": "1. Cadastre-se", - "description": "Crie seu perfil gratuitamente em poucos minutos" - }, - "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" - } + "featuredJobs": { + "title": "Últimas Vagas Cadastradas", + "yesterday": "Ontem", + "apply": "Aplicar agora", + "viewJob": "Ver Vaga", + "favorite": "Favoritar" }, - "testimonials": { - "title": "O que nossos usuários dizem?", - "subtitle": "Histórias de sucesso de quem encontrou sua oportunidade" + "levels": { + "junior": "Júnior", + "mid": "Pleno", + "senior": "Sênior" + }, + "moreJobs": { + "title": "Mais Vagas", + "viewAll": "Ver Todas Vagas" }, "cta": { - "title": "Pronto para começar?", - "subtitle": "Crie seu perfil gratuito e comece a receber ofertas de emprego hoje mesmo!", - "button": "Criar perfil gratuito" + "badge": "Redes Sociais", + "title": "Milhares de oportunidades esperam você.", + "subtitle": "Conecte cargos, talentos, tomada de ações de vagas.", + "button": "Cadastre-se" } }, "jobs": { @@ -386,14 +393,11 @@ "remote": "Remoto" }, "footer": { - "company": "Empresa", - "about": "Sobre", - "careers": "Carreiras", - "jobsByTech": "Vagas por Tecnologia", - "legal": "Legal", - "privacy": "Política de Privacidade", - "terms": "Termos de Uso", - "copyright": "© {year} GoHorse Jobs. Todos os direitos reservados." + "vagas": "Vagas", + "empresas": "Empresas", + "blog": "Blog", + "login": "Login", + "copyright": "Ghorse jobs. Todos os direitos reservados." }, "auth": { "login": { @@ -1329,56 +1333,52 @@ "subjectRequired": "O assunto é obrigatório" } }, - "home": { + "companiesPage": { "hero": { - "title": "Encontre a Vaga de TI", - "titleLine2": "dos Seus Sonhos.", - "subtitle": "Conectamos você com as melhores empresas e techs.", - "cta": "Buscar Vagas" + "title": "Descubra as Melhores Empresas", + "subtitle": "Conheça empresas incríveis que estão contratando agora", + "searchPlaceholder": "Buscar empresas por nome ou setor..." }, - "search": { - "placeholder": "Digite as palavras-chave", - "filter": "Filtrar 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" + "filters": { + "label": "Filtros:", + "allIndustries": "Todas", + "allSizes": "Todos", + "companiesFound": "empresas encontradas" }, - "featuredJobs": { - "title": "Últimas Vagas Cadastradas", - "yesterday": "Ontem", - "apply": "Aplicar agora", - "viewJob": "Ver Vaga", - "favorite": "Favoritar" + "featured": { + "title": "Empresas em Destaque", + "openJobs": "vagas abertas", + "viewProfile": "Ver perfil" }, - "levels": { - "junior": "Júnior", - "mid": "Pleno", - "senior": "Sênior" + "all": { + "title": "Todas as Empresas", + "featured": "Destaque", + "positions": "vagas", + "viewMore": "Ver mais" }, - "moreJobs": { - "title": "Mais Vagas", - "viewAll": "Ver Todas Vagas" - }, - "cta": { - "badge": "Redes Sociais", - "title": "Milhares de oportunidades esperam você.", - "subtitle": "Conecte cargos, talentos, tomada de ações de vagas.", - "button": "Cadastre-se" + "empty": { + "title": "Nenhuma empresa encontrada", + "subtitle": "Tente ajustar seus filtros de busca" } }, - "footer": { - "vagas": "Vagas", - "empresas": "Empresas", - "blog": "Blog", - "login": "Login", - "copyright": "Ghorse jobs. Todos os direitos reservados." + "footerMain": { + "info": { + "title": "Informações", + "about": "Sobre nós", + "contact": "Contato" + }, + "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." } } \ No newline at end of file diff --git a/frontend/src/lib/i18n.tsx b/frontend/src/lib/i18n.tsx index 2ce191f..726262a 100644 --- a/frontend/src/lib/i18n.tsx +++ b/frontend/src/lib/i18n.tsx @@ -1,116 +1,117 @@ -'use client'; - -import React, { createContext, useContext, useState, useCallback, ReactNode, useEffect } from 'react'; -import en from '@/i18n/en.json'; -import es from '@/i18n/es.json'; -import ptBR from '@/i18n/pt-BR.json'; - -type Locale = 'en' | 'es' | 'pt-BR'; - -interface I18nContextType { - locale: Locale; - setLocale: (locale: Locale) => void; - t: (key: string, params?: Record) => string; -} - -const dictionaries: Record = { - en, - es, - 'pt-BR': ptBR, -}; - -const I18nContext = createContext(null); - -const localeStorageKey = 'locale'; - -const normalizeLocale = (language: string): Locale => { - if (language.startsWith('pt')) return 'pt-BR'; - if (language.startsWith('es')) return 'es'; - return 'en'; -}; - -const getInitialLocale = (): Locale => { - if (typeof window === 'undefined') return 'en'; - const storedLocale = localStorage.getItem(localeStorageKey); - if (storedLocale && storedLocale in dictionaries) { - return storedLocale as Locale; - } - // Default to English instead of browser language for consistency - return 'en'; -}; - -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('en'); - - const setLocale = (newLocale: Locale) => { - console.log(`[I18n] Setting locale to: ${newLocale}`); - setLocaleState(newLocale); - }; - - const t = useCallback((key: string, params?: Record) => { - const keys = key.split('.'); - let value: unknown = dictionaries[locale]; - - for (const k of keys) { - if (value && typeof value === 'object' && value !== null && k in value) { - value = (value as Record)[k]; - } else { - return key; // Return key if not found - } - } - - if (typeof value !== 'string') return key; - - // Replace parameters like {count}, {time}, {year} - if (params) { - return Object.entries(params).reduce( - (str, [paramKey, paramValue]) => str.replace(`{${paramKey}}`, String(paramValue)), - value - ); - } - - return value; - }, [locale]); - - // Sync from localStorage on mount (Client-side only) - useEffect(() => { - const stored = getInitialLocale(); - if (stored !== 'en') { - console.log('[I18n] Restoring locale from storage on client:', stored); - setLocaleState(stored); - } - }, []); - - useEffect(() => { - if (typeof window === 'undefined') return; - localStorage.setItem(localeStorageKey, locale); - document.documentElement.lang = locale; - }, [locale]); - - return ( - - {children} - - ); -} - -export function useI18n() { - const context = useContext(I18nContext); - if (!context) { - throw new Error('useI18n must be used within an I18nProvider'); - } - return context; -} - -export function useTranslation() { - const { t, locale, setLocale } = useI18n(); - return { t, locale, setLocale }; -} - -export const locales: { code: Locale; name: string; flag: string }[] = [ - { code: 'en', name: 'English', flag: '🇺🇸' }, - { code: 'es', name: 'Español', flag: '🇪🇸' }, - { code: 'pt-BR', name: 'Português', flag: '🇧🇷' }, -]; +'use client'; + +import React, { createContext, useContext, useState, useCallback, ReactNode, useEffect, startTransition } from 'react'; +import en from '@/i18n/en.json'; +import es from '@/i18n/es.json'; +import ptBR from '@/i18n/pt-BR.json'; + +type Locale = 'en' | 'es' | 'pt-BR'; + +interface I18nContextType { + locale: Locale; + setLocale: (locale: Locale) => void; + t: (key: string, params?: Record) => string; +} + +const dictionaries: Record = { + en, + es, + 'pt-BR': ptBR, +}; + +const I18nContext = createContext(null); + +const localeStorageKey = 'locale'; + +const normalizeLocale = (language: string): Locale => { + if (language.startsWith('pt')) return 'pt-BR'; + if (language.startsWith('es')) return 'es'; + return 'en'; +}; + +const getInitialLocale = (): Locale => { + if (typeof window === 'undefined') return 'en'; + const storedLocale = localStorage.getItem(localeStorageKey); + if (storedLocale && Object.keys(dictionaries).includes(storedLocale)) { + return storedLocale as Locale; + } + if (typeof navigator !== 'undefined' && navigator.language) { + return normalizeLocale(navigator.language); + } + return 'en'; +}; + +export function I18nProvider({ children }: { children: ReactNode }) { + const [locale, setLocaleState] = useState('en'); + + const setLocale = (newLocale: Locale) => { + setLocaleState(newLocale); + }; + + const t = useCallback((key: string, params?: Record) => { + const keys = key.split('.'); + let value: unknown = dictionaries[locale]; + + for (const k of keys) { + if (value && typeof value === 'object' && value !== null && k in value) { + value = (value as Record)[k]; + } else { + return key; + } + } + + if (typeof value !== 'string') return key; + + if (params) { + return Object.entries(params).reduce( + (str, [paramKey, paramValue]) => str.replace(`{${paramKey}}`, String(paramValue)), + value + ); + } + + return value; + }, [locale]); + + // 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(() => { + const stored = getInitialLocale(); + if (stored !== 'en') { + startTransition(() => { + setLocaleState(stored); + }); + } + }, []); + + useEffect(() => { + if (typeof window === 'undefined') return; + localStorage.setItem(localeStorageKey, locale); + document.documentElement.lang = locale; + }, [locale]); + + return ( + + {children} + + ); +} + +export function useI18n() { + const context = useContext(I18nContext); + if (!context) { + throw new Error('useI18n must be used within an I18nProvider'); + } + return context; +} + +export function useTranslation() { + const { t, locale, setLocale } = useI18n(); + return { t, locale, setLocale }; +} + +export const locales: { code: Locale; name: string; flag: string }[] = [ + { code: 'en', name: 'English', flag: '🇺🇸' }, + { code: 'es', name: 'Español', flag: '🇪🇸' }, + { code: 'pt-BR', name: 'Português', flag: '🇧🇷' }, +];