package api import ( "fmt" "net/http" "os" "path/filepath" "time" db "git.nochill.in/nochill/hiling_go/db/repository" "git.nochill.in/nochill/hiling_go/util" "github.com/gin-gonic/gin" "github.com/jackc/pgx/v5" "github.com/jackc/pgx/v5/pgtype" "github.com/meilisearch/meilisearch-go" ysqlc "github.com/yiplee/sqlc" ) type createLocationReq struct { Address string `form:"address" binding:"required"` Name string `form:"name" binding:"required"` SubmittedBy int32 `form:"submitted_by" binding:"required,number"` RegencyID int16 `form:"regency_id" binding:"required,number"` LocationType string `form:"location_type" binding:"required"` Amenities string `form:"amenities"` GoogleMapsLink string `form:"google_maps_link"` } // @Summary Create a location // @Tags locations // @Accept mpfd // @Produce json // @Param address formData string true "Address" // @Param name formData string true "Name" // @Param submitted_by formData int true "Submitted by user ID" // @Param regency_id formData int true "Regency ID" // @Param location_type formData string true "Location type" // @Param amenities formData string false "Amenities (comma-separated)" // @Param google_maps_link formData string false "Google Maps link" // @Param thumbnail formData file false "Thumbnail image" // @Success 200 // @Failure 400 {object} map[string]any // @Router /locations [post] func (server *Server) createLocation(ctx *gin.Context) { var req createLocationReq var imgPath string var tempImg []db.CreateImageParams if err := ctx.Bind(&req); err != nil { ctx.JSON(http.StatusBadRequest, ValidationErrorResponse(err)) return } form, _ := ctx.MultipartForm() thumbnails := form.File["thumbnail"] if len(thumbnails) > 0 { for _, img := range thumbnails { fileExt := filepath.Ext(img.Filename) now := time.Now() dir := fmt.Sprintf("public/upload/images/locations/%s/thumbnail", req.Name) osFilename := fmt.Sprintf("%s%s%s", util.RandomString(5), fmt.Sprintf("%v", now.Unix()), fileExt) imgPath = fmt.Sprintf("%s/%s", dir, osFilename) if _, err := os.Stat(dir); os.IsNotExist(err) { os.Mkdir(dir, 0775) } if err := ctx.SaveUploadedFile(img, imgPath); err != nil { ctx.JSON(http.StatusInternalServerError, ErrorResponse(err, "Error while try to save thumbnail image")) return } } arg := db.CreateLocationTxParams{ Address: req.Address, Name: req.Name, LocationType: db.LocationType(req.LocationType), SubmittedBy: req.SubmittedBy, RegencyID: req.RegencyID, IsDeleted: false, ApprovedBy: pgtype.Int4{Int32: 0, Valid: false}, GoogleMapsLink: pgtype.Text{Valid: len(req.GoogleMapsLink) > 0, String: req.GoogleMapsLink}, Amenities: req.Amenities, Thumbnail: tempImg, } err := server.Store.CreateLocationTx(ctx, arg) if err != nil { ctx.JSON(http.StatusInternalServerError, ErrorResponse(err, "Something went wrong while try to save Location")) return } } ctx.Writer.WriteHeader(http.StatusOK) } type getListLocationsReq struct { Page int32 `form:"page" binding:"required,min=1"` PageSize int32 `form:"page_size" binding:"required,min=5"` } // @Summary List locations // @Tags locations // @Produce json // @Param page query int true "Page number (min 1)" // @Param page_size query int true "Page size (min 5)" // @Success 200 {array} map[string]any // @Failure 400 {object} map[string]any // @Router /locations [get] func (server *Server) getListLocations(ctx *gin.Context) { var req getListLocationsReq if err := ctx.ShouldBindQuery(&req); err != nil { ctx.JSON(http.StatusBadRequest, ValidationErrorResponse(err)) return } locations, err := server.Store.GetListLocations(ysqlc.Build(ctx, func(builder *ysqlc.Builder) { builder.Limit(int(req.PageSize)) builder.Offset(int(req.Page-1) * int(req.PageSize)) builder.Order("created_at ASC") })) if err != nil { ctx.JSON(http.StatusInternalServerError, ErrorResponse(err, "Something went wrong")) return } ctx.JSON(http.StatusOK, locations) } type getTopListLocationsReq struct { Page int32 `form:"page" binding:"required,min=1"` PageSize int32 `form:"page_size" binding:"required,min=5"` OrderBy int16 `form:"order_by" binding:"numeric,min=1,max=3"` RegionType int32 `form:"region_type" binding:"numeric,min=0,max=7"` } // @Summary Top-rated locations // @Tags locations // @Produce json // @Param page query int true "Page number (min 1)" // @Param page_size query int true "Page size (min 5)" // @Param order_by query int false "Sort: 1=overall, 2=critics, 3=users" // @Param region_type query int false "Region type filter (0–7)" // @Success 200 {array} map[string]any // @Failure 400 {object} map[string]any // @Router /locations/top-ratings [get] func (server *Server) getTopListLocations(ctx *gin.Context) { var req getTopListLocationsReq var orderby string if err := ctx.ShouldBindQuery(&req); err != nil { ctx.JSON(http.StatusBadRequest, ValidationErrorResponse(err)) return } switch req.OrderBy { case 1: orderby = "iq2.avg_bayes" case 2: orderby = "iq2.critic_bayes" case 3: orderby = "iq2.user_bayes" } arg := db.GetTopListLocationsParams{ Limit: req.PageSize, Offset: (req.Page - 1) * req.PageSize, OrderBy: orderby, RegionType: pgtype.Int4{Valid: req.RegionType > 0, Int32: req.RegionType}, } locations, err := server.Store.GetTopListLocations(ctx, arg) if err != nil { ctx.JSON(http.StatusInternalServerError, ErrorResponse(err, "Something went wrong")) return } ctx.JSON(http.StatusOK, locations) // str, err := ctx.Cookie("kek123429") // if err != nil { // ctx.JSON(http.StatusUnauthorized, ErrorResponse(err, "")) // } // ctx.JSON(http.StatusOK, gin.H{ // "str": str, // "res": locations, // }) } type getListRecentLocationsWithRatingsReq struct { Page int32 `form:"page_size" binding:"required,min=1"` LocationType string `form:"location_type"` Region string `form:"regions"` } // @Summary Recent locations with ratings // @Tags locations // @Produce json // @Param page_size query int true "Number of results (min 1)" // @Param location_type query string false "Filter by location type" // @Param regions query string false "Filter by region" // @Success 200 {array} map[string]any // @Failure 400 {object} map[string]any // @Router /locations/recent [get] func (server *Server) getListRecentLocationsWithRatings(ctx *gin.Context) { var req getListRecentLocationsWithRatingsReq if err := ctx.ShouldBindQuery(&req); err != nil { ctx.JSON(http.StatusBadRequest, ValidationErrorResponse(err)) return } locations, err := server.Store.GetListRecentLocationsWithRatings(ctx, db.GetListRecentLocationsParams{ Limit: req.Page, Regions: req.Region, LocationTypes: req.LocationType, }) if err != nil { ctx.JSON(http.StatusInternalServerError, ErrorResponse(err, "Something went wrong")) return } ctx.JSON(http.StatusOK, locations) } type getLocationReq struct { ID int32 `uri:"location_id" binding:"required"` Review string `form:"review"` // "user", "critics", or "" (default = both) Page int8 `form:"page"` // default 1 PageSize int8 `form:"page_size"` // default 5 } // @Summary Get location detail // @Tags locations // @Produce json // @Param location_id path int true "Location ID" // @Param review query string false "Review type: 'user', 'critics', or both (default)" // @Param page query int false "Page (default 1)" // @Param page_size query int false "Page size (default 5)" // @Success 200 {object} map[string]any // @Failure 404 {object} map[string]any // @Router /location/{location_id} [get] func (server *Server) getLocation(ctx *gin.Context) { var req getLocationReq if err := ctx.ShouldBindUri(&req); err != nil { ctx.JSON(http.StatusBadRequest, ValidationErrorResponse(err)) return } ctx.ShouldBindQuery(&req) location, err := server.Store.GetLocation(ctx, req.ID) if err != nil { if err == pgx.ErrNoRows { ctx.JSON(http.StatusNotFound, ErrorResponse(err, "")) return } ctx.JSON(http.StatusInternalServerError, ErrorResponse(err, "Something went wrong while retrieving location detail")) return } tags, err := server.Store.GetLocationTag(ctx, req.ID) if err != nil { ctx.JSON(http.StatusBadRequest, ErrorResponse(err, "Something went wrong while retrieving location tags")) return } type reviewWithImages struct { db.GetListLocationReviewsRow Images []db.GetImagesByReviewRow `json:"images"` } attachImages := func(reviews []db.GetListLocationReviewsRow) []reviewWithImages { result := make([]reviewWithImages, 0, len(reviews)) for _, r := range reviews { images, _ := server.Store.GetImagesByReview(ctx, r.ID) if images == nil { images = []db.GetImagesByReviewRow{} } result = append(result, reviewWithImages{r, images}) } return result } fetchUsers := req.Review == "" || req.Review == "user" fetchCritics := req.Review == "" || req.Review == "critics" page := req.Page if page < 1 { page = 1 } pageSize := req.PageSize if pageSize < 1 { pageSize = 5 } offset := (page - 1) * pageSize var users_reviews []db.GetListLocationReviewsRow var critics_reviews []db.GetListLocationReviewsRow if fetchUsers { users_reviews, err = server.Store.GetListLocationReviews(ctx, db.GetListLocationReviewsParams{ LocationID: req.ID, Limit: pageSize, Offset: offset, IsCritics: false, }) if err != nil { ctx.JSON(http.StatusInternalServerError, ErrorResponse(err, "Something went wrong while try to receive user reviews for this location")) return } } if fetchCritics { critics_reviews, err = server.Store.GetListLocationReviews(ctx, db.GetListLocationReviewsParams{ LocationID: req.ID, Limit: pageSize, Offset: offset, IsCritics: true, }) if err != nil { ctx.JSON(http.StatusInternalServerError, ErrorResponse(err, "Something went wrong while try to receive user reviews for this location")) return } } res := gin.H{ "tags": tags, "detail": location, "users_review": attachImages(users_reviews), "critics_review": attachImages(critics_reviews), } ctx.JSON(http.StatusOK, res) } type getListLocationReviewsReq struct { PageSize int8 `form:"page_size" binding:"required"` Page int8 `form:"page" binding:"required,min=1"` Type int8 `form:"type" binding:"required"` // 0 = all, 1 = critics,2 = users LocationID int32 `form:"location_id" binding:"required"` } type getListLocationReviewsRes struct { CriticsReviews []db.GetListLocationReviewsRow `json:"critics_reviews"` UsersReviews []db.GetListLocationReviewsRow `json:"users_reviews"` } // @Summary List location reviews // @Tags locations // @Produce json // @Param location_id query int true "Location ID" // @Param page query int true "Page (min 1)" // @Param page_size query int true "Page size" // @Param type query int true "Review type: 1=critics, 2=users, 0=all" // @Success 200 {object} map[string]any // @Failure 400 {object} map[string]any // @Router /location/reviews [get] func (server *Server) getListLocationReviews(ctx *gin.Context) { var req getListLocationReviewsReq var res getListLocationReviewsRes if err := ctx.ShouldBindQuery(&req); err != nil { ctx.JSON(http.StatusBadRequest, ValidationErrorResponse(err)) return } if req.Type-1 == 1 || req.Type-1 == 0 { arg := db.GetListLocationReviewsParams{ LocationID: req.LocationID, Offset: (req.Page - 1) * req.PageSize, Limit: req.PageSize, IsCritics: true, } reviews, err := server.Store.GetListLocationReviews(ctx, arg) if err != nil { if err == pgx.ErrNoRows { ctx.JSON(http.StatusNotFound, ErrorResponse(err, "There's no critics reviews for this location yet")) return } ctx.JSON(http.StatusInternalServerError, ErrorResponse(err, "Something went wrong while try to get critics reviews for this location")) return } res.CriticsReviews = reviews } if req.Type-1 == 2 || req.Type-1 == 0 { arg := db.GetListLocationReviewsParams{ LocationID: req.LocationID, Limit: req.PageSize, Offset: (req.Page - 1) * req.PageSize, IsCritics: false, } reviews, err := server.Store.GetListLocationReviews(ctx, arg) if err != nil { if err == pgx.ErrNoRows { ctx.JSON(http.StatusNotFound, ErrorResponse(err, "There's no critics reviews for this location yet")) return } ctx.JSON(http.StatusInternalServerError, ErrorResponse(err, "Something went wrong while try to get critics reviews for this location")) return } res.UsersReviews = reviews } ctx.JSON(http.StatusOK, res) } type searchLocationsParams struct { Name string `form:"name"` Filter string `form:"filter"` } // @Summary Search locations // @Tags locations // @Produce json // @Param name query string false "Search query" // @Param filter query string false "Meilisearch filter expression" // @Success 200 {array} map[string]any // @Failure 500 {object} map[string]any // @Router /locations/search [get] func (server *Server) searchLocations(ctx *gin.Context) { var req searchLocationsParams if err := ctx.ShouldBindQuery(&req); err != nil { ctx.JSON(http.StatusBadRequest, ValidationErrorResponse(err)) return } searchRequest := meilisearch.SearchRequest{ Limit: 7, Offset: 0, Filter: req.Filter, } searchRes, err := server.MeilisearchClient.Index("locations").Search(req.Name, &searchRequest) if err != nil { ctx.JSON(http.StatusInternalServerError, ErrorResponse(err, fmt.Sprintf("Something went wrong while try to search locations %s", req.Name))) return } ctx.JSON(http.StatusOK, searchRes.Hits) }