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:
jlightner 2026-04-03 04:37:36 +00:00
parent 3c5985f012
commit 8b912e5a6f
12 changed files with 513 additions and 2 deletions

View file

@ -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 |

View file

@ -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. |

View 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

View 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)

View 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
}

View file

@ -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

View 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

View 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

View 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.

View file

@ -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 {

View file

@ -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[]> {

View file

@ -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