feat: New Home layout, Navbar and I18n improvements
This commit is contained in:
parent
5a38f49279
commit
8070492e48
6 changed files with 605 additions and 819 deletions
5
backend/start_dev.sh
Executable file
5
backend/start_dev.sh
Executable file
|
|
@ -0,0 +1,5 @@
|
||||||
|
#!/bin/sh
|
||||||
|
export GOPATH=/go
|
||||||
|
export PATH=/go/bin:/usr/local/go/bin:$PATH
|
||||||
|
go install github.com/air-verse/air@v1.60.0
|
||||||
|
air
|
||||||
|
|
@ -1,499 +1,159 @@
|
||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import { Navbar } from "@/components/navbar"
|
import { Button } from "@/components/ui/button"
|
||||||
import { Footer } from "@/components/footer"
|
import { mockJobs } from "@/lib/mock-data"
|
||||||
import { JobCard } from "@/components/job-card"
|
import Link from "next/link"
|
||||||
import { Button } from "@/components/ui/button"
|
import { ArrowRight, CheckCircle2 } from 'lucide-react'
|
||||||
import { Card, CardContent } from "@/components/ui/card"
|
import Image from "next/image"
|
||||||
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"
|
import { motion } from "framer-motion"
|
||||||
import { mockJobs, mockTestimonials } from "@/lib/mock-data"
|
import { useTranslation } from "@/lib/i18n"
|
||||||
import { FileText, CheckCircle, ArrowRight, Building2, Users, ChevronLeft, ChevronRight, Eye } from "lucide-react"
|
import { Navbar } from "@/components/navbar"
|
||||||
import Link from "next/link"
|
import { Footer } from "@/components/footer"
|
||||||
import { motion } from "framer-motion"
|
import { HomeSearch } from "@/components/home-search"
|
||||||
import Image from "next/image"
|
import { JobCard } from "@/components/job-card"
|
||||||
import { useTranslation } from "@/lib/i18n"
|
|
||||||
import { useConfig } from "@/contexts/ConfigContext"
|
export default function Home() {
|
||||||
|
const { t } = useTranslation()
|
||||||
import { useState, useEffect } from "react"
|
|
||||||
import type { Job } from "@/lib/types"
|
return (
|
||||||
|
<div className="min-h-screen bg-gray-50 flex flex-col font-sans">
|
||||||
export default function HomePage() {
|
<Navbar />
|
||||||
const { t } = useTranslation()
|
|
||||||
const config = useConfig()
|
<main className="flex-grow">
|
||||||
const [featuredJobs, setFeaturedJobs] = useState<Job[]>(mockJobs.slice(0, 31))
|
{/* Hero Section */}
|
||||||
const [loading, setLoading] = useState(true)
|
<section className="relative h-[500px] flex items-center justify-center bg-[#1F2F40]">
|
||||||
const [featuredIndex, setFeaturedIndex] = useState(0)
|
{/* Background Image with Overlay */}
|
||||||
const [moreJobsIndex, setMoreJobsIndex] = useState(0)
|
<div className="absolute inset-0 z-0">
|
||||||
const [openFilters, setOpenFilters] = useState({
|
<Image
|
||||||
contractType: false,
|
src="/10.png"
|
||||||
workMode: false,
|
alt="Background"
|
||||||
location: false,
|
fill
|
||||||
salary: false
|
className="object-cover opacity-60 contrast-125"
|
||||||
})
|
priority
|
||||||
|
/>
|
||||||
const toggleFilter = (filterName: keyof typeof openFilters) => {
|
<div className="absolute inset-0 bg-gradient-to-r from-[#1F2F40] via-[#1F2F40]/90 to-transparent" />
|
||||||
setOpenFilters(prev => ({
|
</div>
|
||||||
contractType: false,
|
|
||||||
workMode: false,
|
<div className="container mx-auto px-4 relative z-10 text-center sm:text-left">
|
||||||
location: false,
|
<div className="max-w-3xl">
|
||||||
salary: false,
|
<motion.h1
|
||||||
[filterName]: !prev[filterName]
|
initial={{ opacity: 0, y: 20 }}
|
||||||
}))
|
animate={{ opacity: 1, y: 0 }}
|
||||||
}
|
transition={{ duration: 0.5 }}
|
||||||
|
className="text-4xl md:text-5xl lg:text-6xl font-bold text-white mb-6 leading-tight tracking-tight"
|
||||||
useEffect(() => {
|
>
|
||||||
async function fetchFeaturedJobs() {
|
Encontre a Vaga de TI <br className="hidden sm:block" />
|
||||||
try {
|
dos Seus Sonhos.
|
||||||
const apiBase = config.apiUrl
|
</motion.h1>
|
||||||
console.log("[DEBUG] API Base URL:", apiBase)
|
|
||||||
|
<motion.p
|
||||||
const mapJobs = (jobs: any[]): Job[] =>
|
initial={{ opacity: 0, y: 20 }}
|
||||||
jobs.map((j: any) => ({
|
animate={{ opacity: 1, y: 0 }}
|
||||||
id: String(j.id),
|
transition={{ duration: 0.5, delay: 0.1 }}
|
||||||
title: j.title,
|
className="text-lg md:text-xl text-gray-300 mb-8 max-w-xl leading-relaxed"
|
||||||
company: j.companyName || t("jobs.confidential"),
|
>
|
||||||
location: j.location || t("workMode.remote"),
|
Conectamos você com as melhores empresas e techs.
|
||||||
type: j.employmentType || "full-time",
|
</motion.p>
|
||||||
salary: j.salaryMin ? `R$ ${j.salaryMin}` : t("jobs.salary.negotiable"),
|
|
||||||
description: j.description,
|
<motion.div
|
||||||
requirements: j.requirements || [],
|
initial={{ opacity: 0, y: 20 }}
|
||||||
postedAt: j.createdAt,
|
animate={{ opacity: 1, y: 0 }}
|
||||||
isFeatured: j.isFeatured
|
transition={{ duration: 0.5, delay: 0.2 }}
|
||||||
}))
|
>
|
||||||
|
<Link href="/jobs">
|
||||||
console.log("[DEBUG] Fetching featured jobs from:", `${apiBase}/api/v1/jobs?featured=true&limit=31`)
|
<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">
|
||||||
const featuredRes = await fetch(`${apiBase}/api/v1/jobs?featured=true&limit=31`)
|
Buscar Vagas
|
||||||
console.log("[DEBUG] Featured response status:", featuredRes.status)
|
</Button>
|
||||||
|
</Link>
|
||||||
if (!featuredRes.ok) throw new Error("Failed to fetch featured jobs")
|
</motion.div>
|
||||||
const featuredData = await featuredRes.json()
|
</div>
|
||||||
console.log("[DEBUG] Featured data from API:", featuredData)
|
</div>
|
||||||
|
</section>
|
||||||
const featuredList = featuredData.data ? mapJobs(featuredData.data) : []
|
|
||||||
console.log("[DEBUG] Mapped featured jobs:", featuredList.length, "jobs")
|
{/* Search Section */}
|
||||||
|
<section className="px-4 mb-16">
|
||||||
if (featuredList.length >= 24) {
|
<div className="container mx-auto">
|
||||||
console.log("[DEBUG] Using featured/API jobs")
|
<HomeSearch />
|
||||||
setFeaturedJobs(featuredList.slice(0, 31))
|
</div>
|
||||||
return
|
</section>
|
||||||
}
|
|
||||||
|
{/* Latest Jobs Section */}
|
||||||
console.log("[DEBUG] Fetching fallback jobs from:", `${apiBase}/api/v1/jobs?limit=31`)
|
<section className="py-12 bg-gray-50">
|
||||||
const fallbackRes = await fetch(`${apiBase}/api/v1/jobs?limit=31`)
|
<div className="container mx-auto px-4">
|
||||||
console.log("[DEBUG] Fallback response status:", fallbackRes.status)
|
<h2 className="text-2xl md:text-3xl font-bold text-gray-900 mb-8">
|
||||||
|
Últimas Vagas Cadastradas
|
||||||
if (!fallbackRes.ok) throw new Error("Failed to fetch fallback jobs")
|
</h2>
|
||||||
const fallbackData = await fallbackRes.json()
|
|
||||||
console.log("[DEBUG] Fallback data from API:", fallbackData)
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
|
||||||
|
{mockJobs.slice(0, 4).map((job, index) => (
|
||||||
const fallbackList = fallbackData.data ? mapJobs(fallbackData.data) : []
|
<JobCard key={job.id} job={job} />
|
||||||
console.log("[DEBUG] Mapped fallback jobs:", fallbackList.length, "jobs")
|
))}
|
||||||
|
</div>
|
||||||
const combined = [...featuredList, ...fallbackList].slice(0, 31)
|
</div>
|
||||||
console.log("[DEBUG] Combined jobs:", combined.length, "jobs")
|
</section>
|
||||||
|
|
||||||
if (combined.length >= 24) {
|
{/* More Jobs Section */}
|
||||||
console.log("[DEBUG] Using combined jobs")
|
<section className="py-12 bg-white">
|
||||||
setFeaturedJobs(combined)
|
<div className="container mx-auto px-4">
|
||||||
}
|
<div className="flex items-center justify-between mb-8">
|
||||||
} catch (error) {
|
<h2 className="text-2xl md:text-3xl font-bold text-gray-900">
|
||||||
console.error("[DEBUG] ❌ Error fetching featured jobs:", error)
|
Mais Vagas
|
||||||
} finally {
|
</h2>
|
||||||
setLoading(false)
|
<Link href="/jobs">
|
||||||
}
|
<Button className="bg-orange-500 hover:bg-orange-600 text-white font-bold">
|
||||||
}
|
Ver Todas Vagas
|
||||||
fetchFeaturedJobs()
|
</Button>
|
||||||
}, [])
|
</Link>
|
||||||
|
</div>
|
||||||
return (
|
|
||||||
<div className="min-h-screen flex flex-col">
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
|
||||||
<Navbar />
|
{mockJobs.slice(0, 8).map((job, index) => (
|
||||||
|
<JobCard key={`more-${job.id}-${index}`} job={job} />
|
||||||
<main className="flex-1">
|
))}
|
||||||
{/* Hero Section */}
|
</div>
|
||||||
<section className="bg-primary text-white relative overflow-hidden flex items-center min-h-[500px]">
|
</div>
|
||||||
{/* Mobile Background */}
|
</section>
|
||||||
<div className="absolute inset-0 z-0 md:hidden">
|
|
||||||
<Image
|
|
||||||
src="/home-mobile.jpg"
|
{/* Bottom CTA Section */}
|
||||||
alt="Background"
|
<section className="py-16 bg-white">
|
||||||
fill
|
<div className="container mx-auto px-4">
|
||||||
className="object-cover object-center"
|
<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]">
|
||||||
quality={100}
|
|
||||||
priority
|
{/* Content */}
|
||||||
sizes="100vw"
|
<div className="relative z-10 max-w-xl">
|
||||||
/>
|
<h2 className="text-3xl md:text-4xl font-bold text-white mb-4 leading-tight">
|
||||||
{/* Black overlay with 20% opacity */}
|
Milhares de oportunidades <br/> esperam você.
|
||||||
<div className="absolute inset-0 bg-black/20"></div>
|
</h2>
|
||||||
</div>
|
<p className="text-base text-gray-300 mb-8">
|
||||||
{/* Desktop Background */}
|
Conecte cargos, talentos, tomada de ações de vagas.
|
||||||
<div className="absolute inset-0 z-0 hidden md:block">
|
</p>
|
||||||
<Image
|
<Link href="/register/user">
|
||||||
src="/10.png"
|
<Button className="h-12 px-8 bg-white text-gray-900 hover:bg-gray-100 font-bold text-lg rounded-md">
|
||||||
alt="Background"
|
Cadastre-se
|
||||||
fill
|
</Button>
|
||||||
className="object-cover object-center"
|
</Link>
|
||||||
quality={100}
|
</div>
|
||||||
priority
|
|
||||||
sizes="100vw"
|
{/* Image Background for CTA */}
|
||||||
/>
|
<div className="absolute inset-0 z-0">
|
||||||
</div>
|
<div className="absolute right-0 top-0 h-full w-full md:w-2/3 lg:w-1/2">
|
||||||
<div className="container mx-auto px-4 sm:px-6 lg:px-8 w-full relative z-10">
|
<Image
|
||||||
<div className="max-w-7xl mx-auto">
|
src="/muie.jpeg"
|
||||||
<div className="text-left max-w-2xl py-12">
|
alt="Professional"
|
||||||
<motion.h1
|
fill
|
||||||
initial={{ opacity: 0, y: 20 }}
|
className="object-cover object-center md:object-right opacity-40 md:opacity-100" // Opacity adjusted for mobile readability
|
||||||
animate={{ opacity: 1, y: 0 }}
|
/>
|
||||||
transition={{ duration: 0.5 }}
|
{/* Gradient Overlay to blend with dark background */}
|
||||||
className="text-5xl sm:text-6xl lg:text-6xl font-bold mb-6 text-white leading-tight [text-shadow:2px_2px_8px_rgba(0,0,0,0.8),0px_0px_12px_rgba(0,0,0,0.6)] md:[text-shadow:none]"
|
<div className="absolute inset-0 bg-gradient-to-t md:bg-gradient-to-r from-[#1F2F40] via-[#1F2F40]/30 to-transparent" />
|
||||||
>
|
</div>
|
||||||
{t('home.hero.title')}<br />{t('home.hero.titleLine2')}
|
</div>
|
||||||
</motion.h1>
|
</div>
|
||||||
<motion.p
|
</div>
|
||||||
initial={{ opacity: 0, y: 20 }}
|
</section>
|
||||||
animate={{ opacity: 1, y: 0 }}
|
</main>
|
||||||
transition={{ duration: 0.5, delay: 0.1 }}
|
|
||||||
className="text-2xl mb-10 leading-relaxed text-white [text-shadow:2px_2px_6px_rgba(0,0,0,0.8),0px_0px_10px_rgba(0,0,0,0.5)] md:[text-shadow:none]"
|
<Footer />
|
||||||
>
|
</div>
|
||||||
{t('home.hero.subtitle')}
|
)
|
||||||
</motion.p>
|
}
|
||||||
<motion.div
|
|
||||||
initial={{ opacity: 0, y: 20 }}
|
|
||||||
animate={{ opacity: 1, y: 0 }}
|
|
||||||
transition={{ duration: 0.5, delay: 0.2 }}
|
|
||||||
className="flex gap-4"
|
|
||||||
>
|
|
||||||
<Link href="/jobs">
|
|
||||||
<Button size="lg" className="bg-primary hover:bg-primary/90 text-white shadow-2xl hover:shadow-2xl font-bold px-10 py-6 text-lg rounded-lg transition-all duration-200 border-0">
|
|
||||||
{t('home.hero.cta')}
|
|
||||||
</Button>
|
|
||||||
</Link>
|
|
||||||
</motion.div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
{/* Search Bar Section */}
|
|
||||||
<section className="py-16">
|
|
||||||
<div className="container mx-auto px-4 sm:px-6 lg:px-8 max-w-8xl">
|
|
||||||
<div className="mb-8">
|
|
||||||
<div className="mb-6 relative">
|
|
||||||
<svg className="absolute left-4 top-1/2 -translate-y-1/2 w-5 h-5 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
|
|
||||||
</svg>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
placeholder={t('home.search.placeholder')}
|
|
||||||
className="w-full h-14 pl-12 pr-4 border border-gray-200 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent text-base bg-white shadow-sm"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-5 gap-4 items-stretch">
|
|
||||||
{/* Contract Type - Static Expanded */}
|
|
||||||
<div className="bg-white border-2 border-gray-200 rounded-xl shadow-sm hover:border-primary/30 transition-colors relative h-full flex flex-col">
|
|
||||||
<div className="flex items-center justify-between w-full p-5 pb-2">
|
|
||||||
<span className="text-base font-bold text-gray-900">{t('home.search.contractType')}</span>
|
|
||||||
</div>
|
|
||||||
<div className="space-y-3 px-5 pb-5 pt-2 flex-1">
|
|
||||||
<label className="flex items-center text-sm text-gray-700 cursor-pointer hover:text-gray-900 transition-colors">
|
|
||||||
<input type="checkbox" className="w-4 h-4 text-primary border-gray-300 rounded focus:ring-primary mr-3" />
|
|
||||||
<span>{t('home.search.pj')}</span>
|
|
||||||
</label>
|
|
||||||
<label className="flex items-center text-sm text-gray-700 cursor-pointer hover:text-gray-900 transition-colors">
|
|
||||||
<input type="checkbox" className="w-4 h-4 text-primary border-gray-300 rounded focus:ring-primary mr-3" />
|
|
||||||
<span>{t('home.search.clt')}</span>
|
|
||||||
</label>
|
|
||||||
<label className="flex items-center text-sm text-gray-700 cursor-pointer hover:text-gray-900 transition-colors">
|
|
||||||
<input type="checkbox" className="w-4 h-4 text-primary border-gray-300 rounded focus:ring-primary mr-3" />
|
|
||||||
<span>{t('home.search.freelancer')}</span>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Work Mode - Static Expanded */}
|
|
||||||
<div className="bg-white border-2 border-gray-200 rounded-xl shadow-sm hover:border-primary/30 transition-colors relative h-full flex flex-col">
|
|
||||||
<div className="flex items-center justify-between w-full p-5 pb-2">
|
|
||||||
<span className="text-base font-bold text-gray-900">{t('home.search.workMode')}</span>
|
|
||||||
</div>
|
|
||||||
<div className="space-y-3 px-5 pb-5 pt-2 flex-1">
|
|
||||||
<label className="flex items-center text-sm text-gray-700 cursor-pointer hover:text-gray-900 transition-colors">
|
|
||||||
<input type="checkbox" className="w-4 h-4 text-primary border-gray-300 rounded focus:ring-primary mr-3" />
|
|
||||||
<span>{t('home.search.homeOffice')}</span>
|
|
||||||
</label>
|
|
||||||
<label className="flex items-center text-sm text-gray-700 cursor-pointer hover:text-gray-900 transition-colors">
|
|
||||||
<input type="checkbox" className="w-4 h-4 text-primary border-gray-300 rounded focus:ring-primary mr-3" />
|
|
||||||
<span>{t('home.search.presencial')}</span>
|
|
||||||
</label>
|
|
||||||
<label className="flex items-center text-sm text-gray-700 cursor-pointer hover:text-gray-900 transition-colors">
|
|
||||||
<input type="checkbox" className="w-4 h-4 text-primary border-gray-300 rounded focus:ring-primary mr-3" />
|
|
||||||
<span>{t('home.search.hybrid')}</span>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Location - Static Expanded */}
|
|
||||||
<div className="bg-white border-2 border-gray-200 rounded-xl shadow-sm hover:border-primary/30 transition-colors relative h-full flex flex-col">
|
|
||||||
<div className="flex items-center justify-between w-full p-5 pb-2">
|
|
||||||
<span className="text-base font-bold text-gray-900">{t('home.search.location')}</span>
|
|
||||||
</div>
|
|
||||||
<div className="px-5 pb-5 pt-2 flex-1 flex items-center">
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
placeholder="Cidade e estado"
|
|
||||||
className="w-full border border-gray-300 rounded-lg px-4 py-2 focus:outline-none focus:ring-2 focus:ring-primary h-12"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Salary - Static Expanded */}
|
|
||||||
<div className="bg-white border-2 border-gray-200 rounded-xl shadow-sm hover:border-primary/30 transition-colors relative h-full flex flex-col">
|
|
||||||
<div className="flex items-center justify-between w-full p-5 pb-2">
|
|
||||||
<span className="text-base font-bold text-gray-900">{t('home.search.salary')}</span>
|
|
||||||
</div>
|
|
||||||
<div className="px-5 pb-5 pt-2 flex-1 flex items-center">
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
placeholder="R$ 0,00"
|
|
||||||
className="w-full border border-gray-300 rounded-lg px-4 py-2 focus:outline-none focus:ring-2 focus:ring-primary h-12"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Filter Button - Unified */}
|
|
||||||
<Button className="bg-primary hover:bg-primary/90 text-white min-h-[96px] w-full lg:w-[260px] rounded-lg font-bold text-2xl shadow-2xl hover:shadow-2xl transition-all flex flex-col gap-1 items-center justify-center px-2 py-6 lg:self-center">
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={2} stroke="currentColor" className="w-5 h-5">
|
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" d="M21 21l-5.197-5.197m0 0A7.5 7.5 0 105.196 5.196a7.5 7.5 0 0010.607 10.607z" />
|
|
||||||
</svg>
|
|
||||||
<span className="text-2xl font-bold">{t('home.search.filter')}</span>
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
{/* Featured Jobs */}
|
|
||||||
<section className="py-0 mb-0">
|
|
||||||
<div className="container mx-auto px-4 sm:px-6 lg:px-8 max-w-8xl">
|
|
||||||
<div className="flex justify-between items-center mb-8">
|
|
||||||
<h2 className="text-3xl font-bold text-gray-900">{t('home.featuredJobs.title')}</h2>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
|
|
||||||
{(featuredJobs.length >= 4 ? featuredJobs.slice(0, 4) : mockJobs.slice(0, 4))
|
|
||||||
.map((job, index) => {
|
|
||||||
const dates = ['02/06', '05/06', '08/06', '11/06'];
|
|
||||||
const randomDate = dates[index % dates.length];
|
|
||||||
const levels = [t('home.levels.mid'), t('home.levels.junior'), t('home.levels.senior'), t('home.levels.mid')];
|
|
||||||
const level = levels[index % levels.length];
|
|
||||||
const statusLabels = [t('workMode.remote'), t('workMode.hybrid'), t('workMode.onsite'), t('workMode.remote')];
|
|
||||||
const statusLabel = statusLabels[index % statusLabels.length];
|
|
||||||
return (
|
|
||||||
<motion.div
|
|
||||||
key={job.id}
|
|
||||||
initial={{ opacity: 0, y: 20 }}
|
|
||||||
animate={{ opacity: 1, y: 0 }}
|
|
||||||
transition={{ duration: 0.3 }}
|
|
||||||
>
|
|
||||||
<Card className="hover:shadow-lg transition-all border border-gray-200 rounded-2xl overflow-hidden bg-white h-full group cursor-pointer flex flex-col">
|
|
||||||
<div className="p-5 flex flex-col flex-1">
|
|
||||||
{/* Header */}
|
|
||||||
<div className="flex items-center justify-between mb-4">
|
|
||||||
<div className="flex items-center gap-3">
|
|
||||||
<div className="w-10 h-10 bg-gray-50 rounded-lg flex items-center justify-center border border-gray-100">
|
|
||||||
<Building2 className="w-5 h-5 text-gray-700" />
|
|
||||||
</div>
|
|
||||||
<span className="text-sm font-bold text-gray-900 line-clamp-1">{job.company}</span>
|
|
||||||
</div>
|
|
||||||
<span className="text-[10px] px-2.5 py-1 bg-gray-900 text-white rounded-md font-bold uppercase tracking-wide">
|
|
||||||
{statusLabel}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Content */}
|
|
||||||
<div className="flex-1">
|
|
||||||
<h3 className="font-bold text-lg text-gray-900 mb-3 leading-tight line-clamp-2 pb-3 border-b border-gray-100">{job.title}</h3>
|
|
||||||
<div className="relative w-full h-32 mb-4 flex items-center justify-center bg-gray-50/50 rounded-lg border border-gray-50">
|
|
||||||
<Image
|
|
||||||
src="/111.png"
|
|
||||||
alt="Job Illustration"
|
|
||||||
fill
|
|
||||||
className="object-contain p-2"
|
|
||||||
quality={100}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Footer Section with Separator */}
|
|
||||||
<div className="px-5 pb-5 pt-0 mt-auto">
|
|
||||||
<div className="border-t border-gray-200 pt-4 mb-4">
|
|
||||||
<p className="text-sm text-gray-500 font-medium flex items-center gap-2">
|
|
||||||
<span className="text-gray-900 font-bold">{level}</span>
|
|
||||||
<span className="w-1 h-1 rounded-full bg-gray-300"></span>
|
|
||||||
{job.location}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex gap-3">
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
className="flex-1 border-gray-200 text-gray-700 hover:bg-gray-50 hover:text-gray-900 rounded-lg font-bold h-10 text-xs uppercase tracking-wide"
|
|
||||||
>
|
|
||||||
{t('home.featuredJobs.viewJob')}
|
|
||||||
</Button>
|
|
||||||
<Link href={`/jobs/${job.id}`} className="flex-1">
|
|
||||||
<Button className="w-full bg-gray-900 hover:bg-gray-800 text-white rounded-lg font-bold h-10 text-xs uppercase tracking-wide">
|
|
||||||
{t('home.featuredJobs.apply')}
|
|
||||||
</Button>
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Card>
|
|
||||||
</motion.div>
|
|
||||||
)
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
{/* More Jobs Section */}
|
|
||||||
<section className="py-0 mt-0 pt-16">
|
|
||||||
<div className="container mx-auto px-4 sm:px-6 lg:px-8 max-w-8xl">
|
|
||||||
<div className="flex justify-between items-center mb-8">
|
|
||||||
<h2 className="text-3xl font-bold text-gray-900">{t('home.moreJobs.title')}</h2>
|
|
||||||
<Link href="/jobs">
|
|
||||||
<Button className="bg-primary hover:bg-primary/90 text-white rounded-lg px-10 py-4 font-bold text-lg min-w-[220px] shadow-2xl hover:shadow-2xl">
|
|
||||||
{t('home.moreJobs.viewAll')}
|
|
||||||
</Button>
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-6">
|
|
||||||
{mockJobs.slice(0, 8)
|
|
||||||
.map((job, index) => {
|
|
||||||
const colors = [
|
|
||||||
'bg-cyan-500', 'bg-blue-500', 'bg-indigo-500', 'bg-gray-500',
|
|
||||||
'bg-teal-500', 'bg-sky-500', 'bg-orange-500', 'bg-purple-500'
|
|
||||||
];
|
|
||||||
const bgColor = colors[index % colors.length];
|
|
||||||
const icons = ['💻', '🎨', '📊', '🚀', '⚙️', '🔧', '📱', '🎯'];
|
|
||||||
const icon = icons[index % icons.length];
|
|
||||||
|
|
||||||
const descriptions = [
|
|
||||||
'Buscamos um Senior Full Stack para liderar soluções robustas e escaláveis de ponta a ponta.',
|
|
||||||
'O UX/UI Designer ideal para transformar ideias em experiências visuais incríveis.',
|
|
||||||
'Faça parte do time como Data Engineer e construa pipelines de dados inteligentes e eficientes.',
|
|
||||||
'Procuramos um Product Manager para liderar produtos inovadores do conceito ao lançamento.',
|
|
||||||
'Oportunidade para Mobile Developer criar aplicativos modernos e de alto desempenho.',
|
|
||||||
'Junte-se a nós como DevOps Engineer e automatize infraestruturas em ambientes de nuvem.',
|
|
||||||
'Vaga para Backend Developer focado em performance, segurança e APIs escaláveis.',
|
|
||||||
'Buscamos um QA Analyst atento aos detalhes para garantir a máxima qualidade dos produtos.'
|
|
||||||
];
|
|
||||||
const description = descriptions[index % descriptions.length];
|
|
||||||
|
|
||||||
return (
|
|
||||||
<motion.div
|
|
||||||
key={job.id}
|
|
||||||
initial={{ opacity: 0, y: 20 }}
|
|
||||||
animate={{ opacity: 1, y: 0 }}
|
|
||||||
transition={{ duration: 0.3 }}
|
|
||||||
>
|
|
||||||
<Card className="hover:shadow-lg transition-all border border-gray-200 bg-white rounded-xl overflow-hidden group cursor-pointer h-full">
|
|
||||||
<CardContent className="p-5">
|
|
||||||
{/* Cabeçalho com logo e seta */}
|
|
||||||
<div className="flex items-start justify-between mb-4">
|
|
||||||
<div className="flex items-start gap-3 flex-1">
|
|
||||||
<div className={`w-12 h-12 ${bgColor} rounded-full flex items-center justify-center text-white text-xl flex-shrink-0`}>
|
|
||||||
{icon}
|
|
||||||
</div>
|
|
||||||
<div className="flex-1 min-w-0">
|
|
||||||
<h3 className="font-bold text-base text-gray-900 line-clamp-1 mb-1">{job.title}</h3>
|
|
||||||
<p className="text-sm text-gray-600 line-clamp-1 mb-2">{job.company}</p>
|
|
||||||
<p className="text-xs text-gray-500 leading-relaxed">{description}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<ChevronRight className="w-5 h-5 text-gray-400 group-hover:text-gray-600 transition-colors flex-shrink-0 ml-2" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Rodapé com botões */}
|
|
||||||
<div className="pt-4 mt-4 border-t border-gray-200">
|
|
||||||
<div className="flex gap-3">
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
className="flex-1 border-gray-200 text-gray-700 hover:bg-gray-50 hover:text-gray-900 rounded-lg font-bold h-9 text-xs uppercase tracking-wide"
|
|
||||||
>
|
|
||||||
{t('home.featuredJobs.viewJob')}
|
|
||||||
</Button>
|
|
||||||
<Link href={`/jobs/${job.id}`} className="flex-1">
|
|
||||||
<Button className="w-full bg-gray-900 hover:bg-gray-800 text-white rounded-lg font-bold h-9 text-xs uppercase tracking-wide">
|
|
||||||
{t('home.featuredJobs.apply')}
|
|
||||||
</Button>
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</motion.div>
|
|
||||||
)
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
{/* CTA Section */}
|
|
||||||
<section className="py-12">
|
|
||||||
<div className="container mx-auto px-4 sm:px-6 lg:px-8 max-w-8xl">
|
|
||||||
<div className="bg-gray-900 rounded-2xl overflow-hidden shadow-lg relative min-h-[500px] flex items-center">
|
|
||||||
{/* Image Layer: Single Image with Seamless Gradient Overlay */}
|
|
||||||
<div className="absolute inset-y-0 right-0 w-full md:w-3/4 z-0">
|
|
||||||
<Image
|
|
||||||
src="/muie.jpeg"
|
|
||||||
alt="Woman with Notebook"
|
|
||||||
fill
|
|
||||||
className="object-contain object-right"
|
|
||||||
quality={100}
|
|
||||||
priority
|
|
||||||
/>
|
|
||||||
{/*
|
|
||||||
Seamless Blend Gradient:
|
|
||||||
Starts solid gray-900 (matching, container) on left.
|
|
||||||
Fades gradually to transparent on right.
|
|
||||||
This "dyes" the dark background of the photo to match the container.
|
|
||||||
*/}
|
|
||||||
<div className="absolute inset-0 bg-gradient-to-r from-gray-900 from-20% via-gray-900/80 via-50% to-transparent to-100%" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="grid lg:grid-cols-2 gap-8 items-center p-8 lg:p-12 relative z-10">
|
|
||||||
{/* Text Content */}
|
|
||||||
<div className="text-white max-w-lg">
|
|
||||||
<h2 className="text-3xl lg:text-4xl font-bold mb-4 leading-tight">
|
|
||||||
{t('home.cta.title')}
|
|
||||||
</h2>
|
|
||||||
<p className="mb-6 text-white/90 text-lg">
|
|
||||||
{t('home.cta.subtitle')}
|
|
||||||
</p>
|
|
||||||
<Button size="lg" className="bg-white text-primary hover:bg-white/90 font-bold px-12 py-7 text-xl rounded-lg">
|
|
||||||
{t('home.cta.button')}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
</main>
|
|
||||||
|
|
||||||
<Footer />
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function FilterIcon() {
|
|
||||||
return (
|
|
||||||
<svg width="44" height="44" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" className="w-11 h-11">
|
|
||||||
<rect x="4" y="7" width="16" height="3" rx="1.5" fill="white" />
|
|
||||||
<rect x="7" y="12" width="10" height="3" rx="1.5" fill="white" />
|
|
||||||
<rect x="10" y="17" width="4" height="3" rx="1.5" fill="white" />
|
|
||||||
</svg>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
|
||||||
117
frontend/src/components/home-search.tsx
Normal file
117
frontend/src/components/home-search.tsx
Normal file
|
|
@ -0,0 +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 (
|
||||||
|
<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="relative">
|
||||||
|
<Search className="absolute left-3 top-3 h-5 w-5 text-gray-400" />
|
||||||
|
<Input
|
||||||
|
placeholder="Digite cargo, empresa ou palavra-chave"
|
||||||
|
className="pl-10 h-12 bg-gray-50 border-gray-200"
|
||||||
|
value={searchTerm}
|
||||||
|
onChange={(e) => setSearchTerm(e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-5 gap-4">
|
||||||
|
{/* Contract Type */}
|
||||||
|
<div className="space-y-2">
|
||||||
|
<label className="text-xs font-semibold text-gray-700 uppercase tracking-wide">Tipo de contratação</label>
|
||||||
|
<Select value={type} onValueChange={setType}>
|
||||||
|
<SelectTrigger className="!h-12 bg-gray-50 border-gray-200 w-full">
|
||||||
|
<SelectValue placeholder="Selecione" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="all">Todos</SelectItem>
|
||||||
|
<SelectItem value="contract">PJ</SelectItem>
|
||||||
|
<SelectItem value="full-time">CLT</SelectItem>
|
||||||
|
<SelectItem value="freelance">Freelancer</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Work Mode */}
|
||||||
|
<div className="space-y-2">
|
||||||
|
<label className="text-xs font-semibold text-gray-700 uppercase tracking-wide">Regime de Trabalho</label>
|
||||||
|
<Select value={workMode} onValueChange={setWorkMode}>
|
||||||
|
<SelectTrigger className="!h-12 bg-gray-50 border-gray-200 w-full">
|
||||||
|
<SelectValue placeholder="Selecione" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="all">Todos</SelectItem>
|
||||||
|
<SelectItem value="remote">Remoto</SelectItem>
|
||||||
|
<SelectItem value="hybrid">Híbrido</SelectItem>
|
||||||
|
<SelectItem value="onsite">Presencial</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Location */}
|
||||||
|
<div className="space-y-2">
|
||||||
|
<label className="text-xs font-semibold text-gray-700 uppercase tracking-wide">Cidade e estado</label>
|
||||||
|
<div className="relative">
|
||||||
|
<Input
|
||||||
|
placeholder="Cidade e estado"
|
||||||
|
className="h-12 bg-gray-50 border-gray-200"
|
||||||
|
value={location}
|
||||||
|
onChange={(e) => setLocation(e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Salary */}
|
||||||
|
<div className="space-y-2">
|
||||||
|
<label className="text-xs font-semibold text-gray-700 uppercase tracking-wide">Pretensão salarial</label>
|
||||||
|
<div className="relative">
|
||||||
|
<Input
|
||||||
|
placeholder="R$ 0,00"
|
||||||
|
className="h-12 bg-gray-50 border-gray-200"
|
||||||
|
value={salary}
|
||||||
|
onChange={(e) => setSalary(e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Search Button */}
|
||||||
|
<div className="flex items-end">
|
||||||
|
<Button
|
||||||
|
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"
|
||||||
|
>
|
||||||
|
<Search className="w-5 h-5 mr-2" />
|
||||||
|
Filtrar Vagas
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -1,50 +1,50 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useTranslation } from "@/lib/i18n";
|
import { useTranslation } from "@/lib/i18n";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import {
|
import {
|
||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
DropdownMenuContent,
|
DropdownMenuContent,
|
||||||
DropdownMenuItem,
|
DropdownMenuItem,
|
||||||
DropdownMenuTrigger,
|
DropdownMenuTrigger,
|
||||||
} from "@/components/ui/dropdown-menu";
|
} from "@/components/ui/dropdown-menu";
|
||||||
import { Globe } from "lucide-react";
|
import { Globe } from "lucide-react";
|
||||||
|
|
||||||
export function LanguageSwitcher() {
|
export function LanguageSwitcher() {
|
||||||
const { locale, setLocale } = useTranslation();
|
const { locale, setLocale } = useTranslation();
|
||||||
|
|
||||||
const locales = [
|
const locales = [
|
||||||
{ code: "en" as const, name: "English", flag: "🇺🇸" },
|
{ code: "en" as const, name: "English", flag: "🇺🇸" },
|
||||||
{ code: "es" as const, name: "Español", flag: "🇪🇸" },
|
{ code: "es" as const, name: "Español", flag: "🇪🇸" },
|
||||||
{ code: "pt-BR" as const, name: "Português", flag: "🇧🇷" },
|
{ code: "pt-BR" as const, name: "Português", flag: "🇧🇷" },
|
||||||
];
|
];
|
||||||
|
|
||||||
const currentLocale = locales.find((l) => l.code === locale) || locales[0];
|
const currentLocale = locales.find((l) => l.code === locale) || locales[0];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
<DropdownMenuTrigger asChild>
|
<DropdownMenuTrigger asChild>
|
||||||
<Button variant="ghost" size="sm" className="w-12 px-0 gap-2 focus-visible:ring-0 focus-visible:ring-offset-0 hover:bg-transparent">
|
<Button variant="ghost" size="sm" className="w-12 px-0 gap-2 focus-visible:ring-0 focus-visible:ring-offset-0 hover:bg-white/10">
|
||||||
<Globe className="h-4 w-4 text-white" />
|
<Globe className="h-5 w-5 text-white/90 hover:text-white transition-colors" />
|
||||||
<span className="sr-only">Toggle language</span>
|
<span className="sr-only">Toggle language</span>
|
||||||
</Button>
|
</Button>
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent align="end">
|
<DropdownMenuContent align="end">
|
||||||
{locales.map((l) => (
|
{locales.map((l) => (
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
key={l.code}
|
key={l.code}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
console.log(`[LanguageSwitcher] Clicking ${l.code}`);
|
console.log(`[LanguageSwitcher] Clicking ${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"
|
||||||
>
|
>
|
||||||
<span className="text-lg">{l.flag}</span>
|
<span className="text-lg">{l.flag}</span>
|
||||||
<span>{l.name}</span>
|
<span>{l.name}</span>
|
||||||
{locale === l.code && <span className="ml-auto text-xs opacity-50">✓</span>}
|
{locale === l.code && <span className="ml-auto text-xs opacity-50">✓</span>}
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
))}
|
))}
|
||||||
</DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,155 +1,158 @@
|
||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import { useState } from "react"
|
import { useState } from "react"
|
||||||
import Link from "next/link"
|
import Link from "next/link"
|
||||||
import Image from "next/image"
|
import Image from "next/image"
|
||||||
import { Button } from "@/components/ui/button"
|
import { Button } from "@/components/ui/button"
|
||||||
import { Sheet, SheetContent, SheetTrigger } from "@/components/ui/sheet"
|
import { Sheet, SheetContent, SheetTrigger } from "@/components/ui/sheet"
|
||||||
import {
|
import {
|
||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
DropdownMenuContent,
|
DropdownMenuContent,
|
||||||
DropdownMenuItem,
|
DropdownMenuItem,
|
||||||
DropdownMenuTrigger,
|
DropdownMenuTrigger,
|
||||||
} from "@/components/ui/dropdown-menu"
|
} from "@/components/ui/dropdown-menu"
|
||||||
import { Menu, User, LogIn, Building2, UserPlus } from "lucide-react"
|
import { Menu, User, LogIn, Building2, UserPlus } from "lucide-react"
|
||||||
import { getCurrentUser } from "@/lib/auth"
|
import { getCurrentUser } from "@/lib/auth"
|
||||||
import { useTranslation } from "@/lib/i18n"
|
import { useTranslation } from "@/lib/i18n"
|
||||||
import { LanguageSwitcher } from "@/components/language-switcher"
|
import { LanguageSwitcher } from "@/components/language-switcher"
|
||||||
|
|
||||||
export function Navbar() {
|
export function Navbar() {
|
||||||
const [isOpen, setIsOpen] = useState(false)
|
const [isOpen, setIsOpen] = useState(false)
|
||||||
const user = getCurrentUser()
|
const user = getCurrentUser()
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
|
|
||||||
const navigationItems = [
|
const navigationItems = [
|
||||||
{ href: "/jobs", label: t('nav.jobs') },
|
{ href: "/jobs", label: t('nav.jobs') },
|
||||||
{ href: "/companies", label: t('footer.empresas') },
|
{ href: "/companies", label: t('footer.empresas') },
|
||||||
{ href: "/blog", label: t('footer.blog') },
|
{ href: "/blog", label: t('footer.blog') },
|
||||||
]
|
]
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<nav className="bg-[#1F2F40] sticky top-0 z-50 shadow-sm border-b border-white/20">
|
<nav className="bg-[#1F2F40] sticky top-0 z-50 shadow-sm border-b border-white/20">
|
||||||
<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 h-16 items-center justify-between">
|
<div className="flex h-16 items-center justify-between">
|
||||||
{/* Logo */}
|
{/* Logo */}
|
||||||
<Link href="/" className="flex items-center gap-2 hover:opacity-80 transition-opacity">
|
<Link href="/" className="flex items-center gap-2 hover:opacity-80 transition-opacity">
|
||||||
<Image src="/logohorse.png" alt="GoHorse Jobs" width={48} height={48} />
|
<Image src="/logohorse.png" alt="GoHorse Jobs" width={48} height={48} />
|
||||||
<span className="text-xl font-bold">
|
<span className="text-xl font-bold">
|
||||||
<span className="text-white">GoHorse </span>
|
<span className="text-white">GoHorse </span>
|
||||||
<span className="text-primary">Jobs</span>
|
<span className="text-primary">Jobs</span>
|
||||||
</span>
|
</span>
|
||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
{/* Desktop Navigation - moved to right side */}
|
{/* Desktop Navigation - moved to right side */}
|
||||||
<div className="hidden md:flex items-center gap-8 ml-auto mr-8">
|
<div className="hidden md:flex items-center gap-8 ml-auto mr-8">
|
||||||
{navigationItems.map((item) => (
|
{navigationItems.map((item) => (
|
||||||
<Link
|
<Link
|
||||||
key={item.href}
|
key={item.href}
|
||||||
href={item.href}
|
href={item.href}
|
||||||
className="text-sm font-medium text-white hover:text-primary transition-colors"
|
className="text-sm font-medium text-white hover:text-primary transition-colors"
|
||||||
>
|
>
|
||||||
{item.label}
|
{item.label}
|
||||||
</Link>
|
</Link>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Desktop Auth Buttons */}
|
{/* Desktop Auth Buttons */}
|
||||||
<div className="hidden md:flex items-center gap-3">
|
<div className="hidden md:flex items-center gap-3">
|
||||||
{/* LanguageSwitcher removed to match design */}
|
<LanguageSwitcher />
|
||||||
{user ? (
|
{user ? (
|
||||||
<Link href="/dashboard">
|
<Link href="/dashboard">
|
||||||
<Button variant="ghost" className="gap-2 text-white hover:text-primary transition-colors">
|
<Button variant="ghost" className="gap-2 text-white hover:text-primary transition-colors">
|
||||||
<User className="w-4 h-4" />
|
<User className="w-4 h-4" />
|
||||||
Dashboard
|
Dashboard
|
||||||
</Button>
|
</Button>
|
||||||
</Link>
|
</Link>
|
||||||
) : (
|
) : (
|
||||||
<div className="flex items-center gap-0 border border-primary rounded-md overflow-hidden bg-transparent">
|
<div className="flex items-center gap-0 border border-primary rounded-md overflow-hidden bg-transparent">
|
||||||
<Link href="/login">
|
<Link href="/login">
|
||||||
<Button variant="ghost" className="gap-2 text-primary hover:bg-primary/10 rounded-none border-r border-primary px-6 h-9 font-normal">
|
<Button variant="ghost" className="gap-2 text-primary hover:bg-primary/10 rounded-none border-r border-primary px-6 h-9 font-normal">
|
||||||
{t('footer.login')}
|
{t('footer.login')}
|
||||||
</Button>
|
</Button>
|
||||||
</Link>
|
</Link>
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
<DropdownMenuTrigger asChild>
|
<DropdownMenuTrigger asChild>
|
||||||
<Button variant="ghost" size="icon" className="text-primary hover:bg-primary/10 rounded-none px-3 h-9 w-9">
|
<Button variant="ghost" size="icon" className="text-primary hover:bg-primary/10 rounded-none px-3 h-9 w-9">
|
||||||
<Menu className="w-5 h-5" />
|
<Menu className="w-5 h-5" />
|
||||||
</Button>
|
</Button>
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent align="end" className="w-56">
|
<DropdownMenuContent align="end" className="w-56">
|
||||||
<DropdownMenuItem asChild>
|
<DropdownMenuItem asChild>
|
||||||
<Link href="/register/user" className="flex items-center gap-2 cursor-pointer">
|
<Link href="/register/user" className="flex items-center gap-2 cursor-pointer">
|
||||||
<UserPlus className="w-4 h-4" />
|
<UserPlus className="w-4 h-4" />
|
||||||
<span>Cadastrar Usuário</span>
|
<span>Cadastrar Usuário</span>
|
||||||
</Link>
|
</Link>
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
<DropdownMenuItem asChild>
|
<DropdownMenuItem asChild>
|
||||||
<Link href="/register" className="flex items-center gap-2 cursor-pointer">
|
<Link href="/register" className="flex items-center gap-2 cursor-pointer">
|
||||||
<Building2 className="w-4 h-4" />
|
<Building2 className="w-4 h-4" />
|
||||||
<span>Cadastrar Empresa</span>
|
<span>Cadastrar Empresa</span>
|
||||||
</Link>
|
</Link>
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
</DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Mobile Menu */}
|
{/* Mobile Menu */}
|
||||||
<Sheet open={isOpen} onOpenChange={setIsOpen}>
|
<Sheet open={isOpen} onOpenChange={setIsOpen}>
|
||||||
<SheetTrigger asChild className="md:hidden">
|
<SheetTrigger asChild className="md:hidden">
|
||||||
<Button variant="ghost" size="icon">
|
<Button variant="ghost" size="icon">
|
||||||
<Menu className="w-5 h-5" />
|
<Menu className="w-5 h-5" />
|
||||||
</Button>
|
</Button>
|
||||||
</SheetTrigger>
|
</SheetTrigger>
|
||||||
<SheetContent side="right" className="w-80">
|
<SheetContent side="right" className="w-80">
|
||||||
<div className="flex flex-col gap-4 mt-6">
|
<div className="flex flex-col gap-4 mt-6">
|
||||||
<div className="flex items-center gap-2 pb-4 border-b justify-center">
|
<div className="flex items-center gap-2 pb-4 border-b justify-center">
|
||||||
<Image src="/logohorse.png" alt="GoHorse Jobs" width={48} height={48} />
|
<Image src="/logohorse.png" alt="GoHorse Jobs" width={48} height={48} />
|
||||||
<span className="text-lg font-bold">GoHorse Jobs</span>
|
<span className="text-lg font-bold">GoHorse Jobs</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{navigationItems.map((item) => (
|
{navigationItems.map((item) => (
|
||||||
<Link
|
<Link
|
||||||
key={item.href}
|
key={item.href}
|
||||||
href={item.href}
|
href={item.href}
|
||||||
className="text-sm text-muted-foreground hover:text-foreground transition-colors p-2 rounded-lg hover:bg-muted"
|
className="text-sm text-muted-foreground hover:text-foreground transition-colors p-2 rounded-lg hover:bg-muted"
|
||||||
onClick={() => setIsOpen(false)}
|
onClick={() => setIsOpen(false)}
|
||||||
>
|
>
|
||||||
{item.label}
|
{item.label}
|
||||||
</Link>
|
</Link>
|
||||||
))}
|
))}
|
||||||
|
|
||||||
<div className="flex flex-col gap-2 mt-4 pt-4 border-t">
|
<div className="flex flex-col gap-2 mt-4 pt-4 border-t">
|
||||||
{user ? (
|
{user ? (
|
||||||
<Link href="/dashboard" onClick={() => setIsOpen(false)}>
|
<Link href="/dashboard" onClick={() => setIsOpen(false)}>
|
||||||
<Button variant="ghost" className="w-full justify-start gap-2">
|
<Button variant="ghost" className="w-full justify-start gap-2">
|
||||||
<User className="w-4 h-4" />
|
<User className="w-4 h-4" />
|
||||||
Dashboard
|
Dashboard
|
||||||
</Button>
|
</Button>
|
||||||
</Link>
|
</Link>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<Link href="/login" onClick={() => setIsOpen(false)}>
|
<Link href="/login" onClick={() => setIsOpen(false)}>
|
||||||
<Button variant="ghost" className="w-full justify-start gap-2">
|
<Button variant="ghost" className="w-full justify-start gap-2">
|
||||||
<LogIn className="w-4 h-4" />
|
<LogIn className="w-4 h-4" />
|
||||||
{t('nav.login')}
|
{t('nav.login')}
|
||||||
</Button>
|
</Button>
|
||||||
</Link>
|
</Link>
|
||||||
<Link href="/register" onClick={() => setIsOpen(false)}>
|
<Link href="/register" onClick={() => setIsOpen(false)}>
|
||||||
<Button className="w-full justify-start gap-2">
|
<Button className="w-full justify-start gap-2">
|
||||||
<User className="w-4 h-4" />
|
<User className="w-4 h-4" />
|
||||||
{t('nav.register')}
|
{t('nav.register')}
|
||||||
</Button>
|
</Button>
|
||||||
</Link>
|
</Link>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
<div className="mt-4 flex justify-center">
|
||||||
</div>
|
<LanguageSwitcher />
|
||||||
</SheetContent>
|
</div>
|
||||||
</Sheet>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</SheetContent>
|
||||||
</nav>
|
</Sheet>
|
||||||
)
|
</div>
|
||||||
}
|
</div>
|
||||||
|
</nav>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,115 +1,116 @@
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import React, { createContext, useContext, useState, useCallback, ReactNode, useEffect } from 'react';
|
import React, { createContext, useContext, useState, useCallback, ReactNode, useEffect } 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 && storedLocale in dictionaries) {
|
||||||
return storedLocale as Locale;
|
return storedLocale as Locale;
|
||||||
}
|
}
|
||||||
return normalizeLocale(navigator.language);
|
// Default to English instead of browser language for consistency
|
||||||
};
|
return 'en';
|
||||||
|
};
|
||||||
export function I18nProvider({ children }: { children: ReactNode }) {
|
|
||||||
const [locale, setLocaleState] = useState<Locale>(getInitialLocale);
|
export function I18nProvider({ children }: { children: ReactNode }) {
|
||||||
|
// FIX: Initialize with 'en' to match Server-Side Rendering (SSR)
|
||||||
const setLocale = (newLocale: Locale) => {
|
// This prevents hydration mismatch errors.
|
||||||
console.log(`[I18n] Setting locale to: ${newLocale}`);
|
const [locale, setLocaleState] = useState<Locale>('en');
|
||||||
setLocaleState(newLocale);
|
|
||||||
};
|
const setLocale = (newLocale: Locale) => {
|
||||||
|
console.log(`[I18n] Setting locale to: ${newLocale}`);
|
||||||
const t = useCallback((key: string, params?: Record<string, string | number>): string => {
|
setLocaleState(newLocale);
|
||||||
const keys = key.split('.');
|
};
|
||||||
let value: unknown = dictionaries[locale];
|
|
||||||
|
const t = useCallback((key: string, params?: Record<string, string | number>) => {
|
||||||
for (const k of keys) {
|
const keys = key.split('.');
|
||||||
if (value && typeof value === 'object' && value !== null && k in value) {
|
let value: unknown = dictionaries[locale];
|
||||||
value = (value as Record<string, unknown>)[k];
|
|
||||||
} else {
|
for (const k of keys) {
|
||||||
return key; // Return key if not found
|
if (value && typeof value === 'object' && value !== null && k in value) {
|
||||||
}
|
value = (value as Record<string, unknown>)[k];
|
||||||
}
|
} else {
|
||||||
|
return key; // Return key if not found
|
||||||
if (typeof value !== 'string') return key;
|
}
|
||||||
|
}
|
||||||
// Replace parameters like {count}, {time}, {year}
|
|
||||||
if (params) {
|
if (typeof value !== 'string') return key;
|
||||||
return Object.entries(params).reduce(
|
|
||||||
(str, [paramKey, paramValue]) => str.replace(`{${paramKey}}`, String(paramValue)),
|
// Replace parameters like {count}, {time}, {year}
|
||||||
value
|
if (params) {
|
||||||
);
|
return Object.entries(params).reduce(
|
||||||
}
|
(str, [paramKey, paramValue]) => str.replace(`{${paramKey}}`, String(paramValue)),
|
||||||
|
value
|
||||||
return value;
|
);
|
||||||
}, [locale]);
|
}
|
||||||
|
|
||||||
// Sync from localStorage on mount to handle hydration mismatch or initial load
|
return value;
|
||||||
useEffect(() => {
|
}, [locale]);
|
||||||
if (typeof window !== 'undefined') {
|
|
||||||
const stored = localStorage.getItem(localeStorageKey);
|
// Sync from localStorage on mount (Client-side only)
|
||||||
if (stored && stored in dictionaries && stored !== locale) {
|
useEffect(() => {
|
||||||
console.log('[I18n] Restoring locale from storage:', stored);
|
const stored = getInitialLocale();
|
||||||
setLocale(stored as Locale);
|
if (stored !== 'en') {
|
||||||
}
|
console.log('[I18n] Restoring locale from storage on client:', 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