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"`
|
CityID *int `form:"cityId"`
|
||||||
EmploymentType *string `form:"employmentType"`
|
EmploymentType *string `form:"employmentType"`
|
||||||
Status *string `form:"status"`
|
Status *string `form:"status"`
|
||||||
|
IsFeatured *bool `form:"isFeatured"` // Filter by featured status
|
||||||
VisaSupport *bool `form:"visaSupport"`
|
VisaSupport *bool `form:"visaSupport"`
|
||||||
LanguageLevel *string `form:"languageLevel"`
|
LanguageLevel *string `form:"languageLevel"`
|
||||||
Search *string `form:"search"`
|
Search *string `form:"search"`
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,7 @@ func NewJobHandler(service *services.JobService) *JobHandler {
|
||||||
// @Param page query int false "Page number (default: 1)"
|
// @Param page query int false "Page number (default: 1)"
|
||||||
// @Param limit query int false "Items per page (default: 10, max: 100)"
|
// @Param limit query int false "Items per page (default: 10, max: 100)"
|
||||||
// @Param companyId query int false "Filter by company ID"
|
// @Param companyId query int false "Filter by company ID"
|
||||||
|
// @Param featured query bool false "Filter by featured status"
|
||||||
// @Success 200 {object} dto.PaginatedResponse
|
// @Success 200 {object} dto.PaginatedResponse
|
||||||
// @Failure 500 {string} string "Internal Server Error"
|
// @Failure 500 {string} string "Internal Server Error"
|
||||||
// @Router /jobs [get]
|
// @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"))
|
page, _ := strconv.Atoi(r.URL.Query().Get("page"))
|
||||||
limit, _ := strconv.Atoi(r.URL.Query().Get("limit"))
|
limit, _ := strconv.Atoi(r.URL.Query().Get("limit"))
|
||||||
companyID, _ := strconv.Atoi(r.URL.Query().Get("companyId"))
|
companyID, _ := strconv.Atoi(r.URL.Query().Get("companyId"))
|
||||||
|
isFeaturedStr := r.URL.Query().Get("featured")
|
||||||
|
|
||||||
filter := dto.JobFilterQuery{
|
filter := dto.JobFilterQuery{
|
||||||
PaginationQuery: dto.PaginationQuery{
|
PaginationQuery: dto.PaginationQuery{
|
||||||
|
|
@ -43,6 +45,10 @@ func (h *JobHandler) GetJobs(w http.ResponseWriter, r *http.Request) {
|
||||||
if companyID > 0 {
|
if companyID > 0 {
|
||||||
filter.CompanyID = &companyID
|
filter.CompanyID = &companyID
|
||||||
}
|
}
|
||||||
|
if isFeaturedStr == "true" {
|
||||||
|
val := true
|
||||||
|
filter.IsFeatured = &val
|
||||||
|
}
|
||||||
|
|
||||||
jobs, total, err := h.Service.GetJobs(filter)
|
jobs, total, err := h.Service.GetJobs(filter)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
||||||
|
|
@ -35,7 +35,8 @@ type Job struct {
|
||||||
LanguageLevel *string `json:"languageLevel,omitempty" db:"language_level"` // N5-N1, beginner, none
|
LanguageLevel *string `json:"languageLevel,omitempty" db:"language_level"` // N5-N1, beginner, none
|
||||||
|
|
||||||
// Status
|
// Status
|
||||||
Status string `json:"status" db:"status"` // open, closed, draft
|
Status string `json:"status" db:"status"` // open, closed, draft
|
||||||
|
IsFeatured bool `json:"isFeatured" db:"is_featured"` // Featured job flag
|
||||||
|
|
||||||
// Metadata
|
// Metadata
|
||||||
CreatedAt time.Time `json:"createdAt" db:"created_at"`
|
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
|
return job, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *JobService) GetJobs(filter dto.JobFilterQuery) ([]models.Job, int, error) {
|
func (s *JobService) GetJobs(filter dto.JobFilterQuery) ([]models.JobWithCompany, 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`
|
baseQuery := `
|
||||||
countQuery := `SELECT COUNT(*) FROM jobs WHERE 1=1`
|
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{}
|
var args []interface{}
|
||||||
argId := 1
|
argId := 1
|
||||||
|
|
||||||
if filter.CompanyID != nil {
|
if filter.CompanyID != nil {
|
||||||
baseQuery += fmt.Sprintf(" AND company_id = $%d", argId)
|
baseQuery += fmt.Sprintf(" AND j.company_id = $%d", argId)
|
||||||
countQuery += fmt.Sprintf(" AND company_id = $%d", argId)
|
countQuery += fmt.Sprintf(" AND j.company_id = $%d", argId)
|
||||||
args = append(args, *filter.CompanyID)
|
args = append(args, *filter.CompanyID)
|
||||||
argId++
|
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...
|
// Add more filters as needed...
|
||||||
|
|
||||||
// Pagination
|
// Pagination
|
||||||
|
|
@ -98,10 +115,14 @@ func (s *JobService) GetJobs(filter dto.JobFilterQuery) ([]models.Job, int, erro
|
||||||
}
|
}
|
||||||
defer rows.Close()
|
defer rows.Close()
|
||||||
|
|
||||||
var jobs []models.Job
|
var jobs []models.JobWithCompany
|
||||||
for rows.Next() {
|
for rows.Next() {
|
||||||
var j models.Job
|
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.CreatedAt, &j.UpdatedAt); err != nil {
|
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
|
return nil, 0, err
|
||||||
}
|
}
|
||||||
jobs = append(jobs, j)
|
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 { motion } from "framer-motion"
|
||||||
import Image from "next/image"
|
import Image from "next/image"
|
||||||
|
|
||||||
|
import { useState, useEffect } from "react"
|
||||||
|
import type { Job } from "@/lib/types"
|
||||||
|
|
||||||
export default function HomePage() {
|
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 (
|
return (
|
||||||
<div className="min-h-screen flex flex-col py-2.5">
|
<div className="min-h-screen flex flex-col py-2.5">
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ export interface Job {
|
||||||
description: string;
|
description: string;
|
||||||
requirements: string[];
|
requirements: string[];
|
||||||
postedAt: string;
|
postedAt: string;
|
||||||
|
isFeatured?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Application {
|
export interface Application {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue