Merge pull request #6 from rede5/codex/implement-backend-functionality-in-frontend

Integrate backend jobs feed into frontend and improve seeder
This commit is contained in:
Tiago Yamamoto 2025-12-14 20:32:35 -03:00 committed by GitHub
commit 24c6f33ae5
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 84 additions and 31 deletions

View file

@ -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

View file

@ -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

View file

@ -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