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 |
|
||||
| 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 |
|
||||
| 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. |
|
||||
| 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. |
|
||||
| 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
|
||||
|
||||
**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.
|
||||
|
||||
## 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;
|
||||
}
|
||||
|
||||
/* ── 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 {
|
||||
|
|
|
|||
|
|
@ -317,6 +317,22 @@ export async function fetchStats(): Promise<StatsResponse> {
|
|||
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 ───────────────────────────────────────────────────────────────────
|
||||
|
||||
export async function fetchTopics(): Promise<TopicCategory[]> {
|
||||
|
|
|
|||
|
|
@ -16,8 +16,10 @@ import {
|
|||
fetchTopics,
|
||||
fetchRandomTechnique,
|
||||
fetchStats,
|
||||
fetchPopularSearches,
|
||||
type TechniqueListItem,
|
||||
type StatsResponse,
|
||||
type PopularSearchItem,
|
||||
} from "../api/public-client";
|
||||
|
||||
export default function Home() {
|
||||
|
|
@ -29,6 +31,7 @@ export default function Home() {
|
|||
const [randomLoading, setRandomLoading] = useState(false);
|
||||
const [randomError, setRandomError] = useState(false);
|
||||
const [stats, setStats] = useState<StatsResponse | null>(null);
|
||||
const [trending, setTrending] = useState<PopularSearchItem[] | null>(null);
|
||||
const navigate = useNavigate();
|
||||
|
||||
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 (
|
||||
<div className="home">
|
||||
{/* Hero search */}
|
||||
|
|
@ -209,6 +230,25 @@ export default function Home() {
|
|||
</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 */}
|
||||
<div className="home-random">
|
||||
<button
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue