Integrate backend jobs feed into frontend and improve seeder
This commit is contained in:
parent
7e40ba0476
commit
67c2ccdffe
3 changed files with 84 additions and 31 deletions
|
|
@ -64,18 +64,18 @@ func (s *JobService) CreateJob(req dto.CreateJobRequest) (*models.Job, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *JobService) GetJobs(filter dto.JobFilterQuery) ([]models.JobWithCompany, int, error) {
|
func (s *JobService) GetJobs(filter dto.JobFilterQuery) ([]models.JobWithCompany, int, error) {
|
||||||
baseQuery := `
|
baseQuery := `
|
||||||
SELECT
|
SELECT
|
||||||
j.id, j.company_id, j.title, j.description, j.salary_min, j.salary_max, j.salary_type,
|
j.id, j.company_id, j.title, j.description, j.salary_min, j.salary_max, j.salary_type,
|
||||||
j.employment_type, j.location, j.status, j.is_featured, j.created_at, j.updated_at,
|
j.employment_type, j.location, j.status, j.is_featured, j.created_at, j.updated_at,
|
||||||
c.name as company_name, c.logo_url as company_logo_url,
|
c.name as company_name, c.logo_url as company_logo_url,
|
||||||
p.name as region_name, ci.name as city_name
|
r.name as region_name, ci.name as city_name
|
||||||
FROM jobs j
|
FROM jobs j
|
||||||
LEFT JOIN companies c ON j.company_id = c.id
|
LEFT JOIN companies c ON j.company_id = c.id
|
||||||
LEFT JOIN prefectures p ON j.region_id = p.id
|
LEFT JOIN regions r ON j.region_id = r.id
|
||||||
LEFT JOIN cities ci ON j.city_id = ci.id
|
LEFT JOIN cities ci ON j.city_id = ci.id
|
||||||
WHERE 1=1`
|
WHERE 1=1`
|
||||||
countQuery := `SELECT COUNT(*) FROM jobs j WHERE 1=1`
|
countQuery := `SELECT COUNT(*) FROM jobs j WHERE 1=1`
|
||||||
|
|
||||||
var args []interface{}
|
var args []interface{}
|
||||||
argId := 1
|
argId := 1
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import { useState, useMemo, Suspense } from "react"
|
import { useEffect, useState, useMemo, Suspense } from "react"
|
||||||
import { Navbar } from "@/components/navbar"
|
import { Navbar } from "@/components/navbar"
|
||||||
import { Footer } from "@/components/footer"
|
import { Footer } from "@/components/footer"
|
||||||
import { JobCard } from "@/components/job-card"
|
import { JobCard } from "@/components/job-card"
|
||||||
|
|
@ -11,33 +11,72 @@ import { Badge } from "@/components/ui/badge"
|
||||||
import { Card, CardContent } from "@/components/ui/card"
|
import { Card, CardContent } from "@/components/ui/card"
|
||||||
import { PageSkeleton } from "@/components/loading-skeletons"
|
import { PageSkeleton } from "@/components/loading-skeletons"
|
||||||
import { mockJobs } from "@/lib/mock-data"
|
import { mockJobs } from "@/lib/mock-data"
|
||||||
|
import { jobsApi, transformApiJobToFrontend } from "@/lib/api"
|
||||||
import { useDebounce } from "@/hooks/use-utils"
|
import { useDebounce } from "@/hooks/use-utils"
|
||||||
import { Search, MapPin, Briefcase, SlidersHorizontal, X, ArrowUpDown } from "lucide-react"
|
import { Search, MapPin, Briefcase, SlidersHorizontal, X, ArrowUpDown } from "lucide-react"
|
||||||
import { motion, AnimatePresence } from "framer-motion"
|
import { motion, AnimatePresence } from "framer-motion"
|
||||||
|
import type { Job } from "@/lib/types"
|
||||||
|
|
||||||
function JobsContent() {
|
function JobsContent() {
|
||||||
|
const [jobs, setJobs] = useState<Job[]>([])
|
||||||
|
const [loading, setLoading] = useState(true)
|
||||||
|
const [error, setError] = useState<string | null>(null)
|
||||||
const [searchTerm, setSearchTerm] = useState("")
|
const [searchTerm, setSearchTerm] = useState("")
|
||||||
const [locationFilter, setLocationFilter] = useState("all")
|
const [locationFilter, setLocationFilter] = useState("all")
|
||||||
const [typeFilter, setTypeFilter] = useState("all")
|
const [typeFilter, setTypeFilter] = useState("all")
|
||||||
const [sortBy, setSortBy] = useState("recent")
|
const [sortBy, setSortBy] = useState("recent")
|
||||||
const [showFilters, setShowFilters] = useState(false)
|
const [showFilters, setShowFilters] = useState(false)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let isMounted = true
|
||||||
|
|
||||||
|
const fetchJobs = async () => {
|
||||||
|
setLoading(true)
|
||||||
|
setError(null)
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await jobsApi.list({ limit: 50, page: 1 })
|
||||||
|
const mappedJobs = response.data.map(transformApiJobToFrontend)
|
||||||
|
|
||||||
|
if (isMounted) {
|
||||||
|
setJobs(mappedJobs)
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Erro ao buscar vagas", err)
|
||||||
|
if (isMounted) {
|
||||||
|
setError("Não foi possível carregar as vagas agora. Exibindo exemplos.")
|
||||||
|
setJobs(mockJobs)
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
if (isMounted) {
|
||||||
|
setLoading(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fetchJobs()
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
isMounted = false
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
// Debounce search term para otimizar performance
|
// Debounce search term para otimizar performance
|
||||||
const debouncedSearchTerm = useDebounce(searchTerm, 300)
|
const debouncedSearchTerm = useDebounce(searchTerm, 300)
|
||||||
|
|
||||||
// Extrair valores únicos para os filtros
|
// Extrair valores únicos para os filtros
|
||||||
const uniqueLocations = useMemo(() => {
|
const uniqueLocations = useMemo(() => {
|
||||||
const locations = mockJobs.map(job => job.location)
|
const locations = jobs.map(job => job.location)
|
||||||
return Array.from(new Set(locations))
|
return Array.from(new Set(locations))
|
||||||
}, [])
|
}, [jobs])
|
||||||
|
|
||||||
const uniqueTypes = useMemo(() => {
|
const uniqueTypes = useMemo(() => {
|
||||||
const types = mockJobs.map(job => job.type)
|
const types = jobs.map(job => job.type)
|
||||||
return Array.from(new Set(types))
|
return Array.from(new Set(types))
|
||||||
}, [])
|
}, [jobs])
|
||||||
|
|
||||||
const filteredAndSortedJobs = useMemo(() => {
|
const filteredAndSortedJobs = useMemo(() => {
|
||||||
let filtered = mockJobs.filter((job) => {
|
let filtered = jobs.filter((job) => {
|
||||||
const matchesSearch =
|
const matchesSearch =
|
||||||
job.title.toLowerCase().includes(debouncedSearchTerm.toLowerCase()) ||
|
job.title.toLowerCase().includes(debouncedSearchTerm.toLowerCase()) ||
|
||||||
job.company.toLowerCase().includes(debouncedSearchTerm.toLowerCase()) ||
|
job.company.toLowerCase().includes(debouncedSearchTerm.toLowerCase()) ||
|
||||||
|
|
@ -67,7 +106,7 @@ function JobsContent() {
|
||||||
}
|
}
|
||||||
|
|
||||||
return filtered
|
return filtered
|
||||||
}, [debouncedSearchTerm, locationFilter, typeFilter, sortBy])
|
}, [debouncedSearchTerm, locationFilter, typeFilter, sortBy, jobs])
|
||||||
|
|
||||||
const hasActiveFilters = searchTerm || locationFilter !== "all" || typeFilter !== "all"
|
const hasActiveFilters = searchTerm || locationFilter !== "all" || typeFilter !== "all"
|
||||||
|
|
||||||
|
|
@ -96,7 +135,7 @@ function JobsContent() {
|
||||||
transition={{ delay: 0.1 }}
|
transition={{ delay: 0.1 }}
|
||||||
className="text-lg text-muted-foreground text-pretty"
|
className="text-lg text-muted-foreground text-pretty"
|
||||||
>
|
>
|
||||||
{mockJobs.length} vagas disponíveis nas melhores empresas
|
{loading ? "Carregando vagas..." : `${jobs.length} vagas disponíveis nas melhores empresas`}
|
||||||
</motion.p>
|
</motion.p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -252,11 +291,16 @@ function JobsContent() {
|
||||||
<section className="py-12">
|
<section className="py-12">
|
||||||
<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="max-w-5xl mx-auto">
|
<div className="max-w-5xl mx-auto">
|
||||||
{filteredAndSortedJobs.length > 0 ? (
|
{error && (
|
||||||
<motion.div
|
<div className="mb-6 p-4 bg-amber-50 text-amber-900 rounded-lg border border-amber-200">
|
||||||
layout
|
{error}
|
||||||
className="grid gap-6"
|
</div>
|
||||||
>
|
)}
|
||||||
|
|
||||||
|
{loading ? (
|
||||||
|
<div className="text-center text-muted-foreground">Carregando vagas...</div>
|
||||||
|
) : filteredAndSortedJobs.length > 0 ? (
|
||||||
|
<motion.div layout className="grid gap-6">
|
||||||
<AnimatePresence>
|
<AnimatePresence>
|
||||||
{filteredAndSortedJobs.map((job, index) => (
|
{filteredAndSortedJobs.map((job, index) => (
|
||||||
<motion.div
|
<motion.div
|
||||||
|
|
|
||||||
|
|
@ -5,14 +5,23 @@ dotenv.config();
|
||||||
|
|
||||||
const { Pool } = pg;
|
const { Pool } = pg;
|
||||||
|
|
||||||
|
const {
|
||||||
|
DB_HOST = 'localhost',
|
||||||
|
DB_PORT = '5432',
|
||||||
|
DB_USER = 'postgres',
|
||||||
|
DB_PASSWORD = 'postgres',
|
||||||
|
DB_NAME = 'gohorsejobs',
|
||||||
|
DB_SSLMODE = 'disable',
|
||||||
|
} = process.env;
|
||||||
|
|
||||||
// Database connection configuration
|
// Database connection configuration
|
||||||
export const pool = new Pool({
|
export const pool = new Pool({
|
||||||
host: process.env.DB_HOST,
|
host: DB_HOST,
|
||||||
port: process.env.DB_PORT,
|
port: Number(DB_PORT),
|
||||||
user: process.env.DB_USER,
|
user: DB_USER,
|
||||||
password: process.env.DB_PASSWORD,
|
password: DB_PASSWORD,
|
||||||
database: process.env.DB_NAME,
|
database: DB_NAME,
|
||||||
ssl: process.env.DB_SSLMODE === 'require' ? { rejectUnauthorized: false } : false,
|
ssl: DB_SSLMODE === 'require' ? { rejectUnauthorized: false } : false,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Test database connection
|
// Test database connection
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue