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:
commit
24c6f33ae5
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) {
|
||||
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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in a new issue