Compare commits

...

3 Commits

Author SHA1 Message Date
hanif salafi 154bc12287 feat: update image upload and preview
continuous-integration/drone/push Build is passing Details
2026-04-10 15:17:09 +07:00
hanif salafi 9c51a0fb7b feat: update image upload and preview for cms modules 2026-04-10 15:11:47 +07:00
hanif salafi 706b3a4585 feat: update all cms modules 2026-04-10 14:23:24 +07:00
45 changed files with 777 additions and 157 deletions

View File

@ -11,8 +11,7 @@ type AboutUsContentImage struct {
CreatedAt time.Time `json:"created_at" gorm:"default:now()"`
UpdatedAt time.Time `json:"updated_at" gorm:"default:now()"`
// relation (optional tapi bagus)
AboutUsContent AboutUsContent `json:"about_us_content" gorm:"foreignKey:AboutUsContentID"`
AboutUsContent AboutUsContent `json:"-" gorm:"foreignKey:AboutUsContentID"`
}
func (AboutUsContentImage) TableName() string {

View File

@ -12,6 +12,8 @@ type AboutUsContent struct {
IsActive *bool `json:"is_active" gorm:"type:bool;default:true"`
CreatedAt time.Time `json:"created_at" gorm:"default:now()"`
UpdatedAt time.Time `json:"updated_at" gorm:"default:now()"`
Images []AboutUsContentImage `json:"images,omitempty" gorm:"foreignKey:AboutUsContentID"`
}
func (AboutUsContent) TableName() string {

View File

@ -7,7 +7,7 @@ import (
)
type HeroContents struct {
ID uuid.UUID `json:"id" gorm:"primaryKey;type:uuid;default:uuid_generate_v4()"`
ID uuid.UUID `json:"id" gorm:"primaryKey;type:uuid"`
PrimaryTitle string `json:"primary_title" gorm:"type:varchar(255)"`
SecondaryTitle string `json:"secondary_title" gorm:"type:varchar(255)"`
Description string `json:"description" gorm:"type:text"`

View File

@ -7,7 +7,7 @@ import (
)
type HeroContentImages struct {
ID uuid.UUID `json:"id" gorm:"primaryKey;type:uuid;default:uuid_generate_v4()"`
ID uuid.UUID `json:"id" gorm:"primaryKey;type:uuid"`
HeroContentID uuid.UUID `json:"hero_content_id" gorm:"type:uuid;not null"`
ImagePath string `json:"image_path" gorm:"type:text"`
ImageURL string `json:"image_url" gorm:"type:text"`

View File

@ -5,7 +5,7 @@ import (
)
type OurProductContentImage struct {
ID uuid.UUID `json:"id" gorm:"primaryKey;type:uuid;default:uuid_generate_v4()"`
ID uuid.UUID `json:"id" gorm:"primaryKey;type:uuid"`
OurProductContentID uuid.UUID `json:"our_product_content_id" gorm:"type:uuid"`
ImagePath string `json:"image_path" gorm:"type:varchar(255)"`
ImageURL string `json:"image_url" gorm:"type:text"`

View File

@ -7,7 +7,7 @@ import (
)
type OurProductContent struct {
ID uuid.UUID `json:"id" gorm:"primaryKey;type:uuid;default:uuid_generate_v4()"`
ID uuid.UUID `json:"id" gorm:"primaryKey;type:uuid"`
PrimaryTitle string `json:"primary_title" gorm:"type:varchar(255)"`
SecondaryTitle string `json:"secondary_title" gorm:"type:varchar(255)"`
Description string `json:"description" gorm:"type:text"`

View File

@ -7,7 +7,7 @@ import (
)
type PartnerContent struct {
ID uuid.UUID `json:"id" gorm:"primaryKey;type:uuid;default:uuid_generate_v4()"`
ID uuid.UUID `json:"id" gorm:"primaryKey;type:uuid"`
PrimaryTitle string `json:"primary_title" gorm:"type:varchar(255)"`
ImagePath string `json:"image_path" gorm:"type:varchar(255)"`
ImageURL string `json:"image_url" gorm:"type:text"`

View File

@ -103,13 +103,13 @@ func Models() []interface{} {
entity.Bookmarks{},
entity.Cities{},
entity.Clients{},
entity.HeroContents{},
entity.HeroContentImages{},
entity.ClientApprovalSettings{},
entity.CsrfTokenRecords{},
entity.CustomStaticPages{},
entity.Districts{},
entity.Feedbacks{},
entity.HeroContents{},
entity.HeroContentImages{},
entity.ForgotPasswords{},
entity.Magazines{},
entity.MagazineFiles{},
@ -121,7 +121,11 @@ func Models() []interface{} {
entity.OneTimePasswords{},
entity.OurProductContent{},
entity.OurProductContentImage{},
entity.OurServiceContent{},
entity.OurServiceContentImage{},
entity.PartnerContent{},
entity.PopupNewsContents{},
entity.PopupNewsContentImages{},
entity.Subscription{},
entity.Schedules{},
entity.UserLevels{},

View File

@ -46,6 +46,7 @@ func (_i *AboutUsContentImageRouter) RegisterAboutUsContentImageRoutes() {
_i.App.Route("/about-us-content-images", func(router fiber.Router) {
router.Get("/", aboutUsContentImageController.All)
router.Post("/url", aboutUsContentImageController.SaveRemote)
router.Get("/:id", aboutUsContentImageController.Show)
// upload image (pakai form-data)

View File

@ -3,12 +3,14 @@ package controller
import (
"strconv"
"web-qudo-be/app/module/about_us_content_images/request"
"web-qudo-be/app/module/about_us_content_images/service"
"github.com/gofiber/fiber/v2"
"github.com/rs/zerolog"
utilRes "web-qudo-be/utils/response"
utilVal "web-qudo-be/utils/validator"
)
type aboutUsContentImageController struct {
@ -20,6 +22,7 @@ type AboutUsContentImageController interface {
All(c *fiber.Ctx) error
Show(c *fiber.Ctx) error
Save(c *fiber.Ctx) error
SaveRemote(c *fiber.Ctx) error
Delete(c *fiber.Ctx) error
}
@ -95,6 +98,24 @@ func (_i *aboutUsContentImageController) Save(c *fiber.Ctx) error {
})
}
// SaveRemote JSON: public URL for image or video (e.g. CDN .mp4)
func (_i *aboutUsContentImageController) SaveRemote(c *fiber.Ctx) error {
req := new(request.AboutUsContentImageRemoteRequest)
if err := utilVal.ParseAndValidate(c, req); err != nil {
return err
}
result, err := _i.service.SaveRemoteURL(req.AboutUsContentID, req.MediaURL, req.MediaType)
if err != nil {
_i.Log.Error().Err(err).Msg("failed save remote about us media")
return err
}
return utilRes.Resp(c, utilRes.Response{
Success: true,
Messages: utilRes.Messages{"About us media URL saved"},
Data: result,
})
}
// DELETE
func (_i *aboutUsContentImageController) Delete(c *fiber.Ctx) error {
id, err := strconv.Atoi(c.Params("id"))

View File

@ -35,9 +35,7 @@ func NewAboutUsContentImageRepository(db *database.Database, log zerolog.Logger)
// GET ALL
func (_i *aboutUsContentImageRepository) GetAll() (images []*entity.AboutUsContentImage, err error) {
err = _i.DB.DB.
Where("is_active = ?", true).
Find(&images).Error
err = _i.DB.DB.Find(&images).Error
return
}
@ -52,7 +50,7 @@ func (_i *aboutUsContentImageRepository) FindOne(id uint) (image *entity.AboutUs
// GET BY ABOUT US CONTENT ID
func (_i *aboutUsContentImageRepository) FindByContentID(contentID uint) (images []*entity.AboutUsContentImage, err error) {
err = _i.DB.DB.
Where("about_us_content_id = ? AND is_active = ?", contentID, true).
Where("about_us_content_id = ?", contentID).
Find(&images).Error
return
}

View File

@ -0,0 +1,7 @@
package request
type AboutUsContentImageRemoteRequest struct {
AboutUsContentID uint `json:"about_us_content_id" validate:"required"`
MediaURL string `json:"media_url" validate:"required"`
MediaType string `json:"media_type"`
}

View File

@ -2,36 +2,42 @@ package service
import (
"fmt"
"mime"
"mime/multipart"
"path/filepath"
"time"
"strings"
"web-qudo-be/app/database/entity"
"web-qudo-be/app/module/about_us_content_images/repository"
minioStorage "web-qudo-be/config/config"
"web-qudo-be/utils/storage"
"github.com/rs/zerolog"
fileUtil "web-qudo-be/utils/file"
)
type aboutUsContentImageService struct {
Repo repository.AboutUsContentImageRepository
Log zerolog.Logger
Repo repository.AboutUsContentImageRepository
MinioStorage *minioStorage.MinioStorage
Log zerolog.Logger
}
type AboutUsContentImageService interface {
All() (images []*entity.AboutUsContentImage, err error)
Show(id uint) (image *entity.AboutUsContentImage, err error)
Save(aboutUsContentId uint, file *multipart.FileHeader) (image *entity.AboutUsContentImage, err error)
SaveRemoteURL(aboutUsContentID uint, mediaURL, mediaType string) (image *entity.AboutUsContentImage, err error)
Delete(id uint) error
}
func NewAboutUsContentImageService(
repo repository.AboutUsContentImageRepository,
minio *minioStorage.MinioStorage,
log zerolog.Logger,
) AboutUsContentImageService {
return &aboutUsContentImageService{
Repo: repo,
Log: log,
Repo: repo,
MinioStorage: minio,
Log: log,
}
}
@ -48,31 +54,28 @@ func (_i *aboutUsContentImageService) Save(aboutUsContentId uint, file *multipar
_i.Log.Info().
Uint("aboutUsContentId", aboutUsContentId).
Str("filename", file.Filename).
Msg("upload image")
Msg("upload about us media")
// validasi file
ext := filepath.Ext(file.Filename)
if ext != ".jpg" && ext != ".jpeg" && ext != ".png" {
return nil, fmt.Errorf("invalid file type")
}
// generate filename
filename := fmt.Sprintf("about_us_%d_%d%s", aboutUsContentId, time.Now().Unix(), ext)
filePath := fmt.Sprintf("./uploads/%s", filename)
// save file
if err := fileUtil.SaveFile(file, filePath); err != nil {
_i.Log.Error().Err(err).Msg("failed save file")
key, url, err := storage.UploadCMSObject(_i.MinioStorage, "about-us", file, true)
if err != nil {
return nil, err
}
// save ke DB
ext := strings.ToLower(filepath.Ext(file.Filename))
mt := mime.TypeByExtension(ext)
if mt == "" {
if ext == ".mp4" || ext == ".webm" {
mt = "video/" + strings.TrimPrefix(ext, ".")
} else {
mt = "application/octet-stream"
}
}
data := &entity.AboutUsContentImage{
AboutUsContentID: aboutUsContentId,
MediaPath: filePath,
MediaType: ext,
MediaURL: "/uploads/" + filename,
MediaPath: key,
MediaType: mt,
MediaURL: url,
}
result, err := _i.Repo.Create(data)
@ -84,6 +87,27 @@ func (_i *aboutUsContentImageService) Save(aboutUsContentId uint, file *multipar
return result, nil
}
func (_i *aboutUsContentImageService) SaveRemoteURL(aboutUsContentID uint, mediaURL, mediaType string) (image *entity.AboutUsContentImage, err error) {
if strings.TrimSpace(mediaURL) == "" {
return nil, fmt.Errorf("media_url is required")
}
mt := mediaType
if mt == "" {
lower := strings.ToLower(mediaURL)
if strings.HasSuffix(lower, ".mp4") || strings.Contains(lower, "video") {
mt = "video/mp4"
} else {
mt = "image/url"
}
}
data := &entity.AboutUsContentImage{
AboutUsContentID: aboutUsContentID,
MediaURL: mediaURL,
MediaType: mt,
}
return _i.Repo.Create(data)
}
func (_i *aboutUsContentImageService) Delete(id uint) error {
return _i.Repo.Delete(id)
}

View File

@ -3,6 +3,7 @@ package controller
import (
"strconv"
"web-qudo-be/app/module/about_us_contents/request"
"web-qudo-be/app/module/about_us_contents/service"
"github.com/gofiber/fiber/v2"
@ -71,13 +72,13 @@ func (_i *aboutUsContentController) Show(c *fiber.Ctx) error {
// CREATE
func (_i *aboutUsContentController) Save(c *fiber.Ctx) error {
req := new(map[string]interface{})
req := new(request.AboutUsContentCreateRequest)
if err := utilVal.ParseAndValidate(c, req); err != nil {
return err
}
result, err := _i.service.Save(*req)
result, err := _i.service.Save(req.ToEntity())
if err != nil {
_i.Log.Error().Err(err).Msg("failed create about us content")
return err
@ -97,13 +98,13 @@ func (_i *aboutUsContentController) Update(c *fiber.Ctx) error {
return err
}
req := new(map[string]interface{})
req := new(request.AboutUsContentUpdateRequest)
if err := utilVal.ParseAndValidate(c, req); err != nil {
return err
}
err = _i.service.Update(uint(id), *req)
err = _i.service.Update(uint(id), req.ToEntity())
if err != nil {
_i.Log.Error().Err(err).Msg("failed update about us content")
return err

View File

@ -34,7 +34,9 @@ func (r *aboutUsContentRepository) GetAll() ([]*entity.AboutUsContent, error) {
var results []*entity.AboutUsContent
err := r.DB.DB.
Where("is_active = ?", true).
Preload("Images").
Where("is_active IS NULL OR is_active = ?", true).
Order("id ASC").
Find(&results).Error
if err != nil {
@ -69,12 +71,24 @@ func (r *aboutUsContentRepository) Create(data *entity.AboutUsContent) (*entity.
return data, nil
}
// UPDATE
// Update uses a column map so we never SET primary key / created_at to zero values (breaks FK children).
func (r *aboutUsContentRepository) Update(id uint, data *entity.AboutUsContent) error {
updates := map[string]interface{}{
"primary_title": data.PrimaryTitle,
"secondary_title": data.SecondaryTitle,
"description": data.Description,
"primary_cta": data.PrimaryCta,
"secondary_cta_text": data.SecondaryCtaText,
"updated_at": data.UpdatedAt,
}
if data.IsActive != nil {
updates["is_active"] = data.IsActive
}
err := r.DB.DB.
Model(&entity.AboutUsContent{}).
Where("id = ?", id).
Updates(data).Error
Updates(updates).Error
if err != nil {
r.Log.Error().Err(err).Msg("failed update about us content")

View File

@ -1,6 +1,8 @@
package service
import (
"time"
"github.com/rs/zerolog"
"web-qudo-be/app/database/entity"
@ -15,8 +17,8 @@ type aboutUsContentService struct {
type AboutUsContentService interface {
All() ([]*entity.AboutUsContent, error)
Show(id uint) (*entity.AboutUsContent, error)
Save(data map[string]interface{}) (*entity.AboutUsContent, error)
Update(id uint, data map[string]interface{}) error
Save(data *entity.AboutUsContent) (*entity.AboutUsContent, error)
Update(id uint, data *entity.AboutUsContent) error
Delete(id uint) error
}
@ -53,26 +55,8 @@ func (s *aboutUsContentService) Show(id uint) (*entity.AboutUsContent, error) {
}
// CREATE
func (s *aboutUsContentService) Save(data map[string]interface{}) (*entity.AboutUsContent, error) {
entityData := &entity.AboutUsContent{}
if v, ok := data["primary_title"].(string); ok {
entityData.PrimaryTitle = v
}
if v, ok := data["secondary_title"].(string); ok {
entityData.SecondaryTitle = v
}
if v, ok := data["description"].(string); ok {
entityData.Description = v
}
if v, ok := data["primary_cta"].(string); ok {
entityData.PrimaryCta = v
}
if v, ok := data["secondary_cta_text"].(string); ok {
entityData.SecondaryCtaText = v
}
result, err := s.Repo.Create(entityData)
func (s *aboutUsContentService) Save(data *entity.AboutUsContent) (*entity.AboutUsContent, error) {
result, err := s.Repo.Create(data)
if err != nil {
s.Log.Error().Err(err).Msg("failed create about us content")
return nil, err
@ -82,26 +66,8 @@ func (s *aboutUsContentService) Save(data map[string]interface{}) (*entity.About
}
// UPDATE
func (s *aboutUsContentService) Update(id uint, data map[string]interface{}) error {
entityData := &entity.AboutUsContent{}
if v, ok := data["primary_title"].(string); ok {
entityData.PrimaryTitle = v
}
if v, ok := data["secondary_title"].(string); ok {
entityData.SecondaryTitle = v
}
if v, ok := data["description"].(string); ok {
entityData.Description = v
}
if v, ok := data["primary_cta"].(string); ok {
entityData.PrimaryCta = v
}
if v, ok := data["secondary_cta_text"].(string); ok {
entityData.SecondaryCtaText = v
}
err := s.Repo.Update(id, entityData)
func (s *aboutUsContentService) Update(id uint, data *entity.AboutUsContent) error {
err := s.Repo.Update(id, data)
if err != nil {
s.Log.Error().Err(err).Msg("failed update about us content")
return err
@ -119,6 +85,7 @@ func (s *aboutUsContentService) Delete(id uint) error {
isActive := false
result.IsActive = &isActive
result.UpdatedAt = time.Now()
return s.Repo.Update(id, result)
}

View File

@ -0,0 +1,30 @@
package cms_media
import (
"github.com/gofiber/fiber/v2"
"go.uber.org/fx"
"web-qudo-be/app/module/cms_media/controller"
"web-qudo-be/app/module/cms_media/service"
)
type CmsMediaRouter struct {
App *fiber.App
Ctrl *controller.CmsMediaController
}
var NewCmsMediaModule = fx.Options(
fx.Provide(service.NewCmsMediaService),
fx.Provide(controller.NewCmsMediaController),
fx.Provide(NewCmsMediaRouter),
)
func NewCmsMediaRouter(app *fiber.App, ctrl *controller.CmsMediaController) *CmsMediaRouter {
return &CmsMediaRouter{App: app, Ctrl: ctrl}
}
func (r *CmsMediaRouter) RegisterCmsMediaRoutes() {
r.App.Route("/cms-media", func(router fiber.Router) {
router.Get("/viewer/*", r.Ctrl.Viewer)
})
}

View File

@ -0,0 +1,23 @@
package controller
import (
"github.com/gofiber/fiber/v2"
"github.com/rs/zerolog"
"web-qudo-be/app/module/cms_media/service"
)
type CmsMediaController struct {
svc *service.CmsMediaService
Log zerolog.Logger
}
func NewCmsMediaController(svc *service.CmsMediaService, log zerolog.Logger) *CmsMediaController {
return &CmsMediaController{svc: svc, Log: log}
}
// Viewer streams CMS media from MinIO via API URL (for img/video src).
// @Router /cms-media/viewer/{path} [get]
func (ctrl *CmsMediaController) Viewer(c *fiber.Ctx) error {
return ctrl.svc.Viewer(c)
}

View File

@ -0,0 +1,75 @@
package service
import (
"context"
"io"
"mime"
"path/filepath"
"strings"
"github.com/gofiber/fiber/v2"
"github.com/minio/minio-go/v7"
"github.com/rs/zerolog"
minioStorage "web-qudo-be/config/config"
)
type CmsMediaService struct {
Minio *minioStorage.MinioStorage
Log zerolog.Logger
}
func NewCmsMediaService(minio *minioStorage.MinioStorage, log zerolog.Logger) *CmsMediaService {
return &CmsMediaService{Minio: minio, Log: log}
}
// Viewer streams a CMS object from MinIO (same idea as article-files viewer).
func (s *CmsMediaService) Viewer(c *fiber.Ctx) error {
objectKey := strings.TrimSpace(c.Params("*"))
objectKey = strings.TrimPrefix(objectKey, "/")
if objectKey == "" {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
"success": false,
"messages": []string{"object key required"},
})
}
if !strings.HasPrefix(objectKey, "cms/") || strings.Contains(objectKey, "..") {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
"success": false,
"messages": []string{"invalid object key"},
})
}
ctx := context.Background()
bucket := s.Minio.Cfg.ObjectStorage.MinioStorage.BucketName
client, err := s.Minio.ConnectMinio()
if err != nil {
s.Log.Error().Err(err).Msg("cms media viewer: minio connect")
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
"success": false,
"messages": []string{"storage unavailable"},
})
}
obj, err := client.GetObject(ctx, bucket, objectKey, minio.GetObjectOptions{})
if err != nil {
s.Log.Error().Err(err).Str("key", objectKey).Msg("cms media viewer: get object")
return c.Status(fiber.StatusNotFound).SendString("not found")
}
defer obj.Close()
ext := strings.ToLower(filepath.Ext(objectKey))
contentType := mime.TypeByExtension(ext)
if contentType == "" {
contentType = "application/octet-stream"
}
c.Set("Content-Type", contentType)
c.Set("Cache-Control", "public, max-age=86400")
if _, err := io.Copy(c.Response().BodyWriter(), obj); err != nil {
s.Log.Error().Err(err).Msg("cms media viewer: stream")
return err
}
return nil
}

View File

@ -1,6 +1,8 @@
package controller
import (
"strings"
"github.com/gofiber/fiber/v2"
"github.com/google/uuid"
"github.com/rs/zerolog"
@ -82,6 +84,21 @@ func (_i *heroContentImagesController) Update(c *fiber.Ctx) error {
return err
}
if strings.HasPrefix(c.Get("Content-Type"), "multipart/form-data") {
file, err := c.FormFile("file")
if err != nil {
return err
}
if err := _i.service.UpdateWithFile(id, file); err != nil {
_i.Log.Error().Err(err).Msg("failed update hero content image (upload)")
return err
}
return utilRes.Resp(c, utilRes.Response{
Success: true,
Messages: utilRes.Messages{"Hero content image updated"},
})
}
req := new(request.HeroContentImageUpdateRequest)
if err := utilVal.ParseAndValidate(c, req); err != nil {
return err

View File

@ -1,32 +1,41 @@
package service
import (
"mime/multipart"
"github.com/google/uuid"
"github.com/rs/zerolog"
"web-qudo-be/app/database/entity"
"web-qudo-be/app/module/hero_content_images/repository"
minioStorage "web-qudo-be/config/config"
"web-qudo-be/utils/storage"
)
type heroContentImagesService struct {
Repo repository.HeroContentImagesRepository
Log zerolog.Logger
Repo repository.HeroContentImagesRepository
MinioStorage *minioStorage.MinioStorage
Log zerolog.Logger
}
type HeroContentImagesService interface {
FindByHeroID(heroID uuid.UUID) (*entity.HeroContentImages, error)
Save(data *entity.HeroContentImages) (*entity.HeroContentImages, error)
SaveWithFile(heroContentID uuid.UUID, file *multipart.FileHeader) (*entity.HeroContentImages, error)
Update(id uuid.UUID, data *entity.HeroContentImages) error
UpdateWithFile(id uuid.UUID, file *multipart.FileHeader) error
Delete(id uuid.UUID) error
}
func NewHeroContentImagesService(
repo repository.HeroContentImagesRepository,
minio *minioStorage.MinioStorage,
log zerolog.Logger,
) HeroContentImagesService {
return &heroContentImagesService{
Repo: repo,
Log: log,
Repo: repo,
MinioStorage: minio,
Log: log,
}
}
@ -52,6 +61,19 @@ func (s *heroContentImagesService) Save(data *entity.HeroContentImages) (*entity
return result, nil
}
func (s *heroContentImagesService) SaveWithFile(heroContentID uuid.UUID, file *multipart.FileHeader) (*entity.HeroContentImages, error) {
key, url, err := storage.UploadCMSObject(s.MinioStorage, "hero", file, false)
if err != nil {
return nil, err
}
data := &entity.HeroContentImages{
HeroContentID: heroContentID,
ImagePath: key,
ImageURL: url,
}
return s.Save(data)
}
func (s *heroContentImagesService) Update(id uuid.UUID, data *entity.HeroContentImages) error {
err := s.Repo.Update(id, data)
if err != nil {
@ -62,6 +84,17 @@ func (s *heroContentImagesService) Update(id uuid.UUID, data *entity.HeroContent
return nil
}
func (s *heroContentImagesService) UpdateWithFile(id uuid.UUID, file *multipart.FileHeader) error {
key, url, err := storage.UploadCMSObject(s.MinioStorage, "hero", file, false)
if err != nil {
return err
}
return s.Repo.Update(id, &entity.HeroContentImages{
ImagePath: key,
ImageURL: url,
})
}
func (s *heroContentImagesService) Delete(id uuid.UUID) error {
err := s.Repo.Delete(id)
if err != nil {
@ -70,4 +103,4 @@ func (s *heroContentImagesService) Delete(id uuid.UUID) error {
}
return nil
}
}

View File

@ -1,11 +1,13 @@
package repository
import (
"errors"
"web-qudo-be/app/database"
"web-qudo-be/app/database/entity"
"github.com/google/uuid"
"github.com/rs/zerolog"
"gorm.io/gorm"
)
type heroContentsRepository struct {
@ -35,6 +37,9 @@ func (r *heroContentsRepository) Get() (*entity.HeroContents, error) {
First(&data).Error
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, nil
}
r.Log.Error().Err(err).Msg("failed get hero content")
return nil, err
}

View File

@ -1,6 +1,8 @@
package controller
import (
"strings"
"github.com/gofiber/fiber/v2"
"github.com/google/uuid"
"github.com/rs/zerolog"
@ -32,7 +34,7 @@ func NewOurProductContentImagesController(service service.OurProductContentImage
}
func (_i *ourProductContentImagesController) FindByOurProductContentID(c *fiber.Ctx) error {
contentIDStr := c.Params("our_product_content_id")
contentIDStr := c.Params("content_id")
contentID, err := uuid.Parse(contentIDStr)
if err != nil {
@ -53,6 +55,28 @@ func (_i *ourProductContentImagesController) FindByOurProductContentID(c *fiber.
}
func (_i *ourProductContentImagesController) Save(c *fiber.Ctx) error {
if strings.HasPrefix(c.Get("Content-Type"), "multipart/form-data") {
cidStr := c.FormValue("our_product_content_id")
cid, err := uuid.Parse(cidStr)
if err != nil {
return err
}
file, err := c.FormFile("file")
if err != nil {
return err
}
result, err := _i.service.SaveWithFile(cid, file)
if err != nil {
_i.Log.Error().Err(err).Msg("failed create our product content image (upload)")
return err
}
return utilRes.Resp(c, utilRes.Response{
Success: true,
Messages: utilRes.Messages{"Our product content image created"},
Data: result,
})
}
req := new(request.OurProductContentImageCreateRequest)
if err := utilVal.ParseAndValidate(c, req); err != nil {
@ -82,6 +106,21 @@ func (_i *ourProductContentImagesController) Update(c *fiber.Ctx) error {
return err
}
if strings.HasPrefix(c.Get("Content-Type"), "multipart/form-data") {
file, err := c.FormFile("file")
if err != nil {
return err
}
if err := _i.service.UpdateWithFile(id, file); err != nil {
_i.Log.Error().Err(err).Msg("failed update our product content image (upload)")
return err
}
return utilRes.Resp(c, utilRes.Response{
Success: true,
Messages: utilRes.Messages{"Our product content image updated"},
})
}
req := new(request.OurProductContentImageUpdateRequest)
if err := utilVal.ParseAndValidate(c, req); err != nil {
return err

View File

@ -1,32 +1,41 @@
package service
import (
"mime/multipart"
"github.com/google/uuid"
"github.com/rs/zerolog"
"web-qudo-be/app/database/entity"
"web-qudo-be/app/module/our_product_content_images/repository"
minioStorage "web-qudo-be/config/config"
"web-qudo-be/utils/storage"
)
type ourProductContentImagesService struct {
Repo repository.OurProductContentImagesRepository
Log zerolog.Logger
Repo repository.OurProductContentImagesRepository
MinioStorage *minioStorage.MinioStorage
Log zerolog.Logger
}
type OurProductContentImagesService interface {
FindByContentID(contentID uuid.UUID) ([]entity.OurProductContentImage, error)
Save(data *entity.OurProductContentImage) (*entity.OurProductContentImage, error)
SaveWithFile(ourProductContentID uuid.UUID, file *multipart.FileHeader) (*entity.OurProductContentImage, error)
Update(id uuid.UUID, data *entity.OurProductContentImage) error
UpdateWithFile(id uuid.UUID, file *multipart.FileHeader) error
Delete(id uuid.UUID) error
}
func NewOurProductContentImagesService(
repo repository.OurProductContentImagesRepository,
minio *minioStorage.MinioStorage,
log zerolog.Logger,
) OurProductContentImagesService {
return &ourProductContentImagesService{
Repo: repo,
Log: log,
Repo: repo,
MinioStorage: minio,
Log: log,
}
}
@ -52,6 +61,19 @@ func (s *ourProductContentImagesService) Save(data *entity.OurProductContentImag
return result, nil
}
func (s *ourProductContentImagesService) SaveWithFile(ourProductContentID uuid.UUID, file *multipart.FileHeader) (*entity.OurProductContentImage, error) {
key, url, err := storage.UploadCMSObject(s.MinioStorage, "our-products", file, false)
if err != nil {
return nil, err
}
data := &entity.OurProductContentImage{
OurProductContentID: ourProductContentID,
ImagePath: key,
ImageURL: url,
}
return s.Save(data)
}
func (s *ourProductContentImagesService) Update(id uuid.UUID, data *entity.OurProductContentImage) error {
err := s.Repo.Update(id, data)
if err != nil {
@ -62,6 +84,17 @@ func (s *ourProductContentImagesService) Update(id uuid.UUID, data *entity.OurPr
return nil
}
func (s *ourProductContentImagesService) UpdateWithFile(id uuid.UUID, file *multipart.FileHeader) error {
key, url, err := storage.UploadCMSObject(s.MinioStorage, "our-products", file, false)
if err != nil {
return err
}
return s.Repo.Update(id, &entity.OurProductContentImage{
ImagePath: key,
ImageURL: url,
})
}
func (s *ourProductContentImagesService) Delete(id uuid.UUID) error {
err := s.Repo.Delete(id)
if err != nil {
@ -70,4 +103,4 @@ func (s *ourProductContentImagesService) Delete(id uuid.UUID) error {
}
return nil
}
}

View File

@ -1,11 +1,13 @@
package repository
import (
"errors"
"web-qudo-be/app/database"
"web-qudo-be/app/database/entity"
"github.com/google/uuid"
"github.com/rs/zerolog"
"gorm.io/gorm"
)
type ourProductContentRepository struct {
@ -15,6 +17,7 @@ type ourProductContentRepository struct {
type OurProductContentRepository interface {
Get() (*entity.OurProductContent, error)
GetAll() ([]entity.OurProductContent, error)
Create(data *entity.OurProductContent) (*entity.OurProductContent, error)
Update(id uuid.UUID, data *entity.OurProductContent) error
Delete(id uuid.UUID) error
@ -35,6 +38,9 @@ func (r *ourProductContentRepository) Get() (*entity.OurProductContent, error) {
First(&data).Error
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, nil
}
r.Log.Error().Err(err).Msg("failed get our product content")
return nil, err
}
@ -42,6 +48,19 @@ func (r *ourProductContentRepository) Get() (*entity.OurProductContent, error) {
return &data, nil
}
func (r *ourProductContentRepository) GetAll() ([]entity.OurProductContent, error) {
var rows []entity.OurProductContent
err := r.DB.DB.
Preload("Images").
Order("created_at ASC").
Find(&rows).Error
if err != nil {
r.Log.Error().Err(err).Msg("failed list our product contents")
return nil, err
}
return rows, nil
}
func (r *ourProductContentRepository) Create(data *entity.OurProductContent) (*entity.OurProductContent, error) {
data.ID = uuid.New()

View File

@ -16,7 +16,7 @@ type ourProductContentService struct {
}
type OurProductContentService interface {
Show() (*entity.OurProductContent, error)
Show() ([]entity.OurProductContent, error)
Save(data *entity.OurProductContent) (*entity.OurProductContent, error)
Update(id uuid.UUID, data *entity.OurProductContent) error
Delete(id uuid.UUID) error
@ -34,14 +34,13 @@ func NewOurProductContentService(
}
}
func (s *ourProductContentService) Show() (*entity.OurProductContent, error) {
data, err := s.Repo.Get()
func (s *ourProductContentService) Show() ([]entity.OurProductContent, error) {
rows, err := s.Repo.GetAll()
if err != nil {
s.Log.Error().Err(err).Msg("failed get our product content")
s.Log.Error().Err(err).Msg("failed list our product contents")
return nil, err
}
return data, nil
return rows, nil
}
func (s *ourProductContentService) Save(data *entity.OurProductContent) (*entity.OurProductContent, error) {
@ -67,13 +66,5 @@ func (s *ourProductContentService) Update(id uuid.UUID, data *entity.OurProductC
}
func (s *ourProductContentService) Delete(id uuid.UUID) error {
result, err := s.Repo.Get()
if err != nil {
return err
}
isActive := false
result.IsActive = &isActive
return s.Repo.Update(id, result)
return s.Repo.Delete(id)
}

View File

@ -2,6 +2,7 @@ package controller
import (
"strconv"
"strings"
"github.com/gofiber/fiber/v2"
"github.com/rs/zerolog"
@ -56,6 +57,29 @@ func (_i *ourServiceContentImagesController) FindByOurServiceContentID(c *fiber.
}
func (_i *ourServiceContentImagesController) Save(c *fiber.Ctx) error {
if strings.HasPrefix(c.Get("Content-Type"), "multipart/form-data") {
cidStr := c.FormValue("our_service_content_id")
cidInt, err := strconv.Atoi(cidStr)
if err != nil {
return err
}
cid := uint(cidInt)
file, err := c.FormFile("file")
if err != nil {
return err
}
result, err := _i.service.SaveWithFile(cid, file)
if err != nil {
_i.Log.Error().Err(err).Msg("failed create our service content image (upload)")
return err
}
return utilRes.Resp(c, utilRes.Response{
Success: true,
Messages: utilRes.Messages{"Our service content image created"},
Data: result,
})
}
req := new(request.OurServiceContentImageCreateRequest)
if err := utilVal.ParseAndValidate(c, req); err != nil {
@ -87,6 +111,21 @@ func (_i *ourServiceContentImagesController) Update(c *fiber.Ctx) error {
id := uint(idInt)
if strings.HasPrefix(c.Get("Content-Type"), "multipart/form-data") {
file, err := c.FormFile("file")
if err != nil {
return err
}
if err := _i.service.UpdateWithFile(id, file); err != nil {
_i.Log.Error().Err(err).Msg("failed update our service content image (upload)")
return err
}
return utilRes.Resp(c, utilRes.Response{
Success: true,
Messages: utilRes.Messages{"Our service content image updated"},
})
}
req := new(request.OurServiceContentImageUpdateRequest)
if err := utilVal.ParseAndValidate(c, req); err != nil {
return err

View File

@ -1,31 +1,40 @@
package service
import (
"mime/multipart"
"github.com/rs/zerolog"
"web-qudo-be/app/database/entity"
"web-qudo-be/app/module/our_service_content_images/repository"
minioStorage "web-qudo-be/config/config"
"web-qudo-be/utils/storage"
)
type ourServiceContentImagesService struct {
Repo repository.OurServiceContentImagesRepository
Log zerolog.Logger
Repo repository.OurServiceContentImagesRepository
MinioStorage *minioStorage.MinioStorage
Log zerolog.Logger
}
type OurServiceContentImagesService interface {
FindByContentID(contentID uint) ([]entity.OurServiceContentImage, error)
Save(data *entity.OurServiceContentImage) (*entity.OurServiceContentImage, error)
SaveWithFile(ourServiceContentID uint, file *multipart.FileHeader) (*entity.OurServiceContentImage, error)
Update(id uint, data *entity.OurServiceContentImage) error
UpdateWithFile(id uint, file *multipart.FileHeader) error
Delete(id uint) error
}
func NewOurServiceContentImagesService(
repo repository.OurServiceContentImagesRepository,
minio *minioStorage.MinioStorage,
log zerolog.Logger,
) OurServiceContentImagesService {
return &ourServiceContentImagesService{
Repo: repo,
Log: log,
Repo: repo,
MinioStorage: minio,
Log: log,
}
}
@ -49,6 +58,19 @@ func (s *ourServiceContentImagesService) Save(data *entity.OurServiceContentImag
return result, nil
}
func (s *ourServiceContentImagesService) SaveWithFile(ourServiceContentID uint, file *multipart.FileHeader) (*entity.OurServiceContentImage, error) {
key, url, err := storage.UploadCMSObject(s.MinioStorage, "our-services", file, false)
if err != nil {
return nil, err
}
data := &entity.OurServiceContentImage{
OurServiceContentID: ourServiceContentID,
ImagePath: key,
ImageURL: url,
}
return s.Save(data)
}
func (s *ourServiceContentImagesService) Update(id uint, data *entity.OurServiceContentImage) error {
err := s.Repo.Update(id, data)
if err != nil {
@ -59,6 +81,17 @@ func (s *ourServiceContentImagesService) Update(id uint, data *entity.OurService
return nil
}
func (s *ourServiceContentImagesService) UpdateWithFile(id uint, file *multipart.FileHeader) error {
key, url, err := storage.UploadCMSObject(s.MinioStorage, "our-services", file, false)
if err != nil {
return err
}
return s.Repo.Update(id, &entity.OurServiceContentImage{
ImagePath: key,
ImageURL: url,
})
}
func (s *ourServiceContentImagesService) Delete(id uint) error {
err := s.Repo.Delete(id)
if err != nil {
@ -67,4 +100,4 @@ func (s *ourServiceContentImagesService) Delete(id uint) error {
}
return nil
}
}

View File

@ -1,10 +1,12 @@
package repository
import (
"errors"
"web-qudo-be/app/database"
"web-qudo-be/app/database/entity"
"github.com/rs/zerolog"
"gorm.io/gorm"
)
type ourServiceContentRepository struct {
@ -14,6 +16,7 @@ type ourServiceContentRepository struct {
type OurServiceContentRepository interface {
Get() (*entity.OurServiceContent, error)
GetAll() ([]entity.OurServiceContent, error)
Create(data *entity.OurServiceContent) (*entity.OurServiceContent, error)
Update(id uint, data *entity.OurServiceContent) error
Delete(id uint) error
@ -34,6 +37,9 @@ func (r *ourServiceContentRepository) Get() (*entity.OurServiceContent, error) {
First(&data).Error
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, nil
}
r.Log.Error().Err(err).Msg("failed get our service content")
return nil, err
}
@ -41,6 +47,19 @@ func (r *ourServiceContentRepository) Get() (*entity.OurServiceContent, error) {
return &data, nil
}
func (r *ourServiceContentRepository) GetAll() ([]entity.OurServiceContent, error) {
var rows []entity.OurServiceContent
err := r.DB.DB.
Preload("Images").
Order("created_at ASC").
Find(&rows).Error
if err != nil {
r.Log.Error().Err(err).Msg("failed list our service contents")
return nil, err
}
return rows, nil
}
func (r *ourServiceContentRepository) Create(data *entity.OurServiceContent) (*entity.OurServiceContent, error) {
err := r.DB.DB.Create(data).Error
if err != nil {

View File

@ -15,7 +15,7 @@ type ourServiceContentService struct {
}
type OurServiceContentService interface {
Show() (*entity.OurServiceContent, error)
Show() ([]entity.OurServiceContent, error)
Save(data *entity.OurServiceContent) (*entity.OurServiceContent, error)
Update(id uint, data *entity.OurServiceContent) error
Delete(id uint) error
@ -33,14 +33,13 @@ func NewOurServiceContentService(
}
}
func (s *ourServiceContentService) Show() (*entity.OurServiceContent, error) {
data, err := s.Repo.Get()
func (s *ourServiceContentService) Show() ([]entity.OurServiceContent, error) {
rows, err := s.Repo.GetAll()
if err != nil {
s.Log.Error().Err(err).Msg("failed get our service content")
s.Log.Error().Err(err).Msg("failed list our service contents")
return nil, err
}
return data, nil
return rows, nil
}
func (s *ourServiceContentService) Save(data *entity.OurServiceContent) (*entity.OurServiceContent, error) {
@ -75,13 +74,5 @@ func (s *ourServiceContentService) Update(id uint, data *entity.OurServiceConten
}
func (s *ourServiceContentService) Delete(id uint) error {
result, err := s.Repo.Get()
if err != nil {
return err
}
isActive := false
result.IsActive = &isActive
return s.Repo.Update(id, result)
return s.Repo.Delete(id)
}

View File

@ -21,6 +21,7 @@ type PartnerContentController interface {
Show(c *fiber.Ctx) error
Save(c *fiber.Ctx) error
Update(c *fiber.Ctx) error
UploadLogo(c *fiber.Ctx) error
Delete(c *fiber.Ctx) error
}
@ -94,6 +95,26 @@ func (_i *partnerContentController) Update(c *fiber.Ctx) error {
})
}
func (_i *partnerContentController) UploadLogo(c *fiber.Ctx) error {
idStr := c.Params("id")
id, err := uuid.Parse(idStr)
if err != nil {
return err
}
file, err := c.FormFile("file")
if err != nil {
return err
}
if err := _i.service.UploadLogo(id, file); err != nil {
_i.Log.Error().Err(err).Msg("failed upload partner logo")
return err
}
return utilRes.Resp(c, utilRes.Response{
Success: true,
Messages: utilRes.Messages{"Partner logo uploaded"},
})
}
func (_i *partnerContentController) Delete(c *fiber.Ctx) error {
idStr := c.Params("id")

View File

@ -42,6 +42,7 @@ func (_i *PartnerContentsRouter) RegisterPartnerContentsRoutes() {
_i.App.Route("/partner-contents", func(router fiber.Router) {
router.Get("/", partnerController.Show)
router.Post("/", partnerController.Save)
router.Post("/:id/logo", partnerController.UploadLogo)
router.Put("/:id", partnerController.Update)
router.Delete("/:id", partnerController.Delete)
})

View File

@ -17,6 +17,7 @@ type PartnerContentRepository interface {
Get() ([]entity.PartnerContent, error)
Create(data *entity.PartnerContent) (*entity.PartnerContent, error)
Update(id uuid.UUID, data *entity.PartnerContent) error
UpdateImageFields(id uuid.UUID, imagePath, imageURL string) error
Delete(id uuid.UUID) error
FindByID(id uuid.UUID) (*entity.PartnerContent, error) // opsional (buat soft delete)
}
@ -68,6 +69,21 @@ func (r *partnerContentRepository) Update(id uuid.UUID, data *entity.PartnerCont
return nil
}
func (r *partnerContentRepository) UpdateImageFields(id uuid.UUID, imagePath, imageURL string) error {
err := r.DB.DB.
Model(&entity.PartnerContent{}).
Where("id = ?", id).
Updates(map[string]interface{}{
"image_path": imagePath,
"image_url": imageURL,
}).Error
if err != nil {
r.Log.Error().Err(err).Msg("failed update partner logo")
return err
}
return nil
}
func (r *partnerContentRepository) Delete(id uuid.UUID) error {
err := r.DB.DB.Delete(&entity.PartnerContent{}, id).Error
if err != nil {

View File

@ -1,32 +1,40 @@
package service
import (
"mime/multipart"
"github.com/google/uuid"
"github.com/rs/zerolog"
"web-qudo-be/app/database/entity"
"web-qudo-be/app/module/partner_contents/repository"
minioStorage "web-qudo-be/config/config"
"web-qudo-be/utils/storage"
)
type partnerContentService struct {
Repo repository.PartnerContentRepository
Log zerolog.Logger
Repo repository.PartnerContentRepository
MinioStorage *minioStorage.MinioStorage
Log zerolog.Logger
}
type PartnerContentService interface {
Show() ([]entity.PartnerContent, error)
Save(data *entity.PartnerContent) (*entity.PartnerContent, error)
Update(id uuid.UUID, data *entity.PartnerContent) error
UploadLogo(id uuid.UUID, file *multipart.FileHeader) error
Delete(id uuid.UUID) error
}
func NewPartnerContentService(
repo repository.PartnerContentRepository,
minio *minioStorage.MinioStorage,
log zerolog.Logger,
) PartnerContentService {
return &partnerContentService{
Repo: repo,
Log: log,
Repo: repo,
MinioStorage: minio,
Log: log,
}
}
@ -62,6 +70,14 @@ func (s *partnerContentService) Update(id uuid.UUID, data *entity.PartnerContent
return nil
}
func (s *partnerContentService) UploadLogo(id uuid.UUID, file *multipart.FileHeader) error {
key, url, err := storage.UploadCMSObject(s.MinioStorage, "partners", file, false)
if err != nil {
return err
}
return s.Repo.UpdateImageFields(id, key, url)
}
func (s *partnerContentService) Delete(id uuid.UUID) error {
err := s.Repo.Delete(id)
if err != nil {

View File

@ -2,6 +2,7 @@ package controller
import (
"strconv"
"strings"
"github.com/gofiber/fiber/v2"
@ -39,6 +40,32 @@ func NewPopupNewsContentImagesController(s service.PopupNewsContentImagesService
// @Failure 500 {object} response.Response
// @Router /popup-news-content-images [post]
func (_i *popupNewsContentImagesController) Save(c *fiber.Ctx) error {
if strings.HasPrefix(c.Get("Content-Type"), "multipart/form-data") {
cidStr := c.FormValue("popup_news_content_id")
cid64, err := strconv.ParseUint(cidStr, 10, 32)
if err != nil {
return err
}
file, err := c.FormFile("file")
if err != nil {
return err
}
var isThumb *bool
if v := strings.TrimSpace(c.FormValue("is_thumbnail")); v != "" {
b := v == "true" || v == "1" || v == "on"
isThumb = &b
}
result, err := _i.service.SaveWithFile(uint(cid64), file, isThumb)
if err != nil {
return err
}
return utilRes.Resp(c, utilRes.Response{
Success: true,
Messages: utilRes.Messages{"Popup news content image successfully uploaded"},
Data: result,
})
}
req := new(request.PopupNewsContentImagesCreateRequest)
if err := utilVal.ParseAndValidate(c, req); err != nil {
return err
@ -50,6 +77,7 @@ func (_i *popupNewsContentImagesController) Save(c *fiber.Ctx) error {
}
return utilRes.Resp(c, utilRes.Response{
Success: true,
Messages: utilRes.Messages{"Popup news content image successfully uploaded"},
})
}

View File

@ -1,40 +1,76 @@
package service
import (
"mime/multipart"
"github.com/rs/zerolog"
"web-qudo-be/app/database/entity"
"web-qudo-be/app/module/popup_news_content_images/repository"
"web-qudo-be/app/module/popup_news_content_images/request"
minioStorage "web-qudo-be/config/config"
"web-qudo-be/utils/storage"
)
// service struct
type popupNewsContentImagesService struct {
Repo repository.PopupNewsContentImagesRepository
Log zerolog.Logger
Repo repository.PopupNewsContentImagesRepository
MinioStorage *minioStorage.MinioStorage
Log zerolog.Logger
}
// interface
type PopupNewsContentImagesService interface {
Save(req request.PopupNewsContentImagesCreateRequest) error
SaveWithFile(popupNewsContentID uint, file *multipart.FileHeader, isThumbnail *bool) (*entity.PopupNewsContentImages, error)
Delete(id uint) error
}
// constructor
func NewPopupNewsContentImagesService(repo repository.PopupNewsContentImagesRepository, log zerolog.Logger) PopupNewsContentImagesService {
func NewPopupNewsContentImagesService(
repo repository.PopupNewsContentImagesRepository,
minio *minioStorage.MinioStorage,
log zerolog.Logger,
) PopupNewsContentImagesService {
return &popupNewsContentImagesService{
Repo: repo,
Log: log,
Repo: repo,
MinioStorage: minio,
Log: log,
}
}
// Save
func (_i *popupNewsContentImagesService) Save(req request.PopupNewsContentImagesCreateRequest) error {
_i.Log.Info().Interface("data", req).Msg("upload popup news content image")
_i.Log.Info().Interface("data", req).Msg("create popup news content image (json)")
return _i.Repo.Create(req.ToEntity())
}
// Delete
func (_i *popupNewsContentImagesService) SaveWithFile(popupNewsContentID uint, file *multipart.FileHeader, isThumbnail *bool) (*entity.PopupNewsContentImages, error) {
_i.Log.Info().
Uint("popup_news_content_id", popupNewsContentID).
Str("filename", file.Filename).
Msg("upload popup news content image")
key, url, err := storage.UploadCMSObject(_i.MinioStorage, "popup-news", file, false)
if err != nil {
return nil, err
}
if isThumbnail != nil && *isThumbnail {
if err := _i.Repo.ResetThumbnail(popupNewsContentID); err != nil {
return nil, err
}
}
row := &entity.PopupNewsContentImages{
PopupNewsContentID: popupNewsContentID,
MediaPath: key,
MediaURL: url,
IsThumbnail: isThumbnail,
}
if err := _i.Repo.Create(row); err != nil {
return nil, err
}
return row, nil
}
func (_i *popupNewsContentImagesService) Delete(id uint) error {
return _i.Repo.Delete(id)
}
}

View File

@ -110,13 +110,14 @@ func (_i *popupNewsContentsController) Save(c *fiber.Ctx) error {
return err
}
err := _i.service.Save(*req)
data, err := _i.service.Save(*req)
if err != nil {
return err
}
return utilRes.Resp(c, utilRes.Response{
Messages: utilRes.Messages{"Popup news content successfully created"},
Data: data,
})
}

View File

@ -20,7 +20,7 @@ type popupNewsContentsService struct {
type PopupNewsContentsService interface {
All(req request.PopupNewsContentsQueryRequest) (data []*response.PopupNewsContentsResponse, paging paginator.Pagination, err error)
Show(id uint) (*response.PopupNewsContentsResponse, error)
Save(req request.PopupNewsContentsCreateRequest) error
Save(req request.PopupNewsContentsCreateRequest) (*response.PopupNewsContentsResponse, error)
Update(id uint, req request.PopupNewsContentsUpdateRequest) error
Delete(id uint) error
}
@ -58,10 +58,14 @@ func (_i *popupNewsContentsService) Show(id uint) (*response.PopupNewsContentsRe
}
// Save
func (_i *popupNewsContentsService) Save(req request.PopupNewsContentsCreateRequest) error {
func (_i *popupNewsContentsService) Save(req request.PopupNewsContentsCreateRequest) (*response.PopupNewsContentsResponse, error) {
_i.Log.Info().Interface("data", req).Msg("create popup news content")
return _i.Repo.Create(req.ToEntity())
ent := req.ToEntity()
if err := _i.Repo.Create(ent); err != nil {
return nil, err
}
return mapper.PopupNewsContentsResponseMapper(ent), nil
}
// Update

View File

@ -20,6 +20,7 @@ import (
"web-qudo-be/app/module/cities"
"web-qudo-be/app/module/client_approval_settings"
"web-qudo-be/app/module/clients"
"web-qudo-be/app/module/cms_media"
"web-qudo-be/app/module/custom_static_pages"
"web-qudo-be/app/module/districts"
"web-qudo-be/app/module/feedbacks"
@ -72,7 +73,8 @@ type Router struct {
CitiesRouter *cities.CitiesRouter
ClientApprovalSettingsRouter *client_approval_settings.ClientApprovalSettingsRouter
ClientsRouter *clients.ClientsRouter
HeroContentsRouter *hero_content.HeroContentsRouter
CmsMediaRouter *cms_media.CmsMediaRouter
HeroContentsRouter *hero_content.HeroContentsRouter
HeroContentImagesRouter *hero_content_image.HeroContentImagesRouter
CustomStaticPagesRouter *custom_static_pages.CustomStaticPagesRouter
DistrictsRouter *districts.DistrictsRouter
@ -119,6 +121,7 @@ func NewRouter(
citiesRouter *cities.CitiesRouter,
clientApprovalSettingsRouter *client_approval_settings.ClientApprovalSettingsRouter,
clientsRouter *clients.ClientsRouter,
cmsMediaRouter *cms_media.CmsMediaRouter,
heroContentsRouter *hero_content.HeroContentsRouter,
heroContentImagesRouter *hero_content_image.HeroContentImagesRouter,
customStaticPagesRouter *custom_static_pages.CustomStaticPagesRouter,
@ -165,6 +168,7 @@ func NewRouter(
CitiesRouter: citiesRouter,
ClientApprovalSettingsRouter: clientApprovalSettingsRouter,
ClientsRouter: clientsRouter,
CmsMediaRouter: cmsMediaRouter,
HeroContentsRouter: heroContentsRouter,
HeroContentImagesRouter: heroContentImagesRouter,
CustomStaticPagesRouter: customStaticPagesRouter,
@ -221,6 +225,7 @@ func (r *Router) Register() {
r.CitiesRouter.RegisterCitiesRoutes()
r.ClientApprovalSettingsRouter.RegisterClientApprovalSettingsRoutes()
r.ClientsRouter.RegisterClientsRoutes()
r.CmsMediaRouter.RegisterCmsMediaRoutes()
r.HeroContentsRouter.RegisterHeroContentsRoutes()
r.HeroContentImagesRouter.RegisterHeroContentImagesRoutes()
r.CustomStaticPagesRouter.RegisterCustomStaticPagesRoutes()

View File

@ -128,6 +128,23 @@ type Config struct {
Smtp smtp
}
// APIPublicBaseURL is the base URL embedded in links returned to clients (e.g. CMS image preview).
// If app.production is true, it uses app.domain (trimmed). Otherwise it uses http://localhost plus app.port
// so local runs match opening the API in the browser without rewriting https://qudo.id/api.
func (c *Config) APIPublicBaseURL() string {
if c.App.Production {
return strings.TrimSuffix(c.App.Domain, "/")
}
port := strings.TrimSpace(c.App.Port)
if port == "" {
return "http://localhost"
}
if !strings.HasPrefix(port, ":") {
port = ":" + port
}
return "http://localhost" + port
}
// NewConfig : initialize config
func NewConfig() *Config {
config, err := ParseConfig("config")

View File

@ -2,6 +2,7 @@ package config
import (
"context"
"fmt"
"log"
"github.com/minio/minio-go/v7"
@ -69,3 +70,13 @@ func (_minio *MinioStorage) ConnectMinio() (*minio.Client, error) {
log.Printf("[MinIO] Successfully connected to MinIO and bucket '%s' is ready", bucketName)
return minioClient, nil
}
// PublicObjectURL builds a path-style URL for public reads (bucket policy must allow GET).
func (m *MinioStorage) PublicObjectURL(objectKey string) string {
o := m.Cfg.ObjectStorage.MinioStorage
scheme := "http"
if o.UseSSL {
scheme = "https"
}
return fmt.Sprintf("%s://%s/%s/%s", scheme, o.Endpoint, o.BucketName, objectKey)
}

View File

@ -8,11 +8,12 @@ external-port = ":8812"
idle-timeout = 5 # As seconds
print-routes = false
prefork = false
# false: CMS preview URLs use http://localhost + port above. true: use domain (e.g. https://qudo.id/api).
production = false
body-limit = 1048576000 # "100 * 1024 * 1024"
[db.postgres]
dsn = "postgresql://medols_user:MedolsDB@2025@38.47.185.79:5432/medols_db" # <driver>://<username>:<password>@<host>:<port>/<database>
dsn = "postgresql://qudo_user:QudoDB@2026@38.47.185.79:5432/qudo_db" # <driver>://<username>:<password>@<host>:<port>/<database>
log-mode = "ERROR"
migrate = true
seed = false
@ -28,7 +29,7 @@ endpoint = "is3.cloudhost.id"
access-key-id = "YRP1RM617986USRU6NN8"
secret-access-key = "vfbwQDYb1m7nfzo4LVEz90BIyOWfBMZ6bfGQbqDO"
use-ssl = true
bucket-name = "mikulnews"
bucket-name = "qudo"
location = "us-east-1"
[middleware.compress]

View File

@ -23,6 +23,7 @@ import (
"web-qudo-be/app/module/cities"
"web-qudo-be/app/module/client_approval_settings"
"web-qudo-be/app/module/clients"
"web-qudo-be/app/module/cms_media"
"web-qudo-be/app/module/custom_static_pages"
"web-qudo-be/app/module/districts"
"web-qudo-be/app/module/feedbacks"
@ -104,6 +105,7 @@ func main() {
cities.NewCitiesModule,
client_approval_settings.NewClientApprovalSettingsModule,
clients.NewClientsModule,
cms_media.NewCmsMediaModule,
custom_static_pages.NewCustomStaticPagesModule,
districts.NewDistrictsModule,
feedbacks.NewFeedbacksModule,

View File

@ -0,0 +1,76 @@
package storage
import (
"context"
"fmt"
"mime"
"mime/multipart"
"path/filepath"
"strings"
"time"
"github.com/google/uuid"
"github.com/minio/minio-go/v7"
appcfg "web-qudo-be/config/config"
)
// CMSPreviewURL is the absolute URL served by this API (GET /cms-media/viewer/...) for DB image_url / media_url fields.
func CMSPreviewURL(cfg *appcfg.Config, objectKey string) string {
base := cfg.APIPublicBaseURL()
key := strings.TrimPrefix(strings.TrimSpace(objectKey), "/")
return base + "/cms-media/viewer/" + key
}
var imageExts = map[string]bool{
".jpg": true, ".jpeg": true, ".png": true, ".gif": true, ".webp": true,
}
var mediaExts = map[string]bool{
".jpg": true, ".jpeg": true, ".png": true, ".gif": true, ".webp": true,
".mp4": true, ".webm": true,
}
// UploadCMSObject stores a file in MinIO under cms/{folder}/YYYY/MM/{uuid}{ext} and returns object key + preview URL (API viewer, not direct MinIO).
func UploadCMSObject(ms *appcfg.MinioStorage, folder string, file *multipart.FileHeader, allowVideo bool) (objectKey string, previewURL string, err error) {
if file == nil {
return "", "", fmt.Errorf("file is required")
}
ext := strings.ToLower(filepath.Ext(file.Filename))
if allowVideo {
if !mediaExts[ext] {
return "", "", fmt.Errorf("unsupported file type (allowed: images, mp4, webm)")
}
} else if !imageExts[ext] {
return "", "", fmt.Errorf("unsupported image type")
}
client, err := ms.ConnectMinio()
if err != nil {
return "", "", err
}
src, err := file.Open()
if err != nil {
return "", "", err
}
defer src.Close()
bucket := ms.Cfg.ObjectStorage.MinioStorage.BucketName
now := time.Now()
objectKey = fmt.Sprintf("cms/%s/%d/%02d/%s%s", folder, now.Year(), int(now.Month()), uuid.New().String(), ext)
contentType := mime.TypeByExtension(ext)
if contentType == "" {
contentType = "application/octet-stream"
}
_, err = client.PutObject(context.Background(), bucket, objectKey, src, file.Size, minio.PutObjectOptions{
ContentType: contentType,
})
if err != nil {
return "", "", err
}
return objectKey, CMSPreviewURL(ms.Cfg, objectKey), nil
}

Binary file not shown.