diff --git a/.gitignore b/.gitignore index ffcfdc8..2768314 100644 --- a/.gitignore +++ b/.gitignore @@ -28,4 +28,5 @@ go.work.sum # Ignore avatar images /public/uploads/avatars/ -/public/uploads/articles/ \ No newline at end of file +/public/uploads/articles/ +/public/uploads/banners/ \ No newline at end of file diff --git a/config/database.go b/config/database.go index 61fdd06..d782dad 100644 --- a/config/database.go +++ b/config/database.go @@ -44,6 +44,7 @@ func ConnectDatabase() { &model.UserPin{}, &model.Address{}, &model.Article{}, + &model.Banner{}, // ==main feature== ) if err != nil { diff --git a/dto/banner_dto.go b/dto/banner_dto.go new file mode 100644 index 0000000..56f214c --- /dev/null +++ b/dto/banner_dto.go @@ -0,0 +1,29 @@ +package dto + +import "strings" + +type ResponseBannerDTO struct { + ID string `json:"id"` + BannerName string `json:"bannername"` + BannerImage string `json:"bannerimage"` + CreatedAt string `json:"createdAt"` + UpdatedAt string `json:"updatedAt"` +} + +type RequestBannerDTO struct { + BannerName string `json:"bannername"` + BannerImage string `json:"bannerimage"` +} + +func (r *RequestBannerDTO) ValidateBannerInput() (map[string][]string, bool) { + errors := make(map[string][]string) + + if strings.TrimSpace(r.BannerName) == "" { + errors["bannername"] = append(errors["bannername"], "nama banner harus diisi") + } + if len(errors) > 0 { + return errors, false + } + + return nil, true +} diff --git a/internal/handler/article_handler.go b/internal/handler/article_handler.go index 976a3be..f69e275 100644 --- a/internal/handler/article_handler.go +++ b/internal/handler/article_handler.go @@ -41,7 +41,7 @@ func (h *ArticleHandler) CreateArticle(c *fiber.Ctx) error { return utils.GenericResponse(c, fiber.StatusInternalServerError, err.Error()) } - return utils.SuccessResponse(c, articleResponse, "Article created successfully") + return utils.CreateResponse(c, articleResponse, "Article created successfully") } func (h *ArticleHandler) GetAllArticles(c *fiber.Ctx) error { diff --git a/internal/handler/banner_handler.go b/internal/handler/banner_handler.go new file mode 100644 index 0000000..bcf4e3b --- /dev/null +++ b/internal/handler/banner_handler.go @@ -0,0 +1,108 @@ +package handler + +import ( + "github.com/gofiber/fiber/v2" + "github.com/pahmiudahgede/senggoldong/dto" + "github.com/pahmiudahgede/senggoldong/internal/services" + "github.com/pahmiudahgede/senggoldong/utils" +) + +type BannerHandler struct { + BannerService services.BannerService +} + +func NewBannerHandler(bannerService services.BannerService) *BannerHandler { + return &BannerHandler{BannerService: bannerService} +} + +func (h *BannerHandler) CreateBanner(c *fiber.Ctx) error { + var request dto.RequestBannerDTO + + if err := c.BodyParser(&request); err != nil { + return utils.ValidationErrorResponse(c, map[string][]string{"body": {"Invalid body"}}) + } + + errors, valid := request.ValidateBannerInput() + if !valid { + return utils.ValidationErrorResponse(c, errors) + } + + bannerImage, err := c.FormFile("bannerimage") + if err != nil { + return utils.GenericResponse(c, fiber.StatusBadRequest, "Banner image is required") + } + + bannerResponse, err := h.BannerService.CreateBanner(request, bannerImage) + if err != nil { + return utils.GenericResponse(c, fiber.StatusInternalServerError, err.Error()) + } + + return utils.CreateResponse(c, bannerResponse, "Banner created successfully") +} + +func (h *BannerHandler) GetAllBanners(c *fiber.Ctx) error { + banners, err := h.BannerService.GetAllBanners() + if err != nil { + return utils.GenericResponse(c, fiber.StatusInternalServerError, "Failed to fetch banners") + } + + return utils.NonPaginatedResponse(c, banners, len(banners), "Banners fetched successfully") +} + +func (h *BannerHandler) GetBannerByID(c *fiber.Ctx) error { + id := c.Params("banner_id") + if id == "" { + return utils.GenericResponse(c, fiber.StatusBadRequest, "Banner ID is required") + } + + banner, err := h.BannerService.GetBannerByID(id) + if err != nil { + return utils.GenericResponse(c, fiber.StatusInternalServerError, "Failed to fetch banner") + } + + return utils.SuccessResponse(c, banner, "Banner fetched successfully") +} + +func (h *BannerHandler) UpdateBanner(c *fiber.Ctx) error { + id := c.Params("banner_id") + if id == "" { + return utils.GenericResponse(c, fiber.StatusBadRequest, "Banner ID is required") + } + + var request dto.RequestBannerDTO + + if err := c.BodyParser(&request); err != nil { + return utils.ValidationErrorResponse(c, map[string][]string{"body": {"Invalid body"}}) + } + + errors, valid := request.ValidateBannerInput() + if !valid { + return utils.ValidationErrorResponse(c, errors) + } + + bannerImage, err := c.FormFile("bannerimage") + if err != nil && err.Error() != "no such file" { + return utils.GenericResponse(c, fiber.StatusBadRequest, "Banner image is required") + } + + bannerResponse, err := h.BannerService.UpdateBanner(id, request, bannerImage) + if err != nil { + return utils.GenericResponse(c, fiber.StatusInternalServerError, err.Error()) + } + + return utils.SuccessResponse(c, bannerResponse, "Banner updated successfully") +} + +func (h *BannerHandler) DeleteBanner(c *fiber.Ctx) error { + id := c.Params("banner_id") + if id == "" { + return utils.GenericResponse(c, fiber.StatusBadRequest, "Banner ID is required") + } + + err := h.BannerService.DeleteBanner(id) + if err != nil { + return utils.GenericResponse(c, fiber.StatusInternalServerError, err.Error()) + } + + return utils.GenericResponse(c, fiber.StatusOK, "Banner deleted successfully") +} diff --git a/internal/repositories/banner_repo.go b/internal/repositories/banner_repo.go new file mode 100644 index 0000000..8f553f9 --- /dev/null +++ b/internal/repositories/banner_repo.go @@ -0,0 +1,69 @@ +package repositories + +import ( + "fmt" + + "github.com/pahmiudahgede/senggoldong/model" + "gorm.io/gorm" +) + +type BannerRepository interface { + CreateBanner(banner *model.Banner) error + FindBannerByID(id string) (*model.Banner, error) + FindAllBanners() ([]model.Banner, error) + UpdateBanner(id string, banner *model.Banner) error + DeleteBanner(id string) error +} + +type bannerRepository struct { + DB *gorm.DB +} + +func NewBannerRepository(db *gorm.DB) BannerRepository { + return &bannerRepository{DB: db} +} + +func (r *bannerRepository) CreateBanner(banner *model.Banner) error { + if err := r.DB.Create(banner).Error; err != nil { + return fmt.Errorf("failed to create banner: %v", err) + } + return nil +} + +func (r *bannerRepository) FindBannerByID(id string) (*model.Banner, error) { + var banner model.Banner + err := r.DB.Where("id = ?", id).First(&banner).Error + if err != nil { + if err == gorm.ErrRecordNotFound { + return nil, fmt.Errorf("banner with ID %s not found", id) + } + return nil, fmt.Errorf("failed to fetch banner by ID: %v", err) + } + return &banner, nil +} + +func (r *bannerRepository) FindAllBanners() ([]model.Banner, error) { + var banners []model.Banner + err := r.DB.Find(&banners).Error + if err != nil { + return nil, fmt.Errorf("failed to fetch banners: %v", err) + } + + return banners, nil +} + +func (r *bannerRepository) UpdateBanner(id string, banner *model.Banner) error { + err := r.DB.Model(&model.Banner{}).Where("id = ?", id).Updates(banner).Error + if err != nil { + return fmt.Errorf("failed to update banner: %v", err) + } + return nil +} + +func (r *bannerRepository) DeleteBanner(id string) error { + result := r.DB.Delete(&model.Banner{}, "id = ?", id) + if result.Error != nil { + return fmt.Errorf("failed to delete banner: %v", result.Error) + } + return nil +} diff --git a/internal/services/banner_service.go b/internal/services/banner_service.go new file mode 100644 index 0000000..eca89f5 --- /dev/null +++ b/internal/services/banner_service.go @@ -0,0 +1,347 @@ +package services + +import ( + "fmt" + "mime/multipart" + "os" + "path/filepath" + "time" + + "github.com/google/uuid" + "github.com/pahmiudahgede/senggoldong/dto" + "github.com/pahmiudahgede/senggoldong/internal/repositories" + "github.com/pahmiudahgede/senggoldong/model" + "github.com/pahmiudahgede/senggoldong/utils" +) + +type BannerService interface { + CreateBanner(request dto.RequestBannerDTO, bannerImage *multipart.FileHeader) (*dto.ResponseBannerDTO, error) + GetAllBanners() ([]dto.ResponseBannerDTO, error) + GetBannerByID(id string) (*dto.ResponseBannerDTO, error) + UpdateBanner(id string, request dto.RequestBannerDTO, bannerImage *multipart.FileHeader) (*dto.ResponseBannerDTO, error) + DeleteBanner(id string) error +} + +type bannerService struct { + BannerRepo repositories.BannerRepository +} + +func NewBannerService(bannerRepo repositories.BannerRepository) BannerService { + return &bannerService{BannerRepo: bannerRepo} +} + +func (s *bannerService) saveBannerImage(bannerImage *multipart.FileHeader) (string, error) { + bannerImageDir := "./public/uploads/banners" + if _, err := os.Stat(bannerImageDir); os.IsNotExist(err) { + if err := os.MkdirAll(bannerImageDir, os.ModePerm); err != nil { + return "", fmt.Errorf("failed to create directory for banner image: %v", err) + } + } + + allowedExtensions := map[string]bool{".jpg": true, ".jpeg": true, ".png": true} + extension := filepath.Ext(bannerImage.Filename) + if !allowedExtensions[extension] { + return "", fmt.Errorf("invalid file type, only .jpg, .jpeg, and .png are allowed") + } + + bannerImageFileName := fmt.Sprintf("%s_banner%s", uuid.New().String(), extension) + bannerImagePath := filepath.Join(bannerImageDir, bannerImageFileName) + + src, err := bannerImage.Open() + if err != nil { + return "", fmt.Errorf("failed to open uploaded file: %v", err) + } + defer src.Close() + + dst, err := os.Create(bannerImagePath) + if err != nil { + return "", fmt.Errorf("failed to create banner image file: %v", err) + } + defer dst.Close() + + if _, err := dst.ReadFrom(src); err != nil { + return "", fmt.Errorf("failed to save banner image: %v", err) + } + + return bannerImagePath, nil +} + +func (s *bannerService) CreateBanner(request dto.RequestBannerDTO, bannerImage *multipart.FileHeader) (*dto.ResponseBannerDTO, error) { + + errors, valid := request.ValidateBannerInput() + if !valid { + return nil, fmt.Errorf("validation error: %v", errors) + } + + bannerImagePath, err := s.saveBannerImage(bannerImage) + if err != nil { + return nil, fmt.Errorf("failed to save banner image: %v", err) + } + + banner := model.Banner{ + BannerName: request.BannerName, + BannerImage: bannerImagePath, + } + + if err := s.BannerRepo.CreateBanner(&banner); err != nil { + return nil, fmt.Errorf("failed to create banner: %v", err) + } + + createdAt, _ := utils.FormatDateToIndonesianFormat(banner.CreatedAt) + updatedAt, _ := utils.FormatDateToIndonesianFormat(banner.UpdatedAt) + + bannerResponseDTO := &dto.ResponseBannerDTO{ + ID: banner.ID, + BannerName: banner.BannerName, + BannerImage: banner.BannerImage, + CreatedAt: createdAt, + UpdatedAt: updatedAt, + } + + articlesCacheKey := "banners:all" + err = utils.DeleteData(articlesCacheKey) + if err != nil { + fmt.Printf("Error deleting cache for all banners: %v\n", err) + } + + cacheKey := fmt.Sprintf("banner:%s", banner.ID) + cacheData := map[string]interface{}{ + "data": bannerResponseDTO, + } + if err := utils.SetJSONData(cacheKey, cacheData, time.Hour*24); err != nil { + fmt.Printf("Error caching banner: %v\n", err) + } + + banners, err := s.BannerRepo.FindAllBanners() + if err == nil { + var bannersDTO []dto.ResponseBannerDTO + for _, b := range banners { + createdAt, _ := utils.FormatDateToIndonesianFormat(b.CreatedAt) + updatedAt, _ := utils.FormatDateToIndonesianFormat(b.UpdatedAt) + + bannersDTO = append(bannersDTO, dto.ResponseBannerDTO{ + ID: b.ID, + BannerName: b.BannerName, + BannerImage: b.BannerImage, + CreatedAt: createdAt, + UpdatedAt: updatedAt, + }) + } + + cacheData = map[string]interface{}{ + "data": bannersDTO, + } + if err := utils.SetJSONData(articlesCacheKey, cacheData, time.Hour*24); err != nil { + fmt.Printf("Error caching updated banners to Redis: %v\n", err) + } + } else { + fmt.Printf("Error fetching all banners: %v\n", err) + } + + return bannerResponseDTO, nil +} + +func (s *bannerService) GetAllBanners() ([]dto.ResponseBannerDTO, error) { + var banners []dto.ResponseBannerDTO + + cacheKey := "banners:all" + cachedData, err := utils.GetJSONData(cacheKey) + if err == nil && cachedData != nil { + + if data, ok := cachedData["data"].([]interface{}); ok { + for _, item := range data { + if bannerData, ok := item.(map[string]interface{}); ok { + banners = append(banners, dto.ResponseBannerDTO{ + ID: bannerData["id"].(string), + BannerName: bannerData["bannername"].(string), + BannerImage: bannerData["bannerimage"].(string), + CreatedAt: bannerData["createdAt"].(string), + UpdatedAt: bannerData["updatedAt"].(string), + }) + } + } + return banners, nil + } + } + + records, err := s.BannerRepo.FindAllBanners() + if err != nil { + return nil, fmt.Errorf("failed to fetch banners: %v", err) + } + + for _, record := range records { + createdAt, _ := utils.FormatDateToIndonesianFormat(record.CreatedAt) + updatedAt, _ := utils.FormatDateToIndonesianFormat(record.UpdatedAt) + + banners = append(banners, dto.ResponseBannerDTO{ + ID: record.ID, + BannerName: record.BannerName, + BannerImage: record.BannerImage, + CreatedAt: createdAt, + UpdatedAt: updatedAt, + }) + } + + cacheData := map[string]interface{}{ + "data": banners, + } + if err := utils.SetJSONData(cacheKey, cacheData, time.Hour*24); err != nil { + fmt.Printf("Error caching banners: %v\n", err) + } + + return banners, nil +} + +func (s *bannerService) GetBannerByID(id string) (*dto.ResponseBannerDTO, error) { + + cacheKey := fmt.Sprintf("banner:%s", id) + cachedData, err := utils.GetJSONData(cacheKey) + if err == nil && cachedData != nil { + if data, ok := cachedData["data"].(map[string]interface{}); ok { + return &dto.ResponseBannerDTO{ + ID: data["id"].(string), + BannerName: data["bannername"].(string), + BannerImage: data["bannerimage"].(string), + CreatedAt: data["createdAt"].(string), + UpdatedAt: data["updatedAt"].(string), + }, nil + } + } + + banner, err := s.BannerRepo.FindBannerByID(id) + if err != nil { + return nil, fmt.Errorf("failed to fetch banner by ID: %v", err) + } + + createdAt, _ := utils.FormatDateToIndonesianFormat(banner.CreatedAt) + updatedAt, _ := utils.FormatDateToIndonesianFormat(banner.UpdatedAt) + + bannerResponseDTO := &dto.ResponseBannerDTO{ + ID: banner.ID, + BannerName: banner.BannerName, + BannerImage: banner.BannerImage, + CreatedAt: createdAt, + UpdatedAt: updatedAt, + } + + cacheData := map[string]interface{}{ + "data": bannerResponseDTO, + } + if err := utils.SetJSONData(cacheKey, cacheData, time.Hour*24); err != nil { + fmt.Printf("Error caching banner: %v\n", err) + } + + return bannerResponseDTO, nil +} + +func (s *bannerService) UpdateBanner(id string, request dto.RequestBannerDTO, bannerImage *multipart.FileHeader) (*dto.ResponseBannerDTO, error) { + // Cari banner yang ingin diupdate + banner, err := s.BannerRepo.FindBannerByID(id) + if err != nil { + return nil, fmt.Errorf("banner not found: %v", err) + } + + // Update data banner + banner.BannerName = request.BannerName + if bannerImage != nil { + // Hapus file lama jika ada gambar baru yang diupload + bannerImagePath, err := s.saveBannerImage(bannerImage) + if err != nil { + return nil, fmt.Errorf("failed to save banner image: %v", err) + } + banner.BannerImage = bannerImagePath + } + + // Simpan perubahan ke database + if err := s.BannerRepo.UpdateBanner(id, banner); err != nil { + return nil, fmt.Errorf("failed to update banner: %v", err) + } + + // Format tanggal + createdAt, _ := utils.FormatDateToIndonesianFormat(banner.CreatedAt) + updatedAt, _ := utils.FormatDateToIndonesianFormat(banner.UpdatedAt) + + // Membuat Response DTO + bannerResponseDTO := &dto.ResponseBannerDTO{ + ID: banner.ID, + BannerName: banner.BannerName, + BannerImage: banner.BannerImage, + CreatedAt: createdAt, + UpdatedAt: updatedAt, + } + + // Menghapus cache untuk banner yang lama + cacheKey := fmt.Sprintf("banner:%s", id) + err = utils.DeleteData(cacheKey) + if err != nil { + fmt.Printf("Error deleting cache for banner: %v\n", err) + } + + // Cache banner yang terbaru + cacheData := map[string]interface{}{ + "data": bannerResponseDTO, + } + if err := utils.SetJSONData(cacheKey, cacheData, time.Hour*24); err != nil { + fmt.Printf("Error caching updated banner: %v\n", err) + } + + // Menghapus dan memperbarui cache untuk seluruh banner + articlesCacheKey := "banners:all" + err = utils.DeleteData(articlesCacheKey) + if err != nil { + fmt.Printf("Error deleting cache for all banners: %v\n", err) + } + + // Cache seluruh daftar banner yang terbaru + banners, err := s.BannerRepo.FindAllBanners() + if err == nil { + var bannersDTO []dto.ResponseBannerDTO + for _, b := range banners { + createdAt, _ := utils.FormatDateToIndonesianFormat(b.CreatedAt) + updatedAt, _ := utils.FormatDateToIndonesianFormat(b.UpdatedAt) + + bannersDTO = append(bannersDTO, dto.ResponseBannerDTO{ + ID: b.ID, + BannerName: b.BannerName, + BannerImage: b.BannerImage, + CreatedAt: createdAt, + UpdatedAt: updatedAt, + }) + } + + cacheData = map[string]interface{}{ + "data": bannersDTO, + } + if err := utils.SetJSONData(articlesCacheKey, cacheData, time.Hour*24); err != nil { + fmt.Printf("Error caching updated banners to Redis: %v\n", err) + } + } else { + fmt.Printf("Error fetching all banners: %v\n", err) + } + + return bannerResponseDTO, nil +} + +// DeleteBanner - Menghapus banner dan memperbarui cache +func (s *bannerService) DeleteBanner(id string) error { + // Hapus banner dari database + if err := s.BannerRepo.DeleteBanner(id); err != nil { + return fmt.Errorf("failed to delete banner: %v", err) + } + + // Menghapus cache untuk banner yang dihapus + cacheKey := fmt.Sprintf("banner:%s", id) + err := utils.DeleteData(cacheKey) + if err != nil { + fmt.Printf("Error deleting cache for banner: %v\n", err) + } + + // Menghapus cache untuk seluruh banner + articlesCacheKey := "banners:all" + err = utils.DeleteData(articlesCacheKey) + if err != nil { + fmt.Printf("Error deleting cache for all banners: %v\n", err) + } + + return nil +} diff --git a/model/banner_model.go b/model/banner_model.go new file mode 100644 index 0000000..1ae8171 --- /dev/null +++ b/model/banner_model.go @@ -0,0 +1,11 @@ +package model + +import "time" + +type Banner struct { + ID string `gorm:"primaryKey;type:uuid;default:uuid_generate_v4()" json:"id"` + BannerName string `gorm:"not null" json:"bannername"` + BannerImage string `gorm:"not null" json:"bannerimage"` + CreatedAt time.Time `gorm:"default:current_timestamp" json:"createdAt"` + UpdatedAt time.Time `gorm:"default:current_timestamp" json:"updatedAt"` +} diff --git a/presentation/banner_route.go b/presentation/banner_route.go new file mode 100644 index 0000000..7139c4f --- /dev/null +++ b/presentation/banner_route.go @@ -0,0 +1,25 @@ +package presentation + +import ( + "github.com/gofiber/fiber/v2" + "github.com/pahmiudahgede/senggoldong/config" + "github.com/pahmiudahgede/senggoldong/internal/handler" + "github.com/pahmiudahgede/senggoldong/internal/repositories" + "github.com/pahmiudahgede/senggoldong/internal/services" + "github.com/pahmiudahgede/senggoldong/middleware" + "github.com/pahmiudahgede/senggoldong/utils" +) + +func BannerRouter(api fiber.Router) { + bannerRepo := repositories.NewBannerRepository(config.DB) + bannerService := services.NewBannerService(bannerRepo) + BannerHandler := handler.NewBannerHandler(bannerService) + + bannerAPI := api.Group("/banner-rijik") + + bannerAPI.Post("/create-banner", middleware.AuthMiddleware, middleware.RoleMiddleware(utils.RoleAdministrator), BannerHandler.CreateBanner) + bannerAPI.Get("/getall-banner", BannerHandler.GetAllBanners) + bannerAPI.Get("/get-banner/:banner_id", BannerHandler.GetBannerByID) + bannerAPI.Put("/update-banner/:banner_id", middleware.AuthMiddleware, middleware.RoleMiddleware(utils.RoleAdministrator), BannerHandler.UpdateBanner) + bannerAPI.Delete("/delete-banner/:banner_id", middleware.AuthMiddleware, middleware.RoleMiddleware(utils.RoleAdministrator), BannerHandler.DeleteBanner) +} diff --git a/router/setup_routes.go.go b/router/setup_routes.go.go index 94806eb..da34d32 100644 --- a/router/setup_routes.go.go +++ b/router/setup_routes.go.go @@ -17,4 +17,5 @@ func SetupRoutes(app *fiber.App) { presentation.WilayahRouter(api) presentation.AddressRouter(api) presentation.ArticleRouter(api) + presentation.BannerRouter(api) }