package handlers import ( "net/http" "os" "strconv" "strings" "gauth-central/config" "gauth-central/internal/services" "gauth-central/pkg/utils" "github.com/gin-gonic/gin" "github.com/google/uuid" ) type PostCategoryHandler struct { categoryService *services.PostCategoryService } func NewPostCategoryHandler(categoryService *services.PostCategoryService) *PostCategoryHandler { return &PostCategoryHandler{categoryService: categoryService} } // GetAllPostCategories godoc // @Summary Get all active post categories // @Description Retrieve a list of active post categories // @Tags post-categories // @Produce json // @Success 200 {array} models.PostCategory // @Failure 500 {object} map[string]string // @Router /post-categories [get] // GetAllPostCategories returns active post categories. func (h *PostCategoryHandler) GetAllPostCategories(c *gin.Context) { items, err := h.categoryService.GetAllPostCategories(true) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, items) } // GetPostCategoryBySlug godoc // @Summary Get post category by slug // @Description Retrieve an active post category by slug // @Tags post-categories // @Produce json // @Param slug path string true "Category Slug" // @Success 200 {object} models.PostCategory // @Failure 404 {object} map[string]string // @Router /post-categories/{slug} [get] // GetPostCategoryBySlug returns a post category by slug. func (h *PostCategoryHandler) GetPostCategoryBySlug(c *gin.Context) { slug := c.Param("slug") item, err := h.categoryService.GetPostCategoryBySlug(slug, true) if err != nil { c.JSON(http.StatusNotFound, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, item) } // AdminGetAllPostCategories godoc // @Summary Get all post categories (Admin) // @Description Retrieve a list of all post categories including inactive ones // @Tags admin // @Security ApiKeyAuth // @Produce json // @Success 200 {array} models.PostCategory // @Failure 500 {object} map[string]string // @Router /admin/post-categories [get] // AdminGetAllPostCategories returns all categories. func (h *PostCategoryHandler) AdminGetAllPostCategories(c *gin.Context) { items, err := h.categoryService.GetAllPostCategories(false) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, items) } // AdminGetPostCategoryByID godoc // @Summary Get post category by ID (Admin) // @Description Retrieve a post category by ID // @Tags admin // @Security ApiKeyAuth // @Produce json // @Param id path string true "Category ID" // @Success 200 {object} models.PostCategory // @Failure 404 {object} map[string]string // @Router /admin/post-categories/{id} [get] // AdminGetPostCategoryByID returns a category by ID. func (h *PostCategoryHandler) AdminGetPostCategoryByID(c *gin.Context) { id := c.Param("id") item, err := h.categoryService.GetPostCategoryByID(id) if err != nil { c.JSON(http.StatusNotFound, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, item) } // CreatePostCategory godoc // @Summary Create a post category (Admin) // @Description Create a new post category // @Tags admin // @Security ApiKeyAuth // @Accept multipart/form-data // @Produce json // @Param title formData string true "Title" // @Param keywords formData string true "Keywords" // @Param description formData string true "Description" // @Param order formData int false "Order" // @Param parent_id formData string false "Parent ID" // @Param image formData file false "Image" // @Param is_active formData bool false "Is active" // @Success 201 {object} models.PostCategory // @Failure 400 {object} map[string]string // @Router /admin/post-categories [post] // CreatePostCategory creates a new category. func (h *PostCategoryHandler) CreatePostCategory(c *gin.Context) { title := strings.TrimSpace(c.PostForm("title")) keywords := strings.TrimSpace(c.PostForm("keywords")) description := strings.TrimSpace(c.PostForm("description")) if title == "" || keywords == "" || description == "" { c.JSON(http.StatusBadRequest, gin.H{"error": "title, keywords, and description are required"}) return } order := 1 if rawOrder := strings.TrimSpace(c.PostForm("order")); rawOrder != "" { parsed, err := strconv.Atoi(rawOrder) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "order must be an integer"}) return } order = parsed } parentID, err := parseUUIDPtr(c.PostForm("parent_id")) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid parent_id"}) return } isActive := true isActivePtr, err := parseOptionalBool(c, "is_active") if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "is_active must be true or false"}) return } if isActivePtr != nil { isActive = *isActivePtr } imageURL := "" file, fileErr := c.FormFile("image") if fileErr == nil { if file.Size > 5*1024*1024 { c.JSON(http.StatusBadRequest, gin.H{"error": "Image size exceeds 5MB limit"}) return } saved, saveErr := utils.SaveOptimizedImage(file, "./uploads/post-categories", "category", &utils.ImageOptions{ Width: config.AppConfig.PostCategoryImageWidth, Height: config.AppConfig.PostCategoryImageHeight, Quality: float32(config.AppConfig.PostCategoryImageQuality), Format: config.AppConfig.PostCategoryImageFormat, Mode: config.AppConfig.PostCategoryImageMode, }) if saveErr != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to save image: " + saveErr.Error()}) return } imageURL = saved } item, err := h.categoryService.CreatePostCategory( title, keywords, description, imageURL, order, parentID, isActive, ) if err != nil { status := http.StatusInternalServerError if err.Error() == "slug cannot be empty" || err.Error() == "slug already exists" { status = http.StatusBadRequest } c.JSON(status, gin.H{"error": err.Error()}) return } c.JSON(http.StatusCreated, item) } // UpdatePostCategory godoc // @Summary Update a post category (Admin) // @Description Update an existing post category // @Tags admin // @Security ApiKeyAuth // @Accept multipart/form-data // @Produce json // @Param id path string true "Category ID" // @Param title formData string false "Title" // @Param keywords formData string false "Keywords" // @Param description formData string false "Description" // @Param order formData int false "Order" // @Param parent_id formData string false "Parent ID" // @Param image formData file false "Image" // @Param slug formData string false "Slug" // @Param is_active formData bool false "Is active" // @Success 200 {object} models.PostCategory // @Failure 400 {object} map[string]string // @Failure 404 {object} map[string]string // @Router /admin/post-categories/{id} [put] // UpdatePostCategory updates a category. func (h *PostCategoryHandler) UpdatePostCategory(c *gin.Context) { id := c.Param("id") title, hasTitle := getOptionalFormValue(c, "title") keywords, hasKeywords := getOptionalFormValue(c, "keywords") description, hasDescription := getOptionalFormValue(c, "description") slug, hasSlug := getOptionalFormValue(c, "slug") var titlePtr *string var keywordsPtr *string var descriptionPtr *string var slugPtr *string if hasTitle { titlePtr = &title } if hasKeywords { keywordsPtr = &keywords } if hasDescription { descriptionPtr = &description } if hasSlug { slugPtr = &slug } var orderPtr *int if rawOrder, hasOrder := getOptionalFormValue(c, "order"); hasOrder { if rawOrder == "" { orderPtr = nil } else { parsed, err := strconv.Atoi(rawOrder) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "order must be an integer"}) return } orderPtr = &parsed } } parentIDValue, hasParent := getOptionalFormValue(c, "parent_id") parentIDSet := false var parentIDPtr *uuid.UUID if hasParent { parentIDSet = true if parentIDValue == "" { parentIDPtr = nil } else { parsed, err := parseUUIDPtr(parentIDValue) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid parent_id"}) return } parentIDPtr = parsed } } isActivePtr, err := parseOptionalBool(c, "is_active") if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "is_active must be true or false"}) return } var imagePtr *string file, fileErr := c.FormFile("image") if fileErr == nil { if file.Size > 5*1024*1024 { c.JSON(http.StatusBadRequest, gin.H{"error": "Image size exceeds 5MB limit"}) return } item, fetchErr := h.categoryService.GetPostCategoryByID(id) if fetchErr != nil { c.JSON(http.StatusNotFound, gin.H{"error": fetchErr.Error()}) return } if item.Image != "" && strings.HasPrefix(item.Image, "/uploads/") { _ = os.Remove("." + item.Image) } saved, saveErr := utils.SaveOptimizedImage(file, "./uploads/post-categories", id, &utils.ImageOptions{ Width: config.AppConfig.PostCategoryImageWidth, Height: config.AppConfig.PostCategoryImageHeight, Quality: float32(config.AppConfig.PostCategoryImageQuality), Format: config.AppConfig.PostCategoryImageFormat, Mode: config.AppConfig.PostCategoryImageMode, }) if saveErr != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to save image: " + saveErr.Error()}) return } imagePtr = &saved } item, err := h.categoryService.UpdatePostCategory( id, titlePtr, keywordsPtr, descriptionPtr, imagePtr, orderPtr, parentIDPtr, parentIDSet, slugPtr, isActivePtr, ) if err != nil { status := http.StatusInternalServerError switch err.Error() { case "post category not found": status = http.StatusNotFound case "slug cannot be empty", "slug already exists": status = http.StatusBadRequest } c.JSON(status, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, item) } // DeletePostCategory godoc // @Summary Delete a post category (Admin) // @Description Delete a post category by ID // @Tags admin // @Security ApiKeyAuth // @Param id path string true "Category ID" // @Success 200 {object} map[string]string // @Failure 404 {object} map[string]string // @Router /admin/post-categories/{id} [delete] // DeletePostCategory deletes a category by ID. func (h *PostCategoryHandler) DeletePostCategory(c *gin.Context) { id := c.Param("id") if err := h.categoryService.DeletePostCategory(id); err != nil { c.JSON(http.StatusNotFound, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, gin.H{"message": "Post category deleted successfully"}) }