package landing import ( "context" "strings" "sync" "time" "github.com/google/uuid" "github.com/rs/zerolog" "go.uber.org/fx" "golang.org/x/sync/errgroup" domainAsset "base/internal/domain/asset" domainProfile "base/internal/domain/profile" "base/internal/dto" "base/pkg/cache" ) const ( defaultAssetsPerCategory = 6 defaultSpecialistsLimit = 6 defaultRolesLimit = 20 landingCacheKey = "landing:page" landingCacheTTL = 5 * time.Minute ) type Service interface { GetLanding(ctx context.Context) (*dto.Landing, error) } type service struct { logger zerolog.Logger cache cache.Cache[dto.Landing] categoryRepo domainAsset.CategoryRepository assetRepo domainAsset.AssetRepository profileRepo domainProfile.Repository roleRepo domainProfile.RoleRepository } type Param struct { Logger zerolog.Logger Cache cache.Cache[dto.Landing] CategoryRepo domainAsset.CategoryRepository AssetRepo domainAsset.AssetRepository ProfileRepo domainProfile.Repository RoleRepo domainProfile.RoleRepository fx.In } func New(param Param) Service { return &service{ logger: param.Logger, cache: param.Cache, categoryRepo: param.CategoryRepo, assetRepo: param.AssetRepo, profileRepo: param.ProfileRepo, roleRepo: param.RoleRepo, } } func (s *service) GetLanding(ctx context.Context) (*dto.Landing, error) { result, err := s.cache.WithCache(ctx, landingCacheKey, s.fetchLanding, landingCacheTTL) if err != nil { return nil, err } return &result, nil } func (s *service) fetchLanding(ctx context.Context) (dto.Landing, error) { data := &dto.LandingPageData{ Categories: []dto.CategoryDTO{}, SpecialistRoles: []dto.ProfileRole{}, Assets: []dto.LandingAssetData{}, Specialists: []dto.Specialist{}, Blogs: []dto.Blog{}, } g, gCtx := errgroup.WithContext(ctx) g.Go(func() error { categories, err := s.categoryRepo.FindAll(gCtx) if err != nil { return err } data.Categories = make([]dto.CategoryDTO, len(categories)) for i, c := range categories { data.Categories[i] = dto.CategoryDTO{ ID: c.ID, Name: c.Name, Icon: c.Icon, Color: c.Color, CardType: c.CardType, Featured: c.Featured, Description: c.Description, } } return nil }) g.Go(func() error { domainRoles, err := s.roleRepo.List(gCtx, defaultRolesLimit, 0) if err != nil { return err } data.SpecialistRoles = make([]dto.ProfileRole, len(domainRoles)) for i, r := range domainRoles { data.SpecialistRoles[i] = dto.ProfileRole{ Id: r.ID.String(), Title: r.Title, } } return nil }) g.Go(func() error { profiles, _, err := s.profileRepo.FindAll( gCtx, domainProfile.Filter{ Page: 1, PageSize: defaultSpecialistsLimit, SortedBy: "created_at", Ascending: false, }) if err != nil { return err } data.Specialists = make([]dto.Specialist, len(profiles)) for i, p := range profiles { data.Specialists[i] = dto.Specialist{ Id: p.ID.String(), Handle: p.Handle, Avatar: p.About.ProfilePicture, } } return nil }) g.Go(func() error { categories, err := s.categoryRepo.FindAll(gCtx) if err != nil { return err } assetsByCat := make([]dto.LandingAssetData, len(categories)) mu := &sync.Mutex{} eg, egCtx := errgroup.WithContext(gCtx) for index, category := range categories { i, cat := index, category eg.Go(func() error { assets, findLatestAssetErr := s.assetRepo.FindLatestByCategory(egCtx, cat.ID, defaultAssetsPerCategory) if findLatestAssetErr != nil { return findLatestAssetErr } assetResp := make([]dto.AssetResponse, len(assets)) for j, a := range assets { assetResp[j] = *s.toAssetResponse(a) } mu.Lock() assetsByCat[i] = dto.LandingAssetData{ AssetCategory: dto.AssetCategory{ Id: cat.ID.String(), Title: cat.Name, Icon: cat.Icon, }, Assets: assetResp, } mu.Unlock() return nil }) } if err = eg.Wait(); err != nil { return err } data.Assets = assetsByCat return nil }) if err := g.Wait(); err != nil { return dto.Landing{}, err } return dto.Landing{ Message: "Landing page fetched successfully", Data: *data, }, nil } func (s *service) toAssetResponse(a *domainAsset.Asset) *dto.AssetResponse { coverImage := "" for _, art := range a.AssetArtifacts { if strings.Contains(strings.ToLower(art.Type), "image") { coverImage = art.DownloadURL break } } resp := &dto.AssetResponse{ ID: a.ID, ProfileID: a.ProfileID, AssetCategoryID: a.AssetCategoryID, Title: a.Title, Description: a.Description, Link: a.Link, CoverImage: coverImage, Status: int(a.Status), CreatedAt: formatTime(a.CreatedAt), UpdatedAt: formatTime(a.UpdatedAt), } if a.AssetCategory.ID != uuid.Nil { resp.Category = dto.CategoryDTO{ ID: a.AssetCategory.ID, Name: a.AssetCategory.Name, Icon: a.AssetCategory.Icon, Color: a.AssetCategory.Color, CardType: a.AssetCategory.CardType, Featured: a.AssetCategory.Featured, Description: a.AssetCategory.Description, } } return resp } func formatTime(t time.Time) string { if t.IsZero() { return "" } return t.Format(time.RFC3339) }