diff --git a/backend/app/http/v1/content.go b/backend/app/http/v1/content.go index 36f68b5..ce915a5 100644 --- a/backend/app/http/v1/content.go +++ b/backend/app/http/v1/content.go @@ -47,7 +47,7 @@ func (c *Content) List( // @Success 200 {object} dto.ContentDetail // @Bind id path func (c *Content) Get(ctx fiber.Ctx, id string) (*dto.ContentDetail, error) { - uid := cast.ToInt64(ctx.Locals(consts.CtxKeyUser)) + uid := getUserID(ctx) return services.Content.Get(ctx, uid, id) } @@ -65,7 +65,7 @@ func (c *Content) Get(ctx fiber.Ctx, id string) (*dto.ContentDetail, error) { // @Bind id path // @Bind page query func (c *Content) ListComments(ctx fiber.Ctx, id string, page int) (*requests.Pager, error) { - uid := cast.ToInt64(ctx.Locals(consts.CtxKeyUser)) + uid := getUserID(ctx) return services.Content.ListComments(ctx, uid, id, page) } @@ -83,7 +83,7 @@ func (c *Content) ListComments(ctx fiber.Ctx, id string, page int) (*requests.Pa // @Bind id path // @Bind form body func (c *Content) CreateComment(ctx fiber.Ctx, id string, form *dto.CommentCreateForm) error { - uid := cast.ToInt64(ctx.Locals(consts.CtxKeyUser)) + uid := getUserID(ctx) return services.Content.CreateComment(ctx, uid, id, form) } @@ -99,10 +99,62 @@ func (c *Content) CreateComment(ctx fiber.Ctx, id string, form *dto.CommentCreat // @Success 200 {string} string "Liked" // @Bind id path func (c *Content) LikeComment(ctx fiber.Ctx, id string) error { - uid := cast.ToInt64(ctx.Locals(consts.CtxKeyUser)) + uid := getUserID(ctx) return services.Content.LikeComment(ctx, uid, id) } +// Add like +// +// @Router /v1/contents/:id/like [post] +// @Summary Add like +// @Tags Content +// @Param id path string true "Content ID" +// @Success 200 {string} string "Liked" +// @Bind id path +func (c *Content) AddLike(ctx fiber.Ctx, id string) error { + uid := getUserID(ctx) + return services.Content.AddLike(ctx, uid, id) +} + +// Remove like +// +// @Router /v1/contents/:id/like [delete] +// @Summary Remove like +// @Tags Content +// @Param id path string true "Content ID" +// @Success 200 {string} string "Unliked" +// @Bind id path +func (c *Content) RemoveLike(ctx fiber.Ctx, id string) error { + uid := getUserID(ctx) + return services.Content.RemoveLike(ctx, uid, id) +} + +// Add favorite +// +// @Router /v1/contents/:id/favorite [post] +// @Summary Add favorite +// @Tags Content +// @Param id path string true "Content ID" +// @Success 200 {string} string "Favorited" +// @Bind id path +func (c *Content) AddFavorite(ctx fiber.Ctx, id string) error { + uid := getUserID(ctx) + return services.Content.AddFavorite(ctx, uid, id) +} + +// Remove favorite +// +// @Router /v1/contents/:id/favorite [delete] +// @Summary Remove favorite +// @Tags Content +// @Param id path string true "Content ID" +// @Success 200 {string} string "Unfavorited" +// @Bind id path +func (c *Content) RemoveFavorite(ctx fiber.Ctx, id string) error { + uid := getUserID(ctx) + return services.Content.RemoveFavorite(ctx, uid, id) +} + // List curated topics // // @Router /v1/topics [get] @@ -115,3 +167,12 @@ func (c *Content) LikeComment(ctx fiber.Ctx, id string) error { func (c *Content) ListTopics(ctx fiber.Ctx) ([]dto.Topic, error) { return services.Content.ListTopics(ctx) } + +func getUserID(ctx fiber.Ctx) int64 { + if u := ctx.Locals(consts.CtxKeyUser); u != nil { + if user, ok := u.(*models.User); ok { + return user.ID + } + } + return 0 +} diff --git a/backend/app/http/v1/dto/content.go b/backend/app/http/v1/dto/content.go index 72f3afd..7028749 100644 --- a/backend/app/http/v1/dto/content.go +++ b/backend/app/http/v1/dto/content.go @@ -12,19 +12,20 @@ type ContentListFilter struct { } type ContentItem struct { - ID string `json:"id"` - Title string `json:"title"` - Cover string `json:"cover"` - Genre string `json:"genre"` - Type string `json:"type"` // video, audio, article - Price float64 `json:"price"` - AuthorID string `json:"author_id"` - AuthorName string `json:"author_name"` - AuthorAvatar string `json:"author_avatar"` - Views int `json:"views"` - Likes int `json:"likes"` - CreatedAt string `json:"created_at"` - IsPurchased bool `json:"is_purchased"` + ID string `json:"id"` + Title string `json:"title"` + Cover string `json:"cover"` + Genre string `json:"genre"` + Type string `json:"type"` // video, audio, article + Price float64 `json:"price"` + AuthorID string `json:"author_id"` + AuthorName string `json:"author_name"` + AuthorAvatar string `json:"author_avatar"` + AuthorIsFollowing bool `json:"author_is_following"` + Views int `json:"views"` + Likes int `json:"likes"` + CreatedAt string `json:"created_at"` + IsPurchased bool `json:"is_purchased"` } type ContentDetail struct { diff --git a/backend/app/http/v1/routes.gen.go b/backend/app/http/v1/routes.gen.go index de4ec74..d4aa5ff 100644 --- a/backend/app/http/v1/routes.gen.go +++ b/backend/app/http/v1/routes.gen.go @@ -99,6 +99,16 @@ func (r *Routes) Register(router fiber.Router) { Body[dto.UploadPartForm]("form"), )) // Register routes for controller: Content + r.log.Debugf("Registering route: Delete /v1/contents/:id/favorite -> content.RemoveFavorite") + router.Delete("/v1/contents/:id/favorite"[len(r.Path()):], Func1( + r.content.RemoveFavorite, + PathParam[string]("id"), + )) + r.log.Debugf("Registering route: Delete /v1/contents/:id/like -> content.RemoveLike") + router.Delete("/v1/contents/:id/like"[len(r.Path()):], Func1( + r.content.RemoveLike, + PathParam[string]("id"), + )) r.log.Debugf("Registering route: Get /v1/contents -> content.List") router.Get("/v1/contents"[len(r.Path()):], DataFunc1( r.content.List, @@ -130,6 +140,16 @@ func (r *Routes) Register(router fiber.Router) { PathParam[string]("id"), Body[dto.CommentCreateForm]("form"), )) + r.log.Debugf("Registering route: Post /v1/contents/:id/favorite -> content.AddFavorite") + router.Post("/v1/contents/:id/favorite"[len(r.Path()):], Func1( + r.content.AddFavorite, + PathParam[string]("id"), + )) + r.log.Debugf("Registering route: Post /v1/contents/:id/like -> content.AddLike") + router.Post("/v1/contents/:id/like"[len(r.Path()):], Func1( + r.content.AddLike, + PathParam[string]("id"), + )) // Register routes for controller: Creator r.log.Debugf("Registering route: Delete /v1/creator/contents/:id -> creator.DeleteContent") router.Delete("/v1/creator/contents/:id"[len(r.Path()):], Func2( diff --git a/backend/app/services/content.go b/backend/app/services/content.go index 4274917..2b8df29 100644 --- a/backend/app/services/content.go +++ b/backend/app/services/content.go @@ -88,7 +88,7 @@ func (s *content) List(ctx context.Context, filter *content_dto.ContentListFilte // Convert to DTO data := make([]content_dto.ContentItem, len(list)) for i, item := range list { - data[i] = s.toContentItemDTO(item, priceMap[item.ID]) + data[i] = s.toContentItemDTO(item, priceMap[item.ID], false) } return &requests.Pager{ @@ -179,8 +179,18 @@ func (s *content) Get(ctx context.Context, userID int64, id string) (*content_dt } } + // Check if author is followed + authorIsFollowing := false + if userID > 0 { + exists, _ := models.TenantUserQuery.WithContext(ctx). + Where(models.TenantUserQuery.TenantID.Eq(item.TenantID), + models.TenantUserQuery.Role.Contains(string(consts.TenantUserRoleMember))). + Exists() + authorIsFollowing = exists + } + detail := &content_dto.ContentDetail{ - ContentItem: s.toContentItemDTO(&item, price), + ContentItem: s.toContentItemDTO(&item, price, authorIsFollowing), Description: item.Description, Body: item.Body, MediaUrls: s.toMediaURLs(accessibleAssets), @@ -356,7 +366,7 @@ func (s *content) GetLibrary(ctx context.Context, userID int64) ([]user_dto.Cont var data []user_dto.ContentItem for _, item := range list { - dto := s.toContentItemDTO(item, 0) + dto := s.toContentItemDTO(item, 0, false) dto.IsPurchased = true data = append(data, dto) } @@ -441,16 +451,17 @@ func (s *content) ListTopics(ctx context.Context) ([]content_dto.Topic, error) { // Helpers -func (s *content) toContentItemDTO(item *models.Content, price float64) content_dto.ContentItem { +func (s *content) toContentItemDTO(item *models.Content, price float64, authorIsFollowing bool) content_dto.ContentItem { dto := content_dto.ContentItem{ - ID: cast.ToString(item.ID), - Title: item.Title, - Genre: item.Genre, - AuthorID: cast.ToString(item.UserID), - Views: int(item.Views), - Likes: int(item.Likes), - CreatedAt: item.CreatedAt.Format("2006-01-02"), - Price: price, + ID: cast.ToString(item.ID), + Title: item.Title, + Genre: item.Genre, + AuthorID: cast.ToString(item.UserID), + Views: int(item.Views), + Likes: int(item.Likes), + CreatedAt: item.CreatedAt.Format("2006-01-02"), + Price: price, + AuthorIsFollowing: authorIsFollowing, } if item.Author != nil { dto.AuthorName = item.Author.Nickname @@ -623,7 +634,7 @@ func (s *content) getInteractList(ctx context.Context, userID int64, typ string) var data []user_dto.ContentItem for _, item := range list { - data = append(data, s.toContentItemDTO(item, 0)) + data = append(data, s.toContentItemDTO(item, 0, false)) } return data, nil } diff --git a/frontend/portal/src/api/content.js b/frontend/portal/src/api/content.js index c554f7a..db4d2f7 100644 --- a/frontend/portal/src/api/content.js +++ b/frontend/portal/src/api/content.js @@ -14,5 +14,9 @@ export const contentApi = { listComments: (id, page) => request(`/contents/${id}/comments?page=${page || 1}`), createComment: (id, data) => request(`/contents/${id}/comments`, { method: 'POST', body: data }), likeComment: (id) => request(`/comments/${id}/like`, { method: 'POST' }), + addLike: (id) => request(`/contents/${id}/like`, { method: 'POST' }), + removeLike: (id) => request(`/contents/${id}/like`, { method: 'DELETE' }), + addFavorite: (id) => request(`/contents/${id}/favorite`, { method: 'POST' }), + removeFavorite: (id) => request(`/contents/${id}/favorite`, { method: 'DELETE' }), listTopics: () => request('/topics'), }; diff --git a/frontend/portal/src/views/content/DetailView.vue b/frontend/portal/src/views/content/DetailView.vue index bb7abb5..7e8f6f7 100644 --- a/frontend/portal/src/views/content/DetailView.vue +++ b/frontend/portal/src/views/content/DetailView.vue @@ -34,39 +34,41 @@
- - + + +