Skip to content

Commit 7291231

Browse files
add post functionality
1 parent 95708f0 commit 7291231

File tree

6 files changed

+405
-13
lines changed

6 files changed

+405
-13
lines changed

internal/handlers/post.go

Lines changed: 275 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,275 @@
1+
package handlers
2+
3+
import (
4+
"net/http"
5+
"strconv"
6+
"strings"
7+
8+
"github.com/gin-gonic/gin"
9+
"github.com/go-playground/validator/v10"
10+
"github.com/nevzattalhaozcan/forgotten/internal/models"
11+
"github.com/nevzattalhaozcan/forgotten/internal/services"
12+
)
13+
14+
type PostHandler struct {
15+
postService *services.PostService
16+
validator *validator.Validate
17+
}
18+
19+
func NewPostHandler(postService *services.PostService) *PostHandler {
20+
return &PostHandler{
21+
postService: postService,
22+
validator: validator.New(),
23+
}
24+
}
25+
26+
func (h *PostHandler) CreatePost(c *gin.Context) {
27+
useridRaw, exists := c.Get("user_id")
28+
if !exists {
29+
c.JSON(http.StatusUnauthorized, gin.H{"error": "user not authenticated"})
30+
return
31+
}
32+
33+
userID, ok := useridRaw.(uint)
34+
if !ok {
35+
c.JSON(http.StatusInternalServerError, gin.H{"error": "invalid user ID"})
36+
return
37+
}
38+
39+
var req models.CreatePostRequest
40+
if err := c.ShouldBindJSON(&req); err != nil {
41+
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
42+
return
43+
}
44+
45+
if err := h.validator.Struct(&req); err != nil {
46+
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
47+
return
48+
}
49+
50+
post, err := h.postService.CreatePost(userID, &req)
51+
if err != nil {
52+
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
53+
return
54+
}
55+
56+
c.JSON(http.StatusCreated, gin.H{
57+
"message": "post created successfully",
58+
"post": post,
59+
})
60+
}
61+
62+
func (h *PostHandler) GetPostByID(c *gin.Context) {
63+
idParam := c.Param("id")
64+
id, err := strconv.ParseUint(idParam, 10, 32)
65+
if err != nil {
66+
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid post ID"})
67+
return
68+
}
69+
70+
post, err := h.postService.GetPostByID(uint(id))
71+
if err != nil {
72+
if err.Error() == "post not found" {
73+
c.JSON(http.StatusNotFound, gin.H{"error": err.Error()})
74+
return
75+
}
76+
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
77+
return
78+
}
79+
80+
c.JSON(http.StatusOK, gin.H{"post": post})
81+
}
82+
83+
func (h *PostHandler) UpdatePost(c *gin.Context) {
84+
idParam := c.Param("id")
85+
id, err := strconv.ParseUint(idParam, 10, 32)
86+
if err != nil {
87+
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid post ID"})
88+
return
89+
}
90+
91+
var req models.UpdatePostRequest
92+
if err := c.ShouldBindJSON(&req); err != nil {
93+
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
94+
return
95+
}
96+
97+
if err := h.validator.Struct(&req); err != nil {
98+
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
99+
return
100+
}
101+
102+
post, err := h.postService.UpdatePost(uint(id), &req)
103+
if err != nil {
104+
if err.Error() == "post not found" {
105+
c.JSON(http.StatusNotFound, gin.H{"error": err.Error()})
106+
return
107+
}
108+
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
109+
return
110+
}
111+
112+
c.JSON(http.StatusOK, gin.H{
113+
"message": "post updated successfully",
114+
"post": post,
115+
})
116+
}
117+
118+
func (h *PostHandler) DeletePost(c *gin.Context) {
119+
idParam := c.Param("id")
120+
id, err := strconv.ParseUint(idParam, 10, 32)
121+
if err != nil {
122+
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid post ID"})
123+
return
124+
}
125+
126+
if err := h.postService.DeletePost(uint(id)); err != nil {
127+
if err.Error() == "post not found" {
128+
c.JSON(http.StatusNotFound, gin.H{"error": err.Error()})
129+
return
130+
}
131+
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
132+
return
133+
}
134+
135+
c.Status(http.StatusNoContent)
136+
}
137+
138+
func (h *PostHandler) ListPostsByUserID(c *gin.Context) {
139+
userIDParam := c.Param("id")
140+
userID, err := strconv.ParseUint(userIDParam, 10, 32)
141+
if err != nil {
142+
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid user ID"})
143+
return
144+
}
145+
146+
posts, err := h.postService.ListPostsByUserID(uint(userID))
147+
if err != nil {
148+
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
149+
return
150+
}
151+
152+
c.JSON(http.StatusOK, gin.H{"posts": posts})
153+
}
154+
155+
func (h *PostHandler) ListPostsByClubID(c *gin.Context) {
156+
clubIDParam := c.Param("id")
157+
clubID, err := strconv.ParseUint(clubIDParam, 10, 32)
158+
if err != nil {
159+
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid club ID"})
160+
return
161+
}
162+
163+
posts, err := h.postService.ListPostsByClubID(uint(clubID))
164+
if err != nil {
165+
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
166+
return
167+
}
168+
169+
c.JSON(http.StatusOK, gin.H{"posts": posts})
170+
}
171+
172+
func (h *PostHandler) ListAllPosts(c *gin.Context) {
173+
posts, err := h.postService.ListAllPosts()
174+
if err != nil {
175+
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
176+
return
177+
}
178+
179+
c.JSON(http.StatusOK, gin.H{"posts": posts})
180+
}
181+
182+
func (h *PostHandler) LikePost(c *gin.Context) {
183+
postIdParam := c.Param("id")
184+
postID, err := strconv.ParseUint(postIdParam, 10, 32)
185+
if err != nil {
186+
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid post ID"})
187+
return
188+
}
189+
190+
useridRaw, exists := c.Get("user_id")
191+
if !exists {
192+
c.JSON(http.StatusUnauthorized, gin.H{"error": "user not authenticated"})
193+
return
194+
}
195+
196+
userID, ok := useridRaw.(uint)
197+
if !ok {
198+
c.JSON(http.StatusInternalServerError, gin.H{"error": "invalid user ID"})
199+
return
200+
}
201+
202+
if err := h.postService.LikePost(userID, uint(postID)); err != nil {
203+
if err.Error() == "post not found" {
204+
c.JSON(http.StatusNotFound, gin.H{"error": err.Error()})
205+
return
206+
} else if strings.Contains(err.Error(), "already liked") {
207+
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
208+
return
209+
}
210+
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
211+
return
212+
}
213+
214+
c.JSON(http.StatusOK, gin.H{"message": "post liked successfully"})
215+
}
216+
217+
func (h *PostHandler) UnlikePost(c *gin.Context) {
218+
postIdParam := c.Param("id")
219+
postID, err := strconv.ParseUint(postIdParam, 10, 32)
220+
if err != nil {
221+
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid post ID"})
222+
return
223+
}
224+
225+
useridRaw, exists := c.Get("user_id")
226+
if !exists {
227+
c.JSON(http.StatusUnauthorized, gin.H{"error": "user not authenticated"})
228+
return
229+
}
230+
231+
userID, ok := useridRaw.(uint)
232+
if !ok {
233+
c.JSON(http.StatusInternalServerError, gin.H{"error": "invalid user ID"})
234+
return
235+
}
236+
237+
if err := h.postService.UnlikePost(userID, uint(postID)); err != nil {
238+
if err.Error() == "post not found" {
239+
c.JSON(http.StatusNotFound, gin.H{"error": err.Error()})
240+
return
241+
} else if strings.Contains(err.Error(), "has not liked") {
242+
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
243+
return
244+
}
245+
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
246+
return
247+
}
248+
249+
c.JSON(http.StatusOK, gin.H{"message": "post unliked successfully"})
250+
}
251+
252+
func (h *PostHandler) ListLikesByPostID(c *gin.Context) {
253+
postIdParam := c.Param("id")
254+
postID, err := strconv.ParseUint(postIdParam, 10, 32)
255+
if err != nil {
256+
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid post ID"})
257+
return
258+
}
259+
260+
likes, err := h.postService.ListLikesByPostID(uint(postID))
261+
if err != nil {
262+
if err.Error() == "post not found" {
263+
c.JSON(http.StatusNotFound, gin.H{"error": err.Error()})
264+
return
265+
}
266+
if err.Error() == "no likes found for this post" {
267+
c.JSON(http.StatusNotFound, gin.H{"error": err.Error()})
268+
return
269+
}
270+
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
271+
return
272+
}
273+
274+
c.JSON(http.StatusOK, gin.H{"likes": likes})
275+
}

internal/handlers/server.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ func (s *Server) setupRoutes() {
5050
var clubRepo repository.ClubRepository = repository.NewClubRepository(s.db)
5151
var eventRepo repository.EventRepository = repository.NewEventRepository(s.db)
5252
var bookRepo repository.BookRepository = repository.NewBookRepository(s.db)
53+
var postRepo repository.PostRepository = repository.NewPostRepository(s.db)
5354

5455
var rdbAvailable bool
5556
var ttl time.Duration
@@ -83,6 +84,9 @@ func (s *Server) setupRoutes() {
8384
bookService := services.NewBookService(bookRepo, s.config)
8485
bookHandler := NewBookHandler(bookService)
8586

87+
postService := services.NewPostService(postRepo, userRepo, clubRepo, s.config)
88+
postHandler := NewPostHandler(postService)
89+
8690
if s.config.Server.Environment != "production" {
8791
s.router.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler))
8892
s.router.GET("/metrics", gin.WrapH(promhttp.Handler()))
@@ -130,6 +134,16 @@ func (s *Server) setupRoutes() {
130134
protected.PUT("/books/:id", bookHandler.UpdateBook)
131135
protected.DELETE("/books/:id", bookHandler.DeleteBook)
132136
protected.GET("/books", bookHandler.ListBooks)
137+
138+
protected.POST("/posts", postHandler.CreatePost)
139+
protected.GET("/posts/:id", postHandler.GetPostByID)
140+
protected.PUT("/posts/:id", postHandler.UpdatePost)
141+
protected.DELETE("/posts/:id", postHandler.DeletePost)
142+
protected.GET("/posts", postHandler.ListAllPosts)
143+
144+
protected.POST("/posts/:id/like", postHandler.LikePost)
145+
protected.POST("/posts/:id/unlike", postHandler.UnlikePost)
146+
protected.GET("/posts/:id/likes", postHandler.ListLikesByPostID)
133147
}
134148
}
135149

internal/models/post.go

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,8 @@ import (
88

99
type PostLike struct {
1010
ID uint `json:"id" gorm:"primaryKey"`
11-
UserID uint `json:"user_id"`
12-
PostID uint `json:"post_id"`
11+
UserID uint `json:"user_id" gorm:"uniqueIndex:idx_user_post_like"`
12+
PostID uint `json:"post_id" gorm:"uniqueIndex:idx_user_post_like"`
1313
User User `json:"user" gorm:"foreignKey:UserID"`
1414
Post Post `json:"post" gorm:"foreignKey:PostID"`
1515

@@ -41,12 +41,14 @@ type CreatePostRequest struct {
4141
Title string `json:"title" validate:"required,min=1,max=255"`
4242
Content string `json:"content" validate:"required,min=1"`
4343
Type string `json:"type" validate:"required,oneof=discussion announcement event poll review"`
44+
ClubID uint `json:"club_id" validate:"required"`
4445
}
4546

4647
type UpdatePostRequest struct {
4748
Title *string `json:"title,omitempty" validate:"omitempty,min=1,max=255"`
4849
Content *string `json:"content,omitempty" validate:"omitempty,min=1"`
4950
Type *string `json:"type,omitempty" validate:"omitempty,oneof=discussion announcement event poll review"`
51+
ClubID *uint `json:"club_id,omitempty" validate:"omitempty"`
5052
IsPinned *bool `json:"is_pinned,omitempty"`
5153
}
5254

internal/repository/interfaces.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ type PostRepository interface {
5555
ListByClubID(clubID uint) ([]models.Post, error)
5656
ListAll() ([]models.Post, error)
5757
AddLike(like *models.PostLike) error
58-
RemoveLike(postID, userID uint) error
58+
RemoveLike(userID, postID uint) error
5959
ListLikesByPostID(postID uint) ([]models.PostLike, error)
60+
HasUserLiked(userID, postID uint) (bool, error)
6061
}

internal/repository/post.go

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -75,12 +75,19 @@ func (r *postRepository) ListAll() ([]models.Post, error) {
7575
return posts, nil
7676
}
7777

78-
func (r *postRepository) LikePost(like *models.PostLike) error {
78+
func (r *postRepository) AddLike(like *models.PostLike) error {
7979
return r.db.Create(like).Error
8080
}
8181

82-
func (r *postRepository) UnlikePost(userID, postID uint) error {
83-
return r.db.Where("user_id = ? AND post_id = ?", userID, postID).Delete(&models.PostLike{}).Error
82+
func (r *postRepository) RemoveLike(userID, postID uint) error {
83+
result := r.db.Where("user_id = ? AND post_id = ?", userID, postID).Delete(&models.PostLike{})
84+
if result.Error != nil {
85+
return result.Error
86+
}
87+
if result.RowsAffected == 0 {
88+
return gorm.ErrRecordNotFound
89+
}
90+
return nil
8491
}
8592

8693
func (r *postRepository) CountLikes(postID uint) (int64, error) {
@@ -101,3 +108,14 @@ func (r *postRepository) ListLikesByPostID(postID uint) ([]models.PostLike, erro
101108
}
102109
return likes, nil
103110
}
111+
112+
func (r *postRepository) HasUserLiked(userID, postID uint) (bool, error) {
113+
var count int64
114+
err := r.db.Model(&models.PostLike{}).
115+
Where("user_id = ? AND post_id = ?", userID, postID).
116+
Count(&count).Error
117+
if err != nil {
118+
return false, err
119+
}
120+
return count > 0, nil
121+
}

0 commit comments

Comments
 (0)