Update infrastructure and frontend configuration
This commit is contained in:
parent
2649e48a8b
commit
1569deb1ce
13 changed files with 764 additions and 772 deletions
|
|
@ -1,19 +1,16 @@
|
||||||
services:
|
services:
|
||||||
frontend:
|
frontend:
|
||||||
image: node:20-alpine
|
image: node:20-alpine
|
||||||
working_dir: /app
|
working_dir: /app
|
||||||
volumes:
|
volumes:
|
||||||
- ./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
|
||||||
- HOST=0.0.0.0
|
- API_URL=https://api-local.gohorsejobs.com
|
||||||
- HOSTNAME=0.0.0.0
|
- BACKOFFICE_URL=https://b-local.gohorsejobs.com
|
||||||
labels:
|
- SEEDER_API_URL=https://s-local.gohorsejobs.com
|
||||||
- "traefik.enable=true"
|
- HOST=0.0.0.0
|
||||||
- "traefik.http.routers.gohorse-frontend.rule=Host(`dev.gohorsejobs.com`)"
|
restart: always
|
||||||
- "traefik.http.services.gohorse-frontend.loadbalancer.server.port=3000"
|
|
||||||
- "traefik.http.routers.gohorse-frontend.entrypoints=web"
|
|
||||||
restart: always
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
poweredByHeader: false, // Remove X-Powered-By header (security)
|
compress: true,
|
||||||
compress: true, // Enable gzip compression
|
|
||||||
|
// Allow dev server behind proxy
|
||||||
// Optional: Configure allowed image domains
|
allowedDevOrigins: ["https://dev.gohorsejobs.com"],
|
||||||
images: {
|
|
||||||
remotePatterns: [
|
// Optional: Configure allowed image domains
|
||||||
{
|
images: {
|
||||||
protocol: "https",
|
remotePatterns: [
|
||||||
hostname: "**",
|
{
|
||||||
},
|
protocol: "https",
|
||||||
],
|
hostname: "**",
|
||||||
qualities: [25, 50, 75, 80, 90, 100],
|
},
|
||||||
},
|
],
|
||||||
};
|
qualities: [25, 50, 75, 80, 90, 100],
|
||||||
|
},
|
||||||
export default nextConfig;
|
};
|
||||||
|
|
||||||
|
export default nextConfig;
|
||||||
|
|
|
||||||
|
|
@ -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")
|
||||||
|
|
@ -158,7 +160,7 @@ export default function CompaniesPage() {
|
||||||
company.description.toLowerCase().includes(searchTerm.toLowerCase())
|
company.description.toLowerCase().includes(searchTerm.toLowerCase())
|
||||||
const matchesIndustry = selectedIndustry === "Todas" || company.industry === selectedIndustry
|
const matchesIndustry = selectedIndustry === "Todas" || company.industry === selectedIndustry
|
||||||
const matchesSize = selectedSize === "Todos" || company.employees === selectedSize
|
const matchesSize = selectedSize === "Todos" || company.employees === selectedSize
|
||||||
|
|
||||||
return matchesSearch && matchesIndustry && matchesSize
|
return matchesSearch && matchesIndustry && matchesSize
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
@ -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,33 +183,30 @@ 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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<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,9 +219,9 @@ 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
|
||||||
value={selectedIndustry}
|
value={selectedIndustry}
|
||||||
onChange={(e) => setSelectedIndustry(e.target.value)}
|
onChange={(e) => setSelectedIndustry(e.target.value)}
|
||||||
|
|
@ -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")} →
|
||||||
</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,13 +338,13 @@ 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>
|
||||||
|
|
||||||
<h3 className="text-xl font-bold text-gray-900 mb-2">{company.name}</h3>
|
<h3 className="text-xl font-bold text-gray-900 mb-2">{company.name}</h3>
|
||||||
|
|
||||||
<div className="flex items-center gap-2 mb-3">
|
<div className="flex items-center gap-2 mb-3">
|
||||||
<span className="bg-[#F0932B]/10 text-[#F0932B] px-3 py-1 rounded-full text-xs font-semibold">
|
<span className="bg-[#F0932B]/10 text-[#F0932B] px-3 py-1 rounded-full text-xs font-semibold">
|
||||||
{company.industry}
|
{company.industry}
|
||||||
|
|
@ -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")} →
|
||||||
</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>
|
||||||
|
|
|
||||||
|
|
@ -1,219 +1,211 @@
|
||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import { useState, useCallback, useEffect } from "react"
|
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"
|
||||||
import { Navbar } from "@/components/navbar"
|
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
|
})
|
||||||
})
|
|
||||||
|
const [prevBtnDisabled, setPrevBtnDisabled] = useState(true)
|
||||||
const [prevBtnDisabled, setPrevBtnDisabled] = useState(true)
|
const [nextBtnDisabled, setNextBtnDisabled] = useState(true)
|
||||||
const [nextBtnDisabled, setNextBtnDisabled] = useState(true)
|
|
||||||
|
const scrollPrev = useCallback(() => {
|
||||||
const scrollPrev = useCallback(() => {
|
if (emblaApi) emblaApi.scrollPrev()
|
||||||
if (emblaApi) emblaApi.scrollPrev()
|
}, [emblaApi])
|
||||||
}, [emblaApi])
|
|
||||||
|
const scrollNext = useCallback(() => {
|
||||||
const scrollNext = useCallback(() => {
|
if (emblaApi) emblaApi.scrollNext()
|
||||||
if (emblaApi) emblaApi.scrollNext()
|
}, [emblaApi])
|
||||||
}, [emblaApi])
|
|
||||||
|
const onSelect = useCallback((emblaApi: any) => {
|
||||||
const onSelect = useCallback((emblaApi: any) => {
|
setPrevBtnDisabled(!emblaApi.canScrollPrev())
|
||||||
setPrevBtnDisabled(!emblaApi.canScrollPrev())
|
setNextBtnDisabled(!emblaApi.canScrollNext())
|
||||||
setNextBtnDisabled(!emblaApi.canScrollNext())
|
}, [])
|
||||||
}, [])
|
|
||||||
|
useEffect(() => {
|
||||||
useEffect(() => {
|
if (!emblaApi) return
|
||||||
if (!emblaApi) return
|
onSelect(emblaApi)
|
||||||
|
emblaApi.on("reInit", onSelect)
|
||||||
onSelect(emblaApi)
|
emblaApi.on("select", onSelect)
|
||||||
emblaApi.on('reInit', onSelect)
|
}, [emblaApi, onSelect])
|
||||||
emblaApi.on('select', onSelect)
|
|
||||||
}, [emblaApi, onSelect])
|
return (
|
||||||
|
<div className="min-h-screen bg-gray-50 flex flex-col font-sans">
|
||||||
return (
|
<Navbar />
|
||||||
<div className="min-h-screen bg-gray-50 flex flex-col font-sans">
|
|
||||||
<Navbar />
|
<main className="flex-grow">
|
||||||
|
{/* Hero Section */}
|
||||||
<main className="flex-grow">
|
<section className="relative h-[500px] flex items-center justify-center bg-[#1F2F40]">
|
||||||
{/* Hero Section */}
|
<div className="absolute inset-0 z-0">
|
||||||
<section className="relative h-[500px] flex items-center justify-center bg-[#1F2F40]">
|
<Image
|
||||||
{/* Background Image with Overlay */}
|
src="/10.png"
|
||||||
<div className="absolute inset-0 z-0">
|
alt="Background"
|
||||||
<Image
|
fill
|
||||||
src="/10.png"
|
className="object-cover opacity-60 contrast-125"
|
||||||
alt="Background"
|
priority
|
||||||
fill
|
/>
|
||||||
className="object-cover opacity-60 contrast-125"
|
<div className="absolute inset-0 bg-gradient-to-r from-[#1F2F40] via-[#1F2F40]/90 to-transparent" />
|
||||||
priority
|
</div>
|
||||||
/>
|
|
||||||
<div className="absolute inset-0 bg-gradient-to-r from-[#1F2F40] via-[#1F2F40]/90 to-transparent" />
|
<div className="container mx-auto px-4 relative z-10 text-center sm:text-left">
|
||||||
</div>
|
<div className="max-w-3xl">
|
||||||
|
<motion.h1
|
||||||
<div className="container mx-auto px-4 relative z-10 text-center sm:text-left">
|
initial={{ opacity: 0, y: 20 }}
|
||||||
<div className="max-w-3xl">
|
animate={{ opacity: 1, y: 0 }}
|
||||||
<motion.h1
|
transition={{ duration: 0.5 }}
|
||||||
initial={{ opacity: 0, y: 20 }}
|
className="text-4xl md:text-5xl lg:text-6xl font-bold text-white mb-6 leading-tight tracking-tight"
|
||||||
animate={{ opacity: 1, y: 0 }}
|
>
|
||||||
transition={{ duration: 0.5 }}
|
{t("home.hero.title")} <br className="hidden sm:block" />
|
||||||
className="text-4xl md:text-5xl lg:text-6xl font-bold text-white mb-6 leading-tight tracking-tight"
|
{t("home.hero.titleLine2")}
|
||||||
>
|
</motion.h1>
|
||||||
Encontre a Vaga de TI <br className="hidden sm:block" />
|
|
||||||
dos Seus Sonhos.
|
<motion.p
|
||||||
</motion.h1>
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
<motion.p
|
transition={{ duration: 0.5, delay: 0.1 }}
|
||||||
initial={{ opacity: 0, y: 20 }}
|
className="text-lg md:text-xl text-gray-300 mb-8 max-w-xl leading-relaxed"
|
||||||
animate={{ opacity: 1, y: 0 }}
|
>
|
||||||
transition={{ duration: 0.5, delay: 0.1 }}
|
{t("home.hero.subtitle")}
|
||||||
className="text-lg md:text-xl text-gray-300 mb-8 max-w-xl leading-relaxed"
|
</motion.p>
|
||||||
>
|
|
||||||
Conectamos você com as melhores empresas e techs.
|
<motion.div
|
||||||
</motion.p>
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
<motion.div
|
transition={{ duration: 0.5, delay: 0.2 }}
|
||||||
initial={{ opacity: 0, y: 20 }}
|
>
|
||||||
animate={{ opacity: 1, y: 0 }}
|
<Link href="/jobs">
|
||||||
transition={{ duration: 0.5, delay: 0.2 }}
|
<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">
|
||||||
>
|
{t("home.hero.cta")}
|
||||||
<Link href="/jobs">
|
</Button>
|
||||||
<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">
|
</Link>
|
||||||
Buscar Vagas
|
</motion.div>
|
||||||
</Button>
|
</div>
|
||||||
</Link>
|
</div>
|
||||||
</motion.div>
|
</section>
|
||||||
</div>
|
|
||||||
</div>
|
{/* Search Section */}
|
||||||
</section>
|
<section className="px-4 mb-16">
|
||||||
|
<div className="container mx-auto">
|
||||||
{/* Search Section */}
|
<HomeSearch />
|
||||||
<section className="px-4 mb-16">
|
</div>
|
||||||
<div className="container mx-auto">
|
</section>
|
||||||
<HomeSearch />
|
|
||||||
</div>
|
{/* Latest Jobs Section */}
|
||||||
</section>
|
<section className="py-12 bg-gray-50">
|
||||||
|
<div className="container mx-auto px-4">
|
||||||
{/* Latest Jobs Section - WITH CAROUSEL */}
|
<div className="flex items-center justify-between mb-8">
|
||||||
<section className="py-12 bg-gray-50">
|
<h2 className="text-2xl md:text-3xl font-bold text-gray-900">
|
||||||
<div className="container mx-auto px-4">
|
{t("home.featuredJobs.title")}
|
||||||
<div className="flex items-center justify-between mb-8">
|
</h2>
|
||||||
<h2 className="text-2xl md:text-3xl font-bold text-gray-900">
|
<div className="flex items-center gap-2">
|
||||||
Últimas Vagas Cadastradas
|
<Button
|
||||||
</h2>
|
variant="outline"
|
||||||
<div className="flex items-center gap-2">
|
size="icon"
|
||||||
<Button
|
className="rounded-full border-gray-300 hover:border-orange-500 hover:text-orange-500 transition-all w-10 h-10"
|
||||||
variant="outline"
|
onClick={scrollPrev}
|
||||||
size="icon"
|
disabled={prevBtnDisabled}
|
||||||
className="rounded-full border-gray-300 hover:border-orange-500 hover:text-orange-500 transition-all w-10 h-10"
|
>
|
||||||
onClick={scrollPrev}
|
<ChevronLeft className="w-5 h-5" />
|
||||||
disabled={prevBtnDisabled}
|
</Button>
|
||||||
>
|
<Button
|
||||||
<ChevronLeft className="w-5 h-5" />
|
variant="outline"
|
||||||
</Button>
|
size="icon"
|
||||||
<Button
|
className="rounded-full border-gray-300 hover:border-orange-500 hover:text-orange-500 transition-all w-10 h-10"
|
||||||
variant="outline"
|
onClick={scrollNext}
|
||||||
size="icon"
|
disabled={nextBtnDisabled}
|
||||||
className="rounded-full border-gray-300 hover:border-orange-500 hover:text-orange-500 transition-all w-10 h-10"
|
>
|
||||||
onClick={scrollNext}
|
<ChevronRight className="w-5 h-5" />
|
||||||
disabled={nextBtnDisabled}
|
</Button>
|
||||||
>
|
</div>
|
||||||
<ChevronRight className="w-5 h-5" />
|
</div>
|
||||||
</Button>
|
|
||||||
</div>
|
<div className="overflow-hidden" ref={emblaRef}>
|
||||||
</div>
|
<div className="flex gap-6">
|
||||||
|
{mockJobs.slice(0, 8).map((job, index) => (
|
||||||
<div className="overflow-hidden" ref={emblaRef}>
|
<div key={`latest-${job.id}-${index}`} className="flex-[0_0_100%] sm:flex-[0_0_50%] lg:flex-[0_0_25%] min-w-0">
|
||||||
<div className="flex gap-6">
|
<JobCard job={job} />
|
||||||
{mockJobs.slice(0, 8).map((job, index) => (
|
</div>
|
||||||
<div key={`latest-${job.id}-${index}`} className="flex-[0_0_100%] sm:flex-[0_0_50%] lg:flex-[0_0_25%] min-w-0">
|
))}
|
||||||
<JobCard job={job} />
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
</div>
|
||||||
</div>
|
</section>
|
||||||
</div>
|
|
||||||
</div>
|
{/* More Jobs Section */}
|
||||||
</section>
|
<section className="py-12 bg-white">
|
||||||
|
<div className="container mx-auto px-4">
|
||||||
{/* More Jobs Section */}
|
<div className="flex items-center justify-between mb-8">
|
||||||
<section className="py-12 bg-white">
|
<h2 className="text-2xl md:text-3xl font-bold text-gray-900">
|
||||||
<div className="container mx-auto px-4">
|
{t("home.moreJobs.title")}
|
||||||
<div className="flex items-center justify-between mb-8">
|
</h2>
|
||||||
<h2 className="text-2xl md:text-3xl font-bold text-gray-900">
|
<Link href="/jobs">
|
||||||
Mais Vagas
|
<Button className="bg-orange-500 hover:bg-orange-600 text-white font-bold">
|
||||||
</h2>
|
{t("home.moreJobs.viewAll")}
|
||||||
<Link href="/jobs">
|
</Button>
|
||||||
<Button className="bg-orange-500 hover:bg-orange-600 text-white font-bold">
|
</Link>
|
||||||
Ver Todas Vagas
|
</div>
|
||||||
</Button>
|
|
||||||
</Link>
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
|
||||||
</div>
|
{mockJobs.slice(0, 8).map((job, index) => (
|
||||||
|
<JobCard key={`more-${job.id}-${index}`} job={job} />
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
|
))}
|
||||||
{mockJobs.slice(0, 8).map((job, index) => (
|
</div>
|
||||||
<JobCard key={`more-${job.id}-${index}`} job={job} />
|
</div>
|
||||||
))}
|
</section>
|
||||||
</div>
|
|
||||||
</div>
|
{/* Bottom CTA Section */}
|
||||||
</section>
|
<section className="py-16 bg-white">
|
||||||
|
<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]">
|
||||||
{/* Bottom CTA Section */}
|
<div className="relative z-10 max-w-xl">
|
||||||
<section className="py-16 bg-white">
|
<h2 className="text-3xl md:text-4xl font-bold text-white mb-4 leading-tight">
|
||||||
<div className="container mx-auto px-4">
|
{t("home.cta.title")}
|
||||||
<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]">
|
</h2>
|
||||||
|
<p className="text-base text-gray-300 mb-8">
|
||||||
{/* Content */}
|
{t("home.cta.subtitle")}
|
||||||
<div className="relative z-10 max-w-xl">
|
</p>
|
||||||
<h2 className="text-3xl md:text-4xl font-bold text-white mb-4 leading-tight">
|
<Link href="/register/user">
|
||||||
Milhares de oportunidades <br /> esperam você.
|
<Button className="h-12 px-8 bg-white text-gray-900 hover:bg-gray-100 font-bold text-lg rounded-md">
|
||||||
</h2>
|
{t("home.cta.button")}
|
||||||
<p className="text-base text-gray-300 mb-8">
|
</Button>
|
||||||
Conecte cargos, talentos, tomada de ações de vagas.
|
</Link>
|
||||||
</p>
|
</div>
|
||||||
<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">
|
<div className="absolute inset-0 z-0">
|
||||||
Cadastre-se
|
<div className="absolute right-0 top-0 h-full w-full md:w-2/3 lg:w-1/2">
|
||||||
</Button>
|
<Image
|
||||||
</Link>
|
src="/muie.jpeg"
|
||||||
</div>
|
alt="Professional"
|
||||||
|
fill
|
||||||
{/* Image Background for CTA */}
|
className="object-cover object-center md:object-right opacity-40 md:opacity-100"
|
||||||
<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 inset-0 bg-gradient-to-t md:bg-gradient-to-r from-[#1F2F40] via-[#1F2F40]/30 to-transparent" />
|
||||||
<Image
|
</div>
|
||||||
src="/muie.jpeg"
|
</div>
|
||||||
alt="Professional"
|
</div>
|
||||||
fill
|
</div>
|
||||||
className="object-cover object-center md:object-right opacity-40 md:opacity-100"
|
</section>
|
||||||
/>
|
</main>
|
||||||
{/* 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" />
|
<Footer />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
)
|
||||||
</div>
|
}
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
</main>
|
|
||||||
|
|
||||||
<Footer />
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -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">© {currentYear} {t("footerMain.copyright")}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
|
|
|
||||||
|
|
@ -1,117 +1,117 @@
|
||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import { useState } from "react"
|
import { useState } from "react"
|
||||||
import { Search, MapPin, DollarSign, Briefcase } from "lucide-react"
|
import { Search, MapPin, DollarSign, Briefcase } from "lucide-react"
|
||||||
import { Button } from "@/components/ui/button"
|
import { Button } from "@/components/ui/button"
|
||||||
import { Input } from "@/components/ui/input"
|
import { Input } from "@/components/ui/input"
|
||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
|
||||||
import { useRouter } from "next/navigation"
|
import { useRouter } from "next/navigation"
|
||||||
import { useTranslation } from "@/lib/i18n"
|
import { useTranslation } from "@/lib/i18n"
|
||||||
|
|
||||||
export function HomeSearch() {
|
export function HomeSearch() {
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const [searchTerm, setSearchTerm] = useState("")
|
const [searchTerm, setSearchTerm] = useState("")
|
||||||
const [location, setLocation] = useState("")
|
const [location, setLocation] = useState("")
|
||||||
const [type, setType] = useState("")
|
const [type, setType] = useState("")
|
||||||
const [workMode, setWorkMode] = useState("")
|
const [workMode, setWorkMode] = useState("")
|
||||||
const [salary, setSalary] = useState("")
|
const [salary, setSalary] = useState("")
|
||||||
|
|
||||||
const handleSearch = () => {
|
const handleSearch = () => {
|
||||||
const params = new URLSearchParams()
|
const params = new URLSearchParams()
|
||||||
if (searchTerm) params.set("q", searchTerm)
|
if (searchTerm) params.set("q", searchTerm)
|
||||||
if (location) params.set("location", location)
|
if (location) params.set("location", location)
|
||||||
if (type && type !== "all") params.set("type", type)
|
if (type && type !== "all") params.set("type", type)
|
||||||
if (workMode && workMode !== "all") params.set("mode", workMode)
|
if (workMode && workMode !== "all") params.set("mode", workMode)
|
||||||
if (salary) params.set("salary", salary)
|
if (salary) params.set("salary", salary)
|
||||||
|
|
||||||
router.push(`/jobs?${params.toString()}`)
|
router.push(`/jobs?${params.toString()}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="bg-white rounded-lg shadow-lg p-6 max-w-5xl mx-auto -mt-24 relative z-20 border border-gray-100">
|
<div className="bg-white rounded-lg shadow-lg p-6 max-w-5xl mx-auto -mt-24 relative z-20 border border-gray-100">
|
||||||
<div className="mb-4">
|
<div className="mb-4">
|
||||||
<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)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<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)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 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"
|
||||||
className="h-12 bg-gray-50 border-gray-200"
|
className="h-12 bg-gray-50 border-gray-200"
|
||||||
value={salary}
|
value={salary}
|
||||||
onChange={(e) => setSalary(e.target.value)}
|
onChange={(e) => setSalary(e.target.value)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Search Button */}
|
{/* Search Button */}
|
||||||
<div className="flex items-end">
|
<div className="flex items-end">
|
||||||
<Button
|
<Button
|
||||||
onClick={handleSearch}
|
onClick={handleSearch}
|
||||||
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>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
|
|
||||||
|
|
@ -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" },
|
||||||
|
|
|
||||||
|
|
@ -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."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -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."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -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."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1,116 +1,117 @@
|
||||||
'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';
|
||||||
|
|
||||||
type Locale = 'en' | 'es' | 'pt-BR';
|
type Locale = 'en' | 'es' | 'pt-BR';
|
||||||
|
|
||||||
interface I18nContextType {
|
interface I18nContextType {
|
||||||
locale: Locale;
|
locale: Locale;
|
||||||
setLocale: (locale: Locale) => void;
|
setLocale: (locale: Locale) => void;
|
||||||
t: (key: string, params?: Record<string, string | number>) => string;
|
t: (key: string, params?: Record<string, string | number>) => string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const dictionaries: Record<Locale, typeof en> = {
|
const dictionaries: Record<Locale, typeof en> = {
|
||||||
en,
|
en,
|
||||||
es,
|
es,
|
||||||
'pt-BR': ptBR,
|
'pt-BR': ptBR,
|
||||||
};
|
};
|
||||||
|
|
||||||
const I18nContext = createContext<I18nContextType | null>(null);
|
const I18nContext = createContext<I18nContextType | null>(null);
|
||||||
|
|
||||||
const localeStorageKey = 'locale';
|
const localeStorageKey = 'locale';
|
||||||
|
|
||||||
const normalizeLocale = (language: string): Locale => {
|
const normalizeLocale = (language: string): Locale => {
|
||||||
if (language.startsWith('pt')) return 'pt-BR';
|
if (language.startsWith('pt')) return 'pt-BR';
|
||||||
if (language.startsWith('es')) return 'es';
|
if (language.startsWith('es')) return 'es';
|
||||||
return 'en';
|
return 'en';
|
||||||
};
|
};
|
||||||
|
|
||||||
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 'en';
|
return normalizeLocale(navigator.language);
|
||||||
};
|
}
|
||||||
|
return 'en';
|
||||||
export function I18nProvider({ children }: { children: ReactNode }) {
|
};
|
||||||
// FIX: Initialize with 'en' to match Server-Side Rendering (SSR)
|
|
||||||
// This prevents hydration mismatch errors.
|
export function I18nProvider({ children }: { children: ReactNode }) {
|
||||||
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);
|
};
|
||||||
};
|
|
||||||
|
const t = useCallback((key: string, params?: Record<string, string | number>) => {
|
||||||
const t = useCallback((key: string, params?: Record<string, string | number>) => {
|
const keys = key.split('.');
|
||||||
const keys = key.split('.');
|
let value: unknown = dictionaries[locale];
|
||||||
let value: unknown = dictionaries[locale];
|
|
||||||
|
for (const k of keys) {
|
||||||
for (const k of keys) {
|
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; // Return key if not found
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
if (typeof value !== 'string') return key;
|
||||||
if (typeof value !== 'string') return key;
|
|
||||||
|
if (params) {
|
||||||
// Replace parameters like {count}, {time}, {year}
|
return Object.entries(params).reduce(
|
||||||
if (params) {
|
(str, [paramKey, paramValue]) => str.replace(`{${paramKey}}`, String(paramValue)),
|
||||||
return Object.entries(params).reduce(
|
value
|
||||||
(str, [paramKey, paramValue]) => str.replace(`{${paramKey}}`, String(paramValue)),
|
);
|
||||||
value
|
}
|
||||||
);
|
|
||||||
}
|
return value;
|
||||||
|
}, [locale]);
|
||||||
return value;
|
|
||||||
}, [locale]);
|
// Restore locale from localStorage after hydration completes.
|
||||||
|
// Using startTransition to mark this as non-urgent so React
|
||||||
// Sync from localStorage on mount (Client-side only)
|
// 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);
|
||||||
}
|
});
|
||||||
}, []);
|
}
|
||||||
|
}, []);
|
||||||
useEffect(() => {
|
|
||||||
if (typeof window === 'undefined') return;
|
useEffect(() => {
|
||||||
localStorage.setItem(localeStorageKey, locale);
|
if (typeof window === 'undefined') return;
|
||||||
document.documentElement.lang = locale;
|
localStorage.setItem(localeStorageKey, locale);
|
||||||
}, [locale]);
|
document.documentElement.lang = locale;
|
||||||
|
}, [locale]);
|
||||||
return (
|
|
||||||
<I18nContext.Provider value={{ locale, setLocale, t }}>
|
return (
|
||||||
{children}
|
<I18nContext.Provider value={{ locale, setLocale, t }}>
|
||||||
</I18nContext.Provider>
|
{children}
|
||||||
);
|
</I18nContext.Provider>
|
||||||
}
|
);
|
||||||
|
}
|
||||||
export function useI18n() {
|
|
||||||
const context = useContext(I18nContext);
|
export function useI18n() {
|
||||||
if (!context) {
|
const context = useContext(I18nContext);
|
||||||
throw new Error('useI18n must be used within an I18nProvider');
|
if (!context) {
|
||||||
}
|
throw new Error('useI18n must be used within an I18nProvider');
|
||||||
return context;
|
}
|
||||||
}
|
return context;
|
||||||
|
}
|
||||||
export function useTranslation() {
|
|
||||||
const { t, locale, setLocale } = useI18n();
|
export function useTranslation() {
|
||||||
return { t, locale, setLocale };
|
const { t, locale, setLocale } = useI18n();
|
||||||
}
|
return { t, locale, setLocale };
|
||||||
|
}
|
||||||
export const locales: { code: Locale; name: string; flag: string }[] = [
|
|
||||||
{ code: 'en', name: 'English', flag: '🇺🇸' },
|
export const locales: { code: Locale; name: string; flag: string }[] = [
|
||||||
{ code: 'es', name: 'Español', flag: '🇪🇸' },
|
{ code: 'en', name: 'English', flag: '🇺🇸' },
|
||||||
{ code: 'pt-BR', name: 'Português', flag: '🇧🇷' },
|
{ code: 'es', name: 'Español', flag: '🇪🇸' },
|
||||||
];
|
{ code: 'pt-BR', name: 'Português', flag: '🇧🇷' },
|
||||||
|
];
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue