From 9b4601f1d85da2fc56e10f4f0073d7473814dd1e Mon Sep 17 00:00:00 2001 From: Tiago Yamamoto Date: Sun, 14 Dec 2025 15:43:43 -0300 Subject: [PATCH] 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 --- backend/internal/dto/requests.go | 1 + backend/internal/handlers/job_handler.go | 6 +++ backend/internal/models/job.go | 3 +- backend/internal/services/job_service.go | 37 ++++++++++++---- .../011_add_is_featured_to_jobs.sql | 5 +++ frontend/src/app/page.tsx | 42 ++++++++++++++++++- frontend/src/lib/types.ts | 1 + 7 files changed, 85 insertions(+), 10 deletions(-) create mode 100644 backend/migrations/011_add_is_featured_to_jobs.sql diff --git a/backend/internal/dto/requests.go b/backend/internal/dto/requests.go index 5b4fa63..7847bc1 100755 --- a/backend/internal/dto/requests.go +++ b/backend/internal/dto/requests.go @@ -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"` diff --git a/backend/internal/handlers/job_handler.go b/backend/internal/handlers/job_handler.go index 8f4f547..39cd429 100755 --- a/backend/internal/handlers/job_handler.go +++ b/backend/internal/handlers/job_handler.go @@ -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 { diff --git a/backend/internal/models/job.go b/backend/internal/models/job.go index 341e03c..ebc3139 100755 --- a/backend/internal/models/job.go +++ b/backend/internal/models/job.go @@ -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"` diff --git a/backend/internal/services/job_service.go b/backend/internal/services/job_service.go index 7600307..08e3dc5 100644 --- a/backend/internal/services/job_service.go +++ b/backend/internal/services/job_service.go @@ -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) diff --git a/backend/migrations/011_add_is_featured_to_jobs.sql b/backend/migrations/011_add_is_featured_to_jobs.sql new file mode 100644 index 0000000..484dded --- /dev/null +++ b/backend/migrations/011_add_is_featured_to_jobs.sql @@ -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); diff --git a/frontend/src/app/page.tsx b/frontend/src/app/page.tsx index 5ce90a8..5ac65b4 100644 --- a/frontend/src/app/page.tsx +++ b/frontend/src/app/page.tsx @@ -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([]) + 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 (
diff --git a/frontend/src/lib/types.ts b/frontend/src/lib/types.ts index 4bd8545..02d4042 100644 --- a/frontend/src/lib/types.ts +++ b/frontend/src/lib/types.ts @@ -8,6 +8,7 @@ export interface Job { description: string; requirements: string[]; postedAt: string; + isFeatured?: boolean; } export interface Application {