diff --git a/backend/internal/handlers/job_handler.go b/backend/internal/handlers/job_handler.go index cc88944..8f4f547 100755 --- a/backend/internal/handlers/job_handler.go +++ b/backend/internal/handlers/job_handler.go @@ -17,6 +17,18 @@ func NewJobHandler(service *services.JobService) *JobHandler { return &JobHandler{Service: service} } +// GetJobs godoc +// @Summary List all jobs +// @Description Get a paginated list of job postings with optional filters +// @Tags Jobs +// @Accept json +// @Produce json +// @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" +// @Success 200 {object} dto.PaginatedResponse +// @Failure 500 {string} string "Internal Server Error" +// @Router /jobs [get] 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")) @@ -51,6 +63,17 @@ func (h *JobHandler) GetJobs(w http.ResponseWriter, r *http.Request) { json.NewEncoder(w).Encode(response) } +// CreateJob godoc +// @Summary Create a new job +// @Description Create a new job posting +// @Tags Jobs +// @Accept json +// @Produce json +// @Param job body dto.CreateJobRequest true "Job data" +// @Success 201 {object} models.Job +// @Failure 400 {string} string "Bad Request" +// @Failure 500 {string} string "Internal Server Error" +// @Router /jobs [post] func (h *JobHandler) CreateJob(w http.ResponseWriter, r *http.Request) { var req dto.CreateJobRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { @@ -71,29 +94,19 @@ func (h *JobHandler) CreateJob(w http.ResponseWriter, r *http.Request) { json.NewEncoder(w).Encode(job) } +// GetJobByID godoc +// @Summary Get job by ID +// @Description Get a single job posting by its ID +// @Tags Jobs +// @Accept json +// @Produce json +// @Param id path int true "Job ID" +// @Success 200 {object} models.Job +// @Failure 400 {string} string "Bad Request" +// @Failure 404 {string} string "Not Found" +// @Router /jobs/{id} [get] func (h *JobHandler) GetJobByID(w http.ResponseWriter, r *http.Request) { idStr := r.PathValue("id") // Go 1.22+ routing - if idStr == "" { - // Fallback for older Go versions or if not using PathValue compatible mux - // But let's assume standard mux or we might need to parse URL - // For now, let's assume we can get it from context or URL - // If using standard http.ServeMux in Go 1.22, PathValue works. - // If not, we might need a helper. - // Let's assume standard mux with Go 1.22 for now as it's modern. - // If not, we'll fix it. - // Actually, let's check go.mod version. - } - - // Wait, I should check go.mod version to be safe. - // But let's write standard code. - - // Assuming we use a router that puts params in context or we parse URL. - // Since I am editing router.go later, I can ensure we use Go 1.22 patterns or a library. - // The existing router.go used `mux.HandleFunc("/jobs", ...)` which suggests standard lib. - - // Let's try to parse from URL path if PathValue is not available (it is in Go 1.22). - // I'll use a helper to extract ID from URL for now to be safe if I'm not sure about Go version. - // But `r.PathValue` is the way forward. id, err := strconv.Atoi(idStr) if err != nil { @@ -111,6 +124,18 @@ func (h *JobHandler) GetJobByID(w http.ResponseWriter, r *http.Request) { json.NewEncoder(w).Encode(job) } +// UpdateJob godoc +// @Summary Update a job +// @Description Update an existing job posting +// @Tags Jobs +// @Accept json +// @Produce json +// @Param id path int true "Job ID" +// @Param job body dto.UpdateJobRequest true "Updated job data" +// @Success 200 {object} models.Job +// @Failure 400 {string} string "Bad Request" +// @Failure 500 {string} string "Internal Server Error" +// @Router /jobs/{id} [put] func (h *JobHandler) UpdateJob(w http.ResponseWriter, r *http.Request) { idStr := r.PathValue("id") id, err := strconv.Atoi(idStr) @@ -135,6 +160,17 @@ func (h *JobHandler) UpdateJob(w http.ResponseWriter, r *http.Request) { json.NewEncoder(w).Encode(job) } +// DeleteJob godoc +// @Summary Delete a job +// @Description Delete a job posting +// @Tags Jobs +// @Accept json +// @Produce json +// @Param id path int true "Job ID" +// @Success 204 "No Content" +// @Failure 400 {string} string "Bad Request" +// @Failure 500 {string} string "Internal Server Error" +// @Router /jobs/{id} [delete] func (h *JobHandler) DeleteJob(w http.ResponseWriter, r *http.Request) { idStr := r.PathValue("id") id, err := strconv.Atoi(idStr) diff --git a/frontend/public/robots.txt b/frontend/public/robots.txt new file mode 100644 index 0000000..bea2daf --- /dev/null +++ b/frontend/public/robots.txt @@ -0,0 +1,15 @@ +# https://www.robotstxt.org/robotstxt.html +User-agent: * +Allow: / + +# Sitemap +Sitemap: https://gohorsejobs.com/sitemap.xml + +# Crawl-delay for polite bots +Crawl-delay: 1 + +# Disallow admin/dashboard areas from indexing +Disallow: /dashboard/ +Disallow: /api/ +Disallow: /login +Disallow: /cadastro/ diff --git a/frontend/public/sitemap.xml b/frontend/public/sitemap.xml new file mode 100644 index 0000000..709c392 --- /dev/null +++ b/frontend/public/sitemap.xml @@ -0,0 +1,82 @@ + + + + + https://gohorsejobs.com/ + 2025-12-14 + daily + 1.0 + + + https://gohorsejobs.com/vagas + 2025-12-14 + daily + 0.9 + + + https://gohorsejobs.com/sobre + 2025-12-14 + monthly + 0.6 + + + https://gohorsejobs.com/contato + 2025-12-14 + monthly + 0.6 + + + + + https://gohorsejobs.com/vagas?tech=python + 2025-12-14 + daily + 0.8 + + + https://gohorsejobs.com/vagas?tech=react + 2025-12-14 + daily + 0.8 + + + https://gohorsejobs.com/vagas?tech=nodejs + 2025-12-14 + daily + 0.8 + + + https://gohorsejobs.com/vagas?tech=dados + 2025-12-14 + daily + 0.8 + + + + + https://gohorsejobs.com/vagas?type=remoto + 2025-12-14 + daily + 0.8 + + + https://gohorsejobs.com/vagas?type=full-time + 2025-12-14 + daily + 0.7 + + + + + https://gohorsejobs.com/privacidade + 2025-12-14 + yearly + 0.3 + + + https://gohorsejobs.com/termos + 2025-12-14 + yearly + 0.3 + + diff --git a/frontend/src/components/footer.tsx b/frontend/src/components/footer.tsx index c1abe8b..0541e58 100644 --- a/frontend/src/components/footer.tsx +++ b/frontend/src/components/footer.tsx @@ -1,52 +1,76 @@ import Link from "next/link" -import { Briefcase } from "lucide-react" export function Footer() { return ( ) } + diff --git a/frontend/src/lib/api.ts b/frontend/src/lib/api.ts index 0da010e..8ef64ce 100644 --- a/frontend/src/lib/api.ts +++ b/frontend/src/lib/api.ts @@ -97,6 +97,94 @@ export const companiesApi = { }; // Jobs API (public) +export interface ApiJob { + id: number; + companyId: number; + createdBy: number; + title: string; + description: string; + salaryMin?: number; + salaryMax?: number; + salaryType?: string; + employmentType?: string; + workingHours?: string; + location?: string; + regionId?: number; + cityId?: number; + requirements?: Record | string[]; + benefits?: Record | string[]; + visaSupport: boolean; + languageLevel?: string; + status: string; + createdAt: string; + updatedAt: string; + companyName?: string; + companyLogoUrl?: string; + regionName?: string; + cityName?: string; +} + +export interface PaginatedResponse { + data: T[]; + pagination: { + page: number; + limit: number; + total: number; + }; +} + export const jobsApi = { - list: () => apiRequest("/jobs"), + list: (params?: { page?: number; limit?: number; companyId?: number }) => { + const query = new URLSearchParams(); + if (params?.page) query.set('page', String(params.page)); + if (params?.limit) query.set('limit', String(params.limit)); + if (params?.companyId) query.set('companyId', String(params.companyId)); + const queryStr = query.toString(); + return apiRequest>(`/jobs${queryStr ? `?${queryStr}` : ''}`); + }, + + getById: (id: number) => apiRequest(`/jobs/${id}`), }; + +// Transform API job to frontend Job format +export function transformApiJobToFrontend(apiJob: ApiJob): import('./types').Job { + // Format salary + let salary: string | undefined; + if (apiJob.salaryMin && apiJob.salaryMax) { + salary = `R$ ${apiJob.salaryMin.toLocaleString('pt-BR')} - R$ ${apiJob.salaryMax.toLocaleString('pt-BR')}`; + } else if (apiJob.salaryMin) { + salary = `A partir de R$ ${apiJob.salaryMin.toLocaleString('pt-BR')}`; + } else if (apiJob.salaryMax) { + salary = `Até R$ ${apiJob.salaryMax.toLocaleString('pt-BR')}`; + } + + // Determine type + type JobType = 'full-time' | 'part-time' | 'contract' | 'Remoto' | 'Tempo Integral'; + let type: JobType = 'Tempo Integral'; + if (apiJob.employmentType === 'full-time') type = 'Tempo Integral'; + else if (apiJob.employmentType === 'part-time') type = 'part-time'; + else if (apiJob.employmentType === 'contract') type = 'contract'; + else if (apiJob.location?.toLowerCase().includes('remoto')) type = 'Remoto'; + + // Extract requirements + const requirements: string[] = []; + if (apiJob.requirements) { + if (Array.isArray(apiJob.requirements)) { + requirements.push(...apiJob.requirements.map(String)); + } else if (typeof apiJob.requirements === 'object') { + Object.values(apiJob.requirements).forEach(v => requirements.push(String(v))); + } + } + + return { + id: String(apiJob.id), + title: apiJob.title, + company: apiJob.companyName || 'Empresa', + location: apiJob.location || apiJob.cityName || 'Localização não informada', + type, + salary, + description: apiJob.description, + requirements: requirements.length > 0 ? requirements : ['Ver detalhes'], + postedAt: apiJob.createdAt?.split('T')[0] || new Date().toISOString().split('T')[0], + }; +} diff --git a/JobScraper_MultiSite/README.md b/job-scraper-multisite/README.md similarity index 100% rename from JobScraper_MultiSite/README.md rename to job-scraper-multisite/README.md diff --git a/JobScraper_MultiSite/main_scraper.py b/job-scraper-multisite/main_scraper.py similarity index 100% rename from JobScraper_MultiSite/main_scraper.py rename to job-scraper-multisite/main_scraper.py diff --git a/JobScraper_MultiSite/output/.gitkeep b/job-scraper-multisite/output/.gitkeep similarity index 100% rename from JobScraper_MultiSite/output/.gitkeep rename to job-scraper-multisite/output/.gitkeep diff --git a/JobScraper_MultiSite/requirements.txt b/job-scraper-multisite/requirements.txt similarity index 100% rename from JobScraper_MultiSite/requirements.txt rename to job-scraper-multisite/requirements.txt diff --git a/JobScraper_MultiSite/scrapers/__init__.py b/job-scraper-multisite/scrapers/__init__.py similarity index 100% rename from JobScraper_MultiSite/scrapers/__init__.py rename to job-scraper-multisite/scrapers/__init__.py diff --git a/JobScraper_MultiSite/scrapers/geekhunter_scraper.py b/job-scraper-multisite/scrapers/geekhunter_scraper.py similarity index 100% rename from JobScraper_MultiSite/scrapers/geekhunter_scraper.py rename to job-scraper-multisite/scrapers/geekhunter_scraper.py diff --git a/JobScraper_MultiSite/scrapers/programathor_scraper.py b/job-scraper-multisite/scrapers/programathor_scraper.py similarity index 100% rename from JobScraper_MultiSite/scrapers/programathor_scraper.py rename to job-scraper-multisite/scrapers/programathor_scraper.py