feat: implement dynamic featured jobs
- Add is_featured column to jobs table (migration) - Update Job model and Service to support featured jobs - Update JobHandler to expose featured jobs API - Support filtering by featured status in GET /jobs - Frontend: Fetch and display featured jobs from API - Frontend: Update Job type definition
This commit is contained in:
parent
361d36dc38
commit
9b4601f1d8
7 changed files with 85 additions and 10 deletions
|
|
@ -115,6 +115,7 @@ type JobFilterQuery struct {
|
|||
CityID *int `form:"cityId"`
|
||||
EmploymentType *string `form:"employmentType"`
|
||||
Status *string `form:"status"`
|
||||
IsFeatured *bool `form:"isFeatured"` // Filter by featured status
|
||||
VisaSupport *bool `form:"visaSupport"`
|
||||
LanguageLevel *string `form:"languageLevel"`
|
||||
Search *string `form:"search"`
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ func NewJobHandler(service *services.JobService) *JobHandler {
|
|||
// @Param page query int false "Page number (default: 1)"
|
||||
// @Param limit query int false "Items per page (default: 10, max: 100)"
|
||||
// @Param companyId query int false "Filter by company ID"
|
||||
// @Param featured query bool false "Filter by featured status"
|
||||
// @Success 200 {object} dto.PaginatedResponse
|
||||
// @Failure 500 {string} string "Internal Server Error"
|
||||
// @Router /jobs [get]
|
||||
|
|
@ -33,6 +34,7 @@ func (h *JobHandler) GetJobs(w http.ResponseWriter, r *http.Request) {
|
|||
page, _ := strconv.Atoi(r.URL.Query().Get("page"))
|
||||
limit, _ := strconv.Atoi(r.URL.Query().Get("limit"))
|
||||
companyID, _ := strconv.Atoi(r.URL.Query().Get("companyId"))
|
||||
isFeaturedStr := r.URL.Query().Get("featured")
|
||||
|
||||
filter := dto.JobFilterQuery{
|
||||
PaginationQuery: dto.PaginationQuery{
|
||||
|
|
@ -43,6 +45,10 @@ func (h *JobHandler) GetJobs(w http.ResponseWriter, r *http.Request) {
|
|||
if companyID > 0 {
|
||||
filter.CompanyID = &companyID
|
||||
}
|
||||
if isFeaturedStr == "true" {
|
||||
val := true
|
||||
filter.IsFeatured = &val
|
||||
}
|
||||
|
||||
jobs, total, err := h.Service.GetJobs(filter)
|
||||
if err != nil {
|
||||
|
|
|
|||
|
|
@ -36,6 +36,7 @@ type Job struct {
|
|||
|
||||
// Status
|
||||
Status string `json:"status" db:"status"` // open, closed, draft
|
||||
IsFeatured bool `json:"isFeatured" db:"is_featured"` // Featured job flag
|
||||
|
||||
// Metadata
|
||||
CreatedAt time.Time `json:"createdAt" db:"created_at"`
|
||||
|
|
|
|||
|
|
@ -63,20 +63,37 @@ func (s *JobService) CreateJob(req dto.CreateJobRequest) (*models.Job, error) {
|
|||
return job, nil
|
||||
}
|
||||
|
||||
func (s *JobService) GetJobs(filter dto.JobFilterQuery) ([]models.Job, int, error) {
|
||||
baseQuery := `SELECT id, company_id, title, description, salary_min, salary_max, salary_type, employment_type, location, status, created_at, updated_at FROM jobs WHERE 1=1`
|
||||
countQuery := `SELECT COUNT(*) FROM jobs WHERE 1=1`
|
||||
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`
|
||||
|
||||
var args []interface{}
|
||||
argId := 1
|
||||
|
||||
if filter.CompanyID != nil {
|
||||
baseQuery += fmt.Sprintf(" AND company_id = $%d", argId)
|
||||
countQuery += fmt.Sprintf(" AND company_id = $%d", argId)
|
||||
baseQuery += fmt.Sprintf(" AND j.company_id = $%d", argId)
|
||||
countQuery += fmt.Sprintf(" AND j.company_id = $%d", argId)
|
||||
args = append(args, *filter.CompanyID)
|
||||
argId++
|
||||
}
|
||||
|
||||
if filter.IsFeatured != nil {
|
||||
baseQuery += fmt.Sprintf(" AND j.is_featured = $%d", argId)
|
||||
countQuery += fmt.Sprintf(" AND j.is_featured = $%d", argId)
|
||||
args = append(args, *filter.IsFeatured)
|
||||
argId++
|
||||
}
|
||||
|
||||
// Add more filters as needed...
|
||||
|
||||
// Pagination
|
||||
|
|
@ -98,10 +115,14 @@ func (s *JobService) GetJobs(filter dto.JobFilterQuery) ([]models.Job, int, erro
|
|||
}
|
||||
defer rows.Close()
|
||||
|
||||
var jobs []models.Job
|
||||
var jobs []models.JobWithCompany
|
||||
for rows.Next() {
|
||||
var j models.Job
|
||||
if err := rows.Scan(&j.ID, &j.CompanyID, &j.Title, &j.Description, &j.SalaryMin, &j.SalaryMax, &j.SalaryType, &j.EmploymentType, &j.Location, &j.Status, &j.CreatedAt, &j.UpdatedAt); err != nil {
|
||||
var j models.JobWithCompany
|
||||
if err := rows.Scan(
|
||||
&j.ID, &j.CompanyID, &j.Title, &j.Description, &j.SalaryMin, &j.SalaryMax, &j.SalaryType,
|
||||
&j.EmploymentType, &j.Location, &j.Status, &j.IsFeatured, &j.CreatedAt, &j.UpdatedAt,
|
||||
&j.CompanyName, &j.CompanyLogoURL, &j.RegionName, &j.CityName,
|
||||
); err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
jobs = append(jobs, j)
|
||||
|
|
|
|||
5
backend/migrations/011_add_is_featured_to_jobs.sql
Normal file
5
backend/migrations/011_add_is_featured_to_jobs.sql
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
-- Add is_featured column to jobs table
|
||||
ALTER TABLE jobs ADD COLUMN IF NOT EXISTS is_featured BOOLEAN DEFAULT FALSE;
|
||||
|
||||
-- Create an index to optimize filtering by featured status
|
||||
CREATE INDEX IF NOT EXISTS idx_jobs_is_featured ON jobs(is_featured);
|
||||
|
|
@ -12,8 +12,48 @@ import Link from "next/link"
|
|||
import { motion } from "framer-motion"
|
||||
import Image from "next/image"
|
||||
|
||||
import { useState, useEffect } from "react"
|
||||
import type { Job } from "@/lib/types"
|
||||
|
||||
export default function HomePage() {
|
||||
const featuredJobs = mockJobs.slice(0, 6)
|
||||
const [featuredJobs, setFeaturedJobs] = useState<Job[]>([])
|
||||
const [loading, setLoading] = useState(true)
|
||||
|
||||
useEffect(() => {
|
||||
async function fetchFeaturedJobs() {
|
||||
try {
|
||||
// Assuming API proxy is set up or env var is available. using relative path for now as per usual Next.js setup
|
||||
const res = await fetch(`${process.env.NEXT_PUBLIC_API_URL || 'http://localhost:8080'}/jobs?featured=true&limit=6`)
|
||||
if (!res.ok) throw new Error('Failed to fetch jobs')
|
||||
const data = await res.json()
|
||||
|
||||
// Backend returns: { data: JobWithCompany[], pagination: ... }
|
||||
if (data.data) {
|
||||
const mappedJobs: Job[] = data.data.map((j: any) => ({
|
||||
id: String(j.id),
|
||||
title: j.title,
|
||||
company: j.companyName || 'Empresa Confidencial',
|
||||
location: j.location || 'Remoto',
|
||||
type: j.employmentType || 'full-time',
|
||||
salary: j.salaryMin ? `R$ ${j.salaryMin}` : 'A combinar',
|
||||
description: j.description,
|
||||
requirements: j.requirements || [],
|
||||
postedAt: j.createdAt,
|
||||
isFeatured: j.isFeatured
|
||||
}))
|
||||
setFeaturedJobs(mappedJobs)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error fetching featured jobs:", error)
|
||||
// Fallback to mock data if API fails? Or just empty.
|
||||
// For MVP let's leave empty or maybe keep mock as fallback if needed.
|
||||
// setFeaturedJobs(mockJobs.slice(0, 6))
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
fetchFeaturedJobs()
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div className="min-h-screen flex flex-col py-2.5">
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ export interface Job {
|
|||
description: string;
|
||||
requirements: string[];
|
||||
postedAt: string;
|
||||
isFeatured?: boolean;
|
||||
}
|
||||
|
||||
export interface Application {
|
||||
|
|
|
|||
Loading…
Reference in a new issue