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), db: AsyncSession = Depends(get_session),
) -> PaginatedResponse: ) -> PaginatedResponse:
"""List technique pages with optional category/creator filtering.""" """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: if category:
stmt = stmt.where(TechniquePage.topic_category == category) base_stmt = base_stmt.where(TechniquePage.topic_category == category)
if creator_slug: if creator_slug:
# Join to Creator to filter by slug base_stmt = base_stmt.join(Creator, TechniquePage.creator_id == Creator.id).where(
stmt = stmt.join(Creator, TechniquePage.creator_id == Creator.id).where(
Creator.slug == creator_slug Creator.slug == creator_slug
) )
# Count total before pagination # Count total before pagination
from sqlalchemy import func count_stmt = select(func.count()).select_from(base_stmt.subquery())
count_stmt = select(func.count()).select_from(stmt.subquery())
count_result = await db.execute(count_stmt) count_result = await db.execute(count_stmt)
total = count_result.scalar() or 0 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) stmt = stmt.options(selectinload(TechniquePage.creator)).order_by(TechniquePage.created_at.desc()).offset(offset).limit(limit)
result = await db.execute(stmt) result = await db.execute(stmt)
pages = result.scalars().all() rows = result.all()
items = [] items = []
for p in pages: for row in rows:
p = row[0]
km_count = row[1] or 0
item = TechniquePageRead.model_validate(p) item = TechniquePageRead.model_validate(p)
if p.creator: if p.creator:
item.creator_name = p.creator.name item.creator_name = p.creator.name
item.creator_slug = p.creator.slug item.creator_slug = p.creator.slug
item.key_moment_count = km_count
items.append(item) items.append(item)
return PaginatedResponse( return PaginatedResponse(

View file

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

View file

@ -1110,6 +1110,17 @@ a.app-footer__repo:hover {
line-height: 1.4; 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 ──────────────────────────────────────────────────── */
.search-results-page { .search-results-page {

View file

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

View file

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