package handler import ( "log" "net/http" "strconv" "evanpage-backend/internal/domain" "evanpage-backend/internal/service" "github.com/gin-gonic/gin" ) type BookmarkHandler struct { bookmarkService *service.BookmarkService userService *service.UserService } func NewBookmarkHandler(bookmarkService *service.BookmarkService, userService *service.UserService) *BookmarkHandler { return &BookmarkHandler{bookmarkService: bookmarkService, userService: userService} } func getUserID(c *gin.Context) uint { uid, _ := c.Get("userID") id, _ := strconv.ParseUint(uid.(string), 10, 64) return uint(id) } type bookmarkPayload struct { Title string `json:"title"` URL string `json:"url"` Description string `json:"description"` Icon string `json:"icon"` Category string `json:"category"` SortOrder int `json:"sortOrder"` } func (p bookmarkPayload) toInput() service.BookmarkInput { return service.BookmarkInput{ Title: p.Title, URL: p.URL, Description: p.Description, Icon: p.Icon, Category: p.Category, SortOrder: p.SortOrder, } } func (h *BookmarkHandler) Create(c *gin.Context) { var req bookmarkPayload if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } if req.Title == "" || req.URL == "" { c.JSON(http.StatusBadRequest, gin.H{"error": "title and url are required"}) return } bm, err := h.bookmarkService.Create(getUserID(c), req.toInput()) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } c.JSON(http.StatusCreated, bm) } func (h *BookmarkHandler) List(c *gin.Context) { userID := getUserID(c) category := c.Query("category") var bms []domain.Bookmark var err error if category != "" { bms, err = h.bookmarkService.ListByUserAndCategory(userID, category) } else { bms, err = h.bookmarkService.ListByUser(userID) } if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } categories, _ := h.bookmarkService.ListCategoriesByUser(userID) c.JSON(http.StatusOK, gin.H{ "bookmarks": bms, "categories": categories, }) } func (h *BookmarkHandler) PublicList(c *gin.Context) { admin, err := h.userService.FindByRole("admin") if err != nil { c.JSON(http.StatusOK, gin.H{ "bookmarks": []domain.Bookmark{}, "categories": []string{}, }) return } category := c.Query("category") var bms []domain.Bookmark if category != "" { bms, err = h.bookmarkService.ListByUserAndCategory(admin.ID, category) } else { bms, err = h.bookmarkService.ListByUser(admin.ID) } if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } categories, _ := h.bookmarkService.ListCategoriesByUser(admin.ID) c.JSON(http.StatusOK, gin.H{ "bookmarks": bms, "categories": categories, }) } func (h *BookmarkHandler) Update(c *gin.Context) { id, err := strconv.ParseUint(c.Param("id"), 10, 64) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid id"}) return } var req bookmarkPayload if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } bm, err := h.bookmarkService.Update(getUserID(c), uint(id), req.toInput()) if err != nil { if err.Error() == "forbidden" { c.JSON(http.StatusForbidden, gin.H{"error": err.Error()}) return } c.JSON(http.StatusNotFound, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, bm) } func (h *BookmarkHandler) Delete(c *gin.Context) { id, err := strconv.ParseUint(c.Param("id"), 10, 64) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid id"}) return } if err := h.bookmarkService.Delete(getUserID(c), uint(id)); err != nil { if err.Error() == "forbidden" { c.JSON(http.StatusForbidden, gin.H{"error": err.Error()}) return } c.JSON(http.StatusNotFound, gin.H{"error": err.Error()}) return } c.JSON(http.StatusNoContent, nil) } func (h *BookmarkHandler) FetchMeta(c *gin.Context) { var req struct { URL string `json:"url" binding:"required"` } if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } meta, err := service.FetchURLMeta(c.Request.Context(), req.URL) if err != nil { log.Printf("FetchMeta failed: url=%q err=%v", req.URL, err) c.JSON(http.StatusBadGateway, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, meta) } func (h *BookmarkHandler) BulkCreate(c *gin.Context) { var req struct { Bookmarks []bookmarkPayload `json:"bookmarks" binding:"required"` } if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } items := make([]service.BookmarkInput, len(req.Bookmarks)) for i, p := range req.Bookmarks { items[i] = p.toInput() } created, err := h.bookmarkService.BulkCreate(getUserID(c), items) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } c.JSON(http.StatusCreated, gin.H{"created": created}) } func (h *BookmarkHandler) BulkDelete(c *gin.Context) { var req struct { IDs []uint `json:"ids" binding:"required"` } if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } deleted, err := h.bookmarkService.BulkDelete(getUserID(c), req.IDs) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, gin.H{"deleted": deleted}) } func (h *BookmarkHandler) BulkMove(c *gin.Context) { var req struct { IDs []uint `json:"ids" binding:"required"` Category string `json:"category"` } if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } updated, err := h.bookmarkService.BulkUpdateCategory(getUserID(c), req.IDs, req.Category) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, gin.H{"updated": updated}) } func (h *BookmarkHandler) RenameCategory(c *gin.Context) { var req struct { From string `json:"from" binding:"required"` To string `json:"to" binding:"required"` } if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } updated, err := h.bookmarkService.RenameCategory(getUserID(c), req.From, req.To) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, gin.H{"updated": updated}) } func (h *BookmarkHandler) Reorder(c *gin.Context) { var req struct { Order []uint `json:"order" binding:"required"` } if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } if err := h.bookmarkService.Reorder(getUserID(c), req.Order); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, gin.H{"ok": true}) }