feat: Added key_moment_count correlated subquery to technique list API…

- "backend/schemas.py"
- "backend/routers/techniques.py"
- "frontend/src/api/public-client.ts"
- "frontend/src/pages/Home.tsx"
- "frontend/src/App.css"

GSD-Task: S03/T01
This commit is contained in:
jlightner 2026-03-31 05:23:37 +00:00
parent 0c8bbb32d6
commit 80439d43cf
5 changed files with 50 additions and 10 deletions

View file

@ -38,34 +38,53 @@ async def list_techniques(
db: AsyncSession = Depends(get_session),
) -> PaginatedResponse:
"""List technique pages with optional category/creator filtering."""
stmt = select(TechniquePage)
# Correlated subquery for key moment count (same pattern as creators.py)
key_moment_count_sq = (
select(func.count())
.where(KeyMoment.technique_page_id == TechniquePage.id)
.correlate(TechniquePage)
.scalar_subquery()
)
# Build base query with filters
base_stmt = select(TechniquePage.id)
if category:
stmt = stmt.where(TechniquePage.topic_category == category)
base_stmt = base_stmt.where(TechniquePage.topic_category == category)
if creator_slug:
# Join to Creator to filter by slug
stmt = stmt.join(Creator, TechniquePage.creator_id == Creator.id).where(
base_stmt = base_stmt.join(Creator, TechniquePage.creator_id == Creator.id).where(
Creator.slug == creator_slug
)
# Count total before pagination
from sqlalchemy import func
count_stmt = select(func.count()).select_from(stmt.subquery())
count_stmt = select(func.count()).select_from(base_stmt.subquery())
count_result = await db.execute(count_stmt)
total = count_result.scalar() or 0
# Main query with subquery column
stmt = select(
TechniquePage,
key_moment_count_sq.label("key_moment_count"),
)
if category:
stmt = stmt.where(TechniquePage.topic_category == category)
if creator_slug:
stmt = stmt.join(Creator, TechniquePage.creator_id == Creator.id).where(
Creator.slug == creator_slug
)
stmt = stmt.options(selectinload(TechniquePage.creator)).order_by(TechniquePage.created_at.desc()).offset(offset).limit(limit)
result = await db.execute(stmt)
pages = result.scalars().all()
rows = result.all()
items = []
for p in pages:
for row in rows:
p = row[0]
km_count = row[1] or 0
item = TechniquePageRead.model_validate(p)
if p.creator:
item.creator_name = p.creator.name
item.creator_slug = p.creator.slug
item.key_moment_count = km_count
items.append(item)
return PaginatedResponse(

View file

@ -138,6 +138,7 @@ class TechniquePageRead(TechniquePageBase):
creator_slug: str = ""
source_quality: str | None = None
view_count: int = 0
key_moment_count: int = 0
created_at: datetime
updated_at: datetime

View file

@ -1110,6 +1110,17 @@ a.app-footer__repo:hover {
line-height: 1.4;
}
.recent-card__moments {
font-size: 0.75rem;
color: var(--color-text-tertiary);
white-space: nowrap;
}
.pill--tag {
font-size: 0.625rem;
padding: 0 0.375rem;
}
/* ── Search results page ──────────────────────────────────────────────────── */
.search-results-page {

View file

@ -102,6 +102,7 @@ export interface TechniqueListItem {
creator_slug: string;
source_quality: string | null;
view_count: number;
key_moment_count: number;
created_at: string;
updated_at: string;
}

View file

@ -210,6 +210,9 @@ export default function Home() {
<span className="badge badge--category">
{t.topic_category}
</span>
{t.topic_tags && t.topic_tags.length > 0 && t.topic_tags.map(tag => (
<span key={tag} className="pill pill--tag">{tag}</span>
))}
{t.summary && (
<span className="recent-card__summary">
{t.summary.length > 100
@ -217,6 +220,11 @@ export default function Home() {
: t.summary}
</span>
)}
{t.key_moment_count > 0 && (
<span className="recent-card__moments">
{t.key_moment_count} moment{t.key_moment_count !== 1 ? 's' : ''}
</span>
)}
</span>
</Link>
))}