From a53b200c21e14917ae72150093bf86ef30ee9096 Mon Sep 17 00:00:00 2001 From: Abdussamet Kocak Date: Sat, 21 Feb 2026 00:49:34 +0300 Subject: [PATCH] feat: Add location search --- internal/api/api.go | 34 +++++- pkg/citydb/provider.go | 54 ++++++++- templates/static/index.html | 45 ++++++- templates/static/main.js | 228 +++++++++++++++++++++--------------- templates/static/style.css | 71 ++++++++++- 5 files changed, 327 insertions(+), 105 deletions(-) diff --git a/internal/api/api.go b/internal/api/api.go index c21c24a..8e2bbb4 100644 --- a/internal/api/api.go +++ b/internal/api/api.go @@ -36,6 +36,7 @@ type PrayerProvider interface { type LocationProvider interface { SearchLocations(ctx context.Context, query string) ([]prayer.Location, error) + SearchLocationsByCoords(ctx context.Context, coords prayer.Coordinates) ([]prayer.Location, error) } func New(services Services) *fiber.App { @@ -117,18 +118,43 @@ func New(services Services) *fiber.App { app.Get("/api/v1/diyanet/location", func(ctx fiber.Ctx) error { var query struct { - Text string `query:"query"` + Text string `query:"query"` + Latitude string `query:"latitude"` + Longitude string `query:"longitude"` } if err := ctx.Bind().Query(&query); err != nil { return fmt.Errorf("failed to bind location query parameters: %w", errors.Join(fiber.ErrBadRequest, err)) } query.Text = strings.TrimSpace(query.Text) - if query.Text == "" { + query.Latitude = strings.TrimSpace(query.Latitude) + query.Longitude = strings.TrimSpace(query.Longitude) + + var ( + locations []prayer.Location + err error + ) + + switch { + case query.Text != "" && query.Latitude == "" && query.Longitude == "": + locations, err = services.LocationProvider.SearchLocations(ctx.Context(), query.Text) + case query.Text == "" && query.Latitude != "" && query.Longitude != "": + lat, latErr := strconv.ParseFloat(query.Latitude, 64) + if latErr != nil { + return fmt.Errorf("failed to parse latitude query parameter: %w", errors.Join(fiber.ErrBadRequest, latErr)) + } + lng, lngErr := strconv.ParseFloat(query.Longitude, 64) + if lngErr != nil { + return fmt.Errorf("failed to parse longitude query parameter: %w", errors.Join(fiber.ErrBadRequest, lngErr)) + } + + locations, err = services.LocationProvider.SearchLocationsByCoords(ctx.Context(), prayer.Coordinates{ + Latitude: lat, + Longitude: lng, + }) + default: return fmt.Errorf("failed to validate location query parameters: %w", fiber.ErrBadRequest) } - - locations, err := services.LocationProvider.SearchLocations(ctx.Context(), query.Text) if err != nil { return fmt.Errorf("failed to search locations: %w", err) } diff --git a/pkg/citydb/provider.go b/pkg/citydb/provider.go index e03b63e..aa2746e 100644 --- a/pkg/citydb/provider.go +++ b/pkg/citydb/provider.go @@ -35,7 +35,11 @@ func Open(path string) (Provider, error) { } func (p Provider) Close() error { - return p.db.Db.(*sql.DB).Close() + if err := p.db.Db.(*sql.DB).Close(); err != nil { + return fmt.Errorf("failed to close cities database: %w", err) + } + + return nil } type locationRow struct { @@ -46,6 +50,7 @@ type locationRow struct { CountryCode string `db:"country_code"` Latitude float64 `db:"latitude"` Longitude float64 `db:"longitude"` + DistanceSq float64 `db:"distance_sq"` } func (p Provider) SearchLocations(ctx context.Context, query string) ([]prayer.Location, error) { @@ -66,6 +71,7 @@ func (p Provider) SearchLocations(ctx context.Context, query string) ([]prayer.L goqu.I("country_code"), goqu.I("latitude"), goqu.I("longitude"), + goqu.V(0).As("distance_sq"), ). Where( goqu.Or( @@ -97,3 +103,49 @@ func (p Provider) SearchLocations(ctx context.Context, query string) ([]prayer.L return locations, nil } + +func (p Provider) SearchLocationsByCoords(ctx context.Context, coords prayer.Coordinates) ([]prayer.Location, error) { + q := p.db. + From("cities"). + Select( + goqu.I("geoname_id"), + goqu.I("name"), + goqu.I("ascii_name"), + goqu.COALESCE(goqu.I("alternate_names"), "").As("alternate_names"), + goqu.I("country_code"), + goqu.I("latitude"), + goqu.I("longitude"), + goqu.L( + "((latitude - ?) * (latitude - ?) + (longitude - ?) * (longitude - ?))", + coords.Latitude, + coords.Latitude, + coords.Longitude, + coords.Longitude, + ).As("distance_sq"), + ). + Order( + goqu.I("distance_sq").Asc(), + goqu.I("population").Desc(), + ). + Limit(50) + + var rows []locationRow + if err := q.ScanStructsContext(ctx, &rows); err != nil { + return nil, fmt.Errorf("failed to query locations by coordinates from cities database: %w", err) + } + + locations := make([]prayer.Location, 0, len(rows)) + for _, row := range rows { + locations = append(locations, prayer.Location{ + ID: row.ID, + Name: row.Name, + ASCIIName: row.ASCIIName, + AlternateNames: row.AlternateNames, + CountryCode: row.CountryCode, + Latitude: row.Latitude, + Longitude: row.Longitude, + }) + } + + return locations, nil +} diff --git a/templates/static/index.html b/templates/static/index.html index 7d6a24b..e07c1f4 100644 --- a/templates/static/index.html +++ b/templates/static/index.html @@ -35,8 +35,47 @@ - +