diff --git a/.gsd/DECISIONS.md b/.gsd/DECISIONS.md index 0cd100b..5f58314 100644 --- a/.gsd/DECISIONS.md +++ b/.gsd/DECISIONS.md @@ -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 | diff --git a/.gsd/milestones/M015/M015-ROADMAP.md b/.gsd/milestones/M015/M015-ROADMAP.md index 9710b4c..5e95325 100644 --- a/.gsd/milestones/M015/M015-ROADMAP.md +++ b/.gsd/milestones/M015/M015-ROADMAP.md @@ -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. | diff --git a/.gsd/milestones/M015/slices/S03/S03-SUMMARY.md b/.gsd/milestones/M015/slices/S03/S03-SUMMARY.md new file mode 100644 index 0000000..8b55bda --- /dev/null +++ b/.gsd/milestones/M015/slices/S03/S03-SUMMARY.md @@ -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 diff --git a/.gsd/milestones/M015/slices/S03/S03-UAT.md b/.gsd/milestones/M015/slices/S03/S03-UAT.md new file mode 100644 index 0000000..b0d6d8b --- /dev/null +++ b/.gsd/milestones/M015/slices/S03/S03-UAT.md @@ -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) diff --git a/.gsd/milestones/M015/slices/S03/tasks/T02-VERIFY.json b/.gsd/milestones/M015/slices/S03/tasks/T02-VERIFY.json new file mode 100644 index 0000000..55afb2a --- /dev/null +++ b/.gsd/milestones/M015/slices/S03/tasks/T02-VERIFY.json @@ -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 +} diff --git a/.gsd/milestones/M015/slices/S04/S04-PLAN.md b/.gsd/milestones/M015/slices/S04/S04-PLAN.md index ee5fc5b..0551a04 100644 --- a/.gsd/milestones/M015/slices/S04/S04-PLAN.md +++ b/.gsd/milestones/M015/slices/S04/S04-PLAN.md @@ -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(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 contains an H2 heading (e.g., "Trending Searches") and a flex-wrap container of `` 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 diff --git a/.gsd/milestones/M015/slices/S04/S04-RESEARCH.md b/.gsd/milestones/M015/slices/S04/S04-RESEARCH.md new file mode 100644 index 0000000..df1248a --- /dev/null +++ b/.gsd/milestones/M015/slices/S04/S04-RESEARCH.md @@ -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 diff --git a/.gsd/milestones/M015/slices/S04/tasks/T01-PLAN.md b/.gsd/milestones/M015/slices/S04/tasks/T01-PLAN.md new file mode 100644 index 0000000..b0c9dc0 --- /dev/null +++ b/.gsd/milestones/M015/slices/S04/tasks/T01-PLAN.md @@ -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(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 contains an H2 heading (e.g., "Trending Searches") and a flex-wrap container of `` 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 diff --git a/.gsd/milestones/M015/slices/S04/tasks/T01-SUMMARY.md b/.gsd/milestones/M015/slices/S04/tasks/T01-SUMMARY.md new file mode 100644 index 0000000..e60abef --- /dev/null +++ b/.gsd/milestones/M015/slices/S04/tasks/T01-SUMMARY.md @@ -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. diff --git a/frontend/src/App.css b/frontend/src/App.css index 56ce400..25eeb32 100644 --- a/frontend/src/App.css +++ b/frontend/src/App.css @@ -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 { diff --git a/frontend/src/api/public-client.ts b/frontend/src/api/public-client.ts index d1d2572..954874a 100644 --- a/frontend/src/api/public-client.ts +++ b/frontend/src/api/public-client.ts @@ -317,6 +317,22 @@ export async function fetchStats(): Promise { return request(`${BASE}/stats`); } +// ── Popular Searches ───────────────────────────────────────────────────────── + +export interface PopularSearchItem { + query: string; + count: number; +} + +export interface PopularSearchesResponse { + items: PopularSearchItem[]; + cached: boolean; +} + +export async function fetchPopularSearches(): Promise { + return request(`${BASE}/search/popular`); +} + // ── Topics ─────────────────────────────────────────────────────────────────── export async function fetchTopics(): Promise { diff --git a/frontend/src/pages/Home.tsx b/frontend/src/pages/Home.tsx index 97fbaa6..6449c63 100644 --- a/frontend/src/pages/Home.tsx +++ b/frontend/src/pages/Home.tsx @@ -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(null); + const [trending, setTrending] = useState(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 (
{/* Hero search */} @@ -209,6 +230,25 @@ export default function Home() { )} + {/* Trending Searches */} + {trending && trending.length > 0 && ( +
+

Trending Searches

+
+ {trending.map((item, i) => ( + + {item.query} + + ))} +
+
+ )} + {/* Random technique discovery */}