gohorsejobs/backend/internal/api/handlers/location_handlers.go
Tiago Yamamoto fb79e987bb feat: add location selector and contract types
Backend:
- Created LocationHandler, LocationService, LocationRepository
- Added endpoints: GET /api/v1/locations/countries, states, cities, search
- Added migration 029_expand_employment_types.sql with new contract types (permanent, training, temporary, voluntary)
- Fixed .gitignore to allow internal/api folder

Frontend:
- Created LocationPicker component with country dropdown and city/state autocomplete search
- Integrated LocationPicker into PostJobPage
- Updated contract type options in job form (Permanent, Contract, Training, Temporary, Voluntary)
- Added locationsApi with search functionality to api.ts
2025-12-26 15:18:16 -03:00

113 lines
3 KiB
Go

package handlers
import (
"encoding/json"
"net/http"
"strconv"
"github.com/rede5/gohorsejobs/backend/internal/services"
)
type LocationHandlers struct {
service *services.LocationService
}
func NewLocationHandlers(service *services.LocationService) *LocationHandlers {
return &LocationHandlers{service: service}
}
// ListCountries returns all countries
func (h *LocationHandlers) ListCountries(w http.ResponseWriter, r *http.Request) {
countries, err := h.service.ListCountries(r.Context())
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(countries)
}
// ListStatesByCountry returns states for a given country ID
// Expects path param {id}
func (h *LocationHandlers) ListStatesByCountry(w http.ResponseWriter, r *http.Request) {
idStr := r.PathValue("id")
if idStr == "" {
http.Error(w, "Country ID is required", http.StatusBadRequest)
return
}
countryID, err := strconv.ParseInt(idStr, 10, 64)
if err != nil {
http.Error(w, "Invalid Country ID", http.StatusBadRequest)
return
}
states, err := h.service.ListStates(r.Context(), countryID)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(states)
}
// ListCitiesByState returns cities for a given state ID
// Expects path param {id}
func (h *LocationHandlers) ListCitiesByState(w http.ResponseWriter, r *http.Request) {
idStr := r.PathValue("id")
if idStr == "" {
http.Error(w, "State ID is required", http.StatusBadRequest)
return
}
stateID, err := strconv.ParseInt(idStr, 10, 64)
if err != nil {
http.Error(w, "Invalid State ID", http.StatusBadRequest)
return
}
cities, err := h.service.ListCities(r.Context(), stateID)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(cities)
}
// SearchLocations searches for cities or states in a country
// Query params: q (query), country_id (required)
func (h *LocationHandlers) SearchLocations(w http.ResponseWriter, r *http.Request) {
q := r.URL.Query().Get("q")
countryIDStr := r.URL.Query().Get("country_id")
if q == "" || len(q) < 2 {
// Return empty list if query too short to avoid massive scan (though DB handles it, better UX)
w.Header().Set("Content-Type", "application/json")
w.Write([]byte("[]"))
return
}
if countryIDStr == "" {
http.Error(w, "country_id is required", http.StatusBadRequest)
return
}
countryID, err := strconv.ParseInt(countryIDStr, 10, 64)
if err != nil {
http.Error(w, "Invalid country_id", http.StatusBadRequest)
return
}
results, err := h.service.Search(r.Context(), q, countryID)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(results)
}