459 lines
14 KiB
Go
Executable File
459 lines
14 KiB
Go
Executable File
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)
|
||
}
|