feat: Added Trending Searches section to homepage with real-time popula…
- "frontend/src/api/public-client.ts" - "frontend/src/pages/Home.tsx" - "frontend/src/App.css" GSD-Task: S04/T01
This commit is contained in:
parent
3c5985f012
commit
8b912e5a6f
12 changed files with 513 additions and 2 deletions
|
|
@ -32,3 +32,4 @@
|
||||||
| D024 | M014/S01 | architecture | Content model for sections with subsections | Sections with subsections use empty-string content field; substance lives in subsections | Avoids duplication between section-level content and subsection content. The section heading serves as H2 container; all prose lives in subsection content fields. Sections without subsections use the section content field directly. | Yes | agent |
|
| D024 | M014/S01 | architecture | Content model for sections with subsections | Sections with subsections use empty-string content field; substance lives in subsections | Avoids duplication between section-level content and subsection content. The section heading serves as H2 container; all prose lives in subsection content fields. Sections without subsections use the section content field directly. | Yes | agent |
|
||||||
| D025 | M015 | architecture | Search query storage and popular searches architecture | PostgreSQL search_log table + Redis read-through cache with 5-min TTL | PostgreSQL gives full historical data for future analytics (zero-result queries, time-of-day patterns). Redis cache prevents DB query on every homepage load. 5-min TTL balances freshness with load. Volume is tiny at current scale. | Yes | collaborative |
|
| D025 | M015 | architecture | Search query storage and popular searches architecture | PostgreSQL search_log table + Redis read-through cache with 5-min TTL | PostgreSQL gives full historical data for future analytics (zero-result queries, time-of-day patterns). Redis cache prevents DB query on every homepage load. 5-min TTL balances freshness with load. Volume is tiny at current scale. | Yes | collaborative |
|
||||||
| D026 | | requirement | R033 | validated | Creators browse page shows "Last updated: Apr 2/3" per creator with techniques, omits for 0-technique creators. Homepage recently-added cards show subtle date stamps. Both verified live on ub01:8096. | Yes | agent |
|
| D026 | | requirement | R033 | validated | Creators browse page shows "Last updated: Apr 2/3" per creator with techniques, omits for 0-technique creators. Homepage recently-added cards show subtle date stamps. Both verified live on ub01:8096. | Yes | agent |
|
||||||
|
| D027 | | requirement | R034 | validated | Homepage renders stats block with real counts from the API: GET /api/v1/stats returns {"technique_count":21,"creator_count":7}, and the frontend scorecard displays "21 ARTICLES" and "7 CREATORS" in cyan-on-dark design. Visual and API verification both pass. | Yes | agent |
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,6 @@ Add social proof and freshness signals to the public site: search query logging
|
||||||
|----|-------|------|---------|------|------------|
|
|----|-------|------|---------|------|------------|
|
||||||
| S01 | Search Query Logging + Popular Searches API | medium | — | ✅ | GET /api/v1/search/popular returns top 10 queries from last 7 days, cached in Redis. search_log table has rows from real searches. |
|
| S01 | Search Query Logging + Popular Searches API | medium | — | ✅ | GET /api/v1/search/popular returns top 10 queries from last 7 days, cached in Redis. search_log table has rows from real searches. |
|
||||||
| S02 | Creator Freshness + Homepage Card Dates | low | — | ✅ | Creators browse page shows 'Last updated: Apr 3' per creator. Homepage recently-added cards show a subtle date. |
|
| S02 | Creator Freshness + Homepage Card Dates | low | — | ✅ | Creators browse page shows 'Last updated: Apr 3' per creator. Homepage recently-added cards show a subtle date. |
|
||||||
| S03 | Homepage Stats Scorecard | low | — | ⬜ | Homepage shows a metric block with article count, creator count in scorecard style matching the dark/cyan design. |
|
| S03 | Homepage Stats Scorecard | low | — | ✅ | Homepage shows a metric block with article count, creator count in scorecard style matching the dark/cyan design. |
|
||||||
| S04 | Trending Searches Homepage Block | low | S01 | ⬜ | Homepage shows a 'Trending Searches' section with real-time terms people are searching. |
|
| S04 | Trending Searches Homepage Block | low | S01 | ⬜ | Homepage shows a 'Trending Searches' section with real-time terms people are searching. |
|
||||||
| S05 | Admin Dropdown Hover on Desktop | low | — | ⬜ | Admin dropdown opens on hover at desktop widths. On mobile, opens on tap. |
|
| S05 | Admin Dropdown Hover on Desktop | low | — | ⬜ | Admin dropdown opens on hover at desktop widths. On mobile, opens on tap. |
|
||||||
|
|
|
||||||
90
.gsd/milestones/M015/slices/S03/S03-SUMMARY.md
Normal file
90
.gsd/milestones/M015/slices/S03/S03-SUMMARY.md
Normal file
|
|
@ -0,0 +1,90 @@
|
||||||
|
---
|
||||||
|
id: S03
|
||||||
|
parent: M015
|
||||||
|
milestone: M015
|
||||||
|
provides:
|
||||||
|
- GET /api/v1/stats endpoint returning live technique_count and creator_count
|
||||||
|
- Homepage stats scorecard component
|
||||||
|
requires:
|
||||||
|
[]
|
||||||
|
affects:
|
||||||
|
[]
|
||||||
|
key_files:
|
||||||
|
- backend/routers/stats.py
|
||||||
|
- backend/main.py
|
||||||
|
- frontend/src/api/public-client.ts
|
||||||
|
- frontend/src/pages/Home.tsx
|
||||||
|
- frontend/src/App.css
|
||||||
|
key_decisions:
|
||||||
|
- Used `or 0` fallback on scalar COUNT results for safety against empty tables
|
||||||
|
- Used existing --color-* CSS custom properties to match real codebase conventions
|
||||||
|
patterns_established:
|
||||||
|
- Simple read-only stats endpoint pattern: async SQLAlchemy COUNT queries with scalar result
|
||||||
|
observability_surfaces:
|
||||||
|
- none
|
||||||
|
drill_down_paths:
|
||||||
|
- .gsd/milestones/M015/slices/S03/tasks/T01-SUMMARY.md
|
||||||
|
- .gsd/milestones/M015/slices/S03/tasks/T02-SUMMARY.md
|
||||||
|
duration: ""
|
||||||
|
verification_result: passed
|
||||||
|
completed_at: 2026-04-03T04:30:55.043Z
|
||||||
|
blocker_discovered: false
|
||||||
|
---
|
||||||
|
|
||||||
|
# S03: Homepage Stats Scorecard
|
||||||
|
|
||||||
|
**Added a live stats endpoint and homepage scorecard displaying article and creator counts in cyan-on-dark design.**
|
||||||
|
|
||||||
|
## What Happened
|
||||||
|
|
||||||
|
Two-task slice delivering social proof metrics on the Chrysopedia homepage.
|
||||||
|
|
||||||
|
**T01 — Backend stats endpoint:** Created `backend/routers/stats.py` with `GET /api/v1/stats` that runs COUNT(*) queries against TechniquePage and Creator tables via async SQLAlchemy. Returns `{"technique_count": N, "creator_count": N}`. Registered in `main.py` with the standard `/api/v1` prefix. Uses `or 0` fallback on scalar results for safety against empty tables.
|
||||||
|
|
||||||
|
**T02 — Frontend scorecard:** Added `fetchStats()` to `public-client.ts` following existing API client patterns. Home.tsx renders a `.home-stats` section between the nav-cards and random technique button, with stagger animation. CSS uses existing design tokens (`--color-accent`, `--color-bg-surface`, `--color-border`, `--color-text-muted`). The scorecard only renders when stats are non-null, avoiding flash of empty content.
|
||||||
|
|
||||||
|
Live result: homepage shows "21 ARTICLES" and "7 CREATORS" in a subtle, centered scorecard that communicates volume without being boastful (per R034).
|
||||||
|
|
||||||
|
## Verification
|
||||||
|
|
||||||
|
All slice-level verification checks pass:
|
||||||
|
1. `curl -s http://ub01:8096/api/v1/stats | python3 -c "assert d['technique_count']>0 and d['creator_count']>0"` — exits 0 (21, 7)
|
||||||
|
2. `curl -s -o /dev/null -w '%{http_code}' http://ub01:8096/api/v1/stats` — returns 200
|
||||||
|
3. `cd frontend && npm run build` — exits 0, builds in 976ms
|
||||||
|
4. Browser screenshot confirms `.home-stats` section visible with "21 ARTICLES" and "7 CREATORS" in cyan-on-dark styling
|
||||||
|
|
||||||
|
## Requirements Advanced
|
||||||
|
|
||||||
|
- R034 — Validated — homepage renders stats block with real counts from the API (technique_count=21, creator_count=7) in cyan-on-dark scorecard design.
|
||||||
|
|
||||||
|
## Requirements Validated
|
||||||
|
|
||||||
|
- R034 — GET /api/v1/stats returns {technique_count:21, creator_count:7}. Browser screenshot confirms scorecard visible with correct values and styling.
|
||||||
|
|
||||||
|
## New Requirements Surfaced
|
||||||
|
|
||||||
|
None.
|
||||||
|
|
||||||
|
## Requirements Invalidated or Re-scoped
|
||||||
|
|
||||||
|
None.
|
||||||
|
|
||||||
|
## Deviations
|
||||||
|
|
||||||
|
T02 used real CSS custom property names (--color-accent, --color-bg-surface) instead of the plan's shorthand names (--accent, --surface-1) because those are what the codebase actually uses.
|
||||||
|
|
||||||
|
## Known Limitations
|
||||||
|
|
||||||
|
Stats are fetched on every homepage load with no caching. Acceptable for current traffic (single-admin tool) but would benefit from Redis caching or a stale-while-revalidate header if traffic grows.
|
||||||
|
|
||||||
|
## Follow-ups
|
||||||
|
|
||||||
|
None.
|
||||||
|
|
||||||
|
## Files Created/Modified
|
||||||
|
|
||||||
|
- `backend/routers/stats.py` — New file — GET /api/v1/stats endpoint with COUNT queries for techniques and creators
|
||||||
|
- `backend/main.py` — Added stats router import and include_router registration
|
||||||
|
- `frontend/src/api/public-client.ts` — Added StatsResponse interface and fetchStats() function
|
||||||
|
- `frontend/src/pages/Home.tsx` — Added stats state, useEffect fetch, and scorecard section render
|
||||||
|
- `frontend/src/App.css` — Added .home-stats, .home-stats__metric, .home-stats__number, .home-stats__label styles
|
||||||
40
.gsd/milestones/M015/slices/S03/S03-UAT.md
Normal file
40
.gsd/milestones/M015/slices/S03/S03-UAT.md
Normal file
|
|
@ -0,0 +1,40 @@
|
||||||
|
# S03: Homepage Stats Scorecard — UAT
|
||||||
|
|
||||||
|
**Milestone:** M015
|
||||||
|
**Written:** 2026-04-03T04:30:55.043Z
|
||||||
|
|
||||||
|
## UAT: Homepage Stats Scorecard
|
||||||
|
|
||||||
|
### Preconditions
|
||||||
|
- Chrysopedia stack running on ub01 (API + web containers healthy)
|
||||||
|
- At least 1 technique page and 1 creator exist in PostgreSQL
|
||||||
|
|
||||||
|
### Test 1: Stats API returns valid data
|
||||||
|
1. Run: `curl -s http://ub01:8096/api/v1/stats`
|
||||||
|
2. **Expected:** JSON response `{"technique_count": N, "creator_count": N}` where both N > 0
|
||||||
|
3. Verify HTTP status: `curl -s -o /dev/null -w '%{http_code}' http://ub01:8096/api/v1/stats` → `200`
|
||||||
|
|
||||||
|
### Test 2: Scorecard visible on homepage
|
||||||
|
1. Open http://ub01:8096 in a browser
|
||||||
|
2. Scroll to the section between the how-it-works cards and the "Random Technique" button
|
||||||
|
3. **Expected:** A scorecard section shows two metrics — article count (e.g., "21") and creator count (e.g., "7")
|
||||||
|
4. Numbers should be in cyan/accent color, labels ("ARTICLES", "CREATORS") in muted text below
|
||||||
|
5. Section should have a subtle dark surface background with a border
|
||||||
|
|
||||||
|
### Test 3: Scorecard uses live data
|
||||||
|
1. Note the current technique_count from the API
|
||||||
|
2. Add a new technique page via the pipeline (or verify count matches DB: `SELECT count(*) FROM technique_pages`)
|
||||||
|
3. Refresh the homepage
|
||||||
|
4. **Expected:** The article count matches the current database count
|
||||||
|
|
||||||
|
### Test 4: Frontend build succeeds
|
||||||
|
1. Run: `cd frontend && npm run build`
|
||||||
|
2. **Expected:** Build completes with exit code 0, no TypeScript errors
|
||||||
|
|
||||||
|
### Test 5: Scorecard handles empty state gracefully
|
||||||
|
1. If stats fetch fails (API down), the scorecard section should not render (no empty/broken UI)
|
||||||
|
2. The rest of the homepage should remain functional
|
||||||
|
|
||||||
|
### Edge Cases
|
||||||
|
- **API unavailable:** Scorecard section is conditionally rendered — hidden when stats are null
|
||||||
|
- **Zero counts:** Would display "0" — acceptable but unlikely in production (data always exists)
|
||||||
24
.gsd/milestones/M015/slices/S03/tasks/T02-VERIFY.json
Normal file
24
.gsd/milestones/M015/slices/S03/tasks/T02-VERIFY.json
Normal file
|
|
@ -0,0 +1,24 @@
|
||||||
|
{
|
||||||
|
"schemaVersion": 1,
|
||||||
|
"taskId": "T02",
|
||||||
|
"unitId": "M015/S03/T02",
|
||||||
|
"timestamp": 1775190561837,
|
||||||
|
"passed": false,
|
||||||
|
"discoverySource": "task-plan",
|
||||||
|
"checks": [
|
||||||
|
{
|
||||||
|
"command": "cd frontend",
|
||||||
|
"exitCode": 0,
|
||||||
|
"durationMs": 7,
|
||||||
|
"verdict": "pass"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"command": "npm run build",
|
||||||
|
"exitCode": 254,
|
||||||
|
"durationMs": 91,
|
||||||
|
"verdict": "fail"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"retryAttempt": 1,
|
||||||
|
"maxRetries": 2
|
||||||
|
}
|
||||||
|
|
@ -1,6 +1,55 @@
|
||||||
# S04: Trending Searches Homepage Block
|
# S04: Trending Searches Homepage Block
|
||||||
|
|
||||||
**Goal:** Render popular search terms on the homepage, sourced from the popular searches API.
|
**Goal:** Homepage shows a "Trending Searches" section with clickable terms sourced from GET /api/v1/search/popular. Section hidden when no data.
|
||||||
**Demo:** After this: Homepage shows a 'Trending Searches' section with real-time terms people are searching.
|
**Demo:** After this: Homepage shows a 'Trending Searches' section with real-time terms people are searching.
|
||||||
|
|
||||||
## Tasks
|
## Tasks
|
||||||
|
- [x] **T01: Added Trending Searches section to homepage with real-time popular search terms from GET /api/v1/search/popular** — Wire the existing GET /api/v1/search/popular endpoint into the homepage as a Trending Searches section.
|
||||||
|
|
||||||
|
## Steps
|
||||||
|
|
||||||
|
1. In `frontend/src/api/public-client.ts`, add `PopularSearchItem` and `PopularSearchesResponse` interfaces mirroring the backend schemas (query: string, count: number; items array + cached boolean). Add `fetchPopularSearches()` async function following the `fetchStats()` pattern.
|
||||||
|
|
||||||
|
2. In `frontend/src/pages/Home.tsx`:
|
||||||
|
- Import `fetchPopularSearches` and `PopularSearchItem` from public-client
|
||||||
|
- Add `const [trending, setTrending] = useState<PopularSearchItem[] | null>(null)`
|
||||||
|
- Add a useEffect (with cancellation flag) that calls `fetchPopularSearches()` and sets `trending` to `data.items` when non-empty, or null on error/empty. Can share the existing stats useEffect or add a parallel one.
|
||||||
|
- After the stats scorecard section and before the random technique button, add a conditional render: `{trending && trending.length > 0 && (<section className="home-trending">...</section>)}`
|
||||||
|
- Section contains an H2 heading (e.g., "Trending Searches") and a flex-wrap container of `<Link>` elements, each navigating to `/search?q=${encodeURIComponent(item.query)}` with class `pill pill--trending`
|
||||||
|
- Apply stagger animation via `card-stagger` class with appropriate `--stagger-index`
|
||||||
|
|
||||||
|
3. In `frontend/src/App.css`, add `.home-trending` styles:
|
||||||
|
- Match scorecard aesthetic: `var(--color-bg-surface)` background, `var(--color-border)` border, centered with max-width, border-radius
|
||||||
|
- `.pill--trending` variant: subtle accent border (`var(--color-accent)` at ~0.3 opacity), hover brightens border. Use existing pill sizing/spacing.
|
||||||
|
- Heading styled with `var(--color-text-muted)`, small caps or uppercase letter-spacing to match section label patterns
|
||||||
|
|
||||||
|
4. Build: `cd frontend && npm run build` — must succeed with zero TS errors
|
||||||
|
|
||||||
|
5. Deploy to ub01: push changes, rebuild web container, verify at http://ub01:8096 that trending section appears with real search terms
|
||||||
|
|
||||||
|
## Must-Haves
|
||||||
|
|
||||||
|
- [ ] PopularSearchItem and PopularSearchesResponse types in public-client.ts
|
||||||
|
- [ ] fetchPopularSearches() function in public-client.ts
|
||||||
|
- [ ] Trending section renders on homepage between stats scorecard and random technique button
|
||||||
|
- [ ] Each term is a clickable Link to /search?q={term}
|
||||||
|
- [ ] Section hidden when API returns empty items or errors
|
||||||
|
- [ ] CSS matches existing dark/cyan design system
|
||||||
|
- [ ] npm run build passes
|
||||||
|
|
||||||
|
## Verification
|
||||||
|
|
||||||
|
- `cd frontend && npx tsc --noEmit` — zero errors
|
||||||
|
- `cd frontend && npm run build` — succeeds
|
||||||
|
- `grep -q 'fetchPopularSearches' frontend/src/api/public-client.ts` — function exists
|
||||||
|
- `grep -q 'home-trending' frontend/src/pages/Home.tsx` — section wired
|
||||||
|
- `grep -q 'home-trending' frontend/src/App.css` — styles exist
|
||||||
|
- Deploy and verify at http://ub01:8096 — trending section visible with real terms
|
||||||
|
|
||||||
|
## Negative Tests
|
||||||
|
|
||||||
|
- Empty items array: section must not render (verified by conditional `trending.length > 0`)
|
||||||
|
- API error/timeout: caught in useEffect, trending stays null, section hidden
|
||||||
|
- Estimate: 30m
|
||||||
|
- Files: frontend/src/api/public-client.ts, frontend/src/pages/Home.tsx, frontend/src/App.css
|
||||||
|
- Verify: cd frontend && npm run build && npx tsc --noEmit && grep -q 'fetchPopularSearches' src/api/public-client.ts && grep -q 'home-trending' src/pages/Home.tsx && grep -q 'home-trending' src/App.css
|
||||||
|
|
|
||||||
50
.gsd/milestones/M015/slices/S04/S04-RESEARCH.md
Normal file
50
.gsd/milestones/M015/slices/S04/S04-RESEARCH.md
Normal file
|
|
@ -0,0 +1,50 @@
|
||||||
|
# S04 Research: Trending Searches Homepage Block
|
||||||
|
|
||||||
|
## Depth: Light
|
||||||
|
|
||||||
|
Straightforward frontend wiring — existing API endpoint → new API client function → new homepage section with CSS.
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
|
||||||
|
S01 delivered `GET /api/v1/search/popular` returning `{ items: [{query: string, count: number}], cached: bool }` with Redis read-through cache (5-min TTL, 7-day window). The frontend just needs to consume this endpoint and render a "Trending Searches" block on the homepage.
|
||||||
|
|
||||||
|
## Requirement Target
|
||||||
|
|
||||||
|
- **R035 (active, supporting slice):** Frontend display of popular search terms on homepage. Backend half already delivered by S01.
|
||||||
|
|
||||||
|
## Implementation Landscape
|
||||||
|
|
||||||
|
### API Client (`frontend/src/api/public-client.ts`)
|
||||||
|
- **Missing:** No `fetchPopularSearches` function exists yet. Needs adding.
|
||||||
|
- **Pattern:** Follow existing `fetchStats()` pattern (simple GET, type interface, async function).
|
||||||
|
- **Types needed:** `PopularSearchItem { query: string; count: number }`, `PopularSearchesResponse { items: PopularSearchItem[]; cached: boolean }` — mirroring backend schemas.
|
||||||
|
|
||||||
|
### Homepage (`frontend/src/pages/Home.tsx`)
|
||||||
|
- **Current sections in order:** Hero search → How It Works → CTA → Popular Topics → Nav Cards → Stats Scorecard → Random Technique → Featured Technique → Recently Added.
|
||||||
|
- **Placement:** Trending Searches block fits naturally after Stats Scorecard and before Random Technique button — groups the "social proof" signals together (stats + trending).
|
||||||
|
- **Pattern:** Identical to existing `popularTopics` and `stats` patterns — useState, useEffect with cancellation, conditional render. Already imported: `useEffect`, `useState`, `Link`, `useNavigate`.
|
||||||
|
- **Graceful degradation:** If endpoint returns empty items or errors, section simply doesn't render (match existing pattern of silently ignoring optional sections).
|
||||||
|
|
||||||
|
### Styling (`frontend/src/App.css`)
|
||||||
|
- **Stats scorecard pattern at line 1456:** Uses `var(--color-bg-surface)`, `var(--color-border)`, `var(--color-accent)`, `var(--color-text-muted)`. Max-width 36rem, centered.
|
||||||
|
- **Trending block should match:** Same surface background, border radius, centered layout. Terms rendered as clickable pills (navigate to `/search?q=...`) using the existing `pill` class pattern or a new `pill--trending` variant.
|
||||||
|
- **Popular Topics pattern already in hero:** Uses `pill pill--topic-quick` class on Links. Trending can follow the same pill approach but with distinct styling (e.g., `pill--trending` with a subtle accent border or muted background).
|
||||||
|
|
||||||
|
### Backend
|
||||||
|
- **No changes needed.** Endpoint is deployed and returning data on ub01:8096.
|
||||||
|
|
||||||
|
## Recommendation
|
||||||
|
|
||||||
|
Single task:
|
||||||
|
1. Add `PopularSearchItem` / `PopularSearchesResponse` types and `fetchPopularSearches()` to public-client.ts
|
||||||
|
2. Add state, useEffect, and JSX section to Home.tsx (after stats scorecard, before random button)
|
||||||
|
3. Add `.home-trending` CSS block matching the scorecard aesthetic — pills are clickable search links
|
||||||
|
4. Build frontend, deploy to ub01, verify visually
|
||||||
|
|
||||||
|
Clicking a trending term should navigate to `/search?q={term}` — same pattern as Popular Topics pills.
|
||||||
|
|
||||||
|
## Constraints
|
||||||
|
|
||||||
|
- Empty response (no search data yet) → hide section entirely
|
||||||
|
- Terms should show the query text; count display is optional (showing count may feel metric-heavy alongside the scorecard)
|
||||||
|
- Keep pill styling consistent with existing design system variables
|
||||||
70
.gsd/milestones/M015/slices/S04/tasks/T01-PLAN.md
Normal file
70
.gsd/milestones/M015/slices/S04/tasks/T01-PLAN.md
Normal file
|
|
@ -0,0 +1,70 @@
|
||||||
|
---
|
||||||
|
estimated_steps: 34
|
||||||
|
estimated_files: 3
|
||||||
|
skills_used: []
|
||||||
|
---
|
||||||
|
|
||||||
|
# T01: Add popular searches API client, homepage section, and CSS
|
||||||
|
|
||||||
|
Wire the existing GET /api/v1/search/popular endpoint into the homepage as a Trending Searches section.
|
||||||
|
|
||||||
|
## Steps
|
||||||
|
|
||||||
|
1. In `frontend/src/api/public-client.ts`, add `PopularSearchItem` and `PopularSearchesResponse` interfaces mirroring the backend schemas (query: string, count: number; items array + cached boolean). Add `fetchPopularSearches()` async function following the `fetchStats()` pattern.
|
||||||
|
|
||||||
|
2. In `frontend/src/pages/Home.tsx`:
|
||||||
|
- Import `fetchPopularSearches` and `PopularSearchItem` from public-client
|
||||||
|
- Add `const [trending, setTrending] = useState<PopularSearchItem[] | null>(null)`
|
||||||
|
- Add a useEffect (with cancellation flag) that calls `fetchPopularSearches()` and sets `trending` to `data.items` when non-empty, or null on error/empty. Can share the existing stats useEffect or add a parallel one.
|
||||||
|
- After the stats scorecard section and before the random technique button, add a conditional render: `{trending && trending.length > 0 && (<section className="home-trending">...</section>)}`
|
||||||
|
- Section contains an H2 heading (e.g., "Trending Searches") and a flex-wrap container of `<Link>` elements, each navigating to `/search?q=${encodeURIComponent(item.query)}` with class `pill pill--trending`
|
||||||
|
- Apply stagger animation via `card-stagger` class with appropriate `--stagger-index`
|
||||||
|
|
||||||
|
3. In `frontend/src/App.css`, add `.home-trending` styles:
|
||||||
|
- Match scorecard aesthetic: `var(--color-bg-surface)` background, `var(--color-border)` border, centered with max-width, border-radius
|
||||||
|
- `.pill--trending` variant: subtle accent border (`var(--color-accent)` at ~0.3 opacity), hover brightens border. Use existing pill sizing/spacing.
|
||||||
|
- Heading styled with `var(--color-text-muted)`, small caps or uppercase letter-spacing to match section label patterns
|
||||||
|
|
||||||
|
4. Build: `cd frontend && npm run build` — must succeed with zero TS errors
|
||||||
|
|
||||||
|
5. Deploy to ub01: push changes, rebuild web container, verify at http://ub01:8096 that trending section appears with real search terms
|
||||||
|
|
||||||
|
## Must-Haves
|
||||||
|
|
||||||
|
- [ ] PopularSearchItem and PopularSearchesResponse types in public-client.ts
|
||||||
|
- [ ] fetchPopularSearches() function in public-client.ts
|
||||||
|
- [ ] Trending section renders on homepage between stats scorecard and random technique button
|
||||||
|
- [ ] Each term is a clickable Link to /search?q={term}
|
||||||
|
- [ ] Section hidden when API returns empty items or errors
|
||||||
|
- [ ] CSS matches existing dark/cyan design system
|
||||||
|
- [ ] npm run build passes
|
||||||
|
|
||||||
|
## Verification
|
||||||
|
|
||||||
|
- `cd frontend && npx tsc --noEmit` — zero errors
|
||||||
|
- `cd frontend && npm run build` — succeeds
|
||||||
|
- `grep -q 'fetchPopularSearches' frontend/src/api/public-client.ts` — function exists
|
||||||
|
- `grep -q 'home-trending' frontend/src/pages/Home.tsx` — section wired
|
||||||
|
- `grep -q 'home-trending' frontend/src/App.css` — styles exist
|
||||||
|
- Deploy and verify at http://ub01:8096 — trending section visible with real terms
|
||||||
|
|
||||||
|
## Negative Tests
|
||||||
|
|
||||||
|
- Empty items array: section must not render (verified by conditional `trending.length > 0`)
|
||||||
|
- API error/timeout: caught in useEffect, trending stays null, section hidden
|
||||||
|
|
||||||
|
## Inputs
|
||||||
|
|
||||||
|
- `frontend/src/api/public-client.ts`
|
||||||
|
- `frontend/src/pages/Home.tsx`
|
||||||
|
- `frontend/src/App.css`
|
||||||
|
|
||||||
|
## Expected Output
|
||||||
|
|
||||||
|
- `frontend/src/api/public-client.ts`
|
||||||
|
- `frontend/src/pages/Home.tsx`
|
||||||
|
- `frontend/src/App.css`
|
||||||
|
|
||||||
|
## Verification
|
||||||
|
|
||||||
|
cd frontend && npm run build && npx tsc --noEmit && grep -q 'fetchPopularSearches' src/api/public-client.ts && grep -q 'home-trending' src/pages/Home.tsx && grep -q 'home-trending' src/App.css
|
||||||
84
.gsd/milestones/M015/slices/S04/tasks/T01-SUMMARY.md
Normal file
84
.gsd/milestones/M015/slices/S04/tasks/T01-SUMMARY.md
Normal file
|
|
@ -0,0 +1,84 @@
|
||||||
|
---
|
||||||
|
id: T01
|
||||||
|
parent: S04
|
||||||
|
milestone: M015
|
||||||
|
provides: []
|
||||||
|
requires: []
|
||||||
|
affects: []
|
||||||
|
key_files: ["frontend/src/api/public-client.ts", "frontend/src/pages/Home.tsx", "frontend/src/App.css"]
|
||||||
|
key_decisions: ["Placed trending section between stats scorecard and random button", "Used color-mix for accent border at 30% opacity on trending pills"]
|
||||||
|
patterns_established: []
|
||||||
|
drill_down_paths: []
|
||||||
|
observability_surfaces: []
|
||||||
|
duration: ""
|
||||||
|
verification_result: "TypeScript check (zero errors), build succeeds, grep checks for fetchPopularSearches/home-trending in all three files pass. Browser verification confirms trending section visible with real terms (reverb, synthesis, fx, drums) and pill clicks navigate to search results."
|
||||||
|
completed_at: 2026-04-03T04:37:22.744Z
|
||||||
|
blocker_discovered: false
|
||||||
|
---
|
||||||
|
|
||||||
|
# T01: Added Trending Searches section to homepage with real-time popular search terms from GET /api/v1/search/popular
|
||||||
|
|
||||||
|
> Added Trending Searches section to homepage with real-time popular search terms from GET /api/v1/search/popular
|
||||||
|
|
||||||
|
## What Happened
|
||||||
|
---
|
||||||
|
id: T01
|
||||||
|
parent: S04
|
||||||
|
milestone: M015
|
||||||
|
key_files:
|
||||||
|
- frontend/src/api/public-client.ts
|
||||||
|
- frontend/src/pages/Home.tsx
|
||||||
|
- frontend/src/App.css
|
||||||
|
key_decisions:
|
||||||
|
- Placed trending section between stats scorecard and random button
|
||||||
|
- Used color-mix for accent border at 30% opacity on trending pills
|
||||||
|
duration: ""
|
||||||
|
verification_result: passed
|
||||||
|
completed_at: 2026-04-03T04:37:22.745Z
|
||||||
|
blocker_discovered: false
|
||||||
|
---
|
||||||
|
|
||||||
|
# T01: Added Trending Searches section to homepage with real-time popular search terms from GET /api/v1/search/popular
|
||||||
|
|
||||||
|
**Added Trending Searches section to homepage with real-time popular search terms from GET /api/v1/search/popular**
|
||||||
|
|
||||||
|
## What Happened
|
||||||
|
|
||||||
|
Added PopularSearchItem/PopularSearchesResponse types and fetchPopularSearches() to public-client.ts. Wired into Home.tsx with conditional rendering — section hidden when API returns empty or errors. Each term is a Link pill to /search?q={term}. CSS matches the dark/cyan design system with accent-bordered pills. Deployed to ub01 and verified with real data.
|
||||||
|
|
||||||
|
## Verification
|
||||||
|
|
||||||
|
TypeScript check (zero errors), build succeeds, grep checks for fetchPopularSearches/home-trending in all three files pass. Browser verification confirms trending section visible with real terms (reverb, synthesis, fx, drums) and pill clicks navigate to search results.
|
||||||
|
|
||||||
|
## Verification Evidence
|
||||||
|
|
||||||
|
| # | Command | Exit Code | Verdict | Duration |
|
||||||
|
|---|---------|-----------|---------|----------|
|
||||||
|
| 1 | `cd frontend && npx tsc --noEmit` | 0 | ✅ pass | 3500ms |
|
||||||
|
| 2 | `cd frontend && npm run build` | 0 | ✅ pass | 3500ms |
|
||||||
|
| 3 | `grep -q 'fetchPopularSearches' frontend/src/api/public-client.ts` | 0 | ✅ pass | 100ms |
|
||||||
|
| 4 | `grep -q 'home-trending' frontend/src/pages/Home.tsx` | 0 | ✅ pass | 100ms |
|
||||||
|
| 5 | `grep -q 'home-trending' frontend/src/App.css` | 0 | ✅ pass | 100ms |
|
||||||
|
| 6 | `browser_assert: .home-trending visible + pill click navigates to /search?q=reverb` | 0 | ✅ pass | 3000ms |
|
||||||
|
|
||||||
|
|
||||||
|
## Deviations
|
||||||
|
|
||||||
|
Docker Compose service named chrysopedia-web not chrysopedia-web-8096 as in CLAUDE.md
|
||||||
|
|
||||||
|
## Known Issues
|
||||||
|
|
||||||
|
None.
|
||||||
|
|
||||||
|
## Files Created/Modified
|
||||||
|
|
||||||
|
- `frontend/src/api/public-client.ts`
|
||||||
|
- `frontend/src/pages/Home.tsx`
|
||||||
|
- `frontend/src/App.css`
|
||||||
|
|
||||||
|
|
||||||
|
## Deviations
|
||||||
|
Docker Compose service named chrysopedia-web not chrysopedia-web-8096 as in CLAUDE.md
|
||||||
|
|
||||||
|
## Known Issues
|
||||||
|
None.
|
||||||
|
|
@ -3269,6 +3269,53 @@ a.app-footer__repo:hover {
|
||||||
margin: 1.5rem 0 0.5rem;
|
margin: 1.5rem 0 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ── Trending Searches ──────────────────────────────────────────────────── */
|
||||||
|
|
||||||
|
.home-trending {
|
||||||
|
max-width: 36rem;
|
||||||
|
margin: 0 auto 1rem;
|
||||||
|
padding: 1rem 1.5rem;
|
||||||
|
background: var(--color-bg-surface);
|
||||||
|
border: 1px solid var(--color-border);
|
||||||
|
border-radius: 0.625rem;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.home-trending__title {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
color: var(--color-text-muted);
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.08em;
|
||||||
|
margin: 0 0 0.75rem;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.home-trending__list {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pill--trending {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 0.375rem 0.875rem;
|
||||||
|
border-radius: 9999px;
|
||||||
|
font-size: 0.8125rem;
|
||||||
|
font-weight: 500;
|
||||||
|
background: var(--color-pill-bg);
|
||||||
|
color: var(--color-pill-text);
|
||||||
|
border: 1px solid color-mix(in srgb, var(--color-accent) 30%, transparent);
|
||||||
|
text-decoration: none;
|
||||||
|
transition: border-color 0.15s, background 0.15s, color 0.15s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pill--trending:hover {
|
||||||
|
border-color: var(--color-accent);
|
||||||
|
background: var(--color-accent-subtle);
|
||||||
|
color: var(--color-accent);
|
||||||
|
}
|
||||||
|
|
||||||
/* ── Admin Reports ──────────────────────────────────────────────────────── */
|
/* ── Admin Reports ──────────────────────────────────────────────────────── */
|
||||||
|
|
||||||
.admin-reports {
|
.admin-reports {
|
||||||
|
|
|
||||||
|
|
@ -317,6 +317,22 @@ export async function fetchStats(): Promise<StatsResponse> {
|
||||||
return request<StatsResponse>(`${BASE}/stats`);
|
return request<StatsResponse>(`${BASE}/stats`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Popular Searches ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
export interface PopularSearchItem {
|
||||||
|
query: string;
|
||||||
|
count: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PopularSearchesResponse {
|
||||||
|
items: PopularSearchItem[];
|
||||||
|
cached: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchPopularSearches(): Promise<PopularSearchesResponse> {
|
||||||
|
return request<PopularSearchesResponse>(`${BASE}/search/popular`);
|
||||||
|
}
|
||||||
|
|
||||||
// ── Topics ───────────────────────────────────────────────────────────────────
|
// ── Topics ───────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
export async function fetchTopics(): Promise<TopicCategory[]> {
|
export async function fetchTopics(): Promise<TopicCategory[]> {
|
||||||
|
|
|
||||||
|
|
@ -16,8 +16,10 @@ import {
|
||||||
fetchTopics,
|
fetchTopics,
|
||||||
fetchRandomTechnique,
|
fetchRandomTechnique,
|
||||||
fetchStats,
|
fetchStats,
|
||||||
|
fetchPopularSearches,
|
||||||
type TechniqueListItem,
|
type TechniqueListItem,
|
||||||
type StatsResponse,
|
type StatsResponse,
|
||||||
|
type PopularSearchItem,
|
||||||
} from "../api/public-client";
|
} from "../api/public-client";
|
||||||
|
|
||||||
export default function Home() {
|
export default function Home() {
|
||||||
|
|
@ -29,6 +31,7 @@ export default function Home() {
|
||||||
const [randomLoading, setRandomLoading] = useState(false);
|
const [randomLoading, setRandomLoading] = useState(false);
|
||||||
const [randomError, setRandomError] = useState(false);
|
const [randomError, setRandomError] = useState(false);
|
||||||
const [stats, setStats] = useState<StatsResponse | null>(null);
|
const [stats, setStats] = useState<StatsResponse | null>(null);
|
||||||
|
const [trending, setTrending] = useState<PopularSearchItem[] | null>(null);
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
const handleRandomTechnique = async () => {
|
const handleRandomTechnique = async () => {
|
||||||
|
|
@ -115,6 +118,24 @@ export default function Home() {
|
||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
// Load trending searches
|
||||||
|
useEffect(() => {
|
||||||
|
let cancelled = false;
|
||||||
|
void (async () => {
|
||||||
|
try {
|
||||||
|
const data = await fetchPopularSearches();
|
||||||
|
if (!cancelled && data.items.length > 0) {
|
||||||
|
setTrending(data.items);
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// optional section — silently ignore
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
return () => {
|
||||||
|
cancelled = true;
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="home">
|
<div className="home">
|
||||||
{/* Hero search */}
|
{/* Hero search */}
|
||||||
|
|
@ -209,6 +230,25 @@ export default function Home() {
|
||||||
</section>
|
</section>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Trending Searches */}
|
||||||
|
{trending && trending.length > 0 && (
|
||||||
|
<section className="home-trending card-stagger" style={{ '--stagger-index': 3 } as React.CSSProperties}>
|
||||||
|
<h2 className="home-trending__title">Trending Searches</h2>
|
||||||
|
<div className="home-trending__list">
|
||||||
|
{trending.map((item, i) => (
|
||||||
|
<Link
|
||||||
|
key={item.query}
|
||||||
|
to={`/search?q=${encodeURIComponent(item.query)}`}
|
||||||
|
className="pill pill--trending card-stagger"
|
||||||
|
style={{ '--stagger-index': i } as React.CSSProperties}
|
||||||
|
>
|
||||||
|
{item.query}
|
||||||
|
</Link>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Random technique discovery */}
|
{/* Random technique discovery */}
|
||||||
<div className="home-random">
|
<div className="home-random">
|
||||||
<button
|
<button
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue