diff --git a/api/location.go b/api/location.go index 368fbdc..29e8b20 100644 --- a/api/location.go +++ b/api/location.go @@ -110,6 +110,26 @@ func (server *Server) getListLocations(ctx *gin.Context) { ctx.JSON(http.StatusOK, locations) } +type getListRecentLocationsWithRatingsReq struct { + Page int32 `form:"page_size" binding:"required,min=1"` +} + +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, req.Page) + 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"` } diff --git a/api/middleware.go b/api/middleware.go new file mode 100644 index 0000000..909d17b --- /dev/null +++ b/api/middleware.go @@ -0,0 +1,19 @@ +package api + +import "github.com/gin-gonic/gin" + +func CORSMiddleware() gin.HandlerFunc { + return func(c *gin.Context) { + c.Writer.Header().Set("Access-Control-Allow-Origin", "*") + c.Writer.Header().Set("Access-Control-Allow-Credentials", "true") + c.Writer.Header().Set("Access-Control-Allow-Headers", "Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, Authorization, accept, origin, Cache-Control, X-Requested-With") + c.Writer.Header().Set("Access-Control-Allow-Methods", "POST, OPTIONS, GET, PUT") + + if c.Request.Method == "OPTIONS" { + c.AbortWithStatus(204) + return + } + + c.Next() + } +} diff --git a/api/server.go b/api/server.go index 1100fb4..c91e2e1 100644 --- a/api/server.go +++ b/api/server.go @@ -35,10 +35,13 @@ func NewServer(config util.Config, store db.Store) (*Server, error) { func (server *Server) getRoutes() { router := gin.Default() + router.Use(CORSMiddleware()) + router.POST("/user/signup", server.createUser) // LOCATION router.POST("/locations", server.createLocation) + router.GET("/recent-locations/ratings", server.getListRecentLocationsWithRatings) router.GET("/locations", server.getListLocations) router.GET("/location/:location_id", server.getLocation) diff --git a/db/csv_seeder/locations.csv b/db/csv_seeder/locations.csv index 329bf28..fdc23f9 100644 --- a/db/csv_seeder/locations.csv +++ b/db/csv_seeder/locations.csv @@ -7,4 +7,7 @@ id,address,name,submitted_by,thumbnail,regency_id,google_maps_link,approved_by 6#Jl. Bukit Golf I BSD Sektor VI Lengkong Karya Kec. Serpong Utara#Damai Indah Golf#1#https://lh3.googleusercontent.com/p/AF1QipN5Z-0J6vIfIO6gqPO0z5HDWlNKqp0t816XIJPS=s680-w500-h500#3674#https://www.google.com/maps/place/Damai+Indah+Golf+-+BSD+Course/@-6.2815644,106.6496566,17z/data=!3m1!4b1!4m6!3m5!1s0x2e69fb152983d973:0x89e58e219f8b93ef!8m2!3d-6.2815644!4d106.6522315!16s%2Fg%2F11c54c9r94?entry=ttu#1 7#Jl. P. Mangkubumi No.72A Cokrodiningratan Kec. Jetis#Hotel Tentrem Yogyakarta#1#https://cdn.discordapp.com/attachments/743422487882104837/1150987888553623653/image.png#3471#https://www.google.com/maps?q=Hotel+Tentrem+Yogyakarta&source=lmns&entry=mc&bih=1115&biw=2124&hl=en-US&sa=X&ved=2ahUKEwjjl-HHiKSBAxUu5jgGHTU3BiwQ0pQJKAJ6BAgBEAY#1 8#Moluo Kec.Kwandang#Pulau Saronde Gorontalo#1#https://dynamic-media-cdn.tripadvisor.com/media/photo-o/0d/ec/58/21/saronde-island-a-place.jpg?w=700&h=-1&s=1#7505#https://www.google.com/maps/place/Pulau+Saronde+Gorontalo/@0.9263376,122.8613201,17z/data=!3m1!4b1!4m6!3m5!1s0x32795bf34dff4467:0xa8beb2a832ae8176!8m2!3d0.9263376!4d122.863895!16s%2Fg%2F11l241cc1d?hl=id&entry=ttu#1 -9#Dusun Katiet Desa Bosua Kecamatan Sipora#Pantai Katiet#1#https://dynamic-media-cdn.tripadvisor.com/media/photo-o/1a/d7/fe/f4/mentawai-islands.jpg?w=500&h=-1&s=1#1301#https://www.google.com/maps/place/Katiet,+Bosua,+Sipora+Selatan,+Mentawai+Islands+Regency,+West+Sumatra/@-2.375793,99.848187,15z/data=!3m1!4b1!4m6!3m5!1s0x2fd27efa8363912f:0x8c9c19bd76cba179!8m2!3d-2.375793!4d99.848187!16s%2Fg%2F1tcwz0mt?hl=en-US&entry=ttu#1 \ No newline at end of file +9#Dusun Katiet Desa Bosua Kecamatan Sipora#Pantai Katiet#1#https://dynamic-media-cdn.tripadvisor.com/media/photo-o/1a/d7/fe/f4/mentawai-islands.jpg?w=500&h=-1&s=1#1301#https://www.google.com/maps/place/Katiet,+Bosua,+Sipora+Selatan,+Mentawai+Islands+Regency,+West+Sumatra/@-2.375793,99.848187,15z/data=!3m1!4b1!4m6!3m5!1s0x2fd27efa8363912f:0x8c9c19bd76cba179!8m2!3d-2.375793!4d99.848187!16s%2Fg%2F1tcwz0mt?hl=en-US&entry=ttu#1 +10#Pulau Padar#Pulau Padar#2#https://dynamic-media-cdn.tripadvisor.com/media/photo-o/18/4e/11/f3/padar-island.jpg?w=800&h=-1&s=1#5315#https://www.google.com/maps/place/Pulau+Padar/@-8.6554183,119.570686,14.83z/data=!4m6!3m5!1s0x2db4f84ff6cd01ab:0xf7e6fd33b692a898!8m2!3d-8.6489909!4d119.5832593!16s%2Fg%2F121d16wd?entry=ttu#1 +11#Jl. Ahmad Yani 18, Kel. Kelimutu, Kec. Ende Tengah#Sari rasa#3#https://cdn.discordapp.com/attachments/743422487882104837/1151903310295601172/image.png#5315#https://www.google.com/maps/place/Rumah+Makan+Cha+Cha/@-8.6153697,120.4652533,21z/data=!4m6!3m5!1s0x2db37469e54c0dd3:0x36ac988c726ed544!8m2!3d-8.6153631!4d120.465275!16s%2Fg%2F11bwpclp91?entry=ttu#1 +12#Danau Sentani#Danau Sentani#3#https://dynamic-media-cdn.tripadvisor.com/media/photo-o/1a/c7/e8/17/20190630-143008-largejpg.jpg?w=800&h=-1&s=1#9403#https://www.google.com/maps/place/Danau+Sentani/@-2.639976,140.3889748,12.46z/data=!4m7!3m6!1s0x686cf33c72660fbf:0x7e5c1e7d20d930d7!4b1!8m2!3d-2.6133004!4d140.518734!16s%2Fm%2F0tkjf6v?entry=ttu#1 \ No newline at end of file diff --git a/db/csv_seeder/user.csv b/db/csv_seeder/user.csv index c2e1c8b..429f77d 100644 --- a/db/csv_seeder/user.csv +++ b/db/csv_seeder/user.csv @@ -1,2 +1,4 @@ id,username,password -1,user123,password \ No newline at end of file +1,user123,password +2,critics,password +3,plebs,password \ No newline at end of file diff --git a/db/mock/store.go b/db/mock/store.go index f99e3a7..21527bb 100644 --- a/db/mock/store.go +++ b/db/mock/store.go @@ -79,6 +79,21 @@ func (mr *MockStoreMockRecorder) GetListLocations(arg0 interface{}) *gomock.Call return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetListLocations", reflect.TypeOf((*MockStore)(nil).GetListLocations), arg0) } +// GetListRecentLocationsWithRatings mocks base method. +func (m *MockStore) GetListRecentLocationsWithRatings(arg0 context.Context, arg1 int32) ([]db.GetListRecentLocationsWithRatingsRow, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetListRecentLocationsWithRatings", arg0, arg1) + ret0, _ := ret[0].([]db.GetListRecentLocationsWithRatingsRow) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetListRecentLocationsWithRatings indicates an expected call of GetListRecentLocationsWithRatings. +func (mr *MockStoreMockRecorder) GetListRecentLocationsWithRatings(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetListRecentLocationsWithRatings", reflect.TypeOf((*MockStore)(nil).GetListRecentLocationsWithRatings), arg0, arg1) +} + // GetLocation mocks base method. func (m *MockStore) GetLocation(arg0 context.Context, arg1 int32) (db.Location, error) { m.ctrl.T.Helper() diff --git a/db/queries/locations.sql b/db/queries/locations.sql index a79e1d7..d166949 100644 --- a/db/queries/locations.sql +++ b/db/queries/locations.sql @@ -1,6 +1,23 @@ -- name: GetListLocations :many SELECT * FROM locations; +-- name: GetListRecentLocationsWithRatings :many +SELECT + l.id, + name, + thumbnail, + re.regency_name, + (SELECT COALESCE(SUM(score), -1) from reviews re where re.is_from_critic = true and re.location_id = l.id) as critic_score, + (SELECT COUNT(id) from reviews re where re.is_from_critic = true and re.location_id = l.id) as critic_count, + (SELECT COALESCE(SUM(score), -1) from reviews re where re.is_from_critic = false and re.location_id = l.id) as user_score, + (SELECT COUNT(id) from reviews re where re.is_from_critic = false and re.location_id = l.id) as user_count +FROM locations l +JOIN regencies re on re.id = l.regency_id +WHERE approved_by IS NOT NULL +ORDER BY l.created_at ASC +LIMIT $1; + + -- name: GetLocation :one SELECT * FROM locations WHERE id = $1; diff --git a/db/sqlc/locations.sql.go b/db/sqlc/locations.sql.go index 7d2fb5c..041f24f 100644 --- a/db/sqlc/locations.sql.go +++ b/db/sqlc/locations.sql.go @@ -82,6 +82,66 @@ func (q *Queries) GetListLocations(ctx context.Context) ([]Location, error) { return items, nil } +const getListRecentLocationsWithRatings = `-- name: GetListRecentLocationsWithRatings :many +SELECT + l.id, + name, + thumbnail, + re.regency_name, + (SELECT COALESCE(SUM(score), -1) from reviews re where re.is_from_critic = true and re.location_id = l.id) as critic_score, + (SELECT COUNT(id) from reviews re where re.is_from_critic = true and re.location_id = l.id) as critic_count, + (SELECT COALESCE(SUM(score), -1) from reviews re where re.is_from_critic = false and re.location_id = l.id) as user_score, + (SELECT COUNT(id) from reviews re where re.is_from_critic = false and re.location_id = l.id) as user_count +FROM locations l +JOIN regencies re on re.id = l.regency_id +WHERE approved_by IS NOT NULL +ORDER BY l.created_at ASC +LIMIT $1 +` + +type GetListRecentLocationsWithRatingsRow struct { + ID int32 `json:"id"` + Name string `json:"name"` + Thumbnail sql.NullString `json:"thumbnail"` + RegencyName sql.NullString `json:"regency_name"` + CriticScore interface{} `json:"critic_score"` + CriticCount int64 `json:"critic_count"` + UserScore interface{} `json:"user_score"` + UserCount int64 `json:"user_count"` +} + +func (q *Queries) GetListRecentLocationsWithRatings(ctx context.Context, limit int32) ([]GetListRecentLocationsWithRatingsRow, error) { + rows, err := q.db.QueryContext(ctx, getListRecentLocationsWithRatings, limit) + if err != nil { + return nil, err + } + defer rows.Close() + items := []GetListRecentLocationsWithRatingsRow{} + for rows.Next() { + var i GetListRecentLocationsWithRatingsRow + if err := rows.Scan( + &i.ID, + &i.Name, + &i.Thumbnail, + &i.RegencyName, + &i.CriticScore, + &i.CriticCount, + &i.UserScore, + &i.UserCount, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + const getLocation = `-- name: GetLocation :one SELECT id, address, name, google_maps_link, submitted_by, total_visited, thumbnail, regency_id, is_deleted, created_at, updated_at, approved_by, approved_at FROM locations WHERE id = $1 diff --git a/db/sqlc/models.go b/db/sqlc/models.go index 704eace..c2dbca8 100644 --- a/db/sqlc/models.go +++ b/db/sqlc/models.go @@ -173,14 +173,15 @@ type Region struct { } type Review struct { - ID int32 `json:"id"` - SubmittedBy int32 `json:"submitted_by"` - Comments string `json:"comments"` - Score int16 `json:"score"` - IsHided sql.NullBool `json:"is_hided"` - LocationID int32 `json:"location_id"` - CreatedAt sql.NullTime `json:"created_at"` - UpdatedAt sql.NullTime `json:"updated_at"` + ID int32 `json:"id"` + SubmittedBy int32 `json:"submitted_by"` + Comments string `json:"comments"` + Score int16 `json:"score"` + IsFromCritic bool `json:"is_from_critic"` + IsHided sql.NullBool `json:"is_hided"` + LocationID int32 `json:"location_id"` + CreatedAt sql.NullTime `json:"created_at"` + UpdatedAt sql.NullTime `json:"updated_at"` } type Tag struct { diff --git a/db/sqlc/querier.go b/db/sqlc/querier.go index 993b026..af16368 100644 --- a/db/sqlc/querier.go +++ b/db/sqlc/querier.go @@ -12,6 +12,7 @@ type Querier interface { CreateLocation(ctx context.Context, arg CreateLocationParams) error CreateUser(ctx context.Context, arg CreateUserParams) (User, error) GetListLocations(ctx context.Context) ([]Location, error) + GetListRecentLocationsWithRatings(ctx context.Context, limit int32) ([]GetListRecentLocationsWithRatingsRow, error) GetLocation(ctx context.Context, id int32) (Location, error) UpdatePassword(ctx context.Context, arg UpdatePasswordParams) error UpdateUser(ctx context.Context, arg UpdateUserParams) (User, error)