Skip to content

Commit 083ea25

Browse files
authored
Merge pull request #58 from alimkhann/feature/search-filters-pagination
Feature/search filters pagination
2 parents eacd74a + 1d9e97a commit 083ea25

File tree

12 files changed

+787
-63
lines changed

12 files changed

+787
-63
lines changed

NOTES.md

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
# Prompt 1 — Core Data Model Upgrades (GeneratedRoadmap + User Repo State)
2+
3+
Date: 2025-11-16
4+
5+
## Changes made
6+
- Extended `generated_roadmaps` table/model with richer metadata columns:
7+
- `primary_language`, `languages`, `topics`, `difficulty`, `star_count`, `fork_count`, `last_pushed_at`, `license`, `contributor_count`, `view_count`, `sync_count`, `rating_count`, `rating_sum`.
8+
- Extended per-user repo state (`user_synced_repos` table/model) with lifecycle and progress fields:
9+
- `status`, `is_archived`, `progress_percent`, timestamps `created_at`, `updated_at` (kept `pinned_at`).
10+
- Updated Pydantic `RoadmapRepoSummary` to accept the new metadata fields (all optional to stay backward compatible). Existing API shapes are unchanged.
11+
12+
## Prompt 2 additions (Your Repositories + statuses)
13+
- Added `UserRepoStateResponse` schema and backend endpoint `GET /api/v1/roadmap/user-repos` (auth) to return per-user repo states with status, archive flag, progress, and basic roadmap summary.
14+
- Enhanced `UserSyncedRepoStore` with `upsert_state` and `list_states` to manage statusful per-user repos.
15+
- Frontend `repoService` now has `listUserRepos`; `RoadmapCatalogProvider` fetches/stores `yourRepos` with Clerk auth token.
16+
- Sidebar section renamed to “Your repositories”; now shows status badges and includes unsynced entries (non-archived), falling back to old synced list if no user data.
17+
- Search page “Your repositories” section uses the new user-repo list and keeps stage counts via cached synced map.
18+
19+
## Public Repositories Catalog (Prompt 3)
20+
- Backend `/api/v1/roadmap/catalog` now paginated (`items`, `page`, `page_size`, `total_count`, `total_pages`) backed by `GeneratedRoadmap` with new metadata included.
21+
- Roadmap service exposes `list_catalog`; repository store supports paginated listing.
22+
- Frontend `repoService.listCatalog(page, pageSize)` returns catalog page; `RoadmapCatalogProvider` consumes `.items` to populate synced list.
23+
- Search page renders a separate “Public repositories” section using the new endpoint (with static fallback when API is unconfigured). Displays stage count, language, and links to timelines.
24+
25+
## Migrations
26+
- Added Alembic revision `20241116_upgrade_roadmaps_and_user_repo_state.py` (down_revision `20241115_add_user_synced_repos`).
27+
- New columns have safe defaults (`0`, `false`, or nullable) to allow migration on existing data.
28+
29+
## Notes / Next steps
30+
- No API or UI wiring yet; data can be populated later when GitHub fetch/sync logic is updated.
31+
- `progress_percent` currently unused; kept within 0–100 expectation but not enforced yet (could add a CHECK later if needed).
32+
33+
## How to apply
34+
```bash
35+
cd commitly-backend
36+
alembic upgrade head
37+
```

SEARCH_FILTERS_IMPLEMENTATION.md

Lines changed: 205 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,205 @@
1+
# Search Page: Filters, Pagination & UI Integration
2+
3+
**Date**: 2025-11-16
4+
**Branch**: `feature/search-filters-pagination`
5+
**Feature**: Complete search experience with filters, sorting, pagination, and UI integration
6+
7+
## Overview
8+
This document tracks the implementation of comprehensive search functionality for the Commitly platform, including backend filter support and frontend UI integration.
9+
10+
## Requirements
11+
12+
### Backend
13+
- Extend `GET /api/v1/roadmap/catalog` endpoint with filters:
14+
- `language`: Filter by programming language
15+
- `tag`: Filter by topics/tags
16+
- `difficulty`: Filter by difficulty level
17+
- `min_rating`: Minimum average rating
18+
- `min_views`: Minimum view count
19+
- `min_syncs`: Minimum sync count
20+
- Add sorting options (already implemented: newest, most_viewed, most_synced, highest_rated, trending)
21+
- Implement proper pagination with filters
22+
23+
### Frontend
24+
- Add filter controls for:
25+
- Language (dropdown/multi-select)
26+
- Tags/topics
27+
- Difficulty
28+
- Min rating, min views, min syncs (sliders/inputs)
29+
- Add pagination controls (page numbers/next/prev)
30+
- Wire query params → API calls → state
31+
- Separate sections for "Your Repositories" vs "Public Repositories"
32+
- Clean, non-cluttered UX
33+
34+
## Implementation Plan
35+
36+
### Phase 1: Backend Filter Implementation
37+
1. Update `RoadmapResultStore.list_catalog()` to accept filter parameters
38+
2. Modify SQL query to apply filters dynamically
39+
3. Update `RoadmapService.list_catalog()` to pass through filters
40+
4. Update API endpoint to accept filter query parameters
41+
5. Create Pydantic models for filter validation
42+
43+
### Phase 2: Frontend UI Components
44+
1. Create filter UI components
45+
2. Add pagination controls
46+
3. Wire query parameters to API calls
47+
4. Update state management for filters
48+
5. Improve UX with loading states
49+
50+
### Phase 3: Testing
51+
1. Add backend unit tests for filters
52+
2. Add integration tests for catalog endpoint
53+
3. Test pagination with various filter combinations
54+
4. Test UI responsiveness and error handling
55+
56+
## Changes Made
57+
58+
### Backend Changes
59+
60+
#### Models (app/models/roadmap.py)
61+
- **CatalogFilters**: Pydantic model for filter parameters
62+
- `language`: Optional[str] - Filter by programming language
63+
- `tag`: Optional[str] - Filter by topic/tag
64+
- `difficulty`: Optional[str] - Filter by difficulty level (beginner/intermediate/advanced)
65+
- `min_rating`: Optional[float] - Minimum average rating (1.0-5.0)
66+
- `min_views`: Optional[int] - Minimum view count
67+
- `min_syncs`: Optional[int] - Minimum sync count
68+
- `sort`: str - Sort order (newest, most_viewed, most_synced, highest_rated, trending)
69+
- `page`: int - Page number (default 1)
70+
- `page_size`: int - Items per page (default 20, max 100)
71+
72+
- **CatalogPage**: Pydantic model for paginated response
73+
- `items`: list[RoadmapResponse] - List of roadmap items for current page
74+
- `page`: int - Current page number
75+
- `page_size`: int - Number of items per page
76+
- `total_count`: int - Total number of items matching filters
77+
- `total_pages`: int - Total number of pages
78+
79+
#### Repository Layer (app/services/roadmap_repository.py)
80+
- **RoadmapResultStore.list_catalog()**:
81+
- Queries GeneratedRoadmap table with is_public=True
82+
- Applies filters using JSON field queries:
83+
- Language: `repo_summary["language"].astext == language`
84+
- Tag: `repo_summary["topics"].contains([tag])`
85+
- Difficulty: `repo_summary["difficulty"].astext == difficulty`
86+
- Min rating: Calculates avg_rating and filters >= min_rating
87+
- Min views: `repo_summary["view_count"].astext.cast(Integer) >= min_views`
88+
- Min syncs: `repo_summary["sync_count"].astext.cast(Integer) >= min_syncs`
89+
90+
- Implements 5 sorting options:
91+
- **newest**: ORDER BY created_at DESC
92+
- **most_viewed**: ORDER BY view_count DESC
93+
- **most_synced**: ORDER BY sync_count DESC
94+
- **highest_rated**: ORDER BY (rating_sum / rating_count) DESC
95+
- **trending**: ORDER BY (view_count * 0.4 + sync_count * 0.3 + avg_rating * 6 * 0.3) DESC
96+
97+
- Pagination: Uses offset/limit based on page and page_size
98+
- Returns: tuple of (list[RoadmapResponse], total_count)
99+
100+
#### Service Layer (app/services/roadmap_service.py)
101+
- **RoadmapService.list_catalog()**:
102+
- Simple wrapper that passes all parameters to repository store
103+
- Returns: tuple of (list[RoadmapResponse], total_count)
104+
105+
#### API Layer (app/api/roadmap.py)
106+
- **GET /catalog**: Updated to accept filter query parameters
107+
- Query Parameters:
108+
- page: int (default 1, >= 1)
109+
- page_size: int (default 20, 1-100)
110+
- language: Optional[str]
111+
- tag: Optional[str]
112+
- difficulty: Optional[str]
113+
- min_rating: Optional[float] (1.0-5.0)
114+
- min_views: Optional[int] (>= 0)
115+
- min_syncs: Optional[int] (>= 0)
116+
- sort: str (default "newest")
117+
118+
- Response: CatalogPage with items, pagination metadata
119+
- Calculates total_pages: math.ceil(total_count / page_size)
120+
121+
#### Tests (tests/test_catalog_filters.py)
122+
Created comprehensive test suite with 22 test cases:
123+
124+
**TestCatalogFilters** (8 tests):
125+
- test_list_catalog_no_filters: Basic listing without filters
126+
- test_list_catalog_language_filter: Filter by programming language
127+
- test_list_catalog_tag_filter: Filter by topic tag
128+
- test_list_catalog_difficulty_filter: Filter by difficulty
129+
- test_list_catalog_min_rating_filter: Filter by minimum rating
130+
- test_list_catalog_min_views_filter: Filter by minimum views
131+
- test_list_catalog_min_syncs_filter: Filter by minimum syncs
132+
- test_list_catalog_combined_filters: Multiple filters combined
133+
134+
**TestCatalogSorting** (5 tests):
135+
- test_sort_by_newest: Verify created_at DESC ordering
136+
- test_sort_by_most_viewed: Verify view_count DESC ordering
137+
- test_sort_by_most_synced: Verify sync_count DESC ordering
138+
- test_sort_by_highest_rated: Verify avg_rating DESC ordering
139+
- test_sort_by_trending: Verify trending score calculation and ordering
140+
141+
**TestCatalogPagination** (5 tests):
142+
- test_pagination_first_page: First page results
143+
- test_pagination_second_page: Second page has different items
144+
- test_pagination_beyond_last_page: Empty results beyond last page
145+
- test_pagination_page_size_consistency: Page size is respected
146+
- test_pagination_total_count_consistency: Total count consistent across pages
147+
148+
**TestCatalogEdgeCases** (4 tests):
149+
- test_empty_results: Handle empty result sets
150+
- test_invalid_page_number: Handle edge case page numbers
151+
- test_min_rating_boundary: Boundary values for min_rating
152+
- test_filter_with_sorting: Filters work correctly with sorting
153+
154+
#### Code Quality
155+
- ✅ All flake8 linting issues resolved
156+
- ✅ Code formatted with black
157+
- ✅ Imports sorted with isort --profile black
158+
- ✅ Line length within 88 character limit
159+
- ✅ No whitespace issues
160+
161+
### Frontend Changes
162+
163+
#### API Service (lib/services/repos.ts)
164+
- **Updated Type Definitions**:
165+
- Added `CatalogFilters` type with 9 optional filter parameters
166+
- Added `CatalogPage` type matching backend pagination response
167+
- Includes: items[], page, page_size, total_count, total_pages
168+
169+
- **Updated listCatalog() Method**:
170+
- Now accepts optional `CatalogFilters` parameter
171+
- Builds URL query string from filter parameters
172+
- Returns `CatalogPage` instead of array
173+
- Maintains backward compatibility with no filters
174+
175+
#### Provider (components/providers/roadmap-catalog-provider.tsx)
176+
- **Updated Data Fetching**:
177+
- Modified to handle `CatalogPage.items` response structure
178+
- Extracts items from paginated response
179+
- Maintains existing synced/pending/loading state management
180+
181+
#### Search Page (app/search/page.tsx)
182+
- **Current State**: Using existing provider data
183+
- **Future Enhancements** (not implemented yet):
184+
- Add filter UI components for all 6 filter types
185+
- Add pagination controls (page navigation)
186+
- Wire filters to URL query params
187+
- Add sorting dropdown
188+
- Show total results count
189+
- Add loading/empty states for filtered results
190+
191+
**Note**: The search page currently uses the existing provider pattern which now supports the paginated API. Advanced filtering UI (dropdowns, sliders, etc.) can be added as a future enhancement.
192+
193+
### Database Changes
194+
- No schema changes required (using existing columns from previous prompts)
195+
196+
## Testing Strategy
197+
- Unit tests for filter logic
198+
- Integration tests for catalog endpoint
199+
- End-to-end tests for search page
200+
- Manual testing of UX flow
201+
202+
## Notes
203+
- Leveraging existing metadata columns added in Prompt 1
204+
- No social features or complex tracking
205+
- Focus on clean, simple catalog/search experience

commitly-backend/app/api/roadmap.py

Lines changed: 45 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,20 @@
11
from __future__ import annotations
22

3-
from typing import Annotated, Optional
3+
from typing import Optional
44

5-
from fastapi import APIRouter, Depends, HTTPException, Response, status
5+
from fastapi import APIRouter, Depends, HTTPException, Query, Response, status
66
from sqlalchemy.orm import Session
77

88
from app.core.auth import ClerkClaims, optional_clerk_auth, require_clerk_auth
99
from app.core.database import get_db
1010
from app.models.roadmap import (
11+
CatalogPage,
1112
RatingRequest,
1213
RatingResponse,
13-
RoadmapCatalogPage,
1414
RoadmapRequest,
1515
RoadmapResponse,
1616
UserRepoStateResponse,
1717
)
18-
from app.services.roadmap_repository import SortOption
1918
from app.services.roadmap_service import RoadmapService, build_roadmap_service
2019

2120
router = APIRouter()
@@ -36,27 +35,50 @@ def get_user_id(claims: ClerkClaims) -> str:
3635
return user_id
3736

3837

39-
@router.get("/catalog", response_model=RoadmapCatalogPage)
38+
@router.get("/catalog", response_model=CatalogPage)
4039
async def list_roadmaps(
41-
page: int = 1,
42-
page_size: int = 20,
43-
sort: Annotated[
44-
SortOption,
45-
"Sort option: newest, most_viewed, most_synced, highest_rated, trending",
46-
] = "newest",
40+
page: int = Query(1, ge=1, description="Page number"),
41+
page_size: int = Query(20, ge=1, le=100, description="Items per page"),
42+
language: str | None = Query(None, description="Filter by programming language"),
43+
tag: str | None = Query(None, description="Filter by topic/tag"),
44+
difficulty: str | None = Query(None, description="Filter by difficulty"),
45+
min_rating: float | None = Query(
46+
None, ge=1.0, le=5.0, description="Minimum average rating"
47+
),
48+
min_views: int | None = Query(None, ge=0, description="Minimum view count"),
49+
min_syncs: int | None = Query(None, ge=0, description="Minimum sync count"),
50+
sort: str = Query(
51+
"newest",
52+
description=(
53+
"Sort order: newest, most_viewed, most_synced, " "highest_rated, trending"
54+
),
55+
),
4756
service: RoadmapService = Depends(get_roadmap_service),
48-
) -> RoadmapCatalogPage:
49-
"""
50-
List roadmaps with pagination and sorting.
51-
52-
Sort options:
53-
- newest: Order by updated_at DESC (default)
54-
- most_viewed: Order by view_count DESC
55-
- most_synced: Order by sync_count DESC
56-
- highest_rated: Order by average rating DESC
57-
- trending: Order by trending score (combines views, syncs, and ratings)
58-
"""
59-
return await service.list_catalog(page, page_size, sort)
57+
) -> CatalogPage:
58+
"""List catalog with filters, sorting, and pagination."""
59+
import math
60+
61+
items, total_count = await service.list_catalog(
62+
page=page,
63+
page_size=page_size,
64+
language=language,
65+
tag=tag,
66+
difficulty=difficulty,
67+
min_rating=min_rating,
68+
min_views=min_views,
69+
min_syncs=min_syncs,
70+
sort=sort,
71+
)
72+
73+
total_pages = math.ceil(total_count / page_size) if total_count > 0 else 0
74+
75+
return CatalogPage(
76+
items=items,
77+
page=page,
78+
page_size=page_size,
79+
total_count=total_count,
80+
total_pages=total_pages,
81+
)
6082

6183

6284
@router.get("/cached/{owner}/{repo}", response_model=RoadmapResponse)

commitly-backend/app/models/roadmap.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,34 @@ class RoadmapRequest(BaseModel):
155155
)
156156

157157

158+
class CatalogFilters(BaseModel):
159+
"""Filters for roadmap catalog search."""
160+
161+
language: Optional[str] = Field(None, description="Filter by programming language")
162+
tag: Optional[str] = Field(None, description="Filter by topic/tag")
163+
difficulty: Optional[Literal["beginner", "intermediate", "advanced"]] = Field(
164+
None, description="Filter by difficulty level"
165+
)
166+
min_rating: Optional[float] = Field(
167+
None, ge=1.0, le=5.0, description="Minimum average rating"
168+
)
169+
min_views: Optional[int] = Field(None, ge=0, description="Minimum view count")
170+
min_syncs: Optional[int] = Field(None, ge=0, description="Minimum sync count")
171+
sort: Optional[
172+
Literal["newest", "most_viewed", "most_synced", "highest_rated", "trending"]
173+
] = Field("newest", description="Sort order")
174+
175+
176+
class CatalogPage(BaseModel):
177+
"""Paginated catalog response."""
178+
179+
items: List["RoadmapResponse"]
180+
page: int = Field(ge=1, description="Current page number")
181+
page_size: int = Field(ge=1, le=100, description="Number of items per page")
182+
total_count: int = Field(ge=0, description="Total number of items")
183+
total_pages: int = Field(ge=0, description="Total number of pages")
184+
185+
158186
class TimelineResource(BaseModel):
159187
label: str
160188
href: Annotated[str, Field(max_length=500)]

0 commit comments

Comments
 (0)