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:
Tiago Yamamoto 2025-12-14 15:43:43 -03:00
parent 361d36dc38
commit 9b4601f1d8
7 changed files with 85 additions and 10 deletions

View file

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

View file

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

View file

@ -35,7 +35,8 @@ type Job struct {
LanguageLevel *string `json:"languageLevel,omitempty" db:"language_level"` // N5-N1, beginner, none
// 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
CreatedAt time.Time `json:"createdAt" db:"created_at"`

View file

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

View 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);

View file

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

View file

@ -8,6 +8,7 @@ export interface Job {
description: string;
requirements: string[];
postedAt: string;
isFeatured?: boolean;
}
export interface Application {