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
113 lines
3 KiB
Go
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)
|
|
}
|