feat: Added GET /api/v1/techniques/random endpoint returning {slug}, fe…
- "backend/routers/techniques.py" - "frontend/src/api/public-client.ts" - "frontend/src/pages/Home.tsx" - "frontend/src/App.css" GSD-Task: S01/T02
This commit is contained in:
parent
d94995453e
commit
717f6c0785
7 changed files with 203 additions and 1 deletions
|
|
@ -45,7 +45,7 @@ CSS-first visual polish covering R016 (card hover + stagger) and R017 (featured
|
||||||
- Estimate: 45m
|
- 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
|
- 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
|
- 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.
|
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.
|
||||||
|
|
||||||
|
|
|
||||||
42
.gsd/milestones/M011/slices/S01/tasks/T01-VERIFY.json
Normal file
42
.gsd/milestones/M011/slices/S01/tasks/T01-VERIFY.json
Normal file
|
|
@ -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
|
||||||
|
}
|
||||||
88
.gsd/milestones/M011/slices/S01/tasks/T02-SUMMARY.md
Normal file
88
.gsd/milestones/M011/slices/S01/tasks/T02-SUMMARY.md
Normal file
|
|
@ -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.
|
||||||
|
|
@ -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)
|
@router.get("/{slug}", response_model=TechniquePageDetail)
|
||||||
async def get_technique(
|
async def get_technique(
|
||||||
slug: str,
|
slug: str,
|
||||||
|
|
|
||||||
|
|
@ -2729,6 +2729,33 @@ a.app-footer__repo:hover {
|
||||||
opacity: 0.85;
|
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 ──────────────────────────────────────────────────────── */
|
||||||
|
|
||||||
.admin-reports {
|
.admin-reports {
|
||||||
|
|
|
||||||
|
|
@ -258,6 +258,10 @@ export async function fetchTechnique(
|
||||||
return request<TechniquePageDetail>(`${BASE}/techniques/${slug}`);
|
return request<TechniquePageDetail>(`${BASE}/techniques/${slug}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function fetchRandomTechnique(): Promise<{ slug: string }> {
|
||||||
|
return request<{ slug: string }>(`${BASE}/techniques/random`);
|
||||||
|
}
|
||||||
|
|
||||||
export async function fetchTechniqueVersions(
|
export async function fetchTechniqueVersions(
|
||||||
slug: string,
|
slug: string,
|
||||||
): Promise<TechniquePageVersionListResponse> {
|
): Promise<TechniquePageVersionListResponse> {
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ import { Link, useNavigate } from "react-router-dom";
|
||||||
import {
|
import {
|
||||||
fetchTechniques,
|
fetchTechniques,
|
||||||
fetchTopics,
|
fetchTopics,
|
||||||
|
fetchRandomTechnique,
|
||||||
type TechniqueListItem,
|
type TechniqueListItem,
|
||||||
} from "../api/public-client";
|
} from "../api/public-client";
|
||||||
|
|
||||||
|
|
@ -20,8 +21,24 @@ export default function Home() {
|
||||||
const [recent, setRecent] = useState<TechniqueListItem[]>([]);
|
const [recent, setRecent] = useState<TechniqueListItem[]>([]);
|
||||||
const [recentLoading, setRecentLoading] = useState(true);
|
const [recentLoading, setRecentLoading] = useState(true);
|
||||||
const [popularTopics, setPopularTopics] = useState<{name: string; count: number}[]>([]);
|
const [popularTopics, setPopularTopics] = useState<{name: string; count: number}[]>([]);
|
||||||
|
const [randomLoading, setRandomLoading] = useState(false);
|
||||||
|
const [randomError, setRandomError] = useState(false);
|
||||||
const navigate = useNavigate();
|
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)
|
// Load featured technique (random)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let cancelled = false;
|
let cancelled = false;
|
||||||
|
|
@ -156,6 +173,17 @@ export default function Home() {
|
||||||
</Link>
|
</Link>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
{/* Random technique discovery */}
|
||||||
|
<div className="home-random">
|
||||||
|
<button
|
||||||
|
className="btn btn--random"
|
||||||
|
onClick={handleRandomTechnique}
|
||||||
|
disabled={randomLoading}
|
||||||
|
>
|
||||||
|
{randomLoading ? "Loading…" : randomError ? "Try again" : "🎲 Random Technique"}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Featured Technique Spotlight */}
|
{/* Featured Technique Spotlight */}
|
||||||
{featured && (
|
{featured && (
|
||||||
<section className="home-featured">
|
<section className="home-featured">
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue