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) {
baseQuery := `
SELECT
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,
c.name as company_name, c.logo_url as company_logo_url,
p.name as region_name, ci.name as city_name
FROM jobs j
LEFT JOIN companies c ON j.company_id = c.id
LEFT JOIN prefectures p ON j.region_id = p.id
LEFT JOIN cities ci ON j.city_id = ci.id
WHERE 1=1`
countQuery := `SELECT COUNT(*) FROM jobs j WHERE 1=1`
baseQuery := `
SELECT
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,
c.name as company_name, c.logo_url as company_logo_url,
r.name as region_name, ci.name as city_name
FROM jobs j
LEFT JOIN companies c ON j.company_id = c.id
LEFT JOIN regions r ON j.region_id = r.id
LEFT JOIN cities ci ON j.city_id = ci.id
WHERE 1=1`
countQuery := `SELECT COUNT(*) FROM jobs j WHERE 1=1`
var args []interface{}
argId := 1

View file

@ -1,6 +1,6 @@
"use client"
import { useState, useMemo, Suspense } from "react"
import { useEffect, useState, useMemo, Suspense } from "react"
import { Navbar } from "@/components/navbar"
import { Footer } from "@/components/footer"
import { JobCard } from "@/components/job-card"
@ -11,33 +11,72 @@ import { Badge } from "@/components/ui/badge"
import { Card, CardContent } from "@/components/ui/card"
import { PageSkeleton } from "@/components/loading-skeletons"
import { mockJobs } from "@/lib/mock-data"
import { jobsApi, transformApiJobToFrontend } from "@/lib/api"
import { useDebounce } from "@/hooks/use-utils"
import { Search, MapPin, Briefcase, SlidersHorizontal, X, ArrowUpDown } from "lucide-react"
import { motion, AnimatePresence } from "framer-motion"
import type { Job } from "@/lib/types"
function JobsContent() {
const [jobs, setJobs] = useState<Job[]>([])
const [loading, setLoading] = useState(true)
const [error, setError] = useState<string | null>(null)
const [searchTerm, setSearchTerm] = useState("")
const [locationFilter, setLocationFilter] = useState("all")
const [typeFilter, setTypeFilter] = useState("all")
const [sortBy, setSortBy] = useState("recent")
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
const debouncedSearchTerm = useDebounce(searchTerm, 300)
// Extrair valores únicos para os filtros
const uniqueLocations = useMemo(() => {
const locations = mockJobs.map(job => job.location)
const locations = jobs.map(job => job.location)
return Array.from(new Set(locations))
}, [])
}, [jobs])
const uniqueTypes = useMemo(() => {
const types = mockJobs.map(job => job.type)
const types = jobs.map(job => job.type)
return Array.from(new Set(types))
}, [])
}, [jobs])
const filteredAndSortedJobs = useMemo(() => {
let filtered = mockJobs.filter((job) => {
let filtered = jobs.filter((job) => {
const matchesSearch =
job.title.toLowerCase().includes(debouncedSearchTerm.toLowerCase()) ||
job.company.toLowerCase().includes(debouncedSearchTerm.toLowerCase()) ||
@ -67,7 +106,7 @@ function JobsContent() {
}
return filtered
}, [debouncedSearchTerm, locationFilter, typeFilter, sortBy])
}, [debouncedSearchTerm, locationFilter, typeFilter, sortBy, jobs])
const hasActiveFilters = searchTerm || locationFilter !== "all" || typeFilter !== "all"
@ -96,7 +135,7 @@ function JobsContent() {
transition={{ delay: 0.1 }}
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>
</div>
</div>
@ -252,11 +291,16 @@ function JobsContent() {
<section className="py-12">
<div className="container mx-auto px-4 sm:px-6 lg:px-8">
<div className="max-w-5xl mx-auto">
{filteredAndSortedJobs.length > 0 ? (
<motion.div
layout
className="grid gap-6"
>
{error && (
<div className="mb-6 p-4 bg-amber-50 text-amber-900 rounded-lg border border-amber-200">
{error}
</div>
)}
{loading ? (
<div className="text-center text-muted-foreground">Carregando vagas...</div>
) : filteredAndSortedJobs.length > 0 ? (
<motion.div layout className="grid gap-6">
<AnimatePresence>
{filteredAndSortedJobs.map((job, index) => (
<motion.div

View file

@ -5,14 +5,23 @@ dotenv.config();
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
export const pool = new Pool({
host: process.env.DB_HOST,
port: process.env.DB_PORT,
user: process.env.DB_USER,
password: process.env.DB_PASSWORD,
database: process.env.DB_NAME,
ssl: process.env.DB_SSLMODE === 'require' ? { rejectUnauthorized: false } : false,
host: DB_HOST,
port: Number(DB_PORT),
user: DB_USER,
password: DB_PASSWORD,
database: DB_NAME,
ssl: DB_SSLMODE === 'require' ? { rejectUnauthorized: false } : false,
});
// Test database connection