diff --git a/.gsd/milestones/M011/slices/S01/S01-PLAN.md b/.gsd/milestones/M011/slices/S01/S01-PLAN.md index 1183584..85d83d7 100644 --- a/.gsd/milestones/M011/slices/S01/S01-PLAN.md +++ b/.gsd/milestones/M011/slices/S01/S01-PLAN.md @@ -45,7 +45,7 @@ CSS-first visual polish covering R016 (card hover + stagger) and R017 (featured - Estimate: 45m - Files: frontend/src/App.css, frontend/src/pages/Home.tsx, frontend/src/pages/TopicsBrowse.tsx, frontend/src/pages/CreatorDetail.tsx, frontend/src/pages/SubTopicPage.tsx, frontend/src/pages/SearchResults.tsx - Verify: cd frontend && npx tsc --noEmit && npm run build && grep -q 'cardEnter' src/App.css && grep -q 'card-stagger' src/App.css -- [ ] **T02: Add random technique endpoint and discovery button on homepage** β€” ## Description +- [x] **T02: Added GET /api/v1/techniques/random endpoint returning {slug}, fetchRandomTechnique() client function, and 🎲 Random Technique button with loading/error states on homepage** β€” ## Description Vertical feature for R018: backend endpoint, API client function, and frontend button with navigation. The existing `GET /api/v1/techniques?sort=random&limit=1` works but returns the full paginated list response. A dedicated `GET /api/v1/techniques/random` returning just `{ slug: string }` is cleaner. diff --git a/.gsd/milestones/M011/slices/S01/tasks/T01-VERIFY.json b/.gsd/milestones/M011/slices/S01/tasks/T01-VERIFY.json new file mode 100644 index 0000000..7ec71f1 --- /dev/null +++ b/.gsd/milestones/M011/slices/S01/tasks/T01-VERIFY.json @@ -0,0 +1,42 @@ +{ + "schemaVersion": 1, + "taskId": "T01", + "unitId": "M011/S01/T01", + "timestamp": 1774945357944, + "passed": false, + "discoverySource": "task-plan", + "checks": [ + { + "command": "cd frontend", + "exitCode": 0, + "durationMs": 4, + "verdict": "pass" + }, + { + "command": "npx tsc --noEmit", + "exitCode": 1, + "durationMs": 868, + "verdict": "fail" + }, + { + "command": "npm run build", + "exitCode": 254, + "durationMs": 81, + "verdict": "fail" + }, + { + "command": "grep -q 'cardEnter' src/App.css", + "exitCode": 2, + "durationMs": 8, + "verdict": "fail" + }, + { + "command": "grep -q 'card-stagger' src/App.css", + "exitCode": 2, + "durationMs": 6, + "verdict": "fail" + } + ], + "retryAttempt": 1, + "maxRetries": 2 +} diff --git a/.gsd/milestones/M011/slices/S01/tasks/T02-SUMMARY.md b/.gsd/milestones/M011/slices/S01/tasks/T02-SUMMARY.md new file mode 100644 index 0000000..0f58fc6 --- /dev/null +++ b/.gsd/milestones/M011/slices/S01/tasks/T02-SUMMARY.md @@ -0,0 +1,88 @@ +--- +id: T02 +parent: S01 +milestone: M011 +provides: [] +requires: [] +affects: [] +key_files: ["backend/routers/techniques.py", "frontend/src/api/public-client.ts", "frontend/src/pages/Home.tsx", "frontend/src/App.css"] +key_decisions: ["Dedicated /random endpoint returning just {slug} rather than reusing sort=random on list endpoint", "Route placed before /{slug} to avoid slug capture"] +patterns_established: [] +drill_down_paths: [] +observability_surfaces: [] +duration: "" +verification_result: "npx tsc --noEmit passes. npm run build succeeds (50 modules). grep confirms fetchRandomTechnique in client, /random in backend router, Random in Home.tsx. All slice-level verification checks pass including stagger-index in all 5 pages." +completed_at: 2026-03-31T08:24:27.607Z +blocker_discovered: false +--- + +# T02: Added GET /api/v1/techniques/random endpoint returning {slug}, fetchRandomTechnique() client function, and 🎲 Random Technique button with loading/error states on homepage + +> Added GET /api/v1/techniques/random endpoint returning {slug}, fetchRandomTechnique() client function, and 🎲 Random Technique button with loading/error states on homepage + +## What Happened +--- +id: T02 +parent: S01 +milestone: M011 +key_files: + - backend/routers/techniques.py + - frontend/src/api/public-client.ts + - frontend/src/pages/Home.tsx + - frontend/src/App.css +key_decisions: + - Dedicated /random endpoint returning just {slug} rather than reusing sort=random on list endpoint + - Route placed before /{slug} to avoid slug capture +duration: "" +verification_result: passed +completed_at: 2026-03-31T08:24:27.607Z +blocker_discovered: false +--- + +# T02: Added GET /api/v1/techniques/random endpoint returning {slug}, fetchRandomTechnique() client function, and 🎲 Random Technique button with loading/error states on homepage + +**Added GET /api/v1/techniques/random endpoint returning {slug}, fetchRandomTechnique() client function, and 🎲 Random Technique button with loading/error states on homepage** + +## What Happened + +Added a dedicated GET /techniques/random endpoint in backend/routers/techniques.py before the /{slug} route to avoid slug capture. The endpoint runs SELECT slug FROM technique_pages ORDER BY random() LIMIT 1 and returns 404 if no techniques exist. Added fetchRandomTechnique() in the API client. On the homepage, added a Random Technique button between the nav-cards and featured technique sections with loading state during fetch, error state with auto-reset, and navigation on success. Added .btn--random and .home-random CSS. + +## Verification + +npx tsc --noEmit passes. npm run build succeeds (50 modules). grep confirms fetchRandomTechnique in client, /random in backend router, Random in Home.tsx. All slice-level verification checks pass including stagger-index in all 5 pages. + +## Verification Evidence + +| # | Command | Exit Code | Verdict | Duration | +|---|---------|-----------|---------|----------| +| 1 | `cd frontend && npx tsc --noEmit` | 0 | βœ… pass | 3300ms | +| 2 | `cd frontend && npm run build` | 0 | βœ… pass | 2600ms | +| 3 | `grep -q 'fetchRandomTechnique' frontend/src/api/public-client.ts` | 0 | βœ… pass | 50ms | +| 4 | `grep -q '/random' backend/routers/techniques.py` | 0 | βœ… pass | 50ms | +| 5 | `grep -q 'Random' frontend/src/pages/Home.tsx` | 0 | βœ… pass | 50ms | +| 6 | `grep -q 'cardEnter' frontend/src/App.css` | 0 | βœ… pass | 50ms | +| 7 | `grep -q 'card-stagger' frontend/src/App.css` | 0 | βœ… pass | 50ms | +| 8 | `grep -c 'stagger-index' (5 pages)` | 0 | βœ… pass | 50ms | + + +## Deviations + +None. + +## Known Issues + +None. + +## Files Created/Modified + +- `backend/routers/techniques.py` +- `frontend/src/api/public-client.ts` +- `frontend/src/pages/Home.tsx` +- `frontend/src/App.css` + + +## Deviations +None. + +## Known Issues +None. diff --git a/backend/routers/techniques.py b/backend/routers/techniques.py index 0c2dbea..358c86b 100644 --- a/backend/routers/techniques.py +++ b/backend/routers/techniques.py @@ -182,6 +182,19 @@ async def list_techniques( ) +@router.get("/random") +async def random_technique( + db: AsyncSession = Depends(get_session), +) -> dict: + """Return the slug of a single random technique page.""" + stmt = select(TechniquePage.slug).order_by(func.random()).limit(1) + result = await db.execute(stmt) + slug = result.scalar_one_or_none() + if slug is None: + raise HTTPException(status_code=404, detail="No techniques available") + return {"slug": slug} + + @router.get("/{slug}", response_model=TechniquePageDetail) async def get_technique( slug: str, diff --git a/frontend/src/App.css b/frontend/src/App.css index 6b1e7de..d0f58f5 100644 --- a/frontend/src/App.css +++ b/frontend/src/App.css @@ -2729,6 +2729,33 @@ a.app-footer__repo:hover { opacity: 0.85; } +.btn--random { + background: var(--color-bg-input); + color: var(--color-text-primary); + border-color: var(--color-border); + font-size: 0.95rem; + padding: 0.6rem 1.4rem; + display: inline-flex; + align-items: center; + gap: 0.4rem; + transition: background 0.15s, border-color 0.15s, transform 0.15s; +} + +.btn--random:hover:not(:disabled) { + background: var(--color-border); + transform: scale(1.04); +} + +.btn--random:disabled { + opacity: 0.6; + cursor: not-allowed; +} + +.home-random { + text-align: center; + margin: 1.5rem 0 0.5rem; +} + /* ── Admin Reports ──────────────────────────────────────────────────────── */ .admin-reports { diff --git a/frontend/src/api/public-client.ts b/frontend/src/api/public-client.ts index 3ecee06..6f571b6 100644 --- a/frontend/src/api/public-client.ts +++ b/frontend/src/api/public-client.ts @@ -258,6 +258,10 @@ export async function fetchTechnique( return request(`${BASE}/techniques/${slug}`); } +export async function fetchRandomTechnique(): Promise<{ slug: string }> { + return request<{ slug: string }>(`${BASE}/techniques/random`); +} + export async function fetchTechniqueVersions( slug: string, ): Promise { diff --git a/frontend/src/pages/Home.tsx b/frontend/src/pages/Home.tsx index 2abfc18..0f6e642 100644 --- a/frontend/src/pages/Home.tsx +++ b/frontend/src/pages/Home.tsx @@ -12,6 +12,7 @@ import { Link, useNavigate } from "react-router-dom"; import { fetchTechniques, fetchTopics, + fetchRandomTechnique, type TechniqueListItem, } from "../api/public-client"; @@ -20,8 +21,24 @@ export default function Home() { const [recent, setRecent] = useState([]); const [recentLoading, setRecentLoading] = useState(true); const [popularTopics, setPopularTopics] = useState<{name: string; count: number}[]>([]); + const [randomLoading, setRandomLoading] = useState(false); + const [randomError, setRandomError] = useState(false); const navigate = useNavigate(); + const handleRandomTechnique = async () => { + setRandomLoading(true); + setRandomError(false); + try { + const { slug } = await fetchRandomTechnique(); + navigate(`/techniques/${slug}`); + } catch { + setRandomError(true); + setTimeout(() => setRandomError(false), 2000); + } finally { + setRandomLoading(false); + } + }; + // Load featured technique (random) useEffect(() => { let cancelled = false; @@ -156,6 +173,17 @@ export default function Home() { + {/* Random technique discovery */} +
+ +
+ {/* Featured Technique Spotlight */} {featured && (