fix(templates): real scene count on template pages (was always 0)

The card + detail read template.sceneCount, but the API never sent one — so the
frontend mapper hardcoded sceneCount:0 for every DB-backed template.

- content-svc: ContainerSummaryResponse + ContainerDetailResponse now carry
  SceneCount. The list computes it with one grouped query (scenes per aspect project,
  max across aspects); the detail loads scenes and counts them.
- frontend: V2ContainerSummary.scene_count → AdminProject.sceneCount → the catalog
  card/detail (adminProjectToCatalogTemplate no longer hardcodes 0).

Verified on the live local API: fr-instagram-promo → 5, single-scene templates → 1.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
soroush.asadi
2026-06-25 10:51:12 +03:30
parent 21b6a30f08
commit a36e96d933
4 changed files with 27 additions and 9 deletions
@@ -51,8 +51,18 @@ public class TemplateService(ContentDbContext db)
var total = await q.LongCountAsync();
var items = await q.Skip((req.Page - 1) * req.PageSize).Take(req.PageSize).ToListAsync();
// Scene count per template = scenes in a single aspect project (max across
// projects). One grouped query, no scene entities loaded.
var ids = items.Select(i => i.Id).ToList();
var sceneCounts = (await db.Projects
.Where(p => p.DeletedAt == null && ids.Contains(p.ContainerId))
.Select(p => new { p.ContainerId, Cnt = p.Scenes.Count(s => s.DeletedAt == null) })
.ToListAsync())
.GroupBy(x => x.ContainerId)
.ToDictionary(g => g.Key, g => g.Max(x => x.Cnt));
return new PagedResponse<ContainerSummaryResponse>(
items.Select(MapContainerSummary),
items.Select(c => MapContainerSummary(c, sceneCounts.GetValueOrDefault(c.Id, 0))),
new PaginationMeta(req.Page, req.PageSize, total, (int)Math.Ceiling((double)total / req.PageSize))
);
}
@@ -62,7 +72,7 @@ public class TemplateService(ContentDbContext db)
var container = await db.ProjectContainers
.Include(x => x.ContainerCategories).ThenInclude(x => x.Category)
.Include(x => x.ContainerTags).ThenInclude(x => x.Tag)
.Include(x => x.Projects.Where(p => p.DeletedAt == null))
.Include(x => x.Projects.Where(p => p.DeletedAt == null)).ThenInclude(p => p.Scenes.Where(s => s.DeletedAt == null))
.FirstOrDefaultAsync(x => x.Slug == slug)
?? throw new KeyNotFoundException($"Container '{slug}' not found");
@@ -74,7 +84,7 @@ public class TemplateService(ContentDbContext db)
var container = await db.ProjectContainers
.Include(x => x.ContainerCategories).ThenInclude(x => x.Category)
.Include(x => x.ContainerTags).ThenInclude(x => x.Tag)
.Include(x => x.Projects.Where(p => p.DeletedAt == null))
.Include(x => x.Projects.Where(p => p.DeletedAt == null)).ThenInclude(p => p.Scenes.Where(s => s.DeletedAt == null))
.FirstOrDefaultAsync(x => x.Id == id)
?? throw new KeyNotFoundException($"Container {id} not found");
@@ -450,12 +460,13 @@ public class TemplateService(ContentDbContext db)
await db.SaveChangesAsync();
}
private static ContainerSummaryResponse MapContainerSummary(ProjectContainer c) => new(
private static ContainerSummaryResponse MapContainerSummary(ProjectContainer c, int sceneCount = 0) => new(
c.Id, c.Slug, c.Name, c.Description, c.Image, c.Demo, c.MiniDemo,
c.IsPublished, c.IsPremium, c.IsMockup, c.PrimaryMode.ToString(),
c.RateAvg, c.RateCount, c.ViewCount, c.UseCount, c.Sort, c.SortDate,
c.ContainerCategories.Select(cc => cc.Category.Slug).ToList(),
c.ContainerTags.Select(ct => ct.Tag.Name).ToList()
c.ContainerTags.Select(ct => ct.Tag.Name).ToList(),
sceneCount
);
private static ContainerDetailResponse MapContainerDetail(ProjectContainer c) => new(
@@ -465,7 +476,8 @@ public class TemplateService(ContentDbContext db)
c.RateAvg, c.RateCount, c.ViewCount, c.UseCount, c.Sort, c.SortDate,
c.Projects.Select(MapProject).ToList(),
c.ContainerCategories.Select(cc => MapCategoryFlat(cc.Category)).ToList(),
c.ContainerTags.Select(ct => new TagResponse(ct.Tag.Id, ct.Tag.Name, ct.Tag.LatinName, ct.Tag.Slug, ct.Tag.AppliesToMode, ct.Tag.IsActive)).ToList()
c.ContainerTags.Select(ct => new TagResponse(ct.Tag.Id, ct.Tag.Name, ct.Tag.LatinName, ct.Tag.Slug, ct.Tag.AppliesToMode, ct.Tag.IsActive)).ToList(),
c.Projects.Count == 0 ? 0 : c.Projects.Max(p => p.Scenes.Count)
);
private static CategoryResponse MapCategoryFlat(Category c) => new(
@@ -82,7 +82,8 @@ public record ContainerSummaryResponse(
int Sort,
DateTime SortDate,
List<string> CategorySlugs,
List<string> Tags
List<string> Tags,
int SceneCount
);
public record ContainerDetailResponse(
@@ -109,7 +110,8 @@ public record ContainerDetailResponse(
DateTime SortDate,
List<ProjectResponse> Projects,
List<CategoryResponse> Categories,
List<TagResponse> Tags
List<TagResponse> Tags,
int SceneCount
);
public record ProjectResponse(
+3
View File
@@ -39,6 +39,7 @@ export interface AdminProject {
metaJson?: string;
sortOrder: number;
mediaCount: number;
sceneCount: number;
createdAt: string;
updatedAt: string;
}
@@ -85,6 +86,7 @@ interface V2ContainerSummary {
sort_date: string;
category_slugs: string[];
tags: string[];
scene_count?: number;
}
interface V2PagedContainers {
@@ -130,6 +132,7 @@ function containerToAdminProject(c: V2ContainerSummary): AdminProject {
metaJson: undefined,
sortOrder: c.sort,
mediaCount: 0,
sceneCount: c.scene_count ?? 0,
createdAt: c.sort_date ?? "",
updatedAt: c.sort_date ?? "",
};
+2 -1
View File
@@ -511,6 +511,7 @@ export interface AdminProjectLike {
categoryName?: string;
coverImageUrl?: string;
previewVideoUrl?: string;
sceneCount?: number;
}
export function adminProjectToCatalogTemplate(
@@ -523,7 +524,7 @@ export function adminProjectToCatalogTemplate(
aspectRatio: "widescreen",
durationType: "flexible",
premium: false,
sceneCount: 0,
sceneCount: p.sceneCount ?? 0,
supports4k: false,
colorChange: true,
scriptToVideo: false,